Skip to content

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()