feat: configurable vehicle compatibility source (TecDoc / QWEN / Both)
Backend: - Added GET/PUT /pos/api/config/vehicle-compat-source endpoints - Added get_compat_source() helper reading from tenant_config - create_item() now respects config: runs TecDoc and/or QWEN accordingly - auto_match_item_vehicles() respects config: runs only configured source Frontend: - Added 'Compatibilidad de Vehiculos' section in config.html - Added loadVehicleCompatSource() / saveVehicleCompatSource() in config.js - Regenerated config.min.js
This commit is contained in:
@@ -409,3 +409,45 @@ def upgrade_billing():
|
|||||||
if 'error' in result:
|
if 'error' in result:
|
||||||
return jsonify(result), 400
|
return jsonify(result), 400
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Vehicle Compatibility Source ────────────────────
|
||||||
|
|
||||||
|
@config_bp.route('/vehicle-compat-source', methods=['GET'])
|
||||||
|
@require_auth()
|
||||||
|
def get_vehicle_compat_source():
|
||||||
|
"""Get the configured vehicle compatibility source.
|
||||||
|
|
||||||
|
Returns: {'source': 'tecdoc' | 'qwen' | 'both'}
|
||||||
|
"""
|
||||||
|
conn = get_tenant_conn(g.tenant_id)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("SELECT value FROM tenant_config WHERE key = 'vehicle_compat_source'")
|
||||||
|
row = cur.fetchone()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
source = row[0] if row else 'both'
|
||||||
|
if source not in ('tecdoc', 'qwen', 'both'):
|
||||||
|
source = 'both'
|
||||||
|
return jsonify({'source': source})
|
||||||
|
|
||||||
|
|
||||||
|
@config_bp.route('/vehicle-compat-source', methods=['PUT'])
|
||||||
|
@require_auth('config.edit')
|
||||||
|
def update_vehicle_compat_source():
|
||||||
|
"""Set the vehicle compatibility source."""
|
||||||
|
data = request.get_json() or {}
|
||||||
|
source = data.get('source', 'both')
|
||||||
|
if source not in ('tecdoc', 'qwen', 'both'):
|
||||||
|
return jsonify({'error': 'source must be tecdoc, qwen, or both'}), 400
|
||||||
|
|
||||||
|
conn = get_tenant_conn(g.tenant_id)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO tenant_config (key, value) VALUES ('vehicle_compat_source', %s)
|
||||||
|
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
|
||||||
|
""", (source,))
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'message': 'Vehicle compatibility source updated', 'source': source})
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from services.audit import log_action
|
|||||||
from tenant_db import get_master_conn
|
from tenant_db import get_master_conn
|
||||||
from services.inventory_vehicle_compat import (
|
from services.inventory_vehicle_compat import (
|
||||||
auto_match_vehicle_compatibility, add_compatibility, remove_compatibility,
|
auto_match_vehicle_compatibility, add_compatibility, remove_compatibility,
|
||||||
remove_all_compatibility, get_compatibility, search_mye,
|
remove_all_compatibility, get_compatibility, search_mye, get_compat_source,
|
||||||
)
|
)
|
||||||
|
|
||||||
inventory_bp = Blueprint('inventory', __name__, url_prefix='/pos/api/inventory')
|
inventory_bp = Blueprint('inventory', __name__, url_prefix='/pos/api/inventory')
|
||||||
@@ -283,38 +283,43 @@ def create_item():
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
cur.close()
|
cur.close()
|
||||||
|
|
||||||
# Auto-match vehicle compatibility via TecDoc
|
# ── Vehicle compatibility (respects tenant config) ────────────────
|
||||||
try:
|
compat_source = get_compat_source(g.tenant_id)
|
||||||
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}")
|
|
||||||
|
|
||||||
# QWEN AI fitment (complementa TecDoc mientras se termina)
|
|
||||||
qwen_added = 0
|
qwen_added = 0
|
||||||
try:
|
|
||||||
from services.qwen_fitment import get_vehicle_fitment
|
# TecDoc auto-match
|
||||||
fitment = get_vehicle_fitment(
|
if compat_source in ('tecdoc', 'both'):
|
||||||
data['part_number'],
|
try:
|
||||||
data['name'],
|
master = get_master_conn()
|
||||||
data.get('brand', '')
|
auto_match_vehicle_compatibility(master, conn, item_id, data['part_number'],
|
||||||
)
|
brand=data.get('brand'), name=data.get('name'))
|
||||||
for v in fitment.get('vehicles', []):
|
master.close()
|
||||||
if v.get('mye_id'):
|
except Exception as am_err:
|
||||||
cur = conn.cursor()
|
print(f"[auto_match] Error for item {item_id}: {am_err}")
|
||||||
cur.execute("""
|
|
||||||
INSERT INTO inventory_vehicle_compat
|
# QWEN AI fitment
|
||||||
(inventory_id, model_year_engine_id, source, confidence, created_at)
|
if compat_source in ('qwen', 'both'):
|
||||||
VALUES (%s, %s, %s, %s, NOW())
|
try:
|
||||||
ON CONFLICT (inventory_id, model_year_engine_id) DO NOTHING
|
from services.qwen_fitment import get_vehicle_fitment
|
||||||
""", (item_id, v['mye_id'], 'qwen_ai', fitment.get('confidence', 0)))
|
fitment = get_vehicle_fitment(
|
||||||
cur.close()
|
data['part_number'],
|
||||||
qwen_added += 1
|
data['name'],
|
||||||
conn.commit()
|
data.get('brand', '')
|
||||||
except Exception as qwen_err:
|
)
|
||||||
print(f"[qwen_fitment] Error for item {item_id}: {qwen_err}")
|
for v in fitment.get('vehicles', []):
|
||||||
|
if v.get('mye_id'):
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO inventory_vehicle_compat
|
||||||
|
(inventory_id, model_year_engine_id, source, confidence, created_at)
|
||||||
|
VALUES (%s, %s, %s, %s, NOW())
|
||||||
|
ON CONFLICT (inventory_id, model_year_engine_id) DO NOTHING
|
||||||
|
""", (item_id, v['mye_id'], 'qwen_ai', fitment.get('confidence', 0)))
|
||||||
|
cur.close()
|
||||||
|
qwen_added += 1
|
||||||
|
conn.commit()
|
||||||
|
except Exception as qwen_err:
|
||||||
|
print(f"[qwen_fitment] Error for item {item_id}: {qwen_err}")
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -1363,14 +1368,51 @@ def auto_match_item_vehicles(item_id):
|
|||||||
return jsonify({'error': 'Item not found'}), 404
|
return jsonify({'error': 'Item not found'}), 404
|
||||||
|
|
||||||
part_number, brand, name = row
|
part_number, brand, name = row
|
||||||
master = get_master_conn()
|
compat_source = get_compat_source(g.tenant_id)
|
||||||
try:
|
|
||||||
result = auto_match_vehicle_compatibility(master, conn, item_id, part_number,
|
# TecDoc auto-match
|
||||||
brand=brand, name=name)
|
if compat_source in ('tecdoc', 'both'):
|
||||||
return jsonify(result)
|
master = get_master_conn()
|
||||||
finally:
|
try:
|
||||||
master.close()
|
result = auto_match_vehicle_compatibility(master, conn, item_id, part_number,
|
||||||
conn.close()
|
brand=brand, name=name)
|
||||||
|
return jsonify(result)
|
||||||
|
finally:
|
||||||
|
master.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# QWEN AI auto-match
|
||||||
|
if compat_source == 'qwen':
|
||||||
|
try:
|
||||||
|
from services.qwen_fitment import get_vehicle_fitment
|
||||||
|
fitment = get_vehicle_fitment(part_number, name, brand)
|
||||||
|
# Insert results
|
||||||
|
inserted = 0
|
||||||
|
cur2 = conn.cursor()
|
||||||
|
for v in fitment.get('vehicles', []):
|
||||||
|
if v.get('mye_id'):
|
||||||
|
cur2.execute("""
|
||||||
|
INSERT INTO inventory_vehicle_compat
|
||||||
|
(inventory_id, model_year_engine_id, source, confidence, created_at)
|
||||||
|
VALUES (%s, %s, %s, %s, NOW())
|
||||||
|
ON CONFLICT (inventory_id, model_year_engine_id) DO NOTHING
|
||||||
|
""", (item_id, v['mye_id'], 'qwen_ai', fitment.get('confidence', 0)))
|
||||||
|
if cur2.rowcount > 0:
|
||||||
|
inserted += 1
|
||||||
|
conn.commit()
|
||||||
|
cur2.close()
|
||||||
|
return jsonify({
|
||||||
|
'matched': inserted > 0,
|
||||||
|
'matches': [],
|
||||||
|
'myes': [v['mye_id'] for v in fitment.get('vehicles', []) if v.get('mye_id')],
|
||||||
|
'inserted': inserted,
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'error': 'No compatibility source configured'}), 400
|
||||||
|
|
||||||
|
|
||||||
@inventory_bp.route('/mye/search', methods=['GET'])
|
@inventory_bp.route('/mye/search', methods=['GET'])
|
||||||
|
|||||||
@@ -12,6 +12,29 @@ Features:
|
|||||||
from typing import List, Dict, Optional
|
from typing import List, Dict, Optional
|
||||||
|
|
||||||
|
|
||||||
|
def get_compat_source(tenant_id):
|
||||||
|
"""Return the configured compatibility source: 'tecdoc', 'qwen', or 'both'.
|
||||||
|
|
||||||
|
Reads from tenant_config table. Defaults to 'both'.
|
||||||
|
"""
|
||||||
|
from tenant_db import get_tenant_conn
|
||||||
|
try:
|
||||||
|
conn = get_tenant_conn(tenant_id)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"SELECT value FROM tenant_config WHERE key = 'vehicle_compat_source'"
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
source = row[0] if row else 'both'
|
||||||
|
if source in ('tecdoc', 'qwen', 'both'):
|
||||||
|
return source
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return 'both'
|
||||||
|
|
||||||
|
|
||||||
def auto_match_vehicle_compatibility(master_conn, tenant_conn, inventory_id, part_number,
|
def auto_match_vehicle_compatibility(master_conn, tenant_conn, inventory_id, part_number,
|
||||||
brand=None, name=None):
|
brand=None, name=None):
|
||||||
"""Find vehicle compatibility for an inventory item by part_number.
|
"""Find vehicle compatibility for an inventory item by part_number.
|
||||||
|
|||||||
@@ -580,6 +580,49 @@ const Config = (() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Vehicle Compatibility Source
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
async function loadVehicleCompatSource() {
|
||||||
|
try {
|
||||||
|
var res = await fetch(API + '/vehicle-compat-source', { headers: headers() });
|
||||||
|
if (!res.ok) return;
|
||||||
|
var d = await res.json();
|
||||||
|
var sel = document.getElementById('cfg-compat-source');
|
||||||
|
if (sel) sel.value = d.source || 'both';
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Config.loadVehicleCompatSource:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveVehicleCompatSource() {
|
||||||
|
var sel = document.getElementById('cfg-compat-source');
|
||||||
|
var btn = document.getElementById('btn-save-compat-source');
|
||||||
|
if (!sel) return;
|
||||||
|
|
||||||
|
if (btn) { btn.disabled = true; btn.textContent = 'Guardando...'; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
var res = await fetch(API + '/vehicle-compat-source', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: headers(),
|
||||||
|
body: JSON.stringify({ source: sel.value })
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
var err = await res.json().catch(function() { return { error: res.statusText }; });
|
||||||
|
throw new Error(err.error || 'Save failed');
|
||||||
|
}
|
||||||
|
toast('Fuente de compatibilidad actualizada');
|
||||||
|
} catch (e) {
|
||||||
|
toast(e.message, 'error');
|
||||||
|
} finally {
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Guardar';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Init
|
// Init
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -601,6 +644,12 @@ const Config = (() => {
|
|||||||
// Bind UI events
|
// Bind UI events
|
||||||
bindEvents();
|
bindEvents();
|
||||||
|
|
||||||
|
// Vehicle compat source save button
|
||||||
|
var btnCompat = document.getElementById('btn-save-compat-source');
|
||||||
|
if (btnCompat) {
|
||||||
|
btnCompat.addEventListener('click', saveVehicleCompatSource);
|
||||||
|
}
|
||||||
|
|
||||||
// Kiosk mode toggle
|
// Kiosk mode toggle
|
||||||
var kioskToggle = document.getElementById('cfg-kiosk-mode');
|
var kioskToggle = document.getElementById('cfg-kiosk-mode');
|
||||||
if (kioskToggle && window.NexusKiosk) {
|
if (kioskToggle && window.NexusKiosk) {
|
||||||
@@ -621,6 +670,7 @@ const Config = (() => {
|
|||||||
loadEmployees();
|
loadEmployees();
|
||||||
loadBusiness();
|
loadBusiness();
|
||||||
loadCurrency();
|
loadCurrency();
|
||||||
|
loadVehicleCompatSource();
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
|||||||
2
pos/static/js/config.min.js
vendored
2
pos/static/js/config.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -551,7 +551,40 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ===============================================================
|
<!-- ===============================================================
|
||||||
SECTION 8: MONEDA / CURRENCY
|
SECTION 8: VEHICLE COMPATIBILITY SOURCE
|
||||||
|
=============================================================== -->
|
||||||
|
<div class="settings-section">
|
||||||
|
<div class="settings-section__header">
|
||||||
|
<div class="settings-section__icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="settings-section__title">Compatibilidad de Vehículos</div>
|
||||||
|
<div class="settings-section__desc">Elige la fuente para asignar vehículos compatibles a tus productos</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-card">
|
||||||
|
<div class="settings-card__title">Fuente de Datos</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Método de asignación automática</label>
|
||||||
|
<select class="form-select" id="cfg-compat-source">
|
||||||
|
<option value="tecdoc">TecDoc — Base de datos oficial de autopartes</option>
|
||||||
|
<option value="qwen">QWEN AI — Inteligencia artificial (experimental)</option>
|
||||||
|
<option value="both">Ambos — TecDoc primero, QWEN como complemento</option>
|
||||||
|
</select>
|
||||||
|
<span class="form-hint">Afecta la creación de productos y el botón "Auto-Match"</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: var(--space-4); display: flex; justify-content: flex-end;">
|
||||||
|
<button class="btn btn--primary" id="btn-save-compat-source">Guardar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===============================================================
|
||||||
|
SECTION 9: MONEDA / CURRENCY
|
||||||
=============================================================== -->
|
=============================================================== -->
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<div class="settings-section__header">
|
<div class="settings-section__header">
|
||||||
|
|||||||
Reference in New Issue
Block a user