Skip to content

[DE][MultiObjects] xRechnung and ZUGFeRD - Ability to overwrite the Line or add additional fields in the Format #29314

@miljance

Description

@miljance

Why do you need this change?

1. Problem Statement

The current xRechnung and ZUGFeRD export implementations do not provide sufficient extensibility for ISV solutions that require custom manipulation of invoice line output. Several ISV solutions already implement events to support ISV-specific export logic, but Business Central’s standard xRechnung and ZUGFeRD implementation lacks equivalent hooks.

Typical requirements include:

  • Fixed Price Printout
    Always export quantity as 1 and use the line amount instead of unit price, regardless of the actual sales invoice line quantity. This reflects fixed-price agreements where the customer does not care about quantities.
    (Requires IsHandled pattern or ability to manipulate dummy records used for XML line creation.)

  • Shipment Dates per Sales Invoice Line
    Add shipment dates to the line element in XML (BT-134 and BT-135). This is also relevant for the Subscription Billing module, which should populate these fields for subscription periods.
    (Requires a non-IsHandled event before the invoice line element is created in XML.)

  • Line Bundling
    Bundle multiple posted lines (e.g., 10 lines) into a single invoice line for customer-facing output, while still posting all lines individually. The ISV solution ensures only the bundled line is printed and contains summed values.
    (Requires IsHandled pattern or ability to manipulate dummy records used for XML line creation.)

  • Serial Numbers / Item Attributes
    Export serial numbers already printed on the sales invoice using ITEM ATTRIBUTES (BC-31, BC-32).
    (Requires a non-IsHandled event before the invoice line element is created in XML.)

Without extensibility at the correct stage of XML line creation, ISVs must resort to post-processing the generated XML, which is inefficient.


2. Proposed Code Change

Introduce new events in the xRechnung and ZUGFeRD export process, specifically before invoice line elements are created in XML.

  • Events should provide access to a the Sales Invoice Line record that can be manipulated prior to export.
  • For scenarios requiring suppression or replacement of lines (e.g., bundling), the event should support the IsHandled pattern.
  • For scenarios requiring additional fields (e.g., shipment dates, attributes), a non-IsHandled event before line creation is sufficient.

3. Invocation Example (Optional)

Not provided, but a typical subscriber would manipulate the Sales Invoice Line record to adjust quantity, amount, or add attributes before the XML element is created.


4. Alternatives Evaluated

  • OnCreateXMLOnBeforeSalesInvXmlDocumentWriteToFile
  • OnCreateXMLOnBeforeSalesCrMemoXmlDocumentWriteToFile

Both occur after all lines have already been exported. Using them would require removing and regenerating parts of the XML document, which is technically possible but inefficient.

Alternatively, the code could be refactored so that it works with Dummy Sales Invoice Line prior to export and introduces a non-IsHandled Event that enables manipulating a Dummy Sales Invoice Line prior to the export.


5. Justification for IsHandled

  • A non-IsHandled event is sufficient for adding additional fields (e.g., shipment dates, attributes).
  • However, IsHandled is required for scenarios where lines must be skipped, replaced, or bundled (e.g., fixed price, line grouping).
  • Without IsHandled, ISVs cannot prevent standard line export logic from executing, leading to the need to manipulate XML output after it has been created.

6. Performance Considerations

  • Frequency: Almost every sales invoice is affected for ISV solution in mind.
  • Dataset: Remains roughly the same, except in bundling scenarios where the dataset is reduced.
  • No loops, locking issues, or external calls are expected.
  • Performance impact is negligible.

7. Data Sensitivity Review

  • Events only expose data already in scope of the document being exported.
  • No additional sensitive or security-relevant data is introduced.

8. Multi‑Extension Interaction

  • Multiple ISV solutions may extend xRechnung or ZUGFeRD simultaneously.
  • In case of conflicting logic, the exported XML may be incorrect.
  • Customers would need to coordinate with ISVs to resolve conflicts.
  • This risk is inherent to extensibility but manageable through clear documentation and ISV collaboration.

Describe the request

