Reimpresión de Y con datos de la Y anterior

Análisis de causa raíz, corrección y validación · PCPOS · 14/09/2026
Reclamos: San Vicente (Y 12507→12506) · RB Vesta (Y 763→762) Módulo: cierre de cajero (Y) Estado: corregido y validado
Resumen ejecutivo. Al reimprimir una Y, salía la Y anterior. La reimpresión no era la culpable: el archivo de la Y nacía con el contenido de la Y previa. La causa es un spool de impresión que quedaba abierto por otra operación; el cierre de cajero no detectaba esa condición y copiaba el archivo viejo con el número de la Y nueva. El fix detecta el spool ocupado, lo cierra a la fuerza y valida el número antes de copiar. Se reprodujo el caso real en DOSBox y el fix detectó y corrigió el problema.

1. El problema

Reportado en varias sucursales. En San Vicente (PV 841 / POS 15) se emitieron las Y 12504–12506 y al pedir la 12507 se volvía a emitir la 12506. En RB Vesta (POS 75) al pedir la Y 763 salía la 762. En ambos casos la Y contenía la facturación de una cuenta corriente.

Verificación previa: en el servidor, los archivos de dos Y consecutivas tenían el mismo tamaño, y el log del POS mostraba el contenido correcto de la Y nueva mientras el archivo quedaba con el de la anterior.

2. Causa raíz

El cierre de cajero arma la Y en un archivo spool y luego lo copia a YLOG\NNNNNNNN.y y .2se, que viajan al servidor y a la base CierresImpresosPOS. El mecanismo del fallo:

1
Se vende una factura de cuenta corriente en una caja sin impresora fiscal (modo CAEA). El ticket abre el spool logprnti.$X$ (LogPrnTICKET).
2
Se anula ese ticket. En TICKET::DeleteAll (pos_vnta.cpp:2249), para tickets de factura (ImprimeTicket==1) los dos StopLog2File() están comentados (líneas 2250 y 2253). El spool queda abierto.
3
En el siguiente cierre de cajero, INFORME::DoInforme llama a StaLogPrn2File(LogPrnCIEZCAJR) e ignora el retorno. Esa función devuelve BAD sin hacer nada cuando el spool ya está ocupado (fpl || StaLog2File).
4
El informe nuevo no entra al spool. El archivo viejo queda intacto y se copia tal cual con el número de la Y nueva. Nadie loguea el error.
El síntoma de "reimpresión mala" es una consecuencia: el archivo de la Y ya estaba contaminado desde su generación. La reimpresión sólo lo lee fielmente.

3. La corrección

Tres archivos de fuente. El binario resultante es POS_MAIN.EXE (no cambian DLLs).

3.1 · pos_info.cpp — guarda al iniciar el cierre + validación de NroY

