buildFacturXml function
String
buildFacturXml({
- required String invoiceNumber,
- required DateTime issueDate,
- required DeliveryDate deliveryDate,
- required DateTime? dueDate,
- required InvoiceParty seller,
- required InvoiceParty buyer,
- required InvoiceItemList itemList,
- PaymentInfo? paymentInfo,
Builds a Factur-X / ZUGFeRD 2.x compliant XML string.
Profile: EN 16931, Comfort
Implementation
String buildFacturXml({
required String invoiceNumber,
required DateTime issueDate,
required DeliveryDate deliveryDate,
required DateTime? dueDate,
required InvoiceParty seller,
required InvoiceParty buyer,
required InvoiceItemList itemList,
PaymentInfo? paymentInfo,
}) {
// Group line items by VAT rate for tax breakdown (BG-23)
final taxGroups = <double, _TaxGroup>{};
for (final item in itemList.items) {
taxGroups.putIfAbsent(
item.vatPercent,
() => _TaxGroup(
rate: item.vatPercent,
categoryCode: item.vatCategoryCode,
exemptionReason: item.vatExemptionReason,
),
);
taxGroups[item.vatPercent]!.basisAmount += item.lineTotal;
}
for (final group in taxGroups.values) {
group.taxAmount = group.basisAmount * group.rate / 100;
}
final buf = StringBuffer();
buf.writeln('<?xml version="1.0" encoding="utf-8"?>');
buf.writeln(
'<rsm:CrossIndustryInvoice'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
' xsi:schemaLocation="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'
' ../schema/D16B%20SCRDM%20(Subset)/uncoupled%20clm/CII/uncefact/data/standard/CrossIndustryInvoice_100pD16B.xsd"'
' xmlns:qdt="urn:un:unece:uncefact:data:standard:QualifiedDataType:100"'
' xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100"'
' xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"'
' xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100">',
);
// === BG-2: ExchangedDocumentContext ===
buf.writeln('\t<rsm:ExchangedDocumentContext>');
buf.writeln('\t\t<ram:GuidelineSpecifiedDocumentContextParameter>');
buf.writeln('\t\t\t<ram:ID>urn:cen.eu:en16931:2017</ram:ID>');
buf.writeln('\t\t</ram:GuidelineSpecifiedDocumentContextParameter>');
buf.writeln('\t</rsm:ExchangedDocumentContext>');
// === BG-3: ExchangedDocument ===
buf.writeln('\t<rsm:ExchangedDocument>');
buf.writeln('\t\t<ram:ID>${_xmlEscape(invoiceNumber)}</ram:ID>');
buf.writeln('\t\t<ram:TypeCode>380</ram:TypeCode>');
buf.writeln('\t\t<ram:IssueDateTime>');
buf.writeln(
'\t\t\t<udt:DateTimeString format="102">${_xmlDateFormat.format(issueDate)}</udt:DateTimeString>');
buf.writeln('\t\t</ram:IssueDateTime>');
buf.writeln('\t</rsm:ExchangedDocument>');
// === SupplyChainTradeTransaction ===
buf.writeln('\t<rsm:SupplyChainTradeTransaction>');
// -- BG-25: Line Items --
for (final item in itemList.items) {
buf.writeln('\t\t<ram:IncludedSupplyChainTradeLineItem>');
buf.writeln('\t\t\t<ram:AssociatedDocumentLineDocument>');
buf.writeln('\t\t\t\t<ram:LineID>${_xmlEscape(item.lineId)}</ram:LineID>');
buf.writeln('\t\t\t</ram:AssociatedDocumentLineDocument>');
buf.writeln('\t\t\t<ram:SpecifiedTradeProduct>');
buf.writeln('\t\t\t\t<ram:Name>${_xmlEscape(item.name)}</ram:Name>');
buf.writeln('\t\t\t</ram:SpecifiedTradeProduct>');
buf.writeln('\t\t\t<ram:SpecifiedLineTradeAgreement>');
buf.writeln('\t\t\t\t<ram:NetPriceProductTradePrice>');
buf.writeln(
'\t\t\t\t\t<ram:ChargeAmount>${_xmlAmountFormat.format(item.unitPrice)}</ram:ChargeAmount>');
buf.writeln('\t\t\t\t</ram:NetPriceProductTradePrice>');
buf.writeln('\t\t\t</ram:SpecifiedLineTradeAgreement>');
buf.writeln('\t\t\t<ram:SpecifiedLineTradeDelivery>');
buf.writeln(
'\t\t\t\t<ram:BilledQuantity unitCode="${_xmlEscape(item.unitCode)}">${_xmlQuantityFormat(item.quantity)}</ram:BilledQuantity>');
buf.writeln('\t\t\t</ram:SpecifiedLineTradeDelivery>');
buf.writeln('\t\t\t<ram:SpecifiedLineTradeSettlement>');
buf.writeln('\t\t\t\t<ram:ApplicableTradeTax>');
buf.writeln('\t\t\t\t\t<ram:TypeCode>VAT</ram:TypeCode>');
buf.writeln(
'\t\t\t\t\t<ram:CategoryCode>${_xmlEscape(item.vatCategoryCode)}</ram:CategoryCode>');
buf.writeln(
'\t\t\t\t\t<ram:RateApplicablePercent>${_xmlPercentFormat(item.vatPercent)}</ram:RateApplicablePercent>');
buf.writeln('\t\t\t\t</ram:ApplicableTradeTax>');
buf.writeln('\t\t\t\t<ram:SpecifiedTradeSettlementLineMonetarySummation>');
buf.writeln(
'\t\t\t\t\t<ram:LineTotalAmount>${_xmlAmountFormat.format(item.lineTotal)}</ram:LineTotalAmount>');
buf.writeln('\t\t\t\t</ram:SpecifiedTradeSettlementLineMonetarySummation>');
buf.writeln('\t\t\t</ram:SpecifiedLineTradeSettlement>');
buf.writeln('\t\t</ram:IncludedSupplyChainTradeLineItem>');
}
// -- BG-4 / BG-7: ApplicableHeaderTradeAgreement (Seller & Buyer) --
buf.writeln('\t\t<ram:ApplicableHeaderTradeAgreement>');
// BG-4: Seller
buf.writeln('\t\t\t<ram:SellerTradeParty>');
buf.writeln('\t\t\t\t<ram:Name>${_xmlEscape(seller.name)}</ram:Name>');
buf.writeln('\t\t\t\t<ram:PostalTradeAddress>');
buf.writeln(
'\t\t\t\t\t<ram:PostcodeCode>${_xmlEscape(seller.postcode)}</ram:PostcodeCode>');
buf.writeln(
'\t\t\t\t\t<ram:LineOne>${_xmlEscape(seller.street)}</ram:LineOne>');
buf.writeln(
'\t\t\t\t\t<ram:CityName>${_xmlEscape(seller.city)}</ram:CityName>');
buf.writeln(
'\t\t\t\t\t<ram:CountryID>${_xmlEscape(seller.countryCode)}</ram:CountryID>');
buf.writeln('\t\t\t\t</ram:PostalTradeAddress>');
if (seller.email != null) {
buf.writeln('\t\t\t\t<ram:URIUniversalCommunication>');
buf.writeln(
'\t\t\t\t\t<ram:URIID schemeID="EM">${_xmlEscape(seller.email!)}</ram:URIID>');
buf.writeln('\t\t\t\t</ram:URIUniversalCommunication>');
}
if (seller.vatId != null) {
buf.writeln('\t\t\t\t<ram:SpecifiedTaxRegistration>');
buf.writeln(
'\t\t\t\t\t<ram:ID schemeID="VA">${_xmlEscape(seller.vatId!)}</ram:ID>');
buf.writeln('\t\t\t\t</ram:SpecifiedTaxRegistration>');
}
if (seller.taxId != null) {
buf.writeln('\t\t\t\t<ram:SpecifiedTaxRegistration>');
buf.writeln(
'\t\t\t\t\t<ram:ID schemeID="${_xmlEscape(seller.taxIdSchemeId)}">${_xmlEscape(seller.taxId!)}</ram:ID>');
buf.writeln('\t\t\t\t</ram:SpecifiedTaxRegistration>');
}
buf.writeln('\t\t\t</ram:SellerTradeParty>');
// BG-7: Buyer
buf.writeln('\t\t\t<ram:BuyerTradeParty>');
buf.writeln('\t\t\t\t<ram:Name>${_xmlEscape(buyer.name)}</ram:Name>');
buf.writeln('\t\t\t\t<ram:PostalTradeAddress>');
buf.writeln(
'\t\t\t\t\t<ram:PostcodeCode>${_xmlEscape(buyer.postcode)}</ram:PostcodeCode>');
buf.writeln(
'\t\t\t\t\t<ram:LineOne>${_xmlEscape(buyer.street)}</ram:LineOne>');
buf.writeln(
'\t\t\t\t\t<ram:CityName>${_xmlEscape(buyer.city)}</ram:CityName>');
buf.writeln(
'\t\t\t\t\t<ram:CountryID>${_xmlEscape(buyer.countryCode)}</ram:CountryID>');
buf.writeln('\t\t\t\t</ram:PostalTradeAddress>');
if (buyer.email != null) {
buf.writeln('\t\t\t\t<ram:URIUniversalCommunication>');
buf.writeln(
'\t\t\t\t\t<ram:URIID schemeID="EM">${_xmlEscape(buyer.email!)}</ram:URIID>');
buf.writeln('\t\t\t\t</ram:URIUniversalCommunication>');
}
if (buyer.vatId != null) {
buf.writeln('\t\t\t\t<ram:SpecifiedTaxRegistration>');
buf.writeln(
'\t\t\t\t\t<ram:ID schemeID="VA">${_xmlEscape(buyer.vatId!)}</ram:ID>');
buf.writeln('\t\t\t\t</ram:SpecifiedTaxRegistration>');
}
if (buyer.taxId != null) {
buf.writeln('\t\t\t\t<ram:SpecifiedTaxRegistration>');
buf.writeln(
'\t\t\t\t\t<ram:ID schemeID="${_xmlEscape(buyer.taxIdSchemeId)}">${_xmlEscape(buyer.taxId!)}</ram:ID>');
buf.writeln('\t\t\t\t</ram:SpecifiedTaxRegistration>');
}
buf.writeln('\t\t\t</ram:BuyerTradeParty>');
buf.writeln('\t\t</ram:ApplicableHeaderTradeAgreement>');
// -- BG-13: ApplicableHeaderTradeDelivery --
buf.writeln('\t\t<ram:ApplicableHeaderTradeDelivery>');
buf.writeln('\t\t\t<ram:ActualDeliverySupplyChainEvent>');
buf.writeln('\t\t\t\t<ram:OccurrenceDateTime>');
buf.writeln(
'\t\t\t\t\t<udt:DateTimeString format="102">${_xmlDateFormat.format(deliveryDate.endDate ?? deliveryDate.date)}</udt:DateTimeString>');
buf.writeln('\t\t\t\t</ram:OccurrenceDateTime>');
buf.writeln('\t\t\t</ram:ActualDeliverySupplyChainEvent>');
buf.writeln('\t\t</ram:ApplicableHeaderTradeDelivery>');
// -- BG-19: ApplicableHeaderTradeSettlement --
buf.writeln('\t\t<ram:ApplicableHeaderTradeSettlement>');
buf.writeln('\t\t\t<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>');
// BG-16: Payment means
if (paymentInfo != null) {
buf.writeln('\t\t\t<ram:SpecifiedTradeSettlementPaymentMeans>');
buf.writeln(
'\t\t\t\t<ram:TypeCode>${_xmlEscape(paymentInfo.typeCode)}</ram:TypeCode>');
buf.writeln('\t\t\t\t<ram:PayeePartyCreditorFinancialAccount>');
buf.writeln(
'\t\t\t\t\t<ram:IBANID>${_xmlEscape(paymentInfo.iban)}</ram:IBANID>');
if (paymentInfo.accountName != null) {
buf.writeln(
'\t\t\t\t\t<ram:AccountName>${_xmlEscape(paymentInfo.accountName!)}</ram:AccountName>');
}
buf.writeln('\t\t\t\t</ram:PayeePartyCreditorFinancialAccount>');
if (paymentInfo.bic != null) {
buf.writeln('\t\t\t\t<ram:PayeeSpecifiedCreditorFinancialInstitution>');
buf.writeln(
'\t\t\t\t\t<ram:BICID>${_xmlEscape(paymentInfo.bic!)}</ram:BICID>');
buf.writeln('\t\t\t\t</ram:PayeeSpecifiedCreditorFinancialInstitution>');
}
buf.writeln('\t\t\t</ram:SpecifiedTradeSettlementPaymentMeans>');
}
// BG-23: VAT breakdown
for (final group in taxGroups.values) {
buf.writeln('\t\t\t<ram:ApplicableTradeTax>');
buf.writeln(
'\t\t\t\t<ram:CalculatedAmount>${_xmlAmountFormat.format(group.taxAmount)}</ram:CalculatedAmount>');
buf.writeln('\t\t\t\t<ram:TypeCode>VAT</ram:TypeCode>');
if (group.categoryCode == 'AE') {
final reason = group.exemptionReason ?? "Reverse charge";
buf.writeln(
'\t\t\t\t<ram:ExemptionReason>${_xmlEscape(reason)}</ram:ExemptionReason>');
}
buf.writeln(
'\t\t\t\t<ram:BasisAmount>${_xmlAmountFormat.format(group.basisAmount)}</ram:BasisAmount>');
buf.writeln(
'\t\t\t\t<ram:CategoryCode>${group.categoryCode}</ram:CategoryCode>');
buf.writeln(
'\t\t\t\t<ram:RateApplicablePercent>${_xmlPercentFormat(group.rate)}</ram:RateApplicablePercent>');
buf.writeln('\t\t\t</ram:ApplicableTradeTax>');
}
if (deliveryDate.endDate != null) {
buf.writeln('\t\t\t<ram:BillingSpecifiedPeriod>');
buf.writeln('\t\t\t\t<ram:StartDateTime>');
buf.writeln(
'\t\t\t\t\t<udt:DateTimeString format="102">${_xmlDateFormat.format(deliveryDate.date)}</udt:DateTimeString>');
buf.writeln('\t\t\t\t</ram:StartDateTime>');
buf.writeln('\t\t\t\t<ram:EndDateTime>');
buf.writeln(
'\t\t\t\t\t<udt:DateTimeString format="102">${_xmlDateFormat.format(deliveryDate.endDate!)}</udt:DateTimeString>');
buf.writeln('\t\t\t\t</ram:EndDateTime>');
buf.writeln('\t\t\t</ram:BillingSpecifiedPeriod>');
}
// BT-20: Payment terms
buf.writeln('\t\t\t<ram:SpecifiedTradePaymentTerms>');
final dueDateStr = dueDate != null ? _xmlDateFormat.format(dueDate) : null;
if (dueDateStr != null) {
buf.writeln('\t\t\t\t<ram:DueDateDateTime>');
buf.writeln(
'\t\t\t\t\t<udt:DateTimeString format="102">$dueDateStr</udt:DateTimeString>');
buf.writeln('\t\t\t\t</ram:DueDateDateTime>');
}
buf.writeln('\t\t\t</ram:SpecifiedTradePaymentTerms>');
// BG-22: Document totals
final totalTax = taxGroups.values.fold(0.0, (sum, g) => sum + g.taxAmount);
final lineTotalSum = itemList.total;
final grandTotal = lineTotalSum + totalTax;
buf.writeln('\t\t\t<ram:SpecifiedTradeSettlementHeaderMonetarySummation>');
buf.writeln(
'\t\t\t\t<ram:LineTotalAmount>${_xmlAmountFormat.format(lineTotalSum)}</ram:LineTotalAmount>');
buf.writeln(
'\t\t\t\t<ram:TaxBasisTotalAmount>${_xmlAmountFormat.format(lineTotalSum)}</ram:TaxBasisTotalAmount>');
buf.writeln(
'\t\t\t\t<ram:TaxTotalAmount currencyID="EUR">${_xmlAmountFormat.format(totalTax)}</ram:TaxTotalAmount>');
buf.writeln(
'\t\t\t\t<ram:GrandTotalAmount>${_xmlAmountFormat.format(grandTotal)}</ram:GrandTotalAmount>');
buf.writeln(
'\t\t\t\t<ram:DuePayableAmount>${_xmlAmountFormat.format(grandTotal)}</ram:DuePayableAmount>');
buf.writeln('\t\t\t</ram:SpecifiedTradeSettlementHeaderMonetarySummation>');
buf.writeln('\t\t</ram:ApplicableHeaderTradeSettlement>');
buf.writeln('\t</rsm:SupplyChainTradeTransaction>');
buf.writeln('</rsm:CrossIndustryInvoice>');
return buf.toString();
}