Source code for cadrumo.application.aggregation.invoice_devengo

"""LIVA art. 75 devengo resolution and period attribution for one invoice record.

Art. 75.Uno binds the general regime to the operation date -- when the entrega
or prestación actually took place -- not to the bank movement or the invoice's
issue date. Art. 75.Dos moves devengo forward to the date of collection for a
pago anticipado, "por los importes efectivamente percibidos", except for the
entregas comprendidas en el artículo 25 (an entrega intracomunitaria exenta,
which always devengues under art. 75.Uno.8.º regardless of any advance
received).

:class:`~cadrumo.domain.invoices.models.Invoice` already carries both cases behind
one field: :attr:`~cadrumo.domain.invoices.models.Invoice.operation_date` records the
date, and :attr:`~cadrumo.domain.invoices.models.Invoice.operation_date_role` records
which of art. 75's two clauses it answers. The art. 25 exclusion and the
"money was actually received" precondition are enforced at construction time
on the invoice itself, so a record carrying an ``ADVANCE_PAYMENT_RECEIVED``
role that reaches this module is already a legally consistent one; this
module supplies nothing beyond reading the fact the invoice already declares
and, where the invoice declares nothing, saying so out loud.

That last part is why the resolution is a record and not a date. Not every
invoice carries a recorded operation date, so a fallback exists, and a
fallback that returns a bare date collapses two structurally different answers
-- a declared legal fact and a substitute for one -- into one indistinguishable
return. The issue date is a substitute that agrees with the fact for every
operation invoiced inside its own period and diverges exactly at the month and
quarter boundaries where attribution changes, so the difference is the whole
point.
:class:`~cadrumo.core.aggregation.InvoiceDevengoRank` carries it.

:func:`invoice_devengo_in_period` is the single period-attribution predicate
for the invoice catalogue. Every aggregation path that asks "does this invoice
belong to this filing period" calls it rather than comparing a date of its own
choosing, so the calculate path and the Sheets pull path cannot answer the
question differently: there is one implementation and both consume it.

A staged schedule of several partial advance payments, each devengoing
separately for its own amount, is not representable by a single date and is
not attempted here.

See Also:
    :attr:`cadrumo.domain.invoices.models.Invoice.operation_date`
        The recorded devengo-relevant date this module reads.
    :attr:`cadrumo.domain.invoices.models.Invoice.operation_date_role`
        The discriminator naming which art. 75 clause the date answers.
    :class:`cadrumo.core.aggregation.InvoiceDevengoRank`
        The marker naming which source produced the resolved date.
    :func:`cadrumo.domain.transactions.dates.transaction_eligible_date_span`
        The equivalent ledger-transaction-side devengo span, already wired
        into IVA period attribution.
"""

from __future__ import annotations

from datetime import date
from typing import TYPE_CHECKING, Final

from pydantic import BaseModel

from ...core.aggregation import InvoiceDevengoRank
from ...core.models import STRICT_FROZEN_CONFIG as _STRICT_FROZEN
from .source_mesh import CalculationSourceDiagnostic

if TYPE_CHECKING:
    from collections.abc import Iterable

    from ...core.period import Period
    from ...domain.invoices.models import Invoice

__all__ = [
    "InvoiceDevengo",
    "devengo_proxy_attribution_diagnostics",
    "invoice_devengo_in_period",
    "proxy_attributed_invoice_ids",
    "resolve_invoice_devengo",
]

_PROXY_SAMPLE_LIMIT: Final = 5
"""How many invoice numbers the advisory names before eliding the rest.

Chosen so the rendered message stays inside
:data:`~application.aggregation.source_mesh.DIAGNOSTIC_MESSAGE_MAX_LENGTH` for realistic
invoice numbering. The bound truncates rather than raises, so overflowing it
would silently swallow the elision notice and leave the operator reading a
sample they could not tell was incomplete.
"""


