Anexo: revisión de la cadena de percepciones frente a ARCA

Revisión de sólo lectura del cálculo de percepciones (IIBB, Comercio e Industria, percepción de IVA 21 % y 10,5 %) en el POS MS-DOS (Borland C++ 3.1, Btrieve), desde la activación por cliente hasta el payload del CAE, la Z y la Nota de Crédito. Plan de corrección; no se aplicó ningún cambio.

Fecha: 23/09/2026
Árbol revisado: C:\Work\AI\DINO_20260902 (fuentes al 23/09/2026, incluye F18 y F19)
Antecedentes: Percepciones_PostPago_QR.html (secciones 3 y 7), paranoia\MATRIX_RESULTS.md (F7, F12, F13, F17, F18, F19)
Método: lectura de código con referencias archivo:línea; campos Btrieve confirmados con paranoia\ddf_scan.py fields sobre DINO_BUILD\BASES (sólo lectura). No se ejecutó el POS ni nada bajo POSKIT.
Convención: "no verificado" indica que la afirmación sale del código y no de una corrida en vivo o de la documentación de ARCA.
19
hallazgos (P1 a P19)
9
rompen consistencia cobrado == ARCA o == persistido
7
inconsistencias latentes
3
deuda técnica
+64,36
desbalance del CAE en M4cm (tipo 1): N + V + Trib − ImpTotal

1. Resumen ejecutivo

Requisito del product owner. Las percepciones se calculan en una sola cadena coherente y lo informado a ARCA en la factura electrónica (payload CAE) es igual a lo cobrado al cliente, que a su vez es igual a lo persistido y a lo impreso.

Resultado. Hoy no hay una cadena: hay al menos cinco cálculos independientes de la misma magnitud (alta al cobro, recómputo por promoción, Nota de Crédito, re-cálculo del CAE en el camino no global, y la Z) y tres fuentes que los consumidores leen sin conciliar (Ticket.Percepcion, los campos de dE_TResu filtrados por UtilizaPercepcion y las filas sintéticas de dB_TDeta). Se encontraron 19 hallazgos:

Severidad Cantidad Hallazgos
rompe rompe consistencia cobrado == ARCA (o cobrado == persistido) 9 P1, P2, P6, P7, P8, P9, P10, P13, P14
latente inconsistencia latente 7 P3, P4, P5, P11, P15, P16, P17
deuda deuda técnica 3 P12, P18, P19

Identidad del CAE. El POS arma importeTotal = Ticket.TotalCobrar y importeTributo = percepciones de dE_TResu + impuestos internos por diferencia (sección 3.6). La igualdad importeNeto + importeIva + importeTributo == importeTotal se cumple sólo si se dan las tres condiciones siguientes:

  1. el comprobante es de tipo 2, 3, 6, 8 o 4 y el perfil 544 MODOTICKECTFACTURAC está en 0;
  2. el residuo de redondeo de los impuestos internos por diferencia no queda en el rango (−∞, 0,015], que se descarta;
  3. la suma de percepciones de dE_TResu que lee UtilizaPercepcion es igual a Ticket.Percepcion.

Si falla la primera condición (Factura B / consumidor final con percepción), la suma excede el total en exactamente el monto de la percepción (P9; con los buckets de M4cm: 2878,72 contra 2814,36). Si falla la segunda, difiere en ±0,01 (P10). Si falla la tercera, difiere en el monto que no se ve (P1, P6, P7). Los importes importeOpEx e importeTotConc no se envían (pos_cae.cpp:244-245, comentados).

Prioridad sugerida (menor riesgo primero). Observabilidad (invariante [PERCCHK] y volcado del XML del CAE en TEST) → P14 y P13 (aislados) → P9 y P10 (identidad del CAE) → P7, P8 y P6 (recómputo) → P1 y P3 (requieren decisión) → P2 y P11 → P4 y P5 → cadena única.

1.1 Tabla de hallazgos

Id Hallazgo Severidad Esfuerzo (h)
P1 Mínimo IIBB en 0: UtilizaPercepcion la ignora pero el alta la cobra si hay ComInd o PIVA rompe 4 a 7
P2 Base IIBB por precio al alta y por neto en el recómputo rompe 10
P3 Un recargo que cruza el mínimo nunca crea la percepción (y sí la crea si ya había otra) latente 6
P4 La NC re-evalúa mínimo y tasa con su propia copia y usa la base bruta latente 14
P5 El recargo/descuento clásico de tarjeta no recalcula la percepción latente 10
P6 El recómputo crea IIBB para un cliente sin IIBB (habilitado por perfil, no por cliente) rompe 5
P7 PIVA con perfil 518 = 1: Ticket.Percepcion cambia y dE_TResu no rompe 4
P8 La base PIVA (NetoConPIVA21/105) nunca se reescala rompe 6
P9 II por diferencia absorbe la percepción fuera de tipos 2/3/6/8/4: el tributo se duplica en el CAE rompe 4
P10 Residuo ≤ 0,015 de II por diferencia se descarta: CAE desbalanceado en ±0,01 rompe 5
P11 Alícuota IIBB al CAE = fTax3/100 (0,01 para clientes "usar tasa del perfil"); base = neto total latente 4
P12 Camino CAE "no global" recalcula percepciones sin redondeo y sin PIVA deuda 2
P13 Z: PIVA leída de ETres en lugar del registro; compuerta con la tasa global rompe 4
P14 NC escribe la PIVA en campos inexistentes de dE_TResu rompe 3,5
P15 Re-ejecución de ComputarBinario: filas PIVA en el bucket 21 % y IIBB fuera de fPrecioTotal latente 6
P16 Promo con un pago previo descarta el delta de percepción ya aplicado latente 5
P17 Impresora fiscal: ajuste de ±0,09 no persistido; etiqueta de ComInd heredada latente 3
P18 Ratio global único para bases de IIBB, ComInd y PIVA deuda 7
P19 Ids de tributo (2/3/99) sin verificar; Informe Impositivo sin desglose por tipo deuda 2

2. Mapa de la cadena

1 Activación SetearClienteFactuacionXCuit OK_SelectCliente SetPercepcionVariable flags fTax3 / CI / IVA tasa en el perfil global P1 2 Alta al cobro ComputarPercepcionExistente pos_mppo.cpp:4927 base neto o precio (407) escribe dE_TResu Ticket.Percepcion P1 P2 3 Recómputo RecomputePercepcionxPromoMdeP modos 0 / 1 / 2 / 3 ratio global único dE_TResu + Ticket.Percepcion sin NetoConPIVA P3 P6 P7 P8 P16 P18 4 Persistencia TICKET::ComputarBinario filas iTax 3 / 4 / 7 / 8 fPrecioTotal += percepción lee UtilizaPercepcion P15 5 Impresión EnviarPercepciones NCR_PERC.SCP / TIC_IVA lee UtilizaPercepcion TOTAL = TotalCobrar P17 6 CAE / ARCA ImprimirIVAS → globalTaxes ObtenerCAE → BuildXML ImpTotal = TotalCobrar ImpTrib = perc. + II dif. P9 P10 P11 P12 P19 7 Nota de Crédito NCREDITO::UpdateScreen Update_dE_TResuMontos copia propia de perfiles base bruta de ítems P4 P14 8 Z / Informes GetPercepcionTicket ObtenerTotalesPercepciones Informe Impositivo sin desglose por tipo P13 P19 dE_TResu, Ticket.Percepcion, filas sintéticas, globalTaxesTickets Recargo de tarjeta clásico: CalculateRecDescMdeP → ViewSaldo, sin recómputo P5
Figura 1. Cadena actual de percepciones. Cada etapa lee su propia fuente (Ticket.Percepcion, dE_TResu filtrado por UtilizaPercepcion, filas sintéticas o globalTaxesTickets); las marcas rojas son los hallazgos de la sección 4. La etapa 3 concentra la mayor cantidad; la 6 es donde se rompe la identidad del CAE.
Etapa Función (archivo:línea) Escribe Lee
1 Activación SetearClienteFactuacionXCuit (pos_ctac.cpp:697), OK_SelectCliente (pos_taxs.cpp:249), SetPercepcionVariable (pos_taxs.cpp:84) tick->ticket.iTienePercepcion*, ETres.fTax3/iTienePercepcionCI/iTienePercepcionIVA, globales profile.dPorcentajePercepciones y profile.dPorcPercepcionComInd dM_Clien.iConsumos/iFPonderCom/iTienePercepcionIVA, perfiles 271/273
2 Alta al cobro ComputarPercepcionExistente (pos_taxs.cpp:2083), llamada en pos_mppo.cpp:4927 ETres.fTax1/fMontoPercepcionCI/fMontoPercepIVA21/105, Ticket.Percepcion, Ticket.NetoTicketReal, Ticket.NetoConPIVA21/105 ETdet (netos, precio fiscal, iPuntosVta == -1), ETres.fTax3/iTienePercepcionCI, perfiles 272/273/378/519/524/525/407
3 Recómputo RecomputePercepcionxPromoMdeP (pos_tic2.cpp:1841), PercepcionesElement (pos_tic2.cpp:1691), PromoCalculatePercepcion (pos_tic2.cpp:1913) ETres (4 campos), Ticket.Percepcion, Ticket.NetoTicketReal, historial percepcionPromoArray UtilizaPercepcion, Ticket.TotalCobrar/TotalInicial/NetoTicketReal/NetoConPIVA*
4 Persistencia TICKET::ComputarBinario (pos_tic2.cpp:1329, bloque 1502-1599) filas dB_TDeta iTax 3/4/7/8, ETres.fPrecioTotal UtilizaPercepcion, CorrespondePercIVA (pos_mis2.cpp:4501)
5 Impresión EnviarPercepciones (pos_taxs.cpp:1774), ImprimirIVAS (pos_inve.cpp:4581) líneas NCR_PERCEPCIONES (9003, NCR_PERC.SCP), TICK_IVA UtilizaPercepcion, Ticket.Percepcion
6 CAE ControlDeMdeP::FinalPrintAjusteDiferencias (pos_taxs.cpp:2701), ObtenerYGrabarCAEA (pos_taxs.cpp:913), FacturacionCAE::ObtenerCAE (pos_cae.cpp:699), BuildXML (pos_cae.cpp:211) globalTaxesTickets, XML globalTaxesTickets, ETres.fTax3/iTienePercepcionCI
7 NC NCREDITO::UpdateScreen (pos_ncre.cpp:363), SetDataAdjustTResu (pos_ncre.cpp:649), Update_dE_TResuMontos (pos_ncre.cpp:2243), filas (pos_ncre.cpp:2645-2760) dE_TResu de la NC, filas iTax 3/4/7/8 hE_TResu del original en el servidor, copia propia de perfiles (pos_ncre.cpp:312-338)
8 Z / Informes ObtenerTotalesPercepciones (pos_taxs.cpp:1918), GetPercepcionTicket (pos_taxs.cpp:1873), InformeImpositivo::LeerDatos (pos_info.cpp:4043) líneas de informe dE_TResu del día

3. Traza de la cadena

3.1 Activación

SetearClienteFactuacionXCuit lee los flags del cliente sólo si el perfil correspondiente está habilitado, los copia al ticket, llama a SetPercepcionVariable y los refleja en ETres.

pos_ctac.cpp:731-736

if(profile.iUsaPercepciones)
    bCli->field("iConsumos", iLocalTienePercepcionIIBB);    
if(profile.iPercepcionComIndUsa)
    bCli->field("iFPonderCom", iLocalTienePercepcionCOMIND);    
if(profile.iPERCIVA_USA)
    bCli->field(str_iTienePercepcionIVA, iLocalTienePercepcionIVA); 

pos_ctac.cpp:800 y :812-815

