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

@@ -15,6 +15,11 @@ from services.inventory_engine import (
)
from services.barcode_generator import generate_barcode
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,
)
inventory_bp = Blueprint('inventory', __name__, url_prefix='/pos/api/inventory')
@@ -276,7 +281,18 @@ def create_item():
new_value={'part_number': data['part_number'], 'name': data['name'], 'initial_stock': initial_stock})
conn.commit()
cur.close(); conn.close()
cur.close()
# Auto-match vehicle compatibility via TecDoc
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}")
conn.close()
return jsonify({'id': item_id, 'barcode': barcode, 'message': 'Item created'}), 201
except Exception as e:
@@ -338,7 +354,21 @@ def update_item(item_id):
new_value={k: data[k] for k in changing_prices})
conn.commit()
cur.close(); conn.close()
cur.close()
# Re-run auto-match if part_number changed
if 'part_number' in data and data['part_number'] != old_dict.get('part_number'):
try:
master = get_master_conn()
# Clear old compatibilities and re-match
remove_all_compatibility(conn, item_id)
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] Re-match error for item {item_id}: {am_err}")
conn.close()
return jsonify({'message': 'Item updated'})
@@ -1244,3 +1274,88 @@ def api_reorder_suggest_po():
suggestion = suggest_po_from_alerts(conn, supplier_id=supplier_id, branch_id=branch_id)
conn.close()
return jsonify(suggestion)
# ─── Vehicle Compatibility ───────────────────────────
@inventory_bp.route('/items/<int:item_id>/vehicles', methods=['GET'])
@require_auth('inventory.view')
def get_item_vehicles(item_id):
"""Get all vehicle compatibilities for an inventory item."""
tenant = get_tenant_conn(g.tenant_id)
master = get_master_conn()
try:
vehicles = get_compatibility(tenant, master, item_id)
return jsonify({'vehicles': vehicles})
finally:
tenant.close()
master.close()
@inventory_bp.route('/items/<int:item_id>/vehicles', methods=['POST'])
@require_auth('inventory.edit')
def add_item_vehicle(item_id):
"""Manually add a vehicle compatibility."""
data = request.get_json() or {}
mye_id = data.get('model_year_engine_id')
if not mye_id:
return jsonify({'error': 'model_year_engine_id required'}), 400
conn = get_tenant_conn(g.tenant_id)
try:
cid = add_compatibility(conn, item_id, mye_id, source='manual')
return jsonify({'id': cid, 'message': 'Compatibility added'}), 201
finally:
conn.close()
@inventory_bp.route('/items/<int:item_id>/vehicles/<int:mye_id>', methods=['DELETE'])
@require_auth('inventory.edit')
def delete_item_vehicle(item_id, mye_id):
"""Remove a vehicle compatibility."""
conn = get_tenant_conn(g.tenant_id)
try:
deleted = remove_compatibility(conn, item_id, mye_id)
return jsonify({'message': 'Compatibility removed', 'deleted': deleted})
finally:
conn.close()
@inventory_bp.route('/items/<int:item_id>/vehicles/auto-match', methods=['POST'])
@require_auth('inventory.edit')
def auto_match_item_vehicles(item_id):
"""Run auto-match for an existing inventory item."""
conn = get_tenant_conn(g.tenant_id)
cur = conn.cursor()
cur.execute("SELECT part_number, brand, name FROM inventory WHERE id = %s", (item_id,))
row = cur.fetchone()
cur.close()
if not row:
conn.close()
return jsonify({'error': 'Item not found'}), 404
part_number, brand, name = row
master = get_master_conn()
try:
result = auto_match_vehicle_compatibility(master, conn, item_id, part_number,
brand=brand, name=name)
return jsonify(result)
finally:
master.close()
conn.close()
@inventory_bp.route('/mye/search', methods=['GET'])
@require_auth()
def search_mye_endpoint():
"""Search model_year_engine records for manual compatibility assignment."""
brand_id = request.args.get('brand_id', type=int)
model_id = request.args.get('model_id', type=int)
year_id = request.args.get('year_id', type=int)
engine_id = request.args.get('engine_id', type=int)
master = get_master_conn()
try:
results = search_mye(master, brand_id=brand_id, model_id=model_id,
year_id=year_id, engine_id=engine_id)
return jsonify({'data': results})
finally:
master.close()