g360-cli 1.10.1 → 1.11.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
@@ -70,7 +70,7 @@ CLI tool para el ecosistema G360 que permite inicializar proyectos con estructur
70
70
 
71
71
  ## Versión
72
72
 
73
- **Current: v1.10.0** — [Ver en npm](https://www.npmjs.com/package/g360-cli)
73
+ **Current: v1.11.0** — [Ver en npm](https://www.npmjs.com/package/g360-cli)
74
74
 
75
75
  ---
76
76
 
@@ -91,7 +91,7 @@ npm install -g g360-cli
91
91
 
92
92
  ```bash
93
93
  g360 --version
94
- # → 1.10.0
94
+ # → 1.11.0
95
95
 
96
96
  g360 health
97
97
  ```
@@ -646,6 +646,10 @@ mi-app/
646
646
  - **Precio efectivo**: PRECIO_BASE (físico) y RECARGO_UNITARIO (financiero) separados
647
647
  - Cruce de NC/NDB contra facturas referenciadas para determinar ajustes de precio por línea
648
648
  - Purga de filas total/general/acumulado
649
+ - **Columnas derivadas para UI**: `cliente_label`, `vendedor_label`, `articulo_label`, `linea_label`, etc. (ID - NOMBRE)
650
+ - **Cliente completo**: `cliente_full_label` = ID_CLIENTE - DOC_CLIENTE_CLEAN - NOM_CLIENTE
651
+ - **Código de factura**: `doc_completo` = TPO_DOC + SERIE_DOC + NRO_DOC (ej: "F204-56287")
652
+ - **Precio unitario**: `precio_base` = SOLES / CANTIDAD
649
653
 
650
654
  ### python-flet-migrate
651
655
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "g360-cli",
3
- "version": "1.10.1",
3
+ "version": "1.11.0",
4
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",
@@ -25,6 +25,17 @@ _KEYWORDS_CANTIDAD = [
25
25
  ]
26
26
  _KEYWORDS_FECHA = ["fecha", "fec_", "venc"]
27
27
 
28
+ _ENTITY_LABELS = {
29
+ ("id_cliente", "nom_cliente"): "cliente_label",
30
+ ("id_vendedor", "nom_vendedor"): "vendedor_label",
31
+ ("id_articulo", "nom_articulo"): "articulo_label",
32
+ ("id_linea", "nom_linea"): "linea_label",
33
+ ("id_grupo", "nom_grupo"): "grupo_label",
34
+ ("id_tipo", "nom_tipo"): "tipo_label",
35
+ ("id_familia", "nom_familia"): "familia_label",
36
+ ("cod_sucursal", "nom_sucursal"): "sucursal_label",
37
+ }
38
+
28
39
  _MAPA_TPO_DOC = {
29
40
  "NCR": "NOTA DE CREDITO",
30
41
  "NDB": "NOTA DE DEBITO",
@@ -247,6 +258,26 @@ def _clasificar_transaccion(row: dict) -> str:
247
258
  return "indefinido"
248
259
 
249
260
 
261
+ def _build_entity_labels(df: pd.DataFrame) -> pd.DataFrame:
262
+ for (id_col, name_col), label_col in _ENTITY_LABELS.items():
263
+ id_exists = id_col in df.columns
264
+ name_exists = name_col in df.columns
265
+
266
+ if id_exists and name_exists:
267
+ id_vals = df[id_col].fillna("").astype(str).str.strip()
268
+ name_vals = df[name_col].fillna("").astype(str).str.strip()
269
+ df[label_col] = np.where(
270
+ (id_vals != "") & (name_vals != ""),
271
+ id_vals + " - " + name_vals,
272
+ np.where(id_vals != "", id_vals, name_vals)
273
+ )
274
+ elif id_exists:
275
+ df[label_col] = df[id_col].astype(str).str.strip()
276
+ elif name_exists:
277
+ df[label_col] = df[name_col].astype(str).str.strip()
278
+ return df
279
+
280
+
250
281
  def estabilizar_excel_crudo(ruta_archivo: str | Path) -> tuple[pd.DataFrame, dict]:
251
282
  ruta = Path(ruta_archivo)
252
283
  if not ruta.exists():
@@ -359,6 +390,22 @@ def estabilizar_excel_crudo(ruta_archivo: str | Path) -> tuple[pd.DataFrame, dic
359
390
  f"{col}: normalizado ({ruc_count} RUC, {dni_count} DNI, "
360
391
  f"{(~df[tipo_col].isin(['RUC','DNI'])).sum()} otros)"
361
392
  )
393
+
394
+ # ── 9b. CLIENTE_FULL_LABEL = ID + DOC_CLIENTE_CLEAN + NOMBRE ──
395
+ if "id_cliente" in df.columns and "doc_cliente_clean" in df.columns:
396
+ id_vals = df["id_cliente"].fillna("").astype(str).str.strip()
397
+ doc_vals = df["doc_cliente_clean"].fillna("").astype(str).str.strip()
398
+ name_vals = df["nom_cliente"].fillna("").astype(str).str.strip() if "nom_cliente" in df.columns else None
399
+
400
+ if name_vals is not None:
401
+ df["cliente_full_label"] = np.where(
402
+ (id_vals != "") & (doc_vals != "") & (name_vals != ""),
403
+ id_vals + " - " + doc_vals + " - " + name_vals,
404
+ np.where((id_vals != "") & (doc_vals != ""), id_vals + " - " + doc_vals, id_vals)
405
+ )
406
+ else:
407
+ df["cliente_full_label"] = id_vals + " - " + doc_vals
408
+ transformaciones.append("cliente_full_label: ID + DOC_CLIENTE_CLEAN + NOM_CLIENTE")
362
409
 
363
410
  # ── 10. Columnas TEXTO ──
364
411
  for col in df.columns:
@@ -25,6 +25,17 @@ _KEYWORDS_CANTIDAD = [
25
25
  ]
26
26
  _KEYWORDS_FECHA = ["fecha", "fec_", "venc"]
27
27
 
28
+ _ENTITY_LABELS = {
29
+ ("id_cliente", "nom_cliente"): "cliente_label",
30
+ ("id_vendedor", "nom_vendedor"): "vendedor_label",
31
+ ("id_articulo", "nom_articulo"): "articulo_label",
32
+ ("id_linea", "nom_linea"): "linea_label",
33
+ ("id_grupo", "nom_grupo"): "grupo_label",
34
+ ("id_tipo", "nom_tipo"): "tipo_label",
35
+ ("id_familia", "nom_familia"): "familia_label",
36
+ ("cod_sucursal", "nom_sucursal"): "sucursal_label",
37
+ }
38
+
28
39
  _MAPA_TPO_DOC = {
29
40
  "NCR": "NOTA DE CREDITO",
30
41
  "NDB": "NOTA DE DEBITO",
@@ -247,6 +258,26 @@ def _clasificar_transaccion(row: dict) -> str:
247
258
  return "indefinido"
248
259
 
249
260
 
261
+ def _build_entity_labels(df: pd.DataFrame) -> pd.DataFrame:
262
+ for (id_col, name_col), label_col in _ENTITY_LABELS.items():
263
+ id_exists = id_col in df.columns
264
+ name_exists = name_col in df.columns
265
+
266
+ if id_exists and name_exists:
267
+ id_vals = df[id_col].fillna("").astype(str).str.strip()
268
+ name_vals = df[name_col].fillna("").astype(str).str.strip()
269
+ df[label_col] = np.where(
270
+ (id_vals != "") & (name_vals != ""),
271
+ id_vals + " - " + name_vals,
272
+ np.where(id_vals != "", id_vals, name_vals)
273
+ )
274
+ elif id_exists:
275
+ df[label_col] = df[id_col].astype(str).str.strip()
276
+ elif name_exists:
277
+ df[label_col] = df[name_col].astype(str).str.strip()
278
+ return df
279
+
280
+
250
281
  def estabilizar_excel_crudo(ruta_archivo: str | Path) -> tuple[pd.DataFrame, dict]:
251
282
  ruta = Path(ruta_archivo)
252
283
  if not ruta.exists():
@@ -359,6 +390,22 @@ def estabilizar_excel_crudo(ruta_archivo: str | Path) -> tuple[pd.DataFrame, dic
359
390
  f"{col}: normalizado ({ruc_count} RUC, {dni_count} DNI, "
360
391
  f"{(~df[tipo_col].isin(['RUC','DNI'])).sum()} otros)"
361
392
  )
393
+
394
+ # ── 9b. CLIENTE_FULL_LABEL = ID + DOC_CLIENTE_CLEAN + NOMBRE ──
395
+ if "id_cliente" in df.columns and "doc_cliente_clean" in df.columns:
396
+ id_vals = df["id_cliente"].fillna("").astype(str).str.strip()
397
+ doc_vals = df["doc_cliente_clean"].fillna("").astype(str).str.strip()
398
+ name_vals = df["nom_cliente"].fillna("").astype(str).str.strip() if "nom_cliente" in df.columns else None
399
+
400
+ if name_vals is not None:
401
+ df["cliente_full_label"] = np.where(
402
+ (id_vals != "") & (doc_vals != "") & (name_vals != ""),
403
+ id_vals + " - " + doc_vals + " - " + name_vals,
404
+ np.where((id_vals != "") & (doc_vals != ""), id_vals + " - " + doc_vals, id_vals)
405
+ )
406
+ else:
407
+ df["cliente_full_label"] = id_vals + " - " + doc_vals
408
+ transformaciones.append("cliente_full_label: ID + DOC_CLIENTE_CLEAN + NOM_CLIENTE")
362
409
 
363
410
  # ── 10. Columnas TEXTO ──
364
411
  for col in df.columns:
@@ -386,6 +433,18 @@ def estabilizar_excel_crudo(ruta_archivo: str | Path) -> tuple[pd.DataFrame, dic
386
433
  df[col] = pd.to_numeric(df[col], errors="coerce").fillna(0.0)
387
434
  transformaciones.append(f"{col}: forzado a float64, NaN → 0.0")
388
435
 
436
+ # ── 12b. PRECIO_BASE (precio unitario por fila) ──
437
+ if "soles" in df.columns and cols_cantidad:
438
+ cant_col = cols_cantidad[0]
439
+ cant = df[cant_col].fillna(0)
440
+ soles = df["soles"].fillna(0)
441
+ df["precio_base"] = np.where(
442
+ cant != 0,
443
+ np.round(soles / cant.abs(), 4),
444
+ np.nan
445
+ )
446
+ transformaciones.append(f"precio_base: SOLES / {cant_col}")
447
+
389
448
  # ── 13. cantidad + cantidad_fae → cantidad_total, tipo_transaccion ──
390
449
  if "cantidad" in df.columns:
391
450
  if "cantidad_fae" in df.columns:
@@ -397,6 +456,13 @@ def estabilizar_excel_crudo(ruta_archivo: str | Path) -> tuple[pd.DataFrame, dic
397
456
  t_counts = df["tipo_transaccion"].value_counts().to_dict()
398
457
  transformaciones.append(f"tipo_transaccion: {t_counts}")
399
458
 
459
+ # ── 13b. Construir columnas _LABEL (ID - Nombre) para UI ──
460
+ cols_before_labels = list(df.columns)
461
+ df = _build_entity_labels(df)
462
+ label_cols_added = [c for c in df.columns if c.endswith("_label") and c not in cols_before_labels]
463
+ if label_cols_added:
464
+ transformaciones.append(f"labels: {', '.join(label_cols_added)} generados")
465
+
400
466
  # ── 14. Columnas FECHA ──
401
467
  for col in df.columns:
402
468
  if _coincide_keywords(col, _KEYWORDS_FECHA) and col != "fec_":
@@ -46,9 +46,12 @@ _SAMPLE_DF = pd.DataFrame({
46
46
  "ANHO": [2026.0, 2026.0, np.nan],
47
47
  "ID_ARTICULO": ["11030", "78456", np.nan],
48
48
  "NOM_CLIENTE": [" CLIENTE A ", "CLIENTE B", np.nan],
49
+ "ID_CLIENTE": ["001", "002", np.nan],
50
+ "DOC_CLIENTE": ["20491653745", "12345678", np.nan],
49
51
  "SOLES": [1172.10, "S/.2,500.50", np.nan],
50
52
  "FECHA_ORIG": ["24/06/2026", "20/06/2026", np.nan],
51
53
  "ID_LINEA": ["0111", "0178", np.nan],
54
+ "NOM_LINEA": ["LINEA A", "LINEA B", np.nan],
52
55
  })
53
56
 
54
57
 
@@ -104,6 +107,17 @@ class TestEstabilizarExcelCrudo(unittest.TestCase):
104
107
  with self.assertRaises(FileNotFoundError):
105
108
  estabilizar_excel_crudo("no_existe_archivo_falso_999.xls")
106
109
 
110
+ def test_cliente_full_label_creado(self):
111
+ df, _ = _patched_ingestion(_SAMPLE_DF)
112
+ self.assertIn("cliente_full_label", df.columns)
113
+ self.assertEqual(df["cliente_full_label"].iloc[0], "001 - 20491653745 - CLIENTE A")
114
+
115
+ def test_labels_generados(self):
116
+ df, meta = _patched_ingestion(_SAMPLE_DF)
117
+ self.assertIn("cliente_label", df.columns)
118
+ self.assertIn("linea_label", df.columns)
119
+ self.assertIn("cliente_full_label", df.columns)
120
+
107
121
 
108
122
  class TestColumnasDuplicadas(unittest.TestCase):
109
123
  def test_duplicados_se_sufijan(self):