# SPEC-IMPUESTOS-AVANZADOS: Sistema de Impuestos Configurables ## Metadata - **Código**: SPEC-IMPUESTOS-AVANZADOS - **Versión**: 1.0.0 - **Fecha**: 2025-01-15 - **Gap Relacionado**: GAP-MGN-004-002 - **Módulo**: MGN-004 (Contabilidad) - **Prioridad**: P1 - **Story Points**: 8 - **Odoo Referencia**: account (account_tax, account_fiscal_position) --- ## 1. Resumen Ejecutivo ### 1.1 Descripción del Gap El sistema contable actual tiene impuestos básicos (IVA porcentual) pero carece de funcionalidades avanzadas como: impuestos en grupo, impuestos incluidos en precio, cálculo en cascada, posiciones fiscales automáticas, y soporte completo para localización mexicana (IEPS, retenciones ISR/IVA, base de caja). ### 1.2 Impacto en el Negocio | Aspecto | Sistema Actual | Con Impuestos Avanzados | |---------|---------------|------------------------| | Tipos de impuesto | Solo porcentual | Porcentual, fijo, división, grupo | | Precio con IVA | Manual | Automático (incluido/excluido) | | Retenciones | No soportado | ISR, IVA configurable | | IEPS | No soportado | Tasas por producto | | Posiciones fiscales | Manual | Automático por ubicación/cliente | | Base de caja | No | Impuesto exigible al pago | | Reportes DIOT | No | Tags automáticos | ### 1.3 Objetivos de la Especificación 1. Implementar motor de cálculo de impuestos multi-tipo 2. Soportar impuestos incluidos y excluidos del precio 3. Configurar posiciones fiscales con mapeo automático 4. Implementar retenciones (ISR, IVA) con factores negativos 5. Soportar IEPS con múltiples tasas 6. Integrar con CFDI 4.0 (TipoFactor, ObjectoImp) 7. Manejar impuestos en base de caja vs acumulado --- ## 2. Arquitectura de Datos ### 2.1 Diagrama del Sistema ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ SISTEMA DE IMPUESTOS AVANZADOS │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ GRUPOS DE IMPUESTOS │ │ │ │ │ │ │ │ IVA 16% IEPS Retenciones Exento │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ │ │payable │ │payable │ │payable │ │ N/A │ │ │ │ │ │receivable│ │receivable│ │receivable│ │ │ │ │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ IMPUESTOS (account_tax) │ │ │ │ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ │ │ Tipo │ Monto │ Uso │ Incluido │ Cascada │ CABA │ │ │ │ │ ├───────────┼────────┼──────────┼──────────┼─────────┼────────┤ │ │ │ │ │ percent │ 16.0 │ sale │ No │ No │ on_pay │ │ │ │ │ │ percent │ 16.0 │ purchase │ No │ No │ on_pay │ │ │ │ │ │ percent │ -10.67 │ purchase │ No │ No │ on_pay │ │ │ │ │ │ fixed │ 5.00 │ sale │ Si │ No │ on_inv │ │ │ │ │ │ group │ --- │ sale │ --- │ --- │ --- │ │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ LÍNEAS DE DISTRIBUCIÓN │ │ │ │ │ │ │ │ Invoice Refund │ │ │ │ ┌───────────────────────────┐ ┌───────────────────────────┐│ │ │ │ │ Base: 100% → cuenta_base │ │ Base: 100% → cuenta_base ││ │ │ │ │ Tax: 100% → cuenta_iva │ │ Tax: 100% → cuenta_iva ││ │ │ │ └───────────────────────────┘ └───────────────────────────┘│ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ POSICIONES FISCALES │ │ │ │ │ │ │ │ Cliente Nacional Cliente Extranjero Zona Fronteriza │ │ │ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │ │ │ │ IVA 16% → 16% │ │ IVA 16% → 0% │ │ IVA 16% → 8% │ │ │ │ │ │ IEPS → IEPS │ │ IEPS → Exento │ │ IEPS → IEPS │ │ │ │ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` ### 2.2 Definición de Tablas ```sql -- ============================================================================= -- SCHEMA: accounting -- ============================================================================= -- ----------------------------------------------------------------------------- -- Tabla: tax_groups -- Descripción: Agrupación de impuestos por tipo (IVA, ISR, IEPS, etc.) -- ----------------------------------------------------------------------------- CREATE TABLE tax_groups ( -- Identificación id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id), -- Datos principales name VARCHAR(100) NOT NULL, sequence INTEGER DEFAULT 10, -- País de aplicación country_id UUID REFERENCES countries(id), -- Cuentas asociadas tax_payable_account_id UUID REFERENCES accounts(id), -- IVA por pagar tax_receivable_account_id UUID REFERENCES accounts(id), -- IVA a favor advance_tax_payment_account_id UUID REFERENCES accounts(id), -- Anticipos -- Configuración de presentación preceding_subtotal VARCHAR(100), -- Etiqueta subtotal anterior -- Auditoría created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), created_by UUID REFERENCES users(id), updated_by UUID REFERENCES users(id), -- Constraints CONSTRAINT uq_tax_groups_name UNIQUE(tenant_id, name, country_id) ); -- Índices CREATE INDEX idx_tax_groups_tenant ON tax_groups(tenant_id); CREATE INDEX idx_tax_groups_country ON tax_groups(country_id); -- RLS ALTER TABLE tax_groups ENABLE ROW LEVEL SECURITY; CREATE POLICY tax_groups_tenant_isolation ON tax_groups USING (tenant_id = current_setting('app.current_tenant')::UUID); -- ----------------------------------------------------------------------------- -- Tabla: taxes -- Descripción: Definición de impuestos individuales -- ----------------------------------------------------------------------------- CREATE TABLE taxes ( -- Identificación id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id), -- Datos principales name VARCHAR(100) NOT NULL, description TEXT, -- Tipo y cálculo amount_type tax_amount_type NOT NULL DEFAULT 'percent', amount DECIMAL(16,4) NOT NULL DEFAULT 0, -- Uso type_tax_use tax_use_type NOT NULL DEFAULT 'sale', tax_scope tax_scope_type DEFAULT 'consu', -- consu, service -- Grupo tax_group_id UUID NOT NULL REFERENCES tax_groups(id), -- Configuración de precio price_include_override tax_price_include_type, -- tax_included, tax_excluded, NULL -- Cascada include_base_amount BOOLEAN DEFAULT FALSE, -- Afecta base de impuestos siguientes is_base_affected BOOLEAN DEFAULT TRUE, -- Afectado por impuestos anteriores -- Exigibilidad (Cash Basis) tax_exigibility tax_exigibility_type DEFAULT 'on_invoice', cash_basis_transition_account_id UUID REFERENCES accounts(id), -- Impuestos hijo (para grupos) -- Se maneja en tabla separada children_taxes -- País country_id UUID NOT NULL REFERENCES countries(id), -- México CFDI l10n_mx_factor_type mx_factor_type DEFAULT 'Tasa', -- Tasa, Cuota, Exento l10n_mx_tax_type mx_tax_type, -- iva, isr, ieps, local -- Estado active BOOLEAN DEFAULT TRUE, sequence INTEGER DEFAULT 1, -- Auditoría created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), created_by UUID REFERENCES users(id), updated_by UUID REFERENCES users(id), -- Constraints CONSTRAINT uq_taxes_name UNIQUE(tenant_id, name, type_tax_use, country_id) ); -- Tipos ENUM CREATE TYPE tax_amount_type AS ENUM ('percent', 'fixed', 'division', 'group'); CREATE TYPE tax_use_type AS ENUM ('sale', 'purchase', 'none'); CREATE TYPE tax_scope_type AS ENUM ('consu', 'service'); CREATE TYPE tax_price_include_type AS ENUM ('tax_included', 'tax_excluded'); CREATE TYPE tax_exigibility_type AS ENUM ('on_invoice', 'on_payment'); CREATE TYPE mx_factor_type AS ENUM ('Tasa', 'Cuota', 'Exento'); CREATE TYPE mx_tax_type AS ENUM ('iva', 'isr', 'ieps', 'local'); -- Índices CREATE INDEX idx_taxes_tenant ON taxes(tenant_id); CREATE INDEX idx_taxes_group ON taxes(tax_group_id); CREATE INDEX idx_taxes_type_use ON taxes(type_tax_use); CREATE INDEX idx_taxes_country ON taxes(country_id); CREATE INDEX idx_taxes_active ON taxes(tenant_id, active) WHERE active = TRUE; -- RLS ALTER TABLE taxes ENABLE ROW LEVEL SECURITY; CREATE POLICY taxes_tenant_isolation ON taxes USING (tenant_id = current_setting('app.current_tenant')::UUID); -- ----------------------------------------------------------------------------- -- Tabla: tax_children -- Descripción: Relación de impuestos grupo con sus hijos -- ----------------------------------------------------------------------------- CREATE TABLE tax_children ( parent_tax_id UUID NOT NULL REFERENCES taxes(id) ON DELETE CASCADE, child_tax_id UUID NOT NULL REFERENCES taxes(id) ON DELETE CASCADE, sequence INTEGER DEFAULT 1, PRIMARY KEY (parent_tax_id, child_tax_id), CONSTRAINT chk_no_self_reference CHECK (parent_tax_id != child_tax_id) ); CREATE INDEX idx_tax_children_parent ON tax_children(parent_tax_id); CREATE INDEX idx_tax_children_child ON tax_children(child_tax_id); -- ----------------------------------------------------------------------------- -- Tabla: tax_repartition_lines -- Descripción: Distribución contable del impuesto -- ----------------------------------------------------------------------------- CREATE TABLE tax_repartition_lines ( -- Identificación id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tax_id UUID NOT NULL REFERENCES taxes(id) ON DELETE CASCADE, -- Tipo de documento y distribución document_type repartition_doc_type NOT NULL, -- invoice, refund repartition_type repartition_type NOT NULL, -- base, tax -- Factor de distribución factor_percent DECIMAL(16,12) NOT NULL DEFAULT 100.0, -- Cuenta destino (NULL para base sin cuenta) account_id UUID REFERENCES accounts(id), -- Orden sequence INTEGER DEFAULT 1, -- Auditoría created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE TYPE repartition_doc_type AS ENUM ('invoice', 'refund'); CREATE TYPE repartition_type AS ENUM ('base', 'tax'); CREATE INDEX idx_tax_repartition_tax ON tax_repartition_lines(tax_id); CREATE INDEX idx_tax_repartition_type ON tax_repartition_lines(document_type, repartition_type); -- ----------------------------------------------------------------------------- -- Tabla: tax_repartition_line_tags -- Descripción: Tags para reportes fiscales (DIOT, etc.) -- ----------------------------------------------------------------------------- CREATE TABLE tax_repartition_line_tags ( repartition_line_id UUID NOT NULL REFERENCES tax_repartition_lines(id) ON DELETE CASCADE, tag_id UUID NOT NULL REFERENCES account_tags(id) ON DELETE CASCADE, PRIMARY KEY (repartition_line_id, tag_id) ); -- ----------------------------------------------------------------------------- -- Tabla: fiscal_positions -- Descripción: Posiciones fiscales para mapeo automático de impuestos -- ----------------------------------------------------------------------------- CREATE TABLE fiscal_positions ( -- Identificación id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id), -- Datos principales name VARCHAR(100) NOT NULL, sequence INTEGER DEFAULT 1, -- Criterios de aplicación automática auto_apply BOOLEAN DEFAULT FALSE, country_id UUID REFERENCES countries(id), country_group_id UUID REFERENCES country_groups(id), state_ids UUID[], -- Array de estados/provincias zip_from VARCHAR(20), zip_to VARCHAR(20), -- Requisitos vat_required BOOLEAN DEFAULT FALSE, foreign_vat VARCHAR(50), -- RFC extranjero si aplica -- Estado active BOOLEAN DEFAULT TRUE, -- Auditoría created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), created_by UUID REFERENCES users(id), updated_by UUID REFERENCES users(id), -- Constraints CONSTRAINT uq_fiscal_positions_name UNIQUE(tenant_id, name) ); -- Índices CREATE INDEX idx_fiscal_positions_tenant ON fiscal_positions(tenant_id); CREATE INDEX idx_fiscal_positions_auto ON fiscal_positions(auto_apply) WHERE auto_apply = TRUE; CREATE INDEX idx_fiscal_positions_country ON fiscal_positions(country_id); -- RLS ALTER TABLE fiscal_positions ENABLE ROW LEVEL SECURITY; CREATE POLICY fiscal_positions_tenant_isolation ON fiscal_positions USING (tenant_id = current_setting('app.current_tenant')::UUID); -- ----------------------------------------------------------------------------- -- Tabla: fiscal_position_tax_mappings -- Descripción: Mapeo de impuestos origen → destino -- ----------------------------------------------------------------------------- CREATE TABLE fiscal_position_tax_mappings ( -- Identificación 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 REFERENCES taxes(id), -- Impuesto original tax_dest_id UUID REFERENCES taxes(id), -- Impuesto destino (NULL = quitar) -- Constraints CONSTRAINT uq_fp_tax_mapping UNIQUE(fiscal_position_id, tax_src_id, tax_dest_id) ); CREATE INDEX idx_fp_tax_mapping_position ON fiscal_position_tax_mappings(fiscal_position_id); CREATE INDEX idx_fp_tax_mapping_src ON fiscal_position_tax_mappings(tax_src_id); -- ----------------------------------------------------------------------------- -- Tabla: fiscal_position_account_mappings -- Descripción: Mapeo de cuentas origen → destino -- ----------------------------------------------------------------------------- CREATE TABLE fiscal_position_account_mappings ( -- Identificación 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 REFERENCES accounts(id), account_dest_id UUID NOT NULL REFERENCES accounts(id), -- Constraints CONSTRAINT uq_fp_account_mapping UNIQUE(fiscal_position_id, account_src_id) ); CREATE INDEX idx_fp_account_mapping_position ON fiscal_position_account_mappings(fiscal_position_id); ``` --- ## 3. Algoritmos de Cálculo ### 3.1 Motor de Cálculo de Impuestos ```python # services/tax_calculator.py from decimal import Decimal, ROUND_HALF_UP from typing import List, Dict, Optional from dataclasses import dataclass from enum import Enum class TaxAmountType(Enum): PERCENT = 'percent' FIXED = 'fixed' DIVISION = 'division' GROUP = 'group' class RoundingMethod(Enum): ROUND_PER_LINE = 'round_per_line' ROUND_GLOBALLY = 'round_globally' @dataclass class TaxResult: tax_id: str name: str amount: Decimal base: Decimal account_id: Optional[str] tax_group_id: str price_include: bool tax_exigibility: str repartition_line_id: str tag_ids: List[str] @dataclass class ComputeAllResult: total_excluded: Decimal total_included: Decimal total_void: Decimal base_tags: List[str] taxes: List[TaxResult] class TaxCalculator: """Motor de cálculo de impuestos compatible con Odoo.""" def __init__( self, precision_rounding: Decimal = Decimal('0.01'), rounding_method: RoundingMethod = RoundingMethod.ROUND_PER_LINE ): self.precision = precision_rounding self.rounding_method = rounding_method def compute_all( self, taxes: List[Dict], price_unit: Decimal, quantity: Decimal = Decimal('1'), currency: Optional[Dict] = None, product: Optional[Dict] = None, partner: Optional[Dict] = None, is_refund: bool = False, handle_price_include: bool = True ) -> ComputeAllResult: """ Calcula todos los impuestos para un precio unitario. Args: taxes: Lista de impuestos a aplicar price_unit: Precio unitario quantity: Cantidad currency: Moneda para redondeo product: Producto (para tags) partner: Cliente/Proveedor is_refund: Si es devolución handle_price_include: Si manejar precio incluido Returns: ComputeAllResult con totales y desglose de impuestos """ if not taxes: return ComputeAllResult( total_excluded=price_unit * quantity, total_included=price_unit * quantity, total_void=Decimal('0'), base_tags=[], taxes=[] ) # Fase 1: Aplanar y ordenar impuestos sorted_taxes, group_per_tax = self._flatten_and_sort(taxes) # Fase 2: Crear batches de cálculo batches = self._create_batches(sorted_taxes, handle_price_include) # Fase 3: Calcular cada impuesto tax_details = self._compute_tax_details( sorted_taxes=sorted_taxes, batches=batches, group_per_tax=group_per_tax, price_unit=price_unit, quantity=quantity, handle_price_include=handle_price_include ) # Fase 4: Aplicar distribución (repartition lines) results = self._apply_repartition( tax_details=tax_details, is_refund=is_refund ) # Fase 5: Calcular totales total_excluded = price_unit * quantity total_included = total_excluded total_void = Decimal('0') for detail in tax_details.values(): if detail['price_include']: total_excluded -= detail['tax_amount'] else: total_included += detail['tax_amount'] return ComputeAllResult( total_excluded=self._round(total_excluded), total_included=self._round(total_included), total_void=total_void, base_tags=[], taxes=results ) def _flatten_and_sort( self, taxes: List[Dict] ) -> tuple[List[Dict], Dict[str, Dict]]: """ Aplana impuestos grupo y ordena por secuencia. Ejemplo: Input: [G, B([A, D, F]), E, C] Output: [A, D, F, C, E, G] """ flat_taxes = [] group_per_tax = {} for tax in sorted(taxes, key=lambda t: t['sequence']): if tax['amount_type'] == TaxAmountType.GROUP.value: children = tax.get('children_taxes', []) for child in sorted(children, key=lambda t: t['sequence']): flat_taxes.append(child) group_per_tax[child['id']] = tax else: flat_taxes.append(tax) return flat_taxes, group_per_tax def _create_batches( self, sorted_taxes: List[Dict], handle_price_include: bool ) -> Dict[str, List[Dict]]: """ Agrupa impuestos en batches para cálculo conjunto. Criterios de mismo batch: - Mismo amount_type - Mismo price_include (si handle_price_include) - Mismo include_base_amount """ batches = {} current_batch = [] for tax in reversed(sorted_taxes): if not current_batch: current_batch = [tax] else: first = current_batch[0] same_batch = ( tax['amount_type'] == first['amount_type'] and (not handle_price_include or tax.get('price_include') == first.get('price_include')) and tax.get('include_base_amount') == first.get('include_base_amount') ) if same_batch: current_batch.insert(0, tax) else: for t in current_batch: batches[t['id']] = current_batch.copy() current_batch = [tax] for t in current_batch: batches[t['id']] = current_batch.copy() return batches def _compute_tax_details( self, sorted_taxes: List[Dict], batches: Dict[str, List[Dict]], group_per_tax: Dict[str, Dict], price_unit: Decimal, quantity: Decimal, handle_price_include: bool ) -> Dict[str, Dict]: """Calcula el detalle de cada impuesto.""" raw_base = price_unit * quantity tax_details = {} # Inicializar estructura for tax in sorted_taxes: tax_details[tax['id']] = { 'tax': tax, 'batch': batches.get(tax['id'], [tax]), 'group': group_per_tax.get(tax['id']), 'raw_base': raw_base, 'tax_amount': Decimal('0'), 'base': Decimal('0'), 'price_include': tax.get('price_include', False), 'extra_base_for_tax': Decimal('0'), 'extra_base_for_base': Decimal('0'), } # Fase 1: Impuestos fijos (orden inverso) for tax in reversed(sorted_taxes): if tax['amount_type'] == TaxAmountType.FIXED.value: detail = tax_details[tax['id']] sign = Decimal('-1') if price_unit < 0 else Decimal('1') detail['tax_amount'] = sign * quantity * Decimal(str(tax['amount'])) if self.rounding_method == RoundingMethod.ROUND_PER_LINE: detail['tax_amount'] = self._round(detail['tax_amount']) # Fase 2: Impuestos incluidos en precio (orden inverso) for tax in reversed(sorted_taxes): if tax.get('price_include') and tax['amount_type'] != TaxAmountType.FIXED.value: detail = tax_details[tax['id']] batch = detail['batch'] if tax['amount_type'] == TaxAmountType.PERCENT.value: total_pct = sum( Decimal(str(t['amount'])) for t in batch ) / Decimal('100') factor = Decimal('1') / (Decimal('1') + total_pct) detail['tax_amount'] = ( detail['raw_base'] * factor * Decimal(str(tax['amount'])) / Decimal('100') ) elif tax['amount_type'] == TaxAmountType.DIVISION.value: detail['tax_amount'] = ( detail['raw_base'] * Decimal(str(tax['amount'])) / Decimal('100') ) if self.rounding_method == RoundingMethod.ROUND_PER_LINE: detail['tax_amount'] = self._round(detail['tax_amount']) # Fase 3: Impuestos excluidos del precio (orden normal) for tax in sorted_taxes: if not tax.get('price_include') and tax['amount_type'] != TaxAmountType.FIXED.value: detail = tax_details[tax['id']] batch = detail['batch'] # Calcular base afectada por impuestos anteriores affected_base = detail['raw_base'] + detail['extra_base_for_tax'] if tax['amount_type'] == TaxAmountType.PERCENT.value: detail['tax_amount'] = ( affected_base * Decimal(str(tax['amount'])) / Decimal('100') ) elif tax['amount_type'] == TaxAmountType.DIVISION.value: total_pct = sum( Decimal(str(t['amount'])) for t in batch ) / Decimal('100') multiplicator = Decimal('1') - total_pct if total_pct != Decimal('1') else Decimal('1') detail['tax_amount'] = ( affected_base * Decimal(str(tax['amount'])) / Decimal('100') / multiplicator ) if self.rounding_method == RoundingMethod.ROUND_PER_LINE: detail['tax_amount'] = self._round(detail['tax_amount']) # Propagar a impuestos siguientes si include_base_amount if tax.get('include_base_amount'): for other_tax in sorted_taxes: if other_tax['sequence'] > tax['sequence']: other_detail = tax_details[other_tax['id']] if other_tax.get('is_base_affected', True): other_detail['extra_base_for_tax'] += detail['tax_amount'] # Calcular base para cada impuesto for tax_id, detail in tax_details.items(): tax = detail['tax'] batch = detail['batch'] total_batch_tax = sum( tax_details[t['id']]['tax_amount'] for t in batch ) if detail['price_include']: detail['base'] = detail['raw_base'] - total_batch_tax else: detail['base'] = detail['raw_base'] detail['base'] += detail['extra_base_for_base'] return tax_details def _apply_repartition( self, tax_details: Dict[str, Dict], is_refund: bool ) -> List[TaxResult]: """Aplica líneas de distribución y genera resultados.""" results = [] doc_type = 'refund' if is_refund else 'invoice' for tax_id, detail in tax_details.items(): tax = detail['tax'] repartition_lines = tax.get('repartition_lines', []) for line in repartition_lines: if line['document_type'] != doc_type: continue if line['repartition_type'] == 'tax': factor = Decimal(str(line['factor_percent'])) / Decimal('100') amount = detail['tax_amount'] * factor if self.rounding_method == RoundingMethod.ROUND_PER_LINE: amount = self._round(amount) results.append(TaxResult( tax_id=tax_id, name=tax['name'], amount=amount, base=detail['base'], account_id=line.get('account_id'), tax_group_id=tax['tax_group_id'], price_include=detail['price_include'], tax_exigibility=tax.get('tax_exigibility', 'on_invoice'), repartition_line_id=line['id'], tag_ids=line.get('tag_ids', []) )) return results def _round(self, value: Decimal) -> Decimal: """Redondea al precision configurado.""" return value.quantize(self.precision, rounding=ROUND_HALF_UP) ``` ### 3.2 Detector de Posición Fiscal ```python # services/fiscal_position_detector.py from typing import Optional, List, Dict from dataclasses import dataclass @dataclass class FiscalPositionMatch: position_id: str name: str score: int reason: str class FiscalPositionDetector: """Detecta la posición fiscal automática para un partner.""" def __init__(self, db_session): self.db = db_session async def detect( self, partner_id: str, delivery_address_id: Optional[str] = None, company_id: str = None ) -> Optional[FiscalPositionMatch]: """ Detecta posición fiscal usando ranking de criterios. Ranking (mayor prioridad primero): 1. Posición manual asignada al partner 2. VAT requerido (score +2) 3. Código postal en rango (score +2) 4. Estado/Provincia coincide (score +2) 5. País coincide (score +2) 6. Grupo de países coincide (score +2) 7. Menor secuencia """ # Verificar posición manual partner = await self.db.get_partner(partner_id) if partner.get('fiscal_position_id'): return FiscalPositionMatch( position_id=partner['fiscal_position_id'], name=partner['fiscal_position_name'], score=100, reason='Asignación manual' ) # Obtener dirección relevante address = await self._get_relevant_address( partner_id, delivery_address_id ) # Obtener posiciones fiscales activas con auto_apply positions = await self.db.get_fiscal_positions( company_id=company_id, auto_apply=True ) # Calcular score para cada posición scored_positions = [] for pos in positions: score = 0 reasons = [] # VAT requerido if pos['vat_required']: if partner.get('vat'): score += 2 reasons.append('VAT presente') else: continue # Descalificar si requiere VAT y no tiene # Código postal if pos['zip_from'] and pos['zip_to']: if address.get('zip'): if pos['zip_from'] <= address['zip'] <= pos['zip_to']: score += 2 reasons.append(f"CP {address['zip']} en rango") else: continue # Descalificar si CP fuera de rango # Estado/Provincia if pos['state_ids']: if address.get('state_id') in pos['state_ids']: score += 2 reasons.append('Estado coincide') else: continue # Descalificar si estado no coincide # País if pos['country_id']: if address.get('country_id') == pos['country_id']: score += 2 reasons.append('País coincide') else: continue # Descalificar si país no coincide # Grupo de países if pos['country_group_id']: country_in_group = await self.db.is_country_in_group( address.get('country_id'), pos['country_group_id'] ) if country_in_group: score += 2 reasons.append('Grupo de países coincide') else: continue # Si llegó aquí, es candidato scored_positions.append(FiscalPositionMatch( position_id=pos['id'], name=pos['name'], score=score, reason=', '.join(reasons) if reasons else 'Match genérico' )) if not scored_positions: return None # Ordenar por score (desc) y luego por sequence (asc) scored_positions.sort( key=lambda x: (-x.score, positions[x.position_id]['sequence']) ) return scored_positions[0] async def _get_relevant_address( self, partner_id: str, delivery_address_id: Optional[str] ) -> Dict: """Obtiene la dirección relevante para determinar posición fiscal.""" if delivery_address_id: return await self.db.get_address(delivery_address_id) partner = await self.db.get_partner(partner_id) # Preferir dirección de facturación if partner.get('invoice_address_id'): return await self.db.get_address(partner['invoice_address_id']) # Usar dirección del partner return { 'country_id': partner.get('country_id'), 'state_id': partner.get('state_id'), 'zip': partner.get('zip'), } ``` ### 3.3 Mapeador de Impuestos ```python # services/tax_mapper.py from typing import List, Dict, Optional class TaxMapper: """Mapea impuestos usando posición fiscal.""" def __init__(self, db_session): self.db = db_session async def map_taxes( self, tax_ids: List[str], fiscal_position_id: Optional[str] ) -> List[str]: """ Mapea impuestos según posición fiscal. Args: tax_ids: Lista de IDs de impuestos originales fiscal_position_id: ID de posición fiscal (opcional) Returns: Lista de IDs de impuestos mapeados """ if not fiscal_position_id or not tax_ids: return tax_ids # Obtener mapeos de la posición fiscal mappings = await self.db.get_tax_mappings(fiscal_position_id) # Crear diccionario de mapeo tax_map = {} for mapping in mappings: src_id = mapping['tax_src_id'] dest_id = mapping['tax_dest_id'] # Puede ser None (quitar impuesto) if src_id not in tax_map: tax_map[src_id] = [] if dest_id: tax_map[src_id].append(dest_id) # Aplicar mapeo result = [] for tax_id in tax_ids: if tax_id in tax_map: # Reemplazar con impuestos mapeados result.extend(tax_map[tax_id]) else: # Mantener original si no hay mapeo result.append(tax_id) # Eliminar duplicados preservando orden seen = set() unique_result = [] for tax_id in result: if tax_id not in seen: seen.add(tax_id) unique_result.append(tax_id) return unique_result async def map_account( self, account_id: str, fiscal_position_id: Optional[str] ) -> str: """ Mapea cuenta según posición fiscal. Args: account_id: ID de cuenta original fiscal_position_id: ID de posición fiscal (opcional) Returns: ID de cuenta mapeada """ if not fiscal_position_id: return account_id mapping = await self.db.get_account_mapping( fiscal_position_id, account_id ) return mapping['account_dest_id'] if mapping else account_id ``` --- ## 4. API REST ### 4.1 Endpoints de Impuestos ```yaml # Grupos de Impuestos POST /api/v1/tax-groups: summary: Crear grupo de impuestos body: name: string (required) country_id: uuid tax_payable_account_id: uuid tax_receivable_account_id: uuid sequence: integer response: TaxGroup GET /api/v1/tax-groups: summary: Listar grupos de impuestos params: country_id: uuid active: boolean response: TaxGroup[] # Impuestos POST /api/v1/taxes: summary: Crear impuesto body: name: string (required) amount_type: enum[percent, fixed, division, group] amount: decimal type_tax_use: enum[sale, purchase, none] tax_group_id: uuid (required) country_id: uuid (required) price_include_override: enum[tax_included, tax_excluded] include_base_amount: boolean tax_exigibility: enum[on_invoice, on_payment] cash_basis_transition_account_id: uuid l10n_mx_factor_type: enum[Tasa, Cuota, Exento] l10n_mx_tax_type: enum[iva, isr, ieps, local] repartition_lines: RepartitionLine[] children_tax_ids: uuid[] # Solo para tipo 'group' response: Tax GET /api/v1/taxes: summary: Listar impuestos params: type_tax_use: enum country_id: uuid active: boolean tax_group_id: uuid response: Tax[] GET /api/v1/taxes/{id}: summary: Obtener impuesto response: Tax PUT /api/v1/taxes/{id}: summary: Actualizar impuesto body: Partial response: Tax DELETE /api/v1/taxes/{id}: summary: Desactivar impuesto response: { success: boolean } # Cálculo de Impuestos POST /api/v1/taxes/compute: summary: Calcular impuestos para una línea body: tax_ids: uuid[] price_unit: decimal quantity: decimal currency_id: uuid product_id: uuid partner_id: uuid is_refund: boolean response: total_excluded: decimal total_included: decimal taxes: TaxResult[] # Posiciones Fiscales POST /api/v1/fiscal-positions: summary: Crear posición fiscal body: name: string (required) auto_apply: boolean country_id: uuid country_group_id: uuid state_ids: uuid[] zip_from: string zip_to: string vat_required: boolean tax_mappings: TaxMapping[] account_mappings: AccountMapping[] response: FiscalPosition GET /api/v1/fiscal-positions: summary: Listar posiciones fiscales params: active: boolean auto_apply: boolean response: FiscalPosition[] POST /api/v1/fiscal-positions/detect: summary: Detectar posición fiscal para partner body: partner_id: uuid (required) delivery_address_id: uuid response: fiscal_position_id: uuid name: string reason: string POST /api/v1/fiscal-positions/{id}/map-taxes: summary: Mapear impuestos con posición fiscal body: tax_ids: uuid[] response: mapped_tax_ids: uuid[] ``` ### 4.2 Schemas de Respuesta ```typescript // types/tax.ts interface TaxGroup { id: string; name: string; sequence: number; country_id: string | null; tax_payable_account_id: string | null; tax_receivable_account_id: string | null; advance_tax_payment_account_id: string | null; preceding_subtotal: string | null; created_at: string; updated_at: string; } interface Tax { id: string; name: string; description: string | null; amount_type: 'percent' | 'fixed' | 'division' | 'group'; amount: number; type_tax_use: 'sale' | 'purchase' | 'none'; tax_scope: 'consu' | 'service' | null; tax_group_id: string; tax_group: TaxGroup; price_include: boolean; // Computado price_include_override: 'tax_included' | 'tax_excluded' | null; include_base_amount: boolean; is_base_affected: boolean; tax_exigibility: 'on_invoice' | 'on_payment'; cash_basis_transition_account_id: string | null; country_id: string; l10n_mx_factor_type: 'Tasa' | 'Cuota' | 'Exento'; l10n_mx_tax_type: 'iva' | 'isr' | 'ieps' | 'local' | null; active: boolean; sequence: number; repartition_lines: TaxRepartitionLine[]; children_taxes: Tax[]; // Solo para tipo 'group' created_at: string; updated_at: string; } interface TaxRepartitionLine { id: string; document_type: 'invoice' | 'refund'; repartition_type: 'base' | 'tax'; factor_percent: number; account_id: string | null; account: Account | null; sequence: number; tag_ids: string[]; } interface FiscalPosition { id: string; name: string; sequence: number; auto_apply: boolean; country_id: string | null; country_group_id: string | null; state_ids: string[]; zip_from: string | null; zip_to: string | null; vat_required: boolean; foreign_vat: string | null; active: boolean; tax_mappings: FiscalPositionTaxMapping[]; account_mappings: FiscalPositionAccountMapping[]; created_at: string; updated_at: string; } interface FiscalPositionTaxMapping { id: string; tax_src_id: string; tax_src: Tax; tax_dest_id: string | null; tax_dest: Tax | null; } interface FiscalPositionAccountMapping { id: string; account_src_id: string; account_src: Account; account_dest_id: string; account_dest: Account; } interface TaxComputeResult { total_excluded: number; total_included: number; total_void: number; base_tags: string[]; taxes: TaxResultLine[]; } interface TaxResultLine { tax_id: string; name: string; amount: number; base: number; account_id: string | null; tax_group_id: string; price_include: boolean; tax_exigibility: 'on_invoice' | 'on_payment'; repartition_line_id: string; tag_ids: string[]; } ``` --- ## 5. Interfaz de Usuario ### 5.1 Configuración de Impuestos ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ Contabilidad > Configuración > Impuestos │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ [+ Nuevo Impuesto] [Filtros ▼] [Agrupar ▼] 🔍 Buscar... │ │ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ Grupo: IVA │ │ │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ ☑ │ Nombre │ Tipo │ Monto │ Uso │ Incluido │ Act │ │ │ │───│─────────────────│──────────│─────────│─────────│──────────│─────│ │ │ │ □ │ IVA 16% │ Porcent. │ 16.00% │ Ventas │ No │ ✓ │ │ │ │ □ │ IVA 16% │ Porcent. │ 16.00% │ Compras │ No │ ✓ │ │ │ │ □ │ IVA 8% │ Porcent. │ 8.00% │ Ventas │ No │ ✓ │ │ │ │ □ │ IVA 0% │ Porcent. │ 0.00% │ Ventas │ No │ ✓ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ Grupo: Retenciones │ │ │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ □ │ Ret. IVA 10.67% │ Porcent. │ -10.67% │ Compras │ No │ ✓ │ │ │ │ □ │ Ret. ISR 10% │ Porcent. │ -10.00% │ Compras │ No │ ✓ │ │ │ │ □ │ Ret. ISR 1.25% │ Porcent. │ -1.25% │ Compras │ No │ ✓ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ Grupo: IEPS │ │ │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ □ │ IEPS 8% │ Porcent. │ 8.00% │ Ventas │ No │ ✓ │ │ │ │ □ │ IEPS 25% │ Porcent. │ 25.00% │ Ventas │ No │ ✓ │ │ │ │ □ │ IEPS 53% │ Porcent. │ 53.00% │ Ventas │ No │ ✓ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` ### 5.2 Formulario de Impuesto ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ Impuesto: IVA 16% Ventas [Guardar] [✕] │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────────────────────┬──────────────────────────────────┐ │ │ │ Información General │ Configuración México │ │ │ ├──────────────────────────────────┼──────────────────────────────────┤ │ │ │ │ │ │ │ │ Nombre: [IVA 16% ] │ Tipo Factor: (●) Tasa │ │ │ │ │ ( ) Cuota │ │ │ │ Tipo Cálculo: [Porcentaje ▼] │ ( ) Exento │ │ │ │ │ │ │ │ │ Monto: [16.00 ] % │ Tipo Impuesto: [IVA ▼] │ │ │ │ │ │ │ │ │ Ámbito: [Ventas ▼] │ │ │ │ │ │ │ │ │ │ Grupo: [IVA 16% ▼] │ │ │ │ │ │ │ │ │ └──────────────────────────────────┴──────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ Opciones Avanzadas │ │ │ ├──────────────────────────────────────────────────────────────────────┤ │ │ │ │ │ │ │ Precio: [ ] Impuesto incluido en precio │ │ │ │ │ │ │ │ Cascada: [ ] Afectar base de impuestos siguientes │ │ │ │ [✓] Afectado por impuestos anteriores │ │ │ │ │ │ │ │ Exigibilidad: (●) Al facturar │ │ │ │ ( ) Al cobrar (Base de caja) │ │ │ │ │ │ │ │ Cuenta transición: [118.01 - IVA pagado pendiente ▼] │ │ │ │ │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ Distribución Contable [+ Línea] │ │ │ ├──────────────────────────────────────────────────────────────────────┤ │ │ │ │ │ │ │ Facturas: │ │ │ │ ┌────────────┬────────────┬──────────────────────────────┬────────┐ │ │ │ │ │ Tipo │ % │ Cuenta │ Tags │ │ │ │ │ ├────────────┼────────────┼──────────────────────────────┼────────┤ │ │ │ │ │ Base │ 100% │ │ +DIOT │ │ │ │ │ │ Impuesto │ 100% │ 208.01 - IVA trasladado │ +DIOT │ │ │ │ │ └────────────┴────────────┴──────────────────────────────┴────────┘ │ │ │ │ │ │ │ │ Notas de Crédito: │ │ │ │ ┌────────────┬────────────┬──────────────────────────────┬────────┐ │ │ │ │ │ Tipo │ % │ Cuenta │ Tags │ │ │ │ │ ├────────────┼────────────┼──────────────────────────────┼────────┤ │ │ │ │ │ Base │ 100% │ │ +DIOT │ │ │ │ │ │ Impuesto │ 100% │ 208.01 - IVA trasladado │ +DIOT │ │ │ │ │ └────────────┴────────────┴──────────────────────────────┴────────┘ │ │ │ │ │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` ### 5.3 Posiciones Fiscales ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ Posición Fiscal: Cliente Extranjero [Guardar] [✕] │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ Información General │ │ │ ├──────────────────────────────────────────────────────────────────────┤ │ │ │ │ │ │ │ Nombre: [Cliente Extranjero ] │ │ │ │ │ │ │ │ [✓] Detectar automáticamente │ │ │ │ │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ Criterios de Detección │ │ │ ├──────────────────────────────────────────────────────────────────────┤ │ │ │ │ │ │ │ País: [≠ México ▼] │ │ │ │ │ │ │ │ Grupo de países: [ ▼] │ │ │ │ │ │ │ │ Estados: [ ▼] │ │ │ │ │ │ │ │ Código Postal: Desde [ ] Hasta [ ] │ │ │ │ │ │ │ │ [ ] Requiere RFC/VAT │ │ │ │ │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ Mapeo de Impuestos [+ Mapeo] │ │ │ ├──────────────────────────────────────────────────────────────────────┤ │ │ │ │ │ │ │ ┌──────────────────────────┬──────────────────────────┬───────────┐ │ │ │ │ │ Impuesto Origen │ Impuesto Destino │ Acciones │ │ │ │ │ ├──────────────────────────┼──────────────────────────┼───────────┤ │ │ │ │ │ IVA 16% Ventas │ IVA 0% Exportación │ [🗑] │ │ │ │ │ │ IVA 8% Ventas │ IVA 0% Exportación │ [🗑] │ │ │ │ │ │ IEPS Ventas │ (Sin impuesto) │ [🗑] │ │ │ │ │ └──────────────────────────┴──────────────────────────┴───────────┘ │ │ │ │ │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ Mapeo de Cuentas [+ Mapeo] │ │ │ ├──────────────────────────────────────────────────────────────────────┤ │ │ │ │ │ │ │ ┌──────────────────────────┬──────────────────────────┬───────────┐ │ │ │ │ │ Cuenta Origen │ Cuenta Destino │ Acciones │ │ │ │ │ ├──────────────────────────┼──────────────────────────┼───────────┤ │ │ │ │ │ 401.01 - Ventas Nac. │ 401.02 - Ventas Exp. │ [🗑] │ │ │ │ │ └──────────────────────────┴──────────────────────────┴───────────┘ │ │ │ │ │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` --- ## 6. Datos de Referencia - México ### 6.1 Impuestos Predefinidos ```sql -- Insertar grupos de impuestos México INSERT INTO tax_groups (tenant_id, name, sequence, country_id) VALUES ('{tenant}', 'IVA 0%', 1, '{mx}'), ('{tenant}', 'IVA 8%', 2, '{mx}'), ('{tenant}', 'IVA 16%', 3, '{mx}'), ('{tenant}', 'Exento', 4, '{mx}'), ('{tenant}', 'Retención IVA', 10, '{mx}'), ('{tenant}', 'Retención ISR', 11, '{mx}'), ('{tenant}', 'IEPS 8%', 20, '{mx}'), ('{tenant}', 'IEPS 25%', 21, '{mx}'), ('{tenant}', 'IEPS 26.5%', 22, '{mx}'), ('{tenant}', 'IEPS 30%', 23, '{mx}'), ('{tenant}', 'IEPS 53%', 24, '{mx}'); -- IVA Ventas INSERT INTO taxes (tenant_id, name, amount_type, amount, type_tax_use, tax_group_id, country_id, tax_exigibility, l10n_mx_factor_type, l10n_mx_tax_type) VALUES ('{tenant}', 'IVA 16%', 'percent', 16.00, 'sale', '{iva_16_group}', '{mx}', 'on_payment', 'Tasa', 'iva'), ('{tenant}', 'IVA 8%', 'percent', 8.00, 'sale', '{iva_8_group}', '{mx}', 'on_payment', 'Tasa', 'iva'), ('{tenant}', 'IVA 0%', 'percent', 0.00, 'sale', '{iva_0_group}', '{mx}', 'on_invoice', 'Tasa', 'iva'), ('{tenant}', 'Exento', 'percent', 0.00, 'sale', '{exento_group}', '{mx}', 'on_invoice', 'Exento', 'iva'); -- IVA Compras INSERT INTO taxes (tenant_id, name, amount_type, amount, type_tax_use, tax_group_id, country_id, tax_exigibility, l10n_mx_factor_type, l10n_mx_tax_type) VALUES ('{tenant}', 'IVA 16%', 'percent', 16.00, 'purchase', '{iva_16_group}', '{mx}', 'on_payment', 'Tasa', 'iva'), ('{tenant}', 'IVA 8%', 'percent', 8.00, 'purchase', '{iva_8_group}', '{mx}', 'on_payment', 'Tasa', 'iva'), ('{tenant}', 'IVA 0%', 'percent', 0.00, 'purchase', '{iva_0_group}', '{mx}', 'on_invoice', 'Tasa', 'iva'); -- Retenciones IVA (Monto negativo = retención) INSERT INTO taxes (tenant_id, name, amount_type, amount, type_tax_use, tax_group_id, country_id, tax_exigibility, l10n_mx_factor_type, l10n_mx_tax_type) VALUES ('{tenant}', 'Ret. IVA 10.67%', 'percent', -10.67, 'purchase', '{ret_iva_group}', '{mx}', 'on_payment', 'Tasa', 'iva'), ('{tenant}', 'Ret. IVA 10%', 'percent', -10.00, 'purchase', '{ret_iva_group}', '{mx}', 'on_payment', 'Tasa', 'iva'), ('{tenant}', 'Ret. IVA 4%', 'percent', -4.00, 'purchase', '{ret_iva_group}', '{mx}', 'on_payment', 'Tasa', 'iva'); -- Retenciones ISR INSERT INTO taxes (tenant_id, name, amount_type, amount, type_tax_use, tax_group_id, country_id, tax_exigibility, l10n_mx_factor_type, l10n_mx_tax_type) VALUES ('{tenant}', 'Ret. ISR 10%', 'percent', -10.00, 'purchase', '{ret_isr_group}', '{mx}', 'on_invoice', 'Tasa', 'isr'), ('{tenant}', 'Ret. ISR 1.25% RESICO', 'percent', -1.25, 'purchase', '{ret_isr_group}', '{mx}', 'on_invoice', 'Tasa', 'isr'); -- IEPS INSERT INTO taxes (tenant_id, name, amount_type, amount, type_tax_use, tax_group_id, country_id, tax_exigibility, l10n_mx_factor_type, l10n_mx_tax_type) VALUES ('{tenant}', 'IEPS 8%', 'percent', 8.00, 'sale', '{ieps_8_group}', '{mx}', 'on_payment', 'Tasa', 'ieps'), ('{tenant}', 'IEPS 25%', 'percent', 25.00, 'sale', '{ieps_25_group}', '{mx}', 'on_payment', 'Tasa', 'ieps'), ('{tenant}', 'IEPS 26.5%', 'percent', 26.50, 'sale', '{ieps_265_group}', '{mx}', 'on_payment', 'Tasa', 'ieps'), ('{tenant}', 'IEPS 30%', 'percent', 30.00, 'sale', '{ieps_30_group}', '{mx}', 'on_payment', 'Tasa', 'ieps'), ('{tenant}', 'IEPS 53%', 'percent', 53.00, 'sale', '{ieps_53_group}', '{mx}', 'on_payment', 'Tasa', 'ieps'); ``` ### 6.2 Posiciones Fiscales México ```sql -- Posiciones fiscales predefinidas INSERT INTO fiscal_positions (tenant_id, name, auto_apply, country_id, vat_required) VALUES ('{tenant}', 'Cliente Nacional', TRUE, '{mx}', FALSE), ('{tenant}', 'Cliente Extranjero', TRUE, NULL, FALSE), -- NULL = cualquier país excepto México ('{tenant}', 'Zona Fronteriza Norte', TRUE, '{mx}', FALSE); -- Mapeos para Cliente Extranjero INSERT INTO fiscal_position_tax_mappings (fiscal_position_id, tax_src_id, tax_dest_id) VALUES ('{fp_extranjero}', '{iva_16_venta}', '{iva_0_venta}'), ('{fp_extranjero}', '{iva_8_venta}', '{iva_0_venta}'), ('{fp_extranjero}', '{ieps_venta}', NULL); -- NULL = quitar impuesto -- Mapeos para Zona Fronteriza (estados específicos) UPDATE fiscal_positions SET state_ids = ARRAY['{baja_california}', '{sonora}', '{chihuahua}', '{coahuila}', '{tamaulipas}'] WHERE name = 'Zona Fronteriza Norte'; INSERT INTO fiscal_position_tax_mappings (fiscal_position_id, tax_src_id, tax_dest_id) VALUES ('{fp_frontera}', '{iva_16_venta}', '{iva_8_venta}'); ``` --- ## 7. Integración CFDI 4.0 ### 7.1 Mapeo de Campos | Campo ERP | Campo CFDI | Descripción | |-----------|------------|-------------| | `l10n_mx_factor_type` | `TipoFactor` | Tasa, Cuota, Exento | | `l10n_mx_tax_type` | `Impuesto` | 001=ISR, 002=IVA, 003=IEPS | | `amount` | `TasaOCuota` | Tasa decimal (0.160000) | | Calculado | `Base` | Base gravable | | Calculado | `Importe` | Monto del impuesto | ### 7.2 Generación de Nodo Impuestos ```python # services/cfdi_tax_generator.py def generate_cfdi_taxes(invoice_line_taxes: List[Dict]) -> Dict: """ Genera estructura de impuestos para CFDI 4.0. Returns: { "Traslados": [...], "Retenciones": [...], "TotalImpuestosTrasladados": Decimal, "TotalImpuestosRetenidos": Decimal } """ traslados = [] retenciones = [] for tax_line in invoice_line_taxes: tax = tax_line['tax'] # Mapear código de impuesto SAT impuesto_code = { 'isr': '001', 'iva': '002', 'ieps': '003' }.get(tax['l10n_mx_tax_type'], '002') node = { 'Base': str(tax_line['base']), 'Impuesto': impuesto_code, 'TipoFactor': tax['l10n_mx_factor_type'], } if tax['l10n_mx_factor_type'] != 'Exento': # Convertir porcentaje a decimal (16% -> 0.160000) tasa = abs(tax['amount']) / 100 node['TasaOCuota'] = f"{tasa:.6f}" node['Importe'] = str(abs(tax_line['amount'])) # Separar traslados de retenciones if tax['amount'] < 0: retenciones.append(node) else: traslados.append(node) return { 'Traslados': traslados if traslados else None, 'Retenciones': retenciones if retenciones else None, 'TotalImpuestosTrasladados': sum( Decimal(t['Importe']) for t in traslados if 'Importe' in t ), 'TotalImpuestosRetenidos': sum( Decimal(r['Importe']) for r in retenciones if 'Importe' in r ), } ``` --- ## 8. Casos de Prueba ### 8.1 Cálculo Básico IVA 16% ```python def test_iva_16_percent(): """Prueba cálculo básico de IVA 16%.""" calculator = TaxCalculator() taxes = [{ 'id': 'iva-16', 'name': 'IVA 16%', 'amount_type': 'percent', 'amount': 16.0, 'sequence': 1, 'tax_group_id': 'iva-group', 'price_include': False, 'repartition_lines': [ {'id': 'r1', 'document_type': 'invoice', 'repartition_type': 'base', 'factor_percent': 100}, {'id': 'r2', 'document_type': 'invoice', 'repartition_type': 'tax', 'factor_percent': 100, 'account_id': 'iva-account'}, ] }] result = calculator.compute_all( taxes=taxes, price_unit=Decimal('100.00'), quantity=Decimal('1') ) assert result.total_excluded == Decimal('100.00') assert result.total_included == Decimal('116.00') assert len(result.taxes) == 1 assert result.taxes[0].amount == Decimal('16.00') assert result.taxes[0].base == Decimal('100.00') ``` ### 8.2 IVA Incluido en Precio ```python def test_iva_price_included(): """Prueba IVA incluido en precio.""" calculator = TaxCalculator() taxes = [{ 'id': 'iva-16-inc', 'name': 'IVA 16% (incluido)', 'amount_type': 'percent', 'amount': 16.0, 'sequence': 1, 'tax_group_id': 'iva-group', 'price_include': True, 'repartition_lines': [...] }] result = calculator.compute_all( taxes=taxes, price_unit=Decimal('116.00'), # Precio con IVA quantity=Decimal('1') ) assert result.total_excluded == Decimal('100.00') assert result.total_included == Decimal('116.00') assert result.taxes[0].amount == Decimal('16.00') assert result.taxes[0].base == Decimal('100.00') ``` ### 8.3 IVA + Retención ```python def test_iva_with_retention(): """Prueba IVA 16% con retención IVA 10.67%.""" calculator = TaxCalculator() taxes = [ { 'id': 'iva-16', 'name': 'IVA 16%', 'amount_type': 'percent', 'amount': 16.0, 'sequence': 1, 'price_include': False, 'repartition_lines': [...] }, { 'id': 'ret-iva', 'name': 'Ret. IVA 10.67%', 'amount_type': 'percent', 'amount': -10.67, # Negativo = retención 'sequence': 2, 'price_include': False, 'repartition_lines': [...] } ] result = calculator.compute_all( taxes=taxes, price_unit=Decimal('100.00'), quantity=Decimal('1') ) assert result.total_excluded == Decimal('100.00') # Total = 100 + 16 - 10.67 = 105.33 assert result.total_included == Decimal('105.33') assert result.taxes[0].amount == Decimal('16.00') # IVA assert result.taxes[1].amount == Decimal('-10.67') # Retención ``` ### 8.4 IEPS + IVA en Cascada ```python def test_ieps_iva_cascade(): """Prueba IEPS 53% + IVA 16% en cascada (bebidas alcohólicas).""" calculator = TaxCalculator() taxes = [ { 'id': 'ieps-53', 'name': 'IEPS 53%', 'amount_type': 'percent', 'amount': 53.0, 'sequence': 1, 'include_base_amount': True, # Afecta base de IVA 'price_include': False, 'repartition_lines': [...] }, { 'id': 'iva-16', 'name': 'IVA 16%', 'amount_type': 'percent', 'amount': 16.0, 'sequence': 2, 'is_base_affected': True, # Afectado por IEPS 'price_include': False, 'repartition_lines': [...] } ] result = calculator.compute_all( taxes=taxes, price_unit=Decimal('100.00'), quantity=Decimal('1') ) # IEPS: 100 * 0.53 = 53 # IVA: (100 + 53) * 0.16 = 24.48 # Total: 100 + 53 + 24.48 = 177.48 assert result.total_excluded == Decimal('100.00') assert result.total_included == Decimal('177.48') assert result.taxes[0].amount == Decimal('53.00') # IEPS assert result.taxes[1].amount == Decimal('24.48') # IVA sobre base + IEPS ``` --- ## 9. Plan de Implementación ### Fase 1: Infraestructura Base (3 SP) - Crear tablas de impuestos y grupos - Implementar tipos ENUM - Configurar RLS ### Fase 2: Motor de Cálculo (3 SP) - Implementar TaxCalculator - Soportar tipos: percent, fixed, division - Pruebas unitarias ### Fase 3: Posiciones Fiscales (2 SP) - Implementar detector automático - Mapeo de impuestos y cuentas - API REST --- ## 10. Referencias - **Odoo 18.0**: `account/models/account_tax.py` - **SAT México**: Catálogo c_Impuesto CFDI 4.0 - **RFC CFDI**: Anexo 20 versión 4.0