"""Genera Avance-Balam-2026-08-06.xlsx a partir del corte del 1-ago. Corte del jueves 6-ago (sesión nocturna, previa a la demo del vie 7-ago): la LECTURA de la bandeja FAC quedó completa y en la plataforma — sondeo de customfields (IDs capturados), endpoints /api/jira/solicitudes, bandeja de solicitudes y detalle fieles al prototipo con datos reales (solo lectura), adjuntos descargables vía proxy, y el shell con el chrome del prototipo aplicado a toda la app. Disparador y trazabilidad siguen bloqueados por las definiciones de negocio de Balam. Las horas se leen de `Seguimiento-horas.csv` (fuente de la verdad, corte 53.83 h); el script aborta si el reparto por fila no cuadra con el CSV. """ import csv import os from collections import defaultdict import openpyxl from openpyxl.styles import Alignment, Font, PatternFill BASE = os.path.dirname(os.path.abspath(__file__)) SRC = os.path.join(BASE, "Avance-Balam-2026-08-01.xlsx") DST = os.path.join(BASE, "Avance-Balam-2026-08-06.xlsx") CSV_HORAS = os.path.join(BASE, "Seguimiento-horas.csv") CAFE, GRIS = "331F0E", "595959" V_FILL, V_FONT = "C6EFCE", "006100" A_FILL, A_FONT = "FFEB9C", "9C6500" R_FILL, R_FONT = "FFC7CE", "9C0006" G_FILL, G_FONT = "F2F2F2", "595959" C_PCT, C_EST, C_TRAB, C_STAT = 4, 5, 6, 7 # --- CSV: totales por etapa y por actividad ----------------------------------- por_act, por_etapa = defaultdict(float), defaultdict(float) with open(CSV_HORAS, encoding="utf-8", newline="") as fh: for r in csv.DictReader(fh): if not (r.get("fecha") or "").strip(): continue por_act[r["actividad_id"]] += float(r["horas"]) por_etapa[r["etapa"]] += float(r["horas"]) E0, E1 = por_etapa["0"], por_etapa["1"] AJ = por_act["AJ-01"] # ajuste Jira→BIND (vive dentro de la etapa 2 del CSV) E2 = por_etapa["2"] - AJ # Etapa 2 propiamente (frontend) TOTAL = E0 + E1 + E2 + AJ assert abs(TOTAL - sum(por_etapa.values())) < 0.01 PCT_GLOBAL = round(TOTAL / ((119 + 146) / 2), 2) # --- Reparto por fila del delta 1-ago→6-ago (1.25 h nuevas) -------------------- # Trazabilidad: # AJ-01 +1.00 (sondeo de customfields + endpoints + bandeja/detalle + adjuntos) # → fila "Cliente de la API de Jira" (su entregable es la lectura de FAC) # E2-01 +0.25 (shell con el chrome del prototipo) → fila "Layout base" DELTAS_TRAB = { "Cliente de la API de Jira": 1.00, "Layout base": 0.25, } assert abs(sum(DELTAS_TRAB.values()) - 1.25) < 0.01, "El reparto no suma las 1.25 h nuevas" ENCABEZADOS = { "Integracion de plataformas Balam": (TOTAL, PCT_GLOBAL), "Etapa 0": (E0, 1.0), "Etapa 1": (E1, 0.875), "Etapa 2": (E2, 0.50), "» Ajuste dentro de la Etapa 2": (AJ, 0.55), "Etapa 3": (None, 0), } # (pct, estatus, fill, font, bold) — solo lo que cambió respecto al 1-ago. FILAS = { "Cliente de la API de Jira": (1.0, "✅ Completada (6-ago): bandeja de solicitudes y detalle en la plataforma " "leyendo FAC en vivo (solo lectura) · customfields mapeados · adjuntos vía proxy", V_FILL, V_FONT, False), "Layout base": (1.0, "✅ Completada · chrome del prototipo aplicado a toda la app (6-ago)", V_FILL, V_FONT, False), } def pinta(celda, fill, font_color, bold=False, size=9, align="left"): celda.fill = PatternFill("solid", fgColor=fill) celda.font = Font(color=font_color, bold=bold, size=size) celda.alignment = Alignment(horizontal=align, vertical="center", wrap_text=True) wb = openpyxl.load_workbook(SRC) ws = wb.active ws["A2"] = ("Plan y avance al 6-ago-2026 · horas trabajadas al corte del jueves 6-ago · " "lectura de la bandeja Jira (FAC) completa y en pantalla; disparador y escritura en espera de Balam") ws["A2"].font = Font(size=10, italic=True, color=GRIS) # Las notas del corte anterior van fusionadas A:G y algunas empiezan con # "Etapa 1: …", que colisiona con los prefijos de encabezado: se localizan y # limpian ANTES del bucle principal, y este se detiene donde empezaban. primera_nota = None for row in range(5, ws.max_row + 1): v = str(ws.cell(row=row, column=1).value or "") if v.startswith("✅ Completada ·"): primera_nota = row if primera_nota and row >= primera_nota: for rango in list(ws.merged_cells.ranges): if rango.min_row == row: ws.unmerge_cells(str(rango)) ws.cell(row=row, column=1).value = None tope = primera_nota if primera_nota else ws.max_row + 1 aplicado = set() for row in range(5, tope): nombre = str(ws.cell(row=row, column=1).value or "").strip() if not nombre: continue for prefijo, (trab, pct) in ENCABEZADOS.items(): if nombre.startswith(prefijo): if trab is not None: c = ws.cell(row=row, column=C_TRAB, value=round(trab, 2)) c.number_format = "0.00" p = ws.cell(row=row, column=C_PCT, value=pct) p.number_format = "0%" if pct >= 1: pinta(p, V_FILL, V_FONT, bold=True, size=10, align="center") elif pct > 0: pinta(p, A_FILL, A_FONT, bold=True, size=10, align="center") break for prefijo, delta in DELTAS_TRAB.items(): if nombre.startswith(prefijo): actual = ws.cell(row=row, column=C_TRAB).value actual = float(actual) if isinstance(actual, (int, float)) else 0.0 c = ws.cell(row=row, column=C_TRAB, value=round(actual + delta, 2)) c.number_format = "0.00" aplicado.add(prefijo) break for prefijo, (pct, txt, fill, fcolor, bold) in FILAS.items(): if nombre.startswith(prefijo): if pct is not None: p = ws.cell(row=row, column=C_PCT, value=pct) p.number_format = "0%" if pct >= 1: pinta(p, V_FILL, V_FONT, bold=True, size=10, align="center") elif pct > 0: pinta(p, A_FILL, A_FONT, bold=True, size=10, align="center") else: p.font = Font(size=10, color=GRIS) s = ws.cell(row=row, column=C_STAT, value=txt) pinta(s, fill, fcolor, bold) break faltan = set(DELTAS_TRAB) - aplicado assert not faltan, f"Filas no encontradas en el Excel: {faltan}" # --- Notas: se escribe el bloque del 6-ago donde estaba el del 1-ago ---------- notas = [ ("✅ Completada · 🟡 En curso · 🔴 Pendiente/bloqueada · ⚪ Próxima · " "(adelantada) = ejecutada antes de su fecha plan", True), (f"Horas trabajadas al corte del jueves 6-ago: Etapa 0 {E0:.2f} h (cerrada y validada) · " f"Etapa 1 {E1:.2f} h (rango 32–39) · Etapa 2 {E2:.2f} h · ajuste Jira {AJ:.2f} h " f"(de 7–10 aprobadas) · total {TOTAL:.2f} h. Base de facturación a $600/h + IVA.", True), ("Etapa 1: 7 de 8 entregables contractuales terminados. Solo queda la capa de " "escritura, en pausa por la definición del flujo de facturación.", False), ("Integración Jira → BIND (7–10 h, v1.3 aprobada por el Ing. Noé el 30-jul): la " "LECTURA quedó completa el 6-ago — la plataforma muestra la bandeja de solicitudes " "y el detalle de cada ticket de FAC en vivo (solo lectura), con los campos del " "formulario mapeados y los adjuntos descargables. El disparador de cotización y la " "trazabilidad esperan las definiciones de negocio (request type, estatus disparador " "y origen de monto/conceptos) y la prueba de escritura acompañada.", False), ("Etapa 2: además del frontend de consulta (1-ago), toda la aplicación estrenó la " "interfaz del prototipo aprobado (navegación por módulos, marca y estado del " "sistema). Las filas de emisión siguen sujetas al flujo definido.", False), ("En espera de Balam: acceso a Azure (Contributor — con el Ing. Noé desde el " "28-jul), fecha de la sesión del flujo de facturación, y las tres definiciones de " "la integración Jira.", False), ] r = primera_nota if primera_nota else ws.max_row + 2 for txt, bold in notas: c = ws.cell(row=r, column=1, value=txt) c.font = Font(italic=True, size=9, color=CAFE, bold=bold) c.alignment = Alignment(wrap_text=True, vertical="top") try: ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=7) except ValueError: pass # ya estaba fusionada desde el archivo origen ws.row_dimensions[r].height = 28 if len(txt) > 120 else 14 r += 1 ws.print_area = f"A1:G{r}" wb.save(DST) print("OK ->", DST) print(f" E0 {E0:.2f} · E1 {E1:.2f} · E2 {E2:.2f} · Jira {AJ:.2f} · TOTAL {TOTAL:.2f} h " f"· avance global {PCT_GLOBAL:.0%}")