Opción C: Vinculación híbrida de inventario local con vehículos

- Nueva tabla inventory_vehicle_compat (v3.1)
- Motor inventory_vehicle_compat.py: auto-match + gestión manual
- catalog_service.get_parts_local() ahora incluye piezas locales vinculadas
- inventory_bp: auto-match en create/update + endpoints REST /vehicles
- Frontend catalog.js: badge 'Stock Local' para piezas nativas del tenant
- Frontend inventory.js: panel de vehículos compatibles con auto-match
- Tests: test_compatibility.py (9/9 pasan)

Piezas locales aparecen en navegación por vehículo aunque no estén en TecDoc.
Auto-match busca part_number en parts/aftermarket_parts y copia MYEs compatibles.
This commit is contained in:
2026-04-27 06:52:30 +00:00
parent 142abbc217
commit efbd763e43
8 changed files with 690 additions and 14 deletions

View File

@@ -875,7 +875,11 @@
partsGrid.querySelectorAll('.part-card').forEach(function (card) {
card.addEventListener('click', function () {
openPartDetail(parseInt(this.dataset.partId));
var pid = this.dataset.partId;
if (typeof pid === 'string' && pid.indexOf('inv:') === 0) {
return;
}
openPartDetail(parseInt(pid));
});
});
@@ -940,6 +944,10 @@
} else {
stockBadge = '<span class="stock-badge stock-badge--none">Sin stock</span>';
}
// Local inventory native badge
var sourceBadge = p.source === 'local_inventory'
? '<span class="stock-badge stock-badge--local" style="margin-left:4px;background:#4f46e5;">Stock Local</span>'
: '';
var imgHtml = p.image_url
? '<img src="' + esc(p.image_url) + '" alt="' + esc(p.name) + '">'
@@ -961,10 +969,11 @@
? esc(p.part_number) + '<span class="part-card__oem-sub"> · OEM: ' + esc(p.oem_part_number) + '</span>'
: esc(p.oem_part_number);
return '<article class="part-card' + tierClass + '" role="listitem" data-part-id="' + p.id_part + '">' +
return '<article class="part-card' + tierClass + '" role="listitem" data-part-id="' + p.id_part + '" data-source="' + (p.source || '') + '">' +
'<div class="part-card__image">' + imgHtml + '</div>' +
'<div class="part-card__body">' +
manuBadge +
sourceBadge +
'<div class="part-card__oem">' + skuLine + '</div>' +
'<div class="part-card__name">' + esc(p.name) + '</div>' +
'</div>' +
@@ -975,10 +984,15 @@
'</article>';
}).join('');
// Wire part card clicks → open detail panel
// Wire part card clicks → open detail panel (skip local-inventory items)
partsGrid.querySelectorAll('.part-card').forEach(function (card) {
card.addEventListener('click', function () {
openPartDetail(parseInt(this.dataset.partId));
var pid = this.dataset.partId;
if (typeof pid === 'string' && pid.indexOf('inv:') === 0) {
// local-inventory item: info already visible on card
return;
}
openPartDetail(parseInt(pid));
});
});
@@ -1345,7 +1359,11 @@
searchDropdown.querySelectorAll('.search-result-item').forEach(function (el) {
el.addEventListener('click', function () {
searchDropdown.classList.remove('is-visible');
openPartDetail(parseInt(this.dataset.partId));
var pid = this.dataset.partId;
if (typeof pid === 'string' && pid.indexOf('inv:') === 0) {
return;
}
openPartDetail(parseInt(pid));
});
});
});

View File

@@ -653,6 +653,40 @@
el.innerHTML = html2;
}
// Vehicle compatibility section
html += '<div style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:var(--tracking-widest);margin-bottom:8px;">Vehiculos Compatibles</div>';
html += '<div id="compatContent" style="margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--color-border);">';
html += '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Cargando compatibilidades...</p>';
html += '</div>';
// Load vehicle compatibilities
(function loadCompat() {
fetch('/pos/api/inventory/items/' + itemId + '/vehicles', { headers: { 'Authorization': 'Bearer ' + token } })
.then(function(r) { return r.json(); })
.then(function(d) {
var el = document.getElementById('compatContent');
if (!el) return;
var list = d.vehicles || [];
var html2 = '';
if (list.length > 0) {
html2 += '<table class="data-table"><thead><tr><th>Marca</th><th>Modelo</th><th>Ano</th><th>Motor</th><th>Origen</th><th></th></tr></thead><tbody>';
list.forEach(function(c) {
html2 += '<tr><td>' + esc(c.brand || '') + '</td><td>' + esc(c.model || '') + '</td><td>' + esc(c.year || '') + '</td><td>' + esc(c.engine || '') + '</td><td>' + esc(c.source || '') + '</td>';
html2 += '<td><button class="btn btn--ghost btn--sm" style="color:var(--color-error);" onclick="removeCompat(' + itemId + ',' + c.model_year_engine_id + ')">Quitar</button></td></tr>';
});
html2 += '</tbody></table>';
} else {
html2 += '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Sin vehiculos vinculados.</p>';
}
html2 += '<div style="margin-top:8px;"><button class="btn btn--primary btn--sm" onclick="autoMatchCompat(' + itemId + ')">Auto-Match por TecDoc</button> <span style="font-size:var(--text-caption);color:var(--color-text-muted);">Busca en catalogo central y vincula automaticamente</span></div>';
el.innerHTML = html2;
})
.catch(function() {
var el = document.getElementById('compatContent');
if (el) el.innerHTML = '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Error al cargar compatibilidades.</p>';
});
})();
// Movement history
html += '<div style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:var(--tracking-widest);margin-bottom:8px;">Historial de Movimientos</div>';
if (!history.length) {
@@ -677,6 +711,29 @@
});
}
// Vehicle compatibility actions
function autoMatchCompat(itemId) {
fetch('/pos/api/inventory/items/' + itemId + '/vehicles/auto-match', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token }
}).then(function(r) { return r.json(); })
.then(function(d) {
alert('Auto-match completado. Vehiculos vinculados: ' + (d.matched || 0));
viewProductDetail(itemId);
}).catch(function() { alert('Error en auto-match'); });
}
function removeCompat(itemId, myeId) {
if (!confirm('Quitar compatibilidad con este vehiculo?')) return;
fetch('/pos/api/inventory/items/' + itemId + '/compatibility/' + myeId, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + token }
}).then(function(r) { return r.json(); })
.then(function() {
viewProductDetail(itemId);
}).catch(function() { alert('Error al quitar compatibilidad'); });
}
// =====================================================================
// EXPOSE GLOBALS (for onclick handlers in HTML)
// =====================================================================