Files
Autoparts-DB/console/screens/vin_decoder.py
consultoria-as 7b2a904498 feat: migrate to PostgreSQL + SQLAlchemy ORM, rebrand to Nexus Autoparts
- Migrate from SQLite to PostgreSQL with normalized schema
- Add 11 lookup tables (fuel_type, body_type, drivetrain, transmission,
  materials, position_part, manufacture_type, quality_tier, countries,
  reference_type, shapes)
- Rewrite dashboard/server.py (76 routes) using SQLAlchemy text() queries
- Rewrite console/db.py (27 methods) using SQLAlchemy ORM
- Add models.py with 27 SQLAlchemy model definitions
- Add config.py for centralized DB_URL configuration
- Add migrate_to_postgres.py migration script
- Add docs/METABASE_GUIDE.md with complete data entry guide
- Rebrand from "AUTOPARTS DB" to "NEXUS AUTOPARTS"
- Fill vehicle data gaps via NHTSA API + heuristics:
  engines (cylinders, power, torque), brands (country, founded_year),
  models (body_type, production years), MYE (drivetrain, transmission, trim)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 05:24:47 +00:00

260 lines
8.1 KiB
Python

"""
VIN decoder screen for the NEXUS AUTOPARTS console application.
Prompts for a 17-character Vehicle Identification Number, decodes it
via the NHTSA vPIC API (with local caching), and displays the decoded
vehicle information. The user can then navigate to the parts catalog
filtered by the matched vehicle.
"""
import json
from console.core.screens import Screen
from console.core.keybindings import Key
from console.config import APP_NAME, VERSION
from console.utils.vin_api import decode_vin_nhtsa
# Footer labels
_FOOTER_INPUT = [
("ENTER", "Decodificar"),
("ESC", "Atras"),
]
_FOOTER_RESULT = [
("1", "Ver partes"),
("2/F3", "Nuevo VIN"),
("ESC", "Atras"),
]
_FOOTER_ERROR = [
("F3", "Nuevo VIN"),
("ESC", "Atras"),
]
class VinDecoderScreen(Screen):
"""VIN decoder with NHTSA API integration and local cache."""
def __init__(self):
super().__init__(name="vin_decoder", title="Decodificador VIN")
self._vin = None
self._decoded = None
self._error = None
self._needs_input = True
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def _decode(self, vin, db):
"""Decode a VIN using cache first, then NHTSA API."""
self._vin = vin.upper().strip()
self._decoded = None
self._error = None
# Check cache
cached = db.get_vin_cache(self._vin)
if cached:
self._decoded = {
"make": cached.get("make", ""),
"model": cached.get("model", ""),
"year": cached.get("year", ""),
"engine_info": cached.get("engine_info", ""),
"body_class": cached.get("body_class", ""),
"drive_type": cached.get("drive_type", ""),
}
return
# Call NHTSA API
result = decode_vin_nhtsa(self._vin)
if "error" in result:
self._error = result["error"]
return
# Extract fields
make = result.get("make", "")
model = result.get("model", "")
year = result.get("year", "")
body_class = result.get("body_class", "")
drive_type = result.get("drive_type", "")
# Build engine info string
engine_info_dict = result.get("engine_info", {})
engine_parts = []
if engine_info_dict.get("displacement_l"):
engine_parts.append(f"{engine_info_dict['displacement_l']}L")
if engine_info_dict.get("cylinders"):
engine_parts.append(f"{engine_info_dict['cylinders']}cil")
if engine_info_dict.get("fuel_type"):
engine_parts.append(engine_info_dict["fuel_type"])
if engine_info_dict.get("power_hp"):
engine_parts.append(f"{engine_info_dict['power_hp']}hp")
engine_info = " ".join(engine_parts)
self._decoded = {
"make": make,
"model": model,
"year": year,
"engine_info": engine_info,
"body_class": body_class,
"drive_type": drive_type,
}
# Cache the result
try:
year_int = int(year) if year else 0
except (ValueError, TypeError):
year_int = 0
try:
db.save_vin_cache(
vin=self._vin,
data=json.dumps(result),
make=make,
model=model,
year=year_int,
engine_info=engine_info,
body_class=body_class,
drive_type=drive_type,
)
except Exception:
pass # Non-critical: caching failure should not break the flow
def _try_match_vehicle(self, db):
"""Try to match the decoded VIN to a vehicle in the database.
Returns a context dict for the catalogo screen if a match is
found, or None if no match exists.
"""
if not self._decoded:
return None
make = self._decoded.get("make", "")
model = self._decoded.get("model", "")
year = self._decoded.get("year", "")
if not make or not model:
return None
try:
year_int = int(year) if year else None
except (ValueError, TypeError):
year_int = None
# Try to find matching model_year_engine records
if year_int:
mye_records = db.get_model_year_engine(make, model, year_int)
else:
mye_records = []
ctx = {
"level": "categories",
"brand": make,
"model": model,
"year": year,
"engine": self._decoded.get("engine_info", ""),
}
if mye_records:
# Use the first match
ctx["mye_id"] = mye_records[0]["id"]
return ctx
# ------------------------------------------------------------------
# Render
# ------------------------------------------------------------------
def render(self, context, db, renderer):
# Header
renderer.draw_header(
f" {APP_NAME} v{VERSION}",
" DECODIFICADOR VIN ",
)
if self._needs_input:
renderer.draw_footer(_FOOTER_INPUT)
return
if self._error:
renderer.draw_text(5, 4, f"Error: {self._error}", "error")
renderer.draw_footer(_FOOTER_ERROR)
return
if self._decoded is None:
renderer.draw_text(5, 4, "Presione F3 para ingresar un VIN", "info")
renderer.draw_footer(_FOOTER_ERROR)
return
# Display decoded VIN info
fields = [
("VIN", self._vin or ""),
("Marca", self._decoded.get("make", "")),
("Modelo", self._decoded.get("model", "")),
("Ano", str(self._decoded.get("year", ""))),
("Motor", self._decoded.get("engine_info", "")),
("Carroceria", self._decoded.get("body_class", "")),
("Traccion", self._decoded.get("drive_type", "")),
]
renderer.draw_detail(fields, title="INFORMACION DEL VEHICULO")
# Action menu below detail
h, _w = renderer.get_size()
action_row = 5 + len(fields) + 3
if action_row < h - 4:
renderer.draw_text(action_row, 4, "1. Ver partes compatibles", "normal")
renderer.draw_text(action_row + 1, 4, "2. Nueva consulta VIN", "normal")
renderer.draw_footer(_FOOTER_RESULT)
# ------------------------------------------------------------------
# Key handling
# ------------------------------------------------------------------
def on_key(self, key, context, db, renderer, nav):
# If we need input, show the input dialog
if self._needs_input:
self._needs_input = False
value = renderer.show_input("VIN (17 caracteres)", max_len=17)
if value is None:
# User pressed ESC in input dialog
if self._decoded is not None:
return None
return "back"
value = value.strip()
if len(value) != 17:
self._error = "El VIN debe tener exactamente 17 caracteres"
self._decoded = None
return None
self._decode(value, db)
return None
# ESC: go back
if key == Key.ESCAPE:
return "back"
# F3 or '2': new VIN input
if key == Key.F3 or key == ord("2"):
self._needs_input = True
self._error = None
return None
# '1': view compatible parts
if key == ord("1"):
if self._decoded:
cat_context = self._try_match_vehicle(db)
if cat_context:
return ("catalogo", cat_context, "Catalogo")
else:
renderer.show_message(
"No se encontro el vehiculo en la base de datos.\n"
"Se mostrara el catalogo completo.",
"info",
)
return ("catalogo", {"level": "categories"}, "Catalogo")
return None
return None