From 50c0dbe7d4079421b38e6a9e212a2ccc96f75adf Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Mon, 18 May 2026 19:32:35 +0000 Subject: [PATCH] =?UTF-8?q?feat(inventory):=20Qwen=20vehicle=20compatibili?= =?UTF-8?q?ty=20=E2=80=94=20store=20AI=20unmatched=20vehicles=20as=20text,?= =?UTF-8?q?=20Celery=20background=20sync,=20fix=20brand=20filter=20fallbac?= =?UTF-8?q?k,=20increase=20vehicle=20limits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pos/blueprints/inventory_bp.py | 71 +++++++---- pos/migrations/v3.2_qwen_vehicle_compat.sql | 53 ++++++++ pos/services/inventory_vehicle_compat.py | 134 +++++++++++++++----- pos/services/qwen_fitment.py | 131 ++++++++++++------- pos/static/js/inventory.js | 6 +- pos/tasks.py | 44 +++++++ 6 files changed, 330 insertions(+), 109 deletions(-) create mode 100644 pos/migrations/v3.2_qwen_vehicle_compat.sql diff --git a/pos/blueprints/inventory_bp.py b/pos/blueprints/inventory_bp.py index 6d3fe66..1ea2f31 100644 --- a/pos/blueprints/inventory_bp.py +++ b/pos/blueprints/inventory_bp.py @@ -18,8 +18,10 @@ from services.audit import log_action from tenant_db import get_master_conn from services.inventory_vehicle_compat import ( auto_match_vehicle_compatibility, add_compatibility, remove_compatibility, - remove_all_compatibility, get_compatibility, search_mye, get_compat_source, + remove_compatibility_by_id, remove_all_compatibility, get_compatibility, + search_mye, get_compat_source, ) +from tasks import sync_vehicle_compatibility_task inventory_bp = Blueprint('inventory', __name__, url_prefix='/pos/api/inventory') @@ -287,29 +289,40 @@ def create_item(): compat_source = get_compat_source(g.tenant_id) qwen_added = 0 - # TecDoc auto-match - if compat_source in ('tecdoc', 'both'): - try: - master = get_master_conn() - auto_match_vehicle_compatibility(master, conn, item_id, data['part_number'], - brand=data.get('brand'), name=data.get('name')) - master.close() - except Exception as am_err: - print(f"[auto_match] Error for item {item_id}: {am_err}") + # Offload to Celery background task if possible (QWEN can take 90s+) + try: + sync_vehicle_compatibility_task.delay( + g.tenant_id, item_id, data['part_number'], + data['name'], data.get('brand', ''), compat_source + ) + compat_background = True + except Exception as celery_err: + print(f"[celery] Failed to queue compatibility task for item {item_id}: {celery_err}") + compat_background = False - # QWEN AI fitment - if compat_source in ('qwen', 'both'): - try: - from services.qwen_fitment import get_vehicle_fitment - from services.inventory_vehicle_compat import save_qwen_fitment - fitment = get_vehicle_fitment( - data['part_number'], - data['name'], - data.get('brand', '') - ) - qwen_added = save_qwen_fitment(conn, item_id, fitment) - except Exception as qwen_err: - print(f"[qwen_fitment] Error for item {item_id}: {qwen_err}") + if not compat_background: + # Fallback: synchronous processing + if compat_source in ('tecdoc', 'both'): + try: + master = get_master_conn() + auto_match_vehicle_compatibility(master, conn, item_id, data['part_number'], + brand=data.get('brand'), name=data.get('name')) + master.close() + except Exception as am_err: + print(f"[auto_match] Error for item {item_id}: {am_err}") + + if compat_source in ('qwen', 'both'): + try: + from services.qwen_fitment import get_vehicle_fitment + from services.inventory_vehicle_compat import save_qwen_fitment + fitment = get_vehicle_fitment( + data['part_number'], + data['name'], + data.get('brand', '') + ) + qwen_added = save_qwen_fitment(conn, item_id, fitment) + except Exception as qwen_err: + print(f"[qwen_fitment] Error for item {item_id}: {qwen_err}") conn.close() return jsonify({ @@ -317,6 +330,7 @@ def create_item(): 'barcode': barcode, 'message': 'Item created', 'vehicle_compatibilities_added': qwen_added, + 'vehicle_compat_queued': compat_background, }), 201 except Exception as e: @@ -1482,13 +1496,16 @@ def add_item_vehicle(item_id): conn.close() -@inventory_bp.route('/items//vehicles/', methods=['DELETE']) +@inventory_bp.route('/items//vehicles/', methods=['DELETE']) @require_auth('inventory.edit') -def delete_item_vehicle(item_id, mye_id): - """Remove a vehicle compatibility.""" +def delete_item_vehicle(item_id, compat_id): + """Remove a vehicle compatibility by its row id. + + Works for both TecDoc-linked (mye_id present) and text-only QWEN records. + """ conn = get_tenant_conn(g.tenant_id) try: - deleted = remove_compatibility(conn, item_id, mye_id) + deleted = remove_compatibility_by_id(conn, compat_id) return jsonify({'message': 'Compatibility removed', 'deleted': deleted}) finally: conn.close() diff --git a/pos/migrations/v3.2_qwen_vehicle_compat.sql b/pos/migrations/v3.2_qwen_vehicle_compat.sql new file mode 100644 index 0000000..7f0b838 --- /dev/null +++ b/pos/migrations/v3.2_qwen_vehicle_compat.sql @@ -0,0 +1,53 @@ +-- v3.2 QWEN Vehicle Compatibility — store unmatched AI vehicles as text +-- Allows saving QWEN fitment results even when the vehicle is not in TecDoc. + +-- 1. Allow NULL model_year_engine_id for QWEN vehicles not in master DB +ALTER TABLE inventory_vehicle_compat + ALTER COLUMN model_year_engine_id DROP NOT NULL; + +-- 2. Add text columns for QWEN vehicle details +ALTER TABLE inventory_vehicle_compat + ADD COLUMN IF NOT EXISTS make VARCHAR(100), + ADD COLUMN IF NOT EXISTS model VARCHAR(100), + ADD COLUMN IF NOT EXISTS year INTEGER, + ADD COLUMN IF NOT EXISTS engine VARCHAR(100), + ADD COLUMN IF NOT EXISTS engine_code VARCHAR(50); + +-- 3. Drop old unique constraint and recreate to handle NULL mye_id +-- (PostgreSQL allows multiple NULLs in a UNIQUE constraint) +ALTER TABLE inventory_vehicle_compat + DROP CONSTRAINT IF EXISTS inventory_vehicle_compat_inventory_id_model_year_engine_id_key; + +ALTER TABLE inventory_vehicle_compat + ADD CONSTRAINT inventory_vehicle_compat_unique_match + UNIQUE (inventory_id, model_year_engine_id, make, model, year); + +-- 4. Index for fast filtering by inventory + text vehicles +CREATE INDEX IF NOT EXISTS idx_ivc_text_vehicle + ON inventory_vehicle_compat(inventory_id, make, model, year) + WHERE model_year_engine_id IS NULL; + +-- 5. Update view to include new columns +DROP VIEW IF EXISTS v_inventory_vehicle_compat; +CREATE VIEW v_inventory_vehicle_compat AS +SELECT + ivc.id, + ivc.inventory_id, + ivc.model_year_engine_id, + ivc.make, + ivc.model, + ivc.year, + ivc.engine, + ivc.engine_code, + ivc.source, + ivc.confidence, + ivc.created_at, + i.part_number, + i.name as item_name, + i.brand as item_brand, + i.price_1, + i.price_2, + i.price_3, + i.image_url +FROM inventory_vehicle_compat ivc +JOIN inventory i ON i.id = ivc.inventory_id; diff --git a/pos/services/inventory_vehicle_compat.py b/pos/services/inventory_vehicle_compat.py index a4b79ee..98b31cf 100644 --- a/pos/services/inventory_vehicle_compat.py +++ b/pos/services/inventory_vehicle_compat.py @@ -178,18 +178,31 @@ def auto_match_vehicle_compatibility(master_conn, tenant_conn, inventory_id, par JOIN brands b ON b.id_brand = m.brand_id WHERE vp.part_id = ANY(%s) AND b.name_brand = %s - LIMIT 200 + LIMIT 500 """, (oem_ids, brand_hint)) + 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 200 + LIMIT 500 """, (oem_ids,)) + mye_ids = [r[0] for r in cur.fetchall()] - mye_ids = [r[0] for r in cur.fetchall()] cur.close() # ── Insert into tenant table ───────────────────────────────────────── @@ -243,6 +256,20 @@ def remove_compatibility(tenant_conn, inventory_id, model_year_engine_id): return deleted +def remove_compatibility_by_id(tenant_conn, compat_id): + """Remove a compatibility by its primary key (works for both MYE-linked + and text-only QWEN records).""" + cur = tenant_conn.cursor() + cur.execute(""" + DELETE FROM inventory_vehicle_compat + WHERE id = %s + """, (compat_id,)) + deleted = cur.rowcount + tenant_conn.commit() + cur.close() + return deleted + + def remove_all_compatibility(tenant_conn, inventory_id): cur = tenant_conn.cursor() cur.execute(""" @@ -259,14 +286,18 @@ def get_compatibility(tenant_conn, master_conn, inventory_id): Queries inventory_vehicle_compat from the tenant DB, then resolves vehicle details (brand/model/year/engine) from the master DB. + + Vehicles with model_year_engine_id IS NULL are text-only QWEN records + (master DB lacks the vehicle) and are returned using their stored text. """ - # 1. Get MYE IDs + metadata from tenant + # 1. Get all rows from tenant cur_t = tenant_conn.cursor() cur_t.execute(""" - SELECT model_year_engine_id, source, confidence, created_at + SELECT id, model_year_engine_id, make, model, year, engine, engine_code, + source, confidence, created_at FROM inventory_vehicle_compat WHERE inventory_id = %s - ORDER BY model_year_engine_id + ORDER BY COALESCE(make, ''), COALESCE(model, ''), COALESCE(year, 0) """, (inventory_id,)) rows = cur_t.fetchall() cur_t.close() @@ -274,34 +305,52 @@ def get_compatibility(tenant_conn, master_conn, inventory_id): if not rows: return [] - mye_ids = [r[0] for r in rows] - - # 2. Resolve vehicle details from master DB - cur_m = master_conn.cursor() - cur_m.execute(""" - SELECT mye.id_mye, b.name_brand, m.name_model, y.year_car, e.name_engine - FROM model_year_engine mye - JOIN models m ON m.id_model = mye.model_id - JOIN brands b ON b.id_brand = m.brand_id - JOIN years y ON y.id_year = mye.year_id - JOIN engines e ON e.id_engine = mye.engine_id - WHERE mye.id_mye = ANY(%s) - ORDER BY b.name_brand, m.name_model, y.year_car - """, (mye_ids,)) - details = {r[0]: r for r in cur_m.fetchall()} - cur_m.close() + # 2. Resolve MYE-linked vehicles from master DB + mye_ids = [r[0] for r in rows if r[0] is not None] + details = {} + if mye_ids: + cur_m = master_conn.cursor() + cur_m.execute(""" + SELECT mye.id_mye, b.name_brand, m.name_model, y.year_car, e.name_engine + FROM model_year_engine mye + JOIN models m ON m.id_model = mye.model_id + JOIN brands b ON b.id_brand = m.brand_id + JOIN years y ON y.id_year = mye.year_id + JOIN engines e ON e.id_engine = mye.engine_id + WHERE mye.id_mye = ANY(%s) + ORDER BY b.name_brand, m.name_model, y.year_car + """, (mye_ids,)) + details = {r[0]: r for r in cur_m.fetchall()} + cur_m.close() # 3. Merge result = [] - for mye_id, source, confidence, created_at in rows: - d = details.get(mye_id) - if d: + for (compat_id, mye_id, make, model, year, engine, engine_code, + source, confidence, created_at) in rows: + if mye_id is not None and mye_id in details: + d = details[mye_id] result.append({ + 'id': compat_id, 'model_year_engine_id': mye_id, 'brand': d[1], 'model': d[2], 'year': d[3], 'engine': d[4], + 'engine_code': '', + 'source': source, + 'confidence': float(confidence), + 'created_at': str(created_at), + }) + else: + # Text-only QWEN record + result.append({ + 'id': compat_id, + 'model_year_engine_id': None, + 'brand': make or '', + 'model': model or '', + 'year': year, + 'engine': engine or '', + 'engine_code': engine_code or '', 'source': source, 'confidence': float(confidence), 'created_at': str(created_at), @@ -374,6 +423,9 @@ def batch_add_compatibilities(tenant_conn, inventory_id, mye_ids, source='manual def save_qwen_fitment(tenant_conn, inventory_id, fitment_result): """Save QWEN fitment results into inventory_vehicle_compat. + Supports both TecDoc-linked vehicles (mye_id present) and text-only + QWEN vehicles (mye_id=None) when the master DB lacks the vehicle. + Args: tenant_conn: Connection to tenant DB. inventory_id: The inventory item ID. @@ -390,14 +442,30 @@ def save_qwen_fitment(tenant_conn, inventory_id, fitment_result): cur = tenant_conn.cursor() for v in vehicles: mye_id = v.get('mye_id') - if not mye_id: - continue - cur.execute(""" - INSERT INTO inventory_vehicle_compat - (inventory_id, model_year_engine_id, source, confidence, created_at) - VALUES (%s, %s, 'qwen_ai', %s, NOW()) - ON CONFLICT (inventory_id, model_year_engine_id) DO NOTHING - """, (inventory_id, mye_id, fitment_result.get('confidence', 0))) + if mye_id is not None and mye_id: + # TecDoc-linked vehicle + cur.execute(""" + INSERT INTO inventory_vehicle_compat + (inventory_id, model_year_engine_id, source, confidence, created_at) + VALUES (%s, %s, 'qwen_ai', %s, NOW()) + ON CONFLICT (inventory_id, model_year_engine_id, make, model, year) DO NOTHING + """, (inventory_id, mye_id, fitment_result.get('confidence', 0))) + else: + # Text-only QWEN vehicle (master DB doesn't have this vehicle) + cur.execute(""" + INSERT INTO inventory_vehicle_compat + (inventory_id, model_year_engine_id, make, model, year, engine, engine_code, source, confidence, created_at) + VALUES (%s, NULL, %s, %s, %s, %s, %s, 'qwen_ai', %s, NOW()) + ON CONFLICT (inventory_id, model_year_engine_id, make, model, year) DO NOTHING + """, ( + inventory_id, + v.get('make', '') or '', + v.get('model', '') or '', + v.get('year', 0) or 0, + v.get('engine', '') or '', + v.get('engine_code', '') or '', + fitment_result.get('confidence', 0), + )) if cur.rowcount > 0: inserted += 1 tenant_conn.commit() diff --git a/pos/services/qwen_fitment.py b/pos/services/qwen_fitment.py index a9f4ebf..f2a0388 100644 --- a/pos/services/qwen_fitment.py +++ b/pos/services/qwen_fitment.py @@ -42,16 +42,22 @@ def get_vehicle_fitment(part_number, name, brand): {'role': 'user', 'content': prompt} ], 'temperature': 0.2, - 'max_tokens': 4096, + 'max_tokens': 8192, }, - timeout=45, + timeout=120, ) response.raise_for_status() raw = response.json() + finish_reason = None if raw.get('choices') and len(raw['choices']) > 0: - msg = raw['choices'][0].get('message', {}) + choice = raw['choices'][0] + msg = choice.get('message', {}) + finish_reason = choice.get('finish_reason') if msg: content = msg.get('content') or '' + if not content: + # Fallback for reasoning models that return output in reasoning_content + content = msg.get('reasoning_content') or '' if content: break except requests.RequestException as exc: @@ -62,6 +68,8 @@ def get_vehicle_fitment(part_number, name, brand): if not content: err_msg = f'QWEN request failed: {last_error}' if last_error else 'Empty response from QWEN after 3 attempts' + if finish_reason == 'length': + err_msg += ' (response truncated by token limit — consider reducing prompt or increasing max_tokens)' return {'vehicles': [], 'confidence': 0, 'notes': err_msg} # Parse JSON from QWEN response (sometimes wrapped in markdown) @@ -91,32 +99,21 @@ def _build_prompt(part_number, name, brand): - Nombre/descripcion: {name} - Marca del fabricante: {brand_str} -Devuelve UNICAMENTE un JSON valido (sin markdown, sin backticks, sin texto adicional) con esta estructura exacta: -{{ - "vehicles": [ - {{ - "make": "Toyota", - "model": "Corolla", - "year": 2015, - "engine": "1.8L 16V", - "engine_code": "2ZR-FE", - "notes": "Sedan y hatchback" - }} - ], - "confidence": 0.92, - "notes": "Compatible con plataforma E170. Verificar traccion delantera." -}} +Devuelve UNICAMENTE un JSON valido (sin markdown, sin backticks, sin texto adicional) con esta estructura: +{{"vehicles":[{{"make":"Toyota","model":"Corolla","year_range":"2014-2019","engine":"1.8L","engine_code":"2ZR-FE","notes":""}}],"confidence":0.92,"notes":""}} -Reglas obligatorias: -1. "make" = marca del vehiculo (ej: Toyota, Nissan, Ford, Volkswagen, Chevrolet, Honda, Hyundai, Kia, Mazda, Subaru). -2. "model" = modelo exacto. Si hay variantes (ej: Civic Sedan vs Civic Coupe), incluye la variante. -3. "year" = ano numerico (int). Si hay rango de anos (ej: 2003-2008), genera una entrada POR CADA ANO del rango. NO uses rangos. -4. "engine" = descripcion del motor (ej: "1.8L", "2.0L TDI", "V6 3.5L", "1.6L Turbo"). Si no conoces el motor, usa "desconocido". -5. "engine_code" = codigo exacto del motor SI LO CONOCES (ej: "2ZR-FE", "K24Z7", "EA888"). Si no lo conoces, usa "" (string vacio). -6. Devuelve TODOS los vehiculos compatibles que conozcas. Minimo 1, maximo 100. Para piezas genericas (bujias, filtros, balatas, amortiguadores) incluye TODOS los modelos aplicables. -7. "confidence" entre 0.0 y 1.0. Usa valores altos (>0.85) solo si estas muy seguro. -8. Incluye marcas y modelos populares en Mexico (Nissan Tsuru, VW Sedan/Vocho, Chevy Monza, Ford Ka, etc.) cuando apliquen. -9. Si la pieza es universal o de alta compatibilidad, indicalo en "notes". +REGLAS OBLIGATORIAS: +1. "make" = marca del vehiculo. +2. "model" = modelo exacto (incluye variante si aplica). +3. USA "year_range" = string "YYYY-YYYY" cuando el MISMO modelo/motor abarca multiples anos consecutivos. Esto ahorra tokens y permite mas resultados. +4. USA "year" = int SOLO cuando sea un ano aislado sin rangos adyacentes. +5. "engine" = descripcion corta del motor (ej: "1.8L", "V6 3.5L"). Si no lo conoces, usa "". +6. "engine_code" = codigo exacto SI LO CONOCES. Si no, usa "". +7. "notes" = string vacio "" para ahorrar tokens, salvo que haya una advertencia critica. +8. Devuelve TODOS los vehiculos compatibles que conozcas. Minimo 1, maximo 200. Para piezas genericas (filtros de aceite, bujias, balatas, amortiguadores) incluye TODOS los modelos aplicables. +9. "confidence" entre 0.0 y 1.0. Valores >0.85 solo si estas muy seguro. +10. Incluye marcas y modelos populares en Mexico cuando apliquen. +11. Si la pieza es universal, indicalo en "notes". """ @@ -153,28 +150,42 @@ def _extract_vehicles(parsed): def _normalize_vehicle(v): - """Normalize vehicle dict from QWEN to standard keys.""" + """Normalize vehicle dict from QWEN to standard keys. + + Supports: + - year: int or str (single year) + - year_range: str like "2003-2008" or "2003-2008" + - legacy: year as range string + """ make = v.get('make') or v.get('marca') or '' model = v.get('model') or v.get('modelo') or '' - year_raw = v.get('year') or v.get('ano') or v.get('año') or v.get('years') or v.get('anos') or '' engine = v.get('engine') or v.get('motor') or '' engine_code = v.get('engine_code') or v.get('codigo_motor') or v.get('motor_code') or '' - # Parse year (may be int, string, or range like "2003-2008") years = [] - if isinstance(year_raw, int): - years = [year_raw] - elif isinstance(year_raw, str): - # Try range "2003-2008" - m = re.match(r'(\d{4})\s*[-–]\s*(\d{4})', year_raw) + + # Prefer explicit year_range + year_range = v.get('year_range') or v.get('rango_ano') or '' + if isinstance(year_range, str): + m = re.match(r'(\d{4})\s*[-–]\s*(\d{4})', year_range) if m: start, end = int(m.group(1)), int(m.group(2)) years = list(range(start, end + 1)) - else: - # Try single year - m2 = re.match(r'(\d{4})', year_raw) - if m2: - years = [int(m2.group(1))] + + # Fallback to year (int or str) + if not years: + year_raw = v.get('year') or v.get('ano') or v.get('año') or v.get('years') or v.get('anos') or '' + if isinstance(year_raw, int): + years = [year_raw] + elif isinstance(year_raw, str): + m = re.match(r'(\d{4})\s*[-–]\s*(\d{4})', year_raw) + if m: + start, end = int(m.group(1)), int(m.group(2)) + years = list(range(start, end + 1)) + else: + m2 = re.match(r'(\d{4})', year_raw) + if m2: + years = [int(m2.group(1))] return make, model, years, engine, engine_code @@ -200,16 +211,30 @@ def _validate_vehicles(vehicles): 1. Exact engine_code match (most precise) 2. Displacement-based match (e.g. all 1.8L engines for that make/model/year) 3. Broad make/model/year match (all engines for that make/model/year) + + If the master DB does not contain the vehicle (e.g. North-American models + missing from TecDoc), the vehicle is returned with mye_id=None so it can + be stored as a text-only QWEN record. """ from tenant_db import get_master_conn try: master = get_master_conn() cur = master.cursor() except Exception: - return [] + # Master DB unreachable — return all vehicles as unmatched text + return [ + {'make': v.get('make') or v.get('marca') or '', + 'model': v.get('model') or v.get('modelo') or '', + 'year': v.get('year') or v.get('ano') or v.get('año') or 0, + 'engine': v.get('engine') or v.get('motor') or '', + 'engine_code': v.get('engine_code') or v.get('codigo_motor') or '', + 'mye_id': None} + for v in vehicles + ] validated = [] seen_mye = set() + seen_text = set() # (make, model, year) for text-only dedup for v in vehicles: make, model, years, engine, engine_code = _normalize_vehicle(v) @@ -285,16 +310,30 @@ def _validate_vehicles(vehicles): matched_myes = [r[0] for r in cur.fetchall()] # Deduplicate and add to results - for mye_id in matched_myes: - if mye_id not in seen_mye: - seen_mye.add(mye_id) + if matched_myes: + for mye_id in matched_myes: + if mye_id not in seen_mye: + seen_mye.add(mye_id) + validated.append({ + 'make': make, + 'model': model, + 'year': year, + 'engine': engine, + 'engine_code': engine_code, + 'mye_id': mye_id, + }) + else: + # No match in master DB — store as text-only QWEN record + text_key = (make.upper(), model.upper(), year) + if text_key not in seen_text: + seen_text.add(text_key) validated.append({ 'make': make, 'model': model, 'year': year, 'engine': engine, 'engine_code': engine_code, - 'mye_id': mye_id, + 'mye_id': None, }) cur.close() diff --git a/pos/static/js/inventory.js b/pos/static/js/inventory.js index 650739f..ea326a1 100644 --- a/pos/static/js/inventory.js +++ b/pos/static/js/inventory.js @@ -843,7 +843,7 @@ list.forEach(function(c) { var sourceLabel = c.source === 'qwen_ai' ? 'IA' : (c.source === 'auto_match' ? 'TecDoc' : esc(c.source || '')); html2 += '' + esc(c.brand || '') + '' + esc(c.model || '') + '' + esc(c.year || '') + '' + esc(c.engine || '') + '' + sourceLabel + ''; - html2 += ''; + html2 += ''; }); html2 += ''; } else { @@ -907,9 +907,9 @@ }).catch(function() { alert('Error en auto-match'); }); } - function removeCompat(itemId, myeId) { + function removeCompat(itemId, compatId) { if (!confirm('Quitar compatibilidad con este vehiculo?')) return; - fetch('/pos/api/inventory/items/' + itemId + '/compatibility/' + myeId, { + fetch('/pos/api/inventory/items/' + itemId + '/vehicles/' + compatId, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } }).then(function(r) { return r.json(); }) diff --git a/pos/tasks.py b/pos/tasks.py index 8d66a80..8423169 100644 --- a/pos/tasks.py +++ b/pos/tasks.py @@ -96,3 +96,47 @@ def generate_report_task(self, report_type, params, tenant_id): 'status': 'completed', 'url': f'/pos/static/reports/{report_type}_{tenant_id}.pdf', } + + +@celery.task(bind=True, max_retries=2) +def sync_vehicle_compatibility_task(self, tenant_id, item_id, part_number, name, brand, compat_source): + """Fetch AI/TecDoc vehicle compatibility in background after item creation.""" + from tenant_db import get_tenant_conn, get_master_conn + from services.inventory_vehicle_compat import auto_match_vehicle_compatibility, save_qwen_fitment + from services.qwen_fitment import get_vehicle_fitment + + tenant_conn = None + master_conn = None + try: + tenant_conn = get_tenant_conn(tenant_id) + + if compat_source in ('tecdoc', 'both'): + try: + master_conn = get_master_conn() + auto_match_vehicle_compatibility( + master_conn, tenant_conn, item_id, part_number, + brand=brand, name=name + ) + master_conn.close() + master_conn = None + except Exception as am_err: + print(f"[sync_vehicle_compat] TecDoc error for item {item_id}: {am_err}") + + if compat_source in ('qwen', 'both'): + try: + fitment = get_vehicle_fitment(part_number, name, brand or '') + save_qwen_fitment(tenant_conn, item_id, fitment) + except Exception as qwen_err: + print(f"[sync_vehicle_compat] QWEN error for item {item_id}: {qwen_err}") + + tenant_conn.commit() + return {'status': 'ok', 'item_id': item_id, 'tenant_id': tenant_id} + except Exception as exc: + if tenant_conn: + tenant_conn.rollback() + raise self.retry(exc=exc, countdown=10) + finally: + if tenant_conn: + tenant_conn.close() + if master_conn: + master_conn.close()