[TASK-2026-02-05-EJECUCION-REMEDIATION-ERP-CORE] feat: DDL fixes and new schemas
- 00-auth-base.sql: Extracted auth.tenants+users from recreate-database.sh - 03b-core-companies.sql: DDL for auth.companies entity - 21b-inventory-extended.sql: 7 new tables for inventory entities without DDL - 24-invoices.sql: billing→operations schema to resolve duplication - 27/28/29-cfdi: Track existing CFDI DDL files - recreate-database.sh: Updated ddl_files array (17→43 entries) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c9ddeb8b5a
commit
02ab2caf26
52
ddl/00-auth-base.sql
Normal file
52
ddl/00-auth-base.sql
Normal file
@ -0,0 +1,52 @@
|
||||
-- =============================================================
|
||||
-- ARCHIVO: 00-auth-base.sql
|
||||
-- DESCRIPCION: Tablas base de autenticacion (tenants y users)
|
||||
-- Estas tablas son prerequisito para todos los demas DDL
|
||||
-- VERSION: 1.0.0
|
||||
-- PROYECTO: ERP-Core V2
|
||||
-- FECHA: 2026-02-05
|
||||
-- =============================================================
|
||||
|
||||
-- =====================
|
||||
-- SCHEMA: auth (si no existe)
|
||||
-- =====================
|
||||
CREATE SCHEMA IF NOT EXISTS auth;
|
||||
|
||||
-- =====================
|
||||
-- TABLA: tenants
|
||||
-- Organizaciones/empresas en el sistema multi-tenant
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS auth.tenants (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(200) NOT NULL,
|
||||
slug VARCHAR(100) UNIQUE NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- =====================
|
||||
-- TABLA: users
|
||||
-- Usuarios del sistema, vinculados a un tenant
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS auth.users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
password_hash TEXT,
|
||||
first_name VARCHAR(100),
|
||||
last_name VARCHAR(100),
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
email_verified BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMPTZ,
|
||||
UNIQUE(tenant_id, email)
|
||||
);
|
||||
|
||||
-- =====================
|
||||
-- INDICES
|
||||
-- =====================
|
||||
CREATE INDEX IF NOT EXISTS idx_users_tenant ON auth.users(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON auth.users(email);
|
||||
95
ddl/03b-core-companies.sql
Normal file
95
ddl/03b-core-companies.sql
Normal file
@ -0,0 +1,95 @@
|
||||
-- =============================================================
|
||||
-- ARCHIVO: 03b-core-companies.sql
|
||||
-- DESCRIPCION: Tabla de empresas/companies del tenant
|
||||
-- VERSION: 1.0.0
|
||||
-- PROYECTO: ERP-Core V2
|
||||
-- FECHA: 2026-02-05
|
||||
-- DEPENDE DE: 00-auth-base.sql (auth.tenants, auth.users)
|
||||
-- 20-core-catalogs.sql (core.currencies)
|
||||
-- 16-partners.sql (partners.partners)
|
||||
-- NOTA: La entidad TypeORM usa schema 'auth'. Las FK en
|
||||
-- 45-hr.sql apuntan a core.companies (inconsistencia
|
||||
-- conocida a corregir en tarea separada).
|
||||
-- =============================================================
|
||||
|
||||
-- =====================
|
||||
-- SCHEMA: auth (si no existe)
|
||||
-- =====================
|
||||
CREATE SCHEMA IF NOT EXISTS auth;
|
||||
|
||||
-- =====================
|
||||
-- TABLA: companies
|
||||
-- Empresas registradas dentro de un tenant (multi-empresa)
|
||||
-- Soporta jerarquia de empresas (parent_company_id)
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS auth.companies (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
|
||||
-- Identificacion
|
||||
name VARCHAR(255) NOT NULL,
|
||||
legal_name VARCHAR(255),
|
||||
tax_id VARCHAR(50),
|
||||
|
||||
-- Relaciones
|
||||
currency_id UUID REFERENCES core.currencies(id) ON DELETE SET NULL,
|
||||
parent_company_id UUID REFERENCES auth.companies(id) ON DELETE SET NULL,
|
||||
partner_id UUID REFERENCES partners.partners(id) ON DELETE SET NULL,
|
||||
|
||||
-- Configuracion
|
||||
settings JSONB DEFAULT '{}',
|
||||
|
||||
-- Auditoria
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
deleted_at TIMESTAMPTZ,
|
||||
deleted_by UUID REFERENCES auth.users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- =====================
|
||||
-- INDICES
|
||||
-- =====================
|
||||
CREATE INDEX IF NOT EXISTS idx_companies_tenant_id ON auth.companies(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_companies_parent_company_id ON auth.companies(parent_company_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_companies_active ON auth.companies(tenant_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_companies_tax_id ON auth.companies(tax_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_companies_currency ON auth.companies(currency_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_companies_partner ON auth.companies(partner_id);
|
||||
|
||||
-- =====================
|
||||
-- RLS POLICIES
|
||||
-- =====================
|
||||
ALTER TABLE auth.companies ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY tenant_isolation_companies ON auth.companies
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
||||
|
||||
-- =====================
|
||||
-- TRIGGER: updated_at automatico
|
||||
-- =====================
|
||||
CREATE OR REPLACE FUNCTION auth.update_companies_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at := CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_companies_updated_at
|
||||
BEFORE UPDATE ON auth.companies
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION auth.update_companies_updated_at();
|
||||
|
||||
-- =====================
|
||||
-- COMENTARIOS
|
||||
-- =====================
|
||||
COMMENT ON TABLE auth.companies IS 'Empresas registradas dentro de un tenant. Soporta jerarquia via parent_company_id.';
|
||||
COMMENT ON COLUMN auth.companies.tenant_id IS 'Tenant al que pertenece la empresa';
|
||||
COMMENT ON COLUMN auth.companies.name IS 'Nombre comercial de la empresa';
|
||||
COMMENT ON COLUMN auth.companies.legal_name IS 'Razon social / nombre legal';
|
||||
COMMENT ON COLUMN auth.companies.tax_id IS 'RFC o identificador fiscal';
|
||||
COMMENT ON COLUMN auth.companies.currency_id IS 'Moneda principal de la empresa (ref core.currencies)';
|
||||
COMMENT ON COLUMN auth.companies.parent_company_id IS 'Empresa padre para jerarquia corporativa';
|
||||
COMMENT ON COLUMN auth.companies.partner_id IS 'Partner asociado a la empresa (ref partners.partners)';
|
||||
COMMENT ON COLUMN auth.companies.settings IS 'Configuraciones especificas de la empresa en formato JSON';
|
||||
478
ddl/21b-inventory-extended.sql
Normal file
478
ddl/21b-inventory-extended.sql
Normal file
@ -0,0 +1,478 @@
|
||||
-- =============================================================
|
||||
-- ARCHIVO: 21b-inventory-extended.sql
|
||||
-- DESCRIPCION: Tablas extendidas de inventario (Odoo-style)
|
||||
-- Incluye: locations, products (inventory-specific), stock_quants,
|
||||
-- stock_moves, stock_valuation_layers, inventory_adjustments,
|
||||
-- inventory_adjustment_lines
|
||||
-- VERSION: 1.0.0
|
||||
-- PROYECTO: ERP-Core V2
|
||||
-- FECHA: 2026-02-05
|
||||
-- DEPENDE DE: 18-warehouses.sql, 21-inventory.sql
|
||||
-- COHERENCIA: Entidades TypeORM sin DDL previo
|
||||
-- =============================================================
|
||||
|
||||
-- =====================
|
||||
-- SCHEMA: inventory (ya creado en 18-warehouses.sql)
|
||||
-- =====================
|
||||
|
||||
-- =====================
|
||||
-- TIPO ENUM: location_type
|
||||
-- Tipos de ubicacion Odoo-style
|
||||
-- =====================
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'location_type_enum') THEN
|
||||
CREATE TYPE inventory.location_type_enum AS ENUM (
|
||||
'internal',
|
||||
'supplier',
|
||||
'customer',
|
||||
'inventory',
|
||||
'production',
|
||||
'transit'
|
||||
);
|
||||
END IF;
|
||||
END$$;
|
||||
|
||||
-- =====================
|
||||
-- TIPO ENUM: product_type
|
||||
-- Tipos de producto para inventario
|
||||
-- =====================
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'inv_product_type_enum') THEN
|
||||
CREATE TYPE inventory.inv_product_type_enum AS ENUM (
|
||||
'storable',
|
||||
'consumable',
|
||||
'service'
|
||||
);
|
||||
END IF;
|
||||
END$$;
|
||||
|
||||
-- =====================
|
||||
-- TIPO ENUM: tracking_type
|
||||
-- Tipo de seguimiento de producto
|
||||
-- =====================
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'tracking_type_enum') THEN
|
||||
CREATE TYPE inventory.tracking_type_enum AS ENUM (
|
||||
'none',
|
||||
'lot',
|
||||
'serial'
|
||||
);
|
||||
END IF;
|
||||
END$$;
|
||||
|
||||
-- =====================
|
||||
-- TIPO ENUM: valuation_method
|
||||
-- Metodo de valuacion de inventario
|
||||
-- =====================
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'valuation_method_enum') THEN
|
||||
CREATE TYPE inventory.valuation_method_enum AS ENUM (
|
||||
'standard',
|
||||
'fifo',
|
||||
'average'
|
||||
);
|
||||
END IF;
|
||||
END$$;
|
||||
|
||||
-- =====================
|
||||
-- TIPO ENUM: adjustment_status
|
||||
-- Estado de ajuste de inventario
|
||||
-- =====================
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'adjustment_status_enum') THEN
|
||||
CREATE TYPE inventory.adjustment_status_enum AS ENUM (
|
||||
'draft',
|
||||
'confirmed',
|
||||
'done',
|
||||
'cancelled'
|
||||
);
|
||||
END IF;
|
||||
END$$;
|
||||
|
||||
-- =====================
|
||||
-- TABLA: locations
|
||||
-- Ubicaciones logicas de inventario (Odoo-style)
|
||||
-- Complementa warehouse_locations con modelo Odoo de ubicaciones virtuales
|
||||
-- (supplier, customer, inventory loss, production, transit)
|
||||
-- Coherencia: location.entity.ts
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS inventory.locations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
warehouse_id UUID REFERENCES inventory.warehouses(id) ON DELETE SET NULL,
|
||||
|
||||
-- Identificacion
|
||||
name VARCHAR(255) NOT NULL,
|
||||
complete_name VARCHAR(500),
|
||||
|
||||
-- Tipo de ubicacion
|
||||
location_type inventory.location_type_enum NOT NULL,
|
||||
|
||||
-- Jerarquia (auto-referencia)
|
||||
parent_id UUID REFERENCES inventory.locations(id) ON DELETE SET NULL,
|
||||
|
||||
-- Flags especiales
|
||||
is_scrap_location BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_return_location BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
-- Estado
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
|
||||
-- Metadata
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by UUID REFERENCES auth.users(id),
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_by UUID REFERENCES auth.users(id)
|
||||
);
|
||||
|
||||
-- Indices para locations
|
||||
CREATE INDEX IF NOT EXISTS idx_locations_tenant_id ON inventory.locations(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_locations_warehouse_id ON inventory.locations(warehouse_id) WHERE warehouse_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_locations_parent_id ON inventory.locations(parent_id) WHERE parent_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_locations_type ON inventory.locations(location_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_locations_active ON inventory.locations(active) WHERE active = TRUE;
|
||||
CREATE INDEX IF NOT EXISTS idx_locations_scrap ON inventory.locations(is_scrap_location) WHERE is_scrap_location = TRUE;
|
||||
CREATE INDEX IF NOT EXISTS idx_locations_return ON inventory.locations(is_return_location) WHERE is_return_location = TRUE;
|
||||
|
||||
-- =====================
|
||||
-- TABLA: products
|
||||
-- Productos especificos de inventario (Odoo-style)
|
||||
-- Diferente de products.products: este es para gestion de almacen,
|
||||
-- valuacion, tracking por lote/serie, mientras products.products
|
||||
-- es para comercio/retail con codigos SAT, impuestos, etc.
|
||||
-- Coherencia: product.entity.ts (inventory module)
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS inventory.products (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
|
||||
-- Identificacion
|
||||
name VARCHAR(255) NOT NULL,
|
||||
code VARCHAR(100),
|
||||
barcode VARCHAR(100),
|
||||
description TEXT,
|
||||
|
||||
-- Clasificacion
|
||||
product_type inventory.inv_product_type_enum NOT NULL DEFAULT 'storable',
|
||||
tracking inventory.tracking_type_enum NOT NULL DEFAULT 'none',
|
||||
category_id UUID,
|
||||
|
||||
-- Unidades de medida
|
||||
uom_id UUID NOT NULL,
|
||||
purchase_uom_id UUID,
|
||||
|
||||
-- Precios y costo
|
||||
cost_price DECIMAL(12, 2) NOT NULL DEFAULT 0,
|
||||
list_price DECIMAL(12, 2) NOT NULL DEFAULT 0,
|
||||
|
||||
-- Valuacion
|
||||
valuation_method inventory.valuation_method_enum NOT NULL DEFAULT 'fifo',
|
||||
|
||||
-- Flags
|
||||
is_storable BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
can_be_sold BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
can_be_purchased BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
|
||||
-- Medidas fisicas
|
||||
weight DECIMAL(12, 4),
|
||||
volume DECIMAL(12, 4),
|
||||
|
||||
-- Imagen
|
||||
image_url VARCHAR(500),
|
||||
|
||||
-- Metadata
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by UUID REFERENCES auth.users(id),
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_by UUID REFERENCES auth.users(id),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
deleted_by UUID REFERENCES auth.users(id)
|
||||
);
|
||||
|
||||
-- Indices para products
|
||||
CREATE INDEX IF NOT EXISTS idx_inv_products_tenant_id ON inventory.products(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_inv_products_code ON inventory.products(code) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_inv_products_barcode ON inventory.products(barcode) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_inv_products_category_id ON inventory.products(category_id) WHERE category_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_inv_products_active ON inventory.products(active) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_inv_products_type ON inventory.products(product_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_inv_products_tracking ON inventory.products(tracking) WHERE tracking != 'none';
|
||||
CREATE INDEX IF NOT EXISTS idx_inv_products_valuation ON inventory.products(valuation_method);
|
||||
|
||||
-- Unique code per tenant (soft-delete aware)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_inv_products_tenant_code
|
||||
ON inventory.products(tenant_id, code) WHERE deleted_at IS NULL AND code IS NOT NULL;
|
||||
|
||||
-- =====================
|
||||
-- TABLA: stock_quants
|
||||
-- Cantidades de stock por producto/ubicacion/lote (modelo Odoo)
|
||||
-- Representa la cantidad real de un producto en una ubicacion especifica,
|
||||
-- opcionalmente separada por lote
|
||||
-- Coherencia: stock-quant.entity.ts
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS inventory.stock_quants (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
|
||||
-- Producto y ubicacion
|
||||
product_id UUID NOT NULL REFERENCES inventory.products(id) ON DELETE CASCADE,
|
||||
location_id UUID NOT NULL REFERENCES inventory.locations(id) ON DELETE CASCADE,
|
||||
|
||||
-- Lote (opcional)
|
||||
lot_id UUID REFERENCES inventory.lots(id) ON DELETE SET NULL,
|
||||
|
||||
-- Cantidades
|
||||
quantity DECIMAL(16, 4) NOT NULL DEFAULT 0,
|
||||
reserved_quantity DECIMAL(16, 4) NOT NULL DEFAULT 0,
|
||||
|
||||
-- Metadata
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- Unicidad: un solo registro por combinacion producto+ubicacion+lote
|
||||
UNIQUE(product_id, location_id, COALESCE(lot_id, '00000000-0000-0000-0000-000000000000'::UUID))
|
||||
);
|
||||
|
||||
-- Indices para stock_quants
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_quants_tenant_id ON inventory.stock_quants(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_quants_product_id ON inventory.stock_quants(product_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_quants_location_id ON inventory.stock_quants(location_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_quants_lot_id ON inventory.stock_quants(lot_id) WHERE lot_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_quants_quantity ON inventory.stock_quants(quantity) WHERE quantity != 0;
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_quants_reserved ON inventory.stock_quants(reserved_quantity) WHERE reserved_quantity != 0;
|
||||
|
||||
-- =====================
|
||||
-- TABLA: stock_moves
|
||||
-- Movimientos individuales de stock vinculados a un picking (Odoo-style)
|
||||
-- Cada linea de un picking genera un stock_move
|
||||
-- Coherencia: stock-move.entity.ts
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS inventory.stock_moves (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
|
||||
-- Picking padre
|
||||
picking_id UUID NOT NULL REFERENCES inventory.pickings(id) ON DELETE CASCADE,
|
||||
|
||||
-- Producto
|
||||
product_id UUID NOT NULL REFERENCES inventory.products(id) ON DELETE RESTRICT,
|
||||
product_uom_id UUID NOT NULL,
|
||||
|
||||
-- Ubicaciones origen y destino
|
||||
location_id UUID NOT NULL REFERENCES inventory.locations(id),
|
||||
location_dest_id UUID NOT NULL REFERENCES inventory.locations(id),
|
||||
|
||||
-- Cantidades
|
||||
product_qty DECIMAL(16, 4) NOT NULL,
|
||||
quantity_done DECIMAL(16, 4) NOT NULL DEFAULT 0,
|
||||
|
||||
-- Lote (opcional)
|
||||
lot_id UUID REFERENCES inventory.lots(id) ON DELETE SET NULL,
|
||||
|
||||
-- Estado (reutiliza move_status_enum de 21-inventory.sql)
|
||||
status inventory.move_status_enum NOT NULL DEFAULT 'draft',
|
||||
|
||||
-- Fecha del movimiento
|
||||
date TIMESTAMPTZ,
|
||||
|
||||
-- Documento origen
|
||||
origin VARCHAR(255),
|
||||
|
||||
-- Metadata
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by UUID REFERENCES auth.users(id),
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_by UUID REFERENCES auth.users(id)
|
||||
);
|
||||
|
||||
-- Indices para stock_moves
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_moves_tenant_id ON inventory.stock_moves(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_moves_picking_id ON inventory.stock_moves(picking_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_moves_product_id ON inventory.stock_moves(product_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_moves_status ON inventory.stock_moves(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_moves_date ON inventory.stock_moves(date) WHERE date IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_moves_location ON inventory.stock_moves(location_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_moves_location_dest ON inventory.stock_moves(location_dest_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_moves_lot_id ON inventory.stock_moves(lot_id) WHERE lot_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_moves_origin ON inventory.stock_moves(origin) WHERE origin IS NOT NULL;
|
||||
|
||||
-- =====================
|
||||
-- TABLA: stock_valuation_layers
|
||||
-- Capas de valuacion de inventario (FIFO/AVCO/Standard)
|
||||
-- Cada movimiento de stock genera una capa de valuacion para
|
||||
-- rastrear el costo historico del inventario
|
||||
-- Coherencia: stock-valuation-layer.entity.ts
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS inventory.stock_valuation_layers (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
|
||||
-- Producto y empresa
|
||||
product_id UUID NOT NULL REFERENCES inventory.products(id) ON DELETE RESTRICT,
|
||||
company_id UUID NOT NULL REFERENCES auth.companies(id) ON DELETE CASCADE,
|
||||
|
||||
-- Cantidades y valores
|
||||
quantity DECIMAL(16, 4) NOT NULL,
|
||||
unit_cost DECIMAL(12, 2) NOT NULL,
|
||||
value DECIMAL(16, 2) NOT NULL,
|
||||
|
||||
-- Remanente (para FIFO)
|
||||
remaining_qty DECIMAL(16, 4) NOT NULL,
|
||||
remaining_value DECIMAL(16, 2) NOT NULL,
|
||||
|
||||
-- Movimiento de stock origen
|
||||
stock_move_id UUID REFERENCES inventory.stock_moves(id) ON DELETE SET NULL,
|
||||
|
||||
-- Descripcion
|
||||
description VARCHAR(255),
|
||||
|
||||
-- Contabilidad (referencia a asientos contables)
|
||||
account_move_id UUID,
|
||||
journal_entry_id UUID,
|
||||
|
||||
-- Metadata
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by UUID REFERENCES auth.users(id),
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_by UUID REFERENCES auth.users(id)
|
||||
);
|
||||
|
||||
-- Indices para stock_valuation_layers
|
||||
CREATE INDEX IF NOT EXISTS idx_valuation_layers_tenant_id ON inventory.stock_valuation_layers(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_valuation_layers_product_id ON inventory.stock_valuation_layers(product_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_valuation_layers_company_id ON inventory.stock_valuation_layers(company_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_valuation_layers_stock_move_id ON inventory.stock_valuation_layers(stock_move_id) WHERE stock_move_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_valuation_layers_remaining_qty ON inventory.stock_valuation_layers(remaining_qty) WHERE remaining_qty > 0;
|
||||
CREATE INDEX IF NOT EXISTS idx_valuation_layers_account_move ON inventory.stock_valuation_layers(account_move_id) WHERE account_move_id IS NOT NULL;
|
||||
|
||||
-- =====================
|
||||
-- TABLA: inventory_adjustments
|
||||
-- Ajustes de inventario (conteos y correcciones)
|
||||
-- Agrupa lineas de ajuste para una ubicacion y fecha especifica
|
||||
-- Coherencia: inventory-adjustment.entity.ts
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS inventory.inventory_adjustments (
|
||||
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,
|
||||
|
||||
-- Identificacion
|
||||
name VARCHAR(100) NOT NULL,
|
||||
|
||||
-- Ubicacion de ajuste
|
||||
location_id UUID NOT NULL REFERENCES inventory.locations(id),
|
||||
|
||||
-- Fecha del ajuste
|
||||
date DATE NOT NULL,
|
||||
|
||||
-- Estado
|
||||
status inventory.adjustment_status_enum NOT NULL DEFAULT 'draft',
|
||||
|
||||
-- Notas
|
||||
notes TEXT,
|
||||
|
||||
-- Metadata
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by UUID REFERENCES auth.users(id),
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_by UUID REFERENCES auth.users(id)
|
||||
);
|
||||
|
||||
-- Indices para inventory_adjustments
|
||||
CREATE INDEX IF NOT EXISTS idx_adjustments_tenant_id ON inventory.inventory_adjustments(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_adjustments_company_id ON inventory.inventory_adjustments(company_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_adjustments_status ON inventory.inventory_adjustments(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_adjustments_date ON inventory.inventory_adjustments(date);
|
||||
CREATE INDEX IF NOT EXISTS idx_adjustments_location ON inventory.inventory_adjustments(location_id);
|
||||
|
||||
-- =====================
|
||||
-- TABLA: inventory_adjustment_lines
|
||||
-- Lineas individuales de ajuste de inventario
|
||||
-- Cada linea compara cantidad teorica vs contada para un producto
|
||||
-- Coherencia: inventory-adjustment-line.entity.ts
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS inventory.inventory_adjustment_lines (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
adjustment_id UUID NOT NULL REFERENCES inventory.inventory_adjustments(id) ON DELETE CASCADE,
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
|
||||
-- Producto y ubicacion
|
||||
product_id UUID NOT NULL REFERENCES inventory.products(id) ON DELETE RESTRICT,
|
||||
location_id UUID NOT NULL REFERENCES inventory.locations(id),
|
||||
|
||||
-- Lote (opcional)
|
||||
lot_id UUID REFERENCES inventory.lots(id) ON DELETE SET NULL,
|
||||
|
||||
-- Cantidades
|
||||
theoretical_qty DECIMAL(16, 4) NOT NULL DEFAULT 0,
|
||||
counted_qty DECIMAL(16, 4) NOT NULL DEFAULT 0,
|
||||
difference_qty DECIMAL(16, 4) GENERATED ALWAYS AS (counted_qty - theoretical_qty) STORED,
|
||||
|
||||
-- Unidad de medida
|
||||
uom_id UUID,
|
||||
|
||||
-- Notas
|
||||
notes TEXT,
|
||||
|
||||
-- Metadata
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Indices para inventory_adjustment_lines
|
||||
CREATE INDEX IF NOT EXISTS idx_adjustment_lines_adjustment_id ON inventory.inventory_adjustment_lines(adjustment_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_adjustment_lines_product_id ON inventory.inventory_adjustment_lines(product_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_adjustment_lines_location_id ON inventory.inventory_adjustment_lines(location_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_adjustment_lines_lot_id ON inventory.inventory_adjustment_lines(lot_id) WHERE lot_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_adjustment_lines_tenant_id ON inventory.inventory_adjustment_lines(tenant_id);
|
||||
|
||||
-- =====================
|
||||
-- COMENTARIOS
|
||||
-- =====================
|
||||
|
||||
-- locations
|
||||
COMMENT ON TABLE inventory.locations IS 'Ubicaciones logicas de inventario (Odoo-style). Incluye ubicaciones virtuales: supplier, customer, inventory loss, production, transit';
|
||||
COMMENT ON COLUMN inventory.locations.location_type IS 'Tipo: internal (almacen), supplier (proveedor virtual), customer (cliente virtual), inventory (perdida/ajuste), production (produccion), transit (en transito)';
|
||||
COMMENT ON COLUMN inventory.locations.complete_name IS 'Nombre completo con jerarquia (ej: WH/Stock/Zone-A)';
|
||||
COMMENT ON COLUMN inventory.locations.is_scrap_location IS 'Si es TRUE, productos movidos aqui se consideran scrap/desperdicio';
|
||||
COMMENT ON COLUMN inventory.locations.is_return_location IS 'Si es TRUE, esta ubicacion se usa para devoluciones';
|
||||
|
||||
-- products (inventory)
|
||||
COMMENT ON TABLE inventory.products IS 'Productos para gestion de inventario (Odoo-style). Diferente de products.products que es para comercio/retail';
|
||||
COMMENT ON COLUMN inventory.products.product_type IS 'Tipo: storable (se almacena), consumable (se consume sin tracking), service (no se almacena)';
|
||||
COMMENT ON COLUMN inventory.products.tracking IS 'Seguimiento: none, lot (por lote), serial (numero de serie unico)';
|
||||
COMMENT ON COLUMN inventory.products.valuation_method IS 'Metodo de valuacion: standard (costo fijo), fifo (primero en entrar), average (costo promedio)';
|
||||
COMMENT ON COLUMN inventory.products.is_storable IS 'Flag derivado de product_type=storable. Facilita filtros directos';
|
||||
|
||||
-- stock_quants
|
||||
COMMENT ON TABLE inventory.stock_quants IS 'Cantidades reales de stock por producto/ubicacion/lote (modelo Odoo quant)';
|
||||
COMMENT ON COLUMN inventory.stock_quants.quantity IS 'Cantidad total disponible en esta ubicacion';
|
||||
COMMENT ON COLUMN inventory.stock_quants.reserved_quantity IS 'Cantidad reservada para pickings pendientes';
|
||||
|
||||
-- stock_moves
|
||||
COMMENT ON TABLE inventory.stock_moves IS 'Movimientos individuales de stock vinculados a un picking. Cada linea de picking genera un move';
|
||||
COMMENT ON COLUMN inventory.stock_moves.product_qty IS 'Cantidad solicitada del movimiento';
|
||||
COMMENT ON COLUMN inventory.stock_moves.quantity_done IS 'Cantidad realmente procesada';
|
||||
COMMENT ON COLUMN inventory.stock_moves.status IS 'Estado: draft, waiting, confirmed, assigned, done, cancelled';
|
||||
COMMENT ON COLUMN inventory.stock_moves.origin IS 'Documento origen (ej: PO-2026-001, SO-2026-001)';
|
||||
|
||||
-- stock_valuation_layers
|
||||
COMMENT ON TABLE inventory.stock_valuation_layers IS 'Capas de valuacion de inventario para FIFO/AVCO/Standard. Cada movimiento crea una capa';
|
||||
COMMENT ON COLUMN inventory.stock_valuation_layers.remaining_qty IS 'Cantidad restante en esta capa (para FIFO: se consume de las capas mas antiguas)';
|
||||
COMMENT ON COLUMN inventory.stock_valuation_layers.remaining_value IS 'Valor monetario restante en esta capa';
|
||||
COMMENT ON COLUMN inventory.stock_valuation_layers.account_move_id IS 'Referencia al asiento contable generado (si aplica)';
|
||||
|
||||
-- inventory_adjustments
|
||||
COMMENT ON TABLE inventory.inventory_adjustments IS 'Ajustes de inventario para corregir diferencias entre stock teorico y real';
|
||||
COMMENT ON COLUMN inventory.inventory_adjustments.status IS 'Estado: draft (borrador), confirmed (confirmado), done (aplicado), cancelled';
|
||||
|
||||
-- inventory_adjustment_lines
|
||||
COMMENT ON TABLE inventory.inventory_adjustment_lines IS 'Lineas de ajuste: cada una compara cantidad teorica vs contada para un producto en una ubicacion';
|
||||
COMMENT ON COLUMN inventory.inventory_adjustment_lines.theoretical_qty IS 'Cantidad segun el sistema antes del conteo';
|
||||
COMMENT ON COLUMN inventory.inventory_adjustment_lines.counted_qty IS 'Cantidad real contada fisicamente';
|
||||
COMMENT ON COLUMN inventory.inventory_adjustment_lines.difference_qty IS 'Diferencia calculada: counted_qty - theoretical_qty (columna generada)';
|
||||
@ -8,15 +8,15 @@
|
||||
-- =============================================================
|
||||
|
||||
-- =====================
|
||||
-- SCHEMA: billing
|
||||
-- SCHEMA: operations
|
||||
-- =====================
|
||||
CREATE SCHEMA IF NOT EXISTS billing;
|
||||
CREATE SCHEMA IF NOT EXISTS operations;
|
||||
|
||||
-- =====================
|
||||
-- TABLA: invoices
|
||||
-- Facturas de venta y compra
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS billing.invoices (
|
||||
CREATE TABLE IF NOT EXISTS operations.invoices (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
|
||||
@ -82,25 +82,25 @@ CREATE TABLE IF NOT EXISTS billing.invoices (
|
||||
);
|
||||
|
||||
-- Indices para invoices
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_tenant ON billing.invoices(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_number ON billing.invoices(invoice_number);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_type ON billing.invoices(invoice_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_partner ON billing.invoices(partner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_sales_order ON billing.invoices(sales_order_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_purchase_order ON billing.invoices(purchase_order_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_status ON billing.invoices(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_date ON billing.invoices(invoice_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_due_date ON billing.invoices(due_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_cfdi ON billing.invoices(cfdi_uuid);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_unpaid ON billing.invoices(status) WHERE status IN ('validated', 'sent', 'partial');
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_tenant ON operations.invoices(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_number ON operations.invoices(invoice_number);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_type ON operations.invoices(invoice_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_partner ON operations.invoices(partner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_sales_order ON operations.invoices(sales_order_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_purchase_order ON operations.invoices(purchase_order_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_status ON operations.invoices(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_date ON operations.invoices(invoice_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_due_date ON operations.invoices(due_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_cfdi ON operations.invoices(cfdi_uuid);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_unpaid ON operations.invoices(status) WHERE status IN ('validated', 'sent', 'partial');
|
||||
|
||||
-- =====================
|
||||
-- TABLA: invoice_items
|
||||
-- Lineas de factura
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS billing.invoice_items (
|
||||
CREATE TABLE IF NOT EXISTS operations.invoice_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
invoice_id UUID NOT NULL REFERENCES billing.invoices(id) ON DELETE CASCADE,
|
||||
invoice_id UUID NOT NULL REFERENCES operations.invoices(id) ON DELETE CASCADE,
|
||||
product_id UUID REFERENCES products.products(id) ON DELETE SET NULL,
|
||||
|
||||
-- Linea
|
||||
@ -140,15 +140,15 @@ CREATE TABLE IF NOT EXISTS billing.invoice_items (
|
||||
);
|
||||
|
||||
-- Indices para invoice_items
|
||||
CREATE INDEX IF NOT EXISTS idx_invoice_items_invoice ON billing.invoice_items(invoice_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoice_items_product ON billing.invoice_items(product_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoice_items_line ON billing.invoice_items(invoice_id, line_number);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoice_items_invoice ON operations.invoice_items(invoice_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoice_items_product ON operations.invoice_items(product_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoice_items_line ON operations.invoice_items(invoice_id, line_number);
|
||||
|
||||
-- =====================
|
||||
-- TABLA: payments
|
||||
-- Pagos recibidos y realizados
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS billing.payments (
|
||||
CREATE TABLE IF NOT EXISTS operations.payments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
|
||||
@ -195,22 +195,22 @@ CREATE TABLE IF NOT EXISTS billing.payments (
|
||||
);
|
||||
|
||||
-- Indices para payments
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_tenant ON billing.payments(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_number ON billing.payments(payment_number);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_type ON billing.payments(payment_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_partner ON billing.payments(partner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_status ON billing.payments(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_date ON billing.payments(payment_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_method ON billing.payments(payment_method);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_tenant ON operations.payments(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_number ON operations.payments(payment_number);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_type ON operations.payments(payment_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_partner ON operations.payments(partner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_status ON operations.payments(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_date ON operations.payments(payment_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_method ON operations.payments(payment_method);
|
||||
|
||||
-- =====================
|
||||
-- TABLA: payment_allocations
|
||||
-- Aplicacion de pagos a facturas
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS billing.payment_allocations (
|
||||
CREATE TABLE IF NOT EXISTS operations.payment_allocations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
payment_id UUID NOT NULL REFERENCES billing.payments(id) ON DELETE CASCADE,
|
||||
invoice_id UUID NOT NULL REFERENCES billing.invoices(id) ON DELETE CASCADE,
|
||||
payment_id UUID NOT NULL REFERENCES operations.payments(id) ON DELETE CASCADE,
|
||||
invoice_id UUID NOT NULL REFERENCES operations.invoices(id) ON DELETE CASCADE,
|
||||
|
||||
-- Monto aplicado
|
||||
amount DECIMAL(15, 2) NOT NULL,
|
||||
@ -226,25 +226,25 @@ CREATE TABLE IF NOT EXISTS billing.payment_allocations (
|
||||
);
|
||||
|
||||
-- Indices para payment_allocations
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_allocations_payment ON billing.payment_allocations(payment_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_allocations_invoice ON billing.payment_allocations(invoice_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_allocations_payment ON operations.payment_allocations(payment_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_allocations_invoice ON operations.payment_allocations(invoice_id);
|
||||
|
||||
-- =====================
|
||||
-- COMENTARIOS
|
||||
-- =====================
|
||||
COMMENT ON TABLE billing.invoices IS 'Facturas de venta y compra';
|
||||
COMMENT ON COLUMN billing.invoices.invoice_type IS 'Tipo: sale (venta), purchase (compra), credit_note (nota credito), debit_note (nota debito)';
|
||||
COMMENT ON COLUMN billing.invoices.status IS 'Estado: draft, validated, sent, partial (pago parcial), paid, cancelled, voided';
|
||||
COMMENT ON COLUMN billing.invoices.cfdi_uuid IS 'UUID del CFDI para facturacion electronica en Mexico';
|
||||
COMMENT ON COLUMN billing.invoices.amount_due IS 'Saldo pendiente de pago (calculado)';
|
||||
COMMENT ON TABLE operations.invoices IS 'Facturas de venta y compra';
|
||||
COMMENT ON COLUMN operations.invoices.invoice_type IS 'Tipo: sale (venta), purchase (compra), credit_note (nota credito), debit_note (nota debito)';
|
||||
COMMENT ON COLUMN operations.invoices.status IS 'Estado: draft, validated, sent, partial (pago parcial), paid, cancelled, voided';
|
||||
COMMENT ON COLUMN operations.invoices.cfdi_uuid IS 'UUID del CFDI para facturacion electronica en Mexico';
|
||||
COMMENT ON COLUMN operations.invoices.amount_due IS 'Saldo pendiente de pago (calculado)';
|
||||
|
||||
COMMENT ON TABLE billing.invoice_items IS 'Lineas de detalle de facturas';
|
||||
COMMENT ON COLUMN billing.invoice_items.sat_product_code IS 'Clave de producto del catalogo SAT (Mexico)';
|
||||
COMMENT ON COLUMN billing.invoice_items.sat_unit_code IS 'Clave de unidad del catalogo SAT (Mexico)';
|
||||
COMMENT ON TABLE operations.invoice_items IS 'Lineas de detalle de facturas';
|
||||
COMMENT ON COLUMN operations.invoice_items.sat_product_code IS 'Clave de producto del catalogo SAT (Mexico)';
|
||||
COMMENT ON COLUMN operations.invoice_items.sat_unit_code IS 'Clave de unidad del catalogo SAT (Mexico)';
|
||||
|
||||
COMMENT ON TABLE billing.payments IS 'Registro de pagos recibidos y realizados';
|
||||
COMMENT ON COLUMN billing.payments.payment_type IS 'Tipo: received (cobro a cliente), made (pago a proveedor)';
|
||||
COMMENT ON COLUMN billing.payments.status IS 'Estado: draft, confirmed, reconciled, cancelled';
|
||||
COMMENT ON TABLE operations.payments IS 'Registro de pagos recibidos y realizados';
|
||||
COMMENT ON COLUMN operations.payments.payment_type IS 'Tipo: received (cobro a cliente), made (pago a proveedor)';
|
||||
COMMENT ON COLUMN operations.payments.status IS 'Estado: draft, confirmed, reconciled, cancelled';
|
||||
|
||||
COMMENT ON TABLE billing.payment_allocations IS 'Aplicacion de pagos a facturas especificas';
|
||||
COMMENT ON COLUMN billing.payment_allocations.amount IS 'Monto del pago aplicado a esta factura';
|
||||
COMMENT ON TABLE operations.payment_allocations IS 'Aplicacion de pagos a facturas especificas';
|
||||
COMMENT ON COLUMN operations.payment_allocations.amount IS 'Monto del pago aplicado a esta factura';
|
||||
|
||||
460
ddl/27-cfdi-core.sql
Normal file
460
ddl/27-cfdi-core.sql
Normal file
@ -0,0 +1,460 @@
|
||||
-- =============================================================
|
||||
-- ARCHIVO: 27-cfdi-core.sql
|
||||
-- DESCRIPCION: Modulo CFDI - Facturacion electronica SAT Mexico
|
||||
-- Certificados CSD, tipos, extension de facturas
|
||||
-- VERSION: 1.0.0
|
||||
-- PROYECTO: ERP-Core V2
|
||||
-- FECHA: 2026-02-03
|
||||
-- DEPENDE DE: 01-auth-profiles.sql, 24-invoices.sql, 26-fiscal-catalogs.sql
|
||||
-- =============================================================
|
||||
|
||||
-- =====================
|
||||
-- NOTA: El schema fiscal ya existe en 26-fiscal-catalogs.sql
|
||||
-- Aqui solo agregamos las tablas especificas de CFDI
|
||||
-- =====================
|
||||
|
||||
-- =====================
|
||||
-- TIPOS ENUMERADOS CFDI
|
||||
-- =====================
|
||||
|
||||
-- Tipo de comprobante CFDI
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'cfdi_voucher_type') THEN
|
||||
CREATE TYPE fiscal.cfdi_voucher_type AS ENUM (
|
||||
'I', -- Ingreso
|
||||
'E', -- Egreso
|
||||
'T', -- Traslado
|
||||
'N', -- Nomina
|
||||
'P' -- Pago
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Estado del certificado CSD
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'csd_certificate_status') THEN
|
||||
CREATE TYPE fiscal.csd_certificate_status AS ENUM (
|
||||
'active', -- Activo y vigente
|
||||
'expired', -- Expirado
|
||||
'revoked', -- Revocado por el SAT
|
||||
'pending', -- Pendiente de activacion
|
||||
'inactive' -- Desactivado manualmente
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Estado del CFDI
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'cfdi_status') THEN
|
||||
CREATE TYPE fiscal.cfdi_status AS ENUM (
|
||||
'draft', -- Borrador, no timbrado
|
||||
'pending', -- Pendiente de timbrado
|
||||
'stamping', -- En proceso de timbrado
|
||||
'stamped', -- Timbrado exitosamente
|
||||
'delivered', -- Entregado al receptor
|
||||
'cancel_requested', -- Solicitud de cancelacion
|
||||
'cancelled', -- Cancelado
|
||||
'error' -- Error en timbrado
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Tipo de relacionCFDI
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'cfdi_relation_type') THEN
|
||||
CREATE TYPE fiscal.cfdi_relation_type AS ENUM (
|
||||
'01', -- Nota de credito de documentos relacionados
|
||||
'02', -- Nota de debito de documentos relacionados
|
||||
'03', -- Devolucion de mercancia
|
||||
'04', -- Sustitucion de CFDI previos
|
||||
'05', -- Traslados de mercancias facturadas
|
||||
'06', -- Factura generada por traslados previos
|
||||
'07', -- CFDI por aplicacion de anticipo
|
||||
'08', -- Factura generada por pagos en parcialidades
|
||||
'09' -- Factura generada por pagos diferidos
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- =====================
|
||||
-- TABLA: cfdi_certificates
|
||||
-- Certificados de Sello Digital (CSD) del SAT
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS fiscal.cfdi_certificates (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
|
||||
-- Identificacion del certificado
|
||||
certificate_number VARCHAR(20) NOT NULL, -- Numero de certificado SAT (20 digitos)
|
||||
serial_number VARCHAR(50), -- Numero de serie del certificado
|
||||
|
||||
-- RFC asociado
|
||||
rfc VARCHAR(13) NOT NULL, -- RFC del contribuyente
|
||||
|
||||
-- Archivos del certificado (almacenados encriptados)
|
||||
certificate_pem TEXT NOT NULL, -- Certificado .cer en formato PEM
|
||||
private_key_pem_encrypted TEXT NOT NULL, -- Llave privada .key encriptada
|
||||
|
||||
-- Metadatos del certificado
|
||||
issued_at TIMESTAMPTZ NOT NULL, -- Fecha de emision
|
||||
expires_at TIMESTAMPTZ NOT NULL, -- Fecha de expiracion
|
||||
|
||||
-- Estado y configuracion
|
||||
status fiscal.csd_certificate_status NOT NULL DEFAULT 'pending',
|
||||
is_default BOOLEAN DEFAULT FALSE, -- Certificado por defecto para este tenant
|
||||
|
||||
-- Informacion adicional
|
||||
issuer_name VARCHAR(500), -- Nombre del emisor (SAT)
|
||||
subject_name VARCHAR(500), -- Nombre del sujeto (contribuyente)
|
||||
|
||||
-- Notas
|
||||
description VARCHAR(255),
|
||||
notes TEXT,
|
||||
|
||||
-- Auditoria
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by UUID REFERENCES auth.users(id),
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_by UUID REFERENCES auth.users(id),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
|
||||
-- Solo un certificado activo por defecto por tenant
|
||||
UNIQUE(tenant_id, certificate_number),
|
||||
CONSTRAINT chk_only_one_default EXCLUDE USING btree (tenant_id WITH =)
|
||||
WHERE (is_default = TRUE AND deleted_at IS NULL AND status = 'active')
|
||||
);
|
||||
|
||||
COMMENT ON TABLE fiscal.cfdi_certificates IS 'Certificados de Sello Digital (CSD) para timbrado CFDI';
|
||||
COMMENT ON COLUMN fiscal.cfdi_certificates.certificate_number IS 'Numero de certificado emitido por el SAT (20 digitos)';
|
||||
COMMENT ON COLUMN fiscal.cfdi_certificates.certificate_pem IS 'Certificado .cer convertido a formato PEM';
|
||||
COMMENT ON COLUMN fiscal.cfdi_certificates.private_key_pem_encrypted IS 'Llave privada .key encriptada con AES-256';
|
||||
COMMENT ON COLUMN fiscal.cfdi_certificates.is_default IS 'Indica si es el certificado predeterminado para facturacion';
|
||||
|
||||
-- =====================
|
||||
-- TABLA: cfdi_pac_configurations
|
||||
-- Configuracion de Proveedores Autorizados de Certificacion (PAC)
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS fiscal.cfdi_pac_configurations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
|
||||
-- Identificacion del PAC
|
||||
pac_code VARCHAR(20) NOT NULL, -- Codigo del PAC (finkok, sat, etc.)
|
||||
pac_name VARCHAR(100) NOT NULL, -- Nombre comercial del PAC
|
||||
|
||||
-- Credenciales (encriptadas)
|
||||
username VARCHAR(255), -- Usuario del PAC
|
||||
password_encrypted TEXT, -- Contrasena encriptada
|
||||
api_key_encrypted TEXT, -- API Key encriptada (si aplica)
|
||||
|
||||
-- Endpoints
|
||||
production_url VARCHAR(500), -- URL produccion
|
||||
sandbox_url VARCHAR(500), -- URL sandbox/pruebas
|
||||
|
||||
-- Configuracion
|
||||
environment VARCHAR(20) DEFAULT 'sandbox', -- sandbox, production
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
is_default BOOLEAN DEFAULT FALSE,
|
||||
|
||||
-- Contrato
|
||||
contract_number VARCHAR(50),
|
||||
contract_expires_at DATE,
|
||||
|
||||
-- Limites
|
||||
monthly_stamp_limit INTEGER, -- Limite mensual de timbres
|
||||
stamps_used_this_month INTEGER DEFAULT 0,
|
||||
|
||||
-- Auditoria
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by UUID REFERENCES auth.users(id),
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_by UUID REFERENCES auth.users(id),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
|
||||
UNIQUE(tenant_id, pac_code)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE fiscal.cfdi_pac_configurations IS 'Configuracion de conexion con Proveedores Autorizados de Certificacion (PAC)';
|
||||
COMMENT ON COLUMN fiscal.cfdi_pac_configurations.pac_code IS 'Codigo identificador del PAC: finkok, sat, facturapi, etc.';
|
||||
COMMENT ON COLUMN fiscal.cfdi_pac_configurations.environment IS 'Ambiente: sandbox para pruebas, production para produccion';
|
||||
|
||||
-- =====================
|
||||
-- TABLA: cfdi_invoices
|
||||
-- Extension de facturas con datos CFDI completos
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS fiscal.cfdi_invoices (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
|
||||
-- Relacion con factura operacional
|
||||
invoice_id UUID NOT NULL REFERENCES billing.invoices(id) ON DELETE CASCADE,
|
||||
|
||||
-- Identificadores CFDI
|
||||
uuid VARCHAR(36), -- UUID del timbre fiscal (folio fiscal)
|
||||
serie VARCHAR(25), -- Serie del comprobante
|
||||
folio VARCHAR(40), -- Folio del comprobante
|
||||
|
||||
-- Tipo de comprobante
|
||||
voucher_type fiscal.cfdi_voucher_type NOT NULL DEFAULT 'I',
|
||||
|
||||
-- Certificado usado
|
||||
certificate_id UUID REFERENCES fiscal.cfdi_certificates(id),
|
||||
certificate_number VARCHAR(20), -- Numero de certificado usado
|
||||
|
||||
-- Estado del CFDI
|
||||
status fiscal.cfdi_status NOT NULL DEFAULT 'draft',
|
||||
|
||||
-- Datos del emisor
|
||||
issuer_rfc VARCHAR(13) NOT NULL,
|
||||
issuer_name VARCHAR(300) NOT NULL,
|
||||
issuer_fiscal_regime VARCHAR(10) NOT NULL, -- Regimen fiscal del emisor
|
||||
|
||||
-- Datos del receptor
|
||||
receiver_rfc VARCHAR(13) NOT NULL,
|
||||
receiver_name VARCHAR(300) NOT NULL,
|
||||
receiver_fiscal_regime VARCHAR(10), -- Regimen fiscal del receptor (CFDI 4.0)
|
||||
receiver_tax_residence VARCHAR(3), -- Residencia fiscal extranjero
|
||||
receiver_tax_id VARCHAR(40), -- NumRegIdTrib para extranjeros
|
||||
cfdi_use VARCHAR(10) NOT NULL, -- Uso del CFDI (G01, G02, etc.)
|
||||
receiver_zip_code VARCHAR(5), -- Codigo postal receptor (CFDI 4.0)
|
||||
|
||||
-- Lugar de expedicion
|
||||
expedition_place VARCHAR(5) NOT NULL, -- Codigo postal lugar expedicion
|
||||
|
||||
-- Metodo y forma de pago
|
||||
payment_method VARCHAR(10), -- PUE, PPD
|
||||
payment_form VARCHAR(10), -- 01, 02, 03, etc.
|
||||
payment_conditions VARCHAR(1000),
|
||||
|
||||
-- Moneda y tipo de cambio
|
||||
currency VARCHAR(3) NOT NULL DEFAULT 'MXN',
|
||||
exchange_rate DECIMAL(10, 6) DEFAULT 1,
|
||||
|
||||
-- Exportacion
|
||||
exportation VARCHAR(2) DEFAULT '01', -- 01=No aplica, 02=Definitiva, etc.
|
||||
|
||||
-- Totales
|
||||
subtotal DECIMAL(18, 6) NOT NULL DEFAULT 0,
|
||||
discount DECIMAL(18, 6) DEFAULT 0,
|
||||
total DECIMAL(18, 6) NOT NULL DEFAULT 0,
|
||||
|
||||
-- Impuestos trasladados (resumen)
|
||||
total_transferred_taxes DECIMAL(18, 6) DEFAULT 0,
|
||||
-- Impuestos retenidos (resumen)
|
||||
total_withheld_taxes DECIMAL(18, 6) DEFAULT 0,
|
||||
|
||||
-- CFDI relacionados
|
||||
related_cfdi_type fiscal.cfdi_relation_type,
|
||||
|
||||
-- Confirmacion SAT (para montos grandes)
|
||||
confirmation_code VARCHAR(17),
|
||||
|
||||
-- Informacion global (para publico en general)
|
||||
global_info_periodicity VARCHAR(10), -- 01=Diario, 02=Semanal, etc.
|
||||
global_info_months VARCHAR(10), -- 01-12
|
||||
global_info_year VARCHAR(4),
|
||||
|
||||
-- XML y timbrado
|
||||
xml_original TEXT, -- XML antes de timbrar
|
||||
xml_stamped TEXT, -- XML timbrado
|
||||
stamp_date TIMESTAMPTZ, -- Fecha de timbrado
|
||||
stamp_sat_seal TEXT, -- Sello del SAT
|
||||
stamp_cfdi_seal TEXT, -- Sello del CFDI
|
||||
stamp_original_chain TEXT, -- Cadena original del timbre
|
||||
sat_certificate_number VARCHAR(20), -- Numero certificado SAT
|
||||
|
||||
-- PDF
|
||||
pdf_url VARCHAR(500),
|
||||
pdf_generated_at TIMESTAMPTZ,
|
||||
|
||||
-- Cancelacion
|
||||
cancellation_status VARCHAR(50),
|
||||
cancellation_date TIMESTAMPTZ,
|
||||
cancellation_ack_xml TEXT,
|
||||
|
||||
-- Validacion SAT
|
||||
last_sat_validation TIMESTAMPTZ,
|
||||
sat_validation_status VARCHAR(50),
|
||||
sat_validation_response JSONB,
|
||||
|
||||
-- Errores
|
||||
last_error TEXT,
|
||||
error_details JSONB,
|
||||
|
||||
-- Auditoria
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by UUID REFERENCES auth.users(id),
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_by UUID REFERENCES auth.users(id),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
|
||||
UNIQUE(tenant_id, uuid),
|
||||
UNIQUE(invoice_id)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE fiscal.cfdi_invoices IS 'Extension de facturas con datos completos de CFDI 4.0';
|
||||
COMMENT ON COLUMN fiscal.cfdi_invoices.uuid IS 'UUID del timbre fiscal digital (folio fiscal unico del SAT)';
|
||||
COMMENT ON COLUMN fiscal.cfdi_invoices.voucher_type IS 'Tipo de comprobante: I=Ingreso, E=Egreso, T=Traslado, N=Nomina, P=Pago';
|
||||
COMMENT ON COLUMN fiscal.cfdi_invoices.cfdi_use IS 'Uso del CFDI segun catalogo SAT c_UsoCFDI';
|
||||
COMMENT ON COLUMN fiscal.cfdi_invoices.payment_method IS 'Metodo de pago: PUE=Pago en Una Exhibicion, PPD=Pago en Parcialidades o Diferido';
|
||||
COMMENT ON COLUMN fiscal.cfdi_invoices.exportation IS 'Tipo de exportacion: 01=No aplica, 02=Definitiva, 03=Temporal, 04=Definitiva con clave';
|
||||
|
||||
-- =====================
|
||||
-- TABLA: cfdi_invoice_items
|
||||
-- Conceptos/lineas del CFDI con datos fiscales completos
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS fiscal.cfdi_invoice_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
cfdi_invoice_id UUID NOT NULL REFERENCES fiscal.cfdi_invoices(id) ON DELETE CASCADE,
|
||||
|
||||
-- Referencia a linea de factura original
|
||||
invoice_item_id UUID, -- FK a billing.invoice_items si existe
|
||||
|
||||
-- Datos del concepto
|
||||
line_number INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
-- Claves SAT
|
||||
product_service_key VARCHAR(10) NOT NULL, -- Clave producto/servicio SAT
|
||||
product_service_name VARCHAR(1000), -- Descripcion del catalogo SAT
|
||||
unit_key VARCHAR(10) NOT NULL, -- Clave unidad SAT
|
||||
unit_name VARCHAR(50), -- Nombre unidad
|
||||
|
||||
-- Identificacion del producto
|
||||
sku VARCHAR(100), -- NoIdentificacion
|
||||
description VARCHAR(1000) NOT NULL, -- Descripcion
|
||||
|
||||
-- Cantidades y montos
|
||||
quantity DECIMAL(18, 6) NOT NULL,
|
||||
unit_price DECIMAL(18, 6) NOT NULL,
|
||||
subtotal DECIMAL(18, 6) NOT NULL, -- Importe = quantity * unit_price
|
||||
discount DECIMAL(18, 6) DEFAULT 0,
|
||||
|
||||
-- Objeto de impuesto
|
||||
tax_object VARCHAR(2) NOT NULL DEFAULT '02', -- 01=No objeto, 02=Si objeto, 03=Si objeto no desglosado
|
||||
|
||||
-- Cuenta predial (para inmuebles)
|
||||
property_tax_account VARCHAR(150),
|
||||
|
||||
-- Informacion aduanera
|
||||
customs_info JSONB, -- Array de {NumeroPedimento, FechaPedimento, Aduana}
|
||||
|
||||
-- Parte (para componentes)
|
||||
parts JSONB, -- Array de partes si aplica
|
||||
|
||||
-- Auditoria
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
COMMENT ON TABLE fiscal.cfdi_invoice_items IS 'Conceptos del CFDI con claves SAT y desglose fiscal';
|
||||
COMMENT ON COLUMN fiscal.cfdi_invoice_items.product_service_key IS 'Clave de producto/servicio del catalogo SAT c_ClaveProdServ';
|
||||
COMMENT ON COLUMN fiscal.cfdi_invoice_items.unit_key IS 'Clave de unidad del catalogo SAT c_ClaveUnidad';
|
||||
COMMENT ON COLUMN fiscal.cfdi_invoice_items.tax_object IS 'ObjetoImp: 01=No objeto de impuesto, 02=Si objeto, 03=Si objeto no desglosado';
|
||||
|
||||
-- =====================
|
||||
-- TABLA: cfdi_invoice_item_taxes
|
||||
-- Impuestos por concepto del CFDI
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS fiscal.cfdi_invoice_item_taxes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
cfdi_invoice_item_id UUID NOT NULL REFERENCES fiscal.cfdi_invoice_items(id) ON DELETE CASCADE,
|
||||
|
||||
-- Tipo de impuesto
|
||||
tax_type VARCHAR(20) NOT NULL, -- transferred (traslado) o withheld (retencion)
|
||||
|
||||
-- Impuesto
|
||||
tax_code VARCHAR(10) NOT NULL, -- 001=ISR, 002=IVA, 003=IEPS
|
||||
tax_name VARCHAR(50),
|
||||
|
||||
-- Factor
|
||||
factor_type VARCHAR(10) NOT NULL, -- Tasa, Cuota, Exento
|
||||
rate DECIMAL(10, 6), -- Tasa o cuota
|
||||
|
||||
-- Montos
|
||||
base_amount DECIMAL(18, 6) NOT NULL, -- Base del impuesto
|
||||
tax_amount DECIMAL(18, 6) NOT NULL, -- Importe del impuesto
|
||||
|
||||
-- Auditoria
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
COMMENT ON TABLE fiscal.cfdi_invoice_item_taxes IS 'Impuestos trasladados y retenidos por concepto del CFDI';
|
||||
COMMENT ON COLUMN fiscal.cfdi_invoice_item_taxes.tax_type IS 'transferred=Impuesto trasladado, withheld=Impuesto retenido';
|
||||
COMMENT ON COLUMN fiscal.cfdi_invoice_item_taxes.tax_code IS 'Codigo SAT: 001=ISR, 002=IVA, 003=IEPS';
|
||||
COMMENT ON COLUMN fiscal.cfdi_invoice_item_taxes.factor_type IS 'Tipo de factor: Tasa, Cuota, o Exento';
|
||||
|
||||
-- =====================
|
||||
-- TABLA: cfdi_related_documents
|
||||
-- CFDI relacionados
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS fiscal.cfdi_related_documents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
cfdi_invoice_id UUID NOT NULL REFERENCES fiscal.cfdi_invoices(id) ON DELETE CASCADE,
|
||||
|
||||
-- UUID del CFDI relacionado
|
||||
related_uuid VARCHAR(36) NOT NULL,
|
||||
|
||||
-- Tipo de relacion
|
||||
relation_type fiscal.cfdi_relation_type NOT NULL,
|
||||
|
||||
-- Referencia interna (si existe en nuestro sistema)
|
||||
related_cfdi_invoice_id UUID REFERENCES fiscal.cfdi_invoices(id),
|
||||
|
||||
-- Auditoria
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
COMMENT ON TABLE fiscal.cfdi_related_documents IS 'Relaciones entre CFDIs (notas de credito, sustituciones, etc.)';
|
||||
COMMENT ON COLUMN fiscal.cfdi_related_documents.relation_type IS 'Tipo de relacion segun catalogo SAT c_TipoRelacion';
|
||||
|
||||
-- =====================
|
||||
-- INDICES CFDI CORE
|
||||
-- =====================
|
||||
|
||||
-- cfdi_certificates
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_certificates_tenant ON fiscal.cfdi_certificates(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_certificates_rfc ON fiscal.cfdi_certificates(rfc);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_certificates_status ON fiscal.cfdi_certificates(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_certificates_expires ON fiscal.cfdi_certificates(expires_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_certificates_default ON fiscal.cfdi_certificates(tenant_id, is_default)
|
||||
WHERE is_default = TRUE AND deleted_at IS NULL;
|
||||
|
||||
-- cfdi_pac_configurations
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_pac_tenant ON fiscal.cfdi_pac_configurations(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_pac_code ON fiscal.cfdi_pac_configurations(pac_code);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_pac_active ON fiscal.cfdi_pac_configurations(tenant_id, is_active)
|
||||
WHERE is_active = TRUE;
|
||||
|
||||
-- cfdi_invoices
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_invoices_tenant ON fiscal.cfdi_invoices(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_invoices_invoice ON fiscal.cfdi_invoices(invoice_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_invoices_uuid ON fiscal.cfdi_invoices(uuid);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_invoices_serie_folio ON fiscal.cfdi_invoices(serie, folio);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_invoices_status ON fiscal.cfdi_invoices(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_invoices_issuer ON fiscal.cfdi_invoices(issuer_rfc);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_invoices_receiver ON fiscal.cfdi_invoices(receiver_rfc);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_invoices_stamp_date ON fiscal.cfdi_invoices(stamp_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_invoices_voucher_type ON fiscal.cfdi_invoices(voucher_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_invoices_pending ON fiscal.cfdi_invoices(tenant_id, status)
|
||||
WHERE status IN ('draft', 'pending', 'stamping');
|
||||
|
||||
-- cfdi_invoice_items
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_items_cfdi ON fiscal.cfdi_invoice_items(cfdi_invoice_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_items_product_key ON fiscal.cfdi_invoice_items(product_service_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_items_line ON fiscal.cfdi_invoice_items(cfdi_invoice_id, line_number);
|
||||
|
||||
-- cfdi_invoice_item_taxes
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_item_taxes_item ON fiscal.cfdi_invoice_item_taxes(cfdi_invoice_item_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_item_taxes_code ON fiscal.cfdi_invoice_item_taxes(tax_code);
|
||||
|
||||
-- cfdi_related_documents
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_related_cfdi ON fiscal.cfdi_related_documents(cfdi_invoice_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_related_uuid ON fiscal.cfdi_related_documents(related_uuid);
|
||||
|
||||
-- =====================
|
||||
-- FIN DEL ARCHIVO
|
||||
-- =====================
|
||||
442
ddl/28-cfdi-operations.sql
Normal file
442
ddl/28-cfdi-operations.sql
Normal file
@ -0,0 +1,442 @@
|
||||
-- =============================================================
|
||||
-- ARCHIVO: 28-cfdi-operations.sql
|
||||
-- DESCRIPCION: Modulo CFDI - Operaciones: cancelaciones, logs,
|
||||
-- complementos de pago
|
||||
-- VERSION: 1.0.0
|
||||
-- PROYECTO: ERP-Core V2
|
||||
-- FECHA: 2026-02-03
|
||||
-- DEPENDE DE: 27-cfdi-core.sql
|
||||
-- =============================================================
|
||||
|
||||
-- =====================
|
||||
-- TIPOS ENUMERADOS OPERACIONES
|
||||
-- =====================
|
||||
|
||||
-- Motivo de cancelacion CFDI
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'cfdi_cancellation_reason') THEN
|
||||
CREATE TYPE fiscal.cfdi_cancellation_reason AS ENUM (
|
||||
'01', -- Comprobante emitido con errores con relacion
|
||||
'02', -- Comprobante emitido con errores sin relacion
|
||||
'03', -- No se llevo a cabo la operacion
|
||||
'04' -- Operacion nominativa relacionada en factura global
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Estado de solicitud de cancelacion
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'cancellation_request_status') THEN
|
||||
CREATE TYPE fiscal.cancellation_request_status AS ENUM (
|
||||
'pending', -- Pendiente de enviar
|
||||
'submitted', -- Enviada al SAT
|
||||
'accepted', -- Aceptada por el receptor
|
||||
'rejected', -- Rechazada por el receptor
|
||||
'in_process', -- En proceso (plazo de aceptacion)
|
||||
'expired', -- Plazo expirado (cancelada automaticamente)
|
||||
'cancelled', -- Cancelacion exitosa
|
||||
'error' -- Error en el proceso
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Tipo de operacion en log
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'cfdi_operation_type') THEN
|
||||
CREATE TYPE fiscal.cfdi_operation_type AS ENUM (
|
||||
'create', -- Creacion de CFDI
|
||||
'stamp', -- Timbrado
|
||||
'stamp_retry', -- Reintento de timbrado
|
||||
'cancel_request', -- Solicitud de cancelacion
|
||||
'cancel_accept', -- Aceptacion de cancelacion
|
||||
'cancel_reject', -- Rechazo de cancelacion
|
||||
'cancel_complete', -- Cancelacion completada
|
||||
'validate', -- Validacion en SAT
|
||||
'download', -- Descarga de XML/PDF
|
||||
'email', -- Envio por email
|
||||
'error' -- Error en operacion
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Estado del complemento de pago
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'payment_complement_status') THEN
|
||||
CREATE TYPE fiscal.payment_complement_status AS ENUM (
|
||||
'draft', -- Borrador
|
||||
'pending', -- Pendiente de timbrar
|
||||
'stamped', -- Timbrado exitosamente
|
||||
'cancelled', -- Cancelado
|
||||
'error' -- Error
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- =====================
|
||||
-- TABLA: cfdi_cancellation_requests
|
||||
-- Solicitudes de cancelacion de CFDI
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS fiscal.cfdi_cancellation_requests (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
|
||||
-- CFDI a cancelar
|
||||
cfdi_invoice_id UUID NOT NULL REFERENCES fiscal.cfdi_invoices(id) ON DELETE CASCADE,
|
||||
cfdi_uuid VARCHAR(36) NOT NULL, -- UUID del CFDI a cancelar
|
||||
|
||||
-- Motivo de cancelacion
|
||||
cancellation_reason fiscal.cfdi_cancellation_reason NOT NULL,
|
||||
|
||||
-- CFDI sustituto (para motivo 01)
|
||||
substitute_uuid VARCHAR(36), -- UUID del CFDI que sustituye
|
||||
substitute_cfdi_id UUID REFERENCES fiscal.cfdi_invoices(id),
|
||||
|
||||
-- Estado de la solicitud
|
||||
status fiscal.cancellation_request_status NOT NULL DEFAULT 'pending',
|
||||
|
||||
-- Fechas del proceso
|
||||
requested_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
submitted_at TIMESTAMPTZ, -- Fecha de envio al SAT
|
||||
response_at TIMESTAMPTZ, -- Fecha de respuesta
|
||||
expires_at TIMESTAMPTZ, -- Fecha limite para aceptacion (72h)
|
||||
completed_at TIMESTAMPTZ, -- Fecha de cancelacion efectiva
|
||||
|
||||
-- Respuesta del SAT
|
||||
sat_response_code VARCHAR(10),
|
||||
sat_response_message TEXT,
|
||||
sat_ack_xml TEXT, -- Acuse de cancelacion
|
||||
|
||||
-- Respuesta del receptor (si aplica)
|
||||
receiver_response VARCHAR(20), -- accepted, rejected
|
||||
receiver_response_at TIMESTAMPTZ,
|
||||
receiver_response_reason TEXT,
|
||||
|
||||
-- Notas
|
||||
reason_notes TEXT, -- Notas adicionales del motivo
|
||||
internal_notes TEXT,
|
||||
|
||||
-- Errores
|
||||
last_error TEXT,
|
||||
error_details JSONB,
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
|
||||
-- Auditoria
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by UUID REFERENCES auth.users(id),
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_by UUID REFERENCES auth.users(id),
|
||||
|
||||
UNIQUE(cfdi_invoice_id)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE fiscal.cfdi_cancellation_requests IS 'Solicitudes de cancelacion de CFDI segun proceso SAT 2022+';
|
||||
COMMENT ON COLUMN fiscal.cfdi_cancellation_requests.cancellation_reason IS 'Motivo: 01=Con relacion, 02=Sin relacion, 03=No se realizo operacion, 04=Nominativa global';
|
||||
COMMENT ON COLUMN fiscal.cfdi_cancellation_requests.expires_at IS 'Fecha limite para que el receptor acepte/rechace (72 horas habiles)';
|
||||
|
||||
-- =====================
|
||||
-- TABLA: cfdi_operation_logs
|
||||
-- Audit trail de todas las operaciones CFDI
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS fiscal.cfdi_operation_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
|
||||
-- Referencia al CFDI
|
||||
cfdi_invoice_id UUID REFERENCES fiscal.cfdi_invoices(id) ON DELETE SET NULL,
|
||||
cfdi_uuid VARCHAR(36), -- Guardamos UUID por si se elimina el registro
|
||||
|
||||
-- Tipo de operacion
|
||||
operation_type fiscal.cfdi_operation_type NOT NULL,
|
||||
|
||||
-- Estado antes y despues
|
||||
status_before VARCHAR(50),
|
||||
status_after VARCHAR(50),
|
||||
|
||||
-- Resultado
|
||||
success BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
-- Detalles de la operacion
|
||||
request_payload JSONB, -- Datos enviados
|
||||
response_payload JSONB, -- Respuesta recibida
|
||||
|
||||
-- Error (si aplica)
|
||||
error_code VARCHAR(50),
|
||||
error_message TEXT,
|
||||
error_details JSONB,
|
||||
|
||||
-- PAC utilizado
|
||||
pac_code VARCHAR(20),
|
||||
pac_transaction_id VARCHAR(100),
|
||||
|
||||
-- Tiempo de respuesta
|
||||
duration_ms INTEGER, -- Duracion en milisegundos
|
||||
|
||||
-- IP y user agent (para auditorias)
|
||||
ip_address INET,
|
||||
user_agent VARCHAR(500),
|
||||
|
||||
-- Auditoria
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by UUID REFERENCES auth.users(id)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE fiscal.cfdi_operation_logs IS 'Log de todas las operaciones realizadas sobre CFDIs';
|
||||
COMMENT ON COLUMN fiscal.cfdi_operation_logs.operation_type IS 'Tipo de operacion: stamp, cancel_request, validate, etc.';
|
||||
COMMENT ON COLUMN fiscal.cfdi_operation_logs.duration_ms IS 'Tiempo de respuesta del PAC/SAT en milisegundos';
|
||||
|
||||
-- =====================
|
||||
-- TABLA: cfdi_payment_complements
|
||||
-- Complementos de pago (CFDI tipo P - REP)
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS fiscal.cfdi_payment_complements (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
|
||||
-- Pago asociado
|
||||
payment_id UUID REFERENCES billing.payments(id) ON DELETE SET NULL,
|
||||
|
||||
-- Identificadores CFDI
|
||||
uuid VARCHAR(36), -- UUID del complemento de pago
|
||||
serie VARCHAR(25),
|
||||
folio VARCHAR(40),
|
||||
|
||||
-- Estado
|
||||
status fiscal.payment_complement_status NOT NULL DEFAULT 'draft',
|
||||
|
||||
-- Certificado usado
|
||||
certificate_id UUID REFERENCES fiscal.cfdi_certificates(id),
|
||||
certificate_number VARCHAR(20),
|
||||
|
||||
-- Datos del emisor
|
||||
issuer_rfc VARCHAR(13) NOT NULL,
|
||||
issuer_name VARCHAR(300) NOT NULL,
|
||||
issuer_fiscal_regime VARCHAR(10) NOT NULL,
|
||||
|
||||
-- Datos del receptor
|
||||
receiver_rfc VARCHAR(13) NOT NULL,
|
||||
receiver_name VARCHAR(300) NOT NULL,
|
||||
receiver_fiscal_regime VARCHAR(10),
|
||||
receiver_zip_code VARCHAR(5),
|
||||
|
||||
-- Lugar de expedicion
|
||||
expedition_place VARCHAR(5) NOT NULL,
|
||||
|
||||
-- Datos del pago
|
||||
payment_date DATE NOT NULL,
|
||||
payment_form VARCHAR(10) NOT NULL, -- Forma de pago SAT
|
||||
currency VARCHAR(3) NOT NULL DEFAULT 'MXN',
|
||||
exchange_rate DECIMAL(10, 6) DEFAULT 1,
|
||||
total_amount DECIMAL(18, 6) NOT NULL,
|
||||
|
||||
-- Datos bancarios (si aplica)
|
||||
payer_bank_rfc VARCHAR(13),
|
||||
payer_bank_name VARCHAR(300),
|
||||
payer_bank_account VARCHAR(50),
|
||||
payee_bank_rfc VARCHAR(13),
|
||||
payee_bank_account VARCHAR(50),
|
||||
|
||||
-- Cadena de pago (para transferencias)
|
||||
payment_chain TEXT,
|
||||
payment_certificate TEXT,
|
||||
payment_stamp TEXT,
|
||||
|
||||
-- Operacion (efectivo, cheque, etc.)
|
||||
operation_number VARCHAR(100),
|
||||
|
||||
-- XML y timbrado
|
||||
xml_original TEXT,
|
||||
xml_stamped TEXT,
|
||||
stamp_date TIMESTAMPTZ,
|
||||
stamp_sat_seal TEXT,
|
||||
stamp_cfdi_seal TEXT,
|
||||
stamp_original_chain TEXT,
|
||||
sat_certificate_number VARCHAR(20),
|
||||
|
||||
-- PDF
|
||||
pdf_url VARCHAR(500),
|
||||
pdf_generated_at TIMESTAMPTZ,
|
||||
|
||||
-- Cancelacion
|
||||
cancellation_request_id UUID REFERENCES fiscal.cfdi_cancellation_requests(id),
|
||||
cancellation_date TIMESTAMPTZ,
|
||||
|
||||
-- Errores
|
||||
last_error TEXT,
|
||||
error_details JSONB,
|
||||
|
||||
-- Auditoria
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by UUID REFERENCES auth.users(id),
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_by UUID REFERENCES auth.users(id),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
|
||||
UNIQUE(tenant_id, uuid)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE fiscal.cfdi_payment_complements IS 'Complementos de pago (CFDI tipo P - Recibo Electronico de Pago)';
|
||||
COMMENT ON COLUMN fiscal.cfdi_payment_complements.uuid IS 'UUID del timbre fiscal del complemento de pago';
|
||||
COMMENT ON COLUMN fiscal.cfdi_payment_complements.payment_form IS 'Forma de pago segun catalogo SAT c_FormaPago';
|
||||
|
||||
-- =====================
|
||||
-- TABLA: cfdi_payment_complement_documents
|
||||
-- Documentos relacionados en el complemento de pago
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS fiscal.cfdi_payment_complement_documents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
payment_complement_id UUID NOT NULL REFERENCES fiscal.cfdi_payment_complements(id) ON DELETE CASCADE,
|
||||
|
||||
-- CFDI del documento relacionado
|
||||
related_uuid VARCHAR(36) NOT NULL, -- IdDocumento - UUID de la factura pagada
|
||||
related_cfdi_id UUID REFERENCES fiscal.cfdi_invoices(id),
|
||||
|
||||
-- Serie y folio del documento
|
||||
serie VARCHAR(25),
|
||||
folio VARCHAR(40),
|
||||
|
||||
-- Moneda del documento
|
||||
document_currency VARCHAR(3) NOT NULL DEFAULT 'MXN',
|
||||
equivalence DECIMAL(18, 6) DEFAULT 1, -- Equivalencia DR (tipo cambio)
|
||||
|
||||
-- Numero de parcialidad
|
||||
installment_number INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
-- Saldos
|
||||
previous_balance DECIMAL(18, 6) NOT NULL, -- ImpSaldoAnt
|
||||
amount_paid DECIMAL(18, 6) NOT NULL, -- ImpPagado
|
||||
remaining_balance DECIMAL(18, 6) NOT NULL, -- ImpSaldoInsoluto
|
||||
|
||||
-- Objeto de impuesto
|
||||
tax_object VARCHAR(2) DEFAULT '02', -- ObjetoImpDR
|
||||
|
||||
-- Auditoria
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
COMMENT ON TABLE fiscal.cfdi_payment_complement_documents IS 'Documentos (facturas) incluidos en el complemento de pago';
|
||||
COMMENT ON COLUMN fiscal.cfdi_payment_complement_documents.related_uuid IS 'UUID de la factura que se esta pagando';
|
||||
COMMENT ON COLUMN fiscal.cfdi_payment_complement_documents.installment_number IS 'Numero de parcialidad (1, 2, 3...)';
|
||||
COMMENT ON COLUMN fiscal.cfdi_payment_complement_documents.previous_balance IS 'Saldo anterior antes de este pago';
|
||||
COMMENT ON COLUMN fiscal.cfdi_payment_complement_documents.remaining_balance IS 'Saldo pendiente despues de este pago';
|
||||
|
||||
-- =====================
|
||||
-- TABLA: cfdi_payment_complement_document_taxes
|
||||
-- Impuestos por documento en complemento de pago
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS fiscal.cfdi_payment_complement_document_taxes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
payment_complement_document_id UUID NOT NULL REFERENCES fiscal.cfdi_payment_complement_documents(id) ON DELETE CASCADE,
|
||||
|
||||
-- Tipo de impuesto
|
||||
tax_type VARCHAR(20) NOT NULL, -- transferred o withheld
|
||||
|
||||
-- Impuesto
|
||||
tax_code VARCHAR(10) NOT NULL, -- 001=ISR, 002=IVA, 003=IEPS
|
||||
|
||||
-- Factor
|
||||
factor_type VARCHAR(10) NOT NULL, -- Tasa, Cuota, Exento
|
||||
rate DECIMAL(10, 6),
|
||||
|
||||
-- Montos
|
||||
base_amount DECIMAL(18, 6) NOT NULL,
|
||||
tax_amount DECIMAL(18, 6) NOT NULL,
|
||||
|
||||
-- Auditoria
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
COMMENT ON TABLE fiscal.cfdi_payment_complement_document_taxes IS 'Impuestos por documento relacionado en el complemento de pago';
|
||||
|
||||
-- =====================
|
||||
-- TABLA: cfdi_stamp_queue
|
||||
-- Cola de timbrado para procesamiento asincrono
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS fiscal.cfdi_stamp_queue (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
|
||||
-- Tipo de documento a timbrar
|
||||
document_type VARCHAR(20) NOT NULL, -- invoice, payment_complement
|
||||
document_id UUID NOT NULL, -- ID del documento
|
||||
|
||||
-- Prioridad
|
||||
priority INTEGER DEFAULT 5, -- 1=urgente, 5=normal, 10=baja
|
||||
|
||||
-- Estado de la cola
|
||||
queue_status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, processing, completed, failed
|
||||
|
||||
-- Intentos
|
||||
attempts INTEGER DEFAULT 0,
|
||||
max_attempts INTEGER DEFAULT 3,
|
||||
next_retry_at TIMESTAMPTZ,
|
||||
|
||||
-- Resultado
|
||||
completed_at TIMESTAMPTZ,
|
||||
result_cfdi_uuid VARCHAR(36),
|
||||
result_error TEXT,
|
||||
|
||||
-- Auditoria
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by UUID REFERENCES auth.users(id),
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
COMMENT ON TABLE fiscal.cfdi_stamp_queue IS 'Cola de documentos pendientes de timbrar para procesamiento asincrono';
|
||||
|
||||
-- =====================
|
||||
-- INDICES OPERACIONES
|
||||
-- =====================
|
||||
|
||||
-- cfdi_cancellation_requests
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_cancel_tenant ON fiscal.cfdi_cancellation_requests(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_cancel_cfdi ON fiscal.cfdi_cancellation_requests(cfdi_invoice_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_cancel_uuid ON fiscal.cfdi_cancellation_requests(cfdi_uuid);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_cancel_status ON fiscal.cfdi_cancellation_requests(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_cancel_expires ON fiscal.cfdi_cancellation_requests(expires_at)
|
||||
WHERE status IN ('submitted', 'in_process');
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_cancel_pending ON fiscal.cfdi_cancellation_requests(tenant_id, status)
|
||||
WHERE status IN ('pending', 'submitted', 'in_process');
|
||||
|
||||
-- cfdi_operation_logs
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_logs_tenant ON fiscal.cfdi_operation_logs(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_logs_cfdi ON fiscal.cfdi_operation_logs(cfdi_invoice_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_logs_uuid ON fiscal.cfdi_operation_logs(cfdi_uuid);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_logs_type ON fiscal.cfdi_operation_logs(operation_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_logs_date ON fiscal.cfdi_operation_logs(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_logs_success ON fiscal.cfdi_operation_logs(success);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_logs_errors ON fiscal.cfdi_operation_logs(tenant_id, operation_type, created_at)
|
||||
WHERE success = FALSE;
|
||||
|
||||
-- cfdi_payment_complements
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_pcomp_tenant ON fiscal.cfdi_payment_complements(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_pcomp_payment ON fiscal.cfdi_payment_complements(payment_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_pcomp_uuid ON fiscal.cfdi_payment_complements(uuid);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_pcomp_status ON fiscal.cfdi_payment_complements(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_pcomp_issuer ON fiscal.cfdi_payment_complements(issuer_rfc);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_pcomp_receiver ON fiscal.cfdi_payment_complements(receiver_rfc);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_pcomp_date ON fiscal.cfdi_payment_complements(payment_date);
|
||||
|
||||
-- cfdi_payment_complement_documents
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_pcomp_docs_comp ON fiscal.cfdi_payment_complement_documents(payment_complement_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_pcomp_docs_uuid ON fiscal.cfdi_payment_complement_documents(related_uuid);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_pcomp_docs_cfdi ON fiscal.cfdi_payment_complement_documents(related_cfdi_id);
|
||||
|
||||
-- cfdi_payment_complement_document_taxes
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_pcomp_dtax_doc ON fiscal.cfdi_payment_complement_document_taxes(payment_complement_document_id);
|
||||
|
||||
-- cfdi_stamp_queue
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_queue_tenant ON fiscal.cfdi_stamp_queue(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_queue_status ON fiscal.cfdi_stamp_queue(queue_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_queue_priority ON fiscal.cfdi_stamp_queue(priority, created_at)
|
||||
WHERE queue_status = 'pending';
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_queue_retry ON fiscal.cfdi_stamp_queue(next_retry_at)
|
||||
WHERE queue_status = 'failed' AND attempts < max_attempts;
|
||||
CREATE INDEX IF NOT EXISTS idx_cfdi_queue_doc ON fiscal.cfdi_stamp_queue(document_type, document_id);
|
||||
|
||||
-- =====================
|
||||
-- FIN DEL ARCHIVO
|
||||
-- =====================
|
||||
530
ddl/29-cfdi-rls-functions.sql
Normal file
530
ddl/29-cfdi-rls-functions.sql
Normal file
@ -0,0 +1,530 @@
|
||||
-- =============================================================
|
||||
-- ARCHIVO: 29-cfdi-rls-functions.sql
|
||||
-- DESCRIPCION: Modulo CFDI - RLS policies, funciones auxiliares,
|
||||
-- triggers y vistas
|
||||
-- VERSION: 1.0.0
|
||||
-- PROYECTO: ERP-Core V2
|
||||
-- FECHA: 2026-02-03
|
||||
-- DEPENDE DE: 27-cfdi-core.sql, 28-cfdi-operations.sql
|
||||
-- =============================================================
|
||||
|
||||
-- =====================
|
||||
-- ROW LEVEL SECURITY (RLS)
|
||||
-- =====================
|
||||
|
||||
-- cfdi_certificates
|
||||
ALTER TABLE fiscal.cfdi_certificates ENABLE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS tenant_isolation_cfdi_certificates ON fiscal.cfdi_certificates;
|
||||
CREATE POLICY tenant_isolation_cfdi_certificates ON fiscal.cfdi_certificates
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
||||
|
||||
-- cfdi_pac_configurations
|
||||
ALTER TABLE fiscal.cfdi_pac_configurations ENABLE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS tenant_isolation_cfdi_pac ON fiscal.cfdi_pac_configurations;
|
||||
CREATE POLICY tenant_isolation_cfdi_pac ON fiscal.cfdi_pac_configurations
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
||||
|
||||
-- cfdi_invoices
|
||||
ALTER TABLE fiscal.cfdi_invoices ENABLE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS tenant_isolation_cfdi_invoices ON fiscal.cfdi_invoices;
|
||||
CREATE POLICY tenant_isolation_cfdi_invoices ON fiscal.cfdi_invoices
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
||||
|
||||
-- cfdi_cancellation_requests
|
||||
ALTER TABLE fiscal.cfdi_cancellation_requests ENABLE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS tenant_isolation_cfdi_cancellations ON fiscal.cfdi_cancellation_requests;
|
||||
CREATE POLICY tenant_isolation_cfdi_cancellations ON fiscal.cfdi_cancellation_requests
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
||||
|
||||
-- cfdi_operation_logs
|
||||
ALTER TABLE fiscal.cfdi_operation_logs ENABLE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS tenant_isolation_cfdi_logs ON fiscal.cfdi_operation_logs;
|
||||
CREATE POLICY tenant_isolation_cfdi_logs ON fiscal.cfdi_operation_logs
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
||||
|
||||
-- cfdi_payment_complements
|
||||
ALTER TABLE fiscal.cfdi_payment_complements ENABLE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS tenant_isolation_cfdi_payment_comp ON fiscal.cfdi_payment_complements;
|
||||
CREATE POLICY tenant_isolation_cfdi_payment_comp ON fiscal.cfdi_payment_complements
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
||||
|
||||
-- cfdi_stamp_queue
|
||||
ALTER TABLE fiscal.cfdi_stamp_queue ENABLE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS tenant_isolation_cfdi_queue ON fiscal.cfdi_stamp_queue;
|
||||
CREATE POLICY tenant_isolation_cfdi_queue ON fiscal.cfdi_stamp_queue
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
||||
|
||||
-- =====================
|
||||
-- COMENTARIOS RLS
|
||||
-- =====================
|
||||
COMMENT ON POLICY tenant_isolation_cfdi_certificates ON fiscal.cfdi_certificates IS 'Aislamiento multi-tenant para certificados CSD';
|
||||
COMMENT ON POLICY tenant_isolation_cfdi_invoices ON fiscal.cfdi_invoices IS 'Aislamiento multi-tenant para CFDIs';
|
||||
COMMENT ON POLICY tenant_isolation_cfdi_cancellations ON fiscal.cfdi_cancellation_requests IS 'Aislamiento multi-tenant para cancelaciones';
|
||||
COMMENT ON POLICY tenant_isolation_cfdi_logs ON fiscal.cfdi_operation_logs IS 'Aislamiento multi-tenant para logs de operaciones';
|
||||
COMMENT ON POLICY tenant_isolation_cfdi_payment_comp ON fiscal.cfdi_payment_complements IS 'Aislamiento multi-tenant para complementos de pago';
|
||||
|
||||
-- =====================
|
||||
-- FUNCIONES AUXILIARES
|
||||
-- =====================
|
||||
|
||||
-- Funcion: Obtener certificado activo por defecto del tenant
|
||||
CREATE OR REPLACE FUNCTION fiscal.get_default_certificate(p_tenant_id UUID)
|
||||
RETURNS UUID AS $$
|
||||
DECLARE
|
||||
v_certificate_id UUID;
|
||||
BEGIN
|
||||
SELECT id INTO v_certificate_id
|
||||
FROM fiscal.cfdi_certificates
|
||||
WHERE tenant_id = p_tenant_id
|
||||
AND is_default = TRUE
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
AND expires_at > CURRENT_TIMESTAMP
|
||||
LIMIT 1;
|
||||
|
||||
RETURN v_certificate_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
COMMENT ON FUNCTION fiscal.get_default_certificate IS 'Obtiene el ID del certificado CSD activo y vigente por defecto del tenant';
|
||||
|
||||
-- Funcion: Obtener configuracion PAC activa del tenant
|
||||
CREATE OR REPLACE FUNCTION fiscal.get_active_pac(p_tenant_id UUID)
|
||||
RETURNS TABLE (
|
||||
pac_id UUID,
|
||||
pac_code VARCHAR,
|
||||
environment VARCHAR,
|
||||
api_url VARCHAR
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
pc.id,
|
||||
pc.pac_code,
|
||||
pc.environment,
|
||||
CASE
|
||||
WHEN pc.environment = 'production' THEN pc.production_url
|
||||
ELSE pc.sandbox_url
|
||||
END as api_url
|
||||
FROM fiscal.cfdi_pac_configurations pc
|
||||
WHERE pc.tenant_id = p_tenant_id
|
||||
AND pc.is_active = TRUE
|
||||
AND pc.is_default = TRUE
|
||||
AND pc.deleted_at IS NULL
|
||||
LIMIT 1;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
COMMENT ON FUNCTION fiscal.get_active_pac IS 'Obtiene la configuracion del PAC activo del tenant';
|
||||
|
||||
-- Funcion: Validar RFC mexicano
|
||||
CREATE OR REPLACE FUNCTION fiscal.validate_rfc(p_rfc VARCHAR)
|
||||
RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
v_pattern_fisica VARCHAR := '^[A-Z&N]{4}[0-9]{6}[A-Z0-9]{3}$';
|
||||
v_pattern_moral VARCHAR := '^[A-Z&N]{3}[0-9]{6}[A-Z0-9]{3}$';
|
||||
v_rfc VARCHAR;
|
||||
BEGIN
|
||||
-- Normalizar RFC (mayusculas, sin espacios)
|
||||
v_rfc := UPPER(TRIM(p_rfc));
|
||||
|
||||
-- RFC generico publico en general
|
||||
IF v_rfc = 'XAXX010101000' THEN
|
||||
RETURN TRUE;
|
||||
END IF;
|
||||
|
||||
-- RFC generico extranjero
|
||||
IF v_rfc = 'XEXX010101000' THEN
|
||||
RETURN TRUE;
|
||||
END IF;
|
||||
|
||||
-- Validar patron persona fisica (13 caracteres)
|
||||
IF LENGTH(v_rfc) = 13 AND v_rfc ~ v_pattern_fisica THEN
|
||||
RETURN TRUE;
|
||||
END IF;
|
||||
|
||||
-- Validar patron persona moral (12 caracteres)
|
||||
IF LENGTH(v_rfc) = 12 AND v_rfc ~ v_pattern_moral THEN
|
||||
RETURN TRUE;
|
||||
END IF;
|
||||
|
||||
RETURN FALSE;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql IMMUTABLE;
|
||||
|
||||
COMMENT ON FUNCTION fiscal.validate_rfc IS 'Valida el formato de un RFC mexicano (persona fisica o moral)';
|
||||
|
||||
-- Funcion: Generar folio CFDI
|
||||
CREATE OR REPLACE FUNCTION fiscal.generate_cfdi_folio(
|
||||
p_tenant_id UUID,
|
||||
p_serie VARCHAR DEFAULT NULL
|
||||
)
|
||||
RETURNS VARCHAR AS $$
|
||||
DECLARE
|
||||
v_next_folio INTEGER;
|
||||
v_serie VARCHAR;
|
||||
BEGIN
|
||||
-- Serie por defecto si no se proporciona
|
||||
v_serie := COALESCE(p_serie, 'A');
|
||||
|
||||
-- Obtener siguiente folio
|
||||
SELECT COALESCE(MAX(folio::INTEGER), 0) + 1
|
||||
INTO v_next_folio
|
||||
FROM fiscal.cfdi_invoices
|
||||
WHERE tenant_id = p_tenant_id
|
||||
AND serie = v_serie
|
||||
AND folio ~ '^\d+$'; -- Solo folios numericos
|
||||
|
||||
RETURN v_next_folio::VARCHAR;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION fiscal.generate_cfdi_folio IS 'Genera el siguiente folio para una serie de CFDI';
|
||||
|
||||
-- Funcion: Verificar si CFDI puede ser cancelado
|
||||
CREATE OR REPLACE FUNCTION fiscal.can_cancel_cfdi(p_cfdi_id UUID)
|
||||
RETURNS TABLE (
|
||||
can_cancel BOOLEAN,
|
||||
reason VARCHAR,
|
||||
requires_acceptance BOOLEAN
|
||||
) AS $$
|
||||
DECLARE
|
||||
v_cfdi RECORD;
|
||||
v_has_payments BOOLEAN;
|
||||
v_is_month_old BOOLEAN;
|
||||
BEGIN
|
||||
-- Obtener datos del CFDI
|
||||
SELECT ci.*, bi.total, bi.amount_paid
|
||||
INTO v_cfdi
|
||||
FROM fiscal.cfdi_invoices ci
|
||||
JOIN billing.invoices bi ON ci.invoice_id = bi.id
|
||||
WHERE ci.id = p_cfdi_id;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RETURN QUERY SELECT FALSE, 'CFDI no encontrado'::VARCHAR, FALSE;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Verificar estado
|
||||
IF v_cfdi.status != 'stamped' THEN
|
||||
RETURN QUERY SELECT FALSE, 'CFDI no esta timbrado'::VARCHAR, FALSE;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Verificar si ya tiene solicitud de cancelacion
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM fiscal.cfdi_cancellation_requests
|
||||
WHERE cfdi_invoice_id = p_cfdi_id
|
||||
AND status NOT IN ('rejected', 'error')
|
||||
) THEN
|
||||
RETURN QUERY SELECT FALSE, 'Ya existe solicitud de cancelacion'::VARCHAR, FALSE;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Verificar si tiene pagos aplicados
|
||||
v_has_payments := COALESCE(v_cfdi.amount_paid, 0) > 0;
|
||||
|
||||
-- Verificar si tiene mas de un mes
|
||||
v_is_month_old := v_cfdi.stamp_date < (CURRENT_DATE - INTERVAL '1 month');
|
||||
|
||||
-- Determinar si requiere aceptacion del receptor
|
||||
-- Segun reglas SAT 2022+
|
||||
IF v_cfdi.total > 5000 OR v_is_month_old OR v_has_payments THEN
|
||||
RETURN QUERY SELECT TRUE, 'Puede cancelar con aceptacion del receptor'::VARCHAR, TRUE;
|
||||
ELSE
|
||||
RETURN QUERY SELECT TRUE, 'Puede cancelar sin aceptacion'::VARCHAR, FALSE;
|
||||
END IF;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
COMMENT ON FUNCTION fiscal.can_cancel_cfdi IS 'Verifica si un CFDI puede ser cancelado y si requiere aceptacion del receptor';
|
||||
|
||||
-- Funcion: Calcular saldo pendiente de una factura con pagos parciales
|
||||
CREATE OR REPLACE FUNCTION fiscal.get_invoice_payment_balance(p_cfdi_uuid VARCHAR)
|
||||
RETURNS TABLE (
|
||||
total_amount DECIMAL,
|
||||
total_paid DECIMAL,
|
||||
remaining_balance DECIMAL,
|
||||
installment_count INTEGER
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
ci.total,
|
||||
COALESCE(SUM(pcd.amount_paid), 0) as total_paid,
|
||||
ci.total - COALESCE(SUM(pcd.amount_paid), 0) as remaining_balance,
|
||||
COUNT(pcd.id)::INTEGER as installment_count
|
||||
FROM fiscal.cfdi_invoices ci
|
||||
LEFT JOIN fiscal.cfdi_payment_complement_documents pcd
|
||||
ON pcd.related_uuid = ci.uuid
|
||||
LEFT JOIN fiscal.cfdi_payment_complements pc
|
||||
ON pc.id = pcd.payment_complement_id
|
||||
AND pc.status = 'stamped'
|
||||
WHERE ci.uuid = p_cfdi_uuid
|
||||
GROUP BY ci.id, ci.total;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
COMMENT ON FUNCTION fiscal.get_invoice_payment_balance IS 'Obtiene el saldo pendiente de una factura considerando complementos de pago';
|
||||
|
||||
-- Funcion: Obtener resumen de CFDIs por periodo
|
||||
CREATE OR REPLACE FUNCTION fiscal.get_cfdi_summary(
|
||||
p_tenant_id UUID,
|
||||
p_start_date DATE,
|
||||
p_end_date DATE
|
||||
)
|
||||
RETURNS TABLE (
|
||||
voucher_type fiscal.cfdi_voucher_type,
|
||||
status fiscal.cfdi_status,
|
||||
count BIGINT,
|
||||
total_amount DECIMAL
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
ci.voucher_type,
|
||||
ci.status,
|
||||
COUNT(*) as count,
|
||||
SUM(ci.total) as total_amount
|
||||
FROM fiscal.cfdi_invoices ci
|
||||
WHERE ci.tenant_id = p_tenant_id
|
||||
AND ci.created_at::DATE BETWEEN p_start_date AND p_end_date
|
||||
AND ci.deleted_at IS NULL
|
||||
GROUP BY ci.voucher_type, ci.status
|
||||
ORDER BY ci.voucher_type, ci.status;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
COMMENT ON FUNCTION fiscal.get_cfdi_summary IS 'Obtiene resumen de CFDIs agrupados por tipo y estado en un periodo';
|
||||
|
||||
-- =====================
|
||||
-- TRIGGERS
|
||||
-- =====================
|
||||
|
||||
-- Trigger: Actualizar updated_at en cfdi_invoices
|
||||
CREATE OR REPLACE FUNCTION fiscal.update_cfdi_timestamp()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_cfdi_invoices_timestamp ON fiscal.cfdi_invoices;
|
||||
CREATE TRIGGER trg_cfdi_invoices_timestamp
|
||||
BEFORE UPDATE ON fiscal.cfdi_invoices
|
||||
FOR EACH ROW EXECUTE FUNCTION fiscal.update_cfdi_timestamp();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_cfdi_certificates_timestamp ON fiscal.cfdi_certificates;
|
||||
CREATE TRIGGER trg_cfdi_certificates_timestamp
|
||||
BEFORE UPDATE ON fiscal.cfdi_certificates
|
||||
FOR EACH ROW EXECUTE FUNCTION fiscal.update_cfdi_timestamp();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_cfdi_pac_timestamp ON fiscal.cfdi_pac_configurations;
|
||||
CREATE TRIGGER trg_cfdi_pac_timestamp
|
||||
BEFORE UPDATE ON fiscal.cfdi_pac_configurations
|
||||
FOR EACH ROW EXECUTE FUNCTION fiscal.update_cfdi_timestamp();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_cfdi_pcomp_timestamp ON fiscal.cfdi_payment_complements;
|
||||
CREATE TRIGGER trg_cfdi_pcomp_timestamp
|
||||
BEFORE UPDATE ON fiscal.cfdi_payment_complements
|
||||
FOR EACH ROW EXECUTE FUNCTION fiscal.update_cfdi_timestamp();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_cfdi_cancel_timestamp ON fiscal.cfdi_cancellation_requests;
|
||||
CREATE TRIGGER trg_cfdi_cancel_timestamp
|
||||
BEFORE UPDATE ON fiscal.cfdi_cancellation_requests
|
||||
FOR EACH ROW EXECUTE FUNCTION fiscal.update_cfdi_timestamp();
|
||||
|
||||
-- Trigger: Log automatico de cambios de estado
|
||||
CREATE OR REPLACE FUNCTION fiscal.log_cfdi_status_change()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Solo si cambio el status
|
||||
IF OLD.status IS DISTINCT FROM NEW.status THEN
|
||||
INSERT INTO fiscal.cfdi_operation_logs (
|
||||
tenant_id,
|
||||
cfdi_invoice_id,
|
||||
cfdi_uuid,
|
||||
operation_type,
|
||||
status_before,
|
||||
status_after,
|
||||
success,
|
||||
created_by
|
||||
) VALUES (
|
||||
NEW.tenant_id,
|
||||
NEW.id,
|
||||
NEW.uuid,
|
||||
CASE
|
||||
WHEN NEW.status = 'stamped' THEN 'stamp'::fiscal.cfdi_operation_type
|
||||
WHEN NEW.status = 'cancelled' THEN 'cancel_complete'::fiscal.cfdi_operation_type
|
||||
WHEN NEW.status = 'error' THEN 'error'::fiscal.cfdi_operation_type
|
||||
ELSE 'create'::fiscal.cfdi_operation_type
|
||||
END,
|
||||
OLD.status::VARCHAR,
|
||||
NEW.status::VARCHAR,
|
||||
NEW.status NOT IN ('error'),
|
||||
NEW.updated_by
|
||||
);
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_cfdi_status_log ON fiscal.cfdi_invoices;
|
||||
CREATE TRIGGER trg_cfdi_status_log
|
||||
AFTER UPDATE ON fiscal.cfdi_invoices
|
||||
FOR EACH ROW EXECUTE FUNCTION fiscal.log_cfdi_status_change();
|
||||
|
||||
-- Trigger: Validar RFC antes de insertar CFDI
|
||||
CREATE OR REPLACE FUNCTION fiscal.validate_cfdi_rfc()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Validar RFC emisor
|
||||
IF NOT fiscal.validate_rfc(NEW.issuer_rfc) THEN
|
||||
RAISE EXCEPTION 'RFC del emisor invalido: %', NEW.issuer_rfc;
|
||||
END IF;
|
||||
|
||||
-- Validar RFC receptor
|
||||
IF NOT fiscal.validate_rfc(NEW.receiver_rfc) THEN
|
||||
RAISE EXCEPTION 'RFC del receptor invalido: %', NEW.receiver_rfc;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_cfdi_validate_rfc ON fiscal.cfdi_invoices;
|
||||
CREATE TRIGGER trg_cfdi_validate_rfc
|
||||
BEFORE INSERT OR UPDATE ON fiscal.cfdi_invoices
|
||||
FOR EACH ROW EXECUTE FUNCTION fiscal.validate_cfdi_rfc();
|
||||
|
||||
-- =====================
|
||||
-- VISTAS
|
||||
-- =====================
|
||||
|
||||
-- Vista: CFDIs pendientes de timbrar
|
||||
CREATE OR REPLACE VIEW fiscal.v_cfdi_pending_stamp AS
|
||||
SELECT
|
||||
ci.id,
|
||||
ci.tenant_id,
|
||||
ci.invoice_id,
|
||||
ci.serie,
|
||||
ci.folio,
|
||||
ci.voucher_type,
|
||||
ci.status,
|
||||
ci.issuer_rfc,
|
||||
ci.receiver_rfc,
|
||||
ci.receiver_name,
|
||||
ci.total,
|
||||
ci.created_at,
|
||||
bi.invoice_number,
|
||||
bi.invoice_date
|
||||
FROM fiscal.cfdi_invoices ci
|
||||
JOIN billing.invoices bi ON ci.invoice_id = bi.id
|
||||
WHERE ci.status IN ('draft', 'pending')
|
||||
AND ci.deleted_at IS NULL
|
||||
ORDER BY ci.created_at;
|
||||
|
||||
COMMENT ON VIEW fiscal.v_cfdi_pending_stamp IS 'CFDIs pendientes de timbrar';
|
||||
|
||||
-- Vista: Cancelaciones pendientes de respuesta
|
||||
CREATE OR REPLACE VIEW fiscal.v_cfdi_pending_cancellations AS
|
||||
SELECT
|
||||
cr.id,
|
||||
cr.tenant_id,
|
||||
cr.cfdi_uuid,
|
||||
cr.cancellation_reason,
|
||||
cr.status,
|
||||
cr.requested_at,
|
||||
cr.submitted_at,
|
||||
cr.expires_at,
|
||||
ci.serie,
|
||||
ci.folio,
|
||||
ci.receiver_rfc,
|
||||
ci.receiver_name,
|
||||
ci.total
|
||||
FROM fiscal.cfdi_cancellation_requests cr
|
||||
JOIN fiscal.cfdi_invoices ci ON cr.cfdi_invoice_id = ci.id
|
||||
WHERE cr.status IN ('submitted', 'in_process')
|
||||
ORDER BY cr.expires_at;
|
||||
|
||||
COMMENT ON VIEW fiscal.v_cfdi_pending_cancellations IS 'Solicitudes de cancelacion pendientes de respuesta';
|
||||
|
||||
-- Vista: Certificados por vencer
|
||||
CREATE OR REPLACE VIEW fiscal.v_certificates_expiring AS
|
||||
SELECT
|
||||
cc.id,
|
||||
cc.tenant_id,
|
||||
cc.certificate_number,
|
||||
cc.rfc,
|
||||
cc.status,
|
||||
cc.expires_at,
|
||||
cc.is_default,
|
||||
t.name as tenant_name,
|
||||
(cc.expires_at - CURRENT_TIMESTAMP) as time_to_expire
|
||||
FROM fiscal.cfdi_certificates cc
|
||||
JOIN auth.tenants t ON cc.tenant_id = t.id
|
||||
WHERE cc.deleted_at IS NULL
|
||||
AND cc.status = 'active'
|
||||
AND cc.expires_at < (CURRENT_TIMESTAMP + INTERVAL '30 days')
|
||||
ORDER BY cc.expires_at;
|
||||
|
||||
COMMENT ON VIEW fiscal.v_certificates_expiring IS 'Certificados CSD que vencen en los proximos 30 dias';
|
||||
|
||||
-- Vista: Resumen de uso de timbres por PAC
|
||||
CREATE OR REPLACE VIEW fiscal.v_pac_stamp_usage AS
|
||||
SELECT
|
||||
pc.tenant_id,
|
||||
pc.pac_code,
|
||||
pc.pac_name,
|
||||
pc.environment,
|
||||
pc.monthly_stamp_limit,
|
||||
pc.stamps_used_this_month,
|
||||
CASE
|
||||
WHEN pc.monthly_stamp_limit IS NULL THEN NULL
|
||||
ELSE (pc.stamps_used_this_month::DECIMAL / pc.monthly_stamp_limit * 100)
|
||||
END as usage_percentage,
|
||||
pc.contract_expires_at,
|
||||
t.name as tenant_name
|
||||
FROM fiscal.cfdi_pac_configurations pc
|
||||
JOIN auth.tenants t ON pc.tenant_id = t.id
|
||||
WHERE pc.is_active = TRUE
|
||||
AND pc.deleted_at IS NULL
|
||||
ORDER BY pc.tenant_id, pc.pac_code;
|
||||
|
||||
COMMENT ON VIEW fiscal.v_pac_stamp_usage IS 'Uso de timbres por PAC y tenant';
|
||||
|
||||
-- Vista: Facturas con saldo pendiente (PPD)
|
||||
CREATE OR REPLACE VIEW fiscal.v_invoices_pending_payment AS
|
||||
SELECT
|
||||
ci.id,
|
||||
ci.tenant_id,
|
||||
ci.uuid,
|
||||
ci.serie,
|
||||
ci.folio,
|
||||
ci.receiver_rfc,
|
||||
ci.receiver_name,
|
||||
ci.total,
|
||||
ci.stamp_date,
|
||||
COALESCE(SUM(pcd.amount_paid), 0) as total_paid,
|
||||
ci.total - COALESCE(SUM(pcd.amount_paid), 0) as remaining_balance,
|
||||
COUNT(pcd.id) as installment_count
|
||||
FROM fiscal.cfdi_invoices ci
|
||||
LEFT JOIN fiscal.cfdi_payment_complement_documents pcd
|
||||
ON pcd.related_uuid = ci.uuid
|
||||
LEFT JOIN fiscal.cfdi_payment_complements pc
|
||||
ON pc.id = pcd.payment_complement_id
|
||||
AND pc.status = 'stamped'
|
||||
WHERE ci.payment_method = 'PPD'
|
||||
AND ci.status = 'stamped'
|
||||
AND ci.deleted_at IS NULL
|
||||
GROUP BY ci.id
|
||||
HAVING ci.total - COALESCE(SUM(pcd.amount_paid), 0) > 0
|
||||
ORDER BY ci.stamp_date;
|
||||
|
||||
COMMENT ON VIEW fiscal.v_invoices_pending_payment IS 'Facturas PPD con saldo pendiente de complemento de pago';
|
||||
|
||||
-- =====================
|
||||
-- FIN DEL ARCHIVO
|
||||
-- =====================
|
||||
@ -277,6 +277,8 @@ run_ddl_files() {
|
||||
# Orden especifico de ejecucion
|
||||
# Nota: El orden es importante por las dependencias entre tablas
|
||||
local ddl_files=(
|
||||
# Auth base (tenants + users DDL - también creadas inline como safety net)
|
||||
"00-auth-base.sql"
|
||||
# Core existente
|
||||
"01-auth-profiles.sql"
|
||||
"02-auth-devices.sql"
|
||||
@ -286,11 +288,12 @@ run_ddl_files() {
|
||||
# SaaS Extensions - Sprint 1-2 (EPIC-SAAS-001, EPIC-SAAS-002)
|
||||
"06-auth-extended.sql"
|
||||
"07-users-rbac.sql"
|
||||
"07a-permissions-seed.sql"
|
||||
"08-plans.sql"
|
||||
"11-feature-flags.sql"
|
||||
# SaaS Extensions - Sprint 3+ (EPIC-SAAS-003 - EPIC-SAAS-008)
|
||||
"09-notifications.sql"
|
||||
"10-audit.sql"
|
||||
"11-feature-flags.sql"
|
||||
"12-webhooks.sql"
|
||||
"13-storage.sql"
|
||||
"14-ai.sql"
|
||||
@ -299,10 +302,37 @@ run_ddl_files() {
|
||||
"16-partners.sql"
|
||||
"17-products.sql"
|
||||
"18-warehouses.sql"
|
||||
"19-product-attributes.sql"
|
||||
"20-core-catalogs.sql"
|
||||
"21-inventory.sql"
|
||||
"22-sales.sql"
|
||||
"23-purchases.sql"
|
||||
"24-invoices.sql"
|
||||
"25-payment-terminals.sql"
|
||||
# Fiscal / CFDI
|
||||
"26-fiscal-catalogs.sql"
|
||||
"27-cfdi-core.sql"
|
||||
"28-cfdi-operations.sql"
|
||||
"29-cfdi-rls-functions.sql"
|
||||
# Settings & Reports
|
||||
"30-settings.sql"
|
||||
"31-reports.sql"
|
||||
# HR & Purchases Extended
|
||||
"45-hr.sql"
|
||||
"46-purchases-matching.sql"
|
||||
# Financial Module
|
||||
"50-financial-schema.sql"
|
||||
"51-financial-accounts.sql"
|
||||
"52-financial-journals.sql"
|
||||
"53-financial-entries.sql"
|
||||
"54-financial-invoices.sql"
|
||||
"55-financial-payments.sql"
|
||||
"56-financial-taxes.sql"
|
||||
"57-financial-bank-reconciliation.sql"
|
||||
# Projects
|
||||
"60-projects-timesheets.sql"
|
||||
# RLS (must be last - depends on all tables)
|
||||
"99-rls-erp-modules.sql"
|
||||
)
|
||||
|
||||
for ddl_file in "${ddl_files[@]}"; do
|
||||
|
||||
Loading…
Reference in New Issue
Block a user