Some checks failed
CI Pipeline / Lint & Type Check (push) Has been cancelled
CI Pipeline / Validate SSOT Constants (push) Has been cancelled
CI Pipeline / Backend Tests (push) Has been cancelled
CI Pipeline / Frontend Tests (push) Has been cancelled
CI Pipeline / Build (push) Has been cancelled
CI Pipeline / Docker Build (push) Has been cancelled
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
43 KiB
43 KiB
FASE 6: Plan Refinado - ERP Construcción
Proyecto: erp-construccion Fecha: 2026-01-04 Estado: Completado Base: FASE-5-ANALISIS-DEPENDENCIAS.md
1. Ajustes al Plan Original
1.1 Cambios Incorporados
| # | Cambio | Razón |
|---|---|---|
| 1 | proyecto_id → fraccionamiento_id |
No existe tabla proyectos |
| 2 | projects.collaborators → construction.colaboradores_obra |
Adaptación al giro |
| 3 | FKs opcionales a Core | Permitir instalación independiente |
| 4 | Trigger adaptado | Usar avances_obra en lugar de partidas |
| 5 | Orden de ejecución revisado | Respetar dependencias |
1.2 Archivos Finales
| Archivo | Acción | Contenido |
|---|---|---|
| 08-financial-ext-schema-ddl.sql | Crear | 5 tablas, 2 ENUMs |
| 09-projects-ext-schema-ddl.sql | Crear | 3 tablas, 2 funciones (adaptado) |
| 02-hr-schema-ddl.sql | Modificar | +11 tablas, +3 ENUMs |
| 06-inventory-ext-schema-ddl.sql | Modificar | +5 tablas |
| 07-purchase-ext-schema-ddl.sql | Modificar | +1 tabla, +1 función |
| migrations/20260104_fase8.sql | Crear | Migración consolidada |
| seeds/*.sql | Crear | 4 archivos seed |
2. Scripts Finales Refinados
2.1 08-financial-ext-schema-ddl.sql (REFINADO)
-- ============================================================================
-- FINANCIAL EXTENSION Schema DDL - Extensiones Financieras para Construcción
-- Modulos: FASE-8 ERP-Core (COR-035 a COR-039)
-- Version: 1.0.0
-- Fecha: 2026-01-04
-- ============================================================================
-- PREREQUISITOS:
-- 1. ERP-Core instalado (auth.tenants, auth.users)
-- 2. Schema construction instalado
-- NOTA: FKs a tablas de ERP-Core son opcionales
-- ============================================================================
-- Verificar prerequisitos mínimos
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'auth') THEN
RAISE EXCEPTION 'Schema auth no existe. ERP-Core debe estar instalado';
END IF;
END $$;
-- Crear schema si no existe
CREATE SCHEMA IF NOT EXISTS financial;
-- ============================================================================
-- TYPES (ENUMs)
-- ============================================================================
DO $$ BEGIN
CREATE TYPE financial.payment_method_type AS ENUM ('inbound', 'outbound');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
CREATE TYPE financial.reconcile_model_type AS ENUM (
'writeoff_button', 'writeoff_suggestion', 'invoice_matching'
);
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- ============================================================================
-- TABLAS
-- ============================================================================
-- 1. Incoterms (COR-036) - Catálogo estándar
CREATE TABLE IF NOT EXISTS financial.incoterms (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(3) NOT NULL,
name VARCHAR(100) NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT uq_incoterms_code UNIQUE (code)
);
COMMENT ON TABLE financial.incoterms IS 'ERP-Core FASE-8 COR-036: Términos de comercio internacional';
-- 2. Payment Methods (COR-037)
CREATE TABLE IF NOT EXISTS financial.payment_methods (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
code VARCHAR(50) NOT NULL,
payment_type financial.payment_method_type NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT uq_payment_methods_code UNIQUE (tenant_id, code)
);
CREATE INDEX IF NOT EXISTS idx_payment_methods_tenant ON financial.payment_methods(tenant_id);
ALTER TABLE financial.payment_methods ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
DROP POLICY IF EXISTS tenant_isolation_payment_methods ON financial.payment_methods;
CREATE POLICY tenant_isolation_payment_methods ON financial.payment_methods
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
EXCEPTION WHEN undefined_object THEN NULL; END $$;
COMMENT ON TABLE financial.payment_methods IS 'ERP-Core FASE-8 COR-037: Métodos de pago';
-- 3. Payment Term Lines (COR-035)
CREATE TABLE IF NOT EXISTS financial.payment_term_lines (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
payment_term_id UUID, -- FK opcional a financial.payment_terms (ERP-Core)
sequence INTEGER NOT NULL DEFAULT 10,
value VARCHAR(20) NOT NULL DEFAULT 'percent',
value_amount DECIMAL(10,4) NOT NULL DEFAULT 100,
days INTEGER NOT NULL DEFAULT 0,
day_of_month INTEGER,
end_month BOOLEAN DEFAULT FALSE,
-- Extensión construcción
applies_to VARCHAR(50),
created_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT chk_ptl_value CHECK (value IN ('percent', 'fixed', 'balance')),
CONSTRAINT chk_ptl_applies CHECK (applies_to IS NULL OR applies_to IN ('anticipo', 'estimacion', 'retencion', 'finiquito'))
);
CREATE INDEX IF NOT EXISTS idx_payment_term_lines_tenant ON financial.payment_term_lines(tenant_id);
CREATE INDEX IF NOT EXISTS idx_payment_term_lines_term ON financial.payment_term_lines(payment_term_id);
ALTER TABLE financial.payment_term_lines ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
DROP POLICY IF EXISTS tenant_isolation_payment_term_lines ON financial.payment_term_lines;
CREATE POLICY tenant_isolation_payment_term_lines ON financial.payment_term_lines
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
EXCEPTION WHEN undefined_object THEN NULL; END $$;
COMMENT ON TABLE financial.payment_term_lines IS 'ERP-Core FASE-8 COR-035: Líneas de términos de pago';
-- 4. Reconcile Models (COR-038)
CREATE TABLE IF NOT EXISTS financial.reconcile_models (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
company_id UUID, -- FK opcional a core.companies (ERP-Core)
name VARCHAR(100) NOT NULL,
sequence INTEGER DEFAULT 10,
rule_type financial.reconcile_model_type NOT NULL DEFAULT 'writeoff_suggestion',
auto_reconcile BOOLEAN DEFAULT FALSE,
match_amount VARCHAR(20) DEFAULT 'percentage',
match_amount_min DECIMAL(5,2),
match_amount_max DECIMAL(5,2),
match_label VARCHAR(100),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT chk_rm_match CHECK (match_amount IN ('percentage', 'fixed', 'any'))
);
CREATE INDEX IF NOT EXISTS idx_reconcile_models_tenant ON financial.reconcile_models(tenant_id);
ALTER TABLE financial.reconcile_models ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
DROP POLICY IF EXISTS tenant_isolation_reconcile_models ON financial.reconcile_models;
CREATE POLICY tenant_isolation_reconcile_models ON financial.reconcile_models
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
EXCEPTION WHEN undefined_object THEN NULL; END $$;
COMMENT ON TABLE financial.reconcile_models IS 'ERP-Core FASE-8 COR-038: Modelos de conciliación';
-- 5. Reconcile Model Lines (COR-038)
CREATE TABLE IF NOT EXISTS financial.reconcile_model_lines (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
model_id UUID NOT NULL REFERENCES financial.reconcile_models(id) ON DELETE CASCADE,
sequence INTEGER DEFAULT 10,
account_id UUID, -- FK opcional a financial.accounts (ERP-Core)
amount_type VARCHAR(20) NOT NULL DEFAULT 'percentage',
amount_value DECIMAL(18,4) NOT NULL DEFAULT 100,
label VARCHAR(100),
created_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT chk_rml_amount CHECK (amount_type IN ('percentage', 'fixed', 'regex'))
);
CREATE INDEX IF NOT EXISTS idx_reconcile_model_lines_model ON financial.reconcile_model_lines(model_id);
COMMENT ON TABLE financial.reconcile_model_lines IS 'ERP-Core FASE-8 COR-038: Líneas de modelos de conciliación';
-- ============================================================================
-- FIN FINANCIAL EXTENSION
-- Total: 5 tablas, 2 ENUMs
-- ============================================================================
2.2 09-projects-ext-schema-ddl.sql (REFINADO)
-- ============================================================================
-- PROJECTS/CONSTRUCTION EXTENSION Schema DDL
-- Modulos: FASE-8 ERP-Core (COR-056 a COR-060) - ADAPTADO A CONSTRUCCIÓN
-- Version: 1.0.0
-- Fecha: 2026-01-04
-- ============================================================================
-- ADAPTACIONES:
-- - proyecto_id → fraccionamiento_id
-- - projects.collaborators → construction.colaboradores_obra
-- - burndown_chart_data → construction.avance_programado
-- ============================================================================
-- Verificar prerequisitos
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'construction') THEN
RAISE EXCEPTION 'Schema construction no existe';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'construction' AND tablename = 'fraccionamientos') THEN
RAISE EXCEPTION 'Tabla construction.fraccionamientos no existe';
END IF;
END $$;
-- Crear schema projects si no existe (para ratings genéricos)
CREATE SCHEMA IF NOT EXISTS projects;
-- ============================================================================
-- 1. COLABORADORES DE OBRA (COR-056 adaptado)
-- ============================================================================
CREATE TABLE IF NOT EXISTS construction.colaboradores_obra (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id) ON DELETE CASCADE,
partner_id UUID, -- FK opcional a core.partners
user_id UUID REFERENCES auth.users(id),
nombre VARCHAR(100),
email VARCHAR(255),
telefono VARCHAR(20),
can_read BOOLEAN DEFAULT TRUE,
can_write BOOLEAN DEFAULT FALSE,
rol VARCHAR(50),
vigencia_desde DATE,
vigencia_hasta DATE,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ,
CONSTRAINT chk_colob_partner_or_user CHECK (partner_id IS NOT NULL OR user_id IS NOT NULL OR nombre IS NOT NULL),
CONSTRAINT chk_colob_rol CHECK (rol IS NULL OR rol IN ('supervisor', 'perito', 'representante', 'infonavit', 'contratista', 'auditor', 'cliente'))
);
CREATE INDEX IF NOT EXISTS idx_colaboradores_obra_tenant ON construction.colaboradores_obra(tenant_id);
CREATE INDEX IF NOT EXISTS idx_colaboradores_obra_fracc ON construction.colaboradores_obra(fraccionamiento_id);
CREATE INDEX IF NOT EXISTS idx_colaboradores_obra_user ON construction.colaboradores_obra(user_id);
ALTER TABLE construction.colaboradores_obra ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
DROP POLICY IF EXISTS tenant_isolation_colaboradores_obra ON construction.colaboradores_obra;
CREATE POLICY tenant_isolation_colaboradores_obra ON construction.colaboradores_obra
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
EXCEPTION WHEN undefined_object THEN NULL; END $$;
COMMENT ON TABLE construction.colaboradores_obra IS 'ERP-Core FASE-8 COR-056 adaptado: Colaboradores externos de obra';
-- ============================================================================
-- 2. RATINGS/CALIFICACIONES (COR-059)
-- ============================================================================
CREATE TABLE IF NOT EXISTS projects.ratings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
res_model VARCHAR(100) NOT NULL,
res_id UUID NOT NULL,
rating DECIMAL(3,2) NOT NULL,
feedback TEXT,
partner_id UUID, -- FK opcional a core.partners
rated_by UUID REFERENCES auth.users(id),
fraccionamiento_id UUID REFERENCES construction.fraccionamientos(id),
tipo_trabajo VARCHAR(50),
created_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT chk_rating_range CHECK (rating >= 1 AND rating <= 5)
);
CREATE INDEX IF NOT EXISTS idx_ratings_tenant ON projects.ratings(tenant_id);
CREATE INDEX IF NOT EXISTS idx_ratings_res ON projects.ratings(res_model, res_id);
CREATE INDEX IF NOT EXISTS idx_ratings_fracc ON projects.ratings(fraccionamiento_id);
ALTER TABLE projects.ratings ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
DROP POLICY IF EXISTS tenant_isolation_ratings ON projects.ratings;
CREATE POLICY tenant_isolation_ratings ON projects.ratings
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
EXCEPTION WHEN undefined_object THEN NULL; END $$;
COMMENT ON TABLE projects.ratings IS 'ERP-Core FASE-8 COR-059: Calificaciones de proveedores/contratistas';
-- ============================================================================
-- 3. AVANCE PROGRAMADO (COR-060 adaptado)
-- ============================================================================
CREATE TABLE IF NOT EXISTS construction.avance_programado (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id) ON DELETE CASCADE,
fecha DATE NOT NULL,
-- Avance físico
total_conceptos INTEGER DEFAULT 0,
conceptos_completados INTEGER DEFAULT 0,
conceptos_pendientes INTEGER DEFAULT 0,
conceptos_en_proceso INTEGER DEFAULT 0,
-- Avance financiero
presupuesto_total DECIMAL(20,2) DEFAULT 0,
ejercido DECIMAL(20,2) DEFAULT 0,
por_ejercer DECIMAL(20,2) DEFAULT 0,
estimado_actual DECIMAL(20,2) DEFAULT 0,
-- Porcentajes
avance_fisico_programado DECIMAL(5,2) DEFAULT 0,
avance_fisico_real DECIMAL(5,2) DEFAULT 0,
avance_financiero_pct DECIMAL(5,2) DEFAULT 0,
-- Horas
horas_programadas DECIMAL(10,2) DEFAULT 0,
horas_ejecutadas DECIMAL(10,2) DEFAULT 0,
-- Metadatos
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by UUID REFERENCES auth.users(id),
CONSTRAINT uq_avance_fracc_fecha UNIQUE (fraccionamiento_id, fecha)
);
CREATE INDEX IF NOT EXISTS idx_avance_programado_tenant ON construction.avance_programado(tenant_id);
CREATE INDEX IF NOT EXISTS idx_avance_programado_fracc ON construction.avance_programado(fraccionamiento_id);
CREATE INDEX IF NOT EXISTS idx_avance_programado_fecha ON construction.avance_programado(fecha);
ALTER TABLE construction.avance_programado ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
DROP POLICY IF EXISTS tenant_isolation_avance_programado ON construction.avance_programado;
CREATE POLICY tenant_isolation_avance_programado ON construction.avance_programado
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
EXCEPTION WHEN undefined_object THEN NULL; END $$;
COMMENT ON TABLE construction.avance_programado IS 'ERP-Core FASE-8 COR-060 adaptado: Snapshots de avance de obra';
-- ============================================================================
-- 4. CAMPOS ADICIONALES EN FRACCIONAMIENTOS (COR-057)
-- ============================================================================
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'construction' AND table_name = 'fraccionamientos' AND column_name = 'sequence') THEN
ALTER TABLE construction.fraccionamientos ADD COLUMN sequence INTEGER DEFAULT 10;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'construction' AND table_name = 'fraccionamientos' AND column_name = 'is_favorite') THEN
ALTER TABLE construction.fraccionamientos ADD COLUMN is_favorite BOOLEAN DEFAULT FALSE;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'construction' AND table_name = 'fraccionamientos' AND column_name = 'avance_count') THEN
ALTER TABLE construction.fraccionamientos ADD COLUMN avance_count INTEGER DEFAULT 0;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'construction' AND table_name = 'fraccionamientos' AND column_name = 'avance_pct') THEN
ALTER TABLE construction.fraccionamientos ADD COLUMN avance_pct DECIMAL(5,2) DEFAULT 0;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'construction' AND table_name = 'fraccionamientos' AND column_name = 'status_avance') THEN
ALTER TABLE construction.fraccionamientos ADD COLUMN status_avance VARCHAR(20) DEFAULT 'on_track';
END IF;
END $$;
-- ============================================================================
-- 5. FUNCIÓN SNAPSHOT DE AVANCE (COR-060)
-- ============================================================================
CREATE OR REPLACE FUNCTION construction.generate_avance_snapshot(
p_fraccionamiento_id UUID,
p_fecha DATE DEFAULT CURRENT_DATE
)
RETURNS UUID AS $$
DECLARE
v_id UUID;
v_tenant_id UUID;
v_total INTEGER := 0;
v_completados INTEGER := 0;
v_pendientes INTEGER := 0;
v_en_proceso INTEGER := 0;
v_presupuesto DECIMAL(20,2) := 0;
v_ejercido DECIMAL(20,2) := 0;
BEGIN
-- Obtener tenant
SELECT tenant_id INTO v_tenant_id
FROM construction.fraccionamientos
WHERE id = p_fraccionamiento_id;
IF v_tenant_id IS NULL THEN
RAISE EXCEPTION 'Fraccionamiento no encontrado: %', p_fraccionamiento_id;
END IF;
-- Contar avances por estado
SELECT
COUNT(*),
COUNT(*) FILTER (WHERE status = 'approved'),
COUNT(*) FILTER (WHERE status = 'pending'),
COUNT(*) FILTER (WHERE status IN ('captured', 'reviewed'))
INTO v_total, v_completados, v_pendientes, v_en_proceso
FROM construction.avances_obra ao
JOIN construction.lotes l ON ao.lote_id = l.id
JOIN construction.manzanas m ON l.manzana_id = m.id
JOIN construction.etapas e ON m.etapa_id = e.id
WHERE e.fraccionamiento_id = p_fraccionamiento_id;
-- Obtener montos de presupuesto
SELECT
COALESCE(SUM(pp.quantity * pp.unit_price), 0),
0 -- ejercido se calcularía de otra forma
INTO v_presupuesto, v_ejercido
FROM construction.presupuestos p
JOIN construction.presupuesto_partidas pp ON pp.presupuesto_id = p.id
WHERE p.fraccionamiento_id = p_fraccionamiento_id
AND p.is_active = TRUE;
-- Insertar o actualizar snapshot
INSERT INTO construction.avance_programado (
tenant_id, fraccionamiento_id, fecha,
total_conceptos, conceptos_completados, conceptos_pendientes, conceptos_en_proceso,
presupuesto_total, ejercido, por_ejercer,
avance_fisico_real
) VALUES (
v_tenant_id, p_fraccionamiento_id, p_fecha,
v_total, v_completados, v_pendientes, v_en_proceso,
v_presupuesto, v_ejercido, v_presupuesto - v_ejercido,
CASE WHEN v_total > 0 THEN (v_completados::DECIMAL / v_total) * 100 ELSE 0 END
)
ON CONFLICT (fraccionamiento_id, fecha) DO UPDATE SET
total_conceptos = EXCLUDED.total_conceptos,
conceptos_completados = EXCLUDED.conceptos_completados,
conceptos_pendientes = EXCLUDED.conceptos_pendientes,
conceptos_en_proceso = EXCLUDED.conceptos_en_proceso,
presupuesto_total = EXCLUDED.presupuesto_total,
ejercido = EXCLUDED.ejercido,
por_ejercer = EXCLUDED.por_ejercer,
avance_fisico_real = EXCLUDED.avance_fisico_real
RETURNING id INTO v_id;
RETURN v_id;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION construction.generate_avance_snapshot IS 'Genera snapshot diario de avance de obra';
-- ============================================================================
-- FIN PROJECTS/CONSTRUCTION EXTENSION
-- Total: 3 tablas, 2 funciones
-- ============================================================================
2.3 Adiciones a 02-hr-schema-ddl.sql (REFINADO)
-- ============================================================================
-- HR EXTENSION - FASE-8 ERP-Core
-- Agregar al final de 02-hr-schema-ddl.sql
-- ============================================================================
-- ============================================================================
-- ENUMS ADICIONALES
-- ============================================================================
DO $$ BEGIN
CREATE TYPE hr.expense_status AS ENUM ('draft', 'submitted', 'approved', 'posted', 'paid', 'rejected');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
CREATE TYPE hr.resume_line_type AS ENUM ('experience', 'education', 'certification', 'internal');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
CREATE TYPE hr.payslip_status AS ENUM ('draft', 'verify', 'done', 'cancel');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- ============================================================================
-- 1. WORK LOCATIONS (COR-062)
-- ============================================================================
CREATE TABLE IF NOT EXISTS hr.work_locations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
company_id UUID, -- FK opcional
name VARCHAR(100) NOT NULL,
location_type VARCHAR(50) DEFAULT 'office',
address_id UUID, -- FK opcional
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT chk_wl_type CHECK (location_type IN ('office', 'home', 'obra', 'other'))
);
ALTER TABLE hr.work_locations ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
DROP POLICY IF EXISTS tenant_isolation_work_locations ON hr.work_locations;
CREATE POLICY tenant_isolation_work_locations ON hr.work_locations
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
EXCEPTION WHEN undefined_object THEN NULL; END $$;
-- Extensión: Ubicaciones de obra
CREATE TABLE IF NOT EXISTS construction.ubicaciones_obra (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
work_location_id UUID REFERENCES hr.work_locations(id),
fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id),
tipo VARCHAR(50) NOT NULL,
nombre VARCHAR(100),
coordenadas_lat DECIMAL(10,8),
coordenadas_lng DECIMAL(11,8),
radio_metros INTEGER DEFAULT 100,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT chk_uo_tipo CHECK (tipo IN ('frente', 'almacen', 'oficina_obra', 'caseta', 'acceso'))
);
ALTER TABLE construction.ubicaciones_obra ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
DROP POLICY IF EXISTS tenant_isolation_ubicaciones_obra ON construction.ubicaciones_obra;
CREATE POLICY tenant_isolation_ubicaciones_obra ON construction.ubicaciones_obra
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
EXCEPTION WHEN undefined_object THEN NULL; END $$;
-- ============================================================================
-- 2. SKILLS SYSTEM (COR-063)
-- ============================================================================
CREATE TABLE IF NOT EXISTS hr.skill_types (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS hr.skills (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
skill_type_id UUID NOT NULL REFERENCES hr.skill_types(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS hr.skill_levels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
skill_type_id UUID NOT NULL REFERENCES hr.skill_types(id) ON DELETE CASCADE,
name VARCHAR(50) NOT NULL,
level INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS hr.employee_skills (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
employee_id UUID NOT NULL REFERENCES hr.employees(id) ON DELETE CASCADE,
skill_id UUID NOT NULL REFERENCES hr.skills(id),
skill_level_id UUID REFERENCES hr.skill_levels(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT uq_employee_skill UNIQUE (employee_id, skill_id)
);
-- RLS para skills
ALTER TABLE hr.skill_types ENABLE ROW LEVEL SECURITY;
ALTER TABLE hr.skills ENABLE ROW LEVEL SECURITY;
ALTER TABLE hr.skill_levels ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
DROP POLICY IF EXISTS tenant_isolation_skill_types ON hr.skill_types;
CREATE POLICY tenant_isolation_skill_types ON hr.skill_types
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
EXCEPTION WHEN undefined_object THEN NULL; END $$;
DO $$ BEGIN
DROP POLICY IF EXISTS tenant_isolation_skills ON hr.skills;
CREATE POLICY tenant_isolation_skills ON hr.skills
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
EXCEPTION WHEN undefined_object THEN NULL; END $$;
DO $$ BEGIN
DROP POLICY IF EXISTS tenant_isolation_skill_levels ON hr.skill_levels;
CREATE POLICY tenant_isolation_skill_levels ON hr.skill_levels
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
EXCEPTION WHEN undefined_object THEN NULL; END $$;
-- ============================================================================
-- 3. EXPENSE SYSTEM (COR-064)
-- ============================================================================
CREATE TABLE IF NOT EXISTS hr.expense_sheets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
company_id UUID,
employee_id UUID NOT NULL REFERENCES hr.employees(id),
name VARCHAR(100) NOT NULL,
status hr.expense_status DEFAULT 'draft',
total_amount DECIMAL(18,4) DEFAULT 0,
currency_id UUID,
approved_by UUID REFERENCES auth.users(id),
approved_at TIMESTAMPTZ,
fraccionamiento_id UUID REFERENCES construction.fraccionamientos(id),
centro_costo VARCHAR(50),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS hr.expenses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
expense_sheet_id UUID REFERENCES hr.expense_sheets(id) ON DELETE SET NULL,
employee_id UUID NOT NULL REFERENCES hr.employees(id),
name VARCHAR(200) NOT NULL,
product_id UUID,
unit_amount DECIMAL(18,4) NOT NULL,
quantity DECIMAL(10,4) DEFAULT 1,
total_amount DECIMAL(18,4) GENERATED ALWAYS AS (unit_amount * quantity) STORED,
currency_id UUID,
date DATE NOT NULL DEFAULT CURRENT_DATE,
reference VARCHAR(100),
description TEXT,
fraccionamiento_id UUID REFERENCES construction.fraccionamientos(id),
concepto_id UUID REFERENCES construction.conceptos(id),
created_at TIMESTAMPTZ DEFAULT NOW()
);
ALTER TABLE hr.expense_sheets ENABLE ROW LEVEL SECURITY;
ALTER TABLE hr.expenses ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
DROP POLICY IF EXISTS tenant_isolation_expense_sheets ON hr.expense_sheets;
CREATE POLICY tenant_isolation_expense_sheets ON hr.expense_sheets
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
EXCEPTION WHEN undefined_object THEN NULL; END $$;
DO $$ BEGIN
DROP POLICY IF EXISTS tenant_isolation_expenses ON hr.expenses;
CREATE POLICY tenant_isolation_expenses ON hr.expenses
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
EXCEPTION WHEN undefined_object THEN NULL; END $$;
-- ============================================================================
-- 4. RESUME LINES (COR-065)
-- ============================================================================
CREATE TABLE IF NOT EXISTS hr.employee_resume_lines (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
employee_id UUID NOT NULL REFERENCES hr.employees(id) ON DELETE CASCADE,
name VARCHAR(200) NOT NULL,
date_start DATE,
date_end DATE,
line_type hr.resume_line_type NOT NULL,
description TEXT,
company_name VARCHAR(100),
job_title VARCHAR(100),
institution VARCHAR(100),
degree VARCHAR(100),
certificate_number VARCHAR(50),
issuing_authority VARCHAR(100),
expiry_date DATE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================================================
-- 5. PAYSLIP SYSTEM (COR-066)
-- ============================================================================
CREATE TABLE IF NOT EXISTS hr.payslip_structures (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
code VARCHAR(50) NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
tipo_pago VARCHAR(50),
created_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT uq_payslip_struct_code UNIQUE (tenant_id, code)
);
CREATE TABLE IF NOT EXISTS hr.payslips (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
company_id UUID,
employee_id UUID NOT NULL REFERENCES hr.employees(id),
contract_id UUID, -- FK opcional a hr.contracts
structure_id UUID REFERENCES hr.payslip_structures(id),
name VARCHAR(100) NOT NULL,
number VARCHAR(50),
status hr.payslip_status DEFAULT 'draft',
date_from DATE NOT NULL,
date_to DATE NOT NULL,
fraccionamiento_id UUID REFERENCES construction.fraccionamientos(id),
is_destajo BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS hr.payslip_lines (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
payslip_id UUID NOT NULL REFERENCES hr.payslips(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
code VARCHAR(50) NOT NULL,
category VARCHAR(50) NOT NULL,
quantity DECIMAL(10,4) DEFAULT 1,
rate DECIMAL(18,4) DEFAULT 0,
amount DECIMAL(18,4) NOT NULL DEFAULT 0,
sequence INTEGER DEFAULT 10,
created_at TIMESTAMPTZ DEFAULT NOW()
);
ALTER TABLE hr.payslip_structures ENABLE ROW LEVEL SECURITY;
ALTER TABLE hr.payslips ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
DROP POLICY IF EXISTS tenant_isolation_payslip_structures ON hr.payslip_structures;
CREATE POLICY tenant_isolation_payslip_structures ON hr.payslip_structures
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
EXCEPTION WHEN undefined_object THEN NULL; END $$;
DO $$ BEGIN
DROP POLICY IF EXISTS tenant_isolation_payslips ON hr.payslips;
CREATE POLICY tenant_isolation_payslips ON hr.payslips
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
EXCEPTION WHEN undefined_object THEN NULL; END $$;
-- ============================================================================
-- 6. CAMPOS ADICIONALES EN EMPLOYEES (COR-061)
-- ============================================================================
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'hr' AND table_name = 'employees' AND column_name = 'work_location_id') THEN
ALTER TABLE hr.employees ADD COLUMN work_location_id UUID REFERENCES hr.work_locations(id);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'hr' AND table_name = 'employees' AND column_name = 'badge_id') THEN
ALTER TABLE hr.employees ADD COLUMN badge_id VARCHAR(50);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'hr' AND table_name = 'employees' AND column_name = 'pin') THEN
ALTER TABLE hr.employees ADD COLUMN pin VARCHAR(10);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'hr' AND table_name = 'employees' AND column_name = 'barcode') THEN
ALTER TABLE hr.employees ADD COLUMN barcode VARCHAR(50);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'hr' AND table_name = 'employees' AND column_name = 'vehicle') THEN
ALTER TABLE hr.employees ADD COLUMN vehicle VARCHAR(100);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'hr' AND table_name = 'employees' AND column_name = 'vehicle_license_plate') THEN
ALTER TABLE hr.employees ADD COLUMN vehicle_license_plate VARCHAR(20);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'hr' AND table_name = 'employees' AND column_name = 'certificate') THEN
ALTER TABLE hr.employees ADD COLUMN certificate VARCHAR(50);
END IF;
END $$;
-- ============================================================================
-- COMENTARIOS
-- ============================================================================
COMMENT ON TABLE hr.work_locations IS 'ERP-Core FASE-8 COR-062: Ubicaciones de trabajo';
COMMENT ON TABLE construction.ubicaciones_obra IS 'Extensión: Ubicaciones específicas de obra';
COMMENT ON TABLE hr.skill_types IS 'ERP-Core FASE-8 COR-063: Tipos de habilidad';
COMMENT ON TABLE hr.skills IS 'ERP-Core FASE-8 COR-063: Habilidades';
COMMENT ON TABLE hr.skill_levels IS 'ERP-Core FASE-8 COR-063: Niveles de habilidad';
COMMENT ON TABLE hr.employee_skills IS 'ERP-Core FASE-8 COR-063: Habilidades de empleado';
COMMENT ON TABLE hr.expense_sheets IS 'ERP-Core FASE-8 COR-064: Hojas de gastos';
COMMENT ON TABLE hr.expenses IS 'ERP-Core FASE-8 COR-064: Gastos individuales';
COMMENT ON TABLE hr.employee_resume_lines IS 'ERP-Core FASE-8 COR-065: CV de empleado';
COMMENT ON TABLE hr.payslip_structures IS 'ERP-Core FASE-8 COR-066: Estructuras de nómina';
COMMENT ON TABLE hr.payslips IS 'ERP-Core FASE-8 COR-066: Recibos de nómina';
COMMENT ON TABLE hr.payslip_lines IS 'ERP-Core FASE-8 COR-066: Líneas de nómina';
-- ============================================================================
-- FIN HR EXTENSION FASE-8
-- Total: 13 tablas, 3 ENUMs
-- ============================================================================
3. Seed Data Refinado
3.1 seeds/00-fase8-incoterms.sql
-- Incoterms estándar (sin tenant_id)
INSERT INTO financial.incoterms (code, name) VALUES
('EXW', 'Ex Works'),
('FCA', 'Free Carrier'),
('CPT', 'Carriage Paid To'),
('CIP', 'Carriage and Insurance Paid To'),
('DAP', 'Delivered at Place'),
('DPU', 'Delivered at Place Unloaded'),
('DDP', 'Delivered Duty Paid'),
('FAS', 'Free Alongside Ship'),
('FOB', 'Free On Board'),
('CFR', 'Cost and Freight'),
('CIF', 'Cost, Insurance and Freight')
ON CONFLICT (code) DO NOTHING;
3.2 seeds/01-fase8-removal-strategies.sql
-- Estrategias de remoción (sin tenant_id)
INSERT INTO inventory.removal_strategies (code, name, description) VALUES
('fifo', 'First In First Out', 'Salida por fecha de entrada más antigua'),
('lifo', 'Last In First Out', 'Salida por fecha de entrada más reciente'),
('fefo', 'First Expired First Out', 'Salida por fecha de caducidad más próxima'),
('closest', 'Closest Location', 'Salida por ubicación más cercana')
ON CONFLICT (code) DO NOTHING;
3.3 seeds/02-fase8-construccion-skills.sql
-- Seed con tenant específico (usar en contexto de sesión)
-- Ejecutar después de SET app.current_tenant_id = 'UUID-DEL-TENANT';
-- Tipos de habilidad
INSERT INTO hr.skill_types (tenant_id, name)
SELECT current_setting('app.current_tenant_id', true)::UUID, name
FROM (VALUES
('Albañilería'),
('Plomería'),
('Electricidad'),
('Herrería'),
('Carpintería'),
('Acabados'),
('Maquinaria Pesada'),
('Topografía'),
('Soldadura')
) AS t(name)
ON CONFLICT DO NOTHING;
-- Niveles (crear para cada tipo)
INSERT INTO hr.skill_levels (tenant_id, skill_type_id, name, level)
SELECT
current_setting('app.current_tenant_id', true)::UUID,
st.id,
l.name,
l.level
FROM hr.skill_types st
CROSS JOIN (VALUES
('Ayudante', 1),
('Oficial', 2),
('Maestro', 3),
('Especialista', 4)
) AS l(name, level)
WHERE st.tenant_id = current_setting('app.current_tenant_id', true)::UUID
ON CONFLICT DO NOTHING;
3.4 seeds/03-fase8-construccion-catalogos.sql
-- Categorías de almacén
INSERT INTO inventory.storage_categories (tenant_id, name, max_weight, allow_new_product)
SELECT current_setting('app.current_tenant_id', true)::UUID, name, max_weight, allow_new_product
FROM (VALUES
('Área Techada', 10000.0, 'mixed'),
('Área Descubierta', 50000.0, 'mixed'),
('Bodega Cerrada', 5000.0, 'mixed'),
('Caseta Herramienta', 500.0, 'same'),
('Área Inflamables', 200.0, 'same')
) AS t(name, max_weight, allow_new_product)
ON CONFLICT DO NOTHING;
-- Tipos de paquete
INSERT INTO inventory.package_types (tenant_id, name, height, width, length, base_weight, max_weight, sequence)
SELECT current_setting('app.current_tenant_id', true)::UUID, name, height, width, length, base_weight, max_weight, seq
FROM (VALUES
('Tarima Block', 150.0, 100.0, 100.0, 5.0, 500.0, 10),
('Paquete Varilla', 600.0, 30.0, 30.0, 2.0, 1000.0, 20),
('Rollo Cable', 50.0, 50.0, 20.0, 0.5, 50.0, 30),
('Saco Cemento', 60.0, 40.0, 15.0, 0.2, 50.0, 40),
('Cubeta Pintura', 40.0, 30.0, 30.0, 0.3, 25.0, 50),
('Caja Herrajes', 40.0, 30.0, 20.0, 0.5, 30.0, 60)
) AS t(name, height, width, length, base_weight, max_weight, seq)
ON CONFLICT DO NOTHING;
-- Métodos de pago
INSERT INTO financial.payment_methods (tenant_id, name, code, payment_type)
SELECT current_setting('app.current_tenant_id', true)::UUID, name, code, payment_type::financial.payment_method_type
FROM (VALUES
('Anticipo de Obra', 'anticipo_obra', 'outbound'),
('Pago Estimación', 'pago_estimacion', 'outbound'),
('Pago Destajo', 'pago_destajo', 'outbound'),
('Pago Finiquito', 'pago_finiquito', 'outbound'),
('Retención 5%', 'retencion_5', 'outbound'),
('Cobro Cliente', 'cobro_cliente', 'inbound'),
('Anticipo Cliente', 'anticipo_cliente', 'inbound')
) AS t(name, code, payment_type)
ON CONFLICT (tenant_id, code) DO NOTHING;
4. Script de Rollback
4.1 rollback/20260104_fase8_rollback.sql
-- ============================================================================
-- ROLLBACK FASE-8 ERP-CONSTRUCCIÓN
-- PRECAUCIÓN: Elimina todas las tablas y datos de FASE-8
-- ============================================================================
BEGIN;
-- Eliminar seed data primero
DELETE FROM financial.payment_methods WHERE code LIKE '%_obra' OR code LIKE '%_estimacion' OR code LIKE '%_destajo' OR code LIKE '%_finiquito' OR code LIKE 'retencion_%' OR code LIKE '%_cliente';
DELETE FROM inventory.package_types WHERE name IN ('Tarima Block', 'Paquete Varilla', 'Rollo Cable', 'Saco Cemento', 'Cubeta Pintura', 'Caja Herrajes');
DELETE FROM inventory.storage_categories WHERE name IN ('Área Techada', 'Área Descubierta', 'Bodega Cerrada', 'Caseta Herramienta', 'Área Inflamables');
-- Eliminar tablas en orden inverso de dependencias
DROP TABLE IF EXISTS hr.payslip_lines CASCADE;
DROP TABLE IF EXISTS hr.payslips CASCADE;
DROP TABLE IF EXISTS hr.payslip_structures CASCADE;
DROP TABLE IF EXISTS hr.employee_resume_lines CASCADE;
DROP TABLE IF EXISTS hr.expenses CASCADE;
DROP TABLE IF EXISTS hr.expense_sheets CASCADE;
DROP TABLE IF EXISTS hr.employee_skills CASCADE;
DROP TABLE IF EXISTS hr.skill_levels CASCADE;
DROP TABLE IF EXISTS hr.skills CASCADE;
DROP TABLE IF EXISTS hr.skill_types CASCADE;
DROP TABLE IF EXISTS construction.ubicaciones_obra CASCADE;
DROP TABLE IF EXISTS hr.work_locations CASCADE;
DROP TABLE IF EXISTS construction.avance_programado CASCADE;
DROP TABLE IF EXISTS projects.ratings CASCADE;
DROP TABLE IF EXISTS construction.colaboradores_obra CASCADE;
DROP TABLE IF EXISTS purchase.product_supplierinfo CASCADE;
DROP TABLE IF EXISTS inventory.putaway_rules CASCADE;
DROP TABLE IF EXISTS inventory.packages CASCADE;
DROP TABLE IF EXISTS inventory.package_types CASCADE;
DROP TABLE IF EXISTS inventory.storage_categories CASCADE;
DROP TABLE IF EXISTS inventory.removal_strategies CASCADE;
DROP TABLE IF EXISTS financial.reconcile_model_lines CASCADE;
DROP TABLE IF EXISTS financial.reconcile_models CASCADE;
DROP TABLE IF EXISTS financial.payment_term_lines CASCADE;
DROP TABLE IF EXISTS financial.payment_methods CASCADE;
DROP TABLE IF EXISTS financial.incoterms CASCADE;
-- Eliminar funciones
DROP FUNCTION IF EXISTS construction.generate_avance_snapshot CASCADE;
DROP FUNCTION IF EXISTS purchase.action_create_stock_moves CASCADE;
-- Eliminar ENUMs
DROP TYPE IF EXISTS hr.payslip_status CASCADE;
DROP TYPE IF EXISTS hr.resume_line_type CASCADE;
DROP TYPE IF EXISTS hr.expense_status CASCADE;
DROP TYPE IF EXISTS financial.reconcile_model_type CASCADE;
DROP TYPE IF EXISTS financial.payment_method_type CASCADE;
-- Revertir campos adicionales (opcional - comentar si quiere mantenerlos)
-- ALTER TABLE hr.employees DROP COLUMN IF EXISTS work_location_id;
-- ALTER TABLE hr.employees DROP COLUMN IF EXISTS badge_id;
-- etc.
COMMIT;
\echo 'Rollback FASE-8 completado'
5. Checklist de Ejecución
5.1 Pre-Ejecución
- Backup de base de datos
- Verificar que auth.tenants existe
- Verificar que construction.fraccionamientos existe
- Verificar que hr.employees existe
- Confirmar tenant_id para seed data
5.2 Ejecución
- Ejecutar 08-financial-ext-schema-ddl.sql
- Ejecutar 09-projects-ext-schema-ddl.sql
- Ejecutar adiciones a 02-hr-schema-ddl.sql
- Ejecutar adiciones a 06-inventory-ext-schema-ddl.sql
- Ejecutar adiciones a 07-purchase-ext-schema-ddl.sql
- Ejecutar seeds/00-fase8-incoterms.sql
- Ejecutar seeds/01-fase8-removal-strategies.sql
- SET app.current_tenant_id = 'UUID';
- Ejecutar seeds/02-fase8-construccion-skills.sql
- Ejecutar seeds/03-fase8-construccion-catalogos.sql
5.3 Post-Ejecución
- Verificar creación de tablas
- Verificar creación de funciones
- Verificar seed data
- Actualizar documentación
6. Resumen de Cambios vs Plan Original
| Elemento | Plan Original | Plan Refinado |
|---|---|---|
| proyecto_id | Usado | Cambiado a fraccionamiento_id |
| construction.proyectos | Referenciado | Eliminado, usar fraccionamientos |
| construction.partidas | Trigger | Adaptado a avances_obra |
| projects.collaborators | Heredado | Nueva tabla colaboradores_obra |
| FKs a Core | Obligatorias | Opcionales |
| ENUMs | Sin protección | Con IF NOT EXISTS |
| RLS policies | Sin DROP | Con DROP IF EXISTS previo |
Estado: FASE 6 COMPLETADA Siguiente: FASE 7 - Ejecución del Plan Fecha: 2026-01-04