← Back to all articles

Jak generovat faktury Peppol BIS 3.0 v C#

SealDoc Team · · 6 min read

Generování faktury Peppol BIS Billing 3.0 v C# není konceptuálně obtížné. Obtížná část spočívá ve správném nastavení deklarací jmenných prostorů, umístění správných identifikátorů do správných polí a produkci výstupu, který projde Schematron validací.

Tento článek prochází minimální, ale kompletní implementací. Předpokládá, že jste si přečetli úvod do Peppol BIS 3.0 a rozumíte datovému modelu EN16931. Pokud neznáte obchodní termíny jako BT-24 nebo BT-131, začněte tam.

Požadované jmenné prostory

UBL 2.1 používá pro fakturační dokumenty tři XML jmenné prostory:

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 je jmenný prostor kořenového elementu. Cac obsahuje agregátní (kompozitní) elementy jako PartyName a InvoiceLine. Cbc obsahuje základní (skalární) elementy jako ID, Name a Amount.

Každý element ve faktuře používá jeden z těchto tří jmenných prostorů. Chybný jmenný prostor způsobí selhání validace schématu dříve, než se vůbec spustí Schematron.

Povinná pole záhlaví

Minimálně platná faktura Peppol BIS 3.0 potřebuje tyto elementy záhlaví v tomto pořadí:

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")
);

Hodnota CustomizationID není konfigurovatelná. Tento přesný řetězec je vyžadován pro Peppol BIS 3.0. Použití základního URI EN16931 namísto URI Peppol BIS je častá chyba způsobující odmítnutí na úrovni profilu.

Strany prodávajícího a kupujícího

Struktura strany v UBL je vnořená: strana sedí uvnitř elementu role.

// 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"))
    )
);

Atribut schemeID na EndpointID je schéma ISO 6523. 0208 je belgické číslo podniku, 0106 je nizozemské číslo KvK. Použití špatného kódu schématu způsobí selhání vyhledání účastníka v síti Peppol.

Položky řádků

Každá položka řádku mapuje na element 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 je kód dle doporučení UN/ECE 20. C62 znamená kus. Další běžné kódy: HUR (hodina), KGM (kilogram), MTR (metr).

Daňové součty

EN16931 vyžaduje daňové částky seskupené podle kategorie a sazby. Pro jednoduchou jednosazbovou fakturu:

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"))))
    );
}

Celkové hodnoty dokumentu

LegalMonetaryTotal musí splňovat několik výpočetních pravidel EN16931. Zaokrouhlujte na úrovni agregátu, nikoli po řádcích:

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)
    );
}

Počítejte částky jako decimal, zaokrouhlujte s Math.Round(value, 2, MidpointRounding.AwayFromZero) konzistentně v celém kódu. Míchání strategií zaokrouhlování mezi řádky a součty je nejčastější příčinou selhání BR-CO-10 a BR-CO-15.

Schematron validace

Validace schématu (XSD) zachycuje strukturální chyby. Schematron validace zachycuje porušení obchodních pravidel. Potřebujete obojí.

Stáhněte předkompilované soubory XSLT z vydání Peppol BIS na GitHubu. Pak je spusťte se 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));
}

Spusťte toto vůči peppol-en16931-ubl.xsl z artefaktů vydání. Soubor XSLT aplikuje jak základní pravidla EN16931, tak pravidla rozšíření Peppol BIS 3.0.

Platební prostředky

Pokud potřebujete uvést bankovní převod (SEPA), přidejte toto před 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

Kód BT-81 30 (bankovní převod) je nejčastější. 58 (SEPA bankovní převod) je varianta, když jsou potřeba SEPA specifická pole.

Co přijde dál

Generování platné XML faktury je základ. Další vrstvou je přenos: faktura musí dorazit ke kupujícímu přes přístupový bod Peppol. To vyžaduje znalost endpointu kupujícího, který získáte prostřednictvím SMP/SML discovery. Viz jak funguje Peppol SMP a SML discovery pro popis procesu vyhledání.

Pro produkční použití SealDoc vystavuje REST API, které zpracovává generování UBL, Schematron validaci a směrování sítí Peppol v jednom volání. Vstupem jsou data vaší faktury ve formátu JSON; výstupem je validovaný, orazítkovaný dokument připravený k doručení.


← Back to all articles