Skip to content

layout-gastos

Consolidación completa 2026-09-10: ~/proyectos/conciliacion-master/ ya no existe — todo su contenido se migró y la carpeta se borró (backup completo en ~/backups/conciliacion-master-final-2026-09-10.zip). Los scripts 01-13 (la reconciliación Cargo/Abono por CECO, secciones "Piezas resueltas" en adelante) y 14-17 (reconstrucción vía Poliza_Configuracion, conciliación XML por origen) viven en layout_gastos_poliza/ dentro del repo github.com/ehalso/conciliacion-cfdi. Su UI (CONT-1/2/4/5) vive en reportes_streamlit/layout_gastos_poliza/ del mismo repo; CONT-6 (promoción de recibidos/nivel_documento/ 04_conciliacion_mpro_vs_xml.py, ex adjuntar-xml) se separó a su propio hub, reportes_streamlit/recibidos_nivel_documento/. Las 3 páginas RPTRV79 (auditoría de compras, dominio distinto) pasaron al repo github.com/ehalso/reportes-mpro. El visor de CSV genérico (viewer-*.html) y los logs de sesión ya curados en este documento no se migraron — quedaron solo en el backup. Todo lo demás de este documento sigue siendo la referencia vigente para el código ya migrado — no se reescribió el historial, solo cambió dónde vive.

Reconstruir vía SQL el layout de gastos que usa el área de contabilidad, y conciliar cada folio de Gasto_Registro contra su póliza contable (Cargo/Abono de Poliza_Detalle) para validar que el importe nativo del gasto cuadra con lo que se contabilizó. Trabajo histórico en ~/trivasa-bi-dev/exploracion/layout_gastos y layout-gastos-ctunlinux (v0.1 → v0.5.1); esta entrada documenta el estado consolidado, no el detalle de cada iteración.

Piezas resueltas

  • v0.1 — reporte nativo: Gasto_Registro + Gasto_Registro_Documento + Comprobante_Digital (UUID). Validado 1:1 contra export real de MPRO (enero 2026: $39,469,269.50 vs $39,469,269.59, diferencia $0.09).
  • v0.5 — Cargo/Abono vs póliza, orígenes "normales" (general/sin Gr_Tabla, VIAJE, ORDEN_COMPRA, CONTROL_COMBUSTIBLE, GASTO_RECLASIFICACION): 100% dentro de $1 de tolerancia (1,425/1,425 folios, enero 2026; 99.90% en 2025 completo). 4 casos especiales — ver Casos especiales de conciliación abajo.

CONSUMO_INTERNO — antes excluido, ahora conciliado (2026-08-18)

La documentación histórica de v0.5 excluía CONSUMO_INTERNO del universo reconciliable asumiendo "sin póliza por diseño" (se registra por otro subsistema). Falso: sí tiene póliza, solo que estructurada distinto a los demás orígenes. Detalle completo del join ambiguo y la corrección en Calidad de datos → joins que parecen obvios pero son falsos.

Resumen: cada folio de CONSUMO_INTERNO genera dos pólizas paralelas bajo el mismo Pc_Documento — una de movimiento de inventario (cuenta 10500.012.003, raíz de grupo H = Cuentas de Orden/memo) y otra de reconocimiento de gasto real (cuenta variable, raíz F = Gastos, o sin grupo con descripción "gastos a cuenta de costo estandar"). Filtrando el Cargo a la póliza de gasto real (dos formas equivalentes, mismo resultado exacto: por cuenta contable, o por Poliza.Pl_Comentario excluyendo "CUENTAS DE ORDEN"/"CTS ORDEN"/"CUENTA ORDEN"):

Folios (enero 2026) 2,970
Concilian (±$1) 2,944 (99.12%)
Sin póliza de gasto real 26 (0.88%) — causa real, no de filtro
Importe total $6,594,522.55
Cargo (gasto real) total $6,388,627.47

El Abono siempre sale en 0 al filtrar por Pd_Referencia = Gr_Folio (como hace este query): la contrapartida sí existe, va a una cuenta de enlace/almacén (1140.010.XXX.007 "Salida Consumo Interno", una por sucursal/almacén), pero esa línea de Abono no lleva Pd_Referencia — está agregada a nivel de almacén para toda la póliza consolidada del día, no por folio individual. Sigue sin ser la partida doble Cargo=Gasto/Abono=CxP de los demás orígenes (aquí es Cargo=Gasto/Abono=Activo de almacén), pero si el Abono llega a hacer falta, hay que traer el Pl_Folio completo sin filtrar por referencia — detalle completo en consumo-interno-fifo y en Calidad de datos.

