Files
MP-Manager/scripts/backfill_brand_opp_cf_from_source.py
T
2026-05-30 20:16:12 -06:00

179 lines
7.7 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Backfill de custom fields descriptivos en una opp de MARCA que quedó "solo
enlace" (solo `opportunity.id_oportunidad_sucursal`), tomando los valores de la
opp de sucursal enlazada y, como respaldo, del CONTACTO de sucursal.
Caso de origen: la opp de Marca `8HITkGkOn3gN23Tl8LBr` (Miguel Temixco) quedó sin
Sucursal / TIENDA / Canal de Origen / Vehículo. El nodo de réplica las copia por
`fieldKey`, pero esta opp se creó fuera del flujo normal. Mismo `fieldKey`
canónico (sufijo) en contact y opportunity.
Prioridad por campo: (a) valor en la opp de sucursal → (b) valor en el contacto
de sucursal. Solo rellena campos que estén VACÍOS en la opp de Marca (no
sobreescribe). dry-run por defecto; snapshot + script_audit para rollback.
Uso:
python scripts/backfill_brand_opp_cf_from_source.py \\
--brand-opp-id 8HITkGkOn3gN23Tl8LBr --branch-location-id yjqKxoO02rsdwdJZSPmD
# añade --apply para escribir en Bucéfalo
"""
import argparse
import datetime
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)
import script_audit # noqa: E402
import sync_engine # noqa: E402
from scripts import common # noqa: E402
from paths import MIGRATIONS_DIR # noqa: E402
gc = sync_engine.ghl_client
BRAND_LOCATION_ID = common.BRAND_LOCATION_ID
LINK_KEY = "opportunity.id_oportunidad_sucursal"
# (opp fieldKey canónico, contact fieldKey de respaldo o None).
TARGETS = [
("opportunity.sucursal", "contact.sucursal"),
("opportunity.tienda", "contact.tienda"),
("opportunity.fuente_de_posible_cliente", "contact.fuente_de_posible_cliente"), # CANAL DE ORIGEN
("opportunity.fuente_de_prospecto", "contact.fuente_de_prospecto"),
("opportunity.vehiculo", None), # el contacto guarda marca/versión/año por separado
]
def clean(v):
return "" if v is None else str(v).strip()
def opp_cf_value(cf):
for k in ("fieldValue", "fieldValueString", "value"):
if cf.get(k) is not None:
return cf[k]
return None
def schema_key_by_id(token, location_id, object_key):
"""id -> fieldKey usando el schema dinámico de la location."""
fields = gc.get_object_schema_fields(token, location_id, object_key)
return {f["id"]: f.get("fieldKey") for f in fields if f.get("id")}, \
{f.get("fieldKey"): f for f in fields if f.get("fieldKey")}
def main():
ap = argparse.ArgumentParser(description="Backfill CF descriptivos en opp de Marca desde la opp/contacto de sucursal.")
ap.add_argument("--brand-opp-id", required=True)
ap.add_argument("--branch-location-id", required=True)
ap.add_argument("--brand-location-id", default=BRAND_LOCATION_ID)
ap.add_argument("--apply", action="store_true")
ap.add_argument("--run-id", default=None)
args = ap.parse_args()
accounts = {a["location_id"]: a for a in common.load_accounts()}
brand = accounts.get(args.brand_location_id)
branch = accounts.get(args.branch_location_id)
if not brand or not branch:
raise SystemExit("No se encontró el token de la location de Marca o sucursal en el CSV.")
brand_token, branch_token = brand["token"], branch["token"]
# 1. Opp de Marca (estado actual).
brand_opp = (gc.get_opportunity(brand_token, args.brand_opp_id) or {}).get("opportunity") or {}
if not brand_opp:
raise SystemExit(f"No se pudo leer la opp de Marca {args.brand_opp_id}.")
brand_id2key, brand_key2def = schema_key_by_id(brand_token, args.brand_location_id, "opportunity")
brand_val_by_key = {}
for cf in brand_opp.get("customFields") or []:
k = brand_id2key.get(cf.get("id"))
if k:
brand_val_by_key[k] = opp_cf_value(cf)
# 2. Resolver la opp de sucursal enlazada (del CF link de Marca).
branch_opp_id = clean(brand_val_by_key.get(LINK_KEY))
if not branch_opp_id:
raise SystemExit(f"La opp de Marca no tiene {LINK_KEY}; no se puede resolver el origen.")
branch_opp = (gc.get_opportunity(branch_token, branch_opp_id) or {}).get("opportunity") or {}
if not branch_opp:
raise SystemExit(f"No se pudo leer la opp de sucursal {branch_opp_id}.")
branch_id2key, _ = schema_key_by_id(branch_token, args.branch_location_id, "opportunity")
branch_opp_val_by_key = {}
for cf in branch_opp.get("customFields") or []:
k = branch_id2key.get(cf.get("id"))
if k:
branch_opp_val_by_key[k] = opp_cf_value(cf)
# 3. Contacto de sucursal (respaldo).
branch_contact_val_by_key = {}
cid = branch_opp.get("contactId") or branch_opp.get("contact", {}).get("id")
if cid:
contact = (gc._request("GET", f"/contacts/{cid}", branch_token) or {}).get("contact") or {}
c_id2key, _ = schema_key_by_id(branch_token, args.branch_location_id, "contact")
for cf in contact.get("customFields") or []:
k = c_id2key.get(cf.get("id"))
if k:
branch_contact_val_by_key[k] = cf.get("value")
# 4. Calcular el backfill (solo campos vacíos en Marca).
run_id = args.run_id or f"backfill-opp-cf-{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}"
plan = []
for opp_key, contact_key in TARGETS:
if clean(brand_val_by_key.get(opp_key)) != "":
continue # ya tiene valor, no sobreescribir
value = clean(branch_opp_val_by_key.get(opp_key))
source = "opp_sucursal"
if value == "" and contact_key:
value = clean(branch_contact_val_by_key.get(contact_key))
source = "contacto_sucursal"
if value == "":
continue
bdef = brand_key2def.get(opp_key)
if not bdef:
print(f" WARN: Marca no tiene el campo {opp_key}; se omite.")
continue
plan.append({"opp_key": opp_key, "field_id": bdef["id"], "name": bdef.get("name"),
"value": value, "source": source})
print(f"Opp Marca: {args.brand_opp_id} <- opp sucursal: {branch_opp_id} (contacto {cid})")
if not plan:
print("Nada que rellenar (todos los campos objetivo ya tienen valor o no hay fuente).")
return
print(f"Campos a rellenar ({len(plan)}):")
for p in plan:
print(f" {p['name']:20} [{p['opp_key']}] = {p['value']!r} (fuente: {p['source']})")
if not args.apply:
print("\nDRY-RUN. Vuelve a correr con --apply para escribir en Bucéfalo.")
return
# 5. Snapshot + audit + PUT.
os.makedirs(MIGRATIONS_DIR, exist_ok=True)
snap_path = os.path.join(MIGRATIONS_DIR, f"backfill_opp_cf_{args.brand_opp_id}_{run_id}.json")
with open(snap_path, "w", encoding="utf-8") as fh:
json.dump({"run_id": run_id, "brand_opp_id": args.brand_opp_id, "before": brand_opp, "plan": plan},
fh, ensure_ascii=False, indent=2)
print(f" snapshot -> {snap_path}")
script_audit.create_run(run_id, "backfill_brand_opp_cf_from_source", arguments=" ".join(sys.argv[1:]),
locations=[args.brand_location_id])
change_ids = []
for p in plan:
cidc = script_audit.record_change(run_id, args.brand_location_id, "opportunity", args.brand_opp_id,
p["field_id"], p["name"], None, p["value"])
change_ids.append(cidc)
payload = {"customFields": [{"id": p["field_id"], "key": p["opp_key"], "field_value": p["value"]} for p in plan]}
gc.update_opportunity(brand_token, args.brand_opp_id, payload)
for cidc in change_ids:
script_audit.mark_change(cidc, "applied")
script_audit.update_run_status(run_id, "success")
print(f" PUT aplicado. run_id={run_id} (reversible desde el dashboard).")
if __name__ == "__main__":
main()