Files
balam/planeacion/generar_avance_2026_08_06.py
T

193 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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%}")