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:
@@ -6,7 +6,10 @@ import os
|
|||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
import openai
|
import openai
|
||||||
from typing import List, Dict, Any, Optional, Tuple, Callable, Awaitable
|
from typing import List, Dict, Any, Optional, Tuple, Callable, Awaitable, TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from application.service import InvoiceProcessingService
|
||||||
|
|
||||||
from application.profiles import get_profile
|
from application.profiles import get_profile
|
||||||
from application.prompts import PromptBuilder
|
from application.prompts import PromptBuilder
|
||||||
@@ -21,7 +24,7 @@ from core.csv_utils import rows_from_data
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class BatchProcessor:
|
class BatchProcessor:
|
||||||
def __init__(self, service):
|
def __init__(self, service: 'InvoiceProcessingService'):
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
service: Instancia de InvoiceProcessingService para delegar la extracción individual.
|
service: Instancia de InvoiceProcessingService para delegar la extracción individual.
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class RetrySettings(BaseSettings):
|
|||||||
|
|
||||||
class ProcessingSettings(BaseSettings):
|
class ProcessingSettings(BaseSettings):
|
||||||
"""Configuración de procesamiento y concurrencia."""
|
"""Configuración de procesamiento y concurrencia."""
|
||||||
concurrency_limit: int = Field(20, validation_alias=AliasChoices("CONCURRENCY_LIMIT"))
|
concurrency_limit: int = Field(100, validation_alias=AliasChoices("CONCURRENCY_LIMIT"))
|
||||||
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
|
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
Encapsula la lógica de estimación previa al procesamiento.
|
Encapsula la lógica de estimación previa al procesamiento.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from application.service import InvoiceProcessingService
|
||||||
|
|
||||||
from application.profiles import get_profile
|
from application.profiles import get_profile
|
||||||
from application.prompts import PromptBuilder
|
from application.prompts import PromptBuilder
|
||||||
@@ -13,7 +16,7 @@ from application.file_discovery import get_sorted_radicados, find_file_for_type_
|
|||||||
from core.utils import collect_radicado_docs, collect_documents_flat
|
from core.utils import collect_radicado_docs, collect_documents_flat
|
||||||
|
|
||||||
class Estimator:
|
class Estimator:
|
||||||
def __init__(self, service):
|
def __init__(self, service: 'InvoiceProcessingService'):
|
||||||
self.service = service
|
self.service = service
|
||||||
|
|
||||||
def estimate(
|
def estimate(
|
||||||
|
|||||||
@@ -32,11 +32,16 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class InvoiceProcessingService:
|
class InvoiceProcessingService:
|
||||||
"""Fachada de negocio para extraer y transformar datos de facturas."""
|
"""Fachada de negocio para extraer y transformar datos de facturas."""
|
||||||
def __init__(self, extractor: InvoiceExtractorStrategy) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
extractor: InvoiceExtractorStrategy,
|
||||||
|
batch_processor_factory: Callable[[InvoiceProcessingService], BatchProcessor],
|
||||||
|
estimator_factory: Callable[[InvoiceProcessingService], Estimator]
|
||||||
|
) -> None:
|
||||||
self.extractor = extractor
|
self.extractor = extractor
|
||||||
# Componentes delegados
|
# Componentes delegados inyectados mediante factories para evitar dependencia circular directa en construcción
|
||||||
self.batch_processor = BatchProcessor(self)
|
self.batch_processor = batch_processor_factory(self)
|
||||||
self.estimator = Estimator(self)
|
self.estimator = estimator_factory(self)
|
||||||
|
|
||||||
# Lock para escrituras concurrentes al mismo CSV desde múltiples hilos
|
# Lock para escrituras concurrentes al mismo CSV desde múltiples hilos
|
||||||
# (Mantenido por retrocompatibilidad si se accede directo, aunque BatchProcessor usa su propio CSVManager)
|
# (Mantenido por retrocompatibilidad si se accede directo, aunque BatchProcessor usa su propio CSVManager)
|
||||||
|
|||||||
+4
-6
@@ -1,7 +1,5 @@
|
|||||||
"""Paquete `gui`: componentes de la interfaz gráfica basada en customtkinter.
|
"""Módulo de GUI."""
|
||||||
|
from gui.view import InvoiceAppView
|
||||||
|
from gui.styles import AppStyles
|
||||||
|
|
||||||
Incluye:
|
__all__ = ["InvoiceAppView", "AppStyles"]
|
||||||
- `view`: ventana principal y widgets.
|
|
||||||
- `controller`: controlador de flujos de facturas.
|
|
||||||
- `field_selector`: diálogo para seleccionar campos y alias.
|
|
||||||
"""
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"""Configuración centralizada de estilos, temas y fuentes para la GUI."""
|
||||||
|
import customtkinter as ctk
|
||||||
|
|
||||||
|
|
||||||
|
class AppStyles:
|
||||||
|
"""Clase con configuración de estilos y fuentes de la aplicación."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def configure_theme():
|
||||||
|
"""Configura el tema y apariencia general de la aplicación."""
|
||||||
|
ctk.set_appearance_mode("System")
|
||||||
|
ctk.set_default_color_theme("blue")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_fonts():
|
||||||
|
"""Devuelve un diccionario con las fuentes personalizadas."""
|
||||||
|
return {
|
||||||
|
"title": ctk.CTkFont(family="Roboto", size=22, weight="bold"),
|
||||||
|
"section": ctk.CTkFont(family="Roboto", size=16, weight="bold"),
|
||||||
|
"ui": ctk.CTkFont(family="Roboto", size=13),
|
||||||
|
"mono": ctk.CTkFont(family="Consolas", size=12),
|
||||||
|
"mono_small": ctk.CTkFont(family="Consolas", size=10),
|
||||||
|
"italic_small": ctk.CTkFont(size=12, slant="italic"),
|
||||||
|
"bold_large": ctk.CTkFont(size=16, weight="bold"),
|
||||||
|
"button_large": ctk.CTkFont(size=14, weight="bold"),
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_colors():
|
||||||
|
"""Devuelve un diccionario con los colores personalizados."""
|
||||||
|
return {
|
||||||
|
"primary_button": ("#106BA3", "#106BA3"),
|
||||||
|
"primary_hover": ("#0D5582", "#0D5582"),
|
||||||
|
"secondary_button": ("#2E86C1", "#2E86C1"),
|
||||||
|
"secondary_hover": ("#21618C", "#21618C"),
|
||||||
|
"success": ("#27AE60", "#27AE60"),
|
||||||
|
"success_hover": ("#1E8449", "#1E8449"),
|
||||||
|
"warning": ("#D35400", "#D35400"),
|
||||||
|
"warning_hover": ("#A04000", "#A04000"),
|
||||||
|
"danger": ("#C0392B", "#C0392B"),
|
||||||
|
"danger_hover": ("#A93226", "#A93226"),
|
||||||
|
"gray_light": "gray60",
|
||||||
|
"gray_dark": "gray70",
|
||||||
|
"text_muted": ("gray50", "gray70"),
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_spacing():
|
||||||
|
"""Devuelve un diccionario con espaciados y padding estándar."""
|
||||||
|
return {
|
||||||
|
"padding_small": 2,
|
||||||
|
"padding_medium": 5,
|
||||||
|
"padding_large": 10,
|
||||||
|
"padding_section": 15,
|
||||||
|
"margin_small": (0, 2),
|
||||||
|
"margin_medium": (5, 5),
|
||||||
|
"margin_large": (10, 15),
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
"""Módulo de pestañas de la GUI."""
|
||||||
|
from gui.tabs.settings_tab import SettingsTab
|
||||||
|
from gui.tabs.results_tab import ResultsTab
|
||||||
|
from gui.tabs.main_tab import MainProcessingTab
|
||||||
|
from gui.tabs.preclass_tab import PreclassificationTab
|
||||||
|
from gui.tabs.metrics_tab import MetricsTab
|
||||||
|
|
||||||
|
__all__ = ["SettingsTab", "ResultsTab", "MainProcessingTab", "PreclassificationTab", "MetricsTab"]
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
"""Pestaña Dashboard & Métricas de la aplicación GUI."""
|
||||||
|
import customtkinter as ctk
|
||||||
|
import tkinter.ttk as ttk
|
||||||
|
from gui.styles import AppStyles
|
||||||
|
|
||||||
|
# matplotlib es opcional
|
||||||
|
try:
|
||||||
|
from matplotlib.figure import Figure
|
||||||
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||||||
|
HAS_MPL = True
|
||||||
|
except Exception:
|
||||||
|
HAS_MPL = False
|
||||||
|
|
||||||
|
|
||||||
|
class MetricsTab:
|
||||||
|
"""Encapsula la UI de la pestaña Dashboard & Métricas."""
|
||||||
|
|
||||||
|
def __init__(self, parent_tab):
|
||||||
|
"""Inicializa la pestaña de Métricas.
|
||||||
|
|
||||||
|
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 para gráficos
|
||||||
|
self._cost_line_points_x = []
|
||||||
|
self._cost_line_points_y = []
|
||||||
|
|
||||||
|
self._setup_layout()
|
||||||
|
self._create_kpi_section()
|
||||||
|
self._create_charts_section()
|
||||||
|
self._create_comparison_section()
|
||||||
|
self._create_usage_section()
|
||||||
|
|
||||||
|
def _setup_layout(self):
|
||||||
|
"""Configura el layout base de la pestaña."""
|
||||||
|
self.tab.grid_columnconfigure(0, weight=1)
|
||||||
|
self.tab.grid_rowconfigure(1, weight=1)
|
||||||
|
self.tab.grid_rowconfigure(3, weight=1)
|
||||||
|
|
||||||
|
def _create_kpi_section(self):
|
||||||
|
"""Crea la sección de KPIs."""
|
||||||
|
self.kpi_frame = ctk.CTkFrame(self.tab)
|
||||||
|
self.kpi_frame.grid(row=0, column=0, sticky="ew", padx=15, pady=15)
|
||||||
|
self.kpi_frame.grid_columnconfigure((0, 1, 2), weight=1)
|
||||||
|
|
||||||
|
# Helper para crear cards KPI
|
||||||
|
def create_kpi_card(parent, title, row, col):
|
||||||
|
card = ctk.CTkFrame(parent)
|
||||||
|
card.grid(row=row, column=col, sticky="nsew", padx=5, pady=5)
|
||||||
|
lbl_title = ctk.CTkLabel(
|
||||||
|
card,
|
||||||
|
text=title,
|
||||||
|
font=ctk.CTkFont(size=14, weight="bold")
|
||||||
|
)
|
||||||
|
lbl_title.pack(pady=(10, 5), padx=10, anchor="w")
|
||||||
|
lbl_content = ctk.CTkLabel(
|
||||||
|
card,
|
||||||
|
text="...",
|
||||||
|
font=self.fonts["mono"],
|
||||||
|
justify="left"
|
||||||
|
)
|
||||||
|
lbl_content.pack(pady=(0, 10), padx=10, anchor="w")
|
||||||
|
return lbl_content
|
||||||
|
|
||||||
|
self.kpi_overall = create_kpi_card(self.kpi_frame, "Total General", 0, 0)
|
||||||
|
self.kpi_extract = create_kpi_card(self.kpi_frame, "Fase: Extracción", 0, 1)
|
||||||
|
self.kpi_normalize = create_kpi_card(self.kpi_frame, "Fase: Normalización", 0, 2)
|
||||||
|
|
||||||
|
self.kpi_overall.configure(text="input: 0\ncached: 0\noutput: 0\nUSD: $0.00")
|
||||||
|
self.kpi_extract.configure(text="input: 0\ncached: 0\noutput: 0\nUSD: $0.00")
|
||||||
|
self.kpi_normalize.configure(text="input: 0\ncached: 0\noutput: 0\nUSD: $0.00")
|
||||||
|
|
||||||
|
def _create_charts_section(self):
|
||||||
|
"""Crea la sección de gráficos."""
|
||||||
|
self.charts_frame = ctk.CTkFrame(self.tab)
|
||||||
|
self.charts_frame.grid(row=1, column=0, sticky="nsew", padx=15, pady=5)
|
||||||
|
self.charts_frame.grid_columnconfigure((0, 1), weight=1)
|
||||||
|
self.charts_frame.grid_rowconfigure(0, weight=1)
|
||||||
|
|
||||||
|
# Gráfico Línea
|
||||||
|
if HAS_MPL:
|
||||||
|
self.fig_line = Figure(figsize=(4, 3), dpi=100)
|
||||||
|
self.fig_line.patch.set_facecolor('#f0f0f0')
|
||||||
|
self.ax_line = self.fig_line.add_subplot(111)
|
||||||
|
self.ax_line.set_title("Costo Acumulado (USD)", fontsize=10)
|
||||||
|
self.ax_line.tick_params(labelsize=8)
|
||||||
|
self._line_plot, = self.ax_line.plot([], [], marker="o", color="#1f77b4")
|
||||||
|
|
||||||
|
self.canvas_line = FigureCanvasTkAgg(self.fig_line, master=self.charts_frame)
|
||||||
|
self.canvas_line.get_tk_widget().grid(row=0, column=0, padx=10, pady=10, sticky="nsew")
|
||||||
|
else:
|
||||||
|
ctk.CTkLabel(self.charts_frame, text="Gráficos no disponibles").grid(row=0, column=0)
|
||||||
|
|
||||||
|
# Gráfico Barras
|
||||||
|
if HAS_MPL:
|
||||||
|
self.fig_bars = Figure(figsize=(4, 3), dpi=100)
|
||||||
|
self.fig_bars.patch.set_facecolor('#f0f0f0')
|
||||||
|
self.ax_bars = self.fig_bars.add_subplot(111)
|
||||||
|
self.ax_bars.set_title("Consumo de Tokens", fontsize=10)
|
||||||
|
self.ax_bars.tick_params(labelsize=8)
|
||||||
|
|
||||||
|
self.canvas_bars = FigureCanvasTkAgg(self.fig_bars, master=self.charts_frame)
|
||||||
|
self.canvas_bars.get_tk_widget().grid(row=0, column=1, padx=10, pady=10, sticky="nsew")
|
||||||
|
|
||||||
|
def _create_comparison_section(self):
|
||||||
|
"""Crea la sección de comparación de tarifas."""
|
||||||
|
self.comparison_frame = ctk.CTkFrame(self.tab)
|
||||||
|
self.comparison_frame.grid(row=2, column=0, sticky="ew", padx=15, pady=5)
|
||||||
|
self.comparison_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
lbl_comp_title = ctk.CTkLabel(
|
||||||
|
self.comparison_frame,
|
||||||
|
text="Tarifas de Modelos (USD por 1M tokens)",
|
||||||
|
font=ctk.CTkFont(size=14, weight="bold")
|
||||||
|
)
|
||||||
|
lbl_comp_title.grid(row=0, column=0, sticky="w", padx=10, pady=5)
|
||||||
|
|
||||||
|
# Tabla de comparación
|
||||||
|
self.tree_comparison = ttk.Treeview(
|
||||||
|
self.comparison_frame,
|
||||||
|
columns=("model", "input", "cached", "output"),
|
||||||
|
show="headings",
|
||||||
|
height=4
|
||||||
|
)
|
||||||
|
self.tree_comparison.heading("model", text="Modelo")
|
||||||
|
self.tree_comparison.heading("input", text="Input / 1M")
|
||||||
|
self.tree_comparison.heading("cached", text="Cached / 1M")
|
||||||
|
self.tree_comparison.heading("output", text="Output / 1M")
|
||||||
|
|
||||||
|
self.tree_comparison.column("model", width=150, anchor="w")
|
||||||
|
self.tree_comparison.column("input", width=100, anchor="e")
|
||||||
|
self.tree_comparison.column("cached", width=100, anchor="e")
|
||||||
|
self.tree_comparison.column("output", width=100, anchor="e")
|
||||||
|
|
||||||
|
self.tree_comparison.grid(row=1, column=0, sticky="ew", padx=10, pady=(0, 10))
|
||||||
|
|
||||||
|
def _create_usage_section(self):
|
||||||
|
"""Crea la sección de detalle de uso."""
|
||||||
|
self.usage_text = ctk.CTkTextbox(self.tab, height=150, font=self.fonts["mono"])
|
||||||
|
self.usage_text.grid(row=3, column=0, sticky="ew", padx=15, pady=15)
|
||||||
|
|
||||||
|
# --- API Pública ---
|
||||||
|
|
||||||
|
def append_usage(self, text: str) -> None:
|
||||||
|
"""Añade texto al área de uso."""
|
||||||
|
self.usage_text.insert("end", text + "\n")
|
||||||
|
self.usage_text.see("end")
|
||||||
|
|
||||||
|
def append_usage_safe(self, text: str) -> None:
|
||||||
|
"""Añade texto al área de uso de forma thread-safe."""
|
||||||
|
self.append_usage(text)
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
"""Limpia todos los datos de la pestaña."""
|
||||||
|
self.usage_text.delete("1.0", "end")
|
||||||
|
try:
|
||||||
|
for item in self.tree_comparison.get_children():
|
||||||
|
self.tree_comparison.delete(item)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Reset KPIs
|
||||||
|
zero_txt = "input: 0\ncached: 0\noutput: 0\nUSD: $0.0000"
|
||||||
|
self.kpi_overall.configure(text=zero_txt)
|
||||||
|
self.kpi_extract.configure(text=zero_txt)
|
||||||
|
self.kpi_normalize.configure(text=zero_txt)
|
||||||
|
|
||||||
|
# Reset Charts
|
||||||
|
if HAS_MPL:
|
||||||
|
self._cost_line_points_x.clear()
|
||||||
|
self._cost_line_points_y.clear()
|
||||||
|
self._line_plot.set_data([], [])
|
||||||
|
self.ax_line.relim()
|
||||||
|
self.ax_line.autoscale_view()
|
||||||
|
self.canvas_line.draw_idle()
|
||||||
|
|
||||||
|
self.ax_bars.clear()
|
||||||
|
self.ax_bars.set_title("Consumo de Tokens")
|
||||||
|
self.canvas_bars.draw_idle()
|
||||||
|
|
||||||
|
def set_usage_totals(self, **kwargs):
|
||||||
|
"""Legacy method, handled by KPIs now."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update_kpis(self, overall: dict, extract_phase: dict, normalize_phase: dict) -> None:
|
||||||
|
"""Actualiza los KPIs con los datos proporcionados."""
|
||||||
|
def fmt(d):
|
||||||
|
return (
|
||||||
|
f"input: {d.get('input',0)}\n"
|
||||||
|
f"cached: {d.get('cached',0)}\n"
|
||||||
|
f"output: {d.get('output',0)}\n"
|
||||||
|
f"USD: ${d.get('usd',0.0):.4f}"
|
||||||
|
)
|
||||||
|
self.kpi_overall.configure(text=fmt(overall))
|
||||||
|
self.kpi_extract.configure(text=fmt(extract_phase))
|
||||||
|
self.kpi_normalize.configure(text=fmt(normalize_phase))
|
||||||
|
|
||||||
|
def update_cost_line(self, doc_index: int, total_cost_usd: float) -> None:
|
||||||
|
"""Actualiza el gráfico de línea de costo acumulado."""
|
||||||
|
if not HAS_MPL:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self._cost_line_points_x.append(doc_index)
|
||||||
|
self._cost_line_points_y.append(total_cost_usd)
|
||||||
|
self._line_plot.set_data(self._cost_line_points_x, self._cost_line_points_y)
|
||||||
|
self.ax_line.relim()
|
||||||
|
self.ax_line.autoscale_view()
|
||||||
|
self.canvas_line.draw_idle()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update_token_bars(self, input_extract: int, cached_extract: int, output_extract: int,
|
||||||
|
input_norm: int, cached_norm: int, output_norm: int) -> None:
|
||||||
|
"""Actualiza el gráfico de barras de tokens."""
|
||||||
|
if not HAS_MPL:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.ax_bars.clear()
|
||||||
|
self.ax_bars.set_title("Tokens por Fase")
|
||||||
|
labels = ["Input", "Cached", "Output"]
|
||||||
|
extract_vals = [input_extract, cached_extract, output_extract]
|
||||||
|
norm_vals = [input_norm, cached_norm, output_norm]
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
x = np.arange(len(labels))
|
||||||
|
width = 0.35
|
||||||
|
|
||||||
|
self.ax_bars.bar(x - width/2, extract_vals, width, label="Extracción", color="#1f77b4")
|
||||||
|
self.ax_bars.bar(x + width/2, norm_vals, width, label="Normalización", color="#ff7f0e")
|
||||||
|
|
||||||
|
self.ax_bars.set_xticks(x, labels)
|
||||||
|
self.ax_bars.legend()
|
||||||
|
self.canvas_bars.draw_idle()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update_comparison_table(self, comparisons: list[dict]) -> None:
|
||||||
|
"""Actualiza la tabla de comparación de tarifas."""
|
||||||
|
try:
|
||||||
|
# Limpiar tabla
|
||||||
|
for item in self.tree_comparison.get_children():
|
||||||
|
self.tree_comparison.delete(item)
|
||||||
|
|
||||||
|
# Insertar filas
|
||||||
|
for c in comparisons:
|
||||||
|
self.tree_comparison.insert("", "end", values=(
|
||||||
|
c.get("model", "N/D"),
|
||||||
|
f"${c.get('input', 0.0):.2f}",
|
||||||
|
f"${c.get('cached_input', 0.0):.2f}",
|
||||||
|
f"${c.get('output', 0.0):.2f}"
|
||||||
|
))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def append_usage_row(self, phase: str, radicado: str, usage: dict | None, cost: dict | None) -> None:
|
||||||
|
"""Añade una fila de detalle de uso."""
|
||||||
|
try:
|
||||||
|
usd = (cost or {}).get('total_usd', 0.0)
|
||||||
|
self.append_usage(f"[{phase:<10}] {radicado:<20} | cost=${usd:.4f} | usage={usage}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
"""Pestaña de Resultados de la aplicación GUI."""
|
||||||
|
import os
|
||||||
|
import customtkinter as ctk
|
||||||
|
import tkinter.ttk as ttk
|
||||||
|
from gui.styles import AppStyles
|
||||||
|
|
||||||
|
|
||||||
|
class ResultsTab:
|
||||||
|
"""Encapsula la UI de la pestaña Resultados."""
|
||||||
|
|
||||||
|
def __init__(self, parent_tab):
|
||||||
|
"""Inicializa la pestaña de Resultados.
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
self._setup_layout()
|
||||||
|
self._create_subtabs()
|
||||||
|
self._setup_treeview_style()
|
||||||
|
self._create_extractor_tab()
|
||||||
|
self._create_preclass_results_tab()
|
||||||
|
|
||||||
|
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.tabs_res = ctk.CTkTabview(self.tab)
|
||||||
|
self.tabs_res.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
|
||||||
|
|
||||||
|
def _create_subtabs(self):
|
||||||
|
"""Crea las sub-pestañas de Resultados."""
|
||||||
|
self.tab_res_extractor = self.tabs_res.add("Extractor (Próximamente)")
|
||||||
|
self.tab_res_preclass = self.tabs_res.add("Preclasificación")
|
||||||
|
|
||||||
|
def _setup_treeview_style(self):
|
||||||
|
"""Configura el estilo de los treeviews."""
|
||||||
|
style = ttk.Style()
|
||||||
|
style.theme_use("clam")
|
||||||
|
style.configure("Treeview", rowheight=25)
|
||||||
|
|
||||||
|
def _create_extractor_tab(self):
|
||||||
|
"""Crea la sub-pestaña del Extractor (en construcción)."""
|
||||||
|
lbl_const = ctk.CTkLabel(
|
||||||
|
self.tab_res_extractor,
|
||||||
|
text="En construcción...",
|
||||||
|
font=ctk.CTkFont(size=20)
|
||||||
|
)
|
||||||
|
lbl_const.place(relx=0.5, rely=0.5, anchor="center")
|
||||||
|
|
||||||
|
def _create_preclass_results_tab(self):
|
||||||
|
"""Crea la sub-pestaña de resultados de preclasificación."""
|
||||||
|
self.tab_res_preclass.grid_columnconfigure(0, weight=1)
|
||||||
|
self.tab_res_preclass.grid_rowconfigure(1, weight=1)
|
||||||
|
|
||||||
|
self._create_filters_section()
|
||||||
|
self._create_tables_section()
|
||||||
|
self._create_actions_section()
|
||||||
|
|
||||||
|
def _create_filters_section(self):
|
||||||
|
"""Crea la sección de filtros."""
|
||||||
|
self.frame_res_filters = ctk.CTkFrame(self.tab_res_preclass)
|
||||||
|
self.frame_res_filters.grid(row=0, column=0, sticky="ew", padx=10, pady=5)
|
||||||
|
|
||||||
|
ctk.CTkLabel(self.frame_res_filters, text="Run ID:").grid(row=0, column=0, padx=5, pady=5)
|
||||||
|
self.combo_res_run = ctk.CTkComboBox(self.frame_res_filters, values=["Todos"], width=150)
|
||||||
|
self.combo_res_run.grid(row=0, column=1, padx=5, pady=5)
|
||||||
|
|
||||||
|
ctk.CTkLabel(self.frame_res_filters, text="Estado:").grid(row=0, column=2, padx=5, pady=5)
|
||||||
|
self.combo_res_status = ctk.CTkComboBox(
|
||||||
|
self.frame_res_filters,
|
||||||
|
values=["Todos", "MATCH", "NO_MATCH", "ERROR", "NO_FILES"],
|
||||||
|
width=120
|
||||||
|
)
|
||||||
|
self.combo_res_status.grid(row=0, column=3, padx=5, pady=5)
|
||||||
|
|
||||||
|
self.entry_res_search = ctk.CTkEntry(
|
||||||
|
self.frame_res_filters,
|
||||||
|
placeholder_text="Buscar radicado/archivo...",
|
||||||
|
width=200
|
||||||
|
)
|
||||||
|
self.entry_res_search.grid(row=0, column=4, padx=5, pady=5)
|
||||||
|
|
||||||
|
self.btn_res_search = ctk.CTkButton(self.frame_res_filters, text="Buscar", width=100)
|
||||||
|
self.btn_res_search.grid(row=0, column=5, padx=10, pady=5)
|
||||||
|
|
||||||
|
def _create_tables_section(self):
|
||||||
|
"""Crea la sección con las tablas (principal y de detalle)."""
|
||||||
|
self.frame_res_table = ctk.CTkFrame(self.tab_res_preclass, fg_color="transparent")
|
||||||
|
self.frame_res_table.grid(row=1, column=0, sticky="nsew", padx=10, pady=5)
|
||||||
|
self.frame_res_table.grid_columnconfigure(0, weight=1)
|
||||||
|
self.frame_res_table.grid_rowconfigure(0, weight=3)
|
||||||
|
self.frame_res_table.grid_rowconfigure(2, weight=2)
|
||||||
|
|
||||||
|
self._create_main_results_table()
|
||||||
|
self._create_detail_label()
|
||||||
|
self._create_matches_table()
|
||||||
|
|
||||||
|
def _create_main_results_table(self):
|
||||||
|
"""Crea la tabla principal de resultados."""
|
||||||
|
cols = ("ID", "Run", "Fecha", "Radicado", "Estado", "Archivo")
|
||||||
|
self.tree_results = ttk.Treeview(
|
||||||
|
self.frame_res_table,
|
||||||
|
columns=cols,
|
||||||
|
show="headings",
|
||||||
|
selectmode="browse"
|
||||||
|
)
|
||||||
|
|
||||||
|
for c in cols:
|
||||||
|
self.tree_results.heading(c, text=c)
|
||||||
|
self.tree_results.column(c, width=100)
|
||||||
|
|
||||||
|
self.tree_results.column("Archivo", width=300)
|
||||||
|
self.tree_results.column("ID", width=50)
|
||||||
|
self.tree_results.column("Run", width=50)
|
||||||
|
|
||||||
|
# Scrollbars
|
||||||
|
sb_y = ttk.Scrollbar(self.frame_res_table, orient="vertical", command=self.tree_results.yview)
|
||||||
|
sb_x = ttk.Scrollbar(self.frame_res_table, orient="horizontal", command=self.tree_results.xview)
|
||||||
|
self.tree_results.configure(yscrollcommand=sb_y.set, xscrollcommand=sb_x.set)
|
||||||
|
|
||||||
|
self.tree_results.grid(row=0, column=0, sticky="nsew")
|
||||||
|
sb_y.grid(row=0, column=1, sticky="ns")
|
||||||
|
sb_x.grid(row=1, column=0, sticky="ew")
|
||||||
|
|
||||||
|
def _create_detail_label(self):
|
||||||
|
"""Crea la etiqueta de la sección de detalle."""
|
||||||
|
lbl_detail = ctk.CTkLabel(
|
||||||
|
self.frame_res_table,
|
||||||
|
text="Detalle de Matches (Seleccionar fila arriba)",
|
||||||
|
font=ctk.CTkFont(size=12, weight="bold")
|
||||||
|
)
|
||||||
|
lbl_detail.grid(row=1, column=0, sticky="w", pady=(10, 5))
|
||||||
|
|
||||||
|
def _create_matches_table(self):
|
||||||
|
"""Crea la tabla de detalle de matches."""
|
||||||
|
cols_detail = ("Key", "Valor Encontrado", "Existe", "Valor Esperado")
|
||||||
|
self.tree_matches = ttk.Treeview(
|
||||||
|
self.frame_res_table,
|
||||||
|
columns=cols_detail,
|
||||||
|
show="headings",
|
||||||
|
selectmode="browse",
|
||||||
|
height=5
|
||||||
|
)
|
||||||
|
|
||||||
|
for c in cols_detail:
|
||||||
|
self.tree_matches.heading(c, text=c)
|
||||||
|
self.tree_matches.column(c, width=150)
|
||||||
|
|
||||||
|
sb_detail_y = ttk.Scrollbar(self.frame_res_table, orient="vertical", command=self.tree_matches.yview)
|
||||||
|
self.tree_matches.configure(yscrollcommand=sb_detail_y.set)
|
||||||
|
|
||||||
|
self.tree_matches.grid(row=2, column=0, sticky="nsew")
|
||||||
|
sb_detail_y.grid(row=2, column=1, sticky="ns")
|
||||||
|
|
||||||
|
def _create_actions_section(self):
|
||||||
|
"""Crea la sección de acciones."""
|
||||||
|
self.frame_res_actions = ctk.CTkFrame(self.tab_res_preclass)
|
||||||
|
self.frame_res_actions.grid(row=2, column=0, sticky="ew", padx=10, pady=10)
|
||||||
|
|
||||||
|
self.btn_res_export = ctk.CTkButton(
|
||||||
|
self.frame_res_actions,
|
||||||
|
text="Exportar Vista a Excel",
|
||||||
|
fg_color=self.colors["success"]
|
||||||
|
)
|
||||||
|
self.btn_res_export.pack(side="right", padx=10, pady=5)
|
||||||
|
|
||||||
|
self.btn_res_organize = ctk.CTkButton(
|
||||||
|
self.frame_res_actions,
|
||||||
|
text="Organizar Carpetas (Run)",
|
||||||
|
fg_color=self.colors["warning"],
|
||||||
|
hover_color=self.colors["warning_hover"]
|
||||||
|
)
|
||||||
|
self.btn_res_organize.pack(side="right", padx=10, pady=5)
|
||||||
|
|
||||||
|
self.btn_res_refresh = ctk.CTkButton(self.frame_res_actions, text="Refrescar Runs")
|
||||||
|
self.btn_res_refresh.pack(side="left", padx=10, pady=5)
|
||||||
|
|
||||||
|
# --- API Pública ---
|
||||||
|
|
||||||
|
def set_on_search(self, callback):
|
||||||
|
"""Vincula el callback al botón de búsqueda."""
|
||||||
|
self.btn_res_search.configure(command=callback)
|
||||||
|
|
||||||
|
def set_on_export(self, callback):
|
||||||
|
"""Vincula el callback al botón de exportación."""
|
||||||
|
self.btn_res_export.configure(command=callback)
|
||||||
|
|
||||||
|
def set_on_refresh(self, callback):
|
||||||
|
"""Vincula el callback al botón de refrescar."""
|
||||||
|
self.btn_res_refresh.configure(command=callback)
|
||||||
|
|
||||||
|
def set_on_organize(self, callback):
|
||||||
|
"""Vincula el callback al botón de organizar."""
|
||||||
|
self.btn_res_organize.configure(command=callback)
|
||||||
|
|
||||||
|
def set_on_select_result(self, callback):
|
||||||
|
"""Vincula el callback al evento de selección de fila."""
|
||||||
|
self.tree_results.bind("<<TreeviewSelect>>", callback)
|
||||||
|
|
||||||
|
def update_matches_table(self, rows):
|
||||||
|
"""Actualiza la tabla de matches con los datos proporcionados."""
|
||||||
|
for item in self.tree_matches.get_children():
|
||||||
|
self.tree_matches.delete(item)
|
||||||
|
|
||||||
|
for r in rows:
|
||||||
|
exists_str = "✅" if r.get("exists_bool") else "❌"
|
||||||
|
vals = (r.get("key"), r.get("found_value"), exists_str, r.get("expected_value") or "-")
|
||||||
|
self.tree_matches.insert("", "end", values=vals)
|
||||||
|
|
||||||
|
def update_runs_combo(self, runs: list):
|
||||||
|
"""Actualiza el combo box con los IDs de ejecuciones disponibles."""
|
||||||
|
vals = ["Todos"] + [str(r) for r in runs]
|
||||||
|
self.combo_res_run.configure(values=vals)
|
||||||
|
self.combo_res_run.set("Todos")
|
||||||
|
|
||||||
|
def set_run_id(self, run_id: int | str):
|
||||||
|
"""Establece el ID de ejecución seleccionado."""
|
||||||
|
try:
|
||||||
|
self.combo_res_run.set(str(run_id))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_filters(self) -> dict:
|
||||||
|
"""Devuelve los filtros actuales."""
|
||||||
|
return {
|
||||||
|
"run_id": self.combo_res_run.get(),
|
||||||
|
"status": self.combo_res_status.get(),
|
||||||
|
"text": self.entry_res_search.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
def update_results_table(self, rows):
|
||||||
|
"""Actualiza la tabla de resultados con los datos proporcionados."""
|
||||||
|
for item in self.tree_results.get_children():
|
||||||
|
self.tree_results.delete(item)
|
||||||
|
|
||||||
|
for r in rows:
|
||||||
|
vals = (
|
||||||
|
r.get("id"),
|
||||||
|
r.get("run_id"),
|
||||||
|
r.get("timestamp"),
|
||||||
|
r.get("radicado"),
|
||||||
|
r.get("status"),
|
||||||
|
os.path.basename(r.get("file_path") or "")
|
||||||
|
)
|
||||||
|
self.tree_results.insert("", "end", values=vals)
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
"""Pestaña de Configuración de la aplicación GUI."""
|
||||||
|
import customtkinter as ctk
|
||||||
|
from gui.styles import AppStyles
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsTab:
|
||||||
|
"""Encapsula la UI de la pestaña Configuración."""
|
||||||
|
|
||||||
|
def __init__(self, parent_tab):
|
||||||
|
"""Inicializa la pestaña de Configuració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()
|
||||||
|
|
||||||
|
self._setup_layout()
|
||||||
|
self._create_api_section()
|
||||||
|
self._create_processing_section()
|
||||||
|
self._create_save_button()
|
||||||
|
|
||||||
|
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.settings_container = ctk.CTkFrame(self.tab, fg_color="transparent")
|
||||||
|
self.settings_container.grid(row=0, column=0, sticky="nsew", padx=20, pady=20)
|
||||||
|
self.settings_container.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
def _create_api_section(self):
|
||||||
|
"""Crea la sección de configuración del modelo LLM."""
|
||||||
|
self.frame_api = ctk.CTkFrame(self.settings_container)
|
||||||
|
self.frame_api.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
|
||||||
|
self.frame_api.grid_columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
# Título
|
||||||
|
lbl_api_title = ctk.CTkLabel(
|
||||||
|
self.frame_api,
|
||||||
|
text="Configuración del Modelo LLM",
|
||||||
|
font=self.fonts["section"]
|
||||||
|
)
|
||||||
|
lbl_api_title.grid(row=0, column=0, columnspan=2, sticky="w", padx=15, pady=10)
|
||||||
|
|
||||||
|
# Selector de modelo
|
||||||
|
lbl_model = ctk.CTkLabel(
|
||||||
|
self.frame_api,
|
||||||
|
text="Modelo (Ollama):",
|
||||||
|
font=self.fonts["ui"]
|
||||||
|
)
|
||||||
|
lbl_model.grid(row=1, column=0, padx=15, pady=10, sticky="w")
|
||||||
|
|
||||||
|
self.combo_model = ctk.CTkComboBox(
|
||||||
|
self.frame_api,
|
||||||
|
values=["5.1", "4.1", "4.1 mini", "4.1 nano"],
|
||||||
|
width=250,
|
||||||
|
font=self.fonts["ui"]
|
||||||
|
)
|
||||||
|
self.combo_model.grid(row=1, column=1, padx=15, pady=10, sticky="w")
|
||||||
|
self.combo_model.set("4.1")
|
||||||
|
|
||||||
|
# Texto de ayuda
|
||||||
|
help_text = (
|
||||||
|
"ℹ 4.1 mini: Muy rápido (recomendado)\n"
|
||||||
|
"ℹ 4.1: Estándar (balanceado)\n"
|
||||||
|
"ℹ 5.1: Lento pero mayor razonamiento"
|
||||||
|
)
|
||||||
|
lbl_help = ctk.CTkLabel(
|
||||||
|
self.frame_api,
|
||||||
|
text=help_text,
|
||||||
|
font=self.fonts["italic_small"],
|
||||||
|
text_color=self.colors["gray_light"],
|
||||||
|
justify="left"
|
||||||
|
)
|
||||||
|
lbl_help.grid(row=2, column=1, padx=15, pady=(0, 10), sticky="w")
|
||||||
|
|
||||||
|
def _create_processing_section(self):
|
||||||
|
"""Crea la sección de procesamiento y rendimiento."""
|
||||||
|
self.frame_proc = ctk.CTkFrame(self.settings_container)
|
||||||
|
self.frame_proc.grid(row=1, column=0, sticky="ew", padx=5, pady=5)
|
||||||
|
self.frame_proc.grid_columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
# Título
|
||||||
|
lbl_proc_title = ctk.CTkLabel(
|
||||||
|
self.frame_proc,
|
||||||
|
text="Procesamiento y Rendimiento",
|
||||||
|
font=self.fonts["section"]
|
||||||
|
)
|
||||||
|
lbl_proc_title.grid(row=0, column=0, columnspan=2, sticky="w", padx=15, pady=10)
|
||||||
|
|
||||||
|
# Límite de concurrencia
|
||||||
|
lbl_conc = ctk.CTkLabel(
|
||||||
|
self.frame_proc,
|
||||||
|
text="Límite de Concurrencia:",
|
||||||
|
font=self.fonts["ui"]
|
||||||
|
)
|
||||||
|
lbl_conc.grid(row=1, column=0, padx=15, pady=10, sticky="w")
|
||||||
|
|
||||||
|
self.concurrency_var = ctk.StringVar(value="100")
|
||||||
|
self.concurrency_entry = ctk.CTkEntry(
|
||||||
|
self.frame_proc,
|
||||||
|
textvariable=self.concurrency_var,
|
||||||
|
width=100,
|
||||||
|
font=self.fonts["ui"]
|
||||||
|
)
|
||||||
|
self.concurrency_entry.grid(row=1, column=1, padx=15, pady=10, sticky="w")
|
||||||
|
|
||||||
|
# Advertencia
|
||||||
|
warning_text = (
|
||||||
|
"⚠️ Advertencia: Entre mayor el número, mayor la probabilidad de Rate Limits (429)."
|
||||||
|
)
|
||||||
|
lbl_conc_warn = ctk.CTkLabel(
|
||||||
|
self.frame_proc,
|
||||||
|
text=warning_text,
|
||||||
|
font=self.fonts["italic_small"],
|
||||||
|
text_color="orange"
|
||||||
|
)
|
||||||
|
lbl_conc_warn.grid(row=2, column=0, columnspan=2, padx=15, pady=(0, 10), sticky="w")
|
||||||
|
|
||||||
|
def _create_save_button(self):
|
||||||
|
"""Crea el botón de guardar configuración."""
|
||||||
|
self.btn_save_settings = ctk.CTkButton(
|
||||||
|
self.settings_container,
|
||||||
|
text="Aplicar Configuración",
|
||||||
|
font=self.fonts["ui"],
|
||||||
|
height=35
|
||||||
|
)
|
||||||
|
self.btn_save_settings.grid(row=2, column=0, padx=15, pady=15, sticky="ew")
|
||||||
|
|
||||||
|
# --- API Pública ---
|
||||||
|
|
||||||
|
def set_on_save(self, callback):
|
||||||
|
"""Vincula el callback al botón de guardar."""
|
||||||
|
self.btn_save_settings.configure(command=callback)
|
||||||
|
|
||||||
|
def get_selected_model(self) -> str:
|
||||||
|
"""Devuelve el modelo seleccionado."""
|
||||||
|
return self.combo_model.get()
|
||||||
|
|
||||||
|
def set_selected_model(self, model: str) -> None:
|
||||||
|
"""Establece el modelo seleccionado."""
|
||||||
|
self.combo_model.set(model)
|
||||||
|
|
||||||
|
def get_concurrency_limit(self) -> int:
|
||||||
|
"""Devuelve el límite de concurrencia."""
|
||||||
|
try:
|
||||||
|
return int(self.concurrency_var.get())
|
||||||
|
except Exception:
|
||||||
|
return 20
|
||||||
|
|
||||||
|
def set_concurrency_limit(self, limit: int) -> None:
|
||||||
|
"""Establece el límite de concurrencia."""
|
||||||
|
self.concurrency_var.set(str(limit))
|
||||||
|
|
||||||
|
def enable_controls(self, enabled: bool):
|
||||||
|
"""Habilita o deshabilita los controles."""
|
||||||
|
state = "normal" if enabled else "disabled"
|
||||||
|
self.combo_model.configure(state=state)
|
||||||
|
self.concurrency_entry.configure(state=state)
|
||||||
|
self.btn_save_settings.configure(state=state)
|
||||||
+88
-1055
File diff suppressed because it is too large
Load Diff
+9
-1
@@ -38,7 +38,15 @@ def main():
|
|||||||
base_url=settings.api.base_url,
|
base_url=settings.api.base_url,
|
||||||
model=settings.api.model
|
model=settings.api.model
|
||||||
)
|
)
|
||||||
service = InvoiceProcessingService(extractor)
|
|
||||||
|
from application.batch_processor import BatchProcessor
|
||||||
|
from application.estimator import Estimator
|
||||||
|
|
||||||
|
service = InvoiceProcessingService(
|
||||||
|
extractor,
|
||||||
|
batch_processor_factory=lambda s: BatchProcessor(s),
|
||||||
|
estimator_factory=lambda s: Estimator(s)
|
||||||
|
)
|
||||||
|
|
||||||
app = InvoiceAppView()
|
app = InvoiceAppView()
|
||||||
AppController(app, service)
|
AppController(app, service)
|
||||||
|
|||||||
Reference in New Issue
Block a user