g360-cli 1.6.1 → 1.7.0
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.
- package/README.md +71 -10
- package/package.json +1 -1
- package/py/pyproject.toml +34 -0
- package/py/src/g360_core/__init__.py +7 -0
- package/py/src/g360_core/flet/__init__.py +3 -0
- package/py/src/g360_core/flet/ingestion_panel.py +218 -0
- package/py/src/g360_core/ingestion.py +480 -0
- package/src/assets/ingestion/core/__init__.py +0 -0
- package/src/assets/ingestion/core/ingestion.py +480 -0
- package/src/assets/ingestion/ui/__init__.py +0 -0
- package/src/assets/ingestion/ui/ingestion_panel.py +221 -0
- package/src/assets/templates/python-flet/pyproject.toml +2 -0
- package/src/assets/templates/python-flet/src/core/ingestion.py +480 -0
- package/src/assets/templates/python-flet/src/main.py +22 -3
- package/src/assets/templates/python-flet/src/test_ingestion.py +121 -0
- package/src/assets/templates/python-flet/src/ui/ingestion_panel.py +221 -0
- package/src/commands/bring.js +87 -0
- package/src/commands/list.js +18 -1
package/README.md
CHANGED
|
@@ -54,6 +54,8 @@ CLI tool para el ecosistema G360 que permite inicializar proyectos con estructur
|
|
|
54
54
|
|
|
55
55
|
- **Inicialización rápida** - Crea proyectos G360 con estructura estándar
|
|
56
56
|
- **Gestión de assets** - Trae componentes, skills y plantillas embebidas
|
|
57
|
+
- **Ingesta ERP** - Normaliza `.xls/.xlsx` de SAP, StarSoft, Spring con `g360 bring ingestion`
|
|
58
|
+
- **Paquete Python** - `g360-core` en PyPI para pipelines de datos independientes
|
|
57
59
|
- **Auditoría** - Verifica compliance de proyectos G360
|
|
58
60
|
- **Limpieza** - Elimina assets embebidos antes de deployment
|
|
59
61
|
- **Multi-plantilla** - Web (Lit, Solid, Svelte, React), Python (CLI, Flet, CustomTkinter), VBA Excel
|
|
@@ -270,6 +272,41 @@ g360 bring skills
|
|
|
270
272
|
|
|
271
273
|
# Traer engine específico
|
|
272
274
|
g360 bring engine/g360-skill-audit
|
|
275
|
+
|
|
276
|
+
# Traer ingestion module ERP a proyecto Flet existente
|
|
277
|
+
g360 bring ingestion
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### `g360 bring ingestion`
|
|
281
|
+
|
|
282
|
+
Instala el módulo de normalización de datos ERP en proyectos Flet.
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
g360 bring ingestion [opciones]
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Opciones:**
|
|
289
|
+
|
|
290
|
+
| Opción | Descripción |
|
|
291
|
+
|--------|-------------|
|
|
292
|
+
| `-p, --path <ruta>` | Ruta al proyecto |
|
|
293
|
+
| `--dry-run` | Previsualizar |
|
|
294
|
+
| `--force` | Sobrescribir archivos existentes |
|
|
295
|
+
|
|
296
|
+
**Archivos instalados:**
|
|
297
|
+
- `src/core/ingestion.py` — Normalización de datos (estabilizar_excel_crudo)
|
|
298
|
+
- `src/ui/ingestion_panel.py` — Panel Flet para carga de archivos
|
|
299
|
+
|
|
300
|
+
**Importación (auto-detects pip → local):**
|
|
301
|
+
```python
|
|
302
|
+
# Si g360-core está instalado via pip → from g360_core.ingestion import ...
|
|
303
|
+
# Si no → from core.ingestion import ...
|
|
304
|
+
from ui.ingestion_panel import IngestionPanel
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
El paquete pip acompañante `g360-core` se publica en PyPI:
|
|
308
|
+
```bash
|
|
309
|
+
pip install g360-core
|
|
273
310
|
```
|
|
274
311
|
|
|
275
312
|
---
|
|
@@ -289,6 +326,7 @@ g360 list [tipo] [opciones]
|
|
|
289
326
|
| `templates` | Lista de plantillas |
|
|
290
327
|
| `components` | Lista de componentes |
|
|
291
328
|
| `skills` | Lista de skills |
|
|
329
|
+
| `ingestion` | Módulo de ingesta ERP |
|
|
292
330
|
| `all` | Todo (por defecto) |
|
|
293
331
|
|
|
294
332
|
**Ejemplos:**
|
|
@@ -530,20 +568,34 @@ mi-cli/
|
|
|
530
568
|
|
|
531
569
|
### python-flet
|
|
532
570
|
|
|
533
|
-
Plantilla GUI de escritorio con **Flet**
|
|
571
|
+
Plantilla GUI de escritorio con **Flet** para apps de contexto ERP.
|
|
534
572
|
|
|
535
573
|
```
|
|
536
574
|
mi-app/
|
|
537
575
|
├── src/
|
|
538
|
-
│ ├── main.py
|
|
539
|
-
│
|
|
540
|
-
│
|
|
541
|
-
├──
|
|
576
|
+
│ ├── main.py ← Punto de entrada con IngestionPanel integrado
|
|
577
|
+
│ ├── core/
|
|
578
|
+
│ │ ├── ingestion.py ← Normalizador de datos ERP
|
|
579
|
+
│ │ ├── g360_theme.py ← Tema visual G360
|
|
580
|
+
│ │ └── skill.json
|
|
581
|
+
│ ├── ui/
|
|
582
|
+
│ │ └── ingestion_panel.py ← Panel Flet para carga .xls/.xlsx
|
|
583
|
+
│ └── export/
|
|
584
|
+
│ └── __init__.py
|
|
585
|
+
├── pyproject.toml ← Dependencias: flet, pandas, g360-core
|
|
542
586
|
├── run.bat
|
|
543
|
-
├── build
|
|
587
|
+
├── build.bat
|
|
544
588
|
└── skill.json
|
|
545
589
|
```
|
|
546
590
|
|
|
591
|
+
**Normalización de datos:** La ingesta aplica 16 transformaciones automáticas:
|
|
592
|
+
- Parseo de referencias (`F01/201-243065` → tipo, serie, periodo, número)
|
|
593
|
+
- Separación de sucursales (nombre + dirección)
|
|
594
|
+
- Clasificación de documentos (RUC 11 dígitos / DNI 8 dígitos)
|
|
595
|
+
- Normalización monetaria con auto-detección de formato SAP/Spring
|
|
596
|
+
- Cantidad + Cantidad FAE → cantidad_total + tipo_transaccion
|
|
597
|
+
- Purga de filas total/general/acumulado
|
|
598
|
+
|
|
547
599
|
### python-flet-migrate
|
|
548
600
|
|
|
549
601
|
Plantilla para migrar aplicaciones Tkinter/CTkinter a Flet.
|
|
@@ -749,9 +801,12 @@ g360-cli/
|
|
|
749
801
|
│ └── assets/ # Assets embebidos
|
|
750
802
|
│ ├── templates/ # Plantillas de proyecto
|
|
751
803
|
│ ├── components/ # Componentes G360
|
|
752
|
-
│ ├──
|
|
804
|
+
│ ├── ingestion/ # Módulo de ingesta ERP (bring)
|
|
753
805
|
│ ├── engine/ # G360 Engine
|
|
754
806
|
│ └── config/ # Configuraciones
|
|
807
|
+
├── py/ # Paquete Python publicable en PyPI
|
|
808
|
+
│ ├── pyproject.toml # g360-core
|
|
809
|
+
│ └── src/g360_core/ # ingestion.py + flet/ingestion_panel.py
|
|
755
810
|
├── package.json
|
|
756
811
|
├── README.md
|
|
757
812
|
└── LICENSE
|
|
@@ -765,7 +820,7 @@ g360-cli/
|
|
|
765
820
|
|---------|-------------|
|
|
766
821
|
| `npm run build` | Build portable con pkg (g360.exe) |
|
|
767
822
|
| `npm run build:portable` | Especificar target node18-win-x64 |
|
|
768
|
-
| `npm test` | Ejecutar tests (
|
|
823
|
+
| `npm test` | Ejecutar tests con Vitest (51 tests, 7 suites) |
|
|
769
824
|
| `npm run prepublishOnly` | Validación antes de publicar en npm |
|
|
770
825
|
|
|
771
826
|
---
|
|
@@ -773,10 +828,16 @@ g360-cli/
|
|
|
773
828
|
## Testing
|
|
774
829
|
|
|
775
830
|
```bash
|
|
776
|
-
npm test
|
|
831
|
+
npm test # Vitest — 51 tests, 8 suites
|
|
832
|
+
npm run test:watch # Modo watch
|
|
833
|
+
npm run test:ui # UI interactiva
|
|
834
|
+
npm run test:coverage
|
|
777
835
|
```
|
|
778
836
|
|
|
779
|
-
|
|
837
|
+
**Cobertura actual (v1.7.0):**
|
|
838
|
+
- `commands/`: init, bring, list, audit, set-skill
|
|
839
|
+
- `lib/`: manifest, validator, asset-validator
|
|
840
|
+
- **51 passing / 1 timeout** (init.test.js requiere import pesado de inquirer)
|
|
780
841
|
|
|
781
842
|
---
|
|
782
843
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "g360-core"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "G360 ERP data ingestion engine — normalize .xls/.xlsx from SAP, StarSoft, Spring"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "ccusi", email = "ccusi@cipsa.com.pe" },
|
|
13
|
+
]
|
|
14
|
+
keywords = ["g360", "erp", "data", "normalization", "flet"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
dependencies = [
|
|
21
|
+
"pandas>=2.0.0",
|
|
22
|
+
"numpy>=1.24.0",
|
|
23
|
+
"openpyxl>=3.1.0",
|
|
24
|
+
"xlrd>=2.0.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
flet = ["flet>=0.25.0"]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/ccusi/g360-cli"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/g360_core"]
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import flet as ft
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from g360_core.ingestion import estabilizar_excel_crudo
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IngestionPanel(ft.Container):
|
|
11
|
+
def __init__(self, theme, on_data_loaded=None):
|
|
12
|
+
super().__init__()
|
|
13
|
+
self.theme = theme
|
|
14
|
+
self.on_data_loaded = on_data_loaded
|
|
15
|
+
self.df: pd.DataFrame | None = None
|
|
16
|
+
self.metadata: dict | None = None
|
|
17
|
+
self._build()
|
|
18
|
+
|
|
19
|
+
def _build(self):
|
|
20
|
+
self.file_picker = ft.FilePicker(on_result=self._on_file_result)
|
|
21
|
+
self.status_text = ft.Text(
|
|
22
|
+
"Selecciona un archivo .xls o .xlsx del ERP",
|
|
23
|
+
size=14,
|
|
24
|
+
color=self.theme.muted,
|
|
25
|
+
)
|
|
26
|
+
self.stats_container = ft.Column(spacing=4, visible=False)
|
|
27
|
+
self.alertas_container = ft.Column(spacing=2, visible=False)
|
|
28
|
+
self.preview_table = ft.Column(scroll=ft.ScrollMode.AUTO, visible=False)
|
|
29
|
+
|
|
30
|
+
self.content = ft.Column(
|
|
31
|
+
controls=[
|
|
32
|
+
self.file_picker,
|
|
33
|
+
ft.Row(
|
|
34
|
+
controls=[
|
|
35
|
+
self.theme.accent_button(
|
|
36
|
+
text="Cargar Archivo Excel",
|
|
37
|
+
on_click=self._open_picker,
|
|
38
|
+
),
|
|
39
|
+
ft.Container(width=12),
|
|
40
|
+
self.status_text,
|
|
41
|
+
],
|
|
42
|
+
alignment=ft.MainAxisAlignment.START,
|
|
43
|
+
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
|
44
|
+
),
|
|
45
|
+
ft.Container(height=12),
|
|
46
|
+
self.stats_container,
|
|
47
|
+
ft.Container(height=8),
|
|
48
|
+
self.alertas_container,
|
|
49
|
+
ft.Container(height=8),
|
|
50
|
+
self.preview_table,
|
|
51
|
+
],
|
|
52
|
+
spacing=0,
|
|
53
|
+
)
|
|
54
|
+
self.bgcolor = self.theme.surface
|
|
55
|
+
self.border_radius = self.theme.rounded
|
|
56
|
+
self.padding = 20
|
|
57
|
+
self.expand = True
|
|
58
|
+
|
|
59
|
+
def _open_picker(self, e):
|
|
60
|
+
self.file_picker.pick_files(
|
|
61
|
+
file_type=ft.FilePickerFileType.CUSTOM,
|
|
62
|
+
allowed_extensions=["xls", "xlsx", "xlsm"],
|
|
63
|
+
dialog_title="Seleccionar reporte del ERP",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def _on_file_result(self, e: ft.FilePickerResultEvent):
|
|
67
|
+
if not e.files:
|
|
68
|
+
self.status_text.value = "No se selecciono ningun archivo."
|
|
69
|
+
self.status_text.color = self.theme.warning
|
|
70
|
+
self.update()
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
archivo = e.files[0]
|
|
74
|
+
ruta = archivo.path
|
|
75
|
+
nombre = archivo.name
|
|
76
|
+
self.status_text.value = f"Procesando: {nombre}..."
|
|
77
|
+
self.status_text.color = self.theme.text
|
|
78
|
+
self.update()
|
|
79
|
+
|
|
80
|
+
self._show_loading(True)
|
|
81
|
+
|
|
82
|
+
def _procesar():
|
|
83
|
+
try:
|
|
84
|
+
df, metadata = estabilizar_excel_crudo(ruta)
|
|
85
|
+
self.df = df
|
|
86
|
+
self.metadata = metadata
|
|
87
|
+
self._mostrar_resultado(df, metadata, nombre)
|
|
88
|
+
if self.on_data_loaded:
|
|
89
|
+
self.on_data_loaded(df, metadata)
|
|
90
|
+
except Exception as exc:
|
|
91
|
+
self.status_text.value = f"Error: {exc}"
|
|
92
|
+
self.status_text.color = self.theme.error
|
|
93
|
+
self._show_loading(False)
|
|
94
|
+
self.update()
|
|
95
|
+
|
|
96
|
+
thread = threading.Thread(target=_procesar, daemon=True)
|
|
97
|
+
thread.start()
|
|
98
|
+
|
|
99
|
+
def _show_loading(self, visible: bool):
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
def _mostrar_resultado(self, df: pd.DataFrame, metadata: dict, nombre: str):
|
|
103
|
+
self.status_text.value = f"OK: {nombre} ({len(df):,} filas)"
|
|
104
|
+
self.status_text.color = self.theme.success
|
|
105
|
+
|
|
106
|
+
columnas = metadata.get("columnas_finales", metadata.get("columnas", []))
|
|
107
|
+
transformaciones = metadata.get("transformaciones", [])
|
|
108
|
+
alertas = metadata.get("alertas", [])
|
|
109
|
+
columnas_nuevas = metadata.get("columnas_nuevas", [])
|
|
110
|
+
|
|
111
|
+
self.stats_container.controls = [
|
|
112
|
+
ft.Text("Resumen de Ingesta", size=16, weight=ft.FontWeight.BOLD, color=self.theme.text),
|
|
113
|
+
ft.Text(f"Filas estabilizadas: {len(df):,}", size=13, color=self.theme.muted),
|
|
114
|
+
ft.Text(f"Columnas originales: {metadata.get('filas_originales', 0)}", size=13, color=self.theme.muted),
|
|
115
|
+
ft.Text(f"Columnas finales: {len(columnas)}", size=13, color=self.theme.muted),
|
|
116
|
+
ft.Text(f"Archivo: {metadata.get('archivo', 'N/A')}", size=13, color=self.theme.muted),
|
|
117
|
+
ft.Text(f"Moneda: {metadata.get('moneda', 'N/A')}", size=13, color=self.theme.muted),
|
|
118
|
+
ft.Divider(color=self.theme.bg, height=8),
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
if columnas_nuevas:
|
|
122
|
+
nuevas_text = ft.Text(
|
|
123
|
+
"Columnas derivadas:",
|
|
124
|
+
size=13, weight=ft.FontWeight.BOLD, color=self.theme.accent,
|
|
125
|
+
)
|
|
126
|
+
chips = ft.Row(
|
|
127
|
+
controls=[
|
|
128
|
+
ft.Container(
|
|
129
|
+
content=ft.Text(col, size=10, color=self.theme.bg),
|
|
130
|
+
bgcolor=self.theme.accent,
|
|
131
|
+
border_radius=12,
|
|
132
|
+
padding=ft.padding.only(left=8, right=8, top=3, bottom=3),
|
|
133
|
+
)
|
|
134
|
+
for col in columnas_nuevas
|
|
135
|
+
],
|
|
136
|
+
spacing=6,
|
|
137
|
+
wrap=True,
|
|
138
|
+
)
|
|
139
|
+
self.stats_container.controls.extend([nuevas_text, chips])
|
|
140
|
+
|
|
141
|
+
if transformaciones:
|
|
142
|
+
self.stats_container.controls.append(
|
|
143
|
+
ft.Divider(color=self.theme.bg, height=8)
|
|
144
|
+
)
|
|
145
|
+
self.stats_container.controls.append(
|
|
146
|
+
ft.Text("Transformaciones:", size=13, weight=ft.FontWeight.BOLD, color=self.theme.text)
|
|
147
|
+
)
|
|
148
|
+
for t in transformaciones[-8:]:
|
|
149
|
+
self.stats_container.controls.append(
|
|
150
|
+
ft.Row(
|
|
151
|
+
controls=[
|
|
152
|
+
ft.Container(
|
|
153
|
+
content=ft.Text(">", size=11, color=self.theme.accent),
|
|
154
|
+
width=16,
|
|
155
|
+
),
|
|
156
|
+
ft.Text(t, size=11, color=self.theme.muted),
|
|
157
|
+
],
|
|
158
|
+
spacing=0,
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
self.stats_container.visible = True
|
|
163
|
+
|
|
164
|
+
if alertas:
|
|
165
|
+
self.alertas_container.controls = [
|
|
166
|
+
ft.Divider(color=self.theme.bg, height=4),
|
|
167
|
+
ft.Text("Alertas:", size=13, weight=ft.FontWeight.BOLD, color=self.theme.warning),
|
|
168
|
+
]
|
|
169
|
+
for a in alertas:
|
|
170
|
+
self.alertas_container.controls.append(
|
|
171
|
+
ft.Row(
|
|
172
|
+
controls=[
|
|
173
|
+
ft.Icon(ft.icons.WARNING_AMBER_ROUNDED, size=14, color=self.theme.warning),
|
|
174
|
+
ft.Text(a, size=11, color=self.theme.warning),
|
|
175
|
+
],
|
|
176
|
+
spacing=4,
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
self.alertas_container.visible = True
|
|
180
|
+
|
|
181
|
+
filas_preview = df.head(5)
|
|
182
|
+
columnas_preview = columnas[:8]
|
|
183
|
+
|
|
184
|
+
data_rows = []
|
|
185
|
+
for _, row in filas_preview.iterrows():
|
|
186
|
+
cells = []
|
|
187
|
+
for col in columnas_preview:
|
|
188
|
+
val = row.get(col, "")
|
|
189
|
+
v_str = str(val)[:30] if not pd.isna(val) else "-"
|
|
190
|
+
cells.append(
|
|
191
|
+
ft.DataCell(ft.Text(v_str, size=10, color=self.theme.text))
|
|
192
|
+
)
|
|
193
|
+
data_rows.append(ft.DataRow(cells=cells))
|
|
194
|
+
|
|
195
|
+
header_cells = [
|
|
196
|
+
ft.DataColumn(ft.Text(col[:16], size=10, color=self.theme.accent, weight=ft.FontWeight.BOLD))
|
|
197
|
+
for col in columnas_preview
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
tabla = ft.DataTable(
|
|
201
|
+
columns=header_cells,
|
|
202
|
+
rows=data_rows,
|
|
203
|
+
heading_text_color=self.theme.accent,
|
|
204
|
+
horizontal_margin=4,
|
|
205
|
+
column_spacing=16,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
self.preview_table.controls = [
|
|
209
|
+
ft.Text("Vista previa (5 primeras filas):", size=13, weight=ft.FontWeight.BOLD, color=self.theme.text),
|
|
210
|
+
ft.Container(
|
|
211
|
+
content=tabla,
|
|
212
|
+
bgcolor=self.theme.bg,
|
|
213
|
+
border_radius=8,
|
|
214
|
+
padding=8,
|
|
215
|
+
),
|
|
216
|
+
]
|
|
217
|
+
self.preview_table.visible = True
|
|
218
|
+
self.update()
|