Skip to content

Instantly share code, notes, and snippets.

@kwmiebach
Created January 20, 2026 22:47
Show Gist options
  • Select an option

  • Save kwmiebach/005630f91d8515d0af91f3600e333b97 to your computer and use it in GitHub Desktop.

Select an option

Save kwmiebach/005630f91d8515d0af91f3600e333b97 to your computer and use it in GitHub Desktop.
Cómo funciona la asignación automática de posiciones fiscales (`_get_fiscal_position`) en Odoo 18

Aquí tienes el desglose técnico de cómo funciona la asignación automática de posiciones fiscales (_get_fiscal_position) en Odoo 18.

Lógica de IVA Intra-UE (Contexto OSS)

Antes de que se ejecute cualquier coincidencia de posición fiscal, Odoo decide qué dirección usar: la dirección de facturación o la dirección de envío.

La Regla

Normalmente, la dirección de envío determina la posición fiscal (los impuestos se aplican donde se entregan las mercancías). Sin embargo, existe una excepción especial para transacciones B2B intra-UE:

Si tanto la empresa como el cliente tienen NIF/CIF, y ambos números de IVA son del mismo país de la UE, entonces Odoo ignora la dirección de envío y usa la dirección de facturación en su lugar.

Cómo Funciona (Lógica del Código)

# Extraer código de país de los primeros 2 caracteres del NIF
company_vat_country = company.vat[:2]  # ej: "ES" de "ESB12345678"
partner_vat_country = partner.vat[:2]  # ej: "ES" de "ESA87654321"

# Comprobar si ambos son países de la UE
intra_eu = ambos_paises_estan_en_UE

# Comprobar si es el mismo país
mismo_pais = company_vat_country == partner_vat_country

# Decisión
if intra_eu AND mismo_pais:
    usar dirección de facturación (ignorar envío)
else:
    usar dirección de envío

Por Qué Existe Esta Regla

Esto gestiona las ventas B2B nacionales donde la ubicación de entrega no debería cambiar el tratamiento fiscal.

Ejemplo:

  • Tu empresa: España (NIF: ESB12345678)
  • Cliente: Empresa española (NIF: ESA87654321)
  • Dirección de envío: Francia (almacén del cliente)

Sin esta regla: Odoo miraría Francia → aplicaría posición fiscal francesa → impuestos franceses. ¡Incorrecto!

Con esta regla: Ambos NIF son españoles (ES = ES), así que Odoo ignora Francia y usa la dirección de facturación española → posición fiscal española → IVA español. ¡Correcto!

Cuándo SÍ Se Usa la Dirección de Envío

La dirección de envío se usa cuando:

  • La empresa o el cliente no tiene NIF/CIF
  • Los NIF son de países diferentes (aunque ambos sean de la UE)
  • Alguno de los NIF es de fuera de la UE

Ejemplo (Venta Intracomunitaria):

  • Tu empresa: España (NIF: ESB12345678)
  • Cliente: Empresa francesa (NIF: FR12345678901)
  • Envío: Francia

Aquí, ES ≠ FR, así que Odoo usa la dirección de envío francesa → posición fiscal intracomunitaria (IVA 0% con inversión del sujeto pasivo).

Contexto OSS (Ventanilla Única)

Esta lógica es fundamental para el cumplimiento del OSS. Para ventas B2C a otros países de la UE, típicamente:

  1. No tienes NIF del cliente (los consumidores no tienen NIF)
  2. Por lo tanto se usa la dirección de envío
  3. Lo que activa la posición fiscal del país de destino

La excepción de "mismo país en el NIF" asegura que las ventas B2B nacionales no se traten incorrectamente como transfronterizas.


Cómo Funciona el Algoritmo de Coincidencia

Una vez que Odoo sabe qué dirección usar (facturación o envío), ejecuta el algoritmo de coincidencia en dos fases:

  1. Filtrado: Se eliminan las posiciones fiscales que no coinciden
  2. Ranking: Los candidatos restantes se comparan usando una tupla; gana el más alto

Fase 0: Asignación Manual

Si el cliente (o la dirección de envío) tiene una posición fiscal configurada manualmente en property_account_position_id, esa posición siempre gana. La detección automática se omite por completo.

Fase 1: Filtrado (Pasa/No Pasa)

Solo se consideran las posiciones fiscales con auto_apply=True. Cada candidata debe pasar todos estos filtros:

Campo Regla
vat_required Si es True, el cliente debe tener NIF. Sin NIF = descalificada
zip_from/zip_to Si está configurado, el CP del cliente debe estar en el rango. Fuera de rango = descalificada
state_ids Si está configurado, la provincia del cliente debe estar en la lista. Provincia diferente = descalificada
country_id Si está configurado, el país del cliente debe coincidir exactamente. País diferente = descalificada
country_group_id Si está configurado, el país del cliente debe pertenecer al grupo. No está en el grupo = descalificada

Punto clave: Los campos vacíos/no configurados actúan como comodines (coinciden con todo). Una posición fiscal sin país configurado coincidirá con clientes de cualquier país.

Fase 2: Ranking (Comparación de Tuplas)

