Files
balam/planeacion/ordenar_fechas_avance.py
JohannVelazquez e057fb9127 Etapa 1: plan de ejecución, corte de avance del 16-jul y fuentes
- Plan-Etapa1.md: plan detallado por fases/bloques (B0-B9) con horas,
  decisiones de arquitectura y lo bloqueado hasta el 22-jul
- Avance-Etapa1-2026-07-16.md + Excel de avance ordenado por fechas
  (con script generador y de ordenamiento)
- WhatsApp 16-jul: invitación al Git recibida, procedimientos de
  clientes enviados por correo, Erika pide avance de Etapa 1
- Actualiza PENDIENTES y REGISTRO
2026-07-19 20:05:01 -06:00

114 lines
3.8 KiB
Python

"""Ordena las actividades del Excel de avance por fecha de Comienzo, dentro de
cada etapa, conservando estilos, formatos, fórmulas de resumen y encabezados.
Estructura del plan:
fila 2 = Integración (resumen maestro) -> fija
fila 3 = Etapa 0 (encabezado) -> fija
filas 4-10 = tareas de Etapa 0 -> se ordenan por Comienzo
fila 11 = Etapa 1 (encabezado) -> fija
filas 12-24 = tareas de Etapa 1 -> se ordenan por Comienzo
Las celdas de fecha son texto ("mié 01/07/26"), así que se parsea el dd/mm/yy
para ordenar cronológicamente (no alfabéticamente, que ordenaría por día de
semana). En empate de Comienzo, se desempata por Fin y luego por el orden
original (estable), para no barajar tareas que arrancan el mismo día.
"""
import re
from copy import copy
from pathlib import Path
import openpyxl
DIRECTORY = Path(__file__).parent
TARGET = DIRECTORY / "Plan-actividades-avance-2026-07-16.xlsx"
# Grupos de filas de tareas a ordenar (rango inclusivo). Los encabezados
# (2, 3, 11) quedan fuera y no se mueven.
GROUPS = [(4, 10), (12, 24)]
DATE_RE = re.compile(r"(\d{2})/(\d{2})/(\d{2})")
def sort_key_from_cell(value):
"""Devuelve (aaaa, mm, dd) desde 'mié 01/07/26'; None si no hay fecha."""
if value is None:
return (9999, 99, 99)
m = DATE_RE.search(str(value))
if not m:
return (9999, 99, 99)
dd, mm, yy = (int(g) for g in m.groups())
return (2000 + yy, mm, dd)
def snapshot_row(sheet, row, max_col):
"""Captura valor + estilo + formato de cada celda de la fila, y el alto."""
cells = []
for col in range(1, max_col + 1):
c = sheet.cell(row, col)
cells.append(
{
"value": c.value,
"style": copy(c._style),
"number_format": c.number_format,
}
)
height = sheet.row_dimensions[row].height
return {"cells": cells, "height": height}
def restore_row(sheet, row, snap, max_col):
for col in range(1, max_col + 1):
c = sheet.cell(row, col)
data = snap["cells"][col - 1]
c.value = data["value"]
c._style = copy(data["style"])
c.number_format = data["number_format"]
if snap["height"] is not None:
sheet.row_dimensions[row].height = snap["height"]
def main():
wb = openpyxl.load_workbook(TARGET)
sh = wb["Plan"]
max_col = sh.max_column
for start, end in GROUPS:
snaps = [snapshot_row(sh, r, max_col) for r in range(start, end + 1)]
# Orden estable: por (Comienzo, Fin) usando el índice original como
# tercer criterio implícito de estabilidad de sorted().
order = sorted(
range(len(snaps)),
key=lambda i: (
sort_key_from_cell(snaps[i]["cells"][2]["value"]), # col C Comienzo
sort_key_from_cell(snaps[i]["cells"][3]["value"]), # col D Fin
),
)
reordered = [snaps[i] for i in order]
for offset, snap in enumerate(reordered):
restore_row(sh, start + offset, snap, max_col)
tmp = TARGET.with_suffix(".tmp.xlsx")
wb.save(tmp)
# Verificación: cada grupo queda no-decreciente por Comienzo.
check = openpyxl.load_workbook(tmp)["Plan"]
for start, end in GROUPS:
prev = (0, 0, 0)
for r in range(start, end + 1):
k = sort_key_from_cell(check.cell(r, 3).value)
assert k >= prev, f"Fila {r} fuera de orden: {k} < {prev}"
prev = k
# Los encabezados de etapa siguen en su lugar.
assert check.cell(3, 1).value == "Etapa 0"
assert check.cell(11, 1).value == "Etapa 1"
# Las fórmulas de resumen se conservan.
assert str(check.cell(11, 8).value).startswith("=SUM")
tmp.replace(TARGET)
print("Ordenado correctamente:", TARGET.name)
if __name__ == "__main__":
main()