- 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
157 lines
5.6 KiB
Python
157 lines
5.6 KiB
Python
#!/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()
|