Antes de abrir el spool del cierre: si quedó ocupado, se loguea y se cierra a la fuerza; si aun así no abre, se aborta el cierre con aviso, antes de tocar contadores. Y antes de copiar la Y, se verifica que el spool contenga el NroY actual.

  if (Modo==MODO_Z){
    NoPrintPorFacturacion=1;
    // si el spool quedó ocupado por otro flujo, lo cerramos a la fuerza
    if (fpl || StaLog2File){
      Log(LOGERROR, LOG_ZETAMANUAL, GCmos.NroZ, fs_SpoolBusyAtClose, CurrentLogFName);
      StopLog2File();
      }
    if (StaLogPrn2File(Alcance==aPOS ? LogPrnCIEZCAJA : LogPrnCIEZCAJR)!=OK){
      Log(LOGERROR, LOG_ZETAMANUAL, GCmos.NroZ, fs_SpoolOpenFailed, ...);
      NoPrintPorFacturacion=0;
      PError(msg_ErrorImpresEnCurso_141);
      return BAD;   // cierre abortado, sin generar Y falsa
      }
  // antes de copiar el spool a YLOG:
  sprintf(cNroYEsperado, "Y%08ld", NroY);
  while (!feof(fp)){ if(!fgets(cLineaSpool,sizeof(cLineaSpool),fp)) break;
    if(strstr(cLineaSpool, cNroYEsperado)!=NULL){ iNroYEncontrado=1; break; } }
  if(!iNroYEncontrado)   // avisa si el spool no trae el NroY del cierre
    Log(LOGERROR, LOG_ZETAMANUAL, GCmos.NroZ, fs_SpoolMissingNroY, LogPrnCIEZCAJR, cNroYEsperado);

3.2 · pos_misc.cppEndLog2File resetea el flag

Dejaba StaLog2File en "ocupado" con fpl en NULL, marcando el spool como ocupado para siempre.

void POS_EXPORT EndLog2File(char* FileLog){
  if (fpl!=NULL) fclose(fpl);
  remove(FileLog); fpl=NULL;
  StaLog2File=0;   // mantiene el flag consistente con fpl
  }

3.3 · pos_main.cppSaveYNoImpresa (recuperación tras apagón)

En la ruta de recuperación al reiniciar el POS, no genera la Y si el spool no contiene su NroY (evita publicar una Y equivocada). Además corrige una fuga del handle fpx.

  sprintf(cNroYEsperado, "Y%08ld", lNroY);
  while (!feof(fp)){ ... if(strstr(cLineaSpool, cNroYEsperado)) { iNroYEncontrado=1; break; } }
  if(!iNroYEncontrado){
    Log(LOGERROR, LOG_ZETAMANUAL, GCmos.NroZ, fs_SpoolNoNroYNoGen, LogPrnCIEZCAJR, cNroYEsperado);
    Error(fs_WarnSpoolNoNroYNoGen, lNroY); fclose(fp); return;   // NO genera Y
    }

Nota técnica: los mensajes nuevos se declararon como static char _far fs_*[N] porque el segmento DGROUP (datos near de 16 bits) tenía sólo 190 bytes libres. No se tocó ninguna cadena preexistente.

Decisión: NO se descomentan los StopLog2File de DeleteAll. Estaban deshabilitados a propósito por un historial de "doble impresión de Y". El fix neutraliza la fuga en el cierre, sin tocar esa rama.

4. Validación (POS real, DOSBox)

Escenario del reclamo: caja sin fiscal / CAEA, bases del kit. Secuencia: habilitar cajero → vender ítem → Factura A (cuenta corriente) → anular el ticket → cierre de cajero.

Detección (línea de log del cierre):
'20260914 17:09:21','1','165',...,'Spool ocupado al iniciar cierre (logprnti.$X$), se fuerza cierre'
El spool culpable es logprnti.$X$ (LogPrnTICKET): confirma que la anulación de la factura lo dejó abierto.
Corrección (archivo de la Y): YLOG\00004960.Y contiene únicamente Y00004960 (ninguna Y anterior). La Y salió con sus propios datos.
Sin el fixCon el fix
El cierre encuentra el spool ocupado, StaLogPrn2File falla en silencio, se copia el spool viejo como la Y nueva → Y con datos de la anterior. Detecta el spool ocupado, lo cierra a la fuerza, genera la Y correcta y loguea el evento → Y correcta + traza para diagnóstico.

5. Archivos y despliegue

ArchivoUbicaciónCambio
pos_info.cppC:\Work\PCPOS\20260914_DINO\srcsGuarda de spool al iniciar cierre + validación de NroY
pos_misc.cppC:\Work\PCPOS\20260914_DINO\srcsEndLog2File resetea StaLog2File
pos_main.cppC:\Work\PCPOS\20260914_DINO\srcsSaveYNoImpresa: no publica Y sin su NroY + fix de handle
POS_MAIN.EXEC:\Work\PCPOS\20260914_DINO\build_20260914Binario compilado con el fix (3.060.461 bytes, 14/09)

Despliegue: copiar POS_MAIN.EXE a las cajas (afecta sólo al EXE; las DLLs no cambian). En producción, la nueva línea Spool ocupado al iniciar cierre (<archivo>) permite identificar qué operación deja el spool abierto, para corregirlo también en el origen si se decide.