479 lines
20 KiB
Python
479 lines
20 KiB
Python
"""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 set_root_path(self, path: str) -> None:
|
||
"""Establece la ruta raíz de preclasificación."""
|
||
self.preclass_root_entry.configure(state="normal")
|
||
self.preclass_root_entry.delete(0, "end")
|
||
self.preclass_root_entry.insert(0, path)
|
||
|
||
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)
|