From 2c6b6e0160cce728b37e9e02a59e82acc1134aed Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Thu, 19 Feb 2026 05:52:12 +0000 Subject: [PATCH] fix(console): fix PostgreSQL connection + add Metabase Actions guide - Fix console/main.py: import DB_URL instead of missing DB_PATH - Add sqlalchemy text() import for connection test - Replace file-exists check with actual PostgreSQL connection test - Mask password in startup banner - Add docs/METABASE_ACTIONS.md: complete guide for data entry via Metabase Actions (models, forms, dashboard layout, workflows) Co-Authored-By: Claude Opus 4.6 --- console/main.py | 62 +++-- docs/METABASE_ACTIONS.md | 473 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 513 insertions(+), 22 deletions(-) create mode 100644 docs/METABASE_ACTIONS.md diff --git a/console/main.py b/console/main.py index eb2b58f..00e6dc9 100644 --- a/console/main.py +++ b/console/main.py @@ -8,10 +8,11 @@ Usage: """ import argparse -import os import sys -from console.config import VERSION, APP_NAME, APP_SUBTITLE, DB_PATH +from sqlalchemy import text + +from console.config import VERSION, APP_NAME, APP_SUBTITLE, DB_URL def parse_args(argv=None): @@ -27,20 +28,29 @@ def parse_args(argv=None): ) parser.add_argument( "--db", - default=DB_PATH, - help="Path to the vehicle database (default: auto-detected)", + default=DB_URL, + help="PostgreSQL connection URL (default: from config)", ) return parser.parse_args(argv) -def _print_banner(db_path): +def _print_banner(db_url): """Print a startup banner before entering terminal mode.""" + # Mask password in display + display_url = db_url + if '@' in db_url: + pre_at = db_url.split('@')[0] + post_at = db_url.split('@', 1)[1] + if ':' in pre_at.split('//')[-1]: + user = pre_at.split('//')[-1].split(':')[0] + display_url = f"postgresql://{user}:****@{post_at}" + border = "=" * 58 print(border) print(f" {APP_NAME} v{VERSION}") print(f" {APP_SUBTITLE}") print(border) - print(f" DB : {db_path}") + print(f" DB : {display_url}") print(border) print() @@ -49,20 +59,7 @@ def main(argv=None): """Main entry point: parse args, set up renderer, DB, and launch the app.""" args = parse_args(argv) - db_path = args.db - - # Verify the database file exists before proceeding - if not os.path.isfile(db_path): - print( - f"Error: Database not found at '{db_path}'.\n" - f"\n" - f"Make sure the vehicle database exists. You can specify a\n" - f"custom path with the --db flag:\n" - f"\n" - f" python -m console --db /path/to/vehicle_database.db\n", - file=sys.stderr, - ) - sys.exit(1) + db_url = args.db # Lazy imports so the module can be loaded without curses available # (e.g. during tests or when just checking --version). @@ -71,9 +68,30 @@ def main(argv=None): from console.core.app import App # Print startup banner - _print_banner(db_path) + _print_banner(db_url) + + # Test database connection before entering curses mode + db = Database(db_url) + try: + db._get_engine() + session = db._session() + session.execute(text("SELECT 1")) + session.close() + except Exception as e: + print( + f"Error: Cannot connect to database.\n" + f"\n" + f" URL: {db_url}\n" + f" Error: {e}\n" + f"\n" + f"Make sure PostgreSQL is running and the connection URL is correct.\n" + f"You can specify a custom URL with the --db flag:\n" + f"\n" + f" python -m console --db postgresql://user:pass@host/dbname\n", + file=sys.stderr, + ) + sys.exit(1) - db = Database(db_path) renderer = CursesRenderer() app = App(renderer=renderer, db=db) diff --git a/docs/METABASE_ACTIONS.md b/docs/METABASE_ACTIONS.md new file mode 100644 index 0000000..d7aa223 --- /dev/null +++ b/docs/METABASE_ACTIONS.md @@ -0,0 +1,473 @@ +# Metabase Actions — Alta de Piezas e Intercambios + +## Requisitos +- Metabase v0.44+ (Open Source o Pro) +- Actions habilitadas en Admin → Settings +- Database con "Model actions" activado + +--- + +## 1. Configuración Inicial + +### 1.1 Habilitar Actions +1. Ir a **Admin** → **Settings** +2. Buscar **"Enable Actions"** → Activar +3. Ir a **Admin** → **Databases** → Click en `nexus_autoparts` +4. Activar **"Model actions"** + +### 1.2 Crear Modelos Base + +En Metabase, un **Modelo** es una pregunta (query) guardada como tabla virtual. +Los Actions se vinculan a Modelos. + +**Modelo 1: Piezas OEM** → New → SQL Query: +```sql +SELECT + p.id_part, + p.oem_part_number, + p.name_part, + p.name_es, + pg.name_part_group AS grupo, + pc.name_part_category AS categoria, + p.description, + p.description_es, + p.weight_kg, + mat.name_material AS material +FROM parts p +JOIN part_groups pg ON p.group_id = pg.id_part_group +JOIN part_categories pc ON pg.category_id = pc.id_part_category +LEFT JOIN materials mat ON p.id_material = mat.id_material +ORDER BY p.oem_part_number +``` +Guardar → Click ⋯ → **Turn into a model** → Nombrar: `Piezas OEM` + +**Modelo 2: Fitments** → New → SQL Query: +```sql +SELECT + vp.id_vehicle_part, + b.name_brand AS marca, + m.name_model AS modelo, + y.year_car AS año, + e.name_engine AS motor, + p.oem_part_number, + p.name_part AS pieza, + vp.quantity_required AS cantidad, + pp.name_position_part AS posicion, + vp.fitment_notes AS notas +FROM vehicle_parts vp +JOIN model_year_engine mye ON vp.model_year_engine_id = mye.id_mye +JOIN models m ON mye.model_id = m.id_model +JOIN brands b ON m.brand_id = b.id_brand +JOIN years y ON mye.year_id = y.id_year +JOIN engines e ON mye.engine_id = e.id_engine +JOIN parts p ON vp.part_id = p.id_part +LEFT JOIN position_part pp ON vp.id_position_part = pp.id_position_part +ORDER BY b.name_brand, m.name_model, y.year_car +``` +Guardar → **Turn into a model** → Nombrar: `Fitments` + +**Modelo 3: Aftermarket** → New → SQL Query: +```sql +SELECT + ap.id_aftermarket_parts, + p.oem_part_number AS oem_ref, + p.name_part AS pieza_oem, + mfr.name_manufacture AS fabricante, + ap.part_number AS numero_aftermarket, + ap.name_aftermarket_parts AS nombre, + ap.name_es AS nombre_es, + qt.name_quality AS calidad, + ap.price_usd AS precio_usd, + ap.warranty_months AS garantia_meses +FROM aftermarket_parts ap +JOIN parts p ON ap.oem_part_id = p.id_part +JOIN manufacturers mfr ON ap.manufacturer_id = mfr.id_manufacture +LEFT JOIN quality_tier qt ON ap.id_quality_tier = qt.id_quality_tier +ORDER BY p.oem_part_number, mfr.name_manufacture +``` +Guardar → **Turn into a model** → Nombrar: `Aftermarket` + +**Modelo 4: Cross-References** → New → SQL Query: +```sql +SELECT + pcr.id_part_cross_ref, + p.oem_part_number AS oem_ref, + p.name_part AS pieza, + pcr.cross_reference_number AS numero_cruzado, + rt.name_ref_type AS tipo_referencia, + pcr.source_ref AS fuente, + pcr.notes AS notas +FROM part_cross_references pcr +JOIN parts p ON pcr.part_id = p.id_part +LEFT JOIN reference_type rt ON pcr.id_ref_type = rt.id_ref_type +ORDER BY p.oem_part_number, rt.name_ref_type +``` +Guardar → **Turn into a model** → Nombrar: `Cross-References` + +--- + +## 2. Crear Actions (Formularios de Alta) + +Para cada Modelo, crear Actions que permiten insertar datos. + +### Action 1: Nueva Pieza OEM + +1. Abrir el modelo **Piezas OEM** +2. Click **⋯** → **Info** → **Actions** → **New action** +3. Nombrar: `Alta de Pieza OEM` +4. Pegar este SQL: + +```sql +INSERT INTO parts ( + oem_part_number, + name_part, + name_es, + group_id, + description, + description_es, + weight_kg, + id_material +) VALUES ( + {{oem_part_number}}, + {{name_part}}, + {{name_es}}, + {{group_id}}, + {{description}}, + {{description_es}}, + {{weight_kg}}, + {{id_material}} +) +``` + +5. Configurar campos del formulario: + +| Variable | Label | Tipo | Requerido | +|----------|-------|------|-----------| +| `oem_part_number` | Número OEM | string | Si | +| `name_part` | Nombre (EN) | string | Si | +| `name_es` | Nombre (ES) | string | No | +| `group_id` | ID Grupo | number | Si | +| `description` | Descripción (EN) | string (long) | No | +| `description_es` | Descripción (ES) | string (long) | No | +| `weight_kg` | Peso (kg) | number | No | +| `id_material` | ID Material | number | No | + +6. Click **Save** + +> **Tip:** Para que el usuario no tenga que memorizar `group_id`, crea una pregunta +> auxiliar con los grupos disponibles: +> ```sql +> SELECT pg.id_part_group AS id, pg.name_part_group AS grupo, +> pc.name_part_category AS categoria +> FROM part_groups pg +> JOIN part_categories pc ON pg.category_id = pc.id_part_category +> ORDER BY pc.display_order, pg.display_order +> ``` + +--- + +### Action 2: Nuevo Fitment (vincular pieza a vehículo) + +1. Abrir el modelo **Fitments** +2. **New action** → Nombrar: `Alta de Fitment` +3. SQL: + +```sql +INSERT INTO vehicle_parts ( + model_year_engine_id, + part_id, + quantity_required, + id_position_part, + fitment_notes +) VALUES ( + {{model_year_engine_id}}, + {{part_id}}, + {{quantity_required}}, + {{id_position_part}}, + {{fitment_notes}} +) +``` + +| Variable | Label | Tipo | Requerido | Default | +|----------|-------|------|-----------|---------| +| `model_year_engine_id` | ID Vehículo (MYE) | number | Si | — | +| `part_id` | ID Pieza | number | Si | — | +| `quantity_required` | Cantidad | number | No | 1 | +| `id_position_part` | Posición (1=front, 2=rear) | number | No | — | +| `fitment_notes` | Notas | string | No | — | + +> **Tip:** Para encontrar el `model_year_engine_id`, crea esta pregunta auxiliar: +> ```sql +> SELECT mye.id_mye AS id, b.name_brand AS marca, +> m.name_model AS modelo, y.year_car AS año, e.name_engine AS motor +> FROM model_year_engine mye +> JOIN models m ON mye.model_id = m.id_model +> JOIN brands b ON m.brand_id = b.id_brand +> JOIN years y ON mye.year_id = y.id_year +> JOIN engines e ON mye.engine_id = e.id_engine +> WHERE b.name_brand ILIKE {{'%' || marca || '%'}} +> AND m.name_model ILIKE {{'%' || modelo || '%'}} +> ORDER BY b.name_brand, m.name_model, y.year_car +> ``` + +--- + +### Action 3: Fitment Masivo (una pieza → varios vehículos) + +1. En el modelo **Fitments** → **New action** +2. Nombrar: `Fitment Masivo por Marca/Modelo/Años` +3. SQL: + +```sql +INSERT INTO vehicle_parts (model_year_engine_id, part_id, quantity_required, id_position_part) +SELECT mye.id_mye, {{part_id}}, {{quantity_required}}, {{id_position_part}} +FROM model_year_engine mye +JOIN models m ON mye.model_id = m.id_model +JOIN brands b ON m.brand_id = b.id_brand +JOIN years y ON mye.year_id = y.id_year +WHERE b.name_brand ILIKE {{marca}} + AND m.name_model ILIKE {{modelo}} + AND y.year_car BETWEEN {{year_from}} AND {{year_to}} +ON CONFLICT DO NOTHING +``` + +| Variable | Label | Tipo | Requerido | +|----------|-------|------|-----------| +| `part_id` | ID Pieza | number | Si | +| `marca` | Marca (ej: TOYOTA) | string | Si | +| `modelo` | Modelo (ej: Camry) | string | Si | +| `year_from` | Año desde | number | Si | +| `year_to` | Año hasta | number | Si | +| `quantity_required` | Cantidad | number | Si | +| `id_position_part` | Posición (1=front, 2=rear, vacío=N/A) | number | No | + +--- + +### Action 4: Nueva Pieza Aftermarket + +1. En el modelo **Aftermarket** → **New action** +2. Nombrar: `Alta de Pieza Aftermarket` +3. SQL: + +```sql +INSERT INTO aftermarket_parts ( + oem_part_id, + manufacturer_id, + part_number, + name_aftermarket_parts, + name_es, + id_quality_tier, + price_usd, + warranty_months +) VALUES ( + {{oem_part_id}}, + {{manufacturer_id}}, + {{part_number}}, + {{name_aftermarket_parts}}, + {{name_es}}, + {{id_quality_tier}}, + {{price_usd}}, + {{warranty_months}} +) +``` + +| Variable | Label | Tipo | Requerido | +|----------|-------|------|-----------| +| `oem_part_id` | ID Pieza OEM | number | Si | +| `manufacturer_id` | ID Fabricante | number | Si | +| `part_number` | Número Aftermarket | string | Si | +| `name_aftermarket_parts` | Nombre (EN) | string | No | +| `name_es` | Nombre (ES) | string | No | +| `id_quality_tier` | Calidad (1=economy, 2=oem, 3=premium, 4=standard) | number | No | +| `price_usd` | Precio USD | number | No | +| `warranty_months` | Garantía (meses) | number | No | + +> **Tip:** Pregunta auxiliar para fabricantes: +> ```sql +> SELECT id_manufacture AS id, name_manufacture AS fabricante +> FROM manufacturers ORDER BY name_manufacture +> ``` + +--- + +### Action 5: Nueva Cross-Reference + +1. En el modelo **Cross-References** → **New action** +2. Nombrar: `Alta de Cross-Reference` +3. SQL: + +```sql +INSERT INTO part_cross_references ( + part_id, + cross_reference_number, + id_ref_type, + source_ref, + notes +) VALUES ( + {{part_id}}, + {{cross_reference_number}}, + {{id_ref_type}}, + {{source_ref}}, + {{notes}} +) +``` + +| Variable | Label | Tipo | Requerido | +|----------|-------|------|-----------| +| `part_id` | ID Pieza OEM | number | Si | +| `cross_reference_number` | Número de Referencia | string | Si | +| `id_ref_type` | Tipo (1=competitor, 2=interchange, 3=oem_alternate, 4=supersession) | number | No | +| `source_ref` | Fuente | string | No | +| `notes` | Notas | string | No | + +--- + +## 3. Dashboard de Carga de Datos + +Crear un **Dashboard** que agrupe todo el flujo de carga: + +1. **New** → **Dashboard** → Nombrar: `Panel de Carga de Datos` +2. Agregar estas tarjetas: + +### Fila 1: Consulta rápida +- **Buscar Pieza** (pregunta con filtro `{{oem_number}}`) +- **Buscar Vehículo** (pregunta con filtros `{{marca}}`, `{{modelo}}`) + +### Fila 2: Botones de Actions +- **+ Nueva Pieza OEM** → Action 1 +- **+ Nuevo Fitment** → Action 2 +- **+ Fitment Masivo** → Action 3 +- **+ Aftermarket** → Action 4 +- **+ Cross-Reference** → Action 5 + +### Fila 3: Referencias +- **Tabla de Grupos** (grupos con IDs para referencia) +- **Tabla de Fabricantes** (fabricantes con IDs) +- **Estadísticas** (conteos actuales) + +### Fila 4: Últimos registros +- **Últimas piezas cargadas**: + ```sql + SELECT oem_part_number, name_part, name_es, created_at + FROM parts ORDER BY created_at DESC LIMIT 10 + ``` +- **Últimos fitments**: + ```sql + SELECT b.name_brand, m.name_model, y.year_car, p.oem_part_number, vp.created_at + FROM vehicle_parts vp + JOIN model_year_engine mye ON vp.model_year_engine_id = mye.id_mye + JOIN models m ON mye.model_id = m.id_model + JOIN brands b ON m.brand_id = b.id_brand + JOIN years y ON mye.year_id = y.id_year + JOIN parts p ON vp.part_id = p.id_part + ORDER BY vp.created_at DESC LIMIT 10 + ``` + +--- + +## 4. Flujo de Trabajo Recomendado + +### Para cargar una pieza nueva con todo su contexto: + +``` +1. Abrir "Panel de Carga de Datos" + +2. Click [+ Nueva Pieza OEM] + → Llenar: OEM#, Nombre, Grupo, Descripción + → Submit → Anotar el ID generado + +3. Click [+ Fitment Masivo] + → Ingresar: ID pieza, Marca, Modelo, Rango de años + → Submit → La pieza queda vinculada a todos los vehículos + +4. Click [+ Aftermarket] (repetir por cada fabricante) + → Ingresar: ID pieza OEM, ID fabricante, # aftermarket, precio + → Submit + +5. Click [+ Cross-Reference] (repetir por cada referencia) + → Ingresar: ID pieza, # referencia, tipo + → Submit + +6. Verificar en "Últimos registros" que todo se cargó +``` + +--- + +## 5. Preguntas Auxiliares Importantes + +Guardar estas como preguntas para tener a mano durante la carga: + +### Buscar pieza por número +```sql +SELECT id_part, oem_part_number, name_part, name_es +FROM parts +WHERE oem_part_number ILIKE {{'%' || numero || '%'}} + OR name_part ILIKE {{'%' || numero || '%'}} +ORDER BY oem_part_number +LIMIT 50 +``` + +### Buscar vehículo por marca/modelo +```sql +SELECT mye.id_mye, b.name_brand, m.name_model, y.year_car, e.name_engine +FROM model_year_engine mye +JOIN models m ON mye.model_id = m.id_model +JOIN brands b ON m.brand_id = b.id_brand +JOIN years y ON mye.year_id = y.id_year +JOIN engines e ON mye.engine_id = e.id_engine +WHERE b.name_brand ILIKE {{'%' || marca || '%'}} + AND m.name_model ILIKE {{'%' || modelo || '%'}} +ORDER BY y.year_car DESC +LIMIT 100 +``` + +### Catálogo de grupos (referencia para group_id) +```sql +SELECT pg.id_part_group AS id, pg.name_part_group AS grupo, + pc.name_part_category AS categoria +FROM part_groups pg +JOIN part_categories pc ON pg.category_id = pc.id_part_category +ORDER BY pc.display_order, pg.display_order +``` + +### Catálogo de fabricantes (referencia para manufacturer_id) +```sql +SELECT m.id_manufacture AS id, m.name_manufacture AS fabricante, + mt.name_type_manu AS tipo, qt.name_quality AS calidad +FROM manufacturers m +LEFT JOIN manufacture_type mt ON m.id_type_manu = mt.id_type_manu +LEFT JOIN quality_tier qt ON m.id_quality_tier = qt.id_quality_tier +ORDER BY m.name_manufacture +``` + +--- + +## 6. IDs de Referencia Rápida + +### Calidad (`id_quality_tier`) +| ID | Valor | +|----|-------| +| 1 | economy | +| 2 | oem | +| 3 | premium | +| 4 | standard | + +### Tipo de referencia (`id_ref_type`) +| ID | Valor | Uso | +|----|-------|-----| +| 1 | competitor | Número de competidor equivalente | +| 2 | interchange | Intercambio directo compatible | +| 3 | oem_alternate | Número OEM alterno | +| 4 | supersession | Pieza que esta reemplaza | + +### Posición (`id_position_part`) +| ID | Valor | +|----|-------| +| 1 | front | +| 2 | rear | + +### Tipo de fabricante (`id_type_manu`) +| ID | Valor | +|----|-------| +| 1 | aftermarket | +| 2 | oem |