Percepciones con el beneficio QR post-pago

Regla del mínimo de percepción, modo 3 de RecomputePercepcionxPromoMdeP y corrección F19 de la impresión, en el POS MS-DOS (rama DINO).

Trabajo: 22/09/2026 (F18, F19) y 23/09/2026 (re-verificación de F19)
Árbol: C:\Work\AI\DINO_20260902 · copia entregada: C:\tmp\20260914\pos\real\
Marcadores: //20260922 QRDTO percepcion minimo · //20260922 F19
Fuentes: paranoia\MATRIX_RESULTS.md (F18, F19), IMPL_NOTES.md, Cambios_POS_Descuento_QR.html (5.5, 10.4) y el código

1. Resumen y regla

Las percepciones (IIBB, Convenio/ComInd e IVA) sólo aplican cuando el neto del ticket supera un mínimo configurado. Con el beneficio de billetera QR post-pago, la billetera ya cobró el total incluida la percepción: el descuento D se calcula sobre el total con percepción y importe_final = total − D. Si el descuento dejaba el neto por debajo del mínimo, el recómputo anulaba la percepción y el ticket persistía menos de lo efectivamente cobrado.

Regla decidida por el product owner (22/09/2026)
  1. El mínimo decide si la percepción se cobra. Mínimos: perfil 272 MONTOMINIMOPERCEPCIONES (IIBB, profile.dMinimoPercepciones), perfil 378 PERCEP_COMIND_MMINIMO (ComInd, dPercepcionComIndMontoMin) y perfil 519 PERCIVA_MONTOMIN (IVA, dPERCIVA_MONTOMIN).
  2. Una vez que el cliente pagó (beneficio QR post-pago), el recómputo sólo puede reescalar la percepción por el ratio r = D / TotalCobrar; nunca vuelve a evaluar el mínimo.
  3. Nunca se inventa una percepción que el ticket no tenía al momento del pago.
  4. Los caminos previos al pago (promo por medio de pago clásica, consulta de promo, entrada al cobro) siguen re-evaluando el mínimo como siempre.
57.92
faltante persistido que el código anterior habría dejado en M4m (razonado, no ejecutado)
8 / 8
escenarios del alcance en PASS (M4, M4m, M4n, M4cm, M5m, M5mq, M1, MFA)
1
llamador que cambia de modo: PagoQR_ImputarDescuento (1 → 3)
+186 / +3
bytes de código: POS_TIC2_TEXT (F18) y POS_VNTA_TEXT (F19); DGROUP sin cambio

Implementación en una línea: nuevo modo 3 de RecomputePercepcionxPromoMdeP (pos_tic2.cpp:1841) que se normaliza a modo 1 y sólo cambia el mínimo aplicado a las percepciones que ya existían; el único llamador que lo usa es PagoQR_ImputarDescuento (pos_mppo.cpp:3680). En paralelo se corrigió F19: PrintRecDescMdeP (pos_vnta.cpp:4244) imprimía una línea Rec/Desc espuria para promociones clásicas pagadas con medios que no son PAGO QR.

2. Diagramas

2.1 ¿Se computa o no la percepción?

Cada evento que toca la percepción entra por un camino distinto. Las columnas 1 a 3 ocurren antes de cobrar y evalúan el mínimo; la columna 4 (el cambio de este trabajo) ocurre después de cobrar y sólo reescala; la columna 5 deshace el último recómputo.

Diagrama de decisión: se computa o no la percepción Cinco caminos: entrada a cobro, promo previa al pago modo 1, consulta modo 0, beneficio QR post-pago modo 3 y borrado o reversa modo 2. Evento que toca la percepción IIBB · ComInd · PIVA 21 / 10.5 Alta del cliente / entrada a cobro bootstrap pos_mppo.cpp:4927 Promo x MdeP previa al pago modo 1 pos_prom.cpp:2656 Consulta de promo (sin imputar) modo 0 pos_prom.cpp:4755 Beneficio QR post-pago modo 3 (nuevo) pos_mppo.cpp:3680 Borrado / reversa del medio modo 2 pos_prom.cpp:2810 · tic2:1215 cliente con percepción habilitada guarda: Ticket.Percepcion ≠ 0 guarda: Ticket.Percepcion ≠ 0 guarda: Ticket.Percepcion ≠ 0 historial percepcionPromoArray ¿neto supera el mínimo? ¿neto' supera el mínimo? Calcula el delta no escribe dE_TResu ni Ticket.Percepcion (evalúa el mínimo sin persistir) ¿el ticket ya la tenía? (por tipo) RecoverLast() restaura fTax1, CI, PIVA21/105, Ticket.Percepcion y NetoTicketReal del historial no no no se cobra neto × tasa (redondeada) 0 no se cobra reescala neto' × tasa cae a 0 regla vieja, correcta aquí × (1 − r) mínimo ignorado sigue en 0 no se inventa (mínimo vigente) Antes de cobrar: el mínimo se evalúa (sin cambios) Después de cobrar: sólo reescala Deshace el último recómputo neto' = NetoTicketReal × (1 + ratio) · ratio = dRecDescPromo / (TotalCobrar − Percepcion) · en modo 3 ratio = −r exacto
Figura 1. Diagrama de decisión. El borde grueso marca el único camino que cambió. En la columna 1 los mínimos están en pos_taxs.cpp:2197 (PIVA), :2229 (IIBB) y :2236 (ComInd); en las columnas 2 a 4 el mínimo lo aplican PercepcionesElement::Compute (IIBB/ComInd) y el bloque PIVA de RecomputePercepcionxPromoMdeP.

