Add admin panel, enhanced search, Gonher import and expand API

- Add admin interface (admin.html, admin.js) for managing catalog data
- Add enhanced search module with advanced filtering capabilities
- Expand server.py with new API endpoints and admin functionality
- Add Gonher catalog import scripts (import_gonher_catalog.py, import_gonher_complete.py)
- Add demo data population script and sample CSV data
- Update customer landing page and dashboard with UI improvements
- Update database with enriched vehicle and parts data

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 00:35:05 +00:00
parent 6fb2a52f86
commit e66b18f6ae
15 changed files with 7308 additions and 141 deletions

View File

@@ -0,0 +1,342 @@
#!/usr/bin/env python3
"""
Import Gonher Filter Catalog PDF into the autoparts database.
Extracts filter part numbers and vehicle compatibility.
"""
import sqlite3
import re
import pypdf
from pathlib import Path
# Database path
DB_PATH = Path(__file__).parent.parent / 'vehicle_database.db'
PDF_PATH = '/tmp/filtros_catalog.pdf'
# Filter type mapping
FILTER_TYPES = {
'ACEITE': {'category': 'Engine', 'group': 'Oil Filters', 'name_prefix': 'Oil Filter'},
'SINTÉTICO': {'category': 'Engine', 'group': 'Oil Filters', 'name_prefix': 'Synthetic Oil Filter'},
'AIRE': {'category': 'Engine', 'group': 'Air Filters', 'name_prefix': 'Air Filter'},
'COMB.': {'category': 'Fuel & Air', 'group': 'Fuel Filters', 'name_prefix': 'Fuel Filter'},
'CABINA': {'category': 'Heat & Air Conditioning', 'group': 'Cabin Air Filters', 'name_prefix': 'Cabin Air Filter'}
}
def get_db_connection():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def ensure_manufacturer(cursor, name='Gonher'):
"""Ensure Gonher manufacturer exists"""
cursor.execute("SELECT id FROM manufacturers WHERE name = ?", (name,))
row = cursor.fetchone()
if row:
return row['id']
cursor.execute("""
INSERT INTO manufacturers (name, type, quality_tier, country)
VALUES (?, 'aftermarket', 'standard', 'Mexico')
""", (name,))
return cursor.lastrowid
def get_or_create_group(cursor, category_name, group_name):
"""Get or create a part group"""
# Find category
cursor.execute("SELECT id FROM part_categories WHERE name = ?", (category_name,))
cat_row = cursor.fetchone()
if not cat_row:
print(f" Warning: Category '{category_name}' not found")
return None
category_id = cat_row['id']
# Find or create group
cursor.execute("""
SELECT id FROM part_groups
WHERE category_id = ? AND name = ?
""", (category_id, group_name))
group_row = cursor.fetchone()
if group_row:
return group_row['id']
# Create group
cursor.execute("""
INSERT INTO part_groups (category_id, name, name_es)
VALUES (?, ?, ?)
""", (category_id, group_name, group_name))
return cursor.lastrowid
def parse_year_range(year_str):
"""Parse year range like '2019 - 2016' or '2020- 2018' into list of years"""
year_str = year_str.strip()
# Handle single year
if re.match(r'^\d{4}$', year_str):
return [int(year_str)]
# Handle range with various separators
match = re.match(r'(\d{4})\s*[-]\s*(\d{4})', year_str)
if match:
start, end = int(match.group(1)), int(match.group(2))
if start > end:
start, end = end, start
return list(range(end, start + 1))
return []
def parse_catalog_page(text, current_brand=None):
"""Parse a catalog page and extract vehicle-filter mappings"""
entries = []
lines = text.split('\n')
brand = current_brand
model = None
for line in lines:
line = line.strip()
if not line:
continue
# Skip header lines
if 'AÑO' in line and 'MOTOR' in line:
continue
if 'ACEITE' in line and 'AIRE' in line:
continue
if 'Los filtros Gonher' in line:
continue
if 'Para vehículos anteriores' in line:
continue
if '(Continúa)' in line:
# Extract brand/model from continuation
match = re.match(r'^([A-Z\s]+)\s*\(Continúa\)', line)
if match:
potential = match.group(1).strip()
if len(potential) > 2:
if potential in ['ACURA', 'ALFA ROMEO', 'AUDI', 'BMW', 'BUICK', 'CADILLAC',
'CHEVROLET', 'CHRYSLER', 'DODGE', 'FIAT', 'FORD', 'GMC',
'HONDA', 'HYUNDAI', 'INFINITI', 'JAGUAR', 'JEEP', 'KIA',
'LEXUS', 'LINCOLN', 'MAZDA', 'MERCEDES BENZ', 'MERCURY',
'MINI', 'MITSUBISHI', 'NISSAN', 'PEUGEOT', 'PONTIAC',
'PORSCHE', 'RAM', 'RENAULT', 'SEAT', 'SMART', 'SUBARU',
'SUZUKI', 'TOYOTA', 'VOLKSWAGEN', 'VOLVO']:
brand = potential
else:
model = potential
continue
# Check if this is a brand line (all caps, single word or known brand)
if re.match(r'^[A-Z][A-Z\s]+$', line) and len(line) > 2:
potential_brand = line.strip()
if potential_brand in ['ACURA', 'ALFA ROMEO', 'AUDI', 'BMW', 'BUICK', 'CADILLAC',
'CHEVROLET', 'CHRYSLER', 'DODGE', 'FIAT', 'FORD', 'GMC',
'HONDA', 'HYUNDAI', 'INFINITI', 'JAGUAR', 'JEEP', 'KIA',
'LEXUS', 'LINCOLN', 'MAZDA', 'MERCEDES BENZ', 'MERCURY',
'MINI', 'MITSUBISHI', 'NISSAN', 'PEUGEOT', 'PONTIAC',
'PORSCHE', 'RAM', 'RENAULT', 'SEAT', 'SMART', 'SUBARU',
'SUZUKI', 'TOYOTA', 'VOLKSWAGEN', 'VOLVO']:
brand = potential_brand
model = None
continue
# Check if this is a model line (letters/numbers, no year pattern at start)
if brand and re.match(r'^[A-Z][A-Z0-9\s\-]+$', line) and not re.match(r'^\d{4}', line):
if not any(c.isdigit() for c in line[:4]): # Model names don't start with 4 digits
potential_model = line.strip()
if len(potential_model) >= 2 and potential_model not in ['ACEITE', 'AIRE', 'COMB', 'CABINA']:
model = potential_model
continue
# Try to parse data line (year, motor, filters)
# Pattern: YEAR[-YEAR] MOTOR FILTER1 FILTER2 ...
if brand and model:
# Look for year at start
match = re.match(r'^(\d{4}(?:\s*[-]\s*\d{4})?)\s+(.+)$', line)
if match:
year_str = match.group(1)
rest = match.group(2)
# Extract motor (usually like L4-2.0L or V6-3.5L)
motor_match = re.match(r'^([LV]\d+[-][\d.]+L(?:\s+(?:Turbo|TURBOCHARGED|diésel|ELECTRIC))?)\s*(.*)$', rest, re.IGNORECASE)
if motor_match:
motor = motor_match.group(1).strip()
filters_str = motor_match.group(2).strip()
# Parse filter part numbers
# They follow pattern like: GP-149 GPS-149 GA-1113 GAC-92
filter_parts = re.findall(r'[A-Z]+[-][\dA-Z]+(?:\(\d+\))?', filters_str)
years = parse_year_range(year_str)
if years and filter_parts:
for year in years:
entry = {
'brand': brand,
'model': model,
'year': year,
'motor': motor,
'filters': {}
}
# Categorize filters by prefix
for fp in filter_parts:
fp_clean = re.sub(r'\(\d+\)', '', fp) # Remove (1) markers
if fp_clean.startswith('GP-') or fp_clean.startswith('G-'):
entry['filters']['ACEITE'] = fp_clean
elif fp_clean.startswith('GPS-'):
entry['filters']['SINTÉTICO'] = fp_clean
elif fp_clean.startswith('GA-') and not fp_clean.startswith('GAC-') and not fp_clean.startswith('GAVW-'):
entry['filters']['AIRE'] = fp_clean
elif fp_clean.startswith('GG-'):
entry['filters']['COMB.'] = fp_clean
elif fp_clean.startswith('GAC-'):
entry['filters']['CABINA'] = fp_clean
if entry['filters']:
entries.append(entry)
return entries, brand
def import_catalog():
"""Main import function"""
print("="*60)
print("GONHER FILTER CATALOG IMPORT")
print("="*60)
# Read PDF
print(f"\nReading PDF: {PDF_PATH}")
pdf = pypdf.PdfReader(PDF_PATH)
total_pages = len(pdf.pages)
print(f"Total pages: {total_pages}")
# Parse all pages
all_entries = []
current_brand = None
for i in range(total_pages):
text = pdf.pages[i].extract_text()
if text:
entries, current_brand = parse_catalog_page(text, current_brand)
all_entries.extend(entries)
if (i + 1) % 20 == 0:
print(f" Processed {i + 1}/{total_pages} pages, {len(all_entries)} entries so far...")
print(f"\nTotal entries extracted: {len(all_entries)}")
# Get unique filters
unique_filters = {}
for entry in all_entries:
for filter_type, part_num in entry['filters'].items():
if part_num not in unique_filters:
unique_filters[part_num] = filter_type
print(f"Unique filter part numbers: {len(unique_filters)}")
# Import to database
conn = get_db_connection()
cursor = conn.cursor()
# Ensure Gonher manufacturer exists
manufacturer_id = ensure_manufacturer(cursor, 'Gonher')
print(f"\nManufacturer ID: {manufacturer_id}")
# Create parts for each unique filter
print("\nCreating filter parts...")
filter_part_ids = {}
for part_num, filter_type in unique_filters.items():
config = FILTER_TYPES.get(filter_type)
if not config:
continue
group_id = get_or_create_group(cursor, config['category'], config['group'])
if not group_id:
continue
# Check if part exists
cursor.execute("SELECT id FROM parts WHERE oem_part_number = ?", (part_num,))
existing = cursor.fetchone()
if existing:
filter_part_ids[part_num] = existing['id']
else:
# Create new part
name = f"{config['name_prefix']} {part_num}"
name_es = f"Filtro {part_num}"
cursor.execute("""
INSERT INTO parts (oem_part_number, name, name_es, group_id, description)
VALUES (?, ?, ?, ?, ?)
""", (part_num, name, name_es, group_id, f"Gonher {config['name_prefix']}"))
filter_part_ids[part_num] = cursor.lastrowid
print(f" Created/found {len(filter_part_ids)} filter parts")
# Create vehicle fitments
print("\nCreating vehicle fitments...")
fitments_created = 0
fitments_skipped = 0
for entry in all_entries:
# Find matching vehicle (model_year_engine)
cursor.execute("""
SELECT mye.id
FROM model_year_engine mye
JOIN models m ON mye.model_id = m.id
JOIN brands b ON m.brand_id = b.id
JOIN years y ON mye.year_id = y.id
WHERE UPPER(b.name) = UPPER(?)
AND UPPER(m.name) = UPPER(?)
AND y.year = ?
LIMIT 1
""", (entry['brand'], entry['model'], entry['year']))
mye_row = cursor.fetchone()
if not mye_row:
fitments_skipped += 1
continue
mye_id = mye_row['id']
# Create fitments for each filter
for filter_type, part_num in entry['filters'].items():
part_id = filter_part_ids.get(part_num)
if not part_id:
continue
# Check if fitment exists
cursor.execute("""
SELECT id FROM vehicle_parts
WHERE model_year_engine_id = ? AND part_id = ?
""", (mye_id, part_id))
if not cursor.fetchone():
cursor.execute("""
INSERT INTO vehicle_parts (model_year_engine_id, part_id, quantity_required, fitment_notes)
VALUES (?, ?, 1, ?)
""", (mye_id, part_id, f"Gonher catalog 2022 - {filter_type}"))
fitments_created += 1
print(f" Fitments created: {fitments_created}")
print(f" Entries skipped (vehicle not found): {fitments_skipped}")
conn.commit()
conn.close()
print("\n" + "="*60)
print("IMPORT COMPLETE")
print("="*60)
return {
'total_entries': len(all_entries),
'unique_filters': len(unique_filters),
'parts_created': len(filter_part_ids),
'fitments_created': fitments_created
}
if __name__ == '__main__':
results = import_catalog()
print(f"\nSummary: {results}")