SetPercepcionVariable(NULL,tick->ticket.iTienePercepcionIIBB, tick->ticket.iTienePercepcionCOMIND); //20140220 OK //NO SE SI TAN OK!!!
ETres->assign("fTax3", tick->ticket.iTienePercepcionIIBB);                  //20140220 OK
ETres->assign("iTienePercepcionCI", tick->ticket.iTienePercepcionCOMIND);   //20140220 OK
ETres->assign(str_iTienePercepcionIVA, tick->ticket.iTienePercepcionIVA);   //20140220 OK
ETres->Update();

SetPercepcionVariable modifica el perfil global: la tasa de ComInd siempre sale del cliente (iFPonderCom / 100) y la de IIBB también cuando el perfil 271 vale 2 (tasa variable). Con iConsumos == 1 en modo variable relee el perfil 273 y además registra Err:percdef.

pos_taxs.cpp:99 y :117-133

profile.dPorcPercepcionComInd=0L;   
//
if(iTienePercepcionCOMIND){
    profile.dPorcPercepcionComInd   = ((double)iTienePercCI)/100L;  //sin validacion de porcentajes
    ROUND_DOUBLE(profile.dPorcPercepcionComInd);
    }
if(profile.iUsaPercepciones!=2)
    return;
//variable!
profile.dPorcentajePercepciones=0L;
if(iTienePercIIBB==1){
    Log(LOGERROR, LOG_SELECCIONAFACTURACION,GCmos.NroZ, "Err:percdef");
    GetProfile(pro_PORCENTAJEPERCEPCIONES, profile.dPorcentajePercepciones);
    }
else{
    profile.dPorcentajePercepciones= ((double)iTienePercIIBB)/100L;
    ROUND_DOUBLE(profile.dPorcentajePercepciones);
    }

Cada StartTicket pasa por InitTRes_Values, que pone los flags en 0 y vuelve a llamar a SetPercepcionVariable; con el perfil 271 = 2 eso deja profile.dPorcentajePercepciones = 0 hasta que se elija otro cliente (consecuencia en P13).

pos_vnta.cpp:4963-4968

if(tick!=NULL){
    tick->ticket.iTienePercepcionIIBB=0;
    tick->ticket.iTienePercepcionCOMIND=0;
    tick->ticket.iTienePercepcionIVA=0;
    SetPercepcionVariable(NULL,tick->ticket.iTienePercepcionIIBB, tick->ticket.iTienePercepcionCOMIND); //20140220 OK //NO SE SI TAN OK!!!
    }

Perfiles. 271 pro_USAPERCEPCIONES, 272 pro_MONTOMINIMOPERCEPCIONES, 273 pro_PORCENTAJEPERCEPCIONES, 276 pro_DEPTO_PERCEPCIONES (pos_prof.h:298-304); 373 pro_PERCEP_COMIND_USA, 374 texto, 378 pro_PERCEP_COMIND_MMINIMO (pos_prof.h:434-439; ComInd no tiene tasa de perfil, pos_main.cpp:4386 "no tiene valor default"); 518 pro_PERCIVA_USA, 519 pro_PERCIVA_MONTOMIN, 524/525 tasas PIVA (pos_prof.h:607-614; se leen sólo si 518 ≠ 0, pos_main.cpp:4634-4643); 407 PERCEPCIONES_USAPRECIOVTA, lista de tipos de cliente con base por precio (pos_prof.h:480, pos_lnov.cpp:3141-3165). En la copia Becerra: 271 = 2, 272 = 1000, 273 = 0, 544 = 0, 482 CAEA_HABILITADO = 0 (MATRIX F7 y log de M4cm).

UtilizaPercepcion y el mínimo 0 (confirmado). Para IIBB exige mínimo y tasa distintos de cero; ComInd y PIVA no tienen esa condición.

pos_taxs.cpp:1741-1748

if( profile.iUsaPercepciones && VALIDAR(profile.dMinimoPercepciones) && VALIDAR(profile.dPorcentajePercepciones)){
    double dUsaPerc=0L;
    ETres->field(strfTax3, dUsaPerc);
    if(VALIDAR(dUsaPerc)){
        ETres->field(strfTax1, dMontoYaExistePercepcionIIBB);
        ir|=1;
        }
    }

La misma compuerta está en ObtenerTotalesPercepciones (pos_taxs.cpp:1923) y en la copia de perfiles de la NC (pos_ncre.cpp:320-323). Con mínimo 0 la IIBB queda deshabilitada sólo en los consumidores que pasan por UtilizaPercepcion; el alta (pos_taxs.cpp:2228) no tiene esa condición (P1).

3.2 Cálculo inicial al cobro

Llamada única al entrar al cobro (y en cada reingreso), después de las promociones previas al medio de pago:

pos_mppo.cpp:4922-4935

Ticket.Percepcion=Ticket.NetoConPIVA21=Ticket.NetoConPIVA105=Ticket.NetoTicketReal=0L;
if(profile.iCAEA_Habilitado && EsFacturaTipo2_3(Fact.TipoFacturacion)){
    ComputarPercepcionExistente(Ticket.Percepcion, Ticket.NetoTicketReal,Ticket.NetoConPIVA21,Ticket.NetoConPIVA105, 1);    //1=solo netopor las dudas no tenga percepciones!
    }
if(POSConPercepcionesHabilitadas()){
    ComputarPercepcionExistente(Ticket.Percepcion, Ticket.NetoTicketReal,Ticket.NetoConPIVA21,Ticket.NetoConPIVA105,0); //0=fullmode
    if(VALIDAR(Ticket.Percepcion)){
        Ticket.TotRecDesc+=Ticket.Percepcion;
        Ticket.Saldo= Ticket.Saldo + Ticket.Percepcion; DIG_SIG( Ticket.Saldo);
        Ticket.SaldoBack= Ticket.Saldo ;
        Ticket.TotalNeto=Ticket.Saldo;
        ViewSaldo();
        }
    }

No hay otros llamadores activos (pos_mppo.cpp:1630 está comentado). ComputarPercepcionExistente suma fNetoFiscal y PrecioFiscal de todas las filas de ETdet (incluidas las filas de promoción con bAcumulado), más bonificaciones mayoristas y por bulto; la base PIVA sólo toma ítems con iPuntosVta == -1 (marcado en pos_tick.cpp:2020-2022 cuando el artículo tiene iTienePercepcionIVA).

Base IIBB por precio para los tipos de cliente del perfil 407:

pos_taxs.cpp:2177-2181 y :2218-2221

int iUsaPrecioParaComputo=0;
int iTipoCli=0;
ETres->field(str_iClientType, iTipoCli);
if(ClienteTienePercepcionPVta && ClienteTienePercepcionPVta(iTipoCli))
    iUsaPrecioParaComputo=1;
//######
double dMontoPCalculo=dMontoNeto;
if(iUsaPrecioParaComputo)
    dMontoPCalculo=dPrecioFiscalTot;

Reglas de mínimo en el alta: PIVA compara la suma de los importes contra el mínimo con >= 0 (pos_taxs.cpp:2197); IIBB compara la base con > 0.009 (pos_taxs.cpp:2229); ComInd compara siempre el neto con > 0.009 (pos_taxs.cpp:2236).

pos_taxs.cpp:2228-2246

if(profile.iUsaPercepciones && iTienePercepcionIIBB){
    if((dMontoPCalculo - profile.dMinimoPercepciones)>0.009L){
        dPercIIBB= (dMontoPCalculo * profile.dPorcentajePercepciones) / 100L;
        ROUND_DOUBLE(dPercIIBB);
        }
    }

if(profile.iPercepcionComIndUsa && iTienePercepcionCOMIND){
    if((dMontoNeto - profile.dPercepcionComIndMontoMin)>0.009L){
        dPercCI= (dMontoNeto * profile.dPorcPercepcionComInd) / 100L;
        ROUND_DOUBLE(dPercCI);
        }
    }
//
ETres->assign(strfTax1, dPercIIBB);
ETres->assign(str_fMontoPercepcionCI, dPercCI);
ETres->Update();
//
dPercExistente=  dPercIIBB + dPercCI+dPercIVA;

Nota: Ticket.NetoTicketReal recibe siempre el neto (dRealNetoTicket=dMontoNeto, pos_taxs.cpp:2211), aun cuando la IIBB se calculó sobre el precio (P2).

3.3 Recómputo

RecomputePercepcionxPromoMdeP reescala las bases por un ratio global dRecDescPromo / (TotalCobrar − Percepcion) y recalcula los cuatro elementos. Modos: 0 consulta, 1 actualiza, 2 revierte el último (RecoverLast), 3 actualiza sin mínimo para percepciones preexistentes (F18).

pos_tic2.cpp:1861-1876

double dRatio= dRecDescPromo/dNetoSinPercepcion;    
double dCurrentNetoTicketReal=Ticket.NetoTicketReal;
dDeltaNeto= dCurrentNetoTicketReal * dRatio;
dNuevoNeto= dCurrentNetoTicketReal + dDeltaNeto;
//OJO REVISAR, me hace fallar cuando ingreso el MDEP!!!
//if(VALIDAR(dPreCalculadoDeltaNeto)){
//  dRatio=dPreCalculadoDeltaNeto/Ticket.NetoTicketReal;    //TODO: dNuevoNeto no seria el dPreCalculadoDeltaNeto?!
//  }
//20260922 QRDTO percepcion minimo: en modo 3 el minimo es 0 solo si la
//percepcion ya existia en el ticket.
double dMinIIBB= (iIgnorarMinimo && VALIDAR(dMontoYaExistePercepcionIIBB)) ? 0.0L : profile.dMinimoPercepciones;
double dMinCOMIND= (iIgnorarMinimo && VALIDAR(dMontoYaExistePercepcionCOMIND)) ? 0.0L : profile.dPercepcionComIndMontoMin;
PercepcionesElement peIIBB(UpdatePercepcion, eptype_IIBB,profile.iUsaPercepciones, dMinIIBB, profile.dPorcentajePercepciones, dMontoYaExistePercepcionIIBB, dCurrentNetoTicketReal,dRatio); 
PercepcionesElement peCOMIND(UpdatePercepcion, eptype_COMIND,profile.iPercepcionComIndUsa, dMinCOMIND, profile.dPorcPercepcionComInd,dMontoYaExistePercepcionCOMIND, dCurrentNetoTicketReal,dRatio);
PercepcionesElement pePIVA21(UpdatePercepcion, eptype_PIVA21,profile.iPERCIVA_USA>1?profile.iPERCIVA_USA:0, profile.dPERCIVA_MONTOMIN, profile.dPERCIVA_21, dMontoYaExistePPIVA21, Ticket.NetoConPIVA21,dRatio);
PercepcionesElement pePIVA105(UpdatePercepcion, eptype_PIVA105,profile.iPERCIVA_USA>1?profile.iPERCIVA_USA:0, profile.dPERCIVA_MONTOMIN, profile.dPERCIVA_105, dMontoYaExistePPIVA105, Ticket.NetoConPIVA105,dRatio);

Tres observaciones que originan hallazgos: el elemento IIBB se habilita por perfil y no por el flag del cliente (P6); los elementos PIVA se habilitan sólo si el perfil 518 es mayor que 1, pero su dNewValue se asigna igual (P7); sólo se actualiza Ticket.NetoTicketReal, nunca NetoConPIVA21/105 (P8).

pos_tic2.cpp:1880-1904

//Ahora vamos el tema de PIVA, ya que para que se aplique la suma de piva21 y piva105 tiene que ser mayor que el minimo!
pePIVA21.dNewValue= (pePIVA21.dNewNeto*pePIVA21.dPercentage)/100L;
ROUND_DOUBLE(pePIVA21.dNewValue);
pePIVA105.dNewValue= (pePIVA105.dNewNeto*pePIVA105.dPercentage)/100L;
ROUND_DOUBLE(pePIVA105.dNewValue);
//20260922 QRDTO percepcion minimo: en modo 3 no se anula la PIVA que ya existia.
if( (pePIVA21.dNewValue+pePIVA105.dNewValue)<profile.dPERCIVA_MONTOMIN
    && !(iIgnorarMinimo && VALIDAR(dMontoYaExistePPIVA21+dMontoYaExistePPIVA105))){
    pePIVA21.dNewValue=0L;
    pePIVA105.dNewValue=0L;
    }
