Add GAMes table and daily margin table in Slack
- GAMes: new Airtable table aggregating daily fco_ metrics (coste, ingreso_sum, ingreso_lxp, leads, leads_lake) - run.py: accumulate fco_ daily aggregate and write to GAMes each run - slack_reporter.py: replace sparkline with daily margin % table (Sumatorio + LeadsxPPL per day) - backfill_games_mayo.py: populated GAMes with all 17 existing May days Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
523e04d61f
commit
bdc0d5ede3
@ -14,6 +14,7 @@ class AirtableClient:
|
|||||||
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")
|
self.cursomes = self.api.table(config.AIRTABLE_BASE_ID, "CursoMes")
|
||||||
self.gacampaignmes = self.api.table(config.AIRTABLE_BASE_ID, "GACampaignMes")
|
self.gacampaignmes = self.api.table(config.AIRTABLE_BASE_ID, "GACampaignMes")
|
||||||
|
self.games = self.api.table(config.AIRTABLE_BASE_ID, "GAMes")
|
||||||
|
|
||||||
MESES_ES = {
|
MESES_ES = {
|
||||||
1: "Enero", 2: "Febrero", 3: "Marzo", 4: "Abril",
|
1: "Enero", 2: "Febrero", 3: "Marzo", 4: "Abril",
|
||||||
@ -478,6 +479,25 @@ class AirtableClient:
|
|||||||
for i in range(0, len(batch), 10):
|
for i in range(0, len(batch), 10):
|
||||||
self.gacampaignmes.batch_update(batch[i:i+10])
|
self.gacampaignmes.batch_update(batch[i:i+10])
|
||||||
|
|
||||||
|
# ── GAMes ──────────────────────────────────────────────────────────────── #
|
||||||
|
|
||||||
|
def get_or_create_games_record(self, mes: int, anio: int) -> str:
|
||||||
|
records = self.games.all(formula=f"AND({{Mes}}='{mes}',{{Año}}='{anio}')")
|
||||||
|
if records:
|
||||||
|
return records[0]["id"]
|
||||||
|
r = self.games.create({"Mes": str(mes), "Año": str(anio)})
|
||||||
|
return r["id"]
|
||||||
|
|
||||||
|
def update_games_metricas(self, record_id: str, metricas_json: str) -> None:
|
||||||
|
self.games.update(record_id, {"MetricasDiarias": metricas_json})
|
||||||
|
|
||||||
|
def get_games_metricas(self, record_id: str) -> dict:
|
||||||
|
r = self.games.get(record_id)
|
||||||
|
try:
|
||||||
|
return json.loads(r["fields"].get("MetricasDiarias") or "{}")
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
return {}
|
||||||
|
|
||||||
def batch_update_gacampaignmes_advice(self, updates: list[tuple]) -> None:
|
def batch_update_gacampaignmes_advice(self, updates: list[tuple]) -> None:
|
||||||
"""
|
"""
|
||||||
Actualiza en lote los campos 'Consejo', 'Criticidad' y 'Log' de GACampaignMes.
|
Actualiza en lote los campos 'Consejo', 'Criticidad' y 'Log' de GACampaignMes.
|
||||||
|
|||||||
73
backfill_games_mayo.py
Normal file
73
backfill_games_mayo.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
"""
|
||||||
|
Script one-off: crea el registro de mayo 2026 en GAMes y rellena MetricasDiarias
|
||||||
|
con el agregado diario de todas las campañas fco_ a partir de GACampaignMes.
|
||||||
|
|
||||||
|
Ejecutar una sola vez:
|
||||||
|
python backfill_games_mayo.py
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from airtable_client import AirtableClient
|
||||||
|
|
||||||
|
at = AirtableClient()
|
||||||
|
|
||||||
|
MES = 5
|
||||||
|
ANIO = 2026
|
||||||
|
|
||||||
|
print("Cargando registros GACampaignMes de mayo (campañas fco_)...")
|
||||||
|
records = at.gacampaignmes.all(
|
||||||
|
formula="AND({Mes}='5',{Año}='2026')",
|
||||||
|
fields=["CampaignID", "MetricasDiarias", "Campaign Name (from CampaignID)"],
|
||||||
|
)
|
||||||
|
|
||||||
|
campaigns_records = at.campaigns.all(fields=["CampaignID", "PPL"])
|
||||||
|
at_id_to_info = {
|
||||||
|
r["id"]: {
|
||||||
|
"gid": str(r["fields"].get("CampaignID", "")).strip(),
|
||||||
|
"ppl": float(r["fields"].get("PPL", 0) or 0),
|
||||||
|
}
|
||||||
|
for r in campaigns_records
|
||||||
|
}
|
||||||
|
|
||||||
|
# Agregar métricas por día para campañas fco_
|
||||||
|
daily_agg: dict[str, dict] = {}
|
||||||
|
|
||||||
|
for r in records:
|
||||||
|
at_cids = r["fields"].get("CampaignID", [])
|
||||||
|
if not at_cids:
|
||||||
|
continue
|
||||||
|
info = at_id_to_info.get(at_cids[0], {})
|
||||||
|
ppl = info.get("ppl", 0)
|
||||||
|
|
||||||
|
campaign_names = r["fields"].get("Campaign Name (from CampaignID)", [])
|
||||||
|
campaign_name = (campaign_names[0] if campaign_names else "").lower()
|
||||||
|
if not campaign_name.startswith("fco_"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
md = json.loads(r["fields"].get("MetricasDiarias") or "{}")
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
md = {}
|
||||||
|
|
||||||
|
for day_str, vals in md.items():
|
||||||
|
if day_str not in daily_agg:
|
||||||
|
daily_agg[day_str] = {"coste": 0.0, "ingreso_sum": 0.0, "ingreso_lxp": 0.0, "leads": 0, "leads_lake": 0}
|
||||||
|
daily_agg[day_str]["coste"] += vals.get("coste", 0)
|
||||||
|
daily_agg[day_str]["ingreso_sum"] += vals.get("ingreso", 0)
|
||||||
|
daily_agg[day_str]["ingreso_lxp"] += vals.get("leads_lake", 0) * ppl
|
||||||
|
daily_agg[day_str]["leads"] += int(vals.get("leads", 0))
|
||||||
|
daily_agg[day_str]["leads_lake"] += int(vals.get("leads_lake", 0))
|
||||||
|
|
||||||
|
print(f" > Dias agregados: {sorted(daily_agg.keys())}")
|
||||||
|
|
||||||
|
# Redondear
|
||||||
|
for d in daily_agg:
|
||||||
|
daily_agg[d] = {k: round(v, 2) if isinstance(v, float) else v for k, v in daily_agg[d].items()}
|
||||||
|
|
||||||
|
print("Creando/obteniendo registro GAMes de mayo 2026...")
|
||||||
|
games_rid = at.get_or_create_games_record(MES, ANIO)
|
||||||
|
print(f" > Record ID: {games_rid}")
|
||||||
|
|
||||||
|
print("Guardando MetricasDiarias en GAMes...")
|
||||||
|
at.update_games_metricas(games_rid, json.dumps(daily_agg, ensure_ascii=False))
|
||||||
|
|
||||||
|
print(f"OK Backfill GAMes completado: {len(daily_agg)} dias guardados.")
|
||||||
20
run.py
20
run.py
@ -219,6 +219,7 @@ def run():
|
|||||||
resumen = []
|
resumen = []
|
||||||
advice_updates = [] # (gcm_record_id, consejo, criticidad) para batch update final
|
advice_updates = [] # (gcm_record_id, consejo, criticidad) para batch update final
|
||||||
metricas_updates = [] # {airtable_id, metricas_json} para MetricasDiarias
|
metricas_updates = [] # {airtable_id, metricas_json} para MetricasDiarias
|
||||||
|
games_agg: dict = {} # dia_hoy → {coste, ingreso_sum, ingreso_lxp, leads, leads_lake}
|
||||||
ayer = datetime.now() - timedelta(days=1)
|
ayer = datetime.now() - timedelta(days=1)
|
||||||
dia_hoy = ayer.strftime("%d")
|
dia_hoy = ayer.strftime("%d")
|
||||||
cambio_mes = ayer.month != datetime.now().month
|
cambio_mes = ayer.month != datetime.now().month
|
||||||
@ -306,6 +307,15 @@ def run():
|
|||||||
leads_lake_hoy = leads_yesterday.get(cid, 0)
|
leads_lake_hoy = leads_yesterday.get(cid, 0)
|
||||||
md[dia_hoy] = {"coste": coste_hoy, "ingreso": ingreso_hoy, "margen": margen_hoy, "leads": int(conv_hoy), "leads_lake": leads_lake_hoy}
|
md[dia_hoy] = {"coste": coste_hoy, "ingreso": ingreso_hoy, "margen": margen_hoy, "leads": int(conv_hoy), "leads_lake": leads_lake_hoy}
|
||||||
campaign["metricas_diarias"] = json.dumps(md, ensure_ascii=False)
|
campaign["metricas_diarias"] = json.dumps(md, ensure_ascii=False)
|
||||||
|
|
||||||
|
if campaign["curso"].lower().startswith("fco_"):
|
||||||
|
if dia_hoy not in games_agg:
|
||||||
|
games_agg[dia_hoy] = {"coste": 0.0, "ingreso_sum": 0.0, "ingreso_lxp": 0.0, "leads": 0, "leads_lake": 0}
|
||||||
|
games_agg[dia_hoy]["coste"] += coste_hoy
|
||||||
|
games_agg[dia_hoy]["ingreso_sum"] += ingreso_hoy
|
||||||
|
games_agg[dia_hoy]["ingreso_lxp"] += leads_lake_hoy * campaign["ppl"]
|
||||||
|
games_agg[dia_hoy]["leads"] += int(conv_hoy)
|
||||||
|
games_agg[dia_hoy]["leads_lake"] += leads_lake_hoy
|
||||||
metricas_updates.append({
|
metricas_updates.append({
|
||||||
"airtable_id": campaign["airtable_id"],
|
"airtable_id": campaign["airtable_id"],
|
||||||
"metricas_json": json.dumps(md, ensure_ascii=False),
|
"metricas_json": json.dumps(md, ensure_ascii=False),
|
||||||
@ -355,6 +365,16 @@ def run():
|
|||||||
at.batch_update_metricas_diarias(metricas_updates)
|
at.batch_update_metricas_diarias(metricas_updates)
|
||||||
print(" ✓ MetricasDiarias actualizado.")
|
print(" ✓ MetricasDiarias actualizado.")
|
||||||
|
|
||||||
|
# Actualizar GAMes con el agregado diario fco_
|
||||||
|
if games_agg:
|
||||||
|
print("→ Actualizando GAMes...")
|
||||||
|
games_rid = at.get_or_create_games_record(ayer.month, ayer.year)
|
||||||
|
games_md = at.get_games_metricas(games_rid)
|
||||||
|
for d, vals in games_agg.items():
|
||||||
|
games_md[d] = {k: round(v, 2) if isinstance(v, float) else v for k, v in vals.items()}
|
||||||
|
at.update_games_metricas(games_rid, json.dumps(games_md, ensure_ascii=False))
|
||||||
|
print(" ✓ GAMes actualizado.")
|
||||||
|
|
||||||
# Snapshot diario: ConvLeadsLakeMesFinal + ConvLeadsLakeMesGrupo
|
# Snapshot diario: ConvLeadsLakeMesFinal + ConvLeadsLakeMesGrupo
|
||||||
# PMX con companion Search → Grupo = conversiones Google (ya calculado en leads_grupo)
|
# PMX con companion Search → Grupo = conversiones Google (ya calculado en leads_grupo)
|
||||||
final_leads_data = [
|
final_leads_data = [
|
||||||
|
|||||||
@ -151,6 +151,42 @@ def build_and_send(collected: list, dry_run: bool, prev_month_metricas: dict = N
|
|||||||
worst_month = month_rows[:TOP_N]
|
worst_month = month_rows[:TOP_N]
|
||||||
best_month = list(reversed(month_rows[-TOP_N:]))
|
best_month = list(reversed(month_rows[-TOP_N:]))
|
||||||
|
|
||||||
|
# ── Tabla de márgenes diarios ─────────────────────────────────────────────
|
||||||
|
daily_totals: dict[int, dict] = {}
|
||||||
|
for item in fco:
|
||||||
|
ppl = item["campaign"].get("ppl", 0)
|
||||||
|
md = _parse_metricas(item["campaign"].get("metricas_diarias", "{}"))
|
||||||
|
for day_str, vals in md.items():
|
||||||
|
try:
|
||||||
|
d = int(day_str)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if d not in daily_totals:
|
||||||
|
daily_totals[d] = {"coste": 0.0, "ingreso_sum": 0.0, "ingreso_lxp": 0.0}
|
||||||
|
daily_totals[d]["coste"] += vals.get("coste", 0)
|
||||||
|
daily_totals[d]["ingreso_sum"] += vals.get("ingreso", 0)
|
||||||
|
daily_totals[d]["ingreso_lxp"] += vals.get("leads_lake", 0) * ppl
|
||||||
|
|
||||||
|
margin_table_block = None
|
||||||
|
if daily_totals and not primer_dia_mes:
|
||||||
|
rows = ["Día Sumatorio LeadsxPPL"]
|
||||||
|
for d in sorted(daily_totals):
|
||||||
|
coste = daily_totals[d]["coste"]
|
||||||
|
ing_sum = daily_totals[d]["ingreso_sum"]
|
||||||
|
ing_lxp = daily_totals[d]["ingreso_lxp"]
|
||||||
|
pct_sum = round((ing_sum - coste) / ing_sum * 100, 1) if ing_sum > 0 else 0.0
|
||||||
|
pct_lxp = round((ing_lxp - coste) / ing_lxp * 100, 1) if ing_lxp > 0 else 0.0
|
||||||
|
s_sum = ("+" if pct_sum >= 0 else "") + f"{pct_sum:.1f}%"
|
||||||
|
s_lxp = ("+" if pct_lxp >= 0 else "") + f"{pct_lxp:.1f}%"
|
||||||
|
rows.append(f"{d:02d} {s_sum:>9} {s_lxp:>9}")
|
||||||
|
margin_table_block = {
|
||||||
|
"type": "section",
|
||||||
|
"text": {
|
||||||
|
"type": "mrkdwn",
|
||||||
|
"text": f"📅 *MÁRGENES POR DÍA — {MESES_ES[now.month].upper()}*\n```\n" + "\n".join(rows) + "\n```",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
# ── Alertas ──────────────────────────────────────────────────────────────
|
# ── Alertas ──────────────────────────────────────────────────────────────
|
||||||
alerts = [
|
alerts = [
|
||||||
r for r in month_rows
|
r for r in month_rows
|
||||||
@ -232,6 +268,10 @@ def build_and_send(collected: list, dry_run: bool, prev_month_metricas: dict = N
|
|||||||
)
|
)
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
if margin_table_block:
|
||||||
|
blocks.append({"type": "divider"})
|
||||||
|
blocks.append(margin_table_block)
|
||||||
|
|
||||||
blocks.append({"type": "divider"})
|
blocks.append({"type": "divider"})
|
||||||
blocks.append({
|
blocks.append({
|
||||||
"type": "section",
|
"type": "section",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user