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>
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user