Skip to content

[Bug][PEPPOL] Delivery information is treated as mandatory although it is optional in PEPPOL/EN 16931, and an empty delivery block is exported when it is missing #9815

Description

@miljance

Describe the issue

The PEPPOL export treats the delivery address as mandatory, although it is optional in every standard Business Central targets here. This shows up as two related problems.

1. An empty, invalid delivery block is written to the XML.

When the PEPPOL XML is generated, the delivery block on the document header is always written — even when there is nothing to put in it. With an empty ship-to address it produces a delivery block containing an empty street name and an empty country code, which receivers reject (EN 16931 rule BR-57 requires a country code as soon as a deliver-to address is present at all).

The XML generation already does this correctly one level down: on the document lines, the delivery block is skipped when it has no content. The same is not done for the document header.

This part needs no unusual setup to hit. Service documents are exported through the same XML generation, but the service validation does not check the ship-to address at all — so a service document without a ship-to address silently produces an invalid delivery block today.

2. The sales validation compensates for this by making the ship-to address mandatory.

Instead of leaving the optional block out, the sales validation requires Ship-to Address, City, Post Code and Country/Region Code on every document. Because the validation runs when the document is released, and releasing is part of posting, this does not merely block sending — it blocks posting, so accounting is stopped by an e-invoicing rule:

Ship-to Address must have a value in Sales Header: Document Type=Credit Memo, No.=…

A delivery address is not mandatory in any of the standards involved:

  • PEPPOL BIS Billing 3.0 — the delivery group (BG-13) and the deliver-to address (BG-15) are optional, in the Credit Note as well as in the Invoice.
  • EN 16931 — rule BR-57 requires a delivery country code only if a deliver-to address is supplied.
  • XRechnung — does not make the delivery address mandatory. KoSIT explicitly clarified the German rules BR-DE-10 and BR-DE-11 to apply only "wenn die Gruppe „DELIVER TO ADDRESS" (BG-15) übermittelt wird".
  • ZUGFeRD / Factur-X (EN 16931 profile) — follows EN 16931 and is equally optional.

Where this surfaces in practice: sales credit memos.

On a credit memo the goods move back from the customer, so Business Central deliberately does not copy the customer address into the ship-to fields. Instead it fills them from the Location on the document, or — when there is no Location Code — from Company Information → Shipping → Ship-to Address. If those company ship-to fields have never been filled and the credit memo carries no Location Code, the ship-to address stays empty and the credit memo cannot be posted, while the sales invoice being credited posts and sends without any problem.

Filling Company Information → Shipping → Ship-to Address (or setting a Location Code) is a valid workaround for the credit memo case. But:

  • Those fields are not mandatory anywhere in Business Central, are not part of any assisted setup, and are otherwise mainly relevant for purchase documents — so they are easy to leave empty for years without noticing.
  • The PEPPOL validation already requires the main company address earlier in the same routine, so companies hitting this do have a complete company address. It is specifically the shipping block that is empty, and nothing in the error message points there.
  • The Sales Credit Memo page has no Ship-to fields at all. The user is told to fill a field that is neither visible nor editable on that page, and the message names the Sales Header rather than Company Information.
  • It does not help the export problem in point 1 at all.

One more point for the new PEPPOL app. The old Base Application validation offers an event (OnBeforeCheckShipToAddress) that partners can subscribe to in order to switch the check off — today the only available workaround for the validation itself. The replacement in the new PEPPOL app has no such event, so that escape hatch disappears once the old codeunit is removed in 29.0.

Exact code locations and line numbers are listed under Additional context below.

Expected behavior

  1. When there is no delivery information, the export omits the delivery block entirely instead of writing an empty one. This applies to sales and service documents alike, and is independent of any validation change.
  2. The ship-to address is validated only when delivery information will actually be exported — i.e. when at least one of Delivery ID (GLN), Ship-to Code, Ship-to Address, Ship-to City, Ship-to Post Code, Ship-to Country/Region Code or Actual Delivery Date is filled. When all are empty, no error, and a valid document without a delivery block is produced.
  3. When the delivery address is partially filled, the check still enforces group completeness so the document stays valid (BR-57 country code; BR-DE-10 city, BR-DE-11 post code).
  4. Sales and service documents behave consistently — today one errors and the other exports invalid XML.
  5. The new PEPPOL app offers the same extensibility as the obsolete Base Application codeunit did.

