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:
erik
2026-05-13 01:55:32 -05:00
parent e2536380d4
commit 6ee6df5469
17 changed files with 1031 additions and 54 deletions
+125
View File
@@ -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
View File
@@ -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)