unit ZUGFeRD_xml;

{$mode objfpc}{$H+}{$modeswitch advancedrecords}


{
   Unit ZUGFeRD_xml, version from 2025-01-04, created by Jorg3000

   Diese Pascal-Unit ist ein Hobby-Projekt und ist nicht nach dem ZUGFeRD-Standard zertifiziert.
   Diese Unit und der Autor stehen in keiner Verbindung zu FeRD (Forum elektronische Rechnung Deutschland), siehe unten.

   Diskussionsforum:  https://www.lazarusforum.de/viewtopic.php?f=29&t=16410

   Die Nutzung dieser Pascal-Unit ist auch in proprietären, kommerziellen Programmen erlaubt.
   Der Verwender/Programmierer muss selber dafür Sorge tragen, dass diese kostenlose Pascal-Unit seinen Anforderungen genügt.
   Dieser Quellcode wird gemäß der LGPL ohne jede Gewähr bereitgestellt; jegliche Haftung für Schäden wird im gesetzlich zulässigen Umfang ausgeschlossen.

   Diese Unit steht unter der LGPL (GNU Lesser General Public License) Version 3 oder höher.
   Anstelle einer Lizenztext-Datei siehe: https://www.gnu.org/licenses/lgpl-3.0.html

   "ZUGFeRD ist ein branchenübergreifendes Datenformat für den elektronischen Rechnungsdatenaustausch,
    das vom Forum elektronische Rechnung Deutschland (FeRD) – mit Unterstützung des Bundesministeriums für Wirtschaft und Energie – erarbeitet wurde."
   https://www.ferd-net.de/standards/zugferd

   Änderungs-Historie:
   - 27.12.2024  nur Entwurf, fehlerhaft
   - 2024-12-31  Erste Version, die mit mehreren Steuersätzen den Validator-Test bestanden hat.
   - 2025-01-04  Erweiterungen von Soner: 1.) Minimum_Profile (Buchungshilfe)  2.) Preise mit mehr als 2 Nachkommastellen im XML.
   Möglicherweise ist im o.g. Diskussionsforum ein Update verfügbar (wichtig für evtl. Fehlerkorrekturen).
}


interface


uses DOM, XMLWrite;


type
    TProfileType = (NoProfile, Basic_Profile, Minimum_Profile);  // e.g. Basic_Profile = full invoice


const ProfileIDs: Array[TProfileType] of String = (
       '',                                                             // not set
       'urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic',  // Basic_Profile   (full invoice)
       'urn:factur-x.eu:1p0:minimum'                                   // Minimum_Profile (nur Buchungshilfe)
       );


