cadrumo.domain.iva.classification module

Generic IVA classification mechanics (issuer / customer / kind / direction).

Layered on top of the cadrumo.domain.iva substrate (the cadrumo.domain.iva.schema.IvaCategory enum, cadrumo.domain.iva.schema.IvaRateRecord records, and cadrumo.domain.iva.lookup.lookup_rate()), this module adds the classification axes needed to tag a transaction deterministically based on the parties’ tax residency, the customer’s IVA status, the transaction kind, and the invoice direction.

The legal decision table is registry-owned. This module supplies only the typed criteria/result records and the first-match evaluator that consumes a registry-projected sequence of rule definitions. No legal rows, labels, priorities, or fallback treatment are authored here.

Examples

>>> from datetime import date
>>> from . import (
...     EUMemberState,
...     InvoiceKind,
...     TransactionKind,
...     IvaRateKind,
...     IvaInvoiceClassificationCriteria,
...     customer_tax_status_alias,
...     territorial_scope_alias,
...     require_eu_member_state,
...     classify_iva,
... )
>>> criteria = IvaInvoiceClassificationCriteria(
...     transaction_date=date(2025, 6, 15),
...     issuer_residency=territorial_scope_alias("mainland"),
...     customer_residency=territorial_scope_alias("eu_member"),
...     customer_identification_state=require_eu_member_state("DE"),
...     customer_tax_status=customer_tax_status_alias("b2b_registered"),
...     kind=registry_transaction_kind,
...     direction=InvoiceKind.ISSUED,
... )
>>> classify_iva(criteria, rules=registry_rules).category
'intra_community_supply'
class IvaTerritorialScope(value, *, _registry_validated=False)[source]

Bases: _RegistryProjectedToken

Registry-projected territorial scope token for an invoice party.

Parameters:
  • value (str)

  • _registry_validated (bool)

Return type:

Self

class PartyFact(*values)[source]

Bases: StrEnum

The two legally distinct facts a rule may need about a party.

They were one output before they were two, and the conflation had a direction. A printed foreign IVA prefix was read as decisive EVIDENCE OF PLACE, so a German-identified entity actually established in Spain resolved silently to a registry-projected EU-member token and fed the table as settled fact — while the mirror case, a non-resident holding a Spanish registration, was correctly refused. Every Member State registers non-residents on the same terms Spain does, so the two are one situation seen from two sides; splitting the fact restores the symmetry at the principle instead of patching it at one rung.

Naming them lets a branch DECLARE which it consumes, so an operator is asked only for what the branch it lands on actually turns on. The domestic and territorial rules need the place and not the identification. The intra-community families need the identification, and need the place only narrowly beside it — not to say which Member State a party belongs to, which is the conflation, but to place the supply in the peninsula and keep the Spanish territories out of “otro Estado miembro”. A row that reads a residency is not thereby reading it as a registration.

Variables:
  • IVA_IDENTIFICATION_STATE – The Member State under whose IVA identification the party operates, carried as cadrumo.domain.iva.schema.EUMemberState. Registration evidence settles it decisively, because registration is precisely what it asserts.

  • TERRITORIAL_ESTABLISHMENT – Where the party has its sede de actividad económica or an establecimiento permanente (Ley 37/1992 arts. 69-70), carried as IvaTerritorialScope. NO registration evidences it, foreign or Spanish.

IVA_IDENTIFICATION_STATE
TERRITORIAL_ESTABLISHMENT
class InvoiceKind(*values)[source]

Bases: StrEnum

Whether the autónomo issued or received the invoice.

Single canonical enum spanning both the substrate classifier (IvaInvoiceClassificationCriteria.direction) and ledger / invoice records (Invoice.kind). Replaces the prior split between InvoiceDirection (substrate) and InvoiceKind (invoices) that carried identical semantics with mismatched lowercase / uppercase string values. Values are lowercase to align with TOML registry selectors (invoice_direction = "issued").

Variables:
  • ISSUED – The autónomo is the issuer (sale).

  • RECEIVED – The autónomo is the recipient (purchase).

ISSUED
RECEIVED
class CustomerTaxStatus(value, *, _registry_validated=False)[source]

Bases: _RegistryProjectedToken

Registry-projected customer IVA-status token.

Parameters:
  • value (str)

  • _registry_validated (bool)

Return type:

Self

class IvaClassificationCatalogue(territorial_scopes, customer_tax_statuses, territorial_aliases, customer_status_aliases)[source]

Bases: object

Typed projection of the 0083 territorial/status vocabularies.

Parameters:
territorial_scopes: tuple[IvaTerritorialScope, ...]
customer_tax_statuses: tuple[CustomerTaxStatus, ...]
territorial_aliases: Mapping[str, IvaTerritorialScope]
customer_status_aliases: Mapping[str, CustomerTaxStatus]
property territorial_scope_set: frozenset[IvaTerritorialScope]

Return all territorial tokens declared by the selected fact.

property customer_tax_status_set: frozenset[CustomerTaxStatus]

Return all customer-status tokens declared by the selected fact.

require_territorial_scope(value)[source]

Validate and return one registry-projected territorial token.