Validado también a grano más fino: - Grd_ID (línea de documento): mismo 99.12%, coincide 1:1 con folio — los 2,970 folios de CONSUMO_INTERNO de enero 2026 tienen exactamente 1 línea cada uno. - Grc_ID (Gasto_Registro_Control, reparto por centro de costo vía Grc_Factor): 2,942/2,970 folios tienen 1 sola línea Grc, el resto se reparte hasta en 14 centros de costo. La póliza de gasto real también se reparte por Pd_Centro_Costo cuando aplica — emparejando por (folio + centro de costo): 3,044/3,071 líneas concilian (99.1%, consistente con el resultado a nivel folio).

Los 26 folios sin póliza de gasto real: corregido 2026-09-02 — no es un hueco de datos como se documentaba aquí. Drill-down folio a folio (proyecto conciliacion-master/layout-gastos/, código en ~/trivasa/proyectos-bi/conciliacion-master/layout-gastos/, scripts 01-09) encontró que 21 de 23 folios revisados (rango enero-marzo) sí tienen su gasto contabilizado, solo que capitalizado como activo fijo en vez de reconocido como gasto del periodo: Grd_Comentario empieza con un código de proyecto (U-NNN/UNNN), y la póliza real usa Pd_Referencia = 'NNN' (el código corto, no el folio completo) en cuenta 1210.xxx (Activo Fijo) — por eso un query que filtra Pd_Referencia = Gr_Folio nunca la encuentra. Solo quedan 2 folios genuinamente sin ninguna póliza (05-0175286, 05-0176482).

Corrección 2026-09-11: 05-0174748 NO es un caso de "sin póliza" — tiene 4 pólizas reales (configs 0274/0277/0234/0396, no 2 como asume el diseño memo/gasto-real). El filtro de doble póliza excluye correctamente las 2 de memo (10500.012.003, comentario con "orden"), pero una tercera póliza tipo "Provisión" (2120.010.004.005.002, config 0274, comentario "CONSUMO INTERNO LLANTAS DEL..." sin la palabra "orden") se escapa del filtro y duplica el Cargo real ($64.51 × 2 = $129.02 en vez de $64.51). Cuantificado a escala completa: 1 combinación (FOLIO,CECO) de 9,090 en todo enero-marzo 2026 — bug real, extremadamente raro, no vale la pena una regla nueva para cerrarlo. Detalle completo, incluida la prueba de que filtrar por Poliza_Configuracion.Pc_Descripcion en vez de Pl_Comentario tampoco lo resuelve (la config 0274 tampoco menciona "orden" en su descripción), en github.com/ehalso/conciliacion-cfdi, layout_gastos_poliza/PROGRESS.md (entrada 2026-09-11).

GASTO_REGISTRO_NOMINA — investigado, sí liga con póliza real (2026-09-03)

Igual que pasó con CONSUMO_INTERNO, este origen quedaba excluido desde v0.5 asumiendo "se contabiliza por otro mecanismo, ajeno a este reporte" (comentario textual en queries/v03_detalle_cuenta_centro_costo.sql). Falso — se liga por el mismo mecanismo Poliza_Control (Pc_Tabla='GASTO_REGISTRO') → Poliza → Poliza_Detalle (Pd_Referencia=Gr_Folio) que los orígenes normales, sin estructura de doble póliza como CONSUMO_INTERNO.

Diferencia de grano: cada folio de nómina desagrega el Cargo en muchas más cuentas 6xxx por concepto (Sueldos, Bono Por Productividad/Puntualidad/ Asistencia, Fondo de ahorro, Comisiones, Vacaciones, Prima Vacacional, Pagos Por Retiro) de las que distingue Grc_ID/Tg_Cve_Tipo_Gasto — la comparación se hace a nivel (FOLIO, Centro) en vez de por cuenta. Cada póliza también trae líneas de Cargo/Abono sin centro de costo (reclasificaciones de pasivo: ISR retenido, Cuotas IMSS, Sueldos y salarios por pagar, Fondo de ahorro por pagar, Vales de despensa por pagar) que cierran el balance Cargo=Abono de la póliza pero no son gasto por centro — se excluyen filtrando Pd_Centro_Costo <> ''.

Resultado: 100.00% (2,549/2,549 filas FOLIO×CENTRO, 270 folios activos de 294 totales, enero-marzo 2026, empresa 0001) — cero folios sin póliza ligada, mejor resultado de cualquier origen de este proyecto.

Validado también a grano más fino (FOLIO, Centro, Concepto) — no solo que la suma agregada por centro cuadre, sino que el reparto entre tipos de gasto dentro del mismo centro también sea correcto (1,155 de 2,549 combinaciones folio×centro traen 2+ tipos de gasto distintos en el mismo centro). No hay FK real Tipo_Gasto→Cuenta_Contable para nómina (Tg_Cuenta_Contable vacío en los 249 tipos — mismo hueco ya conocido de la exploración general de layout-gastos); se unió por texto normalizado (Tg_Descripcion=Cc_Descripcion exacto). También 100.00% (6,591/6,591 filas, cero conceptos huérfanos). Código: ~/trivasa/proyectos-bi/conciliacion-master/layout-gastos/13_reconciliacion_nomina_ceco.py, detalle en el PROGRESS.md de ese repo (entrada 2026-09-03).

