feat(pos): chatbot busca en inventario local + catalogo TecDoc
El chatbot ahora busca primero en el inventario local del tenant y luego en el catalogo TecDoc. Resultados muestran badge: - Verde "MI INVENTARIO" para partes locales - Azul "CATALOGO" para partes del catalogo TecDoc Busqueda local funciona en español e inglés. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -59,14 +59,30 @@ def chat():
|
||||
if len(part_words) >= 3:
|
||||
effective_query = part_words
|
||||
|
||||
if effective_query and master and tenant:
|
||||
if effective_query and tenant:
|
||||
# First: search local inventory
|
||||
try:
|
||||
results = catalog_service.smart_search(
|
||||
master, effective_query, tenant, branch_id, limit=10
|
||||
)
|
||||
search_results = results if results else []
|
||||
local_results = _search_local_inventory(tenant, effective_query, search_query or '', branch_id)
|
||||
if local_results:
|
||||
search_results.extend(local_results)
|
||||
except Exception:
|
||||
pass # search failure is non-fatal
|
||||
pass
|
||||
|
||||
# Then: search TecDoc catalog
|
||||
if master:
|
||||
try:
|
||||
catalog_results = catalog_service.smart_search(
|
||||
master, effective_query, tenant, branch_id, limit=10
|
||||
)
|
||||
if catalog_results:
|
||||
# Mark as catalog results and avoid duplicates
|
||||
local_parts = {r.get('part_number', '') for r in search_results}
|
||||
for cr in catalog_results:
|
||||
if cr.get('oem_part_number', '') not in local_parts:
|
||||
cr['source'] = 'catalog'
|
||||
search_results.append(cr)
|
||||
except Exception:
|
||||
pass # search failure is non-fatal
|
||||
|
||||
except Exception:
|
||||
pass # DB failure is non-fatal for chat
|
||||
@@ -159,3 +175,61 @@ def _resolve_vehicle(master_conn, vehicle):
|
||||
cur.close()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _search_local_inventory(tenant_conn, query_en, query_es, branch_id):
|
||||
"""Search tenant's local inventory by part name/number in both English and Spanish."""
|
||||
cur = tenant_conn.cursor()
|
||||
results = []
|
||||
try:
|
||||
# Search by part_number, name, or brand — try both English and Spanish terms
|
||||
terms = set()
|
||||
terms.add(query_en)
|
||||
if query_es:
|
||||
terms.add(query_es)
|
||||
|
||||
where_parts = []
|
||||
params = []
|
||||
for term in terms:
|
||||
if not term:
|
||||
continue
|
||||
where_parts.append("(i.part_number ILIKE %s OR i.name ILIKE %s OR i.brand ILIKE %s)")
|
||||
params.extend([f'%{term}%', f'%{term}%', f'%{term}%'])
|
||||
|
||||
if not where_parts:
|
||||
return []
|
||||
|
||||
where = " OR ".join(where_parts)
|
||||
if branch_id:
|
||||
where = f"({where}) AND i.branch_id = %s"
|
||||
params.append(branch_id)
|
||||
|
||||
cur.execute(f"""
|
||||
SELECT i.id, i.part_number, i.name, i.brand, i.price_1, i.price_2, i.price_3, i.cost,
|
||||
COALESCE((SELECT SUM(quantity) FROM inventory_operations WHERE inventory_id = i.id), 0) as stock
|
||||
FROM inventory i
|
||||
WHERE i.is_active = true AND ({where})
|
||||
ORDER BY i.name
|
||||
LIMIT 10
|
||||
""", params)
|
||||
|
||||
for r in cur.fetchall():
|
||||
results.append({
|
||||
'source': 'local',
|
||||
'inventory_id': r[0],
|
||||
'part_number': r[1],
|
||||
'oem_part_number': r[1],
|
||||
'name_part': r[2],
|
||||
'brand': r[3],
|
||||
'price_1': float(r[4]) if r[4] else 0,
|
||||
'price_2': float(r[5]) if r[5] else 0,
|
||||
'price_3': float(r[6]) if r[6] else 0,
|
||||
'cost': float(r[7]) if r[7] else 0,
|
||||
'local_stock': r[8],
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
cur.close()
|
||||
|
||||
return results
|
||||
|
||||
@@ -191,16 +191,21 @@
|
||||
const card = document.createElement('div');
|
||||
card.className = 'chat-part-card';
|
||||
|
||||
const isLocal = p.source === 'local';
|
||||
const stockQty = p.local_stock || 0;
|
||||
const stockClass = stockQty > 0 ? 'in-stock' : '';
|
||||
const stockText = stockQty > 0 ? (stockQty + ' en stock') : 'Sin stock local';
|
||||
const name = p.name_es || p.name_part || '';
|
||||
const partNum = p.oem_part_number || '';
|
||||
const partNum = p.oem_part_number || p.part_number || '';
|
||||
const brand = p.brand || '';
|
||||
const priceText = p.price_1 ? ('$' + parseFloat(p.price_1).toFixed(2)) : '';
|
||||
const sourceTag = isLocal
|
||||
? '<span style="background:var(--color-success);color:#fff;padding:1px 6px;border-radius:4px;font-size:0.65rem;margin-left:6px;">MI INVENTARIO</span>'
|
||||
: '<span style="background:var(--color-primary);color:#fff;padding:1px 6px;border-radius:4px;font-size:0.65rem;margin-left:6px;">CATÁLOGO</span>';
|
||||
|
||||
card.innerHTML =
|
||||
'<div class="part-number">' + esc(partNum) + (priceText ? ' — ' + priceText : '') + '</div>' +
|
||||
'<div class="part-name">' + esc(name) + '</div>' +
|
||||
'<div class="part-number">' + esc(partNum) + sourceTag + (priceText ? ' — ' + priceText : '') + '</div>' +
|
||||
'<div class="part-name">' + esc(name) + (brand ? ' <span style="color:var(--color-text-muted);">(' + esc(brand) + ')</span>' : '') + '</div>' +
|
||||
'<div class="part-stock ' + stockClass + '">' + esc(stockText) + '</div>';
|
||||
|
||||
// Click to open detail (if catalog page has a detail function)
|
||||
|
||||
Reference in New Issue
Block a user