Initial commit - erp-mecanicas-diesel-database
This commit is contained in:
commit
0207e1788a
324
HERENCIA-ERP-CORE.md
Normal file
324
HERENCIA-ERP-CORE.md
Normal file
@ -0,0 +1,324 @@
|
||||
# Referencia de Base de Datos - ERP Mecánicas Diesel
|
||||
|
||||
**Fecha:** 2025-12-08
|
||||
**Versión:** 1.1
|
||||
**Proyecto:** ERP Mecánicas Diesel
|
||||
**Nivel:** 2B.2 (Proyecto Independiente)
|
||||
|
||||
---
|
||||
|
||||
## RESUMEN
|
||||
|
||||
ERP Mecánicas Diesel es un **proyecto independiente** que implementa y adapta patrones del ERP-Core para el dominio de talleres de reparación de motores diesel. No es una extensión del core, sino un sistema autónomo que:
|
||||
|
||||
1. **Implementa** schemas propios para gestión de órdenes de servicio
|
||||
2. **Adapta** estructuras de inventario para refacciones especializadas
|
||||
3. **Reutiliza** patrones de autenticación y multi-tenancy
|
||||
4. **Opera independientemente** como sistema completo
|
||||
|
||||
**DDL de Referencia (Core):** `apps/erp-core/database/ddl/`
|
||||
**DDL Propio:** `database/init/`
|
||||
|
||||
---
|
||||
|
||||
## ARQUITECTURA DEL PROYECTO
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ERP CORE (Referencia) │
|
||||
│ Patrones, specs y estructuras reutilizables │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||
│ │ auth │ │ core │ │inventory│ │ sales │ │
|
||||
│ │ patrones│ │ patrones│ │ patrones│ │ patrones │ │
|
||||
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ REFERENCIA / FORK
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ERP MECÁNICAS DIESEL (Proyecto Independiente) │
|
||||
│ │
|
||||
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
|
||||
│ │ service_ │ │ parts_ │ │ vehicle_ │ │
|
||||
│ │ management │ │ management │ │ management │ │
|
||||
│ │ 18 tbl │ │ 12 tbl │ │ 8 tbl │ │
|
||||
│ │ (órdenes) │ │ (refacciones) │ │ (vehículos) │ │
|
||||
│ └───────────────┘ └───────────────┘ └───────────────┘ │
|
||||
│ │
|
||||
│ Schemas propios: 3 | Tablas propias: 38 │
|
||||
│ Opera de forma INDEPENDIENTE │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PATRONES REUTILIZADOS DEL CORE
|
||||
|
||||
Los siguientes patrones del ERP-Core fueron **adaptados e implementados** en este proyecto:
|
||||
|
||||
| Patrón del Core | Adaptación en Mecánicas Diesel |
|
||||
|-----------------|-------------------------------|
|
||||
| `auth.*` | Multi-tenancy con RLS propio |
|
||||
| `core.partners` | Clientes, flotas de vehículos |
|
||||
| `inventory.*` | Refacciones, partes OEM, stock |
|
||||
| `sales.*` | Cotizaciones, órdenes de servicio |
|
||||
|
||||
**Nota:** Este proyecto NO depende del ERP-Core para ejecutarse. Implementa sus propios schemas y puede operar de forma completamente standalone.
|
||||
|
||||
---
|
||||
|
||||
## SCHEMAS ESPECÍFICOS DE MECÁNICAS DIESEL
|
||||
|
||||
### 1. Schema `service_management` (10+ tablas)
|
||||
|
||||
**Propósito:** Gestión de órdenes de servicio y diagnósticos
|
||||
|
||||
```sql
|
||||
-- Tablas principales:
|
||||
service_management.service_orders -- Órdenes de trabajo
|
||||
service_management.order_items -- Líneas (servicios/refacciones)
|
||||
service_management.work_bays -- Bahías de trabajo
|
||||
service_management.diagnostics -- Diagnósticos
|
||||
service_management.diagnostic_items -- Hallazgos
|
||||
service_management.quotes -- Cotizaciones
|
||||
service_management.services -- Catálogo de servicios
|
||||
```
|
||||
|
||||
**Relaciones con Core:**
|
||||
- `service_orders.customer_id` -> `core.partners`
|
||||
- `service_orders.vehicle_id` -> `vehicle_management.vehicles`
|
||||
- `order_items.part_id` -> `parts_management.parts`
|
||||
|
||||
### 2. Schema `parts_management` (12+ tablas)
|
||||
|
||||
**Propósito:** Inventario de refacciones especializado
|
||||
|
||||
```sql
|
||||
-- Extiende: inventory schema del core
|
||||
-- Adiciona campos específicos:
|
||||
-- OEM numbers, compatibilidad vehicular, garantías
|
||||
|
||||
parts_management.parts -- Refacciones (extiende inventory.products)
|
||||
parts_management.part_categories -- Categorías
|
||||
parts_management.suppliers -- Proveedores especializados
|
||||
parts_management.warehouse_locations -- Ubicaciones en almacén
|
||||
parts_management.inventory_movements -- Kardex
|
||||
parts_management.inventory_adjustments -- Ajustes
|
||||
parts_management.part_compatibility -- Compatibilidad con vehículos
|
||||
```
|
||||
|
||||
**Relaciones con Core:**
|
||||
- `parts.base_product_id` -> `inventory.products` (herencia)
|
||||
- `suppliers.partner_id` -> `core.partners`
|
||||
|
||||
### 3. Schema `vehicle_management` (8+ tablas)
|
||||
|
||||
**Propósito:** Gestión de vehículos diesel y flotas
|
||||
|
||||
```sql
|
||||
vehicle_management.vehicles -- Vehículos registrados
|
||||
vehicle_management.vehicle_engines -- Especificaciones del motor
|
||||
vehicle_management.fleets -- Flotas de clientes
|
||||
vehicle_management.engine_catalog -- Catálogo de motores diesel
|
||||
vehicle_management.maintenance_reminders -- Recordatorios de servicio
|
||||
```
|
||||
|
||||
**Relaciones con Core:**
|
||||
- `vehicles.customer_id` -> `core.partners`
|
||||
- `fleets.customer_id` -> `core.partners`
|
||||
|
||||
---
|
||||
|
||||
## ORDEN DE EJECUCIÓN DDL
|
||||
|
||||
Para recrear la base de datos completa:
|
||||
|
||||
```bash
|
||||
# PASO 1: Cargar ERP Core (base)
|
||||
cd apps/erp-core/database
|
||||
./scripts/reset-database.sh --force
|
||||
|
||||
# PASO 2: Cargar extensiones de Mecánicas Diesel
|
||||
cd apps/verticales/mecanicas-diesel/database
|
||||
psql $DATABASE_URL -f init/00-extensions.sql
|
||||
psql $DATABASE_URL -f init/01-create-schemas.sql
|
||||
psql $DATABASE_URL -f init/02-rls-functions.sql
|
||||
psql $DATABASE_URL -f init/03-service-management-tables.sql
|
||||
psql $DATABASE_URL -f init/04-parts-management-tables.sql
|
||||
psql $DATABASE_URL -f init/05-vehicle-management-tables.sql
|
||||
psql $DATABASE_URL -f init/06-seed-data.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DEPENDENCIAS CRUZADAS
|
||||
|
||||
### Tablas de Mecánicas que dependen del Core
|
||||
|
||||
| Tabla Mecánicas | Depende de (Core) |
|
||||
|-----------------|-------------------|
|
||||
| `service_management.service_orders` | `core.partners` (customer) |
|
||||
| `service_management.service_orders` | `auth.users` (assigned_to) |
|
||||
| `parts_management.parts` | `inventory.products` (herencia) |
|
||||
| `parts_management.suppliers` | `core.partners` |
|
||||
| `vehicle_management.vehicles` | `core.partners` (owner) |
|
||||
| `vehicle_management.fleets` | `core.partners` |
|
||||
| Todas las tablas | `auth.users` (audit) |
|
||||
|
||||
---
|
||||
|
||||
## SPECS DEL CORE IMPLEMENTADAS
|
||||
|
||||
| Spec Core | Aplicación en Mecánicas | Estado |
|
||||
|-----------|------------------------|--------|
|
||||
| SPEC-VALORACION-INVENTARIO | Costeo de refacciones | ✅ DDL LISTO |
|
||||
| SPEC-TRAZABILIDAD-LOTES-SERIES | Garantías de partes | ✅ DDL LISTO |
|
||||
| SPEC-INVENTARIOS-CICLICOS | Conteos de refacciones | ✅ DDL LISTO |
|
||||
| SPEC-MAIL-THREAD-TRACKING | Historial de órdenes | PENDIENTE |
|
||||
| SPEC-TAREAS-RECURRENTES | Mantenimientos preventivos | PENDIENTE |
|
||||
|
||||
### Correcciones de DDL Core (2025-12-08)
|
||||
|
||||
El DDL del ERP-Core fue corregido para resolver FK inválidas:
|
||||
|
||||
1. **stock_valuation_layers**: Campos `journal_entry_id` y `journal_entry_line_id` (antes `account_move_*`)
|
||||
2. **stock_move_consume_rel**: Nueva tabla de trazabilidad (antes `move_line_consume_rel`)
|
||||
3. **category_stock_accounts**: FK corregida a `core.product_categories`
|
||||
4. **product_categories**: ALTERs ahora apuntan a schema `core`
|
||||
|
||||
Estas correcciones permiten que el DDL de inventory se ejecute correctamente y habilitan:
|
||||
- Valoración FIFO/AVCO de refacciones
|
||||
- Trazabilidad de lotes y números de serie (garantías)
|
||||
- Conteos cíclicos de inventario con clasificación ABC
|
||||
|
||||
### Validación DDL Mecánicas-Diesel (2025-12-08)
|
||||
|
||||
**Estado:** ✅ VÁLIDO - Compatible con ERP-Core
|
||||
|
||||
| Archivo | Líneas | Tablas | Estado |
|
||||
|---------|--------|--------|--------|
|
||||
| `init/00-extensions.sql` | 14 | 0 | ✅ Válido |
|
||||
| `init/01-create-schemas.sql` | 30 | 0 | ✅ Válido |
|
||||
| `init/02-rls-functions.sql` | 106 | 0 | ✅ Válido |
|
||||
| `init/03-service-management-tables.sql` | 567 | ~18 | ✅ Válido |
|
||||
| `init/04-parts-management-tables.sql` | 398 | ~12 | ✅ Válido |
|
||||
| `init/05-vehicle-management-tables.sql` | 365 | ~8 | ✅ Válido |
|
||||
|
||||
**Enfoque de FK:** Este proyecto usa **referencias comentadas** en lugar de FK explícitas:
|
||||
|
||||
```sql
|
||||
-- Usa columnas sin FK explícitas:
|
||||
tenant_id UUID NOT NULL, -- Referencia conceptual: auth.tenants
|
||||
customer_id UUID NOT NULL, -- Referencia conceptual: core.partners
|
||||
assigned_to UUID, -- Referencia conceptual: auth.users
|
||||
```
|
||||
|
||||
**Ventajas:**
|
||||
- ✅ No depende del schema específico de ERP-Core
|
||||
- ✅ Puede operar standalone o integrado
|
||||
- ✅ Sin discrepancias con cambios en auth/core
|
||||
|
||||
**Desventajas:**
|
||||
- ⚠️ No hay integridad referencial a nivel de BD
|
||||
- ⚠️ La integridad debe garantizarse a nivel de aplicación
|
||||
|
||||
**Recomendación para integración completa:**
|
||||
Si se requiere integridad referencial estricta, agregar FK explícitas a `auth.tenants` y `auth.users` (no `core.*`).
|
||||
|
||||
---
|
||||
|
||||
## MAPEO DE NOMENCLATURA
|
||||
|
||||
| Core | Mecánicas Diesel |
|
||||
|------|------------------|
|
||||
| `core.partners` | Clientes, Flotas |
|
||||
| `inventory.products` | Refacciones base |
|
||||
| `inventory.locations` | Warehouse locations |
|
||||
| `sales.sale_orders` | Base para cotizaciones |
|
||||
| `purchase.purchase_orders` | Compras de refacciones |
|
||||
|
||||
---
|
||||
|
||||
## CATÁLOGO DE MOTORES DIESEL
|
||||
|
||||
El schema `vehicle_management` incluye un catálogo de motores diesel preconfigurado:
|
||||
|
||||
| Marca | Modelos |
|
||||
|-------|---------|
|
||||
| Cummins | ISX15, ISB6.7, X15 |
|
||||
| Detroit | DD15, DD13 |
|
||||
| Paccar | MX-13, MX-11 |
|
||||
| International | A26 |
|
||||
| Volvo | D13, D11 |
|
||||
| Navistar | N13 |
|
||||
|
||||
---
|
||||
|
||||
## VALIDACIÓN DE HERENCIA
|
||||
|
||||
### Verificar schemas heredados
|
||||
|
||||
```sql
|
||||
-- Verificar que existen los schemas del core
|
||||
SELECT schema_name
|
||||
FROM information_schema.schemata
|
||||
WHERE schema_name IN ('auth', 'core', 'financial', 'inventory',
|
||||
'purchase', 'sales', 'analytics', 'system');
|
||||
```
|
||||
|
||||
### Verificar extensiones de mecánicas
|
||||
|
||||
```sql
|
||||
-- Verificar schemas específicos
|
||||
SELECT schema_name
|
||||
FROM information_schema.schemata
|
||||
WHERE schema_name IN ('service_management', 'parts_management', 'vehicle_management');
|
||||
|
||||
-- Contar tablas por schema
|
||||
SELECT schemaname, COUNT(*) as tables
|
||||
FROM pg_tables
|
||||
WHERE schemaname LIKE '%_management'
|
||||
GROUP BY schemaname;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SPECS DEL CORE APLICABLES
|
||||
|
||||
Según el [MAPEO-SPECS-VERTICALES.md](../../../../erp-core/docs/04-modelado/MAPEO-SPECS-VERTICALES.md):
|
||||
|
||||
| Categoría | Total | Obligatorias | Opcionales | No Aplican |
|
||||
|-----------|-------|--------------|------------|------------|
|
||||
| **Mecánicas-Diesel** | 30 | 23 | 2 | 5 |
|
||||
|
||||
### SPECS Críticas para Mecánicas-Diesel
|
||||
|
||||
| SPEC | Aplicación | Estado DDL |
|
||||
|------|------------|------------|
|
||||
| SPEC-VALORACION-INVENTARIO | Costeo de refacciones | ✅ DDL LISTO |
|
||||
| SPEC-TRAZABILIDAD-LOTES-SERIES | Garantías de partes | ✅ DDL LISTO |
|
||||
| SPEC-INVENTARIOS-CICLICOS | Conteos de refacciones | ✅ DDL LISTO |
|
||||
| SPEC-PRICING-RULES | Precios por tipo de servicio | PENDIENTE |
|
||||
| SPEC-MAIL-THREAD-TRACKING | Historial de órdenes | PENDIENTE |
|
||||
|
||||
### SPECS No Aplicables
|
||||
|
||||
- `SPEC-INTEGRACION-CALENDAR` - No requiere calendario externo
|
||||
- `SPEC-PROYECTOS-DEPENDENCIAS-BURNDOWN` - No aplica a taller mecánico
|
||||
- `SPEC-FIRMA-ELECTRONICA-NOM151` - Opcional
|
||||
- `SPEC-OAUTH2-SOCIAL-LOGIN` - Opcional
|
||||
- `SPEC-CONSOLIDACION-FINANCIERA` - Opcional
|
||||
|
||||
---
|
||||
|
||||
## REFERENCIAS
|
||||
|
||||
- ERP Core DDL: `apps/erp-core/database/ddl/`
|
||||
- ERP Core README: `apps/erp-core/database/README.md`
|
||||
- MAPEO-SPECS-VERTICALES: `apps/erp-core/docs/04-modelado/MAPEO-SPECS-VERTICALES.md`
|
||||
- DATABASE_INVENTORY.yml: `orchestration/inventarios/`
|
||||
|
||||
---
|
||||
|
||||
**Documento de herencia oficial**
|
||||
**Última actualización:** 2025-12-08
|
||||
173
README.md
Normal file
173
README.md
Normal file
@ -0,0 +1,173 @@
|
||||
# Base de Datos - Mecánicas Diesel
|
||||
|
||||
## Descripción
|
||||
|
||||
Base de datos PostgreSQL para el sistema de gestión de talleres de mecánica diesel.
|
||||
|
||||
## Schemas
|
||||
|
||||
| Schema | Descripción |
|
||||
|--------|-------------|
|
||||
| `service_management` | Órdenes de servicio, diagnósticos, cotizaciones |
|
||||
| `parts_management` | Inventario de refacciones específico del taller |
|
||||
| `vehicle_management` | Vehículos diesel, flotas, motores |
|
||||
|
||||
**NOTA:** Los schemas `auth`, `core`, `inventory` se heredan de erp-core.
|
||||
|
||||
## Configuración
|
||||
|
||||
### 1. Variables de Entorno
|
||||
|
||||
Copiar `.env.example` a `.env` y configurar:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Editar valores en .env
|
||||
```
|
||||
|
||||
### 2. Iniciar con Docker
|
||||
|
||||
```bash
|
||||
# Solo base de datos y Redis
|
||||
docker-compose up -d postgres redis
|
||||
|
||||
# Verificar salud
|
||||
docker-compose ps
|
||||
|
||||
# Ver logs
|
||||
docker-compose logs -f postgres
|
||||
```
|
||||
|
||||
### 3. Ejecutar Scripts Manualmente
|
||||
|
||||
Si no usas Docker, ejecutar en orden:
|
||||
|
||||
```bash
|
||||
psql -U mecanicas_user -d mecanicas_diesel -f database/init/00-extensions.sql
|
||||
psql -U mecanicas_user -d mecanicas_diesel -f database/init/01-create-schemas.sql
|
||||
psql -U mecanicas_user -d mecanicas_diesel -f database/init/02-rls-functions.sql
|
||||
psql -U mecanicas_user -d mecanicas_diesel -f database/init/03-service-management-tables.sql
|
||||
psql -U mecanicas_user -d mecanicas_diesel -f database/init/04-parts-management-tables.sql
|
||||
psql -U mecanicas_user -d mecanicas_diesel -f database/init/05-vehicle-management-tables.sql
|
||||
psql -U mecanicas_user -d mecanicas_diesel -f database/init/06-seed-data.sql
|
||||
```
|
||||
|
||||
## Arquitectura Multi-Tenant
|
||||
|
||||
### Row-Level Security (RLS)
|
||||
|
||||
Todas las tablas principales implementan RLS con políticas completas:
|
||||
|
||||
```sql
|
||||
-- Establecer tenant en sesión
|
||||
SELECT set_current_tenant_id('uuid-del-tenant');
|
||||
|
||||
-- Las queries se filtran automáticamente
|
||||
SELECT * FROM service_management.service_orders;
|
||||
-- Solo retorna órdenes del tenant actual
|
||||
```
|
||||
|
||||
### Crear Políticas RLS
|
||||
|
||||
Para nuevas tablas:
|
||||
|
||||
```sql
|
||||
-- Automático: crea políticas SELECT, INSERT, UPDATE, DELETE
|
||||
SELECT create_tenant_rls_policies('schema_name', 'table_name');
|
||||
```
|
||||
|
||||
## Tablas Principales
|
||||
|
||||
### service_management
|
||||
|
||||
- `service_orders` - Órdenes de servicio
|
||||
- `order_items` - Líneas de trabajo/refacciones
|
||||
- `work_bays` - Bahías de trabajo
|
||||
- `diagnostics` - Diagnósticos realizados
|
||||
- `diagnostic_items` - Hallazgos del diagnóstico
|
||||
- `quotes` - Cotizaciones
|
||||
- `services` - Catálogo de servicios
|
||||
|
||||
### parts_management
|
||||
|
||||
- `parts` - Refacciones
|
||||
- `part_categories` - Categorías
|
||||
- `suppliers` - Proveedores
|
||||
- `warehouse_locations` - Ubicaciones
|
||||
- `inventory_movements` - Kardex
|
||||
- `inventory_adjustments` - Ajustes
|
||||
|
||||
### vehicle_management
|
||||
|
||||
- `vehicles` - Vehículos registrados
|
||||
- `vehicle_engines` - Especificaciones del motor
|
||||
- `fleets` - Flotas
|
||||
- `engine_catalog` - Catálogo de motores (global)
|
||||
- `maintenance_reminders` - Recordatorios
|
||||
|
||||
## Convenciones
|
||||
|
||||
### Nombres
|
||||
|
||||
- Tablas: plural en inglés, snake_case
|
||||
- Columnas: snake_case
|
||||
- PKs: `id UUID`
|
||||
- FKs: `entity_id UUID`
|
||||
- Tenant: `tenant_id UUID`
|
||||
|
||||
### Campos Estándar
|
||||
|
||||
```sql
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
|
||||
tenant_id UUID NOT NULL
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
created_by UUID
|
||||
```
|
||||
|
||||
### Soft Delete (opcional)
|
||||
|
||||
```sql
|
||||
deleted_at TIMESTAMP WITH TIME ZONE
|
||||
deleted_by UUID
|
||||
```
|
||||
|
||||
## Triggers Automáticos
|
||||
|
||||
- `trigger_set_updated_at()` - Actualiza `updated_at` en cada UPDATE
|
||||
- `update_fleet_vehicle_count()` - Mantiene conteo de vehículos en flotas
|
||||
|
||||
## Herramientas
|
||||
|
||||
### Adminer (UI de BD)
|
||||
|
||||
```bash
|
||||
docker-compose --profile tools up -d adminer
|
||||
# Acceder en http://localhost:8080
|
||||
```
|
||||
|
||||
### Conexión Directa
|
||||
|
||||
```bash
|
||||
docker-compose exec postgres psql -U mecanicas_user -d mecanicas_diesel
|
||||
```
|
||||
|
||||
## Migraciones
|
||||
|
||||
Para cambios futuros, usar el sistema de migraciones de tu ORM (Prisma, TypeORM, etc.).
|
||||
|
||||
Ejemplo con Prisma:
|
||||
|
||||
```bash
|
||||
npx prisma migrate dev --name add_new_field
|
||||
```
|
||||
|
||||
## Backup
|
||||
|
||||
```bash
|
||||
# Backup
|
||||
docker-compose exec postgres pg_dump -U mecanicas_user mecanicas_diesel > backup.sql
|
||||
|
||||
# Restore
|
||||
docker-compose exec -T postgres psql -U mecanicas_user -d mecanicas_diesel < backup.sql
|
||||
```
|
||||
14
init/00-extensions.sql
Normal file
14
init/00-extensions.sql
Normal file
@ -0,0 +1,14 @@
|
||||
-- ===========================================
|
||||
-- MECANICAS DIESEL - Extensiones PostgreSQL
|
||||
-- ===========================================
|
||||
-- Ejecutar primero
|
||||
|
||||
-- UUID generation
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
|
||||
-- Full text search en español
|
||||
CREATE EXTENSION IF NOT EXISTS "unaccent";
|
||||
|
||||
-- Para auditoría y triggers
|
||||
CREATE EXTENSION IF NOT EXISTS "hstore";
|
||||
158
init/00.5-workshop-core-tables.sql
Normal file
158
init/00.5-workshop-core-tables.sql
Normal file
@ -0,0 +1,158 @@
|
||||
-- ===========================================
|
||||
-- MECANICAS DIESEL - Schema workshop_core
|
||||
-- ===========================================
|
||||
-- Autenticación y gestión de usuarios
|
||||
-- Ejecutar después de 00-extensions.sql
|
||||
|
||||
-- Crear schema
|
||||
CREATE SCHEMA IF NOT EXISTS workshop_core;
|
||||
COMMENT ON SCHEMA workshop_core IS 'Autenticación, usuarios y gestión de talleres multi-tenant';
|
||||
|
||||
-- Grants básicos
|
||||
GRANT USAGE ON SCHEMA workshop_core TO mecanicas_user;
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA workshop_core TO mecanicas_user;
|
||||
|
||||
-- Default privileges para tablas futuras
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA workshop_core GRANT ALL ON TABLES TO mecanicas_user;
|
||||
|
||||
SET search_path TO workshop_core, public;
|
||||
|
||||
-- -------------------------------------------
|
||||
-- WORKSHOPS - Talleres (tenants)
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE workshop_core.workshops (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- Información general
|
||||
name VARCHAR(100) NOT NULL,
|
||||
|
||||
-- Datos fiscales
|
||||
rfc VARCHAR(13),
|
||||
|
||||
-- Contacto
|
||||
email VARCHAR(255),
|
||||
phone VARCHAR(20),
|
||||
|
||||
-- Dirección
|
||||
address TEXT,
|
||||
city VARCHAR(100),
|
||||
state VARCHAR(50),
|
||||
postal_code VARCHAR(10),
|
||||
|
||||
-- Branding
|
||||
logo_url TEXT,
|
||||
|
||||
-- Estado
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
|
||||
-- Audit
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Índices
|
||||
CREATE INDEX idx_workshops_is_active ON workshop_core.workshops(is_active);
|
||||
CREATE INDEX idx_workshops_rfc ON workshop_core.workshops(rfc) WHERE rfc IS NOT NULL;
|
||||
|
||||
-- Trigger updated_at
|
||||
CREATE TRIGGER trg_workshops_updated_at
|
||||
BEFORE UPDATE ON workshop_core.workshops
|
||||
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
COMMENT ON TABLE workshop_core.workshops IS 'Talleres mecánicos (tenants del sistema multi-tenant)';
|
||||
COMMENT ON COLUMN workshop_core.workshops.id IS 'Identificador único del taller (tenant_id)';
|
||||
COMMENT ON COLUMN workshop_core.workshops.name IS 'Nombre comercial del taller';
|
||||
COMMENT ON COLUMN workshop_core.workshops.rfc IS 'RFC (Registro Federal de Contribuyentes) para facturación';
|
||||
COMMENT ON COLUMN workshop_core.workshops.is_active IS 'Indica si el taller está activo y puede operar en el sistema';
|
||||
|
||||
-- -------------------------------------------
|
||||
-- USERS - Usuarios del sistema
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE workshop_core.users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- Multi-tenant
|
||||
tenant_id UUID NOT NULL REFERENCES workshop_core.workshops(id) ON DELETE CASCADE,
|
||||
|
||||
-- Credenciales
|
||||
email VARCHAR(255) NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
|
||||
-- Información personal
|
||||
full_name VARCHAR(150) NOT NULL,
|
||||
avatar_url TEXT,
|
||||
|
||||
-- Rol y permisos
|
||||
role VARCHAR(30) NOT NULL DEFAULT 'mecanico'
|
||||
CHECK (role IN ('admin', 'jefe_taller', 'mecanico', 'recepcion', 'almacen')),
|
||||
|
||||
-- Estado
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
email_verified BOOLEAN DEFAULT false,
|
||||
|
||||
-- Sesión
|
||||
last_login_at TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
-- Audit
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT uq_user_email_global UNIQUE (email),
|
||||
CONSTRAINT uq_user_email_tenant UNIQUE (tenant_id, email)
|
||||
);
|
||||
|
||||
-- Índices
|
||||
CREATE INDEX idx_users_tenant ON workshop_core.users(tenant_id);
|
||||
CREATE INDEX idx_users_email ON workshop_core.users(email);
|
||||
CREATE INDEX idx_users_role ON workshop_core.users(tenant_id, role);
|
||||
CREATE INDEX idx_users_is_active ON workshop_core.users(tenant_id, is_active);
|
||||
CREATE INDEX idx_users_last_login ON workshop_core.users(last_login_at DESC);
|
||||
|
||||
-- Trigger updated_at
|
||||
CREATE TRIGGER trg_users_updated_at
|
||||
BEFORE UPDATE ON workshop_core.users
|
||||
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
-- RLS
|
||||
SELECT create_tenant_rls_policies('workshop_core', 'users');
|
||||
|
||||
COMMENT ON TABLE workshop_core.users IS 'Usuarios del sistema con autenticación y roles';
|
||||
COMMENT ON COLUMN workshop_core.users.tenant_id IS 'Taller al que pertenece el usuario';
|
||||
COMMENT ON COLUMN workshop_core.users.email IS 'Correo electrónico único para login';
|
||||
COMMENT ON COLUMN workshop_core.users.password_hash IS 'Hash bcrypt de la contraseña';
|
||||
COMMENT ON COLUMN workshop_core.users.role IS 'Rol del usuario: admin, jefe_taller, mecanico, recepcion, almacen';
|
||||
COMMENT ON COLUMN workshop_core.users.is_active IS 'Indica si el usuario puede acceder al sistema';
|
||||
COMMENT ON COLUMN workshop_core.users.email_verified IS 'Indica si el email ha sido verificado';
|
||||
|
||||
-- -------------------------------------------
|
||||
-- REFRESH_TOKENS - Tokens de refresco JWT
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE workshop_core.refresh_tokens (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- Usuario
|
||||
user_id UUID NOT NULL REFERENCES workshop_core.users(id) ON DELETE CASCADE,
|
||||
|
||||
-- Token
|
||||
token VARCHAR(500) NOT NULL,
|
||||
|
||||
-- Validez
|
||||
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
revoked_at TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
-- Audit
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Índices
|
||||
CREATE INDEX idx_refresh_tokens_user ON workshop_core.refresh_tokens(user_id);
|
||||
CREATE INDEX idx_refresh_tokens_token ON workshop_core.refresh_tokens(token);
|
||||
CREATE INDEX idx_refresh_tokens_expires ON workshop_core.refresh_tokens(expires_at);
|
||||
CREATE INDEX idx_refresh_tokens_active ON workshop_core.refresh_tokens(user_id, expires_at)
|
||||
WHERE revoked_at IS NULL;
|
||||
|
||||
COMMENT ON TABLE workshop_core.refresh_tokens IS 'Tokens de refresco para autenticación JWT';
|
||||
COMMENT ON COLUMN workshop_core.refresh_tokens.token IS 'Token de refresco único para renovar access tokens';
|
||||
COMMENT ON COLUMN workshop_core.refresh_tokens.expires_at IS 'Fecha de expiración del token';
|
||||
COMMENT ON COLUMN workshop_core.refresh_tokens.revoked_at IS 'Fecha de revocación (si el token fue invalidado)';
|
||||
29
init/01-create-schemas.sql
Normal file
29
init/01-create-schemas.sql
Normal file
@ -0,0 +1,29 @@
|
||||
-- ===========================================
|
||||
-- MECANICAS DIESEL - Creacion de Schemas
|
||||
-- ===========================================
|
||||
-- Ejecutar despues de extensiones
|
||||
|
||||
-- Schemas propios de mecanicas-diesel
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS service_management;
|
||||
COMMENT ON SCHEMA service_management IS 'Órdenes de servicio, diagnósticos, cotizaciones';
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS parts_management;
|
||||
COMMENT ON SCHEMA parts_management IS 'Inventario de refacciones específico del taller';
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS vehicle_management;
|
||||
COMMENT ON SCHEMA vehicle_management IS 'Vehículos diesel, flotas, motores';
|
||||
|
||||
-- Grants básicos
|
||||
GRANT USAGE ON SCHEMA service_management TO mecanicas_user;
|
||||
GRANT USAGE ON SCHEMA parts_management TO mecanicas_user;
|
||||
GRANT USAGE ON SCHEMA vehicle_management TO mecanicas_user;
|
||||
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA service_management TO mecanicas_user;
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA parts_management TO mecanicas_user;
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA vehicle_management TO mecanicas_user;
|
||||
|
||||
-- Default privileges para tablas futuras
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA service_management GRANT ALL ON TABLES TO mecanicas_user;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA parts_management GRANT ALL ON TABLES TO mecanicas_user;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA vehicle_management GRANT ALL ON TABLES TO mecanicas_user;
|
||||
106
init/02-rls-functions.sql
Normal file
106
init/02-rls-functions.sql
Normal file
@ -0,0 +1,106 @@
|
||||
-- ===========================================
|
||||
-- MECANICAS DIESEL - Funciones RLS Multi-Tenant
|
||||
-- ===========================================
|
||||
-- Funciones para Row-Level Security
|
||||
|
||||
-- Función para obtener el tenant_id actual de la sesión
|
||||
CREATE OR REPLACE FUNCTION get_current_tenant_id()
|
||||
RETURNS UUID AS $$
|
||||
BEGIN
|
||||
RETURN NULLIF(current_setting('app.current_tenant_id', true), '')::UUID;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
|
||||
|
||||
COMMENT ON FUNCTION get_current_tenant_id() IS 'Obtiene el tenant_id de la sesión actual para RLS';
|
||||
|
||||
-- Función para establecer el tenant_id en la sesión
|
||||
CREATE OR REPLACE FUNCTION set_current_tenant_id(p_tenant_id UUID)
|
||||
RETURNS VOID AS $$
|
||||
BEGIN
|
||||
PERFORM set_config('app.current_tenant_id', p_tenant_id::TEXT, false);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
COMMENT ON FUNCTION set_current_tenant_id(UUID) IS 'Establece el tenant_id para la sesión actual';
|
||||
|
||||
-- Función para obtener el user_id actual
|
||||
CREATE OR REPLACE FUNCTION get_current_user_id()
|
||||
RETURNS UUID AS $$
|
||||
BEGIN
|
||||
RETURN NULLIF(current_setting('app.current_user_id', true), '')::UUID;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
|
||||
|
||||
-- Trigger function para updated_at automático
|
||||
CREATE OR REPLACE FUNCTION trigger_set_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION trigger_set_updated_at() IS 'Actualiza automáticamente updated_at';
|
||||
|
||||
-- Trigger function para created_by automático
|
||||
CREATE OR REPLACE FUNCTION trigger_set_created_by()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.created_by IS NULL THEN
|
||||
NEW.created_by = get_current_user_id();
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Macro para crear políticas RLS completas
|
||||
-- Uso: SELECT create_tenant_rls_policies('schema_name', 'table_name');
|
||||
CREATE OR REPLACE FUNCTION create_tenant_rls_policies(
|
||||
p_schema TEXT,
|
||||
p_table TEXT,
|
||||
p_tenant_column TEXT DEFAULT 'tenant_id'
|
||||
)
|
||||
RETURNS VOID AS $$
|
||||
DECLARE
|
||||
v_full_table TEXT;
|
||||
BEGIN
|
||||
v_full_table := p_schema || '.' || p_table;
|
||||
|
||||
-- Habilitar RLS
|
||||
EXECUTE format('ALTER TABLE %I.%I ENABLE ROW LEVEL SECURITY', p_schema, p_table);
|
||||
|
||||
-- Política SELECT
|
||||
EXECUTE format(
|
||||
'CREATE POLICY %I ON %I.%I FOR SELECT USING (%I = get_current_tenant_id())',
|
||||
p_table || '_select_policy', p_schema, p_table, p_tenant_column
|
||||
);
|
||||
|
||||
-- Política INSERT
|
||||
EXECUTE format(
|
||||
'CREATE POLICY %I ON %I.%I FOR INSERT WITH CHECK (%I = get_current_tenant_id())',
|
||||
p_table || '_insert_policy', p_schema, p_table, p_tenant_column
|
||||
);
|
||||
|
||||
-- Política UPDATE
|
||||
EXECUTE format(
|
||||
'CREATE POLICY %I ON %I.%I FOR UPDATE USING (%I = get_current_tenant_id()) WITH CHECK (%I = get_current_tenant_id())',
|
||||
p_table || '_update_policy', p_schema, p_table, p_tenant_column, p_tenant_column
|
||||
);
|
||||
|
||||
-- Política DELETE
|
||||
EXECUTE format(
|
||||
'CREATE POLICY %I ON %I.%I FOR DELETE USING (%I = get_current_tenant_id())',
|
||||
p_table || '_delete_policy', p_schema, p_table, p_tenant_column
|
||||
);
|
||||
|
||||
RAISE NOTICE 'RLS policies created for %.%', p_schema, p_table;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION create_tenant_rls_policies(TEXT, TEXT, TEXT) IS 'Crea políticas RLS completas (SELECT, INSERT, UPDATE, DELETE) para una tabla';
|
||||
566
init/03-service-management-tables.sql
Normal file
566
init/03-service-management-tables.sql
Normal file
@ -0,0 +1,566 @@
|
||||
-- ===========================================
|
||||
-- MECANICAS DIESEL - Schema service_management
|
||||
-- ===========================================
|
||||
-- Ordenes de servicio, diagnosticos, cotizaciones
|
||||
|
||||
SET search_path TO service_management, public;
|
||||
|
||||
-- -------------------------------------------
|
||||
-- SERVICE_ORDERS - Ordenes de servicio
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE service_management.service_orders (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL, -- Identificador del taller (multi-tenant)
|
||||
|
||||
-- Identificación
|
||||
order_number VARCHAR(20) NOT NULL,
|
||||
|
||||
-- Relaciones (referencias a tablas de otros schemas)
|
||||
customer_id UUID NOT NULL, -- core.partners
|
||||
vehicle_id UUID NOT NULL, -- vehicle_management.vehicles
|
||||
quote_id UUID, -- service_management.quotes
|
||||
|
||||
-- Asignación
|
||||
assigned_to UUID, -- auth.users
|
||||
bay_id UUID, -- Bahía de trabajo
|
||||
|
||||
-- Estado con CHECK constraint
|
||||
status VARCHAR(30) DEFAULT 'received'
|
||||
CHECK (status IN ('received', 'diagnosed', 'quoted', 'approved',
|
||||
'in_progress', 'waiting_parts', 'completed', 'delivered', 'cancelled')),
|
||||
|
||||
priority VARCHAR(20) DEFAULT 'normal'
|
||||
CHECK (priority IN ('low', 'normal', 'high', 'urgent')),
|
||||
|
||||
-- Fechas
|
||||
received_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
promised_at TIMESTAMP WITH TIME ZONE,
|
||||
started_at TIMESTAMP WITH TIME ZONE,
|
||||
completed_at TIMESTAMP WITH TIME ZONE,
|
||||
delivered_at TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
-- Kilometraje
|
||||
odometer_in INTEGER CHECK (odometer_in >= 0),
|
||||
odometer_out INTEGER CHECK (odometer_out >= 0),
|
||||
|
||||
-- Síntomas reportados
|
||||
customer_symptoms TEXT,
|
||||
|
||||
-- Totales
|
||||
labor_total DECIMAL(12,2) DEFAULT 0 CHECK (labor_total >= 0),
|
||||
parts_total DECIMAL(12,2) DEFAULT 0 CHECK (parts_total >= 0),
|
||||
discount_amount DECIMAL(12,2) DEFAULT 0 CHECK (discount_amount >= 0),
|
||||
discount_percent DECIMAL(5,2) DEFAULT 0 CHECK (discount_percent >= 0 AND discount_percent <= 100),
|
||||
tax DECIMAL(12,2) DEFAULT 0 CHECK (tax >= 0),
|
||||
grand_total DECIMAL(12,2) DEFAULT 0 CHECK (grand_total >= 0),
|
||||
|
||||
-- Notas
|
||||
internal_notes TEXT,
|
||||
customer_notes TEXT,
|
||||
|
||||
-- Audit
|
||||
created_by UUID,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_order_number UNIQUE (tenant_id, order_number),
|
||||
CONSTRAINT chk_odometer CHECK (odometer_out IS NULL OR odometer_out >= odometer_in)
|
||||
);
|
||||
|
||||
-- Índices
|
||||
CREATE INDEX idx_orders_tenant ON service_management.service_orders(tenant_id);
|
||||
CREATE INDEX idx_orders_status ON service_management.service_orders(tenant_id, status);
|
||||
CREATE INDEX idx_orders_vehicle ON service_management.service_orders(vehicle_id);
|
||||
CREATE INDEX idx_orders_customer ON service_management.service_orders(customer_id);
|
||||
CREATE INDEX idx_orders_assigned ON service_management.service_orders(assigned_to);
|
||||
CREATE INDEX idx_orders_received ON service_management.service_orders(received_at DESC);
|
||||
|
||||
-- Trigger updated_at
|
||||
CREATE TRIGGER trg_service_orders_updated_at
|
||||
BEFORE UPDATE ON service_management.service_orders
|
||||
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
-- RLS
|
||||
SELECT create_tenant_rls_policies('service_management', 'service_orders');
|
||||
|
||||
-- -------------------------------------------
|
||||
-- ORDER_ITEMS - Líneas de trabajo/refacciones
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE service_management.order_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
order_id UUID NOT NULL REFERENCES service_management.service_orders(id) ON DELETE CASCADE,
|
||||
|
||||
-- Tipo
|
||||
item_type VARCHAR(20) NOT NULL CHECK (item_type IN ('service', 'part')),
|
||||
|
||||
-- Referencias opcionales
|
||||
service_id UUID, -- core.services
|
||||
part_id UUID, -- parts_management.parts
|
||||
|
||||
-- Descripción
|
||||
description VARCHAR(500) NOT NULL,
|
||||
|
||||
-- Cantidades y precios
|
||||
quantity DECIMAL(10,3) DEFAULT 1 CHECK (quantity > 0),
|
||||
unit_price DECIMAL(12,2) NOT NULL CHECK (unit_price >= 0),
|
||||
discount_pct DECIMAL(5,2) DEFAULT 0 CHECK (discount_pct >= 0 AND discount_pct <= 100),
|
||||
subtotal DECIMAL(12,2) NOT NULL CHECK (subtotal >= 0),
|
||||
|
||||
-- Estado
|
||||
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'in_progress', 'completed')),
|
||||
|
||||
-- Para mano de obra
|
||||
estimated_hours DECIMAL(5,2) CHECK (estimated_hours >= 0),
|
||||
actual_hours DECIMAL(5,2) CHECK (actual_hours >= 0),
|
||||
|
||||
-- Mecánico
|
||||
performed_by UUID,
|
||||
completed_at TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
notes TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_order_items_order ON service_management.order_items(order_id);
|
||||
CREATE INDEX idx_order_items_type ON service_management.order_items(order_id, item_type);
|
||||
|
||||
-- -------------------------------------------
|
||||
-- ORDER_STATUS_HISTORY - Historial de estados
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE service_management.order_status_history (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
order_id UUID NOT NULL REFERENCES service_management.service_orders(id) ON DELETE CASCADE,
|
||||
|
||||
from_status VARCHAR(30),
|
||||
to_status VARCHAR(30) NOT NULL,
|
||||
|
||||
changed_by UUID,
|
||||
notes TEXT,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_status_history_order ON service_management.order_status_history(order_id);
|
||||
|
||||
-- -------------------------------------------
|
||||
-- WORK_BAYS - Bahías de trabajo
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE service_management.work_bays (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
name VARCHAR(50) NOT NULL,
|
||||
description VARCHAR(200),
|
||||
|
||||
bay_type VARCHAR(50) CHECK (bay_type IN ('general', 'diagnostic', 'heavy_duty', 'quick_service')),
|
||||
|
||||
-- Estado (current_order_id es NULLABLE para evitar referencia circular)
|
||||
status VARCHAR(20) DEFAULT 'available' CHECK (status IN ('available', 'occupied', 'maintenance')),
|
||||
current_order_id UUID, -- NULLABLE - se actualiza después
|
||||
|
||||
-- Capacidad
|
||||
max_weight DECIMAL(10,2) CHECK (max_weight > 0),
|
||||
has_lift BOOLEAN DEFAULT FALSE,
|
||||
has_pit BOOLEAN DEFAULT FALSE,
|
||||
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_bay_name UNIQUE (tenant_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_bays_tenant ON service_management.work_bays(tenant_id);
|
||||
CREATE INDEX idx_bays_status ON service_management.work_bays(tenant_id, status);
|
||||
|
||||
CREATE TRIGGER trg_work_bays_updated_at
|
||||
BEFORE UPDATE ON service_management.work_bays
|
||||
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
SELECT create_tenant_rls_policies('service_management', 'work_bays');
|
||||
|
||||
-- FK de service_orders.bay_id (se agrega después de crear work_bays)
|
||||
ALTER TABLE service_management.service_orders
|
||||
ADD CONSTRAINT fk_orders_bay
|
||||
FOREIGN KEY (bay_id) REFERENCES service_management.work_bays(id);
|
||||
|
||||
-- -------------------------------------------
|
||||
-- DIAGNOSTICS - Diagnósticos realizados
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE service_management.diagnostics (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
order_id UUID REFERENCES service_management.service_orders(id),
|
||||
vehicle_id UUID NOT NULL,
|
||||
|
||||
diagnostic_type VARCHAR(50) NOT NULL
|
||||
CHECK (diagnostic_type IN ('scanner', 'injector_test', 'pump_test', 'compression', 'turbo_test', 'other')),
|
||||
|
||||
equipment VARCHAR(200),
|
||||
|
||||
performed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
performed_by UUID,
|
||||
|
||||
result VARCHAR(20) CHECK (result IN ('pass', 'fail', 'needs_attention')),
|
||||
summary TEXT,
|
||||
|
||||
raw_data JSONB,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_diagnostics_tenant ON service_management.diagnostics(tenant_id);
|
||||
CREATE INDEX idx_diagnostics_vehicle ON service_management.diagnostics(vehicle_id);
|
||||
CREATE INDEX idx_diagnostics_order ON service_management.diagnostics(order_id);
|
||||
CREATE INDEX idx_diagnostics_date ON service_management.diagnostics(performed_at DESC);
|
||||
|
||||
CREATE TRIGGER trg_diagnostics_updated_at
|
||||
BEFORE UPDATE ON service_management.diagnostics
|
||||
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
SELECT create_tenant_rls_policies('service_management', 'diagnostics');
|
||||
|
||||
-- -------------------------------------------
|
||||
-- DIAGNOSTIC_ITEMS - Hallazgos del diagnóstico
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE service_management.diagnostic_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
diagnostic_id UUID NOT NULL REFERENCES service_management.diagnostics(id) ON DELETE CASCADE,
|
||||
|
||||
item_type VARCHAR(50) NOT NULL
|
||||
CHECK (item_type IN ('dtc_code', 'test_result', 'measurement', 'observation')),
|
||||
|
||||
-- Para códigos DTC
|
||||
code VARCHAR(20),
|
||||
description VARCHAR(500),
|
||||
severity VARCHAR(20) CHECK (severity IN ('critical', 'warning', 'info')),
|
||||
|
||||
-- Para mediciones
|
||||
parameter VARCHAR(100),
|
||||
value DECIMAL(12,4),
|
||||
unit VARCHAR(20),
|
||||
min_ref DECIMAL(12,4),
|
||||
max_ref DECIMAL(12,4),
|
||||
status VARCHAR(20) CHECK (status IN ('ok', 'warning', 'fail', 'no_reference')),
|
||||
|
||||
-- Componente
|
||||
component VARCHAR(100),
|
||||
cylinder INTEGER CHECK (cylinder >= 1 AND cylinder <= 12),
|
||||
|
||||
notes TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_diag_items_diagnostic ON service_management.diagnostic_items(diagnostic_id);
|
||||
|
||||
-- -------------------------------------------
|
||||
-- DIAGNOSTIC_PHOTOS - Fotos de evidencia
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE service_management.diagnostic_photos (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
diagnostic_id UUID NOT NULL REFERENCES service_management.diagnostics(id) ON DELETE CASCADE,
|
||||
|
||||
url VARCHAR(500) NOT NULL,
|
||||
thumbnail_url VARCHAR(500),
|
||||
|
||||
description VARCHAR(300),
|
||||
category VARCHAR(50) CHECK (category IN ('before', 'damage', 'process', 'after', 'other')),
|
||||
|
||||
file_size INTEGER,
|
||||
mime_type VARCHAR(50),
|
||||
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
uploaded_by UUID,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_diag_photos_diagnostic ON service_management.diagnostic_photos(diagnostic_id);
|
||||
|
||||
-- -------------------------------------------
|
||||
-- DIAGNOSTIC_RECOMMENDATIONS - Recomendaciones
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE service_management.diagnostic_recommendations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
diagnostic_id UUID NOT NULL REFERENCES service_management.diagnostics(id) ON DELETE CASCADE,
|
||||
diagnostic_item_id UUID REFERENCES service_management.diagnostic_items(id),
|
||||
|
||||
description TEXT NOT NULL,
|
||||
|
||||
priority VARCHAR(20) DEFAULT 'medium'
|
||||
CHECK (priority IN ('critical', 'high', 'medium', 'low')),
|
||||
urgency VARCHAR(20) DEFAULT 'soon'
|
||||
CHECK (urgency IN ('immediate', 'soon', 'scheduled', 'preventive')),
|
||||
|
||||
suggested_service_id UUID,
|
||||
estimated_cost DECIMAL(12,2) CHECK (estimated_cost >= 0),
|
||||
|
||||
status VARCHAR(20) DEFAULT 'pending'
|
||||
CHECK (status IN ('pending', 'approved', 'declined', 'completed')),
|
||||
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_recommendations_diagnostic ON service_management.diagnostic_recommendations(diagnostic_id);
|
||||
|
||||
-- -------------------------------------------
|
||||
-- QUOTES - Cotizaciones
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE service_management.quotes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
quote_number VARCHAR(20) NOT NULL,
|
||||
|
||||
customer_id UUID NOT NULL,
|
||||
vehicle_id UUID NOT NULL,
|
||||
diagnostic_id UUID REFERENCES service_management.diagnostics(id),
|
||||
|
||||
status VARCHAR(20) DEFAULT 'draft'
|
||||
CHECK (status IN ('draft', 'sent', 'viewed', 'approved', 'rejected', 'expired', 'converted')),
|
||||
|
||||
-- Fechas
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
sent_at TIMESTAMP WITH TIME ZONE,
|
||||
viewed_at TIMESTAMP WITH TIME ZONE,
|
||||
responded_at TIMESTAMP WITH TIME ZONE,
|
||||
expires_at TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
-- Totales
|
||||
labor_total DECIMAL(12,2) DEFAULT 0 CHECK (labor_total >= 0),
|
||||
parts_total DECIMAL(12,2) DEFAULT 0 CHECK (parts_total >= 0),
|
||||
discount_amount DECIMAL(12,2) DEFAULT 0 CHECK (discount_amount >= 0),
|
||||
discount_percent DECIMAL(5,2) DEFAULT 0 CHECK (discount_percent >= 0 AND discount_percent <= 100),
|
||||
discount_reason VARCHAR(200),
|
||||
tax DECIMAL(12,2) DEFAULT 0 CHECK (tax >= 0),
|
||||
grand_total DECIMAL(12,2) DEFAULT 0 CHECK (grand_total >= 0),
|
||||
|
||||
validity_days INTEGER DEFAULT 15 CHECK (validity_days > 0),
|
||||
|
||||
terms TEXT,
|
||||
notes TEXT,
|
||||
|
||||
-- Conversión a orden
|
||||
converted_order_id UUID REFERENCES service_management.service_orders(id),
|
||||
|
||||
-- Aprobación digital
|
||||
approved_by_name VARCHAR(200),
|
||||
approval_signature TEXT,
|
||||
approval_ip INET,
|
||||
|
||||
created_by UUID,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_quote_number UNIQUE (tenant_id, quote_number)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_quotes_tenant ON service_management.quotes(tenant_id);
|
||||
CREATE INDEX idx_quotes_status ON service_management.quotes(tenant_id, status);
|
||||
CREATE INDEX idx_quotes_customer ON service_management.quotes(customer_id);
|
||||
CREATE INDEX idx_quotes_created ON service_management.quotes(created_at DESC);
|
||||
|
||||
CREATE TRIGGER trg_quotes_updated_at
|
||||
BEFORE UPDATE ON service_management.quotes
|
||||
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
SELECT create_tenant_rls_policies('service_management', 'quotes');
|
||||
|
||||
-- -------------------------------------------
|
||||
-- QUOTE_ITEMS - Líneas de cotización
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE service_management.quote_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
quote_id UUID NOT NULL REFERENCES service_management.quotes(id) ON DELETE CASCADE,
|
||||
|
||||
item_type VARCHAR(20) NOT NULL CHECK (item_type IN ('service', 'part')),
|
||||
|
||||
service_id UUID,
|
||||
part_id UUID,
|
||||
|
||||
description VARCHAR(500) NOT NULL,
|
||||
|
||||
quantity DECIMAL(10,3) DEFAULT 1 CHECK (quantity > 0),
|
||||
unit_price DECIMAL(12,2) NOT NULL CHECK (unit_price >= 0),
|
||||
discount_pct DECIMAL(5,2) DEFAULT 0 CHECK (discount_pct >= 0 AND discount_pct <= 100),
|
||||
subtotal DECIMAL(12,2) NOT NULL CHECK (subtotal >= 0),
|
||||
|
||||
is_approved BOOLEAN DEFAULT TRUE,
|
||||
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_quote_items_quote ON service_management.quote_items(quote_id);
|
||||
|
||||
-- -------------------------------------------
|
||||
-- QUOTE_TRACKING - Tracking de cotizaciones
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE service_management.quote_tracking (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
quote_id UUID NOT NULL REFERENCES service_management.quotes(id) ON DELETE CASCADE,
|
||||
|
||||
event_type VARCHAR(30) NOT NULL
|
||||
CHECK (event_type IN ('sent_email', 'sent_whatsapp', 'opened', 'link_clicked', 'approved', 'rejected')),
|
||||
channel VARCHAR(20) CHECK (channel IN ('email', 'whatsapp', 'link')),
|
||||
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
device_type VARCHAR(20),
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_quote_tracking_quote ON service_management.quote_tracking(quote_id);
|
||||
|
||||
-- -------------------------------------------
|
||||
-- QUOTE_FOLLOWUPS - Seguimiento de cotizaciones
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE service_management.quote_followups (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
quote_id UUID NOT NULL REFERENCES service_management.quotes(id) ON DELETE CASCADE,
|
||||
|
||||
action VARCHAR(100) NOT NULL,
|
||||
notes TEXT,
|
||||
|
||||
next_action VARCHAR(100),
|
||||
next_action_at TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
created_by UUID,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_quote_followups_quote ON service_management.quote_followups(quote_id);
|
||||
|
||||
-- -------------------------------------------
|
||||
-- TEST_TYPES - Tipos de prueba configurables
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE service_management.test_types (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID, -- NULL = global
|
||||
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
|
||||
component_type VARCHAR(50) CHECK (component_type IN ('injector', 'pump', 'turbo', 'engine', 'other')),
|
||||
|
||||
is_system BOOLEAN DEFAULT FALSE,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_test_types_tenant ON service_management.test_types(tenant_id);
|
||||
|
||||
CREATE TRIGGER trg_test_types_updated_at
|
||||
BEFORE UPDATE ON service_management.test_types
|
||||
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
-- -------------------------------------------
|
||||
-- TEST_PARAMETERS - Parámetros de prueba
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE service_management.test_parameters (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
test_type_id UUID NOT NULL REFERENCES service_management.test_types(id) ON DELETE CASCADE,
|
||||
|
||||
name VARCHAR(100) NOT NULL,
|
||||
unit VARCHAR(20),
|
||||
data_type VARCHAR(20) DEFAULT 'numeric' CHECK (data_type IN ('numeric', 'boolean', 'text')),
|
||||
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_test_params_type ON service_management.test_parameters(test_type_id);
|
||||
|
||||
-- -------------------------------------------
|
||||
-- PARAMETER_REFERENCES - Valores de referencia por motor
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE service_management.parameter_references (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
parameter_id UUID NOT NULL REFERENCES service_management.test_parameters(id) ON DELETE CASCADE,
|
||||
engine_model_id UUID, -- vehicle_management.engine_catalog
|
||||
|
||||
min_value DECIMAL(12,4),
|
||||
max_value DECIMAL(12,4),
|
||||
nominal_value DECIMAL(12,4),
|
||||
tolerance_pct DECIMAL(5,2) CHECK (tolerance_pct >= 0),
|
||||
|
||||
source VARCHAR(200),
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_param_engine UNIQUE (parameter_id, engine_model_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_param_refs_param ON service_management.parameter_references(parameter_id);
|
||||
CREATE INDEX idx_param_refs_engine ON service_management.parameter_references(engine_model_id);
|
||||
|
||||
-- -------------------------------------------
|
||||
-- SERVICE_CATEGORIES - Categorías de servicios
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE service_management.service_categories (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description VARCHAR(300),
|
||||
color VARCHAR(7),
|
||||
icon VARCHAR(50),
|
||||
|
||||
parent_id UUID REFERENCES service_management.service_categories(id),
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_svc_categories_tenant ON service_management.service_categories(tenant_id);
|
||||
CREATE INDEX idx_svc_categories_parent ON service_management.service_categories(parent_id);
|
||||
|
||||
CREATE TRIGGER trg_service_categories_updated_at
|
||||
BEFORE UPDATE ON service_management.service_categories
|
||||
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
SELECT create_tenant_rls_policies('service_management', 'service_categories');
|
||||
|
||||
-- -------------------------------------------
|
||||
-- SERVICES - Catálogo de servicios
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE service_management.services (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
code VARCHAR(20) NOT NULL,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
description TEXT,
|
||||
|
||||
category_id UUID REFERENCES service_management.service_categories(id),
|
||||
|
||||
price DECIMAL(12,2) NOT NULL CHECK (price >= 0),
|
||||
cost DECIMAL(12,2) CHECK (cost >= 0),
|
||||
|
||||
estimated_hours DECIMAL(5,2) CHECK (estimated_hours >= 0),
|
||||
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_service_code UNIQUE (tenant_id, code)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_services_tenant ON service_management.services(tenant_id);
|
||||
CREATE INDEX idx_services_category ON service_management.services(category_id);
|
||||
CREATE INDEX idx_services_code ON service_management.services(code);
|
||||
|
||||
CREATE TRIGGER trg_services_updated_at
|
||||
BEFORE UPDATE ON service_management.services
|
||||
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
SELECT create_tenant_rls_policies('service_management', 'services');
|
||||
91
init/03.5-customers-table.sql
Normal file
91
init/03.5-customers-table.sql
Normal file
@ -0,0 +1,91 @@
|
||||
-- ===========================================
|
||||
-- MECANICAS DIESEL - Customers Table
|
||||
-- ===========================================
|
||||
-- Tabla de clientes para el taller
|
||||
-- Nota: Esta tabla se crea en service_management para mantener
|
||||
-- consistencia con las referencias de service_orders
|
||||
|
||||
SET search_path TO service_management, public;
|
||||
|
||||
-- -------------------------------------------
|
||||
-- CUSTOMERS - Clientes del taller
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS service_management.customers (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL, -- Identificador del taller (multi-tenant)
|
||||
|
||||
-- Tipo de cliente
|
||||
customer_type VARCHAR(20) DEFAULT 'individual'
|
||||
CHECK (customer_type IN ('individual', 'company', 'fleet')),
|
||||
|
||||
-- Identificación
|
||||
name VARCHAR(200) NOT NULL,
|
||||
legal_name VARCHAR(300),
|
||||
rfc VARCHAR(13),
|
||||
|
||||
-- Contacto
|
||||
email VARCHAR(200),
|
||||
phone VARCHAR(20),
|
||||
phone_secondary VARCHAR(20),
|
||||
|
||||
-- Dirección
|
||||
address TEXT,
|
||||
city VARCHAR(100),
|
||||
state VARCHAR(100),
|
||||
postal_code VARCHAR(10),
|
||||
|
||||
-- Términos comerciales (para empresas/flotas)
|
||||
credit_days INTEGER DEFAULT 0 CHECK (credit_days >= 0),
|
||||
credit_limit DECIMAL(12,2) DEFAULT 0 CHECK (credit_limit >= 0),
|
||||
discount_labor_pct DECIMAL(5,2) DEFAULT 0 CHECK (discount_labor_pct >= 0 AND discount_labor_pct <= 100),
|
||||
discount_parts_pct DECIMAL(5,2) DEFAULT 0 CHECK (discount_parts_pct >= 0 AND discount_parts_pct <= 100),
|
||||
|
||||
-- Estadísticas
|
||||
total_orders INTEGER DEFAULT 0 CHECK (total_orders >= 0),
|
||||
total_spent DECIMAL(14,2) DEFAULT 0 CHECK (total_spent >= 0),
|
||||
last_visit_at TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
-- Notas y preferencias
|
||||
notes TEXT,
|
||||
preferred_contact VARCHAR(20) DEFAULT 'phone'
|
||||
CHECK (preferred_contact IN ('phone', 'email', 'whatsapp')),
|
||||
|
||||
-- Estado
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
|
||||
-- Audit
|
||||
created_by UUID,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT uq_customer_email UNIQUE (tenant_id, email),
|
||||
CONSTRAINT uq_customer_rfc UNIQUE (tenant_id, rfc)
|
||||
);
|
||||
|
||||
-- Índices
|
||||
CREATE INDEX idx_customers_tenant ON service_management.customers(tenant_id);
|
||||
CREATE INDEX idx_customers_email ON service_management.customers(tenant_id, email);
|
||||
CREATE INDEX idx_customers_phone ON service_management.customers(phone);
|
||||
CREATE INDEX idx_customers_rfc ON service_management.customers(rfc);
|
||||
CREATE INDEX idx_customers_type ON service_management.customers(tenant_id, customer_type);
|
||||
CREATE INDEX idx_customers_name ON service_management.customers(tenant_id, name);
|
||||
CREATE INDEX idx_customers_active ON service_management.customers(tenant_id, is_active);
|
||||
|
||||
-- Trigger updated_at
|
||||
CREATE TRIGGER trg_customers_updated_at
|
||||
BEFORE UPDATE ON service_management.customers
|
||||
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
-- RLS
|
||||
SELECT create_tenant_rls_policies('service_management', 'customers');
|
||||
|
||||
-- Comentarios
|
||||
COMMENT ON TABLE service_management.customers IS 'Clientes del taller mecánico';
|
||||
COMMENT ON COLUMN service_management.customers.customer_type IS 'Tipo: individual (persona física), company (empresa), fleet (flota)';
|
||||
COMMENT ON COLUMN service_management.customers.credit_days IS 'Días de crédito otorgados al cliente';
|
||||
COMMENT ON COLUMN service_management.customers.credit_limit IS 'Límite de crédito en pesos';
|
||||
COMMENT ON COLUMN service_management.customers.discount_labor_pct IS 'Descuento en mano de obra (%)';
|
||||
COMMENT ON COLUMN service_management.customers.discount_parts_pct IS 'Descuento en refacciones (%)';
|
||||
COMMENT ON COLUMN service_management.customers.total_orders IS 'Total de órdenes de servicio acumuladas';
|
||||
COMMENT ON COLUMN service_management.customers.total_spent IS 'Total gastado por el cliente';
|
||||
397
init/04-parts-management-tables.sql
Normal file
397
init/04-parts-management-tables.sql
Normal file
@ -0,0 +1,397 @@
|
||||
-- ===========================================
|
||||
-- MECANICAS DIESEL - Schema parts_management
|
||||
-- ===========================================
|
||||
-- Inventario de refacciones especifico del taller
|
||||
|
||||
SET search_path TO parts_management, public;
|
||||
|
||||
-- -------------------------------------------
|
||||
-- PART_CATEGORIES - Categorías de refacciones
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE parts_management.part_categories (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description VARCHAR(300),
|
||||
|
||||
parent_id UUID REFERENCES parts_management.part_categories(id),
|
||||
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_part_categories_tenant ON parts_management.part_categories(tenant_id);
|
||||
CREATE INDEX idx_part_categories_parent ON parts_management.part_categories(parent_id);
|
||||
|
||||
CREATE TRIGGER trg_part_categories_updated_at
|
||||
BEFORE UPDATE ON parts_management.part_categories
|
||||
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
SELECT create_tenant_rls_policies('parts_management', 'part_categories');
|
||||
|
||||
-- -------------------------------------------
|
||||
-- SUPPLIERS - Proveedores
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE parts_management.suppliers (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
name VARCHAR(200) NOT NULL,
|
||||
legal_name VARCHAR(300),
|
||||
rfc VARCHAR(13),
|
||||
|
||||
contact_name VARCHAR(200),
|
||||
email VARCHAR(200),
|
||||
phone VARCHAR(20),
|
||||
|
||||
address TEXT,
|
||||
|
||||
credit_days INTEGER DEFAULT 0 CHECK (credit_days >= 0),
|
||||
discount_pct DECIMAL(5,2) DEFAULT 0 CHECK (discount_pct >= 0 AND discount_pct <= 100),
|
||||
|
||||
rating DECIMAL(3,2) CHECK (rating >= 0 AND rating <= 5),
|
||||
|
||||
notes TEXT,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_suppliers_tenant ON parts_management.suppliers(tenant_id);
|
||||
CREATE INDEX idx_suppliers_name ON parts_management.suppliers(name);
|
||||
|
||||
CREATE TRIGGER trg_suppliers_updated_at
|
||||
BEFORE UPDATE ON parts_management.suppliers
|
||||
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
SELECT create_tenant_rls_policies('parts_management', 'suppliers');
|
||||
|
||||
-- -------------------------------------------
|
||||
-- WAREHOUSE_LOCATIONS - Ubicaciones de almacén
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE parts_management.warehouse_locations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
code VARCHAR(20) NOT NULL,
|
||||
name VARCHAR(100),
|
||||
description VARCHAR(200),
|
||||
|
||||
zone VARCHAR(10),
|
||||
aisle VARCHAR(10),
|
||||
level VARCHAR(10),
|
||||
|
||||
max_weight DECIMAL(10,2) CHECK (max_weight > 0),
|
||||
max_volume DECIMAL(10,2) CHECK (max_volume > 0),
|
||||
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_location_code UNIQUE (tenant_id, code)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_locations_tenant ON parts_management.warehouse_locations(tenant_id);
|
||||
CREATE INDEX idx_locations_zone ON parts_management.warehouse_locations(zone);
|
||||
|
||||
CREATE TRIGGER trg_warehouse_locations_updated_at
|
||||
BEFORE UPDATE ON parts_management.warehouse_locations
|
||||
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
SELECT create_tenant_rls_policies('parts_management', 'warehouse_locations');
|
||||
|
||||
-- -------------------------------------------
|
||||
-- PARTS - Refacciones del taller
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE parts_management.parts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
sku VARCHAR(50) NOT NULL,
|
||||
name VARCHAR(300) NOT NULL,
|
||||
description TEXT,
|
||||
|
||||
category_id UUID REFERENCES parts_management.part_categories(id),
|
||||
|
||||
brand VARCHAR(100),
|
||||
manufacturer VARCHAR(100),
|
||||
|
||||
compatible_engines TEXT[],
|
||||
|
||||
cost DECIMAL(12,2) CHECK (cost >= 0),
|
||||
price DECIMAL(12,2) NOT NULL CHECK (price >= 0),
|
||||
|
||||
-- Inventario
|
||||
current_stock DECIMAL(10,3) DEFAULT 0 CHECK (current_stock >= 0),
|
||||
reserved_stock DECIMAL(10,3) DEFAULT 0 CHECK (reserved_stock >= 0),
|
||||
min_stock DECIMAL(10,3) DEFAULT 0 CHECK (min_stock >= 0),
|
||||
max_stock DECIMAL(10,3) CHECK (max_stock > 0),
|
||||
reorder_point DECIMAL(10,3) CHECK (reorder_point >= 0),
|
||||
|
||||
location_id UUID REFERENCES parts_management.warehouse_locations(id),
|
||||
|
||||
unit VARCHAR(20) DEFAULT 'pza',
|
||||
|
||||
barcode VARCHAR(50),
|
||||
|
||||
preferred_supplier_id UUID REFERENCES parts_management.suppliers(id),
|
||||
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_part_sku UNIQUE (tenant_id, sku),
|
||||
CONSTRAINT chk_min_max_stock CHECK (max_stock IS NULL OR max_stock >= min_stock)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_parts_tenant ON parts_management.parts(tenant_id);
|
||||
CREATE INDEX idx_parts_sku ON parts_management.parts(sku);
|
||||
CREATE INDEX idx_parts_barcode ON parts_management.parts(barcode);
|
||||
CREATE INDEX idx_parts_category ON parts_management.parts(category_id);
|
||||
CREATE INDEX idx_parts_supplier ON parts_management.parts(preferred_supplier_id);
|
||||
CREATE INDEX idx_parts_location ON parts_management.parts(location_id);
|
||||
|
||||
CREATE TRIGGER trg_parts_updated_at
|
||||
BEFORE UPDATE ON parts_management.parts
|
||||
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
SELECT create_tenant_rls_policies('parts_management', 'parts');
|
||||
|
||||
-- -------------------------------------------
|
||||
-- PART_ALTERNATES - Códigos alternos
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE parts_management.part_alternates (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
part_id UUID NOT NULL REFERENCES parts_management.parts(id) ON DELETE CASCADE,
|
||||
|
||||
alternate_code VARCHAR(50) NOT NULL,
|
||||
manufacturer VARCHAR(100),
|
||||
|
||||
is_preferred BOOLEAN DEFAULT FALSE,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_alternates_code ON parts_management.part_alternates(alternate_code);
|
||||
CREATE INDEX idx_alternates_part ON parts_management.part_alternates(part_id);
|
||||
|
||||
-- -------------------------------------------
|
||||
-- PART_LOCATIONS - Multi-ubicación por refacción
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE parts_management.part_locations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
part_id UUID NOT NULL REFERENCES parts_management.parts(id) ON DELETE CASCADE,
|
||||
location_id UUID NOT NULL REFERENCES parts_management.warehouse_locations(id),
|
||||
|
||||
quantity DECIMAL(10,3) DEFAULT 0 CHECK (quantity >= 0),
|
||||
is_primary BOOLEAN DEFAULT FALSE,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_part_location UNIQUE (part_id, location_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_part_locations_part ON parts_management.part_locations(part_id);
|
||||
CREATE INDEX idx_part_locations_location ON parts_management.part_locations(location_id);
|
||||
|
||||
-- -------------------------------------------
|
||||
-- INVENTORY_MOVEMENTS - Kardex
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE parts_management.inventory_movements (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
part_id UUID NOT NULL REFERENCES parts_management.parts(id),
|
||||
|
||||
movement_type VARCHAR(30) NOT NULL
|
||||
CHECK (movement_type IN ('purchase', 'consumption', 'adjustment_in', 'adjustment_out', 'return', 'transfer')),
|
||||
|
||||
reference_type VARCHAR(30) CHECK (reference_type IN ('service_order', 'purchase_order', 'adjustment', 'return')),
|
||||
reference_id UUID,
|
||||
reference_number VARCHAR(50),
|
||||
|
||||
quantity DECIMAL(10,3) NOT NULL,
|
||||
previous_stock DECIMAL(10,3) NOT NULL,
|
||||
new_stock DECIMAL(10,3) NOT NULL,
|
||||
|
||||
unit_cost DECIMAL(12,2) CHECK (unit_cost >= 0),
|
||||
total_cost DECIMAL(12,2) CHECK (total_cost >= 0),
|
||||
|
||||
location_id UUID REFERENCES parts_management.warehouse_locations(id),
|
||||
|
||||
notes TEXT,
|
||||
|
||||
created_by UUID,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_movements_tenant ON parts_management.inventory_movements(tenant_id);
|
||||
CREATE INDEX idx_movements_part ON parts_management.inventory_movements(part_id);
|
||||
CREATE INDEX idx_movements_date ON parts_management.inventory_movements(created_at DESC);
|
||||
CREATE INDEX idx_movements_reference ON parts_management.inventory_movements(reference_type, reference_id);
|
||||
|
||||
SELECT create_tenant_rls_policies('parts_management', 'inventory_movements');
|
||||
|
||||
-- -------------------------------------------
|
||||
-- INVENTORY_ADJUSTMENTS - Ajustes de inventario
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE parts_management.inventory_adjustments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
adjustment_number VARCHAR(20) NOT NULL,
|
||||
|
||||
adjustment_type VARCHAR(30) NOT NULL
|
||||
CHECK (adjustment_type IN ('count', 'damage', 'expiry', 'correction', 'other')),
|
||||
|
||||
status VARCHAR(20) DEFAULT 'pending'
|
||||
CHECK (status IN ('pending', 'approved', 'applied', 'rejected')),
|
||||
|
||||
total_items INTEGER DEFAULT 0,
|
||||
total_value DECIMAL(12,2) DEFAULT 0,
|
||||
|
||||
reason TEXT NOT NULL,
|
||||
notes TEXT,
|
||||
|
||||
approved_by UUID,
|
||||
approved_at TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
created_by UUID,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_adjustment_number UNIQUE (tenant_id, adjustment_number)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_adjustments_tenant ON parts_management.inventory_adjustments(tenant_id);
|
||||
CREATE INDEX idx_adjustments_status ON parts_management.inventory_adjustments(status);
|
||||
|
||||
CREATE TRIGGER trg_adjustments_updated_at
|
||||
BEFORE UPDATE ON parts_management.inventory_adjustments
|
||||
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
SELECT create_tenant_rls_policies('parts_management', 'inventory_adjustments');
|
||||
|
||||
-- -------------------------------------------
|
||||
-- ADJUSTMENT_ITEMS - Ítems del ajuste
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE parts_management.adjustment_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
adjustment_id UUID NOT NULL REFERENCES parts_management.inventory_adjustments(id) ON DELETE CASCADE,
|
||||
|
||||
part_id UUID NOT NULL REFERENCES parts_management.parts(id),
|
||||
|
||||
system_qty DECIMAL(10,3) NOT NULL,
|
||||
physical_qty DECIMAL(10,3) NOT NULL,
|
||||
difference DECIMAL(10,3) NOT NULL,
|
||||
|
||||
unit_cost DECIMAL(12,2) CHECK (unit_cost >= 0),
|
||||
value_impact DECIMAL(12,2),
|
||||
|
||||
notes TEXT,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_adjustment_items_adjustment ON parts_management.adjustment_items(adjustment_id);
|
||||
CREATE INDEX idx_adjustment_items_part ON parts_management.adjustment_items(part_id);
|
||||
|
||||
-- -------------------------------------------
|
||||
-- STOCK_ALERTS - Alertas de stock
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE parts_management.stock_alerts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
part_id UUID NOT NULL REFERENCES parts_management.parts(id),
|
||||
|
||||
alert_type VARCHAR(30) NOT NULL
|
||||
CHECK (alert_type IN ('low_stock', 'out_of_stock', 'overstock')),
|
||||
|
||||
current_stock DECIMAL(10,3),
|
||||
threshold DECIMAL(10,3),
|
||||
|
||||
status VARCHAR(20) DEFAULT 'active'
|
||||
CHECK (status IN ('active', 'acknowledged', 'resolved')),
|
||||
|
||||
acknowledged_by UUID,
|
||||
acknowledged_at TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_alerts_tenant ON parts_management.stock_alerts(tenant_id);
|
||||
CREATE INDEX idx_alerts_status ON parts_management.stock_alerts(status);
|
||||
CREATE INDEX idx_alerts_part ON parts_management.stock_alerts(part_id);
|
||||
|
||||
SELECT create_tenant_rls_policies('parts_management', 'stock_alerts');
|
||||
|
||||
-- -------------------------------------------
|
||||
-- PHYSICAL_INVENTORY - Sesiones de inventario físico
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE parts_management.physical_inventory (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
inventory_number VARCHAR(20) NOT NULL,
|
||||
|
||||
inventory_type VARCHAR(30) NOT NULL
|
||||
CHECK (inventory_type IN ('full', 'zone', 'category', 'cyclic')),
|
||||
|
||||
status VARCHAR(20) DEFAULT 'in_progress'
|
||||
CHECK (status IN ('in_progress', 'completed', 'cancelled')),
|
||||
|
||||
zones TEXT[],
|
||||
categories UUID[],
|
||||
|
||||
total_items INTEGER DEFAULT 0,
|
||||
counted_items INTEGER DEFAULT 0,
|
||||
with_difference INTEGER DEFAULT 0,
|
||||
|
||||
started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
completed_at TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
adjustment_id UUID REFERENCES parts_management.inventory_adjustments(id),
|
||||
|
||||
created_by UUID,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_inventory_number UNIQUE (tenant_id, inventory_number)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_physical_inv_tenant ON parts_management.physical_inventory(tenant_id);
|
||||
CREATE INDEX idx_physical_inv_status ON parts_management.physical_inventory(status);
|
||||
|
||||
SELECT create_tenant_rls_policies('parts_management', 'physical_inventory');
|
||||
|
||||
-- -------------------------------------------
|
||||
-- INVENTORY_COUNTS - Conteos del inventario físico
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE parts_management.inventory_counts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
inventory_id UUID NOT NULL REFERENCES parts_management.physical_inventory(id) ON DELETE CASCADE,
|
||||
|
||||
part_id UUID NOT NULL REFERENCES parts_management.parts(id),
|
||||
location_id UUID REFERENCES parts_management.warehouse_locations(id),
|
||||
|
||||
system_qty DECIMAL(10,3) NOT NULL,
|
||||
counted_qty DECIMAL(10,3),
|
||||
difference DECIMAL(10,3),
|
||||
|
||||
counted_by UUID,
|
||||
counted_at TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
notes TEXT,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_count_part_location UNIQUE (inventory_id, part_id, location_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_inventory_counts_inventory ON parts_management.inventory_counts(inventory_id);
|
||||
CREATE INDEX idx_inventory_counts_part ON parts_management.inventory_counts(part_id);
|
||||
365
init/05-vehicle-management-tables.sql
Normal file
365
init/05-vehicle-management-tables.sql
Normal file
@ -0,0 +1,365 @@
|
||||
-- ===========================================
|
||||
-- MECANICAS DIESEL - Schema vehicle_management
|
||||
-- ===========================================
|
||||
-- Vehículos diesel, flotas, motores
|
||||
|
||||
SET search_path TO vehicle_management, public;
|
||||
|
||||
-- -------------------------------------------
|
||||
-- ENGINE_CATALOG - Catálogo de motores (global)
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE vehicle_management.engine_catalog (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
make VARCHAR(50) NOT NULL,
|
||||
model VARCHAR(50) NOT NULL,
|
||||
|
||||
cylinders INTEGER CHECK (cylinders > 0),
|
||||
displacement DECIMAL(5,2) CHECK (displacement > 0),
|
||||
|
||||
fuel_type VARCHAR(20) DEFAULT 'diesel',
|
||||
|
||||
horsepower_min INTEGER CHECK (horsepower_min > 0),
|
||||
horsepower_max INTEGER CHECK (horsepower_max > 0),
|
||||
torque_max INTEGER CHECK (torque_max > 0),
|
||||
|
||||
injection_system VARCHAR(50),
|
||||
|
||||
year_start INTEGER CHECK (year_start >= 1950),
|
||||
year_end INTEGER CHECK (year_end >= 1950),
|
||||
|
||||
notes TEXT,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_engine_model UNIQUE (make, model),
|
||||
CONSTRAINT chk_horsepower CHECK (horsepower_max >= horsepower_min),
|
||||
CONSTRAINT chk_years CHECK (year_end IS NULL OR year_end >= year_start)
|
||||
);
|
||||
|
||||
-- Datos iniciales de motores comunes
|
||||
INSERT INTO vehicle_management.engine_catalog (make, model, cylinders, displacement, horsepower_min, horsepower_max, torque_max, injection_system) VALUES
|
||||
('Cummins', 'ISX15', 6, 14.9, 400, 600, 2050, 'common_rail'),
|
||||
('Cummins', 'ISL9', 6, 8.9, 260, 380, 1250, 'common_rail'),
|
||||
('Cummins', 'X15', 6, 14.9, 400, 605, 2050, 'common_rail'),
|
||||
('Cummins', 'ISB6.7', 6, 6.7, 200, 325, 750, 'common_rail'),
|
||||
('Detroit', 'DD15', 6, 14.8, 400, 505, 1850, 'common_rail'),
|
||||
('Detroit', 'DD13', 6, 12.8, 350, 470, 1650, 'common_rail'),
|
||||
('Paccar', 'MX-13', 6, 12.9, 380, 510, 1850, 'common_rail'),
|
||||
('Paccar', 'MX-11', 6, 10.8, 355, 430, 1550, 'common_rail'),
|
||||
('Navistar', 'MaxxForce 13', 6, 12.4, 410, 475, 1700, 'common_rail'),
|
||||
('Volvo', 'D13', 6, 12.8, 375, 500, 1850, 'unit_injector'),
|
||||
('Caterpillar', 'C15', 6, 15.2, 435, 625, 2050, 'unit_injector'),
|
||||
('Caterpillar', 'C13', 6, 12.5, 380, 520, 1750, 'unit_injector');
|
||||
|
||||
-- -------------------------------------------
|
||||
-- FLEETS - Flotas de vehículos
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE vehicle_management.fleets (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
name VARCHAR(200) NOT NULL,
|
||||
code VARCHAR(20),
|
||||
|
||||
contact_name VARCHAR(200),
|
||||
contact_email VARCHAR(200),
|
||||
contact_phone VARCHAR(20),
|
||||
|
||||
-- Condiciones comerciales
|
||||
discount_labor_pct DECIMAL(5,2) DEFAULT 0 CHECK (discount_labor_pct >= 0 AND discount_labor_pct <= 100),
|
||||
discount_parts_pct DECIMAL(5,2) DEFAULT 0 CHECK (discount_parts_pct >= 0 AND discount_parts_pct <= 100),
|
||||
credit_days INTEGER DEFAULT 0 CHECK (credit_days >= 0),
|
||||
credit_limit DECIMAL(12,2) DEFAULT 0 CHECK (credit_limit >= 0),
|
||||
|
||||
vehicle_count INTEGER DEFAULT 0 CHECK (vehicle_count >= 0),
|
||||
|
||||
notes TEXT,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_fleets_tenant ON vehicle_management.fleets(tenant_id);
|
||||
CREATE INDEX idx_fleets_name ON vehicle_management.fleets(name);
|
||||
|
||||
CREATE TRIGGER trg_fleets_updated_at
|
||||
BEFORE UPDATE ON vehicle_management.fleets
|
||||
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
SELECT create_tenant_rls_policies('vehicle_management', 'fleets');
|
||||
|
||||
-- -------------------------------------------
|
||||
-- VEHICLES - Vehículos registrados
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE vehicle_management.vehicles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
customer_id UUID NOT NULL, -- core.partners
|
||||
fleet_id UUID REFERENCES vehicle_management.fleets(id),
|
||||
|
||||
vin VARCHAR(17),
|
||||
license_plate VARCHAR(15) NOT NULL,
|
||||
economic_number VARCHAR(20),
|
||||
|
||||
make VARCHAR(50) NOT NULL,
|
||||
model VARCHAR(100) NOT NULL,
|
||||
year INTEGER NOT NULL CHECK (year >= 1950 AND year <= 2100),
|
||||
color VARCHAR(30),
|
||||
|
||||
vehicle_type VARCHAR(30) DEFAULT 'truck'
|
||||
CHECK (vehicle_type IN ('truck', 'trailer', 'bus', 'pickup', 'other')),
|
||||
|
||||
current_odometer INTEGER CHECK (current_odometer >= 0),
|
||||
odometer_updated_at TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
photo_url VARCHAR(500),
|
||||
|
||||
status VARCHAR(20) DEFAULT 'active'
|
||||
CHECK (status IN ('active', 'inactive', 'sold')),
|
||||
|
||||
notes TEXT,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_vehicle_plate UNIQUE (tenant_id, license_plate)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_vehicles_tenant ON vehicle_management.vehicles(tenant_id);
|
||||
CREATE INDEX idx_vehicles_customer ON vehicle_management.vehicles(customer_id);
|
||||
CREATE INDEX idx_vehicles_fleet ON vehicle_management.vehicles(fleet_id);
|
||||
CREATE INDEX idx_vehicles_vin ON vehicle_management.vehicles(vin);
|
||||
CREATE INDEX idx_vehicles_plate ON vehicle_management.vehicles(license_plate);
|
||||
|
||||
CREATE TRIGGER trg_vehicles_updated_at
|
||||
BEFORE UPDATE ON vehicle_management.vehicles
|
||||
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
SELECT create_tenant_rls_policies('vehicle_management', 'vehicles');
|
||||
|
||||
-- -------------------------------------------
|
||||
-- VEHICLE_ENGINES - Especificaciones del motor
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE vehicle_management.vehicle_engines (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
vehicle_id UUID NOT NULL REFERENCES vehicle_management.vehicles(id) ON DELETE CASCADE,
|
||||
|
||||
engine_catalog_id UUID REFERENCES vehicle_management.engine_catalog(id),
|
||||
|
||||
serial_number VARCHAR(50),
|
||||
|
||||
horsepower INTEGER CHECK (horsepower > 0),
|
||||
torque INTEGER CHECK (torque > 0),
|
||||
|
||||
ecm_model VARCHAR(50),
|
||||
ecm_software VARCHAR(50),
|
||||
|
||||
injection_system VARCHAR(50),
|
||||
rail_pressure_max DECIMAL(10,2) CHECK (rail_pressure_max > 0),
|
||||
injector_count INTEGER CHECK (injector_count > 0),
|
||||
|
||||
turbo_type VARCHAR(50) CHECK (turbo_type IN ('VGT', 'wastegate', 'twin', 'compound')),
|
||||
turbo_make VARCHAR(50),
|
||||
turbo_model VARCHAR(50),
|
||||
|
||||
manufacture_date DATE,
|
||||
rebuild_date DATE,
|
||||
rebuild_odometer INTEGER CHECK (rebuild_odometer >= 0),
|
||||
|
||||
notes TEXT,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_vehicle_engines_vehicle ON vehicle_management.vehicle_engines(vehicle_id);
|
||||
CREATE INDEX idx_vehicle_engines_serial ON vehicle_management.vehicle_engines(serial_number);
|
||||
CREATE INDEX idx_vehicle_engines_catalog ON vehicle_management.vehicle_engines(engine_catalog_id);
|
||||
|
||||
CREATE TRIGGER trg_vehicle_engines_updated_at
|
||||
BEFORE UPDATE ON vehicle_management.vehicle_engines
|
||||
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
-- -------------------------------------------
|
||||
-- VEHICLE_HISTORY - Historial de cambios
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE vehicle_management.vehicle_history (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
vehicle_id UUID NOT NULL REFERENCES vehicle_management.vehicles(id) ON DELETE CASCADE,
|
||||
|
||||
field_name VARCHAR(50) NOT NULL,
|
||||
old_value TEXT,
|
||||
new_value TEXT,
|
||||
|
||||
changed_by UUID,
|
||||
changed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_vehicle_history_vehicle ON vehicle_management.vehicle_history(vehicle_id);
|
||||
CREATE INDEX idx_vehicle_history_date ON vehicle_management.vehicle_history(changed_at DESC);
|
||||
|
||||
-- -------------------------------------------
|
||||
-- MAINTENANCE_REMINDERS - Recordatorios
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE vehicle_management.maintenance_reminders (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
vehicle_id UUID NOT NULL REFERENCES vehicle_management.vehicles(id) ON DELETE CASCADE,
|
||||
|
||||
service_type VARCHAR(100) NOT NULL,
|
||||
service_id UUID,
|
||||
|
||||
frequency_type VARCHAR(20) NOT NULL CHECK (frequency_type IN ('time', 'odometer', 'both')),
|
||||
|
||||
interval_days INTEGER CHECK (interval_days > 0),
|
||||
interval_km INTEGER CHECK (interval_km > 0),
|
||||
|
||||
last_service_date DATE,
|
||||
last_service_km INTEGER,
|
||||
|
||||
next_due_date DATE,
|
||||
next_due_km INTEGER,
|
||||
|
||||
notify_days_before INTEGER DEFAULT 7 CHECK (notify_days_before >= 0),
|
||||
notify_km_before INTEGER DEFAULT 1000 CHECK (notify_km_before >= 0),
|
||||
|
||||
status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'paused', 'completed')),
|
||||
|
||||
notes TEXT,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_reminders_tenant ON vehicle_management.maintenance_reminders(tenant_id);
|
||||
CREATE INDEX idx_reminders_vehicle ON vehicle_management.maintenance_reminders(vehicle_id);
|
||||
CREATE INDEX idx_reminders_due_date ON vehicle_management.maintenance_reminders(next_due_date);
|
||||
|
||||
CREATE TRIGGER trg_reminders_updated_at
|
||||
BEFORE UPDATE ON vehicle_management.maintenance_reminders
|
||||
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
SELECT create_tenant_rls_policies('vehicle_management', 'maintenance_reminders');
|
||||
|
||||
-- -------------------------------------------
|
||||
-- REMINDER_NOTIFICATIONS - Notificaciones enviadas
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE vehicle_management.reminder_notifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
reminder_id UUID NOT NULL REFERENCES vehicle_management.maintenance_reminders(id) ON DELETE CASCADE,
|
||||
|
||||
notification_type VARCHAR(20) NOT NULL CHECK (notification_type IN ('email', 'sms', 'push', 'whatsapp')),
|
||||
|
||||
sent_to VARCHAR(200),
|
||||
sent_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
status VARCHAR(20) DEFAULT 'sent' CHECK (status IN ('sent', 'delivered', 'failed')),
|
||||
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX idx_reminder_notif_reminder ON vehicle_management.reminder_notifications(reminder_id);
|
||||
|
||||
-- -------------------------------------------
|
||||
-- VEHICLE_DOCUMENTS - Documentos del vehículo
|
||||
-- -------------------------------------------
|
||||
CREATE TABLE vehicle_management.vehicle_documents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
vehicle_id UUID NOT NULL REFERENCES vehicle_management.vehicles(id) ON DELETE CASCADE,
|
||||
|
||||
document_type VARCHAR(50) NOT NULL
|
||||
CHECK (document_type IN ('registration', 'insurance', 'permit', 'verification', 'other')),
|
||||
|
||||
document_number VARCHAR(100),
|
||||
|
||||
issue_date DATE,
|
||||
expiry_date DATE,
|
||||
|
||||
file_url VARCHAR(500),
|
||||
|
||||
notes TEXT,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_vehicle_docs_vehicle ON vehicle_management.vehicle_documents(vehicle_id);
|
||||
CREATE INDEX idx_vehicle_docs_expiry ON vehicle_management.vehicle_documents(expiry_date);
|
||||
|
||||
-- -------------------------------------------
|
||||
-- VISTA: Vehicle Summary
|
||||
-- -------------------------------------------
|
||||
CREATE OR REPLACE VIEW vehicle_management.vw_vehicle_summary AS
|
||||
SELECT
|
||||
v.id,
|
||||
v.tenant_id,
|
||||
v.license_plate,
|
||||
v.economic_number,
|
||||
v.make,
|
||||
v.model,
|
||||
v.year,
|
||||
v.current_odometer,
|
||||
v.status,
|
||||
v.customer_id,
|
||||
f.name as fleet_name,
|
||||
f.id as fleet_id,
|
||||
ec.make as engine_make,
|
||||
ec.model as engine_model,
|
||||
ve.serial_number as engine_serial,
|
||||
ve.horsepower,
|
||||
(SELECT COUNT(*) FROM service_management.service_orders so
|
||||
WHERE so.vehicle_id = v.id) as total_orders,
|
||||
(SELECT MAX(so.completed_at) FROM service_management.service_orders so
|
||||
WHERE so.vehicle_id = v.id AND so.status = 'completed') as last_service_date
|
||||
FROM vehicle_management.vehicles v
|
||||
LEFT JOIN vehicle_management.fleets f ON v.fleet_id = f.id
|
||||
LEFT JOIN vehicle_management.vehicle_engines ve ON ve.vehicle_id = v.id
|
||||
LEFT JOIN vehicle_management.engine_catalog ec ON ve.engine_catalog_id = ec.id
|
||||
WHERE v.tenant_id = get_current_tenant_id();
|
||||
|
||||
COMMENT ON VIEW vehicle_management.vw_vehicle_summary IS 'Vista resumida de vehículos con filtro RLS';
|
||||
|
||||
-- -------------------------------------------
|
||||
-- Trigger para actualizar vehicle_count en fleets
|
||||
-- -------------------------------------------
|
||||
CREATE OR REPLACE FUNCTION vehicle_management.update_fleet_vehicle_count()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Actualizar conteo de la flota anterior (si existe)
|
||||
IF TG_OP = 'DELETE' OR (TG_OP = 'UPDATE' AND OLD.fleet_id IS DISTINCT FROM NEW.fleet_id) THEN
|
||||
IF OLD.fleet_id IS NOT NULL THEN
|
||||
UPDATE vehicle_management.fleets
|
||||
SET vehicle_count = (
|
||||
SELECT COUNT(*) FROM vehicle_management.vehicles
|
||||
WHERE fleet_id = OLD.fleet_id AND status = 'active'
|
||||
)
|
||||
WHERE id = OLD.fleet_id;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- Actualizar conteo de la nueva flota
|
||||
IF TG_OP = 'INSERT' OR (TG_OP = 'UPDATE' AND OLD.fleet_id IS DISTINCT FROM NEW.fleet_id) THEN
|
||||
IF NEW.fleet_id IS NOT NULL THEN
|
||||
UPDATE vehicle_management.fleets
|
||||
SET vehicle_count = (
|
||||
SELECT COUNT(*) FROM vehicle_management.vehicles
|
||||
WHERE fleet_id = NEW.fleet_id AND status = 'active'
|
||||
)
|
||||
WHERE id = NEW.fleet_id;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_update_fleet_count
|
||||
AFTER INSERT OR UPDATE OR DELETE ON vehicle_management.vehicles
|
||||
FOR EACH ROW EXECUTE FUNCTION vehicle_management.update_fleet_vehicle_count();
|
||||
81
init/06-seed-data.sql
Normal file
81
init/06-seed-data.sql
Normal file
@ -0,0 +1,81 @@
|
||||
-- ===========================================
|
||||
-- MECANICAS DIESEL - Datos Semilla
|
||||
-- ===========================================
|
||||
-- Datos iniciales para desarrollo y testing
|
||||
|
||||
-- -------------------------------------------
|
||||
-- Tipos de prueba predefinidos (globales)
|
||||
-- -------------------------------------------
|
||||
INSERT INTO service_management.test_types (id, tenant_id, name, description, component_type, is_system, is_active)
|
||||
VALUES
|
||||
(gen_random_uuid(), NULL, 'Diagnóstico Scanner', 'Lectura de códigos DTC con scanner OBD-II/J1939', 'engine', true, true),
|
||||
(gen_random_uuid(), NULL, 'Prueba de Inyector', 'Prueba de banco para inyectores diesel', 'injector', true, true),
|
||||
(gen_random_uuid(), NULL, 'Prueba de Bomba de Alta Presión', 'Prueba de banco para bomba de inyección', 'pump', true, true),
|
||||
(gen_random_uuid(), NULL, 'Prueba de Compresión', 'Medición de compresión por cilindro', 'engine', true, true),
|
||||
(gen_random_uuid(), NULL, 'Prueba de Turbo VGT', 'Prueba de actuador y geometría variable', 'turbo', true, true),
|
||||
(gen_random_uuid(), NULL, 'Prueba de Presión de Riel', 'Medición de presión del sistema common rail', 'pump', true, true);
|
||||
|
||||
-- Parámetros para prueba de inyector
|
||||
WITH test AS (
|
||||
SELECT id FROM service_management.test_types WHERE name = 'Prueba de Inyector' LIMIT 1
|
||||
)
|
||||
INSERT INTO service_management.test_parameters (test_type_id, name, unit, data_type, sort_order)
|
||||
SELECT test.id, params.name, params.unit, params.data_type, params.sort_order
|
||||
FROM test, (VALUES
|
||||
('Presión de apertura', 'PSI', 'numeric', 1),
|
||||
('Caudal de inyección', 'ml/100', 'numeric', 2),
|
||||
('Retorno', 'ml/min', 'numeric', 3),
|
||||
('Patrón de spray', NULL, 'text', 4),
|
||||
('Tiempo de respuesta', 'ms', 'numeric', 5)
|
||||
) AS params(name, unit, data_type, sort_order);
|
||||
|
||||
-- Parámetros para prueba de bomba
|
||||
WITH test AS (
|
||||
SELECT id FROM service_management.test_types WHERE name = 'Prueba de Bomba de Alta Presión' LIMIT 1
|
||||
)
|
||||
INSERT INTO service_management.test_parameters (test_type_id, name, unit, data_type, sort_order)
|
||||
SELECT test.id, params.name, params.unit, params.data_type, params.sort_order
|
||||
FROM test, (VALUES
|
||||
('Presión máxima', 'PSI', 'numeric', 1),
|
||||
('Presión a RPM idle', 'PSI', 'numeric', 2),
|
||||
('Caudal volumétrico', 'lt/min', 'numeric', 3),
|
||||
('Tiempo de cebado', 'seg', 'numeric', 4)
|
||||
) AS params(name, unit, data_type, sort_order);
|
||||
|
||||
-- Parámetros para compresión
|
||||
WITH test AS (
|
||||
SELECT id FROM service_management.test_types WHERE name = 'Prueba de Compresión' LIMIT 1
|
||||
)
|
||||
INSERT INTO service_management.test_parameters (test_type_id, name, unit, data_type, sort_order)
|
||||
SELECT test.id, params.name, params.unit, params.data_type, params.sort_order
|
||||
FROM test, (VALUES
|
||||
('Compresión Cilindro 1', 'PSI', 'numeric', 1),
|
||||
('Compresión Cilindro 2', 'PSI', 'numeric', 2),
|
||||
('Compresión Cilindro 3', 'PSI', 'numeric', 3),
|
||||
('Compresión Cilindro 4', 'PSI', 'numeric', 4),
|
||||
('Compresión Cilindro 5', 'PSI', 'numeric', 5),
|
||||
('Compresión Cilindro 6', 'PSI', 'numeric', 6)
|
||||
) AS params(name, unit, data_type, sort_order);
|
||||
|
||||
-- Valores de referencia para Cummins ISX15
|
||||
WITH
|
||||
engine AS (SELECT id FROM vehicle_management.engine_catalog WHERE make = 'Cummins' AND model = 'ISX15' LIMIT 1),
|
||||
param_presion AS (SELECT id FROM service_management.test_parameters WHERE name = 'Presión de apertura' LIMIT 1),
|
||||
param_retorno AS (SELECT id FROM service_management.test_parameters WHERE name = 'Retorno' LIMIT 1),
|
||||
param_caudal AS (SELECT id FROM service_management.test_parameters WHERE name = 'Caudal de inyección' LIMIT 1)
|
||||
INSERT INTO service_management.parameter_references (parameter_id, engine_model_id, min_value, max_value, nominal_value, source)
|
||||
SELECT * FROM (
|
||||
SELECT param_presion.id, engine.id, 2800, 3200, 3000, 'Manual Cummins ISX15' FROM param_presion, engine
|
||||
UNION ALL
|
||||
SELECT param_retorno.id, engine.id, 0, 20, 10, 'Manual Cummins ISX15' FROM param_retorno, engine
|
||||
UNION ALL
|
||||
SELECT param_caudal.id, engine.id, 45, 55, 50, 'Manual Cummins ISX15' FROM param_caudal, engine
|
||||
) refs
|
||||
WHERE EXISTS (SELECT 1 FROM param_presion) AND EXISTS (SELECT 1 FROM engine);
|
||||
|
||||
-- -------------------------------------------
|
||||
-- Notas de uso
|
||||
-- -------------------------------------------
|
||||
COMMENT ON TABLE service_management.test_types IS 'Datos semilla: 6 tipos de prueba predefinidos';
|
||||
COMMENT ON TABLE service_management.test_parameters IS 'Datos semilla: Parámetros para pruebas de inyector, bomba y compresión';
|
||||
COMMENT ON TABLE service_management.parameter_references IS 'Datos semilla: Referencias para Cummins ISX15';
|
||||
459
init/07-notifications-schema.sql
Normal file
459
init/07-notifications-schema.sql
Normal file
@ -0,0 +1,459 @@
|
||||
-- ===========================================
|
||||
-- MECANICAS DIESEL - Schema de Notificaciones
|
||||
-- ===========================================
|
||||
-- Sistema de tracking, mensajes, followers y actividades
|
||||
-- Permite historial de cambios y notificaciones automaticas
|
||||
|
||||
-- ============================================
|
||||
-- SCHEMA: notifications
|
||||
-- ============================================
|
||||
CREATE SCHEMA IF NOT EXISTS notifications;
|
||||
COMMENT ON SCHEMA notifications IS 'Sistema de tracking, mensajes, followers y actividades (patron mail.thread)';
|
||||
|
||||
-- Grants
|
||||
GRANT USAGE ON SCHEMA notifications TO mecanicas_user;
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA notifications TO mecanicas_user;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA notifications GRANT ALL ON TABLES TO mecanicas_user;
|
||||
|
||||
-- ============================================
|
||||
-- GAP-01: Sistema de Tracking de Cambios
|
||||
-- ============================================
|
||||
|
||||
-- Subtipos de mensaje (clasificacion)
|
||||
CREATE TABLE notifications.message_subtypes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
code VARCHAR(50) NOT NULL UNIQUE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
res_model VARCHAR(100), -- NULL = aplica a todos los modelos
|
||||
is_internal BOOLEAN NOT NULL DEFAULT false,
|
||||
is_default BOOLEAN NOT NULL DEFAULT false,
|
||||
sequence INTEGER NOT NULL DEFAULT 10,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE notifications.message_subtypes IS 'Clasificacion de tipos de mensaje (creacion, edicion, nota, etc.)';
|
||||
|
||||
-- Seed de subtipos predeterminados
|
||||
INSERT INTO notifications.message_subtypes (code, name, description, is_internal, is_default, sequence) VALUES
|
||||
('mt_note', 'Nota', 'Nota interna', true, true, 1),
|
||||
('mt_comment', 'Comentario', 'Comentario publico', false, true, 2),
|
||||
('mt_tracking', 'Cambio de valor', 'Cambio en campo trackeado', true, false, 10),
|
||||
('mt_creation', 'Creacion', 'Documento creado', false, false, 5),
|
||||
('mt_status_change', 'Cambio de estado', 'Estado del documento modificado', false, false, 6),
|
||||
('mt_assignment', 'Asignacion', 'Documento asignado a usuario', false, false, 7);
|
||||
|
||||
-- Mensajes (chatter/historial)
|
||||
CREATE TABLE notifications.messages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
-- Referencia al documento
|
||||
res_model VARCHAR(100) NOT NULL, -- ej: 'service_management.service_orders'
|
||||
res_id UUID NOT NULL, -- ID del documento
|
||||
|
||||
-- Tipo y subtipo
|
||||
message_type VARCHAR(20) NOT NULL DEFAULT 'notification',
|
||||
subtype_id UUID REFERENCES notifications.message_subtypes(id),
|
||||
|
||||
-- Autor
|
||||
author_id UUID, -- Usuario que escribio el mensaje
|
||||
author_name VARCHAR(256), -- Nombre del autor (desnormalizado)
|
||||
|
||||
-- Contenido
|
||||
subject VARCHAR(500),
|
||||
body TEXT,
|
||||
|
||||
-- Tracking de cambios (JSON array)
|
||||
-- Formato: [{"field": "status", "old_value": "draft", "new_value": "confirmed", "field_label": "Estado"}]
|
||||
tracking_values JSONB DEFAULT '[]'::jsonb,
|
||||
|
||||
-- Metadatos
|
||||
is_internal BOOLEAN NOT NULL DEFAULT false,
|
||||
parent_id UUID REFERENCES notifications.messages(id),
|
||||
|
||||
-- Auditoria
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT chk_message_type CHECK (message_type IN ('comment', 'notification', 'note', 'email', 'system'))
|
||||
);
|
||||
|
||||
COMMENT ON TABLE notifications.messages IS 'Historial de mensajes y cambios en documentos (chatter)';
|
||||
COMMENT ON COLUMN notifications.messages.res_model IS 'Nombre completo del modelo (schema.table)';
|
||||
COMMENT ON COLUMN notifications.messages.res_id IS 'ID del registro referenciado';
|
||||
COMMENT ON COLUMN notifications.messages.tracking_values IS 'Array JSON con cambios de campos trackeados';
|
||||
|
||||
-- Indices para messages
|
||||
CREATE INDEX idx_messages_resource ON notifications.messages(res_model, res_id);
|
||||
CREATE INDEX idx_messages_tenant ON notifications.messages(tenant_id);
|
||||
CREATE INDEX idx_messages_created ON notifications.messages(created_at DESC);
|
||||
CREATE INDEX idx_messages_author ON notifications.messages(author_id);
|
||||
CREATE INDEX idx_messages_parent ON notifications.messages(parent_id) WHERE parent_id IS NOT NULL;
|
||||
|
||||
-- RLS para messages
|
||||
SELECT create_tenant_rls_policies('notifications', 'messages');
|
||||
|
||||
-- ============================================
|
||||
-- GAP-02: Sistema de Followers/Suscriptores
|
||||
-- ============================================
|
||||
|
||||
-- Seguidores de documentos
|
||||
CREATE TABLE notifications.followers (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
-- Referencia al documento
|
||||
res_model VARCHAR(100) NOT NULL,
|
||||
res_id UUID NOT NULL,
|
||||
|
||||
-- Seguidor (puede ser usuario o partner/cliente)
|
||||
partner_id UUID NOT NULL, -- ID del contacto/usuario
|
||||
partner_type VARCHAR(20) NOT NULL DEFAULT 'user',
|
||||
|
||||
-- Metadatos
|
||||
reason VARCHAR(100), -- Por que sigue (manual, asignacion, creador, etc.)
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_follower UNIQUE(tenant_id, res_model, res_id, partner_id),
|
||||
CONSTRAINT chk_partner_type CHECK (partner_type IN ('user', 'customer', 'supplier'))
|
||||
);
|
||||
|
||||
COMMENT ON TABLE notifications.followers IS 'Suscriptores a documentos para notificaciones automaticas';
|
||||
|
||||
-- Subtipos a los que esta suscrito cada follower
|
||||
CREATE TABLE notifications.follower_subtypes (
|
||||
follower_id UUID NOT NULL REFERENCES notifications.followers(id) ON DELETE CASCADE,
|
||||
subtype_id UUID NOT NULL REFERENCES notifications.message_subtypes(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (follower_id, subtype_id)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE notifications.follower_subtypes IS 'Tipos de mensaje a los que esta suscrito cada follower';
|
||||
|
||||
-- Indices para followers
|
||||
CREATE INDEX idx_followers_resource ON notifications.followers(res_model, res_id);
|
||||
CREATE INDEX idx_followers_partner ON notifications.followers(partner_id);
|
||||
CREATE INDEX idx_followers_tenant ON notifications.followers(tenant_id);
|
||||
|
||||
-- RLS para followers
|
||||
SELECT create_tenant_rls_policies('notifications', 'followers');
|
||||
|
||||
-- ============================================
|
||||
-- GAP-03: Actividades Programadas
|
||||
-- ============================================
|
||||
|
||||
-- Tipos de actividad
|
||||
CREATE TABLE notifications.activity_types (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
code VARCHAR(50) NOT NULL UNIQUE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
|
||||
-- Configuracion
|
||||
icon VARCHAR(50) DEFAULT 'fa-tasks',
|
||||
color VARCHAR(20) DEFAULT 'primary',
|
||||
default_days INTEGER DEFAULT 0, -- Dias por defecto para deadline
|
||||
|
||||
-- Restriccion por modelo (NULL = todos)
|
||||
res_model VARCHAR(100),
|
||||
|
||||
-- Metadatos
|
||||
sequence INTEGER NOT NULL DEFAULT 10,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE notifications.activity_types IS 'Tipos de actividad disponibles (llamar, reunion, tarea, etc.)';
|
||||
|
||||
-- Seed de tipos predeterminados para taller mecanico
|
||||
INSERT INTO notifications.activity_types (code, name, description, icon, color, default_days, sequence) VALUES
|
||||
('call', 'Llamar cliente', 'Llamada telefonica al cliente', 'fa-phone', 'info', 0, 1),
|
||||
('meeting', 'Cita de entrega', 'Cita para entregar vehiculo', 'fa-calendar-check', 'success', 0, 2),
|
||||
('todo', 'Tarea pendiente', 'Tarea generica por completar', 'fa-tasks', 'warning', 1, 3),
|
||||
('reminder', 'Recordatorio mantenimiento', 'Recordar cliente sobre proximo mantenimiento', 'fa-bell', 'secondary', 30, 4),
|
||||
('followup', 'Seguimiento cotizacion', 'Dar seguimiento a cotizacion enviada', 'fa-envelope', 'primary', 3, 5),
|
||||
('approval', 'Pendiente aprobacion', 'Esperar aprobacion de cliente o supervisor', 'fa-check-circle', 'danger', 1, 6),
|
||||
('parts_arrival', 'Llegada de refacciones', 'Refacciones pendientes de llegar', 'fa-truck', 'info', 2, 7),
|
||||
('quality_check', 'Revision de calidad', 'Inspeccion de trabajo terminado', 'fa-search', 'warning', 0, 8);
|
||||
|
||||
-- Actividades programadas
|
||||
CREATE TABLE notifications.activities (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
-- Referencia al documento
|
||||
res_model VARCHAR(100) NOT NULL,
|
||||
res_id UUID NOT NULL,
|
||||
|
||||
-- Tipo y asignacion
|
||||
activity_type_id UUID NOT NULL REFERENCES notifications.activity_types(id),
|
||||
user_id UUID NOT NULL, -- Usuario asignado
|
||||
|
||||
-- Programacion
|
||||
date_deadline DATE NOT NULL,
|
||||
|
||||
-- Contenido
|
||||
summary VARCHAR(500),
|
||||
note TEXT,
|
||||
|
||||
-- Estado
|
||||
state VARCHAR(20) NOT NULL DEFAULT 'planned',
|
||||
date_done TIMESTAMPTZ,
|
||||
feedback TEXT, -- Comentario al completar
|
||||
|
||||
-- Auditoria
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by UUID NOT NULL,
|
||||
|
||||
CONSTRAINT chk_activity_state CHECK (state IN ('planned', 'today', 'overdue', 'done', 'canceled'))
|
||||
);
|
||||
|
||||
COMMENT ON TABLE notifications.activities IS 'Actividades y recordatorios programados asociados a documentos';
|
||||
COMMENT ON COLUMN notifications.activities.state IS 'Estado: planned (futuro), today (hoy), overdue (vencida), done (completada), canceled';
|
||||
|
||||
-- Indices para activities
|
||||
CREATE INDEX idx_activities_resource ON notifications.activities(res_model, res_id);
|
||||
CREATE INDEX idx_activities_user ON notifications.activities(user_id);
|
||||
CREATE INDEX idx_activities_deadline ON notifications.activities(date_deadline);
|
||||
CREATE INDEX idx_activities_tenant ON notifications.activities(tenant_id);
|
||||
CREATE INDEX idx_activities_pending ON notifications.activities(user_id, date_deadline)
|
||||
WHERE state NOT IN ('done', 'canceled');
|
||||
|
||||
-- RLS para activities
|
||||
SELECT create_tenant_rls_policies('notifications', 'activities');
|
||||
|
||||
-- ============================================
|
||||
-- FUNCIONES AUXILIARES
|
||||
-- ============================================
|
||||
|
||||
-- Funcion para actualizar estado de actividades (planned -> today -> overdue)
|
||||
CREATE OR REPLACE FUNCTION notifications.update_activity_states()
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
v_updated INTEGER;
|
||||
BEGIN
|
||||
-- Actualizar a 'today' las que vencen hoy
|
||||
UPDATE notifications.activities
|
||||
SET state = 'today'
|
||||
WHERE state = 'planned'
|
||||
AND date_deadline = CURRENT_DATE;
|
||||
|
||||
GET DIAGNOSTICS v_updated = ROW_COUNT;
|
||||
|
||||
-- Actualizar a 'overdue' las vencidas
|
||||
UPDATE notifications.activities
|
||||
SET state = 'overdue'
|
||||
WHERE state IN ('planned', 'today')
|
||||
AND date_deadline < CURRENT_DATE;
|
||||
|
||||
RETURN v_updated;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION notifications.update_activity_states() IS 'Actualiza estados de actividades segun fecha (ejecutar diariamente)';
|
||||
|
||||
-- Funcion para agregar follower automaticamente
|
||||
CREATE OR REPLACE FUNCTION notifications.add_follower(
|
||||
p_tenant_id UUID,
|
||||
p_res_model VARCHAR(100),
|
||||
p_res_id UUID,
|
||||
p_partner_id UUID,
|
||||
p_partner_type VARCHAR(20) DEFAULT 'user',
|
||||
p_reason VARCHAR(100) DEFAULT 'manual'
|
||||
)
|
||||
RETURNS UUID AS $$
|
||||
DECLARE
|
||||
v_follower_id UUID;
|
||||
BEGIN
|
||||
INSERT INTO notifications.followers (tenant_id, res_model, res_id, partner_id, partner_type, reason)
|
||||
VALUES (p_tenant_id, p_res_model, p_res_id, p_partner_id, p_partner_type, p_reason)
|
||||
ON CONFLICT (tenant_id, res_model, res_id, partner_id) DO NOTHING
|
||||
RETURNING id INTO v_follower_id;
|
||||
|
||||
-- Si ya existia, obtener el ID
|
||||
IF v_follower_id IS NULL THEN
|
||||
SELECT id INTO v_follower_id
|
||||
FROM notifications.followers
|
||||
WHERE tenant_id = p_tenant_id
|
||||
AND res_model = p_res_model
|
||||
AND res_id = p_res_id
|
||||
AND partner_id = p_partner_id;
|
||||
END IF;
|
||||
|
||||
RETURN v_follower_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION notifications.add_follower IS 'Agrega un follower a un documento (idempotente)';
|
||||
|
||||
-- Funcion para registrar mensaje de tracking
|
||||
CREATE OR REPLACE FUNCTION notifications.log_tracking_message(
|
||||
p_tenant_id UUID,
|
||||
p_res_model VARCHAR(100),
|
||||
p_res_id UUID,
|
||||
p_author_id UUID,
|
||||
p_tracking_values JSONB,
|
||||
p_body TEXT DEFAULT NULL
|
||||
)
|
||||
RETURNS UUID AS $$
|
||||
DECLARE
|
||||
v_message_id UUID;
|
||||
v_subtype_id UUID;
|
||||
BEGIN
|
||||
-- Obtener subtipo de tracking
|
||||
SELECT id INTO v_subtype_id
|
||||
FROM notifications.message_subtypes
|
||||
WHERE code = 'mt_tracking';
|
||||
|
||||
INSERT INTO notifications.messages (
|
||||
tenant_id, res_model, res_id, message_type,
|
||||
subtype_id, author_id, body, tracking_values, is_internal
|
||||
)
|
||||
VALUES (
|
||||
p_tenant_id, p_res_model, p_res_id, 'notification',
|
||||
v_subtype_id, p_author_id, p_body, p_tracking_values, true
|
||||
)
|
||||
RETURNING id INTO v_message_id;
|
||||
|
||||
RETURN v_message_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION notifications.log_tracking_message IS 'Registra un mensaje de tracking de cambios';
|
||||
|
||||
-- Funcion para crear actividad
|
||||
CREATE OR REPLACE FUNCTION notifications.create_activity(
|
||||
p_tenant_id UUID,
|
||||
p_res_model VARCHAR(100),
|
||||
p_res_id UUID,
|
||||
p_activity_type_code VARCHAR(50),
|
||||
p_user_id UUID,
|
||||
p_date_deadline DATE DEFAULT NULL,
|
||||
p_summary VARCHAR(500) DEFAULT NULL,
|
||||
p_note TEXT DEFAULT NULL,
|
||||
p_created_by UUID DEFAULT NULL
|
||||
)
|
||||
RETURNS UUID AS $$
|
||||
DECLARE
|
||||
v_activity_id UUID;
|
||||
v_activity_type_id UUID;
|
||||
v_default_days INTEGER;
|
||||
BEGIN
|
||||
-- Obtener tipo de actividad
|
||||
SELECT id, default_days INTO v_activity_type_id, v_default_days
|
||||
FROM notifications.activity_types
|
||||
WHERE code = p_activity_type_code AND is_active = true;
|
||||
|
||||
IF v_activity_type_id IS NULL THEN
|
||||
RAISE EXCEPTION 'Activity type % not found', p_activity_type_code;
|
||||
END IF;
|
||||
|
||||
-- Calcular deadline si no se proporciono
|
||||
IF p_date_deadline IS NULL THEN
|
||||
p_date_deadline := CURRENT_DATE + v_default_days;
|
||||
END IF;
|
||||
|
||||
-- Determinar estado inicial
|
||||
INSERT INTO notifications.activities (
|
||||
tenant_id, res_model, res_id, activity_type_id, user_id,
|
||||
date_deadline, summary, note, state, created_by
|
||||
)
|
||||
VALUES (
|
||||
p_tenant_id, p_res_model, p_res_id, v_activity_type_id, p_user_id,
|
||||
p_date_deadline, p_summary, p_note,
|
||||
CASE
|
||||
WHEN p_date_deadline < CURRENT_DATE THEN 'overdue'
|
||||
WHEN p_date_deadline = CURRENT_DATE THEN 'today'
|
||||
ELSE 'planned'
|
||||
END,
|
||||
COALESCE(p_created_by, get_current_user_id())
|
||||
)
|
||||
RETURNING id INTO v_activity_id;
|
||||
|
||||
RETURN v_activity_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION notifications.create_activity IS 'Crea una nueva actividad programada';
|
||||
|
||||
-- Funcion para completar actividad
|
||||
CREATE OR REPLACE FUNCTION notifications.complete_activity(
|
||||
p_activity_id UUID,
|
||||
p_feedback TEXT DEFAULT NULL
|
||||
)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
UPDATE notifications.activities
|
||||
SET state = 'done',
|
||||
date_done = NOW(),
|
||||
feedback = p_feedback
|
||||
WHERE id = p_activity_id
|
||||
AND state NOT IN ('done', 'canceled');
|
||||
|
||||
RETURN FOUND;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION notifications.complete_activity IS 'Marca una actividad como completada';
|
||||
|
||||
-- ============================================
|
||||
-- VISTAS UTILES
|
||||
-- ============================================
|
||||
|
||||
-- Vista de actividades pendientes por usuario
|
||||
CREATE VIEW notifications.v_pending_activities AS
|
||||
SELECT
|
||||
a.id,
|
||||
a.tenant_id,
|
||||
a.res_model,
|
||||
a.res_id,
|
||||
at.code as activity_type_code,
|
||||
at.name as activity_type_name,
|
||||
at.icon,
|
||||
at.color,
|
||||
a.user_id,
|
||||
a.date_deadline,
|
||||
a.summary,
|
||||
a.note,
|
||||
a.state,
|
||||
CASE
|
||||
WHEN a.date_deadline < CURRENT_DATE THEN a.date_deadline - CURRENT_DATE
|
||||
ELSE 0
|
||||
END as days_overdue,
|
||||
a.created_at
|
||||
FROM notifications.activities a
|
||||
JOIN notifications.activity_types at ON at.id = a.activity_type_id
|
||||
WHERE a.state NOT IN ('done', 'canceled')
|
||||
ORDER BY a.date_deadline ASC;
|
||||
|
||||
COMMENT ON VIEW notifications.v_pending_activities IS 'Actividades pendientes con informacion de tipo';
|
||||
|
||||
-- Vista de historial de mensajes por documento
|
||||
CREATE VIEW notifications.v_message_history AS
|
||||
SELECT
|
||||
m.id,
|
||||
m.tenant_id,
|
||||
m.res_model,
|
||||
m.res_id,
|
||||
m.message_type,
|
||||
ms.code as subtype_code,
|
||||
ms.name as subtype_name,
|
||||
m.author_id,
|
||||
m.author_name,
|
||||
m.subject,
|
||||
m.body,
|
||||
m.tracking_values,
|
||||
m.is_internal,
|
||||
m.parent_id,
|
||||
m.created_at
|
||||
FROM notifications.messages m
|
||||
LEFT JOIN notifications.message_subtypes ms ON ms.id = m.subtype_id
|
||||
ORDER BY m.created_at DESC;
|
||||
|
||||
COMMENT ON VIEW notifications.v_message_history IS 'Historial de mensajes formateado con subtipos';
|
||||
|
||||
-- ============================================
|
||||
-- GRANTS ADICIONALES
|
||||
-- ============================================
|
||||
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA notifications TO mecanicas_user;
|
||||
GRANT SELECT ON ALL TABLES IN SCHEMA notifications TO mecanicas_user;
|
||||
387
init/08-analytics-schema.sql
Normal file
387
init/08-analytics-schema.sql
Normal file
@ -0,0 +1,387 @@
|
||||
-- ===========================================
|
||||
-- MECANICAS DIESEL - Schema de Contabilidad Analitica
|
||||
-- ===========================================
|
||||
-- Resuelve: GAP-05
|
||||
-- Permite P&L por orden de servicio
|
||||
-- Referencia: SPEC-CONTABILIDAD-ANALITICA-MULTIDIMENSIONAL.md
|
||||
|
||||
-- ============================================
|
||||
-- SCHEMA: analytics
|
||||
-- ============================================
|
||||
CREATE SCHEMA IF NOT EXISTS analytics;
|
||||
COMMENT ON SCHEMA analytics IS 'Contabilidad analitica simplificada - costos e ingresos por orden';
|
||||
|
||||
-- Grants
|
||||
GRANT USAGE ON SCHEMA analytics TO mecanicas_user;
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA analytics TO mecanicas_user;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA analytics GRANT ALL ON TABLES TO mecanicas_user;
|
||||
|
||||
-- ============================================
|
||||
-- CUENTAS ANALITICAS
|
||||
-- ============================================
|
||||
|
||||
-- Tipos de cuenta analitica
|
||||
CREATE TABLE analytics.account_types (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
code VARCHAR(20) NOT NULL UNIQUE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
sequence INTEGER NOT NULL DEFAULT 10,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE analytics.account_types IS 'Clasificacion de cuentas analiticas';
|
||||
|
||||
-- Seed de tipos predeterminados para taller
|
||||
INSERT INTO analytics.account_types (code, name, description, sequence) VALUES
|
||||
('service_order', 'Orden de Servicio', 'Cuenta por orden de servicio individual', 1),
|
||||
('project', 'Proyecto', 'Agrupacion de multiples ordenes', 2),
|
||||
('vehicle', 'Vehiculo', 'Costos historicos por vehiculo', 3),
|
||||
('customer', 'Cliente', 'Rentabilidad por cliente', 4),
|
||||
('department', 'Departamento', 'Costos por area del taller', 5);
|
||||
|
||||
-- Cuentas analiticas
|
||||
CREATE TABLE analytics.accounts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
-- Identificacion
|
||||
code VARCHAR(30) NOT NULL,
|
||||
name VARCHAR(150) NOT NULL,
|
||||
|
||||
-- Clasificacion
|
||||
account_type_id UUID NOT NULL REFERENCES analytics.account_types(id),
|
||||
|
||||
-- Referencia al documento origen (opcional)
|
||||
res_model VARCHAR(100), -- ej: 'service_management.service_orders'
|
||||
res_id UUID, -- ID del documento
|
||||
|
||||
-- Jerarquia (para agrupaciones)
|
||||
parent_id UUID REFERENCES analytics.accounts(id),
|
||||
|
||||
-- Presupuesto (opcional)
|
||||
budget_amount DECIMAL(20,6) DEFAULT 0,
|
||||
|
||||
-- Metadatos
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ,
|
||||
|
||||
CONSTRAINT uq_analytics_account UNIQUE(tenant_id, code)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE analytics.accounts IS 'Cuentas analiticas para tracking de costos/ingresos';
|
||||
COMMENT ON COLUMN analytics.accounts.res_model IS 'Modelo relacionado (ej: service_orders)';
|
||||
COMMENT ON COLUMN analytics.accounts.res_id IS 'ID del documento relacionado';
|
||||
|
||||
-- Indices para accounts
|
||||
CREATE INDEX idx_analytics_accounts_tenant ON analytics.accounts(tenant_id);
|
||||
CREATE INDEX idx_analytics_accounts_type ON analytics.accounts(account_type_id);
|
||||
CREATE INDEX idx_analytics_accounts_parent ON analytics.accounts(parent_id) WHERE parent_id IS NOT NULL;
|
||||
CREATE INDEX idx_analytics_accounts_resource ON analytics.accounts(res_model, res_id) WHERE res_model IS NOT NULL;
|
||||
|
||||
-- RLS para accounts
|
||||
SELECT create_tenant_rls_policies('analytics', 'accounts');
|
||||
|
||||
-- Trigger para updated_at
|
||||
CREATE TRIGGER set_updated_at_analytics_accounts
|
||||
BEFORE UPDATE ON analytics.accounts
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
-- ============================================
|
||||
-- LINEAS ANALITICAS (Movimientos)
|
||||
-- ============================================
|
||||
|
||||
-- Categorias de linea (costo vs ingreso)
|
||||
CREATE TYPE analytics.line_category AS ENUM ('cost', 'revenue', 'adjustment');
|
||||
|
||||
-- Lineas analiticas
|
||||
CREATE TABLE analytics.lines (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
-- Cuenta analitica
|
||||
account_id UUID NOT NULL REFERENCES analytics.accounts(id),
|
||||
|
||||
-- Fecha y descripcion
|
||||
date DATE NOT NULL,
|
||||
name VARCHAR(256) NOT NULL,
|
||||
ref VARCHAR(100), -- Referencia externa (factura, orden, etc.)
|
||||
|
||||
-- Importes
|
||||
amount DECIMAL(20,6) NOT NULL, -- Positivo = ingreso, negativo = costo
|
||||
category analytics.line_category NOT NULL,
|
||||
unit_amount DECIMAL(20,6), -- Cantidad de unidades (horas, piezas)
|
||||
unit_cost DECIMAL(20,6), -- Costo unitario
|
||||
|
||||
-- Origen del movimiento
|
||||
source_model VARCHAR(100), -- Modelo que genero la linea
|
||||
source_id UUID, -- ID del registro origen
|
||||
|
||||
-- Detalle adicional
|
||||
product_id UUID, -- Producto/refaccion si aplica
|
||||
employee_id UUID, -- Empleado si es mano de obra
|
||||
|
||||
-- Auditoria
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by UUID
|
||||
);
|
||||
|
||||
COMMENT ON TABLE analytics.lines IS 'Movimientos de costos/ingresos en cuentas analiticas';
|
||||
COMMENT ON COLUMN analytics.lines.amount IS 'Monto: positivo=ingreso, negativo=costo';
|
||||
COMMENT ON COLUMN analytics.lines.unit_amount IS 'Cantidad (horas de mano de obra, unidades de refaccion)';
|
||||
|
||||
-- Indices para lines
|
||||
CREATE INDEX idx_analytics_lines_tenant ON analytics.lines(tenant_id);
|
||||
CREATE INDEX idx_analytics_lines_account ON analytics.lines(account_id);
|
||||
CREATE INDEX idx_analytics_lines_date ON analytics.lines(date);
|
||||
CREATE INDEX idx_analytics_lines_category ON analytics.lines(category);
|
||||
CREATE INDEX idx_analytics_lines_source ON analytics.lines(source_model, source_id) WHERE source_model IS NOT NULL;
|
||||
CREATE INDEX idx_analytics_lines_product ON analytics.lines(product_id) WHERE product_id IS NOT NULL;
|
||||
|
||||
-- RLS para lines
|
||||
SELECT create_tenant_rls_policies('analytics', 'lines');
|
||||
|
||||
-- ============================================
|
||||
-- FUNCIONES PARA GESTION ANALITICA
|
||||
-- ============================================
|
||||
|
||||
-- Funcion para crear cuenta analitica automatica para orden de servicio
|
||||
CREATE OR REPLACE FUNCTION analytics.create_service_order_account(
|
||||
p_tenant_id UUID,
|
||||
p_service_order_id UUID,
|
||||
p_order_number VARCHAR(50),
|
||||
p_customer_name VARCHAR(256) DEFAULT NULL
|
||||
)
|
||||
RETURNS UUID AS $$
|
||||
DECLARE
|
||||
v_account_id UUID;
|
||||
v_type_id UUID;
|
||||
v_code VARCHAR(30);
|
||||
v_name VARCHAR(150);
|
||||
BEGIN
|
||||
-- Obtener tipo 'service_order'
|
||||
SELECT id INTO v_type_id
|
||||
FROM analytics.account_types
|
||||
WHERE code = 'service_order';
|
||||
|
||||
-- Generar codigo y nombre
|
||||
v_code := 'OS-' || p_order_number;
|
||||
v_name := 'Orden ' || p_order_number;
|
||||
IF p_customer_name IS NOT NULL THEN
|
||||
v_name := v_name || ' - ' || LEFT(p_customer_name, 50);
|
||||
END IF;
|
||||
|
||||
-- Insertar cuenta
|
||||
INSERT INTO analytics.accounts (
|
||||
tenant_id, code, name, account_type_id,
|
||||
res_model, res_id
|
||||
)
|
||||
VALUES (
|
||||
p_tenant_id, v_code, v_name, v_type_id,
|
||||
'service_management.service_orders', p_service_order_id
|
||||
)
|
||||
ON CONFLICT (tenant_id, code) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
updated_at = NOW()
|
||||
RETURNING id INTO v_account_id;
|
||||
|
||||
RETURN v_account_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION analytics.create_service_order_account IS 'Crea cuenta analitica automatica para orden de servicio';
|
||||
|
||||
-- Funcion para registrar costo de refaccion
|
||||
CREATE OR REPLACE FUNCTION analytics.log_parts_cost(
|
||||
p_tenant_id UUID,
|
||||
p_account_id UUID,
|
||||
p_part_id UUID,
|
||||
p_part_name VARCHAR(256),
|
||||
p_quantity DECIMAL(20,6),
|
||||
p_unit_cost DECIMAL(20,6),
|
||||
p_source_model VARCHAR(100) DEFAULT NULL,
|
||||
p_source_id UUID DEFAULT NULL
|
||||
)
|
||||
RETURNS UUID AS $$
|
||||
DECLARE
|
||||
v_line_id UUID;
|
||||
v_total_cost DECIMAL(20,6);
|
||||
BEGIN
|
||||
v_total_cost := p_quantity * p_unit_cost * -1; -- Negativo porque es costo
|
||||
|
||||
INSERT INTO analytics.lines (
|
||||
tenant_id, account_id, date, name, ref,
|
||||
amount, category, unit_amount, unit_cost,
|
||||
source_model, source_id, product_id, created_by
|
||||
)
|
||||
VALUES (
|
||||
p_tenant_id, p_account_id, CURRENT_DATE,
|
||||
'Refaccion: ' || p_part_name, NULL,
|
||||
v_total_cost, 'cost', p_quantity, p_unit_cost,
|
||||
p_source_model, p_source_id, p_part_id, get_current_user_id()
|
||||
)
|
||||
RETURNING id INTO v_line_id;
|
||||
|
||||
RETURN v_line_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION analytics.log_parts_cost IS 'Registra costo de refacciones usadas';
|
||||
|
||||
-- Funcion para registrar costo de mano de obra
|
||||
CREATE OR REPLACE FUNCTION analytics.log_labor_cost(
|
||||
p_tenant_id UUID,
|
||||
p_account_id UUID,
|
||||
p_employee_id UUID,
|
||||
p_employee_name VARCHAR(256),
|
||||
p_hours DECIMAL(20,6),
|
||||
p_hourly_rate DECIMAL(20,6),
|
||||
p_description VARCHAR(256) DEFAULT NULL,
|
||||
p_source_model VARCHAR(100) DEFAULT NULL,
|
||||
p_source_id UUID DEFAULT NULL
|
||||
)
|
||||
RETURNS UUID AS $$
|
||||
DECLARE
|
||||
v_line_id UUID;
|
||||
v_total_cost DECIMAL(20,6);
|
||||
BEGIN
|
||||
v_total_cost := p_hours * p_hourly_rate * -1; -- Negativo porque es costo
|
||||
|
||||
INSERT INTO analytics.lines (
|
||||
tenant_id, account_id, date, name, ref,
|
||||
amount, category, unit_amount, unit_cost,
|
||||
source_model, source_id, employee_id, created_by
|
||||
)
|
||||
VALUES (
|
||||
p_tenant_id, p_account_id, CURRENT_DATE,
|
||||
COALESCE(p_description, 'Mano de obra: ' || p_employee_name), NULL,
|
||||
v_total_cost, 'cost', p_hours, p_hourly_rate,
|
||||
p_source_model, p_source_id, p_employee_id, get_current_user_id()
|
||||
)
|
||||
RETURNING id INTO v_line_id;
|
||||
|
||||
RETURN v_line_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION analytics.log_labor_cost IS 'Registra costo de mano de obra';
|
||||
|
||||
-- Funcion para registrar ingreso (facturacion)
|
||||
CREATE OR REPLACE FUNCTION analytics.log_revenue(
|
||||
p_tenant_id UUID,
|
||||
p_account_id UUID,
|
||||
p_amount DECIMAL(20,6),
|
||||
p_description VARCHAR(256),
|
||||
p_ref VARCHAR(100) DEFAULT NULL,
|
||||
p_source_model VARCHAR(100) DEFAULT NULL,
|
||||
p_source_id UUID DEFAULT NULL
|
||||
)
|
||||
RETURNS UUID AS $$
|
||||
DECLARE
|
||||
v_line_id UUID;
|
||||
BEGIN
|
||||
INSERT INTO analytics.lines (
|
||||
tenant_id, account_id, date, name, ref,
|
||||
amount, category, unit_amount, unit_cost,
|
||||
source_model, source_id, created_by
|
||||
)
|
||||
VALUES (
|
||||
p_tenant_id, p_account_id, CURRENT_DATE,
|
||||
p_description, p_ref,
|
||||
ABS(p_amount), 'revenue', NULL, NULL,
|
||||
p_source_model, p_source_id, get_current_user_id()
|
||||
)
|
||||
RETURNING id INTO v_line_id;
|
||||
|
||||
RETURN v_line_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION analytics.log_revenue IS 'Registra ingreso en cuenta analitica';
|
||||
|
||||
-- ============================================
|
||||
-- VISTAS PARA REPORTES
|
||||
-- ============================================
|
||||
|
||||
-- Vista de P&L por cuenta analitica
|
||||
CREATE VIEW analytics.v_account_pnl AS
|
||||
SELECT
|
||||
a.id as account_id,
|
||||
a.tenant_id,
|
||||
a.code,
|
||||
a.name,
|
||||
at.code as account_type,
|
||||
a.res_model,
|
||||
a.res_id,
|
||||
a.budget_amount,
|
||||
COALESCE(SUM(CASE WHEN l.category = 'revenue' THEN l.amount ELSE 0 END), 0) as total_revenue,
|
||||
COALESCE(SUM(CASE WHEN l.category = 'cost' THEN ABS(l.amount) ELSE 0 END), 0) as total_cost,
|
||||
COALESCE(SUM(l.amount), 0) as net_profit,
|
||||
CASE
|
||||
WHEN COALESCE(SUM(CASE WHEN l.category = 'revenue' THEN l.amount ELSE 0 END), 0) = 0 THEN 0
|
||||
ELSE ROUND(
|
||||
(COALESCE(SUM(l.amount), 0) /
|
||||
COALESCE(SUM(CASE WHEN l.category = 'revenue' THEN l.amount ELSE 0 END), 1)) * 100,
|
||||
2
|
||||
)
|
||||
END as margin_percent,
|
||||
COUNT(DISTINCT l.id) as line_count
|
||||
FROM analytics.accounts a
|
||||
JOIN analytics.account_types at ON at.id = a.account_type_id
|
||||
LEFT JOIN analytics.lines l ON l.account_id = a.id
|
||||
WHERE a.is_active = true
|
||||
GROUP BY a.id, a.tenant_id, a.code, a.name, at.code, a.res_model, a.res_id, a.budget_amount;
|
||||
|
||||
COMMENT ON VIEW analytics.v_account_pnl IS 'Estado de resultados por cuenta analitica';
|
||||
|
||||
-- Vista de detalle de costos por orden
|
||||
CREATE VIEW analytics.v_service_order_costs AS
|
||||
SELECT
|
||||
a.res_id as service_order_id,
|
||||
a.tenant_id,
|
||||
a.code as account_code,
|
||||
l.date,
|
||||
l.name,
|
||||
l.category,
|
||||
l.amount,
|
||||
l.unit_amount,
|
||||
l.unit_cost,
|
||||
l.product_id,
|
||||
l.employee_id,
|
||||
l.source_model,
|
||||
l.source_id,
|
||||
l.created_at
|
||||
FROM analytics.accounts a
|
||||
JOIN analytics.account_types at ON at.id = a.account_type_id AND at.code = 'service_order'
|
||||
JOIN analytics.lines l ON l.account_id = a.id
|
||||
WHERE a.res_model = 'service_management.service_orders'
|
||||
ORDER BY l.created_at DESC;
|
||||
|
||||
COMMENT ON VIEW analytics.v_service_order_costs IS 'Detalle de costos e ingresos por orden de servicio';
|
||||
|
||||
-- Vista resumen mensual
|
||||
CREATE VIEW analytics.v_monthly_summary AS
|
||||
SELECT
|
||||
a.tenant_id,
|
||||
DATE_TRUNC('month', l.date) as month,
|
||||
at.code as account_type,
|
||||
COUNT(DISTINCT a.id) as account_count,
|
||||
SUM(CASE WHEN l.category = 'revenue' THEN l.amount ELSE 0 END) as total_revenue,
|
||||
SUM(CASE WHEN l.category = 'cost' THEN ABS(l.amount) ELSE 0 END) as total_cost,
|
||||
SUM(l.amount) as net_profit
|
||||
FROM analytics.lines l
|
||||
JOIN analytics.accounts a ON a.id = l.account_id
|
||||
JOIN analytics.account_types at ON at.id = a.account_type_id
|
||||
GROUP BY a.tenant_id, DATE_TRUNC('month', l.date), at.code
|
||||
ORDER BY month DESC, account_type;
|
||||
|
||||
COMMENT ON VIEW analytics.v_monthly_summary IS 'Resumen de rentabilidad mensual por tipo de cuenta';
|
||||
|
||||
-- ============================================
|
||||
-- GRANTS ADICIONALES
|
||||
-- ============================================
|
||||
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA analytics TO mecanicas_user;
|
||||
GRANT SELECT ON ALL TABLES IN SCHEMA analytics TO mecanicas_user;
|
||||
531
init/09-purchasing-schema.sql
Normal file
531
init/09-purchasing-schema.sql
Normal file
@ -0,0 +1,531 @@
|
||||
-- ===========================================
|
||||
-- MECANICAS DIESEL - Schema de Compras
|
||||
-- ===========================================
|
||||
-- Sistema de ordenes de compra, proveedores y recepciones
|
||||
-- Gestion completa del ciclo de compras del taller
|
||||
|
||||
-- ============================================
|
||||
-- SCHEMA: purchasing
|
||||
-- ============================================
|
||||
CREATE SCHEMA IF NOT EXISTS purchasing;
|
||||
COMMENT ON SCHEMA purchasing IS 'Gestion de compras: ordenes de compra, recepciones, proveedores';
|
||||
|
||||
-- Grants
|
||||
GRANT USAGE ON SCHEMA purchasing TO mecanicas_user;
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA purchasing TO mecanicas_user;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA purchasing GRANT ALL ON TABLES TO mecanicas_user;
|
||||
|
||||
-- ============================================
|
||||
-- PROVEEDORES (complementa partners existentes)
|
||||
-- ============================================
|
||||
|
||||
-- Extension de datos de proveedor
|
||||
CREATE TABLE purchasing.suppliers (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
-- Datos basicos
|
||||
code VARCHAR(20) NOT NULL,
|
||||
name VARCHAR(256) NOT NULL,
|
||||
trade_name VARCHAR(256), -- Nombre comercial
|
||||
rfc VARCHAR(13),
|
||||
|
||||
-- Contacto
|
||||
contact_name VARCHAR(256),
|
||||
email VARCHAR(256),
|
||||
phone VARCHAR(50),
|
||||
mobile VARCHAR(50),
|
||||
|
||||
-- Direccion
|
||||
street VARCHAR(256),
|
||||
city VARCHAR(100),
|
||||
state VARCHAR(100),
|
||||
zip_code VARCHAR(10),
|
||||
country VARCHAR(100) DEFAULT 'Mexico',
|
||||
|
||||
-- Datos comerciales
|
||||
payment_term_days INTEGER DEFAULT 30,
|
||||
credit_limit DECIMAL(20,6) DEFAULT 0,
|
||||
currency_code VARCHAR(3) DEFAULT 'MXN',
|
||||
|
||||
-- Clasificacion
|
||||
category VARCHAR(50), -- refacciones, lubricantes, herramientas, etc.
|
||||
is_preferred BOOLEAN NOT NULL DEFAULT false,
|
||||
rating INTEGER CHECK (rating BETWEEN 1 AND 5),
|
||||
|
||||
-- Notas
|
||||
notes TEXT,
|
||||
|
||||
-- Metadatos
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ,
|
||||
created_by UUID,
|
||||
|
||||
CONSTRAINT uq_supplier_code UNIQUE(tenant_id, code)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE purchasing.suppliers IS 'Proveedores del taller';
|
||||
|
||||
-- Indices para suppliers
|
||||
CREATE INDEX idx_suppliers_tenant ON purchasing.suppliers(tenant_id);
|
||||
CREATE INDEX idx_suppliers_name ON purchasing.suppliers(name);
|
||||
CREATE INDEX idx_suppliers_category ON purchasing.suppliers(category);
|
||||
CREATE INDEX idx_suppliers_preferred ON purchasing.suppliers(is_preferred) WHERE is_preferred = true;
|
||||
|
||||
-- RLS para suppliers
|
||||
SELECT create_tenant_rls_policies('purchasing', 'suppliers');
|
||||
|
||||
-- Trigger para updated_at
|
||||
CREATE TRIGGER set_updated_at_suppliers
|
||||
BEFORE UPDATE ON purchasing.suppliers
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
-- ============================================
|
||||
-- ORDENES DE COMPRA
|
||||
-- ============================================
|
||||
|
||||
-- Estados de orden de compra
|
||||
CREATE TYPE purchasing.po_status AS ENUM (
|
||||
'draft', -- Borrador
|
||||
'sent', -- Enviada a proveedor
|
||||
'confirmed', -- Confirmada por proveedor
|
||||
'partial', -- Parcialmente recibida
|
||||
'received', -- Completamente recibida
|
||||
'cancelled' -- Cancelada
|
||||
);
|
||||
|
||||
-- Ordenes de compra
|
||||
CREATE TABLE purchasing.purchase_orders (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
-- Identificacion
|
||||
order_number VARCHAR(50) NOT NULL,
|
||||
reference VARCHAR(100), -- Referencia del proveedor
|
||||
|
||||
-- Proveedor
|
||||
supplier_id UUID NOT NULL REFERENCES purchasing.suppliers(id),
|
||||
|
||||
-- Estado
|
||||
status purchasing.po_status NOT NULL DEFAULT 'draft',
|
||||
|
||||
-- Fechas
|
||||
order_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||
expected_date DATE, -- Fecha esperada de entrega
|
||||
received_date DATE, -- Fecha de recepcion completa
|
||||
|
||||
-- Importes
|
||||
subtotal DECIMAL(20,6) NOT NULL DEFAULT 0,
|
||||
discount_amount DECIMAL(20,6) NOT NULL DEFAULT 0,
|
||||
tax_amount DECIMAL(20,6) NOT NULL DEFAULT 0,
|
||||
total DECIMAL(20,6) NOT NULL DEFAULT 0,
|
||||
currency_code VARCHAR(3) DEFAULT 'MXN',
|
||||
|
||||
-- Urgencia (para taller)
|
||||
priority VARCHAR(20) DEFAULT 'normal',
|
||||
service_order_id UUID, -- Orden de servicio relacionada (si aplica)
|
||||
|
||||
-- Notas
|
||||
notes TEXT,
|
||||
internal_notes TEXT,
|
||||
|
||||
-- Auditoria
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ,
|
||||
created_by UUID NOT NULL,
|
||||
confirmed_by UUID,
|
||||
confirmed_at TIMESTAMPTZ,
|
||||
|
||||
CONSTRAINT uq_po_number UNIQUE(tenant_id, order_number),
|
||||
CONSTRAINT chk_po_priority CHECK (priority IN ('low', 'normal', 'high', 'urgent'))
|
||||
);
|
||||
|
||||
COMMENT ON TABLE purchasing.purchase_orders IS 'Ordenes de compra a proveedores';
|
||||
COMMENT ON COLUMN purchasing.purchase_orders.service_order_id IS 'Orden de servicio que origino la compra (para urgencias)';
|
||||
|
||||
-- Indices para purchase_orders
|
||||
CREATE INDEX idx_po_tenant ON purchasing.purchase_orders(tenant_id);
|
||||
CREATE INDEX idx_po_supplier ON purchasing.purchase_orders(supplier_id);
|
||||
CREATE INDEX idx_po_status ON purchasing.purchase_orders(status);
|
||||
CREATE INDEX idx_po_date ON purchasing.purchase_orders(order_date DESC);
|
||||
CREATE INDEX idx_po_expected ON purchasing.purchase_orders(expected_date) WHERE status NOT IN ('received', 'cancelled');
|
||||
CREATE INDEX idx_po_service_order ON purchasing.purchase_orders(service_order_id) WHERE service_order_id IS NOT NULL;
|
||||
|
||||
-- RLS para purchase_orders
|
||||
SELECT create_tenant_rls_policies('purchasing', 'purchase_orders');
|
||||
|
||||
-- Triggers
|
||||
CREATE TRIGGER set_updated_at_purchase_orders
|
||||
BEFORE UPDATE ON purchasing.purchase_orders
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
-- ============================================
|
||||
-- LINEAS DE ORDEN DE COMPRA
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE purchasing.purchase_order_lines (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
purchase_order_id UUID NOT NULL REFERENCES purchasing.purchase_orders(id) ON DELETE CASCADE,
|
||||
|
||||
-- Linea
|
||||
line_number INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
-- Producto
|
||||
part_id UUID, -- Referencia a parts_management.parts
|
||||
product_code VARCHAR(50), -- Codigo del producto (desnormalizado)
|
||||
description VARCHAR(500) NOT NULL,
|
||||
|
||||
-- Cantidades
|
||||
quantity DECIMAL(20,6) NOT NULL,
|
||||
unit_of_measure VARCHAR(20) DEFAULT 'PZA',
|
||||
received_quantity DECIMAL(20,6) NOT NULL DEFAULT 0,
|
||||
|
||||
-- Precios
|
||||
unit_price DECIMAL(20,6) NOT NULL,
|
||||
discount_percent DECIMAL(5,2) NOT NULL DEFAULT 0,
|
||||
subtotal DECIMAL(20,6) NOT NULL,
|
||||
tax_percent DECIMAL(5,2) NOT NULL DEFAULT 16.00, -- IVA Mexico
|
||||
tax_amount DECIMAL(20,6) NOT NULL DEFAULT 0,
|
||||
total DECIMAL(20,6) NOT NULL,
|
||||
|
||||
-- Fechas
|
||||
expected_date DATE,
|
||||
|
||||
-- Estado de linea
|
||||
is_closed BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
-- Auditoria
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE purchasing.purchase_order_lines IS 'Lineas de detalle de ordenes de compra';
|
||||
|
||||
-- Indices para lines
|
||||
CREATE INDEX idx_pol_order ON purchasing.purchase_order_lines(purchase_order_id);
|
||||
CREATE INDEX idx_pol_part ON purchasing.purchase_order_lines(part_id) WHERE part_id IS NOT NULL;
|
||||
CREATE INDEX idx_pol_pending ON purchasing.purchase_order_lines(purchase_order_id)
|
||||
WHERE received_quantity < quantity AND is_closed = false;
|
||||
|
||||
-- ============================================
|
||||
-- RECEPCIONES DE COMPRA
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE purchasing.purchase_receipts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
-- Identificacion
|
||||
receipt_number VARCHAR(50) NOT NULL,
|
||||
purchase_order_id UUID NOT NULL REFERENCES purchasing.purchase_orders(id),
|
||||
|
||||
-- Fecha y proveedor
|
||||
receipt_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||
supplier_id UUID NOT NULL REFERENCES purchasing.suppliers(id),
|
||||
|
||||
-- Documentos del proveedor
|
||||
supplier_invoice VARCHAR(50), -- Numero de factura proveedor
|
||||
supplier_delivery_note VARCHAR(50), -- Remision del proveedor
|
||||
|
||||
-- Notas
|
||||
notes TEXT,
|
||||
|
||||
-- Auditoria
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by UUID NOT NULL,
|
||||
|
||||
CONSTRAINT uq_receipt_number UNIQUE(tenant_id, receipt_number)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE purchasing.purchase_receipts IS 'Recepciones de mercancia de ordenes de compra';
|
||||
|
||||
-- Indices para receipts
|
||||
CREATE INDEX idx_pr_tenant ON purchasing.purchase_receipts(tenant_id);
|
||||
CREATE INDEX idx_pr_order ON purchasing.purchase_receipts(purchase_order_id);
|
||||
CREATE INDEX idx_pr_date ON purchasing.purchase_receipts(receipt_date DESC);
|
||||
|
||||
-- RLS para receipts
|
||||
SELECT create_tenant_rls_policies('purchasing', 'purchase_receipts');
|
||||
|
||||
-- Lineas de recepcion
|
||||
CREATE TABLE purchasing.purchase_receipt_lines (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
receipt_id UUID NOT NULL REFERENCES purchasing.purchase_receipts(id) ON DELETE CASCADE,
|
||||
order_line_id UUID NOT NULL REFERENCES purchasing.purchase_order_lines(id),
|
||||
|
||||
-- Cantidades
|
||||
quantity_received DECIMAL(20,6) NOT NULL,
|
||||
quantity_rejected DECIMAL(20,6) NOT NULL DEFAULT 0,
|
||||
rejection_reason VARCHAR(256),
|
||||
|
||||
-- Lote/Serie (si aplica)
|
||||
lot_number VARCHAR(100),
|
||||
serial_numbers TEXT[],
|
||||
|
||||
-- Ubicacion destino
|
||||
location_id UUID, -- Referencia a inventory location
|
||||
|
||||
-- Auditoria
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE purchasing.purchase_receipt_lines IS 'Detalle de productos recibidos';
|
||||
|
||||
CREATE INDEX idx_prl_receipt ON purchasing.purchase_receipt_lines(receipt_id);
|
||||
CREATE INDEX idx_prl_order_line ON purchasing.purchase_receipt_lines(order_line_id);
|
||||
|
||||
-- ============================================
|
||||
-- FUNCIONES AUXILIARES
|
||||
-- ============================================
|
||||
|
||||
-- Funcion para generar numero de orden de compra
|
||||
CREATE OR REPLACE FUNCTION purchasing.generate_po_number(p_tenant_id UUID)
|
||||
RETURNS VARCHAR(50) AS $$
|
||||
DECLARE
|
||||
v_year TEXT;
|
||||
v_sequence INTEGER;
|
||||
v_number VARCHAR(50);
|
||||
BEGIN
|
||||
v_year := TO_CHAR(CURRENT_DATE, 'YYYY');
|
||||
|
||||
-- Obtener siguiente secuencia del año
|
||||
SELECT COALESCE(MAX(
|
||||
CAST(SUBSTRING(order_number FROM 'OC-' || v_year || '-(\d+)') AS INTEGER)
|
||||
), 0) + 1
|
||||
INTO v_sequence
|
||||
FROM purchasing.purchase_orders
|
||||
WHERE tenant_id = p_tenant_id
|
||||
AND order_number LIKE 'OC-' || v_year || '-%';
|
||||
|
||||
v_number := 'OC-' || v_year || '-' || LPAD(v_sequence::TEXT, 5, '0');
|
||||
|
||||
RETURN v_number;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION purchasing.generate_po_number IS 'Genera numero secuencial de orden de compra (OC-YYYY-NNNNN)';
|
||||
|
||||
-- Funcion para generar numero de recepcion
|
||||
CREATE OR REPLACE FUNCTION purchasing.generate_receipt_number(p_tenant_id UUID)
|
||||
RETURNS VARCHAR(50) AS $$
|
||||
DECLARE
|
||||
v_year TEXT;
|
||||
v_sequence INTEGER;
|
||||
v_number VARCHAR(50);
|
||||
BEGIN
|
||||
v_year := TO_CHAR(CURRENT_DATE, 'YYYY');
|
||||
|
||||
SELECT COALESCE(MAX(
|
||||
CAST(SUBSTRING(receipt_number FROM 'REC-' || v_year || '-(\d+)') AS INTEGER)
|
||||
), 0) + 1
|
||||
INTO v_sequence
|
||||
FROM purchasing.purchase_receipts
|
||||
WHERE tenant_id = p_tenant_id
|
||||
AND receipt_number LIKE 'REC-' || v_year || '-%';
|
||||
|
||||
v_number := 'REC-' || v_year || '-' || LPAD(v_sequence::TEXT, 5, '0');
|
||||
|
||||
RETURN v_number;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION purchasing.generate_receipt_number IS 'Genera numero secuencial de recepcion (REC-YYYY-NNNNN)';
|
||||
|
||||
-- Funcion para calcular totales de linea
|
||||
CREATE OR REPLACE FUNCTION purchasing.calculate_line_totals()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Calcular subtotal (con descuento)
|
||||
NEW.subtotal := NEW.quantity * NEW.unit_price * (1 - NEW.discount_percent / 100);
|
||||
|
||||
-- Calcular impuesto
|
||||
NEW.tax_amount := NEW.subtotal * NEW.tax_percent / 100;
|
||||
|
||||
-- Calcular total
|
||||
NEW.total := NEW.subtotal + NEW.tax_amount;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER calculate_pol_totals
|
||||
BEFORE INSERT OR UPDATE OF quantity, unit_price, discount_percent, tax_percent
|
||||
ON purchasing.purchase_order_lines
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION purchasing.calculate_line_totals();
|
||||
|
||||
-- Funcion para actualizar totales de orden
|
||||
CREATE OR REPLACE FUNCTION purchasing.update_order_totals()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
UPDATE purchasing.purchase_orders po
|
||||
SET
|
||||
subtotal = COALESCE((
|
||||
SELECT SUM(subtotal) FROM purchasing.purchase_order_lines WHERE purchase_order_id = po.id
|
||||
), 0),
|
||||
tax_amount = COALESCE((
|
||||
SELECT SUM(tax_amount) FROM purchasing.purchase_order_lines WHERE purchase_order_id = po.id
|
||||
), 0),
|
||||
total = COALESCE((
|
||||
SELECT SUM(total) FROM purchasing.purchase_order_lines WHERE purchase_order_id = po.id
|
||||
), 0),
|
||||
updated_at = NOW()
|
||||
WHERE id = COALESCE(NEW.purchase_order_id, OLD.purchase_order_id);
|
||||
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER update_po_totals
|
||||
AFTER INSERT OR UPDATE OR DELETE
|
||||
ON purchasing.purchase_order_lines
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION purchasing.update_order_totals();
|
||||
|
||||
-- Funcion para actualizar cantidades recibidas
|
||||
CREATE OR REPLACE FUNCTION purchasing.update_received_quantities()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
v_order_id UUID;
|
||||
v_total_lines INTEGER;
|
||||
v_received_lines INTEGER;
|
||||
BEGIN
|
||||
-- Actualizar cantidad recibida en linea de orden
|
||||
UPDATE purchasing.purchase_order_lines pol
|
||||
SET received_quantity = COALESCE((
|
||||
SELECT SUM(prl.quantity_received)
|
||||
FROM purchasing.purchase_receipt_lines prl
|
||||
WHERE prl.order_line_id = pol.id
|
||||
), 0)
|
||||
WHERE id = NEW.order_line_id;
|
||||
|
||||
-- Obtener orden de compra
|
||||
SELECT purchase_order_id INTO v_order_id
|
||||
FROM purchasing.purchase_order_lines
|
||||
WHERE id = NEW.order_line_id;
|
||||
|
||||
-- Verificar si toda la orden fue recibida
|
||||
SELECT
|
||||
COUNT(*),
|
||||
COUNT(*) FILTER (WHERE received_quantity >= quantity)
|
||||
INTO v_total_lines, v_received_lines
|
||||
FROM purchasing.purchase_order_lines
|
||||
WHERE purchase_order_id = v_order_id;
|
||||
|
||||
-- Actualizar estado de la orden
|
||||
UPDATE purchasing.purchase_orders
|
||||
SET status = CASE
|
||||
WHEN v_received_lines = v_total_lines THEN 'received'::purchasing.po_status
|
||||
WHEN v_received_lines > 0 THEN 'partial'::purchasing.po_status
|
||||
ELSE status
|
||||
END,
|
||||
received_date = CASE
|
||||
WHEN v_received_lines = v_total_lines THEN CURRENT_DATE
|
||||
ELSE received_date
|
||||
END,
|
||||
updated_at = NOW()
|
||||
WHERE id = v_order_id;
|
||||
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER update_po_received
|
||||
AFTER INSERT
|
||||
ON purchasing.purchase_receipt_lines
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION purchasing.update_received_quantities();
|
||||
|
||||
-- ============================================
|
||||
-- VISTAS UTILES
|
||||
-- ============================================
|
||||
|
||||
-- Vista de ordenes de compra pendientes
|
||||
CREATE VIEW purchasing.v_pending_orders AS
|
||||
SELECT
|
||||
po.id,
|
||||
po.tenant_id,
|
||||
po.order_number,
|
||||
po.status,
|
||||
po.order_date,
|
||||
po.expected_date,
|
||||
s.name as supplier_name,
|
||||
s.contact_name,
|
||||
s.phone as supplier_phone,
|
||||
po.total,
|
||||
po.priority,
|
||||
po.service_order_id,
|
||||
CASE
|
||||
WHEN po.expected_date < CURRENT_DATE THEN 'overdue'
|
||||
WHEN po.expected_date = CURRENT_DATE THEN 'today'
|
||||
WHEN po.expected_date <= CURRENT_DATE + 3 THEN 'soon'
|
||||
ELSE 'normal'
|
||||
END as urgency,
|
||||
COUNT(pol.id) as line_count,
|
||||
SUM(CASE WHEN pol.received_quantity < pol.quantity THEN 1 ELSE 0 END) as pending_lines
|
||||
FROM purchasing.purchase_orders po
|
||||
JOIN purchasing.suppliers s ON s.id = po.supplier_id
|
||||
LEFT JOIN purchasing.purchase_order_lines pol ON pol.purchase_order_id = po.id
|
||||
WHERE po.status NOT IN ('received', 'cancelled')
|
||||
GROUP BY po.id, po.tenant_id, po.order_number, po.status, po.order_date,
|
||||
po.expected_date, s.name, s.contact_name, s.phone, po.total,
|
||||
po.priority, po.service_order_id
|
||||
ORDER BY po.expected_date ASC NULLS LAST, po.priority DESC;
|
||||
|
||||
COMMENT ON VIEW purchasing.v_pending_orders IS 'Ordenes de compra pendientes de recibir';
|
||||
|
||||
-- Vista de productos pendientes de recibir
|
||||
CREATE VIEW purchasing.v_pending_products AS
|
||||
SELECT
|
||||
po.tenant_id,
|
||||
pol.part_id,
|
||||
pol.product_code,
|
||||
pol.description,
|
||||
po.order_number,
|
||||
po.supplier_id,
|
||||
s.name as supplier_name,
|
||||
pol.quantity,
|
||||
pol.received_quantity,
|
||||
pol.quantity - pol.received_quantity as pending_quantity,
|
||||
pol.unit_price,
|
||||
po.expected_date,
|
||||
po.service_order_id
|
||||
FROM purchasing.purchase_order_lines pol
|
||||
JOIN purchasing.purchase_orders po ON po.id = pol.purchase_order_id
|
||||
JOIN purchasing.suppliers s ON s.id = po.supplier_id
|
||||
WHERE pol.received_quantity < pol.quantity
|
||||
AND pol.is_closed = false
|
||||
AND po.status NOT IN ('cancelled')
|
||||
ORDER BY po.expected_date ASC NULLS LAST;
|
||||
|
||||
COMMENT ON VIEW purchasing.v_pending_products IS 'Productos pendientes de recibir por orden';
|
||||
|
||||
-- Vista de historial de compras por proveedor
|
||||
CREATE VIEW purchasing.v_supplier_history AS
|
||||
SELECT
|
||||
s.id as supplier_id,
|
||||
s.tenant_id,
|
||||
s.code as supplier_code,
|
||||
s.name as supplier_name,
|
||||
s.category,
|
||||
s.rating,
|
||||
COUNT(DISTINCT po.id) as total_orders,
|
||||
COUNT(DISTINCT po.id) FILTER (WHERE po.status = 'received') as completed_orders,
|
||||
SUM(po.total) FILTER (WHERE po.status = 'received') as total_purchased,
|
||||
AVG(po.received_date - po.expected_date) FILTER (WHERE po.status = 'received' AND po.expected_date IS NOT NULL) as avg_delivery_days,
|
||||
MAX(po.order_date) as last_order_date
|
||||
FROM purchasing.suppliers s
|
||||
LEFT JOIN purchasing.purchase_orders po ON po.supplier_id = s.id
|
||||
WHERE s.is_active = true
|
||||
GROUP BY s.id, s.tenant_id, s.code, s.name, s.category, s.rating
|
||||
ORDER BY total_purchased DESC NULLS LAST;
|
||||
|
||||
COMMENT ON VIEW purchasing.v_supplier_history IS 'Historial y estadisticas de compras por proveedor';
|
||||
|
||||
-- ============================================
|
||||
-- GRANTS ADICIONALES
|
||||
-- ============================================
|
||||
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA purchasing TO mecanicas_user;
|
||||
GRANT SELECT ON ALL TABLES IN SCHEMA purchasing TO mecanicas_user;
|
||||
469
init/10-warranty-claims.sql
Normal file
469
init/10-warranty-claims.sql
Normal file
@ -0,0 +1,469 @@
|
||||
-- ===========================================
|
||||
-- MECANICAS DIESEL - Tracking de Garantias
|
||||
-- ===========================================
|
||||
-- Resuelve: GAP-10
|
||||
-- Sistema de seguimiento de garantias de refacciones
|
||||
-- Permite reclamar garantias a proveedores
|
||||
|
||||
-- ============================================
|
||||
-- EXTENSION DE PARTS PARA GARANTIAS
|
||||
-- ============================================
|
||||
|
||||
-- Agregar campos de garantia a parts si no existen
|
||||
DO $$
|
||||
BEGIN
|
||||
-- warranty_months
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'parts_management'
|
||||
AND table_name = 'parts'
|
||||
AND column_name = 'warranty_months'
|
||||
) THEN
|
||||
ALTER TABLE parts_management.parts
|
||||
ADD COLUMN warranty_months INTEGER DEFAULT 0;
|
||||
COMMENT ON COLUMN parts_management.parts.warranty_months IS 'Meses de garantia del fabricante';
|
||||
END IF;
|
||||
|
||||
-- warranty_policy
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'parts_management'
|
||||
AND table_name = 'parts'
|
||||
AND column_name = 'warranty_policy'
|
||||
) THEN
|
||||
ALTER TABLE parts_management.parts
|
||||
ADD COLUMN warranty_policy TEXT;
|
||||
COMMENT ON COLUMN parts_management.parts.warranty_policy IS 'Descripcion de politica de garantia';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ============================================
|
||||
-- TABLA DE GARANTIAS DE PARTES INSTALADAS
|
||||
-- ============================================
|
||||
|
||||
-- Estados de garantia
|
||||
CREATE TYPE parts_management.warranty_status AS ENUM (
|
||||
'active', -- Garantia vigente
|
||||
'expired', -- Garantia expirada
|
||||
'claimed', -- Reclamo en proceso
|
||||
'approved', -- Reclamo aprobado
|
||||
'rejected', -- Reclamo rechazado
|
||||
'replaced' -- Pieza reemplazada por garantia
|
||||
);
|
||||
|
||||
-- Garantias de piezas instaladas
|
||||
CREATE TABLE parts_management.warranty_claims (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
-- Referencia a la pieza
|
||||
part_id UUID NOT NULL,
|
||||
part_name VARCHAR(256) NOT NULL, -- Desnormalizado para historial
|
||||
part_sku VARCHAR(50),
|
||||
|
||||
-- Referencia al proveedor/fabricante
|
||||
supplier_id UUID,
|
||||
supplier_name VARCHAR(256),
|
||||
manufacturer VARCHAR(256),
|
||||
|
||||
-- Datos de instalacion
|
||||
service_order_id UUID,
|
||||
service_order_number VARCHAR(50),
|
||||
installation_date DATE NOT NULL,
|
||||
installation_notes TEXT,
|
||||
|
||||
-- Datos de garantia
|
||||
warranty_months INTEGER NOT NULL DEFAULT 12,
|
||||
expiration_date DATE NOT NULL,
|
||||
serial_number VARCHAR(100),
|
||||
lot_number VARCHAR(100),
|
||||
|
||||
-- Vehiculo (contexto)
|
||||
vehicle_id UUID,
|
||||
vehicle_plate VARCHAR(20),
|
||||
vehicle_description VARCHAR(256),
|
||||
|
||||
-- Cliente
|
||||
customer_id UUID,
|
||||
customer_name VARCHAR(256),
|
||||
|
||||
-- Estado y reclamo
|
||||
status parts_management.warranty_status NOT NULL DEFAULT 'active',
|
||||
|
||||
-- Datos del reclamo (si aplica)
|
||||
claim_date DATE,
|
||||
claim_reason TEXT,
|
||||
claim_description TEXT,
|
||||
defect_photos TEXT[], -- URLs de fotos del defecto
|
||||
|
||||
-- Resolucion
|
||||
resolution_date DATE,
|
||||
resolution_type VARCHAR(50), -- replacement, refund, repair, rejected
|
||||
resolution_notes TEXT,
|
||||
replacement_part_id UUID, -- Nueva pieza si fue reemplazo
|
||||
|
||||
-- Costos
|
||||
original_cost DECIMAL(20,6),
|
||||
claim_amount DECIMAL(20,6),
|
||||
approved_amount DECIMAL(20,6),
|
||||
|
||||
-- Auditoria
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ,
|
||||
created_by UUID,
|
||||
claimed_by UUID,
|
||||
resolved_by UUID
|
||||
);
|
||||
|
||||
COMMENT ON TABLE parts_management.warranty_claims IS 'Registro de garantias de piezas instaladas en vehiculos';
|
||||
COMMENT ON COLUMN parts_management.warranty_claims.defect_photos IS 'Array de URLs a fotos del defecto';
|
||||
|
||||
-- Indices para warranty_claims
|
||||
CREATE INDEX idx_wc_tenant ON parts_management.warranty_claims(tenant_id);
|
||||
CREATE INDEX idx_wc_part ON parts_management.warranty_claims(part_id);
|
||||
CREATE INDEX idx_wc_service_order ON parts_management.warranty_claims(service_order_id) WHERE service_order_id IS NOT NULL;
|
||||
CREATE INDEX idx_wc_vehicle ON parts_management.warranty_claims(vehicle_id) WHERE vehicle_id IS NOT NULL;
|
||||
CREATE INDEX idx_wc_customer ON parts_management.warranty_claims(customer_id);
|
||||
CREATE INDEX idx_wc_status ON parts_management.warranty_claims(status);
|
||||
CREATE INDEX idx_wc_expiration ON parts_management.warranty_claims(expiration_date)
|
||||
WHERE status = 'active';
|
||||
CREATE INDEX idx_wc_supplier ON parts_management.warranty_claims(supplier_id) WHERE supplier_id IS NOT NULL;
|
||||
|
||||
-- RLS para warranty_claims
|
||||
SELECT create_tenant_rls_policies('parts_management', 'warranty_claims');
|
||||
|
||||
-- Trigger para updated_at
|
||||
CREATE TRIGGER set_updated_at_warranty_claims
|
||||
BEFORE UPDATE ON parts_management.warranty_claims
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trigger_set_updated_at();
|
||||
|
||||
-- ============================================
|
||||
-- FUNCIONES AUXILIARES
|
||||
-- ============================================
|
||||
|
||||
-- Funcion para crear registro de garantia al instalar pieza
|
||||
CREATE OR REPLACE FUNCTION parts_management.create_warranty_record(
|
||||
p_tenant_id UUID,
|
||||
p_part_id UUID,
|
||||
p_service_order_id UUID,
|
||||
p_vehicle_id UUID DEFAULT NULL,
|
||||
p_customer_id UUID DEFAULT NULL,
|
||||
p_serial_number VARCHAR(100) DEFAULT NULL,
|
||||
p_supplier_id UUID DEFAULT NULL
|
||||
)
|
||||
RETURNS UUID AS $$
|
||||
DECLARE
|
||||
v_warranty_id UUID;
|
||||
v_part RECORD;
|
||||
v_service_order RECORD;
|
||||
v_vehicle RECORD;
|
||||
v_customer RECORD;
|
||||
v_supplier RECORD;
|
||||
BEGIN
|
||||
-- Obtener datos de la pieza
|
||||
SELECT id, sku, name, warranty_months, warranty_policy, cost
|
||||
INTO v_part
|
||||
FROM parts_management.parts
|
||||
WHERE id = p_part_id;
|
||||
|
||||
IF v_part.id IS NULL THEN
|
||||
RAISE EXCEPTION 'Part % not found', p_part_id;
|
||||
END IF;
|
||||
|
||||
-- Si no tiene garantia, no crear registro
|
||||
IF COALESCE(v_part.warranty_months, 0) <= 0 THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
-- Obtener datos de orden de servicio
|
||||
SELECT id, order_number
|
||||
INTO v_service_order
|
||||
FROM service_management.service_orders
|
||||
WHERE id = p_service_order_id;
|
||||
|
||||
-- Obtener datos de vehiculo (si aplica)
|
||||
IF p_vehicle_id IS NOT NULL THEN
|
||||
SELECT id, plate_number,
|
||||
CONCAT(brand, ' ', model, ' ', COALESCE(year::TEXT, '')) as description
|
||||
INTO v_vehicle
|
||||
FROM vehicle_management.vehicles
|
||||
WHERE id = p_vehicle_id;
|
||||
END IF;
|
||||
|
||||
-- Obtener datos de cliente (si aplica)
|
||||
IF p_customer_id IS NOT NULL THEN
|
||||
SELECT id, name
|
||||
INTO v_customer
|
||||
FROM workshop_core.customers
|
||||
WHERE id = p_customer_id;
|
||||
END IF;
|
||||
|
||||
-- Obtener datos de proveedor (si aplica)
|
||||
IF p_supplier_id IS NOT NULL THEN
|
||||
SELECT id, name
|
||||
INTO v_supplier
|
||||
FROM purchasing.suppliers
|
||||
WHERE id = p_supplier_id;
|
||||
END IF;
|
||||
|
||||
-- Crear registro de garantia
|
||||
INSERT INTO parts_management.warranty_claims (
|
||||
tenant_id,
|
||||
part_id, part_name, part_sku,
|
||||
supplier_id, supplier_name,
|
||||
service_order_id, service_order_number,
|
||||
installation_date, warranty_months, expiration_date,
|
||||
serial_number,
|
||||
vehicle_id, vehicle_plate, vehicle_description,
|
||||
customer_id, customer_name,
|
||||
original_cost,
|
||||
created_by
|
||||
)
|
||||
VALUES (
|
||||
p_tenant_id,
|
||||
v_part.id, v_part.name, v_part.sku,
|
||||
p_supplier_id, v_supplier.name,
|
||||
p_service_order_id, v_service_order.order_number,
|
||||
CURRENT_DATE, v_part.warranty_months,
|
||||
CURRENT_DATE + (v_part.warranty_months || ' months')::INTERVAL,
|
||||
p_serial_number,
|
||||
p_vehicle_id, v_vehicle.plate_number, v_vehicle.description,
|
||||
p_customer_id, v_customer.name,
|
||||
v_part.cost,
|
||||
get_current_user_id()
|
||||
)
|
||||
RETURNING id INTO v_warranty_id;
|
||||
|
||||
RETURN v_warranty_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION parts_management.create_warranty_record IS 'Crea registro de garantia al instalar una pieza';
|
||||
|
||||
-- Funcion para iniciar reclamo de garantia
|
||||
CREATE OR REPLACE FUNCTION parts_management.initiate_warranty_claim(
|
||||
p_warranty_id UUID,
|
||||
p_claim_reason TEXT,
|
||||
p_claim_description TEXT DEFAULT NULL,
|
||||
p_defect_photos TEXT[] DEFAULT NULL
|
||||
)
|
||||
RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
v_warranty RECORD;
|
||||
BEGIN
|
||||
-- Obtener garantia
|
||||
SELECT * INTO v_warranty
|
||||
FROM parts_management.warranty_claims
|
||||
WHERE id = p_warranty_id;
|
||||
|
||||
IF v_warranty.id IS NULL THEN
|
||||
RAISE EXCEPTION 'Warranty record % not found', p_warranty_id;
|
||||
END IF;
|
||||
|
||||
-- Verificar que este activa
|
||||
IF v_warranty.status != 'active' THEN
|
||||
RAISE EXCEPTION 'Warranty is not active (current status: %)', v_warranty.status;
|
||||
END IF;
|
||||
|
||||
-- Verificar que no este expirada
|
||||
IF v_warranty.expiration_date < CURRENT_DATE THEN
|
||||
-- Actualizar a expirada primero
|
||||
UPDATE parts_management.warranty_claims
|
||||
SET status = 'expired', updated_at = NOW()
|
||||
WHERE id = p_warranty_id;
|
||||
|
||||
RAISE EXCEPTION 'Warranty expired on %', v_warranty.expiration_date;
|
||||
END IF;
|
||||
|
||||
-- Actualizar con datos del reclamo
|
||||
UPDATE parts_management.warranty_claims
|
||||
SET
|
||||
status = 'claimed',
|
||||
claim_date = CURRENT_DATE,
|
||||
claim_reason = p_claim_reason,
|
||||
claim_description = p_claim_description,
|
||||
defect_photos = p_defect_photos,
|
||||
claim_amount = original_cost,
|
||||
claimed_by = get_current_user_id(),
|
||||
updated_at = NOW()
|
||||
WHERE id = p_warranty_id;
|
||||
|
||||
RETURN TRUE;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION parts_management.initiate_warranty_claim IS 'Inicia un reclamo de garantia';
|
||||
|
||||
-- Funcion para resolver reclamo
|
||||
CREATE OR REPLACE FUNCTION parts_management.resolve_warranty_claim(
|
||||
p_warranty_id UUID,
|
||||
p_resolution_type VARCHAR(50),
|
||||
p_approved_amount DECIMAL(20,6) DEFAULT NULL,
|
||||
p_resolution_notes TEXT DEFAULT NULL,
|
||||
p_replacement_part_id UUID DEFAULT NULL
|
||||
)
|
||||
RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
v_new_status parts_management.warranty_status;
|
||||
BEGIN
|
||||
-- Determinar nuevo estado segun resolucion
|
||||
v_new_status := CASE p_resolution_type
|
||||
WHEN 'replacement' THEN 'replaced'::parts_management.warranty_status
|
||||
WHEN 'refund' THEN 'approved'::parts_management.warranty_status
|
||||
WHEN 'repair' THEN 'approved'::parts_management.warranty_status
|
||||
WHEN 'rejected' THEN 'rejected'::parts_management.warranty_status
|
||||
ELSE 'approved'::parts_management.warranty_status
|
||||
END;
|
||||
|
||||
UPDATE parts_management.warranty_claims
|
||||
SET
|
||||
status = v_new_status,
|
||||
resolution_date = CURRENT_DATE,
|
||||
resolution_type = p_resolution_type,
|
||||
resolution_notes = p_resolution_notes,
|
||||
approved_amount = CASE
|
||||
WHEN p_resolution_type = 'rejected' THEN 0
|
||||
ELSE COALESCE(p_approved_amount, claim_amount)
|
||||
END,
|
||||
replacement_part_id = p_replacement_part_id,
|
||||
resolved_by = get_current_user_id(),
|
||||
updated_at = NOW()
|
||||
WHERE id = p_warranty_id
|
||||
AND status = 'claimed';
|
||||
|
||||
RETURN FOUND;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION parts_management.resolve_warranty_claim IS 'Resuelve un reclamo de garantia';
|
||||
|
||||
-- Funcion para actualizar garantias expiradas (ejecutar diariamente)
|
||||
CREATE OR REPLACE FUNCTION parts_management.expire_warranties()
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
v_count INTEGER;
|
||||
BEGIN
|
||||
UPDATE parts_management.warranty_claims
|
||||
SET
|
||||
status = 'expired',
|
||||
updated_at = NOW()
|
||||
WHERE status = 'active'
|
||||
AND expiration_date < CURRENT_DATE;
|
||||
|
||||
GET DIAGNOSTICS v_count = ROW_COUNT;
|
||||
RETURN v_count;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION parts_management.expire_warranties IS 'Marca como expiradas las garantias vencidas';
|
||||
|
||||
-- ============================================
|
||||
-- VISTAS DE REPORTES
|
||||
-- ============================================
|
||||
|
||||
-- Vista de garantias activas
|
||||
CREATE VIEW parts_management.v_active_warranties AS
|
||||
SELECT
|
||||
wc.id,
|
||||
wc.tenant_id,
|
||||
wc.part_id,
|
||||
wc.part_name,
|
||||
wc.part_sku,
|
||||
wc.supplier_name,
|
||||
wc.manufacturer,
|
||||
wc.service_order_number,
|
||||
wc.installation_date,
|
||||
wc.expiration_date,
|
||||
wc.serial_number,
|
||||
wc.vehicle_plate,
|
||||
wc.vehicle_description,
|
||||
wc.customer_name,
|
||||
wc.original_cost,
|
||||
-- Dias restantes
|
||||
wc.expiration_date - CURRENT_DATE as days_remaining,
|
||||
-- Urgencia de expiracion
|
||||
CASE
|
||||
WHEN wc.expiration_date - CURRENT_DATE <= 7 THEN 'critical'
|
||||
WHEN wc.expiration_date - CURRENT_DATE <= 30 THEN 'warning'
|
||||
WHEN wc.expiration_date - CURRENT_DATE <= 90 THEN 'notice'
|
||||
ELSE 'ok'
|
||||
END as expiration_urgency
|
||||
FROM parts_management.warranty_claims wc
|
||||
WHERE wc.status = 'active'
|
||||
AND wc.expiration_date >= CURRENT_DATE
|
||||
ORDER BY wc.expiration_date ASC;
|
||||
|
||||
COMMENT ON VIEW parts_management.v_active_warranties IS 'Garantias vigentes con dias restantes';
|
||||
|
||||
-- Vista de reclamos pendientes
|
||||
CREATE VIEW parts_management.v_pending_claims AS
|
||||
SELECT
|
||||
wc.id,
|
||||
wc.tenant_id,
|
||||
wc.part_name,
|
||||
wc.part_sku,
|
||||
wc.supplier_id,
|
||||
wc.supplier_name,
|
||||
wc.claim_date,
|
||||
wc.claim_reason,
|
||||
wc.claim_amount,
|
||||
wc.vehicle_plate,
|
||||
wc.customer_name,
|
||||
CURRENT_DATE - wc.claim_date as days_pending
|
||||
FROM parts_management.warranty_claims wc
|
||||
WHERE wc.status = 'claimed'
|
||||
ORDER BY wc.claim_date ASC;
|
||||
|
||||
COMMENT ON VIEW parts_management.v_pending_claims IS 'Reclamos de garantia pendientes de resolucion';
|
||||
|
||||
-- Vista resumen de garantias por proveedor
|
||||
CREATE VIEW parts_management.v_warranty_summary_by_supplier AS
|
||||
SELECT
|
||||
wc.tenant_id,
|
||||
wc.supplier_id,
|
||||
wc.supplier_name,
|
||||
COUNT(*) as total_warranties,
|
||||
COUNT(*) FILTER (WHERE status = 'active') as active_warranties,
|
||||
COUNT(*) FILTER (WHERE status = 'claimed') as pending_claims,
|
||||
COUNT(*) FILTER (WHERE status IN ('approved', 'replaced')) as approved_claims,
|
||||
COUNT(*) FILTER (WHERE status = 'rejected') as rejected_claims,
|
||||
COALESCE(SUM(approved_amount) FILTER (WHERE status IN ('approved', 'replaced')), 0) as total_approved_amount,
|
||||
ROUND(
|
||||
COUNT(*) FILTER (WHERE status IN ('approved', 'replaced'))::DECIMAL /
|
||||
NULLIF(COUNT(*) FILTER (WHERE status IN ('approved', 'replaced', 'rejected')), 0) * 100,
|
||||
2
|
||||
) as approval_rate
|
||||
FROM parts_management.warranty_claims wc
|
||||
WHERE wc.supplier_id IS NOT NULL
|
||||
GROUP BY wc.tenant_id, wc.supplier_id, wc.supplier_name
|
||||
ORDER BY total_warranties DESC;
|
||||
|
||||
COMMENT ON VIEW parts_management.v_warranty_summary_by_supplier IS 'Resumen de garantias agrupado por proveedor';
|
||||
|
||||
-- Vista de garantias por vehiculo
|
||||
CREATE VIEW parts_management.v_vehicle_warranties AS
|
||||
SELECT
|
||||
wc.tenant_id,
|
||||
wc.vehicle_id,
|
||||
wc.vehicle_plate,
|
||||
wc.vehicle_description,
|
||||
wc.customer_id,
|
||||
wc.customer_name,
|
||||
COUNT(*) as total_parts_with_warranty,
|
||||
COUNT(*) FILTER (WHERE status = 'active' AND expiration_date >= CURRENT_DATE) as active_warranties,
|
||||
COUNT(*) FILTER (WHERE status = 'active' AND expiration_date >= CURRENT_DATE AND expiration_date - CURRENT_DATE <= 30) as expiring_soon,
|
||||
COALESCE(SUM(original_cost), 0) as total_warranty_value
|
||||
FROM parts_management.warranty_claims wc
|
||||
WHERE wc.vehicle_id IS NOT NULL
|
||||
GROUP BY wc.tenant_id, wc.vehicle_id, wc.vehicle_plate, wc.vehicle_description,
|
||||
wc.customer_id, wc.customer_name
|
||||
ORDER BY active_warranties DESC;
|
||||
|
||||
COMMENT ON VIEW parts_management.v_vehicle_warranties IS 'Resumen de garantias por vehiculo';
|
||||
|
||||
-- ============================================
|
||||
-- GRANTS
|
||||
-- ============================================
|
||||
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA parts_management TO mecanicas_user;
|
||||
GRANT SELECT ON ALL TABLES IN SCHEMA parts_management TO mecanicas_user;
|
||||
426
init/11-quote-signature.sql
Normal file
426
init/11-quote-signature.sql
Normal file
@ -0,0 +1,426 @@
|
||||
-- ===========================================
|
||||
-- MECANICAS DIESEL - Firma Electronica Basica
|
||||
-- ===========================================
|
||||
-- Resuelve: GAP-12
|
||||
-- Sistema de firma canvas para aprobacion de cotizaciones
|
||||
-- Nota: Para NOM-151 completa ver SPEC-FIRMA-ELECTRONICA-NOM151.md
|
||||
|
||||
-- ============================================
|
||||
-- EXTENSION DE QUOTES PARA FIRMA
|
||||
-- ============================================
|
||||
|
||||
-- Agregar campos de firma a cotizaciones
|
||||
DO $$
|
||||
BEGIN
|
||||
-- signature_data (canvas base64)
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'service_management'
|
||||
AND table_name = 'quotes'
|
||||
AND column_name = 'signature_data'
|
||||
) THEN
|
||||
ALTER TABLE service_management.quotes
|
||||
ADD COLUMN signature_data TEXT;
|
||||
COMMENT ON COLUMN service_management.quotes.signature_data IS 'Firma canvas en formato base64 PNG';
|
||||
END IF;
|
||||
|
||||
-- signed_at
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'service_management'
|
||||
AND table_name = 'quotes'
|
||||
AND column_name = 'signed_at'
|
||||
) THEN
|
||||
ALTER TABLE service_management.quotes
|
||||
ADD COLUMN signed_at TIMESTAMPTZ;
|
||||
COMMENT ON COLUMN service_management.quotes.signed_at IS 'Fecha y hora de firma';
|
||||
END IF;
|
||||
|
||||
-- signed_by_name
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'service_management'
|
||||
AND table_name = 'quotes'
|
||||
AND column_name = 'signed_by_name'
|
||||
) THEN
|
||||
ALTER TABLE service_management.quotes
|
||||
ADD COLUMN signed_by_name VARCHAR(256);
|
||||
COMMENT ON COLUMN service_management.quotes.signed_by_name IS 'Nombre de quien firmo';
|
||||
END IF;
|
||||
|
||||
-- signed_by_ip
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'service_management'
|
||||
AND table_name = 'quotes'
|
||||
AND column_name = 'signed_by_ip'
|
||||
) THEN
|
||||
ALTER TABLE service_management.quotes
|
||||
ADD COLUMN signed_by_ip VARCHAR(45);
|
||||
COMMENT ON COLUMN service_management.quotes.signed_by_ip IS 'IP desde donde se firmo';
|
||||
END IF;
|
||||
|
||||
-- signed_by_email
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'service_management'
|
||||
AND table_name = 'quotes'
|
||||
AND column_name = 'signed_by_email'
|
||||
) THEN
|
||||
ALTER TABLE service_management.quotes
|
||||
ADD COLUMN signed_by_email VARCHAR(256);
|
||||
COMMENT ON COLUMN service_management.quotes.signed_by_email IS 'Email de quien firmo';
|
||||
END IF;
|
||||
|
||||
-- signature_hash
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'service_management'
|
||||
AND table_name = 'quotes'
|
||||
AND column_name = 'signature_hash'
|
||||
) THEN
|
||||
ALTER TABLE service_management.quotes
|
||||
ADD COLUMN signature_hash VARCHAR(128);
|
||||
COMMENT ON COLUMN service_management.quotes.signature_hash IS 'Hash SHA-256 del documento al momento de firmar';
|
||||
END IF;
|
||||
|
||||
-- approval_token
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'service_management'
|
||||
AND table_name = 'quotes'
|
||||
AND column_name = 'approval_token'
|
||||
) THEN
|
||||
ALTER TABLE service_management.quotes
|
||||
ADD COLUMN approval_token VARCHAR(64);
|
||||
COMMENT ON COLUMN service_management.quotes.approval_token IS 'Token unico para link de aprobacion';
|
||||
END IF;
|
||||
|
||||
-- token_expires_at
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'service_management'
|
||||
AND table_name = 'quotes'
|
||||
AND column_name = 'token_expires_at'
|
||||
) THEN
|
||||
ALTER TABLE service_management.quotes
|
||||
ADD COLUMN token_expires_at TIMESTAMPTZ;
|
||||
COMMENT ON COLUMN service_management.quotes.token_expires_at IS 'Expiracion del token de aprobacion';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Indice para busqueda por token
|
||||
CREATE INDEX IF NOT EXISTS idx_quotes_approval_token
|
||||
ON service_management.quotes(approval_token)
|
||||
WHERE approval_token IS NOT NULL;
|
||||
|
||||
-- ============================================
|
||||
-- TABLA DE HISTORIAL DE FIRMAS
|
||||
-- ============================================
|
||||
|
||||
-- Historial de todas las firmas (para auditoria)
|
||||
CREATE TABLE IF NOT EXISTS service_management.signature_audit (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
-- Documento firmado
|
||||
document_type VARCHAR(50) NOT NULL, -- 'quote', 'service_order', etc.
|
||||
document_id UUID NOT NULL,
|
||||
document_number VARCHAR(50),
|
||||
|
||||
-- Datos del firmante
|
||||
signer_name VARCHAR(256) NOT NULL,
|
||||
signer_email VARCHAR(256),
|
||||
signer_phone VARCHAR(50),
|
||||
signer_ip VARCHAR(45),
|
||||
signer_user_agent TEXT,
|
||||
|
||||
-- Firma
|
||||
signature_data TEXT NOT NULL, -- Base64 de la imagen de firma
|
||||
signature_method VARCHAR(50) NOT NULL DEFAULT 'canvas', -- canvas, typed, upload
|
||||
|
||||
-- Integridad
|
||||
document_hash VARCHAR(128) NOT NULL, -- Hash del documento al firmar
|
||||
signature_hash VARCHAR(128), -- Hash de la firma
|
||||
document_snapshot JSONB, -- Snapshot del documento
|
||||
|
||||
-- Contexto
|
||||
action VARCHAR(50) NOT NULL, -- 'approve', 'reject', 'acknowledge'
|
||||
comments TEXT,
|
||||
|
||||
-- Auditoria
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Geolocation (opcional, si el cliente lo permite)
|
||||
geo_latitude DECIMAL(10, 8),
|
||||
geo_longitude DECIMAL(11, 8),
|
||||
geo_accuracy DECIMAL(10, 2)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE service_management.signature_audit IS 'Registro de auditoria de todas las firmas electronicas';
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX idx_sig_audit_tenant ON service_management.signature_audit(tenant_id);
|
||||
CREATE INDEX idx_sig_audit_document ON service_management.signature_audit(document_type, document_id);
|
||||
CREATE INDEX idx_sig_audit_signer ON service_management.signature_audit(signer_email);
|
||||
CREATE INDEX idx_sig_audit_created ON service_management.signature_audit(created_at DESC);
|
||||
|
||||
-- RLS
|
||||
SELECT create_tenant_rls_policies('service_management', 'signature_audit');
|
||||
|
||||
-- ============================================
|
||||
-- FUNCIONES AUXILIARES
|
||||
-- ============================================
|
||||
|
||||
-- Funcion para generar token de aprobacion
|
||||
CREATE OR REPLACE FUNCTION service_management.generate_approval_token(
|
||||
p_quote_id UUID,
|
||||
p_expires_hours INTEGER DEFAULT 72
|
||||
)
|
||||
RETURNS VARCHAR(64) AS $$
|
||||
DECLARE
|
||||
v_token VARCHAR(64);
|
||||
BEGIN
|
||||
-- Generar token aleatorio
|
||||
v_token := encode(gen_random_bytes(32), 'hex');
|
||||
|
||||
-- Actualizar cotizacion con token
|
||||
UPDATE service_management.quotes
|
||||
SET
|
||||
approval_token = v_token,
|
||||
token_expires_at = NOW() + (p_expires_hours || ' hours')::INTERVAL
|
||||
WHERE id = p_quote_id;
|
||||
|
||||
RETURN v_token;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION service_management.generate_approval_token IS 'Genera token de aprobacion para cotizacion';
|
||||
|
||||
-- Funcion para validar token
|
||||
CREATE OR REPLACE FUNCTION service_management.validate_approval_token(
|
||||
p_token VARCHAR(64)
|
||||
)
|
||||
RETURNS TABLE (
|
||||
quote_id UUID,
|
||||
is_valid BOOLEAN,
|
||||
error_message TEXT
|
||||
) AS $$
|
||||
DECLARE
|
||||
v_quote RECORD;
|
||||
BEGIN
|
||||
-- Buscar cotizacion con token
|
||||
SELECT q.id, q.status, q.token_expires_at, q.signed_at
|
||||
INTO v_quote
|
||||
FROM service_management.quotes q
|
||||
WHERE q.approval_token = p_token;
|
||||
|
||||
-- Token no encontrado
|
||||
IF v_quote.id IS NULL THEN
|
||||
RETURN QUERY SELECT
|
||||
NULL::UUID,
|
||||
false,
|
||||
'Token invalido o no encontrado'::TEXT;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Token expirado
|
||||
IF v_quote.token_expires_at < NOW() THEN
|
||||
RETURN QUERY SELECT
|
||||
v_quote.id,
|
||||
false,
|
||||
'El token ha expirado'::TEXT;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Ya firmada
|
||||
IF v_quote.signed_at IS NOT NULL THEN
|
||||
RETURN QUERY SELECT
|
||||
v_quote.id,
|
||||
false,
|
||||
'La cotizacion ya fue firmada'::TEXT;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Cotizacion no esta en estado correcto
|
||||
IF v_quote.status NOT IN ('sent', 'pending') THEN
|
||||
RETURN QUERY SELECT
|
||||
v_quote.id,
|
||||
false,
|
||||
'La cotizacion no esta disponible para aprobacion'::TEXT;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Token valido
|
||||
RETURN QUERY SELECT
|
||||
v_quote.id,
|
||||
true,
|
||||
NULL::TEXT;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION service_management.validate_approval_token IS 'Valida token de aprobacion y estado de cotizacion';
|
||||
|
||||
-- Funcion para firmar cotizacion
|
||||
CREATE OR REPLACE FUNCTION service_management.sign_quote(
|
||||
p_quote_id UUID,
|
||||
p_signature_data TEXT,
|
||||
p_signer_name VARCHAR(256),
|
||||
p_signer_email VARCHAR(256) DEFAULT NULL,
|
||||
p_signer_ip VARCHAR(45) DEFAULT NULL,
|
||||
p_user_agent TEXT DEFAULT NULL,
|
||||
p_comments TEXT DEFAULT NULL,
|
||||
p_action VARCHAR(50) DEFAULT 'approve'
|
||||
)
|
||||
RETURNS UUID AS $$
|
||||
DECLARE
|
||||
v_quote RECORD;
|
||||
v_audit_id UUID;
|
||||
v_document_hash VARCHAR(128);
|
||||
v_signature_hash VARCHAR(128);
|
||||
v_new_status VARCHAR(20);
|
||||
BEGIN
|
||||
-- Obtener cotizacion
|
||||
SELECT q.*, c.name as customer_name
|
||||
INTO v_quote
|
||||
FROM service_management.quotes q
|
||||
LEFT JOIN workshop_core.customers c ON c.id = q.customer_id
|
||||
WHERE q.id = p_quote_id;
|
||||
|
||||
IF v_quote.id IS NULL THEN
|
||||
RAISE EXCEPTION 'Quote % not found', p_quote_id;
|
||||
END IF;
|
||||
|
||||
-- Verificar que no este ya firmada
|
||||
IF v_quote.signed_at IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Quote already signed on %', v_quote.signed_at;
|
||||
END IF;
|
||||
|
||||
-- Generar hash del documento (simplificado - en produccion usar contenido completo)
|
||||
v_document_hash := encode(
|
||||
sha256(
|
||||
(v_quote.id::TEXT || v_quote.total::TEXT || v_quote.created_at::TEXT)::bytea
|
||||
),
|
||||
'hex'
|
||||
);
|
||||
|
||||
-- Generar hash de la firma
|
||||
v_signature_hash := encode(sha256(p_signature_data::bytea), 'hex');
|
||||
|
||||
-- Determinar nuevo estado
|
||||
v_new_status := CASE p_action
|
||||
WHEN 'approve' THEN 'approved'
|
||||
WHEN 'reject' THEN 'rejected'
|
||||
ELSE 'pending'
|
||||
END;
|
||||
|
||||
-- Actualizar cotizacion
|
||||
UPDATE service_management.quotes
|
||||
SET
|
||||
status = v_new_status,
|
||||
signature_data = p_signature_data,
|
||||
signed_at = NOW(),
|
||||
signed_by_name = p_signer_name,
|
||||
signed_by_email = p_signer_email,
|
||||
signed_by_ip = p_signer_ip,
|
||||
signature_hash = v_document_hash,
|
||||
approval_token = NULL, -- Invalidar token
|
||||
token_expires_at = NULL,
|
||||
updated_at = NOW()
|
||||
WHERE id = p_quote_id;
|
||||
|
||||
-- Crear registro de auditoria
|
||||
INSERT INTO service_management.signature_audit (
|
||||
tenant_id,
|
||||
document_type, document_id, document_number,
|
||||
signer_name, signer_email, signer_ip, signer_user_agent,
|
||||
signature_data, signature_method,
|
||||
document_hash, signature_hash,
|
||||
document_snapshot,
|
||||
action, comments
|
||||
)
|
||||
VALUES (
|
||||
v_quote.tenant_id,
|
||||
'quote', p_quote_id, v_quote.quote_number,
|
||||
p_signer_name, p_signer_email, p_signer_ip, p_user_agent,
|
||||
p_signature_data, 'canvas',
|
||||
v_document_hash, v_signature_hash,
|
||||
jsonb_build_object(
|
||||
'quote_number', v_quote.quote_number,
|
||||
'customer_name', v_quote.customer_name,
|
||||
'total', v_quote.total,
|
||||
'created_at', v_quote.created_at,
|
||||
'items_count', (SELECT COUNT(*) FROM service_management.quote_lines WHERE quote_id = p_quote_id)
|
||||
),
|
||||
p_action, p_comments
|
||||
)
|
||||
RETURNING id INTO v_audit_id;
|
||||
|
||||
RETURN v_audit_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION service_management.sign_quote IS 'Firma una cotizacion con firma canvas y crea registro de auditoria';
|
||||
|
||||
-- ============================================
|
||||
-- VISTAS
|
||||
-- ============================================
|
||||
|
||||
-- Vista de cotizaciones pendientes de firma
|
||||
CREATE VIEW service_management.v_quotes_pending_signature AS
|
||||
SELECT
|
||||
q.id,
|
||||
q.tenant_id,
|
||||
q.quote_number,
|
||||
q.status,
|
||||
q.total,
|
||||
c.name as customer_name,
|
||||
c.email as customer_email,
|
||||
c.phone as customer_phone,
|
||||
q.approval_token IS NOT NULL as has_token,
|
||||
q.token_expires_at,
|
||||
CASE
|
||||
WHEN q.token_expires_at IS NULL THEN 'no_token'
|
||||
WHEN q.token_expires_at < NOW() THEN 'expired'
|
||||
WHEN q.token_expires_at < NOW() + INTERVAL '24 hours' THEN 'expiring_soon'
|
||||
ELSE 'valid'
|
||||
END as token_status,
|
||||
q.created_at,
|
||||
q.updated_at
|
||||
FROM service_management.quotes q
|
||||
LEFT JOIN workshop_core.customers c ON c.id = q.customer_id
|
||||
WHERE q.status IN ('sent', 'pending')
|
||||
AND q.signed_at IS NULL
|
||||
ORDER BY q.created_at DESC;
|
||||
|
||||
COMMENT ON VIEW service_management.v_quotes_pending_signature IS 'Cotizaciones pendientes de firma del cliente';
|
||||
|
||||
-- Vista de historial de firmas
|
||||
CREATE VIEW service_management.v_signature_history AS
|
||||
SELECT
|
||||
sa.id,
|
||||
sa.tenant_id,
|
||||
sa.document_type,
|
||||
sa.document_number,
|
||||
sa.signer_name,
|
||||
sa.signer_email,
|
||||
sa.action,
|
||||
sa.created_at as signed_at,
|
||||
sa.signer_ip,
|
||||
sa.comments,
|
||||
-- Info adicional del documento
|
||||
CASE sa.document_type
|
||||
WHEN 'quote' THEN (SELECT total FROM service_management.quotes WHERE id = sa.document_id)
|
||||
ELSE NULL
|
||||
END as document_total
|
||||
FROM service_management.signature_audit sa
|
||||
ORDER BY sa.created_at DESC;
|
||||
|
||||
COMMENT ON VIEW service_management.v_signature_history IS 'Historial de firmas electronicas';
|
||||
|
||||
-- ============================================
|
||||
-- GRANTS
|
||||
-- ============================================
|
||||
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA service_management TO mecanicas_user;
|
||||
GRANT SELECT ON service_management.signature_audit TO mecanicas_user;
|
||||
GRANT INSERT ON service_management.signature_audit TO mecanicas_user;
|
||||
Loading…
Reference in New Issue
Block a user