Pendiente: decidir si se incorpora como séptimo origen al universo del reporte final — el 100% no deja obstáculo técnico, falta decidir a nivel negocio si nómina va en el mismo layout que los demás gastos o se reporta aparte (dato sensible, ver dominios.md: dominio NÓMINA marcado "Baja — definir acceso antes"). Actualización 2026-09-11: sí se incorporó (junto con CONSUMO_INTERNO) al layout de 60 columnas — ver sección nueva más abajo.

Nómina: sin pago vía CXP ni CFDI desde 2022 (2026-09-11)

Tres hallazgos nuevos sobre GASTO_REGISTRO_NOMINA, investigados al incorporarlo al layout de 60 columnas:

  • Gr_Genera_Cxp = 'NO' en el 100% de los folios de nómina — bandera nativa del sistema: nómina no pasa por Cuenta_X_Pagar por diseño (se paga por dispersión bancaria directa, proceso separado).
  • Los "proveedores" que trae nómina (Gasto_Registro_Documento. Pv_Cve_Proveedor) no son proveedores reales — son 4 cuentas de pasivo placeholder (SYP - SUELDOS Y SALARIOS POR PAGAR, VACACIONES POR PAGAR, VALES DE DESPENSA POR PAGAR, AGUINALDOS POR PAGAR), las 4 con el RFC genérico XAXX010101000 ("público en general").
  • Comprobante_Digital.Cd_Tabla='NOMINA' (69,766 filas totales) dejó de alimentarse en 2022 (MAX(Cd_Timbre_Fecha) = 2022-12-31) — confirmado buscando sin restringir Cd_Tabla en absoluto contra folios de nómina de 2026: los únicos "hits" son coincidencias falsas de número de folio con documentos de CHEQUE/FACTURA/TRASLADO de otros módulos/años (numeración de folio independiente por módulo). Conclusión: hoy (2026) la nómina se timbra en un sistema externo no integrado con TRIVASADB3.

Detalle completo en github.com/ehalso/conciliacion-cfdi, layout_gastos_poliza/PROGRESS.md (entrada 2026-09-11).

Casos especiales de conciliación Cargo/Abono

  1. CONSUMO_INTERNO/GASTO_REGISTRO_NOMINA: ver arriba — ninguno de los dos se excluye ya; ambos se reconcilian (CONSUMO_INTERNO con el filtro de cuenta/comentario, GASTO_REGISTRO_NOMINA sin filtro especial, solo grano (FOLIO, Centro)).
  2. GASTO_RECLASIFICACION: no tiene póliza real de Cargo/Abono — se deriva del signo de Grd_Precio_Descontado_Importe (positivo=Cargo, negativo=Abono), misma cuenta vía Tipo_Gasto.Tg_Cuenta_Contable.
  3. Abono (Pd_Tipo=2) solo cuenta si la cuenta contable es raíz F (Gastos), o sin grupo asignado y código que empieza en 6, o sin grupo con descripción "gastos a cuenta de costo estandar" — sin este filtro, partidas dobles legítimas contra cuentas de Activo (ej. depreciación) rompen la conciliación.
  4. Reversiones (Importe de folio ≤ -$1): usar solo el Abono, forzar Cargo a 0.

Reconciliación a nivel CECO (centro de costo) — 2026-09-02

Extiende lo de arriba (que reconciliaba Cargo/Abono a nivel folio) a nivel centro de costo: ¿el reparto por CECO que trae Gasto_Registro_Control (Grc_ID, el lado operativo) coincide con lo que quedó posteado en Poliza_Detalle? Es diagnóstico de calidad de dato, no la fuente del layout final (Poliza_Detalle sola ya reconcilia 100% a nivel folio×cuenta×centro, sin pasar por Grc_ID).

Código: ~/trivasa/proyectos-bi/conciliacion-master/layout-gastos/, scripts 01 a 09 (progresión documentada en el PROGRESS.md de ese repo — cada intento y por qué se descartó o mejoró el siguiente). 08_notebook_hito_ceco.py trae la narrativa completa con SQL comentado, pensada para retomar el razonamiento sin releer la sesión que lo generó.

Resultado final (09_reconciliacion_completa_con_consumo_interno.py, enero 2026, 6 orígenes):

Origen %
CONTROL_COMBUSTIBLE, ORDEN_COMPRA, VIAJE, GASTO_DIRECTO, GASTO_RECLASIFICACION 100%
CONSUMO_INTERNO 99.12% (ver corrección de los "26 folios" arriba)
Total (6 orígenes, 10,762 filas) 99.75%