pePIVA21.Compute();
pePIVA105.Compute();
dRecDescPorPercep= peIIBB.DeltaValueOldNew()+peCOMIND.DeltaValueOldNew()+pePIVA21.DeltaValueOldNew()+pePIVA105.DeltaValueOldNew();
if(UpdatePercepcion){

    //history for later deletion!
    percepcionPromoArray.Add(
    //peIIBB.GetNewValue(),peCOMIND.GetNewValue(),pePIVA21.GetNewValue(),pePIVA105.GetNewValue(),
        dMontoYaExistePercepcionIIBB, dMontoYaExistePercepcionCOMIND,dMontoYaExistePPIVA21,dMontoYaExistePPIVA105,
        dRecDescPorPercep,Ticket.Percepcion, Ticket.NetoTicketReal);

    Ticket.Percepcion= peIIBB.GetNewValue()+peCOMIND.GetNewValue()+pePIVA21.GetNewValue()+pePIVA105.GetNewValue();
    Ticket.NetoTicketReal += dDeltaNeto;    //20100310++ BETA
    }

PercepcionesElement::Compute sale sin escribir ETres si el elemento no está habilitado (pos_tic2.cpp:1738-1739) y crea una percepción que no existía si el neto nuevo supera el mínimo (pos_tic2.cpp:1745-1748).

PromoCalculatePercepcion sólo entra si el ticket ya tiene alguna percepción:

pos_tic2.cpp:1915-1917

double dRecDescPorPercep=0L;
//TODO_PERCEPCIONES_IVA: ESTO ESTARIA MAL SI ES UN RECARGO NO? YA QUE PODRIA NO TENER PERCEPCION AUN y A PARTIR DE LA PROMO SI!
if(VALIDAR(Ticket.Percepcion)){

Llamadores (búsqueda sobre el árbol, sin cambios respecto del 22/09):

Llamador Sitio Modo Camino
PagoQR_ImputarDescuento pos_mppo.cpp:3680 3 beneficio QR post-pago
TICKET::PromocionesMdePPromoCalculatePercepcion(1, …) pos_prom.cpp:2656 → pos_tic2.cpp:1929 1 promo x MdeP clásica
TICKET::PromocionesMdeP_ConsultarPromoCalculatePercepcion(0, …) pos_prom.cpp:4755 → pos_tic2.cpp:1929 0 consulta de promo
TICKET::PromocionesMdeP_DeleteMdeP pos_prom.cpp:2810 2 reversa de la promo
PagoQR_RevertirImputacionMdeP pos_tic2.cpp:1215 2 reversa del PAGO QR

Caminos que nunca recalculan. El recargo/descuento clásico de tarjeta (CalculateRecDescMdeP, pos_mppo.cpp:244, usado en pos_mppo.cpp:3038 sólo cuando !profile.PermitePromosXMdeP) calcula sobre Ticket.Saldo, que ya incluye la percepción, y se aplica con ViewSaldo sin llamar al recómputo (P5). ViewSaldo (pos_mppo.cpp:400-413) sólo acumula TotRecDesc y copia TotalNeto a TotalCobrar.

pos_mppo.cpp:271-282

double xmonto= RoundExp100(dTax* Ticket.Saldo);
xmonto+=Ticket.Saldo;
xmonto= xmonto - dMonto; ROUND_DOUBLE(xmonto);
double _monto;
if ( SIGNO(xmonto)>0)
    _monto= dMonto;                 //lo que ingreso es menor que lo adeudado...
else
    _monto= Ticket.Saldo;       //lo que ingreso es mayor/igual que lo adeudado...
dTax= RoundExp100(_monto * dTax);
DIG_SIG( dTax);
return dTax;
}

Promoción con un pago previo: el delta de percepción ya se aplicó a Ticket.Percepcion y a ETres, pero se descarta del monto de la promoción (P16).

pos_prom.cpp:2652-2659

//Ticket.Percepcion 
double dBackupPercepcion=Ticket.Percepcion;

double dBackPromo=dMontoPromocion;
PromoCalculatePercepcion(1, dMontoPromocion, iEsAjusteEspecial, iIngresoLoSugerido);
if(Ticket.YaHayUnPago()){
    dMontoPromocion=dBackPromo;
    }

3.4 Persistencia

Campos de dE_TResu (nombres confirmados en el DDF de dE_TResu y hE_TResu): fTax1 (monto IIBB), fTax3 (flag o alícuota IIBB × 100), iTienePercepcionCI (flag o alícuota ComInd × 100), fMontoPercepcionCI, iTienePercepcionIVA, fMontoPercepIVA21, fMontoPercepIVA105. Las constantes str_fMontoPercepcionIVA21/105 valen "fMontoPercepIVA21"/"fMontoPercepIVA105" (pos_stri.h:15-16).

Escritores de dE_TResu. Alta (pos_taxs.cpp:2203-2244), PercepcionesElement::Compute (pos_tic2.cpp:1754-1770), RecoverLast (pos_tic2.cpp:1811-1815) y la NC para su propio registro (pos_ncre.cpp:2359-2393). No se encontraron otros escritores.

Filas sintéticas. Se escriben sólo en TICKET::ComputarBinario (venta) y en pos_ncre.cpp:2645-2760 (NC). Los AppendStart de pos_taxs.cpp:599 y :731 que se mencionaban como candidatos no están relacionados (son dE_Clien y dE_Factu); pos_tick.cpp sólo marca el ítem con iPuntosVta = -1. Las filas llevan PrecioFiscal, iTax, iDepto (perfiles 276, ComInd depto y 518 depto), descripción, EAN y PLU; no llevan fNetoFiscal ni Precio.

pos_tic2.cpp:1508 y :1590-1597