type

  TBusinessPartner = record  // for seller or customer, für Verkäufer oder Kunde
    Name:     String;
    LineOne:  String;  // e.g. Street, Straße
    LineTwo:  String;
    City:     String;
    Postcode: String;
    Country:  String;  // e.g. 'DE'

    VATID, TaxID: String;  // in seller record only!  either-or, entweder USt-ID oder Steuernummer
  end;



  TTaxType = record
    TaxRate:     Double;  // e.g. 19.0  = Steuersatz in Prozent
    TaxTypeCode: String;  // 'VAT' (value added tax), Mehrwertsteuer/Umsatzsteuer
    TaxCategory: String;  // 'S' = Standard tax rate    <ram:CategoryCode>  (BT-118)

    CalculatedAmount, BasisAmount: Double;  // tax summation
  end;
  PTaxType = ^TTaxType;



  TInvoiceHeader = record      // Rechnungskopf
    InvoiceNumber: String;     // Rechnungsnummer, Belegnummer, DocumentID
    IssueDate:     String;     // issue date in the format "YYYYMMDD"  // Ausstellungsdatum
    DocumentTypeCode: String;  // '380' = invoice, Rechnung
    Notes: Array of String;    // notes on the invoice, Anmerkungen zur Rechnung

    CurrencyID:   String;  // e.g. 'EUR'  // Währungskennzeichen
    DeliveryDate: String;  // Lieferdatum  // format "YYYYMMDD" e.g. '20241231'  // Pflichtangabe nach deutschem UStG §14
    DueDate:      String;  // Fälligkeitsdatum

    TaxTypes: Array of TTaxType;
    function  makeTaxType(const TaxRate: Double; const TypeCode, Category: String; const addLineTotalNet:Double=0): Integer;
    procedure clearTaxAmounts();
  end;



  TLineItem = record      // <ram:IncludedSupplyChainTradeLineItem>
    LineID:      String;  // Positionsnummer
    GlobalID:    String;  // GTIN (Global Trade Item Number)  if available, falls vorhanden
    ProductName: String;

    Quantity:    Double;  // Menge
    UnitCode:    String;  // Mengeneinheit (e.g. "H87" for hours)
    NetSinglePrice: Double;  // Netto-Einzelpreis
    LineTotalNet:   Double;  // Summe der Zeile netto

    TaxIndex: Integer;  // array index +1 !   see InvoiceHeader.makeTaxType()
  end;
  PLineItem = ^TLineItem;
  TItemArray = Array of TLineItem;



  TMonetarySummation = record  // settlement amounts, Abrechnungsbeträge, Summen   <ram:SpecifiedTradeSettlementHeaderMonetarySummation>
    LineTotalAmount:      Double;  // Gesamtbetrag aller Positionen netto
    ChargeTotalAmount:    Double;  // Summe der Zuschläge
    AllowanceTotalAmount: Double;  // Summe der Nachlässe
    TaxBasisTotalAmount:  Double;  // Steuerbasis          <ram:TaxBasisTotalAmount>
    TaxTotalAmount:       Double;  // Steuerbetrag
    GrandTotalAmount:     Double;  // Gesamtsumme
    DuePayableAmount:     Double;  // Fälliger Betrag
  end;



  TAllData = record
    Seller:   TBusinessPartner;    // Verkäufer, Lieferant         <ram:SellerTradeParty>
    Customer: TBusinessPartner;    // Kunde, Käufer                <ram:BuyerTradeParty>
    InvoiceHeader: TInvoiceHeader; // Rechnungskopf                <rsm:ExchangedDocument>
    LineItems: TItemArray;         // Array mit Rechnungs-Posten   <ram:IncludedSupplyChainTradeLineItem>
    Summation: TMonetarySummation; // Summen, Abrechnungsbeträge   <ram:SpecifiedTradeSettlementHeaderMonetarySummation>

    function createLineItem(): PLineItem;  // append new line item, neuen Rechnungsposten anhängen
  end;



  TZUGFeRD_XML = class
    private
      FDoc:  TXMLDocument;
      FRoot: TDOMElement;  // <rsm:CrossIndustryInvoice>
      FProfileType: TProfileType;  // see applyAllData_createNodes() or therein addNodes_Profile()

      // 3 top level elements, directly linked to root
      FNode1_rsm_ExchangedDocumentContext:    TDOMElement;  // <rsm:ExchangedDocumentContext>
      FNode1_rsm_ExchangedDocument:           TDOMElement;  // <rsm:ExchangedDocument>
      FNode1_rsm_SupplyChainTradeTransaction: TDOMElement;  // <rsm:SupplyChainTradeTransaction>

      procedure _append_ApplicableTradeTax(DestNode: TDOMElement; TaxType: PTaxType);              // <ram:ApplicableTradeTax>                               in  <ram:ApplicableHeaderTradeSettlement>
      procedure _set_SpecifiedTradeSettlementHeaderMonetarySummation(DestNode: TDOMElement; const sum: TMonetarySummation; const CurrencyID: String);  // <ram:SpecifiedTradeSettlementHeaderMonetarySummation>  in  <ram:ApplicableHeaderTradeSettlement>

    public
      Data: TAllData;  // see .applyAllData_createNodes()

      constructor Create;
      destructor  Destroy; override;

      property  ProfileType: TProfileType read FProfileType;  // see applyAllData_createNodes() or therein addNodes_Profile()

      function  appendNode(DestNode: TDOMElement; const Tag: DOMString): TDOMElement;                           // Abbreviation for: DestNode.AppendChild(FDoc.CreateElement(Tag));
      function  makeNode  (DestNode: TDOMElement; const Tag: DOMString; const Content:String=''): TDOMElement;  // Abbreviation for: DestNode.AppendChild(FDoc.CreateElement(Tag)).TextContent := UnicodeString(Content);

      procedure addNodes_Profile(Prof: TProfileType);                  // <rsm:ExchangedDocumentContext>
      procedure addNodes_InvoiceHeader(const Header: TInvoiceHeader);  // <rsm:ExchangedDocument>
      procedure addNodes_TradeAgreement(const Seller, Customer: TBusinessPartner);  // <ram:ApplicableHeaderTradeAgreement>
      procedure addNodes_DeliveryDate(const DeliveryDate: String);
      procedure addNodes_ApplicableHeaderTradeSettlement(const sum: TMonetarySummation; const h: TInvoiceHeader);
      procedure addNodes_LineItem(const Item: TLineItem; const InvoiceHeader: TInvoiceHeader);

      procedure applyAllData_createNodes(Prof: TProfileType; wantCalculateSummation: Boolean);  // uses .Data

      function  generateXmlText(): String;
      function  saveToFile(const FileName: String): Boolean;
    end;



function ZUGFeRD_Example_saveFile(): Boolean;  // see setExampleData()   // saves file: Invoice_ZUGFeRD_Example.xml



implementation


uses Classes, SysUtils;


var FormatSettingsUSA: TFormatSettings;  // see initialization



function formatDecimal(const Value: Double): String;  // instead of FloatToStr()
begin
  Result := FormatFloat('0.00', Value, FormatSettingsUSA);  // exactly 2 decimal places, 2 Nachkommastellen