Hallazgo central: la llave real de consolidación que usa la póliza al postear es Tg_Cve_Tipo_Gasto (tipo de gasto) × Centro de Costo — no el documento (Grd_ID). Cuando varios documentos del mismo folio comparten tipo de gasto y centro, la póliza los junta en una sola línea; agrupar por documento (intento intermedio, scripts 04/05) los deja separados y da peor resultado (93-94%) que agrupar por tipo de gasto (06/07, 99%+). Caso real que lo confirmó: folio de TELEFONIA con 7 recibos — por documento da 53 filas, por tipo de gasto da 27, exacto el número de líneas reales de la póliza.

Otros 2 hallazgos nuevos, sin documentar en ningún proyecto anterior: - Reversión (IMPORTE ≤ -$1): el Cargo cae en cuenta de Provisión/Pasivo sin centro de costo (una sola línea); el Abono sí trae el detalle real por centro. Regla: comparar contra Abono, no Cargo — mismo principio que el caso especial 4 de arriba, pero aquí confirmado también a nivel CECO. - GASTO_RECLASIFICACION: el signo de Grc_Importe por línea (no si el folio completo es negativo) decide si esa línea es Cargo (positivo) o Abono (negativo) — el caso especial 2 de arriba solo llegaba a nivel folio.

Residual conocido (enero-marzo, <0.06% del universo): 3 folios donde el conteo de líneas no coincide exacto entre Grc_ID y Poliza_Detalle en un centro puntual — límite de la técnica de emparejamiento (por posición, sin llave real entre las dos tablas), no un patrón sistémico nuevo.

También se confirmó, cruzando contra Comprobante_Digital (XML/CFDI): el problema de folios con "Varios" comprobantes es casi siempre un artefacto de contar por folio en vez de por documento (16 de 17 casos de enero 2026 desaparecen al bajar a nivel documento) — no ambigüedad real de a qué factura corresponde un gasto.

Rediseño sin máscara — grano base establecido (2026-09-03)

La reconciliación de arriba (01-09) decidía Cargo vs. Abono con una regla de reversión por folio ("si IMPORTE del folio ≤ -$1, usar Abono") calculada del lado operativo (Gasto_Registro_Documento). Existía solo porque la query de póliza (v03_detalle_cuenta_centro_ costo.sql, heredada de FlexMonster) colapsa Pd_Tipo — el dato real, 1=Cargo/2=Abono — en dos columnas sumadas antes de que el script pudiera ver cuál era la línea real; sin ese dato, había que adivinar con una señal indirecta.

11_reporte_base_ceco.py/12_reporte_base_ceco_consumo_interno.py resuelven esto agrupando por Pd_Tipo en vez de colapsarlo — TIPO_MOVIMIENTO sale directo del dato, cero regla, cero máscara. Validado no solo en agregado sino posición por posición dentro de (FOLIO, CECO), contra el CSV real del diseño viejo: cero diferencias en las 6,891 combinaciones (FOLIO, CENTRO) de enero y las 15,647 de enero-marzo. Mismo resultado exacto, mecanismo honesto.

Grano base del reporte, establecido aquí en adelante: (FOLIO, CECO, TIPO_GASTO) — 1 fila por combinación, con CUENTA/CUENTA_DESCRIPCION y CARGO/ABONO (pivotado de TIPO_MOVIMIENTO+IMPORTE) de la línea real de póliza que le tocó por rank-pairing. Confirmado que CARGO y ABONO nunca coexisten !=0 en la misma fila — ni siquiera en el residual de GASTO_RECLASIFICACION que mezcla signos en un mismo centro (05-0182622, centro 103): ahí caen en TIPO_GASTO distintos (uno real, otro huérfano sin match), nunca compiten por la misma fila.

Dos bugs reales encontrados al dejar de colapsar (ninguno cambió el % final, sí la honestidad del mecanismo):

  1. Gr_Tabla viene '' (no NULL) para GASTO_DIRECTO — se tapaba con un .fillna() de pandas después del hecho; ahora se normaliza en el SQL (CASE WHEN ISNULL(Gr_Tabla,'')='' THEN 'GASTO_DIRECTO' ...).
  2. 210 líneas de póliza (enero 2026) sin Pd_Centro_Costo real aparecían al no colapsar — la contrapartida normal de doble entrada (Abono a Proveedor Nacional/Provisión de costo estándar/ Préstamos a Terceros, cuentas de balance sin centro). El diseño viejo las descartaba por casualidad; ahora se excluyen a propósito (AND pd.Pd_Centro_Costo <> '', documentado en el SQL).