View File

@@ -0,0 +1,511 @@
#!/usr/bin/env python3
"""
IMPORTADOR COMPLETO DEL CATÁLOGO GONHER 2022
- Crea vehículos faltantes
- Crea partes de filtros Gonher
- Crea referencias cruzadas con otras marcas (AC Delco, Fram, etc.)
- Crea fitments (vincula partes a vehículos)
"""
import sqlite3
import re
import pypdf
from pathlib import Path
from collections import defaultdict
# Paths
DB_PATH = Path(__file__).parent.parent / 'vehicle_database.db'
PDF_PATH = '/tmp/filtros_catalog.pdf'
# Filter type configuration
FILTER_TYPES = {
'ACEITE': {'category': 'Engine', 'group': 'Oil Filters', 'prefix': 'Oil Filter', 'prefix_es': 'Filtro de Aceite'},
'SINTÉTICO': {'category': 'Engine', 'group': 'Oil Filters', 'prefix': 'Synthetic Oil Filter', 'prefix_es': 'Filtro de Aceite Sintético'},
'AIRE': {'category': 'Engine', 'group': 'Air Filters', 'prefix': 'Air Filter', 'prefix_es': 'Filtro de Aire'},
'COMB.': {'category': 'Fuel & Air', 'group': 'Fuel Filters', 'prefix': 'Fuel Filter', 'prefix_es': 'Filtro de Combustible'},
'CABINA': {'category': 'Heat & Air Conditioning', 'group': 'Cabin Air Filters', 'prefix': 'Cabin Air Filter', 'prefix_es': 'Filtro de Cabina'}
}
# Known brands in catalog
CATALOG_BRANDS = [
'ACURA', 'ALFA ROMEO', 'AUDI', 'BMW', 'BUICK', 'CADILLAC',
'CHEVROLET', 'CHRYSLER', 'DODGE', 'FIAT', 'FORD', 'GMC',
'HONDA', 'HYUNDAI', 'INFINITI', 'JAGUAR', 'JEEP', 'KIA',
'LEXUS', 'LINCOLN', 'MAZDA', 'MERCEDES BENZ', 'MERCURY',
'MINI', 'MITSUBISHI', 'NISSAN', 'PEUGEOT', 'PONTIAC',
'PORSCHE', 'RAM', 'RENAULT', 'SEAT', 'SMART', 'SUBARU',
'SUZUKI', 'TOYOTA', 'VOLKSWAGEN', 'VOLVO'
]
# Cross-reference brands
XREF_BRANDS = ['AC DELCO', 'FRAM', 'INTERFIL', 'MANN', 'MOTORCRAFT']
def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def ensure_manufacturer(cursor, name, type_='aftermarket', quality='standard', country=None):
"""Create manufacturer if not exists"""
cursor.execute("SELECT id FROM manufacturers WHERE UPPER(name) = UPPER(?)", (name,))
row = cursor.fetchone()
if row:
return row['id']
cursor.execute("""
INSERT INTO manufacturers (name, type, quality_tier, country)
VALUES (?, ?, ?, ?)
""", (name, type_, quality, country))
return cursor.lastrowid
def ensure_brand(cursor, name):
"""Create brand if not exists"""
cursor.execute("SELECT id FROM brands WHERE UPPER(name) = UPPER(?)", (name,))
row = cursor.fetchone()
if row:
return row['id']
cursor.execute("INSERT INTO brands (name) VALUES (?)", (name,))
return cursor.lastrowid
def ensure_model(cursor, brand_id, name):
"""Create model if not exists"""
cursor.execute("""
SELECT id FROM models
WHERE brand_id = ? AND UPPER(name) = UPPER(?)
""", (brand_id, name))
row = cursor.fetchone()
if row:
return row['id']
cursor.execute("INSERT INTO models (brand_id, name) VALUES (?, ?)", (brand_id, name))
return cursor.lastrowid
def ensure_year(cursor, year):
"""Create year if not exists"""
cursor.execute("SELECT id FROM years WHERE year = ?", (year,))
row = cursor.fetchone()
if row:
return row['id']
cursor.execute("INSERT INTO years (year) VALUES (?)", (year,))
return cursor.lastrowid
def ensure_engine(cursor, name):
"""Create engine if not exists"""
cursor.execute("SELECT id FROM engines WHERE name = ?", (name,))
row = cursor.fetchone()
if row:
return row['id']
# Parse engine details from name
displacement = None
cylinders = None
fuel_type = 'gasoline' # lowercase to match DB constraint
# Parse displacement and cylinders from patterns like "L4-2.0L" or "V6-3.5L"
match = re.match(r'([LV])(\d+)[-]?([\d.]+)L?', name)
if match:
engine_type = match.group(1) # L or V
cylinders = int(match.group(2))
displacement = int(float(match.group(3)) * 1000)
if 'DIESEL' in name.upper() or 'DIÉSEL' in name.upper():
fuel_type = 'diesel'
elif 'ELECTRIC' in name.upper():
fuel_type = 'electric'
elif 'HYBRID' in name.upper():
fuel_type = 'hybrid'
# Note: 'TURBO' is not a fuel type, it's a modifier - default to gasoline
cursor.execute("""
INSERT INTO engines (name, displacement_cc, cylinders, fuel_type)
VALUES (?, ?, ?, ?)
""", (name, displacement, cylinders, fuel_type))
return cursor.lastrowid
def ensure_mye(cursor, model_id, year_id, engine_id):
"""Create model_year_engine if not exists"""
cursor.execute("""
SELECT id FROM model_year_engine
WHERE model_id = ? AND year_id = ? AND engine_id = ?
""", (model_id, year_id, engine_id))
row = cursor.fetchone()
if row:
return row['id']
cursor.execute("""
INSERT INTO model_year_engine (model_id, year_id, engine_id)
VALUES (?, ?, ?)
""", (model_id, year_id, engine_id))
return cursor.lastrowid
def get_or_create_group(cursor, category_name, group_name):
"""Get or create part group"""
cursor.execute("SELECT id FROM part_categories WHERE name = ?", (category_name,))
cat_row = cursor.fetchone()
if not cat_row:
return None
category_id = cat_row['id']
cursor.execute("""
SELECT id FROM part_groups WHERE category_id = ? AND name = ?
""", (category_id, group_name))
group_row = cursor.fetchone()
if group_row:
return group_row['id']
cursor.execute("""
INSERT INTO part_groups (category_id, name, name_es)
VALUES (?, ?, ?)
""", (category_id, group_name, group_name))
return cursor.lastrowid
def parse_year_range(year_str):
"""Parse year range into list of years"""
year_str = year_str.strip()
if re.match(r'^\d{4}$', year_str):
return [int(year_str)]
match = re.match(r'(\d{4})\s*[-]\s*(\d{4})', year_str)
if match:
start, end = int(match.group(1)), int(match.group(2))
if start > end:
start, end = end, start
return list(range(end, start + 1))
return []
def classify_filter(part_number):
"""Classify filter type by part number prefix"""
part_number = part_number.upper()
if part_number.startswith('GP-') or part_number.startswith('GPS-'):
if part_number.startswith('GPS-'):
return 'SINTÉTICO'
return 'ACEITE'
elif part_number.startswith('GA-') and not part_number.startswith('GAC-') and not part_number.startswith('GAVW-'):
return 'AIRE'
elif part_number.startswith('GG-'):
return 'COMB.'
elif part_number.startswith('GAC-'):
return 'CABINA'
elif part_number.startswith('G-'):
return 'ACEITE' # Generic oil filter
return None
def extract_vehicle_entries(pdf):
"""Extract all vehicle entries from catalog"""
entries = []
current_brand = None
current_model = None
for page in pdf.pages:
text = page.extract_text()
if not text:
continue
for line in text.split('\n'):
line = line.strip()
if not line:
continue
# Skip header/footer
if 'AÑO' in line and 'MOTOR' in line:
continue
if 'Los filtros Gonher' in line:
continue
# Brand detection
if line in CATALOG_BRANDS:
current_brand = line
current_model = None
continue
# Handle (Continúa) lines
if '(Continúa)' in line:
match = re.match(r'^([A-Z][A-Z0-9\s\-]+)\s*\(Continúa\)', line)
if match:
potential = match.group(1).strip()
if potential in CATALOG_BRANDS:
current_brand = potential
elif current_brand:
current_model = potential
continue
# Model detection
if current_brand:
if re.match(r'^[A-Z][A-Z0-9\s\-/]+$', line) and not re.match(r'^\d{4}', line):
if line not in ['ACEITE', 'AIRE', 'COMB', 'CABINA', 'SINTÉTICO', 'AÑO', 'MOTOR']:
if not re.match(r'^G[APCS]?[-]?\d', line): # Not a part number
current_model = line
continue
# Data line with year
if current_brand and current_model:
match = re.match(r'^(\d{4}(?:\s*[-]\s*\d{4})?)\s+(.+)$', line)
if match:
year_str = match.group(1)
rest = match.group(2)
# Extract motor
motor_match = re.match(r'^([LV]\d+[-][\d.]+L(?:\s+(?:Turbo|TURBOCHARGED|diésel|ELECTRIC|HYBRID))?)\s*(.*)$', rest, re.IGNORECASE)
if motor_match:
motor = motor_match.group(1).strip()
filters_str = motor_match.group(2).strip()
# Parse filter part numbers
filter_parts = re.findall(r'G[A-Z]*[-]?[\dA-Z]+(?:\(\d+\))?', filters_str)
years = parse_year_range(year_str)
if years:
for year in years:
entry = {
'brand': current_brand,
'model': current_model,
'year': year,
'motor': motor,
'filters': {}
}
for fp in filter_parts:
fp_clean = re.sub(r'\(\d+\)', '', fp)
filter_type = classify_filter(fp_clean)
if filter_type:
entry['filters'][filter_type] = fp_clean
if entry['filters']:
entries.append(entry)
return entries
def extract_cross_references(pdf):
"""Extract cross-reference data from catalog"""
xrefs = []
current_brand = None
# Cross-references are typically in pages 117+
for i in range(117, len(pdf.pages)):
text = pdf.pages[i].extract_text()
if not text:
continue
for line in text.split('\n'):
line = line.strip()
# Brand header
if line in XREF_BRANDS:
current_brand = line
continue
# Cross-reference line
if current_brand:
match = re.match(r'^([A-Z0-9\-/]+)\s+(G[A-Z]*[-]?\d+[A-Z]*)$', line)
if match:
xrefs.append({
'brand': current_brand,
'part_number': match.group(1),
'gonher_part': match.group(2)
})
return xrefs
def main():
print("=" * 70)
print("IMPORTADOR COMPLETO - CATÁLOGO GONHER 2022")
print("=" * 70)
# Read PDF
print(f"\n[1/7] Leyendo PDF: {PDF_PATH}")
pdf = pypdf.PdfReader(PDF_PATH)
print(f" Total páginas: {len(pdf.pages)}")
# Extract data
print("\n[2/7] Extrayendo datos del catálogo...")
vehicle_entries = extract_vehicle_entries(pdf)
cross_refs = extract_cross_references(pdf)
print(f" Entradas de vehículos: {len(vehicle_entries)}")
print(f" Referencias cruzadas: {len(cross_refs)}")
# Get unique filters
unique_filters = {}
for entry in vehicle_entries:
for filter_type, part_num in entry['filters'].items():
if part_num not in unique_filters:
unique_filters[part_num] = filter_type
print(f" Filtros únicos: {len(unique_filters)}")
# Connect to database
conn = get_db()
cursor = conn.cursor()
# Create manufacturers
print("\n[3/7] Creando fabricantes...")
manufacturers = {
'Gonher': ensure_manufacturer(cursor, 'Gonher', 'aftermarket', 'standard', 'Mexico'),
'AC Delco': ensure_manufacturer(cursor, 'AC Delco', 'oem', 'oem', 'USA'),
'Fram': ensure_manufacturer(cursor, 'Fram', 'aftermarket', 'standard', 'USA'),
'Interfil': ensure_manufacturer(cursor, 'Interfil', 'aftermarket', 'economy', 'Mexico'),
'Mann': ensure_manufacturer(cursor, 'Mann', 'aftermarket', 'premium', 'Germany'),
'Motorcraft': ensure_manufacturer(cursor, 'Motorcraft', 'oem', 'oem', 'USA'),
}
print(f" Fabricantes: {list(manufacturers.keys())}")
# Create vehicles
print("\n[4/7] Creando vehículos faltantes...")
vehicles_created = 0
mye_cache = {}
for entry in vehicle_entries:
cache_key = (entry['brand'], entry['model'], entry['year'], entry['motor'])
if cache_key in mye_cache:
continue
# Check if vehicle exists
cursor.execute("""
SELECT mye.id FROM model_year_engine mye
JOIN models m ON mye.model_id = m.id
JOIN brands b ON m.brand_id = b.id
JOIN years y ON mye.year_id = y.id
JOIN engines e ON mye.engine_id = e.id
WHERE UPPER(b.name) = UPPER(?) AND UPPER(m.name) = UPPER(?)
AND y.year = ? AND e.name = ?
LIMIT 1
""", (entry['brand'], entry['model'], entry['year'], entry['motor']))
existing = cursor.fetchone()
if existing:
mye_cache[cache_key] = existing['id']
else:
# Create vehicle
brand_id = ensure_brand(cursor, entry['brand'])
model_id = ensure_model(cursor, brand_id, entry['model'])
year_id = ensure_year(cursor, entry['year'])
engine_id = ensure_engine(cursor, entry['motor'])
mye_id = ensure_mye(cursor, model_id, year_id, engine_id)
mye_cache[cache_key] = mye_id
vehicles_created += 1
print(f" Vehículos creados: {vehicles_created}")
# Create filter parts
print("\n[5/7] Creando partes de filtros...")
filter_parts = {}
parts_created = 0
for part_num, filter_type in unique_filters.items():
config = FILTER_TYPES.get(filter_type)
if not config:
continue
group_id = get_or_create_group(cursor, config['category'], config['group'])
if not group_id:
continue
# Check if part exists
cursor.execute("SELECT id FROM parts WHERE oem_part_number = ?", (part_num,))
existing = cursor.fetchone()
if existing:
filter_parts[part_num] = existing['id']
else:
name = f"{config['prefix']} {part_num}"
name_es = f"{config['prefix_es']} {part_num}"
cursor.execute("""
INSERT INTO parts (oem_part_number, name, name_es, group_id, description)
VALUES (?, ?, ?, ?, ?)
""", (part_num, name, name_es, group_id, f"Gonher {config['prefix']}"))
filter_parts[part_num] = cursor.lastrowid
parts_created += 1
print(f" Partes creadas: {parts_created}")
# Create fitments
print("\n[6/7] Creando fitments (vehículo-parte)...")
fitments_created = 0
for entry in vehicle_entries:
cache_key = (entry['brand'], entry['model'], entry['year'], entry['motor'])
mye_id = mye_cache.get(cache_key)
if not mye_id:
continue
for filter_type, part_num in entry['filters'].items():
part_id = filter_parts.get(part_num)
if not part_id:
continue
# Check if fitment exists
cursor.execute("""
SELECT id FROM vehicle_parts
WHERE model_year_engine_id = ? AND part_id = ?
""", (mye_id, part_id))
if not cursor.fetchone():
cursor.execute("""
INSERT INTO vehicle_parts (model_year_engine_id, part_id, quantity_required, fitment_notes)
VALUES (?, ?, 1, ?)
""", (mye_id, part_id, f"Catálogo Gonher 2022 - {filter_type}"))
fitments_created += 1
print(f" Fitments creados: {fitments_created}")
# Create cross-references
print("\n[7/7] Creando referencias cruzadas...")
xrefs_created = 0
for xref in cross_refs:
gonher_part_id = filter_parts.get(xref['gonher_part'])
if not gonher_part_id:
# Part might not exist yet, try to find by OEM number
cursor.execute("SELECT id FROM parts WHERE oem_part_number = ?", (xref['gonher_part'],))
row = cursor.fetchone()
if row:
gonher_part_id = row['id']
else:
continue
# Check if cross-reference exists
cursor.execute("""
SELECT id FROM part_cross_references
WHERE part_id = ? AND cross_reference_number = ?
""", (gonher_part_id, xref['part_number']))
if not cursor.fetchone():
# Map brand to reference type
ref_type = 'interchange'
if xref['brand'] in ['AC DELCO', 'MOTORCRAFT']:
ref_type = 'oem_alternate'
cursor.execute("""
INSERT INTO part_cross_references (part_id, cross_reference_number, reference_type)
VALUES (?, ?, ?)
""", (gonher_part_id, xref['part_number'], ref_type))
xrefs_created += 1
print(f" Referencias cruzadas creadas: {xrefs_created}")
# Commit
conn.commit()
conn.close()
print("\n" + "=" * 70)
print("IMPORTACIÓN COMPLETADA")
print("=" * 70)
print(f"""
RESUMEN:
- Vehículos creados: {vehicles_created:,}
- Partes creadas: {parts_created:,}
- Fitments creados: {fitments_created:,}
- Cross-refs creadas: {xrefs_created:,}
- Fabricantes: {len(manufacturers)}
""")
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,315 @@
#!/usr/bin/env python3
"""
Script para poblar la base de datos con datos de demo realistas.
Incluye partes OEM, fabricantes aftermarket, partes alternativas y fitments.
"""
import sqlite3
from pathlib import Path
DB_PATH = Path(__file__).parent.parent / 'vehicle_database.db'
# Demo Parts Data - Partes realistas comunes
DEMO_PARTS = [
# Brake System (category 2, group varies)
{'oem': '04465-33450', 'name': 'Front Brake Pad Set', 'name_es': 'Juego de Pastillas Delanteras', 'group': 'Brake Pads/Shoes', 'desc': 'Ceramic front brake pads', 'weight': 1.2},
{'oem': '04466-33180', 'name': 'Rear Brake Pad Set', 'name_es': 'Juego de Pastillas Traseras', 'group': 'Brake Pads/Shoes', 'desc': 'Ceramic rear brake pads', 'weight': 0.9},
{'oem': '43512-33130', 'name': 'Front Brake Rotor', 'name_es': 'Disco de Freno Delantero', 'group': 'Brake Rotors', 'desc': 'Vented front brake disc', 'weight': 8.5},
{'oem': '42431-33130', 'name': 'Rear Brake Rotor', 'name_es': 'Disco de Freno Trasero', 'group': 'Brake Rotors', 'desc': 'Solid rear brake disc', 'weight': 5.2},
{'oem': '47750-33210', 'name': 'Brake Caliper Front Right', 'name_es': 'Caliper Delantero Derecho', 'group': 'Brake Calipers', 'desc': 'Front right brake caliper assembly', 'weight': 4.5},
{'oem': '47730-33210', 'name': 'Brake Caliper Front Left', 'name_es': 'Caliper Delantero Izquierdo', 'group': 'Brake Calipers', 'desc': 'Front left brake caliper assembly', 'weight': 4.5},
# Engine (category 6)
{'oem': '90915-YZZD4', 'name': 'Oil Filter', 'name_es': 'Filtro de Aceite', 'group': 'Oil Filters', 'desc': 'Spin-on oil filter element', 'weight': 0.3},
{'oem': '17801-0P010', 'name': 'Air Filter', 'name_es': 'Filtro de Aire', 'group': 'Air Filters', 'desc': 'Engine air filter element', 'weight': 0.25},
{'oem': '23300-79525', 'name': 'Fuel Filter', 'name_es': 'Filtro de Combustible', 'group': 'Fuel Filters', 'desc': 'In-line fuel filter', 'weight': 0.4},
{'oem': '90048-51003', 'name': 'Spark Plug Iridium', 'name_es': 'Bujia de Iridio', 'group': 'Spark Plugs', 'desc': 'Long-life iridium spark plug', 'weight': 0.05},
{'oem': '13568-09130', 'name': 'Timing Belt', 'name_es': 'Banda de Tiempo', 'group': 'Timing Components', 'desc': 'Engine timing belt', 'weight': 0.35},
{'oem': 'SU003-02574', 'name': 'Serpentine Belt', 'name_es': 'Banda Serpentina', 'group': 'Belts', 'desc': 'Multi-rib accessory drive belt', 'weight': 0.2},
{'oem': '16100-09515', 'name': 'Water Pump', 'name_es': 'Bomba de Agua', 'group': 'Water Pumps', 'desc': 'Engine coolant water pump', 'weight': 2.1},
{'oem': '16363-0P030', 'name': 'Thermostat', 'name_es': 'Termostato', 'group': 'Thermostats', 'desc': 'Engine thermostat with housing', 'weight': 0.3},
# Suspension (category 11)
{'oem': '48157-42010', 'name': 'Front Strut Mount', 'name_es': 'Soporte de Amortiguador Delantero', 'group': 'Strut Mounts', 'desc': 'Front suspension strut mount', 'weight': 0.8},
{'oem': '48609-48020', 'name': 'Front Strut Assembly', 'name_es': 'Amortiguador Delantero Completo', 'group': 'Struts/Shocks', 'desc': 'Complete front strut assembly', 'weight': 6.5},
{'oem': '48530-09S00', 'name': 'Rear Shock Absorber', 'name_es': 'Amortiguador Trasero', 'group': 'Struts/Shocks', 'desc': 'Rear shock absorber', 'weight': 3.2},
{'oem': '48068-33070', 'name': 'Front Lower Control Arm', 'name_es': 'Brazo de Control Inferior Delantero', 'group': 'Control Arms', 'desc': 'Front lower control arm with bushing', 'weight': 4.8},
{'oem': '48725-33050', 'name': 'Rear Trailing Arm', 'name_es': 'Brazo Trasero', 'group': 'Control Arms', 'desc': 'Rear suspension trailing arm', 'weight': 3.5},
{'oem': '48815-33070', 'name': 'Sway Bar Link Front', 'name_es': 'Enlace de Barra Estabilizadora', 'group': 'Sway Bar Links', 'desc': 'Front stabilizer bar end link', 'weight': 0.4},
# Steering (category 10)
{'oem': '45046-39335', 'name': 'Outer Tie Rod End', 'name_es': 'Terminal Exterior de Direccion', 'group': 'Tie Rods', 'desc': 'Outer steering tie rod end', 'weight': 0.6},
{'oem': '45503-39165', 'name': 'Inner Tie Rod', 'name_es': 'Terminal Interior de Direccion', 'group': 'Tie Rods', 'desc': 'Inner steering tie rod', 'weight': 0.5},
{'oem': '44200-33480', 'name': 'Power Steering Pump', 'name_es': 'Bomba de Direccion', 'group': 'Power Steering', 'desc': 'Hydraulic power steering pump', 'weight': 3.8},
{'oem': '45510-33310', 'name': 'Steering Rack', 'name_es': 'Cremallera de Direccion', 'group': 'Steering Racks', 'desc': 'Power steering rack and pinion', 'weight': 12.5},
# Electrical (category 5)
{'oem': '28100-21030', 'name': 'Starter Motor', 'name_es': 'Motor de Arranque', 'group': 'Starters', 'desc': 'Engine starter motor', 'weight': 4.2},
{'oem': '27060-37030', 'name': 'Alternator', 'name_es': 'Alternador', 'group': 'Alternators', 'desc': '100 amp alternator', 'weight': 5.8},
{'oem': '90919-02240', 'name': 'Ignition Coil', 'name_es': 'Bobina de Ignicion', 'group': 'Ignition Coils', 'desc': 'Direct ignition coil', 'weight': 0.35},
{'oem': '89467-33040', 'name': 'Oxygen Sensor', 'name_es': 'Sensor de Oxigeno', 'group': 'Sensors', 'desc': 'Upstream O2 sensor', 'weight': 0.15},
# Cooling System (category 3)
{'oem': '16400-0W010', 'name': 'Radiator', 'name_es': 'Radiador', 'group': 'Radiators', 'desc': 'Engine cooling radiator', 'weight': 8.5},
{'oem': '16711-31310', 'name': 'Radiator Fan', 'name_es': 'Ventilador de Radiador', 'group': 'Cooling Fans', 'desc': 'Electric radiator cooling fan', 'weight': 3.2},
{'oem': '16361-0P030', 'name': 'Radiator Hose Upper', 'name_es': 'Manguera Superior de Radiador', 'group': 'Hoses', 'desc': 'Upper radiator coolant hose', 'weight': 0.4},
# Exhaust (category 7)
{'oem': '17140-31620', 'name': 'Catalytic Converter', 'name_es': 'Convertidor Catalitico', 'group': 'Catalytic Converters', 'desc': 'Three-way catalytic converter', 'weight': 6.5},
{'oem': '17430-31410', 'name': 'Muffler', 'name_es': 'Silenciador', 'group': 'Mufflers', 'desc': 'Rear exhaust muffler', 'weight': 8.2},
# Fuel System (category 8)
{'oem': '23220-31100', 'name': 'Fuel Pump', 'name_es': 'Bomba de Combustible', 'group': 'Fuel Pumps', 'desc': 'Electric in-tank fuel pump', 'weight': 0.8},
{'oem': '23250-31010', 'name': 'Fuel Injector', 'name_es': 'Inyector de Combustible', 'group': 'Fuel Injectors', 'desc': 'Multi-port fuel injector', 'weight': 0.1},
# Drivetrain (category 4)
{'oem': '31470-52011', 'name': 'Clutch Kit', 'name_es': 'Kit de Embrague', 'group': 'Clutches', 'desc': 'Complete clutch replacement kit', 'weight': 7.5},
{'oem': '43502-35210', 'name': 'CV Axle Front', 'name_es': 'Flecha Homocinética Delantera', 'group': 'CV Axles', 'desc': 'Front CV axle shaft assembly', 'weight': 5.2},
# HVAC (category 9)
{'oem': '88310-33250', 'name': 'AC Compressor', 'name_es': 'Compresor de AC', 'group': 'AC Compressors', 'desc': 'Air conditioning compressor', 'weight': 6.8},
{'oem': '87103-33110', 'name': 'Blower Motor', 'name_es': 'Motor de Ventilador', 'group': 'Blower Motors', 'desc': 'HVAC blower motor', 'weight': 1.5},
{'oem': '87139-YZZ05', 'name': 'Cabin Air Filter', 'name_es': 'Filtro de Cabina', 'group': 'Cabin Filters', 'desc': 'Cabin air filter element', 'weight': 0.15},
]
# Demo Manufacturers
DEMO_MANUFACTURERS = [
{'name': 'Brembo', 'type': 'aftermarket', 'tier': 'premium', 'country': 'Italy'},
{'name': 'Bosch', 'type': 'aftermarket', 'tier': 'premium', 'country': 'Germany'},
{'name': 'Denso', 'type': 'aftermarket', 'tier': 'premium', 'country': 'Japan'},
{'name': 'ACDelco', 'type': 'aftermarket', 'tier': 'standard', 'country': 'USA'},
{'name': 'Raybestos', 'type': 'aftermarket', 'tier': 'standard', 'country': 'USA'},
{'name': 'Wagner', 'type': 'aftermarket', 'tier': 'standard', 'country': 'USA'},
{'name': 'Monroe', 'type': 'aftermarket', 'tier': 'standard', 'country': 'USA'},
{'name': 'KYB', 'type': 'aftermarket', 'tier': 'premium', 'country': 'Japan'},
{'name': 'NGK', 'type': 'aftermarket', 'tier': 'premium', 'country': 'Japan'},
{'name': 'Aisin', 'type': 'aftermarket', 'tier': 'premium', 'country': 'Japan'},
{'name': 'Gates', 'type': 'aftermarket', 'tier': 'premium', 'country': 'USA'},
{'name': 'Continental', 'type': 'aftermarket', 'tier': 'premium', 'country': 'Germany'},
{'name': 'Moog', 'type': 'aftermarket', 'tier': 'standard', 'country': 'USA'},
{'name': 'Dorman', 'type': 'aftermarket', 'tier': 'economy', 'country': 'USA'},
{'name': 'Centric', 'type': 'aftermarket', 'tier': 'standard', 'country': 'USA'},
{'name': 'Beck/Arnley', 'type': 'aftermarket', 'tier': 'standard', 'country': 'USA'},
{'name': 'Cardone', 'type': 'remanufactured', 'tier': 'standard', 'country': 'USA'},
{'name': 'Standard Motor', 'type': 'aftermarket', 'tier': 'standard', 'country': 'USA'},
{'name': 'Spectra Premium', 'type': 'aftermarket', 'tier': 'economy', 'country': 'Canada'},
{'name': 'TRW', 'type': 'aftermarket', 'tier': 'premium', 'country': 'Germany'},
]
def get_group_id(cursor, group_name):
"""Get or create a group by name"""
cursor.execute("SELECT id FROM part_groups WHERE name = ?", (group_name,))
row = cursor.fetchone()
if row:
return row[0]
return None
def populate_parts(cursor):
"""Populate OEM parts"""
print("Adding OEM parts...")
added = 0
for part in DEMO_PARTS:
group_id = get_group_id(cursor, part['group'])
if not group_id:
print(f" Warning: Group '{part['group']}' not found, skipping {part['oem']}")
continue
# Check if part already exists
cursor.execute("SELECT id FROM parts WHERE oem_part_number = ?", (part['oem'],))
if cursor.fetchone():
continue
cursor.execute("""
INSERT INTO parts (oem_part_number, name, name_es, group_id, description, weight_kg)
VALUES (?, ?, ?, ?, ?, ?)
""", (part['oem'], part['name'], part['name_es'], group_id, part['desc'], part['weight']))
added += 1
print(f" Added {added} OEM parts")
return added
def populate_manufacturers(cursor):
"""Populate manufacturers"""
print("Adding manufacturers...")
added = 0
for mfr in DEMO_MANUFACTURERS:
cursor.execute("SELECT id FROM manufacturers WHERE name = ?", (mfr['name'],))
if cursor.fetchone():
continue
cursor.execute("""
INSERT INTO manufacturers (name, type, quality_tier, country)
VALUES (?, ?, ?, ?)
""", (mfr['name'], mfr['type'], mfr['tier'], mfr['country']))
added += 1
print(f" Added {added} manufacturers")
return added
def populate_aftermarket(cursor):
"""Populate aftermarket parts linked to OEM"""
print("Adding aftermarket alternatives...")
# Get all parts and manufacturers
cursor.execute("SELECT id, oem_part_number, name FROM parts")
parts = cursor.fetchall()
cursor.execute("SELECT id, name, quality_tier FROM manufacturers WHERE type != 'oem'")
manufacturers = cursor.fetchall()
added = 0
import random
for part in parts:
part_id, oem_num, part_name = part
# Add 2-4 aftermarket alternatives per OEM part
num_alternatives = random.randint(2, 4)
selected_mfrs = random.sample(manufacturers, min(num_alternatives, len(manufacturers)))
for mfr in selected_mfrs:
mfr_id, mfr_name, tier = mfr
# Check if already exists
cursor.execute("""
SELECT id FROM aftermarket_parts WHERE oem_part_id = ? AND manufacturer_id = ?
""", (part_id, mfr_id))
if cursor.fetchone():
continue
# Generate part number
prefix = mfr_name[:3].upper()
part_num = f"{prefix}-{oem_num.replace('-', '')[:6]}"
# Set price based on tier
base_price = random.uniform(20, 200)
if tier == 'premium':
price = base_price * 1.5
warranty = 36
elif tier == 'economy':
price = base_price * 0.6
warranty = 12
else:
price = base_price
warranty = 24
cursor.execute("""
INSERT INTO aftermarket_parts (oem_part_id, manufacturer_id, part_number, name, quality_tier, price_usd, warranty_months)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (part_id, mfr_id, part_num, part_name, tier, round(price, 2), warranty))
added += 1
print(f" Added {added} aftermarket parts")
return added
def populate_fitments(cursor):
"""Create fitments linking parts to vehicles"""
print("Adding fitments (linking parts to vehicles)...")
# Get some vehicle configurations (first 50)
cursor.execute("""
SELECT mye.id, b.name as brand, m.name as model, y.year
FROM model_year_engine mye
JOIN models m ON mye.model_id = m.id
JOIN brands b ON m.brand_id = b.id
JOIN years y ON mye.year_id = y.id
WHERE b.name IN ('TOYOTA', 'HONDA', 'FORD', 'CHEVROLET', 'NISSAN')
ORDER BY RANDOM()
LIMIT 100
""")
vehicles = cursor.fetchall()
# Get all parts
cursor.execute("SELECT id, name FROM parts")
parts = cursor.fetchall()
added = 0
import random
for vehicle in vehicles:
mye_id = vehicle[0]
# Assign 10-20 random parts to each vehicle
num_parts = random.randint(10, 20)
selected_parts = random.sample(parts, min(num_parts, len(parts)))
for part in selected_parts:
part_id = part[0]
# Check if fitment already exists
cursor.execute("""
SELECT id FROM vehicle_parts WHERE model_year_engine_id = ? AND part_id = ?
""", (mye_id, part_id))
if cursor.fetchone():
continue
# Determine position based on part name
part_name = part[1].lower()
position = None
quantity = 1
if 'front' in part_name:
position = 'front'
elif 'rear' in part_name:
position = 'rear'
elif 'left' in part_name:
position = 'left'
elif 'right' in part_name:
position = 'right'
# Some parts need multiples
if 'spark plug' in part_name or 'injector' in part_name:
quantity = 4
elif 'pad' in part_name or 'rotor' in part_name:
quantity = 2 if position else 1
cursor.execute("""
INSERT INTO vehicle_parts (model_year_engine_id, part_id, quantity_required, position)
VALUES (?, ?, ?, ?)
""", (mye_id, part_id, quantity, position))
added += 1
print(f" Added {added} fitments")
return added
def main():
print("=" * 50)
print("Populating Demo Data")
print("=" * 50)
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
try:
populate_parts(cursor)
populate_manufacturers(cursor)
populate_aftermarket(cursor)
populate_fitments(cursor)
conn.commit()
print("\nDemo data populated successfully!")
# Show final counts
print("\n=== Final Counts ===")
for table in ['parts', 'manufacturers', 'aftermarket_parts', 'vehicle_parts']:
cursor.execute(f"SELECT COUNT(*) FROM {table}")
count = cursor.fetchone()[0]
print(f" {table}: {count}")
except Exception as e:
conn.rollback()
print(f"Error: {e}")
raise
finally:
conn.close()
if __name__ == '__main__':
main()

Binary file not shown.