-- ============================================================================ -- 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 -- ============================================================================