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).
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.
- El mínimo decide si la percepción se cobra. Mínimos: perfil 272
MONTOMINIMOPERCEPCIONES(IIBB,profile.dMinimoPercepciones), perfil 378PERCEP_COMIND_MMINIMO(ComInd,dPercepcionComIndMontoMin) y perfil 519PERCIVA_MONTOMIN(IVA,dPERCIVA_MONTOMIN). - 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. - Nunca se inventa una percepción que el ticket no tenía al momento del pago.
- 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.
PagoQR_ImputarDescuento (1 → 3)POS_TIC2_TEXT (F18) y POS_VNTA_TEXT (F19); DGROUP sin cambioImplementació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.
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)
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) | antes | sí | sí, si neto > mín. | parte de 0 | M4cm (64.36), M4n (0) |
| Promo x MdeP clásica sobre EFECTIVO / tarjeta (modo 1) | antes | sí | no (guarda) | sí | M5m: 64.36 → 0 |
| Promo x MdeP clásica + PAGO QR | antes, luego después | sí (promo) | no | sí (promo) | M5mq: 0 al pagar, 0 después |
| Consulta de promo (modo 0) | antes | sí, sin escribir | no | no escribe | por código |
| Recargo / descuento de tarjeta (fuera de promo x MdeP) | antes | no lo recalcula | no | no | por código (sin llamada) |
| Beneficio QR post-pago (modo 3) | después | no, si ya existía | no | no (sólo reescala) | M4m, M4, M4n |
| Borrado / reversa del medio (modo 2) | después del alta del medio | no | restaura | restaura | por código |
| Sin percepción (tasa 0) / Factura A sin percepción | — | n/a | no | no | M1, 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();
- Se normaliza a 1 antes de calcular
dACobrar: baseTotalCobrar, asignaciones aETres, historialpercepcionPromoArray.Add,Ticket.PercepcionyTicket.NetoTicketRealson exactamente los del modo 1. La reversión (modo 2,RecoverLast) no cambia. PercepcionesElementno se tocó: con mínimo0.0la condicióndNewNeto > 0.009reescala y sólo da 0 si el neto llega a 0 (beneficio del 100%).- "Ya existía" se lee por tipo desde
dE_TResuconUtilizaPercepcion(pos_taxs.cpp:1736):fTax1,fMontoPercepcionCI,fMontoPercepcionIVA21/105.
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.
| # | Llamador | Sitio | Modo | Camino | ¿Cambió? |
|---|---|---|---|---|---|
| 1 | PagoQR_ImputarDescuento | pos_mppo.cpp:3680 | 3 (antes 1) | beneficio QR post-pago | sí, el único |
| 2 | TICKET::PromocionesMdeP → PromoCalculatePercepcion(1, …) | pos_prom.cpp:2656 → pos_tic2.cpp:1929 | 1 (iNoEsConsulta) | promo x MdeP clásica, imputación | no |
| 3 | TICKET::PromocionesMdeP_Consultar → PromoCalculatePercepcion(0, …) | pos_prom.cpp:4755 → pos_tic2.cpp:1929 | 0 | promo x MdeP clásica, consulta | no |
| 4 | TICKET::PromocionesMdeP_DeleteMdeP | pos_prom.cpp:2810 | 2 | reversa de la promo clásica (RecoverLast) | no |
| 5 | PagoQR_RevertirImputacionMdeP | pos_tic2.cpp:1215 | 2 | reversa 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
iUpdatePercepcionescribefTax1/fMontoPercepcionCI/fMontoPercepcionIVA21/fMontoPercepcionIVA105enETres.
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
- Sin estado que se filtre. La función tiene varios
return 0Ltempranos (dRecDescPromoo neto nulos,UtilizaPercepcionen 0, modo 2). Un flag global fijado por el llamador tendría que limpiarse en todos esos caminos; si quedara encendido, la siguiente promo clásica ignoraría el mínimo en silencio. - Explícito en el sitio de llamada. El modo se ve en la única línea que lo usa (
pos_mppo.cpp:3680) y la tabla de llamadores se obtiene con un grep.PromoCalculatePercepcionsólo pasa 0/1, lo que acota el modo 3 a un solo camino. - Firma exportada intacta. El parámetro
int UpdatePercepcionya existía;pos_func.h:311no cambia y ningún módulo que la importa necesita recompilarse por la firma. - Sin datos nuevos en DGROUP. DGROUP tiene 158 bytes libres; el cambio no agrega variables estáticas ni literales (DGROUP sin cambio en TEST y producción).
- Mínimo impacto en
POS_MPPO_TEXT. Enpos_mppo.cppel cambio es un carácter (1→3) más comentario: ese segmento está a 97 bytes del límite en TEST y el linker no avisa si envuelve. Toda la lógica nueva vive enpos_tic2.cpp(≈ 29.8 KB libres).
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. | Ticket | Mín. 272 | Medio | D | Percepción antes → después | Líneas impresas | TOTAL | Código anterior (razonado) | Estado |
|---|---|---|---|---|---|---|---|---|---|
| M4m | 63 (22/09) | 2000 | PAGO QR | 281.44 | 64.36 → 57.92 (neto 1930.69 < 2000) | SUBTOTAL 2750.00 · Rec/Desc −275.00 · PERCEPCION IIBB 57.92 | 2532.92 | percepción 0.00; fPrecioTotal 2475.00 contra 2532.92 cobrados (faltan 57.92) | PASS |
| M4 | 72 (23/09); antes 65, 62 | 1000 | PAGO QR | 281.44 | 64.36 → 57.92 | SUBTOTAL 2750.00 · Rec/Desc −275.00 · PERCEPCION IIBB 57.92 | 2532.92 | igual (1930.69 > 1000) | PASS |
| M4cm | 64 (22/09) | 2000 | EFECTIVO | 0.00 | 64.36 (sin QR; neto 2145.21 > 2000) | PERCEPCION IIBB 64.36 | 2814.36 | igual (no hay recómputo) | PASS |
| M4n | 66 (22/09) | 3000 | PAGO QR | 275.00 | 0.00 → 0.00 (no se inventa) | SUBTOTAL 2750.00 · Rec/Desc −275.00 | 2475.00 | igual (la guarda no entra) | PASS |
| M5m | 73 (23/09); antes 67 | 2100 | EFECTIVO + promo 290000 | 0.00 | 64.36 → 0.00 por la promo, antes de cobrar (modo 1) | SUBTOTAL 2750.00 · Desc. Esp. 3# −82.50 (sin Rec/Desc) | 2667.50 | igual en montos; imprimía además Rec/Desc EFECTIVO −146.86 (F19) | PASS |
| M5mq | 74 (23/09); antes 68 | 2100 | PAGO QR + promo 290000 | 266.75 | 0.00 al pagar (la promo la anuló) → 0.00 | SUBTOTAL 2750.00 · Rec/Desc DINI SIM −266.75 · Desc. Esp. 3# −82.50 | 2400.75 | igual | PASS |
| M1 | 69 (22/09) + smoke 70 | 1000 | PAGO QR (tasa 0) | 275.00 | sin percepción | SUBTOTAL 2750.00 · Rec/Desc −275.00 | 2475.00 | igual | PASS |
| MFA | 75 (23/09); antes 61 | — | PAGO QR, Factura A | 275.00 | sin percepción | detalle neto 2145.21 · Rec/Desc −214.52 · SUBTOTAL 1930.69 | 2475.00 | igual | PASS |
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)
| Id | Qué verifica |
|---|---|
| I1b | Percepció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. |
| I1c | Percepció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. |
| I11 | La promo clásica x MdeP efectivamente disparó: al menos una fila no-QR con bAcumulado == 1 en dB_TDeta. |
| I12 | Exactamente 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. | Ticket | Líneas Rec/Desc | Resultado | I12 |
|---|---|---|---|---|
| M5m | 73 | 0 (antes 1) | sólo Desc. Esp. 3# −82.50, TOTAL 2667.50 | PASS |
| M5mq | 74 | 1 | Rec/Desc DINI SIM −266.75 + línea de promo, TOTAL 2400.75 | PASS |
| M4 | 72 | 1 | 57.92 / 2532.92, sin cambio | PASS |
| MFA | 75 | 1 | SUBTOTAL 1930.69 / Rec/Desc −214.52, sin cambio | PASS |
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
| Archivo | Bytes | Fecha | Marcadores | Qué cambia |
|---|---|---|---|---|
pos_tic2.cpp | 92144 | 22/09 17:16 | //20260922 QRDTO percepcion minimo (4) | modo 3 de RecomputePercepcionxPromoMdeP |
pos_mppo.cpp | 164226 | 22/09 16:43 | //20260922 QRDTO percepcion minimo (1) | llamada (1, …) → (3, …) en PagoQR_ImputarDescuento |
pos_vnta.cpp | 154512 | 22/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)
| Segmento | TEST tras F18/F19 (documentado) | Libres | Producción, MAP leído 23/09 09:16 | Libres | Delta del trabajo |
|---|---|---|---|---|---|
| DGROUP | FF62H | 158 | FF62H | 158 | 0 |
POS_MPPO_TEXT | FF9FH | 97 | FD45H | 699 | 0 |
POS_TIC2_TEXT | 8B59H | 29863 | 8B59H | 29863 | +186 (F18, desde 8A9FH) |
POS_VNTA_TEXT | E5C8H | 6712 | E286H | 7546 | +3 (F19; TEST E5C5H, prod. E283H) |
POS_MAIN_TEXT | — | — | E57FH | 6785 | 0 |
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
| # | Punto | Dónde | Qué verificar |
|---|---|---|---|
| 1 | Alta del cliente y flags | SetearClienteFactuacionXCuit (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". |
| 2 | Bootstrap al entrar al cobro | ComputarPercepcionExistente (pos_taxs.cpp:2083; mínimos en :2197 PIVA, :2229 IIBB, :2236 ComInd), llamado en pos_mppo.cpp:4927 | La 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". |
| 3 | Modos de recómputo | RecomputePercepcionxPromoMdeP (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). |
| 4 | Persistencia en cabecera | dE_TResu: fTax1, fMontoPercepcionCI, fMontoPercepcionIVA21, fMontoPercepcionIVA105 | Escritos por el bootstrap y por Compute; confirmar que no hay otro escritor y que su suma es Ticket.Percepcion al cierre. |
| 5 | Filas sintéticas de detalle | dB_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. |
| 6 | Impresión de IVAs | ImprimirIVAS (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). |
| 7 | Factura 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. |
| 8 | Z e Informe Impositivo | iEsTaxPercepcion (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. |
| 9 | Nota de Crédito | pos_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
- 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). - Fuente única de verdad: proponer
dE_TResu+ filas sintéticas como origen, con invariantesΣ persistido == dE_DDMP == total CAEyimporteTributo == Σ filas de percepción == fTax1 + CI + PIVA21 + PIVA105. - Extender el arnés: capturar el XML del CAE en TEST y agregar una invariante de igualdad con lo cobrado.
- 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.
- 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)
Cambios_POS_Descuento_QR.html§5.5 todavía muestraRecomputePercepcionxPromoMdeP(1, …); el código actual es(3, …)(pos_mppo.cpp:3680). La §10.4 del mismo documento sí refleja el modo 3.- F19 se cita como
pos_vnta.cpp:4269: esa línea es el comentario; la sentenciaif(!EsPagoQR(_uId, _uIdId))está en:4273. MATRIX_RESULTS.md("Estado final") llama al perfil 272PERCEPCION_MONTOMINy al 273PERCEPCION_TASA; enpos_prof.h:299-300sonpro_MONTOMINIMOPERCEPCIONESypro_PORCENTAJEPERCEPCIONES.- El recargo/descuento de tarjeta clásico no re-evalúa el mínimo: no llama al recómputo (sección 2.3).
IMPL_NOTES.mddeja "números de producción pendientes" para F19; la sección 6 los completa desde el MAP de producción del 23/09 (POS_VNTA_TEXT E286H, +3 bytes).- El resto de números y referencias de archivo:línea de este documento se verificaron contra el código del 22/09 (
DINO_20260902).