CONSUMO_INTERNO incorporado con la misma filosofía en 12 — mismo 99.12% ya documentado arriba, pero con una confirmación nueva: TIPO_MOVIMIENTO sale 100% CARGO, 0% ABONO en la póliza de gasto real. 09 nunca miraba Pd_Tipo=2 (su query solo sumaba Pd_Tipo=1), así que "son puros cargos" era un supuesto no verificado — ahora es un hecho leído del dato.

Primer reporte con interfaz: streamlit_app.py + layout_gastos_lib.py (patrón del skill trivasa-streamlit-reportes), Cargo/Abono como columnas separadas, tab de reconciliación con semáforo por origen, descarga a Excel. Validado con AppTest antes de correr el servidor real.

Código: conciliacion-master/layout-gastos/, scripts 11/12 + layout_gastos_lib.py/streamlit_app.py. Detalle completo en el PROGRESS.md de ese repo (entrada 2026-09-03).

Rank-pairing vs. reconstrucción real vía Poliza_Configuracion — exploratorio (2026-09-03)

El rank-pairing de arriba (ordenar por valor dentro de (FOLIO, CECO) y emparejar por posición) existe porque no hay llave real Grc_ID↔Poliza_Detalle visible desde esas dos tablas. Pero sí existe una llave real un nivel más abajo: trivasa-context/docs/ proyectos/poliza-explor/configuracion-polizas.md (sesión 2026-09-03) documentó que Poliza_Configuracion_Detalle.Pcd_Relacion — el JOIN que el motor de MPRO realmente usa para generar cada renglón de póliza — se liga a Gasto_Registro_Control directo por (Gr_Folio, Grd_ID). Se puede reconstruir esa query tal cual y etiquetar cada línea de Grc_ID con el renglón exacto que le corresponde, sin ordenar ni adivinar nada.

Probado contra datos reales para la config 0450 ("REGISTRO DE GASTOS NACIONAL", la que domina GASTO_DIRECTO/CONTROL_COMBUSTIBLE/VIAJE/ ORDEN_COMPRA — 3,403 de ~3,600 folios del rango enero-marzo 2026):

  • La reconstrucción cuadra 100.00% contra Poliza_Detalle real (5,171/5,171 combinaciones FOLIO,CECO) — confirma que es el mismo JOIN que usó el motor, no una aproximación.
  • De las 436 combinaciones (FOLIO,CECO) con más de una línea de Grc_ID (las únicas donde rank-pairing podría emparejar mal):
  • 17 se resuelven en renglones distintos de la configuración — distinción real (el motor las separa por Tipo_Gasto/Proveedor), no coincidencia de orden.
  • 419 caen en el mismo renglón — el motor las suma (SUM(ABS(Grc_Importe)) GROUP BY Centro_Costo, sin agrupar también por Tipo_Gasto). De esas, solo 20 tienen además más de un Tipo_Gasto real — esa es la ambigüedad irreducible: ni reconstruyendo la query exacta del motor se recupera esa distinción, porque el propio motor ya la descartó al generar Poliza_Detalle. No es un límite de la técnica, es un límite del dato fuente.

Neto: 5,151/5,171 (99.61%) de las combinaciones quedan resueltas con certeza real vía la configuración, no una suposición de orden. El límite duro de rank-pairing, medido (no supuesto), es 0.39% — para esta config.

Alcance parcial, no reemplaza 11/12 todavía: solo cubre la config 0450, 1 de ~67 configs distintas que aparecen en el universo de layout-gastos (enero-marzo 2026). Generalizar a las ~66 restantes (y a CONSUMO_INTERNO/GASTO_RECLASIFICACION, que tienen mecanismos propios ya resueltos por otra vía) queda pendiente. Código: 14_reconstruccion_via_poliza_configuracion.py, función reconstruir_config(cve, empresa, fecha_ini, fecha_fin) — reusable para cualquier config con renglones de tipo Cargo+Centro_Costo.

CONT-4/CONT-5 — conciliación 1:1 contra XML (exploratorio, solo local, 2026-09-04)

Extiende la reconstrucción de arriba a una conciliación real contra XML (CFDI). 15_reconciliacion_xml_via_poliza_configuracion.py: grano (FOLIO, GRD_ID) — el documento, no la cuenta contable (Comprobante_ Digital.Cd_Documento decodificado es Gr_Folio + Grd_ID). Comparando Grd_Precio_Neto_Importe (con impuesto, agrupado por XML_UUID — un mismo XML puede repartirse en varios documentos, 1.83% de los casos) contra Cd_Monto: 97.76% (1,922/1,966 XML_UUID) cuadra exacto. Detalle completo (incluida la corrección de comparar contra el importe con impuesto, no contra CARGO neto de póliza) en PROGRESS.md.

