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:
+52
-16
@@ -1201,35 +1201,71 @@ class AppController:
|
||||
if not filepath:
|
||||
return
|
||||
|
||||
try:
|
||||
df, delimiter = csv_cleaning.load_csv(filepath)
|
||||
except Exception as e:
|
||||
logger.exception("Error leyendo CSV para limpieza")
|
||||
mb.showerror("Error", f"No se pudo leer el CSV:\n{e}")
|
||||
return
|
||||
|
||||
self._cleaning_df = df
|
||||
self._cleaning_result_df = None
|
||||
self.view.set_cleaning_csv_path(filepath)
|
||||
self.view.enable_cleaning_controls(False)
|
||||
self.view.enable_cleaning_save(False)
|
||||
self.view.set_cleaning_result(None)
|
||||
self.view.set_cleaning_preview(None)
|
||||
self.view.set_cleaning_status("Cargando CSV...")
|
||||
try:
|
||||
self.view.set_cleaning_loading(True, "Leyendo archivo CSV...")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def worker():
|
||||
try:
|
||||
df, delimiter = csv_cleaning.load_csv(filepath)
|
||||
self.view.after(
|
||||
0,
|
||||
lambda d=df, delim=delimiter: self._on_cleaning_csv_loaded(
|
||||
d, delim
|
||||
),
|
||||
)
|
||||
summaries = csv_cleaning.analyze_dataframe(df)
|
||||
self._cleaning_df = df
|
||||
self.view.after(
|
||||
0,
|
||||
lambda d=df, s=summaries: self._finish_cleaning_csv_load(d, s),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("Error leyendo o analizando CSV para limpieza")
|
||||
err = str(e)
|
||||
self.view.after(0, lambda m=err: self._fail_cleaning_csv_load(m))
|
||||
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def _on_cleaning_csv_loaded(self, df: pd.DataFrame, delimiter: str):
|
||||
self.view.set_cleaning_status(
|
||||
f"CSV cargado: {len(df):,} filas x {len(df.columns)} columnas."
|
||||
f"CSV leído: {len(df):,} filas x {len(df.columns)} columnas. Analizando columnas..."
|
||||
)
|
||||
try:
|
||||
self.view.set_cleaning_loading(True, "Analizando columnas...")
|
||||
self.view.set_cleaning_delimiter_chip(delimiter)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
summaries = csv_cleaning.analyze_dataframe(df)
|
||||
except Exception as e:
|
||||
logger.exception("Error analizando CSV para limpieza")
|
||||
mb.showerror("Error", f"No se pudo analizar el CSV:\n{e}")
|
||||
return
|
||||
|
||||
def _finish_cleaning_csv_load(self, df: pd.DataFrame, summaries):
|
||||
self.view.populate_cleaning_columns(summaries)
|
||||
self.view.set_cleaning_preview(df)
|
||||
self.view.set_cleaning_status(
|
||||
f"CSV cargado: {len(df):,} filas x {len(df.columns)} columnas."
|
||||
)
|
||||
try:
|
||||
self.view.set_cleaning_loading(False)
|
||||
except Exception:
|
||||
pass
|
||||
self.view.enable_cleaning_controls(True)
|
||||
|
||||
def _fail_cleaning_csv_load(self, message: str):
|
||||
self._cleaning_df = None
|
||||
self.view.set_cleaning_status("Error al cargar CSV")
|
||||
try:
|
||||
self.view.set_cleaning_loading(False)
|
||||
except Exception:
|
||||
pass
|
||||
self.view.enable_cleaning_controls(True)
|
||||
mb.showerror("Error", f"No se pudo cargar o analizar el CSV:\n{message}")
|
||||
|
||||
def on_cleaning_apply(self):
|
||||
"""Aplica las reglas seleccionadas en un hilo y actualiza el resultado."""
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -399,5 +399,8 @@ class InvoiceAppView(ctk.CTk):
|
||||
def set_cleaning_status(self, text: str):
|
||||
self.cleaning_tab.set_status(text)
|
||||
|
||||
def set_cleaning_loading(self, loading: bool, text: str = ""):
|
||||
self.cleaning_tab.set_loading(loading, text)
|
||||
|
||||
def set_cleaning_delimiter_chip(self, delimiter: str):
|
||||
self.cleaning_tab.set_delimiter_chip(delimiter)
|
||||
|
||||
Reference in New Issue
Block a user