706 lines
23 KiB
PL/PgSQL
706 lines
23 KiB
PL/PgSQL
-- =====================================================
|
|
-- SCHEMA: sales
|
|
-- PROPÓSITO: Gestión de ventas, cotizaciones, clientes
|
|
-- MÓDULOS: MGN-007 (Ventas Básico)
|
|
-- FECHA: 2025-11-24
|
|
-- =====================================================
|
|
|
|
-- Crear schema
|
|
CREATE SCHEMA IF NOT EXISTS sales;
|
|
|
|
-- =====================================================
|
|
-- TYPES (ENUMs)
|
|
-- =====================================================
|
|
|
|
CREATE TYPE sales.order_status AS ENUM (
|
|
'draft',
|
|
'sent',
|
|
'sale',
|
|
'done',
|
|
'cancelled'
|
|
);
|
|
|
|
CREATE TYPE sales.quotation_status AS ENUM (
|
|
'draft',
|
|
'sent',
|
|
'approved',
|
|
'rejected',
|
|
'converted',
|
|
'expired'
|
|
);
|
|
|
|
CREATE TYPE sales.invoice_policy AS ENUM (
|
|
'order',
|
|
'delivery'
|
|
);
|
|
|
|
CREATE TYPE sales.delivery_status AS ENUM (
|
|
'pending',
|
|
'partial',
|
|
'delivered'
|
|
);
|
|
|
|
CREATE TYPE sales.invoice_status AS ENUM (
|
|
'pending',
|
|
'partial',
|
|
'invoiced'
|
|
);
|
|
|
|
-- =====================================================
|
|
-- TABLES
|
|
-- =====================================================
|
|
|
|
-- Tabla: sales_orders (Órdenes de venta)
|
|
CREATE TABLE sales.sales_orders (
|
|
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,
|
|
|
|
-- Numeración
|
|
name VARCHAR(100) NOT NULL,
|
|
client_order_ref VARCHAR(100), -- Referencia del cliente
|
|
|
|
-- Cliente
|
|
partner_id UUID NOT NULL REFERENCES core.partners(id),
|
|
|
|
-- Fechas
|
|
order_date DATE NOT NULL,
|
|
validity_date DATE,
|
|
commitment_date DATE,
|
|
|
|
-- Configuración
|
|
currency_id UUID NOT NULL REFERENCES core.currencies(id),
|
|
pricelist_id UUID REFERENCES sales.pricelists(id),
|
|
payment_term_id UUID REFERENCES financial.payment_terms(id),
|
|
|
|
-- Usuario
|
|
user_id UUID REFERENCES auth.users(id),
|
|
sales_team_id UUID REFERENCES sales.sales_teams(id),
|
|
|
|
-- Montos
|
|
amount_untaxed DECIMAL(15, 2) NOT NULL DEFAULT 0,
|
|
amount_tax DECIMAL(15, 2) NOT NULL DEFAULT 0,
|
|
amount_total DECIMAL(15, 2) NOT NULL DEFAULT 0,
|
|
|
|
-- Estado
|
|
status sales.order_status NOT NULL DEFAULT 'draft',
|
|
invoice_status sales.invoice_status NOT NULL DEFAULT 'pending',
|
|
delivery_status sales.delivery_status NOT NULL DEFAULT 'pending',
|
|
|
|
-- Facturación
|
|
invoice_policy sales.invoice_policy DEFAULT 'order',
|
|
|
|
-- Relaciones generadas
|
|
picking_id UUID REFERENCES inventory.pickings(id),
|
|
|
|
-- Notas
|
|
notes TEXT,
|
|
terms_conditions TEXT,
|
|
|
|
-- Firma electrónica
|
|
signature TEXT, -- base64
|
|
signature_date TIMESTAMP,
|
|
signature_ip INET,
|
|
|
|
-- 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),
|
|
confirmed_at TIMESTAMP,
|
|
confirmed_by UUID REFERENCES auth.users(id),
|
|
cancelled_at TIMESTAMP,
|
|
cancelled_by UUID REFERENCES auth.users(id),
|
|
|
|
CONSTRAINT uq_sales_orders_name_company UNIQUE (company_id, name),
|
|
CONSTRAINT chk_sales_orders_validity CHECK (validity_date IS NULL OR validity_date >= order_date)
|
|
);
|
|
|
|
-- Tabla: sales_order_lines (Líneas de orden de venta)
|
|
CREATE TABLE sales.sales_order_lines (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
order_id UUID NOT NULL REFERENCES sales.sales_orders(id) ON DELETE CASCADE,
|
|
|
|
product_id UUID NOT NULL REFERENCES inventory.products(id),
|
|
description TEXT NOT NULL,
|
|
|
|
-- Cantidades
|
|
quantity DECIMAL(12, 4) NOT NULL,
|
|
qty_delivered DECIMAL(12, 4) DEFAULT 0,
|
|
qty_invoiced DECIMAL(12, 4) DEFAULT 0,
|
|
uom_id UUID NOT NULL REFERENCES core.uom(id),
|
|
|
|
-- Precios
|
|
price_unit DECIMAL(15, 4) NOT NULL,
|
|
discount DECIMAL(5, 2) DEFAULT 0, -- Porcentaje de descuento
|
|
|
|
-- Impuestos
|
|
tax_ids UUID[] DEFAULT '{}',
|
|
|
|
-- Montos
|
|
amount_untaxed DECIMAL(15, 2) NOT NULL,
|
|
amount_tax DECIMAL(15, 2) NOT NULL,
|
|
amount_total DECIMAL(15, 2) NOT NULL,
|
|
|
|
-- Analítica
|
|
analytic_account_id UUID REFERENCES analytics.analytic_accounts(id), -- Distribución analítica
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP,
|
|
|
|
CONSTRAINT chk_sales_order_lines_quantity CHECK (quantity > 0),
|
|
CONSTRAINT chk_sales_order_lines_discount CHECK (discount >= 0 AND discount <= 100),
|
|
CONSTRAINT chk_sales_order_lines_qty_delivered CHECK (qty_delivered >= 0 AND qty_delivered <= quantity),
|
|
CONSTRAINT chk_sales_order_lines_qty_invoiced CHECK (qty_invoiced >= 0 AND qty_invoiced <= quantity)
|
|
);
|
|
|
|
-- Índices para sales_order_lines
|
|
CREATE INDEX idx_sales_order_lines_tenant_id ON sales.sales_order_lines(tenant_id);
|
|
CREATE INDEX idx_sales_order_lines_order_id ON sales.sales_order_lines(order_id);
|
|
CREATE INDEX idx_sales_order_lines_product_id ON sales.sales_order_lines(product_id);
|
|
|
|
-- RLS para sales_order_lines
|
|
ALTER TABLE sales.sales_order_lines ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY tenant_isolation_sales_order_lines ON sales.sales_order_lines
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- Tabla: quotations (Cotizaciones)
|
|
CREATE TABLE sales.quotations (
|
|
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,
|
|
|
|
-- Numeración
|
|
name VARCHAR(100) NOT NULL,
|
|
|
|
-- Cliente potencial
|
|
partner_id UUID NOT NULL REFERENCES core.partners(id),
|
|
|
|
-- Fechas
|
|
quotation_date DATE NOT NULL,
|
|
validity_date DATE NOT NULL,
|
|
|
|
-- Configuración
|
|
currency_id UUID NOT NULL REFERENCES core.currencies(id),
|
|
pricelist_id UUID REFERENCES sales.pricelists(id),
|
|
|
|
-- Usuario
|
|
user_id UUID REFERENCES auth.users(id),
|
|
sales_team_id UUID REFERENCES sales.sales_teams(id),
|
|
|
|
-- Montos
|
|
amount_untaxed DECIMAL(15, 2) NOT NULL DEFAULT 0,
|
|
amount_tax DECIMAL(15, 2) NOT NULL DEFAULT 0,
|
|
amount_total DECIMAL(15, 2) NOT NULL DEFAULT 0,
|
|
|
|
-- Estado
|
|
status sales.quotation_status NOT NULL DEFAULT 'draft',
|
|
|
|
-- Conversión
|
|
sale_order_id UUID REFERENCES sales.sales_orders(id), -- Orden generada
|
|
|
|
-- Notas
|
|
notes TEXT,
|
|
terms_conditions TEXT,
|
|
|
|
-- Firma electrónica
|
|
signature TEXT, -- base64
|
|
signature_date TIMESTAMP,
|
|
signature_ip INET,
|
|
|
|
-- 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_quotations_name_company UNIQUE (company_id, name),
|
|
CONSTRAINT chk_quotations_validity CHECK (validity_date >= quotation_date)
|
|
);
|
|
|
|
-- Tabla: quotation_lines (Líneas de cotización)
|
|
CREATE TABLE sales.quotation_lines (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
quotation_id UUID NOT NULL REFERENCES sales.quotations(id) ON DELETE CASCADE,
|
|
|
|
product_id UUID REFERENCES inventory.products(id),
|
|
description TEXT NOT NULL,
|
|
quantity DECIMAL(12, 4) NOT NULL,
|
|
uom_id UUID NOT NULL REFERENCES core.uom(id),
|
|
price_unit DECIMAL(15, 4) NOT NULL,
|
|
discount DECIMAL(5, 2) DEFAULT 0,
|
|
tax_ids UUID[] DEFAULT '{}',
|
|
amount_untaxed DECIMAL(15, 2) NOT NULL,
|
|
amount_tax DECIMAL(15, 2) NOT NULL,
|
|
amount_total DECIMAL(15, 2) NOT NULL,
|
|
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT chk_quotation_lines_quantity CHECK (quantity > 0),
|
|
CONSTRAINT chk_quotation_lines_discount CHECK (discount >= 0 AND discount <= 100)
|
|
);
|
|
|
|
-- Índices para quotation_lines
|
|
CREATE INDEX idx_quotation_lines_tenant_id ON sales.quotation_lines(tenant_id);
|
|
CREATE INDEX idx_quotation_lines_quotation_id ON sales.quotation_lines(quotation_id);
|
|
CREATE INDEX idx_quotation_lines_product_id ON sales.quotation_lines(product_id);
|
|
|
|
-- RLS para quotation_lines
|
|
ALTER TABLE sales.quotation_lines ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY tenant_isolation_quotation_lines ON sales.quotation_lines
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- Tabla: pricelists (Listas de precios)
|
|
CREATE TABLE sales.pricelists (
|
|
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,
|
|
currency_id UUID NOT NULL REFERENCES core.currencies(id),
|
|
|
|
-- 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_pricelists_name_tenant UNIQUE (tenant_id, name)
|
|
);
|
|
|
|
-- Tabla: pricelist_items (Items de lista de precios)
|
|
CREATE TABLE sales.pricelist_items (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
pricelist_id UUID NOT NULL REFERENCES sales.pricelists(id) ON DELETE CASCADE,
|
|
|
|
product_id UUID REFERENCES inventory.products(id),
|
|
product_category_id UUID REFERENCES core.product_categories(id),
|
|
|
|
-- Precio
|
|
price DECIMAL(15, 4) NOT NULL,
|
|
|
|
-- Cantidad mínima
|
|
min_quantity DECIMAL(12, 4) DEFAULT 1,
|
|
|
|
-- Validez
|
|
valid_from DATE,
|
|
valid_to DATE,
|
|
|
|
-- Control
|
|
active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_by UUID REFERENCES auth.users(id),
|
|
|
|
CONSTRAINT chk_pricelist_items_price CHECK (price >= 0),
|
|
CONSTRAINT chk_pricelist_items_min_qty CHECK (min_quantity > 0),
|
|
CONSTRAINT chk_pricelist_items_dates CHECK (valid_to IS NULL OR valid_to >= valid_from),
|
|
CONSTRAINT chk_pricelist_items_product_or_category CHECK (
|
|
(product_id IS NOT NULL AND product_category_id IS NULL) OR
|
|
(product_id IS NULL AND product_category_id IS NOT NULL)
|
|
)
|
|
);
|
|
|
|
-- Índices para pricelist_items
|
|
CREATE INDEX idx_pricelist_items_tenant_id ON sales.pricelist_items(tenant_id);
|
|
CREATE INDEX idx_pricelist_items_pricelist_id ON sales.pricelist_items(pricelist_id);
|
|
CREATE INDEX idx_pricelist_items_product_id ON sales.pricelist_items(product_id);
|
|
|
|
-- RLS para pricelist_items
|
|
ALTER TABLE sales.pricelist_items ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY tenant_isolation_pricelist_items ON sales.pricelist_items
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- Tabla: customer_groups (Grupos de clientes)
|
|
CREATE TABLE sales.customer_groups (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
discount_percentage DECIMAL(5, 2) DEFAULT 0,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_by UUID REFERENCES auth.users(id),
|
|
|
|
CONSTRAINT uq_customer_groups_name_tenant UNIQUE (tenant_id, name),
|
|
CONSTRAINT chk_customer_groups_discount CHECK (discount_percentage >= 0 AND discount_percentage <= 100)
|
|
);
|
|
|
|
-- Tabla: customer_group_members (Miembros de grupos)
|
|
CREATE TABLE sales.customer_group_members (
|
|
customer_group_id UUID NOT NULL REFERENCES sales.customer_groups(id) ON DELETE CASCADE,
|
|
partner_id UUID NOT NULL REFERENCES core.partners(id) ON DELETE CASCADE,
|
|
joined_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
PRIMARY KEY (customer_group_id, partner_id)
|
|
);
|
|
|
|
-- Tabla: sales_teams (Equipos de ventas)
|
|
CREATE TABLE sales.sales_teams (
|
|
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),
|
|
team_leader_id UUID REFERENCES auth.users(id),
|
|
|
|
-- Objetivos
|
|
target_monthly DECIMAL(15, 2),
|
|
target_annual DECIMAL(15, 2),
|
|
|
|
-- 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_sales_teams_code_company UNIQUE (company_id, code)
|
|
);
|
|
|
|
-- Tabla: sales_team_members (Miembros de equipos)
|
|
CREATE TABLE sales.sales_team_members (
|
|
sales_team_id UUID NOT NULL REFERENCES sales.sales_teams(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
joined_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
PRIMARY KEY (sales_team_id, user_id)
|
|
);
|
|
|
|
-- =====================================================
|
|
-- INDICES
|
|
-- =====================================================
|
|
|
|
-- Sales Orders
|
|
CREATE INDEX idx_sales_orders_tenant_id ON sales.sales_orders(tenant_id);
|
|
CREATE INDEX idx_sales_orders_company_id ON sales.sales_orders(company_id);
|
|
CREATE INDEX idx_sales_orders_partner_id ON sales.sales_orders(partner_id);
|
|
CREATE INDEX idx_sales_orders_name ON sales.sales_orders(name);
|
|
CREATE INDEX idx_sales_orders_status ON sales.sales_orders(status);
|
|
CREATE INDEX idx_sales_orders_order_date ON sales.sales_orders(order_date);
|
|
CREATE INDEX idx_sales_orders_user_id ON sales.sales_orders(user_id);
|
|
CREATE INDEX idx_sales_orders_sales_team_id ON sales.sales_orders(sales_team_id);
|
|
|
|
-- Sales Order Lines
|
|
CREATE INDEX idx_sales_order_lines_order_id ON sales.sales_order_lines(order_id);
|
|
CREATE INDEX idx_sales_order_lines_product_id ON sales.sales_order_lines(product_id);
|
|
CREATE INDEX idx_sales_order_lines_analytic_account_id ON sales.sales_order_lines(analytic_account_id) WHERE analytic_account_id IS NOT NULL;
|
|
|
|
-- Quotations
|
|
CREATE INDEX idx_quotations_tenant_id ON sales.quotations(tenant_id);
|
|
CREATE INDEX idx_quotations_company_id ON sales.quotations(company_id);
|
|
CREATE INDEX idx_quotations_partner_id ON sales.quotations(partner_id);
|
|
CREATE INDEX idx_quotations_status ON sales.quotations(status);
|
|
CREATE INDEX idx_quotations_validity_date ON sales.quotations(validity_date);
|
|
|
|
-- Quotation Lines
|
|
CREATE INDEX idx_quotation_lines_quotation_id ON sales.quotation_lines(quotation_id);
|
|
CREATE INDEX idx_quotation_lines_product_id ON sales.quotation_lines(product_id);
|
|
|
|
-- Pricelists
|
|
CREATE INDEX idx_pricelists_tenant_id ON sales.pricelists(tenant_id);
|
|
CREATE INDEX idx_pricelists_active ON sales.pricelists(active) WHERE active = TRUE;
|
|
|
|
-- Pricelist Items
|
|
CREATE INDEX idx_pricelist_items_pricelist_id ON sales.pricelist_items(pricelist_id);
|
|
CREATE INDEX idx_pricelist_items_product_id ON sales.pricelist_items(product_id);
|
|
CREATE INDEX idx_pricelist_items_category_id ON sales.pricelist_items(product_category_id);
|
|
|
|
-- Customer Groups
|
|
CREATE INDEX idx_customer_groups_tenant_id ON sales.customer_groups(tenant_id);
|
|
|
|
-- Sales Teams
|
|
CREATE INDEX idx_sales_teams_tenant_id ON sales.sales_teams(tenant_id);
|
|
CREATE INDEX idx_sales_teams_company_id ON sales.sales_teams(company_id);
|
|
CREATE INDEX idx_sales_teams_leader_id ON sales.sales_teams(team_leader_id);
|
|
|
|
-- =====================================================
|
|
-- FUNCTIONS
|
|
-- =====================================================
|
|
|
|
-- Función: calculate_sales_order_totals
|
|
CREATE OR REPLACE FUNCTION sales.calculate_sales_order_totals(p_order_id UUID)
|
|
RETURNS VOID AS $$
|
|
DECLARE
|
|
v_amount_untaxed DECIMAL;
|
|
v_amount_tax DECIMAL;
|
|
v_amount_total DECIMAL;
|
|
BEGIN
|
|
SELECT
|
|
COALESCE(SUM(amount_untaxed), 0),
|
|
COALESCE(SUM(amount_tax), 0),
|
|
COALESCE(SUM(amount_total), 0)
|
|
INTO v_amount_untaxed, v_amount_tax, v_amount_total
|
|
FROM sales.sales_order_lines
|
|
WHERE order_id = p_order_id;
|
|
|
|
UPDATE sales.sales_orders
|
|
SET amount_untaxed = v_amount_untaxed,
|
|
amount_tax = v_amount_tax,
|
|
amount_total = v_amount_total,
|
|
updated_at = CURRENT_TIMESTAMP,
|
|
updated_by = get_current_user_id()
|
|
WHERE id = p_order_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION sales.calculate_sales_order_totals IS 'Calcula los totales de una orden de venta';
|
|
|
|
-- Función: calculate_quotation_totals
|
|
CREATE OR REPLACE FUNCTION sales.calculate_quotation_totals(p_quotation_id UUID)
|
|
RETURNS VOID AS $$
|
|
DECLARE
|
|
v_amount_untaxed DECIMAL;
|
|
v_amount_tax DECIMAL;
|
|
v_amount_total DECIMAL;
|
|
BEGIN
|
|
SELECT
|
|
COALESCE(SUM(amount_untaxed), 0),
|
|
COALESCE(SUM(amount_tax), 0),
|
|
COALESCE(SUM(amount_total), 0)
|
|
INTO v_amount_untaxed, v_amount_tax, v_amount_total
|
|
FROM sales.quotation_lines
|
|
WHERE quotation_id = p_quotation_id;
|
|
|
|
UPDATE sales.quotations
|
|
SET amount_untaxed = v_amount_untaxed,
|
|
amount_tax = v_amount_tax,
|
|
amount_total = v_amount_total,
|
|
updated_at = CURRENT_TIMESTAMP,
|
|
updated_by = get_current_user_id()
|
|
WHERE id = p_quotation_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION sales.calculate_quotation_totals IS 'Calcula los totales de una cotización';
|
|
|
|
-- Función: convert_quotation_to_order
|
|
CREATE OR REPLACE FUNCTION sales.convert_quotation_to_order(p_quotation_id UUID)
|
|
RETURNS UUID AS $$
|
|
DECLARE
|
|
v_quotation RECORD;
|
|
v_order_id UUID;
|
|
BEGIN
|
|
-- Obtener cotización
|
|
SELECT * INTO v_quotation
|
|
FROM sales.quotations
|
|
WHERE id = p_quotation_id;
|
|
|
|
IF NOT FOUND THEN
|
|
RAISE EXCEPTION 'Quotation % not found', p_quotation_id;
|
|
END IF;
|
|
|
|
IF v_quotation.status != 'approved' THEN
|
|
RAISE EXCEPTION 'Quotation must be approved before conversion';
|
|
END IF;
|
|
|
|
-- Crear orden de venta
|
|
INSERT INTO sales.sales_orders (
|
|
tenant_id,
|
|
company_id,
|
|
name,
|
|
partner_id,
|
|
order_date,
|
|
currency_id,
|
|
pricelist_id,
|
|
user_id,
|
|
sales_team_id,
|
|
amount_untaxed,
|
|
amount_tax,
|
|
amount_total,
|
|
notes,
|
|
terms_conditions,
|
|
signature,
|
|
signature_date,
|
|
signature_ip
|
|
) VALUES (
|
|
v_quotation.tenant_id,
|
|
v_quotation.company_id,
|
|
REPLACE(v_quotation.name, 'QT', 'SO'),
|
|
v_quotation.partner_id,
|
|
CURRENT_DATE,
|
|
v_quotation.currency_id,
|
|
v_quotation.pricelist_id,
|
|
v_quotation.user_id,
|
|
v_quotation.sales_team_id,
|
|
v_quotation.amount_untaxed,
|
|
v_quotation.amount_tax,
|
|
v_quotation.amount_total,
|
|
v_quotation.notes,
|
|
v_quotation.terms_conditions,
|
|
v_quotation.signature,
|
|
v_quotation.signature_date,
|
|
v_quotation.signature_ip
|
|
) RETURNING id INTO v_order_id;
|
|
|
|
-- Copiar líneas
|
|
INSERT INTO sales.sales_order_lines (
|
|
order_id,
|
|
product_id,
|
|
description,
|
|
quantity,
|
|
uom_id,
|
|
price_unit,
|
|
discount,
|
|
tax_ids,
|
|
amount_untaxed,
|
|
amount_tax,
|
|
amount_total
|
|
)
|
|
SELECT
|
|
v_order_id,
|
|
product_id,
|
|
description,
|
|
quantity,
|
|
uom_id,
|
|
price_unit,
|
|
discount,
|
|
tax_ids,
|
|
amount_untaxed,
|
|
amount_tax,
|
|
amount_total
|
|
FROM sales.quotation_lines
|
|
WHERE quotation_id = p_quotation_id;
|
|
|
|
-- Actualizar cotización
|
|
UPDATE sales.quotations
|
|
SET status = 'converted',
|
|
sale_order_id = v_order_id,
|
|
updated_at = CURRENT_TIMESTAMP,
|
|
updated_by = get_current_user_id()
|
|
WHERE id = p_quotation_id;
|
|
|
|
RETURN v_order_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION sales.convert_quotation_to_order IS 'Convierte una cotización aprobada en orden de venta';
|
|
|
|
-- =====================================================
|
|
-- TRIGGERS
|
|
-- =====================================================
|
|
|
|
CREATE TRIGGER trg_sales_orders_updated_at
|
|
BEFORE UPDATE ON sales.sales_orders
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_updated_at_column();
|
|
|
|
CREATE TRIGGER trg_quotations_updated_at
|
|
BEFORE UPDATE ON sales.quotations
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_updated_at_column();
|
|
|
|
CREATE TRIGGER trg_pricelists_updated_at
|
|
BEFORE UPDATE ON sales.pricelists
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_updated_at_column();
|
|
|
|
CREATE TRIGGER trg_sales_teams_updated_at
|
|
BEFORE UPDATE ON sales.sales_teams
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_updated_at_column();
|
|
|
|
-- Trigger: Actualizar totales de orden al cambiar líneas
|
|
CREATE OR REPLACE FUNCTION sales.trg_update_so_totals()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF TG_OP = 'DELETE' THEN
|
|
PERFORM sales.calculate_sales_order_totals(OLD.order_id);
|
|
ELSE
|
|
PERFORM sales.calculate_sales_order_totals(NEW.order_id);
|
|
END IF;
|
|
RETURN NULL;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER trg_sales_order_lines_update_totals
|
|
AFTER INSERT OR UPDATE OR DELETE ON sales.sales_order_lines
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION sales.trg_update_so_totals();
|
|
|
|
-- Trigger: Actualizar totales de cotización al cambiar líneas
|
|
CREATE OR REPLACE FUNCTION sales.trg_update_quotation_totals()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF TG_OP = 'DELETE' THEN
|
|
PERFORM sales.calculate_quotation_totals(OLD.quotation_id);
|
|
ELSE
|
|
PERFORM sales.calculate_quotation_totals(NEW.quotation_id);
|
|
END IF;
|
|
RETURN NULL;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER trg_quotation_lines_update_totals
|
|
AFTER INSERT OR UPDATE OR DELETE ON sales.quotation_lines
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION sales.trg_update_quotation_totals();
|
|
|
|
-- =====================================================
|
|
-- TRACKING AUTOMÁTICO (mail.thread pattern)
|
|
-- =====================================================
|
|
|
|
-- Trigger: Tracking automático para órdenes de venta
|
|
CREATE TRIGGER track_sales_order_changes
|
|
AFTER INSERT OR UPDATE OR DELETE ON sales.sales_orders
|
|
FOR EACH ROW EXECUTE FUNCTION system.track_field_changes();
|
|
|
|
COMMENT ON TRIGGER track_sales_order_changes ON sales.sales_orders IS
|
|
'Registra automáticamente cambios en órdenes de venta (estado, cliente, monto, fecha, facturación, entrega)';
|
|
|
|
-- =====================================================
|
|
-- ROW LEVEL SECURITY (RLS)
|
|
-- =====================================================
|
|
|
|
ALTER TABLE sales.sales_orders ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE sales.quotations ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE sales.pricelists ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE sales.customer_groups ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE sales.sales_teams ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY tenant_isolation_sales_orders ON sales.sales_orders
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
CREATE POLICY tenant_isolation_quotations ON sales.quotations
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
CREATE POLICY tenant_isolation_pricelists ON sales.pricelists
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
CREATE POLICY tenant_isolation_customer_groups ON sales.customer_groups
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
CREATE POLICY tenant_isolation_sales_teams ON sales.sales_teams
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
-- =====================================================
|
|
-- COMENTARIOS
|
|
-- =====================================================
|
|
|
|
COMMENT ON SCHEMA sales IS 'Schema de gestión de ventas, cotizaciones y clientes';
|
|
COMMENT ON TABLE sales.sales_orders IS 'Órdenes de venta confirmadas';
|
|
COMMENT ON TABLE sales.sales_order_lines IS 'Líneas de órdenes de venta';
|
|
COMMENT ON TABLE sales.quotations IS 'Cotizaciones enviadas a clientes';
|
|
COMMENT ON TABLE sales.quotation_lines IS 'Líneas de cotizaciones';
|
|
COMMENT ON TABLE sales.pricelists IS 'Listas de precios para clientes';
|
|
COMMENT ON TABLE sales.pricelist_items IS 'Items de listas de precios por producto/categoría';
|
|
COMMENT ON TABLE sales.customer_groups IS 'Grupos de clientes para descuentos y segmentación';
|
|
COMMENT ON TABLE sales.sales_teams IS 'Equipos de ventas con objetivos';
|
|
|
|
-- =====================================================
|
|
-- FIN DEL SCHEMA SALES
|
|
-- =====================================================
|