2.2 Secuencia del beneficio QR post-pago (números de M4m)

Secuencia del beneficio QR post-pago con percepción Cinco pasos desde el total con percepción hasta el ticket igual a lo cobrado, con las dos trampas: doble recorte y orden de llamadas. 1 2 3 4 5 Total con percepción TotalCobrar = 2814.36 venta 2750.00 + IIBB 64.36 neto 2145.21 > mínimo 2000 bootstrap pos_mppo.cpp:4927 La billetera fija D D = 281.44 r = D / 2814.36 = 0.100001 cobra 2532.92 pos_mppo.cpp:3651 D_venta y filas por IVA (2814.36 − 64.36)·r = 275.00 neto 2145.21 → 1930.69 PagoQR_AppendFilasDescuento pos_mppo.cpp:3663 Recómputo modo 3 IIBB 64.36 → 57.92 1930.69 < 2000: mínimo ignorado porque ya existía pos_mppo.cpp:3680 Ticket = cobrado 2750.00 − 275.00 + 57.92 = 2532.92 = dE_DDMP.dImporte ViewSaldo(−D) :3695 Trampa 1: doble recorte Restar a la venta el D completo (281.44) y además reescalar la percepción corta dos veces la parte de la percepción. Por eso la venta baja sólo D_venta (275.00). pos_tic2.cpp PagoQR_AppendFilasDescuento · bug //20260912 Trampa 2: orden de llamadas (F13, //OJO ORDEN) El paso 3 lee Ticket.Percepcion y el paso 4 la reescribe. Invertidos: (2814.36 − 57.92) × r = 275.65 en lugar de 275.00 y el ticket persistía 2532.27 contra 2532.92 cobrados. sin percepción el orden es indiferente Paso 5 sin el fix F18 (modo 1, razonado): IIBB 0.00 → fPrecioTotal 2475.00 contra 2532.92 cobrados, faltan 57.92
Figura 2. Flujo dentro de PagoQR_ImputarDescuento (pos_mppo.cpp:3622) con los números del ticket 63 (M4m, mínimo 2000). La línea roja punteada entre los pasos 3 y 4 marca la dependencia de orden.

2.3 Camino × qué pasa con el mínimo

Camino (tipo de pago / promoción)Momento¿Evalúa el mínimo?¿Puede crear percepción?¿Puede anularla?Evidencia
Entrada a cobro / alta del cliente (bootstrap)antessí, si neto > mín.parte de 0M4cm (64.36), M4n (0)
Promo x MdeP clásica sobre EFECTIVO / tarjeta (modo 1)antesno (guarda)M5m: 64.36 → 0
Promo x MdeP clásica + PAGO QRantes, luego despuéssí (promo)nosí (promo)M5mq: 0 al pagar, 0 después
Consulta de promo (modo 0)antessí, sin escribirnono escribepor código
Recargo / descuento de tarjeta (fuera de promo x MdeP)antesno lo recalculanonopor código (sin llamada)
Beneficio QR post-pago (modo 3)despuésno, si ya existíanono (sólo reescala)M4m, M4, M4n
Borrado / reversa del medio (modo 2)después del alta del medionorestaurarestaurapor código
Sin percepción (tasa 0) / Factura A sin percepciónn/anonoM1, MFA

"Guarda" = PromoCalculatePercepcion y PagoQR_ImputarDescuento sólo entran si VALIDAR(Ticket.Percepcion). El recargo/descuento de tarjeta clásico no llama a RecomputePercepcionxPromoMdeP (IMPL_NOTES y búsqueda en el árbol); la percepción queda como la calculó la entrada a cobro. Si un recargo se configura como promo x MdeP, sigue el camino del modo 1.

3. Detalle técnico

3.1 Modo 3 en RecomputePercepcionxPromoMdeP (verbatim)

Tres fragmentos de pos_tic2.cpp, tal como están en el árbol. Las líneas resaltadas llevan el marcador //20260922 QRDTO percepcion minimo o son el código que agrega.