Steps to reproduce

Common setup: German company (Country/Region Code = DE), E-Document Service with format XRechnung, customer with a valid Leitweg-ID / E-Invoice Routing No.. Leave Company Information → Shipping → Ship-to Address / City / Post Code empty (the main company address on the General FastTab is filled).

A — invalid XML, no error shown (service document)

  1. Add Service Credit Memo (or Service Invoice) to the supported document types of the E-Document Service.
  2. Create a service credit memo for that customer with no Location Code, so the ship-to fields stay empty.
  3. Post and export it.

Actual: no validation error is raised, and the generated XML contains a cac:Delivery block with an empty street name and an empty country code, which fails EN 16931 rule BR-57 at the receiver.
Expected: the cac:Delivery block is omitted and a valid document is produced.

B — posting blocked (sales credit memo)

  1. Create and post a sales invoice for that customer. This works — on an invoice the ship-to fields are filled from the customer.
  2. Create a sales credit memo for the same customer, with no Location Code (via Correct / Create Corrective Credit Memo, or manually). The ship-to fields on the Sales Header stay empty, because on a credit memo they are filled from the Location or from Company Information → Shipping, not from the customer. They are not visible anywhere on the Sales Credit Memo page.
  3. Post the credit memo.

Actual: Ship-to Address must have a value in Sales Header: Document Type=Credit Memo, No.=… — posting is blocked, raised from the PEPPOL validation during Release.
Expected: the credit memo posts and exports a valid XRechnung document without a delivery block.

Additional context

Reported on BC 28.3Base Application 28.3.52162.52579, E-Document Core 28.3.52162.52328, E-Document for Germany 28.3.52162.52273, format XRechnung. Scenario B is a customer observation; scenario A is derived from the code and has not been run on a live system.

Why the ship-to address is empty on a credit memo (by design). SalesHeader.UpdateShipToAddress() (src/Layers/W1/BaseApp/Sales/Document/SalesHeader.Table.al:6066-6094) fills the ship-to fields from the Location, or from CompanyInfo."Ship-to Address" when no Location Code is set — but only for credit document types. Copying the customer address is explicitly suppressed for credit document types at SalesHeader.Table.al:506 (CopyShipToAddress := not IsCreditDocType();). This is coherent: on a credit memo the goods return to the seller. It also means the mandatory ship-to check on a credit memo depends on Company Information setup that nothing else in the product requires.

Code locations. The check is the same in the obsolete Base Application codeunit and in the new PEPPOL app, and all localizations share it:

Where Notes
src/Layers/W1/BaseApp/Sales/Peppol/PEPPOLValidation.Codeunit.al:322-336 obsolete CU 1620 (ObsoleteTag = '29.0') — has OnBeforeCheckShipToAddress
src/Apps/W1/PEPPOL/App/src/Sales/PEPPOL30SalesValidationImpl.Codeunit.al:273-280 new PEPPOL app, Access = Internal, no extensibility point; called from :94 in CheckSalesDocument
src/Apps/W1/PEPPOL/App/src/Sales/PEPPOL30SalesValidation.Codeunit.al:110-115 public facade
src/Apps/DE/PEPPOLDE/app/src/PEPPOL30DESalesValidation.Codeunit.al:155 DE implementation calls the W1 facade
src/Apps/BE/PeppolBE/App/src/PEPPOL30BESalesValidation.Codeunit.al:148 BE implementation calls the W1 facade

The header-level cac:Delivery element is written unconditionally, with no currXMLport.Skip() on Delivery, StreetName or Country/IdentificationCode, in SalesInvoicePEPPOL30.XmlPort.al:962-1102 and SalesCrMemoPEPPOL30.XmlPort.al:1011-1151. The line-level Delivery in the same XmlPort skips correctly at SalesCrMemoPEPPOL30.XmlPort.al:1890-1894. Service documents use the same XMLport "Sales Invoice - PEPPOL30" (ExpServInvPEPPOL30.Codeunit.al:43) but PEPPOL30 Serv Validation Impl does not check the ship-to address at all. The delivery date is only enforced for Invoice/Order at PEPPOL30SalesValidationImpl.Codeunit.al:89-90, so credit memos already export an incomplete delivery block. Page 44 Sales Credit Memo contains no Ship-to fields (src/Layers/W1/BaseApp/Sales/Document/SalesCreditMemo.Page.al).

