Mejoras de sesion: estabilizacion, IA, Nexus Yonke y marketplace B2B
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled

- 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:
2026-10-12 00:57:31 +00:00
parent f2d4558b80
commit a386ca5b4a
165 changed files with 31313 additions and 2203 deletions

View 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()