Skip to content

[Bug]: E-Documents for Germany - FindEDocumentService always selects last matching service, ignoring which service triggered the export#9642

Open
miljance wants to merge 1 commit into
microsoft:mainfrom
miljance:EDocDEFindEDocumentService
Open

[Bug]: E-Documents for Germany - FindEDocumentService always selects last matching service, ignoring which service triggered the export#9642
miljance wants to merge 1 commit into
microsoft:mainfrom
miljance:EDocDEFindEDocumentService

Conversation

@miljance

@miljance miljance commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

What & why

The XRechnung and ZUGFeRD exports located the E-Document Service with a FindLast() lookup by document format. When more than one service shares a format the export always picked the last one by code instead of the service that actually triggered it, so settings such as "Embed PDF in export" were read from the wrong service.

The triggering service is now handed to the export instead:

  • XRechnung: "XRechnung Format" passes it to "Export XRechnung Document" through SetEDocumentService before Run.
  • ZUGFeRD: the XML is built during report rendering, in a separate "Export ZUGFeRD Document" instance spawned by the report extension, which a setter called before Run cannot reach. The new "ZUGFeRD Export Context" codeunit (EventSubscriberInstance = Manual, bound per session rather than SingleInstance so parallel exports do not share state) carries the service from "ZUGFeRD Format" into the four posted-document report extensions, which push it onto their own instance. It is public so partners can do the same in customized report extensions. HasContext() reports true only once a non-blank service was pushed, so a bound-but-empty context cannot suppress the fallback.

FindEDocumentService and OnAfterFindEDocumentService are obsoleted in both codeunits but deliberately kept working under #if not CLEAN29: the calls remain, so the event still fires for existing subscribers, and when no service was provided the original FindLast lookup still runs unchanged. Customized reports that do not use the context keep behaving exactly as before. As of CLEAN29 the functions, the events and the fallback are removed and callers must provide the service through SetEDocumentService.

Tests cover the new and the legacy path for both formats: the triggering service wins over a later-sorting service, the FindLast fallback still applies when no service was provided, OnAfterFindEDocumentService still fires on both paths, and the ZUGFeRD context carries a service, refuses a blank one and clears on Stop.

Also adds an "E-Document for Germany" workspace file, matching the per-app workspaces the other Microsoft apps already track (EDocumentCore, PEPPOL, PeppolBE, Subscription Billing and others), and removes the duplicated "features": [ "TranslationFile" ] property from the demo data app.json, where it was declared twice.

Linked work

Fixes #8414

How I validated this

  • I read the full diff and it contains only changes I intended.
  • I built the affected app(s) locally with no new analyzer warnings.
  • I ran the change in Business Central and confirmed it behaves as expected.
  • I added or updated tests for the new behavior, or explained below why none are needed.

What I tested and the outcome (required — be specific: scenarios, commands, screenshots for UI changes)
I have created two E-document services for xRechnung, once with embedded PDF and once without and confirmed that functionality works as expected.

Risk & compatibility

Risk & compatibility

Behavior change (XRechnung): where several E-Document Services share the XRechnung format, the
export now uses the triggering service instead of the last one by code — service-dependent settings
like Embed PDF in export may resolve differently. This is the #8414 fix. ZUGFeRD output is
unchanged
(its export reads no service field); the context added there is groundwork only.

Deprecation: FindEDocumentService / OnAfterFindEDocumentService are obsoleted but stay fully
functional under #if not CLEAN29 — the calls remain, the event still fires, and the legacy
FindLast() fallback still runs when no service is provided, so existing subscribers and customized
reports are unaffected today. At CLEAN29 the fallback disappears: callers that don't supply a
service via SetEDocumentService (partners' customized reports reading "ZUGFeRD Export Context",
as the standard report extensions do) would then export with a blank service. That's why the new
codeunit is public.

