← Back to all articles

Come generare fatture Peppol BIS 3.0 in C#

SealDoc Team · · 6 min read

Generare una fattura Peppol BIS Billing 3.0 in C# non è concettualmente difficile. La parte difficile è ottenere le dichiarazioni di namespace corrette, inserire gli identificatori giusti nei campi giusti e produrre output che superi la validazione Schematron.

Questo articolo illustra un’implementazione minima ma completa. Presuppone che tu abbia letto l’introduzione a Peppol BIS 3.0 e comprenda il modello di dati EN16931. Se non hai familiarità con i Termini Aziendali come BT-24 o BT-131, parti da lì.

Namespace richiesti

UBL 2.1 usa tre namespace XML per i documenti di fattura:

static readonly XNamespace Ubl = "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2";
static readonly XNamespace Cac = "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2";
static readonly XNamespace Cbc = "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2";

Ubl è il namespace dell’elemento radice. Cac contiene gli elementi aggregati (composti) come PartyName e InvoiceLine. Cbc contiene gli elementi di base (scalari) come ID, Name e Amount.

Ogni elemento nella fattura usa uno di questi tre namespace. Sbagliare un namespace fa fallire la validazione dello schema prima ancora che venga eseguito Schematron.

I campi header obbligatori

Una fattura Peppol BIS 3.0 minimamente valida necessita di questi elementi header, in questo ordine:

var invoice = new XElement(Ubl + "Invoice",
    new XAttribute(XNamespace.Xmlns + "cac", Cac.NamespaceName),
    new XAttribute(XNamespace.Xmlns + "cbc", Cbc.NamespaceName),

    // BT-24: Specification identifier — identifies the exact profile
    new XElement(Cbc + "CustomizationID",
        "urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1"),

    // BT-23: Business process type
    new XElement(Cbc + "ProfileID",
        "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0"),

    // BT-1: Invoice number
    new XElement(Cbc + "ID", "INV-2026-001"),

    // BT-2: Invoice issue date
    new XElement(Cbc + "IssueDate", "2026-01-14"),

    // BT-3: Invoice type code (380 = Commercial Invoice)
    new XElement(Cbc + "InvoiceTypeCode", "380"),

    // BT-5: Invoice currency code
    new XElement(Cbc + "DocumentCurrencyCode", "EUR")
);

Il valore CustomizationID non è configurabile. Questa stringa esatta è obbligatoria per Peppol BIS 3.0. Usare l’URI base EN16931 invece di quello Peppol BIS è un errore comune che causa il rifiuto a livello di profilo.

Le parti venditore e acquirente

La struttura della parte in UBL è annidata: la parte si trova all’interno di un elemento ruolo.

// BG-4: Seller party
var seller = new XElement(Cac + "AccountingSupplierParty",
    new XElement(Cac + "Party",
        // BT-34: Seller electronic address (Peppol endpoint ID)
        new XElement(Cbc + "EndpointID",
            new XAttribute("schemeID", "0208"),
            "0468863455"),

        // BT-27: Seller name
        new XElement(Cac + "PartyName",
            new XElement(Cbc + "Name", "Acme GmbH")),

        // BG-5: Seller postal address
        new XElement(Cac + "PostalAddress",
            new XElement(Cbc + "StreetName", "Hauptstrasse 1"),
            new XElement(Cbc + "CityName", "Berlin"),
            new XElement(Cbc + "PostalZone", "10115"),
            new XElement(Cac + "Country",
                new XElement(Cbc + "IdentificationCode", "DE"))),

        // BT-31: Seller VAT identifier
        new XElement(Cac + "PartyTaxScheme",
            new XElement(Cbc + "CompanyID", "DE123456789"),
            new XElement(Cac + "TaxScheme",
                new XElement(Cbc + "ID", "VAT"))),

        // BT-30: Seller legal registration identifier
        new XElement(Cac + "PartyLegalEntity",
            new XElement(Cbc + "RegistrationName", "Acme GmbH"),
            new XElement(Cbc + "CompanyID", "HRB 12345"))
    )
);

