diff --git a/src/Apps/W1/EDocument/App/docs/patterns.md b/src/Apps/W1/EDocument/App/docs/patterns.md
index 18306a11478..f43634e71cd 100644
--- a/src/Apps/W1/EDocument/App/docs/patterns.md
+++ b/src/Apps/W1/EDocument/App/docs/patterns.md
@@ -214,3 +214,29 @@ The codebase uses compiler directives to manage deprecation:
- `#if not CLEANSCHEMA26` / `#if not CLEANSCHEMA29` -- table schema changes (field removals) that need separate cleanup due to schema migration constraints
When reading the code, content inside these blocks is legacy. The code outside (or in the `#else` branch) is the current implementation.
+
+## Notification state vs. display
+
+Purchase Document Draft notifications are split across two codeunits so that a
+call site tells you whether UI is raised.
+
+`"E-Doc. Draft Notif. State"` (6436) owns the `E-Document Notification` rows, the
+Sub Total mismatch calculation, and its telemetry. It never constructs or sends a
+`Notification` and knows nothing about `My Notifications`.
+
+`"E-Document Notification"` (6123) is the only codeunit consumers call. It gates on
+`My Notifications`, delegates state changes downward, builds `Notification` objects,
+and hosts the `Dismiss…`/`Disable…` action handlers. The dependency is one-way.
+
+The procedure verb is the contract:
+
+| Prefix | Guarantee |
+|---|---|
+| `Add…` / `Refresh…` / `Remove…` / `ReArm…` | State only, never displays |
+| `Show…` / `RefreshAndShow…` | Displays a notification |
+| `Is…` | Query, no side effects |
+
+Handler names passed to `Notification.AddAction` are resolved by string at runtime,
+so `DismissSubTotalMismatchNotification` and friends cannot be renamed safely.
+
+*Updated: 2026-08-05 -- documented the notification state vs. display split.*
diff --git a/src/Apps/W1/EDocument/App/src/Document/Notification/EDocDraftNotifState.Codeunit.al b/src/Apps/W1/EDocument/App/src/Document/Notification/EDocDraftNotifState.Codeunit.al
new file mode 100644
index 00000000000..27196eec4e8
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Document/Notification/EDocDraftNotifState.Codeunit.al
@@ -0,0 +1,383 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+namespace Microsoft.eServices.EDocument;
+
+using Microsoft.eServices.EDocument.Processing.Import.Purchase;
+using System.Telemetry;
+
+///
+/// Owns the persisted state of Purchase Document Draft notifications: the rows in the
+/// "E-Document Notification" table, the Sub Total mismatch evaluation, and its telemetry.
+/// This codeunit never displays anything. Everything user-facing lives in
+/// codeunit "E-Document Notification", which is the only caller of this one.
+///
+codeunit 6436 "E-Doc. Draft Notif. State"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ var
+ VendorMatchedByNameNotAddressMsg: Label 'Vendor matched by name but not by address.';
+ SubTotalMismatchMsg: Label 'The document total does not match the sum of the lines. Review the amounts before finalizing the draft.';
+ SubTotalMismatchNoToleranceTxt: Label 'E-Document purchase draft header Sub Total differs from the sum of the lines.';
+ SubTotalMismatchNotificationCreatedTxt: Label 'E-Document purchase draft Sub Total mismatch notification state created.';
+
+ ///
+ /// Re-evaluates the Sub Total mismatch from the persisted lines and updates the persisted
+ /// notification. A previously dismissed notification stays dismissed.
+ ///
+ /// The draft header as currently loaded by the caller
+ /// True if the state was evaluated; false if the header carries no e-document.
+ procedure RefreshSubTotalMismatch(EDocumentPurchaseHeader: Record "E-Document Purchase Header"): Boolean
+ var
+ RoundingPrecision: Decimal;
+ LinesSubTotal: Decimal;
+ LineCount: Integer;
+ begin
+ if EDocumentPurchaseHeader."E-Document Entry No." = 0 then
+ exit(false);
+ RoundingPrecision := GetRoundingPrecision(EDocumentPurchaseHeader);
+ LinesSubTotal := CalculateLinesSubTotal(EDocumentPurchaseHeader."E-Document Entry No.", RoundingPrecision, LineCount);
+ ApplyMismatchState(EDocumentPurchaseHeader, LinesSubTotal, LineCount, RoundingPrecision);
+ exit(true);
+ end;
+
+ ///
+ /// Re-evaluates the Sub Total mismatch after the user changed an amount on the header,
+ /// re-arming a previously dismissed notification.
+ ///
+ /// The draft header as currently edited by the user
+ /// True if the state was evaluated; false if the header carries no e-document.
+ procedure RefreshSubTotalMismatchAfterHeaderEdit(EDocumentPurchaseHeader: Record "E-Document Purchase Header"): Boolean
+ begin
+ if EDocumentPurchaseHeader."E-Document Entry No." = 0 then
+ exit(false);
+ ReArmSubTotalMismatch(EDocumentPurchaseHeader."E-Document Entry No.");
+ exit(RefreshSubTotalMismatch(EDocumentPurchaseHeader));
+ end;
+
+ ///
+ /// Re-evaluates the Sub Total mismatch after the user changed an amount on a draft line,
+ /// re-arming a previously dismissed notification. The supplied line is used instead of its
+ /// persisted version, because page field validation runs before the record is written.
+ ///
+ /// The line as currently edited by the user
+ /// True if the state was evaluated; false if the owning header could not be read.
+ procedure RefreshSubTotalMismatchAfterLineEdit(EDocumentPurchaseLine: Record "E-Document Purchase Line"): Boolean
+ var
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ RoundingPrecision: Decimal;
+ LinesSubTotal: Decimal;
+ LineCount: Integer;
+ begin
+ if not GetHeaderForLine(EDocumentPurchaseLine, EDocumentPurchaseHeader) then
+ exit(false);
+ ReArmSubTotalMismatch(EDocumentPurchaseHeader."E-Document Entry No.");
+ RoundingPrecision := GetRoundingPrecision(EDocumentPurchaseHeader);
+ LinesSubTotal := CalculateLinesSubTotalWithPendingLine(EDocumentPurchaseHeader."E-Document Entry No.", EDocumentPurchaseLine, RoundingPrecision, LineCount);
+ ApplyMismatchState(EDocumentPurchaseHeader, LinesSubTotal, LineCount, RoundingPrecision);
+ exit(true);
+ end;
+
+ ///
+ /// Re-evaluates the Sub Total mismatch while a draft line is being deleted, re-arming a
+ /// previously dismissed notification. The line is still persisted when this runs, so it is
+ /// excluded explicitly.
+ ///
+ /// The line being deleted
+ /// True if the state was evaluated; false if the owning header could not be read.
+ procedure RefreshSubTotalMismatchAfterLineDeletion(EDocumentPurchaseLine: Record "E-Document Purchase Line"): Boolean
+ var
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ RoundingPrecision: Decimal;
+ LinesSubTotal: Decimal;
+ LineCount: Integer;
+ begin
+ if not GetHeaderForLine(EDocumentPurchaseLine, EDocumentPurchaseHeader) then
+ exit(false);
+ ReArmSubTotalMismatch(EDocumentPurchaseHeader."E-Document Entry No.");
+ RoundingPrecision := GetRoundingPrecision(EDocumentPurchaseHeader);
+ LinesSubTotal := CalculateLinesSubTotalExcludingLine(EDocumentPurchaseHeader."E-Document Entry No.", EDocumentPurchaseLine, RoundingPrecision, LineCount);
+ ApplyMismatchState(EDocumentPurchaseHeader, LinesSubTotal, LineCount, RoundingPrecision);
+ exit(true);
+ end;
+
+ ///
+ /// Persists the Vendor Matched By Name Not Address notification for the current user.
+ /// Gating on My Notifications is the caller's responsibility.
+ ///
+ /// Id of e-document
+ procedure AddVendorMatchedByNameNotAddress(EDocumentEntryNo: Integer)
+ begin
+ AddNotification(EDocumentEntryNo, VendorMatchedByNameNotAddressNotificationId(), "E-Document Notification Type"::"Vendor Matched By Name Not Address", VendorMatchedByNameNotAddressMsg);
+ end;
+
+ ///
+ /// Persists the Sub Total Mismatch notification for the current user.
+ /// Gating on My Notifications is the caller's responsibility.
+ ///
+ /// Id of e-document
+ procedure AddSubTotalMismatch(EDocumentEntryNo: Integer)
+ begin
+ AddNotification(EDocumentEntryNo, SubTotalMismatchNotificationId(), "E-Document Notification Type"::"Sub Total Mismatch", SubTotalMismatchMsg);
+ end;
+
+ local procedure AddNotification(EDocumentEntryNo: Integer; NotificationId: Guid; NotificationType: Enum "E-Document Notification Type"; NotificationMessage: Text)
+ var
+ EDocumentNotification: Record "E-Document Notification";
+ begin
+ if EDocumentNotification.Get(EDocumentEntryNo, NotificationId, UserId()) then
+ exit;
+ EDocumentNotification.Validate("E-Document Entry No.", EDocumentEntryNo);
+ EDocumentNotification.Validate(ID, NotificationId);
+ EDocumentNotification.Validate("User Id", CopyStr(UserId(), 1, MaxStrLen(EDocumentNotification."User Id")));
+ EDocumentNotification.Validate(Type, NotificationType);
+ EDocumentNotification.Validate(Message, CopyStr(NotificationMessage, 1, MaxStrLen(EDocumentNotification.Message)));
+ EDocumentNotification.Insert(true);
+ end;
+
+ ///
+ /// Removes the persisted Sub Total Mismatch notification for the current user, e.g. when the totals re-converge.
+ ///
+ /// Id of e-document
+ procedure RemoveSubTotalMismatch(EDocumentEntryNo: Integer)
+ var
+ EDocumentNotification: Record "E-Document Notification";
+ begin
+ if EDocumentNotification.Get(EDocumentEntryNo, SubTotalMismatchNotificationId(), UserId()) then
+ EDocumentNotification.Delete(true);
+ end;
+
+ ///
+ /// Removes a previously dismissed Sub Total Mismatch row so the mismatch can be shown again after an amount edit.
+ ///
+ /// Id of e-document
+ procedure ReArmSubTotalMismatch(EDocumentEntryNo: Integer)
+ var
+ EDocumentNotification: Record "E-Document Notification";
+ begin
+ if not EDocumentNotification.Get(EDocumentEntryNo, SubTotalMismatchNotificationId(), UserId()) then
+ exit;
+ if EDocumentNotification.Dismissed then
+ EDocumentNotification.Delete(true);
+ end;
+
+ ///
+ /// Marks a persisted notification as dismissed, keeping the row so it is not re-shown to this user.
+ ///
+ /// Id of e-document
+ /// Id of the notification
+ procedure MarkDismissed(EDocumentEntryNo: Integer; NotificationId: Guid)
+ var
+ EDocumentNotification: Record "E-Document Notification";
+ begin
+ if not EDocumentNotification.Get(EDocumentEntryNo, NotificationId, UserId()) then
+ exit;
+ EDocumentNotification.Dismissed := true;
+ EDocumentNotification.Modify(true);
+ end;
+
+ ///
+ /// Deletes every persisted notification of a type for the current user.
+ ///
+ /// The notification type to clear
+ procedure DeleteAllOfType(NotificationType: Enum "E-Document Notification Type")
+ var
+ EDocumentNotification: Record "E-Document Notification";
+ begin
+ EDocumentNotification.SetRange(Type, NotificationType);
+ EDocumentNotification.SetRange("User Id", UserId());
+ EDocumentNotification.DeleteAll(true);
+ end;
+
+ ///
+ /// Reads one persisted notification for the current user.
+ ///
+ /// Id of e-document
+ /// Id of the notification
+ /// The row, when found
+ procedure GetNotification(EDocumentEntryNo: Integer; NotificationId: Guid; var EDocumentNotification: Record "E-Document Notification"): Boolean
+ begin
+ exit(EDocumentNotification.Get(EDocumentEntryNo, NotificationId, UserId()));
+ end;
+
+ ///
+ /// Finds the Purchase Document Draft notifications that are pending display for the current user.
+ ///
+ /// Id of e-document
+ /// Filtered and positioned on the first pending row
+ procedure FindPendingDraftNotifications(EDocumentEntryNo: Integer; var EDocumentNotification: Record "E-Document Notification"): Boolean
+ begin
+ EDocumentNotification.SetRange("E-Document Entry No.", EDocumentEntryNo);
+ EDocumentNotification.SetFilter(Type, '%1|%2',
+ "E-Document Notification Type"::"Vendor Matched By Name Not Address",
+ "E-Document Notification Type"::"Sub Total Mismatch");
+ EDocumentNotification.SetRange("User Id", UserId());
+ EDocumentNotification.SetRange(Dismissed, false);
+ exit(EDocumentNotification.FindSet());
+ end;
+
+ ///
+ /// Returns whether a Sub Total Mismatch row exists for the current user and e-document.
+ ///
+ /// Id of e-document
+ procedure SubTotalMismatchExists(EDocumentEntryNo: Integer): Boolean
+ var
+ EDocumentNotification: Record "E-Document Notification";
+ begin
+ exit(EDocumentNotification.Get(EDocumentEntryNo, SubTotalMismatchNotificationId(), UserId()));
+ end;
+
+ ///
+ /// Returns whether the current user has dismissed the Sub Total Mismatch notification for the e-document.
+ ///
+ /// Id of e-document
+ procedure IsSubTotalMismatchDismissed(EDocumentEntryNo: Integer): Boolean
+ var
+ EDocumentNotification: Record "E-Document Notification";
+ begin
+ if EDocumentNotification.Get(EDocumentEntryNo, SubTotalMismatchNotificationId(), UserId()) then
+ exit(EDocumentNotification.Dismissed);
+ exit(false);
+ end;
+
+ procedure VendorMatchedByNameNotAddressNotificationId(): Guid
+ begin
+ exit('bc0d8537-8e8d-4d94-a07a-a5a54c729d2a');
+ end;
+
+ procedure SubTotalMismatchNotificationId(): Guid
+ begin
+ exit('a1e6c0d2-3b4f-4c8a-9d1e-2f7b6a5c4d3e');
+ end;
+
+ local procedure ApplyMismatchState(EDocumentPurchaseHeader: Record "E-Document Purchase Header"; LinesSubTotal: Decimal; LineCount: Integer; RoundingPrecision: Decimal)
+ var
+ Telemetry: Codeunit Telemetry;
+ CustomDimensions: Dictionary of [Text, Text];
+ Difference: Decimal;
+ Tolerance: Decimal;
+ EDocumentEntryNo: Integer;
+ NotificationExisted: Boolean;
+ begin
+ EDocumentEntryNo := EDocumentPurchaseHeader."E-Document Entry No.";
+ Difference := Abs(EDocumentPurchaseHeader."Sub Total" - LinesSubTotal);
+ Tolerance := LineCount * RoundingPrecision;
+
+ CustomDimensions.Add('EntryNo', Format(EDocumentEntryNo));
+ CustomDimensions.Add('LineCount', Format(LineCount));
+ CustomDimensions.Add('WithinTolerance', Format(Difference <= Tolerance, 0, 9));
+ CustomDimensions.Add('DifferenceMagnitude', DifferenceMagnitudeBucket(Difference, EDocumentPurchaseHeader."Sub Total"));
+
+ if Difference <= Tolerance then begin
+ RemoveSubTotalMismatch(EDocumentEntryNo);
+ exit;
+ end;
+
+ // Only log on the transition into the mismatch state, not on every subsequent amount edit.
+ NotificationExisted := SubTotalMismatchExists(EDocumentEntryNo);
+ if not NotificationExisted then
+ Telemetry.LogMessage('0000UVL', SubTotalMismatchNoToleranceTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::All, CustomDimensions);
+
+ if IsSubTotalMismatchDismissed(EDocumentEntryNo) then
+ exit;
+ AddSubTotalMismatch(EDocumentEntryNo);
+ if not NotificationExisted then
+ Telemetry.LogMessage('0000UVM', SubTotalMismatchNotificationCreatedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, CustomDimensions);
+ end;
+
+ local procedure GetHeaderForLine(EDocumentPurchaseLine: Record "E-Document Purchase Line"; var EDocumentPurchaseHeader: Record "E-Document Purchase Header"): Boolean
+ begin
+ if EDocumentPurchaseLine."E-Document Entry No." = 0 then
+ exit(false);
+ exit(EDocumentPurchaseHeader.Get(EDocumentPurchaseLine."E-Document Entry No."));
+ end;
+
+ local procedure GetRoundingPrecision(EDocumentPurchaseHeader: Record "E-Document Purchase Header"): Decimal
+ var
+ EDocumentImportHelper: Codeunit "E-Document Import Helper";
+ begin
+ exit(Abs(EDocumentImportHelper.GetCurrencyRoundingPrecision(EDocumentPurchaseHeader."Currency Code")));
+ end;
+
+ local procedure CalculateLinesSubTotal(EDocumentEntryNo: Integer; RoundingPrecision: Decimal; var LineCount: Integer) LinesSubTotal: Decimal
+ var
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ begin
+ LineCount := 0;
+ if not FindLines(EDocumentEntryNo, EDocumentPurchaseLine) then
+ exit;
+ repeat
+ LinesSubTotal += LineSubTotal(EDocumentPurchaseLine, RoundingPrecision);
+ LineCount += 1;
+ until EDocumentPurchaseLine.Next() = 0;
+ end;
+
+ local procedure CalculateLinesSubTotalWithPendingLine(EDocumentEntryNo: Integer; PendingEDocumentPurchaseLine: Record "E-Document Purchase Line"; RoundingPrecision: Decimal; var LineCount: Integer) LinesSubTotal: Decimal
+ var
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ PendingLineFound: Boolean;
+ begin
+ LineCount := 0;
+ if FindLines(EDocumentEntryNo, EDocumentPurchaseLine) then
+ repeat
+ if EDocumentPurchaseLine."Line No." = PendingEDocumentPurchaseLine."Line No." then begin
+ PendingLineFound := true;
+ LinesSubTotal += LineSubTotal(PendingEDocumentPurchaseLine, RoundingPrecision);
+ end else
+ LinesSubTotal += LineSubTotal(EDocumentPurchaseLine, RoundingPrecision);
+ LineCount += 1;
+ until EDocumentPurchaseLine.Next() = 0;
+
+ if not PendingLineFound then begin
+ LinesSubTotal += LineSubTotal(PendingEDocumentPurchaseLine, RoundingPrecision);
+ LineCount += 1;
+ end;
+ end;
+
+ local procedure CalculateLinesSubTotalExcludingLine(EDocumentEntryNo: Integer; ExcludedEDocumentPurchaseLine: Record "E-Document Purchase Line"; RoundingPrecision: Decimal; var LineCount: Integer) LinesSubTotal: Decimal
+ var
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ begin
+ LineCount := 0;
+ if not FindLines(EDocumentEntryNo, EDocumentPurchaseLine) then
+ exit;
+ repeat
+ if EDocumentPurchaseLine."Line No." <> ExcludedEDocumentPurchaseLine."Line No." then begin
+ LinesSubTotal += LineSubTotal(EDocumentPurchaseLine, RoundingPrecision);
+ LineCount += 1;
+ end;
+ until EDocumentPurchaseLine.Next() = 0;
+ end;
+
+ local procedure FindLines(EDocumentEntryNo: Integer; var EDocumentPurchaseLine: Record "E-Document Purchase Line"): Boolean
+ begin
+ EDocumentPurchaseLine.SetLoadFields("E-Document Entry No.", "Line No.", Quantity, "Unit Price", "Total Discount");
+ EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo);
+ exit(EDocumentPurchaseLine.FindSet());
+ end;
+
+ local procedure LineSubTotal(EDocumentPurchaseLine: Record "E-Document Purchase Line"; RoundingPrecision: Decimal): Decimal
+ begin
+ exit(Round(EDocumentPurchaseLine.Quantity * EDocumentPurchaseLine."Unit Price", RoundingPrecision) - EDocumentPurchaseLine."Total Discount");
+ end;
+
+ local procedure DifferenceMagnitudeBucket(Difference: Decimal; HeaderSubTotal: Decimal): Text
+ var
+ RelativeDifference: Decimal;
+ begin
+ if Difference = 0 then
+ exit('None');
+ if HeaderSubTotal = 0 then
+ exit('Unknown');
+ RelativeDifference := Abs(Difference / HeaderSubTotal);
+ if RelativeDifference < 0.01 then
+ exit('Below1Pct');
+ if RelativeDifference < 0.1 then
+ exit('Below10Pct');
+ exit('AtLeast10Pct');
+ end;
+}
diff --git a/src/Apps/W1/EDocument/App/src/Document/Notification/EDocumentNotification.Codeunit.al b/src/Apps/W1/EDocument/App/src/Document/Notification/EDocumentNotification.Codeunit.al
index acefe71c5f8..a5321f99694 100644
--- a/src/Apps/W1/EDocument/App/src/Document/Notification/EDocumentNotification.Codeunit.al
+++ b/src/Apps/W1/EDocument/App/src/Document/Notification/EDocumentNotification.Codeunit.al
@@ -4,6 +4,7 @@
// ------------------------------------------------------------------------------------------------
namespace Microsoft.eServices.EDocument;
+using Microsoft.eServices.EDocument.Processing.Import.Purchase;
using System.Environment.Configuration;
codeunit 6123 "E-Document Notification"
@@ -12,45 +13,81 @@ codeunit 6123 "E-Document Notification"
InherentEntitlements = X;
InherentPermissions = X;
+ var
+ EDocDraftNotifState: Codeunit "E-Doc. Draft Notif. State";
+
///
- /// Adds a notification that informs a user of Purchase Document Draft that a vendor is matched by name but not by address.
- /// Id of e-document
+ /// Persists a notification that informs a user of Purchase Document Draft that a vendor is matched by name but not by address.
+ /// Does not display anything.
///
+ /// Id of e-document
procedure AddVendorMatchedByNameNotAddressNotification(EDocumentEntryNo: Integer)
var
- EDocumentNotification: Record "E-Document Notification";
MyNotifications: Record "My Notifications";
- VendorMatchedByNameNotAddressMsg: Label 'Vendor matched by name but not by address.';
begin
if not GuiAllowed() then
exit;
- if not MyNotifications.IsEnabled(GetVendorMatchedByNameNotAddressNotificationId()) then
+ if not MyNotifications.IsEnabled(EDocDraftNotifState.VendorMatchedByNameNotAddressNotificationId()) then
exit;
- if EDocumentNotification.Get(EDocumentEntryNo, GetVendorMatchedByNameNotAddressNotificationId(), UserId()) then
+ EDocDraftNotifState.AddVendorMatchedByNameNotAddress(EDocumentEntryNo);
+ end;
+
+ ///
+ /// Persists a notification that informs a user of Purchase Document Draft that the header Sub Total no longer matches the sum of the lines.
+ /// Does not display anything.
+ ///
+ /// Id of e-document
+ procedure AddSubTotalMismatchNotification(EDocumentEntryNo: Integer)
+ begin
+ if not IsSubTotalMismatchNotificationEnabled() then
exit;
- EDocumentNotification.Validate("E-Document Entry No.", EDocumentEntryNo);
- EDocumentNotification.Validate(ID, GetVendorMatchedByNameNotAddressNotificationId());
- EDocumentNotification.Validate("User Id", UserId());
- EDocumentNotification.Validate(Type, "E-Document Notification Type"::"Vendor Matched By Name Not Address");
- EDocumentNotification.Validate(Message, VendorMatchedByNameNotAddressMsg);
- EDocumentNotification.Insert(true);
+ EDocDraftNotifState.AddSubTotalMismatch(EDocumentEntryNo);
end;
///
- /// Send notifications for Purchase Document Draft page
+ /// Removes the persisted Sub Total Mismatch notification for the current user and e-document, e.g. when the totals re-converge.
+ ///
/// Id of e-document
+ procedure RemoveSubTotalMismatchNotification(EDocumentEntryNo: Integer)
+ begin
+ EDocDraftNotifState.RemoveSubTotalMismatch(EDocumentEntryNo);
+ end;
+
+ ///
+ /// Re-evaluates the Sub Total mismatch and updates the persisted notification. Does not display anything.
+ ///
+ /// The draft header as currently loaded by the caller
+ procedure RefreshSubTotalMismatch(EDocumentPurchaseHeader: Record "E-Document Purchase Header")
+ begin
+ if not IsSubTotalMismatchNotificationEnabled() then
+ exit;
+ EDocDraftNotifState.RefreshSubTotalMismatch(EDocumentPurchaseHeader);
+ end;
+
+ ///
+ /// Refreshes the Sub Total mismatch state and then shows every pending Purchase Document Draft notification once.
+ ///
+ /// Id of e-document
+ procedure RefreshAndShowPendingDraftNotifications(EDocumentEntryNo: Integer)
+ var
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ begin
+ if EDocumentPurchaseHeader.Get(EDocumentEntryNo) then
+ RefreshSubTotalMismatch(EDocumentPurchaseHeader);
+ SendPurchaseDocumentDraftNotifications(EDocumentEntryNo);
+ end;
+
+ ///
+ /// Shows every pending Purchase Document Draft notification for the current user.
///
+ /// Id of e-document
procedure SendPurchaseDocumentDraftNotifications(EDocumentEntryNo: Integer)
var
EDocumentNotification: Record "E-Document Notification";
begin
if not GuiAllowed() then
exit;
-
- EDocumentNotification.SetRange("E-Document Entry No.", EDocumentEntryNo);
- EDocumentNotification.SetRange(Type, "E-Document Notification Type"::"Vendor Matched By Name Not Address");
- EDocumentNotification.SetRange("User Id", UserId());
- if not EDocumentNotification.FindSet() then
+ if not EDocDraftNotifState.FindPendingDraftNotifications(EDocumentEntryNo, EDocumentNotification) then
exit;
repeat
@@ -58,21 +95,94 @@ codeunit 6123 "E-Document Notification"
until EDocumentNotification.Next() = 0;
end;
+ ///
+ /// Re-evaluates the Sub Total mismatch after a header amount edit, re-arming a dismissal, and shows the notification if it applies.
+ ///
+ /// The draft header as currently edited by the user
+ procedure RefreshAndShowSubTotalMismatchAfterHeaderEdit(EDocumentPurchaseHeader: Record "E-Document Purchase Header")
+ begin
+ if not IsSubTotalMismatchNotificationEnabled() then
+ exit;
+ if not EDocDraftNotifState.RefreshSubTotalMismatchAfterHeaderEdit(EDocumentPurchaseHeader) then
+ exit;
+ ShowSubTotalMismatchNotification(EDocumentPurchaseHeader."E-Document Entry No.");
+ end;
+
+ ///
+ /// Re-evaluates the Sub Total mismatch after a line amount edit, re-arming a dismissal, and shows the notification if it applies.
+ ///
+ /// The line as currently edited by the user
+ procedure RefreshAndShowSubTotalMismatchAfterLineEdit(EDocumentPurchaseLine: Record "E-Document Purchase Line")
+ begin
+ if not IsSubTotalMismatchNotificationEnabled() then
+ exit;
+ if not EDocDraftNotifState.RefreshSubTotalMismatchAfterLineEdit(EDocumentPurchaseLine) then
+ exit;
+ ShowSubTotalMismatchNotification(EDocumentPurchaseLine."E-Document Entry No.");
+ end;
+
+ ///
+ /// Re-evaluates the Sub Total mismatch while a line is being deleted, re-arming a dismissal, and shows the notification if it applies.
+ ///
+ /// The line being deleted
+ procedure RefreshAndShowSubTotalMismatchAfterLineDeletion(EDocumentPurchaseLine: Record "E-Document Purchase Line")
+ begin
+ if not IsSubTotalMismatchNotificationEnabled() then
+ exit;
+ if not EDocDraftNotifState.RefreshSubTotalMismatchAfterLineDeletion(EDocumentPurchaseLine) then
+ exit;
+ ShowSubTotalMismatchNotification(EDocumentPurchaseLine."E-Document Entry No.");
+ end;
+
+ ///
+ /// Shows the Sub Total Mismatch notification for the e-document, if it is persisted and not dismissed.
+ ///
+ /// Id of e-document
+ procedure ShowSubTotalMismatchNotification(EDocumentEntryNo: Integer)
+ var
+ EDocumentNotification: Record "E-Document Notification";
+ begin
+ if not GuiAllowed() then
+ exit;
+ if not EDocDraftNotifState.GetNotification(EDocumentEntryNo, EDocDraftNotifState.SubTotalMismatchNotificationId(), EDocumentNotification) then
+ exit;
+ if EDocumentNotification.Dismissed then
+ exit;
+ SendNotification(EDocumentNotification);
+ end;
+
+ ///
+ /// Returns whether the current user has the Sub Total Mismatch notification enabled.
+ ///
+ procedure IsSubTotalMismatchNotificationEnabled(): Boolean
+ var
+ MyNotifications: Record "My Notifications";
+ begin
+ exit(GuiAllowed() and MyNotifications.IsEnabled(EDocDraftNotifState.SubTotalMismatchNotificationId()));
+ end;
+
+ ///
+ /// Returns whether the current user has dismissed the Sub Total Mismatch notification for the e-document.
+ ///
+ /// Id of e-document
+ procedure IsSubTotalMismatchDismissed(EDocumentEntryNo: Integer): Boolean
+ begin
+ exit(EDocDraftNotifState.IsSubTotalMismatchDismissed(EDocumentEntryNo));
+ end;
+
///
/// Dismisses the notification of the certain Purchase Document Draft that informs a user about a vendor that is matched by name but not by address.
+ /// The persisted notification row is kept and marked as dismissed so it is not re-shown to this user.
///
- ///
+ /// Current notification
procedure DismissVendorMatchedByNameNotAddressNotification(Notification: Notification)
var
- EDocumentNotification: Record "E-Document Notification";
EDocumentEntryNo: Integer;
Id: Guid;
begin
- Evaluate(EDocumentEntryNo, Notification.GetData(EDocumentNotification.FieldName("E-Document Entry No.")));
- Evaluate(Id, Notification.GetData(EDocumentNotification.FieldName(ID)));
- if not EDocumentNotification.Get(EDocumentEntryNo, Id, UserId()) then
+ if not TryGetNotificationKeys(Notification, EDocumentEntryNo, Id) then
exit;
- EDocumentNotification.Delete(true);
+ EDocDraftNotifState.MarkDismissed(EDocumentEntryNo, Id);
end;
///
@@ -82,31 +192,69 @@ codeunit 6123 "E-Document Notification"
procedure DisableVendorMatchedByNameNotAddressNotification(Notification: Notification)
var
MyNotifications: Record "My Notifications";
- EDocumentNotification: Record "E-Document Notification";
VendorMatchedByNameNotAddressNotificationNameTok: Label 'Notify user of Purchase Document Draft that vendor is matched by name but not by address.';
VendorMatchedByNameNotAddressNotificationDescTok: Label 'Show a notification informing a user of Purchase Document Draft that a vendor is matched by name but not by address.';
begin
if MyNotifications.WritePermission() then
- if not MyNotifications.Disable(GetVendorMatchedByNameNotAddressNotificationId()) then
- MyNotifications.InsertDefault(GetVendorMatchedByNameNotAddressNotificationId(), VendorMatchedByNameNotAddressNotificationNameTok, VendorMatchedByNameNotAddressNotificationDescTok, false);
- EDocumentNotification.SetRange(Type, "E-Document Notification Type"::"Vendor Matched By Name Not Address");
- EDocumentNotification.SetRange("User Id", UserId());
- EDocumentNotification.DeleteAll(true);
+ if not MyNotifications.Disable(EDocDraftNotifState.VendorMatchedByNameNotAddressNotificationId()) then
+ MyNotifications.InsertDefault(EDocDraftNotifState.VendorMatchedByNameNotAddressNotificationId(), VendorMatchedByNameNotAddressNotificationNameTok, VendorMatchedByNameNotAddressNotificationDescTok, false);
+ EDocDraftNotifState.DeleteAllOfType("E-Document Notification Type"::"Vendor Matched By Name Not Address");
+ end;
+
+ ///
+ /// Dismisses the Sub Total Mismatch notification for the current Purchase Document Draft.
+ /// The persisted notification row is kept and marked as dismissed so the mismatch is not
+ /// re-shown to this user until an amount edit re-arms it.
+ ///
+ /// Current notification
+ procedure DismissSubTotalMismatchNotification(Notification: Notification)
+ var
+ EDocumentEntryNo: Integer;
+ Id: Guid;
+ begin
+ if not TryGetNotificationKeys(Notification, EDocumentEntryNo, Id) then
+ exit;
+ EDocDraftNotifState.MarkDismissed(EDocumentEntryNo, Id);
+ end;
+
+ ///
+ /// Disables the Sub Total Mismatch notification for the current user.
+ ///
+ /// Current notification
+ procedure DisableSubTotalMismatchNotification(Notification: Notification)
+ var
+ MyNotifications: Record "My Notifications";
+ SubTotalMismatchNotificationNameTok: Label 'Notify user of Purchase Document Draft that the document total does not match the sum of the lines.';
+ SubTotalMismatchNotificationDescTok: Label 'Show a notification informing a user of Purchase Document Draft that the header total no longer matches the sum of the lines.';
+ begin
+ if MyNotifications.WritePermission() then
+ if not MyNotifications.Disable(EDocDraftNotifState.SubTotalMismatchNotificationId()) then
+ MyNotifications.InsertDefault(EDocDraftNotifState.SubTotalMismatchNotificationId(), SubTotalMismatchNotificationNameTok, SubTotalMismatchNotificationDescTok, false);
+ EDocDraftNotifState.DeleteAllOfType("E-Document Notification Type"::"Sub Total Mismatch");
+ end;
+
+ local procedure TryGetNotificationKeys(Notification: Notification; var EDocumentEntryNo: Integer; var Id: Guid): Boolean
+ var
+ EDocumentNotification: Record "E-Document Notification";
+ begin
+ if not Evaluate(EDocumentEntryNo, Notification.GetData(EDocumentNotification.FieldName("E-Document Entry No."))) then
+ exit(false);
+ exit(Evaluate(Id, Notification.GetData(EDocumentNotification.FieldName(ID))));
end;
local procedure SendNotification(EDocumentNotification: Record "E-Document Notification")
var
MyNotifications: Record "My Notifications";
- VendorMatchedByNameNotAddressNotification: Notification;
+ DraftNotification: Notification;
begin
if not MyNotifications.IsEnabled(EDocumentNotification.ID) then
exit;
- VendorMatchedByNameNotAddressNotification.Id := EDocumentNotification.ID;
- VendorMatchedByNameNotAddressNotification.Message := EDocumentNotification.Message;
- VendorMatchedByNameNotAddressNotification.Scope := NotificationScope::LocalScope;
- AddActionsToNotification(VendorMatchedByNameNotAddressNotification, EDocumentNotification);
- VendorMatchedByNameNotAddressNotification.Send();
+ DraftNotification.Id := EDocumentNotification.ID;
+ DraftNotification.Message := EDocumentNotification.Message;
+ DraftNotification.Scope := NotificationScope::LocalScope;
+ AddActionsToNotification(DraftNotification, EDocumentNotification);
+ DraftNotification.Send();
end;
local procedure AddActionsToNotification(var Notification: Notification; EDocumentNotification: Record "E-Document Notification")
@@ -114,16 +262,19 @@ codeunit 6123 "E-Document Notification"
DismissMsg: Label 'Dismiss';
DontShowThisAgainMsg: Label 'Don''t show this again.';
begin
- if EDocumentNotification.Type <> "E-Document Notification Type"::"Vendor Matched By Name Not Address" then
- exit;
Notification.SetData(EDocumentNotification.FieldName("E-Document Entry No."), Format(EDocumentNotification."E-Document Entry No."));
Notification.SetData(EDocumentNotification.FieldName(ID), EDocumentNotification.ID);
- Notification.AddAction(DismissMsg, Codeunit::"E-Document Notification", 'DismissVendorMatchedByNameNotAddressNotification');
- Notification.AddAction(DontShowThisAgainMsg, Codeunit::"E-Document Notification", 'DisableVendorMatchedByNameNotAddressNotification');
- end;
-
- local procedure GetVendorMatchedByNameNotAddressNotificationId(): Guid
- begin
- exit('bc0d8537-8e8d-4d94-a07a-a5a54c729d2a');
+ case EDocumentNotification.Type of
+ "E-Document Notification Type"::"Vendor Matched By Name Not Address":
+ begin
+ Notification.AddAction(DismissMsg, Codeunit::"E-Document Notification", 'DismissVendorMatchedByNameNotAddressNotification');
+ Notification.AddAction(DontShowThisAgainMsg, Codeunit::"E-Document Notification", 'DisableVendorMatchedByNameNotAddressNotification');
+ end;
+ "E-Document Notification Type"::"Sub Total Mismatch":
+ begin
+ Notification.AddAction(DismissMsg, Codeunit::"E-Document Notification", 'DismissSubTotalMismatchNotification');
+ Notification.AddAction(DontShowThisAgainMsg, Codeunit::"E-Document Notification", 'DisableSubTotalMismatchNotification');
+ end;
+ end;
end;
-}
\ No newline at end of file
+}
diff --git a/src/Apps/W1/EDocument/App/src/Document/Notification/EDocumentNotification.Table.al b/src/Apps/W1/EDocument/App/src/Document/Notification/EDocumentNotification.Table.al
index 901e6946f9d..8b696bc59e5 100644
--- a/src/Apps/W1/EDocument/App/src/Document/Notification/EDocumentNotification.Table.al
+++ b/src/Apps/W1/EDocument/App/src/Document/Notification/EDocumentNotification.Table.al
@@ -39,6 +39,12 @@ table 6126 "E-Document Notification"
Caption = 'Message';
ToolTip = 'Specifies the message of the E-Document notification.';
}
+ field(6; Dismissed; Boolean)
+ {
+ Caption = 'Dismissed';
+ DataClassification = SystemMetadata;
+ ToolTip = 'Specifies whether the user has dismissed the notification.';
+ }
}
keys
{
diff --git a/src/Apps/W1/EDocument/App/src/Document/Notification/EDocumentNotificationType.Enum.al b/src/Apps/W1/EDocument/App/src/Document/Notification/EDocumentNotificationType.Enum.al
index 7324cbae959..f7b4c00f77b 100644
--- a/src/Apps/W1/EDocument/App/src/Document/Notification/EDocumentNotificationType.Enum.al
+++ b/src/Apps/W1/EDocument/App/src/Document/Notification/EDocumentNotificationType.Enum.al
@@ -16,4 +16,8 @@ enum 6126 "E-Document Notification Type"
{
Caption = 'Vendor Matched By Name Not Address';
}
+ value(2; "Sub Total Mismatch")
+ {
+ Caption = 'Sub Total Mismatch';
+ }
}
\ No newline at end of file
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocPurchaseDraftSubform.Page.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocPurchaseDraftSubform.Page.al
index 6526749b9cb..7b208ae2ae1 100644
--- a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocPurchaseDraftSubform.Page.al
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocPurchaseDraftSubform.Page.al
@@ -346,6 +346,7 @@ page 6183 "E-Doc. Purchase Draft Subform"
TempEDocumentPOMatchWarnings: Record "E-Doc PO Match Warning";
EDocPurchaseHistMapping: Codeunit "E-Doc. Purchase Hist. Mapping";
EDocPOMatching: Codeunit "E-Doc. PO Matching";
+ EDocumentNotification: Codeunit "E-Document Notification";
AdditionalColumns, OrderMatchedCaption, MatchWarningsCaption, MatchWarningsStyleExpr, MatchedEntityName : Text;
LineAmount: Decimal;
DimVisible1, DimVisible2, HasAdditionalColumns, IsEDocumentMatchedToAnyPOLine, IsLineMatchedToOrderLine, IsLineMatchedToReceiptLine, HasEDocumentOrderMatchWarnings, VATProdPostGroupIsVisible : Boolean;
@@ -382,6 +383,12 @@ page 6183 "E-Doc. Purchase Draft Subform"
SetVATProductPostingGroupVisibility();
end;
+ trigger OnDeleteRecord(): Boolean
+ begin
+ EDocumentNotification.RefreshAndShowSubTotalMismatchAfterLineDeletion(Rec);
+ exit(true);
+ end;
+
internal procedure SetEDocumentPurchaseHeader(EDocPurchHeader: Record "E-Document Purchase Header")
begin
EDocumentPurchaseHeader := EDocPurchHeader;
@@ -407,10 +414,8 @@ page 6183 "E-Doc. Purchase Draft Subform"
VATProdPostGroupIsVisible := PurchSetup."Resolve VAT Group Purch EDoc";
end;
- local procedure UpdateCalculatedAmounts(UpdateParentRecord: Boolean)
+ local procedure UpdateCalculatedAmounts(UserModifiedAmount: Boolean)
var
- TotalEDocPurchaseLine: Record "E-Document Purchase Line";
- EDocumentImportHelper: Codeunit "E-Document Import Helper";
LineSubtotal: Decimal;
DiscountExceedsSubtotalErr: Label 'Discount should not exceed the subtotal of the line';
begin
@@ -423,19 +428,9 @@ page 6183 "E-Doc. Purchase Draft Subform"
else
if Rec."Total Discount" / LineSubtotal > 1 then
Error(DiscountExceedsSubtotalErr);
- if not UpdateParentRecord then
+ if not UserModifiedAmount then
exit;
- if not EDocumentPurchaseHeader.Get(Rec."E-Document Entry No.") then
- exit;
- EDocumentPurchaseHeader."Sub Total" := 0;
- TotalEDocPurchaseLine.SetRange("E-Document Entry No.", Rec."E-Document Entry No.");
- if TotalEDocPurchaseLine.FindSet() then
- repeat
- EDocumentPurchaseHeader."Sub Total" += Round(TotalEDocPurchaseLine.Quantity * TotalEDocPurchaseLine."Unit Price", EDocumentImportHelper.GetCurrencyRoundingPrecision(EDocumentPurchaseHeader."Currency Code")) - TotalEDocPurchaseLine."Total Discount";
- until TotalEDocPurchaseLine.Next() = 0;
- EDocumentPurchaseHeader.Total := EDocumentPurchaseHeader."Sub Total" + EDocumentPurchaseHeader."Total VAT" - EDocumentPurchaseHeader."Total Discount";
- EDocumentPurchaseHeader.Modify();
- CurrPage.Update();
+ EDocumentNotification.RefreshAndShowSubTotalMismatchAfterLineEdit(Rec);
end;
local procedure SetHasAdditionalColumns()
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseDraft.Page.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseDraft.Page.al
index 0dd57bc3350..01b95af20c6 100644
--- a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseDraft.Page.al
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseDraft.Page.al
@@ -214,6 +214,7 @@ page 6181 "E-Document Purchase Draft"
begin
UpdateTotal();
EDocumentPurchaseHeader.Modify();
+ GlobalEDocumentNotification.RefreshAndShowSubTotalMismatchAfterHeaderEdit(EDocumentPurchaseHeader);
CurrPage.Update();
end;
}
@@ -266,6 +267,7 @@ page 6181 "E-Document Purchase Draft"
trigger OnValidate()
begin
EDocumentPurchaseHeader.Modify();
+ GlobalEDocumentNotification.RefreshAndShowSubTotalMismatchAfterHeaderEdit(EDocumentPurchaseHeader);
CurrPage.Update();
end;
}
@@ -509,7 +511,6 @@ page 6181 "E-Document Purchase Draft"
var
EDocumentDataStorage: Record "E-Doc. Data Storage";
PurchasesPayablesSetup: Record "Purchases & Payables Setup";
- EDocumentNotification: Codeunit "E-Document Notification";
EDocPOMatching: Codeunit "E-Doc. PO Matching";
MatchesRemovedMsg: Label 'This e-document was matched to purchase order lines, but the matches are no longer consistent with the current data. The matches have been removed';
begin
@@ -528,7 +529,7 @@ page 6181 "E-Document Purchase Draft"
HasErrors := false;
PageEditable := IsEditable();
IsCreditMemo := Rec."Document Type" = Enum::"E-Document Type"::"Purchase Credit Memo";
- EDocumentNotification.SendPurchaseDocumentDraftNotifications(Rec."Entry No");
+ GlobalEDocumentNotification.RefreshAndShowPendingDraftNotifications(Rec."Entry No");
if PurchasesPayablesSetup.Get() then
ApplyVATDiffEnabled := PurchasesPayablesSetup."Apply VAT Diff. For Purch EDoc";
@@ -779,6 +780,7 @@ page 6181 "E-Document Purchase Draft"
EDocumentPurchaseHeader: Record "E-Document Purchase Header";
EDocumentServiceStatus: Record "E-Document Service Status";
EDocumentErrorHelper: Codeunit "E-Document Error Helper";
+ GlobalEDocumentNotification: Codeunit "E-Document Notification";
EDocumentProcessing: Codeunit "E-Document Processing";
FeatureTelemetry: Codeunit "Feature Telemetry";
GlobalEDocumentHelper: Codeunit "E-Document Helper";
diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocPurchDraftTotalsTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocPurchDraftTotalsTests.Codeunit.al
new file mode 100644
index 00000000000..b0f5e8b5c3c
--- /dev/null
+++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocPurchDraftTotalsTests.Codeunit.al
@@ -0,0 +1,957 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+namespace Microsoft.eServices.EDocument.Test;
+
+using Microsoft.eServices.EDocument;
+using Microsoft.eServices.EDocument.Integration;
+using Microsoft.eServices.EDocument.Processing.Import;
+using Microsoft.eServices.EDocument.Processing.Import.Purchase;
+using Microsoft.Finance.Currency;
+using Microsoft.Purchases.Vendor;
+
+codeunit 135648 "E-Doc Purch Draft Totals Tests"
+{
+ Subtype = Test;
+ TestType = IntegrationTest;
+ TestPermissions = Disabled;
+
+ var
+ EDocumentService: Record "E-Document Service";
+ Assert: Codeunit Assert;
+ LibraryEDoc: Codeunit "Library - E-Document";
+ LibraryUtility: Codeunit "Library - Utility";
+ ExpectedNotificationId: Guid;
+ ExpectedNotificationEntryNo: Integer;
+ SentNotificationCount: Integer;
+
+ [Test]
+ procedure AddSubTotalMismatchNotificationPersistsRecord()
+ var
+ EDocumentNotification: Codeunit "E-Document Notification";
+ EntryNo: Integer;
+ begin
+ // [FEATURE] [AI test]
+ // [SCENARIO] Adding a Sub Total Mismatch notification persists exactly one record for the user + e-document
+ Initialize();
+
+ // [GIVEN] A clean notification table for a given entry no
+ EntryNo := 909091;
+
+ // [WHEN] Adding the notification twice (idempotent)
+ EDocumentNotification.AddSubTotalMismatchNotification(EntryNo);
+ EDocumentNotification.AddSubTotalMismatchNotification(EntryNo);
+
+ // [THEN] Exactly one record of type Sub Total Mismatch exists
+ Assert.AreEqual(1, CountSubTotalMismatchNotifications(EntryNo), 'Exactly one Sub Total Mismatch notification must exist.');
+ end;
+
+ [Test]
+ procedure RemoveSubTotalMismatchNotificationDeletesRecord()
+ var
+ EDocumentNotification: Codeunit "E-Document Notification";
+ EntryNo: Integer;
+ begin
+ // [FEATURE] [AI test]
+ // [SCENARIO] Removing the notification deletes the persisted record (totals re-converged)
+ Initialize();
+
+ // [GIVEN] A persisted Sub Total Mismatch notification
+ EntryNo := 909092;
+ EDocumentNotification.AddSubTotalMismatchNotification(EntryNo);
+
+ // [GIVEN] The notification was actually persisted
+ Assert.AreEqual(1, CountSubTotalMismatchNotifications(EntryNo), 'The Sub Total Mismatch notification must exist before removal.');
+
+ // [WHEN] Removing it
+ EDocumentNotification.RemoveSubTotalMismatchNotification(EntryNo);
+
+ // [THEN] No record remains for that entry no
+ Assert.AreEqual(0, CountSubTotalMismatchNotifications(EntryNo), 'The Sub Total Mismatch notification must be removed.');
+ end;
+
+ [Test]
+ [HandlerFunctions('SendNotificationHandler')]
+ procedure EditingLineDoesNotOverwriteHeaderSubTotal()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ EDocumentPurchaseDraft: TestPage "E-Document Purchase Draft";
+ begin
+ // [FEATURE] [AI test]
+ // [SCENARIO] Editing a draft line no longer overwrites the extracted header Sub Total / Total
+ Initialize();
+
+ // [GIVEN] An inbound e-document with header Sub Total intentionally different from the sum of the lines
+ CreatePurchaseDraft(EDocument, EDocumentPurchaseHeader, 1000);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 500);
+
+ // [WHEN] Editing the line quantity on the draft page
+ ExpectSubTotalMismatchNotification(EDocument."Entry No");
+ OpenPurchaseDraft(EDocumentPurchaseDraft, EDocument);
+ EDocumentPurchaseDraft.Lines.Quantity.SetValue(3);
+ EDocumentPurchaseDraft.Close();
+
+ // [THEN] The header Sub Total / Total are unchanged (no overwrite from the sum of the lines)
+ VerifyHeaderTotals(EDocumentPurchaseHeader, 1000, 1000);
+ VerifySubTotalMismatchNotificationShown();
+ end;
+
+ [Test]
+ [HandlerFunctions('SendNotificationHandler')]
+ procedure AddingLineTriggersSubTotalMismatchNotification()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ EDocumentPurchaseDraft: TestPage "E-Document Purchase Draft";
+ begin
+ // [FEATURE] [AI test]
+ // [SCENARIO] Adding a new draft line that makes the sum of the lines diverge from the header Sub Total shows the Sub Total Mismatch notification
+ Initialize();
+
+ // [GIVEN] An inbound e-document "E" whose header Sub Total (1000) matches its single line (1000)
+ CreatePurchaseDraft(EDocument, EDocumentPurchaseHeader, 1000);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 1000);
+
+ // [GIVEN] The purchase draft page is open on "E" while the header Sub Total still matches the sum of the lines
+ ExpectSubTotalMismatchNotification(EDocument."Entry No");
+ OpenPurchaseDraft(EDocumentPurchaseDraft, EDocument);
+ Assert.AreEqual(0, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'No Sub Total Mismatch notification should exist before adding the line.');
+
+ // [WHEN] Adding a new line of 500 so the sum of the lines (1500) no longer matches the header Sub Total (1000)
+ EDocumentPurchaseDraft.Lines.New();
+ EDocumentPurchaseDraft.Lines.Description.SetValue('Added line');
+ EDocumentPurchaseDraft.Lines.Quantity.SetValue(1);
+ EDocumentPurchaseDraft.Lines."Direct Unit Cost".SetValue(500);
+ // Leave the new row so it is committed and the totals are re-evaluated against the persisted lines
+ EDocumentPurchaseDraft.Lines.First();
+ EDocumentPurchaseDraft.Close();
+
+ // [THEN] The Sub Total Mismatch notification is shown (SendNotificationHandler) and persisted for "E"
+ Assert.AreEqual(1, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'A Sub Total Mismatch notification should exist after adding the line.');
+ VerifySubTotalMismatchNotificationShown();
+ end;
+
+ [Test]
+ procedure DifferenceEqualToToleranceDoesNotCreateNotification()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ EDocumentPurchaseDraft: TestPage "E-Document Purchase Draft";
+ begin
+ // [FEATURE] [AI test]
+ // [SCENARIO] A Sub Total difference equal to the rounding tolerance does not create a notification
+ Initialize();
+
+ // [GIVEN] An inbound e-document "E" with one line whose subtotal differs from the header by exactly 0.01
+ CreatePurchaseDraft(EDocument, EDocumentPurchaseHeader, 1000);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 1000.01);
+
+ // [WHEN] Opening the purchase draft for "E"
+ OpenPurchaseDraft(EDocumentPurchaseDraft, EDocument);
+
+ // [THEN] No Sub Total Mismatch notification is persisted because the difference equals the one-line tolerance
+ Assert.AreEqual(0, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'A difference equal to the tolerance must not create a notification.');
+ EDocumentPurchaseDraft.Close();
+ end;
+
+ [Test]
+ [HandlerFunctions('SendNotificationHandler')]
+ procedure HeaderSubtotalAboveLinesBeyondToleranceCreatesNotification()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ EDocumentPurchaseDraft: TestPage "E-Document Purchase Draft";
+ begin
+ // [FEATURE] [AI test]
+ // [SCENARIO] A header Sub Total above the lines by more than the tolerance creates a notification
+ Initialize();
+
+ // [GIVEN] An inbound e-document "E" with header Sub Total 1000.02 and line subtotal 1000
+ CreatePurchaseDraft(EDocument, EDocumentPurchaseHeader, 1000.02);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 1000);
+
+ // [WHEN] Opening the purchase draft for "E"
+ ExpectSubTotalMismatchNotification(EDocument."Entry No");
+ OpenPurchaseDraft(EDocumentPurchaseDraft, EDocument);
+
+ // [THEN] One Sub Total Mismatch notification is persisted for "E"
+ Assert.AreEqual(1, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'A header subtotal above the lines beyond tolerance must create a notification.');
+ VerifySubTotalMismatchNotificationShown();
+
+ // [THEN] The extracted header totals remain unchanged
+ VerifyHeaderTotals(EDocumentPurchaseHeader, 1000.02, 1000.02);
+ EDocumentPurchaseDraft.Close();
+ end;
+
+ [Test]
+ [HandlerFunctions('SendNotificationHandler')]
+ procedure LinesSubtotalAboveHeaderBeyondToleranceCreatesNotification()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ EDocumentPurchaseDraft: TestPage "E-Document Purchase Draft";
+ begin
+ // [FEATURE] [AI test]
+ // [SCENARIO] A lines Sub Total above the header by more than the tolerance creates a notification
+ Initialize();
+
+ // [GIVEN] An inbound e-document "E" with header Sub Total 1000 and line subtotal 1000.02
+ CreatePurchaseDraft(EDocument, EDocumentPurchaseHeader, 1000);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 1000.02);
+
+ // [WHEN] Opening the purchase draft for "E"
+ ExpectSubTotalMismatchNotification(EDocument."Entry No");
+ OpenPurchaseDraft(EDocumentPurchaseDraft, EDocument);
+
+ // [THEN] One Sub Total Mismatch notification is persisted for "E"
+ Assert.AreEqual(1, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'A lines subtotal above the header beyond tolerance must create a notification.');
+ VerifySubTotalMismatchNotificationShown();
+
+ // [THEN] The extracted header totals remain unchanged
+ VerifyHeaderTotals(EDocumentPurchaseHeader, 1000, 1000);
+ EDocumentPurchaseDraft.Close();
+ end;
+
+ [Test]
+ procedure MultipleLinesUsePerLineRoundingAndAccumulatedTolerance()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ EDocumentPurchaseDraft: TestPage "E-Document Purchase Draft";
+ begin
+ // [FEATURE] [AI test]
+ // [SCENARIO] Multiple lines use per-line currency rounding and accumulate the allowed tolerance
+ Initialize();
+
+ // [GIVEN] An inbound e-document "E" with two lines that each round from 500.004 to 500
+ CreatePurchaseDraft(EDocument, EDocumentPurchaseHeader, 1000.02);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 500.004);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 500.004);
+
+ // [WHEN] Opening the purchase draft for "E"
+ OpenPurchaseDraft(EDocumentPurchaseDraft, EDocument);
+
+ // [THEN] No notification is persisted because the 0.02 difference equals the two-line tolerance
+ Assert.AreEqual(0, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'A difference equal to the accumulated tolerance must not create a notification.');
+ EDocumentPurchaseDraft.Close();
+ end;
+
+ [Test]
+ [HandlerFunctions('SendNotificationHandler')]
+ procedure EditingLineToReconcileSubtotalRemovesMismatchNotification()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ EDocumentPurchaseDraft: TestPage "E-Document Purchase Draft";
+ begin
+ // [FEATURE] [AI test]
+ // [SCENARIO] Editing a line so its subtotal matches the header removes the mismatch notification
+ Initialize();
+
+ // [GIVEN] An inbound e-document "E" with header Sub Total 1000, line subtotal 1000.02, and a mismatch notification
+ CreatePurchaseDraft(EDocument, EDocumentPurchaseHeader, 1000);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 1000.02);
+ ExpectSubTotalMismatchNotification(EDocument."Entry No");
+ OpenPurchaseDraft(EDocumentPurchaseDraft, EDocument);
+ Assert.AreEqual(1, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'A Sub Total Mismatch notification must exist before reconciling the line.');
+ VerifySubTotalMismatchNotificationShown();
+
+ // [WHEN] Changing the line direct unit cost to 1000
+ EDocumentPurchaseDraft.Lines."Direct Unit Cost".SetValue(1000);
+ EDocumentPurchaseDraft.Close();
+ OpenPurchaseDraft(EDocumentPurchaseDraft, EDocument);
+
+ // [THEN] The Sub Total Mismatch notification is removed
+ Assert.AreEqual(0, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'The Sub Total Mismatch notification must be removed after reconciling the line.');
+
+ // [THEN] The extracted header totals remain unchanged
+ VerifyHeaderTotals(EDocumentPurchaseHeader, 1000, 1000);
+ EDocumentPurchaseDraft.Close();
+ end;
+
+ [Test]
+ [HandlerFunctions('SendNotificationHandler')]
+ procedure DeletingLineReevaluatesMismatchWithoutChangingHeaderTotals()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ EDocumentPurchaseDraft: TestPage "E-Document Purchase Draft";
+ begin
+ // [FEATURE] [AI test]
+ // [SCENARIO] Deleting a line re-evaluates the mismatch without changing the extracted header totals
+ Initialize();
+
+ // [GIVEN] An inbound e-document "E" with header Sub Total 1000 matching two lines of 500
+ CreatePurchaseDraft(EDocument, EDocumentPurchaseHeader, 1000);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 500);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 500);
+ ExpectSubTotalMismatchNotification(EDocument."Entry No");
+ OpenPurchaseDraft(EDocumentPurchaseDraft, EDocument);
+ Assert.AreEqual(0, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'No Sub Total Mismatch notification should exist before deleting the line.');
+
+ // [WHEN] Deleting the second line
+ EDocumentPurchaseLine.Delete();
+ EDocumentPurchaseDraft.Close();
+ OpenPurchaseDraft(EDocumentPurchaseDraft, EDocument);
+
+ // [THEN] One Sub Total Mismatch notification is persisted for "E"
+ Assert.AreEqual(1, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'Deleting a line must create a Sub Total Mismatch notification.');
+ VerifySubTotalMismatchNotificationShown();
+
+ // [THEN] The extracted header totals remain unchanged
+ VerifyHeaderTotals(EDocumentPurchaseHeader, 1000, 1000);
+ EDocumentPurchaseDraft.Close();
+ end;
+
+ [Test]
+ procedure DismissedNotificationSuppressesSubTotalMismatchNotification()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ EDocumentPurchaseDraft: TestPage "E-Document Purchase Draft";
+ begin
+ // [SCENARIO] When the notification is marked dismissed, opening the draft (OnAfterGetRecord) does not re-show the mismatch notification
+ Initialize();
+
+ // [GIVEN] A draft with header Sub Total 1000 and a single line subtotal 500 (mismatch beyond tolerance)
+ CreatePurchaseDraft(EDocument, EDocumentPurchaseHeader, 1000);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 500);
+
+ // [GIVEN] The notification row is already marked as dismissed for the user
+ SetSubTotalMismatchDismissed(EDocument."Entry No", true);
+
+ // [WHEN] Opening the purchase draft (fires OnAfterGetRecord repeatedly)
+ OpenPurchaseDraft(EDocumentPurchaseDraft, EDocument);
+
+ // [THEN] No Sub Total Mismatch notification is persisted while dismissed
+ Assert.AreEqual(0, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'A dismissed header must suppress the Sub Total Mismatch notification.');
+ EDocumentPurchaseDraft.Close();
+ end;
+
+ [Test]
+ [HandlerFunctions('SendNotificationHandler')]
+ procedure EditingAmountReArmsDismissedSubTotalMismatchNotification()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ EDocumentPurchaseDraft: TestPage "E-Document Purchase Draft";
+ begin
+ // [SCENARIO] Editing a line amount while dismissed re-arms and re-shows the mismatch notification
+ Initialize();
+
+ // [GIVEN] A draft with header Sub Total 1000, one line subtotal 500 (mismatch), marked as dismissed
+ CreatePurchaseDraft(EDocument, EDocumentPurchaseHeader, 1000);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 500);
+ SetSubTotalMismatchDismissed(EDocument."Entry No", true);
+
+ // [GIVEN] The draft is open and the notification is suppressed
+ ExpectSubTotalMismatchNotification(EDocument."Entry No");
+ OpenPurchaseDraft(EDocumentPurchaseDraft, EDocument);
+ Assert.AreEqual(0, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'The notification must be suppressed before the amount edit.');
+
+ // [WHEN] Editing the line quantity to 3 (lines subtotal 1500, still a mismatch)
+ EDocumentPurchaseDraft.Lines.Quantity.SetValue(3);
+
+ // [THEN] The notification is shown again and the dismissed state is cleared
+ Assert.AreEqual(1, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'Editing an amount must re-arm and re-show the mismatch notification.');
+ Assert.IsFalse(GetSubTotalMismatchDismissed(EDocument."Entry No"), 'Editing an amount must clear the dismissed state.');
+ VerifySubTotalMismatchNotificationShown();
+ EDocumentPurchaseDraft.Close();
+ end;
+
+ [Test]
+ procedure DismissActionMarksNotificationDismissed()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ EDocumentNotification: Codeunit "E-Document Notification";
+ DismissNotification: Notification;
+ begin
+ // [SCENARIO] Invoking the Dismiss action marks the persisted notification as dismissed (row is kept, not deleted)
+ Initialize();
+
+ // [GIVEN] A draft with a persisted Sub Total Mismatch notification
+ CreatePurchaseDraft(EDocument, EDocumentPurchaseHeader, 1000);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 500);
+ EDocumentNotification.AddSubTotalMismatchNotification(EDocument."Entry No");
+ Assert.AreEqual(1, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'The notification must be shown before dismissing.');
+
+ // [WHEN] The Dismiss action runs
+ DismissNotification := BuildSubTotalMismatchNotification(EDocument."Entry No");
+ EDocumentNotification.DismissSubTotalMismatchNotification(DismissNotification);
+
+ // [THEN] The notification is no longer shown but the row persists marked as dismissed
+ Assert.AreEqual(0, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'Dismissing must stop showing the notification.');
+ Assert.IsTrue(GetSubTotalMismatchDismissed(EDocument."Entry No"), 'Dismissing must mark the notification as dismissed.');
+ end;
+
+ [Test]
+ [HandlerFunctions('SendNotificationHandler')]
+ procedure HeaderSubTotalEditBeyondToleranceCreatesMismatchNotification()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ EDocumentPurchaseDraft: TestPage "E-Document Purchase Draft";
+ begin
+ // [FEATURE] [AI test]
+ // [SCENARIO] Editing the header Amount Excl. VAT so it diverges from the lines creates and shows the mismatch notification
+ Initialize();
+
+ // [GIVEN] An inbound e-document "E" whose header Sub Total (1000) matches its single line (1000)
+ CreatePurchaseDraft(EDocument, EDocumentPurchaseHeader, 1000);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 1000);
+
+ // [GIVEN] The purchase draft page is open on "E" without a mismatch
+ ExpectSubTotalMismatchNotification(EDocument."Entry No");
+ OpenPurchaseDraft(EDocumentPurchaseDraft, EDocument);
+ Assert.AreEqual(0, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'No Sub Total Mismatch notification should exist before the header edit.');
+
+ // [WHEN] Setting the header Amount Excl. VAT to 1500
+ EDocumentPurchaseDraft."Amount Excl. VAT".SetValue(1500);
+
+ // [THEN] The Sub Total Mismatch notification is persisted and shown for "E"
+ Assert.AreEqual(1, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'Editing the header Sub Total beyond the tolerance must create a notification.');
+ VerifySubTotalMismatchNotificationShown();
+
+ // [THEN] The edited header totals are kept
+ VerifyHeaderTotals(EDocumentPurchaseHeader, 1500, 1500);
+ EDocumentPurchaseDraft.Close();
+ end;
+
+ [Test]
+ [HandlerFunctions('SendNotificationHandler')]
+ procedure HeaderSubTotalEditToMatchLinesRemovesMismatchNotification()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ EDocumentPurchaseDraft: TestPage "E-Document Purchase Draft";
+ begin
+ // [FEATURE] [AI test]
+ // [SCENARIO] Editing the header Amount Excl. VAT so it matches the lines removes the mismatch notification
+ Initialize();
+
+ // [GIVEN] An inbound e-document "E" with header Sub Total 1500 and a single line of 1000
+ CreatePurchaseDraft(EDocument, EDocumentPurchaseHeader, 1500);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 1000);
+
+ // [GIVEN] The purchase draft page is open on "E" and the mismatch notification was shown
+ ExpectSubTotalMismatchNotification(EDocument."Entry No");
+ OpenPurchaseDraft(EDocumentPurchaseDraft, EDocument);
+ Assert.AreEqual(1, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'A Sub Total Mismatch notification must exist before the header edit.');
+ VerifySubTotalMismatchNotificationShown();
+
+ // [WHEN] Setting the header Amount Excl. VAT to 1000
+ EDocumentPurchaseDraft."Amount Excl. VAT".SetValue(1000);
+
+ // [THEN] The Sub Total Mismatch notification is removed for "E"
+ Assert.AreEqual(0, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'Reconciling the header Sub Total must remove the notification.');
+
+ // [THEN] The edited header totals are kept
+ VerifyHeaderTotals(EDocumentPurchaseHeader, 1000, 1000);
+ EDocumentPurchaseDraft.Close();
+ end;
+
+ [Test]
+ [HandlerFunctions('SendNotificationHandler')]
+ procedure HeaderSubTotalEditReArmsDismissedMismatchNotification()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ EDocumentPurchaseDraft: TestPage "E-Document Purchase Draft";
+ begin
+ // [FEATURE] [AI test]
+ // [SCENARIO] Editing the header Amount Excl. VAT re-arms a previously dismissed mismatch notification
+ Initialize();
+
+ // [GIVEN] An inbound e-document "E" with header Sub Total 1000, a single line of 500, and a dismissed notification
+ CreatePurchaseDraft(EDocument, EDocumentPurchaseHeader, 1000);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 500);
+ SetSubTotalMismatchDismissed(EDocument."Entry No", true);
+
+ // [GIVEN] The purchase draft page is open on "E" and the notification is suppressed
+ ExpectSubTotalMismatchNotification(EDocument."Entry No");
+ OpenPurchaseDraft(EDocumentPurchaseDraft, EDocument);
+ Assert.AreEqual(0, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'The notification must be suppressed before the header edit.');
+
+ // [WHEN] Setting the header Amount Excl. VAT to 1500 (lines subtotal 500, still a mismatch)
+ EDocumentPurchaseDraft."Amount Excl. VAT".SetValue(1500);
+
+ // [THEN] The notification is shown again and the dismissed state is cleared
+ Assert.AreEqual(1, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'Editing the header Sub Total must re-arm and re-show the mismatch notification.');
+ Assert.IsFalse(GetSubTotalMismatchDismissed(EDocument."Entry No"), 'Editing the header Sub Total must clear the dismissed state.');
+ VerifySubTotalMismatchNotificationShown();
+ EDocumentPurchaseDraft.Close();
+ end;
+
+ [Test]
+ [HandlerFunctions('SendNotificationHandler')]
+ procedure CurrencyCodeEditToCoarserRoundingRemovesMismatchNotification()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ EDocumentPurchaseDraft: TestPage "E-Document Purchase Draft";
+ CurrencyCode: Code[10];
+ begin
+ // [FEATURE] [AI test]
+ // [SCENARIO] Changing the header Currency Code to a currency with a coarser rounding precision widens the tolerance and removes the mismatch notification
+ Initialize();
+
+ // [GIVEN] A currency "C" with Amount Rounding Precision 1
+ CurrencyCode := CreateCurrencyWithRoundingPrecision(1);
+
+ // [GIVEN] An inbound e-document "E" in local currency with header Sub Total 1000.5 and a single line of 1000
+ CreatePurchaseDraft(EDocument, EDocumentPurchaseHeader, 1000.5);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 1000);
+
+ // [GIVEN] The purchase draft page is open on "E" and the mismatch notification was shown for the 0.01 tolerance
+ ExpectSubTotalMismatchNotification(EDocument."Entry No");
+ OpenPurchaseDraft(EDocumentPurchaseDraft, EDocument);
+ Assert.AreEqual(1, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'A Sub Total Mismatch notification must exist in local currency.');
+ VerifySubTotalMismatchNotificationShown();
+
+ // [WHEN] Setting the header Currency Code to "C"
+ EDocumentPurchaseDraft."Currency Code".SetValue(CurrencyCode);
+
+ // [THEN] The 0.5 difference is within the one-line tolerance of 1 and the notification is removed
+ Assert.AreEqual(0, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'A coarser rounding precision must widen the tolerance and remove the notification.');
+ EDocumentPurchaseDraft.Close();
+ end;
+
+ [Test]
+ [HandlerFunctions('SendNotificationHandler')]
+ procedure CurrencyCodeEditToFinerRoundingCreatesMismatchNotification()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ EDocumentPurchaseDraft: TestPage "E-Document Purchase Draft";
+ CurrencyCode: Code[10];
+ begin
+ // [FEATURE] [AI test]
+ // [SCENARIO] Changing the header Currency Code to local currency narrows the tolerance and creates the mismatch notification
+ Initialize();
+
+ // [GIVEN] A currency "C" with Amount Rounding Precision 1
+ CurrencyCode := CreateCurrencyWithRoundingPrecision(1);
+
+ // [GIVEN] An inbound e-document "E" in "C" with header Sub Total 1000.5 and a single line of 1000
+ CreatePurchaseDraft(EDocument, EDocumentPurchaseHeader, 1000.5);
+ SetHeaderCurrencyCode(EDocumentPurchaseHeader, CurrencyCode);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 1000);
+
+ // [GIVEN] The purchase draft page is open on "E" without a mismatch because the tolerance is 1
+ ExpectSubTotalMismatchNotification(EDocument."Entry No");
+ OpenPurchaseDraft(EDocumentPurchaseDraft, EDocument);
+ Assert.AreEqual(0, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'No Sub Total Mismatch notification should exist for the coarse rounding precision.');
+
+ // [WHEN] Clearing the header Currency Code so local currency rounding applies
+ EDocumentPurchaseDraft."Currency Code".SetValue('');
+
+ // [THEN] The 0.5 difference exceeds the one-line tolerance of 0.01 and the notification is shown
+ Assert.AreEqual(1, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'A finer rounding precision must narrow the tolerance and create the notification.');
+ VerifySubTotalMismatchNotificationShown();
+ EDocumentPurchaseDraft.Close();
+ end;
+
+ [Test]
+ [HandlerFunctions('SendNotificationHandler')]
+ procedure LineDeletionEvaluationExcludesDeletedLineAndCreatesNotification()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ EDocumentNotification: Codeunit "E-Document Notification";
+ begin
+ // [FEATURE] [AI test]
+ // [SCENARIO] The line deletion evaluation, as run by the subform OnDeleteRecord trigger, excludes the line being deleted while it is still persisted
+ Initialize();
+
+ // [GIVEN] An inbound e-document "E" with header Sub Total 1000 matching two lines "L1" and "L2" of 500
+ CreatePurchaseDraft(EDocument, EDocumentPurchaseHeader, 1000);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 500);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 500);
+ Assert.AreEqual(0, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'No Sub Total Mismatch notification should exist while the lines match the header.');
+
+ // [WHEN] Evaluating the mismatch for "L2" as deleted, before the row is removed from the database
+ ExpectSubTotalMismatchNotification(EDocument."Entry No");
+ EDocumentNotification.RefreshAndShowSubTotalMismatchAfterLineDeletion(EDocumentPurchaseLine);
+
+ // [THEN] The remaining line subtotal of 500 no longer matches the header and the notification is shown
+ Assert.AreEqual(1, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'Deleting a line must exclude it from the lines subtotal and create a notification.');
+ VerifySubTotalMismatchNotificationShown();
+
+ // [THEN] The extracted header totals remain unchanged
+ VerifyHeaderTotals(EDocumentPurchaseHeader, 1000, 1000);
+ end;
+
+ [Test]
+ [HandlerFunctions('SendNotificationHandler')]
+ procedure LineDeletionEvaluationReconcilingSubTotalRemovesNotification()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ EDocumentNotification: Codeunit "E-Document Notification";
+ EDocumentPurchaseDraft: TestPage "E-Document Purchase Draft";
+ begin
+ // [FEATURE] [AI test]
+ // [SCENARIO] Deleting the line that caused the divergence removes the mismatch notification
+ Initialize();
+
+ // [GIVEN] An inbound e-document "E" with header Sub Total 1000 and lines "L1" of 1000 and "L2" of 500
+ CreatePurchaseDraft(EDocument, EDocumentPurchaseHeader, 1000);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 1000);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 500);
+
+ // [GIVEN] The purchase draft page is open on "E" and the mismatch notification was shown
+ ExpectSubTotalMismatchNotification(EDocument."Entry No");
+ OpenPurchaseDraft(EDocumentPurchaseDraft, EDocument);
+ Assert.AreEqual(1, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'A Sub Total Mismatch notification must exist before deleting the line.');
+ VerifySubTotalMismatchNotificationShown();
+
+ // [WHEN] Evaluating the mismatch for "L2" as deleted
+ EDocumentNotification.RefreshAndShowSubTotalMismatchAfterLineDeletion(EDocumentPurchaseLine);
+
+ // [THEN] The remaining line subtotal of 1000 matches the header and the notification is removed
+ Assert.AreEqual(0, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'Deleting the diverging line must remove the notification.');
+
+ // [THEN] The extracted header totals remain unchanged
+ VerifyHeaderTotals(EDocumentPurchaseHeader, 1000, 1000);
+ EDocumentPurchaseDraft.Close();
+ end;
+
+ [Test]
+ [HandlerFunctions('SendNotificationHandler')]
+ procedure DismissVendorMatchNotificationKeepsRowMarkedDismissed()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentNotification: Codeunit "E-Document Notification";
+ DismissNotification: Notification;
+ begin
+ // [FEATURE] [AI test]
+ // [SCENARIO] Dismissing the Vendor Matched By Name Not Address notification keeps the row and marks it as dismissed
+ Initialize();
+
+ // [GIVEN] An inbound e-document "E" with a Vendor Matched By Name Not Address notification
+ CreatePurchaseDraft(EDocument, EDocumentPurchaseHeader, 1000);
+ EDocumentNotification.AddVendorMatchedByNameNotAddressNotification(EDocument."Entry No");
+
+ // [GIVEN] The notification is sent for "E"
+ ExpectVendorMatchNotification(EDocument."Entry No");
+ EDocumentNotification.SendPurchaseDocumentDraftNotifications(EDocument."Entry No");
+ VerifyNotificationSentCount(1);
+
+ // [WHEN] The Dismiss action runs
+ DismissNotification := BuildVendorMatchNotification(EDocument."Entry No");
+ EDocumentNotification.DismissVendorMatchedByNameNotAddressNotification(DismissNotification);
+
+ // [THEN] The row is kept and marked as dismissed
+ Assert.AreEqual(1, CountVendorMatchNotificationRows(EDocument."Entry No"), 'Dismissing must keep the persisted notification row.');
+ Assert.IsTrue(GetVendorMatchDismissed(EDocument."Entry No"), 'Dismissing must mark the notification as dismissed.');
+ end;
+
+ [Test]
+ [HandlerFunctions('SendNotificationHandler')]
+ procedure DismissedVendorMatchNotificationIsNotSentAgain()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentNotification: Codeunit "E-Document Notification";
+ DismissNotification: Notification;
+ begin
+ // [FEATURE] [AI test]
+ // [SCENARIO] A dismissed Vendor Matched By Name Not Address notification is not sent again when the draft notifications are sent
+ Initialize();
+
+ // [GIVEN] An inbound e-document "E" with a Vendor Matched By Name Not Address notification that was sent once
+ CreatePurchaseDraft(EDocument, EDocumentPurchaseHeader, 1000);
+ EDocumentNotification.AddVendorMatchedByNameNotAddressNotification(EDocument."Entry No");
+ ExpectVendorMatchNotification(EDocument."Entry No");
+ EDocumentNotification.SendPurchaseDocumentDraftNotifications(EDocument."Entry No");
+ VerifyNotificationSentCount(1);
+
+ // [GIVEN] The user dismissed the notification
+ DismissNotification := BuildVendorMatchNotification(EDocument."Entry No");
+ EDocumentNotification.DismissVendorMatchedByNameNotAddressNotification(DismissNotification);
+
+ // [WHEN] Sending the purchase draft notifications for "E" again
+ EDocumentNotification.SendPurchaseDocumentDraftNotifications(EDocument."Entry No");
+
+ // [THEN] The notification is not sent a second time
+ VerifyNotificationSentCount(1);
+ end;
+
+ [Test]
+ procedure RefreshSubTotalMismatchPersistsWithoutShowingNotification()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ EDocumentNotification: Codeunit "E-Document Notification";
+ begin
+ // [FEATURE] [AI test]
+ // [SCENARIO] Refreshing the Sub Total mismatch state persists the notification without showing it
+ Initialize();
+
+ // [GIVEN] An inbound e-document "E" with header Sub Total 1000 and a single line of 500
+ CreatePurchaseDraft(EDocument, EDocumentPurchaseHeader, 1000);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 500);
+
+ // [WHEN] Refreshing the Sub Total mismatch state, with no notification handler registered
+ EDocumentNotification.RefreshSubTotalMismatch(EDocumentPurchaseHeader);
+
+ // [THEN] The notification is persisted, and nothing was shown - a shown notification would fail this test as unhandled
+ Assert.AreEqual(1, CountSubTotalMismatchNotifications(EDocument."Entry No"), 'Refreshing must persist the Sub Total Mismatch notification.');
+ end;
+
+ [Test]
+ [HandlerFunctions('SendNotificationHandler')]
+ procedure OnlyHeaderEditRefreshReArmsDismissedNotification()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ EDocumentNotification: Codeunit "E-Document Notification";
+ begin
+ // [FEATURE] [AI test]
+ // [SCENARIO] A plain refresh keeps a dismissal, while a refresh after a header edit re-arms it
+ Initialize();
+
+ // [GIVEN] An inbound e-document "E" whose header Sub Total 1000 does not match its single line of 500
+ CreatePurchaseDraft(EDocument, EDocumentPurchaseHeader, 1000);
+ CreatePurchaseLine(EDocumentPurchaseLine, EDocument, 1, 500);
+
+ // [GIVEN] The user dismissed the Sub Total Mismatch notification for "E"
+ SetSubTotalMismatchDismissed(EDocument."Entry No", true);
+
+ // [WHEN] Refreshing the state without an amount edit
+ EDocumentNotification.RefreshSubTotalMismatch(EDocumentPurchaseHeader);
+
+ // [THEN] The notification stays dismissed
+ Assert.IsTrue(GetSubTotalMismatchDismissed(EDocument."Entry No"), 'A plain refresh must not re-arm a dismissed notification.');
+
+ // [WHEN] Refreshing after a header amount edit
+ ExpectSubTotalMismatchNotification(EDocument."Entry No");
+ EDocumentNotification.RefreshAndShowSubTotalMismatchAfterHeaderEdit(EDocumentPurchaseHeader);
+
+ // [THEN] The notification is re-armed and shown again
+ Assert.IsFalse(GetSubTotalMismatchDismissed(EDocument."Entry No"), 'A refresh after a header edit must re-arm a dismissed notification.');
+ VerifySubTotalMismatchNotificationShown();
+ end;
+
+ local procedure Initialize()
+ var
+ EDocument: Record "E-Document";
+ EDocumentServiceStatus: Record "E-Document Service Status";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ EDocumentNotificationRec: Record "E-Document Notification";
+ begin
+ Clear(ExpectedNotificationId);
+ ExpectedNotificationEntryNo := 0;
+ SentNotificationCount := 0;
+
+ EDocumentNotificationRec.SetRange("User Id", UserId());
+ EDocumentNotificationRec.DeleteAll();
+ EDocumentPurchaseLine.DeleteAll();
+ EDocumentPurchaseHeader.DeleteAll();
+ EDocumentServiceStatus.DeleteAll();
+ EDocument.DeleteAll();
+ end;
+
+ local procedure CreatePurchaseDraft(var EDocument: Record "E-Document"; var EDocumentPurchaseHeader: Record "E-Document Purchase Header"; HeaderSubTotal: Decimal)
+ var
+ Vendor: Record Vendor;
+ begin
+ LibraryEDoc.SetupStandardVAT();
+ LibraryEDoc.SetupStandardPurchaseScenario(Vendor, EDocumentService, Enum::"E-Document Format"::Mock, Enum::"Service Integration"::Mock, Enum::"E-Document Import Process"::"Version 2.0");
+ LibraryEDoc.CreateInboundEDocument(EDocument, EDocumentService);
+
+ EDocumentPurchaseHeader := LibraryEDoc.MockPurchaseDraftPrepared(EDocument);
+ EDocumentPurchaseHeader."Sub Total" := HeaderSubTotal;
+ EDocumentPurchaseHeader."Total VAT" := 0;
+ EDocumentPurchaseHeader.Total := HeaderSubTotal;
+ EDocumentPurchaseHeader.Modify();
+ end;
+
+ local procedure CreatePurchaseLine(var EDocumentPurchaseLine: Record "E-Document Purchase Line"; EDocument: Record "E-Document"; Quantity: Decimal; UnitPrice: Decimal)
+ begin
+ EDocumentPurchaseLine := LibraryEDoc.InsertPurchaseDraftLine(EDocument);
+ EDocumentPurchaseLine.Description := 'Totals test line';
+ EDocumentPurchaseLine.Quantity := Quantity;
+ EDocumentPurchaseLine."Unit Price" := UnitPrice;
+ EDocumentPurchaseLine.Modify();
+ end;
+
+ local procedure OpenPurchaseDraft(var EDocumentPurchaseDraft: TestPage "E-Document Purchase Draft"; EDocument: Record "E-Document")
+ begin
+ // The page must be opened on the e-document so that OnOpenPage, which re-evaluates the
+ // Sub Total mismatch, runs with the record instead of an initialized one.
+ EDocumentPurchaseDraft.Trap();
+ Page.Run(Page::"E-Document Purchase Draft", EDocument);
+ EDocumentPurchaseDraft.Lines.First();
+ end;
+
+ local procedure CreateCurrencyWithRoundingPrecision(RoundingPrecision: Decimal): Code[10]
+ var
+ Currency: Record Currency;
+ begin
+ // The Code validation raises the base application missing exchange rates notification, which would reach the shared notification handler.
+ Currency.Init();
+ Currency.Code := CopyStr(LibraryUtility.GenerateRandomCode(Currency.FieldNo(Code), Database::Currency), 1, MaxStrLen(Currency.Code));
+ Currency."Amount Rounding Precision" := RoundingPrecision;
+ Currency."Unit-Amount Rounding Precision" := RoundingPrecision;
+ Currency.Insert();
+ exit(Currency.Code);
+ end;
+
+ local procedure SetHeaderCurrencyCode(var EDocumentPurchaseHeader: Record "E-Document Purchase Header"; CurrencyCode: Code[10])
+ begin
+ EDocumentPurchaseHeader."Currency Code" := CurrencyCode;
+ EDocumentPurchaseHeader.Modify();
+ end;
+
+ local procedure CountSubTotalMismatchNotifications(EDocumentEntryNo: Integer): Integer
+ var
+ EDocumentNotificationRec: Record "E-Document Notification";
+ begin
+ EDocumentNotificationRec.SetRange("E-Document Entry No.", EDocumentEntryNo);
+ EDocumentNotificationRec.SetRange(Type, "E-Document Notification Type"::"Sub Total Mismatch");
+ EDocumentNotificationRec.SetRange("User Id", UserId());
+ EDocumentNotificationRec.SetRange(Dismissed, false);
+ exit(EDocumentNotificationRec.Count());
+ end;
+
+ local procedure VerifyHeaderTotals(var EDocumentPurchaseHeader: Record "E-Document Purchase Header"; ExpectedSubTotal: Decimal; ExpectedTotal: Decimal)
+ begin
+ EDocumentPurchaseHeader.Get(EDocumentPurchaseHeader."E-Document Entry No.");
+ Assert.AreEqual(ExpectedSubTotal, EDocumentPurchaseHeader."Sub Total", 'Header Sub Total must not be changed by line edits.');
+ Assert.AreEqual(ExpectedTotal, EDocumentPurchaseHeader.Total, 'Header Total must not be changed by line edits.');
+ end;
+
+ local procedure GetSubTotalMismatchDismissed(EDocumentEntryNo: Integer): Boolean
+ var
+ EDocumentNotificationRec: Record "E-Document Notification";
+ begin
+ if EDocumentNotificationRec.Get(EDocumentEntryNo, SubTotalMismatchNotificationId(), UserId()) then
+ exit(EDocumentNotificationRec.Dismissed);
+ exit(false);
+ end;
+
+ local procedure SetSubTotalMismatchDismissed(EDocumentEntryNo: Integer; Dismissed: Boolean)
+ var
+ EDocumentNotificationRec: Record "E-Document Notification";
+ begin
+ if not EDocumentNotificationRec.Get(EDocumentEntryNo, SubTotalMismatchNotificationId(), UserId()) then begin
+ EDocumentNotificationRec.Init();
+ EDocumentNotificationRec."E-Document Entry No." := EDocumentEntryNo;
+ EDocumentNotificationRec.ID := SubTotalMismatchNotificationId();
+ EDocumentNotificationRec."User Id" := UserId();
+ EDocumentNotificationRec.Type := "E-Document Notification Type"::"Sub Total Mismatch";
+ EDocumentNotificationRec.Insert();
+ end;
+ EDocumentNotificationRec.Dismissed := Dismissed;
+ EDocumentNotificationRec.Modify();
+ end;
+
+ local procedure SubTotalMismatchNotificationId(): Guid
+ begin
+ exit('a1e6c0d2-3b4f-4c8a-9d1e-2f7b6a5c4d3e');
+ end;
+
+ local procedure BuildSubTotalMismatchNotification(EDocumentEntryNo: Integer) Notification: Notification
+ var
+ EDocumentNotificationRec: Record "E-Document Notification";
+ begin
+ Notification.SetData(EDocumentNotificationRec.FieldName("E-Document Entry No."), Format(EDocumentEntryNo));
+ Notification.SetData(EDocumentNotificationRec.FieldName(ID), SubTotalMismatchNotificationId());
+ end;
+
+ local procedure ExpectSubTotalMismatchNotification(EDocumentEntryNo: Integer)
+ begin
+ ExpectedNotificationId := SubTotalMismatchNotificationId();
+ ExpectedNotificationEntryNo := EDocumentEntryNo;
+ SentNotificationCount := 0;
+ end;
+
+ local procedure VerifySubTotalMismatchNotificationShown()
+ begin
+ Assert.IsTrue(SentNotificationCount > 0, 'The Sub Total Mismatch notification must be shown.');
+ end;
+
+ local procedure VerifyNotificationSentCount(ExpectedCount: Integer)
+ begin
+ Assert.AreEqual(ExpectedCount, SentNotificationCount, 'The notification was sent an unexpected number of times.');
+ end;
+
+ local procedure VendorMatchNotificationId(): Guid
+ begin
+ exit('bc0d8537-8e8d-4d94-a07a-a5a54c729d2a');
+ end;
+
+ local procedure CountVendorMatchNotificationRows(EDocumentEntryNo: Integer): Integer
+ var
+ EDocumentNotificationRec: Record "E-Document Notification";
+ begin
+ EDocumentNotificationRec.SetRange("E-Document Entry No.", EDocumentEntryNo);
+ EDocumentNotificationRec.SetRange(Type, "E-Document Notification Type"::"Vendor Matched By Name Not Address");
+ EDocumentNotificationRec.SetRange("User Id", UserId());
+ exit(EDocumentNotificationRec.Count());
+ end;
+
+ local procedure GetVendorMatchDismissed(EDocumentEntryNo: Integer): Boolean
+ var
+ EDocumentNotificationRec: Record "E-Document Notification";
+ begin
+ if EDocumentNotificationRec.Get(EDocumentEntryNo, VendorMatchNotificationId(), UserId()) then
+ exit(EDocumentNotificationRec.Dismissed);
+ exit(false);
+ end;
+
+ local procedure BuildVendorMatchNotification(EDocumentEntryNo: Integer) Notification: Notification
+ var
+ EDocumentNotificationRec: Record "E-Document Notification";
+ begin
+ Notification.SetData(EDocumentNotificationRec.FieldName("E-Document Entry No."), Format(EDocumentEntryNo));
+ Notification.SetData(EDocumentNotificationRec.FieldName(ID), VendorMatchNotificationId());
+ end;
+
+ local procedure ExpectVendorMatchNotification(EDocumentEntryNo: Integer)
+ begin
+ ExpectedNotificationId := VendorMatchNotificationId();
+ ExpectedNotificationEntryNo := EDocumentEntryNo;
+ SentNotificationCount := 0;
+ end;
+
+ [SendNotificationHandler]
+ procedure SendNotificationHandler(var Notification: Notification): Boolean
+ var
+ EDocumentNotificationRec: Record "E-Document Notification";
+ ActualEntryNo: Integer;
+ begin
+ SentNotificationCount += 1;
+ Assert.AreEqual(ExpectedNotificationId, Notification.Id, 'An unexpected notification was shown.');
+ Assert.AreEqual(Format(ExpectedNotificationId), Notification.GetData(EDocumentNotificationRec.FieldName(ID)), 'The notification carries an unexpected ID.');
+ Evaluate(ActualEntryNo, Notification.GetData(EDocumentNotificationRec.FieldName("E-Document Entry No.")));
+ Assert.AreEqual(ExpectedNotificationEntryNo, ActualEntryNo, 'The notification was shown for an unexpected e-document.');
+ exit(true);
+ end;
+}