- 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>
109 lines
3.5 KiB
Python
109 lines
3.5 KiB
Python
# /home/Autopartes/pos/services/plate_lookup.py
|
|
"""Mexican license plate lookup service.
|
|
|
|
Validates Mexican plate formats and searches the local plate_vehicles table.
|
|
Since there is no free REPUVE API, this uses a tenant-local lookup table
|
|
populated when customers register their vehicles.
|
|
|
|
Mexican plate formats:
|
|
- Standard: ABC-1234 (3 letters + hyphen + 3-4 digits)
|
|
- Alternate: AB-123-C (2 letters + 3 digits + 1 letter)
|
|
- Also accepted without hyphens: ABC1234, AB123C
|
|
"""
|
|
|
|
import re
|
|
|
|
# Patterns for Mexican plates (with or without hyphens)
|
|
_PATTERNS = [
|
|
re.compile(r'^[A-Z]{3}-?\d{3,4}$'), # ABC-1234 or ABC1234
|
|
re.compile(r'^[A-Z]{2}-?\d{3}-?[A-Z]$'), # AB-123-C or AB123C
|
|
]
|
|
|
|
|
|
def normalize_plate(plate):
|
|
"""Normalize a plate string: uppercase, strip spaces/hyphens."""
|
|
if not plate:
|
|
return ''
|
|
return re.sub(r'[\s\-]+', '', plate.strip().upper())
|
|
|
|
|
|
def is_valid_mexican_plate(plate):
|
|
"""Check if a string looks like a valid Mexican license plate."""
|
|
norm = normalize_plate(plate)
|
|
return any(p.match(norm) for p in _PATTERNS)
|
|
|
|
|
|
def search_plate(tenant_conn, plate):
|
|
"""Search plate_vehicles table for a matching plate.
|
|
|
|
Returns dict with vehicle info or None if not found.
|
|
"""
|
|
norm = normalize_plate(plate)
|
|
if not norm:
|
|
return None
|
|
|
|
cur = tenant_conn.cursor()
|
|
try:
|
|
cur.execute("""
|
|
SELECT id, plate, make, model, year, vin, customer_id, created_at
|
|
FROM plate_vehicles
|
|
WHERE REPLACE(REPLACE(UPPER(plate), '-', ''), ' ', '') = %s
|
|
LIMIT 1
|
|
""", (norm,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
return None
|
|
return {
|
|
'id': row[0],
|
|
'plate': row[1],
|
|
'make': row[2],
|
|
'model': row[3],
|
|
'year': row[4],
|
|
'vin': row[5],
|
|
'customer_id': row[6],
|
|
'created_at': row[7].isoformat() if row[7] else None,
|
|
}
|
|
finally:
|
|
cur.close()
|
|
|
|
|
|
def register_plate(tenant_conn, plate, make=None, model=None, year=None,
|
|
vin=None, customer_id=None):
|
|
"""Register or update a plate-to-vehicle mapping.
|
|
|
|
Returns the plate_vehicles record id.
|
|
"""
|
|
norm_display = normalize_plate(plate)
|
|
if not norm_display:
|
|
raise ValueError('Plate is required')
|
|
|
|
# Format nicely: ABC-1234 or AB-123-C
|
|
if re.match(r'^[A-Z]{3}\d{3,4}$', norm_display):
|
|
display = norm_display[:3] + '-' + norm_display[3:]
|
|
elif re.match(r'^[A-Z]{2}\d{3}[A-Z]$', norm_display):
|
|
display = norm_display[:2] + '-' + norm_display[2:5] + '-' + norm_display[5:]
|
|
else:
|
|
display = norm_display
|
|
|
|
cur = tenant_conn.cursor()
|
|
try:
|
|
cur.execute("""
|
|
INSERT INTO plate_vehicles (plate, make, model, year, vin, customer_id)
|
|
VALUES (%s, %s, %s, %s, %s, %s)
|
|
ON CONFLICT (plate) DO UPDATE SET
|
|
make = COALESCE(EXCLUDED.make, plate_vehicles.make),
|
|
model = COALESCE(EXCLUDED.model, plate_vehicles.model),
|
|
year = COALESCE(EXCLUDED.year, plate_vehicles.year),
|
|
vin = COALESCE(EXCLUDED.vin, plate_vehicles.vin),
|
|
customer_id = COALESCE(EXCLUDED.customer_id, plate_vehicles.customer_id)
|
|
RETURNING id
|
|
""", (display, make, model, year, vin, customer_id))
|
|
rec_id = cur.fetchone()[0]
|
|
tenant_conn.commit()
|
|
return rec_id
|
|
except Exception:
|
|
tenant_conn.rollback()
|
|
raise
|
|
finally:
|
|
cur.close()
|