🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
559 lines
19 KiB
PL/PgSQL
559 lines
19 KiB
PL/PgSQL
-- =====================================================
|
|
-- SCHEMA: analytics
|
|
-- PROPÓSITO: Contabilidad analítica, tracking de costos/ingresos
|
|
-- MÓDULOS: MGN-008 (Contabilidad Analítica)
|
|
-- FECHA: 2025-11-24
|
|
-- =====================================================
|
|
|
|
-- Crear schema
|
|
CREATE SCHEMA IF NOT EXISTS analytics;
|
|
|
|
-- =====================================================
|
|
-- TYPES (ENUMs)
|
|
-- =====================================================
|
|
|
|
CREATE TYPE analytics.account_type AS ENUM (
|
|
'project',
|
|
'department',
|
|
'cost_center',
|
|
'customer',
|
|
'product',
|
|
'other'
|
|
);
|
|
|
|
CREATE TYPE analytics.line_type AS ENUM (
|
|
'expense',
|
|
'income',
|
|
'timesheet'
|
|
);
|
|
|
|
CREATE TYPE analytics.account_status AS ENUM (
|
|
'active',
|
|
'inactive',
|
|
'closed'
|
|
);
|
|
|
|
-- =====================================================
|
|
-- TABLES
|
|
-- =====================================================
|
|
|
|
-- Tabla: analytic_plans (Planes analíticos - multi-dimensional)
|
|
-- COR-015: Soporte para jerarquia de planes
|
|
CREATE TABLE analytics.analytic_plans (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
company_id UUID REFERENCES auth.companies(id),
|
|
|
|
name VARCHAR(255) NOT NULL,
|
|
code VARCHAR(50),
|
|
description TEXT,
|
|
|
|
-- COR-015: Jerarquia de planes
|
|
parent_id UUID REFERENCES analytics.analytic_plans(id),
|
|
full_path TEXT, -- Generado automaticamente
|
|
sequence INTEGER DEFAULT 10,
|
|
|
|
-- COR-015: Configuracion de aplicabilidad
|
|
applicability VARCHAR(50) DEFAULT 'optional', -- mandatory, optional, unavailable
|
|
default_applicability VARCHAR(50) DEFAULT 'optional',
|
|
|
|
-- COR-015: Color para UI
|
|
color VARCHAR(20),
|
|
|
|
-- Control
|
|
active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_by UUID REFERENCES auth.users(id),
|
|
updated_at TIMESTAMP,
|
|
updated_by UUID REFERENCES auth.users(id),
|
|
|
|
CONSTRAINT uq_analytic_plans_name_tenant UNIQUE (tenant_id, name),
|
|
CONSTRAINT uq_analytic_plans_code_tenant UNIQUE (tenant_id, code),
|
|
CONSTRAINT chk_analytic_plans_no_self_parent CHECK (id != parent_id),
|
|
CONSTRAINT chk_analytic_plans_applicability CHECK (applicability IN ('mandatory', 'optional', 'unavailable'))
|
|
);
|
|
|
|
-- Tabla: analytic_accounts (Cuentas analíticas)
|
|
CREATE TABLE analytics.analytic_accounts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
company_id UUID NOT NULL REFERENCES auth.companies(id) ON DELETE CASCADE,
|
|
|
|
plan_id UUID REFERENCES analytics.analytic_plans(id),
|
|
|
|
-- Identificación
|
|
name VARCHAR(255) NOT NULL,
|
|
code VARCHAR(50),
|
|
account_type analytics.account_type NOT NULL DEFAULT 'other',
|
|
|
|
-- Jerarquía
|
|
parent_id UUID REFERENCES analytics.analytic_accounts(id),
|
|
full_path TEXT, -- Generado automáticamente
|
|
|
|
-- Referencias
|
|
partner_id UUID REFERENCES core.partners(id), -- Cliente/proveedor asociado
|
|
|
|
-- Presupuesto
|
|
budget DECIMAL(15, 2) DEFAULT 0,
|
|
|
|
-- Estado
|
|
status analytics.account_status NOT NULL DEFAULT 'active',
|
|
|
|
-- Fechas
|
|
date_start DATE,
|
|
date_end DATE,
|
|
|
|
-- Notas
|
|
description TEXT,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_by UUID REFERENCES auth.users(id),
|
|
updated_at TIMESTAMP,
|
|
updated_by UUID REFERENCES auth.users(id),
|
|
deleted_at TIMESTAMP,
|
|
deleted_by UUID REFERENCES auth.users(id),
|
|
|
|
CONSTRAINT uq_analytic_accounts_code_company UNIQUE (company_id, code),
|
|
CONSTRAINT chk_analytic_accounts_no_self_parent CHECK (id != parent_id),
|
|
CONSTRAINT chk_analytic_accounts_budget CHECK (budget >= 0),
|
|
CONSTRAINT chk_analytic_accounts_dates CHECK (date_end IS NULL OR date_end >= date_start)
|
|
);
|
|
|
|
-- Tabla: analytic_tags (Etiquetas analíticas - clasificación cross-cutting)
|
|
CREATE TABLE analytics.analytic_tags (
|
|
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,
|
|
color VARCHAR(20), -- Color hex
|
|
description TEXT,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_by UUID REFERENCES auth.users(id),
|
|
|
|
CONSTRAINT uq_analytic_tags_name_tenant UNIQUE (tenant_id, name)
|
|
);
|
|
|
|
-- Tabla: cost_centers (Centros de costo)
|
|
CREATE TABLE analytics.cost_centers (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
company_id UUID NOT NULL REFERENCES auth.companies(id) ON DELETE CASCADE,
|
|
|
|
name VARCHAR(255) NOT NULL,
|
|
code VARCHAR(50),
|
|
analytic_account_id UUID NOT NULL REFERENCES analytics.analytic_accounts(id),
|
|
|
|
-- Responsable
|
|
manager_id UUID REFERENCES auth.users(id),
|
|
|
|
-- Presupuesto
|
|
budget_monthly DECIMAL(15, 2) DEFAULT 0,
|
|
budget_annual DECIMAL(15, 2) DEFAULT 0,
|
|
|
|
-- Control
|
|
active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_by UUID REFERENCES auth.users(id),
|
|
updated_at TIMESTAMP,
|
|
updated_by UUID REFERENCES auth.users(id),
|
|
|
|
CONSTRAINT uq_cost_centers_code_company UNIQUE (company_id, code)
|
|
);
|
|
|
|
-- Tabla: analytic_lines (Líneas analíticas - registro de costos/ingresos)
|
|
CREATE TABLE analytics.analytic_lines (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
company_id UUID NOT NULL REFERENCES auth.companies(id) ON DELETE CASCADE,
|
|
|
|
analytic_account_id UUID NOT NULL REFERENCES analytics.analytic_accounts(id),
|
|
|
|
-- Fecha
|
|
date DATE NOT NULL,
|
|
|
|
-- Montos
|
|
amount DECIMAL(15, 2) NOT NULL, -- Negativo=costo, Positivo=ingreso
|
|
unit_amount DECIMAL(12, 4) DEFAULT 0, -- Horas para timesheet, cantidades para productos
|
|
|
|
-- Tipo
|
|
line_type analytics.line_type NOT NULL,
|
|
|
|
-- Referencias
|
|
product_id UUID REFERENCES inventory.products(id),
|
|
employee_id UUID, -- FK a hr.employees (se crea después)
|
|
partner_id UUID REFERENCES core.partners(id),
|
|
|
|
-- Descripción
|
|
name VARCHAR(255),
|
|
description TEXT,
|
|
|
|
-- Documento origen (polimórfico)
|
|
source_model VARCHAR(100), -- 'Invoice', 'PurchaseOrder', 'SaleOrder', 'Timesheet', etc.
|
|
source_id UUID,
|
|
source_document VARCHAR(255), -- "invoice/123", "purchase_order/456"
|
|
|
|
-- Moneda
|
|
currency_id UUID REFERENCES core.currencies(id),
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_by UUID REFERENCES auth.users(id),
|
|
|
|
CONSTRAINT chk_analytic_lines_unit_amount CHECK (unit_amount >= 0)
|
|
);
|
|
|
|
-- Tabla: analytic_line_tags (Many-to-many: líneas analíticas - tags)
|
|
CREATE TABLE analytics.analytic_line_tags (
|
|
analytic_line_id UUID NOT NULL REFERENCES analytics.analytic_lines(id) ON DELETE CASCADE,
|
|
analytic_tag_id UUID NOT NULL REFERENCES analytics.analytic_tags(id) ON DELETE CASCADE,
|
|
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
PRIMARY KEY (analytic_line_id, analytic_tag_id)
|
|
);
|
|
|
|
-- Tabla: analytic_distributions (Distribución analítica multi-cuenta)
|
|
CREATE TABLE analytics.analytic_distributions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
-- Línea origen (polimórfico)
|
|
source_model VARCHAR(100) NOT NULL, -- 'PurchaseOrderLine', 'InvoiceLine', etc.
|
|
source_id UUID NOT NULL,
|
|
|
|
-- Cuenta analítica destino
|
|
analytic_account_id UUID NOT NULL REFERENCES analytics.analytic_accounts(id),
|
|
|
|
-- Distribución
|
|
percentage DECIMAL(5, 2) NOT NULL, -- 0-100
|
|
amount DECIMAL(15, 2), -- Calculado automáticamente
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT chk_analytic_distributions_percentage CHECK (percentage >= 0 AND percentage <= 100)
|
|
);
|
|
|
|
-- =====================================================
|
|
-- INDICES
|
|
-- =====================================================
|
|
|
|
-- Analytic Plans
|
|
CREATE INDEX idx_analytic_plans_tenant_id ON analytics.analytic_plans(tenant_id);
|
|
CREATE INDEX idx_analytic_plans_active ON analytics.analytic_plans(active) WHERE active = TRUE;
|
|
CREATE INDEX idx_analytic_plans_parent_id ON analytics.analytic_plans(parent_id); -- COR-015
|
|
CREATE INDEX idx_analytic_plans_code ON analytics.analytic_plans(code); -- COR-015
|
|
|
|
-- Analytic Accounts
|
|
CREATE INDEX idx_analytic_accounts_tenant_id ON analytics.analytic_accounts(tenant_id);
|
|
CREATE INDEX idx_analytic_accounts_company_id ON analytics.analytic_accounts(company_id);
|
|
CREATE INDEX idx_analytic_accounts_plan_id ON analytics.analytic_accounts(plan_id);
|
|
CREATE INDEX idx_analytic_accounts_parent_id ON analytics.analytic_accounts(parent_id);
|
|
CREATE INDEX idx_analytic_accounts_partner_id ON analytics.analytic_accounts(partner_id);
|
|
CREATE INDEX idx_analytic_accounts_code ON analytics.analytic_accounts(code);
|
|
CREATE INDEX idx_analytic_accounts_type ON analytics.analytic_accounts(account_type);
|
|
CREATE INDEX idx_analytic_accounts_status ON analytics.analytic_accounts(status);
|
|
|
|
-- Analytic Tags
|
|
CREATE INDEX idx_analytic_tags_tenant_id ON analytics.analytic_tags(tenant_id);
|
|
CREATE INDEX idx_analytic_tags_name ON analytics.analytic_tags(name);
|
|
|
|
-- Cost Centers
|
|
CREATE INDEX idx_cost_centers_tenant_id ON analytics.cost_centers(tenant_id);
|
|
CREATE INDEX idx_cost_centers_company_id ON analytics.cost_centers(company_id);
|
|
CREATE INDEX idx_cost_centers_analytic_account_id ON analytics.cost_centers(analytic_account_id);
|
|
CREATE INDEX idx_cost_centers_manager_id ON analytics.cost_centers(manager_id);
|
|
CREATE INDEX idx_cost_centers_active ON analytics.cost_centers(active) WHERE active = TRUE;
|
|
|
|
-- Analytic Lines
|
|
CREATE INDEX idx_analytic_lines_tenant_id ON analytics.analytic_lines(tenant_id);
|
|
CREATE INDEX idx_analytic_lines_company_id ON analytics.analytic_lines(company_id);
|
|
CREATE INDEX idx_analytic_lines_analytic_account_id ON analytics.analytic_lines(analytic_account_id);
|
|
CREATE INDEX idx_analytic_lines_date ON analytics.analytic_lines(date);
|
|
CREATE INDEX idx_analytic_lines_line_type ON analytics.analytic_lines(line_type);
|
|
CREATE INDEX idx_analytic_lines_product_id ON analytics.analytic_lines(product_id);
|
|
CREATE INDEX idx_analytic_lines_employee_id ON analytics.analytic_lines(employee_id);
|
|
CREATE INDEX idx_analytic_lines_source ON analytics.analytic_lines(source_model, source_id);
|
|
|
|
-- Analytic Line Tags
|
|
CREATE INDEX idx_analytic_line_tags_line_id ON analytics.analytic_line_tags(analytic_line_id);
|
|
CREATE INDEX idx_analytic_line_tags_tag_id ON analytics.analytic_line_tags(analytic_tag_id);
|
|
|
|
-- Analytic Distributions
|
|
CREATE INDEX idx_analytic_distributions_source ON analytics.analytic_distributions(source_model, source_id);
|
|
CREATE INDEX idx_analytic_distributions_analytic_account_id ON analytics.analytic_distributions(analytic_account_id);
|
|
|
|
-- =====================================================
|
|
-- FUNCTIONS
|
|
-- =====================================================
|
|
|
|
-- Función: update_analytic_account_path
|
|
CREATE OR REPLACE FUNCTION analytics.update_analytic_account_path()
|
|
RETURNS TRIGGER AS $$
|
|
DECLARE
|
|
v_parent_path TEXT;
|
|
BEGIN
|
|
IF NEW.parent_id IS NULL THEN
|
|
NEW.full_path := NEW.name;
|
|
ELSE
|
|
SELECT full_path INTO v_parent_path
|
|
FROM analytics.analytic_accounts
|
|
WHERE id = NEW.parent_id;
|
|
|
|
NEW.full_path := v_parent_path || ' / ' || NEW.name;
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION analytics.update_analytic_account_path IS 'Actualiza el path completo de la cuenta analítica';
|
|
|
|
-- COR-015: Función para actualizar path de planes
|
|
CREATE OR REPLACE FUNCTION analytics.update_analytic_plan_path()
|
|
RETURNS TRIGGER AS $$
|
|
DECLARE
|
|
v_parent_path TEXT;
|
|
BEGIN
|
|
IF NEW.parent_id IS NULL THEN
|
|
NEW.full_path := NEW.name;
|
|
ELSE
|
|
SELECT full_path INTO v_parent_path
|
|
FROM analytics.analytic_plans
|
|
WHERE id = NEW.parent_id;
|
|
|
|
NEW.full_path := v_parent_path || ' / ' || NEW.name;
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION analytics.update_analytic_plan_path IS
|
|
'COR-015: Actualiza el path completo del plan analitico';
|
|
|
|
-- Función: get_analytic_balance
|
|
CREATE OR REPLACE FUNCTION analytics.get_analytic_balance(
|
|
p_analytic_account_id UUID,
|
|
p_date_from DATE DEFAULT NULL,
|
|
p_date_to DATE DEFAULT NULL
|
|
)
|
|
RETURNS TABLE(
|
|
total_income DECIMAL,
|
|
total_expense DECIMAL,
|
|
balance DECIMAL
|
|
) AS $$
|
|
BEGIN
|
|
RETURN QUERY
|
|
SELECT
|
|
COALESCE(SUM(CASE WHEN line_type = 'income' THEN amount ELSE 0 END), 0) AS total_income,
|
|
COALESCE(SUM(CASE WHEN line_type = 'expense' THEN ABS(amount) ELSE 0 END), 0) AS total_expense,
|
|
COALESCE(SUM(amount), 0) AS balance
|
|
FROM analytics.analytic_lines
|
|
WHERE analytic_account_id = p_analytic_account_id
|
|
AND (p_date_from IS NULL OR date >= p_date_from)
|
|
AND (p_date_to IS NULL OR date <= p_date_to);
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
COMMENT ON FUNCTION analytics.get_analytic_balance IS 'Obtiene el balance de una cuenta analítica en un período';
|
|
|
|
-- Función: validate_distribution_100_percent
|
|
CREATE OR REPLACE FUNCTION analytics.validate_distribution_100_percent()
|
|
RETURNS TRIGGER AS $$
|
|
DECLARE
|
|
v_total_percentage DECIMAL;
|
|
BEGIN
|
|
SELECT COALESCE(SUM(percentage), 0)
|
|
INTO v_total_percentage
|
|
FROM analytics.analytic_distributions
|
|
WHERE source_model = NEW.source_model
|
|
AND source_id = NEW.source_id;
|
|
|
|
IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
|
|
v_total_percentage := v_total_percentage + NEW.percentage;
|
|
END IF;
|
|
|
|
IF v_total_percentage > 100 THEN
|
|
RAISE EXCEPTION 'Total distribution percentage cannot exceed 100%% (currently: %%)', v_total_percentage;
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION analytics.validate_distribution_100_percent IS 'Valida que la distribución analítica no exceda el 100%';
|
|
|
|
-- Función: create_analytic_line_from_invoice
|
|
CREATE OR REPLACE FUNCTION analytics.create_analytic_line_from_invoice(p_invoice_line_id UUID)
|
|
RETURNS UUID AS $$
|
|
DECLARE
|
|
v_line RECORD;
|
|
v_invoice RECORD;
|
|
v_analytic_line_id UUID;
|
|
v_amount DECIMAL;
|
|
BEGIN
|
|
-- Obtener datos de la línea de factura
|
|
SELECT il.*, i.invoice_type, i.company_id, i.tenant_id, i.partner_id, i.invoice_date
|
|
INTO v_line
|
|
FROM financial.invoice_lines il
|
|
JOIN financial.invoices i ON il.invoice_id = i.id
|
|
WHERE il.id = p_invoice_line_id;
|
|
|
|
IF NOT FOUND OR v_line.analytic_account_id IS NULL THEN
|
|
RETURN NULL; -- Sin cuenta analítica, no crear línea
|
|
END IF;
|
|
|
|
-- Determinar monto (negativo para compras, positivo para ventas)
|
|
IF v_line.invoice_type = 'supplier' THEN
|
|
v_amount := -ABS(v_line.amount_total);
|
|
ELSE
|
|
v_amount := v_line.amount_total;
|
|
END IF;
|
|
|
|
-- Crear línea analítica
|
|
INSERT INTO analytics.analytic_lines (
|
|
tenant_id,
|
|
company_id,
|
|
analytic_account_id,
|
|
date,
|
|
amount,
|
|
unit_amount,
|
|
line_type,
|
|
product_id,
|
|
partner_id,
|
|
name,
|
|
description,
|
|
source_model,
|
|
source_id,
|
|
source_document
|
|
) VALUES (
|
|
v_line.tenant_id,
|
|
v_line.company_id,
|
|
v_line.analytic_account_id,
|
|
v_line.invoice_date,
|
|
v_amount,
|
|
v_line.quantity,
|
|
CASE WHEN v_line.invoice_type = 'supplier' THEN 'expense'::analytics.line_type ELSE 'income'::analytics.line_type END,
|
|
v_line.product_id,
|
|
v_line.partner_id,
|
|
v_line.description,
|
|
v_line.description,
|
|
'InvoiceLine',
|
|
v_line.id,
|
|
'invoice_line/' || v_line.id::TEXT
|
|
) RETURNING id INTO v_analytic_line_id;
|
|
|
|
RETURN v_analytic_line_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION analytics.create_analytic_line_from_invoice IS 'Crea una línea analítica a partir de una línea de factura';
|
|
|
|
-- =====================================================
|
|
-- TRIGGERS
|
|
-- =====================================================
|
|
|
|
CREATE TRIGGER trg_analytic_plans_updated_at
|
|
BEFORE UPDATE ON analytics.analytic_plans
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_updated_at_column();
|
|
|
|
CREATE TRIGGER trg_analytic_accounts_updated_at
|
|
BEFORE UPDATE ON analytics.analytic_accounts
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_updated_at_column();
|
|
|
|
CREATE TRIGGER trg_cost_centers_updated_at
|
|
BEFORE UPDATE ON analytics.cost_centers
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_updated_at_column();
|
|
|
|
-- Trigger: Actualizar full_path de cuenta analítica
|
|
CREATE TRIGGER trg_analytic_accounts_update_path
|
|
BEFORE INSERT OR UPDATE OF name, parent_id ON analytics.analytic_accounts
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION analytics.update_analytic_account_path();
|
|
|
|
-- COR-015: Trigger para actualizar full_path de plan analítico
|
|
CREATE TRIGGER trg_analytic_plans_update_path
|
|
BEFORE INSERT OR UPDATE OF name, parent_id ON analytics.analytic_plans
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION analytics.update_analytic_plan_path();
|
|
|
|
-- Trigger: Validar distribución 100%
|
|
CREATE TRIGGER trg_analytic_distributions_validate_100
|
|
BEFORE INSERT OR UPDATE ON analytics.analytic_distributions
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION analytics.validate_distribution_100_percent();
|
|
|
|
-- =====================================================
|
|
-- ROW LEVEL SECURITY (RLS)
|
|
-- =====================================================
|
|
|
|
ALTER TABLE analytics.analytic_plans ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE analytics.analytic_accounts ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE analytics.analytic_tags ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE analytics.cost_centers ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE analytics.analytic_lines ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY tenant_isolation_analytic_plans ON analytics.analytic_plans
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
CREATE POLICY tenant_isolation_analytic_accounts ON analytics.analytic_accounts
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
CREATE POLICY tenant_isolation_analytic_tags ON analytics.analytic_tags
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
CREATE POLICY tenant_isolation_cost_centers ON analytics.cost_centers
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
CREATE POLICY tenant_isolation_analytic_lines ON analytics.analytic_lines
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
-- =====================================================
|
|
-- COMENTARIOS
|
|
-- =====================================================
|
|
|
|
COMMENT ON SCHEMA analytics IS 'Schema de contabilidad analítica y tracking de costos/ingresos';
|
|
COMMENT ON TABLE analytics.analytic_plans IS 'Planes analíticos para análisis multi-dimensional';
|
|
COMMENT ON TABLE analytics.analytic_accounts IS 'Cuentas analíticas (proyectos, departamentos, centros de costo)';
|
|
COMMENT ON TABLE analytics.analytic_tags IS 'Etiquetas analíticas para clasificación cross-cutting';
|
|
COMMENT ON TABLE analytics.cost_centers IS 'Centros de costo con presupuestos';
|
|
COMMENT ON TABLE analytics.analytic_lines IS 'Líneas analíticas de costos e ingresos';
|
|
COMMENT ON TABLE analytics.analytic_line_tags IS 'Relación many-to-many entre líneas y tags';
|
|
COMMENT ON TABLE analytics.analytic_distributions IS 'Distribución de montos a múltiples cuentas analíticas';
|
|
|
|
-- =====================================================
|
|
-- VISTAS ÚTILES
|
|
-- =====================================================
|
|
|
|
-- Vista: balance analítico por cuenta
|
|
CREATE OR REPLACE VIEW analytics.analytic_balance_view AS
|
|
SELECT
|
|
aa.id AS analytic_account_id,
|
|
aa.code,
|
|
aa.name,
|
|
aa.budget,
|
|
COALESCE(SUM(CASE WHEN al.line_type = 'income' THEN al.amount ELSE 0 END), 0) AS total_income,
|
|
COALESCE(SUM(CASE WHEN al.line_type = 'expense' THEN ABS(al.amount) ELSE 0 END), 0) AS total_expense,
|
|
COALESCE(SUM(al.amount), 0) AS balance,
|
|
aa.budget - COALESCE(SUM(CASE WHEN al.line_type = 'expense' THEN ABS(al.amount) ELSE 0 END), 0) AS budget_variance
|
|
FROM analytics.analytic_accounts aa
|
|
LEFT JOIN analytics.analytic_lines al ON aa.id = al.analytic_account_id
|
|
WHERE aa.deleted_at IS NULL
|
|
GROUP BY aa.id, aa.code, aa.name, aa.budget;
|
|
|
|
COMMENT ON VIEW analytics.analytic_balance_view IS 'Vista de balance analítico por cuenta con presupuesto vs real';
|
|
|
|
-- =====================================================
|
|
-- FIN DEL SCHEMA ANALYTICS
|
|
-- =====================================================
|