pos_tic2.cpp:1826-1846 — comentario de la función, firma y normalización

1826//---------------------------------------------------------------------------
1827//UpdatePercepcion: 0=consulta, 1=actualiza, 2=revierte la ultima (RecoverLast),
1828//3=actualiza ignorando los minimos para las percepciones que el ticket YA tenia.
1829//20260922 QRDTO percepcion minimo
1830//El modo 3 existe para el beneficio QR post-pago (PagoQR_ImputarDescuento).
1831//Ahi la billetera YA cobro el total INCLUIDA la percepcion (D se calculo sobre
1832//el total con percepcion, importe_final = total - D). Si el descuento deja el
1833//neto por debajo del minimo (profile 272 / dPercepcionComIndMontoMin /
1834//dPERCIVA_MONTOMIN) el modo 1 anula la percepcion y el ticket persiste menos
1835//de lo cobrado. En el modo 3 una percepcion preexistente solo se reescala por
1836//el ratio y nunca se anula por minimo; una percepcion que el ticket NO tenia
1837//sigue sujeta al minimo (nunca se inventa una). Las promos x MdeP clasicas
1838//siguen en modo 1: son descuentos previos al cobro y ahi re-evaluar el minimo
1839//es correcto. El historial de percepcionPromoArray es identico al del modo 1,
1840//asi que la reversion (modo 2) no cambia.
1841int POS_EXPORT RecomputePercepcionxPromoMdeP(int UpdatePercepcion, double dRecDescPromo, double& dRecDescPorPercep)
1842{
1843int iIgnorarMinimo=(UpdatePercepcion==3);	//20260922 QRDTO percepcion minimo
1844if(iIgnorarMinimo)
1845	UpdatePercepcion=1;
1846double dACobrar=UpdatePercepcion==2 ? Ticket.TotalInicial:Ticket.TotalCobrar;	

pos_tic2.cpp:1869-1874 — mínimo 0 sólo para IIBB/ComInd preexistentes

1869//20260922 QRDTO percepcion minimo: en modo 3 el minimo es 0 solo si la
1870//percepcion ya existia en el ticket.
1871double dMinIIBB= (iIgnorarMinimo && VALIDAR(dMontoYaExistePercepcionIIBB)) ? 0.0L : profile.dMinimoPercepciones;
1872double dMinCOMIND= (iIgnorarMinimo && VALIDAR(dMontoYaExistePercepcionCOMIND)) ? 0.0L : profile.dPercepcionComIndMontoMin;
1873PercepcionesElement peIIBB(UpdatePercepcion, eptype_IIBB,profile.iUsaPercepciones, dMinIIBB, profile.dPorcentajePercepciones, dMontoYaExistePercepcionIIBB, dCurrentNetoTicketReal,dRatio);	
1874PercepcionesElement peCOMIND(UpdatePercepcion, eptype_COMIND,profile.iPercepcionComIndUsa, dMinCOMIND, profile.dPorcPercepcionComInd,dMontoYaExistePercepcionCOMIND, dCurrentNetoTicketReal,dRatio);

pos_tic2.cpp:1880-1892 — PIVA: no se anula el par que ya existía

1880//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!
1881pePIVA21.dNewValue= (pePIVA21.dNewNeto*pePIVA21.dPercentage)/100L;
1882ROUND_DOUBLE(pePIVA21.dNewValue);
1883pePIVA105.dNewValue= (pePIVA105.dNewNeto*pePIVA105.dPercentage)/100L;
1884ROUND_DOUBLE(pePIVA105.dNewValue);
1885//20260922 QRDTO percepcion minimo: en modo 3 no se anula la PIVA que ya existia.
1886if(	(pePIVA21.dNewValue+pePIVA105.dNewValue)<profile.dPERCIVA_MONTOMIN
1887	&& !(iIgnorarMinimo && VALIDAR(dMontoYaExistePPIVA21+dMontoYaExistePPIVA105))){
1888	pePIVA21.dNewValue=0L;
1889	pePIVA105.dNewValue=0L;
1890	}
1891pePIVA21.Compute();
1892pePIVA105.Compute();

3.2 Llamada en PagoQR_ImputarDescuento (verbatim)

pos_mppo.cpp:3651 y 3662-3683 — ratio, filas QR, recómputo de percepción (modo 3) y prorrateo

3651double dRatio= dD/dTotalAntes;		//ratio global unico: buckets y percepciones
3662if((tick!=NULL) && tick)
3663	tick->PagoQR_AppendFilasDescuento(dTotalAntes, dRatio, trx.mediodepago);
3664
3665//OJO ORDEN: esta llamada va ANTES del recomputo de percepcion.
3666//PagoQR_AppendFilasDescuento lee Ticket.Percepcion para descontar del lado
3667//VENTA solo (TotalCobrar - Percepcion)*r, y RecomputePercepcionxPromoMdeP
3668//reescribe Ticket.Percepcion en el lugar. Al reves, el monto de las filas
3669//sale calculado sobre la percepcion YA reducida y el ticket queda corto
3670//(visto en vivo en M4: 275.65 en vez de 275.00, 0.65 de menos).
3671//Percepciones: se alimenta con -r*NetoSinPercepcion para que el ratio interno
3672//de RecomputePercepcionxPromoMdeP reproduzca exactamente r. Ademas deja el
3673//historial en percepcionPromoArray para una futura reversion.
3674if(POSConPercepcionesHabilitadas() && VALIDAR(Ticket.Percepcion)){
3675	double dRecDescPorPercep=0.0L;
3676	double dNetoSinPercepcion= dTotalAntes - Ticket.Percepcion;
3677	//20260922 QRDTO percepcion minimo: modo 3 = la billetera ya cobro la
3678	//percepcion, asi que no se re-evalua el minimo para la que ya existia
3679	//(ver el comentario de RecomputePercepcionxPromoMdeP en pos_tic2.cpp).
3680	RecomputePercepcionxPromoMdeP(3, -1.0L*dRatio*dNetoSinPercepcion, dRecDescPorPercep);
3681	}
3682
3683PagoQR_ProrratearDataRedondeos(dRatio);

Orden efectivo: ratio r (3651) → PagoQR_AppendFilasDescuento (3663, lee Ticket.Percepcion) → recómputo modo 3 (3680, reescribe Ticket.Percepcion) → PagoQR_ProrratearDataRedondeos (3683) → ViewSaldo(-D) (3695). El cálculo inicial al entrar al cobro no se tocó y sigue aplicando el mínimo:

pos_mppo.cpp:4922-4934 — bootstrap al entrar al cobro (sin cambios)

4922Ticket.Percepcion=Ticket.NetoConPIVA21=Ticket.NetoConPIVA105=Ticket.NetoTicketReal=0L;
4923if(profile.iCAEA_Habilitado && EsFacturaTipo2_3(Fact.TipoFacturacion)){
4924	ComputarPercepcionExistente(Ticket.Percepcion, Ticket.NetoTicketReal,Ticket.NetoConPIVA21,Ticket.NetoConPIVA105, 1);	//1=solo netopor las dudas no tenga percepciones!
4925	}
4926if(POSConPercepcionesHabilitadas()){
4927	ComputarPercepcionExistente(Ticket.Percepcion, Ticket.NetoTicketReal,Ticket.NetoConPIVA21,Ticket.NetoConPIVA105,0);	//0=fullmode
4928	if(VALIDAR(Ticket.Percepcion)){
4929		Ticket.TotRecDesc+=Ticket.Percepcion;
4930		Ticket.Saldo= Ticket.Saldo + Ticket.Percepcion;	DIG_SIG( Ticket.Saldo);
4931		Ticket.SaldoBack= Ticket.Saldo ;
4932		Ticket.TotalNeto=Ticket.Saldo;
4933		ViewSaldo();
4934		}

3.3 Llamadores de RecomputePercepcionxPromoMdeP y sus modos

Búsqueda sobre todo el árbol: cinco llamadas efectivas (dos pasan por PromoCalculatePercepcion, cuya llamada real está en pos_tic2.cpp:1929). Declaración en pos_func.h:311, sin cambios de firma.

#LlamadorSitioModoCamino¿Cambió?
1PagoQR_ImputarDescuentopos_mppo.cpp:36803 (antes 1)beneficio QR post-pagosí, el único
2TICKET::PromocionesMdePPromoCalculatePercepcion(1, …)pos_prom.cpp:2656pos_tic2.cpp:19291 (iNoEsConsulta)promo x MdeP clásica, imputaciónno
3TICKET::PromocionesMdeP_ConsultarPromoCalculatePercepcion(0, …)pos_prom.cpp:4755pos_tic2.cpp:19290promo x MdeP clásica, consultano
4TICKET::PromocionesMdeP_DeleteMdePpos_prom.cpp:28102reversa de la promo clásica (RecoverLast)no
5PagoQR_RevertirImputacionMdePpos_tic2.cpp:12152reversa del PAGO QR (RecoverLast)no

PromoCalculatePercepcion sólo recibe 0 o 1 (pos_prom.cpp:2656 y :4755), así que ningún camino clásico puede llegar al modo 3 por accidente.

3.4 Regla de PercepcionesElement::Compute y regla PIVA

pos_tic2.cpp:1736-1756 — IIBB y ComInd (sin cambios)

1736	void POS_EXPORT Compute()
1737	{
1738	if(!iIsEnabled)
1739		return;
1740	//cuanta percepcion da ahora, si da 0, ojo ver si antes tenia, hay que considerar!
1741	//lo que de, comparar con el valor previo!
1742
1743	//lo de PIVA no se puede hacer individual!!!
1744	if(	(eptype==eptype_IIBB)||(eptype==eptype_COMIND)){
1745		if((dNewNeto-dMinimunValueToBeComputed)>0.009L){
1746			dNewValue=((dNewNeto*dPercentage)/100L);
1747			ROUND_DOUBLE(dNewValue);
1748			}
1749		else if(VALIDAR(dPreviousValue)){
1750			dNewValue=0L;//no aplica mas, entonces tenemos que saber que el valor completo previo hay que considerarlo como variacion.
1751			}
1752	}
1753		
1754	if(iUpdatePercepcion){
1755		switch(eptype){
1756			case eptype_IIBB:

IIBB / ComInd, por elemento

  • dNewNeto = dPreviousNeto × (1 + ratio), acotado a ≥ 0 (constructor).
  • Si dNewNeto − mínimo > 0.009: dNewValue = ROUND(dNewNeto × tasa / 100).
  • Si no, y había valor previo: dNewValue = 0 ("no aplica más").
  • Si no había valor previo, queda en 0 (el elemento arranca en cero con Clear()).
  • Con iUpdatePercepcion escribe fTax1 / fMontoPercepcionCI / fMontoPercepcionIVA21 / fMontoPercepcionIVA105 en ETres.

PIVA, por par (21 + 10.5)

  • Cada alícuota se calcula sin mínimo individual: ROUND(neto' × tasa / 100).
  • Si PIVA21 + PIVA105 < dPERCIVA_MONTOMIN, las dos van a 0, salvo en modo 3 si el par ya existía.
  • El bootstrap usa la misma frontera: se cobra si (suma − mínimo) ≥ 0 (pos_taxs.cpp:2197).

pos_taxs.cpp:2228-2240 — bootstrap IIBB / ComInd (sin cambios)

2228if(profile.iUsaPercepciones && iTienePercepcionIIBB){
2229	if((dMontoPCalculo - profile.dMinimoPercepciones)>0.009L){
2230		dPercIIBB= (dMontoPCalculo * profile.dPorcentajePercepciones) / 100L;
2231		ROUND_DOUBLE(dPercIIBB);
2232		}
2233	}
2234	
2235if(profile.iPercepcionComIndUsa && iTienePercepcionCOMIND){
2236	if((dMontoNeto - profile.dPercepcionComIndMontoMin)>0.009L){
2237		dPercCI= (dMontoNeto * profile.dPorcPercepcionComInd) / 100L;
2238		ROUND_DOUBLE(dPercCI);
2239		}
2240	}

3.5 Por qué el modo 3 es un parámetro y no una variable global

4. Evidencia

Build de TEST (-DQRSIM_TEST), matriz de paranoia, Z 20260903. Percepción IIBB forzada al 3% (SETPROF 273 3 + QRSIMPER.TXT) y perfil 272 variando; DM_PROFI.DAT respaldado antes de escribir y readback después de cada SETPROF. Estado final: 272 = 1000, 273 = 0, promo 290000 iCliLoyalty = 3, marcadores QRSIM*.TXT borrados. D = beneficio de la billetera; "antes → después" = percepción IIBB al momento del pago QR y al cierre.

Esc.TicketMín. 272MedioDPercepción antes → despuésLíneas impresasTOTALCódigo anterior (razonado)Estado
M4m63 (22/09)2000PAGO QR281.4464.36 → 57.92 (neto 1930.69 < 2000)SUBTOTAL 2750.00 · Rec/Desc −275.00 · PERCEPCION IIBB 57.922532.92percepción 0.00; fPrecioTotal 2475.00 contra 2532.92 cobrados (faltan 57.92)PASS
M472 (23/09); antes 65, 621000PAGO QR281.4464.36 → 57.92SUBTOTAL 2750.00 · Rec/Desc −275.00 · PERCEPCION IIBB 57.922532.92igual (1930.69 > 1000)PASS
M4cm64 (22/09)2000EFECTIVO0.0064.36 (sin QR; neto 2145.21 > 2000)PERCEPCION IIBB 64.362814.36igual (no hay recómputo)PASS
M4n66 (22/09)3000PAGO QR275.000.00 → 0.00 (no se inventa)SUBTOTAL 2750.00 · Rec/Desc −275.002475.00igual (la guarda no entra)PASS
M5m73 (23/09); antes 672100EFECTIVO + promo 2900000.0064.36 → 0.00 por la promo, antes de cobrar (modo 1)SUBTOTAL 2750.00 · Desc. Esp. 3# −82.50 (sin Rec/Desc)2667.50igual en montos; imprimía además Rec/Desc EFECTIVO −146.86 (F19)PASS
M5mq74 (23/09); antes 682100PAGO QR + promo 290000266.750.00 al pagar (la promo la anuló) → 0.00SUBTOTAL 2750.00 · Rec/Desc DINI SIM −266.75 · Desc. Esp. 3# −82.502400.75igualPASS
M169 (22/09) + smoke 701000PAGO QR (tasa 0)275.00sin percepciónSUBTOTAL 2750.00 · Rec/Desc −275.002475.00igualPASS
MFA75 (23/09); antes 61PAGO QR, Factura A275.00sin percepcióndetalle neto 2145.21 · Rec/Desc −214.52 · SUBTOTAL 1930.692475.00igualPASS

Buckets persistidos (dE_TResu) de M4m: 21% 1930.69 + 405.44 · II 138.87 · perc IIBB 57.92; fPrecioTotal 2532.92 == dE_DDMP.dImporte 2532.92. M5m: 21% 2080.85 + 436.99 · II 149.66 · perc 0.00; fRecDescMPP −146.86 = promo −82.50 + percepción anulada −64.36 (preexistente). M5m y M5mq usan mínimo 2100 porque la promo del 3% deja el neto en 2080.85, que no cruza 2000.

"Código anterior" de M4m es razonado, no ejecutado en vivo. Con modo 1, peIIBB se arma con mínimo 2000; dNewNeto = 2145.21 × (1 − 0.100001) = 1930.69; 1930.69 − 2000 ≤ 0.009 con valor previo 64.36 → dNewValue = 0. Las filas QR ya se habían calculado sobre 2814.36 − 64.36 (orden F13), así que la venta queda en 2475.00 y fPrecioTotal = 2475.00, mientras ViewSaldo(−281.44) deja TotalCobrar = dE_DDMP.dImporte = 2532.92. El ticket impreso tampoco cerraría (2750.00 − 275.00 sin línea de percepción contra TOTAL 2532.92). Fallarían I1, I1b, I1c, I3 e I5.

Invariantes nuevas o ajustadas (assert_dump.py)

IdQué verifica
I1bPercepción IIBB fTax1 == ROUND(neto × tasa) cuando aplica: sin QR, si neto − mínimo > 0.009; con QR, si el ticket ya la tenía al dispararse el QR. Si no, 0.00.
I1cPercepción a través del post-pago QR: antes = G_log − (G_catálogo + filas de promo), contrastado con ROUND(neto_antes × tasa). Si antes > 0, después == ROUND2(neto_después × tasa) > 0 sin importar el mínimo; si antes == 0, después == 0.
I11La promo clásica x MdeP efectivamente disparó: al menos una fila no-QR con bAcumulado == 1 en dB_TDeta.
I12Exactamente una línea Rec/Desc en el slice de impresora si se imputó beneficio QR (D > 0), cero si no (guarda de regresión de F19).

Re-evaluadas contra la evidencia guardada de M1/M2/M3/M4/M4c/M7/M8/M9a/M9b/M10/MFA/MD0: todas siguen en PASS.

5. F19: línea Rec/Desc duplicada en promos clásicas (encontrado y corregido)

Encontrado por M5m (ticket 67, 22/09): con la promo clásica 290000 pagada con EFECTIVO el ticket imprimía el descuento dos veces y no cerraba a la vista.

Antes (ticket 67)

SUBTOTAL:                        2750.00
Rec/Desc EFECTIVO              -146.86
00000000  Desc. Esp. 3#     -82.50
TOTAL :                          2667.50

2750.00 − 146.86 − 82.50 ≠ 2667.50. Los −146.86 son dE_MdeP.dRecDesc del EFECTIVO: promo −82.50 + la percepción −64.36 que la promo anuló.

Después (ticket 73, 23/09)

SUBTOTAL:                        2750.00
00000000  Desc. Esp. 3#     -82.50
TOTAL :                          2667.50

2750.00 − 82.50 = 2667.50. Montos persistidos idénticos antes y después (I1/I3d/I9 ya pasaban).

Causa. PrintRecDescMdeP (pos_vnta.cpp:4244, //20260903 QRSIM, código productivo) revivió la rama SF_Status==3 de TIC_VDET.SCP para cualquier medio con dRecDesc ≠ 0; sólo la fila del PAGO QR se reemplazaba por la suma de sus filas. Antes de esa función la rama estaba muerta, así que es una regresión de impresión del trabajo QRDTO. M5 original no la vio porque su promo iba sobre PAGO QR.

Corrección (//20260922 F19): la línea Rec/Desc <medio> se imprime sólo para PAGO QR. Una promo x MdeP clásica ya imprime su propio renglón Desc. Esp. ….

pos_vnta.cpp:4258-4280 (verbatim)

4258do{
4259	double dRD=0.0L;
4260	sfield(mbase, str_dRecDesc, dRD);
4261	if(!VALIDAR(dRD))
4262		continue;
4263	char cDes[24];memset(cDes,0,sizeof(cDes));
4264	mbase->field(strcDescripcion, cDes);
4265	double dPrint=dRD;			//20260917 QRDTO impresion neto
4266	int _uId=0, _uIdId=0;
4267	sfield(mbase, str_uId,   _uId);
4268	sfield(mbase, str_uIdId, _uIdId);
4269	//20260922 F19: solo el PAGO QR imprime esta linea. Una promo x MdeP clasica
4270	//deja su monto (+delta de percepcion) en dRecDesc del medio con que se pago
4271	//(p.ej. EFECTIVO) y ya se imprime como linea de promo: imprimirla aca la
4272	//duplicaba. Antes del 20260903 esta rama del script no la llamaba nadie.
4273	if(!EsPagoQR(_uId, _uIdId))
4274		continue;
4275	{
4276	double dQR= tick->PagoQR_SumaFilasDescuento(
4277					EsFacturaTipo2_3(Fact.TipoFacturacion) ? 1 : 0);
4278	if(VALIDAR(dQR))
4279		dPrint=dQR;
4280	}

Re-verificación en vivo del 23/09/2026

Sobre el binario TEST reconstruido el 22/09 (pos_vnta.obj/.exe más nuevos que pos_vnta.cpp, verificado por fecha). Perfiles con readback antes y después; revertidos al final (SETPROF 272 1000, SETPROF 273 0, SETPRCLI 290000 0 3).

Esc.TicketLíneas Rec/DescResultadoI12
M5m730 (antes 1)sólo Desc. Esp. 3# −82.50, TOTAL 2667.50PASS
M5mq741Rec/Desc DINI SIM −266.75 + línea de promo, TOTAL 2400.75PASS
M472157.92 / 2532.92, sin cambioPASS
MFA751SUBTOTAL 1930.69 / Rec/Desc −214.52, sin cambioPASS

I12 se corrió contra toda la evidencia existente: las únicas fallas aparecen en evidencia histórica ya marcada como FAIL por otros motivos (MD1, por F14; y un dump de limpieza mal rotulado M5promo que corresponde a una corrida de MCLEAN). Efecto colateral buscado: un recargo/descuento clásico en un medio no QR vuelve a no imprimir esa línea, como antes del 03/09. El camino fiscal no cambia (la función sale antes con impresora fiscal).

6. Archivos entregados

ArchivoBytesFechaMarcadoresQué cambia
pos_tic2.cpp9214422/09 17:16//20260922 QRDTO percepcion minimo (4)modo 3 de RecomputePercepcionxPromoMdeP
pos_mppo.cpp16422622/09 16:43//20260922 QRDTO percepcion minimo (1)llamada (1, …)(3, …) en PagoQR_ImputarDescuento
pos_vnta.cpp15451222/09 17:37//20260922 F19 (1)continue salvo EsPagoQR en PrintRecDescMdeP

Los tres archivos de C:\tmp\20260914\pos\real\ son byte a byte idénticos a los de C:\Work\AI\DINO_20260902 (comparados con cmp el 23/09). El marcador 20260922 no aparece en ningún otro fuente del árbol. Sin cambios de perfiles de producción, DDF, .SCP ni scripts; los perfiles 272/273 y la promo 290000 sólo se modificaron en las bases de prueba y quedaron revertidos.

Segmentos (POS_MAIN.MAP)

SegmentoTEST tras F18/F19 (documentado)LibresProducción, MAP leído 23/09 09:16LibresDelta del trabajo
DGROUPFF62H158FF62H1580
POS_MPPO_TEXTFF9FH97FD45H6990
POS_TIC2_TEXT8B59H298638B59H29863+186 (F18, desde 8A9FH)
POS_VNTA_TEXTE5C8H6712E286H7546+3 (F19; TEST E5C5H, prod. E283H)
POS_MAIN_TEXTE57FH67850

La columna de producción sale de C:\Work\AI\DINO_20260902\POS_MAIN.MAP (23/09 09:16, después de cerrarse la ventana "DINO BUILD"); el POS_MAIN.EXE de ese build no contiene la cadena QRSIM. POS_MPPO_TEXT queda a 97 bytes del límite en TEST: revisar el MAP después de cualquier cambio en pos_mppo.cpp.

7. Pendiente: revisión general de percepciones

Pedido del product owner, no realizado. Una sola cadena de cálculo de percepciones, y que lo informado a ARCA en la factura electrónica (CAE) sea igual a lo cobrado. Lo que sigue es un plan acotado de auditoría de sólo lectura; no incluye cambios de código.

7.1 Puntos de la cadena a auditar

#PuntoDóndeQué verificar
1Alta del cliente y flagsSetearClienteFactuacionXCuit (pos_ctac.cpp:697, llama a SetPercepcionVariable en :800); SetPercepcionVariable (pos_taxs.cpp:84)Origen de iTienePercepcionIIBB/COMIND (dM_Clien: iConsumos, iFPonderCom) y de la tasa variable (iUsaPercepciones == 2). El comentario del código dice "NO SE SI TAN OK".
2Bootstrap al entrar al cobroComputarPercepcionExistente (pos_taxs.cpp:2083; mínimos en :2197 PIVA, :2229 IIBB, :2236 ComInd), llamado en pos_mppo.cpp:4927La base IIBB puede ser PrecioFiscal (iUsaPrecioParaComputo, :2181), pero el recómputo reescala NetoTicketReal: confirmar si ambas bases coinciden para esos clientes. Frontera distinta por tipo (IIBB/ComInd > 0.009, PIVA ≥ 0). UtilizaPercepcion (:1741) exige mínimo IIBB distinto de 0: un mínimo 0 deshabilita IIBB en lugar de significar "sin mínimo".
3Modos de recómputoRecomputePercepcionxPromoMdeP (pos_tic2.cpp:1841), PromoCalculatePercepcion (:1913)Modos 0/1/2/3 contra la regla de la sección 1. El TODO de :1916 señala que un recargo que lleve el neto por encima del mínimo nunca crea la percepción (guarda Ticket.Percepcion ≠ 0).
4Persistencia en cabeceradE_TResu: fTax1, fMontoPercepcionCI, fMontoPercepcionIVA21, fMontoPercepcionIVA105Escritos por el bootstrap y por Compute; confirmar que no hay otro escritor y que su suma es Ticket.Percepcion al cierre.
5Filas sintéticas de detalledB_TDeta con iTax 3/4/7/8 (pos_defi.h:786-791), escritas en TICKET::ComputarBinario (pos_tic2.cpp:1329, bloque ~1508-1563)Montos de las filas == campos de dE_TResu, también después del modo 3 y de un modo 2.
6Impresión de IVAsImprimirIVAS (pos_inve.cpp:4581; II por diferencia en :4667-4670)Resta Ticket.Percepcion sólo para facturación tipo 2/3/4: verificar qué pasa con los otros tipos que tengan percepción (el II impreso absorbería la percepción).
7Factura electrónica (ARCA / CAE)pos_cae.cpp: suma por ttPERCEPCION* (:150-164), UtilizaPercepcion (:206), importeTributo (:243), tributos con tipoTributo 2/3/4/99 (:316-364), iEsTaxPercepcion (:1184)Importe total y tributos del payload == lo cobrado (dE_DDMP) y == dE_TResu, en particular tras un beneficio QR con percepción.
8Z e Informe ImpositivoiEsTaxPercepcion (pos_mis2.cpp:4495); pos_info.cpp:707 y :4093 (InformeImpositivo::LeerDatos, :4043)Las filas de percepción se saltean en los barridos de ítems: confirmar de dónde toman la Z y el informe el total de percepciones y que no se cuente dos veces ni se pierda.
9Nota de Créditopos_ncre.cpp: mínimo propio (:315), IIBB (:466), ComInd (:484), filas iTax 3/4/7/8 (:2654-2748)La NC re-evalúa el mínimo con su propia copia de perfiles (salvo NC parcial no libre) y usa el original sólo en ticket completo. Cruzar con F12: la NC de un ticket con beneficio QR devuelve el bruto.

7.2 Método propuesto

  1. Inventario de sólo lectura de los nueve puntos: quién escribe y quién lee cada magnitud (Ticket.Percepcion, dE_TResu, filas sintéticas, payload CAE).
  2. Fuente única de verdad: proponer dE_TResu + filas sintéticas como origen, con invariantes Σ persistido == dE_DDMP == total CAE y importeTributo == Σ filas de percepción == fTax1 + CI + PIVA21 + PIVA105.
  3. Extender el arnés: capturar el XML del CAE en TEST y agregar una invariante de igualdad con lo cobrado.
  4. Matriz nueva: Factura A y B; cliente con base por precio; PIVA; ComInd; recargo que cruza el mínimo; NC total y parcial de un ticket QR con percepción.
  5. Decisiones para el product owner: mínimo 0 en IIBB, recargos que cruzan el mínimo, base de cálculo por precio y NC de tickets con beneficio.

Fuera de alcance del trabajo del 22-23/09: todo lo de esta sección.

8. Notas de trazabilidad (documentos frente a código)