Return type:

IvaTerritorialScope

Parameters:

value (object)

require_customer_tax_status(value)[source]

Validate and return one registry-projected customer-status token.

Return type:

CustomerTaxStatus

Parameters:

value (object)

territorial_scope_alias(alias)[source]

Return a named territorial alias from the selected fact.

Return type:

IvaTerritorialScope

Parameters:

alias (str)

customer_tax_status_alias(alias)[source]

Return a named customer-status alias from the selected fact.

Return type:

CustomerTaxStatus

Parameters:

alias (str)

class TransactionKind[source]

Bases: str

Opaque registry-projected kind-of-supply token.

property value: str

Return the opaque token for string-oriented serialization.

class TransactionKindDefinition(token, description, legal_refs=None, supply_nature=None, oss_regime=None, default_rate_kind=None)[source]

Bases: object

One registry-declared transaction-kind token and its semantics.

Parameters:
token: TransactionKind
description: str
legal_refs: str | None
supply_nature: str | None
oss_regime: str | None
default_rate_kind: str | None
class TransactionKindCatalogue(definitions, common_semantics, union_scheme_semantics)[source]

Bases: object

Typed projection of the dated IVA classification kind vocabulary.

Parameters:
definitions: tuple[TransactionKindDefinition, ...]
common_semantics: str
union_scheme_semantics: str
property all_kinds: frozenset[TransactionKind]

Return every registry-declared transaction-kind token.

require(value)[source]

Validate one opaque token against this registry projection.

Return type:

TransactionKind

Parameters:

value (object)

for_supply_nature(supply_nature)[source]

Return the unique kind projection for a registry supply-nature token.

Return type:

TransactionKind

Parameters:

supply_nature (str)

kinds_for_oss_regime(oss_regime)[source]

Return registry kinds routed through one OSS regime token.

Return type:

frozenset[TransactionKind]

Parameters:

oss_regime (str)

resolve_iva_classification_catalogue(effective_date=None, *, operation)[source]

Resolve the territorial and customer-status vocabulary from fact 0083.

Return type:

IvaClassificationCatalogue

Parameters:
require_iva_territorial_scope(value, *, effective_date=None, operation)[source]

Return a territorial scope only when 0083 declares it.

Return type:

IvaTerritorialScope

Parameters:
require_customer_tax_status(value, *, effective_date=None, operation)[source]

Return a customer status only when 0083 declares it.

Return type:

CustomerTaxStatus

Parameters:
territorial_scope_alias(alias, *, effective_date=None, operation)[source]

Return one named territorial projection from fact 0083.

Return type:

IvaTerritorialScope

Parameters:
customer_tax_status_alias(alias, *, effective_date=None, operation)[source]

Return one named customer-status projection from fact 0083.

Return type:

CustomerTaxStatus

Parameters:
resolve_transaction_kind_catalogue(effective_date, *, operation)[source]

Resolve all transaction-kind membership through the 0083 fact query.

Return type:

TransactionKindCatalogue

Parameters:
require_transaction_kind(value, *, effective_date, operation)[source]

Return one registry-declared transaction-kind token or refuse it.

Return type:

TransactionKind

Parameters:
domestic_rate_tier_is_required(*, issuer_residency, customer_residency, kind, transaction_date=None, customer_tax_status=None, art_69_dos_service=None, operation)[source]

Return whether any registry row this operation can reach derives its category from a rate tier.

Asked of the registry-projected rows themselves rather than of a second list of exempt kinds or territories: the operation needs a tier exactly when its first matching row is a rate_categories[rate_tier] row. A still-open customer status is a union over every status, so an open status demands the tier rather than excusing it, and the direction is unioned the same way. Identification is probed absent: a row that needs one cannot then preempt a later rate-tier row, which errs toward asking.

Return type:

bool

Parameters:
class IvaInvoiceClassificationCriteria(**data)[source]

Bases: _IvaStrictFrozen

Input record for classify_iva().

Carries every axis the closed decision table inspects. The record is strict and frozen so it can be used as a dict key in upstream caches.

The two party facts are carried separately and are never derived from each other (PartyFact). The residency fields are the TERRITORIAL ESTABLISHMENT fact; the identification-state fields are the IVA IDENTIFICATION STATE fact. A party may hold a German identification while being established in Spain, or a Spanish one while established abroad — both are ordinary, and a model that could not express them forced the reader to pick one meaning for a value that had two.

Variables:
  • transaction_date – When the supply takes place.

  • issuer_residency – Where the issuer is ESTABLISHED — its sede or establecimiento permanente under Ley 37/1992 arts. 69-70. Never a statement about where it is registered. The field name keeps the role label; the type carries the territorial framing.

  • customer_residency – The same for the customer.

  • customer_tax_status – Customer’s IVA status.

  • kind – Kind of supply.

  • direction – ISSUED or RECEIVED.

  • issuer_identification_state – The cadrumo.domain.iva.schema.EUMemberState under whose IVA identification the issuer operates, where established. Optional independently of issuer_residency: an EU establishment does not supply an identification and an identification does not supply an establishment, so demanding one because of the other would be the conflation PartyFact exists to end. Branches that need it declare so, and the producer demands it only for those.

  • customer_identification_state – The same for the customer.

  • rate_tier – Explicit rate-tier axis for ES-to-ES domestic rules.

