02_reconciliacion_cheque.py
Ruta original: docs/proyectos/poliza-explor/scripts/02_reconciliacion_cheque.py
"""
Reconciliacion Cheque vs poliza contable (origen Pc_Tabla='Cheque' en
Poliza_Control), el mas simple de los 15 origenes censados en
01_conteo_polizas_por_origen.py: Ch_Tabla siempre viene vacio (sin
sub-casos de negocio como los de GASTO_REGISTRO) y la relacion es 1
documento : 1 poliza (confirmado: max_docs_por_poliza=1 en el censo).
Hallazgo de calidad de datos: Poliza_Detalle.Pd_Referencia NO sirve para
unir de forma confiable con el folio del cheque (Pc_Documento) -- en 812 de
1,128 polizas activas de enero 2026 (72%), Pd_Referencia trae
Cheque.Ch_Referencia (la referencia externa de pago, ej. folio de factura
del beneficiario) en vez del folio del cheque. Aislar la linea de Abono por
MONTO (Pd_Tipo=2 AND ABS(Pd_Importe - Ch_Importe) <= 1) evita ese problema
por completo: cubre el 100% de las 1,128 polizas activas sin ambiguedad
(ninguna tuvo mas de una linea candidata).
Los Pc_Documento que aparecen en 2 polizas (max_polizas_por_doc=2 del censo)
no son un caso de negocio nuevo: son pares cancelada+activa del mismo
cheque (confirmado con folios reales, ej. 01-0085512: poliza 0000471897 CA
seguida de 0000473559 AC, mismo folio, mismo comentario) -- se resuelven
solos al filtrar Es_Cve_Estado <> 'CA'.
Uso:
python3 02_reconciliacion_cheque.py --fecha-ini 2026-01-01 --fecha-fin 2026-02-01
"""
import argparse
from connection_205_trivasadb3 import engine # default de exploracion -- ver skill trivasa-sql-exploracion
import pandas as pd
from sqlalchemy import text
from helpers_output import console, mostrar_tabla, resumen
# LEFT JOIN a proposito: deja ver polizas de origen Cheque que NO encontraron
# ninguna linea de Abono con monto compatible (para no esconder excepciones).
QUERY = """
SELECT
plc.Pl_Folio, plc.Pc_Documento AS CHEQUE_FOLIO,
ch.Ch_Fecha, ch.Ch_Importe, ch.Ch_Beneficiario, ch.Ch_Referencia,
pd.Cc_Cve_Cuenta_Contable AS CUENTA_BANCO, pd.Pd_Importe AS ABONO
FROM Poliza pl
JOIN Poliza_Control plc ON plc.Pl_Folio = pl.Pl_Folio AND plc.Pc_Tabla = 'Cheque'
JOIN Cheque ch ON ch.Ch_Folio = plc.Pc_Documento
LEFT JOIN Poliza_Detalle pd
ON pd.Pl_Folio = plc.Pl_Folio
AND pd.Pd_Tipo = 2
AND ABS(pd.Pd_Importe - ch.Ch_Importe) <= 1
WHERE pl.Pl_Fecha >= :fecha_ini AND pl.Pl_Fecha < :fecha_fin
AND pl.Es_Cve_Estado <> 'CA'
"""
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--fecha-ini", required=True)
ap.add_argument("--fecha-fin", required=True)
args = ap.parse_args()
df = pd.read_sql(
text(QUERY), engine,
params={"fecha_ini": args.fecha_ini, "fecha_fin": args.fecha_fin},
)
# Duplicados posibles si un folio tuviera >1 linea candidata -- confirmar que no pasa
dup = df.groupby("Pl_Folio").size()
ambiguos = dup[dup > 1]
if len(ambiguos):
console.print(f"[bold red]ADVERTENCIA: {len(ambiguos)} polizas con >1 linea candidata de Abono[/bold red]")
mostrar_tabla(df[df["Pl_Folio"].isin(ambiguos.index)], "Polizas ambiguas", max_filas=20)
df["CUADRA"] = df["ABONO"].notna() & ((df["ABONO"] - df["Ch_Importe"]).abs() <= 1)
out = f"02_reconciliacion_cheque_{args.fecha_ini}_{args.fecha_fin}.csv"
df.to_csv(out, index=False)
n, c = len(df), int(df["CUADRA"].sum())
resumen(
polizas_cheque=n,
cuadran=c,
pct=f"{100 * c / n:.2f}%",
sin_linea_abono_compatible=int((~df["CUADRA"]).sum()),
)
no_cuadra = df[~df["CUADRA"]]
if len(no_cuadra):
console.print(f"\n[bold red]Sin linea de Abono con monto compatible:[/bold red] {len(no_cuadra)}")
mostrar_tabla(no_cuadra, "Excepciones", max_filas=30)
else:
console.print("\n[green]100% de las polizas de origen Cheque cuadran contra su Ch_Importe.[/green]")
console.print(f"\n[green]Guardado: {out}[/green]")
if __name__ == "__main__":
main()