feat(catalog): supplier catalog cleanup, fuzzy matching, and navigation fixes
- Cleaned 137+ fake engine-displacement models from supplier imports (v3/v4 scripts: Chevrolet, Ford, Chrysler, Dodge, Jeep, Nissan, etc.) - Removed 1,251+ corrupted models (INT. prefixes, year-suffix, torque specs, empty names, trailing-year variants) - Migrated supplier tables to master DB (supplier_catalog, supplier_catalog_compat, supplier_catalog_interchange) - Fixed _get_mye_ids_with_parts() to query supplier_catalog_compat from master DB so supplier-only vehicles appear for all tenants - Added fuzzy model matcher with parenthesis stripping, noise suffix removal, compact matching, prefix/substring fallback, model aliases, and ±3 year proximity - Matched compat rows: KEEP GREEN +14,152, KNADIAN +3,021, VAZLO +127,500, LUK +477, RAYBESTOS +1,743 - Added KNADIAN catalog importer with year-range expansion and future-year filtering - Added VAZLO catalog importer with position parsing and SKU-in-model cleanup - Added Keep Green, LUK, Yokomitsu, Raybestos catalog importers - Cache clearing after cleanups (_classify_cache_*, nexus:mye_ids:*, nexus:brand_mye_counts:*) Final match rates: - KEEP GREEN: 90.3% - VAZLO: 93.6% - YOKOMITSU: 100.0% - KNADIAN: 57.4% - LUK: 51.0% - RAYBESTOS: 55.9%
This commit is contained in:
183
scripts/import_rached_excel.py
Executable file
183
scripts/import_rached_excel.py
Executable file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Importar inventario de refaccionaria_rached desde Excel.
|
||||
|
||||
Archivo fuente: /home/Autopartes/data/PRODUCTOS_RACHED_2026.xlsx
|
||||
Hoja: Hoja1
|
||||
Columnas:
|
||||
A: Codigo -> part_number
|
||||
B: CB -> barcode (ignored, mostly empty)
|
||||
C: Cve -> sku_alias (inventory_sku_aliases)
|
||||
D: Descripcion -> name
|
||||
E: Precio Costo -> cost
|
||||
F: Precio Venta -> price_1
|
||||
|
||||
No hay columnas de stock, marca, ni vehiculo. Stock se deja en 0.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'pos'))
|
||||
|
||||
import psycopg2
|
||||
from services.barcode_generator import generate_barcodes_batch
|
||||
|
||||
# ─── Config ──────────────────────────────────────────
|
||||
DB_NAME = "tenant_refaccionaria_rached"
|
||||
BRANCH_ID = 1
|
||||
EXCEL_PATH = "/home/Autopartes/data/PRODUCTOS_RACHED_2026.xlsx"
|
||||
BATCH_SIZE = 500
|
||||
|
||||
# Connect as local postgres user (peer auth)
|
||||
conn = psycopg2.connect(f"dbname={DB_NAME} user=postgres")
|
||||
conn.autocommit = False
|
||||
cur = conn.cursor()
|
||||
|
||||
# ─── Read Excel ──────────────────────────────────────
|
||||
import openpyxl
|
||||
wb = openpyxl.load_workbook(EXCEL_PATH, data_only=True)
|
||||
ws = wb["Hoja1"]
|
||||
rows = list(ws.iter_rows(min_row=2, values_only=True))
|
||||
print(f"Filas leidas del Excel: {len(rows)}")
|
||||
|
||||
# ─── Pre-fetch existing part_numbers ─────────────────
|
||||
existing_map = {}
|
||||
cur.execute("SELECT id, part_number FROM inventory WHERE branch_id = %s", (BRANCH_ID,))
|
||||
for item_id, pn in cur.fetchall():
|
||||
existing_map[pn.strip().upper()] = item_id
|
||||
cur.close()
|
||||
conn.commit()
|
||||
|
||||
# ─── Prepare lists ───────────────────────────────────
|
||||
to_insert = [] # (part_number, name, cost, price_1)
|
||||
to_alias = [] # (part_number, alias_sku)
|
||||
skipped = 0
|
||||
|
||||
for row in rows:
|
||||
codigo = str(row[0]).strip() if row[0] is not None else ""
|
||||
cve = str(row[2]).strip() if row[2] is not None else ""
|
||||
descripcion = str(row[3]).strip() if row[3] is not None else ""
|
||||
precio_costo = float(row[4]) if row[4] is not None else 0.0
|
||||
precio_venta = float(row[5]) if row[5] is not None else 0.0
|
||||
|
||||
if not codigo or not descripcion:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
# Clean description (remove weird chars)
|
||||
descripcion = descripcion.replace("\x81", "").replace("\x80", "").strip()
|
||||
|
||||
to_insert.append((codigo, descripcion, precio_costo, precio_venta))
|
||||
if cve:
|
||||
to_alias.append((codigo, cve))
|
||||
|
||||
print(f"Filas validas para importar: {len(to_insert)}")
|
||||
print(f"Filas con SKU alternativo (Cve): {len(to_alias)}")
|
||||
print(f"Filas saltadas (sin codigo/descripcion): {skipped}")
|
||||
|
||||
# ─── Batch insert / update inventory ─────────────────
|
||||
cur = conn.cursor()
|
||||
inserted_count = 0
|
||||
updated_count = 0
|
||||
|
||||
# Split into new vs existing
|
||||
new_items = []
|
||||
update_items = []
|
||||
for codigo, descripcion, cost, price in to_insert:
|
||||
key = codigo.upper()
|
||||
if key in existing_map:
|
||||
update_items.append((descripcion, cost, price, existing_map[key]))
|
||||
else:
|
||||
new_items.append((codigo, descripcion, cost, price))
|
||||
|
||||
print(f"Nuevos: {len(new_items)} | Existentes a actualizar: {len(update_items)}")
|
||||
|
||||
# Generate barcodes for new items in batch
|
||||
barcodes = []
|
||||
if new_items:
|
||||
barcodes = generate_barcodes_batch(conn, DB_NAME, len(new_items))
|
||||
|
||||
# Insert new items
|
||||
for i, (codigo, descripcion, cost, price) in enumerate(new_items):
|
||||
barcode = barcodes[i]
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO inventory
|
||||
(branch_id, part_number, barcode, name, cost, price_1, unit, is_active)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||
ON CONFLICT (branch_id, part_number) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
cost = CASE WHEN EXCLUDED.cost > 0 THEN EXCLUDED.cost ELSE inventory.cost END,
|
||||
price_1 = CASE WHEN EXCLUDED.price_1 > 0 THEN EXCLUDED.price_1 ELSE inventory.price_1 END
|
||||
RETURNING id, (xmax = 0) AS inserted
|
||||
""",
|
||||
(BRANCH_ID, codigo, barcode, descripcion, cost, price, "PZA", True)
|
||||
)
|
||||
item_id, was_inserted = cur.fetchone()
|
||||
if was_inserted:
|
||||
inserted_count += 1
|
||||
else:
|
||||
updated_count += 1
|
||||
# Add to map for alias linking
|
||||
existing_map[codigo.upper()] = item_id
|
||||
|
||||
if (i + 1) % BATCH_SIZE == 0:
|
||||
conn.commit()
|
||||
print(f" Procesados {i + 1}/{len(new_items)} nuevos...")
|
||||
|
||||
# Update existing items (that weren't caught by ON CONFLICT above, if any)
|
||||
for descripcion, cost, price, item_id in update_items:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE inventory SET
|
||||
name = %s,
|
||||
cost = CASE WHEN %s > 0 THEN %s ELSE cost END,
|
||||
price_1 = CASE WHEN %s > 0 THEN %s ELSE price_1 END
|
||||
WHERE id = %s
|
||||
""",
|
||||
(descripcion, cost, cost, price, price, item_id)
|
||||
)
|
||||
updated_count += 1
|
||||
|
||||
conn.commit()
|
||||
print(f"Insertados: {inserted_count} | Actualizados: {updated_count}")
|
||||
|
||||
# ─── Insert SKU aliases ──────────────────────────────
|
||||
alias_inserted = 0
|
||||
alias_skipped = 0
|
||||
for codigo, cve in to_alias:
|
||||
item_id = existing_map.get(codigo.upper())
|
||||
if not item_id:
|
||||
alias_skipped += 1
|
||||
continue
|
||||
try:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO inventory_sku_aliases (inventory_id, sku, label)
|
||||
VALUES (%s, %s, %s)
|
||||
ON CONFLICT (inventory_id, sku) DO NOTHING
|
||||
""",
|
||||
(item_id, cve, "Cve")
|
||||
)
|
||||
if cur.rowcount > 0:
|
||||
alias_inserted += 1
|
||||
except Exception as e:
|
||||
print(f" Alias error for {codigo}/{cve}: {e}")
|
||||
alias_skipped += 1
|
||||
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
print("\n========================================")
|
||||
print("IMPORTACION RACHED COMPLETADA")
|
||||
print("========================================")
|
||||
print(f"Filas procesadas: {len(to_insert)}")
|
||||
print(f"Nuevos insertados: {inserted_count}")
|
||||
print(f"Exist. actualizados:{updated_count}")
|
||||
print(f"SKU aliases creados:{alias_inserted}")
|
||||
print(f"Aliases fallidos: {alias_skipped}")
|
||||
print(f"Filas saltadas: {skipped}")
|
||||
print("========================================")
|
||||
Reference in New Issue
Block a user