buildFacturXml function

String buildFacturXml({
  1. required String invoiceNumber,
  2. required DateTime issueDate,
  3. required DeliveryDate deliveryDate,
  4. required DateTime? dueDate,
  5. required InvoiceParty seller,
  6. required InvoiceParty buyer,
  7. required InvoiceItemList itemList,
  8. 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();
}