g360-cli 1.7.0 → 1.9.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.
Files changed (42) hide show
  1. package/README.md +37 -3
  2. package/package.json +16 -6
  3. package/py/pyproject.toml +4 -4
  4. package/py/requirements.txt +4 -0
  5. package/py/src/g360_core/__init__.py +67 -4
  6. package/py/src/g360_core/__pycache__/__init__.cpython-312.pyc +0 -0
  7. package/py/src/g360_core/__pycache__/__init__.cpython-314.pyc +0 -0
  8. package/py/src/g360_core/__pycache__/batch_processor.cpython-312.pyc +0 -0
  9. package/py/src/g360_core/__pycache__/batch_processor.cpython-314.pyc +0 -0
  10. package/py/src/g360_core/__pycache__/commercial_engine.cpython-314.pyc +0 -0
  11. package/py/src/g360_core/__pycache__/logger.cpython-312.pyc +0 -0
  12. package/py/src/g360_core/__pycache__/logger.cpython-314.pyc +0 -0
  13. package/py/src/g360_core/__pycache__/pipeline.cpython-312.pyc +0 -0
  14. package/py/src/g360_core/__pycache__/pipeline.cpython-314.pyc +0 -0
  15. package/py/src/g360_core/__pycache__/processor.cpython-312.pyc +0 -0
  16. package/py/src/g360_core/__pycache__/processor.cpython-314.pyc +0 -0
  17. package/py/src/g360_core/__pycache__/processor_segmentacion.cpython-312.pyc +0 -0
  18. package/py/src/g360_core/__pycache__/processor_segmentacion.cpython-314.pyc +0 -0
  19. package/py/src/g360_core/__pycache__/processor_sku.cpython-312.pyc +0 -0
  20. package/py/src/g360_core/__pycache__/processor_sku.cpython-314.pyc +0 -0
  21. package/py/src/g360_core/__pycache__/scanner.cpython-312.pyc +0 -0
  22. package/py/src/g360_core/__pycache__/scanner.cpython-314.pyc +0 -0
  23. package/py/src/g360_core/__pycache__/utils.cpython-312.pyc +0 -0
  24. package/py/src/g360_core/__pycache__/utils.cpython-314.pyc +0 -0
  25. package/py/src/g360_core/batch_processor.py +120 -0
  26. package/py/src/g360_core/commercial_engine.py +305 -0
  27. package/py/src/g360_core/logger.py +40 -0
  28. package/py/src/g360_core/pipeline.py +578 -0
  29. package/py/src/g360_core/processor.py +634 -0
  30. package/py/src/g360_core/processor_segmentacion.py +859 -0
  31. package/py/src/g360_core/processor_sku.py +427 -0
  32. package/py/src/g360_core/scanner.py +218 -0
  33. package/py/src/g360_core/utils.py +435 -0
  34. package/src/assets/templates/python-flet/src/test_ingestion.py +1 -1
  35. package/src/cli.js +25 -2
  36. package/src/commands/ingest.js +193 -0
  37. package/src/commands/scan.js +102 -0
  38. package/src/commands/validate.js +126 -0
  39. package/src/lib/python_runner.js +89 -0
  40. package/py/src/g360_core/flet/__init__.py +0 -3
  41. package/py/src/g360_core/flet/ingestion_panel.py +0 -218
  42. package/py/src/g360_core/ingestion.py +0 -480
package/README.md CHANGED
@@ -55,6 +55,8 @@ CLI tool para el ecosistema G360 que permite inicializar proyectos con estructur
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
57
  - **Ingesta ERP** - Normaliza `.xls/.xlsx` de SAP, StarSoft, Spring con `g360 bring ingestion`
58
+ - **Motor de clasificación comercial** - `commercial_engine` clasifica documentos en VENTA/DEVOLUCION/AJUSTE con subtipos (PRECIO_LINEA, CARGO_FIJO, SIN_BASE)
59
+ - **Precio efectivo** - PRECIO_BASE, RECARGO_UNITARIO y PRECIO_EFECTIVO separan precio físico de ajustes financieros FAE
58
60
  - **Paquete Python** - `g360-core` en PyPI para pipelines de datos independientes
