Files
balam/planeacion/generar_avance_2026_09_15.py
T
Johann d9047de6fa Corte de horas al 15-sep (94.08 h): 39.25 h del 10-ago al 15-sep reconstruidas con evidencia y Excel de avance; seis sesiones sin bitácora listadas como huecos
- Seguimiento-horas.csv: 30 filas nuevas (sesiones 'Seguimiento PAF' del 21, 24
  y 26-ago y 3, 9 y 10-sep acotadas por los informes de Read AI; bloques de
  desarrollo por commits y mtimes; comunicación con Pedro y Noé). Marcadas
  `reconstruida` salvo la sesión del 12-ago (transcript). Actividades nuevas
  E1-09, E2-02, E2-03, E3-01, E3-02. Etapa 1 32.25 h, Etapa 2 23.75 h, ajuste
  Jira 14.75 h (rebasa las 7–10 aprobadas), Etapa 3 1.50 h.
- Avance-Balam-2026-09-15.xlsx generado desde generar_avance_2026_09_15.py
  (parte del corte 06b; el reparto por fila aborta si no cuadra con el CSV):
  publicación provisional en paputec.mx, cotización individual y en lote,
  prefactura por API bloqueada por BIND.
- Seguimiento-horas.md: sección del corte y criterios de la reconstrucción.
- PENDIENTES.md: huecos de registro del corte (seis sesiones sin REGISTRO,
  publicación del 3-sep y lote del 7-sep sin registrar, validación de horas,
  correos de Noé del 24-ago).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 08:48:23 -06:00

