g360-cli 1.6.1 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,221 @@
1
+ import threading
2
+ from pathlib import Path
3
+
4
+ import flet as ft
5
+ import pandas as pd
6
+
7
+ try:
8
+ from g360_core.ingestion import estabilizar_excel_crudo
9
+ except ImportError:
10
+ from core.ingestion import estabilizar_excel_crudo
11
+
12
+
13
+ class IngestionPanel(ft.Container):
14
+ def __init__(self, theme, on_data_loaded=None):
15
+ super().__init__()
16
+ self.theme = theme
17
+ self.on_data_loaded = on_data_loaded
18
+ self.df: pd.DataFrame | None = None
19
+ self.metadata: dict | None = None
20
+ self._build()
21
+
22
+ def _build(self):
23
+ self.file_picker = ft.FilePicker(on_result=self._on_file_result)
24
+ self.status_text = ft.Text(
25
+ "Selecciona un archivo .xls o .xlsx del ERP",
26
+ size=14,
27
+ color=self.theme.muted,
28
+ )
29
+ self.stats_container = ft.Column(spacing=4, visible=False)
30
+ self.alertas_container = ft.Column(spacing=2, visible=False)
31
+ self.preview_table = ft.Column(scroll=ft.ScrollMode.AUTO, visible=False)
32
+
33
+ self.content = ft.Column(
34
+ controls=[
35
+ self.file_picker,
36
+ ft.Row(
37
+ controls=[
38
+ self.theme.accent_button(
39
+ text="Cargar Archivo Excel",
40
+ on_click=self._open_picker,
41
+ ),
42
+ ft.Container(width=12),
43
+ self.status_text,
44
+ ],
45
+ alignment=ft.MainAxisAlignment.START,
46
+ vertical_alignment=ft.CrossAxisAlignment.CENTER,
47
+ ),
48
+ ft.Container(height=12),
49
+ self.stats_container,
50
+ ft.Container(height=8),
51
+ self.alertas_container,
52
+ ft.Container(height=8),
53
+ self.preview_table,
54
+ ],
55
+ spacing=0,
56
+ )
57
+ self.bgcolor = self.theme.surface
58
+ self.border_radius = self.theme.rounded
59
+ self.padding = 20
60
+ self.expand = True
61
+
62
+ def _open_picker(self, e):
63
+ self.file_picker.pick_files(
64
+ file_type=ft.FilePickerFileType.CUSTOM,
65
+ allowed_extensions=["xls", "xlsx", "xlsm"],
66
+ dialog_title="Seleccionar reporte del ERP",
67
+ )
68
+
69
+ def _on_file_result(self, e: ft.FilePickerResultEvent):
70
+ if not e.files:
71
+ self.status_text.value = "No se selecciono ningun archivo."
72
+ self.status_text.color = self.theme.warning
73
+ self.update()
74
+ return
75
+
76
+ archivo = e.files[0]
77
+ ruta = archivo.path
78
+ nombre = archivo.name
79
+ self.status_text.value = f"Procesando: {nombre}..."
80
+ self.status_text.color = self.theme.text
81
+ self.update()
82
+
83
+ self._show_loading(True)
84
+
85
+ def _procesar():
86
+ try:
87
+ df, metadata = estabilizar_excel_crudo(ruta)
88
+ self.df = df
89
+ self.metadata = metadata
90
+ self._mostrar_resultado(df, metadata, nombre)
91
+ if self.on_data_loaded:
92
+ self.on_data_loaded(df, metadata)
93
+ except Exception as exc:
94
+ self.status_text.value = f"Error: {exc}"
95
+ self.status_text.color = self.theme.error
96
+ self._show_loading(False)
97
+ self.update()
98
+
99
+ thread = threading.Thread(target=_procesar, daemon=True)
100
+ thread.start()
101
+
102
+ def _show_loading(self, visible: bool):
103
+ pass
104
+
105
+ def _mostrar_resultado(self, df: pd.DataFrame, metadata: dict, nombre: str):
106
+ self.status_text.value = f"OK: {nombre} ({len(df):,} filas)"
107
+ self.status_text.color = self.theme.success
108
+
109
+ columnas = metadata.get("columnas_finales", metadata.get("columnas", []))
110
+ transformaciones = metadata.get("transformaciones", [])
111
+ alertas = metadata.get("alertas", [])
112
+ columnas_nuevas = metadata.get("columnas_nuevas", [])
113
+
114
+ self.stats_container.controls = [
115
+ ft.Text("Resumen de Ingesta", size=16, weight=ft.FontWeight.BOLD, color=self.theme.text),
116
+ ft.Text(f"Filas estabilizadas: {len(df):,}", size=13, color=self.theme.muted),
117
+ ft.Text(f"Columnas originales: {metadata.get('filas_originales', 0)}", size=13, color=self.theme.muted),
118
+ ft.Text(f"Columnas finales: {len(columnas)}", size=13, color=self.theme.muted),
119
+ ft.Text(f"Archivo: {metadata.get('archivo', 'N/A')}", size=13, color=self.theme.muted),
120
+ ft.Text(f"Moneda: {metadata.get('moneda', 'N/A')}", size=13, color=self.theme.muted),
121
+ ft.Divider(color=self.theme.bg, height=8),
122
+ ]
123
+
124
+ if columnas_nuevas:
125
+ nuevas_text = ft.Text(
126
+ "Columnas derivadas:",
127
+ size=13, weight=ft.FontWeight.BOLD, color=self.theme.accent,
128
+ )
129
+ chips = ft.Row(
130
+ controls=[
131
+ ft.Container(
132
+ content=ft.Text(col, size=10, color=self.theme.bg),
133
+ bgcolor=self.theme.accent,
134
+ border_radius=12,
135
+ padding=ft.padding.only(left=8, right=8, top=3, bottom=3),
136
+ )
137
+ for col in columnas_nuevas
138
+ ],
139
+ spacing=6,
140
+ wrap=True,
141
+ )
142
+ self.stats_container.controls.extend([nuevas_text, chips])
143
+
144
+ if transformaciones:
145
+ self.stats_container.controls.append(
146
+ ft.Divider(color=self.theme.bg, height=8)
147
+ )
148
+ self.stats_container.controls.append(
149
+ ft.Text("Transformaciones:", size=13, weight=ft.FontWeight.BOLD, color=self.theme.text)
150
+ )
151
+ for t in transformaciones[-8:]:
152
+ self.stats_container.controls.append(
153
+ ft.Row(
154
+ controls=[
155
+ ft.Container(
156
+ content=ft.Text(">", size=11, color=self.theme.accent),
157
+ width=16,
158
+ ),
159
+ ft.Text(t, size=11, color=self.theme.muted),
160
+ ],
161
+ spacing=0,
162
+ )
163
+ )
164
+
165
+ self.stats_container.visible = True
166
+
167
+ if alertas:
168
+ self.alertas_container.controls = [
169
+ ft.Divider(color=self.theme.bg, height=4),
170
+ ft.Text("Alertas:", size=13, weight=ft.FontWeight.BOLD, color=self.theme.warning),
171
+ ]
172
+ for a in alertas:
173
+ self.alertas_container.controls.append(
174
+ ft.Row(
175
+ controls=[
176
+ ft.Icon(ft.icons.WARNING_AMBER_ROUNDED, size=14, color=self.theme.warning),
177
+ ft.Text(a, size=11, color=self.theme.warning),
178
+ ],
179
+ spacing=4,
180
+ )
181
+ )
182
+ self.alertas_container.visible = True
183
+
184
+ filas_preview = df.head(5)
185
+ columnas_preview = columnas[:8]
186
+
187
+ data_rows = []
188
+ for _, row in filas_preview.iterrows():
189
+ cells = []
190
+ for col in columnas_preview:
191
+ val = row.get(col, "")
192
+ v_str = str(val)[:30] if not pd.isna(val) else "-"
193
+ cells.append(
194
+ ft.DataCell(ft.Text(v_str, size=10, color=self.theme.text))
195
+ )
196
+ data_rows.append(ft.DataRow(cells=cells))
197
+
198
+ header_cells = [
199
+ ft.DataColumn(ft.Text(col[:16], size=10, color=self.theme.accent, weight=ft.FontWeight.BOLD))
200
+ for col in columnas_preview
201
+ ]
202
+
203
+ tabla = ft.DataTable(
204
+ columns=header_cells,
205
+ rows=data_rows,
206
+ heading_text_color=self.theme.accent,
207
+ horizontal_margin=4,
208
+ column_spacing=16,
209
+ )
210
+
211
+ self.preview_table.controls = [
212
+ ft.Text("Vista previa (5 primeras filas):", size=13, weight=ft.FontWeight.BOLD, color=self.theme.text),
213
+ ft.Container(
214
+ content=tabla,
215
+ bgcolor=self.theme.bg,
216
+ border_radius=8,
217
+ padding=8,
218
+ ),
219
+ ]
220
+ self.preview_table.visible = True
221
+ self.update()
@@ -10,6 +10,8 @@ dependencies = [
10
10
  "flet>=0.25.0",
11
11
  "openpyxl>=3.1.0",
12
12
  "pandas>=2.0.0",
13
+ "xlrd>=2.0.0",
14
+ "g360-core>=0.1.0",
13
15
  ]
14
16
 
15
17
  [project.optional-dependencies]