Invoice authoring¶
anafpy.efactura.authoring composes complete CIUS-RO invoices and credit notes
from business fields — no invoicing software and no UBL knowledge required. The
models are bidirectional: the same InvoiceDocument authors a filing and
views a parsed one, with byte-stable round-trips.
If your invoicing software already exports UBL XML, strongly prefer the pass-through path: its document is authoritative, anafpy never re-composes it, and — the part that outlives the filing — ANAF's SPV is not invoice storage: it purges filed messages after ~60 days, and an invoicing system is what keeps your durable record. Authoring exists for everyone else — a freelancer, an agent drafting an invoice in a chat, a script issuing a handful of invoices a month — with one obligation attached: download and keep the signed ZIP of every filing, because your copy is the one that lasts.
Composing an invoice¶
import datetime as dt
from decimal import Decimal
from anafpy.efactura.authoring import (
InvoiceDocument, InvoiceLine, Party, PostalAddress, Seller,
)
address = PostalAddress(
street="Str. Exemplu 1", city="Cluj-Napoca", county="RO-CJ", country="RO",
)
invoice = InvoiceDocument(
number="INV-2026-0042",
issue_date=dt.date(2026, 7, 8),
due_date=dt.date(2026, 8, 7),
currency="RON",
seller=Seller(name="Furnizor SRL", vat_id="RO12345678", address=address),
buyer=Party(name="Client SRL", vat_id="RO87654321", address=address),
lines=[
InvoiceLine(
name="Servicii de consultanta",
quantity=Decimal("10"),
unit="H87", # UN/ECE Rec 20/21: H87 = piece, KGM = kilogram
unit_price=Decimal("150.00"),
vat_category="S",
vat_rate=Decimal("19"),
),
],
)
That is a complete, fileable document: the totals and the VAT breakdown are
computed from the lines, document-level allowances and charges (grouped by VAT
category and rate, EN 16931 rounding). Supply explicit values only to reproduce
an upstream document's own arithmetic — they are preserved on render and
cross-checked by validate().
One semantic model covers both document types: pass kind="credit_note" (with a
preceding_invoices reference to the corrected invoice) and the same fields
render a UBL CreditNote instead, type code 381 and all.
The full EN 16931 surface is available when needed: payee and tax representative, delivery details, payment instructions (credit transfers, card, direct debit), document- and line-level allowances/charges, item identifiers/classifications/attributes, attachments, periods, and every reference term (contract, order, despatch, ...).
A party not registered for VAT¶
vat_id (BT-31/48) carries a VAT identifier, country prefix included. A party
below the VAT-registration threshold — a PFA, an ÎI, a micro-entity — has
none, and identifies itself by its bare CIF in tax_registration_id instead:
buyer = Party(name="Cumparator SRL", tax_registration_id="18888888", address=address)
On the seller that field is BT-32. EN 16931 gives the buyer no BT-32
counterpart, but CIUS-RO reads the same syntax slot for the buyer too
(BR-RO-120 accepts any PartyTaxScheme/CompanyID, VAT-schemed or not), which is
how Romanian issuers file to a customer under the threshold — so anafpy surfaces
the field on both parties. The scheme marker written alongside it defaults to
FC for the seller and !VAT for the buyer, each side's market convention;
tax_registration_scheme overrides it, and the reader keeps whatever marker the
source document used.
Two-tier validation¶
Construction enforces what a single field or model can know unconditionally — formats, the CIUS-RO length caps, the closed code lists (currencies, countries, units, payment means, VATEX, ...), decimal budgets, per-category VAT rate shapes, Romanian county/sector rules. Invalid data fails fast with a pointed message; ANAF would reject it with certainty anyway.
The cross-cutting rules run on demand:
from anafpy.efactura.authoring import validate
report = validate(invoice)
for finding in report.findings:
print(finding.rule, finding.message) # e.g. BR-CO-25, BR-S-08, BR-RO-120
validate() is a hand-translated port of the official EN 16931 + CIUS-RO
Schematron — totals arithmetic, VAT-regime identifier requirements, breakdown
consistency — reporting findings with the official rule ids and mirroring
the Schematron's own numeric tolerances. It is fast local feedback, not a
verdict: ANAF's server-side validare stays authoritative
(PublicClient.validate_invoice).
Rendering and filing¶
from anafpy.efactura.authoring import render_invoice
xml = render_invoice(invoice) # upload-ready UTF-8 bytes
# or file directly:
result = await efactura.upload_invoice(invoice, cif="12345678")
Both run validate() first and raise InvoiceValidationError (carrying the
report) on fatal findings; pass skip_validation=True to let ANAF be the only
judge.
Reading wire XML back¶
from anafpy.efactura.authoring import parse_invoice, read_invoice
document = parse_invoice(xml_bytes) # from bytes
document = read_invoice(message.document) # from a DownloadedMessage's UBL
The reader is full-fidelity: every wire amount lands in the explicit fields
(never recomputed), so re-rendering is byte-stable
(render_invoice(parse_invoice(rendered)) == rendered) and validate() can
judge an upstream document's arithmetic. This is the natural
starting point for drafting a credit note from a received invoice — and it is
exactly what DownloadedMessage.view returns for a
downloaded message, wrapped to yield
None instead of raising when the content is not a representable invoice.
It is also lenient about shape, and deliberately so. Authoring and reading
are the same models, not the same contract: the construction-time checks above
exist to stop you filing an invalid document, and re-applying them to one ANAF
already accepted can only lose it. So reading stands them down — a storno
filed the Romanian way (a type-380 invoice with negative amounts throughout), an
empty <cbc:PostalZone/>, two addresses in one BT-43, a category-O line with a
redundant 0 rate all read cleanly. validate() still judges the
cross-aggregate rules (totals arithmetic, breakdown consistency, regime
identifiers) on the result; single-field shapes that arrived in an accepted
document are ones ANAF tolerates, and are deliberately not re-reported as
findings either.
What still refuses to read is a missing mandatory element, or a value failing
one of the ANAF-certain mirrors — the closed BR-CL-* code lists, format
patterns, length limits, decimal budgets, all rules ANAF itself enforces
fatally. A refusal there means the vendored CIUS-RO edition has drifted from
ANAF's current one — see
schemas/README.md
for the re-vendoring playbook.