feat(pos): add plate lookup (#8), 326 translations (#12), bulk image import (#11)

- 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:
2026-04-05 04:17:55 +00:00
parent 1bea31e83f
commit 4cc2c66208
8 changed files with 917 additions and 43 deletions

View File

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