439 lines
17 KiB
Python
439 lines
17 KiB
Python
"""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)
|