// BG-7: Buyer party
var buyer = new XElement(Cac + "AccountingCustomerParty",
    new XElement(Cac + "Party",
        // BT-49: Buyer electronic address (Peppol endpoint ID)
        new XElement(Cbc + "EndpointID",
            new XAttribute("schemeID", "0106"),
            "123456789B01"),

        // BT-44: Buyer name
        new XElement(Cac + "PartyName",
            new XElement(Cbc + "Name", "Buyer BV")),

        new XElement(Cac + "PostalAddress",
            new XElement(Cbc + "StreetName", "Hoofdstraat 1"),
            new XElement(Cbc + "CityName", "Amsterdam"),
            new XElement(Cbc + "PostalZone", "1000 AA"),
            new XElement(Cac + "Country",
                new XElement(Cbc + "IdentificationCode", "NL"))),

        new XElement(Cac + "PartyLegalEntity",
            new XElement(Cbc + "RegistrationName", "Buyer BV"),
            new XElement(Cbc + "CompanyID", "123456789B01"))
    )
);

L’attributo schemeID su EndpointID è lo schema ISO 6523. 0208 è il numero d’impresa belga, 0106 è il numero KvK olandese. Usare il codice schema sbagliato fa fallire la ricerca del partecipante nella rete Peppol.

Voci di riga

Ogni voce di riga si mappa a un elemento InvoiceLine:

static XElement BuildLine(int lineId, string description, decimal quantity,
    decimal unitPrice, decimal vatRate)
{
    var lineNetAmount = Math.Round(quantity * unitPrice, 2);

    return new XElement(Cac + "InvoiceLine",
        // BT-126: Invoice line identifier
        new XElement(Cbc + "ID", lineId.ToString()),

        // BT-129: Invoiced quantity
        new XElement(Cbc + "InvoicedQuantity",
            new XAttribute("unitCode", "C62"),  // C62 = piece/unit
            quantity),

        // BT-131: Invoice line net amount
        new XElement(Cbc + "LineExtensionAmount",
            new XAttribute("currencyID", "EUR"),
            lineNetAmount),

        // BG-29: Price
        new XElement(Cac + "Price",
            new XElement(Cbc + "PriceAmount",
                new XAttribute("currencyID", "EUR"),
                unitPrice)),

        // BG-30: Line VAT
        new XElement(Cac + "Item",
            new XElement(Cbc + "Name", description),
            new XElement(Cac + "ClassifiedTaxCategory",
                new XElement(Cbc + "ID", "S"),
                new XElement(Cbc + "Percent", vatRate),
                new XElement(Cac + "TaxScheme",
                    new XElement(Cbc + "ID", "VAT"))))
    );
}

unitCode è un codice della raccomandazione UN/ECE 20. C62 significa ciascuno (pezzo). Altri codici comuni: HUR (ora), KGM (chilogrammo), MTR (metro).

Totali fiscali

EN16931 richiede importi fiscali raggruppati per categoria e aliquota. Per una semplice fattura a aliquota singola:

static XElement BuildTaxTotal(decimal taxableAmount, decimal vatRate, decimal vatAmount)
{
    return new XElement(Cac + "TaxTotal",
        new XElement(Cbc + "TaxAmount",
            new XAttribute("currencyID", "EUR"),
            vatAmount),
        new XElement(Cac + "TaxSubtotal",
            new XElement(Cbc + "TaxableAmount",
                new XAttribute("currencyID", "EUR"),
                taxableAmount),
            new XElement(Cbc + "TaxAmount",
                new XAttribute("currencyID", "EUR"),
                vatAmount),
            new XElement(Cac + "TaxCategory",
                new XElement(Cbc + "ID", "S"),
                new XElement(Cbc + "Percent", vatRate),
                new XElement(Cac + "TaxScheme",
                    new XElement(Cbc + "ID", "VAT"))))
    );
}

Totali del documento

Il LegalMonetaryTotal deve soddisfare diverse regole di calcolo EN16931. Arrotonda al livello aggregato, non per riga:

static XElement BuildTotals(decimal lineNetTotal, decimal vatAmount, decimal grandTotal)
{
    return new XElement(Cac + "LegalMonetaryTotal",
        // BT-106: Sum of line net amounts
        new XElement(Cbc + "LineExtensionAmount",
            new XAttribute("currencyID", "EUR"), lineNetTotal),

        // BT-109: Invoice total amount without VAT
        new XElement(Cbc + "TaxExclusiveAmount",
            new XAttribute("currencyID", "EUR"), lineNetTotal),

        // BT-112: Invoice total amount with VAT
        new XElement(Cbc + "TaxInclusiveAmount",
            new XAttribute("currencyID", "EUR"), grandTotal),

        // BT-115: Amount due for payment
        new XElement(Cbc + "PayableAmount",
            new XAttribute("currencyID", "EUR"), grandTotal)
    );
}

Calcola gli importi come decimal, arrotonda con Math.Round(value, 2, MidpointRounding.AwayFromZero) in modo coerente. Mescolare strategie di arrotondamento tra righe e totali è la causa più comune di fallimenti BR-CO-10 e BR-CO-15.

Validazione Schematron

La validazione dello schema (XSD) rileva gli errori strutturali. La validazione Schematron rileva le violazioni delle regole aziendali. Servono entrambe.

Scarica i file XSLT precompilati dalle release GitHub di Peppol BIS. Poi eseguili con Saxon HE:

dotnet add package SaxonHE12s9apiExtensions
using net.sf.saxon.s9api;

static IEnumerable<string> ValidateWithSchematron(string invoiceXml, string xsltPath)
{
    var proc = new Processor(licensed: false);
    var exec = proc.newXsltCompiler()
                   .compile(new StreamSource(xsltPath));

    var trans  = exec.load30();
    var result = new XdmDestination();
    trans.applyTemplates(
        new StreamSource(new java.io.StringReader(invoiceXml)),
        result);

    // Parse SVRL for failed-assert elements
    var svrl = XDocument.Parse(result.XdmNode.ToString());
    var svrlNs = XNamespace.Get("http://purl.oclc.org/dsdl/svrl");
    return svrl.Descendants(svrlNs + "failed-assert")
               .Select(e => e.Element(svrlNs + "text")?.Value?.Trim() ?? "")
               .Where(t => !string.IsNullOrEmpty(t));
}

Eseguilo rispetto a peppol-en16931-ubl.xsl dagli artefatti di release. Il file XSLT applica sia le regole base EN16931 che le regole di estensione Peppol BIS 3.0.

Mezzi di pagamento

Se hai bisogno di specificare un bonifico bancario (SEPA), aggiungi questo prima di TaxTotal:

new XElement(Cac + "PaymentMeans",
    // BT-81: Payment means code (30 = Credit transfer)
    new XElement(Cbc + "PaymentMeansCode", "30"),
    new XElement(Cac + "PayeeFinancialAccount",
        new XElement(Cbc + "ID", "NL91ABNA0417164300"),  // IBAN
        new XElement(Cac + "FinancialInstitutionBranch",
            new XElement(Cbc + "ID", "ABNANL2A"))))  // BIC

BT-81 codice 30 (bonifico bancario) è il più comune. 58 (bonifico SEPA) è la variante quando sono necessari campi specifici SEPA.

Cosa viene dopo

Generare un XML di fattura valido è la base. Il livello successivo è la trasmissione: la fattura deve raggiungere l’acquirente tramite un Access Point Peppol. Ciò richiede di conoscere l’endpoint dell’acquirente, che si ottiene tramite la discovery SMP/SML. Vedi come funziona la discovery Peppol SMP e SML per il processo di ricerca.

Per l’uso in produzione, SealDoc espone un’API REST che gestisce la generazione UBL, la validazione Schematron e l’instradamento sulla rete Peppol in una singola chiamata. L’input sono i dati della tua fattura in JSON; l’output è un documento validato e timestampato pronto per la consegna.


← Back to all articles