Dos reportes Streamlit nuevos, corriendo solo en local (streamlit_venv/, puerto 8506, 127.0.0.1) — no promovidos a streamlit-reportes:

  • CONT-4 — grano documento, XML 1:1.
  • CONT-5 — grano cuenta contable, XML agregado (N_DOCUMENTOS/ N_XML/AMBIGUO explícitos) — construido junto a CONT-4 a propósito para comparar ambos granos con datos reales.

Misma pestaña de Reconciliación que CONT-1/CONT-2 (reusa resumen_por_ folio()/resumen_qa() sin cambios). Cobertura: solo config 0450, igual que arriba — complemento exploratorio, no reemplazo. Código: poliza_configuracion_lib.py, layout_gastos_config_lib.py, reporte_ui_config.py, pages/3_CONT-4_...py/4_CONT-5_...py.

Conciliación XML por origen (16/17, 2026-09-05)

Extiende CONT-4/CONT-5 (arriba, solo config 0450) a un panorama completo sin pasar por Poliza_Configuracion — trabaja directo contra Gasto_Registro/Gasto_Registro_Documento/Comprobante_Digital, para los 4 orígenes que sí deberían llevar XML por lógica de negocio: GASTO_DIRECTO, VIAJE, ORDEN_COMPRA, CONTROL_COMBUSTIBLE (CONSUMO_INTERNO/GASTO_REGISTRO_NOMINA/GASTO_RECLASIFICACION quedan fuera a propósito — 0% de cobertura confirmado por diseño de negocio, no hueco).

Tres bugs reales encontrados y corregidos en el camino (documentados a detalle en Calidad de datos → Comprobante_Digital, resumen aquí):

  1. Comprobante_Digital.Cd_Documento (para Cd_Tabla='GASTO_REGISTRO') tiene dos formatos de longitud (14 y 18 caracteres) — filtrar solo LEN=18 perdía ~23% de los XML ligados. Confirmado con drill-down real de ORDEN_COMPRA: 17 folios "sin XML" en realidad sí tenían, en el formato de 14.
  2. Aceptar ambos formatos puede duplicar el mismo XML en el cálculo (mismo comprobante capturado dos veces, uno por formato) — corregido deduplicando por (FOLIO, GRD_ID, XML_UUID).
  3. Comprobante_Digital.Cd_Monto viene en la moneda original del CFDI, nunca convertido a MXN — comparar el importe ya convertido daba diferencias de hasta 18x en documentos USD/EUR. Corregido comparando sin aplicar Grd_Tipo_Cambio.

Con las 3 correcciones: VIAJE, ORDEN_COMPRA y CONTROL_COMBUSTIBLE llegan a 100%/100% (cobertura y cuadre de monto, enero 2026). GASTO_DIRECTO queda con el hueco real del proyecto: 85.94% de documentos con XML (35.04% del importe), 89.30% de eso cuadra en monto.

Cuarto hallazgo, investigando el residual de GASTO_DIRECTO: 31 de 58 XML_UUID sin cuadrar son CFDI tipo RETENCIONES, cuyo monto real vive en una fila hermana bajo Cd_Tabla='CONSTANCIA_RETENCION' (ver detalle en Calidad de datos) — 16 de esos 31 cuadran exacto aplicando el factor del 90% (ISR de honorarios). Aplicado en 17: 27.56% → 31.40% de importe que cuadra en GASTO_DIRECTO (+3.67pp).

17_reconciliacion_xml_grano_grupo.py también prueba el grano de componente conexa (union-find) — idea tomada de un proyecto paralelo de otra sesión (~/proyectos/conciliacion-master/adjuntar-xml/, ver Calidad de datos) que resuelve el mismo problema con metodología más madura (parseo de Cd_XML, Cuenta_X_Pagar como cadena alterna). Efecto medido en GASTO_DIRECTO: solo +0.17pp — mucho menor de lo esperado, porque el residual de este proyecto no era mayormente por facturas repartidas en varios documentos (a diferencia de adjuntar-xml, donde el mismo cambio resolvía ~9x más descuadres). Reimplementado en versión ligera (sin Cd_XML, el texto completo del CFDI) porque el entorno de exploración solo tiene ~5GB de RAM — confirmado que la versión con Cd_XML completo agota memoria y el proceso muere sin traceback.

Efecto total por rango de fecha (probado en 3 rangos para elegir con evidencia — el % no varía mucho entre rangos, ningún periodo es claramente "mejor"):

Rango GASTO_DIRECTO CONTROL_COMBUSTIBLE TOTAL (4 orígenes)
Enero 2026 31.40% 100.00% 40.28%
Enero-Marzo 2026 33.13% 100.00% 41.15%
Enero-Mayo 2026 32.03% 73.49% 39.94%

