# SPEC-LOCALIZACION-PAISES ## Metadatos | Campo | Valor | |-------|-------| | **Código** | SPEC-TRANS-012 | | **Versión** | 1.0.0 | | **Fecha** | 2025-01-15 | | **Autor** | Requirements-Analyst Agent | | **Estado** | DRAFT | | **Prioridad** | P1 | | **Módulos Afectados** | MGN-002 (Empresas), MGN-004 (Financiero) | | **Gaps Cubiertos** | GAP-MGN-002-002 | ## Resumen Ejecutivo Esta especificación define el sistema completo de localización por país, permitiendo configurar automáticamente: 1. **Plantillas contables (CoA)**: Plan de cuentas específico por país 2. **Impuestos por defecto**: IVA, retenciones, impuestos especiales 3. **Posiciones fiscales**: Mapeo de impuestos según tipo de cliente 4. **Tipos de documentos**: Facturas, notas de crédito según normativa local 5. **Tipos de identificación**: RFC, CUIT, NIF según país 6. **Datos maestros**: Bancos, estados/provincias, formatos de dirección 7. **Configuración de empresa**: Campos específicos por legislación ### Referencia Odoo 18 Basado en análisis exhaustivo de módulos `l10n_*` de Odoo 18: - **221 módulos** de localización disponibles - **Patrón `@template`** para registro de datos - **Herencia de plantillas** para variantes regionales - **Validación externa** con `stdnum` para identificaciones fiscales --- ## Parte 1: Arquitectura del Sistema de Localización ### 1.1 Visión General ``` ┌─────────────────────────────────────────────────────────────────┐ │ SISTEMA DE LOCALIZACIÓN │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Plantilla │ │ Plantilla │ │ Plantilla │ │ │ │ l10n_mx │ │ l10n_ar │ │ l10n_es │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ │ └───────────────────┼───────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────┐ │ │ │ LocalizationService │ │ │ │ ───────────────────── │ │ │ │ • loadTemplate() │ │ │ │ • getCountryConfig() │ │ │ │ • applyToCompany() │ │ │ └──────────────┬───────────┘ │ │ │ │ │ ┌───────────────────┼───────────────────┐ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Empresa │ │ Impuestos │ │ Cuentas │ │ │ │ Config │ │ Config │ │ Config │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ### 1.2 Componentes del Sistema | Componente | Descripción | Datos Incluidos | |------------|-------------|-----------------| | **Plantilla Base** | Configuración raíz del país | CoA, impuestos, posiciones fiscales | | **Datos Maestros** | Catálogos específicos | Bancos, provincias, tipos de ID | | **Extensiones de Modelo** | Campos específicos país | RFC, CUIT, tipo responsabilidad | | **Validadores** | Reglas de validación | Formato RFC, checksum CUIT | | **Formateadores** | Presentación de datos | Formato dirección, moneda | --- ## Parte 2: Modelo de Datos ### 2.1 Tipos Enumerados ```sql -- Estado de plantilla de localización CREATE TYPE localization_template_status AS ENUM ( 'draft', -- En desarrollo 'active', -- Disponible para uso 'deprecated', -- Obsoleto pero funcional 'archived' -- No disponible ); -- Tipo de dato de localización CREATE TYPE localization_data_type AS ENUM ( 'chart_of_accounts', -- Plan de cuentas 'taxes', -- Impuestos 'fiscal_positions', -- Posiciones fiscales 'document_types', -- Tipos de documento 'identification_types', -- Tipos de identificación 'banks', -- Bancos 'address_format', -- Formato de dirección 'company_fields' -- Campos de empresa ); -- Tipo de impuesto por uso CREATE TYPE tax_use_type AS ENUM ( 'sale', -- Ventas 'purchase', -- Compras 'none' -- Ninguno (ajustes) ); -- Tipo de cálculo de impuesto CREATE TYPE tax_amount_type AS ENUM ( 'percent', -- Porcentaje 'fixed', -- Monto fijo 'division', -- División 'group' -- Grupo de impuestos ); ``` ### 2.2 Tabla de Plantillas de Localización ```sql -- Plantillas de localización por país CREATE TABLE localization_templates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Identificación code VARCHAR(20) NOT NULL UNIQUE, -- Ej: 'mx', 'ar_ri', 'es_pymes' name VARCHAR(200) NOT NULL, -- Nombre descriptivo country_id UUID NOT NULL REFERENCES countries(id), -- Jerarquía (para variantes) parent_template_id UUID REFERENCES localization_templates(id), sequence INTEGER DEFAULT 10, -- Orden de prioridad -- Configuración code_digits INTEGER DEFAULT 6, -- Longitud de códigos de cuenta currency_id UUID REFERENCES currencies(id), -- Estado status localization_template_status DEFAULT 'active', -- Metadatos version VARCHAR(20), description TEXT, -- Auditoría is_active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_by UUID NOT NULL, -- Constraints CONSTRAINT uq_template_country_code UNIQUE (country_id, code) ); -- Índices CREATE INDEX idx_loc_templates_country ON localization_templates(country_id); CREATE INDEX idx_loc_templates_parent ON localization_templates(parent_template_id); CREATE INDEX idx_loc_templates_status ON localization_templates(status); COMMENT ON TABLE localization_templates IS 'Plantillas de configuración por país. Soporta herencia para variantes regionales.'; ``` ### 2.3 Datos de Plantilla (Configuración) ```sql -- Configuración específica de cada plantilla CREATE TABLE localization_template_data ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), template_id UUID NOT NULL REFERENCES localization_templates(id) ON DELETE CASCADE, -- Tipo de configuración data_type localization_data_type NOT NULL, -- Datos en formato JSONB para flexibilidad config_data JSONB NOT NULL, -- Para ordenamiento y prioridad sequence INTEGER DEFAULT 10, -- Auditoría created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Constraint único por plantilla y tipo CONSTRAINT uq_template_data_type UNIQUE (template_id, data_type) ); -- Índice para búsqueda por tipo CREATE INDEX idx_loc_template_data_type ON localization_template_data(data_type); CREATE INDEX idx_loc_template_data_gin ON localization_template_data USING GIN (config_data); COMMENT ON TABLE localization_template_data IS 'Datos de configuración por tipo para cada plantilla de localización.'; ``` ### 2.4 Tipos de Identificación por País ```sql -- Tipos de identificación fiscal/legal por país CREATE TABLE identification_types ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Identificación code VARCHAR(20) NOT NULL, -- Ej: 'rfc', 'cuit', 'nif' name VARCHAR(100) NOT NULL, description TEXT, -- País country_id UUID REFERENCES countries(id), -- NULL = global -- Configuración is_vat BOOLEAN DEFAULT false, -- ¿Es identificación fiscal? validation_regex VARCHAR(200), -- Patrón de validación validation_module VARCHAR(100), -- Módulo de validación (ej: 'mx.rfc') format_mask VARCHAR(100), -- Máscara de formato -- Ordenamiento sequence INTEGER DEFAULT 10, -- Estado is_active BOOLEAN DEFAULT true, -- Auditoría created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uq_identification_type_code UNIQUE (country_id, code) ); -- Índices CREATE INDEX idx_id_types_country ON identification_types(country_id); CREATE INDEX idx_id_types_vat ON identification_types(is_vat) WHERE is_vat = true; -- Datos iniciales globales INSERT INTO identification_types (code, name, is_vat, sequence) VALUES ('vat', 'Tax ID / VAT', true, 1), ('passport', 'Passport', false, 100), ('foreign_id', 'Foreign ID', false, 110); COMMENT ON TABLE identification_types IS 'Catálogo de tipos de identificación por país (RFC, CUIT, NIF, etc.)'; ``` ### 2.5 Configuración de Empresa por País ```sql -- Campos adicionales de empresa por localización CREATE TABLE company_localization_config ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE, -- Localización aplicada template_id UUID REFERENCES localization_templates(id), country_id UUID NOT NULL REFERENCES countries(id), -- Identificación fiscal identification_type_id UUID REFERENCES identification_types(id), tax_id VARCHAR(50), -- RFC, CUIT, NIF tax_id_formatted VARCHAR(60), -- Versión formateada -- Configuración contable chart_template_loaded BOOLEAN DEFAULT false, fiscal_country_id UUID REFERENCES countries(id), -- Prefijos de cuentas bank_account_code_prefix VARCHAR(20), cash_account_code_prefix VARCHAR(20), -- Impuestos por defecto default_sale_tax_id UUID, -- REFERENCES taxes(id) default_purchase_tax_id UUID, -- REFERENCES taxes(id) -- Configuración de redondeo tax_calculation_rounding VARCHAR(20) DEFAULT 'round_per_line', -- Campos específicos por país (JSONB para flexibilidad) country_specific_data JSONB DEFAULT '{}', -- Auditoría created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_by UUID NOT NULL, CONSTRAINT uq_company_localization UNIQUE (company_id) ); -- Índices CREATE INDEX idx_company_loc_template ON company_localization_config(template_id); CREATE INDEX idx_company_loc_country ON company_localization_config(country_id); COMMENT ON TABLE company_localization_config IS 'Configuración de localización aplicada a cada empresa.'; ``` ### 2.6 Posiciones Fiscales ```sql -- Posiciones fiscales (mapeo de impuestos) CREATE TABLE fiscal_positions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Identificación name VARCHAR(200) NOT NULL, code VARCHAR(50), -- Ámbito company_id UUID NOT NULL REFERENCES companies(id), country_id UUID REFERENCES countries(id), country_group_id UUID, -- Para grupos como "EU" -- Configuración auto_apply BOOLEAN DEFAULT false, -- Aplicar automáticamente vat_required BOOLEAN DEFAULT false, -- Requiere VAT -- Para matching automático zip_from VARCHAR(20), zip_to VARCHAR(20), -- Ordenamiento sequence INTEGER DEFAULT 10, -- Estado is_active BOOLEAN DEFAULT true, -- Auditoría created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_by UUID NOT NULL ); -- Mapeo de impuestos en posición fiscal CREATE TABLE fiscal_position_tax_mappings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), fiscal_position_id UUID NOT NULL REFERENCES fiscal_positions(id) ON DELETE CASCADE, -- Mapeo tax_src_id UUID NOT NULL, -- Impuesto origen tax_dest_id UUID, -- Impuesto destino (NULL = no aplicar) CONSTRAINT uq_fp_tax_mapping UNIQUE (fiscal_position_id, tax_src_id) ); -- Mapeo de cuentas en posición fiscal CREATE TABLE fiscal_position_account_mappings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), fiscal_position_id UUID NOT NULL REFERENCES fiscal_positions(id) ON DELETE CASCADE, -- Mapeo account_src_id UUID NOT NULL, -- Cuenta origen account_dest_id UUID NOT NULL, -- Cuenta destino CONSTRAINT uq_fp_account_mapping UNIQUE (fiscal_position_id, account_src_id) ); -- Índices CREATE INDEX idx_fiscal_pos_company ON fiscal_positions(company_id); CREATE INDEX idx_fiscal_pos_country ON fiscal_positions(country_id); CREATE INDEX idx_fiscal_pos_auto ON fiscal_positions(auto_apply) WHERE auto_apply = true; COMMENT ON TABLE fiscal_positions IS 'Posiciones fiscales para mapeo automático de impuestos y cuentas según cliente.'; ``` ### 2.7 Tipos de Documento (LATAM) ```sql -- Tipos de documento fiscal (facturas, NC, ND, etc.) CREATE TABLE document_types ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Identificación code VARCHAR(20) NOT NULL, name VARCHAR(200) NOT NULL, -- País country_id UUID REFERENCES countries(id), -- Clasificación internal_type VARCHAR(50) NOT NULL, -- invoice, credit_note, debit_note doc_code_prefix VARCHAR(10), -- Prefijo (FA, NC, ND) -- Configuración active BOOLEAN DEFAULT true, -- Ordenamiento sequence INTEGER DEFAULT 10, -- Campos específicos (JSONB) country_specific_data JSONB DEFAULT '{}', -- Auditoría created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uq_document_type_code UNIQUE (country_id, code) ); -- Índices CREATE INDEX idx_doc_types_country ON document_types(country_id); CREATE INDEX idx_doc_types_internal ON document_types(internal_type); COMMENT ON TABLE document_types IS 'Tipos de documento fiscal por país (facturas A/B/C en Argentina, etc.)'; ``` ### 2.8 Bancos por País ```sql -- Extensión de tabla de bancos para datos de localización ALTER TABLE banks ADD COLUMN IF NOT EXISTS country_id UUID REFERENCES countries(id); ALTER TABLE banks ADD COLUMN IF NOT EXISTS country_specific_code VARCHAR(50); -- Código local (ej: código CLABE en México) ALTER TABLE banks ADD COLUMN IF NOT EXISTS country_specific_data JSONB DEFAULT '{}'; -- Índice por país CREATE INDEX IF NOT EXISTS idx_banks_country ON banks(country_id); COMMENT ON COLUMN banks.country_specific_code IS 'Código específico del país (ej: código interbancario en México)'; ``` ### 2.9 Formato de Dirección por País ```sql -- Configuración de formato de dirección por país CREATE TABLE address_formats ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), country_id UUID NOT NULL REFERENCES countries(id) UNIQUE, -- Formato de dirección address_format TEXT NOT NULL, -- Template con placeholders -- Campos requeridos street_required BOOLEAN DEFAULT true, city_required BOOLEAN DEFAULT true, state_required BOOLEAN DEFAULT false, zip_required BOOLEAN DEFAULT false, -- Configuración adicional zip_regex VARCHAR(100), -- Validación de código postal phone_format VARCHAR(100), -- Formato de teléfono -- Auditoría created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Datos iniciales INSERT INTO address_formats (country_id, address_format, state_required, zip_required) VALUES ((SELECT id FROM countries WHERE code = 'MX'), '%(street)s\n%(street2)s\n%(zip)s %(city)s, %(state_name)s\n%(country_name)s', true, true), ((SELECT id FROM countries WHERE code = 'US'), '%(street)s\n%(street2)s\n%(city)s, %(state_code)s %(zip)s\n%(country_name)s', true, true), ((SELECT id FROM countries WHERE code = 'ES'), '%(street)s\n%(street2)s\n%(zip)s %(city)s\n%(state_name)s\n%(country_name)s', false, true); COMMENT ON TABLE address_formats IS 'Formato de presentación de direcciones por país.'; ``` --- ## Parte 3: Interfaces de Dominio ### 3.1 Entidades de Localización ```typescript // src/modules/localization/domain/entities/localization-template.entity.ts export interface LocalizationTemplateProps { id: string; code: string; name: string; countryId: string; parentTemplateId?: string; sequence: number; codeDigits: number; currencyId?: string; status: LocalizationTemplateStatus; version?: string; description?: string; isActive: boolean; createdAt: Date; createdBy: string; updatedAt: Date; updatedBy: string; } export enum LocalizationTemplateStatus { DRAFT = 'draft', ACTIVE = 'active', DEPRECATED = 'deprecated', ARCHIVED = 'archived' } export class LocalizationTemplate extends Entity { get code(): string { return this.props.code; } get countryId(): string { return this.props.countryId; } get hasParent(): boolean { return !!this.props.parentTemplateId; } get isActive(): boolean { return this.props.isActive && this.props.status === LocalizationTemplateStatus.ACTIVE; } public static create(props: Omit): LocalizationTemplate { return new LocalizationTemplate({ ...props, id: crypto.randomUUID(), createdAt: new Date(), updatedAt: new Date() }); } } ``` ### 3.2 Value Objects ```typescript // src/modules/localization/domain/value-objects/identification-number.vo.ts export class IdentificationNumber { private readonly value: string; private readonly type: IdentificationType; private readonly countryCode: string; private constructor(value: string, type: IdentificationType, countryCode: string) { this.value = value; this.type = type; this.countryCode = countryCode; } get raw(): string { return this.value; } get formatted(): string { return this.format(); } get isVat(): boolean { return this.type.isVat; } private format(): string { // Formateo según país switch (this.countryCode) { case 'MX': return this.formatMexicanRFC(); case 'AR': return this.formatArgentineCUIT(); case 'ES': return this.formatSpanishNIF(); default: return this.value; } } private formatMexicanRFC(): string { // RFC: XXXX-XXXXXX-XXX if (this.value.length === 13) { return `${this.value.slice(0, 4)}-${this.value.slice(4, 10)}-${this.value.slice(10)}`; } if (this.value.length === 12) { return `${this.value.slice(0, 3)}-${this.value.slice(3, 9)}-${this.value.slice(9)}`; } return this.value; } private formatArgentineCUIT(): string { // CUIT: XX-XXXXXXXX-X if (this.value.length === 11) { return `${this.value.slice(0, 2)}-${this.value.slice(2, 10)}-${this.value.slice(10)}`; } return this.value; } private formatSpanishNIF(): string { // NIF ya viene formateado típicamente return this.value; } public static create( value: string, type: IdentificationType, countryCode: string ): IdentificationNumber { const cleanValue = value.replace(/[-\s]/g, '').toUpperCase(); // Validar según tipo if (type.validationRegex) { const regex = new RegExp(type.validationRegex); if (!regex.test(cleanValue)) { throw new DomainError(`Invalid ${type.name} format`); } } return new IdentificationNumber(cleanValue, type, countryCode); } } interface IdentificationType { code: string; name: string; isVat: boolean; validationRegex?: string; } ``` ### 3.3 Configuración de Plantilla ```typescript // src/modules/localization/domain/value-objects/template-config.vo.ts export interface ChartOfAccountsConfig { codeDigits: number; accounts: AccountTemplateData[]; defaultReceivableAccountCode: string; defaultPayableAccountCode: string; defaultIncomeAccountCode: string; defaultExpenseAccountCode: string; bankAccountPrefix: string; cashAccountPrefix: string; } export interface TaxConfig { taxes: TaxTemplateData[]; defaultSaleTaxCode: string; defaultPurchaseTaxCode: string; calculationRounding: 'round_per_line' | 'round_globally'; } export interface FiscalPositionConfig { positions: FiscalPositionData[]; } export interface CompanyConfig { angloSaxonAccounting: boolean; fiscalCountryId: string; additionalFields: Record; } export interface TemplateConfig { chartOfAccounts: ChartOfAccountsConfig; taxes: TaxConfig; fiscalPositions: FiscalPositionConfig; company: CompanyConfig; documentTypes?: DocumentTypeData[]; identificationTypes?: IdentificationTypeData[]; banks?: BankData[]; } // Datos de plantilla de cuenta interface AccountTemplateData { code: string; name: string; accountType: string; reconcile: boolean; tags?: string[]; } // Datos de plantilla de impuesto interface TaxTemplateData { code: string; name: string; description?: string; amountType: 'percent' | 'fixed' | 'division' | 'group'; amount: number; taxUse: 'sale' | 'purchase' | 'none'; tags?: string[]; countrySpecificData?: Record; } ``` ### 3.4 Servicio de Localización ```typescript // src/modules/localization/domain/services/localization.service.ts import { Injectable } from '@nestjs/common'; @Injectable() export class LocalizationService { constructor( private readonly templateRepository: LocalizationTemplateRepository, private readonly companyRepository: CompanyRepository, private readonly accountService: AccountService, private readonly taxService: TaxService, private readonly fiscalPositionService: FiscalPositionService ) {} /** * Obtener plantillas disponibles para un país */ async getTemplatesForCountry(countryId: string): Promise { return this.templateRepository.findByCountry(countryId); } /** * Obtener la mejor plantilla para un país */ async guessTemplate(countryCode: string): Promise { const templates = await this.templateRepository.findByCountryCode(countryCode); // Ordenar por secuencia y retornar la primera return templates.sort((a, b) => a.sequence - b.sequence)[0] || null; } /** * Cargar plantilla de localización en una empresa */ async loadTemplate( templateCode: string, companyId: string, options?: LoadTemplateOptions ): Promise { const template = await this.templateRepository.findByCode(templateCode); if (!template) { throw new NotFoundException(`Template ${templateCode} not found`); } const company = await this.companyRepository.findById(companyId); if (!company) { throw new NotFoundException(`Company ${companyId} not found`); } // Verificar si ya tiene contabilidad if (options?.forceCreate !== true) { const hasAccounting = await this.accountService.companyHasAccounting(companyId); if (hasAccounting) { throw new ConflictError('Company already has accounting data. Use forceCreate to override.'); } } // Obtener configuración completa (con herencia) const config = await this.getTemplateConfig(template); // Cargar en orden de dependencias const result: LocalizationResult = { templateCode, companyId, accountsCreated: 0, taxesCreated: 0, fiscalPositionsCreated: 0, errors: [] }; try { // 1. Pre-procesamiento await this.preLoadData(company, config); // 2. Cargar cuentas result.accountsCreated = await this.loadAccounts(company, config.chartOfAccounts); // 3. Cargar impuestos result.taxesCreated = await this.loadTaxes(company, config.taxes); // 4. Cargar posiciones fiscales result.fiscalPositionsCreated = await this.loadFiscalPositions(company, config.fiscalPositions); // 5. Configurar empresa await this.configureCompany(company, template, config.company); // 6. Post-procesamiento await this.postLoadData(company, config); // 7. Cargar traducciones if (options?.loadTranslations !== false) { await this.loadTranslations(template, company); } } catch (error) { result.errors.push(error.message); throw error; } return result; } /** * Obtener configuración completa de plantilla (resolviendo herencia) */ private async getTemplateConfig(template: LocalizationTemplate): Promise { const configs: TemplateConfig[] = []; // Construir cadena de herencia let current: LocalizationTemplate | null = template; while (current) { const templateData = await this.templateRepository.getTemplateData(current.id); configs.unshift(this.parseTemplateData(templateData)); if (current.hasParent) { current = await this.templateRepository.findById(current.props.parentTemplateId!); } else { current = null; } } // Merge de configuraciones (hijo sobrescribe padre) return configs.reduce((merged, config) => this.mergeConfigs(merged, config), {} as TemplateConfig); } /** * Pre-procesamiento antes de cargar datos */ private async preLoadData(company: Company, config: TemplateConfig): Promise { // Normalizar códigos de cuenta según code_digits config.chartOfAccounts.accounts = config.chartOfAccounts.accounts.map(account => ({ ...account, code: this.normalizeAccountCode(account.code, config.chartOfAccounts.codeDigits) })); // Establecer país fiscal if (!company.fiscalCountryId) { await this.companyRepository.updateFiscalCountry(company.id, company.countryId); } } /** * Cargar cuentas contables */ private async loadAccounts(company: Company, config: ChartOfAccountsConfig): Promise { let created = 0; for (const accountData of config.accounts) { try { await this.accountService.createFromTemplate(company.id, accountData); created++; } catch (error) { // Log pero continuar console.warn(`Error creating account ${accountData.code}: ${error.message}`); } } return created; } /** * Cargar impuestos */ private async loadTaxes(company: Company, config: TaxConfig): Promise { let created = 0; for (const taxData of config.taxes) { try { await this.taxService.createFromTemplate(company.id, taxData); created++; } catch (error) { console.warn(`Error creating tax ${taxData.code}: ${error.message}`); } } return created; } /** * Cargar posiciones fiscales */ private async loadFiscalPositions(company: Company, config: FiscalPositionConfig): Promise { let created = 0; for (const fpData of config.positions) { try { await this.fiscalPositionService.createFromTemplate(company.id, fpData); created++; } catch (error) { console.warn(`Error creating fiscal position ${fpData.name}: ${error.message}`); } } return created; } /** * Configurar empresa con datos de localización */ private async configureCompany( company: Company, template: LocalizationTemplate, config: CompanyConfig ): Promise { const updateData: Partial = { templateId: template.id, chartTemplateLoaded: true, taxCalculationRounding: config.angloSaxonAccounting ? 'round_globally' : 'round_per_line' }; // Asignar cuentas por defecto // ... (lógica de asignación) await this.companyRepository.updateLocalizationConfig(company.id, updateData); } /** * Post-procesamiento después de cargar datos */ private async postLoadData(company: Company, config: TemplateConfig): Promise { // Crear cuenta de ganancias no distribuidas si no existe await this.accountService.ensureUnaffectedEarningsAccount(company.id); // Configurar diarios con impuestos por defecto await this.taxService.assignDefaultTaxesToJournals(company.id); } /** * Normalizar código de cuenta */ private normalizeAccountCode(code: string, digits: number): string { return code.padEnd(digits, '0'); } /** * Merge de configuraciones */ private mergeConfigs(base: TemplateConfig, override: TemplateConfig): TemplateConfig { return { chartOfAccounts: { ...base.chartOfAccounts, ...override.chartOfAccounts, accounts: [ ...(base.chartOfAccounts?.accounts || []), ...(override.chartOfAccounts?.accounts || []) ] }, taxes: { ...base.taxes, ...override.taxes, taxes: [ ...(base.taxes?.taxes || []), ...(override.taxes?.taxes || []) ] }, fiscalPositions: { positions: [ ...(base.fiscalPositions?.positions || []), ...(override.fiscalPositions?.positions || []) ] }, company: { ...base.company, ...override.company } }; } } interface LoadTemplateOptions { forceCreate?: boolean; loadTranslations?: boolean; installDemo?: boolean; } interface LocalizationResult { templateCode: string; companyId: string; accountsCreated: number; taxesCreated: number; fiscalPositionsCreated: number; errors: string[]; } ``` ### 3.5 Servicio de Validación de Identificación ```typescript // src/modules/localization/domain/services/identification-validator.service.ts import { Injectable } from '@nestjs/common'; @Injectable() export class IdentificationValidatorService { /** * Validar número de identificación según país y tipo */ validate(value: string, countryCode: string, typeCode: string): ValidationResult { const cleanValue = this.clean(value); switch (countryCode) { case 'MX': return this.validateMexican(cleanValue, typeCode); case 'AR': return this.validateArgentine(cleanValue, typeCode); case 'ES': return this.validateSpanish(cleanValue, typeCode); default: return { isValid: true, value: cleanValue }; } } /** * Validar RFC mexicano */ private validateMexican(value: string, typeCode: string): ValidationResult { if (typeCode !== 'rfc') { return { isValid: true, value }; } // RFC persona moral: 3 letras + 6 dígitos + 3 homoclave // RFC persona física: 4 letras + 6 dígitos + 3 homoclave const rfcRegex = /^[A-Z&Ñ]{3,4}[0-9]{6}[A-Z0-9]{3}$/; if (!rfcRegex.test(value)) { return { isValid: false, value, error: 'Invalid RFC format' }; } // Validar fecha de nacimiento/constitución const dateStr = value.slice(-9, -3); if (!this.isValidDate(dateStr)) { return { isValid: false, value, error: 'Invalid date in RFC' }; } return { isValid: true, value }; } /** * Validar CUIT argentino */ private validateArgentine(value: string, typeCode: string): ValidationResult { if (typeCode !== 'cuit' && typeCode !== 'cuil') { return { isValid: true, value }; } // CUIT/CUIL: 11 dígitos con dígito verificador if (!/^\d{11}$/.test(value)) { return { isValid: false, value, error: 'CUIT must be 11 digits' }; } // Validar dígito verificador const multipliers = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2]; let sum = 0; for (let i = 0; i < 10; i++) { sum += parseInt(value[i]) * multipliers[i]; } const checkDigit = 11 - (sum % 11); const expectedCheck = checkDigit === 11 ? 0 : checkDigit === 10 ? 9 : checkDigit; if (parseInt(value[10]) !== expectedCheck) { return { isValid: false, value, error: 'Invalid CUIT check digit' }; } return { isValid: true, value }; } /** * Validar NIF/CIF español */ private validateSpanish(value: string, typeCode: string): ValidationResult { if (typeCode !== 'nif' && typeCode !== 'cif') { return { isValid: true, value }; } // NIF: 8 dígitos + letra const nifRegex = /^[0-9]{8}[A-Z]$/; // NIE: X/Y/Z + 7 dígitos + letra const nieRegex = /^[XYZ][0-9]{7}[A-Z]$/; // CIF: letra + 7 dígitos + letra/dígito const cifRegex = /^[ABCDEFGHJNPQRSUVW][0-9]{7}[A-J0-9]$/; if (!nifRegex.test(value) && !nieRegex.test(value) && !cifRegex.test(value)) { return { isValid: false, value, error: 'Invalid NIF/NIE/CIF format' }; } // Validar letra de control para NIF if (nifRegex.test(value)) { const letters = 'TRWAGMYFPDXBNJZSQVHLCKE'; const number = parseInt(value.slice(0, 8)); const expectedLetter = letters[number % 23]; if (value[8] !== expectedLetter) { return { isValid: false, value, error: 'Invalid NIF control letter' }; } } return { isValid: true, value }; } private clean(value: string): string { return value.replace(/[-\s.]/g, '').toUpperCase(); } private isValidDate(dateStr: string): boolean { // Formato YYMMDD const year = parseInt(dateStr.slice(0, 2)); const month = parseInt(dateStr.slice(2, 4)); const day = parseInt(dateStr.slice(4, 6)); if (month < 1 || month > 12) return false; if (day < 1 || day > 31) return false; return true; } } interface ValidationResult { isValid: boolean; value: string; error?: string; } ``` --- ## Parte 4: API REST ### 4.1 Controlador de Localización ```typescript // src/modules/localization/interfaces/http/localization.controller.ts @Controller('localization') @ApiTags('Localization') export class LocalizationController { constructor( private readonly localizationService: LocalizationService ) {} @Get('templates') @ApiOperation({ summary: 'List available localization templates' }) @ApiQuery({ name: 'countryId', required: false }) async getTemplates( @Query('countryId') countryId?: string ): Promise { if (countryId) { return this.localizationService.getTemplatesForCountry(countryId); } return this.localizationService.getAllTemplates(); } @Get('templates/:code') @ApiOperation({ summary: 'Get template details' }) async getTemplate( @Param('code') code: string ): Promise { return this.localizationService.getTemplateByCode(code); } @Post('templates/:code/load') @ApiOperation({ summary: 'Load localization template into company' }) async loadTemplate( @Param('code') code: string, @Body() dto: LoadTemplateDto, @CurrentUser() user: User ): Promise { return this.localizationService.loadTemplate(code, dto.companyId, { forceCreate: dto.forceCreate, loadTranslations: dto.loadTranslations, installDemo: dto.installDemo }); } @Get('guess/:countryCode') @ApiOperation({ summary: 'Guess best template for country' }) async guessTemplate( @Param('countryCode') countryCode: string ): Promise { return this.localizationService.guessTemplate(countryCode); } @Get('identification-types') @ApiOperation({ summary: 'List identification types' }) @ApiQuery({ name: 'countryId', required: false }) async getIdentificationTypes( @Query('countryId') countryId?: string ): Promise { return this.localizationService.getIdentificationTypes(countryId); } @Post('validate-identification') @ApiOperation({ summary: 'Validate identification number' }) async validateIdentification( @Body() dto: ValidateIdentificationDto ): Promise { return this.localizationService.validateIdentification( dto.value, dto.countryCode, dto.typeCode ); } @Get('fiscal-positions') @ApiOperation({ summary: 'List fiscal positions for company' }) async getFiscalPositions( @Query('companyId') companyId: string ): Promise { return this.localizationService.getFiscalPositions(companyId); } @Get('document-types') @ApiOperation({ summary: 'List document types for country' }) async getDocumentTypes( @Query('countryId') countryId: string ): Promise { return this.localizationService.getDocumentTypes(countryId); } } // DTOs class LoadTemplateDto { @IsUUID() companyId: string; @IsOptional() @IsBoolean() forceCreate?: boolean; @IsOptional() @IsBoolean() loadTranslations?: boolean; @IsOptional() @IsBoolean() installDemo?: boolean; } class ValidateIdentificationDto { @IsString() value: string; @IsString() @Length(2, 2) countryCode: string; @IsString() typeCode: string; } ``` --- ## Parte 5: Datos de Localización Iniciales ### 5.1 México (l10n_mx) ```typescript // src/modules/localization/data/templates/mx.template.ts export const mexicoTemplate: TemplateDefinition = { code: 'mx', name: 'Mexico - Plan de Cuentas General', countryCode: 'MX', codeDigits: 9, sequence: 1, chartOfAccounts: { defaultReceivableAccountCode: '105.01', defaultPayableAccountCode: '201.01', defaultIncomeAccountCode: '401.01', defaultExpenseAccountCode: '601.84', bankAccountPrefix: '102.01.0', cashAccountPrefix: '101.01.0', accounts: [ // Activo { code: '101', name: 'Caja', accountType: 'asset_cash', reconcile: false }, { code: '101.01', name: 'Caja y Efectivo', accountType: 'asset_cash', reconcile: false }, { code: '102', name: 'Bancos', accountType: 'asset_cash', reconcile: true }, { code: '102.01', name: 'Bancos Nacionales', accountType: 'asset_cash', reconcile: true }, { code: '105', name: 'Clientes', accountType: 'asset_receivable', reconcile: true }, { code: '105.01', name: 'Clientes Nacionales', accountType: 'asset_receivable', reconcile: true }, // ... más cuentas // Pasivo { code: '201', name: 'Proveedores', accountType: 'liability_payable', reconcile: true }, { code: '201.01', name: 'Proveedores Nacionales', accountType: 'liability_payable', reconcile: true }, { code: '205', name: 'Impuestos por Pagar', accountType: 'liability_current', reconcile: false }, { code: '205.01', name: 'IVA por Pagar', accountType: 'liability_current', reconcile: false }, // ... más cuentas // Ingresos { code: '401', name: 'Ingresos', accountType: 'income', reconcile: false }, { code: '401.01', name: 'Ventas Nacionales', accountType: 'income', reconcile: false }, // Gastos { code: '601', name: 'Gastos Generales', accountType: 'expense', reconcile: false }, { code: '601.84', name: 'Gastos de Operación', accountType: 'expense', reconcile: false }, ] }, taxes: { defaultSaleTaxCode: 'tax_iva_16_venta', defaultPurchaseTaxCode: 'tax_iva_16_compra', calculationRounding: 'round_globally', taxes: [ { code: 'tax_iva_16_venta', name: 'IVA 16% Venta', amountType: 'percent', amount: 16, taxUse: 'sale', countrySpecificData: { l10n_mx_factor_type: 'Tasa', l10n_mx_tax_type: 'iva' } }, { code: 'tax_iva_16_compra', name: 'IVA 16% Compra', amountType: 'percent', amount: 16, taxUse: 'purchase', countrySpecificData: { l10n_mx_factor_type: 'Tasa', l10n_mx_tax_type: 'iva' } }, { code: 'tax_iva_0_venta', name: 'IVA 0% Venta', amountType: 'percent', amount: 0, taxUse: 'sale', countrySpecificData: { l10n_mx_factor_type: 'Tasa', l10n_mx_tax_type: 'iva' } }, { code: 'tax_exento', name: 'Exento de IVA', amountType: 'percent', amount: 0, taxUse: 'sale', countrySpecificData: { l10n_mx_factor_type: 'Exento', l10n_mx_tax_type: 'iva' } }, { code: 'tax_ret_isr_10', name: 'Retención ISR 10%', amountType: 'percent', amount: -10, taxUse: 'purchase', countrySpecificData: { l10n_mx_factor_type: 'Tasa', l10n_mx_tax_type: 'isr' } } ] }, fiscalPositions: { positions: [ { name: 'Régimen Nacional', code: 'fp_nacional', autoApply: true, sequence: 1 }, { name: 'Exportación', code: 'fp_export', autoApply: false, vatRequired: false, sequence: 10, taxMappings: [ { srcCode: 'tax_iva_16_venta', destCode: 'tax_iva_0_venta' } ] } ] }, identificationTypes: [ { code: 'rfc', name: 'RFC', description: 'Registro Federal de Contribuyentes', isVat: true, validationRegex: '^[A-Z&Ñ]{3,4}[0-9]{6}[A-Z0-9]{3}$', sequence: 1 }, { code: 'curp', name: 'CURP', description: 'Clave Única de Registro de Población', isVat: false, validationRegex: '^[A-Z]{4}[0-9]{6}[HM][A-Z]{5}[0-9A-Z]{2}$', sequence: 2 } ], company: { angloSaxonAccounting: true, additionalFields: { l10n_mx_edi_pac: null, l10n_mx_edi_certificate: null } } }; ``` ### 5.2 Argentina (l10n_ar) ```typescript // src/modules/localization/data/templates/ar.template.ts // Plantilla base para Monotributista export const argentinaBaseTemplate: TemplateDefinition = { code: 'ar_base', name: 'Argentina - Plan General Monotributista', countryCode: 'AR', codeDigits: 12, sequence: 2, chartOfAccounts: { defaultReceivableAccountCode: '1.1.03.01.001', defaultPayableAccountCode: '2.1.01.01.001', // ... cuentas base }, taxes: { defaultSaleTaxCode: null, // Monotributista no tiene IVA defaultPurchaseTaxCode: null, taxes: [] }, identificationTypes: [ { code: 'cuit', name: 'CUIT', description: 'Clave Única de Identificación Tributaria', isVat: true, validationRegex: '^[0-9]{11}$', sequence: 1 }, { code: 'cuil', name: 'CUIL', description: 'Código Único de Identificación Laboral', isVat: false, validationRegex: '^[0-9]{11}$', sequence: 2 }, { code: 'dni', name: 'DNI', description: 'Documento Nacional de Identidad', isVat: false, sequence: 3 } ], company: { angloSaxonAccounting: false, additionalFields: { l10n_ar_afip_responsibility_type: 'monotributo', l10n_ar_gross_income_type: null } } }; // Plantilla para Responsable Inscripto (extiende ar_base) export const argentinaRITemplate: TemplateDefinition = { code: 'ar_ri', name: 'Argentina - Plan General Responsable Inscripto', countryCode: 'AR', parentCode: 'ar_base', codeDigits: 12, sequence: 1, taxes: { defaultSaleTaxCode: 'tax_iva_21_venta', defaultPurchaseTaxCode: 'tax_iva_21_compra', calculationRounding: 'round_globally', taxes: [ { code: 'tax_iva_21_venta', name: 'IVA 21% Venta', amountType: 'percent', amount: 21, taxUse: 'sale' }, { code: 'tax_iva_21_compra', name: 'IVA 21% Compra', amountType: 'percent', amount: 21, taxUse: 'purchase' }, { code: 'tax_iva_10_5', name: 'IVA 10.5%', amountType: 'percent', amount: 10.5, taxUse: 'sale' }, { code: 'tax_iva_27', name: 'IVA 27%', amountType: 'percent', amount: 27, taxUse: 'sale' } ] }, documentTypes: [ { code: 'FA-A', name: 'Factura A', internalType: 'invoice', docCodePrefix: 'FA-A', countrySpecificData: { l10n_ar_letter: 'A' } }, { code: 'NC-A', name: 'Nota de Crédito A', internalType: 'credit_note', docCodePrefix: 'NC-A', countrySpecificData: { l10n_ar_letter: 'A' } }, { code: 'FA-B', name: 'Factura B', internalType: 'invoice', docCodePrefix: 'FA-B', countrySpecificData: { l10n_ar_letter: 'B' } }, { code: 'NC-B', name: 'Nota de Crédito B', internalType: 'credit_note', docCodePrefix: 'NC-B', countrySpecificData: { l10n_ar_letter: 'B' } } ], company: { angloSaxonAccounting: false, additionalFields: { l10n_ar_afip_responsibility_type: 'responsable_inscripto' } } }; ``` --- ## Parte 6: Migraciones ### 6.1 Migración Principal ```sql -- migrations/YYYYMMDD_add_localization_system.sql -- Tipos enumerados DO $$ BEGIN CREATE TYPE localization_template_status AS ENUM ('draft', 'active', 'deprecated', 'archived'); EXCEPTION WHEN duplicate_object THEN null; END $$; DO $$ BEGIN CREATE TYPE localization_data_type AS ENUM ( 'chart_of_accounts', 'taxes', 'fiscal_positions', 'document_types', 'identification_types', 'banks', 'address_format', 'company_fields' ); EXCEPTION WHEN duplicate_object THEN null; END $$; -- Tabla de plantillas de localización CREATE TABLE IF NOT EXISTS localization_templates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(200) NOT NULL, country_id UUID NOT NULL REFERENCES countries(id), parent_template_id UUID REFERENCES localization_templates(id), sequence INTEGER DEFAULT 10, code_digits INTEGER DEFAULT 6, currency_id UUID REFERENCES currencies(id), status localization_template_status DEFAULT 'active', version VARCHAR(20), description TEXT, is_active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_by UUID NOT NULL, CONSTRAINT uq_template_country_code UNIQUE (country_id, code) ); -- Datos de plantilla CREATE TABLE IF NOT EXISTS localization_template_data ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), template_id UUID NOT NULL REFERENCES localization_templates(id) ON DELETE CASCADE, data_type localization_data_type NOT NULL, config_data JSONB NOT NULL, sequence INTEGER DEFAULT 10, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uq_template_data_type UNIQUE (template_id, data_type) ); -- Tipos de identificación CREATE TABLE IF NOT EXISTS identification_types ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code VARCHAR(20) NOT NULL, name VARCHAR(100) NOT NULL, description TEXT, country_id UUID REFERENCES countries(id), is_vat BOOLEAN DEFAULT false, validation_regex VARCHAR(200), validation_module VARCHAR(100), format_mask VARCHAR(100), sequence INTEGER DEFAULT 10, is_active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uq_identification_type_code UNIQUE (country_id, code) ); -- Configuración de empresa por localización CREATE TABLE IF NOT EXISTS company_localization_config ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE UNIQUE, template_id UUID REFERENCES localization_templates(id), country_id UUID NOT NULL REFERENCES countries(id), identification_type_id UUID REFERENCES identification_types(id), tax_id VARCHAR(50), tax_id_formatted VARCHAR(60), chart_template_loaded BOOLEAN DEFAULT false, fiscal_country_id UUID REFERENCES countries(id), bank_account_code_prefix VARCHAR(20), cash_account_code_prefix VARCHAR(20), default_sale_tax_id UUID, default_purchase_tax_id UUID, tax_calculation_rounding VARCHAR(20) DEFAULT 'round_per_line', country_specific_data JSONB DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_by UUID NOT NULL ); -- Posiciones fiscales CREATE TABLE IF NOT EXISTS fiscal_positions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(200) NOT NULL, code VARCHAR(50), company_id UUID NOT NULL REFERENCES companies(id), country_id UUID REFERENCES countries(id), country_group_id UUID, auto_apply BOOLEAN DEFAULT false, vat_required BOOLEAN DEFAULT false, zip_from VARCHAR(20), zip_to VARCHAR(20), sequence INTEGER DEFAULT 10, is_active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_by UUID NOT NULL ); CREATE TABLE IF NOT EXISTS fiscal_position_tax_mappings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), fiscal_position_id UUID NOT NULL REFERENCES fiscal_positions(id) ON DELETE CASCADE, tax_src_id UUID NOT NULL, tax_dest_id UUID, CONSTRAINT uq_fp_tax_mapping UNIQUE (fiscal_position_id, tax_src_id) ); CREATE TABLE IF NOT EXISTS fiscal_position_account_mappings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), fiscal_position_id UUID NOT NULL REFERENCES fiscal_positions(id) ON DELETE CASCADE, account_src_id UUID NOT NULL, account_dest_id UUID NOT NULL, CONSTRAINT uq_fp_account_mapping UNIQUE (fiscal_position_id, account_src_id) ); -- Tipos de documento CREATE TABLE IF NOT EXISTS document_types ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code VARCHAR(20) NOT NULL, name VARCHAR(200) NOT NULL, country_id UUID REFERENCES countries(id), internal_type VARCHAR(50) NOT NULL, doc_code_prefix VARCHAR(10), active BOOLEAN DEFAULT true, sequence INTEGER DEFAULT 10, country_specific_data JSONB DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uq_document_type_code UNIQUE (country_id, code) ); -- Formato de dirección CREATE TABLE IF NOT EXISTS address_formats ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), country_id UUID NOT NULL REFERENCES countries(id) UNIQUE, address_format TEXT NOT NULL, street_required BOOLEAN DEFAULT true, city_required BOOLEAN DEFAULT true, state_required BOOLEAN DEFAULT false, zip_required BOOLEAN DEFAULT false, zip_regex VARCHAR(100), phone_format VARCHAR(100), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Índices CREATE INDEX IF NOT EXISTS idx_loc_templates_country ON localization_templates(country_id); CREATE INDEX IF NOT EXISTS idx_loc_templates_parent ON localization_templates(parent_template_id); CREATE INDEX IF NOT EXISTS idx_loc_template_data_type ON localization_template_data(data_type); CREATE INDEX IF NOT EXISTS idx_id_types_country ON identification_types(country_id); CREATE INDEX IF NOT EXISTS idx_company_loc_template ON company_localization_config(template_id); CREATE INDEX IF NOT EXISTS idx_fiscal_pos_company ON fiscal_positions(company_id); CREATE INDEX IF NOT EXISTS idx_doc_types_country ON document_types(country_id); -- Datos iniciales globales INSERT INTO identification_types (code, name, is_vat, sequence) VALUES ('vat', 'Tax ID / VAT', true, 1), ('passport', 'Passport', false, 100), ('foreign_id', 'Foreign ID', false, 110) ON CONFLICT DO NOTHING; COMMENT ON TABLE localization_templates IS 'Plantillas de localización por país'; COMMENT ON TABLE identification_types IS 'Tipos de identificación fiscal por país'; COMMENT ON TABLE fiscal_positions IS 'Posiciones fiscales para mapeo de impuestos'; COMMENT ON TABLE document_types IS 'Tipos de documento fiscal por país'; ``` --- ## Parte 7: Resumen de Implementación ### 7.1 Gap Cubierto | Gap ID | Descripción | Estado | |--------|-------------|--------| | GAP-MGN-002-002 | Configuración de plantillas por país (l10n_*) | ✅ Especificado | ### 7.2 Componentes Definidos | Componente | Tipo | Descripción | |------------|------|-------------| | `localization_templates` | Tabla | Plantillas de localización | | `localization_template_data` | Tabla | Configuración por tipo | | `identification_types` | Tabla | Tipos de ID por país | | `company_localization_config` | Tabla | Config de empresa | | `fiscal_positions` | Tabla | Posiciones fiscales | | `document_types` | Tabla | Tipos de documento | | `address_formats` | Tabla | Formatos de dirección | | `LocalizationService` | Service | Carga de plantillas | | `IdentificationValidatorService` | Service | Validación de IDs | | `LocalizationController` | API | Endpoints REST | ### 7.3 Países Inicialmente Soportados | País | Código | Plantillas | Identificación | |------|--------|------------|----------------| | México | MX | `mx` | RFC | | Argentina | AR | `ar_base`, `ar_ri`, `ar_ex` | CUIT, CUIL, DNI | | España | ES | `es_common`, `es_pymes` | NIF, NIE, CIF | | Colombia | CO | `co` | NIT, CC, CE | | Chile | CL | `cl` | RUT | ### 7.4 Integración con Otros Módulos - **MGN-004 (Financiero)**: Plan de cuentas, impuestos, posiciones fiscales - **MGN-003 (Catálogos)**: Bancos, monedas, países - **MGN-006/007 (Compras/Ventas)**: Tipos de documento, posiciones fiscales --- ## Referencias - Odoo 18 `l10n_mx`, `l10n_ar`, `l10n_es` - Módulos de localización - Odoo 18 `account/models/chart_template.py` - Patrón @template - SPEC-PLANTILLAS-CUENTAS.md - Especificación de chart templates - SPEC-IMPUESTOS-AVANZADOS.md - Sistema de impuestos - ISO 3166-1 - Códigos de país