New public surface: codeunit 11039 "ZUGFeRD Export Context" and SetEDocumentService on both
export codeunits. Session-scoped via BindSubscription (not SingleInstance, so parallel UI and Job
Queue exports don't share state).

No schema, upgrade, permission, telemetry or feature-flag impact.

…te FindEDocumentService

The XRechnung and ZUGFeRD exports located the E-Document Service with a
FindLast() lookup by document format. When more than one service shares a
format the export always picked the last one by code instead of the service
that actually triggered it, so settings such as "Embed PDF in export" were
read from the wrong service.

The triggering service is now handed to the export instead:

* XRechnung: "XRechnung Format" passes it to "Export XRechnung Document"
  through SetEDocumentService before Run.
* ZUGFeRD: the XML is built during report rendering, in a separate "Export
  ZUGFeRD Document" instance spawned by the report extension, which a setter
  called before Run cannot reach. The new "ZUGFeRD Export Context" codeunit
  (EventSubscriberInstance = Manual, bound per session rather than
  SingleInstance so parallel exports do not share state) carries the service
  from "ZUGFeRD Format" into the four posted-document report extensions, which
  push it onto their own instance. It is public so partners can do the same in
  customized report extensions. HasContext() reports true only once a non-blank
  service was pushed, so a bound-but-empty context cannot suppress the fallback.

FindEDocumentService and OnAfterFindEDocumentService are obsoleted in both
codeunits but deliberately kept working under #if not CLEAN29: the calls remain,
so the event still fires for existing subscribers, and when no service was
provided the original FindLast lookup still runs unchanged. Customized reports
that do not use the context keep behaving exactly as before. As of CLEAN29 the
functions, the events and the fallback are removed and callers must provide the
service through SetEDocumentService.

Tests cover the new and the legacy path for both formats: the triggering service
wins over a later-sorting service, the FindLast fallback still applies when no
service was provided, OnAfterFindEDocumentService still fires on both paths, and
the ZUGFeRD context carries a service, refuses a blank one and clears on Stop.

Also adds an "E-Document for Germany" workspace file, matching the per-app
workspaces the other Microsoft apps already track (EDocumentCore, PEPPOL,
PeppolBE, Subscription Billing and others), and removes the duplicated
"features": [ "TranslationFile" ] property from the demo data app.json, where
it was declared twice.

Fixes microsoft#8414

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@miljance
miljance requested a review from a team July 21, 2026 09:57
@miljance
miljance requested review from a team as code owners July 21, 2026 09:57
@github-actions github-actions Bot added From Fork Pull request is coming from a fork Finance GitHub request for Finance area needs-approval Workflow runs require maintainer approval to start labels Jul 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟠\ High\ Severity\ —\ Breaking\ Changes}$

This PR adds fresh compatibility-routing logic inside methods it simultaneously marks obsolete: FindEDocumentService in both "Export XRechnung Document" and "Export ZUGFeRD Document" now contains new service-selection/event-routing behavior (the EDocumentServiceProvided branch) instead of only shrinking toward removal. New behavior belongs on the supported SetEDocumentService/context path; the obsolete methods should only forward to it during the warning window rather than growing new logic themselves.

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.13.4

end;

#region Internal pub/sub bridge - the bound instance captures writes and answers reads
[IntegrationEvent(false, false)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Events}$

OnSetEDocumentService, OnHasContext, and OnGetEDocumentService are described as an internal pub/sub bridge, but they are declared as local [IntegrationEvent] publishers on a public codeunit. Local integration events remain subscribable by dependent extensions, so shipping them as IntegrationEvent turns these implementation-detail hooks into an external subscriber contract. If this bridge is intended to stay module-private, use [InternalEvent] before release.

Suggested fix (apply manually — could not be anchored as a one-click suggestion):

    [InternalEvent(false, false)]
    local procedure OnSetEDocumentService(NewEDocumentService: Record "E-Document Service")
    begin
    end;

    [InternalEvent(false, false)]
    local procedure OnHasContext(var Result: Boolean)
    begin
    end;

    [InternalEvent(false, false)]
    local procedure OnGetEDocumentService(var EDocumentService: Record "E-Document Service")
    begin
    end;

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.13.4

EDocumentService.Modify();
end;

local procedure CreateTrailingXRechnungService(EmbedPDFInExport: Boolean): Code[20]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Testing}$

CreateTrailingXRechnungService introduces an extra E-Document Service into a test codeunit whose Initialize() rebuilds the baseline service only once. The new tests remove the trailing service only after the export/assertion path (RemoveTrailingXRechnungServices called post-assert), so a failing export or assertion leaves the extra service behind and makes later tests in this codeunit order-dependent. Recreate the service fixture per test or move the trailing-service cleanup into teardown that still runs when the test body fails.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.13.4

TempXMLBuffer.LoadFromStream(PdfAttachmentStream);
end;

local procedure CreateTrailingZUGFeRDService(): Code[20]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Testing}$

CreateTrailingZUGFeRDService has the same isolation problem: it adds a second E-Document Service, while Initialize() resets the fixture only on the first test in the codeunit. Because cleanup happens only after the export/assertion path, a failing test can leave the trailing service in place and contaminate later ZUGFeRD tests. Recreate the service fixture per test or move the trailing-service cleanup into teardown that still runs when the test body fails.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.13.4

@github-actions github-actions Bot removed the needs-approval Workflow runs require maintainer approval to start label Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Finance GitHub request for Finance area From Fork Pull request is coming from a fork

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: E-Documents for Germany - FindEDocumentService always selects last matching service, ignoring which service triggered the export

1 participant