Primer commit

This commit is contained in:
2026-05-30 14:31:19 -06:00
commit a35d26fac0
277 changed files with 265240 additions and 0 deletions
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Backfill de la tabla de mapeo Baserow id_opp_sucursal -> id_opp_marca.
Contexto: el workflow n8n "Sincronizar Oportunidad - Nodos Nuevos"
(Cfgwp0bOtDW8zuKW) decide UPDATE-vs-CREATE buscando la opp existente en Marca
SOLO entre las opps del contacto resuelto ese run. Cuando la identidad del
contacto en Marca es ambigua (mismo telefono / nombre variante), resuelve un
contacto distinto al de la opp original, no la encuentra y hace CREATE ->
replica duplicada (causa del descuadre positivo Marca > sucursales).
El fix: una tabla Baserow que mapea de forma GLOBAL el id de la opp de sucursal
al id de la opp de Marca. El workflow consultara esa tabla antes de decidir; si
existe el mapeo -> UPDATE de esa opp (independiente del contacto). Este script
puebla la tabla con el estado ACTUAL: cada opp de Marca con "ID Oportunidad
Sucursal" valido => fila {id_opp_sucursal, id_opp_marca}.
Idempotente: upsert por id_opp_sucursal (crea si falta, actualiza si cambio el
id_opp_marca). Dry-run por defecto.
Uso:
python scripts/backfill_baserow_opp_mapping.py --table-id <ID> # dry-run
python scripts/backfill_baserow_opp_mapping.py --table-id <ID> --apply
python scripts/backfill_baserow_opp_mapping.py --table-id <ID> --json
"""
import argparse
import datetime
import json
import os
import sqlite3
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)
SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
if SCRIPTS_DIR not in sys.path:
sys.path.insert(0, SCRIPTS_DIR)
from paths import DB_PATH # noqa: E402
from baserow_client import BaserowClient # noqa: E402
from audit_brand_vs_branches_totals import ( # noqa: E402
resolve_opp_link_field_id, extract_opp_link_value, OPP_ID_PATTERN, BRAND_LOCATION_ID,
)
# Nombres de columna esperados en la tabla de mapeo (user_field_names).
COL_SUC = "id_opp_sucursal"
COL_MARCA = "id_opp_marca"
COL_LOC = "location_id_sucursal"
COL_UPDATED = "updated_at"
def safe_print(*a):
text = " ".join(str(x) for x in a)
try:
sys.stdout.write(text + "\n"); sys.stdout.flush()
except UnicodeEncodeError:
enc = sys.stdout.encoding or "utf-8"
sys.stdout.write(text.encode(enc, errors="replace").decode(enc) + "\n"); sys.stdout.flush()
def load_brand_mappings():
"""Lee de SQLite las opps de Marca y devuelve [{id_opp_sucursal, id_opp_marca, location_id_sucursal}]."""
conn = sqlite3.connect(DB_PATH); conn.row_factory = sqlite3.Row
try:
fid = resolve_opp_link_field_id(conn, BRAND_LOCATION_ID)
# location de origen: la opp de sucursal con ese id nativo
branch_loc_by_oppid = {
r["id"]: r["location_id"]
for r in conn.execute(
"SELECT id, location_id FROM opportunities WHERE location_id != ?",
(BRAND_LOCATION_ID,),
)
}
out = []
for r in conn.execute(
"SELECT id, custom_fields_json FROM opportunities WHERE location_id = ?",
(BRAND_LOCATION_ID,),
):
v = extract_opp_link_value(r["custom_fields_json"], fid)
if v and OPP_ID_PATTERN.match(str(v)):
out.append({
COL_SUC: str(v),
COL_MARCA: r["id"],
COL_LOC: branch_loc_by_oppid.get(str(v), ""),
})
return out
finally:
conn.close()
def run(table_id, apply=False, log=safe_print):
cli = BaserowClient.from_credentials()
mappings = load_brand_mappings()
log(f"Opps de Marca con link valido: {len(mappings)}")
existing = cli.list_rows(table_id)
by_suc = {}
dup_keys = 0
for row in existing:
k = str(row.get(COL_SUC) or "").strip()
if not k:
continue
if k in by_suc:
dup_keys += 1
by_suc.setdefault(k, row)
log(f"Filas existentes en la tabla {table_id}: {len(existing)} (claves duplicadas: {dup_keys})")
ts = datetime.datetime.now(datetime.timezone.utc).isoformat()
to_create, to_update, ok = [], [], 0
for m in mappings:
cur = by_suc.get(m[COL_SUC])
if cur is None:
to_create.append(m)
elif str(cur.get(COL_MARCA) or "") != m[COL_MARCA]:
to_update.append({"row_id": cur["id"], **m})
else:
ok += 1
log(f"Plan: crear={len(to_create)} actualizar={len(to_update)} ya_ok={ok}")
summary = {"total_mappings": len(mappings), "existing_rows": len(existing),
"to_create": len(to_create), "to_update": len(to_update),
"already_ok": ok, "created": 0, "updated": 0, "errors": 0,
"duplicate_keys_in_table": dup_keys}
if not apply:
log("DRY-RUN. Para aplicar: --apply")
return {"summary": summary, "to_create": to_create, "to_update": to_update}
# Backup de la tabla antes de mutar.
bpath = cli.backup_table(table_id, label="opp_mapping_prebackfill")
log(f"Backup: {bpath}")
for m in to_create:
try:
cli.create_row(table_id, {**m, COL_UPDATED: ts}, dry_run=False)
summary["created"] += 1
except Exception as e:
summary["errors"] += 1; log(f" ERROR create {m[COL_SUC]}: {e}")
for u in to_update:
rid = u.pop("row_id")
try:
cli.update_row(table_id, rid, {**u, COL_UPDATED: ts}, dry_run=False)
summary["updated"] += 1
except Exception as e:
summary["errors"] += 1; log(f" ERROR update {u[COL_SUC]}: {e}")
log(f"Resultado: created={summary['created']} updated={summary['updated']} errors={summary['errors']}")
return {"summary": summary}
def main():
p = argparse.ArgumentParser(description="Backfill tabla Baserow de mapeo opp sucursal->marca.")
p.add_argument("--table-id", type=int, required=True, help="ID de la tabla de mapeo en Baserow.")
p.add_argument("--apply", action="store_true", help="Aplica (sin esto, dry-run).")
p.add_argument("--json", action="store_true", help="Salida JSON.")
args = p.parse_args()
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
res = run(args.table_id, apply=args.apply, log=(lambda *a: None) if args.json else safe_print)
if args.json:
print(json.dumps(res, ensure_ascii=False, indent=2, default=str))
if __name__ == "__main__":
main()