feat: Implement moderadora preferences management and enhance CSV cleaning rules for COP/MOD separation. Introduce new GUI components for moderadora settings, including default values and user input handling. Update application preferences to support new extraction models and improve overall user experience in data processing.
This commit is contained in:
+87
-19
@@ -13,12 +13,19 @@ import os
|
||||
|
||||
from core.utils import parse_comma_separated, collect_radicado_docs, collect_documents_flat
|
||||
from application.logging_config import configure_logging
|
||||
from application.app_prefs import load_app_prefs, save_app_prefs, normalize_document_types
|
||||
from application.app_prefs import (
|
||||
build_cop_mod_config_for_apply,
|
||||
load_app_prefs,
|
||||
merge_moderadora_prefs,
|
||||
normalize_document_types,
|
||||
parse_moderadora_text,
|
||||
save_app_prefs,
|
||||
)
|
||||
from gui.field_selector import FieldSelectorDialog
|
||||
from application.preclassification import PreclassificationService
|
||||
from application.strategies import OllamaExtractor
|
||||
from application.database import DatabaseManager
|
||||
from application.config import settings
|
||||
from application.config import settings, extractor_credentials_for_model, is_grok_extractor_model
|
||||
from application.costs import MODEL_PRICING
|
||||
from application import postprocess_agent
|
||||
from application import csv_cleaning
|
||||
@@ -28,6 +35,21 @@ import pandas as pd
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Etiquetas antiguas del combo → etiquetas actuales (prefs / estado previo)
|
||||
_LEGACY_SETTINGS_MODEL_LABEL = {
|
||||
"5.4": "gpt-5.4",
|
||||
"5.4-mini": "gpt-5.4-mini",
|
||||
"5.4-nano": "gpt-5.4-nano",
|
||||
"5.2": "gpt-5.4",
|
||||
"gpt-5.2": "gpt-5.4",
|
||||
"4.1-mini": "gpt-4.1-mini",
|
||||
}
|
||||
|
||||
|
||||
def _canonical_settings_model_label(name: str) -> str:
|
||||
s = (name or "").strip()
|
||||
return _LEGACY_SETTINGS_MODEL_LABEL.get(s, s)
|
||||
|
||||
|
||||
class AppController:
|
||||
"""Controlador principal de la GUI que orquesta la vista y el servicio.
|
||||
@@ -123,11 +145,11 @@ class AppController:
|
||||
|
||||
# Mapeo de nombres de UI a IDs de modelo (y viceversa)
|
||||
self.ui_to_model = {
|
||||
"5.4": "gpt-5.4",
|
||||
"5.4-mini": "gpt-5.4-mini",
|
||||
"5.4-nano": "gpt-5.4-nano",
|
||||
"5.2": "gpt-5.2",
|
||||
"4.1-mini": "gpt-4.1-mini",
|
||||
"gpt-5.4": "gpt-5.4",
|
||||
"gpt-5.4-mini": "gpt-5.4-mini",
|
||||
"gpt-5.4-nano": "gpt-5.4-nano",
|
||||
"gpt-4.1-mini": "gpt-4.1-mini",
|
||||
"grok-4.3": "grok-4.3",
|
||||
}
|
||||
self.model_to_ui = {v: k for k, v in self.ui_to_model.items()}
|
||||
|
||||
@@ -168,6 +190,25 @@ class AppController:
|
||||
results.sort(key=lambda x: x["input"], reverse=True)
|
||||
return results
|
||||
|
||||
def _apply_extractor_model(self, real_model_id: str, *, interactive: bool = False) -> bool:
|
||||
"""Aplica modelo y credenciales del extractor (Grok usa GROK_API_KEY / GROK_BASE_URL)."""
|
||||
if not hasattr(self.service.extractor, "update_config"):
|
||||
return True
|
||||
if is_grok_extractor_model(real_model_id):
|
||||
api_key, base_url = extractor_credentials_for_model(real_model_id)
|
||||
if not api_key:
|
||||
msg = "Define GROK_API_KEY en el entorno o en .env para usar modelos Grok."
|
||||
if interactive:
|
||||
mb.showwarning("Configuración", msg)
|
||||
else:
|
||||
logger.warning("Modelo persistido %s no aplicado: %s", real_model_id, msg)
|
||||
return False
|
||||
self.service.extractor.update_config(api_key=api_key, base_url=base_url, model=real_model_id)
|
||||
return True
|
||||
api_key, base_url = extractor_credentials_for_model(real_model_id)
|
||||
self.service.extractor.update_config(api_key=api_key, base_url=base_url, model=real_model_id)
|
||||
return True
|
||||
|
||||
def on_save_settings(self):
|
||||
"""Actualiza la configuración del servicio (modelo LLM y concurrencia)."""
|
||||
selected_name = self.view.get_selected_model()
|
||||
@@ -182,19 +223,19 @@ class AppController:
|
||||
return
|
||||
|
||||
# Traducir nombre UI -> ID modelo real
|
||||
real_model_id = self.ui_to_model.get(selected_name, selected_name)
|
||||
canonical_ui = _canonical_settings_model_label(selected_name)
|
||||
real_model_id = self.ui_to_model.get(canonical_ui, canonical_ui)
|
||||
|
||||
try:
|
||||
# Actualizar extractor
|
||||
if hasattr(self.service.extractor, "update_config"):
|
||||
self.service.extractor.update_config(model=real_model_id)
|
||||
|
||||
if not self._apply_extractor_model(real_model_id, interactive=True):
|
||||
return
|
||||
|
||||
# Actualizar settings globales
|
||||
settings.processing.concurrency_limit = concurrency
|
||||
self.view.set_allowed_document_types_hint(self.view.get_allowed_document_types())
|
||||
self._save_preferences()
|
||||
|
||||
mb.showinfo("Configuración", f"Configuración actualizada.\nModelo: {selected_name}\nConcurrencia: {concurrency}")
|
||||
mb.showinfo("Configuración", f"Configuración actualizada.\nModelo: {canonical_ui}\nConcurrencia: {concurrency}")
|
||||
self.view.append_output_safe(f"Configuración actualizada: Modelo={real_model_id}, Concurrencia={concurrency}")
|
||||
except Exception as e:
|
||||
logger.exception("Error actualizando configuración")
|
||||
@@ -514,6 +555,8 @@ class AppController:
|
||||
"root_path": self.view.get_root_path(),
|
||||
"csv_path": self.view.get_csv_path(),
|
||||
"doc_types": doc_types,
|
||||
"moderadora_by_year": parse_moderadora_text(self.view.get_moderadora_cuotas_text()),
|
||||
"fecha_servicio_column_default": self.view.get_fecha_servicio_column_hint(),
|
||||
}
|
||||
|
||||
def _save_preferences(self) -> None:
|
||||
@@ -529,10 +572,10 @@ class AppController:
|
||||
try:
|
||||
selected_model = str(prefs.get("selected_model") or "").strip()
|
||||
if selected_model:
|
||||
self.view.set_selected_model(selected_model)
|
||||
real_model_id = self.ui_to_model.get(selected_model, selected_model)
|
||||
if hasattr(self.service.extractor, "update_config"):
|
||||
self.service.extractor.update_config(model=real_model_id)
|
||||
canonical_ui = _canonical_settings_model_label(selected_model)
|
||||
self.view.set_selected_model(canonical_ui)
|
||||
real_model_id = self.ui_to_model.get(canonical_ui, canonical_ui)
|
||||
self._apply_extractor_model(real_model_id, interactive=False)
|
||||
except Exception:
|
||||
logger.exception("No se pudo restaurar el modelo persistido")
|
||||
|
||||
@@ -581,6 +624,15 @@ class AppController:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
merged = merge_moderadora_prefs(prefs.get("moderadora_by_year"))
|
||||
self.view.set_moderadora_cuotas_ui(
|
||||
merged,
|
||||
str(prefs.get("fecha_servicio_column_default") or ""),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def on_app_close(self):
|
||||
"""Guarda preferencias antes de cerrar la ventana."""
|
||||
try:
|
||||
@@ -1246,8 +1298,8 @@ class AppController:
|
||||
pass
|
||||
|
||||
def _finish_cleaning_csv_load(self, df: pd.DataFrame, summaries):
|
||||
self.view.populate_cleaning_columns(summaries)
|
||||
self.view.set_cleaning_preview(df)
|
||||
self.view.populate_cleaning_columns(summaries)
|
||||
self.view.set_cleaning_status(
|
||||
f"CSV cargado: {len(df):,} filas x {len(df.columns)} columnas."
|
||||
)
|
||||
@@ -1281,6 +1333,15 @@ class AppController:
|
||||
)
|
||||
return
|
||||
|
||||
fecha_map = self.view.get_cleaning_cop_mod_fecha_columns()
|
||||
for col_name, rule_ids in rules.items():
|
||||
if "cop_mod_split" in rule_ids and col_name not in fecha_map:
|
||||
mb.showwarning(
|
||||
"COP/MOD",
|
||||
f"Marque «Separa COP/MOD» y elija la columna «Fecha Servicio» para «{col_name}».",
|
||||
)
|
||||
return
|
||||
|
||||
self.view.enable_cleaning_controls(False)
|
||||
self.view.enable_cleaning_save(False)
|
||||
self.view.set_cleaning_status("Aplicando reglas...")
|
||||
@@ -1290,7 +1351,14 @@ class AppController:
|
||||
|
||||
def worker():
|
||||
try:
|
||||
result_df, log = csv_cleaning.apply_rules(df, rules)
|
||||
cop_cfg = build_cop_mod_config_for_apply()
|
||||
fecha_map = self.view.get_cleaning_cop_mod_fecha_columns()
|
||||
result_df, log = csv_cleaning.apply_rules(
|
||||
df,
|
||||
rules,
|
||||
cop_mod_fecha_column=fecha_map or None,
|
||||
cop_mod_config=cop_cfg,
|
||||
)
|
||||
self._cleaning_result_df = result_df
|
||||
ok_count = sum(1 for line in log if line.startswith("[OK]"))
|
||||
err_count = sum(1 for line in log if line.startswith("[ERROR]"))
|
||||
|
||||
@@ -17,6 +17,7 @@ from application.csv_cleaning import (
|
||||
ColumnSummary,
|
||||
applicable_rule_ids_for_type,
|
||||
)
|
||||
from application.app_prefs import load_cop_mod_preferences
|
||||
from gui.styles import AppStyles
|
||||
|
||||
|
||||
@@ -67,6 +68,8 @@ _RULE_SHORT_LABELS: dict[str, str] = {
|
||||
"round_decimals_custom": "Aprox. decimales (>0.5 sube)",
|
||||
"doc_id_split_tipo_numero": "Dividir doc (tipo + cifras)",
|
||||
"doc_type_homologate": "Homologar tipo doc",
|
||||
"cop_mod_split": "Separa COP/MOD",
|
||||
"nota_paciente_autorizacion": "Extraer doc. y autorización (notas)",
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +99,9 @@ class _ColumnUI:
|
||||
rule_vars: list[tuple[str, ctk.CTkCheckBox, ctk.BooleanVar]]
|
||||
summary: ColumnSummary
|
||||
expanded: bool = True
|
||||
cop_mod_fecha_frame: Optional[ctk.CTkFrame] = None
|
||||
cop_mod_fecha_menu: Optional[ctk.CTkOptionMenu] = None
|
||||
cop_mod_fecha_var: Optional[ctk.StringVar] = None
|
||||
|
||||
|
||||
class CleaningTab:
|
||||
@@ -109,6 +115,7 @@ class CleaningTab:
|
||||
|
||||
self._column_uis: dict[str, _ColumnUI] = {}
|
||||
self._search_var: Optional[ctk.StringVar] = None
|
||||
self._csv_column_names: list[str] = []
|
||||
# Tipo de dato forzado por el usuario (id interno) por columna; vacío = inferido
|
||||
self._type_overrides: dict[str, str] = {}
|
||||
self._syncing_rule_vars = False
|
||||
@@ -473,6 +480,7 @@ class CleaningTab:
|
||||
def set_preview(self, df: Optional[pd.DataFrame]):
|
||||
"""Renderiza el DataFrame original en la tabla de vista previa."""
|
||||
if df is None or len(df) == 0:
|
||||
self._csv_column_names = []
|
||||
self._render_table_placeholder(
|
||||
self.preview_table_holder, "(sin datos)",
|
||||
)
|
||||
@@ -489,6 +497,7 @@ class CleaningTab:
|
||||
)
|
||||
self.chip_rows.configure(text=f"Filas: {len(df):,}")
|
||||
self.chip_cols.configure(text=f"Columnas: {len(df.columns)}")
|
||||
self._csv_column_names = list(df.columns)
|
||||
|
||||
def set_result(self, df: Optional[pd.DataFrame], info_text: str = ""):
|
||||
"""Renderiza el DataFrame resultante en la tabla de resultado."""
|
||||
@@ -574,6 +583,56 @@ class CleaningTab:
|
||||
|
||||
self.chip_cols.configure(text=f"Columnas: {len(summaries)}")
|
||||
self._update_global_count()
|
||||
self._refresh_cop_mod_menus()
|
||||
|
||||
def get_cop_mod_fecha_columns(self) -> dict[str, str]:
|
||||
"""Mapa columna de importe -> columna Fecha Servicio (solo si COP/MOD está marcado)."""
|
||||
out: dict[str, str] = {}
|
||||
for col_name, ui in self._column_uis.items():
|
||||
if ui.cop_mod_fecha_var is None:
|
||||
continue
|
||||
cop_on = any(
|
||||
rid == "cop_mod_split" and bool(var.get())
|
||||
for rid, _chk, var in ui.rule_vars
|
||||
)
|
||||
if not cop_on:
|
||||
continue
|
||||
sel = str(ui.cop_mod_fecha_var.get() or "").strip()
|
||||
if sel:
|
||||
out[col_name] = sel
|
||||
return out
|
||||
|
||||
def _date_columns_for_cop_mod_menu(self) -> list[str]:
|
||||
cols = self._csv_column_names or []
|
||||
return cols if cols else [""]
|
||||
|
||||
def _suggest_fecha_column_option(self) -> str:
|
||||
cols = self._csv_column_names or []
|
||||
if not cols:
|
||||
return ""
|
||||
_, hint = load_cop_mod_preferences()
|
||||
if hint and hint in cols:
|
||||
return hint
|
||||
for c in cols:
|
||||
low = c.lower().replace("_", " ")
|
||||
if "fecha" in low and "servicio" in low:
|
||||
return c
|
||||
return cols[0]
|
||||
|
||||
def _refresh_cop_mod_menus(self) -> None:
|
||||
cols = self._date_columns_for_cop_mod_menu()
|
||||
suggest = self._suggest_fecha_column_option()
|
||||
for ui in self._column_uis.values():
|
||||
if ui.cop_mod_fecha_menu is None or ui.cop_mod_fecha_var is None:
|
||||
continue
|
||||
cur = ui.cop_mod_fecha_var.get()
|
||||
ui.cop_mod_fecha_menu.configure(values=cols)
|
||||
if cur and cur in cols:
|
||||
ui.cop_mod_fecha_var.set(cur)
|
||||
elif suggest and suggest in cols:
|
||||
ui.cop_mod_fecha_var.set(suggest)
|
||||
elif cols and cols[0]:
|
||||
ui.cop_mod_fecha_var.set(cols[0])
|
||||
|
||||
def get_selected_rules(self) -> dict[str, list[str]]:
|
||||
"""Devuelve un mapa columna -> lista de rule_ids en el orden visual."""
|
||||
@@ -700,6 +759,30 @@ class CleaningTab:
|
||||
col_in_row = 0
|
||||
row_in_rules += 1
|
||||
|
||||
fec_frame = None
|
||||
fec_menu = None
|
||||
fec_var = None
|
||||
if "cop_mod_split" in ordered_ids:
|
||||
fec_frame = ctk.CTkFrame(body, fg_color="transparent")
|
||||
fec_frame.grid(row=4, column=0, sticky="ew", padx=4, pady=(4, 0))
|
||||
fec_frame.grid_columnconfigure(1, weight=1)
|
||||
ctk.CTkLabel(
|
||||
fec_frame,
|
||||
text="Fecha Servicio (columna):",
|
||||
font=self.fonts["italic_small"],
|
||||
text_color=self.colors["gray_dark"],
|
||||
).grid(row=0, column=0, sticky="w", padx=(0, 8))
|
||||
fec_var = ctk.StringVar(value=self._suggest_fecha_column_option())
|
||||
fec_menu = ctk.CTkOptionMenu(
|
||||
fec_frame,
|
||||
values=self._date_columns_for_cop_mod_menu(),
|
||||
variable=fec_var,
|
||||
font=self.fonts["ui"],
|
||||
width=260,
|
||||
)
|
||||
fec_menu.grid(row=0, column=1, sticky="ew")
|
||||
fec_menu.configure(state="disabled")
|
||||
|
||||
ui = _ColumnUI(
|
||||
card=card,
|
||||
body=body,
|
||||
@@ -710,6 +793,9 @@ class CleaningTab:
|
||||
rules_frame=rules_frame,
|
||||
rule_vars=vars_list,
|
||||
summary=summary,
|
||||
cop_mod_fecha_frame=fec_frame,
|
||||
cop_mod_fecha_menu=fec_menu,
|
||||
cop_mod_fecha_var=fec_var,
|
||||
)
|
||||
toggle_btn.configure(command=lambda c=summary.name: self._toggle_column(c))
|
||||
return ui
|
||||
@@ -910,6 +996,39 @@ class CleaningTab:
|
||||
col_in_row = 0
|
||||
row_in_rules += 1
|
||||
ui.rule_vars = new_vars
|
||||
if ui.cop_mod_fecha_frame is not None:
|
||||
try:
|
||||
ui.cop_mod_fecha_frame.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
ui.cop_mod_fecha_frame = None
|
||||
ui.cop_mod_fecha_menu = None
|
||||
ui.cop_mod_fecha_var = None
|
||||
ordered_after = [r["id"] for r in CLEANING_RULES if r["id"] in applic]
|
||||
if "cop_mod_split" in ordered_after:
|
||||
fec_frame = ctk.CTkFrame(ui.body, fg_color="transparent")
|
||||
fec_frame.grid(row=4, column=0, sticky="ew", padx=4, pady=(4, 0))
|
||||
fec_frame.grid_columnconfigure(1, weight=1)
|
||||
ctk.CTkLabel(
|
||||
fec_frame,
|
||||
text="Fecha Servicio (columna):",
|
||||
font=self.fonts["italic_small"],
|
||||
text_color=self.colors["gray_dark"],
|
||||
).grid(row=0, column=0, sticky="w", padx=(0, 8))
|
||||
fec_var = ctk.StringVar(value=self._suggest_fecha_column_option())
|
||||
fec_menu = ctk.CTkOptionMenu(
|
||||
fec_frame,
|
||||
values=self._date_columns_for_cop_mod_menu(),
|
||||
variable=fec_var,
|
||||
font=self.fonts["ui"],
|
||||
width=260,
|
||||
)
|
||||
fec_menu.grid(row=0, column=1, sticky="ew")
|
||||
fec_menu.configure(state="disabled")
|
||||
ui.cop_mod_fecha_frame = fec_frame
|
||||
ui.cop_mod_fecha_menu = fec_menu
|
||||
ui.cop_mod_fecha_var = fec_var
|
||||
self._refresh_cop_mod_menus()
|
||||
self.set_status(f"Columna «{col_name}»: tipo «{_TYPE_LABELS.get(eff, eff)}».")
|
||||
self._on_rule_toggle(col_name)
|
||||
|
||||
@@ -954,6 +1073,12 @@ class CleaningTab:
|
||||
f"Columna «{col_name}»: se activó homologación de tipo de documento."
|
||||
)
|
||||
break
|
||||
if ui.cop_mod_fecha_menu is not None:
|
||||
cop_on = any(
|
||||
rid == "cop_mod_split" and bool(var.get())
|
||||
for rid, _chk, var in ui.rule_vars
|
||||
)
|
||||
ui.cop_mod_fecha_menu.configure(state="normal" if cop_on else "disabled")
|
||||
selected = sum(1 for _rid, _chk, var in ui.rule_vars if bool(var.get()))
|
||||
text = f"{selected} regla{'s' if selected != 1 else ''}"
|
||||
color = self.colors["success"] if selected else self.colors["gray_light"]
|
||||
|
||||
+123
-16
@@ -1,7 +1,14 @@
|
||||
"""Pestaña de Configuración de la aplicación GUI."""
|
||||
import customtkinter as ctk
|
||||
from gui.styles import AppStyles
|
||||
from application.app_prefs import get_allowed_document_types, normalize_document_types
|
||||
from application.app_prefs import (
|
||||
format_moderadora_text_from_prefs,
|
||||
get_allowed_document_types,
|
||||
load_cop_mod_preferences,
|
||||
normalize_document_types,
|
||||
parse_moderadora_text,
|
||||
default_moderadora_by_year_payload,
|
||||
)
|
||||
|
||||
|
||||
class SettingsTab:
|
||||
@@ -22,16 +29,25 @@ class SettingsTab:
|
||||
self._create_api_section()
|
||||
self._create_processing_section()
|
||||
self._create_document_types_section()
|
||||
self._create_cop_mod_section()
|
||||
self._create_save_button()
|
||||
|
||||
def _setup_layout(self):
|
||||
"""Configura el layout base de la pestaña."""
|
||||
"""Configura el layout: área con scroll y barra inferior fija con Guardar."""
|
||||
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.tab.grid_rowconfigure(1, weight=0)
|
||||
|
||||
self.settings_container = ctk.CTkScrollableFrame(
|
||||
self.tab,
|
||||
fg_color="transparent",
|
||||
)
|
||||
self.settings_container.grid(row=0, column=0, sticky="nsew", padx=20, pady=(20, 8))
|
||||
self.settings_container.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self.save_footer = ctk.CTkFrame(self.tab, fg_color="transparent")
|
||||
self.save_footer.grid(row=1, column=0, sticky="ew", padx=20, pady=(4, 16))
|
||||
self.save_footer.grid_columnconfigure(0, weight=1)
|
||||
|
||||
def _create_api_section(self):
|
||||
"""Crea la sección de configuración del modelo LLM."""
|
||||
@@ -50,25 +66,32 @@ class SettingsTab:
|
||||
# Selector de modelo
|
||||
lbl_model = ctk.CTkLabel(
|
||||
self.frame_api,
|
||||
text="Modelo (Ollama):",
|
||||
text="Modelo (extracción):",
|
||||
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.4", "5.4-mini", "5.4-nano", "5.2", "4.1-mini"],
|
||||
values=[
|
||||
"gpt-5.4",
|
||||
"gpt-5.4-mini",
|
||||
"gpt-5.4-nano",
|
||||
"gpt-4.1-mini",
|
||||
"grok-4.3",
|
||||
],
|
||||
width=250,
|
||||
font=self.fonts["ui"]
|
||||
)
|
||||
self.combo_model.grid(row=1, column=1, padx=15, pady=10, sticky="w")
|
||||
self.combo_model.set("5.4")
|
||||
self.combo_model.set("gpt-5.4")
|
||||
|
||||
# Texto de ayuda
|
||||
help_text = (
|
||||
"ℹ 5.4: Modelo predeterminado\n"
|
||||
"ℹ 5.4-mini / 4.1-mini: opciones más rápidas\n"
|
||||
"ℹ 5.4-nano: opción más económica"
|
||||
"ℹ gpt-5.4: Modelo predeterminado (OpenAI / compatible)\n"
|
||||
"ℹ gpt-5.4-mini / gpt-4.1-mini: opciones más rápidas\n"
|
||||
"ℹ gpt-5.4-nano: opción más económica\n"
|
||||
"ℹ grok-4.3: API xAI (requiere GROK_API_KEY en .env)"
|
||||
)
|
||||
lbl_help = ctk.CTkLabel(
|
||||
self.frame_api,
|
||||
@@ -151,16 +174,84 @@ class SettingsTab:
|
||||
)
|
||||
self.allowed_doc_types_entry.grid(row=2, column=0, sticky="ew", padx=15, pady=(0, 10))
|
||||
self.set_allowed_document_types(list(get_allowed_document_types()))
|
||||
|
||||
def _create_cop_mod_section(self):
|
||||
"""Cuotas moderadora por año (regla Separa COP/MOD)."""
|
||||
self.frame_cop_mod = ctk.CTkFrame(self.settings_container)
|
||||
self.frame_cop_mod.grid(row=3, column=0, sticky="ew", padx=5, pady=5)
|
||||
self.frame_cop_mod.grid_columnconfigure(0, weight=1)
|
||||
|
||||
lbl_title = ctk.CTkLabel(
|
||||
self.frame_cop_mod,
|
||||
text="Cuota moderadora (COP/MOD)",
|
||||
font=self.fonts["section"],
|
||||
)
|
||||
lbl_title.grid(row=0, column=0, sticky="w", padx=15, pady=(10, 4))
|
||||
|
||||
lbl_help = ctk.CTkLabel(
|
||||
self.frame_cop_mod,
|
||||
text=(
|
||||
"Una línea por año: 2024: 4500, 18200, 47700. "
|
||||
"La columna «Fecha Servicio» se elige en Proceso de Limpieza al activar la regla."
|
||||
),
|
||||
font=self.fonts["italic_small"],
|
||||
text_color=self.colors["gray_light"],
|
||||
justify="left",
|
||||
)
|
||||
lbl_help.grid(row=1, column=0, sticky="w", padx=15, pady=(0, 6))
|
||||
|
||||
merged, fecha_def = load_cop_mod_preferences()
|
||||
initial_txt = format_moderadora_text_from_prefs(merged)
|
||||
|
||||
self.moderadora_text = ctk.CTkTextbox(
|
||||
self.frame_cop_mod,
|
||||
font=self.fonts["mono_small"],
|
||||
height=140,
|
||||
)
|
||||
self.moderadora_text.grid(row=2, column=0, sticky="ew", padx=15, pady=(0, 8))
|
||||
self.moderadora_text.insert("1.0", initial_txt)
|
||||
|
||||
row_fe = ctk.CTkFrame(self.frame_cop_mod, fg_color="transparent")
|
||||
row_fe.grid(row=3, column=0, sticky="ew", padx=15, pady=(0, 6))
|
||||
row_fe.grid_columnconfigure(1, weight=1)
|
||||
|
||||
ctk.CTkLabel(
|
||||
row_fe,
|
||||
text="Sugerencia nombre columna fecha (opcional):",
|
||||
font=self.fonts["ui"],
|
||||
).grid(row=0, column=0, padx=(0, 10), sticky="w")
|
||||
|
||||
self.fecha_column_default_var = ctk.StringVar(value=fecha_def)
|
||||
self.fecha_column_default_entry = ctk.CTkEntry(
|
||||
row_fe,
|
||||
textvariable=self.fecha_column_default_var,
|
||||
font=self.fonts["ui"],
|
||||
placeholder_text="Fecha Servicio",
|
||||
)
|
||||
self.fecha_column_default_entry.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
self.btn_moderadora_defaults = ctk.CTkButton(
|
||||
self.frame_cop_mod,
|
||||
text="Restaurar valores por defecto",
|
||||
font=self.fonts["ui"],
|
||||
command=self._on_restore_moderadora_defaults,
|
||||
)
|
||||
self.btn_moderadora_defaults.grid(row=4, column=0, sticky="w", padx=15, pady=(0, 12))
|
||||
|
||||
def _on_restore_moderadora_defaults(self) -> None:
|
||||
txt = format_moderadora_text_from_prefs(default_moderadora_by_year_payload())
|
||||
self.moderadora_text.delete("1.0", "end")
|
||||
self.moderadora_text.insert("1.0", txt)
|
||||
|
||||
def _create_save_button(self):
|
||||
"""Crea el botón de guardar configuración."""
|
||||
"""Barra inferior fija: siempre visible aunque el formulario sea largo."""
|
||||
self.btn_save_settings = ctk.CTkButton(
|
||||
self.settings_container,
|
||||
text="Aplicar Configuración",
|
||||
self.save_footer,
|
||||
text="Guardar cambios",
|
||||
font=self.fonts["ui"],
|
||||
height=35
|
||||
height=38,
|
||||
)
|
||||
self.btn_save_settings.grid(row=3, column=0, padx=15, pady=15, sticky="ew")
|
||||
self.btn_save_settings.grid(row=0, column=0, sticky="ew")
|
||||
|
||||
# --- API Pública ---
|
||||
|
||||
@@ -197,6 +288,19 @@ class SettingsTab:
|
||||
"""Establece la lista de tipos documentales válidos."""
|
||||
self.allowed_doc_types_entry.delete(0, "end")
|
||||
self.allowed_doc_types_entry.insert(0, ", ".join(normalize_document_types(values)))
|
||||
|
||||
def get_moderadora_cuotas_text(self) -> str:
|
||||
"""Texto multilínea año: valores."""
|
||||
return self.moderadora_text.get("1.0", "end").strip()
|
||||
|
||||
def set_moderadora_cuotas_ui(self, payload: dict[str, list[int]], fecha_hint: str) -> None:
|
||||
"""Establece cuotas y sugerencia de columna fecha desde prefs."""
|
||||
self.moderadora_text.delete("1.0", "end")
|
||||
self.moderadora_text.insert("1.0", format_moderadora_text_from_prefs(payload))
|
||||
self.fecha_column_default_var.set(fecha_hint or "")
|
||||
|
||||
def get_fecha_servicio_column_hint(self) -> str:
|
||||
return str(self.fecha_column_default_var.get() or "").strip()
|
||||
|
||||
def enable_controls(self, enabled: bool):
|
||||
"""Habilita o deshabilita los controles."""
|
||||
@@ -204,4 +308,7 @@ class SettingsTab:
|
||||
self.combo_model.configure(state=state)
|
||||
self.concurrency_entry.configure(state=state)
|
||||
self.allowed_doc_types_entry.configure(state=state)
|
||||
self.moderadora_text.configure(state=state)
|
||||
self.fecha_column_default_entry.configure(state=state)
|
||||
self.btn_moderadora_defaults.configure(state=state)
|
||||
self.btn_save_settings.configure(state=state)
|
||||
|
||||
+12
@@ -119,6 +119,15 @@ class InvoiceAppView(ctk.CTk):
|
||||
def set_allowed_document_types(self, values: list[str]) -> None:
|
||||
self.settings_tab.set_allowed_document_types(values)
|
||||
|
||||
def get_moderadora_cuotas_text(self) -> str:
|
||||
return self.settings_tab.get_moderadora_cuotas_text()
|
||||
|
||||
def set_moderadora_cuotas_ui(self, payload: dict, fecha_hint: str) -> None:
|
||||
self.settings_tab.set_moderadora_cuotas_ui(payload, fecha_hint)
|
||||
|
||||
def get_fecha_servicio_column_hint(self) -> str:
|
||||
return self.settings_tab.get_fecha_servicio_column_hint()
|
||||
|
||||
def set_on_run_preclassification(self, callback):
|
||||
self.preclass_tab.set_on_run(callback)
|
||||
|
||||
@@ -387,6 +396,9 @@ class InvoiceAppView(ctk.CTk):
|
||||
def get_cleaning_selected_rules(self) -> dict[str, list[str]]:
|
||||
return self.cleaning_tab.get_selected_rules()
|
||||
|
||||
def get_cleaning_cop_mod_fecha_columns(self) -> dict[str, str]:
|
||||
return self.cleaning_tab.get_cop_mod_fecha_columns()
|
||||
|
||||
def set_cleaning_result(self, df, info_text: str = ""):
|
||||
self.cleaning_tab.set_result(df, info_text)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user