feat: Enhance CSV cleaning and management functionalities by adding new date normalization rules, improving document type homologation, and implementing robust sanitization for CSV cell values. Update GUI components to support loading indicators and status messages during CSV processing, ensuring a smoother user experience.

This commit is contained in:
erik
2026-05-04 01:54:06 -05:00
parent 37d32a5a96
commit e2536380d4
7 changed files with 864 additions and 72 deletions
+72 -5
View File
@@ -56,15 +56,17 @@ _TYPE_MENU_VALUES: list[str] = [
]
_RULE_SHORT_LABELS: dict[str, str] = {
"strip_special_spaces": "Sin especiales/espacios",
"strip_special_keep_date_seps": "Símbolos: conserva /, -, espacios",
"strip_special_spaces": "Sin especiales/espacios (no antes de fecha+hora)",
"strip_special_keep_date_seps": "Símbolos: conserva /, -, :, espacios",
"strip_spaces": "Sin espacios",
"to_integer": "A entero",
"date_to_ddmmyyyy": "Normalizar fecha → DD/MM/AAAA",
"date_numeric_to_ddmmyyyy": "Fecha numérica → DD/MM/AAAA",
"date_alpha_to_ddmmyyyy": "Fecha alfanumérica → DD/MM/AAAA",
"date_messy_text_to_ddmmyyyy": "Fecha con ruido (texto) → DD/MM/AAAA",
"round_decimals_custom": "Aprox. decimales (>0.5 sube)",
"doc_id_split_tipo_numero": "Dividir doc (tipo + cifras)",
"doc_type_homologate": "Homologar tipo doc",
}
@@ -109,6 +111,7 @@ class CleaningTab:
self._search_var: Optional[ctk.StringVar] = None
# 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
self._setup_layout()
self._create_file_section()
@@ -163,6 +166,27 @@ class CleaningTab:
self.chip_delim = self._make_chip(chips_frame, "Delim: —")
self.chip_delim.pack(side="left")
self.loading_frame = ctk.CTkFrame(frame, fg_color="transparent")
self.loading_frame.grid(
row=2, column=0, columnspan=4, sticky="ew", padx=15, pady=(0, 12)
)
self.loading_frame.grid_columnconfigure(1, weight=1)
self.loading_label = ctk.CTkLabel(
self.loading_frame,
text="",
font=self.fonts["italic_small"],
text_color=self.colors["gray_light"],
anchor="w",
)
self.loading_label.grid(row=0, column=0, sticky="w", padx=(0, 10))
self.loading_progress = ctk.CTkProgressBar(
self.loading_frame,
mode="indeterminate",
height=8,
)
self.loading_progress.grid(row=0, column=1, sticky="ew")
self.loading_frame.grid_remove()
def _make_chip(self, parent, text: str) -> ctk.CTkLabel:
return ctk.CTkLabel(
parent, text=text, font=self.fonts["italic_small"],
@@ -429,6 +453,23 @@ class CleaningTab:
def set_status(self, text: str):
self.lbl_status.configure(text=text)
def set_loading(self, loading: bool, text: str = ""):
"""Muestra u oculta una barra indeterminada para operaciones de CSV."""
if loading:
self.loading_label.configure(text=text or "Procesando CSV...")
self.loading_frame.grid()
try:
self.loading_progress.start()
except Exception:
pass
else:
try:
self.loading_progress.stop()
except Exception:
pass
self.loading_label.configure(text="")
self.loading_frame.grid_remove()
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:
@@ -643,7 +684,10 @@ class CleaningTab:
if rule is None:
continue
var = ctk.BooleanVar(value=False)
var.trace_add("write", lambda *_a, c=summary.name: self._on_rule_toggle(c))
var.trace_add(
"write",
lambda *_a, c=summary.name, r=rid: self._on_rule_toggle(c, r),
)
label = _RULE_SHORT_LABELS.get(rid, rule["name"])
chk = ctk.CTkCheckBox(
rules_frame, text=label, variable=var,
@@ -850,7 +894,10 @@ class CleaningTab:
if rule is None:
continue
var = ctk.BooleanVar(value=False)
var.trace_add("write", lambda *_a, c=col_name: self._on_rule_toggle(c))
var.trace_add(
"write",
lambda *_a, c=col_name, r=rid: self._on_rule_toggle(c, r),
)
label = _RULE_SHORT_LABELS.get(rid, rule["name"])
chk = ctk.CTkCheckBox(
ui.rules_frame, text=label, variable=var,
@@ -883,10 +930,30 @@ class CleaningTab:
if ui.expanded != expanded:
self._toggle_column(col_name)
def _on_rule_toggle(self, col_name: str):
def _on_rule_toggle(self, col_name: str, changed_rule_id: Optional[str] = None):
ui = self._column_uis.get(col_name)
if ui is None:
return
if (
changed_rule_id == "doc_id_split_tipo_numero"
and not self._syncing_rule_vars
):
split_selected = any(
rid == "doc_id_split_tipo_numero" and bool(var.get())
for rid, _chk, var in ui.rule_vars
)
if split_selected:
for rid, _chk, var in ui.rule_vars:
if rid == "doc_type_homologate" and not bool(var.get()):
self._syncing_rule_vars = True
try:
var.set(True)
finally:
self._syncing_rule_vars = False
self.set_status(
f"Columna «{col_name}»: se activó homologación de tipo de documento."
)
break
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"]