Files
balam/planeacion/generar_avance_2026_08_06b.py
T
Johann 6d4b781486 2.º corte del 6-ago (54.83 h): wizard de emisión asistida construido en simulación (1.00 h de sesión vespertina)
El wizard de 5 pasos de la VISTA 3 quedó navegable en balam-plataforma con
datos reales de solo lectura (cotizaciones de /api/quotes y ticket FAC vivo):
selección de cotización, datos de factura con regla PPD/PUE y advertencia SAT,
IVA por regla con totales reactivos, prefactura simulada con marca de agua y
aprobación humana visual; el timbrado queda deshabilitado hasta B7 (commits
2164f6c, 90f7633 y 0600f2f en el repo del cliente). Excel de avance del 2.º
corte generado desde script (el corte matutino no se toca); la fila de
conversión cotización→factura pasa a 40% como interfaz en simulación.
2026-08-06 14:17:06 -06:00

193 lines
8.7 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-06b.xlsx a partir del corte de la tarde del 6-ago.
Segundo corte del jueves 6-ago (sesión vespertina, previa a la demo del vie
7-ago): el wizard de emisión asistida quedó construido en la plataforma como
interfaz navegable de simulación — 5 pasos fieles al prototipo aprobado, con
las cotizaciones reales de BIND y el ticket FAC vivo (solo lectura), prefactura
simulada con marca de agua y aprobación humana visual. El timbrado queda
deshabilitado hasta que Balam autorice la escritura (B7). El corte de la
mañana (Avance-Balam-2026-08-06.xlsx) no se toca.
Las horas se leen de `Seguimiento-horas.csv` (fuente de la verdad, corte
54.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-06.xlsx")
DST = os.path.join(BASE, "Avance-Balam-2026-08-06b.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 corte matutino→vespertino (1.00 h nueva) -------
# Trazabilidad:
# E2-01 +1.00 (wizard de emisión asistida, 5 pasos en simulación)
# → fila "Conversión cotización→factura" (el wizard ES su interfaz;
# la conversión real sigue bloqueada por B7)
DELTAS_TRAB = {
"Conversión cotización→factura": 1.00,
}
assert abs(sum(DELTAS_TRAB.values()) - 1.00) < 0.01, "El reparto no suma la 1.00 h nueva"
ENCABEZADOS = {
"Integracion de plataformas Balam": (TOTAL, PCT_GLOBAL),
"Etapa 0": (E0, 1.0),
"Etapa 1": (E1, 0.875),
"Etapa 2": (E2, 0.55),
"» Ajuste dentro de la Etapa 2": (AJ, 0.55),
"Etapa 3": (None, 0),
}
# (pct, estatus, fill, font, bold) — solo lo que cambió respecto al corte matutino.
FILAS = {
"Conversión cotización→factura": (0.4, "🟡 Interfaz completa en simulación (6-ago): wizard de 5 pasos con "
"cotizaciones y tickets reales, prefactura simulada y aprobación humana "
"visual · el timbrado real espera la autorización de escritura (B7)",
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 6-ago-2026 (2.º corte del día) · lectura de la bandeja Jira (FAC) "
"completa y wizard de emisión asistida en simulación; timbrado 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 vespertino donde estaba el matutino ----------
notas = [
("✅ Completada · 🟡 En curso · 🔴 Pendiente/bloqueada · ⚪ Próxima · "
"(adelantada) = ejecutada antes de su fecha plan", True),
(f"Horas trabajadas al 2.º 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: al frontend de consulta y al chrome del prototipo se sumó el wizard de "
"emisión asistida (6-ago): 5 pasos sobre cotizaciones y tickets reales, con regla "
"PPD/PUE y advertencia SAT, IVA automático, prefactura simulada y aprobación humana "
"visual. El timbrado queda deshabilitado hasta que Balam autorice la escritura (B7).", 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%}")