Files
balam/planeacion/generar_horas_etapas_2026_07_28.py
T
Johann 67bce9d691 Prepara la Adenda 01 (integración Jira→BIND, 9–12 h) y el Excel de horas por etapa
Los dos pendientes que Erika re-preguntó el 28-jul: la adenda formaliza la
generación automática de cotizaciones desde tickets FAC ($5,400–$7,200 + IVA,
consistente con las ~10 h comunicadas el 21-jul) como solicitud de cambio
conforme a la §10 de la propuesta v1.2; el Excel resume horas por etapa
(E0 cerrada en 21.83 h, E1 en 19.75 h, E2/E3 con rangos contractuales) con
importes para facturación. PDF con el mismo diseño del paquete v1.2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 13:02:07 -06:00

188 lines
8.0 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 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")