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:
2026-04-02 07:46:08 +00:00
parent 77e45bdc1e
commit 1a770999f5
2 changed files with 88 additions and 9 deletions

View File

@@ -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

View File

@@ -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 ? ' &mdash; ' + priceText : '') + '</div>' +
'<div class="part-name">' + esc(name) + '</div>' +
'<div class="part-number">' + esc(partNum) + sourceTag + (priceText ? ' &mdash; ' + 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)