Mejoras de sesion: estabilizacion, IA, Nexus Yonke y marketplace B2B
- F0 blindaje: Redis/Celery, pg_hba sin trust LAN, secretos a env 600, backups, webhook auth, jobs muertos - F1 caja: bugs POS, reembolsos al corte, apartados, CFDI, topes de descuento en cotizacion/apartado - F2/F7 IA de catalogo: autocategorizacion, dedup de SKUs con embeddings, OCR de facturas, descripciones, claves SAT, limpieza de fitment - F3 IA en el flujo: vision Yonke, preguntas MeLi, venta cruzada, anomalias, sinonimos, busqueda semantica (rerank/embeddings) - F4/F8 escala y cartera: /metrics+Prometheus+Grafana, pool, score de credito, churn, metricas multi-tenant - Nexus Yonke (M1-M6): modelo de datos 1-de-1, alta por donador, vitrina publica, MeLi usado, consignacion, garantia por dias, reportes, etiquetas - Marketplace B2B entre yonkes (catalogo + pedidos cross-tenant) - Seguridad: auth en /api/admin/* (35 endpoints), alertas por tenant - Migraciones v4.27/v4.28; timers expire-demos y rollup-metrics
This commit is contained in:
156
scripts/import_capi_inventory.py
Normal file
156
scripts/import_capi_inventory.py
Normal file
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python3
|
||||
"""One-off: import Capi Autopartes inventory from CSV into tenant_capi_autopartes.
|
||||
|
||||
Replicates the semantics of pos/blueprints/inventory_bp.py::bulk_import_items:
|
||||
- creates missing categories
|
||||
- INSERT new items (with generated barcode) or UPDATE existing by part_number
|
||||
- records initial stock on the main branch via inventory_engine.record_initial
|
||||
- registers sku_secundario aliases
|
||||
|
||||
Usage (as postgres):
|
||||
MASTER_DB_URL=postgresql://postgres@/nexus_autoparts \
|
||||
TENANT_DB_URL_TEMPLATE='postgresql://postgres@/{db_name}' \
|
||||
POS_JWT_SECRET=... REDIS_ENABLED=false \
|
||||
python3 scripts/import_capi_inventory.py
|
||||
"""
|
||||
|
||||
import csv
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'pos'))
|
||||
|
||||
import psycopg2 # noqa: E402
|
||||
from services.barcode_generator import generate_barcode # noqa: E402
|
||||
from services.inventory_engine import record_initial # noqa: E402
|
||||
|
||||
CSV_PATH = '/home/Autopartes/data/Inventario capi.csv'
|
||||
TENANT_DB = 'tenant_capi_autopartes'
|
||||
BRANCH_ID = 1
|
||||
COMMIT_EVERY = 500
|
||||
|
||||
|
||||
def parse_rows(path):
|
||||
raw = open(path, 'rb').read()
|
||||
text = None
|
||||
for enc in ('utf-8-sig', 'cp1252', 'latin-1'):
|
||||
try:
|
||||
text = raw.decode(enc)
|
||||
break
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
if text is None:
|
||||
raise RuntimeError('Could not decode CSV')
|
||||
return list(csv.DictReader(io.StringIO(text)))
|
||||
|
||||
|
||||
def to_decimal(val, default=0.0):
|
||||
try:
|
||||
v = float(str(val).replace(',', '').strip())
|
||||
return v
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
|
||||
def main():
|
||||
rows = parse_rows(CSV_PATH)
|
||||
print(f'Filas en CSV: {len(rows)}')
|
||||
|
||||
conn = psycopg2.connect(dbname=TENANT_DB)
|
||||
cur = conn.cursor()
|
||||
|
||||
# 1. Create categories
|
||||
cat_names = sorted({r['categoria'].strip() for r in rows if r['categoria'].strip()})
|
||||
for name in cat_names:
|
||||
cur.execute("INSERT INTO categories (name) VALUES (%s) ON CONFLICT DO NOTHING", (name,))
|
||||
conn.commit()
|
||||
cur.execute("SELECT id, name FROM categories")
|
||||
category_map = {r[1].strip().lower(): r[0] for r in cur.fetchall()}
|
||||
print(f'Categorías creadas/existentes: {len(category_map)}')
|
||||
|
||||
# 2. Import items
|
||||
created = updated = skipped = 0
|
||||
warnings = []
|
||||
|
||||
for i, row in enumerate(rows, start=1):
|
||||
part_number = row['numero_de_parte'].strip()
|
||||
name = row['nombre'].strip()
|
||||
if not part_number or not name:
|
||||
skipped += 1
|
||||
warnings.append(f'Fila {i}: sin numero_de_parte o nombre')
|
||||
continue
|
||||
|
||||
brand = row['marca'].strip()
|
||||
price_1 = to_decimal(row['precio'])
|
||||
stock = int(to_decimal(row['cantidad']))
|
||||
cost = to_decimal(row['costo'])
|
||||
description = (row.get('descripcion') or '').strip()
|
||||
category_name = row['categoria'].strip()
|
||||
category_id = category_map.get(category_name.lower()) if category_name else None
|
||||
|
||||
cur.execute("SELECT id FROM inventory WHERE part_number = %s", (part_number,))
|
||||
existing = cur.fetchone()
|
||||
|
||||
if existing:
|
||||
item_id = existing[0]
|
||||
cur.execute("""
|
||||
UPDATE inventory SET
|
||||
name = %s,
|
||||
brand = COALESCE(NULLIF(%s,''), brand),
|
||||
cost = CASE WHEN %s > 0 THEN %s ELSE cost END,
|
||||
price_1 = CASE WHEN %s > 0 THEN %s ELSE price_1 END,
|
||||
description = COALESCE(NULLIF(%s,''), description),
|
||||
category_id = COALESCE(%s, category_id)
|
||||
WHERE id = %s
|
||||
""", (name, brand, cost, cost, price_1, price_1, description, category_id, item_id))
|
||||
updated += 1
|
||||
if stock > 0:
|
||||
record_initial(conn, item_id, BRANCH_ID, stock, cost if cost > 0 else None)
|
||||
else:
|
||||
barcode = generate_barcode(conn, TENANT_DB)
|
||||
cur.execute("""
|
||||
INSERT INTO inventory
|
||||
(part_number, barcode, name, brand, cost, price_1, location, description, category_id, unit)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""", (part_number, barcode, name, brand, cost, price_1, '', description, category_id, 'PZA'))
|
||||
item_id = cur.fetchone()[0]
|
||||
created += 1
|
||||
if stock > 0:
|
||||
record_initial(conn, item_id, BRANCH_ID, stock, cost if cost > 0 else None)
|
||||
|
||||
sku_secondary = (row.get('sku_secundario') or '').strip()
|
||||
if sku_secondary:
|
||||
cur.execute("""
|
||||
INSERT INTO inventory_sku_aliases (inventory_id, sku, label)
|
||||
VALUES (%s, %s, %s) ON CONFLICT DO NOTHING
|
||||
""", (item_id, sku_secondary, 'Secundario'))
|
||||
|
||||
if i % COMMIT_EVERY == 0:
|
||||
conn.commit()
|
||||
print(f' {i}/{len(rows)} procesadas (nuevos={created}, act={updated})', flush=True)
|
||||
|
||||
conn.commit()
|
||||
|
||||
cur.execute("SELECT count(*) FROM inventory")
|
||||
total_inv = cur.fetchone()[0]
|
||||
cur.execute("SELECT COALESCE(SUM(stock),0) FROM inventory_stock WHERE branch_id = %s", (BRANCH_ID,))
|
||||
total_stock = cur.fetchone()[0]
|
||||
cur.execute("SELECT count(*) FROM inventory_sku_aliases")
|
||||
total_alias = cur.fetchone()[0]
|
||||
|
||||
print('\n=== RESUMEN ===')
|
||||
print(f'Nuevos: {created} | Actualizados: {updated} | Saltados: {skipped}')
|
||||
print(f'Total inventario: {total_inv} | Stock total sucursal {BRANCH_ID}: {total_stock} | Alias: {total_alias}')
|
||||
if warnings:
|
||||
print(f'Advertencias ({len(warnings)}):')
|
||||
for w in warnings[:20]:
|
||||
print(' -', w)
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user