Los candidatos que pasan todos los filtros se clasifican usando una comparación de tuplas. Python compara tuplas elemento por elemento de izquierda a derecha, por lo que los elementos anteriores tienen prioridad absoluta.

La tupla se construye en este orden (primero = más importante):

(vat_required, profundidad_empresa, codigo_postal, provincia, pais, grupo_paises, -secuencia)

Cada elemento es:

  • 1 (True) = filtro no configurado (comodín)
  • 2 = filtro configurado Y coincide
Posición Criterio Valor
1 NIF Requerido 2 si vat_required=True y el cliente tiene NIF; 1 en caso contrario
2 Jerarquía de Empresa Profundidad de la empresa en el árbol multi-empresa (empresas hijas tienen preferencia)
3 Código Postal 2 si el rango de CP está configurado y coincide; 1 si no está configurado
4 Provincia 2 si las provincias están configuradas y coinciden; 1 si no está configurado
5 País 2 si el país está configurado y coincide; 1 si no está configurado
6 Grupo de Países 2 si el grupo está configurado y coincide; 1 si no está configurado
7 Secuencia Secuencia negativa (número de secuencia más bajo = mayor rango)

Por Qué Esto es Determinista

Porque las tuplas se comparan lexicográficamente:

  • Una posición con vat_required=True (puntuación 2) siempre gana a una sin él (puntuación 1), independientemente de otros campos
  • Solo cuando el primer elemento empata importa el segundo elemento, y así sucesivamente
  • La secuencia es el desempate final

Referencia de Campos

1. Detectar Automáticamente (auto_apply)

El interruptor maestro para la asignación automática.

  • True: Odoo considerará esta posición para asignación automática
  • False: La posición nunca se asignará automáticamente; debe seleccionarse manualmente

Uso típico para False: Exenciones específicas (ej: "Diplomáticos") que requieren validación humana.

2. NIF Requerido (vat_required)

Actúa como filtro estricto y como impulsor de ranking.

  • True:
    • Cliente sin NIF = posición descalificada
    • Cliente con NIF = la posición recibe impulso en el ranking (2 vs 1)
  • False: Sin requisito de NIF; puede coincidir con cualquier cliente

3. País (country_id)

  • Si está configurado: Solo coincide con clientes de este país exacto. Descalifica a otros.
  • Si está vacío: Coincide con clientes de cualquier país (comodín)
  • Ranking: Coincidencia de país específico (2) supera al comodín (1)

4. Grupo de Países (country_group_id)

  • Si está configurado: Solo coincide con clientes cuyo país pertenezca al grupo. Descalifica a otros.
  • Si está vacío: Coincide con cualquier país
  • Ranking: Coincidencia de país (2) gana a coincidencia de grupo porque el país se evalúa antes en la tupla

5. Provincias / CC.AA. (state_ids)

  • Si está configurado: El cliente debe estar en una de estas provincias. Descalifica a otros.
  • Si está vacío: Coincide con cualquier provincia
  • Útil para fiscalidad regional (Canarias, Ceuta, Melilla, etc.)

6. Rango de Código Postal (zip_from, zip_to)

  • Si está configurado: El CP del cliente debe estar dentro del rango (comparación de strings). Descalifica a otros.
  • Si está vacío: Coincide con cualquier CP
  • Filtro geográfico más específico; se evalúa temprano en el ranking

7. Secuencia

  • Desempate cuando todos los demás criterios son iguales
  • Menor secuencia = mayor prioridad

Ejemplos

Ejemplo 1: NIF Requerido Siempre Gana

Dos posiciones para España:

  1. Régimen General: country=España, vat_required=False, sequence=10
  2. Régimen Intracomunitario: country=España, vat_required=True, sequence=50

Cliente de España con NIF:

  • Ambas pasan el filtrado (país coincide, NIF presente)
  • Tuplas de ranking: General=(1,...), Intra=(2,...)
  • Intracomunitario gana porque 2 > 1 en la primera posición

Cliente de España sin NIF:

  • General pasa el filtrado
  • Intracomunitario queda descalificado (vat_required=True pero sin NIF)
  • Régimen General gana (único candidato)

Ejemplo 2: La Especificidad Gana

Tres posiciones:

  1. Mundo: sin país, sequence=10
  2. UE: country_group=Europa, sequence=20
  3. Francia: country=Francia, sequence=30

Cliente de Francia:

  • Las tres pasan el filtrado
  • Tupla Francia: (..., 2, 1, ...) (país=2, grupo=1)
  • Tupla UE: (..., 1, 2, ...) (país=1, grupo=2)
  • Francia gana porque el país (posición 5) se evalúa antes que el grupo (posición 6)

Ejemplo 3: El CP Gana al País

Dos posiciones:

  1. Francia General: country=Francia
  2. Francia París CP: country=Francia, zip_from=75000, zip_to=75999

Cliente de París (CP 75001):

  • Ambas pasan el filtrado
  • Francia General: (..., 1, ..., 2, 1, ...) (sin cp, tiene país)
  • París CP: (..., 2, ..., 2, 1, ...) (tiene cp, tiene país)
  • París CP gana porque coincidencia de CP (2) supera a sin CP (1) en la posición 3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment