erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-IMPUESTOS-AVANZADOS.md

76 KiB

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

-- =============================================================================
-- 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

# 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

# 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

# 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

# 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<Tax>
  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

// 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

-- 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

-- 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

# 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%

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

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

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

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