"""Pestaña Procesar Documentos de la aplicación GUI.""" import customtkinter as ctk from gui.styles import AppStyles class MainProcessingTab: """Encapsula la UI de la pestaña Procesar Documentos.""" def __init__(self, parent_tab): """Inicializa la pestaña de Procesar Documentos. Args: parent_tab: El CTkFrame que representa la pestaña. """ self.tab = parent_tab self.fonts = AppStyles.get_fonts() self.colors = AppStyles.get_colors() self.spacing = AppStyles.get_spacing() # Variables internas self._doc_type_rows = [] self._setup_layout() self._create_paths_section() self._create_config_section() self._create_process_button() self._create_output_section() # Conectar el trace después de que todos los widgets existen self.mode_var.trace_add("write", lambda *_: self.update_ui_for_mode()) def _setup_layout(self): """Configura el layout base de la pestaña.""" self.tab.grid_columnconfigure(0, weight=1) self.tab.grid_rowconfigure(0, weight=1) self.scroll_container = ctk.CTkScrollableFrame(self.tab, fg_color="transparent") self.scroll_container.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) self.scroll_container.grid_columnconfigure(0, weight=1) def _create_paths_section(self): """Crea la sección de rutas y archivos.""" self.frame_paths = ctk.CTkFrame(self.scroll_container) self.frame_paths.grid(row=0, column=0, sticky="ew", padx=10, pady=(5, 2)) self.frame_paths.grid_columnconfigure(1, weight=1) # Título lbl_paths = ctk.CTkLabel( self.frame_paths, text="Rutas y Archivos", font=self.fonts["section"] ) lbl_paths.grid(row=0, column=0, columnspan=3, sticky="w", padx=15, pady=(5, 2)) # Carpeta Raíz self.root_label = ctk.CTkLabel(self.frame_paths, text="Carpeta Raíz:", font=self.fonts["ui"]) self.root_label.grid(row=1, column=0, padx=15, pady=2, sticky="w") self.root_entry = ctk.CTkEntry( self.frame_paths, placeholder_text="Selecciona la carpeta con los documentos...", font=self.fonts["ui"] ) self.root_entry.grid(row=1, column=1, padx=5, pady=2, sticky="ew") self.root_button = ctk.CTkButton(self.frame_paths, text="Explorar...", font=self.fonts["ui"], width=100) self.root_button.grid(row=1, column=2, padx=15, pady=2) # CSV Salida self.csv_label = ctk.CTkLabel(self.frame_paths, text="Archivo CSV:", font=self.fonts["ui"]) self.csv_label.grid(row=2, column=0, padx=15, pady=2, sticky="w") self.csv_entry = ctk.CTkEntry( self.frame_paths, placeholder_text="Ruta para guardar el reporte CSV...", font=self.fonts["ui"] ) self.csv_entry.grid(row=2, column=1, padx=5, pady=2, sticky="ew") self.csv_button = ctk.CTkButton( self.frame_paths, text="Guardar en...", font=self.fonts["ui"], width=100, fg_color="transparent", border_width=1 ) self.csv_button.grid(row=2, column=2, padx=15, pady=2) # Selector de Modo (Radicados vs General) self.mode_var = ctk.StringVar(value="radicados") lbl_mode = ctk.CTkLabel(self.frame_paths, text="Estructura:", font=self.fonts["ui"]) lbl_mode.grid(row=3, column=0, padx=15, pady=2, sticky="w") self.frame_radios_main = ctk.CTkFrame(self.frame_paths, fg_color="transparent") self.frame_radios_main.grid(row=3, column=1, columnspan=2, sticky="w", padx=5, pady=2) self.radio_radicados_main = ctk.CTkRadioButton( self.frame_radios_main, text="Por Radicados (Subcarpetas)", variable=self.mode_var, value="radicados", font=self.fonts["ui"] ) self.radio_radicados_main.pack(side="left", padx=(0, 20)) self.radio_general_main = ctk.CTkRadioButton( self.frame_radios_main, text="Carpeta General (Archivos Planos)", variable=self.mode_var, value="general", font=self.fonts["ui"] ) self.radio_general_main.pack(side="left") def _create_config_section(self): """Crea la sección de configuración de tipos de documento y TIFF.""" self.frame_config = ctk.CTkFrame(self.scroll_container) self.frame_config.grid(row=1, column=0, sticky="ew", padx=10, pady=2) self.frame_config.grid_columnconfigure(0, weight=1) # Keywords oculto (legacy) self.kw_label = ctk.CTkLabel(self.frame_config, text="Keywords:") self.kw_entry = ctk.CTkEntry(self.frame_config) self.kw_entry.insert(0, "Factura_O_Documento_Equivalente,factura") # Tipos de Documento self.doc_types_container = ctk.CTkFrame(self.frame_config, fg_color="transparent") self.doc_types_container.grid(row=0, column=0, sticky="nsew", padx=10, pady=(5, 2)) self.doc_types_container.grid_columnconfigure(0, weight=1) self.doc_types_header = ctk.CTkFrame(self.doc_types_container, fg_color="transparent") self.doc_types_header.grid(row=0, column=0, sticky="ew", pady=(0, 2)) self.doc_types_header.grid_columnconfigure(0, weight=1) self.doc_types_label = ctk.CTkLabel( self.doc_types_header, text="Definición de Tipos de Documento", font=self.fonts["section"] ) self.doc_types_label.grid(row=0, column=0, sticky="w") self.add_doc_type_btn = ctk.CTkButton( self.doc_types_header, text="+ Añadir Tipo", font=self.fonts["ui"], width=120, height=28 ) self.add_doc_type_btn.grid(row=0, column=1, sticky="e") self.add_doc_type_btn.configure(command=self._add_doc_type_row) # Lista de Tipos self.doc_types_list = ctk.CTkFrame(self.doc_types_container, fg_color="transparent") self.doc_types_list.grid(row=1, column=0, sticky="ew", pady=(2, 0)) self.doc_types_list.grid_columnconfigure(0, weight=1) # Fila por defecto self._add_doc_type_row(name="Factura", keyword="Factura_O_Documento_Equivalente") # Configuración Avanzada (TIFF Range) self.frame_advanced = ctk.CTkFrame(self.frame_config, fg_color="transparent") self.frame_advanced.grid(row=1, column=0, sticky="ew", padx=10, pady=(0, 2)) self.tiff_start_label = ctk.CTkLabel( self.frame_advanced, text="Pág. Inicio TIFF:", font=self.fonts["ui"] ) self.tiff_start_entry = ctk.CTkEntry(self.frame_advanced, width=60) self.tiff_start_entry.insert(0, "0") self.tiff_end_label = ctk.CTkLabel( self.frame_advanced, text="Pág. Fin TIFF:", font=self.fonts["ui"] ) self.tiff_end_entry = ctk.CTkEntry(self.frame_advanced, width=60) self.tiff_end_entry.insert(0, "0") # Ocultar inicialmente self.tiff_start_label.grid_forget() self.tiff_start_entry.grid_forget() self.tiff_end_label.grid_forget() self.tiff_end_entry.grid_forget() # Campos legacy ocultos self.fields_entry = ctk.CTkEntry(self.tab) self.fields_label = ctk.CTkLabel(self.tab, text="") def _create_process_button(self): """Crea el botón de acción principal.""" self.process_button = ctk.CTkButton( self.scroll_container, text="INICIAR PROCESAMIENTO", font=self.fonts["bold_large"], height=35, fg_color=self.colors["primary_button"], hover_color=self.colors["primary_hover"] ) self.process_button.grid(row=2, column=0, padx=15, pady=(10, 15), sticky="ew") def _create_output_section(self): """Crea la sección de logs/salida.""" self.log_frame = ctk.CTkFrame(self.scroll_container) self.log_frame.grid(row=3, column=0, sticky="ew", padx=10, pady=(0, 15)) self.log_frame.grid_columnconfigure(0, weight=1) lbl_logs = ctk.CTkLabel( self.log_frame, text="Registro de Actividad:", font=self.fonts["ui"], text_color=self.colors["text_muted"] ) lbl_logs.grid(row=0, column=0, sticky="w", padx=10, pady=(5, 0)) self.output_text = ctk.CTkTextbox( self.log_frame, font=self.fonts["mono"], activate_scrollbars=True, height=150 ) self.output_text.grid(row=1, column=0, padx=5, pady=5, sticky="ew") def _add_doc_type_row(self, name: str = "", keyword: str = "") -> None: """Añade una fila de tipo de documento.""" row = ctk.CTkFrame(self.doc_types_list, fg_color="transparent") row.pack(fill="x", pady=2) grid_inner = ctk.CTkFrame(row, fg_color="transparent") grid_inner.pack(fill="x", padx=5, pady=2) grid_inner.grid_columnconfigure(0, weight=1) grid_inner.grid_columnconfigure(1, weight=1) name_entry = ctk.CTkEntry( grid_inner, placeholder_text="Nombre Lógico", height=28, font=self.fonts["ui"] ) if name: name_entry.insert(0, name) name_entry.grid(row=0, column=0, padx=(0, 5), sticky="ew") kw_entry = ctk.CTkEntry( grid_inner, placeholder_text="Patrón / Keyword", height=28, font=self.fonts["ui"] ) if keyword: kw_entry.insert(0, keyword) kw_entry.grid(row=0, column=1, padx=(0, 5), sticky="ew") def _remove(): # Buscar y eliminar de la lista interna for i, r_data in enumerate(self._doc_type_rows): if r_data["frame"] == row: self._doc_type_rows.pop(i) break row.destroy() remove_btn = ctk.CTkButton( grid_inner, text="×", width=30, height=28, fg_color=self.colors["danger"], hover_color=self.colors["danger_hover"], command=_remove ) remove_btn.grid(row=0, column=2, sticky="e") self._doc_type_rows.append({ "frame": row, "name_entry": name_entry, "kw_entry": kw_entry, "remove_btn": remove_btn }) # --- API Pública --- def set_on_select_root(self, callback): """Vincula callback al botón de selección de carpeta raíz.""" self.root_button.configure(command=callback) def set_on_select_csv(self, callback): """Vincula callback al botón de selección de CSV.""" self.csv_button.configure(command=callback) def set_on_process(self, callback): """Vincula callback al botón de procesar.""" self.process_button.configure(command=callback) def set_on_mode_change(self, callback): """Vincula callback al cambio de modo.""" self.mode_var.trace("w", lambda *args: callback()) def get_root_path(self) -> str: """Devuelve la ruta raíz seleccionada.""" return self.root_entry.get().strip() def set_root_path(self, path: str) -> None: """Establece la ruta raíz.""" self.root_entry.configure(state="normal") self.root_entry.delete(0, "end") self.root_entry.insert(0, path) def get_csv_path(self) -> str: """Devuelve la ruta del CSV.""" return self.csv_entry.get().strip() def set_csv_path(self, path: str) -> None: """Establece la ruta del CSV.""" self.csv_entry.configure(state="normal") self.csv_entry.delete(0, "end") self.csv_entry.insert(0, path) def get_process_mode(self) -> str: """Devuelve el modo de procesamiento: 'radicados' o 'general'.""" try: v = (self.mode_var.get() or "").strip().lower() return v if v in ("radicados", "general") else "radicados" except Exception: return "radicados" def get_keywords(self) -> str: """Devuelve los keywords.""" return self.kw_entry.get().strip() def get_fields(self) -> str: """Devuelve los fields (legacy).""" return self.fields_entry.get().strip() def get_tiff_start_page(self) -> int: """Devuelve la página de inicio TIFF.""" try: value = self.tiff_start_entry.get().strip() return int(value) if value else 0 except Exception: return 0 def get_tiff_end_page(self) -> int: """Devuelve la página final TIFF.""" try: value = self.tiff_end_entry.get().strip() return int(value) if value else 0 except Exception: return 0 def show_tiff_range_controls(self, show: bool) -> None: """Muestra u oculta los controles de rango TIFF.""" if show: self.tiff_start_label.grid(row=0, column=0, padx=5, pady=5) self.tiff_start_entry.grid(row=0, column=1, padx=5, pady=5) self.tiff_end_label.grid(row=0, column=2, padx=5, pady=5) self.tiff_end_entry.grid(row=0, column=3, padx=5, pady=5) else: self.tiff_start_label.grid_forget() self.tiff_start_entry.grid_forget() self.tiff_end_label.grid_forget() self.tiff_end_entry.grid_forget() def get_doc_types(self) -> list[dict]: """Devuelve la lista de tipos de documento.""" out = [] for it in self._doc_type_rows: try: name = it["name_entry"].get().strip() kw = it["kw_entry"].get().strip() if name and kw: out.append({"name": name, "keyword": kw}) except: pass return out def set_doc_types(self, doc_types: list[dict]) -> None: """Establece los tipos de documento.""" for it in self._doc_type_rows: it["frame"].destroy() self._doc_type_rows = [] if not doc_types: self._add_doc_type_row("Factura", "Factura_O_Documento_Equivalente") return for dt in doc_types: self._add_doc_type_row(str(dt.get("name", "")), str(dt.get("keyword", ""))) def validate_doc_types(self) -> tuple[bool, str | None]: """Valida los tipos de documento.""" seen = set() for dt in self.get_doc_types(): name = dt.get("name") if name in seen: return False, f"Nombre duplicado: {name}" seen.add(name) if not seen: return False, "Debe haber al menos un tipo de documento." return True, None def update_ui_for_mode(self): """Actualiza la visibilidad de componentes según el modo seleccionado.""" mode = self.get_process_mode() if mode == "general": # Ocultar Tipos de Documento self.doc_types_container.grid_forget() # Mostrar Keywords (opcional, para filtrar) self.kw_label.grid(row=1, column=0, padx=15, pady=2, sticky="w") self.kw_entry.grid(row=1, column=1, padx=5, pady=2, sticky="ew") # Limpiar valor por defecto si existe curr = self.kw_entry.get() if "Factura_O_Documento_Equivalente" in curr: self.kw_entry.delete(0, "end") else: # Mostrar Tipos de Documento self.doc_types_container.grid(row=0, column=0, sticky="nsew", padx=10, pady=(5, 2)) # Ocultar Keywords self.kw_label.grid_forget() self.kw_entry.grid_forget() def append_output(self, text: str) -> None: """Añade texto al área de salida.""" self.output_text.insert("end", text + "\n") self.output_text.see("end") def append_output_safe(self, text: str) -> None: """Añade texto al área de salida de forma thread-safe.""" # Nota: se espera que sea llamado con self.after(0, ...) self.append_output(text) def enable_controls(self, enabled: bool): """Habilita o deshabilita los controles.""" state = "normal" if enabled else "disabled" self.process_button.configure(state=state, text="PROCESANDO..." if not enabled else "INICIAR PROCESAMIENTO") if not enabled: self.process_button.configure(fg_color="gray") else: self.process_button.configure(fg_color=self.colors["primary_button"]) self.root_button.configure(state=state) self.root_entry.configure(state=state) self.csv_button.configure(state=state) self.csv_entry.configure(state=state) self.add_doc_type_btn.configure(state=state) for row in self._doc_type_rows: row["name_entry"].configure(state=state) row["kw_entry"].configure(state=state) row["remove_btn"].configure(state=state)