Cambios en el POS: descuento por pago QR
1. Que hace el POS cuando la billetera devuelve un descuento
Al cobrar con PAGO QR, la respuesta de la operacion puede traer un beneficio ya aplicado por la billetera. El POS lo recibe despues de que la operacion fue autorizada — de ahi "post-pago" — y debe reflejarlo en el ticket, en los libros fiscales y en el cierre Z sin pedirle nada al cajero.
1.1 Los tres campos que el POS lee
| Campo | Significado para el POS | Ejemplo (ticket M1) |
|---|---|---|
importe | Importe bruto que el POS pidio cobrar | 2750.00 |
importe_recdesc | Beneficio; negativo cuando es descuento, positivo seria recargo | -275.00 |
importe_final | Lo efectivamente cobrado. Es lo que se persiste como importe del medio de pago | 2475.00 |
D = -importe_recdesc // beneficio, siempre >= 0
r = D / Ticket.TotalCobrar // ratio global (TotalCobrar incluye percepcion)
condicion de aceptacion: importe_final == importe + importe_recdesc (tolerancia 0.005)
1.2 La secuencia dentro del POS
PagoQR_ImputarDescuento() (pos_mppo.cpp:3622) corre dentro del lazo de
autorizacion de _ProcessKeyTOTAL(), justo antes del Update() de la fila del
medio de pago. En ese punto hace, en este orden:
- valida las cinco guardas de entrada (seccion 5.1);
- agrega al detalle del ticket una fila por bucket de IVA
(
TICKET::PagoQR_AppendFilasDescuento,pos_tic2.cpp:1153), conbAcumulado=1y monto negativo; - reescala la percepcion (
RecomputePercepcionxPromoMdeP(1, ...)); - prorratea
dataRedondeos(relevante solo con impresora fiscal); - baja los totales del ticket con
ViewSaldo(-D); - ajusta
Ticket.total[n](importe -= D,recdesc += importe_recdesc); - escribe la fila de
dE_MdeP/dE_DMdePconimporte_final,importe_recdescy el id de autorizacion.
1.3 La decision de diseno que define todo lo demas
El beneficio se persiste igual que una promocion por medio de pago clasica: una fila real
de ETdet → dB_TDeta/dE_TDeta por bucket de IVA. A partir de ahi
TICKET::ComputarBinario netea dE_TResu solo, porque arma
totalTRESU exclusivamente barriendo ETdet; y el ticket impreso,
el CAE, el Informe Impositivo y la Nota de Credito ven el descuento sin ningun parche puntual, porque
todos leen ese mismo detalle.
No se reuso el motor de promociones. ImputaPromocionesXCodigoDescuento esta
acoplada a EProm/MProm/acupro, a
ControlUsoUnaSolaVezMdeP y al monto que teclea el cajero (dMontoIngresado), no a
un hecho post-autorizacion. Lo que se reuso es el patron de datos, no la funcion.
PagoQR_NetearTRESU() (pos_tic2.cpp, reescribia el struct totalTRESU
en memoria) y PagoQR_ProrratearIVAS() (pos_inve.cpp, prorrateaba en tiempo de
impresion). Esta ultima aplicaria el descuento dos veces: ImprimirIVAS
arranca de tick->ComputarBinario(1,...), que ya devuelve el neto. En el arbol solo quedan
comentarios que documentan el retiro.
1.4 Numeros de referencia (ticket M1)
items (bAcumulado=0) 2750.00 neto 2145.21 + IVA21 450.50 + imp.int. 154.29
fila del beneficio -275.00 neto -214.52 + IVA21 -45.06 + imp.int. -15.42
--------
dE_TResu.fPrecioTotal 2475.00 fNetoFiscal1 1930.69 / fTax0Fiscal 405.44 / fImpInterno1 138.87
dE_DDMP PAGO QR 2475.00 dRecDesc -275.00
ticket impreso SUBTOTAL 2750.00 / Rec-Desc -275.00 / TOTAL 2475.00
QR AFIP "importe": 247500
2. Flujo interno del POS
PagoQR_ImputarDescuento, que agrega las filas de detalle por bucket de IVA y baja los totales
en memoria por caminos disjuntos; ComputarBinario netea dE_TResu
desde el detalle, y de ahi salen el cierre Z, la impresion, el CAE, el Informe Impositivo y la Nota de
Credito.
ViewSaldo(-D) mueve
Ticket.TotRecDesc/TotalNeto/TotalCobrar — totales
en memoria: saldo, el TOTAL impreso y los impuestos internos por diferencia
de ImprimirIVAS. ComputarBinario arma totalTRESU
solo desde ETdet y no mira ningun campo de Ticket. Los dos
caminos son disjuntos y ahora coinciden porque salen de la misma fuente. El unico punto donde se
encuentran es ImprimirIVAS, que compone netos e IVA desde ComputarBinario (ya
neto) y saca los impuestos internos por diferencia contra Ticket.TotalCobrar (ya neto): la
identidad neto + IVA + impInt == TotalCobrar cierra por construccion.
3. Tablas afectadas y consumidores
3.1 dB_TDeta / dE_TDeta (detalle del ticket) — el cambio central
Las filas de item (bAcumulado=0) no se tocan: siguen con el
precio bruto de catalogo. Lo nuevo son las filas del beneficio, que escribe
TICKET::PagoQR_AppendFilasDescuento() copiando campo por campo el patron de
ImputaPromocionesXCodigoDescuento (pos_tic2.cpp:869-919).
| Campo | Valor | Origen |
|---|---|---|
cEan | EAN del beneficio QR | perfil 564 (con fallback, seccion 4) |
Articulo | PLU del beneficio QR | perfil 565 |
iDepto | departamento de descuentos | perfil 566 |
iPromocion | numero que identifica la fila en los reportes que filtran por esa columna | perfil 567 |
bAcumulado | 1 (marca de descuento) | fijo |
fPrecio / PrecioFiscal | monto negativo del bucket | calculo (seccion 5) |
fNetoFiscal | PrecioFiscal - (tax + imp.internos) | calculo |
fTax0/Tax0Fiscal o fTax2/Tax2Fiscal | IVA negativo segun bucket | calculo |
fImpInterno | impuestos internos negativos; recibe el residuo de centavos | calculo |
iTax | ttNORMAL21=0 / ttEXENTO=1 / ttNORMAL105=2 | pos_defi.h:783 |
Descripcion | nombre del medio de pago informado, truncado a 25 caracteres; fallback DTO BILLETERA QR | trx.mediodepago |
Cantidad / Unidades / iBonos | 1 / 1 / 0 | fijo |
iPromoPrior | 0. El campo guarda el Metodo_Accion de la promo de origen (MA_MONTO..MA_LISTADESCUENTO); no hay accion del motor de promos detras de este beneficio, y 0 no lo usa ninguna promo real, asi que la fila queda identificable | fijo, deliberado |
iNroTicket, cFecha, AssignPackId | del ticket en curso | contexto |
Ejemplo M1: un item 21%, beneficio 10% → una sola fila
cEan Articulo iDepto iPromocion bAcu iTax PrecioFiscal fNetoFiscal Tax0Fiscal fImpinterno
7799999999991 QRTEST01 777 990077 1 0 -275.00 -214.52 -45.06 -15.42
Ejemplo M2: 21% + 10.5% + exento → tres filas
cEan Articulo iDepto iPromocion bAcu iTax PrecioFiscal fNetoFiscal Tax0/2Fiscal fImpinterno
7799999999991 QRTEST01 777 990077 1 0 -274.99 -214.52 -45.06 -15.41
7799999999991 QRTEST01 777 990077 1 2 -49.91 -45.16 -4.75 0.00
7799999999991 QRTEST01 777 990077 1 1 -149.00 -149.00 0.00 0.00
suma -473.90 == -D
Una fila por bucket con monto distinto de cero. Un bucket sin items no genera fila. Los
valores de cEan/Articulo/iDepto/iPromocion del ejemplo
son los de prueba; en produccion salen de los perfiles cargados (seccion 4).
3.2 dE_TResu (resumen del ticket)
Se llena integramente en TICKET::ComputarBinario(0, ...). Con
-DNOCOMPUTAREDONDEOS activo (postcpnu.mak:137), totalTRESUPromo
— donde caen las filas bAcumulado==1 — se suma a totalTRESU antes de
los assign, asi que las filas del beneficio bajan los campos por si solas.
| Campo | Sin beneficio | Con beneficio (M1) | Comentario |
|---|---|---|---|
fPrecioTotal / fPrecioTot1 / fPrecioFiscal1 | 2750.00 | 2475.00 | total persistido = cobrado |
fNetoFiscal1 / fPrecioNeto | 2145.21 | 1930.69 | neto 21% |
fTax0 / fTax0Fiscal | 450.50 | 405.44 | IVA recalculado sobre el neto nuevo |
fImpInterno / fImpInterno1 | 154.29 | 138.87 | absorbe el residuo de centavos |
fRecDescMPP | 0.00 | 0.00 | siempre 0 para el QR, ver 3.3 |
fTax1 / fTax3 / fMontoPercepcion* | 64.36 | 57.92 | percepcion IIBB reescalada por r; no forma parte de fPrecioFiscal1..3, se suma aparte a fPrecioTotal |
Las tasas salen de Fact.TaxPorc[0] (21%) y Fact.TaxPorc[2] (10.5%), con fallback
IVA/neto de la propia fila si Fact no viene cargada.
3.3 dE_DDMP (medios de pago del ticket) y su espejo .BIN
| Campo | Valor | Ejemplo M1 |
|---|---|---|
uId / uIdId | medio de pago PAGO QR | 22 / 10 |
cDescripcion | nombre del medio informado | DINI SIM |
dImporte | importe_final: lo efectivamente cobrado | 2475.00 |
dRecDesc | importe_recdesc: el beneficio, negativo | -275.00 |
cCuenta | id de transaccion | — |
vData | id de autorizacion — es la marca que lee la guarda de borrado y la que habilita una anulacion futura | SIM-38 |
cPlan | id de intencion | SIM-38 |
.BIN no es equivalente y no hay que confiar en el.
Dump2DDMP (pos_misc.cpp:4743) tiene un bloque
if(EsPagoQR(uId,uIdId)) que pisa cBarCode con el id de intencion y copia
vData; _SaveRecordBinDDMP no tiene ninguno de los dos. Medido: Btrieve
cBarCode=SIM-4 / cPlan=0 / vData=SIM-4 contra .BIN
cBarCode=(vacio) / cPlan=SIM-4 / vData=(vacio). Es preexistente, pero
para identificar la autorizacion el dato confiable es la fila Btrieve.
Cambio de QRFILA en Dump2DDMP (pos_misc.cpp:4717)
if(!EsPagoQR(uId, uIdId)) //20260913 QRFILA
dTotalRecDescMPP += dRecDesc;
Con las filas de detalle, fPrecioTotal ya viene neto; acumular ademas el
dRecDesc del QR en fRecDescMPP haria que ReadTRESU_Totals (camino
de recuperacion ReStartEndVueltoOK, pos_vnta.cpp) lo restara dos veces. El monto
no se pierde: sigue en dE_DDMP.dRecDesc, que es de donde ya lo lee
AnalyzeRecDesc (pos_info.cpp:3873) para el informe de recargos y descuentos.
Cambio de contrato a avisar: una promo por medio de pago sobre un medio que
no sea QR sigue entrando en fRecDescMPP, y como esa promo tambien netea
fPrecioTotal por sus propias filas, ReadTRESU_Totals la restaria dos veces en el
camino de recuperacion. Es un hueco preexistente, anterior a este trabajo, pero ahora
esta identificado.
3.4 dE_MdeP / dE_DMdeP y dE_TMdeP
dE_MdeP(memoria, browse de medios ingresados):dImporte = importe_final,dRecDesc = importe_recdesc. Es la fila que recorrePrintRecDescMdePpara imprimir la lineaRec/Desc.dE_DMdeP: espejo de la anterior; suvDataes lo que leePagoQR_BloquearBorradoMdeP.dE_TMdeP: no tiene campo de ticket (esta indexada poriNroY: son movimientos de valores del cajero).Ticket.total[n].importe -= DyTicket.total[n].recdesc += importe_recdeschacen queUpdateTMdePgrabe el importe cobrado real mas una accionidRECARGO_DESCUENTO(pos_mppo.cpp:1205), que es lo que hace cerrar el Z.CheckLimitesno rechaza porqueVALIDAR(Ticket.TotRecDesc)ya es cierto.
3.5 ZLOG\<Z>.bin
Binario de items de la Z, escrito al cerrar la Z. El equivalente vivo de una Z abierta es
BASES\DB_TDETA.BIN, identico en layout (mismo DDF, mismo escritor DdfBin).
Es la fuente del Informe Impositivo, que lo copia a vT_TDeta y lo recorre separando por
iTax y por bAcumulado: como las filas del beneficio viven en el detalle, ese
informe ya las ve.
3.6 dM_Profi
Cuatro perfiles nuevos (564-567, seccion 4). No se agrego ni se modifico ningun campo DDF en todo el trabajo.
3.7 Matriz de consistencia de los consumidores
| Consumidor | Que lee | Resultado con beneficio QR | Estado |
|---|---|---|---|
Cierre Z — GetGrandesTotales (pos_info.cpp:1179), xHora_Tot (:985), GetPerformanceCajeroVentas (:2877), ComputeValues (pos_mis2.cpp:717) |
dE_TResu.fPrecioTotal / fPrecioFiscal1..3. Ninguno lee fRecDescMPP desde disco |
2475.00: el cobrado real, contado una sola vez | consistente |
Recuperacion de ticket — ReadTRESU_Totals (pos_vnta.cpp:4668) |
fPrecioTotal + fRecDescMPP sobre el ETres en memoria del ticket en curso |
2475.00 + 0.00 = 2475.00. Esa es la razon tecnica de persistir fRecDescMPP = 0 |
consistente |
CAE — pos_cae.cpp BuildXML |
globalTaxesTickets, llenado al final de ImprimirIVAS (exportado en pos_main.def:161). Rama alternativa: ComputarTaxes() desde ETdet, con autocorreccion contra Ticket.TotalNeto |
QR AFIP "importe": 247500 |
consistente |
Ticket impreso — ImprimirIVAS (pos_inve.cpp) + PrintRecDescMdeP (pos_vnta.cpp) |
ComputarBinario(1,...) (ya neto) e impuestos internos por diferencia contra Ticket.TotalCobrar (ya neto) |
SUBTOTAL 2750.00 / Rec/Desc <medio> -275.00 / TOTAL 2475.00. No hay doble impresion: las filas nuevas no salen como linea de detalle, porque el detalle se imprime al escanear cada item y estas se agregan recien en el cobro |
consistente |
Informe Impositivo — InformeImpositivo::LeerDatos (pos_info.cpp:4043) |
binario de items de la Z (vT_TDeta), no dE_TResu |
2750.00 - 275.00 = 2475.00 con IVA 405.44, verificado sobre las nueve corridas con beneficio | consistente |
Nota de Credito — pos_ncre.cpp |
el detalle de items del ticket original; su dFactorConversionPorPromos sale de las filas bAcumulado==1 (pos_ncre.cpp:1056-1104) |
Con la fila presente, la NC deberia ajustar sola sin tocar pos_ncre.cpp. No se pudo ejercitar en el entorno de prueba |
no verificado |
| Envio al servidor | lee dB_TDeta (y el resumen dE_TResu) |
Ambas fuentes quedan netas y coherentes entre si | no ejercitado |
Informe de recargos y descuentos — AnalyzeRecDesc (pos_info.cpp:3873) |
dE_MdeP.dRecDesc |
-275.00: el beneficio sigue visible por medio de pago | consistente |
La identidad que ata los tres libros:
SUM(dB_TDeta.PrecioFiscal, filas no-percepcion)
== dE_TResu.fPrecioTotal - percepciones
== SUM(dE_DDMP.dImporte) - percepciones
4. Configuracion: perfiles
4.1 Perfiles nuevos (564-567)
Identifican la fila de detalle del beneficio. 563 era el ultimo id definido en
pos_prof.h y 553 el ultimo presente en la tabla dM_Profi de la copia de
prueba, asi que 564-567 estaban libres de los dos lados. Declarados en pos_prof.h:672-675;
miembros de GLOBALPROFILE en pcpos.h:571-574; leidos en
pos_main.cpp:4692-4699; resueltos en un solo lugar por
PagoQR_FilaIds() (pos_tic2.cpp:1095).
| id | Simbolo | Tipo | Uso | Fallback si falta o viene vacio |
|---|---|---|---|---|
| 564 | pro_QR_PROMO_EAN | texto | cEan de la fila del beneficio | profile.cIdEAN_StockDescuentos; si tambien esta vacio, 9999999999999 |
| 565 | pro_QR_PROMO_PLU | texto | Articulo de esa fila | profile.cIdArtPromoGlobal; si tambien esta vacio, 0 |
| 566 | pro_QR_PROMO_DEPTO | int | iDepto de esa fila | profile.iIdDepto_StockDescuentos; si es 0, 999 |
| 567 | pro_QR_PROMO_PROMONR | long | iPromocion: identifica la fila en los reportes que filtran por esa columna | 0 |
SETPROF -s el campo cName queda vacio y el
volcado de perfiles del arranque (pos_main.cpp:4982, formato "%d=%.20s=%.60s")
los muestra como 564=?=<valor>. El volcado recorre la tabla entera, asi que los nuevos
aparecen solos sin tocar nada.
4.2 Perfiles existentes que influyen
| id | Simbolo real | Por que importa para este cambio |
|---|---|---|
| 493 | pro_PAGOQR_HABILITADO | Habilita el medio de pago PAGO QR. Sin esto no hay nada que imputar |
| 516 | pro_PAGOQR_IDMDEP | uId del medio PAGO QR (22 en la copia de prueba). Es lo que consulta EsPagoQR(), del que dependen la guarda de borrado y la exclusion de fRecDescMPP |
| 500 | pro_PAGOQR_IDTRX_A_ANULAR | Lo escribe PagoQR_ImputarDescuento y lo consume la Reversa QR. Se limpia en cada COBRO() y en cada StartTicket (pos_main.cpp:1969/:2350), asi que solo referencia la ultima operacion |
| 271 | pro_USAPERCEPCIONES | Gatea UtilizaPercepcion() via VALIDAR() (pos_defi.h:1026). Si esta en cero, todo el bloque de percepcion del calculo es un no-op |
| 272 | pro_MONTOMINIMOPERCEPCIONES | dMinimoPercepciones: monto minimo para que aplique la percepcion |
| 273 | pro_PORCENTAJEPERCEPCIONES | dPorcentajePercepciones: la tasa. Es el perfil que determina si el camino de percepcion (el mas delicado del calculo) se ejecuta o no |
| 276 | pro_DEPTO_PERCEPCIONES | iDepto de la fila sintetica PERCEPCION_IIBB que el POS ya materializa en dB_TDeta (codigo preexistente). Esa fila la excluye ComputarBinario de los buckets de IVA, y el calculo del beneficio tambien la saltea |
| 197 | iUsa_dM_ProAr | Habilita las promociones por medio de pago (LoadMdePGloPromo(), pos_prld.cpp:1101-1141). Relevante porque una promo por MdeP y el beneficio QR pueden convivir en el mismo ticket, y de ahi sale el ratio efectivo de la seccion 5.2 |
| 190 | pro_SOLO_NCREDITO | Habilita las Notas de Credito. Hay que confirmarlo antes de validar el comportamiento de la NC sobre un ticket con beneficio |
PERCEPCION_USA, PERCEPCION_MONTOMIN,
PERCEPCION_TASA y PERCEPCION_DEPTO que circulan en notas previas
no existen: los simbolos reales son los de la tabla
(pos_prof.h:298-304). Los ids si son correctos.
5. Reglas de calculo y guardas
5.1 Guardas de entrada (PagoQR_ImputarDescuento, pos_mppo.cpp:3622)
| # | Condicion | Accion | Log |
|---|---|---|---|
| 1 | !VALIDAR(importe_recdesc) (D == 0) | no-op, flujo normal | ninguno |
| 2 | D < 0 (recdesc positivo = recargo) | rechazo; el ticket cierra sin descuento | [QRDTO] RECHAZADO ... |
| 3 | |importe_final - (importe + importe_recdesc)| >= 0.005 | rechazo | [QRDTO] RECHAZADO ... |
| 4 | D > Ticket.TotalCobrar (estricto) | rechazo | [QRDTO] RECHAZADO ... |
| 5 | id de autorizacion ya imputado (comparado contra cQRDto_TrxId) | no-op idempotente | [QRDTO] ya imputado trx=... |
Fuera de esta funcion hay una guarda mas: ActiveDMdeP() (pos_mppo.cpp:3240)
impide cargar el PAGO QR dos veces en el mismo ticket —
"Error, solo permitido ingreso PAGOQR una vez!".
5.2 Ratio global y ratio efectivo
D = -importe_recdesc ROUND_DOUBLE
r = D / Ticket.TotalCobrar_antes // ratio global, con percepcion incluida
dDVenta = ROUND((Ticket.TotalCobrar_antes - Ticket.Percepcion) * r)
r_eff = dDVenta / SUM(PrecioFiscal de las filas de item)
- Por que
rincluye la percepcion: elDinformado se calculo sobre el total que el POS pidio cobrar, y ese total ya venia inflado por la percepcion bruta. - Por que el lado venta la resta: a la percepcion le baja su parte
RecomputePercepcionxPromoMdePpor separado. Restarle a la venta elDcompleto la cortaria dos veces. Sin percepcion,dDVenta == Dexacto y el ajuste es un no-op bit a bit. - Por que existe
r_eff: un ticket que ya trae una promo por medio de pago tiene elTicket.TotalCobrarbajado por esa promo, pero todavia no tiene su fila enETdet(se escribe mas tarde, enProcesarRedondeos_y_Descuentos). Recalcular el ratio contra el bruto real de los items es lo que hace que las filas sumen exactamente-dDVenta: medido,r_eff = 0.097000contra elr = 0.100000global.
5.3 Reparto por bucket de IVA
Se barre ETdet con los mismos buckets que ComputarBinario
(indice 0 = 21%, 1 = 10.5%, 2 = EXENTO), tomando solo filas de item
(bAcumulado==0 y iTax que no sea percepcion), y acumulando por bucket precio,
IVA, impuestos internos y neto. Despues, por bucket:
K = ROUND(N * r_eff) // recorte del neto
N' = N - K // neto descontado
II' = ROUND(II * (1 - r_eff)) // impuestos internos escalados
T' = ROUND(N' * (1 + tasa)) - N' // IVA RECALCULADO, nunca prorrateado
fila_j = (N' + T' + II') - (N + T + II) // el monto de la fila es el DELTA, negativo
|IVA - ROUND(neto * tasa)| = 0.0000 exacto. Las filas brutas historicas
tienen 1 centavo de desacuerdo (450.50 persistido contra 450.49 calculado), que es preexistente y es la
razon por la que el control de IVA necesita una tolerancia de 0.01.
• indice de bucket de
totalTRESU y del calculo de la fila: 0=21%, 1=10.5%, 2=EXENTO;
• campo
iTax persistido (enum TaxTypes, pos_defi.h:783): 0=ttNORMAL21, 1=ttEXENTO, 2=ttNORMAL105;
•
ImputaPromocionesXCodigoDescuento itera invertido: 0=exento, 1=10.5%, 2=21% (comentado en pos_tic2.cpp:897).
5.4 Residuo de centavos
residuo = -dDVenta - SUM(fila_j)
Va entero a los impuestos internos del bucket de neto mas grande, entre los buckets que
tienen items. Es el mismo destino que le da ImprimirIVAS, que saca los impuestos internos por
diferencia contra Ticket.TotalCobrar: por eso la fila persistida queda identica a la
impresa y a la del CAE. Medido: M1 persiste II 138.87 (residuo +0.01) y M2, con un
articulo 10.5% y uno exento mas, persiste II 138.88. En un ticket solo exento o solo 10.5%
el residuo cae en los impuestos internos de ese unico bucket con neto, que es lo que hace tambien la
impresion.
5.5 Percepcion
if(POSConPercepcionesHabilitadas() && VALIDAR(Ticket.Percepcion)){
double dNetoSinPercepcion = dTotalAntes - Ticket.Percepcion;
RecomputePercepcionxPromoMdeP(1, -1.0L * dRatio * dNetoSinPercepcion, dRecDescPorPercep);
}
Se alimenta con -r * NetoSinPercepcion para que el ratio interno de
RecomputePercepcionxPromoMdeP reproduzca r exacto. Reescala
Ticket.Percepcion y Ticket.NetoTicketReal; no toca ninguna fila de
ETdet, ni siquiera la fila sintetica de percepcion. Deja ademas el historial en
percepcionPromoArray, que es lo que permite revertirlo con
RecomputePercepcionxPromoMdeP(2, ...). Medido: sin QR, neto 2145.21 →
fTax1 64.36; con beneficio del 10%, neto 1930.69 → fTax1 57.92
(cociente 0.8999).
5.6 Orden de las llamadas
PagoQR_AppendFilasDescuento tiene que ir ANTES del bloque de
percepcion. Esa funcion lee Ticket.Percepcion para calcular dDVenta, y
RecomputePercepcionxPromoMdeP reescribe esa variable en el lugar. Al reves, el monto de las
filas sale calculado sobre la percepcion ya reducida: medido,
(2814.36 - 57.92) * 0.100001 = 275.65 en vez de 275.00, con lo que el ticket
persistia 2532.27 contra 2532.92 efectivamente cobrados. En el codigo hay un comentario
//OJO ORDEN que explica la dependencia. Sin percepciones el orden es indiferente,
por eso ningun otro escenario lo detecto.
5.7 Caso limite: beneficio del 100% (r == 1)
La guarda 4 rechaza solo D > TotalCobrar (estricto), asi que D == G pasa.
La formula por bucket con r_eff == 1 da K = N, N' = 0,
II' = 0 y T' recalculado sobre neto cero, sin ningun caso especial.
Medido: filas sumando -4739.00, todos los buckets en 0.00,
fPrecioTotal 0.00, ticket impreso TOTAL 0.00. El POS no rechaza un total
de 0.00. Los rangos que acotan este ratio usan limite superior cerrado
(0 < r <= 1.0 + 1e-9, con clamp a 1.0 exacto): un intervalo abierto dejaria el caso del
100% sin netear (ver seccion 9).
6. Bloqueo de borrado de un PAGO QR autorizado
6.1 La politica
Una fila de PAGO QR ya autorizada no se puede borrar desde la pantalla de medios de pago — la billetera ya cobro. El camino correcto es la Reversa QR. Una fila de PAGO QR cargada pero no autorizada si se puede borrar, y en ese caso se limpia todo lo imputado.
6.2 La exposicion: donde queda el ticket abierto despues de autorizar
En el camino feliz el ticket cierra en la misma tecla y no hay ventana. Las salidas por error posteriores a la imputacion son la exposicion:
| Camino | file:line | Que pasa |
|---|---|---|
_ProcessKeyTOTAL "Si dio error, chau" | pos_mppo.cpp:3950-3955 (return 4) | CloseUpdateTMdeP, ModoVuelto=0, vuelve al browse de medios de pago |
Falla de UpdateTMdeP / limites / ret==999 | pos_mppo.cpp:3985-3994 (return 5) | PError, reversa de cuenta corriente; si ret==999 marca iErrorFisSeCancTickDelAll=999 |
Error fiscal 999 → handler de kbTOTAL | pos_mppo.cpp:4178 | DeleteVueltos() + while(!DeleteLastMDEP(iTeniaPromo)); |
kbQUIT | pos_mppo.cpp:4198 | idem |
kbDELETE_MEDIO | pos_mppo.cpp:4207 y :4212 | borra el ultimo medio; la variante :4212 pasa iEspecialEMV=1 |
ProcessKeyMdeP (dos lazos mas) | pos_mppo.cpp:4966 y :4981 | idem while(!DeleteLastMDEP(...)) |
Sin guarda, al borrarse la fila de PAGO QR quedaban en ETdet las filas del beneficio y en
memoria los acumuladores: un ticket pagado despues en efectivo persistia igual el
descuento.
6.3 El precedente y la guarda
El criterio ya existia en la casa para tarjetas EMV (pos_mppo.cpp:1563,
DeleteLastCardYaAutorizada): una tarjeta ya autorizada no se borra desde la pantalla de medios
de pago. La guarda de QR se inserta en pos_mppo.cpp:1581, inmediatamente despues de la EMV y
antes de SPbase->Remove(); el cuerpo vive en pos_tic2.cpp:1174.
if(PagoQR_BloquearBorradoMdeP(SPbase, _iIdMdeP, _iIdIdMdeP, cQRDto_TrxId))
return 1;
EsPagoQR(uId, uIdId)→ si no es QR, no hace nada.- Marca de autorizacion =
vDatade la fila dedE_DMdePde este ticket; si esa fila no lo trae, cae al acumulador en memoria. - Sin marca → se permite borrar (QR cargado pero nunca autorizado).
- Con marca →
Error("PAGO QR autorizado: anule via Reversa QR"), log de error y corta.
1, no 0 como la guarda EMV. Copiar el 0
colgaria el POS: la guarda EMV solo corre con iEspecialEMV, cuyo unico llamador
(kbDELETE_MEDIO) hace una sola llamada, mientras que la guarda de QR corre
siempre y los cuatro lazos while(!DeleteLastMDEP(...)) de
:4178, :4198, :4966 y :4981 girarian para siempre con
la fila sin borrar. 1 es el mismo valor que DeleteLastMDEP ya devuelve cuando no
hay nada que borrar (pos_mppo.cpp:1519-1523), asi que todos los llamadores lo manejan bien:
los lazos cortan en la fila QR (el barrido es de abajo hacia arriba, las filas por encima
ya se borraron) y las llamadas unicas salen de la pantalla de medios de pago.
El ticket nunca queda bloqueado: siempre se puede cerrar con TOTAL, que es
lo que corresponde cuando la billetera ya cobro.
6.4 Limpieza cuando el borrado si procede
pos_mppo.cpp:1612 llama a PagoQR_RevertirImputacionMdeP() (pos_tic2.cpp:1198), que:
- Saca del detalle las filas del beneficio (
TICKET::PagoQR_RemoveFilasDescuento,pos_tic2.cpp:1207), identificadas por el mismo triple con que se escribieron:bAcumulado==1+iPromocion+cEan+Articulo+iNroTicket.Remove()invalida el cursor, por eso el barrido se reinicia despues de cada baja. - Deshace el recomputo de percepcion con
RecomputePercepcionxPromoMdeP(2, ...), que es exactamente lo que haceTICKET::PromocionesMdeP_DeleteMdePpara una promo por medio de pago clasica (pos_prom.cpp:2810). - Limpia los acumuladores del beneficio.
Los totales no se tocan ahi: DeleteLastMDEP ya los devuelve a los valores previos al QR.
Saldo' = 0 + (G-D) - (-D) = G (vuelve a deber el bruto)
TotRecDesc' = TotRecDesc + D = 0
TotalNeto' = TotalInicial + 0 = G
TotalCobrar' = G
No se revierte dataRedondeos (lo prorratea
PagoQR_ProrratearDataRedondeos, pos_mppo.cpp:3588). Es un no-op sin impresora
fiscal, porque CalcularRedondeos() solo lo llena con
profile.iAjustaRedondeFiscal && HayImpresoraFiscal. Con impresora fiscal real
es un pendiente.
6.5 Que ve y que hace el cajero
- Presiona la tecla de borrar medio de pago sobre la fila
PAGO QR. - Ve el mensaje
PAGO QR autorizado: anule via Reversa QRy la fila queda. - Si tiene que deshacer la operacion, usa la Reversa QR: menu Admin/Tarjetas, tecla
kbANULACION_PAGOQR(pos_cara.cpp:1195). - Si no, simplemente cierra el ticket con
TOTAL: el pago QR ya esta hecho y el ticket cierra por el importe cobrado.
Limites de la Reversa QR que hay que conocer antes de instruir al cajero:
- es total, no parcial;
- alcanza solo la ultima operacion, porque el id sale del perfil 500, que se limpia en
cada
COBRO()y en cadaStartTicket; - no limpia
vDatade la fila dedE_DMdeP, asi que despues de una reversa exitosa la guarda sigue bloqueando el borrado del medio de pago.
6.6 Decisiones abiertas
| # | Decision pendiente | Estado |
|---|---|---|
| 1 | Despues de una reversa exitosa, quien limpia la marca? Hoy nadie. Opciones: (a) que la reversa limpie vData/cCuenta de la fila del ticket en curso, (b) que la reversa borre directamente la fila de medio de pago, (c) dejarlo asi y que el ticket cierre igual. | sin implementar |
| 2 | Camino automatico del error fiscal 999 (pos_mppo.cpp:4178): antes borraba todos los medios sin preguntar; con la guarda el lazo corta en la fila QR y quedan esa fila y las de abajo. Es el comportamiento deseado, o se prefiere un mensaje explicito al cajero? | abierta |
| 3 | Override de supervisor: no se implemento. El lugar natural seria dentro de PagoQR_BloquearBorradoMdeP, pidiendo clave antes de devolver 0. | no implementado |
| 4 | Recuperacion tras reinicio (F14). Si el ticket queda abierto con un PAGO QR ya autorizado y el POS se reinicia, la fila del beneficio sobrevive (esta en disco, en el detalle) pero la fila del medio de pago no (vive en memoria). Medido en la recuperacion: fPrecioTotal 2475.00 contra 2750.00 cobrados en efectivo, es decir descuento persistido sin el pago que lo justificaba. Antes de este cambio el mismo reinicio dejaba el ticket bruto aunque la billetera ya hubiera cobrado: error del mismo tamano y signo contrario. Hay que definir la politica: reconstruir la fila del medio de pago al recuperar, borrar las filas del beneficio, o bloquear el cierre hasta resolver. | NO RESUELTO |
| 5 | Impresora fiscal: falta revertir dataRedondeos (6.4) y falta el comando fiscal de descuento por medio de pago (PROMOMDEPFISCAL / AJUSTE_DIFERENCIAS, pos_mis2.cpp:1787), que hoy depende de filas de promo. La rama fiscal de TIC_VDET.SCP quedo deliberadamente cortada en PrintRecDescMdeP por la guarda profile.HayImpresoraFiscal. | pendiente |
7. Archivos fuente a integrar
| Archivo | Modulo | Funciones agregadas | Funciones retiradas / cambios |
|---|---|---|---|
pos_tic2.cpp | POS_MAIN | PagoQR_FilaIds() (:1095, identidad de la fila en un solo lugar);
TICKET::PagoQR_AppendFilasDescuento() (:1153);
TICKET::PagoQR_RemoveFilasDescuento() (:1207);
PagoQR_BloquearBorradoMdeP() (:1174);
PagoQR_RevertirImputacionMdeP() (:1198) |
Retirada PagoQR_NetearTRESU(). ComputarBinario vuelve a asignar fRecDescMPP = dTotalRecDescMPP sin condicional |
pos_mppo.cpp | POS_MAIN | PagoQR_ImputarDescuento() (:3622); PagoQR_ProrratearDataRedondeos() (:3588); acumuladores dQRDto_Descuento / dQRDto_TotalAntes (POS_EXPORT, es decir _far _export → fuera de DGROUP) y static char _far cQRDto_TrxId[26] |
Tres llamadas finas: append dentro de PagoQR_ImputarDescuento, guarda de borrado (:1581) y reversa (:1612) en DeleteLastMDEP. El cuerpo de esas dos ultimas vive en pos_tic2.cpp por presupuesto de segmento |
pos_misc.cpp | POS_MAIN | — | Dump2DDMP (:4717): if(!EsPagoQR(uId,uIdId)) dTotalRecDescMPP += dRecDesc; |
pos_inve.cpp | DLL POS_INVE | — | Retirada PagoQR_ProrratearIVAS() y su llamada en ImprimirIVAS; quedan los comentarios que documentan por que (:4380, :4655) |
pos_main.cpp | POS_MAIN | — | Los cuatro GetProfile de 564-567 (:4692-4699), junto a los demas |
pos_vnta.cpp | POS_MAIN | PrintRecDescMdeP(): recorre dE_MdeP y por cada fila con dRecDesc != 0 posiciona el registro, setea SF_Status=3 y SF_GranTotal=dRecDesc, y llama a TICK_VAL_DET. Es generica: lista cualquier medio de pago con recargo o descuento, no solo el QR |
Revive una rama de TIC_VDET.SCP que existia desde 1998 y que nadie disparaba, asi que no hace falta tocar ningun .SCP. Guardas: EstaEnModoCobro, tick!=NULL, VALIDAR(Ticket.TotRecDesc) y profile.HayImpresoraFiscal |
pcpos.h | header | GLOBALPROFILE: cQRPromo_EAN[20], cQRPromo_PLU[15], iQRPromo_Depto, lQRPromo_PromoNr (:571-574); dos miembros de TICKET (:944-945) |
profile es _far → FAR_DATA: no consume DGROUP |
pos_prof.h | header | Los cuatro ids 564-567 (:672-675) | — |
pos_func.h | header | Declaraciones de PagoQR_BloquearBorradoMdeP y PagoQR_RevertirImputacionMdeP (:313-315) | — |
pos_main.def | link | _dQRDto_Descuento y _dQRDto_TotalAntes en EXPORTS (:164-165) |
Obligatorio: en este build los simbolos de datos compartidos EXE→DLL se exportan a mano. Sin esto el link de POS_INVE.DLL falla con Undefined symbol _dQRDto_Descuento. Cualquier cambio en el .def obliga a relink de POS_MAIN + implib + regenerar las DLLs |
postcpnu.mak | build | Macro QRSIM_DEFINE |
Ver 7.1 |
7.1 Nota sobre el .mak
postcpnu.mak lleva una macro nueva, declarada en Translator Definitions
antes de la definicion de CC:
!ifdef QRSIM_TEST
QRSIM_DEFINE = -DQRSIM_TEST
!else
QRSIM_DEFINE =
!endif
CC = bcc286 -crtdll +postcpnu.cfg $(QRSIM_DEFINE)
QRSIM_DEFINE queda vacio y CC resulta identico al
de siempre (con un espacio de mas al final, inocuo). El build de produccion es el de siempre y no requiere
ninguna variante. Verificado: strings sobre el POS_MAIN.EXE de
produccion y las 16 DLLs → cero ocurrencias de material de prueba; las unicas
cadenas propias que quedan son las de log de produccion
([QRDTO] D=..., [QRDTO] RECHAZADO ..., [QRDTO] ya imputado ...,
[QRFILA] r=... D=... bruto=... filas=... res=...,
[QRDTO] fila QR eliminada, descuento revertido ...,
[QRDTO] borrado bloqueado trx=..., [QRDTO] linea Rec/Desc MdeP ...).
Trampa de make a tener presente al integrar: Borland MAKE decide recompilar
por fecha de archivo, no por flags. Al alternar configuraciones de build sin tocar los
.cpp, los .obj quedan "al dia" con la configuracion anterior y el link produce un
binario que no corresponde. Hay que borrar a mano los .obj afectados
(pos_main.obj, pos_mppo.obj, pos_vnta.obj, pos_tic2.obj,
pos_misc.obj) o tocar los .cpp para adelantar su fecha.
7.2 Presupuesto de segmentos
| Segmento | Produccion | Libre |
|---|---|---|
| DGROUP | FF42H (65346) | 190 |
POS_MPPO_TEXT | FD45H (64837) | 699 |
POS_TIC2_TEXT | 87D3H (34771) | 30765 |
POS_VNTA_TEXT | E06EH (57454) | 8082 |
POS_MAIN_TEXT | E57FH (58751) | 6785 |
POS_MISC_TEXT | CBAAH (52138) | 13398 |
PrintRecDescMdeP puesta en pos_mppo.cpp hizo que el MAP mostrara
POS_MPPO_TEXT pasando de FEDDH (65245) a 00B7H; mas tarde aparecio
un POS_MPPO_TEXT 0001013F. Por eso PrintRecDescMdeP vive en
pos_vnta.cpp y todo el cuerpo nuevo vive en pos_tic2.cpp
(30 KB libres), dejando en pos_mppo.cpp solo las llamadas y los extern.
Hay que revisar pos_main.map despues de cualquier cambio en
pos_mppo.cpp. Todos los literales nuevos son static char _far /
static double _far, por eso DGROUP no se movio.
8. Resultados de validacion
Corridas end-to-end sobre una copia real de bases, Z 20260903. G = bruto de catalogo,
D = beneficio, "neto persistido" = dE_TResu.fPrecioTotal.
| Escenario | Ticket | Bruto G | D | Neto persistido | Filas | Estado |
|---|---|---|---|---|---|---|
| Un item 21%, beneficio 10% | 38 | 2750.00 | 275.00 | 2475.00 | 1 | PASS |
| 21% + 10.5% + exento | 39 | 4739.00 | 473.90 | 4265.10 | 3 | PASS |
| Tres articulos con impuestos internos | 40 | 3230.00 | 323.00 | 2907.00 | 1 | PASS |
| QR parcial (1000.00) + EFECTIVO | 44 | 2750.00 | 100.00 | 2650.00 | 1 | PASS |
| 15 items, tasas mezcladas, centavos feos | 41 | 8776.46 | 877.65 | 7898.81 | 3 | PASS |
| Beneficio 33%, buckets sub-centavo | 42 | 4739.00 | 1563.87 | 3175.13 | 3 | PASS |
Beneficio 100% (r == 1) | 43 | 4739.00 | 4739.00 | 0.00 | 3 | PASS |
| Guarda 4: beneficio mayor que el total | 45 | 2750.00 | 0.00 (rechazado) | 2750.00 | 0 | PASS |
| Solo EFECTIVO (regresion) | 47 | 2750.00 | 0.00 | 2750.00 | 0 | PASS |
| Recuperacion de ticket abierto | 46 | 2750.00 | 0.00 | 2750.00 | 0 | PASS |
| Control de percepcion IIBB 3%, sin QR | 48 | 2750.00 | 0.00 | 2814.36 | 0 | PASS |
| Percepcion IIBB + beneficio QR | 50 | 2814.36 | 281.44 | 2532.92 | 1 | PASS |
| Promo por medio de pago + beneficio QR | 52 | 2750.00 | 266.75 | 2400.75 | 1 + 1 de promo | PASS |
| Intento de borrado de un PAGO QR autorizado | — | 2750.00 | 275.00 | — | 1 | guarda OK |
Controles verificados en cada corrida: total persistido == cobrado; IVA por tasa == ROUND(neto *
tasa); fila de dE_DDMP con dImporte y dRecDesc correctos y
fRecDescMPP == 0; suma de items de dB_TDeta == bruto de catalogo; suma de las
filas del beneficio == -D; una fila por tasa presente y ninguna de mas; los cuatro campos de
identidad de la fila iguales a los perfiles; suma del detalle == fPrecioTotal; ticket impreso
y QR fiscal coherentes; sin rechazos ni lineas de log inesperadas.
Casos destacados. Percepcion + QR: los buckets de venta dan
1930.69 + 405.44 + 138.87 = 2475.00, identicos al split sin percepcion; la percepcion queda en
57.92 y fPrecioTotal 2532.92 == dE_DDMP.dImporte. Promo por medio de pago + QR: la promo
dispara antes (-82.50), el beneficio se aplica sobre la base ya descontada
(2750.00 - 82.50 - 266.75 = 2400.75) y el ticket impreso cierra exactamente.
8.1 Lo que NO esta cubierto
| Hueco | Por que |
|---|---|
| Beneficio de 0% real | La guarda 1 (!VALIDAR(importe_recdesc)) no se pudo alcanzar en el entorno de prueba. Necesita una autorizacion real con importe_recdesc == 0 |
| Idempotencia (guarda 5) | Cada autorizacion trae un id unico por ticket, y una segunda fila de PAGO QR la rechaza antes ActiveDMdeP(). La unica via restante seria un reintento dentro del lazo de autorizacion |
| Nota de Credito sobre un ticket con beneficio | No se pudo ejercitar. Con la fila de detalle deberia ajustar sola, pero hay que verificarlo antes de habilitarla sobre estos tickets |
| Reversa QR | No se pudo ejercitar en el entorno de prueba |
| Impresora fiscal real | Sin impresora fiscal dataRedondeos queda en cero y el ajuste es un no-op; la rama fiscal de TIC_VDET.SCP quedo deliberadamente cortada. Falta el comando fiscal de descuento por medio de pago |
| Percepcion ComInd e IVA | Solo se ejercito IIBB. Los tipos ttPERCEPCIONCOMIND y ttPERCEPCIONIVA* nunca corrieron |
| Impuestos internos en mas de un bucket de IVA | Limite del catalogo usado: los articulos con impuestos internos son todos 21%. Los caminos fImpInterno2 / fImpInterno3 quedan sin ejercitar |
| Dos pagos QR con beneficio en el mismo ticket | dQRDto_TotalAntes se fija en el primero, asi que el ratio del segundo saldria sobre la base original. Hoy ActiveDMdeP() lo impide |
Camino de limpieza PagoQR_RevertirImputacionMdeP | La politica decidida es bloquear el borrado de una fila autorizada, asi que el escenario nunca llega a ejecutarlo. Verificado solo por lectura de codigo |
9. Bugs encontrados y corregidos durante el desarrollo
| Bug | Causa raiz y correccion |
|---|---|
| Al 100% de beneficio el neteo se apagaba solo | El rango del ratio era el intervalo abierto (0,1) y D == G da r == 1.0 exacto → no-op silencioso, con el resumen en bruto contra un comprobante que declaraba 0.00. Corregido con limite superior cerrado 0 < r <= 1.0 + 1e-9 y clamp a 1.0. |
| La percepcion se recortaba dos veces | El objetivo de neteo se calculaba como bruto - D_completo, pero el bruto ya excluye la percepcion mientras que D se calculo sobre un total con percepcion, y la percepcion ya habia sido reescalada por separado. Resultado: 6.44 menos que lo cobrado. Corregido con objetivo = bruto * (1 - r); es un no-op bit a bit para todo ticket sin percepcion. |
| Orden de llamada: la fila se calculaba sobre la percepcion ya reducida | El append quedo despues del recomputo de percepcion y leia Ticket.Percepcion ya bajada: 275.65 en vez de 275.00, 65 centavos de diferencia contra lo cobrado. Corregido moviendo la llamada antes, con comentario //OJO ORDEN. Sin percepciones el orden es indiferente, por eso ningun otro escenario lo detecto. |
fRecDescMPP restado dos veces | Con las filas de detalle el total ya viene neto; acumular ademas el dRecDesc del QR hacia que el camino de recuperacion del ticket lo restara de nuevo. Corregido excluyendo el PAGO QR en Dump2DDMP. El monto sigue disponible en dE_DDMP.dRecDesc. |
| El descuento se habria aplicado dos veces al imprimir | PagoQR_ProrratearIVAS() prorrateaba dentro de ImprimirIVAS, que a partir de la migracion arranca de un ComputarBinario que ya devuelve el neto. Corregido retirando la funcion y su llamada; no fue limpieza, fue un error de doble aplicacion. |
| Cadenas de desarrollo en el binario de produccion | Un literal de ruta quedaba declarado fuera de todo #ifdef aunque su unico consumidor si estaba condicionado (dato muerto pero presente), y una funcion de produccion arrastraba un prefijo de log heredado. Corregidos ambos; verificado con strings sobre el EXE y las 16 DLLs de produccion. |
Desborde silencioso de POS_MPPO_TEXT | Una funcion nueva puesta en pos_mppo.cpp hizo envolver el segmento de codigo de 64K sin que el linker diera ningun error. Corregido moviendo el cuerpo a modulos con holgura. Riesgo permanente: hay que leer el MAP despues de cada cambio en ese modulo. |