[DE][Codeunit][13916][Export XRechnung Document]

    local procedure InsertInvoiceLine(var InvoiceElement: XmlElement; var SalesInvLine: Record "Sales Invoice Line"; Currency: Record Currency; CurrencyCode: Code[10]; PricesIncVAT: Boolean)
    var
        InvoiceLineElement: XmlElement;
        IsHandled: Boolean;
    begin
        SalesInvLine.FindSet();
        repeat
            IsHandled := false;
            OnBeforeInsertInvoiceLine(InvoiceElement, SalesInvLine, Currency, CurrencyCode, PricesIncVAT, IsHandled);
            if not IsHandled then begin
                InvoiceLineElement := XmlElement.Create('InvoiceLine', XmlNamespaceCAC);

                if PricesIncVAT then
                    ExcludeVAT(SalesInvLine, Currency."Amount Rounding Precision");
                InvoiceLineElement.Add(XmlElement.Create('ID', XmlNamespaceCBC, Format(SalesInvLine."Line No.")));
                InvoiceLineElement.Add(XmlElement.Create('InvoicedQuantity', XmlNamespaceCBC, XmlAttribute.Create('unitCode', GetUoMCode(SalesInvLine."Unit of Measure Code")), FormatDecimal(SalesInvLine.Quantity)));
                InvoiceLineElement.Add(XmlElement.Create('LineExtensionAmount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(SalesInvLine.Amount + SalesInvLine."Inv. Discount Amount")));
                InsertOrderLineReference(InvoiceLineElement, SalesInvLine."Line No.");
                if SalesInvLine."Line Discount Amount" > 0 then
                    InsertAllowanceCharge(
                        InvoiceLineElement, 'LineDiscount', GetTaxCategoryID(SalesInvLine."Tax Category", SalesInvLine."VAT Bus. Posting Group", SalesInvLine."VAT Prod. Posting Group"),
                        SalesInvLine."Line Discount Amount", SalesInvLine."Unit Price" * SalesInvLine.Quantity,
                        CurrencyCode, SalesInvLine."Line Discount %", SalesInvLine."Line Discount %", false);

                InsertItem(InvoiceLineElement, SalesInvLine);
                InsertPrice(InvoiceLineElement, SalesInvLine."Unit Price", CurrencyCode);
                OnBeforeAddInvoiceLineElement(SalesInvLine, InvoiceLineElement);
                InvoiceElement.Add(InvoiceLineElement);
            end;
        until SalesInvLine.Next() = 0;
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeInsertInvoiceLine(var InvoiceElement: XmlElement; var SalesInvLine: Record "Sales Invoice Line"; Currency: Record Currency; CurrencyCode: Code[10]; PricesIncVAT: Boolean; var IsHandled: Boolean)
    begin
    end;
    [IntegrationEvent(false, false)]
    local procedure OnBeforeAddInvoiceLineElement(SalesInvLine: Record "Sales Invoice Line"; var InvoiceLineElement: XmlElement)
    begin
    end;
    local procedure InsertCrMemoLine(var CrMemoElement: XmlElement; var SalesCrMemoLine: Record "Sales Cr.Memo Line"; Currency: Record Currency; CurrencyCode: Code[10]; PricesIncVAT: Boolean)
    var
        CrMemoLineElement: XmlElement;
        IsHandled: Boolean;
    begin
        SalesCrMemoLine.FindSet();
        repeat
            IsHandled := false;
            OnBeforeInsertCrMemoLine(CrMemoElement, SalesCrMemoLine, Currency, CurrencyCode, PricesIncVAT, IsHandled);
            if not IsHandled then begin
                CrMemoLineElement := XmlElement.Create('CreditNoteLine', XmlNamespaceCAC);

                if PricesIncVAT then
                    ExcludeVAT(SalesCrMemoLine, Currency."Amount Rounding Precision");
                CrMemoLineElement.Add(XmlElement.Create('ID', XmlNamespaceCBC, Format(SalesCrMemoLine."Line No.")));
                CrMemoLineElement.Add(XmlElement.Create('CreditedQuantity', XmlNamespaceCBC, XmlAttribute.Create('unitCode', GetUoMCode(SalesCrMemoLine."Unit of Measure Code")), FormatDecimal(SalesCrMemoLine.Quantity)));
                CrMemoLineElement.Add(XmlElement.Create('LineExtensionAmount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(SalesCrMemoLine.Amount + SalesCrMemoLine."Inv. Discount Amount")));
                InsertOrderLineReference(CrMemoLineElement, SalesCrMemoLine."Line No.");
                if SalesCrMemoLine."Line Discount Amount" > 0 then
                    InsertAllowanceCharge(
                        CrMemoLineElement, 'LineDiscount', GetTaxCategoryID(SalesCrMemoLine."Tax Category", SalesCrMemoLine."VAT Bus. Posting Group", SalesCrMemoLine."VAT Prod. Posting Group"),
                        SalesCrMemoLine."Line Discount Amount", SalesCrMemoLine."Unit Price" * SalesCrMemoLine.Quantity,
                        CurrencyCode, SalesCrMemoLine."Line Discount %", SalesCrMemoLine."Line Discount %", false);

                InsertItem(CrMemoLineElement, SalesCrMemoLine);
                InsertPrice(CrMemoLineElement, SalesCrMemoLine."Unit Price", CurrencyCode);
                OnBeforeAddCrMemoElement(SalesCrMemoLine, CrMemoLineElement);
                CrMemoElement.Add(CrMemoLineElement);
            end;
        until SalesCrMemoLine.Next() = 0;
    end;
    [IntegrationEvent(false, false)]
    local procedure OnBeforeInsertCrMemoLine(var CrMemoElement: XmlElement; var SalesCrMemoLine: Record "Sales Cr.Memo Line"; Currency: Record Currency; CurrencyCode: Code[10]; PricesIncVAT: Boolean; var IsHandled: Boolean)
    begin
    end;
   [IntegrationEvent(false, false)]
    local procedure OnBeforeAddCrMemoElement(SalesCrMemoLine: Record "Sales Cr.Memo Line"; var CrMemoLineElement2: XmlElement)
    begin
    end;

[DE][Codeunit][13917][Export ZUGFeRD Document]

Metadata

Metadata

Assignees

No one assigned

    Labels

    FinanceGitHub request for Finance area

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions