erp-construccion-database-v2/schemas/04-estimates-schema-ddl.sql
rckrdmrd bf97e26cdf Migración desde erp-construccion/database - Estándar multi-repo v2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 08:11:21 -06:00

416 lines
19 KiB
PL/PgSQL

-- ============================================================================
-- ESTIMATES Schema DDL - Estimaciones, Anticipos y Retenciones
-- Modulos: MAI-008 (Estimaciones y Facturación)
-- Version: 1.0.0
-- Fecha: 2025-12-08
-- ============================================================================
-- PREREQUISITOS:
-- 1. ERP-Core instalado (auth.tenants, auth.users)
-- 2. Schema construction instalado (fraccionamientos, contratos, conceptos, lotes, departamentos)
-- ============================================================================
-- Verificar prerequisitos
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'auth') THEN
RAISE EXCEPTION 'Schema auth no existe. Ejecutar primero ERP-Core DDL';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'construction') THEN
RAISE EXCEPTION 'Schema construction no existe. Ejecutar primero construction DDL';
END IF;
END $$;
-- Crear schema
CREATE SCHEMA IF NOT EXISTS estimates;
-- ============================================================================
-- TYPES (ENUMs)
-- ============================================================================
DO $$ BEGIN
CREATE TYPE estimates.estimate_status AS ENUM (
'draft', 'submitted', 'reviewed', 'approved', 'invoiced', 'paid', 'rejected', 'cancelled'
);
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
CREATE TYPE estimates.advance_type AS ENUM (
'initial', 'progress', 'materials'
);
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
CREATE TYPE estimates.retention_type AS ENUM (
'guarantee', 'tax', 'penalty', 'other'
);
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
CREATE TYPE estimates.generator_status AS ENUM (
'draft', 'in_progress', 'completed', 'approved'
);
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- ============================================================================
-- TABLES - ESTIMACIONES
-- ============================================================================
-- Tabla: estimaciones (estimaciones de obra)
CREATE TABLE IF NOT EXISTS estimates.estimaciones (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
contrato_id UUID NOT NULL REFERENCES construction.contratos(id),
fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id),
estimate_number VARCHAR(30) NOT NULL,
period_start DATE NOT NULL,
period_end DATE NOT NULL,
sequence_number INTEGER NOT NULL,
status estimates.estimate_status NOT NULL DEFAULT 'draft',
subtotal DECIMAL(16,2) DEFAULT 0,
advance_amount DECIMAL(16,2) DEFAULT 0,
retention_amount DECIMAL(16,2) DEFAULT 0,
tax_amount DECIMAL(16,2) DEFAULT 0,
total_amount DECIMAL(16,2) DEFAULT 0,
submitted_at TIMESTAMPTZ,
submitted_by UUID REFERENCES auth.users(id),
reviewed_at TIMESTAMPTZ,
reviewed_by UUID REFERENCES auth.users(id),
approved_at TIMESTAMPTZ,
approved_by UUID REFERENCES auth.users(id),
invoice_id UUID,
invoiced_at TIMESTAMPTZ,
paid_at TIMESTAMPTZ,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ,
updated_by UUID REFERENCES auth.users(id),
deleted_at TIMESTAMPTZ,
deleted_by UUID REFERENCES auth.users(id),
CONSTRAINT uq_estimaciones_number_tenant UNIQUE (tenant_id, estimate_number),
CONSTRAINT uq_estimaciones_sequence_contrato UNIQUE (contrato_id, sequence_number),
CONSTRAINT chk_estimaciones_period CHECK (period_end >= period_start)
);
-- Tabla: estimacion_conceptos (líneas de estimación)
CREATE TABLE IF NOT EXISTS estimates.estimacion_conceptos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
estimacion_id UUID NOT NULL REFERENCES estimates.estimaciones(id) ON DELETE CASCADE,
concepto_id UUID NOT NULL REFERENCES construction.conceptos(id),
contrato_partida_id UUID REFERENCES construction.contrato_partidas(id),
quantity_contract DECIMAL(12,4) DEFAULT 0,
quantity_previous DECIMAL(12,4) DEFAULT 0,
quantity_current DECIMAL(12,4) DEFAULT 0,
quantity_accumulated DECIMAL(12,4) GENERATED ALWAYS AS (quantity_previous + quantity_current) STORED,
unit_price DECIMAL(12,4) NOT NULL DEFAULT 0,
amount_current DECIMAL(14,2) GENERATED ALWAYS AS (quantity_current * unit_price) STORED,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ,
updated_by UUID REFERENCES auth.users(id),
deleted_at TIMESTAMPTZ,
deleted_by UUID REFERENCES auth.users(id),
CONSTRAINT uq_est_conceptos_estimacion_concepto UNIQUE (estimacion_id, concepto_id)
);
-- Tabla: generadores (soporte de cantidades)
CREATE TABLE IF NOT EXISTS estimates.generadores (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
estimacion_concepto_id UUID NOT NULL REFERENCES estimates.estimacion_conceptos(id) ON DELETE CASCADE,
generator_number VARCHAR(30) NOT NULL,
description TEXT,
status estimates.generator_status NOT NULL DEFAULT 'draft',
lote_id UUID REFERENCES construction.lotes(id),
departamento_id UUID REFERENCES construction.departamentos(id),
location_description VARCHAR(255),
quantity DECIMAL(12,4) NOT NULL DEFAULT 0,
formula TEXT,
photo_url VARCHAR(500),
sketch_url VARCHAR(500),
captured_by UUID NOT NULL REFERENCES auth.users(id),
captured_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
approved_by UUID REFERENCES auth.users(id),
approved_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ,
updated_by UUID REFERENCES auth.users(id),
deleted_at TIMESTAMPTZ,
deleted_by UUID REFERENCES auth.users(id)
);
-- ============================================================================
-- TABLES - ANTICIPOS
-- ============================================================================
-- Tabla: anticipos (anticipos otorgados)
CREATE TABLE IF NOT EXISTS estimates.anticipos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
contrato_id UUID NOT NULL REFERENCES construction.contratos(id),
advance_type estimates.advance_type NOT NULL DEFAULT 'initial',
advance_number VARCHAR(30) NOT NULL,
advance_date DATE NOT NULL,
gross_amount DECIMAL(16,2) NOT NULL,
tax_amount DECIMAL(16,2) DEFAULT 0,
net_amount DECIMAL(16,2) NOT NULL,
amortization_percentage DECIMAL(5,2) DEFAULT 0,
amortized_amount DECIMAL(16,2) DEFAULT 0,
pending_amount DECIMAL(16,2) GENERATED ALWAYS AS (net_amount - amortized_amount) STORED,
is_fully_amortized BOOLEAN DEFAULT FALSE,
approved_at TIMESTAMPTZ,
approved_by UUID REFERENCES auth.users(id),
paid_at TIMESTAMPTZ,
payment_reference VARCHAR(100),
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ,
updated_by UUID REFERENCES auth.users(id),
deleted_at TIMESTAMPTZ,
deleted_by UUID REFERENCES auth.users(id),
CONSTRAINT uq_anticipos_number_tenant UNIQUE (tenant_id, advance_number)
);
-- Tabla: amortizaciones (amortizaciones de anticipos)
CREATE TABLE IF NOT EXISTS estimates.amortizaciones (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
anticipo_id UUID NOT NULL REFERENCES estimates.anticipos(id),
estimacion_id UUID NOT NULL REFERENCES estimates.estimaciones(id),
amount DECIMAL(16,2) NOT NULL,
amortization_date DATE NOT NULL,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ,
updated_by UUID REFERENCES auth.users(id),
deleted_at TIMESTAMPTZ,
deleted_by UUID REFERENCES auth.users(id),
CONSTRAINT uq_amortizaciones_anticipo_estimacion UNIQUE (anticipo_id, estimacion_id)
);
-- ============================================================================
-- TABLES - RETENCIONES
-- ============================================================================
-- Tabla: retenciones (retenciones aplicadas)
CREATE TABLE IF NOT EXISTS estimates.retenciones (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
estimacion_id UUID NOT NULL REFERENCES estimates.estimaciones(id),
retention_type estimates.retention_type NOT NULL,
description VARCHAR(255) NOT NULL,
percentage DECIMAL(5,2),
amount DECIMAL(16,2) NOT NULL,
release_date DATE,
released_at TIMESTAMPTZ,
released_amount DECIMAL(16,2),
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ,
updated_by UUID REFERENCES auth.users(id),
deleted_at TIMESTAMPTZ,
deleted_by UUID REFERENCES auth.users(id)
);
-- Tabla: fondo_garantia (acumulado de fondo de garantía)
CREATE TABLE IF NOT EXISTS estimates.fondo_garantia (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
contrato_id UUID NOT NULL REFERENCES construction.contratos(id),
accumulated_amount DECIMAL(16,2) DEFAULT 0,
released_amount DECIMAL(16,2) DEFAULT 0,
pending_amount DECIMAL(16,2) GENERATED ALWAYS AS (accumulated_amount - released_amount) STORED,
release_date DATE,
released_at TIMESTAMPTZ,
released_by UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ,
updated_by UUID REFERENCES auth.users(id),
deleted_at TIMESTAMPTZ,
deleted_by UUID REFERENCES auth.users(id),
CONSTRAINT uq_fondo_garantia_contrato UNIQUE (contrato_id)
);
-- ============================================================================
-- TABLES - WORKFLOW
-- ============================================================================
-- Tabla: estimacion_workflow (historial de workflow)
CREATE TABLE IF NOT EXISTS estimates.estimacion_workflow (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
estimacion_id UUID NOT NULL REFERENCES estimates.estimaciones(id) ON DELETE CASCADE,
from_status estimates.estimate_status,
to_status estimates.estimate_status NOT NULL,
action VARCHAR(50) NOT NULL,
comments TEXT,
performed_by UUID NOT NULL REFERENCES auth.users(id),
performed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES auth.users(id)
);
-- ============================================================================
-- INDICES
-- ============================================================================
CREATE INDEX IF NOT EXISTS idx_estimaciones_tenant_id ON estimates.estimaciones(tenant_id);
CREATE INDEX IF NOT EXISTS idx_estimaciones_contrato_id ON estimates.estimaciones(contrato_id);
CREATE INDEX IF NOT EXISTS idx_estimaciones_fraccionamiento_id ON estimates.estimaciones(fraccionamiento_id);
CREATE INDEX IF NOT EXISTS idx_estimaciones_status ON estimates.estimaciones(status);
CREATE INDEX IF NOT EXISTS idx_estimaciones_period ON estimates.estimaciones(period_start, period_end);
CREATE INDEX IF NOT EXISTS idx_est_conceptos_tenant_id ON estimates.estimacion_conceptos(tenant_id);
CREATE INDEX IF NOT EXISTS idx_est_conceptos_estimacion_id ON estimates.estimacion_conceptos(estimacion_id);
CREATE INDEX IF NOT EXISTS idx_est_conceptos_concepto_id ON estimates.estimacion_conceptos(concepto_id);
CREATE INDEX IF NOT EXISTS idx_generadores_tenant_id ON estimates.generadores(tenant_id);
CREATE INDEX IF NOT EXISTS idx_generadores_est_concepto_id ON estimates.generadores(estimacion_concepto_id);
CREATE INDEX IF NOT EXISTS idx_generadores_status ON estimates.generadores(status);
CREATE INDEX IF NOT EXISTS idx_anticipos_tenant_id ON estimates.anticipos(tenant_id);
CREATE INDEX IF NOT EXISTS idx_anticipos_contrato_id ON estimates.anticipos(contrato_id);
CREATE INDEX IF NOT EXISTS idx_anticipos_type ON estimates.anticipos(advance_type);
CREATE INDEX IF NOT EXISTS idx_amortizaciones_tenant_id ON estimates.amortizaciones(tenant_id);
CREATE INDEX IF NOT EXISTS idx_amortizaciones_anticipo_id ON estimates.amortizaciones(anticipo_id);
CREATE INDEX IF NOT EXISTS idx_amortizaciones_estimacion_id ON estimates.amortizaciones(estimacion_id);
CREATE INDEX IF NOT EXISTS idx_retenciones_tenant_id ON estimates.retenciones(tenant_id);
CREATE INDEX IF NOT EXISTS idx_retenciones_estimacion_id ON estimates.retenciones(estimacion_id);
CREATE INDEX IF NOT EXISTS idx_retenciones_type ON estimates.retenciones(retention_type);
CREATE INDEX IF NOT EXISTS idx_fondo_garantia_tenant_id ON estimates.fondo_garantia(tenant_id);
CREATE INDEX IF NOT EXISTS idx_fondo_garantia_contrato_id ON estimates.fondo_garantia(contrato_id);
CREATE INDEX IF NOT EXISTS idx_est_workflow_tenant_id ON estimates.estimacion_workflow(tenant_id);
CREATE INDEX IF NOT EXISTS idx_est_workflow_estimacion_id ON estimates.estimacion_workflow(estimacion_id);
-- ============================================================================
-- ROW LEVEL SECURITY (RLS)
-- ============================================================================
ALTER TABLE estimates.estimaciones ENABLE ROW LEVEL SECURITY;
ALTER TABLE estimates.estimacion_conceptos ENABLE ROW LEVEL SECURITY;
ALTER TABLE estimates.generadores ENABLE ROW LEVEL SECURITY;
ALTER TABLE estimates.anticipos ENABLE ROW LEVEL SECURITY;
ALTER TABLE estimates.amortizaciones ENABLE ROW LEVEL SECURITY;
ALTER TABLE estimates.retenciones ENABLE ROW LEVEL SECURITY;
ALTER TABLE estimates.fondo_garantia ENABLE ROW LEVEL SECURITY;
ALTER TABLE estimates.estimacion_workflow ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
DROP POLICY IF EXISTS tenant_isolation_estimaciones ON estimates.estimaciones;
CREATE POLICY tenant_isolation_estimaciones ON estimates.estimaciones
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_est_conceptos ON estimates.estimacion_conceptos;
CREATE POLICY tenant_isolation_est_conceptos ON estimates.estimacion_conceptos
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_generadores ON estimates.generadores;
CREATE POLICY tenant_isolation_generadores ON estimates.generadores
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_anticipos ON estimates.anticipos;
CREATE POLICY tenant_isolation_anticipos ON estimates.anticipos
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_amortizaciones ON estimates.amortizaciones;
CREATE POLICY tenant_isolation_amortizaciones ON estimates.amortizaciones
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_retenciones ON estimates.retenciones;
CREATE POLICY tenant_isolation_retenciones ON estimates.retenciones
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_fondo_garantia ON estimates.fondo_garantia;
CREATE POLICY tenant_isolation_fondo_garantia ON estimates.fondo_garantia
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_est_workflow ON estimates.estimacion_workflow;
CREATE POLICY tenant_isolation_est_workflow ON estimates.estimacion_workflow
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
EXCEPTION WHEN undefined_object THEN NULL; END $$;
-- ============================================================================
-- FUNCIONES
-- ============================================================================
-- Función: calcular totales de estimación
CREATE OR REPLACE FUNCTION estimates.calculate_estimate_totals(p_estimacion_id UUID)
RETURNS VOID AS $$
DECLARE
v_subtotal DECIMAL(16,2);
v_advance DECIMAL(16,2);
v_retention DECIMAL(16,2);
v_tax_rate DECIMAL(5,2) := 0.16;
v_tax DECIMAL(16,2);
v_total DECIMAL(16,2);
BEGIN
SELECT COALESCE(SUM(amount_current), 0) INTO v_subtotal
FROM estimates.estimacion_conceptos
WHERE estimacion_id = p_estimacion_id AND deleted_at IS NULL;
SELECT COALESCE(SUM(amount), 0) INTO v_advance
FROM estimates.amortizaciones
WHERE estimacion_id = p_estimacion_id AND deleted_at IS NULL;
SELECT COALESCE(SUM(amount), 0) INTO v_retention
FROM estimates.retenciones
WHERE estimacion_id = p_estimacion_id AND deleted_at IS NULL;
v_tax := v_subtotal * v_tax_rate;
v_total := v_subtotal + v_tax - v_advance - v_retention;
UPDATE estimates.estimaciones
SET subtotal = v_subtotal,
advance_amount = v_advance,
retention_amount = v_retention,
tax_amount = v_tax,
total_amount = v_total,
updated_at = NOW()
WHERE id = p_estimacion_id;
END;
$$ LANGUAGE plpgsql;
-- ============================================================================
-- COMENTARIOS
-- ============================================================================
COMMENT ON SCHEMA estimates IS 'Schema de estimaciones, anticipos y retenciones de obra';
COMMENT ON TABLE estimates.estimaciones IS 'Estimaciones de obra periódicas';
COMMENT ON TABLE estimates.estimacion_conceptos IS 'Líneas de concepto por estimación';
COMMENT ON TABLE estimates.generadores IS 'Generadores de cantidades para estimaciones';
COMMENT ON TABLE estimates.anticipos IS 'Anticipos otorgados a subcontratistas';
COMMENT ON TABLE estimates.amortizaciones IS 'Amortizaciones de anticipos por estimación';
COMMENT ON TABLE estimates.retenciones IS 'Retenciones aplicadas a estimaciones';
COMMENT ON TABLE estimates.fondo_garantia IS 'Fondo de garantía acumulado por contrato';
COMMENT ON TABLE estimates.estimacion_workflow IS 'Historial de workflow de estimaciones';
-- ============================================================================
-- FIN DEL SCHEMA ESTIMATES
-- Total tablas: 8
-- ============================================================================