"""IVA flow direction enum — repercutido / soportado / INVERSION_SUJETO_PASIVO.
Codifies the IVA collectability axis the ledger and modelo registries
need alongside the operation-kind axis (:class:`IvaCategory`) and the
rate axis (:class:`IvaRateKind`). Every IVA-bearing ledger line
classifies along all three axes:
* :class:`IvaCategory` — operation kind (domestic, intracomunitaria,
recargo, OSS, etc.).
* :class:`IvaRateKind` — rate tier (general / reduced / super-reduced /
zero / exempt).
* :class:`IvaFlowDirection` — flow direction (output / input /
self-assessed reverse charge).
Together the triple resolves to a single ledger-bookable Cuota.
The three flow directions are anchored to LIVA articles:
* :attr:`IvaFlowDirection.REPERCUTIDO` — LIVA art. 88 (repercusión
del impuesto). The sujeto pasivo charges IVA to its customer; the
customer is the obligado a soportar.
* :attr:`IvaFlowDirection.SOPORTADO` — LIVA art. 92 (cuotas
tributarias deducibles). The sujeto pasivo bears IVA via direct
repercusión from suppliers and may deduct that IVA from its own
output IVA.
* :attr:`IvaFlowDirection.INVERSION_SUJETO_PASIVO` — LIVA art. 84.Uno.2.º
(inversión del sujeto pasivo). The recipient of certain
operations (intra-community acquisitions, art. 84.Uno.2.º.f
construction reverse charge, etc.) is the sujeto pasivo and
self-assesses both an IVA repercutido entry (output) and a
matching IVA soportado entry (input) on the same operation.
The :func:`derive_flow_for_classification` helper computes the flow
direction from the substrate's :class:`IvaCategory` plus the invoice
:class:`InvoiceKind` axis so consumers do not have to encode the
mapping by hand.
==============================================================
Settlement-side cornerstones — devengada vs deducible
==============================================================
The IVA settlement model rests on two cornerstone concepts:
* **IVA devengada** (output IVA, "cuota tributaria devengada") — the
amount the sujeto pasivo OWES to the Treasury as the IVA chargeable
on its sales. LIVA arts. 75-77 establish when IVA accrues
(devengo); LIVA art. 88 governs how the sujeto pasivo charges it
to the customer (repercusión).
* **IVA deducible** (input IVA, "cuotas tributarias deducibles") —
the amount the sujeto pasivo may DEDUCT from its devengada because
it bore IVA on inputs. LIVA art. 92 establishes the right to
deduction; arts. 93-104 establish the conditions, scope, and
limits.
The cuota neta a ingresar (or a devolver) at the period level is
**devengada - deducible**; this is the canonical Modelo 303 "resultado
régimen general" line.
Every :class:`IvaFlowDirection` member contributes to one or both
cornerstones:
======================= ========= =========
Flow direction Devengada Deducible
======================= ========= =========
REPERCUTIDO ✓ ─
SOPORTADO ─ ✓
INVERSION_SUJETO_PASIVO ✓ ✓
======================= ========= =========
INVERSION_SUJETO_PASIVO is the only flow that contributes to BOTH sides on
the SAME operation: the recipient self-assesses an output entry
(devengada) and a matching input entry (deducible) for the cuota
that would have been repercutida by a non-existent or non-EU
supplier. The two entries cancel arithmetically inside Modelo 303
(devengada - deducible = 0 for that line) but both must be booked
to satisfy the LIVA art. 84.Uno.2 inversión-del-sujeto-pasivo
mechanism.
The :class:`IvaSettlementSide` enum + :func:`settlement_sides_for_flow`
helper let the ledger and modelo registries categorize transactions
without re-deriving the mapping. The closed-set helpers
:func:`is_devengada_flow` and :func:`is_deducible_flow` are the
canonical predicates for "does this flow contribute to the cuota
devengada / cuota deducible total?".
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Self
from ...core.registry_token import StrictRegistryToken
from ..calculations.registry.iva_category_catalogue import IvaCategoryCatalogue, resolve_iva_category_catalogue
from .classification import InvoiceKind
from .schema import IvaCategory
if TYPE_CHECKING:
from ..calculations.registry.iva_flow_catalogue import IvaFlowDirectionCatalogue
[docs]
class IvaFlowDirection(StrictRegistryToken):
"""Opaque IVA flow token projected from fact 0083.
Flow membership, legal descriptions, and settlement-side semantics belong
to the dated facts registry. This wire type deliberately carries no
closed Python member list; callers must obtain tokens through the typed
registry projection.
"""
__slots__ = ()
def __copy__(self) -> Self:
"""Share an immutable token without repeating membership admission."""
return self
def __deepcopy__(self, memo: dict[int, object]) -> Self:
"""Preserve the admitted token when copying a containing projection."""
return self
[docs]
def flow_direction_for_invoice_kind(invoice_kind: InvoiceKind) -> IvaFlowDirection:
"""Return the IVA flow direction an invoice's issuance side settles as.
This is the base rule the tax itself states: an invoice the autónomo
ISSUED charges output IVA onward (:attr:`IvaFlowDirection.REPERCUTIDO`);
one it RECEIVED bears input IVA (:attr:`IvaFlowDirection.SOPORTADO`).
:func:`derive_flow_for_classification` and
:func:`~cadrumo.domain.iva.invoice_classification.classify_invoice_line_for_iva` both call this
for their standard-case resolution and OVERRIDE it for the special
regimes (reverse charge, intra-community) that route the same members
differently.
"""
flow_catalogue = _flow_direction_catalogue()
return flow_catalogue.issued_token if invoice_kind is InvoiceKind.ISSUED else flow_catalogue.received_token
def _flow_direction_catalogue() -> IvaFlowDirectionCatalogue:
"""Resolve the selected 0083 flow catalogue without an import cycle."""
from ..calculations.registry.iva_flow_catalogue import resolve_iva_flow_direction_catalogue
return resolve_iva_flow_direction_catalogue()
def _recipient_only_reverse_charge_categories(
catalogue: IvaCategoryCatalogue,
) -> frozenset[IvaCategory]:
"""Return the reverse-charge categories that ignore the invoice direction.
These route to ``INVERSION_SUJETO_PASIVO`` on EITHER invoice direction,
because only the recipient's side exists.
Both are ACQUISITIONS. The supplier's counterpart of an intra-community
acquisition is a different category entirely (an exempt art. 25 supply, or an
operation not located in Spain), so no invoice direction can put this taxpayer
on the supplying side of one of these. Direction is therefore genuinely
irrelevant here, and collapsing it is correct.
The domestic reverse-charge category is deliberately NOT a member. A domestic
art. 84.Uno.2 operation has both of its sides in Spain, so the same category
legitimately describes a supply this taxpayer MADE and a purchase it RECEIVED
-- and those settle differently. It is handled by direction in
:func:`derive_flow_for_classification`.
"""
return frozenset(
{
catalogue.require("intra_community_acquisition_reverse_charge"),
# A B2B service received from an EU supplier sits in the same
# position as the goods acquisition above: art. 69.Uno.1.o locates
# it in Spain because the recipient is established here, and art.
# 84.Uno.2.o makes that recipient the sujeto pasivo. Its supply
# counterpart is deliberately absent -- there the operation is not
# located in Spain at all, so no Spanish cuota arises to self-assess.
catalogue.require("intra_community_service_acquisition_reverse_charge"),
},
)
[docs]
def derive_flow_for_classification(
*,
category: IvaCategory,
invoice_direction: InvoiceKind,
) -> IvaFlowDirection:
"""Return the IVA flow direction for a substrate-classified line.
The mapping is:
* Recipient-only reverse-charge categories (the intra-community
acquisitions in :func:`_recipient_only_reverse_charge_categories`)
resolve to :attr:`IvaFlowDirection.INVERSION_SUJETO_PASIVO`
irrespective of the invoice direction, because their supply
counterpart is not located in Spain and so raises no Spanish cuota
to self-assess.
* Domestic reverse charge resolves BY DIRECTION, because both sides
of the operation are Spanish and the form asks for them
separately: :attr:`InvoiceKind.RECEIVED` is the recipient
self-assessing, so
:attr:`IvaFlowDirection.INVERSION_SUJETO_PASIVO`;
:attr:`InvoiceKind.ISSUED` is the supplier making a sujeta y no
exenta supply that repercutes nothing, so
:attr:`IvaFlowDirection.OPERACION_CON_INVERSION`. Collapsing the
two once put the supplier's turnover on the recipient's line.
* Otherwise: :attr:`InvoiceKind.ISSUED` resolves to
:attr:`IvaFlowDirection.REPERCUTIDO` (the autónomo charged
output IVA on a sale) and :attr:`InvoiceKind.RECEIVED`
resolves to :attr:`IvaFlowDirection.SOPORTADO` (the autónomo
bore input IVA on a purchase).
Args:
category: The :class:`IvaCategory` resolved by the substrate
classifier.
invoice_direction: Whether the invoice was issued or received
by the autónomo.
Returns:
The :class:`IvaFlowDirection` that matches the classification.
"""
category_catalogue = resolve_iva_category_catalogue()
flow_catalogue = _flow_direction_catalogue()
if category in _recipient_only_reverse_charge_categories(category_catalogue):
return flow_catalogue.recipient_reverse_charge_token
if category == category_catalogue.require("domestic_reverse_charge"):
if invoice_direction is InvoiceKind.ISSUED:
return flow_catalogue.supplier_reverse_charge_token
return flow_catalogue.recipient_reverse_charge_token
return flow_direction_for_invoice_kind(invoice_direction)
[docs]
class IvaSettlementSide(StrictRegistryToken):
"""Opaque IVA settlement-side token projected from fact 0083."""
__slots__ = ()
[docs]
def settlement_sides_for_flow(
flow: IvaFlowDirection,
) -> frozenset[IvaSettlementSide]:
"""Return the settlement side(s) ``flow`` contributes to.
This is the CUOTA axis: which side of the settlement a line's cuota is
reckoned on. It is not the axis of which half of the return a line's
amounts appear in, and the two come apart on every zero-cuota operation.
An exempt sale is an output operation whose base belongs on the devengada
half of the return while settling no cuota at all, so reading a
``{DEVENGADA}`` here as "this line owes output IVA" is wrong for it.
That distinction is easy to lose, because the Axis-A component table's
``cuota_settlement`` column and this function look like two statements of
one fact and are not. Compare the two directly and roughly half the
arising pairs appear to disagree; every one of those apparent conflicts is
a zero-cuota operation where the table says "no cuota arises" and this
function says "output side", both of which are true. The check worth making
is per consumer, not per pair.
Both production consumers screen on a positive cuota before asking, so the
zero-cuota rows never reach the question. A future consumer that does not
screen first is the one that would be misled, and it is the reason this
paragraph exists rather than a coherence gate over two axes that do not
answer the same question.
Args:
flow: The :class:`IvaFlowDirection` to classify.
Returns:
Frozenset of :class:`IvaSettlementSide` values: ``{DEVENGADA}`` for
:attr:`IvaFlowDirection.REPERCUTIDO`, ``{DEDUCIBLE}`` for
:attr:`IvaFlowDirection.SOPORTADO`, ``{DEVENGADA, DEDUCIBLE}`` for
:attr:`IvaFlowDirection.INVERSION_SUJETO_PASIVO`, and the empty set for
:attr:`IvaFlowDirection.OPERACION_CON_INVERSION`, whose supplier
repercutes no cuota and bears none.
"""
return _flow_direction_catalogue().settlement_sides_for(flow)
[docs]
def is_devengada_flow(flow: IvaFlowDirection) -> bool:
"""Return ``True`` iff ``flow`` contributes to cuota devengada.
Canonical predicate for the ledger / modelo registries when
aggregating cuota devengada totals — equivalent to
``flow in {REPERCUTIDO, INVERSION_SUJETO_PASIVO}`` but anchored to the
substrate's settlement-side codification so downstream consumers
don't have to re-enumerate the mapping.
"""
return _flow_direction_catalogue().is_devengada(flow)
[docs]
def is_deducible_flow(flow: IvaFlowDirection) -> bool:
"""Return ``True`` iff ``flow`` contributes to cuota deducible.
Canonical predicate for the ledger / modelo registries when
aggregating cuota deducible totals — equivalent to
``flow in {SOPORTADO, INVERSION_SUJETO_PASIVO}`` but anchored to the
substrate's settlement-side codification.
"""
return _flow_direction_catalogue().is_deducible(flow)
[docs]
def is_inversion_sujeto_pasivo_flow(flow: IvaFlowDirection) -> bool:
"""Return whether ``flow`` is the recipient reverse-charge direction."""
return flow == _flow_direction_catalogue().recipient_reverse_charge_token
[docs]
def is_standard_issued_or_received_flow(flow: IvaFlowDirection) -> bool:
"""Return whether ``flow`` is one of the ordinary invoice-side flows."""
catalogue = _flow_direction_catalogue()
return flow in {catalogue.issued_token, catalogue.received_token}
[docs]
def issued_flow_direction() -> IvaFlowDirection:
"""Return the registry-declared flow for an issued invoice."""
return _flow_direction_catalogue().issued_token
[docs]
def received_flow_direction() -> IvaFlowDirection:
"""Return the registry-declared flow for a received invoice."""
return _flow_direction_catalogue().received_token
__all__ = [
"IvaFlowDirection",
"IvaSettlementSide",
"derive_flow_for_classification",
"flow_direction_for_invoice_kind",
"is_deducible_flow",
"is_devengada_flow",
"is_inversion_sujeto_pasivo_flow",
"is_standard_issued_or_received_flow",
"issued_flow_direction",
"received_flow_direction",
"settlement_sides_for_flow",
]