59
61
  - **Auditoría** - Verifica compliance de proyectos G360
60
62
  - **Limpieza** - Elimina assets embebidos antes de deployment
@@ -309,6 +311,35 @@ El paquete pip acompañante `g360-core` se publica en PyPI:
309
311
  pip install g360-core
310
312
  ```
311
313
 
314
+ ### `g360-core` — Módulos principales
315
+
316
+ #### `commercial_engine.py`
317
+
318
+ Motor de lógica de negocio para clasificación documental. Única fuente de verdad para reglas comerciales.
319
+
320
+ | Función | Propósito |
321
+ |---------|-----------|
322
+ | `classify_base()` | Clasificación primaria: VENTA, DEVOLUCION, AJUSTE |
323
+ | `build_invoice_index()` | Índice de facturas para cruce de referencias |
324
+ | `resolve_document_relationships()` | Asigna SUBTIPO_AJUSTE (PRECIO_LINEA, CARGO_FIJO, SIN_BASE) |
325
+ | `calculate_prices()` | PRECIO_BASE, RECARGO_UNITARIO, PRECIO_EFECTIVO |
326
+ | `parse_referencia()` | Descompone REFERENCIA "F01/204-56287" en tipo/serie/número |
327
+
328
+ **Clasificación de documentos:**
329
+ ```
330
+ TPO_DOC CANTIDAD CANTIDAD_FAE → CATEGORIA_OP SUBTIPO_AJUSTE
331
+ F01/BDI ≠0 = VENTA —
332
+ NCR ≠0 = DEVOLUCION —
333
+ NCR 0 ≠0 AJUSTE PRECIO_LINEA / SIN_BASE
334
+ NDB 0 ≠0 AJUSTE CARGO_FIJO / SIN_BASE
335
+ ```
336
+
337
+ #### `batch_processor.py`
338
+
339
+ | Función | Propósito |
340
+ |---------|-----------|
341
+ | `read_erp_file()` | Punto único de lectura: .xls (xlrd), .xlsx (openpyxl), .csv. `dtype=str` preserva ceros a la izquierda |
342
+
312
343
  ---
313
344
 
314
345
  ### `g360 list`
@@ -588,12 +619,15 @@ mi-app/
588
619
  └── skill.json
589
620
  ```
590
621
 
591
- **Normalización de datos:** La ingesta aplica 16 transformaciones automáticas:
622
+ **Normalización de datos:** La ingesta aplica transformaciones automáticas:
592
623
  - Parseo de referencias (`F01/201-243065` → tipo, serie, periodo, número)
593
624
  - Separación de sucursales (nombre + dirección)
594
625
  - Clasificación de documentos (RUC 11 dígitos / DNI 8 dígitos)
595
626
  - Normalización monetaria con auto-detección de formato SAP/Spring
596
627
  - Cantidad + Cantidad FAE → cantidad_total + tipo_transaccion
628
+ - **Clasificación comercial**: VENTA / DEVOLUCION / AJUSTE con subtipos PRECIO_LINEA, CARGO_FIJO, SIN_BASE
629
+ - **Precio efectivo**: PRECIO_BASE (físico) y RECARGO_UNITARIO (financiero) separados
630
+ - Cruce de NC/NDB contra facturas referenciadas para determinar ajustes de precio por línea
597
631
  - Purga de filas total/general/acumulado
598
632
 
599
633
  ### python-flet-migrate
@@ -806,7 +840,7 @@ g360-cli/
806
840
  │ └── config/ # Configuraciones
807
841
  ├── py/ # Paquete Python publicable en PyPI
808
842
  │ ├── pyproject.toml # g360-core
809
- │ └── src/g360_core/ # ingestion.py + flet/ingestion_panel.py
843
+ │ └── src/g360_core/ # commercial_engine.py, pipeline.py, processor.py, batch_processor.py, utils.py
810
844
  ├── package.json
811
845
  ├── README.md
812
846
  └── LICENSE
@@ -834,7 +868,7 @@ npm run test:ui # UI interactiva
834
868
  npm run test:coverage
835
869
  ```
836
870
 
837
- **Cobertura actual (v1.7.0):**
871
+ **Cobertura actual (v1.9.0):**
838
872
  - `commands/`: init, bring, list, audit, set-skill
839
873
  - `lib/`: manifest, validator, asset-validator
840
874
  - **51 passing / 1 timeout** (init.test.js requiere import pesado de inquirer)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "g360-cli",
3
- "version": "1.7.0",
4
- "description": "CLI tool for bootstrapping G360 projects with standardized structure, assets, and identity",
3
+ "version": "1.9.0",
4
+ "description": "CLI tool for bootstrapping G360 projects with standardized structure, assets, identity, and ERP data processing",
5
5
  "type": "module",
6
6
  "main": "src/cli.js",
7
7
  "bin": {
@@ -14,19 +14,25 @@
14
14
  "test:ui": "vitest --ui",
15
15
  "test:coverage": "vitest --coverage",
16
16
  "build": "npm run build:portable",
17
- "build:portable": "pkg . --targets node18-win-x64 --output dist/g360.exe"
17
+ "build:portable": "pkg . --targets node18-win-x64 --output dist/g360.exe",
18
+ "postinstall": "echo '✅ g360-cli instalado. Asegúrate de tener Python 3.11+ y pip instalados.'"
18
19
  },
19
20
  "pkg": {
20
21
  "assets": [
21
- "src/assets/**/*"
22
+ "src/assets/**/*",
23
+ "py/**/*"
22
24
  ],
23
- "outputPath": "dist"
25
+ "outputPath": "dist",
26
+ "scripts": [],
27
+ "targets": "node18-win-x64"
24
28
  },
25
29
  "keywords": [
26
30
  "g360",
27
31
  "cli",
28
32
  "scaffolding",
29
- "generator"
33
+ "generator",
34
+ "erp",
35
+ "data-processing"
30
36
  ],
31
37
  "author": "Carlos Cusi",
32
38
  "license": "MIT",
@@ -46,5 +52,9 @@
46
52
  },
47
53
  "engines": {
48
54
  "node": ">=18.0.0"
55
+ },
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "https://github.com/ccusi/g360-cli.git"
49
59
  }
50
60
  }
package/py/pyproject.toml CHANGED
@@ -18,10 +18,10 @@ classifiers = [
18
18
  ]
19
19
 
20
20
  dependencies = [
21
- "pandas>=2.0.0",
22
- "numpy>=1.24.0",
23
- "openpyxl>=3.1.0",
24
- "xlrd>=2.0.0",
21
+ "pandas>=2.2.3",
22
+ "numpy>=1.26.0",
23
+ "openpyxl>=3.1.5",
24
+ "xlrd>=2.0.1",
25
25
  ]
26
26
 
27
27
  [project.optional-dependencies]
@@ -0,0 +1,4 @@
1
+ pandas>=2.2.3
2
+ numpy>=1.26.0
3
+ openpyxl>=3.1.5
4
+ xlrd>=2.0.1
@@ -1,7 +1,70 @@
1
- from g360_core.ingestion import estabilizar_excel_crudo, _MAPA_TPO_DOC, _MAPA_MESES
1
+ """
2
+ g360_core - Módulo principal de procesamiento ERP para g360-cli.
3
+
4
+ Proporciona:
5
+ - Estabilización de datos crudos de Excel/CSV
6
+ - Pipeline de ingesta con normalización
7
+ - Scanner de directorios
8
+ - Protección de ceros a la izquierda en IDs
9
+ - Batch processing
10
+ """
11
+
12
+ from .commercial_engine import (classify_base, parse_referencia, resolve_document_relationships, calculate_prices, CAT_VENTA, CAT_DEVOLUCION, CAT_AJUSTE, SUBTIPO_PRECIO_LINEA, SUBTIPO_PRECIO_PARCIAL, SUBTIPO_CARGO_FIJO, SUBTIPO_SIN_BASE)
13
+ from .pipeline import ingest_to_master, batch_ingest
14
+ from .scanner import (
15
+ ERPScanner,
16
+ ERPFileInfo,
17
+ find_erp_files_in_dir,
18
+ process_single_file,
19
+ batch_process_files,
20
+ merge_processed_data,
21
+ )
22
+ from .processor import InsightProcessor
23
+ from .utils import (
24
+ clean_erp_headers,
25
+ normalize_ids,
26
+ build_doc_completo,
27
+ build_entity_labels,
28
+ validate_columns,
29
+ parse_excel_date,
30
+ ID_COLS_PRESERVE_ZEROS,
31
+ NC_PREFIXES,
32
+ )
33
+ from .batch_processor import read_erp_file
2
34
 
3
35
  __all__ = [
4
- "estabilizar_excel_crudo",
5
- "_MAPA_TPO_DOC",
6
- "_MAPA_MESES",
36
+ # Commercial Engine
37
+ "classify_base",
38
+ "parse_referencia",
39
+ "resolve_document_relationships",
40
+ "calculate_prices",
41
+ "CAT_VENTA",
42
+ "CAT_DEVOLUCION",
43
+ "CAT_AJUSTE",
44
+ "SUBTIPO_PRECIO_LINEA",
45
+ "SUBTIPO_PRECIO_PARCIAL",
46
+ "SUBTIPO_CARGO_FIJO",
47
+ "SUBTIPO_SIN_BASE",
48
+ # Ingestion
49
+ "ingest_to_master",
50
+ "batch_ingest",
51
+ # Scanner
52
+ "ERPScanner",
53
+ "ERPFileInfo",
54
+ "find_erp_files_in_dir",
55
+ "process_single_file",
56
+ "batch_process_files",
57
+ "merge_processed_data",
58
+ # Processor
59
+ "InsightProcessor",
60
+ # Utils
61
+ "clean_erp_headers",
62
+ "normalize_ids",
63
+ "build_doc_completo",
64
+ "build_entity_labels",
65
+ "validate_columns",
66
+ "parse_excel_date",
67
+ "ID_COLS_PRESERVE_ZEROS",
68
+ "NC_PREFIXES",
69
+ "read_erp_file",
7
70
  ]
@@ -0,0 +1,120 @@
1
+ """
2
+ Procesador por lotes para archivos ERP grandes.
3
+
4
+ Lee archivos grandes y los procesa en un solo paso usando InsightProcessor.
5
+ El chunking original causaba procesamiento duplicado; ahora se procesa
6
+ todo el DataFrame de una vez para garantizar consistencia.
7
+ """
8
+ import pandas as pd
9
+ from pathlib import Path
10
+ from typing import Optional, Callable
11
+ import gc
12
+ from .processor import InsightProcessor
13
+
14
+
15
+ class BatchProcessor:
16
+ """Procesa archivos ERP grandes con feedback de progreso."""
17
+
18
+ def __init__(self, progress_callback: Optional[Callable] = None):
19
+ self.progress_callback = progress_callback
20
+
21
+ def process_large_file(self, df: pd.DataFrame, filename: str) -> InsightProcessor:
22
+ """
23
+ Procesa un DataFrame completo en un solo paso.
24
+
25
+ Args:
26
+ df: DataFrame con datos crudos
27
+ filename: Nombre del archivo para logs
28
+
29
+ Returns:
30
+ InsightProcessor con todos los datos procesados
31
+ """
32
+ if self.progress_callback:
33
+ self.progress_callback(0.1, f"Procesando {len(df):,} registros...")
34
+
35
+ processor = InsightProcessor(df)
36
+ gc.collect()
37
+
38
+ if self.progress_callback:
39
+ self.progress_callback(1.0, "Procesamiento completado")
40
+
41
+ return processor
42
+
43
+ def read_with_progress(self, path: str, file_ext: str,
44
+ on_progress: Callable[[float, str], None]) -> pd.DataFrame:
45
+ """
46
+ Lee archivo con indicador de progreso.
47
+
48
+ Args:
49
+ path: Ruta del archivo
50
+ file_ext: Extension del archivo (.csv, .xls, .xlsx)
51
+ on_progress: Callback de progreso (0-1, mensaje)
52
+
53
+ Returns:
54
+ DataFrame leido
55
+ """
56
+ on_progress(0.05, "Leyendo archivo...")
57
+ df = read_erp_file(path, file_ext)
58
+ on_progress(0.2, f"Archivo leido: {len(df):,} filas")
59
+ return df
60
+
61
+
62
+ def read_erp_file(path: str, file_ext: str = None) -> pd.DataFrame:
63
+ """
64
+ Funcion unificada de lectura de archivos ERP.
65
+
66
+ Es el unico punto de entrada para leer archivos .xls, .xlsx y .csv
67
+ en toda la aplicacion. Usa engines explicitos para evitar problemas
68
+ de compatibilidad con pandas/xlrd/openpyxl.
69
+
70
+ PROTECCIÓN DE CEROS A LA IZQUIERDA:
71
+ Las columnas críticas de IDs se leen obligatoriamente como strings (dtype=str)
72
+ para preservar ceros iniciales (ej: "01240" no se convierte a 1240).
73
+
74
+ Args:
75
+ path: Ruta absoluta al archivo.
76
+ file_ext: Extension (incluye el punto). Si es None se infiere.
77
+
78
+ Returns:
79
+ DataFrame con columnas críticas como strings para preservar ceros.
80
+
81
+ Raises:
82
+ ValueError: Si la extension no es soportada o hay error de lectura.
83
+ """
84
+ if file_ext is None:
85
+ file_ext = Path(path).suffix.lower()
86
+ else:
87
+ file_ext = file_ext.lower()
88
+
89
+ # Columnas críticas que deben preservar ceros a la izquierda
90
+ critical_cols = [
91
+ "ID_ARTICULO", "ID_CLIENTE", "ID_VENDEDOR", "ID_LINEA",
92
+ "ID_GRUPO", "ID_TIPO", "ID_FAMILIA", "COD_SUCURSAL",
93
+ "NRO_DOC", "SERIE_DOC", "DOC_CLIENTE"
94
+ ]
95
+
96
+ if file_ext == '.csv':
97
+ # Para CSV, leer todo como string inicialmente
98
+ df = pd.read_csv(
99
+ path, dtype=str, encoding='utf-8-sig',
100
+ sep=None, engine='python', low_memory=False,
101
+ )
102
+ elif file_ext in ('.xls', '.xlsx'):
103
+ engine = "xlrd" if file_ext == ".xls" else "openpyxl"
104
+ try:
105
+ # Leer Excel con dtype=str para forzar texto en TODAS las columnas
106
+ # Esto previene que pandas convierta "01240" -> 1240 automáticamente
107
+ df = pd.read_excel(path, dtype=str, engine=engine)
108
+ except Exception as ex:
109
+ raise ValueError(
110
+ f"Error leyendo archivo Excel ({file_ext}) con motor '{engine}': {ex}"
111
+ )
112
+ else:
113
+ raise ValueError(f"Extension '{file_ext}' no soportada. Use .xls, .xlsx, .csv")
114
+
115
+ # Limpieza post-lectura: eliminar espacios en blanco de columnas críticas
116
+ for col in critical_cols:
117
+ if col in df.columns:
118
+ df[col] = df[col].astype(str).str.strip()
119
+
120
+ return df