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:
@@ -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)
|
||||
Reference in New Issue
Block a user