Suggested fix. The fix belongs in the W1 PEPPOL app: DE, BE and any partner implementation of "PEPPOL30 Validation" call the same facade and use the same XmlPorts, so everyone benefits and behaviour stays consistent across formats.

1. SalesInvoicePEPPOL30.XmlPort.al:962 and SalesCrMemoPEPPOL30.XmlPort.al:1011 — skip the whole Delivery group when empty, mirroring the line-level behaviour at SalesCrMemoPEPPOL30.XmlPort.al:1890:

trigger OnBeforePassVariable()
begin
    // ... existing provider calls ...

    if (DeliveryID = '') and (ActualDeliveryDate = '') and
       (DeliveryStreetName = '') and (DeliveryCityName = '') and
       (DeliveryPostalZone = '') and (DeliveryCountryIdCode = '') and
       (DeliveryPartyNameValue = '')
    then
        currXMLport.Skip();
end;

Also Skip() empty deliverystreetname and deliverycountryidcode so a partially filled group never produces empty elements. This is the part that fixes scenario A, and it is needed regardless of the validation change — without it, relaxing the check only moves the failure from BC to the receiver's schematron.

2. src/Apps/W1/PEPPOL/App/src/Sales/PEPPOL30SalesValidationImpl.Codeunit.al — make the check conditional

internal procedure CheckShipToAddress(SalesHeader: Record "Sales Header")
begin
    // BG-13 DELIVERY INFORMATION / BG-15 DELIVER TO ADDRESS are 0..1 in PEPPOL BIS Billing 3.0.
    // Only validate the group when something will actually be exported.
    if not HasDeliveryInformation(SalesHeader) then
        exit;

    SalesHeader.TestField("Ship-to Address");
    SalesHeader.TestField("Ship-to City");                 // BR-DE-10
    SalesHeader.TestField("Ship-to Post Code");            // BR-DE-11
    SalesHeader.TestField("Ship-to Country/Region Code");  // BR-57
    CheckCountryRegionCode(SalesHeader."Ship-to Country/Region Code");
end;

local procedure HasDeliveryInformation(SalesHeader: Record "Sales Header"): Boolean
begin
    exit(
      (SalesHeader."Ship-to Code" <> '') or
      (SalesHeader."Ship-to Address" <> '') or
      (SalesHeader."Ship-to City" <> '') or
      (SalesHeader."Ship-to Post Code" <> '') or
      (SalesHeader."Ship-to Country/Region Code" <> ''));
end;

3. Extensibility. Add OnBeforeCheckShipToAddress(SalesHeader, var IsHandled) (or an equivalent hook on the "PEPPOL30 Validation" interface / a context flag) to the PEPPOL app, so the escape hatch on the obsolete CU 1620 survives 29.0.

4. Fallback, if a W1 behaviour change to the validation is considered too broad: the validation part could be made conditional only for DE, by replacing the PEPPOL30SalesValidation.CheckShipToAddress(SalesHeader) call at src/Apps/DE/PEPPOLDE/app/src/PEPPOL30DESalesValidation.Codeunit.al:155 with a DE variant, following the existing "DE deviation #1#3" pattern in CheckSalesDocumentDE and the "PEPPOL30 DE Context" mechanism. That only fixes XRechnung/ZUGFeRD and leaves plain PEPPOL BIS and other localizations behind — and change 1 is still required either way.

Optional, outside the PEPPOL app: the error would be far easier to act on if it named Company Information → Shipping as the source for credit memos, or if the ship-to fields were visible on the Sales Credit Memo page.

References

I will provide a fix for a bug

  • I will provide a fix for a bug

Metadata

Metadata

Assignees

No one assigned

    Labels

    ApprovedThe issue is approvedSCMGitHub request for SCM area

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions