import tkinter as tk from tkinter import ttk, messagebox, filedialog from tkinterdnd2 import DND_FILES, TkinterDnD import pandas as pd import openpyxl from openpyxl.styles import PatternFill import os import traceback from datetime import datetime class ExcelCheckerApp(TkinterDnD.Tk): def __init__(self): super().__init__() self.title("Revisador de Excels - AW Hormigón") self.geometry("650x650") self.configure(bg="#1A1C23") self.minsize(550, 600) self.setup_ui() def setup_ui(self): # Contenedor principal con márgenes main_container = tk.Frame(self, bg="#1A1C23") main_container.pack(padx=20, pady=20, fill="both", expand=True) # Cabecera de la Aplicación header_frame = tk.Frame(main_container, bg="#1A1C23") header_frame.pack(fill="x", pady=(0, 15)) accent_bar = tk.Frame(header_frame, bg="#00E676", width=5) accent_bar.pack(side="left", fill="y", padx=(0, 10)) title_label = tk.Label( header_frame, text="REVISADOR DE INFORMES", fg="#FFFFFF", bg="#1A1C23", font=("Segoe UI", 16, "bold") ) title_label.pack(side="left") subtitle_label = tk.Label( header_frame, text="v2.0 • AW Hormigón", fg="#8E9AA8", bg="#1A1C23", font=("Segoe UI", 10, "italic") ) subtitle_label.pack(side="right", anchor="s") # Card de Configuración de Rangos config_frame = tk.Frame(main_container, bg="#232731", bd=0, highlightthickness=1, highlightbackground="#313644") config_frame.pack(fill="x", pady=10, ipady=10) config_title = tk.Label( config_frame, text="Configuración de Parámetros Técnicos", fg="#00E676", bg="#232731", font=("Segoe UI", 11, "bold") ) config_title.grid(row=0, column=0, columnspan=4, padx=15, pady=(12, 12), sticky="w") lbl_font = ("Segoe UI", 9, "bold") entry_font = ("Segoe UI", 10) # Fila 1: Cemento Mín y Máx tk.Label(config_frame, text="Cemento Mín (kg):", fg="#A0A5B5", bg="#232731", font=lbl_font).grid(row=1, column=0, padx=(20, 5), pady=5, sticky="e") self.cem_min_entry = tk.Entry(config_frame, width=10, bg="#2D323F", fg="#FFFFFF", insertbackground="#00E676", bd=0, relief="flat", highlightthickness=1, highlightbackground="#3B4252", highlightcolor="#00E676", font=entry_font, justify="center") self.cem_min_entry.insert(0, "290") self.cem_min_entry.grid(row=1, column=1, padx=5, pady=5) tk.Label(config_frame, text="Cemento Máx (kg):", fg="#A0A5B5", bg="#232731", font=lbl_font).grid(row=1, column=2, padx=15, pady=5, sticky="e") self.cem_max_entry = tk.Entry(config_frame, width=10, bg="#2D323F", fg="#FFFFFF", insertbackground="#00E676", bd=0, relief="flat", highlightthickness=1, highlightbackground="#3B4252", highlightcolor="#00E676", font=entry_font, justify="center") self.cem_max_entry.insert(0, "325") self.cem_max_entry.grid(row=1, column=3, padx=(5, 20), pady=5) # Fila 2: Relación A/C Mín y Máx tk.Label(config_frame, text="Relación A/C Mín:", fg="#A0A5B5", bg="#232731", font=lbl_font).grid(row=2, column=0, padx=(20, 5), pady=5, sticky="e") self.ac_min_entry = tk.Entry(config_frame, width=10, bg="#2D323F", fg="#FFFFFF", insertbackground="#00E676", bd=0, relief="flat", highlightthickness=1, highlightbackground="#3B4252", highlightcolor="#00E676", font=entry_font, justify="center") self.ac_min_entry.insert(0, "0.45") self.ac_min_entry.grid(row=2, column=1, padx=5, pady=5) tk.Label(config_frame, text="Relación A/C Máx:", fg="#A0A5B5", bg="#232731", font=lbl_font).grid(row=2, column=2, padx=15, pady=5, sticky="e") self.ac_max_entry = tk.Entry(config_frame, width=10, bg="#2D323F", fg="#FFFFFF", insertbackground="#00E676", bd=0, relief="flat", highlightthickness=1, highlightbackground="#3B4252", highlightcolor="#00E676", font=entry_font, justify="center") self.ac_max_entry.insert(0, "0.55") self.ac_max_entry.grid(row=2, column=3, padx=(5, 20), pady=5) # Fila 3: Selección de Modo / Destinatario del Reporte self.report_type_var = tk.StringVar(value="estandar") tk.Label(config_frame, text="Modo de Reporte:", fg="#A0A5B5", bg="#232731", font=lbl_font).grid(row=3, column=0, padx=(20, 5), pady=(8, 4), sticky="e") rb_estandar = tk.Radiobutton( config_frame, text="Estándar (Revisado)", variable=self.report_type_var, value="estandar", bg="#232731", fg="#FFFFFF", selectcolor="#2D323F", activebackground="#232731", activeforeground="#00E676", font=("Segoe UI", 9, "bold"), bd=0, padx=5 ) rb_estandar.grid(row=3, column=1, columnspan=2, padx=5, pady=(8, 4), sticky="w") rb_ministerio = tk.Radiobutton( config_frame, text="Revisión Ministerio", variable=self.report_type_var, value="ministerio", bg="#232731", fg="#FFFFFF", selectcolor="#2D323F", activebackground="#232731", activeforeground="#00E676", font=("Segoe UI", 9, "bold"), bd=0, padx=5 ) rb_ministerio.grid(row=3, column=3, padx=5, pady=(8, 4), sticky="w") config_frame.grid_columnconfigure(0, weight=1) config_frame.grid_columnconfigure(1, weight=1) config_frame.grid_columnconfigure(2, weight=1) config_frame.grid_columnconfigure(3, weight=1) # Área de Drag & Drop self.drop_frame = tk.Frame( main_container, bg="#1E2B25", bd=0, highlightthickness=2, highlightbackground="#00E676", cursor="hand2" ) self.drop_frame.pack(padx=0, pady=15, fill="both", expand=True) drop_inner_frame = tk.Frame(self.drop_frame, bg="#1E2B25") drop_inner_frame.pack(expand=True) icon_label = tk.Label( drop_inner_frame, text="🗁", fg="#00E676", bg="#1E2B25", font=("Segoe UI", 36) ) icon_label.pack(pady=(10, 5)) self.drop_label = tk.Label( drop_inner_frame, text="Arrastra tus archivos Excel aquí", fg="#FFFFFF", bg="#1E2B25", font=("Segoe UI", 12, "bold") ) self.drop_label.pack() browse_label = tk.Label( drop_inner_frame, text="o haz clic para explorar los archivos de tu PC", fg="#8E9AA8", bg="#1E2B25", font=("Segoe UI", 9) ) browse_label.pack(pady=(5, 10)) # Eventos Drag & Drop self.drop_frame.drop_target_register(DND_FILES) self.drop_frame.dnd_bind('<>', self.handle_drop) # Vincular clics en toda la zona for w in [self.drop_frame, icon_label, self.drop_label, browse_label, drop_inner_frame]: w.bind("", self.open_file_dialog) # Efecto Hover fluido sin parpadeos def on_enter(e): self.drop_frame.config(bg="#24372E", highlightbackground="#00FF87") icon_label.config(bg="#24372E") self.drop_label.config(bg="#24372E") browse_label.config(bg="#24372E") drop_inner_frame.config(bg="#24372E") def on_leave(e): self.drop_frame.config(bg="#1E2B25", highlightbackground="#00E676") icon_label.config(bg="#1E2B25") self.drop_label.config(bg="#1E2B25") browse_label.config(bg="#1E2B25") drop_inner_frame.config(bg="#1E2B25") for w in [self.drop_frame, icon_label, self.drop_label, browse_label, drop_inner_frame]: w.bind("", on_enter) w.bind("", on_leave) # Consola de Estado (Terminal Linux/macOS) console_frame = tk.Frame(main_container, bg="#1A1C23") console_frame.pack(fill="x", pady=(5, 0)) console_title_frame = tk.Frame(console_frame, bg="#232731", height=24) console_title_frame.pack(fill="x") console_title_frame.pack_propagate(False) dot_frame = tk.Frame(console_title_frame, bg="#232731") dot_frame.pack(side="left", padx=8) for color in ["#FF5F56", "#FFBD2E", "#27C93F"]: lbl = tk.Label(dot_frame, text="●", fg=color, bg="#232731", font=("Arial", 6)) lbl.pack(side="left", padx=1) console_title_lbl = tk.Label( console_title_frame, text="Consola de Estado", fg="#8E9AA8", bg="#232731", font=("Segoe UI", 8, "bold") ) console_title_lbl.pack(side="left", padx=5) self.log_text = tk.Text( console_frame, height=8, bg="#14161D", fg="#A0D8A5", insertbackground="#FFFFFF", font=("Consolas", 9), bd=0, padx=12, pady=10, state="disabled", highlightthickness=1, highlightbackground="#232731" ) self.log_text.pack(fill="x") def log(self, message): self.log_text.config(state="normal") self.log_text.insert(tk.END, message + "\n") self.log_text.see(tk.END) self.log_text.config(state="disabled") def open_file_dialog(self, event=None): files = filedialog.askopenfilenames( title="Seleccionar archivos Excel", filetypes=(("Archivos Excel", "*.xlsx"), ("Todos los archivos", "*.*")) ) for f in files: self.process_file(f) def handle_drop(self, event): # tkinterdnd2 devuelve la ruta de los archivos en event.data files = self.split_dnd_data(event.data) for f in files: self.process_file(f) def split_dnd_data(self, data): # Utiliza el método nativo de tk para separar las listas de archivos correctamente (incluso con espacios y llaves) return self.tk.splitlist(data) def process_file(self, file_path): tipo_reporte = self.report_type_var.get() self.log(f"Procesando: {os.path.basename(file_path)} [Modo: {tipo_reporte.upper()}]") try: # Obtener limites cem_min = float(self.cem_min_entry.get()) cem_max = float(self.cem_max_entry.get()) ac_min = float(self.ac_min_entry.get()) ac_max = float(self.ac_max_entry.get()) self.analyze_excel(file_path, cem_min, cem_max, ac_min, ac_max, tipo_reporte) if tipo_reporte == "ministerio": self.log("Notas de control del ministerio añadidas (Callout Box).") self.log(f"¡Éxito! Archivo generado correctamente.\n") except Exception as e: self.log(f"ERROR: {str(e)}") traceback.print_exc() def analyze_excel(self, file_path, cem_min, cem_max, ac_min, ac_max, tipo_reporte): # 1. Leer los datos raw para sacar constantes df_raw = pd.read_excel(file_path, header=None) # En la fila 3 (índice 3), col 4 (E) está Sv/Sa <= 1.4 value val_sv = float(df_raw.iloc[3, 4]) # En la fila 4 (índice 4), col 2 (C) está d val_d = float(df_raw.iloc[4, 2]) # En la fila 4 (índice 4), col 4 (E) está CV10 val_cv10 = float(df_raw.iloc[4, 4]) # En la fila 4 (índice 4), col 6 (G) está fck (ej. 25) val_fck = float(df_raw.iloc[4, 6]) # 2. Leer la tabla de datos a partir de la fila 8 (índice 8) df = pd.read_excel(file_path, skiprows=8, header=0) # 3. Preparar openpyxl para modificar y colorear wb = openpyxl.load_workbook(file_path) ws = wb.active red_fill = PatternFill(start_color="FF9999", end_color="FF9999", fill_type="solid") yellow_fill = PatternFill(start_color="FFFF99", end_color="FFFF99", fill_type="solid") green_fill = PatternFill(start_color="99FF99", end_color="99FF99", fill_type="solid") # Funciones auxiliares para formatear según el modo (Estándar vs. Ministerio) def aplicar_negrita(cell): font = cell.font if font: cell.font = openpyxl.styles.Font( name=font.name, size=font.size, bold=True, italic=font.italic, color=font.color, underline=font.underline ) def marcar_error(r, c): if tipo_reporte == "estandar": ws.cell(row=r, column=c).fill = red_fill else: # ministerio (se quita el fondo de color, se pone negrita) aplicar_negrita(ws.cell(row=r, column=c)) def marcar_advertencia(r, c): if tipo_reporte == "estandar": ws.cell(row=r, column=c).fill = yellow_fill else: # ministerio (se quita el fondo de color, se pone negrita) aplicar_negrita(ws.cell(row=r, column=c)) # Diccionario para meses meses_autocontrol = {} # { 'YYYY-MM': count } meses_verificacion = {} # { 'YYYY-MM': count } seen_verificaciones = {} # { 'YYYY-MM-DD': first_seen_excel_row } meses_verificacion_dias = {} # { 'YYYY-MM': set(dias) } # Flags y contadores para comentarios dinámicos del ministerio has_cemento_error = False has_ac_error = False cemento_error_count = 0 ac_error_count = 0 has_resistencia_error = False has_estadistica_error = False has_mismo_dia_error = False has_insuficiencia_error = False has_baja_produccion = False has_duplicado_verificacion = False # Pre-procesar todas las fechas de autocontrol para la regla 2 fechas_autocontrol = set() for fa in df.iloc[:, 1]: if pd.notna(fa): if isinstance(fa, str): try: fa = pd.to_datetime(fa, format="%d/%m/%Y") except: fa = pd.to_datetime(fa, errors='coerce') if pd.notna(fa): fechas_autocontrol.add(fa.strftime("%Y-%m-%d")) # Seguimiento de reglas (True = Cumple, False = No cumple) reglas_status = { 1: True, # Mínimo 5 autocontroles 2: True, # Autoevaluación el mismo día que verificación 3: True, # Rango cemento 4: True, # Rango agua/cemento 5: True, # Valor y Xn > fck 6: True, # CV < 0.122 7: True, # Al menos 1 verificación al mes 8: True, # Sv/Sn < 1.4 9: True, # d >= 0 10: True # CV10 <= 0.13 } errores_globales = [] # Recorremos todas las filas de la tabla de datos (empiezan en la fila 10 de Excel) for i in range(len(df)): excel_row = i + 10 # Porque skiprows=8 y header es 1 fila = 9 filas saltadas. Data empieza en fila 10. row = df.iloc[i] # --- ZONA AUTOCONTROL (Columnas A-H) --- fecha_auto = row.iloc[1] if pd.notna(fecha_auto): # Formatear fecha if isinstance(fecha_auto, str): try: fecha_auto = pd.to_datetime(fecha_auto, format="%d/%m/%Y") except: fecha_auto = pd.to_datetime(fecha_auto, errors='coerce') if pd.notna(fecha_auto): fechas_autocontrol.add(fecha_auto.strftime("%Y-%m-%d")) mes_str = fecha_auto.strftime("%Y-%m") meses_autocontrol[mes_str] = meses_autocontrol.get(mes_str, 0) + 1 # Cemento (Columna C Excel -> col=3) cemento = row.iloc[2] if pd.notna(cemento) and not (cem_min <= cemento <= cem_max): marcar_error(excel_row, 3) reglas_status[3] = False has_cemento_error = True cemento_error_count += 1 # A/C (Columna D Excel -> col=4) ac = row.iloc[3] if pd.notna(ac) and not (ac_min <= ac <= ac_max): marcar_error(excel_row, 4) reglas_status[4] = False has_ac_error = True ac_error_count += 1 # Valor (Columna F Excel -> col=6) valor = row.iloc[5] if pd.notna(valor) and valor <= val_fck: marcar_error(excel_row, 6) reglas_status[5] = False has_resistencia_error = True # Xn-Kn*Sn (Columna G Excel -> col=7) xn = row.iloc[6] if pd.notna(xn) and xn <= val_fck: marcar_error(excel_row, 7) reglas_status[5] = False has_resistencia_error = True # CV (Columna H Excel -> col=8) cv = row.iloc[7] if pd.notna(cv) and cv >= 0.122: marcar_error(excel_row, 8) reglas_status[6] = False has_estadistica_error = True # --- ZONA VERIFICACION (Columnas J-N) --- fecha_veri = row.iloc[10] if pd.notna(fecha_veri): if isinstance(fecha_veri, str): try: fecha_veri = pd.to_datetime(fecha_veri, format="%d/%m/%Y") except: fecha_veri = pd.to_datetime(fecha_veri, errors='coerce') if pd.notna(fecha_veri): fecha_veri_str = fecha_veri.strftime("%Y-%m-%d") mes_str = fecha_veri.strftime("%Y-%m") # Verificar si la fecha de verificación ya se ha visto en el reporte if fecha_veri_str in seen_verificaciones: first_row = seen_verificaciones[fecha_veri_str] # Es advertencia y no una no conformidad directa marcar_advertencia(excel_row, 11) marcar_advertencia(first_row, 11) errores_globales.append(f"Advertencia: Hay más de una verificación el mismo día ({fecha_veri_str} en filas {first_row} y {excel_row}). Las verificaciones deben realizarse en días diferentes, por lo que la toma duplicada no se computará.") es_duplicada = True has_duplicado_verificacion = True else: seen_verificaciones[fecha_veri_str] = excel_row es_duplicada = False if not es_duplicada: # Registrar día único para el conteo de verificaciones del mes if mes_str not in meses_verificacion_dias: meses_verificacion_dias[mes_str] = set() meses_verificacion_dias[mes_str].add(fecha_veri_str) # Regla 2: ¿Hay autocontrol el mismo día? if fecha_veri_str not in fechas_autocontrol: marcar_error(excel_row, 11) reglas_status[2] = False has_mismo_dia_error = True # Cemento Verificacion (Columna L Excel -> 12) cemento_v = row.iloc[11] if pd.notna(cemento_v) and not (cem_min <= cemento_v <= cem_max): marcar_error(excel_row, 12) reglas_status[3] = False has_cemento_error = True cemento_error_count += 1 # A/C Verificacion (Columna M Excel -> 13) ac_v = row.iloc[12] if pd.notna(ac_v) and not (ac_min <= ac_v <= ac_max): marcar_error(excel_row, 13) reglas_status[4] = False has_ac_error = True ac_error_count += 1 # Valor Verificacion (Columna N Excel -> 14) valor_v = row.iloc[13] if pd.notna(valor_v) and valor_v <= val_fck: marcar_error(excel_row, 14) reglas_status[5] = False has_resistencia_error = True # Rellenar meses_verificacion con la cantidad de días únicos for mes_str, dias_set in meses_verificacion_dias.items(): meses_verificacion[mes_str] = len(dias_set) # 8. Condición Sv/Sn if val_sv >= 1.4: reglas_status[8] = False errores_globales.append(f"Sv/Sa ≤ 1,4 falló: el valor es {val_sv}") marcar_error(4, 5) # Pinta celda E4 has_estadistica_error = True # 9. Condición de d if val_d < 0: reglas_status[9] = False errores_globales.append(f"Valor 'd' es negativo: {val_d}") marcar_error(5, 3) # Pinta celda C5 # 10. Condición CV10 if val_cv10 > 0.13: reglas_status[10] = False errores_globales.append(f"CV10 superó 0.13: {val_cv10}") marcar_error(5, 5) # Pinta celda E5 has_estadistica_error = True # --- DETERMINACIÓN DEL RÉGIMEN DE PRODUCCIÓN POR MES (Baja Producción vs. Continua) --- # Los meses extremos (inicio o fin del reporte) no se consideran Baja Producción # ya que sus bajos números suelen deberse a meses incompletos (cortados a mitad de mes). todos_meses = sorted(list(set(list(meses_autocontrol.keys()) + list(meses_verificacion.keys())))) regimen_mes = {} for idx, mes in enumerate(todos_meses): es_extremo = (idx == 0 or idx == len(todos_meses) - 1) if es_extremo: regimen_mes[mes] = "Continua" continue # Condición 1: Exactamente 1 verificación if meses_verificacion.get(mes, 0) == 1: regimen_mes[mes] = "Baja" continue # Condición 2: Suma de autocontroles con meses adyacentes < 15 sum_atras_ok = False sum_adelante_ok = False # Verificar con el mes anterior if idx > 0: mes_ant = todos_meses[idx - 1] sum_atras = meses_autocontrol.get(mes, 0) + meses_autocontrol.get(mes_ant, 0) if sum_atras >= 15: sum_atras_ok = True # Verificar con el mes siguiente if idx < len(todos_meses) - 1: mes_sig = todos_meses[idx + 1] sum_adelante = meses_autocontrol.get(mes, 0) + meses_autocontrol.get(mes_sig, 0) if sum_adelante >= 15: sum_adelante_ok = True # Determinar si el mes se salva if sum_atras_ok or sum_adelante_ok: regimen_mes[mes] = "Continua" else: regimen_mes[mes] = "Baja" # Reglas Mensuales (1 y 7) for idx, mes in enumerate(todos_meses): es_extremo = (idx == 0 or idx == len(todos_meses) - 1) regimen = regimen_mes[mes] mes_has_error = False # 1. 5 autocontroles count_auto = meses_autocontrol.get(mes, 0) if count_auto < 5: if es_extremo: errores_globales.append(f"Mes {mes} tiene {count_auto} autocontroles (<5). Advertencia (es mes extremo).") elif regimen == "Baja": # En baja producción se exige al menos 10 tomas en 2 meses (45 días hábiles) # Verificamos si cumple en al menos una de las ventanas de 2 meses adyacentes has_prev = (idx > 0) has_next = (idx < len(todos_meses) - 1) saved = False if has_prev: mes_ant = todos_meses[idx - 1] if meses_autocontrol.get(mes, 0) + meses_autocontrol.get(mes_ant, 0) >= 10: saved = True if has_next: mes_sig = todos_meses[idx + 1] if meses_autocontrol.get(mes, 0) + meses_autocontrol.get(mes_sig, 0) >= 10: saved = True if not has_prev and not has_next: # Sólo hay 1 mes en total saved = (count_auto >= 10) if saved: errores_globales.append(f"Mes {mes} tiene {count_auto} autocontroles (<5). Advertencia (es baja producción).") else: reglas_status[1] = False errores_globales.append(f"Mes {mes} tiene {count_auto} autocontroles (<5). ERROR (no alcanza el mínimo acumulado de 10 tomas en 2 meses en baja producción).") has_insuficiencia_error = True mes_has_error = True else: reglas_status[1] = False errores_globales.append(f"Mes {mes} tiene {count_auto} autocontroles (<5). ERROR.") has_insuficiencia_error = True mes_has_error = True # 7. Verificación mensual (2 en continua, 1 en baja) count_veri = meses_verificacion.get(mes, 0) target_veri = 2 if regimen == "Continua" else 1 if count_veri < target_veri: if es_extremo: errores_globales.append(f"Mes extremo {mes} tiene {count_veri} verificaciones (< {target_veri} requeridas). Advertencia (es mes extremo).") else: reglas_status[7] = False errores_globales.append(f"Mes {mes} tiene {count_veri} verificaciones (< {target_veri} requeridas en régimen {regimen.lower()}). ERROR.") has_insuficiencia_error = True mes_has_error = True # Si el mes es de baja producción if regimen == "Baja": errores_globales.append(f"Mes {mes} está en situación de baja producción. Advertencia: se debe justificar documentalmente la baja producción.") if not mes_has_error: has_baja_produccion = True if tipo_reporte == "estandar": # Añadir CHECKLIST de Reglas al final last_row = ws.max_row + 3 ws.cell(row=last_row, column=1, value="CHECKLIST DE CONDICIONES").font = openpyxl.styles.Font(bold=True, size=12) nombres_reglas = { 1: "1. Mínimo 5 autocontroles al mes", 2: "2. Autocontrol el mismo día que la verificación", 3: f"3. Contenido de cemento dentro del rango [{cem_min} - {cem_max}]", 4: f"4. Relación Agua/Cemento dentro del rango [{ac_min} - {ac_max}]", 5: "5. 'Valor' y 'Xn-Kn*Sn' > fck estipulado", 6: "6. CV < 0,122 en todos los ensayos", 7: "7. Frecuencia de verificación al mes (2 en Continua / 1 en Baja Producción)", 8: "8. Sv/Sa ≤ 1,4", 9: "9. Valor 'd' >= 0", 10: "10. CV10 <= 0,13" } for r_num in range(1, 11): last_row += 1 if reglas_status[r_num]: status_text = "✔ CUMPLE" ws.cell(row=last_row, column=2, value=status_text).font = openpyxl.styles.Font(color="008000", bold=True) # Verde else: status_text = "❌ NO CUMPLE" ws.cell(row=last_row, column=2, value=status_text).font = openpyxl.styles.Font(color="FF0000", bold=True) # Rojo ws.cell(row=last_row, column=1, value=nombres_reglas[r_num]) # Añadir resumen de errores detallados al final if errores_globales: last_row += 2 ws.cell(row=last_row, column=1, value="DETALLE DE ERRORES MENSUALES/GLOBALES").font = openpyxl.styles.Font(bold=True) for e in errores_globales: last_row += 1 ws.cell(row=last_row, column=1, value=e).fill = yellow_fill if "Advertencia" in e else red_fill else: # ministerio # Recopilar las frases correspondientes dinámicamente según los errores detectados lineas_comentario = [ " NOTAS ADICIONALES DE CONTROL (ALL WORLD CERTIFICACIÓN)" ] total_cem_ac_errors = cemento_error_count + ac_error_count if total_cem_ac_errors >= 4: if cemento_error_count > 0: lineas_comentario.append(" - Se constata la existencia de contenidos de cemento fuera del rango establecido de la agrupación, procediéndose a la solicitud de la correspondiente no conformidad.") if ac_error_count > 0: lineas_comentario.append(" - Se observa que las relaciones agua/cemento no cumplen con los límites establecidos en las dosificaciones, procediéndose a la solicitud de la correspondiente no conformidad.") elif 1 <= total_cem_ac_errors <= 3: if cemento_error_count > 0 and ac_error_count > 0: lineas_comentario.append(" - Se observan desviaciones en la relación agua/cemento y el contenido mínimo de cemento, comunicándose a la empresa para que tome las acciones pertinentes.") elif cemento_error_count > 0: lineas_comentario.append(" - Se observan desviaciones en el contenido mínimo de cemento, comunicándose a la empresa para que tome las acciones pertinentes.") elif ac_error_count > 0: lineas_comentario.append(" - Se observan desviaciones en la relación agua/cemento, comunicándose a la empresa para que tome las acciones pertinentes.") if has_resistencia_error: lineas_comentario.append(" - Se detecta que no se han alcanzado los valores de resistencia característica establecidos, procediéndose a la solicitud de la correspondiente no conformidad.") if has_estadistica_error: lineas_comentario.append(" - Se constata que los resultados del lote evaluado no cumplen con los criterios estadísticos exigidos, procediéndose a la solicitud de la correspondiente no conformidad.") if has_mismo_dia_error: lineas_comentario.append(" - Se detecta la ausencia de ensayos de autocontrol correspondientes al mismo día de la toma de verificación, procediéndose a la solicitud de aclaraciones por parte de la empresa.") if has_duplicado_verificacion: lineas_comentario.append(" - Se advierte la existencia de 2 tomas de verificación en el mismo día, se le comunica a la empresa y laboratorio para que no se repita esta circunstancia.") if has_insuficiencia_error: lineas_comentario.append(" - Se constata la falta de ensayos mínimos exigibles (de autocontrol y/o verificación) para el correcto control del periodo evaluado, procediéndose a la solicitud de la correspondiente no conformidad.") if has_baja_produccion: lineas_comentario.append(" - Se identifica que la central se encuentra en régimen de baja producción durante el periodo evaluado, habiéndose solicitado la justificación documental correspondiente.") # Dibujar el cuadro solo si se ha añadido al menos un comentario específico if len(lineas_comentario) > 1: last_row = ws.max_row + 3 start_r = last_row end_r = last_row + len(lineas_comentario) # Incluye 1 fila adicional de padding al final # Estilos visuales box_fill = PatternFill(start_color="E8F0FE", end_color="E8F0FE", fill_type="solid") title_font = openpyxl.styles.Font(name="Segoe UI", size=12, bold=True, color="1A73E8") text_font = openpyxl.styles.Font(name="Segoe UI", size=11, italic=True, color="3C4043") thin_border_side = openpyxl.styles.Side(border_style="thin", color="1A73E8") thick_left_side = openpyxl.styles.Side(border_style="medium", color="1A73E8") # Aplicar relleno, bordes y altura de fila celda por celda para el rango A-N (columnas 1-14) for r in range(start_r, end_r + 1): # Ajustar la altura de la fila para dar espacio a textos envueltos ws.row_dimensions[r].height = 26 if r == start_r else 22 for c in range(1, 15): cell = ws.cell(row=r, column=c) cell.fill = box_fill # Configurar bordes de la caja b_top = thin_border_side if r == start_r else None b_bottom = thin_border_side if r == end_r else None b_left = thick_left_side if c == 1 else None b_right = thin_border_side if c == 14 else None cell.border = openpyxl.styles.Border(top=b_top, bottom=b_bottom, left=b_left, right=b_right) # Rellenar textos, fuentes y alineación for idx, texto in enumerate(lineas_comentario): curr_row = start_r + idx cell = ws.cell(row=curr_row, column=1, value=texto) cell.alignment = openpyxl.styles.Alignment(wrap_text=True, vertical="center") if idx == 0: cell.font = title_font else: cell.font = text_font # Combinar columnas A-N de cada fila de la caja (incluyendo la de padding) para visualización limpia for idx in range(len(lineas_comentario) + 1): ws.merge_cells(start_row=start_r + idx, start_column=1, end_row=start_r + idx, end_column=14) # Guardar con sufijos diferentes para evitar sobrescribir sufijo = "_REVISADO.xlsx" if tipo_reporte == "estandar" else "_MINISTERIO.xlsx" output_path = file_path.replace(".xlsx", sufijo) wb.save(output_path) if __name__ == "__main__": app = ExcelCheckerApp() app.mainloop()