#!/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()