- Plate lookup: new plate_vehicles table (v1.7 migration), plate_lookup service with Mexican plate validation, GET/POST endpoints on catalog_bp, plate search UI in catalog vehicle selector - Translations: extend PART_TRANSLATIONS from ~80 to 326 entries covering brake, engine, fuel, cooling, electrical, drivetrain, suspension, steering, exhaust, A/C, lighting, body, interior, fluids, and category translations - Bulk images: image_scraper service with download+resize+placeholder generation, bulk-images and auto-image endpoints on inventory_bp Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ from middleware import require_auth
|
||||
from tenant_db import get_master_conn, get_tenant_conn
|
||||
from services import catalog_service
|
||||
from services.vin_decoder import decode_vin
|
||||
from services.plate_lookup import search_plate, register_plate, is_valid_mexican_plate, normalize_plate
|
||||
|
||||
catalog_bp = Blueprint('catalog', __name__, url_prefix='/pos/api/catalog')
|
||||
|
||||
@@ -227,6 +228,113 @@ def decode_vin_route(vin):
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
# ─── Plate Lookup ───
|
||||
|
||||
@catalog_bp.route('/plate/<plate>', methods=['GET'])
|
||||
@require_auth('catalog.view')
|
||||
def plate_lookup(plate):
|
||||
"""Look up a vehicle by Mexican license plate in the local plate_vehicles table.
|
||||
If found, also tries to match the vehicle to the catalog DB.
|
||||
"""
|
||||
plate = (plate or '').strip()
|
||||
if not plate:
|
||||
return jsonify({'error': 'Placa requerida.'}), 400
|
||||
|
||||
if not is_valid_mexican_plate(plate):
|
||||
return jsonify({'error': 'Formato de placa no valido. Ej: ABC-1234 o AB-123-C'}), 400
|
||||
|
||||
tenant = None
|
||||
master = None
|
||||
try:
|
||||
tenant = get_tenant_conn(g.tenant_id)
|
||||
result = search_plate(tenant, plate)
|
||||
|
||||
if not result:
|
||||
return jsonify({
|
||||
'found': False,
|
||||
'plate': normalize_plate(plate),
|
||||
'message': 'Placa no registrada.'
|
||||
})
|
||||
|
||||
# Try to match to catalog
|
||||
catalog_match = None
|
||||
try:
|
||||
master = get_master_conn()
|
||||
catalog_match = _match_plate_to_catalog(master, result)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
if master:
|
||||
try: master.close()
|
||||
except: pass
|
||||
master = None
|
||||
|
||||
response = {
|
||||
'found': True,
|
||||
'plate': result['plate'],
|
||||
'make': result['make'],
|
||||
'model': result['model'],
|
||||
'year': result['year'],
|
||||
'vin': result['vin'],
|
||||
'customer_id': result['customer_id'],
|
||||
}
|
||||
if catalog_match:
|
||||
response['catalog_match'] = catalog_match
|
||||
|
||||
return jsonify(response)
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
finally:
|
||||
if tenant:
|
||||
try: tenant.close()
|
||||
except: pass
|
||||
if master:
|
||||
try: master.close()
|
||||
except: pass
|
||||
|
||||
|
||||
@catalog_bp.route('/plate', methods=['POST'])
|
||||
@require_auth('catalog.view')
|
||||
def plate_register():
|
||||
"""Register or update a plate-to-vehicle mapping."""
|
||||
data = request.get_json() or {}
|
||||
plate = (data.get('plate') or '').strip()
|
||||
if not plate:
|
||||
return jsonify({'error': 'plate required'}), 400
|
||||
|
||||
if not is_valid_mexican_plate(plate):
|
||||
return jsonify({'error': 'Formato de placa no valido.'}), 400
|
||||
|
||||
tenant = None
|
||||
try:
|
||||
tenant = get_tenant_conn(g.tenant_id)
|
||||
rec_id = register_plate(
|
||||
tenant, plate,
|
||||
make=data.get('make'),
|
||||
model=data.get('model'),
|
||||
year=data.get('year'),
|
||||
vin=data.get('vin'),
|
||||
customer_id=data.get('customer_id'),
|
||||
)
|
||||
return jsonify({'id': rec_id, 'message': 'Placa registrada.'})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
finally:
|
||||
if tenant:
|
||||
try: tenant.close()
|
||||
except: pass
|
||||
|
||||
|
||||
def _match_plate_to_catalog(master_conn, plate_info):
|
||||
"""Try to match plate vehicle info to the catalog DB (same logic as VIN)."""
|
||||
return _match_vin_to_catalog(master_conn, {
|
||||
'make': plate_info.get('make'),
|
||||
'model': plate_info.get('model'),
|
||||
'year': plate_info.get('year'),
|
||||
})
|
||||
|
||||
|
||||
def _match_vin_to_catalog(master_conn, vin_info):
|
||||
"""Try to find brand_id, model_id, year_id, mye_id from decoded VIN info."""
|
||||
make = (vin_info.get('make') or '').upper().strip()
|
||||
|
||||
@@ -19,6 +19,17 @@ from services.audit import log_action
|
||||
inventory_bp = Blueprint('inventory', __name__, url_prefix='/pos/api/inventory')
|
||||
|
||||
|
||||
# ─── AI Classification ───────────────────────────
|
||||
|
||||
@inventory_bp.route('/classify/<part_number>', methods=['GET'])
|
||||
@require_auth('inventory.create')
|
||||
def classify_part_endpoint(part_number):
|
||||
"""Ask AI to identify a part by its OEM number."""
|
||||
from services.ai_chat import classify_part
|
||||
result = classify_part(part_number)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
# ─── Item CRUD ──────────────────────────────────
|
||||
|
||||
@inventory_bp.route('/items', methods=['GET'])
|
||||
@@ -457,6 +468,80 @@ def delete_image(item_id):
|
||||
return jsonify({'message': 'Image deleted'})
|
||||
|
||||
|
||||
# ─── Bulk Image Import ─────────────────────────
|
||||
|
||||
@inventory_bp.route('/bulk-images', methods=['POST'])
|
||||
@require_auth('inventory.edit')
|
||||
def bulk_upload_images():
|
||||
"""Bulk import images from URLs for multiple inventory items.
|
||||
|
||||
Accepts JSON: {items: [{part_number, image_url}, ...]}
|
||||
Downloads each image, resizes/optimizes, saves to disk, updates DB.
|
||||
Returns {imported: N, errors: [...]}
|
||||
"""
|
||||
data = request.get_json() or {}
|
||||
items_list = data.get('items', [])
|
||||
|
||||
if not items_list:
|
||||
return jsonify({'error': 'items array required'}), 400
|
||||
|
||||
if len(items_list) > 500:
|
||||
return jsonify({'error': 'Maximum 500 items per request'}), 400
|
||||
|
||||
from services.image_scraper import bulk_import
|
||||
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
result = bulk_import(conn, g.tenant_id, items_list)
|
||||
log_action(conn, 'BULK_IMAGE_IMPORT', 'inventory', None,
|
||||
new_value={'imported': result['imported'], 'error_count': len(result['errors'])})
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@inventory_bp.route('/items/<int:item_id>/auto-image', methods=['POST'])
|
||||
@require_auth('inventory.edit')
|
||||
def auto_image(item_id):
|
||||
"""Generate a placeholder image for an inventory item.
|
||||
|
||||
Creates a branded placeholder with the part number text.
|
||||
Useful when no real product image is available.
|
||||
"""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("SELECT part_number, name FROM inventory WHERE id = %s", (item_id,))
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'error': 'Item not found'}), 404
|
||||
|
||||
part_number, name = row
|
||||
|
||||
try:
|
||||
from services.image_scraper import generate_placeholder
|
||||
rel_url = generate_placeholder(g.tenant_id, item_id, part_number, name or '')
|
||||
|
||||
cur.execute("UPDATE inventory SET image_url = %s WHERE id = %s", (rel_url, item_id))
|
||||
conn.commit()
|
||||
|
||||
log_action(conn, 'AUTO_IMAGE_GENERATED', 'inventory', item_id,
|
||||
new_value={'image_url': rel_url})
|
||||
|
||||
cur.close(); conn.close()
|
||||
return jsonify({
|
||||
'image_url': rel_url,
|
||||
'message': 'Placeholder image generated'
|
||||
})
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
# ─── Stock Operations ──────────────────────────
|
||||
|
||||
@inventory_bp.route('/purchase', methods=['POST'])
|
||||
|
||||
Reference in New Issue
Block a user