158 lines
6.9 KiB
Python
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()
|