ADR-0008 · La retención se lee con su propio contrato, no con el de transferencia¶
- Estado: aceptado
- Decisores: mvaliente
- Fecha de la decisión: 2026-08-20
- Relacionado: ADR-18 del paquete
Confirmacion Automatica(lectura síncrona que autocompleta la carga), ADR-0007; paqueteRetenciones(docs 01–04)
Contexto y problema¶
Al cargar una solicitud de retención el vendedor adjunta el comprobante, y desde ADR-18 esa acción dispara la lectura de la imagen. El pedido fue extender esa ayuda a las retenciones: "obtener lo mismo que obtenemos en transferencia".
Lo que se encontró al estudiarlo:
- La retención ya pasaba por el lector. El manejador del adjunto no distingue el método de pago, así que la imagen de retención se mandaba al endpoint de transferencias.
- Y el lector la rechazaba por diseño. Probado contra una muestra real: el
prompt de transferencia instruye explícitamente devolver
legible: falsecuando la imagen no es un comprobante de transferencia. El vendedor veía "no se pudieron leer los datos del comprobante" con una imagen perfecta, y la lectura fallida viajaba igual al log, contaminando la medición de RF-08 con "ilegibles" que no lo eran. - "Lo mismo que en transferencia" no aplica. De los ocho campos del contrato de
transferencia, cinco no existen en una retención: no hay ordenante, cuenta de
origen, banco, referencia SIPAP ni hora, porque no hubo transferencia — el
cliente no mandó dinero, retuvo el IVA para depositarlo al fisco. Además el
volcado de esos datos al borrador vive dentro de la rama
PaymentType.TRANSFER, y una retención cae enCARD: aunque se leyeran, se descartarían. ConTransferSum = 0no hay movimiento de extracto y el matcher no interviene.
Decisión¶
Un contrato, un prompt y un endpoint propios para el comprobante de retención, hermanos de los de transferencia y separados de ellos.
retention-contract.tsyretention-prompt.tsnuevos.POST /receipt-extraction/leer-retencion, aparte dePOST /leer.- Los normalizadores que no son de ningún documento en particular (números, fechas,
moneda, textos ausentes) se extraen a
normalizadores.tsy los usan los dos. - El servicio ejecuta ambos con el mismo camino: cada documento es una constante
DocumentoLeiblecon su prompt, su esquema y su normalizador. Agregar un tercero no agrega una rama.
Los campos que sí corresponden son otros, y son más útiles: número del comprobante
de retención (va a VoucherNum, hoy obligatorio y tipeado a mano), número de la
factura retenida, RUC del agente y del retenido, y los montos discriminados.
Por qué separado y no una bandera en el camino existente¶
El prompt de transferencia está calibrado contra los 12 casos reales del piloto y hoy alimenta la auto-confirmación en modo activo, donde cada lectura contribuye a un asiento contable. Meterle un segundo tipo de documento arriesga una regresión en un camino que mueve dinero, para ahorrar unas líneas. La pantalla ya sabe qué método eligió el vendedor, así que elegir la ruta correcta no le cuesta nada.
Alcance: los dos impuestos, y quién elige cuál se carga¶
Revisión del 2026-08-21. Hasta esta fecha el alcance era solo retención de IVA, apoyado en la definición de contabilidad del 2026-08-20 de que se emite un comprobante por impuesto. Las muestras del usuario mostraron lo contrario: un mismo comprobante puede retener IVA y renta, porque el formulario impreso es único para todos los rubros. La guarda que frenaba el caso mixto habría pasado de excepción a bloqueo diario.
La retención de IVA es la tarjeta SAP 3 (cuenta 113203) y la de renta la 4
(ANTICIPO IRE, cuenta 113210). Las dos están dentro del alcance. A qué cuenta va
cada una lo resuelve SAP por la configuración de la tarjeta: el OMS solo tiene que
mandar el código elegido con el monto de ese impuesto.
Decisión: la lectura transcribe los dos montos y no elige ninguno. Elige el usuario, con la tarjeta. Es la única parte del comprobante que no está impresa —el papel dice cuánto se retuvo por cada impuesto, no cuál de las dos imputaciones se está registrando—, y es también la que no se puede equivocar en silencio: cargar el total general contra una sola tarjeta infla una cuenta y vacía la otra sin que nada avise, porque los totales de la factura cierran igual.
De ahí las tres reglas que sostienen el diseño:
- La lectura NO escribe el monto (corregido el 2026-08-21; ver abajo). El monto lo fija el vendedor al elegir la factura y el importe aplicado; lo retenido según el papel se muestra debajo del campo para contrastarlo, y si no coinciden se frena el guardado. Es el mismo contrato que en transferencia.
- Lo contrastado sale del componente, nunca de
totalRetenido.montoRetenidoSegunTarjetaes la única traducción entre el papel y el formulario, y se deriva de la tarjeta elegida: cambiar de RETENCIONES IVA a ANTICIPO IRE compara contra el otro impuesto sin reescribir nada. - Un comprobante con los dos impuestos son dos solicitudes, cada una con su tarjeta y su monto, ambas con el mismo Nº de certificado. La factura queda abierta por el saldo del otro impuesto hasta que se cargue la segunda.
Corrección del 2026-08-21 (misma jornada). La primera versión de este cambio escribía el monto en el campo, heredado de la decisión del 2026-08-20 de que en retención el importe «está impreso en el papel». Era incorrecto por dos motivos. El de fondo: el monto de un cobro es lo que se aplica a una factura, y eso lo decide quien carga, no la imagen. El técnico, que lo hacía además inconsistente:
handleInvoiceSelectionChangepisapaymentAmountcon la suma aplicada apenas se toca una factura, yamountMatchesInvoicesexige que coincidan — o sea que el valor escrito por la lectura duraba hasta la primera selección y competía con una validación del propio formulario.
Lo que evaluarAlcance sigue frenando es distinto: que los números del papel no
cierren entre sí (totalRetenido ≠ IVA + renta, o un comprobante que no retiene
nada). Ahí no hay decisión que tomar, hay algo mal leído o un concepto que no
estamos viendo. Un componente en null cuenta como cero, así que la misma suma
atrapa el caso viejo: el papel retiene renta y el modelo no la vio.
Qué hace la marca, exactamente: no autocompleta ningún campo y muestra un aviso. No bloquea el guardado. Es deliberado y sigue el principio que ya rige el módulo — una lectura no puede dejar a nadie sin poder registrar una operación real—. Si el modelo se equivocó, el vendedor carga a mano como venía haciendo. Bloquear habría repetido la trampa del botón muerto que ya costó una corrección en este mismo flujo.
Detección cuando el método todavía no se eligió¶
El ruteo por método tiene un agujero que apareció en la primera prueba: el formulario muestra el adjunto tanto en transferencia como en retención, así que un vendedor parado en «Transferencia» puede adjuntar un certificado de retención. La imagen va al lector equivocado y recibe «no se pudieron leer los datos del comprobante» sobre un papel perfectamente legible, sin ninguna pista de que el problema es el método y no la foto.
Decisión: cuando el lector de transferencia devuelve ilegible, se reintenta con
el de retención. Si la segunda lectura trae número de certificado y monto retenido
—los dos campos que ningún otro documento tiene— el formulario se configura solo
(método «Tarjeta», sin cuenta bancaria, sin anticipo) y se avisa que se hizo. La
tarjeta se elige sola solo si el comprobante retiene un único impuesto; si
retiene los dos, el desplegable queda vacío y se pide la decisión, por lo mismo de
la sección anterior. En un cobro directo no se cambia nada, porque ahí la retención no
existe: se explica que va como solicitud.
Se reintenta solo con el motivo ilegible: es el veredicto de que el modelo leyó
la imagen y concluyó que no es una transferencia, o sea que la clasificación ya
está hecha y ya se pagó. lectura_fallida (red, 429) y formato_no_soportado no
dicen nada sobre qué documento es.
Por qué no un clasificador previo. Porque el costo de estas llamadas es la imagen, no el texto: la lectura de la muestra gastó 1.036 tokens de entrada y 295 de salida. Un paso que solo clasifique tiene que mandar la misma imagen, así que cuesta casi lo mismo que leer — duplicaría el costo y la latencia en el 100% de los casos para ahorrar una segunda llamada en la excepción. El reintento cuesta cero en el camino normal y una llamada extra únicamente cuando la primera ya falló.
Por qué no un prompt unificado que devuelva el tipo y los campos de ambos
documentos en una sola llamada: sería lo más limpio, pero no se puede medir si
empeora. Verificar que un lector que hace dos cosas no lee peor que uno que hace
una exige volver a correr los 12 casos del piloto, y esas imágenes no están en el
repositorio (el doc 02 las referencia en transfer-images/, que no existe). Sería
cambiar a ciegas un camino que alimenta asientos contables. Si esas imágenes
aparecen, esta decisión se puede revisar.
Lo que esto no arregla. La causa de fondo es que la retención está escondida dentro del método «Tarjeta» y nadie adivina que hay que buscarla ahí. La detección compensa esa poca visibilidad; no la elimina. Hacerla visible en el selector —una opción «Retención» que por debajo setee el método y deje elegir el impuesto— queda como mejora aparte, y seguiría conviviendo con esta detección: alguien va a adjuntar el papel equivocado igual.
Consecuencias¶
A favor
- El vendedor deja de ver un aviso falso, y la medición de RF-08 deja de contar retenciones como comprobantes ilegibles.
- El número del certificado —trece dígitos que hoy se tipean a mano y son obligatorios— se completa solo.
- El camino de transferencia queda intacto: ni un carácter de su prompt, su esquema ni su normalización cambió. Los 25 tests que lo cubrían siguen verdes.
- Adjuntar el certificado sin haber elegido el método deja de ser un callejón sin salida: el formulario se configura solo y lo dice.
- El PDF pasa a leerse, en los dos documentos (2026-08-20). Se descartaba por un supuesto que resultó falso —que el comprobante siempre sería una foto—: los bancos dejan descargarlo en PDF y el certificado de retención virtual es un PDF. Verificado con el código de producción: mismos 17 campos y los mismos 1.036 tokens de entrada que la imagen. El front dejaba pasar el archivo y no leía en silencio; ahora avisa cuando el formato no se puede leer.
En contra / pendientes
- Calibrado con una sola muestra, y esa muestra no tiene renta. El prompt
funciona 17 de 17 campos contra
docs/retencion muestra.jpg, donde la retención de renta es 0. Nunca se probó que el modelo separe bien las dos columnas en un comprobante mixto real, que es justamente el caso que esta revisión habilita. Es el pendiente más importante: recalibrar con 5–10 comprobantes de clientes distintos, con al menos uno mixto. - Un mismo certificado va a figurar en dos cobros cuando retiene los dos
impuestos. SAP no exige unicidad de
VoucherNum, así que no rompe nada, pero tampoco hay hoy ninguna detección de "este certificado ya se cargó con esta misma tarjeta": el duplicado real —cargar dos veces el mismo impuesto— no se atrapa. - La factura no se selecciona sola todavía. El comprobante trae el número legal
timbrado (
007-003-0005289) y la lista de facturas abiertas del front solo lleva elDocNuminterno de SAP. El número legal existe comoFolioPrefixString+FolioNumberpero no viaja al front; sumarlo toca un$selectcompartido y va como cambio aparte. - Las verificaciones de identidad quedan para después: comparar el RUC del agente contra el del socio de negocio y el del retenido contra el nuestro. La lectura ya entrega los dos datos; falta decidir de dónde sale el RUC propio.
- Nada se escribe en los UDF de retención (
U_NRO_RETE,U_CENT_*): son dos familias muertas de dos épocas y llenar la equivocada es peor que no llenar ninguna. El número del certificado viaja porVoucherNum, que es donde importa.
Alternativas consideradas¶
- Extender el prompt de transferencia para que detecte el tipo. Descartada: pone en riesgo un camino calibrado y en producción para ahorrar una ruta.
- Un solo endpoint con un parámetro
tipo. Menos código, pero cualquier error en el parámetro degrada silenciosamente a leer el documento equivocado. Con dos rutas, un error se ve como un 404. - No leer y dejar la carga manual. Es el estado previo; deja en pie el aviso falso y no aprovecha que el dato más costoso de tipear está impreso.