CONTROL_COMBUSTIBLE cae a 73.49% en enero-mayo (era 100% en los rangos más cortos) — hallazgo nuevo, sin investigar en profundidad, pero relacionado con dos propiedades ya confirmadas de este origen (ver Calidad de datos → CONTROL_COMBUSTIBLE): la factura consolidada del proveedor puede repartirse entre folios de distintas Poliza_Configuracion (caso real: un solo CFDI de $10,486.62 cubre 14 folios repartidos entre las configs 0450 y 0427), y el CFDI casi nunca timbra en el mismo mes que el folio operativo que cubre — por eso la conciliación de este origen debe expandirse por folio real ligado al UUID (sin restringir a una sola config ni al periodo de fecha), no por fecha de timbrado del comprobante. El residual de enero-mayo probablemente es la misma causa a mayor escala (más facturas consolidadas que cruzan fuera del universo expandido con max_iter=3), sin confirmar todavía.

Ideas evaluadas por razonamiento, NO probadas empíricamente (ver Calidad de datos para el detalle de cada una): parsear Cd_XML para complementos de Pago/Vales de Despensa; Cuenta_X_Pagar como cadena alterna para CONTROL_COMBUSTIBLE (validada por otra sesión en layout-contabilidad/layout_gastos/); marcar CFDI intercompañía.

Código: conciliacion-master/layout-gastos/ (16_reconciliacion_xml_por_ origen.py, 17_reconciliacion_xml_grano_grupo.py). Detalle completo, incluida la corrección de que estas ideas se evaluaron por razonamiento y no por prueba, en PROGRESS.md de ese repo (entradas 2026-09-05).

HITO: layout de 60 columnas del Word, construido (2026-09-11)

Este proyecto nació para reconstruir "el layout de gastos que usa el área de contabilidad" (ver intro de este documento) — hasta esta fecha, todo el trabajo de arriba (v0.1-17, CONT-1/2/4/5) resolvía la reconciliación Cargo/Abono por CECO, no el entregable original. Esta sesión (github.com/ehalso/conciliacion-cfdi, layout_gastos_poliza/, scripts 18-25) retomó el requerimiento real: el Word transcrito completo en layout_gastos_60col/legado/layout-gastos-streamlit-claude/ docs/00_requerimiento.md (rescatado de ~/backups/, nunca antes leído por ninguna sesión de este repo) — auditoría externa trimestral, Bates y Asociados, 60 columnas en 6 bloques (pago, proveedor, factura/CFDI, impuestos, XML, póliza), criterio de aceptación: suma de Cargo/Abono == reporte nativo de gastos.

Hallazgo metodológico central: la reconstrucción vía Poliza_Configuracion (arriba, sección "Rank-pairing vs. reconstrucción real") resuelve un problema que este entregable no necesita — atribuir cada línea de póliza a su Tipo_Gasto de origen, algo que sí necesita CONT-1/2 pero que el Word nunca pide. El join directo (Poliza_Detalle.Pd_Referencia = Gasto_Registro.Gr_Folio, lectura de Pd_Tipo sin inferir nada) da Cargo/Abono exactos, sin rank-pairing, sin regla de reversión, para los 5 orígenes normales y para GASTO_RECLASIFICACION completo (que reconstruir_config() no podía cubrir — solo traía el lado Cargo, nunca el Abono del par espejo).

Alcance final: 7 orígenes (los 5 del Word + GASTO_REGISTRO_NOMINA + CONSUMO_INTERNO, ampliado después de cerrar el alcance original). Validado contra el reporte nativo de MPro (no algo derivado): 7 orígenes, enero 2026, 4,494 folios — SUBTOTAL_NETO/IMPUESTO/TOTAL coinciden exacto a $0.00; Cargo−Abono $39,469,326.09 vs. baseline $39,469,269.50 (diferencia $56.59, 0.00014%, redondeo a centavos entre Poliza_Detalle y los campos nativos, no un problema de datos).

Impuestos atribuidos correctamente por centro de costo (no solo por folio) usando Gasto_Registro_Control.Grc_Factor — campo nativo de peso (confirmado que suma 1.0 exacto por documento, de 1 hasta 79 centros), evita inflar la suma cuando un documento se reparte en varios centros (19.3% de los casos).

Pendiente sin cerrar en esta sesión: MONTO_COBRADO infla ~41.6% cuando un Cxp_Folio se comparte entre varios folios (factura consolidada, no corregido); FACTURA_REF/Descuento sin fuente (placeholder); vista agrupada por cuenta contable (segunda vista del Word, solo se construyó la de detalle). Detalle completo en layout_gastos_poliza/PROGRESS.md de ese repo (entrada 2026-09-11) y layout_gastos_60col/README.md.

