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
+472
View File
@@ -0,0 +1,472 @@
"""Pestaña Preclasificación de la aplicación GUI."""
import customtkinter as ctk
from gui.styles import AppStyles
class PreclassificationTab:
"""Encapsula la UI de la pestaña Preclasificación."""
def __init__(self, parent_tab):
"""Inicializa la pestaña de Preclasificación.
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._preclass_pattern_rows = []
self._setup_layout()
self._create_root_section()
self._create_options_section()
self._create_target_section()
self._create_patterns_section()
self._create_action_section()
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.preclass_container = ctk.CTkScrollableFrame(self.tab, fg_color="transparent")
self.preclass_container.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
self.preclass_container.grid_columnconfigure(0, weight=1)
self.preclass_container.grid_rowconfigure(3, weight=1)
self.preclass_container.grid_rowconfigure(5, weight=1)
def _create_root_section(self):
"""Crea la sección de carpeta raíz."""
self.preclass_frame_root = ctk.CTkFrame(self.preclass_container)
self.preclass_frame_root.grid(row=0, column=0, sticky="ew", padx=5, pady=(5, 10))
self.preclass_frame_root.grid_columnconfigure(1, weight=1)
lbl_pre_root_title = ctk.CTkLabel(
self.preclass_frame_root,
text="Carpeta Raíz (Preclasificación)",
font=self.fonts["section"]
)
lbl_pre_root_title.grid(row=0, column=0, columnspan=3, sticky="w", padx=15, pady=(10, 5))
self.preclass_root_label = ctk.CTkLabel(
self.preclass_frame_root,
text="Carpeta Raíz:",
font=self.fonts["ui"]
)
self.preclass_root_label.grid(row=1, column=0, padx=15, pady=(0, 10), sticky="w")
self.preclass_root_entry = ctk.CTkEntry(
self.preclass_frame_root,
placeholder_text="Selecciona la carpeta raíz para preclasificar...",
font=self.fonts["ui"]
)
self.preclass_root_entry.grid(row=1, column=1, padx=5, pady=(0, 10), sticky="ew")
self.preclass_root_button = ctk.CTkButton(
self.preclass_frame_root,
text="Explorar...",
font=self.fonts["ui"],
width=100
)
self.preclass_root_button.grid(row=1, column=2, padx=15, pady=(0, 10))
# Selector de Modo
self.preclass_mode_var = ctk.StringVar(value="radicados")
lbl_mode = ctk.CTkLabel(self.preclass_frame_root, text="Estructura:", font=self.fonts["ui"])
lbl_mode.grid(row=2, column=0, padx=15, pady=(0, 10), sticky="w")
self.frame_radios = ctk.CTkFrame(self.preclass_frame_root, fg_color="transparent")
self.frame_radios.grid(row=2, column=1, columnspan=2, sticky="w", padx=5, pady=(0, 10))
self.radio_radicados = ctk.CTkRadioButton(
self.frame_radios,
text="Por Radicados (Subcarpetas)",
variable=self.preclass_mode_var,
value="radicados",
font=self.fonts["ui"]
)
self.radio_radicados.pack(side="left", padx=(0, 20))
self.radio_general = ctk.CTkRadioButton(
self.frame_radios,
text="Carpeta General (Archivos Planos)",
variable=self.preclass_mode_var,
value="general",
font=self.fonts["ui"]
)
self.radio_general.pack(side="left")
def _create_options_section(self):
"""Crea la sección de opciones de rango."""
self.preclass_frame_options = ctk.CTkFrame(self.preclass_container)
self.preclass_frame_options.grid(row=1, column=0, sticky="ew", padx=5, pady=(0, 10))
self.preclass_frame_options.grid_columnconfigure(1, weight=1)
self.preclass_range_var = ctk.BooleanVar(value=False)
self.preclass_range_cb = ctk.CTkCheckBox(
self.preclass_frame_options,
text="Habilitar rangos de página",
variable=self.preclass_range_var,
font=self.fonts["ui"],
command=self._on_toggle_preclass_range
)
self.preclass_range_cb.grid(row=0, column=0, padx=15, pady=10, sticky="w")
self.preclass_range_inputs_frame = ctk.CTkFrame(self.preclass_frame_options, fg_color="transparent")
lbl_desde = ctk.CTkLabel(self.preclass_range_inputs_frame, text="Desde:", font=self.fonts["ui"])
lbl_desde.grid(row=0, column=0, padx=(10, 5), pady=5)
self.preclass_range_start_entry = ctk.CTkEntry(self.preclass_range_inputs_frame, width=60, font=self.fonts["ui"])
self.preclass_range_start_entry.insert(0, "1")
self.preclass_range_start_entry.grid(row=0, column=1, padx=5, pady=5)
lbl_hasta = ctk.CTkLabel(self.preclass_range_inputs_frame, text="Hasta:", font=self.fonts["ui"])
lbl_hasta.grid(row=0, column=2, padx=(10, 5), pady=5)
self.preclass_range_end_entry = ctk.CTkEntry(self.preclass_range_inputs_frame, width=60, font=self.fonts["ui"])
self.preclass_range_end_entry.insert(0, "1")
self.preclass_range_end_entry.grid(row=0, column=3, padx=5, pady=5)
def _create_target_section(self):
"""Crea la sección de archivo objetivo."""
self.preclass_frame_target = ctk.CTkFrame(self.preclass_container)
self.preclass_frame_target.grid(row=2, column=0, sticky="ew", padx=5, pady=(0, 10))
self.preclass_frame_target.grid_columnconfigure(0, weight=1)
self.preclass_target_var = ctk.BooleanVar(value=False)
self.preclass_target_cb = ctk.CTkCheckBox(
self.preclass_frame_target,
text="Habilitar archivo objetivo",
variable=self.preclass_target_var,
font=self.fonts["ui"],
command=self._on_toggle_preclass_target
)
self.preclass_target_cb.grid(row=0, column=0, padx=15, pady=10, sticky="w")
self.preclass_target_inputs_frame = ctk.CTkFrame(self.preclass_frame_target, fg_color="transparent")
lbl_target_def = ctk.CTkLabel(self.preclass_target_inputs_frame, text="Def. Tipo Doc:", font=self.fonts["ui"])
lbl_target_def.grid(row=0, column=0, padx=(15, 5), pady=5, sticky="w")
self.preclass_target_def_entry = ctk.CTkEntry(
self.preclass_target_inputs_frame,
width=250,
font=self.fonts["ui"],
placeholder_text="Ej: Factura de Venta"
)
self.preclass_target_def_entry.grid(row=0, column=1, padx=5, pady=5, sticky="w")
lbl_target_pattern = ctk.CTkLabel(self.preclass_target_inputs_frame, text="Patrón:", font=self.fonts["ui"])
lbl_target_pattern.grid(row=0, column=2, padx=(20, 5), pady=5, sticky="w")
self.preclass_target_pattern_entry = ctk.CTkEntry(
self.preclass_target_inputs_frame,
width=200,
font=self.fonts["ui"],
placeholder_text="Ej: *_FAC_*.pdf"
)
self.preclass_target_pattern_entry.grid(row=0, column=3, padx=5, pady=5, sticky="w")
def _create_patterns_section(self):
"""Crea la sección de patrones."""
self.preclass_frame_patterns = ctk.CTkFrame(self.preclass_container)
self.preclass_frame_patterns.grid(row=3, column=0, sticky="nsew", padx=5, pady=5)
self.preclass_frame_patterns.grid_columnconfigure(0, weight=1)
self.preclass_patterns_header = ctk.CTkFrame(self.preclass_frame_patterns, fg_color="transparent")
self.preclass_patterns_header.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 2))
self.preclass_patterns_header.grid_columnconfigure(0, weight=1)
self.preclass_patterns_label = ctk.CTkLabel(
self.preclass_patterns_header,
text="Patrones",
font=self.fonts["section"]
)
self.preclass_patterns_label.grid(row=0, column=0, sticky="w")
self.add_preclass_pattern_btn = ctk.CTkButton(
self.preclass_patterns_header,
text="+ Añadir Patrón",
font=self.fonts["ui"],
width=140,
height=28
)
self.add_preclass_pattern_btn.grid(row=0, column=1, sticky="e")
self.add_preclass_pattern_btn.configure(command=self._add_preclass_pattern_row)
self.preclass_patterns_hint = ctk.CTkLabel(
self.preclass_frame_patterns,
text="Agrega patrones de texto. Cada patrón puede ser solo 'key' o 'key + value'.",
font=self.fonts["italic_small"],
text_color=self.colors["gray_light"],
justify="left"
)
self.preclass_patterns_hint.grid(row=1, column=0, sticky="w", padx=15, pady=(0, 5))
self.preclass_patterns_list = ctk.CTkFrame(self.preclass_frame_patterns, fg_color="transparent")
self.preclass_patterns_list.grid(row=2, column=0, sticky="ew", padx=10, pady=(2, 10))
self.preclass_patterns_list.grid_columnconfigure(0, weight=1)
# Fila por defecto (vacía)
self._add_preclass_pattern_row()
def _create_action_section(self):
"""Crea la sección de acciones."""
self.preclass_action_frame = ctk.CTkFrame(self.preclass_container, fg_color="transparent")
self.preclass_action_frame.grid(row=4, column=0, sticky="ew", padx=5, pady=10)
self.preclass_action_frame.grid_columnconfigure(0, weight=1)
self.preclass_run_btn = ctk.CTkButton(
self.preclass_action_frame,
text="EJECUTAR PRECLASIFICACIÓN",
font=self.fonts["bold_large"],
height=40,
fg_color=self.colors["secondary_button"],
hover_color=self.colors["secondary_hover"]
)
self.preclass_run_btn.pack(side="left", fill="x", expand=True, padx=10)
self.preclass_export_btn = ctk.CTkButton(
self.preclass_action_frame,
text="Exportar Excel",
font=self.fonts["ui"],
height=40,
state="disabled",
fg_color=self.colors["success"],
hover_color=self.colors["success_hover"]
)
self.preclass_export_btn.pack(side="left", padx=(0, 10))
self.preclass_organize_btn = ctk.CTkButton(
self.preclass_action_frame,
text="Organizar Carpetas",
font=self.fonts["ui"],
height=40,
state="disabled",
fg_color=self.colors["warning"],
hover_color=self.colors["warning_hover"]
)
self.preclass_organize_btn.pack(side="left", padx=(0, 10))
def _create_output_section(self):
"""Crea la sección de salida/logs."""
self.preclass_log_frame = ctk.CTkFrame(self.preclass_container)
self.preclass_log_frame.grid(row=5, column=0, sticky="nsew", padx=5, pady=(0, 10))
self.preclass_log_frame.grid_columnconfigure(0, weight=1)
self.preclass_log_frame.grid_rowconfigure(1, weight=1)
lbl_pre_logs = ctk.CTkLabel(
self.preclass_log_frame,
text="Resultados:",
font=self.fonts["ui"],
text_color=self.colors["text_muted"]
)
lbl_pre_logs.grid(row=0, column=0, sticky="w", padx=10, pady=(5, 0))
self.preclass_output = ctk.CTkTextbox(
self.preclass_log_frame,
font=self.fonts["mono"],
activate_scrollbars=True
)
self.preclass_output.grid(row=1, column=0, padx=5, pady=5, sticky="nsew")
def _add_preclass_pattern_row(self, key: str = "", mode: str = "key", value: str = "") -> None:
"""Añade una fila editable para un patrón de preclasificación."""
row = ctk.CTkFrame(self.preclass_patterns_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(2, weight=1)
key_entry = ctk.CTkEntry(grid_inner, placeholder_text="Patrón / Key", height=28, font=self.fonts["ui"])
if key:
key_entry.insert(0, key)
key_entry.grid(row=0, column=0, padx=(0, 5), sticky="ew")
mode_combo = ctk.CTkComboBox(
grid_inner,
values=["key", "key+value"],
width=110,
font=self.fonts["ui"]
)
mode_combo.grid(row=0, column=1, padx=(0, 5), sticky="w")
value_entry = ctk.CTkEntry(
grid_inner,
placeholder_text="Value (si aplica)",
height=28,
font=self.fonts["ui"]
)
if value:
value_entry.insert(0, value)
value_entry.grid(row=0, column=2, padx=(0, 5), sticky="ew")
def _apply_mode(m: str):
m_norm = (m or "").strip().lower()
if m_norm in ("key+value", "key + value", "key_value", "key-value"):
value_entry.configure(state="normal", placeholder_text="Value (requerido)")
else:
value_entry.delete(0, "end")
value_entry.configure(state="disabled", placeholder_text="Value (no aplica)")
def _on_mode_change(choice: str):
_apply_mode(choice)
try:
mode_combo.set(mode if mode in ("key", "key+value") else "key")
except Exception:
mode_combo.set("key")
mode_combo.configure(command=_on_mode_change)
_apply_mode(mode_combo.get())
def _remove():
try:
for i, r_data in enumerate(list(self._preclass_pattern_rows)):
if r_data.get("frame") == row:
self._preclass_pattern_rows.pop(i)
break
except Exception:
pass
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=3, sticky="e")
self._preclass_pattern_rows.append({
"frame": row,
"key_entry": key_entry,
"mode_combo": mode_combo,
"value_entry": value_entry,
"remove_btn": remove_btn,
})
def _on_toggle_preclass_range(self):
"""Muestra u oculta los inputs de rango según el checkbox."""
if self.preclass_range_var.get():
self.preclass_range_inputs_frame.grid(row=0, column=1, padx=10, pady=0, sticky="w")
else:
self.preclass_range_inputs_frame.grid_forget()
def _on_toggle_preclass_target(self):
"""Muestra u oculta los inputs de archivo objetivo según el checkbox."""
if self.preclass_target_var.get():
self.preclass_target_inputs_frame.grid(row=1, column=0, padx=10, pady=(0, 10), sticky="w")
else:
self.preclass_target_inputs_frame.grid_forget()
# --- API Pública ---
def set_on_run(self, callback):
"""Vincula callback al botón de ejecutar."""
self.preclass_run_btn.configure(command=callback)
def set_on_export_excel(self, callback):
"""Vincula callback al botón de exportar Excel."""
self.preclass_export_btn.configure(command=callback)
def set_on_organize_files(self, callback):
"""Vincula callback al botón de organizar carpetas."""
self.preclass_organize_btn.configure(command=callback)
def set_on_select_root(self, callback):
"""Vincula callback al botón de seleccionar carpeta."""
self.preclass_root_button.configure(command=callback)
def get_root_path(self) -> str:
"""Devuelve la ruta raíz de preclasificación."""
return self.preclass_root_entry.get().strip()
def get_mode(self) -> str:
"""Devuelve el modo de preclasificación: 'radicados' o 'general'."""
try:
v = (self.preclass_mode_var.get() or "").strip().lower()
return v if v in ("radicados", "general") else "radicados"
except Exception:
return "radicados"
def get_range(self) -> tuple[bool, int, int]:
"""Devuelve (habilitado, desde, hasta) para preclasificación."""
enabled = self.preclass_range_var.get()
try:
start = int(self.preclass_range_start_entry.get().strip() or "1")
end = int(self.preclass_range_end_entry.get().strip() or "1")
except Exception:
start, end = 1, 1
return enabled, start, end
def get_target_info(self) -> dict:
"""Devuelve la configuración del archivo objetivo."""
return {
"enabled": self.preclass_target_var.get(),
"definition": self.preclass_target_def_entry.get().strip(),
"pattern": self.preclass_target_pattern_entry.get().strip()
}
def get_patterns(self) -> list[dict]:
"""Devuelve la lista de patrones definida."""
out = []
for it in getattr(self, "_preclass_pattern_rows", []):
try:
key = it["key_entry"].get().strip()
mode = (it["mode_combo"].get() or "").strip()
value = it["value_entry"].get().strip()
if not key:
continue
if mode == "key+value":
out.append({"key": key, "mode": "key+value", "value": value})
else:
out.append({"key": key, "mode": "key"})
except Exception:
pass
return out
def append_output(self, text: str) -> None:
"""Añade texto al área de salida."""
self.preclass_output.insert("end", text + "\n")
self.preclass_output.see("end")
def append_output_safe(self, text: str) -> None:
"""Añade texto al área de salida de forma thread-safe."""
self.append_output(text)
def enable_actions(self, enabled: bool):
"""Habilita o deshabilita los botones de acción."""
state = "normal" if enabled else "disabled"
self.preclass_export_btn.configure(state=state)
self.preclass_organize_btn.configure(state=state)
def enable_controls(self, enabled: bool):
"""Habilita o deshabilita los controles principales."""
state = "normal" if enabled else "disabled"
self.preclass_root_button.configure(state=state)
self.preclass_root_entry.configure(state=state)
self.preclass_range_cb.configure(state=state)
self.preclass_range_start_entry.configure(state=state)
self.preclass_range_end_entry.configure(state=state)
self.preclass_target_cb.configure(state=state)
self.preclass_target_def_entry.configure(state=state)
self.preclass_target_pattern_entry.configure(state=state)
self.radio_radicados.configure(state=state)
self.radio_general.configure(state=state)
self.add_preclass_pattern_btn.configure(state=state)
self.preclass_run_btn.configure(state=state)
for row in self._preclass_pattern_rows:
row["key_entry"].configure(state=state)
row["mode_combo"].configure(state=state)
row["value_entry"].configure(state=state)
row["remove_btn"].configure(state=state)