[TASK-2026-01-24-GAPS] feat: Add product attributes DDL + fix numbering

Changes:
- Add 19-product-attributes.sql (attributes, values, variants)
- Rename 21-fiscal-catalogs.sql to 25-fiscal-catalogs.sql (fix duplicate 21)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-24 07:27:50 -06:00
parent 57f41859de
commit 67cb99d4b4
2 changed files with 441 additions and 0 deletions

View File

@ -0,0 +1,146 @@
-- =============================================================
-- ARCHIVO: 19-product-attributes.sql
-- DESCRIPCION: Atributos de productos (color, talla, material)
-- VERSION: 1.0.0
-- PROYECTO: ERP-Core V2
-- FECHA: 2026-01-24
-- =============================================================
-- =====================
-- TABLA: product_attributes
-- Atributos configurables de productos
-- =====================
CREATE TABLE IF NOT EXISTS products.product_attributes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Identificacion
code VARCHAR(50) NOT NULL,
name VARCHAR(100) NOT NULL,
description TEXT,
-- Tipo de visualizacion
display_type VARCHAR(20) DEFAULT 'radio', -- radio, select, color, pills
-- Estado
is_active BOOLEAN DEFAULT TRUE,
sort_order INTEGER DEFAULT 0,
-- 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),
UNIQUE(tenant_id, code)
);
-- Indices para product_attributes
CREATE INDEX IF NOT EXISTS idx_product_attributes_tenant ON products.product_attributes(tenant_id);
CREATE INDEX IF NOT EXISTS idx_product_attributes_code ON products.product_attributes(code);
CREATE INDEX IF NOT EXISTS idx_product_attributes_active ON products.product_attributes(is_active) WHERE is_active = TRUE;
-- =====================
-- TABLA: product_attribute_values
-- Valores posibles para cada atributo
-- =====================
CREATE TABLE IF NOT EXISTS products.product_attribute_values (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
attribute_id UUID NOT NULL REFERENCES products.product_attributes(id) ON DELETE CASCADE,
-- Identificacion
code VARCHAR(50),
name VARCHAR(100) NOT NULL,
-- Visualizacion
html_color VARCHAR(20), -- Para atributos de tipo color (#FF0000)
image_url VARCHAR(500), -- Imagen del valor
-- Estado
is_active BOOLEAN DEFAULT TRUE,
sort_order INTEGER DEFAULT 0,
-- Metadata
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Indices para product_attribute_values
CREATE INDEX IF NOT EXISTS idx_product_attribute_values_attribute ON products.product_attribute_values(attribute_id);
CREATE INDEX IF NOT EXISTS idx_product_attribute_values_active ON products.product_attribute_values(is_active) WHERE is_active = TRUE;
-- =====================
-- TABLA: product_variants
-- Variantes de productos (combinaciones de atributos)
-- =====================
CREATE TABLE IF NOT EXISTS products.product_variants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID NOT NULL REFERENCES products.products(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Identificacion
sku VARCHAR(50) NOT NULL,
barcode VARCHAR(50),
name VARCHAR(200) NOT NULL, -- Nombre de la variante (ej: "Camisa Azul - Talla M")
-- Precios (override del producto base)
price_extra DECIMAL(15, 4) DEFAULT 0, -- Ajuste de precio sobre el producto base
cost_extra DECIMAL(15, 4) DEFAULT 0, -- Ajuste de costo sobre el producto base
-- Inventario
stock_qty DECIMAL(15, 4) DEFAULT 0,
-- Imagen
image_url VARCHAR(500),
-- Estado
is_active BOOLEAN 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),
UNIQUE(tenant_id, sku)
);
-- Indices para product_variants
CREATE INDEX IF NOT EXISTS idx_product_variants_product ON products.product_variants(product_id);
CREATE INDEX IF NOT EXISTS idx_product_variants_tenant ON products.product_variants(tenant_id);
CREATE INDEX IF NOT EXISTS idx_product_variants_sku ON products.product_variants(sku);
CREATE INDEX IF NOT EXISTS idx_product_variants_barcode ON products.product_variants(barcode);
CREATE INDEX IF NOT EXISTS idx_product_variants_active ON products.product_variants(is_active) WHERE is_active = TRUE;
-- =====================
-- TABLA: product_variant_attributes
-- Relacion M:N entre variantes y valores de atributos
-- =====================
CREATE TABLE IF NOT EXISTS products.product_variant_attributes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
variant_id UUID NOT NULL REFERENCES products.product_variants(id) ON DELETE CASCADE,
attribute_value_id UUID NOT NULL REFERENCES products.product_attribute_values(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE(variant_id, attribute_value_id)
);
-- Indices para product_variant_attributes
CREATE INDEX IF NOT EXISTS idx_product_variant_attributes_variant ON products.product_variant_attributes(variant_id);
CREATE INDEX IF NOT EXISTS idx_product_variant_attributes_value ON products.product_variant_attributes(attribute_value_id);
-- =====================
-- COMENTARIOS
-- =====================
COMMENT ON TABLE products.product_attributes IS 'Definicion de atributos de productos (color, talla, material, etc.)';
COMMENT ON COLUMN products.product_attributes.display_type IS 'Tipo de visualizacion: radio (botones radio), select (dropdown), color (muestra de colores), pills (etiquetas)';
COMMENT ON TABLE products.product_attribute_values IS 'Valores posibles para cada atributo (ej: Rojo, Verde, Azul para atributo Color)';
COMMENT ON COLUMN products.product_attribute_values.html_color IS 'Color HTML para visualizar en UI (#FF0000)';
COMMENT ON TABLE products.product_variants IS 'Variantes de productos generadas a partir de combinaciones de atributos';
COMMENT ON COLUMN products.product_variants.price_extra IS 'Ajuste de precio sobre el precio base del producto';
COMMENT ON COLUMN products.product_variants.cost_extra IS 'Ajuste de costo sobre el costo base del producto';
COMMENT ON TABLE products.product_variant_attributes IS 'Tabla de union entre variantes y valores de atributos';

295
ddl/25-fiscal-catalogs.sql Normal file
View File

@ -0,0 +1,295 @@
-- =============================================================
-- ARCHIVO: 21-fiscal-catalogs.sql
-- DESCRIPCION: Catalogos fiscales - SAT Mexico, regimenes, CFDI
-- VERSION: 1.0.0
-- PROYECTO: ERP-Core V2
-- FECHA: 2026-01-18
-- =============================================================
-- =====================
-- SCHEMA: fiscal
-- =====================
CREATE SCHEMA IF NOT EXISTS fiscal;
-- =====================
-- TABLA: tax_categories
-- Categorias de impuestos (IVA, ISR, IEPS, etc.)
-- =====================
CREATE TABLE IF NOT EXISTS fiscal.tax_categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(20) NOT NULL UNIQUE, -- IVA, ISR, IEPS, etc.
name VARCHAR(100) NOT NULL,
description TEXT,
-- Tipo de impuesto
tax_nature VARCHAR(20) NOT NULL DEFAULT 'tax', -- tax, withholding, both
-- Configuracion SAT Mexico
sat_code VARCHAR(10), -- Codigo SAT (002=IVA, 001=ISR, etc.)
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_tax_categories_code ON fiscal.tax_categories(code);
CREATE INDEX IF NOT EXISTS idx_tax_categories_sat ON fiscal.tax_categories(sat_code);
CREATE INDEX IF NOT EXISTS idx_tax_categories_active ON fiscal.tax_categories(is_active) WHERE is_active = TRUE;
COMMENT ON TABLE fiscal.tax_categories IS 'Categorias de impuestos (IVA, ISR, IEPS, etc.)';
-- =====================
-- TABLA: fiscal_regimes
-- Regimenes fiscales SAT Mexico (Catalogo c_RegimenFiscal)
-- =====================
CREATE TABLE IF NOT EXISTS fiscal.fiscal_regimes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(10) NOT NULL UNIQUE, -- Codigo SAT (601, 603, 612, etc.)
name VARCHAR(255) NOT NULL,
description TEXT,
-- Aplica a persona fisica o moral
applies_to VARCHAR(20) NOT NULL DEFAULT 'both', -- natural (fisica), legal (moral), both
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_fiscal_regimes_code ON fiscal.fiscal_regimes(code);
CREATE INDEX IF NOT EXISTS idx_fiscal_regimes_applies ON fiscal.fiscal_regimes(applies_to);
CREATE INDEX IF NOT EXISTS idx_fiscal_regimes_active ON fiscal.fiscal_regimes(is_active) WHERE is_active = TRUE;
COMMENT ON TABLE fiscal.fiscal_regimes IS 'Catalogo de regimenes fiscales SAT (c_RegimenFiscal)';
-- =====================
-- TABLA: cfdi_uses
-- Uso del CFDI SAT Mexico (Catalogo c_UsoCFDI)
-- =====================
CREATE TABLE IF NOT EXISTS fiscal.cfdi_uses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(10) NOT NULL UNIQUE, -- Codigo SAT (G01, G02, G03, etc.)
name VARCHAR(255) NOT NULL,
description TEXT,
-- Aplica a persona fisica o moral
applies_to VARCHAR(20) NOT NULL DEFAULT 'both', -- natural, legal, both
-- Regimenes fiscales permitidos (NULL = todos)
allowed_regimes VARCHAR[] DEFAULT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_cfdi_uses_code ON fiscal.cfdi_uses(code);
CREATE INDEX IF NOT EXISTS idx_cfdi_uses_applies ON fiscal.cfdi_uses(applies_to);
CREATE INDEX IF NOT EXISTS idx_cfdi_uses_active ON fiscal.cfdi_uses(is_active) WHERE is_active = TRUE;
COMMENT ON TABLE fiscal.cfdi_uses IS 'Catalogo de uso del CFDI SAT (c_UsoCFDI)';
-- =====================
-- TABLA: payment_methods
-- Formas de pago SAT Mexico (Catalogo c_FormaPago)
-- =====================
CREATE TABLE IF NOT EXISTS fiscal.payment_methods (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(10) NOT NULL UNIQUE, -- Codigo SAT (01, 02, 03, etc.)
name VARCHAR(100) NOT NULL,
description TEXT,
-- Configuracion
requires_bank_info BOOLEAN DEFAULT FALSE, -- Requiere info bancaria
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_payment_methods_code ON fiscal.payment_methods(code);
CREATE INDEX IF NOT EXISTS idx_payment_methods_active ON fiscal.payment_methods(is_active) WHERE is_active = TRUE;
COMMENT ON TABLE fiscal.payment_methods IS 'Catalogo de formas de pago SAT (c_FormaPago)';
-- =====================
-- TABLA: payment_types
-- Metodos de pago SAT Mexico (Catalogo c_MetodoPago)
-- =====================
CREATE TABLE IF NOT EXISTS fiscal.payment_types (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(10) NOT NULL UNIQUE, -- PUE, PPD
name VARCHAR(100) NOT NULL,
description TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_payment_types_code ON fiscal.payment_types(code);
CREATE INDEX IF NOT EXISTS idx_payment_types_active ON fiscal.payment_types(is_active) WHERE is_active = TRUE;
COMMENT ON TABLE fiscal.payment_types IS 'Catalogo de metodos de pago SAT (c_MetodoPago)';
-- =====================
-- TABLA: withholding_types
-- Tipos de retencion
-- =====================
CREATE TABLE IF NOT EXISTS fiscal.withholding_types (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(20) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
description TEXT,
-- Tasa de retencion por defecto
default_rate DECIMAL(5, 2) NOT NULL DEFAULT 0,
-- Categoria de impuesto asociada
tax_category_id UUID REFERENCES fiscal.tax_categories(id),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_withholding_types_code ON fiscal.withholding_types(code);
CREATE INDEX IF NOT EXISTS idx_withholding_types_category ON fiscal.withholding_types(tax_category_id);
CREATE INDEX IF NOT EXISTS idx_withholding_types_active ON fiscal.withholding_types(is_active) WHERE is_active = TRUE;
COMMENT ON TABLE fiscal.withholding_types IS 'Tipos de retencion fiscal';
-- =====================
-- DATOS INICIALES: tax_categories
-- =====================
INSERT INTO fiscal.tax_categories (code, name, description, tax_nature, sat_code) VALUES
('IVA', 'Impuesto al Valor Agregado', 'Impuesto al consumo aplicado a bienes y servicios', 'tax', '002'),
('ISR', 'Impuesto Sobre la Renta', 'Impuesto a los ingresos', 'withholding', '001'),
('IEPS', 'Impuesto Especial sobre Produccion y Servicios', 'Impuesto a productos especiales', 'tax', '003'),
('IVA_RET', 'IVA Retenido', 'Retencion de IVA', 'withholding', '002'),
('ISR_RET', 'ISR Retenido', 'Retencion de ISR', 'withholding', '001'),
('CEDULAR', 'Impuesto Cedular', 'Impuesto local sobre ingresos', 'tax', NULL),
('ISH', 'Impuesto Sobre Hospedaje', 'Impuesto estatal al hospedaje', 'tax', NULL)
ON CONFLICT (code) DO NOTHING;
-- =====================
-- DATOS INICIALES: fiscal_regimes (SAT Mexico c_RegimenFiscal)
-- =====================
INSERT INTO fiscal.fiscal_regimes (code, name, applies_to) VALUES
('601', 'General de Ley Personas Morales', 'legal'),
('603', 'Personas Morales con Fines no Lucrativos', 'legal'),
('605', 'Sueldos y Salarios e Ingresos Asimilados a Salarios', 'natural'),
('606', 'Arrendamiento', 'natural'),
('607', 'Régimen de Enajenación o Adquisición de Bienes', 'natural'),
('608', 'Demás ingresos', 'natural'),
('609', 'Consolidación', 'legal'),
('610', 'Residentes en el Extranjero sin Establecimiento Permanente en México', 'both'),
('611', 'Ingresos por Dividendos (socios y accionistas)', 'natural'),
('612', 'Personas Físicas con Actividades Empresariales y Profesionales', 'natural'),
('614', 'Ingresos por intereses', 'natural'),
('615', 'Régimen de los ingresos por obtención de premios', 'natural'),
('616', 'Sin obligaciones fiscales', 'both'),
('620', 'Sociedades Cooperativas de Producción que optan por diferir sus ingresos', 'legal'),
('621', 'Incorporación Fiscal', 'natural'),
('622', 'Actividades Agrícolas, Ganaderas, Silvícolas y Pesqueras', 'both'),
('623', 'Opcional para Grupos de Sociedades', 'legal'),
('624', 'Coordinados', 'legal'),
('625', 'Régimen de las Actividades Empresariales con ingresos a través de Plataformas Tecnológicas', 'natural'),
('626', 'Régimen Simplificado de Confianza', 'both')
ON CONFLICT (code) DO NOTHING;
-- =====================
-- DATOS INICIALES: cfdi_uses (SAT Mexico c_UsoCFDI)
-- =====================
INSERT INTO fiscal.cfdi_uses (code, name, applies_to) VALUES
('G01', 'Adquisición de mercancías', 'both'),
('G02', 'Devoluciones, descuentos o bonificaciones', 'both'),
('G03', 'Gastos en general', 'both'),
('I01', 'Construcciones', 'both'),
('I02', 'Mobiliario y equipo de oficina por inversiones', 'both'),
('I03', 'Equipo de transporte', 'both'),
('I04', 'Equipo de cómputo y accesorios', 'both'),
('I05', 'Dados, troqueles, moldes, matrices y herramental', 'both'),
('I06', 'Comunicaciones telefónicas', 'both'),
('I07', 'Comunicaciones satelitales', 'both'),
('I08', 'Otra maquinaria y equipo', 'both'),
('D01', 'Honorarios médicos, dentales y gastos hospitalarios', 'natural'),
('D02', 'Gastos médicos por incapacidad o discapacidad', 'natural'),
('D03', 'Gastos funerales', 'natural'),
('D04', 'Donativos', 'natural'),
('D05', 'Intereses reales efectivamente pagados por créditos hipotecarios (casa habitación)', 'natural'),
('D06', 'Aportaciones voluntarias al SAR', 'natural'),
('D07', 'Primas por seguros de gastos médicos', 'natural'),
('D08', 'Gastos de transportación escolar obligatoria', 'natural'),
('D09', 'Depósitos en cuentas para el ahorro, primas que tengan como base planes de pensiones', 'natural'),
('D10', 'Pagos por servicios educativos (colegiaturas)', 'natural'),
('S01', 'Sin efectos fiscales', 'both'),
('CP01', 'Pagos', 'both'),
('CN01', 'Nómina', 'natural')
ON CONFLICT (code) DO NOTHING;
-- =====================
-- DATOS INICIALES: payment_methods (SAT Mexico c_FormaPago)
-- =====================
INSERT INTO fiscal.payment_methods (code, name, requires_bank_info) VALUES
('01', 'Efectivo', false),
('02', 'Cheque nominativo', true),
('03', 'Transferencia electrónica de fondos', true),
('04', 'Tarjeta de crédito', true),
('05', 'Monedero electrónico', false),
('06', 'Dinero electrónico', false),
('08', 'Vales de despensa', false),
('12', 'Dación en pago', false),
('13', 'Pago por subrogación', false),
('14', 'Pago por consignación', false),
('15', 'Condonación', false),
('17', 'Compensación', false),
('23', 'Novación', false),
('24', 'Confusión', false),
('25', 'Remisión de deuda', false),
('26', 'Prescripción o caducidad', false),
('27', 'A satisfacción del acreedor', false),
('28', 'Tarjeta de débito', true),
('29', 'Tarjeta de servicios', true),
('30', 'Aplicación de anticipos', false),
('31', 'Intermediario pagos', false),
('99', 'Por definir', false)
ON CONFLICT (code) DO NOTHING;
-- =====================
-- DATOS INICIALES: payment_types (SAT Mexico c_MetodoPago)
-- =====================
INSERT INTO fiscal.payment_types (code, name, description) VALUES
('PUE', 'Pago en una sola exhibición', 'El pago se realiza en una sola exhibición al momento de emitir el CFDI'),
('PPD', 'Pago en parcialidades o diferido', 'El pago se realiza en parcialidades o de forma diferida')
ON CONFLICT (code) DO NOTHING;
-- =====================
-- DATOS INICIALES: withholding_types
-- =====================
INSERT INTO fiscal.withholding_types (code, name, description, default_rate, tax_category_id) VALUES
('ISR_10', 'Retención ISR 10%', 'Retención de ISR al 10% para servicios profesionales', 10.00,
(SELECT id FROM fiscal.tax_categories WHERE code = 'ISR')),
('ISR_10.67', 'Retención ISR 10.67%', 'Retención de ISR al 10.67% para arrendamiento', 10.67,
(SELECT id FROM fiscal.tax_categories WHERE code = 'ISR')),
('IVA_RET_10.67', 'Retención IVA 10.67%', 'Retención de IVA para servicios profesionales persona moral', 10.67,
(SELECT id FROM fiscal.tax_categories WHERE code = 'IVA_RET')),
('IVA_RET_4', 'Retención IVA 4%', 'Retención de IVA al 4% para autotransporte', 4.00,
(SELECT id FROM fiscal.tax_categories WHERE code = 'IVA_RET'))
ON CONFLICT (code) DO NOTHING;
-- =====================
-- FIN DEL ARCHIVO
-- =====================