Files
Autoparts-DB/pos/services/plate_lookup.py
consultoria-as 4cc2c66208 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>
2026-04-05 04:17:55 +00:00

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