Skip to content

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.