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
+87 -19
View File
@@ -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]"))
+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)
+12
View File
@@ -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)