Aquí tienes el desglose técnico de cómo funciona la asignación automática de posiciones fiscales (_get_fiscal_position) en Odoo 18.
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.
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.
# 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íoEsto 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!
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).
Esta lógica es fundamental para el cumplimiento del OSS. Para ventas B2C a otros países de la UE, típicamente:
- No tienes NIF del cliente (los consumidores no tienen NIF)
- Por lo tanto se usa la dirección de envío
- 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.
Una vez que Odoo sabe qué dirección usar (facturación o envío), ejecuta el algoritmo de coincidencia en dos fases:
- Filtrado: Se eliminan las posiciones fiscales que no coinciden
- Ranking: Los candidatos restantes se comparan usando una tupla; gana el más alto
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.
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.
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) |
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
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.
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 (
2vs1)
- False: Sin requisito de NIF; puede coincidir con cualquier cliente
- 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)
- 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
- 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.)
- 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
- Desempate cuando todos los demás criterios son iguales
- Menor secuencia = mayor prioridad
Dos posiciones para España:
- Régimen General:
country=España,vat_required=False,sequence=10 - 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 > 1en 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)
Tres posiciones:
- Mundo: sin país,
sequence=10 - UE:
country_group=Europa,sequence=20 - 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)
Dos posiciones:
- Francia General:
country=Francia - 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