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:
@@ -35,194 +35,93 @@ def get_compat_source(tenant_id):
|
||||
return 'both'
|
||||
|
||||
|
||||
def _norm_pn(pn):
|
||||
"""Normalize a part number: uppercase, strip spaces and separators."""
|
||||
if not pn:
|
||||
return ''
|
||||
return (str(pn).replace(' ', '').replace('-', '').replace('/', '')
|
||||
.replace('.', '').replace('_', '').upper())
|
||||
|
||||
|
||||
def auto_match_vehicle_compatibility(master_conn, tenant_conn, inventory_id, part_number,
|
||||
brand=None, name=None):
|
||||
"""Find vehicle compatibility for an inventory item by part_number.
|
||||
"""Find vehicle compatibility for an inventory item via the supplier catalog.
|
||||
|
||||
Searches (in order of precision):
|
||||
1. parts.oem_part_number exact normalized
|
||||
2. aftermarket_parts.part_number exact normalized
|
||||
3. part_cross_references.cross_reference_number exact normalized
|
||||
4. parts.oem_part_number partial (ILIKE) — strips separators too
|
||||
5. aftermarket_parts.part_number partial (ILIKE)
|
||||
6. part_cross_references.cross_reference_number partial (ILIKE)
|
||||
7. parts.name_part / parts.name_es by inventory item name (ILIKE)
|
||||
8. parts.name_part / parts.name_es by inventory item name (tsvector)
|
||||
Legacy note: the original implementation queried the removed TecDoc tables
|
||||
(parts / aftermarket_parts / part_cross_references / vehicle_parts) and
|
||||
always failed. This version uses the LIVE master tables:
|
||||
1. supplier_catalog.sku exact-normalized
|
||||
2. supplier_catalog_interchange.part_number exact-normalized (cross-refs)
|
||||
3. prefix fallback on catalog SKU (only for >=5 char numbers, few matches)
|
||||
then collects DISTINCT model_year_engine_id from supplier_catalog_compat
|
||||
and inserts them into the tenant inventory_vehicle_compat table.
|
||||
|
||||
If a brand hint is provided, results are preferentially filtered to
|
||||
vehicles of that brand.
|
||||
|
||||
Returns:
|
||||
dict: {'matched': bool, 'matches': [...], 'myes': [...]}
|
||||
Returns dict {'matched': bool, 'matches': [...], 'myes': [...], 'inserted': int}
|
||||
(same shape as the legacy implementation).
|
||||
"""
|
||||
cur = master_conn.cursor()
|
||||
oem_ids = set()
|
||||
clean_pn = _norm_pn(part_number)
|
||||
catalog_ids = set()
|
||||
|
||||
# Normalization helpers
|
||||
clean_pn = (part_number or '').replace(' ', '').upper()
|
||||
no_seps = clean_pn.replace('-', '').replace('/', '').replace('.', '').replace('_', '')
|
||||
name_q = (name or '').strip()
|
||||
brand_hint = (brand or '').strip().upper()
|
||||
|
||||
# ── 1. Exact normalized ──────────────────────────────────────────────
|
||||
if clean_pn:
|
||||
# 1. Exact match on catalog SKU (with and without inner spaces)
|
||||
cur.execute("""
|
||||
SELECT id_part FROM parts
|
||||
WHERE REPLACE(UPPER(oem_part_number), ' ', '') = %s
|
||||
""", (clean_pn,))
|
||||
SELECT id FROM supplier_catalog
|
||||
WHERE REPLACE(UPPER(sku), '-', '') = %s
|
||||
OR REPLACE(UPPER(REPLACE(sku, ' ', '')), '-', '') = %s
|
||||
LIMIT 50
|
||||
""", (clean_pn, clean_pn))
|
||||
for r in cur.fetchall():
|
||||
oem_ids.add(r[0])
|
||||
catalog_ids.add(r[0])
|
||||
|
||||
if clean_pn and not oem_ids:
|
||||
cur.execute("""
|
||||
SELECT DISTINCT oem_part_id FROM aftermarket_parts
|
||||
WHERE REPLACE(UPPER(part_number), ' ', '') = %s
|
||||
""", (clean_pn,))
|
||||
for r in cur.fetchall():
|
||||
if r[0]:
|
||||
oem_ids.add(r[0])
|
||||
|
||||
if clean_pn and not oem_ids:
|
||||
cur.execute("""
|
||||
SELECT DISTINCT part_id FROM part_cross_references
|
||||
WHERE REPLACE(UPPER(cross_reference_number), ' ', '') = %s
|
||||
""", (clean_pn,))
|
||||
for r in cur.fetchall():
|
||||
if r[0]:
|
||||
oem_ids.add(r[0])
|
||||
|
||||
# ── 2. Partial / separator-stripped ──────────────────────────────────
|
||||
if clean_pn and len(clean_pn) >= 3 and len(oem_ids) < 10:
|
||||
# OEM part number partial
|
||||
cur.execute("""
|
||||
SELECT id_part FROM parts
|
||||
WHERE REPLACE(UPPER(oem_part_number), ' ', '') LIKE %s
|
||||
AND id_part NOT IN (SELECT unnest(%s::int[]))
|
||||
LIMIT 20
|
||||
""", (f'%{clean_pn}%', list(oem_ids) or [0]))
|
||||
for r in cur.fetchall():
|
||||
oem_ids.add(r[0])
|
||||
|
||||
# Aftermarket partial
|
||||
if len(oem_ids) < 10:
|
||||
# 2. Exact match on interchange part numbers (cross references)
|
||||
if not catalog_ids:
|
||||
cur.execute("""
|
||||
SELECT DISTINCT oem_part_id FROM aftermarket_parts
|
||||
WHERE REPLACE(UPPER(part_number), ' ', '') LIKE %s
|
||||
AND oem_part_id NOT IN (SELECT unnest(%s::int[]))
|
||||
LIMIT 20
|
||||
""", (f'%{clean_pn}%', list(oem_ids) or [0]))
|
||||
SELECT DISTINCT catalog_id FROM supplier_catalog_interchange
|
||||
WHERE REPLACE(UPPER(REPLACE(part_number, '-', '')), ' ', '') = %s
|
||||
LIMIT 50
|
||||
""", (clean_pn,))
|
||||
for r in cur.fetchall():
|
||||
if r[0]:
|
||||
oem_ids.add(r[0])
|
||||
catalog_ids.add(r[0])
|
||||
|
||||
# Cross-reference partial
|
||||
if len(oem_ids) < 10:
|
||||
# 3. Prefix fallback for longer numbers when exact found little
|
||||
if len(clean_pn) >= 5 and len(catalog_ids) < 3:
|
||||
cur.execute("""
|
||||
SELECT DISTINCT part_id FROM part_cross_references
|
||||
WHERE REPLACE(UPPER(cross_reference_number), ' ', '') LIKE %s
|
||||
AND part_id NOT IN (SELECT unnest(%s::int[]))
|
||||
LIMIT 20
|
||||
""", (f'%{clean_pn}%', list(oem_ids) or [0]))
|
||||
for r in cur.fetchall():
|
||||
if r[0]:
|
||||
oem_ids.add(r[0])
|
||||
|
||||
# Separator-stripped fallback (e.g. KYB-343412 → KYB343412)
|
||||
if len(no_seps) >= 4 and len(oem_ids) < 10:
|
||||
cur.execute("""
|
||||
SELECT id_part FROM parts
|
||||
WHERE REPLACE(REPLACE(REPLACE(REPLACE(UPPER(oem_part_number), ' ', ''), '-', ''), '/', ''), '.', '') = %s
|
||||
AND id_part NOT IN (SELECT unnest(%s::int[]))
|
||||
SELECT id FROM supplier_catalog
|
||||
WHERE REPLACE(UPPER(sku), '-', '') LIKE %s
|
||||
LIMIT 10
|
||||
""", (no_seps, list(oem_ids) or [0]))
|
||||
""", (clean_pn[:8] + '%',))
|
||||
for r in cur.fetchall():
|
||||
oem_ids.add(r[0])
|
||||
catalog_ids.add(r[0])
|
||||
|
||||
# ── 3. Search by item name (when part_number yields nothing) ─────────
|
||||
if not oem_ids and name_q and len(name_q) >= 3:
|
||||
# Name-based ILIKE search on parts
|
||||
mye_ids = []
|
||||
if catalog_ids:
|
||||
cur.execute("""
|
||||
SELECT id_part FROM parts
|
||||
WHERE UPPER(name_part) LIKE %s OR UPPER(COALESCE(name_es, '')) LIKE %s
|
||||
LIMIT 20
|
||||
""", (f'%{name_q.upper()}%', f'%{name_q.upper()}%'))
|
||||
for r in cur.fetchall():
|
||||
oem_ids.add(r[0])
|
||||
|
||||
# Name-based ILIKE search on aftermarket_parts
|
||||
if len(oem_ids) < 20:
|
||||
cur.execute("""
|
||||
SELECT DISTINCT oem_part_id FROM aftermarket_parts
|
||||
WHERE UPPER(COALESCE(name_aftermarket_parts, '')) LIKE %s
|
||||
OR UPPER(COALESCE(name_es, '')) LIKE %s
|
||||
LIMIT 20
|
||||
""", (f'%{name_q.upper()}%', f'%{name_q.upper()}%'))
|
||||
for r in cur.fetchall():
|
||||
if r[0]:
|
||||
oem_ids.add(r[0])
|
||||
|
||||
if not oem_ids:
|
||||
cur.close()
|
||||
return {'matched': False, 'matches': [], 'myes': []}
|
||||
|
||||
# ── Resolve MYEs ─────────────────────────────────────────────────────
|
||||
oem_ids = list(oem_ids)
|
||||
|
||||
if brand_hint and brand_hint != 'GENERAL':
|
||||
# Brand-aware: only MYEs for vehicles of the hinted brand
|
||||
cur.execute("""
|
||||
SELECT DISTINCT vp.model_year_engine_id
|
||||
FROM vehicle_parts vp
|
||||
JOIN model_year_engine mye ON mye.id_mye = vp.model_year_engine_id
|
||||
JOIN models m ON m.id_model = mye.model_id
|
||||
JOIN brands b ON b.id_brand = m.brand_id
|
||||
WHERE vp.part_id = ANY(%s)
|
||||
AND b.name_brand = %s
|
||||
SELECT DISTINCT model_year_engine_id FROM supplier_catalog_compat
|
||||
WHERE catalog_id = ANY(%s) AND model_year_engine_id IS NOT NULL
|
||||
LIMIT 500
|
||||
""", (oem_ids, brand_hint))
|
||||
""", (list(catalog_ids),))
|
||||
mye_ids = [r[0] for r in cur.fetchall()]
|
||||
|
||||
# Fallback: if brand filter yields nothing, the brand hint may be an
|
||||
# aftermarket supplier (e.g. Motorcraft, NGK, Bosch) rather than an
|
||||
# OEM vehicle brand. Search without brand filter.
|
||||
if not mye_ids:
|
||||
cur.execute("""
|
||||
SELECT DISTINCT model_year_engine_id
|
||||
FROM vehicle_parts
|
||||
WHERE part_id = ANY(%s)
|
||||
LIMIT 500
|
||||
""", (oem_ids,))
|
||||
mye_ids = [r[0] for r in cur.fetchall()]
|
||||
else:
|
||||
# No brand hint — return all MYEs for these parts
|
||||
cur.execute("""
|
||||
SELECT DISTINCT model_year_engine_id
|
||||
FROM vehicle_parts
|
||||
WHERE part_id = ANY(%s)
|
||||
LIMIT 500
|
||||
""", (oem_ids,))
|
||||
mye_ids = [r[0] for r in cur.fetchall()]
|
||||
|
||||
cur.close()
|
||||
|
||||
# ── Insert into tenant table ─────────────────────────────────────────
|
||||
inserted = 0
|
||||
cur2 = tenant_conn.cursor()
|
||||
for mye_id in mye_ids:
|
||||
cur2.execute("""
|
||||
INSERT INTO inventory_vehicle_compat
|
||||
(inventory_id, model_year_engine_id, source, confidence)
|
||||
VALUES (%s, %s, 'auto_match', 1.0)
|
||||
ON CONFLICT (inventory_id, model_year_engine_id) DO NOTHING
|
||||
""", (inventory_id, mye_id))
|
||||
if cur2.rowcount > 0:
|
||||
inserted += 1
|
||||
tenant_conn.commit()
|
||||
cur2.close()
|
||||
if mye_ids:
|
||||
cur2 = tenant_conn.cursor()
|
||||
for mye_id in mye_ids:
|
||||
cur2.execute("""
|
||||
INSERT INTO inventory_vehicle_compat
|
||||
(inventory_id, model_year_engine_id, source, confidence)
|
||||
VALUES (%s, %s, 'supplier_catalog', 0.95)
|
||||
ON CONFLICT (inventory_id, model_year_engine_id) DO NOTHING
|
||||
""", (inventory_id, mye_id))
|
||||
if cur2.rowcount > 0:
|
||||
inserted += 1
|
||||
tenant_conn.commit()
|
||||
cur2.close()
|
||||
|
||||
return {
|
||||
'matched': len(mye_ids) > 0,
|
||||
'matches': oem_ids,
|
||||
'matches': sorted(catalog_ids),
|
||||
'myes': mye_ids,
|
||||
'inserted': inserted,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user