Files
MP-Manager/scripts/fix_baserow_verificador.py
2026-05-30 14:31:19 -06:00

158 lines
6.9 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Corrector automático de Baserow (Mesa de control 749 + Verificador 750) para
que el workflow n8n [2004] deje de fallar.
Base / fuente de verdad:
- Nombre de match (749.Nombre y 750."SC BUCEFALO") <- `n8n/cuentas_oficiales.csv`
- SUCURSAL / TIENDA <- Verificador CSV local (load_verifier_map),
solo donde el CSV tenga dato y 750 esté vacío.
Lo que no tiene fuente (solo lo conoce Erandi) NO se inventa: se entrega en
generated/reports/baserow_pendientes_erandi.json.
Acciones (idempotentes; la auditoría solo reporta lo que difiere):
- PATCH 749.Nombre = oficial (arregla el 1er match)
- PATCH 750."SC BUCEFALO" = oficial (arregla el 2do match)
- PATCH 750.SUCURSAL/TIENDA desde el CSV (donde el CSV los tenga)
- POST fila nueva en 750 para oficiales ausentes
Las filas de 750 cuyo location_id NO es oficial NO se tocan.
Uso:
python scripts/fix_baserow_verificador.py # DRY-RUN (no escribe)
python scripts/fix_baserow_verificador.py --apply # aplica (backup previo de 749 y 750)
"""
import argparse
import json
import os
import sys
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if ROOT_DIR not in sys.path:
sys.path.insert(0, ROOT_DIR)
from paths import REPORTS_DIR # noqa: E402
from baserow_client import BaserowClient # noqa: E402
from audit_baserow_verificador import ( # noqa: E402
audit, TABLE_CUENTAS, TABLE_VERIFICADOR,
F_749_NOMBRE, F_SC_BUCEFALO, F_SUCURSAL, F_TIENDA, F_ID_LOC,
)
ERANDI_REPORT = os.path.join(REPORTS_DIR, "baserow_pendientes_erandi.json")
def build_plan(res):
"""Acciones automatizables (updates 749/750, creates) + lista para Erandi."""
updates = [] # {table, row_id, fields, motivo}
creates = [] # {table, fields, motivo}
erandi = {} # location_id -> {nombre, falta:set}
def need(loc, nombre, col):
erandi.setdefault(loc, {"location_id": loc, "nombre": nombre, "falta": set()})["falta"].add(col)
for f in res["fix_749"]:
updates.append({"table": TABLE_CUENTAS, "row_id": f["row_id"], "fields": {F_749_NOMBRE: f["oficial"]},
"motivo": f"749.Nombre {f['actual']!r} -> {f['oficial']!r}"})
for m in res["mismatch_750"]:
updates.append({"table": TABLE_VERIFICADOR, "row_id": m["row_id"], "fields": {F_SC_BUCEFALO: m["oficial"]},
"motivo": f"750.SC_BUCEFALO {m['actual']!r} -> {m['oficial']!r}"})
for s in res["sucursal_vacia"]:
if s["fuente"] == "csv_tiene":
updates.append({"table": TABLE_VERIFICADOR, "row_id": s["row_id"], "fields": {F_SUCURSAL: s["csv_sucursal"]},
"motivo": f"SUCURSAL vacío -> {s['csv_sucursal']!r} (CSV)"})
else:
need(s["location_id"], s["oficial"], "SUCURSAL")
for t in res["tienda_vacia"]:
if t["fuente"] == "csv_tiene":
updates.append({"table": TABLE_VERIFICADOR, "row_id": t["row_id"], "fields": {F_TIENDA: t["csv_tienda"]},
"motivo": f"TIENDA vacío -> {t['csv_tienda']!r} (CSV)"})
else:
need(t["location_id"], t["oficial"], "TIENDA")
for a in res["ausente_750"]:
fields = {F_ID_LOC: a["location_id"], F_SC_BUCEFALO: a["oficial"]}
if a["csv_sucursal"]:
fields[F_SUCURSAL] = a["csv_sucursal"]
if a["csv_tienda"]:
fields[F_TIENDA] = a["csv_tienda"]
creates.append({"table": TABLE_VERIFICADOR, "fields": fields, "motivo": f"crear fila para {a['oficial']!r}"})
for col, val in [("SUCURSAL", a["csv_sucursal"]), ("TIENDA", a["csv_tienda"])]:
if not val:
need(a["location_id"], a["oficial"], col)
erandi_final = [{"location_id": k, "nombre": v["nombre"], "falta": sorted(v["falta"])}
for k, v in erandi.items()]
return updates, creates, erandi_final
def main():
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Corrige Baserow 749/750 desde la lista oficial + CSV. Dry-run por defecto.")
parser.add_argument("--apply", action="store_true", help="Aplica los cambios en Baserow. Sin esto: dry-run.")
args = parser.parse_args()
dry_run = not args.apply
client = BaserowClient.from_credentials()
res = audit(client=client)
updates, creates, erandi = build_plan(res)
u749 = [u for u in updates if u["table"] == TABLE_CUENTAS]
u750 = [u for u in updates if u["table"] == TABLE_VERIFICADOR]
print("=" * 72)
print("CORRECTOR BASEROW (749 Mesa de control + 750 Verificador)")
print("=" * 72)
print(f"Modo: {'DRY-RUN (no escribe)' if dry_run else 'APPLY (escribe en Baserow)'}")
print(f"PATCH 749: {len(u749)} | PATCH 750: {len(u750)} | POST nuevas: {len(creates)} | pendientes Erandi: {len(erandi)}")
print("\n--- PATCH tabla 749 (Mesa de control) ---")
for u in u749:
print(f" row {u['row_id']}: {u['motivo']}")
print("\n--- PATCH tabla 750 (Verificador) ---")
for u in u750:
print(f" row {u['row_id']}: {u['motivo']}")
print("\n--- POST tabla 750 (crear ausentes) ---")
for c in creates:
print(f" {c['motivo']}: {c['fields']}")
print("\n--- PENDIENTES ERANDI (sin fuente; NO se inventan) ---")
for e in erandi:
print(f" {e['nombre']!r} falta: {e['falta']} [{e['location_id']}]")
os.makedirs(REPORTS_DIR, exist_ok=True)
with open(ERANDI_REPORT, "w", encoding="utf-8") as fh:
json.dump(erandi, fh, ensure_ascii=False, indent=2)
print(f"\nLista para Erandi -> {ERANDI_REPORT}")
if dry_run:
print("\nDry-run terminado. Revisa el plan y vuelve a correr con --apply para aplicar.")
return
if not updates and not creates:
print("\nNada que aplicar (Baserow ya está alineado).")
return
b749 = client.backup_table(TABLE_CUENTAS, label="mesa_control_fix")
b750 = client.backup_table(TABLE_VERIFICADOR, label="verificador_fix")
print(f"\nBackups -> {b749}\n {b750}")
ok = err = 0
for u in updates:
try:
client.update_row(u["table"], u["row_id"], u["fields"], dry_run=False)
ok += 1
print(f" OK PATCH t{u['table']} row {u['row_id']}: {u['motivo']}")
except Exception as exc:
err += 1
print(f" ERROR PATCH t{u['table']} row {u['row_id']}: {exc}")
for c in creates:
try:
client.create_row(c["table"], c["fields"], dry_run=False)
ok += 1
print(f" OK POST t{c['table']}: {c['motivo']}")
except Exception as exc:
err += 1
print(f" ERROR POST t{c['table']} {c['motivo']}: {exc}")
print(f"\nAplicado: {ok} acciones, {err} errores.")
if err:
raise SystemExit(1)
if __name__ == "__main__":
main()