if(!iYaHabiaPercepciones &&  UtilizaPercepcion(dMontoPercepcionQueHayIIBB, dMontoPercepcionQueHayCOMIND,dTrashPIVA,dTrashPIVA)){
        //Actualizamos total!
        if(VALIDAR(xb_dPF)){
            double dTTT=0L;
            ETres->field(str_fPrecioTotal,  dTTT);
            dTTT+=xb_dPF;
            ETres->assign(str_fPrecioTotal, dTTT);
            ETres->Update();
            }

En el barrido previo de ETdet, sólo iTax 3 y 4 se saltean; 7 y 8 caen en default (bucket 21 %):

pos_tic2.cpp:1393-1397

            case ttPERCEPCIONIIBB:
            case ttPERCEPCIONCOMIND:
                iYaHabiaPercepciones=1;
                totalTRESU.iItemsCount++;
                continue;               

En el cierre normal ComputarBinario(0,…) corre una vez (vía ProcesarRedondeos_y_Descuentos, pos_vnta.cpp:1738); también lo invocan los caminos de recuperación pos_prld.cpp:106 y pos_vnta.cpp:4481 (P15).

Consumidores que saltean las filas. iEsTaxPercepcion (pos_mis2.cpp:4495) cubre 3, 4, 7 y 8 y se usa en pos_info.cpp:707 (Z por departamento), :4093 y :4196 (Informe Impositivo), pos_ncre.cpp:395 (NC) y pos_cae.cpp:1184 (que es el ticket regalo, no el payload del CAE). FacturacionCAE::ComputarTaxes (pos_cae.cpp:150-165) separa 3/4 y acumula 7/8 en variables propias.

3.5 Impresión

Las líneas de percepción las imprime EnviarPercepciones con los montos que devuelve UtilizaPercepcion, no con Ticket.Percepcion; el TOTAL impreso es TotalCobrar (pos_taxs.cpp:2765).

pos_taxs.cpp:1795-1808

if(!profile.HayImpresoraFiscal){
    if(VALIDAR(dxIIBB)){
        SFCommMemB(str_SF_cDescTax0, "PERCEPCION IIBB");
        SFCommMemB(str_SF_GranTotal, dxIIBB);
        Printer( NCR_PERCEPCIONES);
        }
    //
    if(VALIDAR(dxCOMIND)){
        if(strlen(profile.cPercepcionComInd_Texto))
            SFCommMemB(str_SF_cDescTax0, profile.cPercepcionComInd_Texto);
        SFCommMemB(str_SF_GranTotal, dxCOMIND);
        Printer( NCR_PERCEPCIONES);
        }

La leyenda de ComInd (por ejemplo "COM e IND RES 240/03") es el perfil 374; si está vacío, la línea hereda la etiqueta anterior (P17). NCR_PERC.SCP imprime SF_cDescTax0 y SF_GranTotal.

ImprimirIVAS e impuestos internos por diferencia. Resta la percepción sólo para EsFacturaTipo2_3 (tipos 2, 3, 8 y 6; devuelve 0 si el perfil 544 está activo, pos_misc.cpp:5519-5525) o tipo 4:

pos_inve.cpp:4663-4676

dImpIntByDif= dTotalImpIntViaComputo - (dTotalIVA21+dTotalIVA105);

//tema Ley Transparencia Fiscal!
//if(!EsFacturaTipo2_3(Fact.TipoFacturacion)){
    dImpIntByDif= Ticket.TotalCobrar - (dNetoExento+dNetoIVA21+dNetoIVA105+dTotalIVA21+dTotalIVA105);
    if((EsFacturaTipo2_3(Fact.TipoFacturacion)||Fact.TipoFacturacion==4) && VALIDAR(Ticket.Percepcion)){
        dImpIntByDif-=Ticket.Percepcion;
        }
    ROUND_DOUBLE(dImpIntByDif);
//  }

if(!VALIDAR(dImpIntByDif) || dImpIntByDif<=0.0150L){
    dImpIntByDif=0L;
    }

Para tipos 1, 5, 7, 9 y 10 el valor de II incluye la percepción. En papel no se ve: TIC_IVA.SCP imprime sólo "IVA Contenido" para tipos 1 y 5 (la línea de II está comentada), y "IMPUESTOS INTERNOS" para 3, 4 y 6. Sí se ve en el CAE (P9).

Subtotal de Factura A (pos_taxs.cpp:2740-2746). SF_GranTotal recibe primero NetoTicketReal + dAjusteSubtotalPromo y en la línea siguiente se pisa con dSubtotalNeto de ImprimirIVAS; la primera asignación es código muerto. El subtotal impreso es el neto de ImprimirIVAS, coherente con el payload.

3.6 Payload ARCA / CAE

Orden en el cierre: FinalPrintAjusteDiferencias (pos_mppo.cpp:907) arma globalTaxesTickets y después ObtenerYGrabarCAEA (pos_mppo.cpp:1119) pide el CAE. Como GCmos.LastTicket sólo se incrementa después del cierre (pos_vnta.cpp:1753), isSameTicket es verdadero y se usa el camino global.

pos_taxs.cpp:2738 y :2751-2765

    ImprimirIVAS(0L,dTrash1,dTrash2,1, dSubtotalNeto);                      
        if(profile.iCAEA_Habilitado && ImprimirIVAS){
            double dD= (NetoTicketReal+Ticket.Percepcion); ROUND_DOUBLE(dD);
            double dTotalImpIntViaComputo= Ticket.TotalCobrar - (dD);
            ROUND_DOUBLE(dTotalImpIntViaComputo);
            dTrash1=dTrash2=0L;
            dSubtotalNeto=0L;
            ImprimirIVAS(dTotalImpIntViaComputo,dTrash1,dTrash2,0, dSubtotalNeto);          
            }

        if(POSConPercepcionesHabilitadas()){        //20140220 OK
            EnviarPercepciones(Ticket.Percepcion);
        }

        globalTaxesTickets.dImporteTotal=TotalCobrar;
        SFCommMemB(str_SF_TotalVend, TotalCobrar);

ImprimirIVAS ejecuta globalTaxesTickets.Clear() y carga netos, IVA e II (pos_inve.cpp:4706-4715); EnviarPercepciones carga las cuatro percepciones desde UtilizaPercepcion (pos_taxs.cpp:1789-1792). El parámetro dTotalImpIntViaComputo no se usa: la línea 4667 lo pisa.

pos_cae.cpp:925-937 (camino global)

    else {
        stCAEix.dBaseImponibleIVA21=globalTaxesTickets.dNetoIVA21;
        stCAEix.dBaseImponibleIVA105=globalTaxesTickets.dNetoIVA105;
        stCAEix.dBaseImponibleEXENTO=globalTaxesTickets.dNetoExento;
        stCAEix.dImporteIVA21=globalTaxesTickets.dIVA21;
        stCAEix.dImporteIVA105=globalTaxesTickets.dIVA105;
        stCAEix.dImporteIva=globalTaxesTickets.getIvas();
        stCAEix.dImporteTributo=globalTaxesTickets.getTributos();//dMontoPercepcionQueHayIIBB+dMontoPercepcionQueHayCOMIND+globalTaxesTickets.dImpuestosInternos;//OJO Perc
        stCAEix.dImporteTotal=globalTaxesTickets.getNeto()+globalTaxesTickets.getIvas()+globalTaxesTickets.getTributos();
        stCAEix.dImporteTotal=globalTaxesTickets.dImporteTotal;//la linea previa?
        stCAEix.dImpInternos=globalTaxesTickets.dImpuestosInternos;
        dMontoPercepcionQueHayIIBB= globalTaxesTickets.dMontoPercepcionIIBB;
        dMontoPercepcionQueHayCOMIND= globalTaxesTickets.dMontoPercepcionCOMIND;

pcpos.h:2932-2934

    double POS_EXPORT getNeto(){return dNetoIVA21+dNetoIVA105+dNetoExento;}
    double POS_EXPORT getIvas(){return dIVA21+dIVA105;}
    double POS_EXPORT getTributos(){return dPercIVA21+dPercIVA105+dMontoPercepcionIIBB+dMontoPercepcionCOMIND+dImpuestosInternos;}

importeNeto viene de stCAEix.dImporteNeto = globalTaxesTickets.getNeto() (pos_cae.cpp:881). En el XML: importeIva, importeNeto, importeTotal, importeTributo (pos_cae.cpp:240-243); importeOpEx e importeTotConc comentados (pos_cae.cpp:244-245); alícuotas de IVA 21 (id 5), 10,5 (id 4) y exento como alícuota 0 % (id 3) (pos_cae.cpp:291-308); tributos IIBB id 2, ComInd id 3, II id 4, PIVA id 99 (pos_cae.cpp:310-365).

Alícuota y base de IIBB/ComInd que se informan:

pos_cae.cpp:723-726 y :957-966

        ETres->field("fTax3"                ,iAlicuotaIIBB);
        dAlicuotaIIBB= iAlicuotaIIBB;   dAlicuotaIIBB/=100L;
        ETres->field("iTienePercepcionCI"   ,iAlicuotaComInd);
        dAlicuotaComInd=iAlicuotaComInd; dAlicuotaComInd/=100L;
    if(VALIDAR(dMontoPercepcionQueHayIIBB)){
        stCAEix.dIIBBAlicuota=dAlicuotaIIBB;
        stCAEix.dIIBBBaseImponible=stCAEix.dImporteNeto;
        stCAEix.dIIBBImporte=dMontoPercepcionQueHayIIBB;
        }
    if(VALIDAR(dMontoPercepcionQueHayCOMIND)){      
        stCAEix.dComIndAlicuota=dAlicuotaComInd;
        stCAEix.dComIndBaseImponible=stCAEix.dImporteNeto;
        stCAEix.dComIndImporte=dMontoPercepcionQueHayCOMIND;
        }

3.6.1 Verificación de la identidad ImpTotal == ImpNeto + ImpIVA + ImpTrib + ImpOpEx + ImpTotConc

Con N = neto (21 + 10,5 + exento, redondeado), V = IVA (redondeado), TC = Ticket.TotalCobrar, Pt = Ticket.Percepcion, Pe = suma de percepciones de dE_TResu según UtilizaPercepcion, e II = ROUND(TC − N − V − [Pt si tipo ∈ {2,3,6,8,4} y perfil 544 = 0]), llevado a 0 si es ≤ 0,015:

Caso II calculado N + V + ImpTrib ImpTotal Diferencia Estado
Tipo 3 (RI), buckets de M4cm: N 2145,21, V 450,50, II real 154,29, IIBB 64,36 2814,36 − 2595,71 − 64,36 = 154,29 2595,71 + 64,36 + 154,29 = 2814,36 2814,36 0,00 cumple
Tipo 1 (CF), mismos buckets (M4cm es tipo 1) 2814,36 − 2595,71 = 218,65 2595,71 + 64,36 + 218,65 = 2878,72 2814,36 +64,36 no cumple (P9)
Tipo 3 sin II real y residuo de redondeo −0,01 (ilustrativo) −0,01 → 0 TC + 0,01 TC +0,01 no cumple (P10)
Tipo 3, Pe ≠ Pt (P1, P6, P7) correcto contra Pt TC − (Pt − Pe) TC Pe − Pt no cumple

Conclusión: el POS no garantiza la identidad; el importeTotal sale de un número (TotalCobrar) y los componentes de otros tres (buckets de ETdet, dE_TResu filtrado y Ticket.Percepcion), y la única pieza que "cierra" (II por diferencia) tiene excepciones. Los cálculos de la tabla usan buckets persistidos de la evidencia M4cm; el CAE no se ejecutó en el arnés (perfil 482 = 0). No se verificó si el middleware TIPRE (TiFacturaOnlineManager) recalcula o rechaza antes de ARCA.

Independencia del redondeo respecto del total impreso. El TOTAL impreso es TotalCobrar (igual a importeTotal), pero los netos e IVA del payload se redondean por bucket en ImprimirIVAS (pos_inve.cpp:4656-4657 y 4684-4686) y el residuo se deposita en II o se pierde (P10). Las percepciones se redondean individualmente al calcularse; el camino global no las vuelve a redondear. En impresora fiscal se agrega un ajuste de hasta ±0,09 que no se persiste (P17).

3.7 Nota de Crédito

La NC arma su propia copia de perfiles en el constructor (pos_ncre.cpp:312-338), con la misma compuerta de mínimo que UtilizaPercepcion (pos_ncre.cpp:320-323). La tasa se reconstruye desde fTax3 del original (pos_ncre.cpp:671-702); el mínimo se re-evalúa salvo en NC parcial no libre; en ticket completo se usa el valor original.

pos_ncre.cpp:458-479

    if(profile_iUsaPercepcionesIIBB && VALIDAR(dNetoFiscal) && (iNCUsaPercepciones & utperc_IIBB) ){

        //
        double dMontoBaseComputoPP=dNetoFiscal;
        if(FacadeClienteTienePercepcionPVta(iCurrTipoCliente))
            dMontoBaseComputoPP=totxPrecioFiscal;


        if( (ncre->EsUnaNCParcial() && (!ncre->EsUnaNCLibre())) ||  ((dMontoBaseComputoPP - profile_dMinimoPercepcionesIIBB)>0.009L)){
        //  
            double dPP=(dMontoBaseComputoPP * profile_dPorcentajePercepcionesIIBB) / 100L;
            ROUND_DOUBLE(dPP);

            //override
            if(VALIDAR(dOriPercIIBB) && iTicketCompleto)
                dPP=dOriPercIIBB;

            dTotalPercepcionesIIBB=dPP;
            //
            dTotal+=dPP;
            }
        }

dNetoFiscal es la suma de fNetoFiscal de los ítems acreditados copiados del original: no descuenta promociones por medio de pago ni el beneficio QR (MATRIX F12). La NC persiste su PIVA en campos que no existen (P14):

pos_ncre.cpp:2386-2393

if(VALIDAR(dTotalPercepcionesIVA21)){
    hE_TResu->assign("iTienePercepcionIVA",1);
    hE_TResu->assign("fMontoPercepcionIVA21",dTotalPercepcionesIVA21);  
    }
if(VALIDAR(dTotalPercepcionesIVA105)){
    hE_TResu->assign("iTienePercepcionIVA",1);
    hE_TResu->assign("fMontoPercepcionIVA105",dTotalPercepcionesIVA105);    
    }

La lectura del original sí usa el nombre correcto (serv_dE_TResu->field("fMontoPercepIVA21", …), pos_ncre.cpp:622-623).

3.8 Z, Informe Impositivo y servidor

Consumidor Fuente de la percepción Observación
Z por departamento (pos_info.cpp:696-842) filas saltadas con iEsTaxPercepcion; total por ObtenerTotalesPercepciones el script no fiscal INFDE_EN.SCP no imprime la línea de percepción pero el TOTAL GRAL. la incluye
Ventas por hora, performance de cajero, informe por tipo de cliente (pos_info.cpp:990, :2880; pos_mis2.cpp:722) GetPercepcionTicket(registro) PIVA leída de ETres (P13)
Informe Impositivo (pos_info.cpp:4043-4375) detalle sin filas de percepción; total único dPercepciones de ObtenerTotalesPercepciones un solo renglón "PERCEPCIONES", sin desglose IIBB / ComInd / PIVA (P19)
Gran total de la Z (GetGrandesTotales, pos_info.cpp:1156) fPrecioTotal (incluye las filas sintéticas) depende de P1, P6, P7 y P15
Servidor / NC hE_TResu y dS_TDeta copiados del POS no se encontró un recálculo en el envío; no verificado en detalle

pos_taxs.cpp:1923-1928 y :1895-1904

if(!profile.iUsaPercepciones || (!VALIDAR(profile.dMinimoPercepciones)) || !VALIDAR(profile.dPorcentajePercepciones)){
    if(!profile.iPercepcionComIndUsa){
        if(!profile.iPERCIVA_USA)
            return;
        }
    }
if(profile.iPERCIVA_USA){   
    int iTPI=0;
    ETres->field(str_iTienePercepcionIVA, iTPI);
    if(iTPI){
        double dpi1, dpi2;dpi1=dpi2=0L;
        ETres->field(str_fMontoPercepcionIVA21, dpi1);
        ETres->field(str_fMontoPercepcionIVA105, dpi2);
        dPercIVA=dpi1+dpi2;
        }
    }

4. Hallazgos

Formato de cada hallazgo: severidad, evidencia, escenario con números, corrección propuesta (plan, no aplicada), riesgo, prueba en el arnés paranoia y esfuerzo. Restricciones respetadas en las propuestas: DGROUP casi lleno (158 bytes) → literales _far; POS_MPPO_TEXT casi lleno (97 bytes en el MAP actual) → lógica nueva en pos_tic2.cpp (29 863 libres) o pos_taxs.cpp (≈ 4 738 libres, ED7EH); POS_INVE_TEXT ≈ 2 521 libres (F627H); sin cambios de DDF; perfiles permitidos.

P1. Mínimo IIBB en 0: deshabilita en unos consumidores y cobra en otros

Severidad: rompe rompe consistencia cobrado == ARCA cuando el cliente tiene además ComInd o PIVA. Con IIBB sola, el mínimo 0 deshabilita la IIBB en toda la cadena (comportamiento confirmado, consistente pero probablemente no deseado).

Evidencia. UtilizaPercepcion exige VALIDAR(profile.dMinimoPercepciones) (pos_taxs.cpp:1741, citado en 3.1); el alta sólo exige profile.iUsaPercepciones && iTienePercepcionIIBB (pos_taxs.cpp:2228, citado en 3.2) y llega a esa línea si UtilizaPercepcion devolvió 1 por ComInd o PIVA (pos_taxs.cpp:2090-2093). La NC copia la compuerta (pos_ncre.cpp:320-323).

Escenario. Perfil 272 = 0, 273 = 3, cliente con IIBB (iConsumos = 1) y ComInd 1 % (iFPonderCom = 100), perfil 378 = 0, un ítem de 1210,00 (neto 1000,00, IVA 21 %), tipo 3.

Magnitud Valor
Alta: IIBB en fTax1 / ComInd 30,00 / 10,00
Ticket.Percepcion y cobrado 40,00 / 1250,00
Impreso (EnviarPercepciones) sólo ComInd 10,00; TOTAL 1250,00 (el ticket no suma: 1210 + 10 ≠ 1250)
Filas sintéticas / fPrecioTotal sólo ComInd / 1220,00 (30,00 menos que lo cobrado)
CAE: II = 1250 − 1210 − 40 = 0; ImpTrib = 10,00 N + V + ImpTrib = 1220,00 contra ImpTotal 1250,00
Z (GetPercepcionTicket no mira el mínimo) 40,00

Corrección propuesta. Requiere decisión del product owner (sección 6):

--- pos_taxs.cpp:2228
-if(profile.iUsaPercepciones && iTienePercepcionIIBB){
+if(profile.iUsaPercepciones && iTienePercepcionIIBB && VALIDAR(profile.dMinimoPercepciones)){  //P1: minimo 0 = deshabilitado, igual que UtilizaPercepcion
--- pos_tic2.cpp:1873 (junto con P6)
-PercepcionesElement peIIBB(UpdatePercepcion, eptype_IIBB,profile.iUsaPercepciones, dMinIIBB, ...
+PercepcionesElement peIIBB(UpdatePercepcion, eptype_IIBB,(VALIDAR(profile.dMinimoPercepciones) && VALIDAR(_dF3))?profile.iUsaPercepciones:0, dMinIIBB, ...

Riesgo. B: bajo; sólo cambia tickets hoy inconsistentes. A: medio; tickets con IIBB sola y perfil 272 = 0 empiezan a percibir.

Prueba. SETPROF 272 0, SETPROF 273 3, QRSIMPER.TXT, más ComInd forzada (requiere la extensión del hook de la sección 5.5; Becerra no tiene cliente con ComInd ni PIVA). Afirmaciones: [PERCCHK] (I13), I1 (fPrecioTotal == cobrado), I5c (línea impresa == fTax1), I14 (identidad del XML del CAE).

Esfuerzo. B: 2 h + 2 h de prueba. A: 4 h + 3 h.

P2. Base IIBB por precio al alta y por neto en el recómputo

Severidad: rompe en el beneficio QR post-pago (modo 3) para los tipos de cliente del perfil 407; en promociones previas al pago (modo 1) el dinero cierra pero la IIBB queda calculada sobre la base equivocada.

Evidencia. El alta usa dPrecioFiscalTot si ClienteTienePercepcionPVta(iTipoCli) (pos_taxs.cpp:2180 y 2219-2221) pero devuelve dRealNetoTicket = dMontoNeto (pos_taxs.cpp:2211); el recómputo construye peIIBB con dCurrentNetoTicketReal (pos_tic2.cpp:1862 y 1873). La NC sí respeta la base por precio (pos_ncre.cpp:461-463).

Escenario (QR post-pago). Tipo de cliente en la lista 407, ítem de 1210,00 (neto 1000,00), IIBB 3 %, mínimo 500, beneficio QR del 10 %.

Paso Valor
Alta: IIBB sobre precio 1210,00 × 3 % = 36,30; TotalCobrar = 1246,30
Billetera: D = 10 % de 1246,30 124,63; cobrado 1121,67
Filas QR: (1246,30 − 36,30) × 0,1 121,00; venta 1089,00
Recómputo modo 3: neto 1000 × 0,9 × 3 % 27,00 (debería ser 1089,00 × 3 % = 32,67)
Persistido: 1089,00 + 27,00 1116,00, 5,67 menos que lo cobrado

Corrección propuesta. Guardar la base IIBB vigente en un global _far de pos_tic2.cpp, fijarla en el alta y reescalarla en el recómputo con el mismo ratio (el ratio es a nivel precio, por lo que es exacto para la base por precio).

--- pos_tic2.cpp (junto a percepcionPromoArray)
+double _far dPercBaseIIBB=0L;                      //P2: base IIBB vigente (neto o precio segun perfil 407)
+static double _far dHistBaseIIBB[MAXPERCPROMELEMENTS]; //P2: historial para el modo 2
--- pos_taxs.cpp:2083 (inicio de ComputarPercepcionExistente) y :2221
+extern double _far dPercBaseIIBB;
+dPercBaseIIBB=0L;
 if(iUsaPrecioParaComputo)
    dMontoPCalculo=dPrecioFiscalTot;
+dPercBaseIIBB=dMontoPCalculo;
--- pos_tic2.cpp:1873
-... dMontoYaExistePercepcionIIBB, dCurrentNetoTicketReal,dRatio);
+... dMontoYaExistePercepcionIIBB, VALIDAR(dPercBaseIIBB)?dPercBaseIIBB:dCurrentNetoTicketReal,dRatio);
--- pos_tic2.cpp:1896-1903 (modo actualiza) y :1856-1858 (modo 2)
+   dHistBaseIIBB[percepcionPromoArray.uIdx]=dPercBaseIIBB; //antes del Add
+   dPercBaseIIBB=peIIBB.dNewNeto;
 ...
+   if(percepcionPromoArray.uIdx) dPercBaseIIBB=dHistBaseIIBB[percepcionPromoArray.uIdx-1]; //antes de RecoverLast

El CAE debe informar esa base (P11). Sin cambios en pcpos.h ni en pos_mppo.cpp.

Riesgo. Medio: cambia montos para los tipos del perfil 407 después de cualquier promoción. Nulo para los demás tipos (la base coincide con NetoTicketReal). Confirmar si uIdx es accesible fuera de la clase (no verificado); si no lo es, agregar un método de lectura.

Prueba. Escenario nuevo M4p: M4 con perfil 407 = "1" (el arnés factura tipo 1). Verificar primero si SETPROF acepta cadenas (MATRIX F7 indica que sólo maneja %d/%ld; no verificado para cadenas). Afirmaciones: I1, I1b con base precio, I1c.

Esfuerzo. 6 h + 4 h.

P3. Un recargo que cruza el mínimo nunca crea la percepción (salvo que ya exista otra)

Severidad: latente (regla de negocio y asimetría entre tipos; la cadena queda coherente en 0).

Evidencia. Guarda if(VALIDAR(Ticket.Percepcion)) y su TODO (pos_tic2.cpp:1915-1917, citado en 3.3). Dentro del recómputo, Compute crea IIBB o ComInd si el neto nuevo supera el mínimo (pos_tic2.cpp:1745-1748) y el bloque PIVA crea PIVA si la suma supera el mínimo (pos_tic2.cpp:1881-1890).

Escenario. Mínimo 1000, IIBB 3 %, neto 980,00 (precio 1185,80): sin percepción. Promo x MdeP con recargo del 5 % → neto 1029,00: la regla pediría 30,87 y no se cobra nada. Si el mismo cliente tiene además ComInd (entonces Ticket.Percepcion ≠ 0), el mismo recargo sí crea los 30,87: la regla depende de un tercero.

Corrección propuesta. Cambiar la guarda por "cliente sujeto a alguna percepción" y dejar que el recómputo cree sólo en modos 0 y 1 (pre-pago) y sólo para los tipos a los que el cliente está sujeto (P6). El modo 3 ya impide inventar.

--- pos_tic2.cpp:1917
-if(VALIDAR(Ticket.Percepcion)){
+if(VALIDAR(Ticket.Percepcion) || ((tick!=NULL) && tick->TieneAlgunTipoDePercepcion())){    //P3

Revisar que RecomputePercepcionxPromoMdeP tolere Ticket.Percepcion == 0 (usa dNetoSinPercepcion = TotalCobrar − 0; correcto).

Riesgo. Medio: tickets con recargo empiezan a percibir; necesita decisión (sección 6).

Prueba. M4r: SETPROF 272 2000, promo 290000 configurada como recargo (requiere SETPRCLI/SETPROAR sobre la promo, no verificado que el arnés tenga una promo de recargo). Afirmaciones: I1, I1b.

Esfuerzo. 3 h + 3 h.

P4. La NC re-evalúa mínimo y tasa con su propia copia de perfiles y usa la base bruta

Severidad: latente; pasa a romper consistencia en NC parcial de tickets con promoción o beneficio QR, y en NC de tickets de P1.

Evidencia. Copia de perfiles y compuerta (pos_ncre.cpp:312-323); tasa desde fTax3 del original o el perfil 273 vigente al momento de la NC (pos_ncre.cpp:671-689); mínimo re-evaluado salvo NC parcial no libre (pos_ncre.cpp:466 y 482-484, citado en 3.7); base = fNetoFiscal de los ítems acreditados, sin promociones ni beneficio (F12).

Escenario. Original: 2 ítems de 2750,00 (neto 4290,43), promo del 10 % → neto 3861,39, IIBB 3 % = 115,84, cobrado 5065,84. NC parcial de 1 ítem: IIBB NC = 2145,21 × 3 % = 64,36 frente a 57,92 proporcional (+6,44 de percepción) y total NC 2814,36 frente a 2532,92 (sobre-devolución de 281,44, de los que 6,44 son percepción). Con perfil 272 = 0 y el original de P1 (IIBB 30,00 cobrada), la NC completa no devuelve la IIBB porque profile_iUsaPercepcionesIIBB quedó en 0.

Corrección propuesta. Percepción de la NC = proporción del original, nunca recalculada:

--- pos_ncre.cpp:466-478 (idem ComInd :482-492)
-if( (ncre->EsUnaNCParcial() && (!ncre->EsUnaNCLibre())) || ((dMontoBaseComputoPP - profile_dMinimoPercepcionesIIBB)>0.009L)){
-   double dPP=(dMontoBaseComputoPP * profile_dPorcentajePercepcionesIIBB) / 100L;
+if(!ncre->EsUnaNCLibre() && VALIDAR(dOriPercIIBB) && VALIDAR(dOriBaseIIBB)){   //P4: proporcional al original
+   double dPP= dOriPercIIBB * (dMontoBaseComputoPP / dOriBaseIIBB);
+   if(dPP>dOriPercIIBB) dPP=dOriPercIIBB;
    ROUND_DOUBLE(dPP);

dOriBaseIIBB = suma de fNetoFiscal1..3 (o fPrecioFiscal1..3 para el perfil 407) del hE_TResu original, leída en SetDataAdjustTResu. La NC libre conserva el cálculo actual. Coordinar con F12 (la NC devuelve el bruto de la venta).

Riesgo. Medio; la NC se prueba poco (MATRIX MA1 no ejecutado).

Prueba. Requiere el escenario de NC de MATRIX F12 (MA1: GETPROF 190, CMD 12000, búsqueda del original sin servidor, no verificado). Afirmaciones nuevas I16: percepción NC ≤ original × proporción; suma original − NC completa == 0.

Esfuerzo. 8 h + 6 h (sin contar habilitar MA1).

P5. El recargo/descuento clásico de tarjeta no recalcula la percepción

Severidad: latente (regla y clasificación fiscal; el dinero cierra).

Evidencia. CalculateRecDescMdeP (pos_mppo.cpp:244-282, citado en 3.3) calcula sobre Ticket.Saldo, que incluye la percepción, y se usa sólo si !profile.PermitePromosXMdeP (pos_mppo.cpp:3037-3038). No hay llamada al recómputo en ese camino. El recargo no se reparte a buckets de IVA: queda en fRecDescMPP (pos_misc.cpp:4718, pos_tic2.cpp:1481) y, en el CAE, dentro de II por diferencia (pos_inve.cpp:4667).

Escenario. Venta de 2750,00 (neto 2145,21), IIBB 64,36, recargo de tarjeta del 10 %: recargo = 281,44 (6,44 de él es recargo sobre la percepción); IIBB queda 64,36 (la regla con el neto recargado daría ≈ 70,79). Cobrado 3095,80 == fPrecioTotal 2814,36 + fRecDescMPP 281,44; en el CAE tipo 3, II = 3095,80 − 2595,71 − 64,36 = 435,73 (154,29 reales + 281,44 del recargo): la identidad cierra, pero el recargo viaja a ARCA como "Imp. Internos" (no verificado en vivo).

Corrección propuesta. Después de la cadena única (sección 5): helper PercepcionPorRecDescTarjeta(double& dRecDesc) en pos_tic2.cpp que calcula el recargo sobre Saldo − Percepcion, llama al recómputo en modo 1 y devuelve el recargo más el delta; una sola llamada en pos_mppo.cpp:3038 (≈ 10 bytes de POS_MPPO_TEXT, medir el MAP). La reversión al borrar el medio necesita el modo 2 en el camino de borrado de tarjeta (no ubicado en esta revisión). La clasificación del recargo en el CAE es un problema aparte (fuera del alcance de percepciones).

Riesgo. Alto (segmento lleno, reversa, clasificación fiscal).

Prueba. Escenario M4t: PermitePromosXMdeP = 0 y un medio con recargo en dM_MdeP (no verificado que Becerra tenga uno). Afirmaciones: I1, I1b sobre el neto recargado.

Esfuerzo. 6 h + 4 h (más la decisión).

P6. El recómputo crea IIBB para un cliente que no está sujeto a IIBB

Severidad: rompe: se cobra una percepción que no se imprime, no se informa a ARCA ni queda en fPrecioTotal.

Evidencia. peIIBB se habilita con profile.iUsaPercepciones (pos_tic2.cpp:1873), no con el flag del cliente. Con el perfil 271 = 1, profile.dPorcentajePercepciones es el perfil 273 aunque el cliente no tenga IIBB (SetPercepcionVariable retorna antes de ponerla en 0, pos_taxs.cpp:122-123). Compute crea el valor (pos_tic2.cpp:1745-1748) y lo escribe en fTax1, pero fTax3 = 0 hace que UtilizaPercepcion, EnviarPercepciones, ComputarBinario, el CAE y GetPercepcionTicket lo ignoren. ComInd no tiene el problema (su tasa global se pone en 0 para clientes sin ComInd, pos_taxs.cpp:99).

Escenario. Perfil 271 = 1, 272 = 1000, 273 = 3; cliente sólo con ComInd 1 %; neto 2000,00 (precio 2420,00); promo x MdeP −10 % (−242,00).

Magnitud Valor
Alta: ComInd / IIBB 20,00 / 0
Recómputo: IIBB creada / ComInd 1800 × 3 % = 54,00 / 18,00; delta +52,00; promo −190,00
Cobrado 2420 + 20 − 190 = 2250,00 (= 2178 + 18 + 54)
Impreso / filas / fPrecioTotal ComInd 18,00 / sólo ComInd / 2196,00 (54,00 menos)
CAE tipo 3 ImpTrib 18,00; N + V + ImpTrib = 2196,00 contra ImpTotal 2250,00

Corrección propuesta. Habilitar cada elemento por el flag del cliente persistido en ETres y, para cualquier elemento deshabilitado, conservar el valor previo (esto también corrige P7):

--- pos_tic2.cpp:1736-1739
    void POS_EXPORT Compute()
    {
-   if(!iIsEnabled)
-       return;
+   if(!iIsEnabled){
+       dNewValue=dPreviousValue;   //P6/P7: deshabilitado = no cambia; Ticket.Percepcion y ETres quedan iguales
+       return;
+       }
--- pos_tic2.cpp:1873-1874
+double _dF3=0L;    ETres->field(strfTax3, _dF3);                   //P6: cliente sujeto a IIBB
+int _iCI=0;        ETres->field(str_iTienePercepcionCI, _iCI);     //P6: cliente sujeto a ComInd
-PercepcionesElement peIIBB(UpdatePercepcion, eptype_IIBB,profile.iUsaPercepciones, ...
+PercepcionesElement peIIBB(UpdatePercepcion, eptype_IIBB,VALIDAR(_dF3)?profile.iUsaPercepciones:0, ...
-PercepcionesElement peCOMIND(UpdatePercepcion, eptype_COMIND,profile.iPercepcionComIndUsa, ...
+PercepcionesElement peCOMIND(UpdatePercepcion, eptype_COMIND,_iCI?profile.iPercepcionComIndUsa:0, ...

Riesgo. Bajo: sólo cambia tickets de clientes no sujetos, donde hoy el resultado es incoherente.

Prueba. Requiere cliente con ComInd y SETPROF 271 1 (Becerra usa 271 = 2). Afirmaciones: I13 ([PERCCHK]), I1, I14.

Esfuerzo. 2 h + 3 h.

P7. PIVA con perfil 518 = 1: Ticket.Percepcion cambia y dE_TResu no

Severidad: rompe.

Evidencia. Elementos PIVA habilitados sólo si profile.iPERCIVA_USA>1 (pos_tic2.cpp:1875-1876), pero dNewValue se asigna antes de Compute (pos_tic2.cpp:1881-1884); Compute sale sin escribir ETres (pos_tic2.cpp:1738-1739); Ticket.Percepcion y DeltaValueOldNew usan el valor nuevo (pos_tic2.cpp:1893 y 1902). El alta, en cambio, calcula PIVA con cualquier valor no nulo del perfil 518 (pos_taxs.cpp:2186).

Escenario. Perfil 518 = 1, 519 = 10, 524 = 3; ítems PIVA por neto 1000,00 (precio 1210,00); promo −10 %.

Magnitud Valor
Alta: PIVA21 en ETres 30,00; cobrado 1240,00
Recómputo: nuevo 27,00, delta −3,00; promo −124,00 cobrado 1116,00; Ticket.Percepcion 27,00
ETres.fMontoPercepIVA21 30,00 (no se actualizó)
Impreso / fPrecioTotal PERCEPCION IVA21 30,00 / 1119,00 (3,00 más que lo cobrado)
CAE tipo 3: II = 1116 − 1089 − 27 = 0 N + V + ImpTrib = 1119,00 contra 1116,00

Corrección propuesta. La modificación de Compute de P6 alcanza (el elemento deshabilitado conserva el valor previo). Si el product owner decide que la PIVA se recalcula por promociones con 518 = 1, cambiar la habilitación a profile.iPERCIVA_USA (una línea en 1875-1876).

Riesgo. Bajo.

Prueba. Requiere artículo con iTienePercepcionIVA y cliente PIVA (herramienta y hook de la sección 5.5); SETPROF 518 1, 519 10, 524 3. Afirmaciones: I13, I1, I14.

Esfuerzo. 1 h + 3 h.

P8. La base PIVA (NetoConPIVA21/105) nunca se reescala

Severidad: rompe a partir del segundo recómputo en el mismo ticket (promo + QR, o dos promociones).

Evidencia. Único escritor: el alta (pos_mppo.cpp:4922-4927). El recómputo sólo actualiza Ticket.NetoTicketReal (pos_tic2.cpp:1903) y construye los elementos PIVA con Ticket.NetoConPIVA21/105 (pos_tic2.cpp:1875-1876).

Escenario. Perfil 518 = 2, PIVA 3 %, neto PIVA 1000,00. Promo −10 %: PIVA 27,00, cobrado 1116,00. Luego QR 10 %: D = 111,60, filas (1116 − 27) × 0,1 = 108,90, venta 980,10; el recómputo usa neto 1000 × 0,9 = 900 (debería ser 810) → PIVA 27,00 en lugar de 24,30. Persistido 1007,10 contra cobrado 1004,40 (2,70 de más).

Corrección propuesta.

--- pos_tic2.cpp:1903
    Ticket.NetoTicketReal += dDeltaNeto;    //20100310++ BETA
+   dHistPIVA[percepcionPromoArray.uIdx-1][0]=Ticket.NetoConPIVA21; //P8: historial (arreglo _far de pos_tic2.cpp)
+   dHistPIVA[percepcionPromoArray.uIdx-1][1]=Ticket.NetoConPIVA105;
+   Ticket.NetoConPIVA21 = pePIVA21.dNewNeto;                           //P8: la base PIVA acompaña al ratio
+   Ticket.NetoConPIVA105= pePIVA105.dNewNeto;
--- pos_tic2.cpp:1856-1858 (modo 2, antes de RecoverLast)
+   if(percepcionPromoArray.uIdx){
+       Ticket.NetoConPIVA21 = dHistPIVA[percepcionPromoArray.uIdx-1][0];
+       Ticket.NetoConPIVA105= dHistPIVA[percepcionPromoArray.uIdx-1][1];
+       }

static double _far dHistPIVA[MAXPERCPROMELEMENTS][2]; en pos_tic2.cpp: sin tocar pcpos.h.

Riesgo. Bajo a medio (toca la reversión).

Prueba. M5 con PIVA (promo 290000 + PAGO QR), requiere PIVA en el arnés. Afirmaciones: I1, I1c extendida a PIVA.

Esfuerzo. 3 h + 3 h.

P9. Impuestos internos por diferencia absorben la percepción en Factura B / consumidor final

Severidad: rompe: importeTributo cuenta la percepción dos veces y el CAE queda desbalanceado en exactamente el monto de la percepción.

Evidencia. pos_inve.cpp:4667-4670 (citado en 3.5) sólo resta la percepción para EsFacturaTipo2_3 o tipo 4; getTributos() suma percepciones e II (pcpos.h:2934); importeTotal = TotalCobrar (pos_taxs.cpp:2764, pos_cae.cpp:934). Los escenarios M4, M4c, M4cm y M4m del arnés son tipo 1 con IIBB (dump de M4m: iClientType = 1, fTax3 = 1.0000).

Escenario. Buckets de M4cm (tipo 1): II = 218,65 en lugar de 154,29; ImpTrib = 283,01; N + V + ImpTrib = 2878,72 contra ImpTotal 2814,36 (sección 3.6.1). Afecta a los tipos 1, 5, 7, 9 y 10, y a todos los tipos si el perfil 544 está activo.

Corrección propuesta. Restar siempre la percepción, usando el mismo valor que viaja en importeTributo:

--- pos_inve.cpp:4668-4670
-   if((EsFacturaTipo2_3(Fact.TipoFacturacion)||Fact.TipoFacturacion==4) && VALIDAR(Ticket.Percepcion)){
-       dImpIntByDif-=Ticket.Percepcion;
-       }
+   if(VALIDAR(Ticket.Percepcion))      //P9: la percepcion nunca es impuesto interno, para ningun tipo
+       dImpIntByDif-=Ticket.Percepcion;

Menos bytes que el original (POS_INVE_TEXT). Alternativa: restar la suma de dE_TResu (la que viaja en importeTributo), que garantiza la identidad por construcción pero esconde en II cualquier diferencia Pe ≠ Pt; se recomienda restar Ticket.Percepcion y detectar la diferencia con I13.

Riesgo. Bajo: en papel, tipos 1 y 5 no imprimen II (TIC_IVA.SCP); sólo cambia el payload y el valor de II en los tipos que sí lo imprimen con el perfil 544.

Prueba. M4cm con el volcado del XML del CAE (sección 5.5, SETPROF 482 1); afirmación I14. Regresión: MFA (tipo 3) debe quedar igual.

Esfuerzo. 1 h + 3 h.

P10. El residuo de redondeo ≤ 0,015 de II por diferencia se descarta

Severidad: rompe por un centavo: importeNeto + importeIva + importeTributo = importeTotal ± 0,01 en tickets sin impuestos internos.

Evidencia. pos_inve.cpp:4674-4676 (citado en 3.5). Los netos e IVA se redondean por bucket (pos_inve.cpp:4656-4657 y 4684-4686). MATRIX F5 documenta que el residuo existe y "siempre cae en impuestos internos"; sin II no tiene dónde caer.

Escenario (ilustrativo, no ejecutado). Bucket 21 % con neto 100,1250 e IVA 21,0250 (valores a 4 decimales que producen las filas de promoción), total 121,15; redondeando half-up: 100,13 + 21,03 = 121,16, II = −0,01 → 0; payload 121,16 contra importeTotal 121,15.

Corrección propuesta. Llevar el residuo menor o igual a 0,015 (positivo o negativo) al neto del bucket mayor antes de imprimir y de cargar globalTaxesTickets; residuos negativos mayores se registran en el log como anomalía.

--- pos_inve.cpp:4674-4676
 if(!VALIDAR(dImpIntByDif) || dImpIntByDif<=0.0150L){
+   if(VALIDAR(dImpIntByDif) && (fabs(dImpIntByDif)<=0.0150L)){ //P10: residuo al neto, identidad ImpTotal
+       if((dNetoIVA21>=dNetoIVA105) && (dNetoIVA21>=dNetoExento))  dNetoIVA21+=dImpIntByDif;
+       else if(dNetoIVA105>=dNetoExento)                           dNetoIVA105+=dImpIntByDif;
+       else                                                        dNetoExento+=dImpIntByDif;
+       }
    dImpIntByDif=0L;
    }

Riesgo. Bajo a medio: el NETO impreso de Factura A puede variar en 0,01 (queda igual al del CAE). Requiere acuerdo sobre la política de redondeo (sección 6).

Prueba. Buscar en la evidencia existente un ticket con [QRNET] res=±0.01 y artículo sin II; si no existe, un escenario con dos artículos de precios que produzcan el empate. Afirmación I14.

Esfuerzo. 2 h + 3 h.

P11. Alícuota y base de IIBB/ComInd informadas al CAE

Severidad: latente (el importe es correcto en el camino global; la alícuota y la base informadas no).

Evidencia. dAlicuotaIIBB = fTax3 / 100 (pos_cae.cpp:723-724): para un cliente con iConsumos = 1 ("usar la tasa del perfil") se informa 0,01 %. La evidencia de M4m tiene fTax3 = 1.0000, es decir 0,01 % para una percepción real del 3 %. La base informada es importeNeto (todos los buckets, incluido el exento) para IIBB y ComInd (pos_cae.cpp:957-966), aunque para el perfil 407 la base real es el precio. No se verificó si ARCA o el middleware validan importe = base × alícuota.

Corrección propuesta.

--- pos_cae.cpp:723-724
        ETres->field("fTax3"                ,iAlicuotaIIBB);
        dAlicuotaIIBB= iAlicuotaIIBB;   dAlicuotaIIBB/=100L;
+       if(iAlicuotaIIBB<=1)                        //P11: 1 = "usar tasa del perfil 273"
+           dAlicuotaIIBB=profile.dPorcentajePercepciones;
--- pos_cae.cpp:959 (y :964 para ComInd)
-       stCAEix.dIIBBBaseImponible=stCAEix.dImporteNeto;
+       stCAEix.dIIBBBaseImponible= VALIDAR(dAlicuotaIIBB) ? dMontoPercepcionQueHayIIBB/(dAlicuotaIIBB/100L) : stCAEix.dImporteNeto;    //P11

Con la cadena única, la base y la alícuota vienen de la estructura de consumo (sección 5).

Riesgo. Bajo; POS_CAE_TEXT tiene espacio (4C72H). Validar la precisión de la base invertida (redondeo a 2 decimales).

Prueba. M4 con el volcado del XML (I14b: alícuota IIBB == perfil 273 cuando fTax3 == 1).

Esfuerzo. 2 h + 2 h.

P12. Camino CAE "no global": recálculo sin redondeo y sin PIVA

Severidad: deuda (en el flujo normal no se ejecuta: isSameTicket es verdadero, sección 3.6).

Evidencia. pos_cae.cpp:889-894 recalcula IIBB y ComInd como neto × alícuota sin redondear; pos_cae.cpp:901 arma dImporteTotal sin PIVA; las filas 7/8 no tienen fNetoFiscal, así que dNetoPercepcionQueHayIVA21 queda en 0 y la PIVA no se envía (pos_cae.cpp:157-158 y 967); el prorrateo por TicketTotalNetoPOS (pos_cae.cpp:903-914) reparte la diferencia sobre netos e IVA.

Corrección propuesta. Eliminar el camino o hacerlo leer lo mismo que el global (montos de dE_TResu vía UtilizaPercepcion y CorrespondePercIVA) y registrar LOGERROR si se usa.

Riesgo. Bajo. Prueba. No aplica en el arnés (camino muerto). Esfuerzo. 1 h + 1 h.

P13. Z e informes: PIVA leída del ticket en curso y compuerta con la tasa global

Severidad: rompe consistencia de la Z con lo persistido (no afecta al CAE).

Evidencia. GetPercepcionTicket(xba) lee la PIVA de ETres (ticket en curso) en lugar de xba (pos_taxs.cpp:1895-1904, citado en 3.8); se usa en pos_taxs.cpp:1946 (Z), pos_info.cpp:990 y :2880, pos_mis2.cpp:722. ObtenerTotalesPercepciones sale sin totalizar si la tasa global es 0 y no hay ComInd ni PIVA habilitados (pos_taxs.cpp:1923-1928); con el perfil 271 = 2, cada StartTicket deja esa tasa en 0 (sección 3.1).

Escenario. Z con un ticket con PIVA 30,00: en reposo ETres es el ticket nuevo, sin flag PIVA, así que la Z suma 0 de PIVA. Con 271 = 2 (Becerra) y sin ComInd ni PIVA habilitados, la Z y el Informe Impositivo informan 0 de percepciones aunque existan tickets con fTax1 > 0 (no verificado en vivo: depende de que la Z se emita después de un StartTicket).

Corrección propuesta.

--- pos_taxs.cpp:1897-1901
-   ETres->field(str_iTienePercepcionIVA, iTPI);
+   xba->field(str_iTienePercepcionIVA, iTPI);      //P13: del registro, no del ticket en curso
    if(iTPI){
        double dpi1, dpi2;dpi1=dpi2=0L;
-       ETres->field(str_fMontoPercepcionIVA21, dpi1);
-       ETres->field(str_fMontoPercepcionIVA105, dpi2);
+       xba->field(str_fMontoPercepcionIVA21, dpi1);
+       xba->field(str_fMontoPercepcionIVA105, dpi2);
--- pos_taxs.cpp:1923-1928
-if(!profile.iUsaPercepciones || (!VALIDAR(profile.dMinimoPercepciones)) || !VALIDAR(profile.dPorcentajePercepciones)){
-   if(!profile.iPercepcionComIndUsa){
-       if(!profile.iPERCIVA_USA)
-           return;
-       }
-   }
+if(!POSConPercepcionesHabilitadas())   //P13: no depender de la tasa del ultimo cliente
+   return;

Riesgo. Bajo (lectura).

Prueba. Después de M4c (IIBB 64,36) emitir la Z / Informe Impositivo y comparar el renglón "PERCEPCIONES" con Σ fTax1 + fMontoPercepcionCI + fMontoPercepIVA21/105 de dE_TResu del día (afirmación nueva I15, extensión de informe_check.py). La parte PIVA requiere PIVA en el arnés.

Esfuerzo. 2 h + 2 h.

P14. La NC escribe la PIVA en campos inexistentes de dE_TResu

Severidad: rompe: el registro de la NC no guarda la PIVA devuelta, aunque sí la devuelve, la imprime y genera las filas 7/8.

Evidencia. pos_ncre.cpp:2388 y :2392 usan "fMontoPercepcionIVA21"/"fMontoPercepcionIVA105" (citado en 3.7); el DDF de dE_TResu y de hE_TResu sólo tiene fMontoPercepIVA21/fMontoPercepIVA105 (verificado con ddf_scan.py fields). La lectura del original en la misma NC usa el nombre correcto (pos_ncre.cpp:622-623). El comportamiento de assign sobre un nombre inexistente (error silencioso o registro) no se verificó.

Escenario. NC completa de un ticket con PIVA21 30,00: se devuelven 30,00, la fila iTax = 7 lleva 30,00, pero dE_TResu de la NC queda con iTienePercepcionIVA = 1 y fMontoPercepIVA21 = 0; la Z (P13 corregido) no resta esa PIVA.

Corrección propuesta.

--- pos_ncre.cpp:2388 y :2392
-   hE_TResu->assign("fMontoPercepcionIVA21",dTotalPercepcionesIVA21);  
+   hE_TResu->assign("fMontoPercepIVA21",dTotalPercepcionesIVA21);      //P14: nombre del DDF
-   hE_TResu->assign("fMontoPercepcionIVA105",dTotalPercepcionesIVA105);    
+   hE_TResu->assign("fMontoPercepIVA105",dTotalPercepcionesIVA105);    //P14

Literales más cortos (4 bytes menos cada uno).

Riesgo. Muy bajo. Prueba. MA1 con PIVA (requiere NC y PIVA en el arnés); alternativa sin NC en vivo: ddf_scan.py scan dE_TResu sobre una NC generada manualmente. Esfuerzo. 0,5 h + 3 h.

P15. Re-ejecución de ComputarBinario: PIVA al bucket 21 % e IIBB fuera de fPrecioTotal

Severidad: latente (sólo en recuperación tras corte).

Evidencia. El barrido saltea sólo 3 y 4 (pos_tic2.cpp:1393-1397); 7 y 8 caen en default y suman su PrecioFiscal al bucket 21 %. Si hay filas 3/4, iYaHabiaPercepciones = 1 evita duplicarlas pero tampoco suma la percepción a fPrecioTotal, que se reasigna desde los buckets (pos_tic2.cpp:1488). Con sólo PIVA, las filas se agregan de nuevo. Se ejecuta otra vez en pos_prld.cpp:106 y pos_vnta.cpp:4481 (recuperación); no se verificó si esos caminos pueden encontrar las filas ya escritas.

Corrección propuesta. Saltear 7 y 8 igual que 3 y 4 y, cuando ya había filas, sumar a fPrecioTotal el total de percepciones de ETres sin agregar filas.

--- pos_tic2.cpp:1393-1397
            case ttPERCEPCIONIIBB:
            case ttPERCEPCIONCOMIND:
+           case ttPERCEPCIONIVA21:     //P15
+           case ttPERCEPCIONIVA105:    //P15
                iYaHabiaPercepciones=1;
--- pos_tic2.cpp:1508 (rama else nueva)
+else if(iYaHabiaPercepciones && !iOnlyCompute){    //P15: fPrecioTotal incluye la percepcion ya materializada
+   double dTTT=0L; ETres->field(str_fPrecioTotal, dTTT);
+   dTTT+=GetPercepcionTicket(ETres);   ETres->assign(str_fPrecioTotal, dTTT);  ETres->Update();
+   }

Riesgo. Medio (camino de recuperación, difícil de probar). Prueba. Corte simulado entre pos_vnta.cpp:1738 y :1753 (no disponible en el arnés; requiere un hook TEST). Esfuerzo. 2 h + 4 h.

P16. Promoción con un pago previo: el delta de percepción se descarta

Severidad: latente, no verificado en vivo.

Evidencia. pos_prom.cpp:2652-2659 (citado en 3.3): PromoCalculatePercepcion(1, …) ya modificó Ticket.Percepcion y ETres, y con YaHayUnPago() se restaura dMontoPromocion sin el delta. La maquinaria ArrayProDelPerc (pos_tic2.cpp:1990-2124) usa el delta sólo para el reparto de IVA de la promoción (pos_inve.cpp:4479-4487), no para el saldo.

Escenario (a confirmar). IIBB 64,36 sobre neto 2145,21; se paga primero 1000,00 en EFECTIVO y luego la promo 290000 del 3 % en otro medio: la percepción baja a ≈ 62,43 (delta −1,93) pero el saldo sólo baja la promoción; cobrado 1,93 por encima de lo persistido.

Corrección propuesta. Primero diagnosticar con el escenario; si se confirma, aplicar el delta también con pago previo o no recalcular la percepción cuando ya hay un pago (decisión de regla).

Riesgo. Medio. Prueba. M5s: EFECTIVO parcial y luego el medio de la promo 290000 (SETPRCLI 290000 0 0, QRSIMPER.TXT, 273 = 3). Afirmaciones I1, I13. Esfuerzo. 2 h de diagnóstico + 3 h.

P17. Impresora fiscal: ajuste no persistido y etiqueta de ComInd heredada

Severidad: latente (sólo impresora fiscal; no aplica al CAE, que exige !HayImpresoraFiscal).

Evidencia. pos_taxs.cpp:1832-1840 suma a la IIBB (o a ComInd) una diferencia de hasta 0,09 contra el importe de la fiscal, sin actualizar fTax1. pos_taxs.cpp:1803-1804 no fija la etiqueta de ComInd si el perfil 374 está vacío, y la línea hereda la anterior ("PERCEPCION IIBB").

Corrección propuesta. Persistir el ajuste en ETres (fTax1 o fMontoPercepcionCI) o registrar el delta; literal _far por defecto para ComInd ("PERCEPCION COM E IND").

Riesgo. Bajo. Prueba. No disponible en el arnés (sin impresora fiscal). Esfuerzo. 1 h + 2 h.

P18. Ratio global único para todas las bases

Severidad: deuda.

Evidencia. dRatio = dRecDescPromo / (TotalCobrar − Percepcion) (pos_tic2.cpp:1861) se aplica igual a la base IIBB, ComInd, PIVA21 y PIVA105. Una promoción que afecta sólo a algunos artículos (otra alícuota de IVA, exentos, o artículos sin PIVA) desplaza las bases de forma distinta a la real; el código comentado de pos_tic2.cpp:1865-1868 muestra que se intentó usar el delta de neto calculado por la promoción.

Corrección propuesta. En la cadena única, recalcular las bases desde ETdet (filas de promoción incluidas) en lugar de reescalarlas.

Riesgo. Medio. Prueba. Promoción por departamento en un ticket con artículos 21 % y exentos. Esfuerzo. 4 h + 3 h.

P19. Ids de tributo y desglose del Informe Impositivo

Severidad: deuda.

Evidencia. Tributos: IIBB tipoTributo 2, ComInd 3, II 4, PIVA 99 con el comentario "Verificar envío de percepción como ?99=otros?" (pos_cae.cpp:310-365). No se verificó contra FEParamGetTiposTributos de ARCA ni contra el mapeo del middleware TIPRE si existen ids específicos para percepciones (IIBB, IVA, municipales). El Informe Impositivo imprime un único renglón "PERCEPCIONES" (pos_info.cpp:4312, :4339) sin desglose por tipo.

Corrección propuesta. Confirmar los ids con el área impositiva y con TIPRE; agregar desglose IIBB / ComInd / PIVA al informe (sumas ya disponibles en dE_TResu).

Riesgo. Bajo. Prueba. Revisión documental. Esfuerzo. 1 h + consulta.

5. Propuesta: cadena única

5.1 Diseño

Una sola función de cálculo y una sola estructura de consumo. Todo en pos_tic2.cpp (espacio disponible), sin cambios de DDF, con la instancia _far fuera de DGROUP.

struct stPercepciones {             //pos_tic2.cpp, instancia _far global
    int    iSujeto;                 //bits: 1 IIBB, 2 ComInd, 4 PIVA (del cliente, al elegirlo)
    double dAlicIIBB, dAlicCI, dAlicPIVA21, dAlicPIVA105;   //congeladas al elegir el cliente
    double dBaseIIBB, dBaseCI, dBasePIVA21, dBasePIVA105;   //bases vigentes
    double dIIBB, dCI, dPIVA21, dPIVA105;                   //montos redondeados
    int    iExistiaAlPago;          //bits, para el modo post-pago
    double Total(){ return dIIBB+dCI+dPIVA21+dPIVA105; }
    };

enum { PERC_ALTA, PERC_CONSULTA, PERC_PREPAGO, PERC_POSTPAGO, PERC_REVERTIR };

int POS_EXPORT ComputarPercepciones(stPercepciones& p, int iModo, double dRatio);
void POS_EXPORT PublicarPercepciones(const stPercepciones& p);  //único escritor

Consumidores (leen la estructura o dE_TResu, nunca recalculan):

Consumidor Hoy Con la cadena única
Alta al cobro ComputarPercepcionExistente ComputarPercepciones(PERC_ALTA) + PublicarPercepciones
Promos, consulta, QR, reversas RecomputePercepcionxPromoMdeP modos 0/1/2/3 misma firma exportada como fachada (no cambia pos_func.h:311)
Recargo de tarjeta ninguno PERC_PREPAGO (P5)
Filas sintéticas UtilizaPercepcion + CorrespondePercIVA dE_TResu publicado
Impresión UtilizaPercepcion dE_TResu publicado
ImprimirIVAS / CAE II por diferencia con excepciones II = TC − N − V − p.Total() para todos los tipos (P9), residuo al neto (P10); globalTaxesTickets gana alícuotas y bases
Z / informes GetPercepcionTicket con lecturas de ETres lectura del registro (P13)
NC copia propia de perfiles y recálculo proporción del original (P4)

5.2 Invariantes al cierre

Se registran con una línea [PERCCHK] al final de ProcesarRedondeos_y_Descuentos y, en TEST, las verifica assert_dump.py:

Id Invariante
I13 Ticket.Percepcion == fTax1 + fMontoPercepcionCI + fMontoPercepIVA21 + fMontoPercepIVA105 (± 0,005) y cada monto no nulo tiene su flag (fTax3, iTienePercepcionCI, iTienePercepcionIVA)
I1 (existente) fPrecioTotal == Σ dE_DDMP.dImporte − vuelto (cobrado == persistido)
I14 CAE: importeNeto + importeIva + importeTributo == importeTotal == Ticket.TotalCobrar
I14b tributos del CAE (IIBB, ComInd, PIVA21, PIVA105) == campos de dE_TResu; alícuota IIBB == tasa efectiva
I5c (existente, ampliada) cada línea de percepción impresa == campo de dE_TResu; SUBTOTAL ± Rec/Desc + percepciones == TOTAL
I9b Σ PrecioFiscal de filas iTax 3/4/7/8 == I13
I15 Z / Informe Impositivo: percepciones del día == Σ de los cuatro campos de dE_TResu del día (NC en negativo)
I16 NC: percepción NC ≤ original × proporción; original − NC completa == 0

5.3 Orden de migración (menor riesgo primero)

  1. Observabilidad sin cambio de comportamiento: [PERCCHK] (I13) en producción (log) y volcado del XML del CAE en TEST. Mide cuántos tickets reales violan hoy las invariantes.
  2. P14 y P13: nombres de campo en la NC y lecturas de la Z. Aislados.
  3. P9 y P10: identidad del CAE.
  4. P7 y P6: Compute conserva el valor previo si está deshabilitado; habilitación por flag del cliente.
  5. P8: base PIVA en el recómputo.
  6. P1 (opción B) y P3: después de la decisión del product owner.
  7. P2 y P11: base por precio y alícuota/base informadas.
  8. P4: NC proporcional, junto con F12.
  9. P16 y P15: diagnóstico y recuperación.
  10. Cadena única: con las invariantes ya en verde, reemplazar los cálculos por ComputarPercepciones detrás de la fachada exportada; después P5, P18 y la limpieza de P12.

5.4 Matriz de pruebas

Base: arnés paranoia (TEST -DQRSIM_TEST), SETPROF con readback y reversión, QRSIMPER.TXT. "Requiere hook" indica que Becerra no tiene cliente con ComInd ni PIVA y hace falta la extensión de la sección 5.5.

Escenario Configuración Verifica Hallazgos Requiere hook
IIBB sola 273 = 3, QRSIMPER.TXT (M4c) I1, I5c, I13, I14, I15 P9, P11, P13 no (CAE: 482 = 1 y volcado)
ComInd sola ComInd 1 %, 378 = 0 I13, I14 P6
PIVA 21 / 10,5 518 = 2, 519 = 10, 524/525 I13, I9b, I14 P8, P13 sí (artículo y cliente)
PIVA con 518 = 1 + promo 518 = 1 I13, I1 P7
Combinada IIBB + ComInd + PIVA todas I13, I14, I14b P1, P6
Mínimo cruzado hacia abajo (promo) 272 = 2100, promo 290000 (M5m) I1, I1b regresión no
Mínimo cruzado hacia arriba (recargo) 272 = 2000, promo de recargo I1, I1b P3 no (requiere promo de recargo)
Mínimo 0 272 = 0 con y sin ComInd I13, I5c, I14 P1 sí para la variante con ComInd
Promo pre-pago M5m / M5mq I1, I11, I12 regresión no
QR post-pago M4, M4m, M4n I1c regresión no
QR post-pago con base por precio 407 = "1" I1, I1b P2 no (verificar SETPROF con cadena)
Promo + QR con PIVA M5mq + PIVA I1, I13 P8
Pago previo + promo EFECTIVO parcial + 290000 I1, I13 P16 no
Recargo de tarjeta PermitePromosXMdeP = 0 I1, I1b P5 no (requiere medio con recargo)
Factura A / B MFA (tipo 3) y M4cm (tipo 1) I14 P9, P10 no
NC completa y parcial MA1 sobre M4c / M4 / M5m I16, I15 P4, P14 NC sí (F12); PIVA sí
Z / Informe Impositivo Z después de M4c I15 P13, P19 no

5.5 Extensiones del arnés

5.6 Estimación de esfuerzo

Ítem Código (h) Prueba (h) Total (h)
P1 (opción B / opción A) 2 / 4 2 / 3 4 / 7
P2 6 4 10
P3 3 3 6
P4 8 6 14
P5 6 4 10
P6 2 3 5
P7 1 3 4
P8 3 3 6
P9 1 3 4
P10 2 3 5
P11 2 2 4
P12 1 1 2
P13 2 2 4
P14 0,5 3 3,5
P15 2 4 6
P16 2 3 5
P17 1 2 3
P18 4 3 7
P19 1 consulta 1
Arnés: volcado CAE, hooks ComInd/PIVA, SETARTPI, afirmaciones 12 4 16
NC en el arnés (MA1) 4 4 8
Cadena única (refactor detrás de la fachada) 28 16 44

Correcciones puntuales P1 a P19: ≈ 110 h (con P1 opción B). Con arnés, NC y cadena única: ≈ 180 h. Las horas suponen el entorno actual (build TEST y producción, DOSBox-X, arnés operativo) y no incluyen el tiempo de decisión del product owner.

6. Decisiones pendientes del product owner

Tema Opciones Recomendación
Mínimo 0 (P1) A: 0 = sin mínimo; B: 0 = deshabilitado en toda la cadena B ahora (coherente con el comportamiento de hecho); A si el negocio lo pide
Recargos que cruzan el mínimo (P3) crear la percepción antes del pago / no crearla nunca crear sólo antes del pago y sólo para tipos a los que el cliente está sujeto
PIVA y promociones con perfil 518 = 1 (P7) recalcular / conservar la del alta conservar (semántica actual del 518 > 1)
Base por precio (P2) base precio en toda la cadena / eliminar el perfil 407 base precio en toda la cadena
Recargo de tarjeta y percepción (P5) recargo sobre Saldo − Percepción y recálculo / sin cambio recálculo, después de la cadena única
Redondeo del CAE (P10) residuo al neto / residuo a II aunque no haya II residuo al neto del bucket mayor
NC de tickets con promoción o QR (P4, F12) proporcional al original / recálculo proporcional al original
Ids de tributo (P19) mantener 2/3/99 / ids específicos de percepción confirmar con el área impositiva y TIPRE
Percepción en Factura B / CF confirmar si hay clientes de tipos 1, 5, 7, 9 o 10 con percepciones en producción consulta de dM_Clien en producción (no se consultó la base Becerra para no compilar datos de clientes)

7. Notas de verificación