Recalculate PPL weighted by centro invalidation rate and sync CapTotalMes from CursoMes
- PPL = sum(PPL_centrocurso × % invalidación_centro) for ABIERTO records (% invalidación stored as decimal 0-1, not percentage) - CapTotalMes synced from CursoMes.Caping Admitido for current month/year (0 if no record found in CursoMes) - Table reference fixed: CENTROCURSO → CentroCurso - Added CursoMes table client in AirtableClient.__init__ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2191a498d7
commit
b0f0ad394b
@ -9,39 +9,80 @@ class AirtableClient:
|
||||
self.leads = self.api.table(config.AIRTABLE_BASE_ID, config.LEADS_TABLE)
|
||||
self.campaigns = self.api.table(config.AIRTABLE_BASE_ID, config.CAMPAIGNS_TABLE)
|
||||
self.cursos = self.api.table(config.AIRTABLE_BASE_ID, "Cursos")
|
||||
self.centrocurso = self.api.table(config.AIRTABLE_BASE_ID, "CENTROCURSO")
|
||||
self.centrocurso = self.api.table(config.AIRTABLE_BASE_ID, "CentroCurso")
|
||||
self.cursomes = self.api.table(config.AIRTABLE_BASE_ID, "CursoMes")
|
||||
|
||||
def build_ppl_lookup(self) -> dict:
|
||||
"""
|
||||
Construye un dict {cursoid_text: ppl_sum} calculando el PPL de cada curso
|
||||
sumando los PPL de los registros CENTROCURSO con Estado ROI = 'ABIERTO'.
|
||||
MESES_ES = {
|
||||
1: "Enero", 2: "Febrero", 3: "Marzo", 4: "Abril",
|
||||
5: "Mayo", 6: "Junio", 7: "Julio", 8: "Agosto",
|
||||
9: "Septiembre", 10: "Octubre", 11: "Noviembre", 12: "Diciembre",
|
||||
}
|
||||
|
||||
Cadena: CursoID Text → Cursos.CursoID → Cursos.CentroCurso → CENTROCURSO.PPL
|
||||
Solo se usan en lotes (2 llamadas bulk), sin llamadas por campaña.
|
||||
def build_campaign_lookups(self) -> tuple[dict, dict]:
|
||||
"""
|
||||
# 1. Cargar CENTROCURSO ABIERTO: {record_id: ppl}
|
||||
Construye en 3 llamadas bulk:
|
||||
ppl_lookup: {cursoid_text: ppl_ponderado}
|
||||
PPL = Σ (PPL_centrocurso × % invalidación_centro / 100)
|
||||
solo registros CentroCurso con Estado ROI = 'ABIERTO'
|
||||
cap_lookup: {cursoid_text: caping_admitido}
|
||||
desde CursoMes para el mes/año en curso; 0 si no existe
|
||||
"""
|
||||
now = datetime.now()
|
||||
mes_nombre = self.MESES_ES[now.month]
|
||||
anio_str = str(now.year)
|
||||
|
||||
# 1. CentroCurso ABIERTO: {record_id: {ppl, pct_invalidacion}}
|
||||
cc_records = self.centrocurso.all(
|
||||
formula="{Estado ROI}='ABIERTO'",
|
||||
fields=["PPL"],
|
||||
fields=["PPL", "% invalidación (from Centros)"],
|
||||
)
|
||||
cc_ppl = {r["id"]: float(r["fields"].get("PPL") or 0) for r in cc_records}
|
||||
cc_data = {}
|
||||
for r in cc_records:
|
||||
ppl_val = float(r["fields"].get("PPL") or 0)
|
||||
pct_raw = r["fields"].get("% invalidación (from Centros)", 0)
|
||||
if isinstance(pct_raw, list):
|
||||
pct = float(pct_raw[0]) if pct_raw else 0.0
|
||||
else:
|
||||
pct = float(pct_raw or 0)
|
||||
cc_data[r["id"]] = {"ppl": ppl_val, "pct": pct}
|
||||
|
||||
# 2. Cargar Cursos: {cursoid_number: [cc_record_ids]}
|
||||
# 2. Cursos: {record_id: cursoid_text} + {cursoid_text: [cc_ids]}
|
||||
cursos_records = self.cursos.all(fields=["CursoID", "CentroCurso"])
|
||||
curso_by_recordid = {}
|
||||
curso_to_cc = {}
|
||||
for r in cursos_records:
|
||||
cid = r["fields"].get("CursoID")
|
||||
cc_ids = r["fields"].get("CentroCurso", [])
|
||||
if cid is not None:
|
||||
curso_to_cc[str(int(cid))] = cc_ids
|
||||
cursoid_text = str(int(cid))
|
||||
curso_by_recordid[r["id"]] = cursoid_text
|
||||
curso_to_cc[cursoid_text] = cc_ids
|
||||
|
||||
# 3. Para cada CursoID Text, sumar PPL de CENTROCURSO ABIERTO
|
||||
# 3. PPL ponderado por % invalidación del centro
|
||||
# El campo % invalidación está en formato decimal (ej. 0.85 = 85%)
|
||||
ppl_lookup = {}
|
||||
for cursoid_text, cc_ids in curso_to_cc.items():
|
||||
total_ppl = sum(cc_ppl[cc_id] for cc_id in cc_ids if cc_id in cc_ppl)
|
||||
total_ppl = sum(
|
||||
cc_data[cc_id]["ppl"] * cc_data[cc_id]["pct"]
|
||||
for cc_id in cc_ids if cc_id in cc_data
|
||||
)
|
||||
ppl_lookup[cursoid_text] = round(total_ppl, 2)
|
||||
|
||||
return ppl_lookup
|
||||
# 4. CursoMes del mes/año actual → cap_lookup
|
||||
cap_formula = f"AND({{Mes}}='{mes_nombre}',{{Año}}='{anio_str}')"
|
||||
cursomes_records = self.cursomes.all(
|
||||
formula=cap_formula,
|
||||
fields=["CursoID", "Caping Admitido"],
|
||||
)
|
||||
cap_lookup = {}
|
||||
for r in cursomes_records:
|
||||
cap = int(r["fields"].get("Caping Admitido") or 0)
|
||||
for curso_rec_id in r["fields"].get("CursoID", []):
|
||||
cursoid_text = curso_by_recordid.get(curso_rec_id)
|
||||
if cursoid_text:
|
||||
cap_lookup[cursoid_text] = cap
|
||||
|
||||
return ppl_lookup, cap_lookup
|
||||
|
||||
def get_active_campaigns(self) -> list[dict]:
|
||||
"""Lee todas las campañas activas desde 'Google Ads Campaigns'."""
|
||||
@ -61,7 +102,7 @@ class AirtableClient:
|
||||
})
|
||||
return [c for c in result if c["google_campaign_id"]] # descartar sin ID
|
||||
|
||||
def sync_campaigns_from_google_ads(self, google_campaigns: list[dict], monthly_metrics: dict = None, ppl_lookup: dict = None) -> dict:
|
||||
def sync_campaigns_from_google_ads(self, google_campaigns: list[dict], monthly_metrics: dict = None, ppl_lookup: dict = None, cap_lookup: dict = None) -> dict:
|
||||
"""
|
||||
Sincroniza campañas de Google Ads → Airtable.
|
||||
- Crea las campañas nuevas con el status real de Google Ads.
|
||||
@ -91,10 +132,11 @@ class AirtableClient:
|
||||
conv = round(metrics.get("conversions", 0), 2)
|
||||
cost = round(metrics.get("cost", 0), 2)
|
||||
|
||||
# PPL calculado desde CENTROCURSO (CursoID Text viene de Airtable)
|
||||
# PPL y CapTotalMes calculados desde CursoID Text (viene de Airtable)
|
||||
cursoid_text = str((at_record or {}).get("fields", {}).get("CursoID Text", "")).strip() if at_record else ""
|
||||
ppl = round((ppl_lookup or {}).get(cursoid_text, 0), 2)
|
||||
cpa_max = round(ppl * 0.70, 2)
|
||||
cap = int((cap_lookup or {}).get(cursoid_text, 0))
|
||||
|
||||
if at_record is None:
|
||||
fields = {"Campaign Name": gc["name"], "CampaignID": gid, "Status": at_status}
|
||||
@ -104,6 +146,8 @@ class AirtableClient:
|
||||
if ppl_lookup is not None:
|
||||
fields["PPL"] = ppl
|
||||
fields["CPAMax"] = cpa_max
|
||||
if cap_lookup is not None:
|
||||
fields["CapTotalMes"] = cap
|
||||
to_create.append(fields)
|
||||
created.append({"name": gc["name"], "id": gid, "status": at_status})
|
||||
else:
|
||||
@ -131,6 +175,9 @@ class AirtableClient:
|
||||
changes["PPL"] = ppl
|
||||
if at_record["fields"].get("CPAMax") != cpa_max:
|
||||
changes["CPAMax"] = cpa_max
|
||||
if cap_lookup is not None:
|
||||
if at_record["fields"].get("CapTotalMes") != cap:
|
||||
changes["CapTotalMes"] = cap
|
||||
if changes:
|
||||
to_update.append((at_record["id"], changes, gc["name"]))
|
||||
if change_detail:
|
||||
|
||||
6
run.py
6
run.py
@ -54,9 +54,9 @@ def run():
|
||||
print("→ Sincronizando campañas desde Google Ads...")
|
||||
google_campaigns = gads.get_all_campaigns()
|
||||
monthly_metrics = gads.get_monthly_metrics_all()
|
||||
print(" Calculando PPL desde CENTROCURSO...")
|
||||
ppl_lookup = at.build_ppl_lookup()
|
||||
sync_result = at.sync_campaigns_from_google_ads(google_campaigns, monthly_metrics, ppl_lookup)
|
||||
print(" Calculando PPL y CapTotalMes...")
|
||||
ppl_lookup, cap_lookup = at.build_campaign_lookups()
|
||||
sync_result = at.sync_campaigns_from_google_ads(google_campaigns, monthly_metrics, ppl_lookup, cap_lookup)
|
||||
if sync_result["created"]:
|
||||
print(f" ✅ Campañas nuevas importadas ({len(sync_result['created'])}):")
|
||||
for c in sync_result["created"]:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user