Pendiente

  • Investigar por qué CONTROL_COMBUSTIBLE cae a 73.49% en enero-mayo (nuevo, sin investigar) — candidato: el mecanismo de Cuenta_X_Pagar de layout-contabilidad/layout_gastos.
  • Los 27 XML_UUID de GASTO_DIRECTO tipo CFDI Ingreso normal sin patrón identificado, y los 15 CFDI de RETENCIONES sin constancia ligada (de los 31 encontrados) — ver sección arriba.
  • Probar contra esta base las ideas de adjuntar-xml/layout- contabilidad que solo se evaluaron por razonamiento (parseo de Cd_XML, Cuenta_X_Pagar, CFDI intercompañía) — pendiente de una máquina con más memoria para el parseo de Cd_XML.
  • ~~Decidir si CONT-4/CONT-5... investigar el residual de cuadre XML (~2.2%, parece concentrado en documentos en USD)~~ — resuelto 2026-09-05: confirmado que sí era doble aplicación de tipo de cambio en documentos USD, no un residual sin explicar. Ver sección arriba.
  • ~~Generalizar la reconstrucción vía Poliza_Configuracion de la config 0450 a las ~66 restantes~~ — hecho 2026-09-11 (las 44 configs reales usadas por los 5 orígenes del Word, no exactamente 66): 99.74%/99.39% de cobertura (folios/Cargo $), 100% cruzado contra rank-pairing donde ambas técnicas tienen dato. Pero ver el hallazgo más grande de esa misma sesión: el layout de 60 columnas no necesita esta técnica en absoluto — el join directo (Pd_Referencia=Gr_Folio, sin rank-pairing) ya resuelve Cargo/Abono sin ambigüedad para los 5 orígenes, porque el Word nunca pide Tipo_Gasto (lo único que reconstruir_config() aporta sobre el join directo). Ver sección nueva más abajo. reconstruir_config() generalizada queda como cross-check, no como fuente del reporte final.
  • Investigar los 2 folios de CONSUMO_INTERNO genuinamente sin póliza (05-0175286, 05-0176482) — sigue abierto. 05-0174748 ya no aplica aquí — ver corrección arriba, resultó ser un caso de póliza duplicada, no de póliza faltante.
  • ~~Construir la rama de capitalización de activo fijo para CONSUMO_INTERNO~~ — resuelto 2026-09-08 en 12_reporte_base_ceco_ consumo_interno.py, pero nunca portado a producción (ni a layout_gastos_lib.py/CONT-1/2). Reincorporado 2026-09-11 al construir el layout de 60 columnas (24_bloque_poliza_directo.py, github.com/ehalso/conciliacion-cfdi) — 45/47 folios recuperados, mismo resultado que en agosto (2 residuales sin resolver, folios que reparten en más de 1 centro de costo).
  • Nuevo 2026-09-11: se probó la alternativa de filtrar la doble póliza de CONSUMO_INTERNO por raíz de cuenta contable (6xxx) en vez de por Pl_Comentario/Pc_Descripcion — descartada, excluye 1,319 de 2,970 folios legítimos de enero 2026 (muchas cuentas de gasto real de este origen NO son raíz 6xxx). El filtro de comentario, pese a su bug conocido (05-0174748 arriba), sigue siendo la mejor opción disponible.
  • ~~GASTO_REGISTRO_NOMINA: nunca se investigó...~~ — resuelto 2026-09-03, ver sección arriba. Falta la decisión de negocio sobre incorporarlo al reporte final.
  • ~~Mejorar el filtro de doble póliza de CONSUMO_INTERNO...~~ — resuelto 2026-09-03: filtro movido de Pl_Comentario (texto libre) a Poliza_ Configuracion.Pc_Descripcion (la fuente real de la regla), ver PROGRESS.md. Validado 0 discrepancias contra el filtro viejo en enero-marzo 2026.
  • ~~Investigar COMPROBACION_GASTO...~~ — causa raíz confirmada 2026-09-03 vía poliza-explor/configuracion-polizas.md: config 0394 no cubre el CeCo 000501 PLANTA CONCRETERA en ninguna condición de cargo — hueco estructural en la regla contable, no ruido de nuestra técnica de match. Ver PROGRESS.md.
  • Decidir qué hacer con los ~9 folios residuales del rank-pairing (<0.06%, ver "Rediseño sin máscara" arriba): hoy el reporte los deja visibles como filas TIPO_GASTO/CUENTA = (residual) en vez de ocultarlos — falta decidir si se excluyen del reporte final o se quedan marcados así.
  • ~~Construir el reporte de producción~~ — resuelto 2026-09-03: promovido a streamlit-reportes (repo Docker, reportes.frento.com.mx/contabilidad), como CONT-1 (sin nómina) y CONT-2 (con GASTO_REGISTRO_NOMINA). Detalle en streamlit-reportes/reportes/layout-gastos-ceco-cont-1.md y layout-gastos-ceco-cont-2.md. El reporte Streamlit local de exploración (streamlit_app.py en conciliacion-master/layout-gastos/) se queda como estaba, para seguir iterando antes de re-promover.