Extended goods invoice
A complete goods invoice in the EXTENDED profile, rebuilt step by step: six line items with packaging details and item discounts, two tax rates, document-level invoice discounts, transport costs, an early payment discount, and a ship-to party and invoicee that both differ from the buyer.
This is not just any example, but the official sample invoice
X19_01_Warenrechnung from the Factur-X / ZUGFeRD
documentation (FeRD sample package, ZUGFeRD 2.5.0, EXTENDED profile).
The code below produces exactly that document – field by field, in the
same order. Put the reference XML next to the output and you will see the
same tree structure.
Some links on this page lead into the in-depth documentation in the customer area and require a sign-in.
In business terms it is a delivery from a food wholesaler to a supermarket branch. That mixture is what makes the example instructive: almost every field that raises questions in day-to-day work appears in it at least once.
The sections below build on one another. All snippets belong to one program and can be written one after another in this order. The comments name the codes from EN 16931 and from the ZUGFeRD EXTENDED extension; the reference gives you the matching XML element for each of them.
Every EXTENDED field in this example – document name, test indicator,
invoicee, packaging details, the components of a sales unit, transport costs
and the extended tax bases – is only written if the document is in fact
saved as Profile.Extended at the end. A smaller profile is not an
error; the fields simply drop out without a word.
1. Invoice header
CreateInvoice sets the three mandatory header fields: invoice number
(BT-1), invoice date (BT-2) and invoice currency (BT-5). Everything else is
attached to the returned object afterwards.
// BT-1 invoice number, BT-2 invoice date, BT-5 invoice currency
FacturXInvoice invoice = FacturXInvoice.CreateInvoice(
invoiceNo: "R87654321012345",
invoiceDate: new DateTime(2025, 10, 1),
currency: CurrencyCodes.EUR);
// BT-3: invoice type 380 = commercial invoice. This is the default, but it is spelled
// out here so that it is visible where the type is set.
invoice.Type = InvoiceType.Invoice;
// BT-X-2: free-form document name. Allowed in the EXTENDED profile only and written
// only there – in BASIC/COMFORT the writer silently drops the element.
invoice.Name = "WARENRECHNUNG";
// ram:TestIndicator marks the document as test data (EXTENDED only as well).
invoice.IsTest = true;
' BT-1 invoice number, BT-2 invoice date, BT-5 invoice currency
Dim invoice As FacturXInvoice = FacturXInvoice.CreateInvoice(
invoiceNo:="R87654321012345",
invoiceDate:=New DateTime(2025, 10, 1),
currency:=CurrencyCodes.EUR)
' BT-3: invoice type 380 = commercial invoice. This is the default, but it is spelled
' out here so that it is visible where the type is set.
invoice.Type = InvoiceType.Invoice
' BT-X-2: free-form document name. Allowed in the EXTENDED profile only and written
' only there – in BASIC/COMFORT the writer silently drops the element.
invoice.Name = "WARENRECHNUNG"
' ram:TestIndicator marks the document as test data (EXTENDED only as well).
invoice.IsTest = True
2. Free-text notes
Free text ends up in ram:IncludedNote (BG-1). Two codes control how
machine-readable it is: subjectCode (BT-21, UNTDID 4451) says
what the note is about, contentCode (BT-X-5) is an
additional standardised text block.
The two codes cannot be combined freely: ST1,
ST2 and ST3 belong to AAK (discount and
bonus agreements), while EEV, WEB and
VEV belong to AAJ (retention of title).
// REG = regulatory information, here the legal representation of the company.
invoice.AddNote("Geschäftsführer: Herr Geschäftsführer , MUSTERLIEFERANT GmbH ",
subjectCode: SubjectCodes.REG);
// AAI = general information.
invoice.AddNote("Es bestehen Vereinbarungen, aus denen sich Minderungen des Entgelts ergeben können.",
subjectCode: SubjectCodes.AAI);
// ACB = additional information; used here for the format identification …
invoice.AddNote("ZUGFeRD vers 2.5.0 (Extended)",
subjectCode: SubjectCodes.ACB);
// … and for the note that this is a sample document.
invoice.AddNote("Dies ist ein Waren-Rechnungs-Beispiel",
subjectCode: SubjectCodes.ACB);
// AAK + ST3: standardised reference to existing discount or bonus agreements.
invoice.AddNote("Es bestehen Rabatt- oder Bonusvereinbarungen.",
subjectCode: SubjectCodes.AAK,
contentCode: ContentCodes.ST3);
// AAJ + EEV: standardised retention of title.
invoice.AddNote("Der Verkäufer bleibt Eigentümer der Waren bis zu vollständigen Erfüllung der Kaufpreisforderung.",
subjectCode: SubjectCodes.AAJ,
contentCode: ContentCodes.EEV);
// Notes without a subjectCode are allowed – plain free text.
invoice.AddNote("Leergutwert: 46,50");
invoice.AddNote("Wichtige Information: Bei Bestellungen bis zum 19.12. ist die Auslieferung bis spätestens 23.12. garantiert.");
' REG = regulatory information, here the legal representation of the company.
invoice.AddNote("Geschäftsführer: Herr Geschäftsführer , MUSTERLIEFERANT GmbH ",
subjectCode:=SubjectCodes.REG)
' AAI = general information.
invoice.AddNote("Es bestehen Vereinbarungen, aus denen sich Minderungen des Entgelts ergeben können.",
subjectCode:=SubjectCodes.AAI)
' ACB = additional information; used here for the format identification …
invoice.AddNote("ZUGFeRD vers 2.5.0 (Extended)",
subjectCode:=SubjectCodes.ACB)
' … and for the note that this is a sample document.
invoice.AddNote("Dies ist ein Waren-Rechnungs-Beispiel",
subjectCode:=SubjectCodes.ACB)
' AAK + ST3: standardised reference to existing discount or bonus agreements.
invoice.AddNote("Es bestehen Rabatt- oder Bonusvereinbarungen.",
subjectCode:=SubjectCodes.AAK,
contentCode:=ContentCodes.ST3)
' AAJ + EEV: standardised retention of title.
invoice.AddNote("Der Verkäufer bleibt Eigentümer der Waren bis zu vollständigen Erfüllung der Kaufpreisforderung.",
subjectCode:=SubjectCodes.AAJ,
contentCode:=ContentCodes.EEV)
' Notes without a subjectCode are allowed – plain free text.
invoice.AddNote("Leergutwert: 46,50")
invoice.AddNote("Wichtige Information: Bei Bestellungen bis zum 19.12. ist die Auslieferung bis spätestens 23.12. garantiert.")
The note texts, product names and discount reasons stay in German throughout this page: they are the payload of the official reference document, and translating them would break the comparison with it.
3. Seller
SetSeller fills ram:SellerTradeParty (BG-4). Besides the
address, the party carries two identifiers: the internal supplier number
(id, BT-29) and the GLN (globalID with schemeID
0088). Contact, electronic address and tax registration are added
through their own methods.
// id → ram:ID (BT-29) internal supplier number
// globalID → ram:GlobalID (BT-29-0) with schemeID "0088" = GLN
invoice.SetSeller(
name: "MUSTERLIEFERANT GMBH",
postcode: "99199",
city: "MUSTERHAUSEN",
street: "BAHNHOFSTRASSE 99",
country: CountryCodes.DE,
id: "549910",
globalID: new GlobalID(GlobalIDSchemeIdentifiers.GLN, "4333741000005"));
// BT-42 / BT-43: contact person. Name and department stay empty – the writer then
// does not emit ram:PersonName / ram:DepartmentName at all.
invoice.SetSellerContact(
emailAddress: "max.mustermann@musterlieferant.de",
phoneno: "+49 932 431 500");
// BT-34: electronic address of the seller, here as an EAN location code (0088).
invoice.SetSellerElectronicAddress("info@musterlieferant.de",
ElectronicAddressSchemeIdentifiers.EanLocationCode);
// BT-31: seller VAT ID (scheme VA). FC would be the national tax number.
invoice.AddSellerTaxRegistration("DE123456788", TaxRegistrationSchemeID.VA);
' id → ram:ID (BT-29) internal supplier number
' globalID → ram:GlobalID (BT-29-0) with schemeID "0088" = GLN
invoice.SetSeller(
name:="MUSTERLIEFERANT GMBH",
postcode:="99199",
city:="MUSTERHAUSEN",
street:="BAHNHOFSTRASSE 99",
country:=CountryCodes.DE,
id:="549910",
globalID:=New GlobalID(GlobalIDSchemeIdentifiers.GLN, "4333741000005"))
' BT-42 / BT-43: contact person. Name and department stay empty – the writer then
' does not emit ram:PersonName / ram:DepartmentName at all.
invoice.SetSellerContact(
emailAddress:="max.mustermann@musterlieferant.de",
phoneno:="+49 932 431 500")
' BT-34: electronic address of the seller, here as an EAN location code (0088).
invoice.SetSellerElectronicAddress("info@musterlieferant.de",
ElectronicAddressSchemeIdentifiers.EanLocationCode)
' BT-31: seller VAT ID (scheme VA). FC would be the national tax number.
invoice.AddSellerTaxRegistration("DE123456788", TaxRegistrationSchemeID.VA)
4. Buyer
ram:BuyerTradeParty (BG-7) works identically. One point matters for
the following sections: the buyer here is the head office – the goods go to
a branch, and the invoice goes to a third site.
// The buyer is the head office here; the ship-to party and the invoicee differ
// from it and follow below.
invoice.SetBuyer(
name: "MUSTER-KUNDE GMBH",
postcode: "40235",
city: "KUNDENSTADT",
street: "KUNDENWEG 88",
country: CountryCodes.DE,
id: "009420",
globalID: new GlobalID(GlobalIDSchemeIdentifiers.GLN, "4304171000002"));
' The buyer is the head office here; the ship-to party and the invoicee differ
' from it and follow below.
invoice.SetBuyer(
name:="MUSTER-KUNDE GMBH",
postcode:="40235",
city:="KUNDENSTADT",
street:="KUNDENWEG 88",
country:=CountryCodes.DE,
id:="009420",
globalID:=New GlobalID(GlobalIDSchemeIdentifiers.GLN, "4304171000002"))
5. Document references in the header
Four references tie the invoice into the surrounding document flow: purchase order, invoice data sheet, despatch advice and delivery date. Other kinds of reference – project and contract numbers, for instance – are covered in document references.
// BT-13: buyer's purchase order number → ram:BuyerOrderReferencedDocument
invoice.SetBuyerOrderReferenceDocument("B123456789");
// BT-18 / BG-24: additional document reference. Type code 130 = invoice data sheet
// (InvoiceDataSheet) – the document number the supplier assigned to that data sheet.
invoice.AddAdditionalReferencedDocument(
id: "A456123",
typeCode: AdditionalReferencedDocumentTypeCode.InvoiceDataSheet);
// BT-16: despatch advice number → ram:DeliveryNoteReferencedDocument (EXTENDED)
invoice.SetDeliveryNoteReferenceDocument("L87654321012345");
// BT-72: actual delivery date → ram:ActualDeliverySupplyChainEvent
invoice.ActualDeliveryDate = new DateTime(2025, 10, 1);
' BT-13: buyer's purchase order number → ram:BuyerOrderReferencedDocument
invoice.SetBuyerOrderReferenceDocument("B123456789")
' BT-18 / BG-24: additional document reference. Type code 130 = invoice data sheet
' (InvoiceDataSheet) – the document number the supplier assigned to that data sheet.
invoice.AddAdditionalReferencedDocument(
id:="A456123",
typeCode:=AdditionalReferencedDocumentTypeCode.InvoiceDataSheet)
' BT-16: despatch advice number → ram:DeliveryNoteReferencedDocument (EXTENDED)
invoice.SetDeliveryNoteReferenceDocument("L87654321012345")
' BT-72: actual delivery date → ram:ActualDeliverySupplyChainEvent
invoice.ActualDeliveryDate = New DateTime(2025, 10, 1)
6. Ship-to party and invoicee
The ship-to party (BG-13) and the invoicee (BG-X-36) are not set through
Set… methods but as Party objects. The ship-to party
carries only a GLN and no internal ID here; ShipToContact.OrgUnit
additionally puts the receiving department of the branch into the document.
// BG-13 ram:ShipToTradeParty: delivery goes to the store, not to the head office.
// There is no internal ID here, only a GLN.
invoice.ShipTo = new Party()
{
GlobalID = new GlobalID(GlobalIDSchemeIdentifiers.GLN, "4304171088093"),
Name = "MUSTER-MARKT",
Postcode = "31157",
Street = "HAUPTSTRASSE 44",
City = "SARSTEDT",
Country = CountryCodes.DE
};
// ram:DefinedTradeContact/ram:DepartmentName – the department ("8211") in the store.
// OrgUnit is exactly that field; name, phone and e-mail stay empty.
invoice.ShipToContact = new Contact()
{
OrgUnit = "8211"
};
// BG-X-36 ram:InvoiceeTradeParty (EXTENDED only): same company as the buyer, but the
// invoice goes to the Düsseldorf site.
invoice.Invoicee = new Party()
{
ID = new GlobalID(null, "009420"),
GlobalID = new GlobalID(GlobalIDSchemeIdentifiers.GLN, "4304171000002"),
Name = "MUSTER-KUNDE GMBH",
Postcode = "40235",
Street = "KUNDENWEG 88",
City = "DUESSELDORF",
Country = CountryCodes.DE
};
' BG-13 ram:ShipToTradeParty: delivery goes to the store, not to the head office.
' There is no internal ID here, only a GLN.
invoice.ShipTo = New Party() With
{
.GlobalID = New GlobalID(GlobalIDSchemeIdentifiers.GLN, "4304171088093"),
.Name = "MUSTER-MARKT",
.Postcode = "31157",
.Street = "HAUPTSTRASSE 44",
.City = "SARSTEDT",
.Country = CountryCodes.DE
}
' ram:DefinedTradeContact/ram:DepartmentName – the department ("8211") in the store.
' OrgUnit is exactly that field; name, phone and e-mail stay empty.
invoice.ShipToContact = New Contact() With
{
.OrgUnit = "8211"
}
' BG-X-36 ram:InvoiceeTradeParty (EXTENDED only): same company as the buyer, but the
' invoice goes to the Düsseldorf site.
invoice.Invoicee = New Party() With
{
.ID = New GlobalID(Nothing, "009420"),
.GlobalID = New GlobalID(GlobalIDSchemeIdentifiers.GLN, "4304171000002"),
.Name = "MUSTER-KUNDE GMBH",
.Postcode = "40235",
.Street = "KUNDENWEG 88",
.City = "DUESSELDORF",
.Country = CountryCodes.DE
}
7. Invoice lines
Six line items (BG-25), each with a speciality of its own. The recurring pattern is the same in all six:
| Parameter | BT code | Meaning |
|---|---|---|
lineID |
BT-126 | Line number. If it is omitted, FactoorSharp assigns it automatically – see line IDs. |
id |
BT-157 | GTIN with schemeID 0160 (GS1), in FactoorSharp GlobalIDSchemeIdentifiers.EAN. |
sellerAssignedID |
BT-155 | The supplier's item number. |
buyerAssignedID |
BT-156 | The buyer's item number for the same goods. |
grossUnitPrice |
BT-148 | Gross price, i.e. the list price before item discounts. |
netUnitPrice |
BT-146 | Net price after item discounts – the basis of the line total. |
lineTotalAmount |
BT-131 | Line total = netUnitPrice × billedQuantity. |
PackageQuantity / PackageUnitCode |
BT-X-9 | Shipping unit, EXTENDED only. XCT = carton, XBC = crate, XBO = bottle, XPX = pallet. |
Line 1 – item attribute and shipping unit
The simplest line: no discounts, one item attribute (BG-32) and the number of cartons the goods arrive in. Item attributes have a page of their own: product characteristics.
// 100 bottles of citric acid at EUR 1.00 = EUR 100.00, 19 % VAT
TradeLineItem line1 = invoice.AddTradeLineItem(
lineID: "1",
name: "Zitronensäure 100ml",
netUnitPrice: 1.00m,
grossUnitPrice: 1.00m,
unitCode: QuantityCodes.H87, // H87 = piece
billedQuantity: 100m,
lineTotalAmount: 100.00m,
taxType: TaxTypes.VAT,
categoryCode: TaxCategoryCodes.S, // S = standard rate
taxPercent: 19m,
sellerAssignedID: "ZS997",
id: new GlobalID(GlobalIDSchemeIdentifiers.EAN, "4123456000014"));
// BG-32 item attribute: a freely definable name/value pair (BT-160 / BT-161).
line1.ApplicableProductCharacteristics.Add(new ApplicableProductCharacteristic()
{
Description = "Verpackungsart",
Value = "BO"
});
// 4 cartons as the shipping unit.
line1.PackageQuantity = 4m;
line1.PackageUnitCode = QuantityCodes.XCT;
' 100 bottles of citric acid at EUR 1.00 = EUR 100.00, 19 % VAT
Dim line1 As TradeLineItem = invoice.AddTradeLineItem(
lineID:="1",
name:="Zitronensäure 100ml",
netUnitPrice:=1.0D,
grossUnitPrice:=1.0D,
unitCode:=QuantityCodes.H87, ' H87 = piece
billedQuantity:=100D,
lineTotalAmount:=100.0D,
taxType:=TaxTypes.VAT,
categoryCode:=TaxCategoryCodes.S, ' S = standard rate
taxPercent:=19D,
sellerAssignedID:="ZS997",
id:=New GlobalID(GlobalIDSchemeIdentifiers.EAN, "4123456000014"))
' BG-32 item attribute: a freely definable name/value pair (BT-160 / BT-161).
line1.ApplicableProductCharacteristics.Add(New ApplicableProductCharacteristic() With
{
.Description = "Verpackungsart",
.Value = "BO"
})
' 4 cartons as the shipping unit.
line1.PackageQuantity = 4D
line1.PackageUnitCode = QuantityCodes.XCT
Line 2 – item discounts inside the gross price
Gross and net price differ here because two item discounts are involved. These
discounts do not live at document level but as
ram:AppliedTradeAllowanceCharge inside the gross price (BT-147).
The amounts are per unit, not for the whole line: 1.50 − 0.03 − 0.02 = 1.45. FactoorSharp does not compute the net price – you set it, and the discounts explain it.
// 50 × preserving sugar, list price EUR 1.50 less two item discounts
// (EUR 0.03 + EUR 0.02) → net price EUR 1.45; 50 × 1.45 = EUR 72.50, 7 %.
TradeLineItem line2 = invoice.AddTradeLineItem(
lineID: "2",
name: "Gelierzucker Extra 250g",
netUnitPrice: 1.45m,
grossUnitPrice: 1.50m,
unitCode: QuantityCodes.H87,
billedQuantity: 50m,
lineTotalAmount: 72.50m,
taxType: TaxTypes.VAT,
categoryCode: TaxCategoryCodes.S,
taxPercent: 7m,
sellerAssignedID: "GZ250",
id: new GlobalID(GlobalIDSchemeIdentifiers.EAN, "4123456000021"));
// BT-147: item discounts are written as ram:AppliedTradeAllowanceCharge INSIDE the
// gross price. The amounts are PER UNIT, not for the whole line. The net price above
// is the result: 1.50 − 0.03 − 0.02 = 1.45.
line2.AddTradeAllowance(CurrencyCodes.EUR, basisAmount: null, actualAmount: 0.03m, reason: "Artikelrabatt 1");
line2.AddTradeAllowance(CurrencyCodes.EUR, basisAmount: null, actualAmount: 0.02m, reason: "Artikelrabatt 2");
line2.PackageQuantity = 1m;
line2.PackageUnitCode = QuantityCodes.XCT;
' 50 × preserving sugar, list price EUR 1.50 less two item discounts
' (EUR 0.03 + EUR 0.02) → net price EUR 1.45; 50 × 1.45 = EUR 72.50, 7 %.
Dim line2 As TradeLineItem = invoice.AddTradeLineItem(
lineID:="2",
name:="Gelierzucker Extra 250g",
netUnitPrice:=1.45D,
grossUnitPrice:=1.5D,
unitCode:=QuantityCodes.H87,
billedQuantity:=50D,
lineTotalAmount:=72.5D,
taxType:=TaxTypes.VAT,
categoryCode:=TaxCategoryCodes.S,
taxPercent:=7D,
sellerAssignedID:="GZ250",
id:=New GlobalID(GlobalIDSchemeIdentifiers.EAN, "4123456000021"))
' BT-147: item discounts are written as ram:AppliedTradeAllowanceCharge INSIDE the
' gross price. The amounts are PER UNIT, not for the whole line. The net price above
' is the result: 1.50 − 0.03 − 0.02 = 1.45.
line2.AddTradeAllowance(CurrencyCodes.EUR, basisAmount:=Nothing, actualAmount:=0.03D, reason:="Artikelrabatt 1")
line2.AddTradeAllowance(CurrencyCodes.EUR, basisAmount:=Nothing, actualAmount:=0.02D, reason:="Artikelrabatt 2")
line2.PackageQuantity = 1D
line2.PackageUnitCode = QuantityCodes.XCT
Line 3 – free goods
Ten pieces of the same item free of charge. The line stays in the document so that the delivered quantity remains traceable, but contributes € 0.00 to the total. The reason sits next to it as the line description (BT-154).
// Free goods: 10 pieces of the same item at EUR 0.00. The line stays in the document
// so that the delivered quantity remains traceable, but adds EUR 0.00 to the total.
TradeLineItem line3 = invoice.AddTradeLineItem(
lineID: "3",
name: "Gelierzucker Extra 250g",
description: "Artikel wie vereinbart ohne Berechnung", // BT-154
netUnitPrice: 0.00m,
grossUnitPrice: 0.00m,
unitCode: QuantityCodes.H87,
billedQuantity: 10m,
lineTotalAmount: 0.00m,
taxType: TaxTypes.VAT,
categoryCode: TaxCategoryCodes.S,
taxPercent: 7m,
sellerAssignedID: "GZ250",
id: new GlobalID(GlobalIDSchemeIdentifiers.EAN, "4123456000021"));
line3.PackageQuantity = 1m;
line3.PackageUnitCode = QuantityCodes.XCT;
' Free goods: 10 pieces of the same item at EUR 0.00. The line stays in the document
' so that the delivered quantity remains traceable, but adds EUR 0.00 to the total.
Dim line3 As TradeLineItem = invoice.AddTradeLineItem(
lineID:="3",
name:="Gelierzucker Extra 250g",
description:="Artikel wie vereinbart ohne Berechnung", ' BT-154
netUnitPrice:=0D,
grossUnitPrice:=0D,
unitCode:=QuantityCodes.H87,
billedQuantity:=10D,
lineTotalAmount:=0D,
taxType:=TaxTypes.VAT,
categoryCode:=TaxCategoryCodes.S,
taxPercent:=7D,
sellerAssignedID:="GZ250",
id:=New GlobalID(GlobalIDSchemeIdentifiers.EAN, "4123456000021"))
line3.PackageQuantity = 1D
line3.PackageUnitCode = QuantityCodes.XCT
Lines 4 and 5 – differing units and a buyer item number
Line 4 shows that the billing unit and the shipping unit may differ: billed in crates, shipped in bottles. Line 5 is the matching empties deposit and additionally carries the buyer's item number (BT-156).
// 15 crates of beer at EUR 12.00 = EUR 180.00, 19 %. The billing unit is the crate
// (XBC), the shipping unit the bottle (XBO) – 20 bottles per crate.
TradeLineItem line4 = invoice.AddTradeLineItem(
lineID: "4",
name: "Bierbrau Pils 20/0500",
description: "EAN-VKE: 4100130913297",
netUnitPrice: 12.00m,
grossUnitPrice: 12.00m,
unitCode: QuantityCodes.XBC, // XBC = crate
billedQuantity: 15m,
lineTotalAmount: 180.00m,
taxType: TaxTypes.VAT,
categoryCode: TaxCategoryCodes.S,
taxPercent: 19m,
sellerAssignedID: "2031",
id: new GlobalID(GlobalIDSchemeIdentifiers.EAN, "4100130013294"));
line4.ApplicableProductCharacteristics.Add(new ApplicableProductCharacteristic()
{
Description = "Verpackung",
Value = "Kiste"
});
line4.PackageQuantity = 20m;
line4.PackageUnitCode = QuantityCodes.XBO; // XBO = bottle
// Empties deposit, 15 × EUR 3.10 = EUR 46.50, 19 %. buyerAssignedID (BT-156) shows
// that the buyer keeps its own number for the same item.
TradeLineItem line5 = invoice.AddTradeLineItem(
lineID: "5",
name: "Leergutpfand 20 x 0,5l",
netUnitPrice: 3.10m,
grossUnitPrice: 3.10m,
unitCode: QuantityCodes.H87,
billedQuantity: 15m,
lineTotalAmount: 46.50m,
taxType: TaxTypes.VAT,
categoryCode: TaxCategoryCodes.S,
taxPercent: 19m,
sellerAssignedID: "1805",
buyerAssignedID: "4711",
id: new GlobalID(GlobalIDSchemeIdentifiers.EAN, "2001015001325"));
line5.ApplicableProductCharacteristics.Add(new ApplicableProductCharacteristic()
{
Description = "Verpackung",
Value = "unverpackt"
});
line5.PackageQuantity = 1m;
line5.PackageUnitCode = QuantityCodes.XBC;
' 15 crates of beer at EUR 12.00 = EUR 180.00, 19 %. The billing unit is the crate
' (XBC), the shipping unit the bottle (XBO) – 20 bottles per crate.
Dim line4 As TradeLineItem = invoice.AddTradeLineItem(
lineID:="4",
name:="Bierbrau Pils 20/0500",
description:="EAN-VKE: 4100130913297",
netUnitPrice:=12D,
grossUnitPrice:=12D,
unitCode:=QuantityCodes.XBC, ' XBC = crate
billedQuantity:=15D,
lineTotalAmount:=180D,
taxType:=TaxTypes.VAT,
categoryCode:=TaxCategoryCodes.S,
taxPercent:=19D,
sellerAssignedID:="2031",
id:=New GlobalID(GlobalIDSchemeIdentifiers.EAN, "4100130013294"))
line4.ApplicableProductCharacteristics.Add(New ApplicableProductCharacteristic() With
{
.Description = "Verpackung",
.Value = "Kiste"
})
line4.PackageQuantity = 20D
line4.PackageUnitCode = QuantityCodes.XBO ' XBO = bottle
' Empties deposit, 15 × EUR 3.10 = EUR 46.50, 19 %. buyerAssignedID (BT-156) shows
' that the buyer keeps its own number for the same item.
Dim line5 As TradeLineItem = invoice.AddTradeLineItem(
lineID:="5",
name:="Leergutpfand 20 x 0,5l",
netUnitPrice:=3.1D,
grossUnitPrice:=3.1D,
unitCode:=QuantityCodes.H87,
billedQuantity:=15D,
lineTotalAmount:=46.5D,
taxType:=TaxTypes.VAT,
categoryCode:=TaxCategoryCodes.S,
taxPercent:=19D,
sellerAssignedID:="1805",
buyerAssignedID:="4711",
id:=New GlobalID(GlobalIDSchemeIdentifiers.EAN, "2001015001325"))
line5.ApplicableProductCharacteristics.Add(New ApplicableProductCharacteristic() With
{
.Description = "Verpackung",
.Value = "unverpackt"
})
line5.PackageQuantity = 1D
line5.PackageUnitCode = QuantityCodes.XBC
Line 6 – mixed pallet with its components
The pallet is billed as a single line, but its content is broken down through
IncludedReferencedProducts (BG-X-1). That breakdown is purely
informational: prices and taxes remain attached to the parent line.
// Mixed pallet with three yoghurt flavours. List price EUR 30.00 less EUR 0.90 item
// discount → EUR 29.10; 2 × 29.10 = EUR 58.20, 7 %.
TradeLineItem line6 = invoice.AddTradeLineItem(
lineID: "6",
name: "Mischpalette Joghurt Karton 3 x 20",
netUnitPrice: 29.10m,
grossUnitPrice: 30.00m,
unitCode: QuantityCodes.H87,
billedQuantity: 2m,
lineTotalAmount: 58.20m,
taxType: TaxTypes.VAT,
categoryCode: TaxCategoryCodes.S,
taxPercent: 7m,
sellerAssignedID: "MP107",
id: new GlobalID(GlobalIDSchemeIdentifiers.EAN, "4123456000038"));
line6.ApplicableProductCharacteristics.Add(new ApplicableProductCharacteristic()
{
Description = "Verpackung",
Value = "Karton"
});
// BG-X-1 ram:IncludedReferencedProduct (EXTENDED only): breaks a sales unit down into
// its components. Purely informational – prices and taxes stay attached to the
// parent line.
//
// The convenience method AddIncludedReferencedProduct() has no GlobalID parameter,
// which is why the objects are created directly here.
line6.IncludedReferencedProducts.Add(new IncludedReferencedProduct()
{
GlobalID = new GlobalID(GlobalIDSchemeIdentifiers.EAN, "4123456001035"),
SellerAssignedID = "JOG103",
Name = "Erdbeer 20 x 150g Becher",
UnitQuantity = 20m,
UnitCode = QuantityCodes.H87
});
line6.IncludedReferencedProducts.Add(new IncludedReferencedProduct()
{
GlobalID = new GlobalID(GlobalIDSchemeIdentifiers.EAN, "4123456002032"),
SellerAssignedID = "JOG203",
Name = "Banane 20 x 150g Becher",
UnitQuantity = 20m,
UnitCode = QuantityCodes.H87
});
line6.IncludedReferencedProducts.Add(new IncludedReferencedProduct()
{
GlobalID = new GlobalID(GlobalIDSchemeIdentifiers.EAN, "4123456003039"),
SellerAssignedID = "JOG303",
Name = "Schoko 20 x 150g Becher",
UnitQuantity = 20m,
UnitCode = QuantityCodes.H87
});
// BT-147 as in line 2: discount per unit, 30.00 − 0.90 = 29.10.
line6.AddTradeAllowance(CurrencyCodes.EUR, basisAmount: null, actualAmount: 0.90m, reason: "Artikelrabatt 1");
line6.PackageQuantity = 1m;
line6.PackageUnitCode = QuantityCodes.XPX; // XPX = pallet
' Mixed pallet with three yoghurt flavours. List price EUR 30.00 less EUR 0.90 item
' discount → EUR 29.10; 2 × 29.10 = EUR 58.20, 7 %.
Dim line6 As TradeLineItem = invoice.AddTradeLineItem(
lineID:="6",
name:="Mischpalette Joghurt Karton 3 x 20",
netUnitPrice:=29.1D,
grossUnitPrice:=30D,
unitCode:=QuantityCodes.H87,
billedQuantity:=2D,
lineTotalAmount:=58.2D,
taxType:=TaxTypes.VAT,
categoryCode:=TaxCategoryCodes.S,
taxPercent:=7D,
sellerAssignedID:="MP107",
id:=New GlobalID(GlobalIDSchemeIdentifiers.EAN, "4123456000038"))
line6.ApplicableProductCharacteristics.Add(New ApplicableProductCharacteristic() With
{
.Description = "Verpackung",
.Value = "Karton"
})
' BG-X-1 ram:IncludedReferencedProduct (EXTENDED only): breaks a sales unit down into
' its components. Purely informational – prices and taxes stay attached to the
' parent line.
'
' The convenience method AddIncludedReferencedProduct() has no GlobalID parameter,
' which is why the objects are created directly here.
line6.IncludedReferencedProducts.Add(New IncludedReferencedProduct() With
{
.GlobalID = New GlobalID(GlobalIDSchemeIdentifiers.EAN, "4123456001035"),
.SellerAssignedID = "JOG103",
.Name = "Erdbeer 20 x 150g Becher",
.UnitQuantity = 20D,
.UnitCode = QuantityCodes.H87
})
line6.IncludedReferencedProducts.Add(New IncludedReferencedProduct() With
{
.GlobalID = New GlobalID(GlobalIDSchemeIdentifiers.EAN, "4123456002032"),
.SellerAssignedID = "JOG203",
.Name = "Banane 20 x 150g Becher",
.UnitQuantity = 20D,
.UnitCode = QuantityCodes.H87
})
line6.IncludedReferencedProducts.Add(New IncludedReferencedProduct() With
{
.GlobalID = New GlobalID(GlobalIDSchemeIdentifiers.EAN, "4123456003039"),
.SellerAssignedID = "JOG303",
.Name = "Schoko 20 x 150g Becher",
.UnitQuantity = 20D,
.UnitCode = QuantityCodes.H87
})
' BT-147 as in line 2: discount per unit, 30.00 − 0.90 = 29.10.
line6.AddTradeAllowance(CurrencyCodes.EUR, basisAmount:=Nothing, actualAmount:=0.9D, reason:="Artikelrabatt 1")
line6.PackageQuantity = 1D
line6.PackageUnitCode = QuantityCodes.XPX ' XPX = pallet
8. Document-level invoice discounts
Two discounts – one percentage based, one a fixed amount – are mapped
through ram:SpecifiedTradeAllowanceCharge (BG-20).
A document-level discount always applies to one tax rate only. Since this document mixes 19 % and 7 %, each discount has to be added twice, each time with the matching basis amount. Two discounts therefore become four calls. The order in the XML follows the order of the calls in your code.
The basis amounts are not simply the line totals: at 19 % lines 1, 4 and 5 would add up to € 326.50, but only € 280.00 was agreed – the empties deposit and part of the goods are exempt from the discount. At 7 % the full sum of lines 2, 3 and 6 applies: € 130.70.
// Invoice discount 1 – percentage based, 2 % (BT-94 CalculationPercent + BT-93 BasisAmount).
invoice.AddTradeAllowance(
basisAmount: 280.00m,
currency: CurrencyCodes.EUR,
actualAmount: 5.60m, // 2 % of 280.00
chargePercentage: 2.00m,
reason: "Rechnungsrabatt 1",
taxTypeCode: TaxTypes.VAT,
taxCategoryCode: TaxCategoryCodes.S,
taxPercent: 19m);
invoice.AddTradeAllowance(
basisAmount: 130.70m,
currency: CurrencyCodes.EUR,
actualAmount: 2.61m, // 2 % of 130.70 (commercially rounded)
chargePercentage: 2.00m,
reason: "Rechnungsrabatt 1",
taxTypeCode: TaxTypes.VAT,
taxCategoryCode: TaxCategoryCodes.S,
taxPercent: 7m);
// Invoice discount 2 – a fixed amount, therefore without chargePercentage.
invoice.AddTradeAllowance(
basisAmount: 280.00m,
currency: CurrencyCodes.EUR,
actualAmount: 2.50m,
reason: "Rechnungsrabatt 2",
taxTypeCode: TaxTypes.VAT,
taxCategoryCode: TaxCategoryCodes.S,
taxPercent: 19m);
invoice.AddTradeAllowance(
basisAmount: 130.70m,
currency: CurrencyCodes.EUR,
actualAmount: 0.50m,
reason: "Rechnungsrabatt 2",
taxTypeCode: TaxTypes.VAT,
taxCategoryCode: TaxCategoryCodes.S,
taxPercent: 7m);
' Invoice discount 1 – percentage based, 2 % (BT-94 CalculationPercent + BT-93 BasisAmount).
invoice.AddTradeAllowance(
basisAmount:=280D,
currency:=CurrencyCodes.EUR,
actualAmount:=5.6D, ' 2 % of 280.00
chargePercentage:=2D,
reason:="Rechnungsrabatt 1",
taxTypeCode:=TaxTypes.VAT,
taxCategoryCode:=TaxCategoryCodes.S,
taxPercent:=19D)
invoice.AddTradeAllowance(
basisAmount:=130.7D,
currency:=CurrencyCodes.EUR,
actualAmount:=2.61D, ' 2 % of 130.70 (commercially rounded)
chargePercentage:=2D,
reason:="Rechnungsrabatt 1",
taxTypeCode:=TaxTypes.VAT,
taxCategoryCode:=TaxCategoryCodes.S,
taxPercent:=7D)
' Invoice discount 2 – a fixed amount, therefore without chargePercentage.
invoice.AddTradeAllowance(
basisAmount:=280D,
currency:=CurrencyCodes.EUR,
actualAmount:=2.5D,
reason:="Rechnungsrabatt 2",
taxTypeCode:=TaxTypes.VAT,
taxCategoryCode:=TaxCategoryCodes.S,
taxPercent:=19D)
invoice.AddTradeAllowance(
basisAmount:=130.7D,
currency:=CurrencyCodes.EUR,
actualAmount:=0.5D,
reason:="Rechnungsrabatt 2",
taxTypeCode:=TaxTypes.VAT,
taxCategoryCode:=TaxCategoryCodes.S,
taxPercent:=7D)
9. Transport costs
Logistics costs have a dedicated element in CII and are not the same as an
ordinary charge via AddTradeCharge. In
XRechnung
the element is not permitted; in ZUGFeRD EXTENDED it is.
// ram:SpecifiedLogisticsServiceCharge – a dedicated element for logistics costs, NOT
// the same as an ordinary charge (AddTradeCharge). It is not permitted in XRechnung,
// but it is in ZUGFeRD EXTENDED. It feeds into ChargeTotalAmount (BT-108).
invoice.AddLogisticsServiceCharge(
amount: 3.00m,
description: "Transportkosten",
taxTypeCode: TaxTypes.VAT,
taxCategoryCode: TaxCategoryCodes.S,
taxPercent: 19m);
' ram:SpecifiedLogisticsServiceCharge – a dedicated element for logistics costs, NOT
' the same as an ordinary charge (AddTradeCharge). It is not permitted in XRechnung,
' but it is in ZUGFeRD EXTENDED. It feeds into ChargeTotalAmount (BT-108).
invoice.AddLogisticsServiceCharge(
amount:=3D,
description:="Transportkosten",
taxTypeCode:=TaxTypes.VAT,
taxCategoryCode:=TaxCategoryCodes.S,
taxPercent:=19D)
10. VAT breakdown
One ram:ApplicableTradeTax group (BG-23) is mandatory per
combination of tax rate and tax category – two of them here. FactoorSharp
does not add the amounts up for you; the values come from your accounting and
are set explicitly. The arithmetic for this document:
| Field | 19 % (lines 1, 4, 5) | 7 % (lines 2, 3, 6) |
|---|---|---|
lineTotalBasisAmount |
326.50 | 130.70 |
allowanceChargeBasisAmount |
−5.60 − 2.50 + 3.00 = −5.10 | −2.61 − 0.50 = −3.11 |
basisAmount |
326.50 − 5.10 = 321.40 | 130.70 − 3.11 = 127.59 |
taxAmount |
321.40 × 19 % = 61.066 → 61.07 | 127.59 × 7 % = 8.9313 → 8.93 |
The transport charge enters the 19 % row with a positive sign, the discounts with a negative one. More on handling tax amounts, in particular with a differing tax currency, is covered under VAT amounts.
// lineTotalBasisAmount and allowanceChargeBasisAmount are EXTENDED fields;
// in BASIC/COMFORT they are dropped without replacement.
invoice.AddApplicableTradeTax(
basisAmount: 321.40m,
percent: 19m,
taxAmount: 61.07m,
typeCode: TaxTypes.VAT,
categoryCode: TaxCategoryCodes.S,
allowanceChargeBasisAmount: -5.10m,
lineTotalBasisAmount: 326.50m);
invoice.AddApplicableTradeTax(
basisAmount: 127.59m,
percent: 7m,
taxAmount: 8.93m,
typeCode: TaxTypes.VAT,
categoryCode: TaxCategoryCodes.S,
allowanceChargeBasisAmount: -3.11m,
lineTotalBasisAmount: 130.70m);
' lineTotalBasisAmount and allowanceChargeBasisAmount are EXTENDED fields;
' in BASIC/COMFORT they are dropped without replacement.
invoice.AddApplicableTradeTax(
basisAmount:=321.4D,
percent:=19D,
taxAmount:=61.07D,
typeCode:=TaxTypes.VAT,
categoryCode:=TaxCategoryCodes.S,
allowanceChargeBasisAmount:=-5.1D,
lineTotalBasisAmount:=326.5D)
invoice.AddApplicableTradeTax(
basisAmount:=127.59D,
percent:=7D,
taxAmount:=8.93D,
typeCode:=TaxTypes.VAT,
categoryCode:=TaxCategoryCodes.S,
allowanceChargeBasisAmount:=-3.11D,
lineTotalBasisAmount:=130.7D)
11. Payment terms with an early payment discount
The Skonto is not only a sentence in free text but also machine-readable in the
XML – that is what PaymentTermsType.Skonto is for.
// paymentTermsType = Skonto creates the ram:ApplicableTradePaymentDiscountTerms block
// with BasisPeriodMeasure (days) and CalculationPercent. Without that type only the
// free text would remain – the early payment discount would not be machine-readable.
invoice.AddTradePaymentTerms(
description: "Bei Zahlung innerhalb 14 Tagen gewähren wir 2,0% Skonto.",
paymentTermsType: PaymentTermsType.Skonto,
dueDays: 14,
percentage: 2.00m);
' paymentTermsType = Skonto creates the ram:ApplicableTradePaymentDiscountTerms block
' with BasisPeriodMeasure (days) and CalculationPercent. Without that type only the
' free text would remain – the early payment discount would not be machine-readable.
invoice.AddTradePaymentTerms(
description:="Bei Zahlung innerhalb 14 Tagen gewähren wir 2,0% Skonto.",
paymentTermsType:=PaymentTermsType.Skonto,
dueDays:=14,
percentage:=2D)
12. Document totals
SetTotals writes ram:SpecifiedTradeSettlementHeaderMonetarySummation
(BG-22). These values, too, are set rather than calculated:
| BT code | Parameter | Calculation |
|---|---|---|
| BT-106 | lineTotalAmount |
100.00 + 72.50 + 0.00 + 180.00 + 46.50 + 58.20 = 457.20 |
| BT-108 | chargeTotalAmount |
3.00 (transport costs) |
| BT-107 | allowanceTotalAmount |
5.60 + 2.61 + 2.50 + 0.50 = 11.21 |
| BT-109 | taxBasisAmount |
457.20 + 3.00 − 11.21 = 448.99 |
| BT-110 | taxTotalAmount |
61.07 + 8.93 = 70.00 |
| BT-112 | grandTotalAmount |
448.99 + 70.00 = 518.99 |
| BT-113 | totalPrepaidAmount |
0.00 – no prepayment. How instalments are itemised is covered under advance payments. |
| BT-115 | duePayableAmount |
518.99 − 0.00 = 518.99 |
invoice.SetTotals(
lineTotalAmount: 457.20m, // BT-106
chargeTotalAmount: 3.00m, // BT-108
allowanceTotalAmount: 11.21m, // BT-107
taxBasisAmount: 448.99m, // BT-109
taxTotalAmount: 70.00m, // BT-110
grandTotalAmount: 518.99m, // BT-112
totalPrepaidAmount: 0.00m, // BT-113
duePayableAmount: 518.99m); // BT-115
invoice.SetTotals(
lineTotalAmount:=457.2D, ' BT-106
chargeTotalAmount:=3D, ' BT-108
allowanceTotalAmount:=11.21D, ' BT-107
taxBasisAmount:=448.99D, ' BT-109
taxTotalAmount:=70D, ' BT-110
grandTotalAmount:=518.99D, ' BT-112
totalPrepaidAmount:=0D, ' BT-113
duePayableAmount:=518.99D) ' BT-115
13. Saving
Only at save time does it become clear which of the fields set above actually end up in the XML. Version, profile and syntax belong together:
// Version25 + Profile.Extended + CII produces the profile identifier
// "urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended".
// Any other profile would silently discard all the EXTENDED fields above.
invoice.Save("X19_01_Warenrechnung.xml",
ZUGFeRDVersion.Version25,
Profile.Extended,
ZUGFeRDFormats.CII);
' Version25 + Profile.Extended + CII produces the profile identifier
' "urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended".
' Any other profile would silently discard all the EXTENDED fields above.
invoice.Save("X19_01_Warenrechnung.xml",
ZUGFeRDVersion.Version25,
Profile.Extended,
ZUGFeRDFormats.CII)
How the same invoice is produced as a PDF/A-3 with embedded XML instead is described in working with PDF files; the remaining ways to save are covered under loading and saving.
14. Cross-check
Because the target is a known reference file, the result can be checked
directly: the generated file has to have the same tree structure as
X19_01_Warenrechnung.xml from the FeRD sample package. Numbers are
compared numerically, so that 1.00 and 1.0000 count as
equal – FactoorSharp writes prices adaptively with two decimal places.
Whether the invoice also complies with the rule set is something you check either in the browser or straight from your code. For a human look at the document, the visualizer renders a readable version:
// A human-readable rendition – not a legal document and not a hybrid ZUGFeRD PDF.
// It is rendered from the object, not from the XML file.
InvoiceVisualizer.RenderPdf(invoice, "X19_01_Warenrechnung.pdf");
InvoiceVisualizer.RenderHtml(invoice, "X19_01_Warenrechnung.html");
' A human-readable rendition – not a legal document and not a hybrid ZUGFeRD PDF.
' It is rendered from the object, not from the XML file.
InvoiceVisualizer.RenderPdf(invoice, "X19_01_Warenrechnung.pdf")
InvoiceVisualizer.RenderHtml(invoice, "X19_01_Warenrechnung.html")
That is a plain rendition, not a legal document and not a hybrid ZUGFeRD PDF – details under visualization.