end;


function formatDecimal_extPrecision(const Value: Double): String;
begin
  // added by Soner, as of version 2025-01-04
  Result := FormatFloat('0.00##', Value, FormatSettingsUSA);  // 2 decimal places or up to 4 decimal places if needed
end;


procedure addLineTotal(const it: TLineItem; const InvoiceHeader: TInvoiceHeader; var sum: TMonetarySummation);
var
   TaxType: PTaxType;
begin
  sum.LineTotalAmount := sum.LineTotalAmount + it.LineTotalNet;  // all lines total, Gesamtbetrag aller Positionen, netto

  TaxType := nil;
  if it.TaxIndex>0 then TaxType := @InvoiceHeader.TaxTypes[it.TaxIndex-1];

  if (TaxType<>nil) and (TaxType^.TaxRate<>0) then
    sum.TaxTotalAmount := sum.TaxTotalAmount + (it.LineTotalNet * TaxType^.TaxRate / 100);

  sum.TaxBasisTotalAmount := sum.LineTotalAmount - sum.AllowanceTotalAmount + sum.ChargeTotalAmount;  // TaxBasisTotalAmount ist die Summe aller Nettobeträge, unabhängig davon, ob sie steuerpflichtig oder steuerfrei sind.
  sum.GrandTotalAmount := sum.TaxBasisTotalAmount + sum.TaxTotalAmount;
  sum.DuePayableAmount := sum.GrandTotalAmount;
end;



function getNode(ParentNode: TDOMElement; const Tag: DOMString): TDOMElement;
begin
  try
    Result := ParentNode.FindNode(Tag) as TDOMElement;
  except
    Result := nil;
  end;
end;



{ TAllData }


function TAllData.createLineItem(): PLineItem;  // append new line item, neuen Rechnungsposten anhängen
var
   i: Integer;
begin
  i := Length(LineItems);
  SetLength(LineItems, i+1);
  Result  := @LineItems[i];
  Result^ := Default(TLineItem);
end;



{ TInvoiceHeader }


{
  TaxRate := 19.0;       // Steuersatz in Prozent
  TaxTypeCode := 'VAT';  // 'VAT' (value added tax), MwSt./Mehrwertsteuer/Umsatzsteuer
  TaxCategory := 'S';    // 'S' = Standard tax rate    <ram:CategoryCode>
}
function TInvoiceHeader.makeTaxType(const TaxRate: Double; const TypeCode, Category: String; const addLineTotalNet:Double=0): Integer;  // returns array index +1
var
   i: Integer;
   p, t: PTaxType; // Pointer!
begin
  Result := 0;
  t := nil;
  for i:=0 to Length(TaxTypes)-1 do
    begin
     p:=@TaxTypes[i];
     if (p^.TaxRate=TaxRate) and (p^.TaxTypeCode=TypeCode) and (p^.TaxCategory=Category) then begin t:=p; Result:=i+1; Break; end;
    end;

  if t=nil then
    begin
     i := Length(TaxTypes);
     Result := i+1;
     SetLength(TaxTypes,Result);
     t := @TaxTypes[i];
     t^ := Default(TTaxType);

     t^.TaxRate := TaxRate;
     t^.TaxTypeCode := TypeCode;
     t^.TaxCategory := Category;
    end;

  if addLineTotalNet<>0 then
    begin
     t^.BasisAmount := t^.BasisAmount + addLineTotalNet;
     t^.CalculatedAmount := t^.BasisAmount * TaxRate / 100;
    end;
end;


procedure TInvoiceHeader.clearTaxAmounts();  // or to remove all tax types: InvoiceHeader.TaxTypes := nil;
var i: Integer;
    t: PTaxType;
begin
  for i:=0 to Length(TaxTypes)-1 do
    begin
     t:=@TaxTypes[i];
     t^.BasisAmount := 0;
     t^.CalculatedAmount := 0;
    end;
end;



{ TZUGFeRD_XML }


