"""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()