[docs] class InvoiceDevengo(BaseModel): """The date one invoice files under, and where that date came from. The two fields are read together or not at all. A consumer that takes :attr:`devengo_date` while ignoring :attr:`rank` has re-created the bare return this record exists to replace, and will treat a proxy as a declared fact at exactly the boundary where the two disagree. Attributes: devengo_date: The resolved LIVA art. 75 devengo date. rank: Which source produced it. """ model_config = _STRICT_FROZEN devengo_date: date rank: InvoiceDevengoRank
[docs] def resolve_invoice_devengo(invoice: Invoice) -> InvoiceDevengo: """Return the LIVA art. 75 devengo date for one invoice, with its rank. Args: invoice: The invoice whose devengo date is being resolved. Returns: The recorded :attr:`~cadrumo.domain.invoices.models.Invoice.operation_date` ranked :attr:`~cadrumo.core.aggregation.InvoiceDevengoRank.OPERATION_DATE_DECLARED` when the invoice carries one -- either art. 75.Uno's operation date or art. 75.Dos's collection date, both read identically here because the role says WHICH clause supplied the date, not how it is used -- otherwise :attr:`~cadrumo.domain.invoices.models.Invoice.issued_at` ranked :attr:`~cadrumo.core.aggregation.InvoiceDevengoRank.ISSUE_DATE_PROXY`. """ operation_date = invoice.operation_date if operation_date is not None: return InvoiceDevengo( devengo_date=operation_date, rank=InvoiceDevengoRank.OPERATION_DATE_DECLARED, ) return InvoiceDevengo(devengo_date=invoice.issued_at, rank=InvoiceDevengoRank.ISSUE_DATE_PROXY)
[docs] def invoice_devengo_in_period(invoice: Invoice, *, period: Period) -> bool: """Return whether *invoice* devengues inside *period*. The single period-attribution predicate for the invoice catalogue. It selects on the art. 75 devengo date rather than on the issue date, so an operation performed in one quarter and invoiced in the next is declared in the quarter it was performed. Args: invoice: The invoice being attributed. period: The filing period, which must carry a calendar span. Returns: Whether the resolved devengo date falls inside the period's inclusive span. """ return period.contains(resolve_invoice_devengo(invoice).devengo_date)
[docs] def proxy_attributed_invoice_ids(invoices: Iterable[Invoice]) -> tuple[str, ...]: """Return the sorted ids of *invoices* attributed on the issue-date proxy. The reporting half of the rank. An aggregation path hands it the invoices it actually folded in, and a non-empty result names the records whose period placement rests on a substitute rather than on a recorded devengo date -- the set an operator has to see before filing, because those are the records that would move quarter if the real date were recorded. Args: invoices: The invoices a period's figures were built from. Returns: The sorted invoice ids ranked :attr:`~cadrumo.core.aggregation.InvoiceDevengoRank.ISSUE_DATE_PROXY`, empty when every contributing invoice declared its own devengo date. """ return tuple( sorted( invoice.invoice_id for invoice in invoices if resolve_invoice_devengo(invoice).rank is InvoiceDevengoRank.ISSUE_DATE_PROXY ), )
[docs] def devengo_proxy_attribution_diagnostics( invoices: Iterable[Invoice], *, source_kind: str, resolver_id: str, ) -> tuple[CalculationSourceDiagnostic, ...]: """Return the non-blocking advisory for a period built on proxy-ranked invoices. One shared builder rather than a per-resolver message, so every aggregation path that folds in invoices discloses the substitution in the same words. A path that phrased it for itself would let the same taxpayer see the same condition described two ways depending on which modelo they were filing. Non-blocking by design. The issue date is the best available attribution and refusing on it would make an ordinary catalogue unfileable; what the operator needs is to know the period placement is a substitution before they file, which is what the advisory says. Args: invoices: The invoices this period's figures were built from. source_kind: The binding source kind the calling resolver owns. resolver_id: The calling resolver's id. Returns: One advisory naming the proxy-attributed records, or an empty tuple when every contributing invoice declared its own devengo date. """ proxied = tuple( invoice for invoice in invoices if resolve_invoice_devengo(invoice).rank is InvoiceDevengoRank.ISSUE_DATE_PROXY ) if not proxied: return () # Named by invoice NUMBER, not by the content-addressed id every other # diagnostic on this surface carries. The remedy asks the operator to open # each record and record a date, and the number is what they see in the # catalogue listing; a 64-hex digest would also exhaust the message bound # after two entries and elide the rest of the sample. numbers = sorted(invoice.invoice_number for invoice in proxied) sample = ", ".join(numbers[:_PROXY_SAMPLE_LIMIT]) elided = len(numbers) - len(numbers[:_PROXY_SAMPLE_LIMIT]) suffix = f" and {elided} more" if elided else "" return ( CalculationSourceDiagnostic( reason="devengo_date_proxy_attribution", source_kind=source_kind, resolver_id=resolver_id, message=( f"{len(numbers)} invoice(s) were placed in this filing period on their issue " f"date because no operation date is recorded on them: {sample}{suffix}. " "LIVA art. 75.Uno binds the devengo to the date the entrega or prestacion took " "place, and an invoice may lawfully be issued in a later period than the one " "its operation belongs to, so this placement is a substitution, not a legal " "determination" ), remedy=( "Record the fecha de operacion on each invoice named so its period is " "determined by the art. 75 devengo date rather than inferred from the issue date" ), # Advisory-asserted: this module holds no revision, snapshot or # casilla definition anywhere -- devengo attribution applies # uniformly across every IVA-relevant modelo, so there is no # single casilla whose own ref this could read instead. asserted_legal_refs=("ley-37-1992:art-75",), ), )