Primer commit
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user