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.leads = self.api.table(config.AIRTABLE_BASE_ID, config.LEADS_TABLE)
|
||||||
self.campaigns = self.api.table(config.AIRTABLE_BASE_ID, config.CAMPAIGNS_TABLE)
|
self.campaigns = self.api.table(config.AIRTABLE_BASE_ID, config.CAMPAIGNS_TABLE)
|
||||||
self.cursos = self.api.table(config.AIRTABLE_BASE_ID, "Cursos")
|
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:
|
MESES_ES = {
|
||||||
"""
|
1: "Enero", 2: "Febrero", 3: "Marzo", 4: "Abril",
|
||||||
Construye un dict {cursoid_text: ppl_sum} calculando el PPL de cada curso
|
5: "Mayo", 6: "Junio", 7: "Julio", 8: "Agosto",
|
||||||
sumando los PPL de los registros CENTROCURSO con Estado ROI = 'ABIERTO'.
|
9: "Septiembre", 10: "Octubre", 11: "Noviembre", 12: "Diciembre",
|
||||||
|
}
|
||||||
|
|
||||||
Cadena: CursoID Text → Cursos.CursoID → Cursos.CentroCurso → CENTROCURSO.PPL
|
def build_campaign_lookups(self) -> tuple[dict, dict]:
|
||||||
Solo se usan en lotes (2 llamadas bulk), sin llamadas por campaña.
|
|
||||||
"""
|
"""
|
||||||
# 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(
|
cc_records = self.centrocurso.all(
|
||||||
formula="{Estado ROI}='ABIERTO'",
|
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"])
|
cursos_records = self.cursos.all(fields=["CursoID", "CentroCurso"])
|
||||||
|
curso_by_recordid = {}
|
||||||
curso_to_cc = {}
|
curso_to_cc = {}
|
||||||
for r in cursos_records:
|
for r in cursos_records:
|
||||||
cid = r["fields"].get("CursoID")
|
cid = r["fields"].get("CursoID")
|
||||||
cc_ids = r["fields"].get("CentroCurso", [])
|
cc_ids = r["fields"].get("CentroCurso", [])
|
||||||
if cid is not None:
|
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 = {}
|
ppl_lookup = {}
|
||||||
for cursoid_text, cc_ids in curso_to_cc.items():
|
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)
|
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]:
|
def get_active_campaigns(self) -> list[dict]:
|
||||||
"""Lee todas las campañas activas desde 'Google Ads Campaigns'."""
|
"""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
|
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.
|
Sincroniza campañas de Google Ads → Airtable.
|
||||||
- Crea las campañas nuevas con el status real de Google Ads.
|
- 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)
|
conv = round(metrics.get("conversions", 0), 2)
|
||||||
cost = round(metrics.get("cost", 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 ""
|
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)
|
ppl = round((ppl_lookup or {}).get(cursoid_text, 0), 2)
|
||||||
cpa_max = round(ppl * 0.70, 2)
|
cpa_max = round(ppl * 0.70, 2)
|
||||||
|
cap = int((cap_lookup or {}).get(cursoid_text, 0))
|
||||||
|
|
||||||
if at_record is None:
|
if at_record is None:
|
||||||
fields = {"Campaign Name": gc["name"], "CampaignID": gid, "Status": at_status}
|
fields = {"Campaign Name": gc["name"], "CampaignID": gid, "Status": at_status}
|
||||||
@ -104,6 +146,8 @@ class AirtableClient:
|
|||||||
if ppl_lookup is not None:
|
if ppl_lookup is not None:
|
||||||
fields["PPL"] = ppl
|
fields["PPL"] = ppl
|
||||||
fields["CPAMax"] = cpa_max
|
fields["CPAMax"] = cpa_max
|
||||||
|
if cap_lookup is not None:
|
||||||
|
fields["CapTotalMes"] = cap
|
||||||
to_create.append(fields)
|
to_create.append(fields)
|
||||||
created.append({"name": gc["name"], "id": gid, "status": at_status})
|
created.append({"name": gc["name"], "id": gid, "status": at_status})
|
||||||
else:
|
else:
|
||||||
@ -131,6 +175,9 @@ class AirtableClient:
|
|||||||
changes["PPL"] = ppl
|
changes["PPL"] = ppl
|
||||||
if at_record["fields"].get("CPAMax") != cpa_max:
|
if at_record["fields"].get("CPAMax") != cpa_max:
|
||||||
changes["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:
|
if changes:
|
||||||
to_update.append((at_record["id"], changes, gc["name"]))
|
to_update.append((at_record["id"], changes, gc["name"]))
|
||||||
if change_detail:
|
if change_detail:
|
||||||
|
|||||||
6
run.py
6
run.py
@ -54,9 +54,9 @@ def run():
|
|||||||
print("→ Sincronizando campañas desde Google Ads...")
|
print("→ Sincronizando campañas desde Google Ads...")
|
||||||
google_campaigns = gads.get_all_campaigns()
|
google_campaigns = gads.get_all_campaigns()
|
||||||
monthly_metrics = gads.get_monthly_metrics_all()
|
monthly_metrics = gads.get_monthly_metrics_all()
|
||||||
print(" Calculando PPL desde CENTROCURSO...")
|
print(" Calculando PPL y CapTotalMes...")
|
||||||
ppl_lookup = at.build_ppl_lookup()
|
ppl_lookup, cap_lookup = at.build_campaign_lookups()
|
||||||
sync_result = at.sync_campaigns_from_google_ads(google_campaigns, monthly_metrics, ppl_lookup)
|
sync_result = at.sync_campaigns_from_google_ads(google_campaigns, monthly_metrics, ppl_lookup, cap_lookup)
|
||||||
if sync_result["created"]:
|
if sync_result["created"]:
|
||||||
print(f" ✅ Campañas nuevas importadas ({len(sync_result['created'])}):")
|
print(f" ✅ Campañas nuevas importadas ({len(sync_result['created'])}):")
|
||||||
for c in sync_result["created"]:
|
for c in sync_result["created"]:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user