g360-cli 1.6.0 → 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 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** (flask-like para Flutter).
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
- └── core/
540
- └── skill.json
541
- ├── requirements.txt
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-portable.bat
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
- │ ├── skills/ # Skills de identidad
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 (actualmente placeholder) |
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
- Actualmente configurado con un placeholder. Próximamente se integrará con Vitest para testing de comandos y utilidades.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "g360-cli",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "CLI tool for bootstrapping G360 projects with standardized structure, assets, and identity",
5
5
  "type": "module",
6
6
  "main": "src/cli.js",
@@ -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,7 @@
1
+ from g360_core.ingestion import estabilizar_excel_crudo, _MAPA_TPO_DOC, _MAPA_MESES
2
+
3
+ __all__ = [
4
+ "estabilizar_excel_crudo",
5
+ "_MAPA_TPO_DOC",
6
+ "_MAPA_MESES",
7
+ ]
@@ -0,0 +1,3 @@
1
+ from g360_core.flet.ingestion_panel import IngestionPanel
2
+
3
+ __all__ = ["IngestionPanel"]
@@ -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()