From 4cc2c6620827451c2dc28313fd1787c3415000f8 Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Sun, 5 Apr 2026 04:17:55 +0000 Subject: [PATCH] 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) --- pos/blueprints/catalog_bp.py | 108 ++++++++++ pos/blueprints/inventory_bp.py | 85 ++++++++ pos/migrations/v1.7_plates.sql | 16 ++ pos/services/image_scraper.py | 188 +++++++++++++++++ pos/services/plate_lookup.py | 108 ++++++++++ pos/services/translations.py | 372 +++++++++++++++++++++++++++++---- pos/static/js/catalog.js | 71 +++++++ pos/templates/catalog.html | 12 ++ 8 files changed, 917 insertions(+), 43 deletions(-) create mode 100644 pos/migrations/v1.7_plates.sql create mode 100644 pos/services/image_scraper.py create mode 100644 pos/services/plate_lookup.py diff --git a/pos/blueprints/catalog_bp.py b/pos/blueprints/catalog_bp.py index 9e3877f..96a48ed 100644 --- a/pos/blueprints/catalog_bp.py +++ b/pos/blueprints/catalog_bp.py @@ -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/', 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() diff --git a/pos/blueprints/inventory_bp.py b/pos/blueprints/inventory_bp.py index d5768b0..c952c9a 100644 --- a/pos/blueprints/inventory_bp.py +++ b/pos/blueprints/inventory_bp.py @@ -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/', 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//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']) diff --git a/pos/migrations/v1.7_plates.sql b/pos/migrations/v1.7_plates.sql new file mode 100644 index 0000000..e5e41b0 --- /dev/null +++ b/pos/migrations/v1.7_plates.sql @@ -0,0 +1,16 @@ +-- Plate-to-vehicle lookup table (tenant DB) +-- Allows instant vehicle identification from Mexican license plates. + +CREATE TABLE IF NOT EXISTS plate_vehicles ( + id SERIAL PRIMARY KEY, + plate VARCHAR(20) UNIQUE NOT NULL, + make VARCHAR(100), + model VARCHAR(100), + year INTEGER, + vin VARCHAR(17), + customer_id INTEGER REFERENCES customers(id), + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_plate_vehicles_plate ON plate_vehicles(plate); +CREATE INDEX IF NOT EXISTS idx_plate_vehicles_customer ON plate_vehicles(customer_id); diff --git a/pos/services/image_scraper.py b/pos/services/image_scraper.py new file mode 100644 index 0000000..32b008f --- /dev/null +++ b/pos/services/image_scraper.py @@ -0,0 +1,188 @@ +# /home/Autopartes/pos/services/image_scraper.py +"""Bulk image downloader and processor for inventory parts. + +Provides two capabilities: +1. Bulk import: Accept a list of {part_number, image_url}, download each image, + resize/optimize with Pillow, save to /pos/static/images/parts/, and update + the inventory.image_url in the tenant DB. +2. Auto-image: Generate a placeholder image with the part number text when no + real image is available. +""" + +import io +import os +import logging +import requests +from PIL import Image, ImageDraw, ImageFont + +logger = logging.getLogger(__name__) + +IMAGES_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'static', 'images', 'parts') +MAX_SIZE = 800 +THUMB_SIZE = 300 +QUALITY = 85 +DOWNLOAD_TIMEOUT = 15 + + +def _ensure_dir(): + """Ensure the parts image directory exists.""" + os.makedirs(IMAGES_DIR, exist_ok=True) + + +def _process_and_save(image_data, tenant_id, item_id): + """Resize image, save full + thumbnail, return the relative URL path.""" + _ensure_dir() + + img = Image.open(io.BytesIO(image_data)) + if img.mode not in ('RGB', 'L'): + img = img.convert('RGB') + + # Full size + full = img.copy() + full.thumbnail((MAX_SIZE, MAX_SIZE), Image.LANCZOS) + full_path = os.path.join(IMAGES_DIR, f'{tenant_id}_{item_id}.jpg') + full.save(full_path, format='JPEG', quality=QUALITY) + + # Thumbnail + thumb = img.copy() + thumb.thumbnail((THUMB_SIZE, THUMB_SIZE), Image.LANCZOS) + thumb_path = os.path.join(IMAGES_DIR, f'{tenant_id}_{item_id}_thumb.jpg') + thumb.save(thumb_path, format='JPEG', quality=QUALITY) + + return f'/pos/static/images/parts/{tenant_id}_{item_id}.jpg' + + +def download_and_process(url, tenant_id, item_id): + """Download an image from a URL, process it, and save locally. + + Returns the relative URL path on success, or raises on failure. + """ + resp = requests.get(url, timeout=DOWNLOAD_TIMEOUT, stream=True) + resp.raise_for_status() + + # Read up to 10 MB + data = resp.content + if len(data) > 10 * 1024 * 1024: + raise ValueError('Image exceeds 10 MB limit') + + return _process_and_save(data, tenant_id, item_id) + + +def bulk_import(tenant_conn, tenant_id, items): + """Process a list of {part_number, image_url} items. + + For each item: + 1. Find the inventory item by part_number + 2. Download the image + 3. Resize and save + 4. Update inventory.image_url + + Returns: {imported: int, errors: [str]} + """ + imported = 0 + errors = [] + cur = tenant_conn.cursor() + + try: + for entry in items: + pn = (entry.get('part_number') or '').strip() + url = (entry.get('image_url') or '').strip() + + if not pn or not url: + errors.append(f'Missing part_number or image_url: {entry}') + continue + + # Find inventory item + cur.execute( + "SELECT id FROM inventory WHERE part_number = %s AND is_active = true LIMIT 1", + (pn,) + ) + row = cur.fetchone() + if not row: + errors.append(f'Part not found: {pn}') + continue + + item_id = row[0] + + try: + rel_url = download_and_process(url, tenant_id, item_id) + cur.execute( + "UPDATE inventory SET image_url = %s WHERE id = %s", + (rel_url, item_id) + ) + tenant_conn.commit() + imported += 1 + except Exception as e: + tenant_conn.rollback() + errors.append(f'{pn}: {str(e)}') + logger.warning('Failed to import image for %s: %s', pn, e) + + finally: + cur.close() + + return {'imported': imported, 'errors': errors} + + +def generate_placeholder(tenant_id, item_id, part_number, name=''): + """Generate a placeholder image with the part number text. + + Creates a 400x400 gray image with the part number centered. + Returns the relative URL path. + """ + _ensure_dir() + + width, height = 400, 400 + bg_color = (240, 240, 240) + text_color = (80, 80, 80) + accent_color = (245, 166, 35) # Nexus orange + + img = Image.new('RGB', (width, height), bg_color) + draw = ImageDraw.Draw(img) + + # Draw accent bar at top + draw.rectangle([0, 0, width, 6], fill=accent_color) + + # Draw part icon placeholder (box outline) + box_margin = 100 + draw.rectangle( + [box_margin, box_margin, width - box_margin, height - box_margin - 40], + outline=(200, 200, 200), width=2 + ) + + # Try to use a built-in font, fall back to default + try: + font_large = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 24) + font_small = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 14) + except (IOError, OSError): + font_large = ImageFont.load_default() + font_small = font_large + + # Part number (centered) + pn_text = part_number or 'SIN NUMERO' + bbox = draw.textbbox((0, 0), pn_text, font=font_large) + tw = bbox[2] - bbox[0] + draw.text(((width - tw) / 2, height - 80), pn_text, fill=accent_color, font=font_large) + + # Name (centered, below part number) + if name: + display_name = name[:30] + ('...' if len(name) > 30 else '') + bbox2 = draw.textbbox((0, 0), display_name, font=font_small) + tw2 = bbox2[2] - bbox2[0] + draw.text(((width - tw2) / 2, height - 50), display_name, fill=text_color, font=font_small) + + # "SIN IMAGEN" text centered in box + no_img = 'SIN IMAGEN' + bbox3 = draw.textbbox((0, 0), no_img, font=font_small) + tw3 = bbox3[2] - bbox3[0] + draw.text(((width - tw3) / 2, (height - 40) / 2), no_img, fill=(180, 180, 180), font=font_small) + + # Save + full_path = os.path.join(IMAGES_DIR, f'{tenant_id}_{item_id}.jpg') + img.save(full_path, format='JPEG', quality=QUALITY) + + thumb = img.copy() + thumb.thumbnail((THUMB_SIZE, THUMB_SIZE), Image.LANCZOS) + thumb_path = os.path.join(IMAGES_DIR, f'{tenant_id}_{item_id}_thumb.jpg') + thumb.save(thumb_path, format='JPEG', quality=QUALITY) + + return f'/pos/static/images/parts/{tenant_id}_{item_id}.jpg' diff --git a/pos/services/plate_lookup.py b/pos/services/plate_lookup.py new file mode 100644 index 0000000..ee505e8 --- /dev/null +++ b/pos/services/plate_lookup.py @@ -0,0 +1,108 @@ +# /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() diff --git a/pos/services/translations.py b/pos/services/translations.py index e9915e0..4310681 100644 --- a/pos/services/translations.py +++ b/pos/services/translations.py @@ -6,72 +6,335 @@ Falls back to the original name if no match is found. """ PART_TRANSLATIONS = { + # ─── Brake System ─── 'Brake Pad Set': 'Juego de Balatas', + 'Brake Pad': 'Balata', 'Brake Disc': 'Disco de Freno', + 'Brake Rotor': 'Disco de Freno', 'Shock Absorber': 'Amortiguador', - 'Oil Filter': 'Filtro de Aceite', - 'Air Filter': 'Filtro de Aire', - 'Spark Plug': 'Bujía', - 'Water Pump': 'Bomba de Agua', - 'Alternator': 'Alternador', - 'Starter Motor': 'Motor de Arranque', - 'Radiator': 'Radiador', - 'Thermostat': 'Termostato', - 'Timing Belt': 'Banda de Distribución', - 'V-Belt': 'Banda Serpentina', - 'Serpentine Belt': 'Banda Serpentina', - 'Clutch Kit': 'Kit de Embrague', - 'Fuel Pump': 'Bomba de Gasolina', - 'Fuel Filter': 'Filtro de Gasolina', - 'Oxygen Sensor': 'Sensor de Oxígeno', - 'Ignition Coil': 'Bobina de Encendido', - 'Wheel Bearing': 'Balero de Rueda', - 'Tie Rod End': 'Terminal de Dirección', - 'Ball Joint': 'Rótula', - 'CV Joint': 'Junta Homocinética', - 'Wiper Blade': 'Pluma Limpiaparabrisas', - 'Battery': 'Batería', - 'Headlight': 'Faro Delantero', - 'Tail Light': 'Calavera Trasera', - 'Mirror': 'Espejo', - 'Muffler': 'Mofle', - 'Exhaust Pipe': 'Tubo de Escape', - 'Catalytic Converter': 'Catalizador', + 'Brake Caliper': 'Caliper de Freno', + 'Brake Drum': 'Tambor de Freno', + 'Brake Hose': 'Manguera de Freno', + 'Brake Line': 'Línea de Freno', + 'Brake Shoe': 'Zapata de Freno', + 'Master Cylinder': 'Cilindro Maestro', + 'Wheel Cylinder': 'Cilindro de Rueda', + 'Brake Booster': 'Booster de Freno', + 'ABS Sensor': 'Sensor de ABS', + 'ABS Module': 'Módulo de ABS', + 'Brake Fluid': 'Líquido de Frenos', + 'Parking Brake': 'Freno de Mano', + 'Parking Brake Cable': 'Cable de Freno de Mano', + 'Brake Pedal': 'Pedal de Freno', + + # ─── Engine ─── 'Piston': 'Pistón', + 'Piston Ring': 'Anillos de Pistón', 'Gasket': 'Junta/Empaque', + 'Head Gasket': 'Junta de Cabeza', + 'Valve Cover Gasket': 'Junta de Tapa de Válvulas', + 'Oil Pan Gasket': 'Junta de Cárter', + 'Intake Manifold Gasket': 'Junta de Múltiple de Admisión', + 'Exhaust Manifold Gasket': 'Junta de Múltiple de Escape', 'Valve': 'Válvula', + 'Valve Spring': 'Resorte de Válvula', + 'Valve Stem Seal': 'Sello de Válvula', 'Camshaft': 'Árbol de Levas', 'Crankshaft': 'Cigüeñal', 'Connecting Rod': 'Biela', 'Engine Mount': 'Soporte de Motor', + 'Flywheel': 'Volante de Motor', + 'Timing Belt': 'Banda de Distribución', + 'Timing Chain': 'Cadena de Distribución', + 'Timing Cover': 'Tapa de Distribución', + 'Rocker Arm': 'Balancín', + 'Push Rod': 'Varilla de Empuje', + 'Turbocharger': 'Turbocompresor', + 'Supercharger': 'Supercargador', + 'Intake Manifold': 'Múltiple de Admisión', + 'Exhaust Manifold': 'Múltiple de Escape', + 'Oil Pump': 'Bomba de Aceite', + 'Oil Pan': 'Cárter', + 'Oil Cooler': 'Enfriador de Aceite', + 'Oil Pressure Switch': 'Bulbo de Aceite', + 'Cylinder Head': 'Cabeza de Cilindro', + 'Engine Block': 'Bloque de Motor', + 'Harmonic Balancer': 'Polea de Cigüeñal', + 'EGR Valve': 'Válvula EGR', + 'PCV Valve': 'Válvula PCV', + 'Vacuum Pump': 'Bomba de Vacío', + + # ─── Filters ─── + 'Oil Filter': 'Filtro de Aceite', + 'Air Filter': 'Filtro de Aire', + 'Fuel Filter': 'Filtro de Gasolina', + 'Cabin Air Filter': 'Filtro de Cabina', + 'Transmission Filter': 'Filtro de Transmisión', + 'Hydraulic Filter': 'Filtro Hidráulico', + + # ─── Ignition ─── + 'Spark Plug': 'Bujía', + 'Ignition Coil': 'Bobina de Encendido', + 'Glow Plug': 'Bujía de Precalentamiento', + 'Distributor Cap': 'Tapa de Distribuidor', + 'Distributor Rotor': 'Rotor de Distribuidor', + 'Ignition Wire': 'Cable de Bujía', + 'Ignition Module': 'Módulo de Encendido', + 'Knock Sensor': 'Sensor de Detonación', + 'Crankshaft Position Sensor': 'Sensor de Posición del Cigüeñal', + 'Camshaft Position Sensor': 'Sensor de Posición del Árbol de Levas', + + # ─── Fuel System ─── + 'Fuel Pump': 'Bomba de Gasolina', + 'Fuel Injector': 'Inyector de Gasolina', + 'Injector': 'Inyector', + 'Throttle Body': 'Cuerpo de Aceleración', + 'Mass Air Flow Sensor': 'Sensor MAF', + 'Oxygen Sensor': 'Sensor de Oxígeno', + 'Fuel Tank': 'Tanque de Gasolina', + 'Fuel Pressure Regulator': 'Regulador de Presión de Combustible', + 'Fuel Rail': 'Riel de Inyectores', + 'Carburetor': 'Carburador', + 'Fuel Sending Unit': 'Flotador de Gasolina', + 'Accelerator Pedal': 'Pedal de Acelerador', + 'Throttle Position Sensor': 'Sensor TPS', + 'MAP Sensor': 'Sensor MAP', + + # ─── Cooling System ─── + 'Radiator': 'Radiador', + 'Water Pump': 'Bomba de Agua', + 'Thermostat': 'Termostato', + 'Coolant': 'Anticongelante', + 'Coolant Temperature Sensor': 'Sensor de Temperatura', + 'Radiator Fan': 'Ventilador de Radiador', + 'Fan Motor': 'Motor de Ventilador de Radiador', + 'Radiator Hose': 'Manguera de Radiador', + 'Coolant Reservoir': 'Depósito de Anticongelante', + 'Fan Clutch': 'Clutch de Ventilador', + 'Thermostat Housing': 'Carcasa de Termostato', + 'Water Outlet': 'Toma de Agua', + + # ─── Electrical ─── + 'Alternator': 'Alternador', + 'Starter Motor': 'Motor de Arranque', + 'Battery': 'Batería', + 'Battery Cable': 'Cable de Batería', + 'Battery Terminal': 'Terminal de Batería', + 'Voltage Regulator': 'Regulador de Voltaje', + 'Sensor': 'Sensor', + 'Switch': 'Interruptor', + 'Relay': 'Relevador', + 'Fuse': 'Fusible', + 'Fuse Box': 'Caja de Fusibles', + 'Bulb': 'Foco', + 'Horn': 'Claxon', + 'Antenna': 'Antena', + 'Wiring Harness': 'Arnés de Cables', + 'Solenoid': 'Solenoide', + 'Ignition Switch': 'Switch de Encendido', + 'Headlight Switch': 'Switch de Faros', + 'Turn Signal Switch': 'Switch de Direccionales', + 'Window Switch': 'Switch de Ventanilla', + 'Blower Motor Resistor': 'Resistencia de Ventilador', + 'Speed Sensor': 'Sensor de Velocidad', + + # ─── Belts & Pulleys ─── + 'V-Belt': 'Banda Serpentina', + 'Serpentine Belt': 'Banda Serpentina', + 'Tensioner': 'Tensor', + 'Belt Tensioner': 'Tensor de Banda', + 'Idler Pulley': 'Polea Loca', + 'Belt': 'Banda', + 'Chain': 'Cadena', + 'Pulley': 'Polea', + + # ─── Clutch & Transmission ─── + 'Clutch Kit': 'Kit de Embrague', + 'Clutch Disc': 'Disco de Embrague', + 'Clutch Pressure Plate': 'Plato de Presión', + 'Clutch Release Bearing': 'Collarín', + 'Clutch Master Cylinder': 'Cilindro Maestro de Embrague', + 'Clutch Slave Cylinder': 'Cilindro Esclavo de Embrague', + 'Clutch Cable': 'Cable de Embrague', + 'Clutch Pedal': 'Pedal de Clutch', 'Transmission Mount': 'Soporte de Transmisión', + 'Transfer Case': 'Caja de Transferencia', + 'Gear Shift': 'Palanca de Velocidades', + 'Shift Cable': 'Cable de Palanca', + 'Transmission Fluid': 'Aceite de Transmisión', + 'Torque Converter': 'Convertidor de Par', + 'Synchronizer Ring': 'Anillo Sincronizador', + + # ─── Drivetrain ─── + 'Wheel Hub': 'Maza de Rueda', + 'Axle Shaft': 'Flecha/Semieje', + 'Drive Shaft': 'Flecha Cardán', + 'U-Joint': 'Cruceta', + 'CV Joint': 'Junta Homocinética', + 'CV Boot': 'Guardapolvo Homocinético', + 'Differential': 'Diferencial', + 'Wheel Bearing': 'Balero de Rueda', + 'Wheel Stud': 'Birlo de Rueda', + 'Lug Nut': 'Tuerca de Rueda', + 'Axle Nut': 'Tuerca de Flecha', + 'Bearing': 'Balero/Rodamiento', + 'Seal': 'Sello/Retén', + 'Bushing': 'Buje', + 'Mount': 'Soporte', + + # ─── Suspension ─── 'Control Arm': 'Brazo de Suspensión', 'Strut': 'Puntal', + 'Strut Mount': 'Base de Amortiguador', 'Spring': 'Resorte', + 'Coil Spring': 'Resorte Helicoidal', + 'Leaf Spring': 'Ballesta', 'Stabilizer Bar': 'Barra Estabilizadora', - 'Brake Caliper': 'Caliper de Freno', - 'Brake Drum': 'Tambor de Freno', - 'Brake Hose': 'Manguera de Freno', - 'Master Cylinder': 'Cilindro Maestro', - 'Wheel Cylinder': 'Cilindro de Rueda', + 'Stabilizer Link': 'Bieleta Estabilizadora', + 'Sway Bar Link': 'Bieleta Estabilizadora', + 'Trailing Arm': 'Brazo Trasero', + 'Torsion Bar': 'Barra de Torsión', + 'Shock Mount': 'Base de Amortiguador', + 'Bump Stop': 'Tope de Amortiguador', + 'Air Spring': 'Bolsa de Aire de Suspensión', + 'Panhard Rod': 'Barra Panhard', + + # ─── Steering ─── 'Power Steering Pump': 'Bomba de Dirección Hidráulica', 'Rack and Pinion': 'Cremallera de Dirección', + 'Tie Rod End': 'Terminal de Dirección', + 'Tie Rod': 'Barra de Dirección', + 'Ball Joint': 'Rótula', + 'Steering Wheel': 'Volante', + 'Steering Column': 'Columna de Dirección', + 'Power Steering Hose': 'Manguera de Dirección', + 'Power Steering Fluid': 'Aceite de Dirección', + 'Pitman Arm': 'Brazo Pitman', + 'Idler Arm': 'Brazo Loco', + 'Center Link': 'Barra Central de Dirección', + 'Drag Link': 'Barra de Acoplamiento', + 'Steering Knuckle': 'Muñón de Dirección', + 'King Pin': 'Perno Rey', + + # ─── Exhaust ─── + 'Muffler': 'Mofle', + 'Exhaust Pipe': 'Tubo de Escape', + 'Catalytic Converter': 'Catalizador', + 'Exhaust Gasket': 'Junta de Escape', + 'Exhaust Clamp': 'Abrazadera de Escape', + 'Resonator': 'Resonador', + 'Flex Pipe': 'Tubo Flexible de Escape', + 'O2 Sensor': 'Sensor de Oxígeno', + 'Exhaust Tip': 'Punta de Escape', + + # ─── A/C & Heating ─── 'A/C Compressor': 'Compresor de Aire Acondicionado', 'Condenser': 'Condensador', 'Evaporator': 'Evaporador', 'Heater Core': 'Radiador de Calefacción', 'Blower Motor': 'Motor de Ventilador', - 'Tensioner': 'Tensor', - 'Idler Pulley': 'Polea Loca', - 'Flywheel': 'Volante de Motor', - 'Injector': 'Inyector', - 'Throttle Body': 'Cuerpo de Aceleración', - 'Mass Air Flow Sensor': 'Sensor MAF', - 'Coolant': 'Anticongelante', - 'Brake Fluid': 'Líquido de Frenos', - 'Transmission Fluid': 'Aceite de Transmisión', + 'A/C Hose': 'Manguera de Aire Acondicionado', + 'Expansion Valve': 'Válvula de Expansión', + 'A/C Accumulator': 'Acumulador de A/C', + 'A/C Receiver Drier': 'Filtro Deshidratador', + 'A/C Clutch': 'Clutch de Compresor', + 'Heater Valve': 'Válvula de Calefacción', + 'Heater Hose': 'Manguera de Calefacción', + + # ─── Lighting ─── + 'Headlight': 'Faro Delantero', + 'Headlight Assembly': 'Faro Delantero Completo', + 'Tail Light': 'Calavera Trasera', + 'Tail Light Assembly': 'Calavera Trasera Completa', + 'Fog Light': 'Faro de Niebla', + 'Turn Signal': 'Direccional', + 'Turn Signal Light': 'Luz Direccional', + 'Side Marker': 'Luz Lateral', + 'Reverse Light': 'Luz de Reversa', + 'Third Brake Light': 'Tercera Luz de Freno', + 'License Plate Light': 'Luz de Placa', + 'Interior Light': 'Luz Interior', + 'Dome Light': 'Luz de Techo', + 'DRL Light': 'Luz Diurna', + 'LED Module': 'Módulo LED', + 'Ballast': 'Balastro', + 'HID Bulb': 'Foco HID', + + # ─── Body & Exterior ─── + 'Bumper': 'Defensa', + 'Front Bumper': 'Defensa Delantera', + 'Rear Bumper': 'Defensa Trasera', + 'Fender': 'Salpicadera', + 'Grille': 'Parrilla', + 'Hood': 'Cofre', + 'Trunk Lid': 'Tapa de Cajuela', + 'Door': 'Puerta', + 'Door Handle': 'Manija de Puerta', + 'Door Hinge': 'Bisagra de Puerta', + 'Door Lock': 'Cerradura', + 'Door Lock Actuator': 'Actuador de Cerradura', + 'Trunk Latch': 'Cerradura de Cajuela', + 'Hood Latch': 'Cerradura de Cofre', + 'Windshield': 'Parabrisas', + 'Rear Window': 'Medallón Trasero', + 'Door Glass': 'Cristal de Puerta', + 'Quarter Panel': 'Panel Trasero', + 'Rocker Panel': 'Estribo', + 'Mud Flap': 'Loderas', + 'Splash Guard': 'Guardabarros', + 'Molding': 'Moldura', + 'Emblem': 'Emblema', + 'Body Clip': 'Grapa de Carrocería', + 'Weather Strip': 'Empaque de Puerta', + + # ─── Glass & Mirrors ─── + 'Mirror': 'Espejo', + 'Side Mirror': 'Espejo Lateral', + 'Rear View Mirror': 'Espejo Retrovisor', + 'Mirror Glass': 'Luna de Espejo', + 'Window Regulator': 'Elevador de Cristal', + 'Window Motor': 'Motor de Elevador', + 'Windshield Wiper Motor': 'Motor de Limpiaparabrisas', + 'Wiper Blade': 'Pluma Limpiaparabrisas', + 'Wiper Arm': 'Brazo de Limpiaparabrisas', + 'Wiper Linkage': 'Varillaje de Limpiaparabrisas', + 'Washer Pump': 'Bomba de Limpiaparabrisas', + 'Washer Reservoir': 'Depósito de Limpiaparabrisas', + + # ─── Interior & Safety ─── + 'Seat Belt': 'Cinturón de Seguridad', + 'Air Bag': 'Bolsa de Aire', + 'Clock Spring': 'Espiral de Reloj', + 'Dashboard': 'Tablero', + 'Instrument Cluster': 'Cuadro de Instrumentos', + 'Glove Box': 'Guantera', + 'Sun Visor': 'Visera', + 'Headliner': 'Cielo de Techo', + 'Floor Mat': 'Tapete', + 'Seat Cover': 'Funda de Asiento', + 'Carpet': 'Alfombra', + 'Center Console': 'Consola Central', + 'Cup Holder': 'Portavasos', + + # ─── Hoses & General ─── + 'Hose': 'Manguera', + 'Hose Clamp': 'Abrazadera de Manguera', + 'Pump': 'Bomba', + + # ─── Fluids & Chemicals ─── 'Engine Oil': 'Aceite de Motor', - # Categories + 'Power Steering Fluid': 'Aceite de Dirección', + 'Brake Cleaner': 'Limpiador de Frenos', + 'Antifreeze': 'Anticongelante', + 'Windshield Washer Fluid': 'Líquido Limpiaparabrisas', + 'Grease': 'Grasa', + 'Thread Locker': 'Fijador de Roscas', + 'Silicone': 'Silicón', + 'Adhesive': 'Adhesivo', + 'Sealant': 'Sellador', + 'Refrigerant': 'Refrigerante', + + # ─── Categories ─── 'Braking System': 'Sistema de Frenos', 'Engine': 'Motor', 'Suspension/Damping': 'Suspensión', @@ -88,6 +351,29 @@ PART_TRANSLATIONS = { 'Axle Drive': 'Transmisión/Ejes', 'Body': 'Carrocería', 'Axle Mounting/ Steering/ Wheels': 'Suspensión/Dirección/Ruedas', + 'Transmission': 'Transmisión', + 'Air Conditioning': 'Aire Acondicionado', + 'Interior': 'Interior', + 'Exterior': 'Exterior', + 'Lighting': 'Iluminación', + 'Wipers': 'Limpiaparabrisas', + 'Accessories': 'Accesorios', + 'Tools': 'Herramientas', + 'Chemicals': 'Químicos/Líquidos', + 'Hardware': 'Tornillería', + 'Clutch/Parts': 'Embrague/Partes', + 'Wheel Suspension': 'Suspensión de Rueda', + 'Gaskets/Seals': 'Juntas/Sellos', + 'Fuel Supply System': 'Sistema de Suministro de Combustible', + 'Air Supply': 'Suministro de Aire', + 'Comfort Systems': 'Sistemas de Confort', + 'Communication Systems': 'Sistemas de Comunicación', + 'Locking System': 'Sistema de Cierre', + 'Windscreen Cleaning': 'Limpieza de Parabrisas', + 'Universal Parts': 'Partes Universales', + 'Oils/Fluids': 'Aceites/Líquidos', + 'Tyres': 'Neumáticos/Llantas', + 'Wheels': 'Rines', } diff --git a/pos/static/js/catalog.js b/pos/static/js/catalog.js index 27af5e2..f22eeb3 100644 --- a/pos/static/js/catalog.js +++ b/pos/static/js/catalog.js @@ -1019,6 +1019,75 @@ }); } + // ─── PLATE LOOKUP ─── + var plateInputWrap = document.getElementById('plateInputWrap'); + var plateInput = document.getElementById('plateInput'); + var plateStatus = document.getElementById('plateStatus'); + var plateToggle = document.getElementById('plateToggle'); + + function togglePlate() { + var isVisible = plateInputWrap.style.display !== 'none'; + plateInputWrap.style.display = isVisible ? 'none' : ''; + plateToggle.textContent = isVisible ? 'Tienes las placas?' : 'Ocultar placas'; + if (!isVisible && plateInput) plateInput.focus(); + } + + function showPlateStatus(msg, isError) { + plateStatus.style.display = msg ? '' : 'none'; + plateStatus.textContent = msg; + plateStatus.style.color = isError ? 'var(--color-error)' : 'var(--color-text-muted)'; + } + + function lookupPlate() { + var plate = (plateInput.value || '').trim().toUpperCase(); + if (!plate || plate.length < 5) { + showPlateStatus('Ingresa una placa valida (Ej: ABC-1234).', true); + return; + } + showPlateStatus('Buscando placa...', false); + + apiFetch(API + '/plate/' + encodeURIComponent(plate)).then(function (data) { + if (!data) { + showPlateStatus('Error de conexion al buscar placa.', true); + return; + } + if (data.error) { + showPlateStatus(data.error, true); + return; + } + if (!data.found) { + plateStatus.style.display = ''; + plateStatus.innerHTML = 'Placa no registrada. Registrar vehiculo'; + plateStatus.style.color = 'var(--color-warning, #e6a700)'; + return; + } + + var parts = []; + if (data.year) parts.push(data.year); + if (data.make) parts.push(data.make); + if (data.model) parts.push(data.model); + var label = parts.join(' ') || 'Vehiculo encontrado'; + + // If we got a catalog match, auto-fill the dropdowns + var match = data.catalog_match; + if (match && match.brand_id) { + showPlateStatus(label + ' — Cargando catalogo...', false); + _autoFillFromVin(match, data); + } else { + showPlateStatus(label + ' — No encontrado en el catalogo TecDoc.', false); + } + }); + } + + if (plateInput) { + plateInput.addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + lookupPlate(); + } + }); + } + // ─── VIN DECODER ─── var vinInputWrap = document.getElementById('vinInputWrap'); var vinInput = document.getElementById('vinInput'); @@ -1160,6 +1229,8 @@ startBarcodeScan: startBarcodeScan, toggleVin: toggleVin, decodeVin: decodeVin, + togglePlate: togglePlate, + lookupPlate: lookupPlate, }; // ─── INIT ─── diff --git a/pos/templates/catalog.html b/pos/templates/catalog.html index cdacf9e..7b6aaca 100644 --- a/pos/templates/catalog.html +++ b/pos/templates/catalog.html @@ -635,6 +635,17 @@ | +
+ Tienes las placas? + +
+ |
Tienes el VIN? +