254 lines
14 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-09-15.xlsx a partir del corte del 6-ago (2.º).
Corte del martes 15-sep: cubre del 10-ago al 15-sep. Lo que pasó en el
periodo: capa de escritura real (primera cotización en BIND desde un ticket el
21-ago, folio 1026; cotización en lote el 7-sep), el camino cotización→
prefactura construido y BLOQUEADO por BIND (26-ago: IsFiscalInvoice=true timbra
al crear, false produce remisión, el tipo Prefactura solo existe en su UI),
la plataforma publicada en https://paputec.mx/balam/ el 3-sep como ambiente
provisional mientras llega Azure, y seis sesiones "Seguimiento PAF" con Balam
(21, 24 y 26-ago; 3, 9 y 10-sep). El corte del 6-ago no se toca.
Las horas se leen de `Seguimiento-horas.csv` (fuente de la verdad); el script
aborta si el reparto por fila no cuadra con las horas nuevas del CSV. Las
39.25 h nuevas están marcadas `reconstruida` (evidencia: commits, correos e
informes de reunión) y pendientes de validación por Johann.
"""
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-06b.xlsx")
DST = os.path.join(BASE, "Avance-Balam-2026-09-15.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
CORTE_ANTERIOR = 54.83 # 2.º corte del 6-ago, ya reportado
# --- CSV: totales por etapa y por actividad -----------------------------------
por_act, por_etapa = defaultdict(float), defaultdict(float)
nuevas = 0.0
with open(CSV_HORAS, encoding="utf-8", newline="") as fh:
for r in csv.DictReader(fh):
if not (r.get("fecha") or "").strip():
continue
h = float(r["horas"])
por_act[r["actividad_id"]] += h
por_etapa[r["etapa"]] += h
if r["fecha"] > "2026-08-06":
nuevas += h
E0, E1, E3 = por_etapa["0"], por_etapa["1"], por_etapa["3"]
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 + emisión)
TOTAL = E0 + E1 + E2 + AJ + E3
assert abs(TOTAL - sum(por_etapa.values())) < 0.01
assert abs(TOTAL - CORTE_ANTERIOR - nuevas) < 0.01, "El CSV no arranca del corte del 6-ago"
PCT_GLOBAL = round(TOTAL / ((119 + 146) / 2), 2)
# --- Reparto por fila de las horas nuevas (10-ago → 15-sep) --------------------
# Trazabilidad CSV → fila del Excel:
# E1-05 +4.50 (bitácora, cruce del buzón, WhatsApp con Pedro, correo Azure,
# corte de horas) → "Demostración semanal + reporte de horas" (gestión)
# E1-09 +2.50 (publicación en paputec.mx) → 2.25 a "Configuración de
# infraestructura en Azure" (ambiente provisional) y 0.25 a "CI/CD"
# (deploy.sh reproducible + secretos fuera del repo)
# AJ-01 +9.25 → 4.50 a "Disparador…" (sondeo FAC-106 + cableado 20-ago 1.50,
# investigación hasta el folio 1026 el 21-ago 3.00) y 4.75 a
# "Trazabilidad… y prueba observada" (definiciones 10-ago 1.50, sesión
# 12-ago 1.00, Excel de campos 0.75, sesiones 21-ago 0.75 y 3-sep 0.75)
# E2-02 +8.00 (capa de escritura de cotización 11/12-ago 4.50, ítem por
# código 3-sep 1.50, lote 7-sep 2.00) → "Creación de cotizaciones"
# E2-03 +13.50 (prefactura 23-ago, prefactura→factura 25-ago, incidente e
# investigación 26-ago, sesiones 24 y 26-ago, 9 y 10-sep) →
# "Conversión cotización→factura"
# E3-01 +1.00 → "Endurecimiento de seguridad"; E3-02 +0.50 → "Documentación técnica"
DELTAS_TRAB = {
"Demostración semanal": 4.50,
"Configuración de infraestructura en Azure": 2.25,
"CI/CD": 0.25,
"Disparador por estatus": 4.50,
"Trazabilidad ticket": 4.75,
"Creación de cotizaciones desde la plataforma": 8.00,
"Conversión cotización→factura": 13.50,
"Endurecimiento de seguridad": 1.00,
"Documentación técnica": 0.50,
}
assert abs(sum(DELTAS_TRAB.values()) - nuevas) < 0.01, (
f"El reparto ({sum(DELTAS_TRAB.values()):.2f}) no suma las {nuevas:.2f} h nuevas del CSV")
ENCABEZADOS = {
"Integracion de plataformas Balam": (TOTAL, PCT_GLOBAL),
"Etapa 0": (E0, 1.0),
"Etapa 1": (E1, 0.95),
"Etapa 2": (E2, 0.70),
"» Ajuste dentro de la Etapa 2": (AJ, 0.85),
"Etapa 3": (E3, 0.05),
}
# (pct, estatus, fill, font, bold) — solo lo que cambió respecto al 6-ago.
FILAS = {
"Demostración semanal": (0.6, "🟡 En curso · el seguimiento pasó a Pedro (sesiones 'Seguimiento PAF' "
"21, 24 y 26-ago; 3, 9 y 10-sep) · corte de horas al 15-sep",
A_FILL, A_FONT, False),
"Capa de escritura controlada": (0.9, "✅ Implementada: dry-run + confirmación humana + auditoría en "
"WriteOperations para cotización (21-ago) y prefactura (23-ago) · "
"el timbrado no existe en el API de BIND (es humano, en su pantalla)",
V_FILL, V_FONT, False),
"Configuración de infraestructura en Azure": (0.5, "🟡 Publicada en https://paputec.mx/balam/ (3-sep) como "
"ambiente provisional · Azure sigue con Balam (requisitos "
"enviados a Noé el 25-ago; datos del tenant revisados el 3-sep)",
A_FILL, A_FONT, False),
"CI/CD": (0.3, "🟡 Publicación reproducible (deploy.sh) con secretos fuera del repo · CI real depende de Azure",
A_FILL, A_FONT, False),
"Disparador por estatus": (0.6, "🟡 Cotización real desde el ticket (21-ago, folio 1026) y en lote (7-sep), "
"con disparo manual · el disparo automático por estatus espera el ambiente definitivo",
A_FILL, A_FONT, False),
"Trazabilidad ticket": (0.6, "🟡 Vínculo ticket→cotización→prefactura en la BD · pruebas observadas con "
"Balam el 12, 21, 24 y 26-ago y el 3-sep",
A_FILL, A_FONT, False),
"Creación de cotizaciones desde la plataforma": (0.9, "✅ Cotización individual y en lote con dry-run y "
"confirmación humana · ítem del catálogo BIND por código y "
"concepto = título del ticket (definición del 3-sep)",
V_FILL, V_FONT, False),
"Conversión cotización→factura": (0.6, "🔴 Prefactura por API bloqueada por BIND: IsFiscalInvoice=true "
"TIMBRA al crear (incidente 26-ago, CFDI 2386) y false produce remisión; "
"el tipo Prefactura solo existe en su UI · en curso la captura del "
"endpoint interno (sesiones 9 y 10-sep)",
R_FILL, R_FONT, True),
"Endurecimiento de seguridad": (0.4, "🟡 Aplicado en la publicación: contenedor de solo lectura, rate limit, "
"bloqueo de login, CSP/HSTS, respaldos diarios",
A_FILL, A_FONT, False),
"Documentación técnica": (0.3, "🟡 Runbook de publicación, operación y rollback (deploy/paputec/README.md)",
A_FILL, A_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 15-sep-2026 · plataforma publicada en paputec.mx (provisional), cotización real "
"desde el ticket (individual y en lote); prefactura por API bloqueada por BIND · horas del "
"10-ago al 15-sep reconstruidas, pendientes de validar")
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 15-sep donde estaba el del 6-ago ---------
notas = [
("✅ Completada · 🟡 En curso · 🔴 Pendiente/bloqueada · ⚪ Próxima · "
"(adelantada) = ejecutada antes de su fecha plan", True),
(f"Horas trabajadas al 15-sep: Etapa 0 {E0:.2f} h (cerrada y validada) · Etapa 1 {E1:.2f} h "
f"(rango 32–39) · Etapa 2 {E2:.2f} h · ajuste Jira {AJ:.2f} h (de 7–10 aprobadas) · "
f"Etapa 3 {E3:.2f} h · total {TOTAL:.2f} h. Las {nuevas:.2f} h posteriores al corte del 6-ago "
"están reconstruidas con evidencia (commits, correos, informes de reunión) y pendientes de "
"validar. Base de facturación a $600/h + IVA.", True),
("Etapa 1: la capa de escritura quedó implementada (cotización y prefactura con dry-run, "
"confirmación humana y auditoría). Solo Azure y CI/CD siguen dependiendo de Balam; mientras "
"tanto la plataforma está publicada en https://paputec.mx/balam/ desde el 3-sep (BIND y Jira "
"en solo lectura fuera de sesión).", False),
("Integración Jira → BIND: primera cotización real desde un ticket el 21-ago (folio 1026, tras "
"5 intentos por dos semánticas indocumentadas del API: Type=2 y VAT como tasa), tres tickets "
"cotizados en la sesión del 3-sep y cotización en lote desde el 7-sep. Rebasa las 7–10 h "
"aprobadas por las semánticas indocumentadas y las sesiones acompañadas (12, 21-ago, 3-sep).", False),
("Etapa 2: la prefactura por API NO es posible con lo que BIND expone (26-ago: IsFiscalInvoice=true "
"timbra al crear —CFDI 2386, a cancelar— y false produce remisión; el tipo Prefactura solo "
"existe en su UI). Sesiones del 9 y 10-sep con Araceli y Pedro para capturar el endpoint "
"interno y decidir el camino (réplica, detección al timbrado o soporte de BIND).", False),
("En espera de Balam: acceso a Azure (requisitos enviados el 25-ago), cancelación del CFDI 2386 "
"y de la cotización 1026, y la definición del camino de prefactura.", 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 = 40 if len(txt) > 220 else (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} · E3 {E3:.2f} · TOTAL {TOTAL:.2f} h "
f"({nuevas:.2f} nuevas) · avance global {PCT_GLOBAL:.0%}")