constructor TZUGFeRD_XML.Create;
begin
  FDoc  := TXMLDocument.Create;
  FRoot := FDoc.CreateElement('rsm:CrossIndustryInvoice');
  FDoc.AppendChild(FRoot);

  // Namespaces
  FRoot.SetAttribute('xmlns:rsm', 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100');
  FRoot.SetAttribute('xmlns:ram', 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100');
  FRoot.SetAttribute('xmlns:qdt', 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100');
  FRoot.SetAttribute('xmlns:udt', 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100');
  FRoot.SetAttribute('xmlns:xs', 'http://www.w3.org/2001/XMLSchema');

  // in correct order: 3 top level elements, directly linked to root
  FNode1_rsm_ExchangedDocumentContext    := makeNode(FRoot, 'rsm:ExchangedDocumentContext');    // <rsm:ExchangedDocumentContext>
  FNode1_rsm_ExchangedDocument           := makeNode(FRoot, 'rsm:ExchangedDocument');           // <rsm:ExchangedDocument>
  FNode1_rsm_SupplyChainTradeTransaction := makeNode(FRoot, 'rsm:SupplyChainTradeTransaction'); // <rsm:SupplyChainTradeTransaction>
end;



destructor TZUGFeRD_XML.Destroy;
begin
  FDoc.Free;
  inherited Destroy;
end;



function TZUGFeRD_XML.appendNode(DestNode: TDOMElement; const Tag: DOMString): TDOMElement;  // Abbreviation for: Dest.AppendChild(FDoc.CreateElement(Tag));
begin
  Result := nil;
  try
    Result := FDoc.CreateElement(Tag);
    DestNode.AppendChild(Result);
  except
    FreeAndNIL(Result);  // .Destroy does FParentNode.DetachChild(Self)
  end;
end;



function TZUGFeRD_XML.makeNode(DestNode: TDOMElement; const Tag: DOMString; const Content:String=''): TDOMElement;  // Abbreviation for: Dest.AppendChild(FDoc.CreateElement(Tag)).TextContent := Content;
begin
  try
    Result := DestNode.FindNode(Tag) as TDOMElement;
  except
    Result := nil;
  end;
  if Result<>nil then Exit;

  try
    Result := FDoc.CreateElement(Tag);
    if Content<>'' then Result.TextContent := UnicodeString(Content);
    DestNode.AppendChild(Result);
  except
    FreeAndNIL(Result);  // .Destroy does FParentNode.DetachChild(Self)   // ShowMessage(IntToStr(PtrInt(DestNode))+'  '+IntToStr(PtrInt(FDoc))+'  '+Tag+'  '+Content);
  end;
end;



procedure TZUGFeRD_XML.addNodes_Profile(Prof: TProfileType);   // <rsm:ExchangedDocumentContext>
var
   n: TDOMElement;
begin
  // if Prof=NoProfile then Exit;    reinnehmen oder nicht?
  FProfileType := Prof;
  n := makeNode(FNode1_rsm_ExchangedDocumentContext, 'ram:GuidelineSpecifiedDocumentContextParameter');
  makeNode(n, 'ram:ID', ProfileIDs[Prof]);
end;



procedure TZUGFeRD_XML.addNodes_InvoiceHeader(const Header: TInvoiceHeader);
var
   NoteText: String;
   HeaderNode, NoteNode, DateTimeNode: TDOMElement;
begin
  HeaderNode := FNode1_rsm_ExchangedDocument;  // <rsm:ExchangedDocument>

  // invoice number and type code, Rechnungsnummer
  makeNode(HeaderNode, 'ram:ID', Header.InvoiceNumber);
  makeNode(HeaderNode, 'ram:TypeCode', Header.DocumentTypeCode);  // "380" = invoice, Rechnung

  // date of issue, format "YYYYMMDD", Ausstellungsdatum
  DateTimeNode := makeNode(HeaderNode, 'ram:IssueDateTime');
  makeNode(DateTimeNode, 'udt:DateTimeString', Header.IssueDate).SetAttribute('format','102');

  // add notes, Anmerkungen hinzufügen
  if FProfileType=Basic_Profile then
    for NoteText in Header.Notes do
     begin
      NoteNode := appendNode(HeaderNode, 'ram:IncludedNote');  // append(!) node, not replace
      makeNode(NoteNode, 'ram:Content', NoteText);
     end;
end;



procedure TZUGFeRD_XML.addNodes_LineItem(const Item: TLineItem; const InvoiceHeader: TInvoiceHeader);  // Rechnungsposten
var
   Tax: PTaxType;
   n, LineNode, SettlementNode: TDOMElement;
begin
  if FProfileType=Minimum_Profile then Exit;  // line items not allowed in Minimum profile

  LineNode := appendNode(FNode1_rsm_SupplyChainTradeTransaction, 'ram:IncludedSupplyChainTradeLineItem');  // append(!) node, not replace

  // line id, Positions-ID
  n := makeNode(LineNode, 'ram:AssociatedDocumentLineDocument');
  makeNode(n, 'ram:LineID', Item.LineID);

  // product id and name
  n := makeNode(LineNode, 'ram:SpecifiedTradeProduct');
  if Item.GlobalID<>'' then makeNode(n, 'ram:GlobalID', Item.GlobalID) .SetAttribute('schemeID','0160');  // '0160' = GTIN (Global Trade Item Number)
  makeNode(n, 'ram:Name', Item.ProductName);

  // price
  n := makeNode(LineNode, 'ram:SpecifiedLineTradeAgreement');
  n := makeNode(n, 'ram:NetPriceProductTradePrice');
  makeNode(n, 'ram:ChargeAmount', formatDecimal_extPrecision(Item.NetSinglePrice));  // _extPrecision added by Soner, as of version 2025-01-04

  // quantity
  n := makeNode(LineNode, 'ram:SpecifiedLineTradeDelivery');
  makeNode(n, 'ram:BilledQuantity', formatDecimal(Item.Quantity)).SetAttribute('unitCode',UnicodeString(Item.UnitCode));

  // 2 children follow
  SettlementNode := makeNode(LineNode, 'ram:SpecifiedLineTradeSettlement');

  // tax
  Tax := nil;
  if Item.TaxIndex>0 then Tax := @InvoiceHeader.TaxTypes[Item.TaxIndex-1];
  if Tax<>nil then
    begin
     n := makeNode(SettlementNode, 'ram:ApplicableTradeTax');
     makeNode(n, 'ram:TypeCode', Tax^.TaxTypeCode);  // 'VAT'
     makeNode(n, 'ram:CategoryCode', Tax^.TaxCategory);  // e.g. 'S'
     makeNode(n, 'ram:RateApplicablePercent', formatDecimal(Tax^.TaxRate) );  // Steuersatz in Prozent
    end;

  // line total, Zeilensumme
  n := makeNode(SettlementNode, 'ram:SpecifiedTradeSettlementLineMonetarySummation');
  makeNode(n, 'ram:LineTotalAmount', formatDecimal(Item.LineTotalNet));
end;



procedure TZUGFeRD_XML.addNodes_TradeAgreement(const Seller, Customer: TBusinessPartner);
var
   n, AgreementNode, SellerNode, BuyerNode, AddressNode: TDOMElement;
begin
  AgreementNode := makeNode(FNode1_rsm_SupplyChainTradeTransaction, 'ram:ApplicableHeaderTradeAgreement');

  // seller/supplier, Verkäufer/Lieferant
  SellerNode := makeNode(AgreementNode, 'ram:SellerTradeParty');
  makeNode(SellerNode, 'ram:Name', Seller.Name);

  AddressNode := makeNode(SellerNode, 'ram:PostalTradeAddress');
  if FProfileType<>Minimum_Profile then
    begin
     makeNode(AddressNode, 'ram:PostcodeCode', Seller.Postcode);
     if Seller.LineOne<>'' then makeNode(AddressNode, 'ram:LineOne', Seller.LineOne);
     if Seller.LineTwo<>'' then makeNode(AddressNode, 'ram:LineTwo', Seller.LineTwo);
     makeNode(AddressNode, 'ram:CityName', Seller.City);
    end;
  makeNode(AddressNode, 'ram:CountryID', Seller.Country);

  if Seller.VATID<>'' then  // VAT ID  (BT-31)
   begin
    n := appendNode(SellerNode, 'ram:SpecifiedTaxRegistration');       // append(!) node, not replace
    makeNode(n, 'ram:ID', Seller.VATID).SetAttribute('schemeID','VA');
   end;

  if Seller.TaxID<>'' then  // tax ID, Steuernummer  (BT-32)
   begin
    n := appendNode(SellerNode, 'ram:SpecifiedTaxRegistration');       // append(!) node, not replace
    makeNode(n, 'ram:ID', Seller.TaxID).SetAttribute('schemeID','FC');
   end;

  // buyer/customer, Käufer/Kunde
  BuyerNode := makeNode(AgreementNode, 'ram:BuyerTradeParty');
  makeNode(BuyerNode, 'ram:Name', Customer.Name);

  if FProfileType<>Minimum_Profile then   // all Minimum switches added by Soner, as of version 2025-01-04
    begin
     AddressNode := makeNode(BuyerNode, 'ram:PostalTradeAddress');
     makeNode(AddressNode, 'ram:PostcodeCode', Customer.Postcode);
     if Customer.LineOne<>'' then makeNode(AddressNode, 'ram:LineOne', Customer.LineOne);
     if Customer.LineTwo<>'' then makeNode(AddressNode, 'ram:LineTwo', Customer.LineTwo);
     makeNode(AddressNode, 'ram:CityName', Customer.City);
     makeNode(AddressNode, 'ram:CountryID', Customer.Country);
    end;
end;



procedure TZUGFeRD_XML.addNodes_DeliveryDate(const DeliveryDate: String);  // e.g. '20241231', from Data.InvoiceHeader.DeliveryDate
var
   n: TDOMElement;
begin
  n := makeNode(FNode1_rsm_SupplyChainTradeTransaction, 'ram:ApplicableHeaderTradeDelivery'); // must!

  if (DeliveryDate<>'') and (FProfileType<>Minimum_Profile) then  // no empty nodes allowed
    begin
     n := makeNode(n, 'ram:ActualDeliverySupplyChainEvent');
     n := makeNode(n, 'ram:OccurrenceDateTime');
     makeNode(n, 'udt:DateTimeString', DeliveryDate).SetAttribute('format', '102');
    end;
end;



procedure TZUGFeRD_XML._append_ApplicableTradeTax(DestNode: TDOMElement; TaxType: PTaxType);  // Element "Umsatzsteueraufschlüsselung"
var
   TaxNode: TDOMElement;
begin
  if (TaxType=nil) or (FProfileType=Minimum_Profile) then Exit;
  TaxNode := appendNode(DestNode, 'ram:ApplicableTradeTax');  // <ram:ApplicableTradeTax>  in  <ram:ApplicableHeaderTradeSettlement>
  makeNode(TaxNode, 'ram:CalculatedAmount', formatDecimal(TaxType^.CalculatedAmount)); // Betrag der Steuer
  makeNode(TaxNode, 'ram:TypeCode', TaxType^.TaxTypeCode); // 'VAT' Steuerart MwSt.
  makeNode(TaxNode, 'ram:BasisAmount', formatDecimal(TaxType^.BasisAmount)); // Basisbetrag
  makeNode(TaxNode, 'ram:CategoryCode', TaxType^.TaxCategory); // e.g. 'S' Standard-Steuersatz
  makeNode(TaxNode, 'ram:RateApplicablePercent', formatDecimal(TaxType^.TaxRate)); // Steuersatz in Prozent
end;



procedure TZUGFeRD_XML._set_SpecifiedTradeSettlementHeaderMonetarySummation(DestNode: TDOMElement; const sum: TMonetarySummation; const CurrencyID: String);
var
   n: TDOMElement;
begin
  n := makeNode(DestNode, 'ram:SpecifiedTradeSettlementHeaderMonetarySummation');  // <ram:SpecifiedTradeSettlementHeaderMonetarySummation>  in  <ram:ApplicableHeaderTradeSettlement>

  if FProfileType<>Minimum_Profile then
    begin
     makeNode(n, 'ram:LineTotalAmount',   formatDecimal(sum.LineTotalAmount));   // Gesamtbetrag der Positionen   netto?
     makeNode(n, 'ram:ChargeTotalAmount', formatDecimal(sum.ChargeTotalAmount)); // Summe der Zuschläge
     makeNode(n, 'ram:AllowanceTotalAmount', formatDecimal(sum.AllowanceTotalAmount)); // Summe der Nachlässe
    end;

  makeNode(n, 'ram:TaxBasisTotalAmount',  formatDecimal(sum.TaxBasisTotalAmount));  // Steuerbasis  <ram:TaxBasisTotalAmount>
  makeNode(n, 'ram:TaxTotalAmount',   formatDecimal(sum.TaxTotalAmount)) .SetAttribute('currencyID', UnicodeString(CurrencyID));   // Steuerbetrag   // Währungskennzeichen
  makeNode(n, 'ram:GrandTotalAmount', formatDecimal(sum.GrandTotalAmount));  // Gesamtsumme
  makeNode(n, 'ram:DuePayableAmount', formatDecimal(sum.DuePayableAmount));  // Fälliger Betrag
end;



procedure TZUGFeRD_XML.addNodes_ApplicableHeaderTradeSettlement(const sum: TMonetarySummation; const h: TInvoiceHeader);
var
   i: Integer;
   n, SettlementNode: TDOMElement;
begin
  SettlementNode := makeNode(FNode1_rsm_SupplyChainTradeTransaction, 'ram:ApplicableHeaderTradeSettlement');  // <ram:ApplicableHeaderTradeSettlement>  in  <rsm:SupplyChainTradeTransaction>
  makeNode(SettlementNode, 'ram:InvoiceCurrencyCode', h.CurrencyID);

  // taxes, Steuerarten
  if FProfileType<>Minimum_Profile then
    for i:=0 to Length(h.TaxTypes)-1 do
      _append_ApplicableTradeTax(SettlementNode, @h.TaxTypes[i]);  // <ram:ApplicableTradeTax>  in  <ram:ApplicableHeaderTradeSettlement>

  // due date, Fälligkeitsdatum
  if (h.DueDate<>'') and (FProfileType<>Minimum_Profile) then  // no empty nodes allowed
    begin
      n := makeNode(SettlementNode, 'ram:SpecifiedTradePaymentTerms');
      n := makeNode(n, 'ram:DueDateDateTime');
      makeNode(n, 'udt:DateTimeString', h.DueDate).SetAttribute('format', '102');
    end;

  // Summation
  _set_SpecifiedTradeSettlementHeaderMonetarySummation(SettlementNode, sum, h.CurrencyID);  // <ram:SpecifiedTradeSettlementHeaderMonetarySummation>  in  <ram:ApplicableHeaderTradeSettlement>
end;



procedure TZUGFeRD_XML.applyAllData_createNodes(Prof: TProfileType; wantCalculateSummation: Boolean);  // of .Data
var
   i: Integer;
begin
  addNodes_Profile(Prof);  // root: <rsm:ExchangedDocumentContext>

  addNodes_InvoiceHeader( Data.InvoiceHeader );  // root: <rsm:ExchangedDocument>

  if wantCalculateSummation then Data.Summation := Default(TMonetarySummation);
  for i:=0 to Length(Data.LineItems)-1 do
    begin
     addNodes_LineItem(Data.LineItems[i], Data.InvoiceHeader);
     if wantCalculateSummation then addLineTotal(Data.LineItems[i], Data.InvoiceHeader, {var}Data.Summation);
    end;

  addNodes_TradeAgreement( Data.Seller, Data.Customer );

  addNodes_DeliveryDate( Data.InvoiceHeader.DeliveryDate );  // e.g. '20241231'

  addNodes_ApplicableHeaderTradeSettlement( Data.Summation, Data.InvoiceHeader);
end;



function TZUGFeRD_XML.generateXmlText(): String;
var
  Stream: TStringStream;
begin
  Stream := TStringStream.Create('');
  try
    WriteXMLFile(FDoc, Stream);
    Result := Stream.DataString;
  finally
    Stream.Free;
  end;
end;



function TZUGFeRD_XML.saveToFile(const FileName: String): Boolean;
var
  fs: TFileStream;
begin
  Result := false;
  fs := nil;
  try
    try
      fs := TFileStream.Create(FileName, fmCreate);
      WriteXMLFile(FDoc, fs);
      Result := true;
    except
      Result := false;
    end;
  finally
    fs.Free;
  end;
end;



{
  Unit Codes   (Mengeneinheiten)

  C62	Generic: Pieces, Stück
  H87	Time:    Hours
  KGM	Weight:  Kilograms
  LTR	Volume:  Liters
  MTR	Length:  Meters
  DAY	Days
  WEE	Weeks
  MON   Months
  MTQ	Volume: Cubic Meters, Kubikmeter
  E48	Packaging Unit, Verpackungseinheit
}


{
  <ram:CategoryCode>  (BT-118)

  5305 Duty/tax/fee category, coded
  https://unece.org/fileadmin/DAM/trade/edifact/code/5305cl.htm

  A      Mixed tax rate
  B      Transferred (VAT)
  E      Exempt from tax
  G      Free export item, tax not charged
  H      Higher rate
  O      Services outside scope of tax
  S      Standard rate
  Z      Zero rated goods
}



procedure setExampleData(var Data: TAllData);   //  BASIC-Beispiel aus ... \ZUGFeRD 2.3.2 ferd-net.de ZF232_DE\Beispiele\2. BASIC\BASIC_Einfach
var
   Item: PLineItem;
begin
  Data.Summation := Default(TMonetarySummation);  // set all numbers to zero
  Data.InvoiceHeader.TaxTypes := nil;  // remove old tax types  // or see InvoiceHeader.clearTaxAmounts();

  with Data.Seller do
    begin
     Name:='Lieferant GmbH';
     LineOne:='Lieferantenstraße 20';
     City:='München';
     Postcode:='80333';
     Country:='DE';

     VATID := 'DE123456789';   // nach deutschem Recht genügt entweder VATID oder TaxID
     TaxID := '201/113/40209';
    end;


  with Data.Customer do
    begin
     Name:='Kunden AG Mitte';
     LineOne:='Hans Muster';
     LineTwo:='Kundenstraße 15';
     City:='Frankfurt';
     Postcode:='69876';
     Country:='DE';
    end;


  with Data.InvoiceHeader do
   begin
    CurrencyID := 'EUR';  // Währungskennzeichen

    InvoiceNumber  := '471102';  // Rechnungsnummer
    DocumentTypeCode  := '380';  // type code for invoice, Rechnung
    IssueDate    := '20241115';  // Ausstellungsdatum // format YYYYMMDD
    DeliveryDate := '20241114';  // Lieferdatum, Pflichtangabe nach deutschem UStG §14
    DueDate      := '20241215';  // Fälligkeitsdatum

    Notes := [  // 3 notes, multi-line example as in German language ZUGFeRD documentation
    'Rechnung gemäß Bestellung vom 01.11.2024.',

    'Lieferant GmbH' +sLineBreak+
    'Lieferantenstraße 20' +sLineBreak+
    '80333 München' +sLineBreak+
    'Deutschland' +sLineBreak+
    'Geschäftsführer: Hans Muster' +sLineBreak+
    'Handelsregisternummer: H A 123',

    'Unsere GLN: 4000001123452' +sLineBreak+
    'Ihre GLN: 4000001987658' +sLineBreak+
    'Ihre Kundennummer: GE2020211' +sLineBreak+
    'USt-ID: DE1234567' +sLineBreak+
    'Steuernummer: 300/300/300' +sLineBreak+
     sLineBreak + 'Zahlbar innerhalb 30 Tagen netto bis 25.12.2024, 3% Skonto innerhalb 10 Tagen bis 25.11.2024.'
    ];
   end;


  // line items, Rechnungs-Posten
  // first line item
  Item := Data.createLineItem();
  with Item^ do
    begin
     LineID := IntToStr(Length(Data.LineItems));  // Positionsnummer
     GlobalID    := '4012345001235';  // GTIN if available, falls vorhanden
     ProductName := 'Trennblätter A4' +sLineBreak+ 'GTIN: 4012345001235' +sLineBreak+ 'Art.-Nr.: TB100A4';
     Quantity    := 20.0;   // Menge
     UnitCode    := 'C62';  // unit: pieces, Einheit: Stück
     NetSinglePrice := 9.90;  // Netto-Einzelpreis
     LineTotalNet := Quantity * NetSinglePrice;  // Zeilensumme netto, z.B. 20 * 9.90 = 198.00
     TaxIndex := Data.InvoiceHeader.makeTaxType(19.0, 'VAT', 'S', LineTotalNet);  // 'S' = Standard tax rate    <ram:CategoryCode>  (BT-118)
     {
      TaxRate := 19.0;       // Steuersatz in Prozent
      TaxTypeCode := 'VAT';  // 'VAT' (value added tax), MwSt./Mehrwertsteuer/Umsatzsteuer
      TaxCategory := 'S';    // 'S' = Standard tax rate    <ram:CategoryCode>
     }
    end;


  // next line item
  Item := Data.createLineItem();
  with Item^ do
    begin
     LineID := IntToStr(Length(Data.LineItems));  // Positionsnummer
     ProductName := 'Sport-Almanach 1950 - 2000';
     Quantity    := 1;      // Menge
     UnitCode    := 'C62';  // unit: pieces, Einheit: Stück
     NetSinglePrice := 50;  // Netto-Einzelpreis
     LineTotalNet := Quantity * NetSinglePrice;  // Zeilensumme netto
     TaxIndex := Data.InvoiceHeader.makeTaxType(7.0, 'VAT', 'S', LineTotalNet);  // es gibt keinen Code für ermäßigten Steuersatz    <ram:CategoryCode>  (BT-118)
    end;


  // next line item
  Item := Data.createLineItem();
  with Item^ do
    begin
     LineID := IntToStr(Length(Data.LineItems));  // Positionsnummer
     ProductName := 'Famoses Zartgemüse aus der Dose';
     Quantity    := 1;      // Menge
     UnitCode    := 'E48';  // E48 Packaging Unit, Verpackungseinheit
     NetSinglePrice := 1.60;  // Netto-Einzelpreis
     LineTotalNet := Quantity * NetSinglePrice;  // Zeilensumme netto
     TaxIndex := Data.InvoiceHeader.makeTaxType(7.0, 'VAT', 'S', LineTotalNet);  // es gibt keinen Code für ermäßigten Steuersatz    <ram:CategoryCode>  (BT-118)
    end;


  // next line item
  Item := Data.createLineItem();
  with Item^ do
    begin
     LineID := IntToStr(Length(Data.LineItems));  // Positionsnummer
     ProductName := 'Wohnungskaltmiete';
     Quantity    := 1;      // Menge
     UnitCode    := 'MON';  // Unit: months, Monat
     NetSinglePrice := 500.00;  // Netto-Einzelpreis
     LineTotalNet := Quantity * NetSinglePrice;  // Zeilensumme netto
     TaxIndex := Data.InvoiceHeader.makeTaxType(0, 'VAT', 'Z', LineTotalNet);  // 'Z' = Zero rated goods  // bei 'E' (Exempt from tax) muss Begründung angegeben werden, [BR-E-10] „VAT BREAKDOWN“ (BG-23)    <ram:CategoryCode>  (BT-118)
    end;


  // next line item
  Item := Data.createLineItem();
  with Item^ do
    begin
     LineID := IntToStr(Length(Data.LineItems));  // Positionsnummer
     ProductName := 'Blumenarrangement';
     Quantity    := 2;      // Menge
     UnitCode    := 'C62';  // unit: pieces, Einheit: Stück
     NetSinglePrice := 50.00;  // Netto-Einzelpreis
     LineTotalNet := Quantity * NetSinglePrice;  // Zeilensumme netto
     TaxIndex := Data.InvoiceHeader.makeTaxType(19.0, 'VAT', 'S', LineTotalNet);  // 'S' = Standard tax rate    <ram:CategoryCode>  (BT-118)
    end;
end;



function ZUGFeRD_Example_saveFile(): Boolean;
var
   Invoice: TZUGFeRD_XML;
begin
  Result := false;
  Invoice := nil;
  try
    Invoice := TZUGFeRD_XML.Create;

    setExampleData( {var}Invoice.Data );

    Invoice.applyAllData_createNodes(Basic_Profile, true);  // of Invoice.Data  // Basic_Profile | Minimum_Profile  // true = do summation

    Result := Invoice.saveToFile( ExtractFilePath(ParamStr(0))+'Invoice_ZUGFeRD_Example.xml' );

  finally
    Invoice.Free;
  end;
end;



initialization
  FormatSettingsUSA := SysUtils.DefaultFormatSettings;
  FormatSettingsUSA.ThousandSeparator := ',';
  FormatSettingsUSA.DecimalSeparator  := '.';
end.