Parameters:

data (Any)

transaction_date: date
issuer_residency: IvaTerritorialScope
customer_residency: IvaTerritorialScope
customer_tax_status: CustomerTaxStatus
kind: TransactionKind
direction: InvoiceKind
issuer_identification_state: EUMemberState | None
customer_identification_state: EUMemberState | None
art_69_dos_service: IvaArt69DosService | None
rate_tier: IvaRateKind | None
class IvaClassificationResult(**data)[source]

Bases: _IvaStrictFrozen

Output record returned by classify_iva().

Exposes the matched cadrumo.domain.iva.schema.IvaCategory, the resolved cadrumo.domain.iva.schema.IvaRateRecord (or None for rate-irrelevant categories), a reverse-charge flag, the matched rule identifier, and any free-form note the resolver emits (typically used for fall-through documentation).

Variables:
  • category – Resolved IVA category.

  • rate – Applicable cadrumo.domain.iva.schema.IvaRateRecord, when relevant.

  • requires_reverse_charge – True when the rule triggers inversión del sujeto pasivo.

  • matched_rule_id – Stable rule identifier (e.g. supplied by the selected registry revision.

  • notes – Free-form explanatory note.

  • consumes_party_facts – Which PartyFact values the matched branch actually turns on. This is how a producer assembling the criteria learns what to demand without holding a second copy of the law: it asks the table which facts the branch consumes rather than hand-writing a rule about the territorial scopes, which is the duplication the lazy-requirement mechanism already refuses elsewhere. Defaults to BOTH facts, so a row or result that forgets to declare demands everything — the fail-toward-asking direction, and the one where forgetting costs a question rather than a silent classification on a fact nobody supplied.

  • place_of_supply –

    The registry row that grounds the matched rule’s placement in the operation’s filing year — which provision locates the operation, and whether that provision fixes the nature of the supply. classify_iva() always stamps it; None survives only on a result assembled by hand, and therefore means “no placement was resolved”, never “the resolution failed”. A lookup that fails raises out of classify_iva() instead of arriving here as an absence.

    Read the nature off supply_nature: a None there is the third state, and it is a statement about the statute — the cited provisions are silent on whether goods or services were supplied. It is never an unfilled row.

    What this carries is registry-declared authority for the rule the table matched. It is not an AEAT ruling on the specific operation, and a consumer must not present it as one.

Parameters:

data (Any)

category: IvaCategory
rate: IvaRateRecord | None
requires_reverse_charge: bool
matched_rule_id: str
notes: str
consumes_party_facts: frozenset[PartyFact]
exemption_article: IvaExemptionArticle | None
place_of_supply: IvaPlaceOfSupplyRule | None
domestic_categories_by_rate_kind(mapping=None)[source]

Expose a registry-projected rate/category mapping as a read-only view.

Return type:

Mapping[IvaRateKind, IvaCategory]

Parameters:

mapping (Mapping[IvaRateKind, IvaCategory] | None)

rate_kind_for_domestic_category(category, mapping=None)[source]

Resolve a domestic category through a caller-supplied registry mapping.

Return type:

IvaRateKind | None

Parameters:
class IvaClassificationRule(rule_id, predicate, category=None, description='', consumes=frozenset({}), requires_reverse_charge=False)[source]

Bases: NamedTuple

One registry-projected classification row consumed by the evaluator.

Parameters:
rule_id: str

Alias for field number 0

predicate: Callable[[IvaInvoiceClassificationCriteria], bool]

Alias for field number 1

category: IvaCategory | None

Alias for field number 2

description: str

Alias for field number 3

consumes: frozenset[PartyFact]

Alias for field number 4

requires_reverse_charge: bool

Alias for field number 5

class IvaClassificationInputs(rules, rate_categories, rate_territories)[source]

Bases: object

Complete typed projection consumed by the IVA classifier.

The fact registry stores rule predicates as a small declarative language. This record is the only boundary that turns that language into executable predicates and rate/category mappings; the evaluator itself remains a generic first-match consumer.

Parameters:
rules: tuple[IvaClassificationRule, ...]
rate_categories: Mapping[IvaRateKind, IvaCategory]
rate_territories: frozenset[IvaTerritorialScope]
matches_nothing(criteria)[source]

Predicate of the registry’s terminal no_match row, which reads no axis.

Return type:

bool

Parameters:

criteria (IvaInvoiceClassificationCriteria)

resolve_iva_classification_inputs(*, effective_date, operation)[source]

Project the dated classification fact into typed evaluator inputs.

Return type:

IvaClassificationInputs

Parameters:
classify_iva(criteria, *, rules=None, rate_categories=None, rate_territories=None, operation)[source]

Evaluate registry-projected rows in their supplied order.

This function intentionally has no module-owned table or fallthrough row. rules and the rate_categories / rate_territories mappings are registry projections; when omitted they are projected from operation for the criteria’s transaction date.

Return type:

IvaClassificationResult

Parameters: