Skip to content

movimientos_uuid_lib.py

Ruta original: docs/proyectos/consumo-interno-fifo/scripts/movimientos_uuid_lib.py

"""
Libreria reusable: enlaza cada SALIDA de movimientos_periodo con el UUID del CFDI de
compra que la respalda, via FIFO "on the fly" (reusa integramente fifo_lib.py, sibling
en este mismo scripts/). Bug de orden corregido aqui, sin tocar fifo_lib.py: fifo_lib
no ordena la cola por su cuenta -- ver el sort explicito en preparar_capas_y_eventos()
mas abajo, antes de invocar fifo_lineas_por_producto().

Usada por:
  - 06_movimientos_uuid_fifo.py (CLI, genera CSV)
  - streamlit_app_uuid_fifo.py (reporte interactivo V2)
"""
import pandas as pd

import fifo_lib as lib  # noqa: E402
from movimientos_lib import reporte_completo  # noqa: E402

TOLERANCIA = 0.5


def preparar_capas_y_eventos(fecha_ini: str, fecha_fin: str, salidas: pd.DataFrame):
    fecha_ini_excl_prev = (pd.Timestamp(fecha_ini) - pd.Timedelta(days=1)).strftime("%Y-%m-%d")
    fecha_fin_excl = (pd.Timestamp(fecha_fin) + pd.Timedelta(days=1)).strftime("%Y-%m-%d")

    claves = sorted(salidas["Pr_Cve_Producto"].unique().tolist())

    semilla = lib.reconstruir_semilla_on_the_fly(claves, fecha_ini_excl_prev)
    nuevas = lib.query_capas(claves, fecha_desde=fecha_ini, fecha_hasta=fecha_fin_excl)

    capas = pd.concat([semilla, nuevas], ignore_index=True)
    # Fix de orden (ver decision doc): fecha_compra ascendente = FIFO real. fifo_lib no
    # ordena por su cuenta -- se ordena aqui, sin tocar la libreria compartida.
    capas = capas.sort_values(["Pr_Cve_Producto", "fecha_compra", "Co_Folio_origen"])
    capas_por_producto = {p: g.copy() for p, g in capas.groupby("Pr_Cve_Producto")}

    eventos = salidas.rename(columns={"Mv_Cantidad_Control_1": "cantidad_neta"})
    eventos = eventos.sort_values(["Pr_Cve_Producto", "Mv_Fecha", "Mv_Folio", "Mv_ID"])
    eventos_por_producto = {p: g.copy() for p, g in eventos.groupby("Pr_Cve_Producto")}

    return capas_por_producto, eventos_por_producto


def correr_fifo(capas_por_producto: dict, eventos_por_producto: dict) -> pd.DataFrame:
    todas_filas = []
    for prod, eventos_prod in eventos_por_producto.items():
        capas_prod = capas_por_producto.get(prod)
        todas_filas.extend(lib.fifo_lineas_por_producto(capas_prod, eventos_prod))
    return pd.DataFrame(todas_filas)


def reporte_completo_uuid_fifo(fecha_ini: str, fecha_fin: str):
    """Todo en uno. Regresa (resultado, salidas, mov, chequeo, descuadres)."""
    mov, resumen_existencia, sin_cuadrar = reporte_completo(fecha_ini, fecha_fin)
    salidas = mov[mov["entrada_salida"] == "Salida"].copy()

    capas_por_producto, eventos_por_producto = preparar_capas_y_eventos(fecha_ini, fecha_fin, salidas)
    asignaciones = correr_fifo(capas_por_producto, eventos_por_producto)

    if asignaciones.empty:
        cols = ["Pr_Cve_Producto", "Mv_Folio", "Mv_ID", "cantidad_asignada",
                "Co_Folio_origen", "fecha_compra", "UUID"]
        return (pd.DataFrame(columns=cols), salidas, mov,
                pd.DataFrame(), pd.DataFrame())

    folios_usados = asignaciones["Co_Folio_origen"].dropna().unique().tolist()
    mapa_uuid = lib.query_uuids(folios_usados)
    asignaciones["UUID"] = asignaciones["Co_Folio_origen"].map(mapa_uuid)

    asignaciones = asignaciones.rename(columns={"Ci_Folio": "Mv_Folio", "Ci_ID": "Mv_ID"})
    salidas_idx = salidas.set_index(["Pr_Cve_Producto", "Mv_Folio", "Mv_ID"])
    asignaciones = asignaciones.set_index(["Pr_Cve_Producto", "Mv_Folio", "Mv_ID"])

    asignaciones["n_asignaciones_del_movimiento"] = asignaciones.groupby(level=[0, 1, 2]).cumcount() + 1
    total_partes = asignaciones.groupby(level=[0, 1, 2]).size()
    asignaciones["total_partes_del_movimiento"] = asignaciones.index.map(total_partes)

    columnas_movimiento = [c for c in salidas_idx.columns if c not in asignaciones.columns]
    resultado = asignaciones.join(salidas_idx[columnas_movimiento], how="left").reset_index()

    resultado["con_capa_de_compra"] = resultado["Co_Folio_origen"].notna()
    resultado["con_uuid"] = resultado["UUID"].notna()

    chequeo = resultado.groupby(["Pr_Cve_Producto", "Mv_Folio", "Mv_ID"], as_index=False).agg(
        cantidad_asignada_total=("cantidad_asignada", "sum"),
        Mv_Cantidad_Control_1=("Mv_Cantidad_Control_1", "first"),
    )
    chequeo["magnitud_original"] = chequeo["Mv_Cantidad_Control_1"].abs()
    chequeo["diferencia"] = chequeo["cantidad_asignada_total"] - chequeo["magnitud_original"]
    descuadres = chequeo[chequeo["diferencia"].abs() > TOLERANCIA]

    return resultado, salidas, mov, chequeo, descuadres