Files
IPSExtractor/gui/tabs/preclass_tab.py
T

479 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)