"""Genera Horas-etapas-Balam-2026-07-28.xlsx — resumen de horas POR ETAPA para Erika. Pedido por Erika el 27-jul (6:46 pm: "las ya completas más que nada para ver cuántas son") y aclarado el 28-jul (8:36 am): el Excel es el insumo del conteo de horas de cada etapa "sobre todo para la FACTURACIÓN correspondiente". Las horas trabajadas se leen del Excel interno (`Plan-actividades-avance-2026-07-27.xlsx`), que a su vez sale de `Seguimiento-horas.csv` — una sola fuente de datos, igual que `generar_resumen_erika_2026_07_27.py`. Los rangos estimados y montos son los contractuales de la propuesta v1.2 (§4) y de la Adenda 01 (Jira → BIND). """ import os from datetime import date import openpyxl from openpyxl.styles import Alignment, Border, Font, PatternFill, Side from openpyxl.utils import get_column_letter BASE = os.path.dirname(os.path.abspath(__file__)) SRC = os.path.join(BASE, "Plan-actividades-avance-2026-07-27.xlsx") DST = os.path.join(BASE, "Horas-etapas-Balam-2026-07-28.xlsx") EMISION = date(2026, 7, 28) # fecha de emisión; horas al corte del lunes 27-jul TARIFA = 600 # MXN/h + IVA (propuesta v1.2 §3.3) # Marca Balam CAFE, AMARILLO, CREMA = "331F0E", "F7BD0C", "FDF3D8" V_FILL, V_FONT = "C6EFCE", "006100" # verde A_FILL, A_FONT = "FFEB9C", "9C6500" # amarillo GRIS = "595959" BORDE = "D9D9D9" thin = Side(style="thin", color=BORDE) box = Border(left=thin, right=thin, top=thin, bottom=thin) # --- Horas trabajadas desde el Excel interno (única fuente) ------------------- wsrc = openpyxl.load_workbook(SRC).active trab = {} for row in range(2, wsrc.max_row + 1): nombre = str(wsrc.cell(row=row, column=1).value or "").strip() if nombre.startswith(("Etapa 0", "Etapa 1", "Integracion")): clave = "TOTAL" if nombre.startswith("Integracion") else nombre.split(" ")[0] + nombre.split(" ")[1] trab[clave] = float(wsrc.cell(row=row, column=10).value or 0) E0, E1, TOTAL = trab["Etapa0"], trab["Etapa1"], trab["TOTAL"] assert abs((E0 + E1) - TOTAL) < 0.01, f"Horas no cuadran: {E0} + {E1} != {TOTAL}" # --- Filas del resumen -------------------------------------------------------- # (etapa, estatus, kind_estatus, est_min, est_max, trabajadas o None) ETAPAS = [ ("Etapa 0 — Discovery, infraestructura y prototipo visual", "✅ Completada y validada (24-jul)", "ok", 18, 22, E0), ("Etapa 1 — Plataforma base y sincronización con BIND", "🟡 En curso · 6 de 8 entregables terminados", "curso", 32, 39, E1), ("Etapa 2 — Facturación (consulta y emisión), catálogo y multimoneda", "⚪ Próxima", "prox", 38, 46, None), ("Etapa 3 — Cobranza, tablero, reportes y cierre", "⚪ Próxima", "prox", 24, 29, None), ] ADENDA = ("Adicional — Integración Jira → BIND (Adenda 01)", "📋 En aprobación · no estaba en la propuesta original", "prox", 9, 12, None) SUB_MIN = sum(e[3] for e in ETAPAS) # 112 SUB_MAX = sum(e[4] for e in ETAPAS) # 136 TOT_MIN, TOT_MAX = SUB_MIN + ADENDA[3], SUB_MAX + ADENDA[4] # 121, 148 def dinero(h): return f"${h * TARIFA:,.0f}" def rango_dinero(lo, hi): return f"{dinero(lo)} – {dinero(hi)}" # --- Construcción ------------------------------------------------------------- wb = openpyxl.Workbook() ws = wb.active ws.title = "Horas por etapa" ws["A1"] = "Proyecto Balam · Plataforma de Automatización Financiera" ws["A1"].font = Font(bold=True, size=14, color=CAFE) ws["A2"] = ("Horas por etapa — emitido el 28-jul-2026 · horas trabajadas al corte " "del lunes 27-jul-2026 · tarifa $600 MXN/h + IVA") ws["A2"].font = Font(size=10, italic=True, color=GRIS) ENC = ["Etapa", "Estatus", "Horas estimadas", "Horas trabajadas\n(al 27-jul)", "Importe estimado\n(MXN, antes de IVA)"] ANCHOS = [52, 34, 15, 15, 20] FILA_ENC = 4 for i, (txt, ancho) in enumerate(zip(ENC, ANCHOS), start=1): c = ws.cell(row=FILA_ENC, column=i, value=txt) c.font = Font(bold=True, size=10, color=CAFE) c.fill = PatternFill("solid", fgColor=AMARILLO) c.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) c.border = box ws.column_dimensions[get_column_letter(i)].width = ancho ws.row_dimensions[FILA_ENC].height = 28 def fila(r, nombre, estatus, kind, est, trabajadas, importe, bold=False, fill=None): c = ws.cell(row=r, column=1, value=nombre) c.font = Font(bold=bold, size=10, color=CAFE) c.alignment = Alignment(vertical="center", wrap_text=True) cs = ws.cell(row=r, column=2, value=estatus) cs.font = Font(size=9, color=GRIS) cs.alignment = Alignment(vertical="center", wrap_text=True) if kind == "ok": cs.font = Font(size=9, color=V_FONT); cs.fill = PatternFill("solid", fgColor=V_FILL) elif kind == "curso": cs.font = Font(size=9, color=A_FONT); cs.fill = PatternFill("solid", fgColor=A_FILL) ce = ws.cell(row=r, column=3, value=est) ce.font = Font(bold=bold, size=10, color=CAFE) ce.alignment = Alignment(horizontal="center", vertical="center") ct = ws.cell(row=r, column=4, value=trabajadas if trabajadas is not None else "—") if trabajadas is not None: ct.number_format = "0.00" ct.font = Font(bold=bold or trabajadas is not None, size=10, color=CAFE) ct.alignment = Alignment(horizontal="center", vertical="center") ci = ws.cell(row=r, column=5, value=importe) ci.font = Font(bold=bold, size=10, color=CAFE) ci.alignment = Alignment(horizontal="right", vertical="center") for col in range(1, 6): ws.cell(row=r, column=col).border = box if fill: ws.cell(row=r, column=col).fill = PatternFill("solid", fgColor=fill) ws.row_dimensions[r].height = 26 r = FILA_ENC + 1 for nombre, estatus, kind, lo, hi, t in ETAPAS: fila(r, nombre, estatus, kind, f"{lo} – {hi}", t, rango_dinero(lo, hi)) r += 1 fila(r, "Subtotal — propuesta v1.2", "", "sub", f"{SUB_MIN} – {SUB_MAX}", TOTAL, rango_dinero(SUB_MIN, SUB_MAX), bold=True, fill=CREMA) r += 1 nombre, estatus, kind, lo, hi, t = ADENDA fila(r, nombre, estatus, kind, f"{lo} – {hi}", t, rango_dinero(lo, hi)) r += 1 fila(r, "Total con Adenda 01", "", "tot", f"{TOT_MIN} – {TOT_MAX}", TOTAL, rango_dinero(TOT_MIN, TOT_MAX), bold=True, fill=CREMA) r += 2 # --- Notas -------------------------------------------------------------------- notas = [ f"Horas trabajadas al corte del lunes 27-jul-2026, del control de horas del proyecto " f"(mismas cifras del Excel de avance enviado el 27-jul): Etapa 0 {E0:.2f} h · " f"Etapa 1 {E1:.2f} h · total {TOTAL:.2f} h.", f"La Etapa 0 cerró en {E0:.2f} h, dentro del rango contractual de 18–22 h " f"(a $600/h: {dinero(E0)} antes de IVA).", "Referencia de facturación (propuesta v1.2, §3.3): la facturación inicial es de 30 h — " "$18,000 + IVA — y cubre la Etapa 0 completa más el inicio de la Etapa 1; se emite en " "cuanto se confirme el visto bueno. Los avances posteriores se facturan semanalmente " "(viernes) por horas efectivamente trabajadas, con pago a 30 días naturales.", "La integración Jira → BIND no estaba contemplada en la propuesta original; se " "formaliza en la Adenda 01 (compartida el 28-jul) y se ejecuta tras su aprobación " "por escrito.", "Los rangos de horas operan con tope por etapa: al aproximarse al máximo, el trabajo " "se detiene y se revisa el alcance antes de continuar.", ] for txt in notas: c = ws.cell(row=r, column=1, value=txt) c.font = Font(italic=True, size=9, color=CAFE) c.alignment = Alignment(wrap_text=True, vertical="top") ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=5) ws.row_dimensions[r].height = 28 if len(txt) > 120 else 16 r += 1 ws.freeze_panes = f"A{FILA_ENC + 1}" ws.sheet_view.showGridLines = False ws.print_area = f"A1:E{r}" ws.page_setup.orientation = "landscape" ws.page_setup.fitToWidth = 1 ws.sheet_properties.pageSetUpPr.fitToPage = True wb.save(DST) print("OK ->", DST) print(f" E0 {E0:.2f} h · E1 {E1:.2f} h · total {TOTAL:.2f} h · " f"v1.2 {SUB_MIN}–{SUB_MAX} h · con adenda {TOT_MIN}–{TOT_MAX} h")