feat: Enhance GUI and processing services with new batch processing and estimation features, update concurrency settings, and improve styling organization.

This commit is contained in:
erik
2026-03-01 19:56:10 -05:00
parent 69debb1c93
commit 3c258ebf12
14 changed files with 1777 additions and 1071 deletions
+438
View File
@@ -0,0 +1,438 @@
"""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()
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 get_csv_path(self) -> str:
"""Devuelve la ruta del CSV."""
return self.csv_entry.get().strip()
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)