diff --git a/src/Apps/W1/EDocumentConnectors/Microsoft365/app/src/OutlookProcessing.Codeunit.al b/src/Apps/W1/EDocumentConnectors/Microsoft365/app/src/OutlookProcessing.Codeunit.al index 47476c3cdd..1a5f7dbb5f 100644 --- a/src/Apps/W1/EDocumentConnectors/Microsoft365/app/src/OutlookProcessing.Codeunit.al +++ b/src/Apps/W1/EDocumentConnectors/Microsoft365/app/src/OutlookProcessing.Codeunit.al @@ -94,6 +94,7 @@ codeunit 6385 "Outlook Processing" FolderConfigured := OutlookSetup."Email Folder Id" <> ''; TempFilters."Load Attachments" := true; + TempFilters."Load Headers" := true; TempFilters."Max No. of Emails" := GetMaxNoOfEmails(); // In folder mode, dedup is enforced server-side by the category-exclude filter only. // Applying "Earliest Email" would drop emails moved into the folder after that floor. diff --git a/src/Apps/W1/PayablesAgent/app/Integration/PAKnownSender.Table.al b/src/Apps/W1/PayablesAgent/app/Integration/PAKnownSender.Table.al index 763495105b..ff3daa3c59 100644 --- a/src/Apps/W1/PayablesAgent/app/Integration/PAKnownSender.Table.al +++ b/src/Apps/W1/PayablesAgent/app/Integration/PAKnownSender.Table.al @@ -27,6 +27,12 @@ table 3308 "PA Known Sender" Caption = 'Email'; ToolTip = 'Specifies the email address of a sender whose e-documents have been processed by the Payables Agent.'; } + field(3; "Sender Policy"; Enum "PA Sender Policy") + { + DataClassification = CustomerContent; + Caption = 'Review policy'; + ToolTip = 'Specifies how an incoming email from this sender is handled. ''Ask'' requests human review. ''Approve'' processes without review (effective when overall email review is ''Only if untrusted''). ''Reject'' ignores the email.'; + } } keys { @@ -39,9 +45,23 @@ table 3308 "PA Known Sender" } } + trigger OnInsert() + begin + LogSenderAdded(); + if Rec."Sender Policy" = Rec."Sender Policy"::Approve then + LogSenderApproved(); + end; + + trigger OnModify() + begin + if (Rec."Sender Policy" = Rec."Sender Policy"::Approve) and (xRec."Sender Policy" <> Rec."Sender Policy"::Approve) then + LogSenderApproved(); + end; + internal procedure InsertIfNew(EDocument: Record "E-Document") var Existing: Record "PA Known Sender"; + Setup: Record "Payables Agent Setup"; SenderEmail: Text[250]; begin SenderEmail := CopyStr(EDocument."Source Details", 1, MaxStrLen(SenderEmail)); @@ -52,7 +72,42 @@ table 3308 "PA Known Sender" exit; Rec.Init(); Rec.Email := SenderEmail; - Rec.Insert(); - Session.LogSecurityAudit(Rec.TableName(), SecurityOperationResult::Success, 'Added new known sender.', AuditCategory::PolicyManagement); + Setup.GetSetup(); + if Setup."Email Review Policy" = "PA Email Review Policy"::OnlyIfUntrusted then + Rec."Sender Policy" := "PA Sender Policy"::Approve + else + Rec."Sender Policy" := "PA Sender Policy"::Ask; + Rec.Insert(true); + end; + + /// + /// Finds the known-sender record matching the e-document's sender, if any. + /// Returns false when the sender is empty or not in the list. + /// + internal procedure GetForEDocument(EDocument: Record "E-Document"; var KnownSender: Record "PA Known Sender"): Boolean + var + SenderEmail: Text[250]; + begin + SenderEmail := CopyStr(EDocument."Source Details", 1, MaxStrLen(KnownSender.Email)); + if SenderEmail = '' then + exit(false); + KnownSender.SetRange(Email, SenderEmail); + exit(KnownSender.FindFirst()); + end; + + // Logs every path that adds a known sender, and separately every path that results in a sender + // marked Approve (which lets its emails be processed without human review). + local procedure LogSenderAdded() + begin + Session.LogSecurityAudit(Rec.TableName(), SecurityOperationResult::Success, StrSubstNo(SenderAddedAuditLbl, Rec.Email), AuditCategory::PolicyManagement); end; + + local procedure LogSenderApproved() + begin + Session.LogSecurityAudit(Rec.TableName(), SecurityOperationResult::Success, StrSubstNo(SenderApprovedAuditLbl, Rec.Email), AuditCategory::PolicyManagement); + end; + + var + SenderAddedAuditLbl: Label 'Added Payables Agent known sender %1.', Locked = true; + SenderApprovedAuditLbl: Label 'Payables Agent known sender %1 was set to Approve, allowing its emails to be processed without review.', Locked = true; } diff --git a/src/Apps/W1/PayablesAgent/app/Integration/PAKnownSenders.Page.al b/src/Apps/W1/PayablesAgent/app/Integration/PAKnownSenders.Page.al index 0ba867b6a5..99719b430f 100644 --- a/src/Apps/W1/PayablesAgent/app/Integration/PAKnownSenders.Page.al +++ b/src/Apps/W1/PayablesAgent/app/Integration/PAKnownSenders.Page.al @@ -5,6 +5,8 @@ namespace Microsoft.Agent.PayablesAgent; +using Microsoft.EServices.EDocumentConnector.Microsoft365; + page 3313 "PA Known Senders" { Caption = 'Payables Agent Known Senders', Comment = 'Payables Agent is a term, and should not be translated.'; @@ -24,7 +26,42 @@ page 3313 "PA Known Senders" ApplicationArea = All; ToolTip = 'Specifies the email address of a sender whose e-documents have been processed by the Payables Agent.'; } + field("Sender Policy"; Rec."Sender Policy") + { + ApplicationArea = All; + ToolTip = 'Specifies how an incoming email from this sender is handled. ''Ask'' requests human review. ''Approve'' processes without review (effective when overall email review is ''Only if untrusted''). ''Reject'' ignores the email.'; + } } } } + + trigger OnOpenPage() + var + SavedSetup: Record "Payables Agent Setup"; + OutlookSetup: Record "Outlook Setup"; + PayablesAgentSetup: Codeunit "Payables Agent Setup"; + Impact: Enum "PA Setup Change Impact"; + UnusedNotification: Notification; + SavedFolder: Text; + begin + SavedSetup.GetSetup(); + if OutlookSetup.FindFirst() then + SavedFolder := OutlookSetup."Email Folder"; + + Impact := PayablesAgentSetup.ClassifyKnownSendersUnusedReason(SavedSetup."Email Review Policy", SavedFolder); + case Impact of + Impact::KnownSendersIgnoredByFolder: + UnusedNotification.Message := StrSubstNo(UnusedByFolderLbl, SavedFolder); + Impact::KnownSendersIgnoredByPolicy: + UnusedNotification.Message := StrSubstNo(UnusedByPolicyLbl, PayablesAgentSetup.PolicyLabel(SavedSetup."Email Review Policy")); + else + exit; + end; + UnusedNotification.Scope := NotificationScope::LocalScope; + UnusedNotification.Send(); + end; + + var + UnusedByFolderLbl: Label 'This list is currently unused. Emails arrive through subfolder ''%1'' and are processed without consulting it.', Comment = '%1 = folder name'; + UnusedByPolicyLbl: Label 'This list doesn''t affect processing while review policy is set to ''%1''.', Comment = '%1 = review policy'; } diff --git a/src/Apps/W1/PayablesAgent/app/Integration/PASenderPolicy.Enum.al b/src/Apps/W1/PayablesAgent/app/Integration/PASenderPolicy.Enum.al new file mode 100644 index 0000000000..52dcc3df7e --- /dev/null +++ b/src/Apps/W1/PayablesAgent/app/Integration/PASenderPolicy.Enum.al @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace Microsoft.Agent.PayablesAgent; + +enum 3307 "PA Sender Policy" +{ + Access = Internal; + Extensible = false; + + value(0; Ask) + { + Caption = 'Ask'; + } + value(1; Approve) + { + Caption = 'Approve'; + } + value(2; Reject) + { + Caption = 'Reject'; + } +} diff --git a/src/Apps/W1/PayablesAgent/app/PayablesAgent.Codeunit.al b/src/Apps/W1/PayablesAgent/app/PayablesAgent.Codeunit.al index c1bc8cfc03..7ef68a0bb8 100644 --- a/src/Apps/W1/PayablesAgent/app/PayablesAgent.Codeunit.al +++ b/src/Apps/W1/PayablesAgent/app/PayablesAgent.Codeunit.al @@ -176,11 +176,19 @@ codeunit 3303 "Payables Agent" implements IAgentMetadata, IAgentFactory if Agent.State = Agent.State::Disabled then exit; - BuildAgentTask(EDocument, Agent); - PayablesAgentSetupRec.GetSetup(); CustomDimensions.Set('Category', PayablesAgentSetup.FeatureName()); CustomDimensions.Set('SystemId', EDocImpSessionTelemetry.CreateSystemIdText(EDocument.SystemId)); + CustomDimensions.Set('EmailReviewPolicy', Format(PayablesAgentSetupRec."Email Review Policy", 0, 9)); + + // A sender explicitly set to Reject is a blocklist: no agent task is created. + if PayablesAgentSetup.IsSenderRejected(EDocument) then begin + Telemetry.LogMessage('0000QJ2', 'Payables Agent Task Skipped: sender rejected', Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::All, CustomDimensions); + exit; + end; + + BuildAgentTask(EDocument, Agent); + CustomDimensions.Set('ReviewIncomingInvoice', Format(PayablesAgentSetupRec."Review Incoming Invoice", 0, 9)); Telemetry.LogMessage('0000PJA', 'Payables Agent Task Received', Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::All, CustomDimensions); PayablesAgentKPI.InsertKPIEntry("PA KPI Scenario"::"Agent Tasks Received"); @@ -190,6 +198,7 @@ codeunit 3303 "Payables Agent" implements IAgentMetadata, IAgentFactory var PayablesAgentSetup: Record "Payables Agent Setup"; PayablesAgent: Codeunit "Payables Agent"; + PASetup: Codeunit "Payables Agent Setup"; AgentTaskBuilder: Codeunit "Agent Task Builder"; AgentTaskMessageBuilder: Codeunit "Agent Task Message Builder"; PATrial: Codeunit "PA Trial"; @@ -203,7 +212,7 @@ codeunit 3303 "Payables Agent" implements IAgentMetadata, IAgentFactory TrialModeTok: Label 'Agent Task created in trial mode. Skipping billing for invoice.', Locked = true; begin PayablesAgentSetup.GetSetup(); - MustRequestReviewOfMessage := PayablesAgentSetup."Review Incoming Invoice"; + MustRequestReviewOfMessage := PASetup.ShouldRequestReview(EDocument); Message := StrSubstNo(MessageLbl, EDocument."Entry No"); AgentTaskTitle := CopyStr(StrSubstNo(TaskTitleLbl, LowerCase(EDocument."Source Details")), 1, MaxStrLen(AgentTaskTitle)); diff --git a/src/Apps/W1/PayablesAgent/app/PayablesAgentUpgrade.Codeunit.al b/src/Apps/W1/PayablesAgent/app/PayablesAgentUpgrade.Codeunit.al index 221535800b..9f6704a57a 100644 --- a/src/Apps/W1/PayablesAgent/app/PayablesAgentUpgrade.Codeunit.al +++ b/src/Apps/W1/PayablesAgent/app/PayablesAgentUpgrade.Codeunit.al @@ -26,6 +26,7 @@ codeunit 3305 "Payables Agent Upgrade" begin AlwaysUpdateAgentInformationOnUpgrade(); UpdatePayablesAgentSetupToUseUserSecurityId(); + ReviewIncomingInvoiceToEmailReviewPolicy(); end; trigger OnUpgradePerDatabase() @@ -107,6 +108,25 @@ codeunit 3305 "Payables Agent Upgrade" end; end; + local procedure ReviewIncomingInvoiceToEmailReviewPolicy() + var + PayablesAgentSetup: Record "Payables Agent Setup"; + begin + if UpgradeTag.HasUpgradeTag(GetMapEmailReviewPolicyTag()) then + exit; + + if PayablesAgentSetup.FindFirst() then + if PayablesAgentSetup."Email Review Policy" = "PA Email Review Policy"::Unset then begin + if PayablesAgentSetup."Review Incoming Invoice" then + PayablesAgentSetup."Email Review Policy" := "PA Email Review Policy"::Always + else + PayablesAgentSetup."Email Review Policy" := "PA Email Review Policy"::OnlyIfUntrusted; + PayablesAgentSetup.Modify(); + end; + + UpgradeTag.SetUpgradeTag(GetMapEmailReviewPolicyTag()); + end; + local procedure GetRegisterPayablesAgentCapabilityTag(): Code[250] begin exit('MS-575373-PayablesAgentCapability-20251021'); @@ -127,6 +147,11 @@ codeunit 3305 "Payables Agent Upgrade" exit('MS-631300-MarkTrialEndedIfPayablesAgentExists-20260417'); end; + local procedure GetMapEmailReviewPolicyTag(): Code[250] + begin + exit('MS-625413-MapPayablesAgentEmailReviewPolicy-20260703'); + end; + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Upgrade Tag", OnGetPerDatabaseUpgradeTags, '', false, false)] local procedure RegisterPerDatabaseUpgradeTags(var PerDatabaseUpgradeTags: List of [Code[250]]) begin @@ -138,6 +163,7 @@ codeunit 3305 "Payables Agent Upgrade" local procedure RegisterPerCompanyUpgradeTags(var PerCompanyUpgradeTags: List of [Code[250]]) begin PerCompanyUpgradeTags.Add(GetUpdatePayablesAgentSetupToUseUserSecurityIdTag()); + PerCompanyUpgradeTags.Add(GetMapEmailReviewPolicyTag()); end; } \ No newline at end of file diff --git a/src/Apps/W1/PayablesAgent/app/Setup/PAEmailReviewPolicy.Enum.al b/src/Apps/W1/PayablesAgent/app/Setup/PAEmailReviewPolicy.Enum.al new file mode 100644 index 0000000000..e4622d0ead --- /dev/null +++ b/src/Apps/W1/PayablesAgent/app/Setup/PAEmailReviewPolicy.Enum.al @@ -0,0 +1,29 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace Microsoft.Agent.PayablesAgent; + +enum 3305 "PA Email Review Policy" +{ + Access = Internal; + Extensible = false; + + value(0; Unset) + { + Caption = ' '; + } + value(1; OnlyIfUntrusted) + { + Caption = 'Only untrusted senders (recommended)'; + } + value(2; Never) + { + Caption = 'Never'; + } + value(3; Always) + { + Caption = 'Always'; + } +} diff --git a/src/Apps/W1/PayablesAgent/app/Setup/PASetupChangeImpact.Enum.al b/src/Apps/W1/PayablesAgent/app/Setup/PASetupChangeImpact.Enum.al new file mode 100644 index 0000000000..78b80c29d0 --- /dev/null +++ b/src/Apps/W1/PayablesAgent/app/Setup/PASetupChangeImpact.Enum.al @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace Microsoft.Agent.PayablesAgent; + +enum 3306 "PA Setup Change Impact" +{ + Access = Internal; + Extensible = false; + + value(0; None) { } + value(1; KnownSendersIgnoredByFolder) { } + value(2; KnownSendersIgnoredByPolicy) { } +} diff --git a/src/Apps/W1/PayablesAgent/app/Setup/PayablesAgentSetup.Codeunit.al b/src/Apps/W1/PayablesAgent/app/Setup/PayablesAgentSetup.Codeunit.al index f89c619451..9419eee230 100644 --- a/src/Apps/W1/PayablesAgent/app/Setup/PayablesAgentSetup.Codeunit.al +++ b/src/Apps/W1/PayablesAgent/app/Setup/PayablesAgentSetup.Codeunit.al @@ -32,7 +32,8 @@ codeunit 3307 "Payables Agent Setup" Permissions = tabledata "Outlook Setup" = rim, - tabledata "Payables Agent Setup" = rmid; + tabledata "Payables Agent Setup" = rmid, + tabledata "PA Known Sender" = r; /// /// Retrieves all the records containing setup information for the payables agent. @@ -94,12 +95,19 @@ codeunit 3307 "Payables Agent Setup" EmailConnectionMessageErr: Label 'Connection to mailbox failed. Please review the email account configuration for email %1', Comment = '%1 - Email account name'; EmailConnectionNavigationActionLbl: Label 'Show email accounts'; ActivateWithoutMailboxNameErr: Label 'To activate the agent with the current settings, a mailbox must be selected first.'; + ReviewPolicyRequiredErr: Label 'Select an Email review option before turning on monitoring. This sets when incoming emails need supervisor approval before the agent processes them.'; begin if AzureADGraphUser.IsUserDelegatedAdmin() or AzureADGraphUser.IsUserDelegatedHelpdesk() then Error(DelegatedAdminErr); Session.LogMessage('0000OUW', 'Setting up payables agent', Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::All, 'Category', PayablesAgentTelemetryTok); + // Require an explicit review policy before monitoring can start. + if (PASetupConfiguration.GetAgentSetupBuffer().State = PASetupConfiguration.GetAgentSetupBuffer().State::Enabled) and + (PASetupConfiguration.GetPayablesAgentSetup()."Monitor Outlook") and + (PASetupConfiguration.GetPayablesAgentSetup()."Email Review Policy" = "PA Email Review Policy"::Unset) then + Error(ReviewPolicyRequiredErr); + // If the agent is to be activated, we check if the privacy consent has been given for the email integration or trigger the consent flow // This has to happen before any write transactions since the consent runs modally (and will block the session) // Similarly we delay the insert/modify of OutlookSetup until we verify that the email connection is succesful (i.e. Codeunit.Run completes succesfully, this forces to have no open write transactions) @@ -535,6 +543,174 @@ codeunit 3307 "Payables Agent Setup" EDocImport.ProcessAutomaticallyIncomingEDocument(EDocument); end; + /// + /// Decides whether an incoming e-document's agent task must be reviewed by a human, based on the + /// configured review policy, the monitored folder, sender authentication (compauth / internal), + /// and the known-senders list. + /// + procedure ShouldRequestReview(EDocument: Record "E-Document"): Boolean + var + PayablesAgentSetup: Record "Payables Agent Setup"; + KnownSender: Record "PA Known Sender"; + begin + // Only incoming emails are subject to email review. Manually uploaded documents + // (e.g. the trial experience) are user-initiated and processed without review. + if EDocument."Outlook Mail Message Id" = '' then + exit(false); + + PayablesAgentSetup.GetSetup(); + case PayablesAgentSetup."Email Review Policy" of + "PA Email Review Policy"::Always, + "PA Email Review Policy"::Unset: + exit(true); + "PA Email Review Policy"::Never: + exit(false); + end; + + // OnlyIfUntrusted: a configured subfolder is explicit consent, everything in it is trusted. + if MonitoredFolderConfigured() then + exit(false); + + // Without an authenticated sender we always review. + if not IsSenderAuthenticated(EDocument) then + exit(true); + + // Authenticated: trust senders explicitly approved in the known-senders list. + if KnownSender.GetForEDocument(EDocument, KnownSender) then + exit(KnownSender."Sender Policy" <> "PA Sender Policy"::Approve); + + // Authenticated and unknown: trust internal (same-organization) senders. + if IsSenderInternal(EDocument) then + exit(false); + + // Authenticated, unknown and external: review. + exit(true); + end; + + /// + /// Returns true when the sender of the e-document is configured with the Reject policy, + /// meaning no agent task should be created for it. + /// + procedure IsSenderRejected(EDocument: Record "E-Document"): Boolean + var + KnownSender: Record "PA Known Sender"; + begin + if KnownSender.GetForEDocument(EDocument, KnownSender) then + exit(KnownSender."Sender Policy" = "PA Sender Policy"::Reject); + exit(false); + end; + + local procedure MonitoredFolderConfigured(): Boolean + var + OutlookSetup: Record "Outlook Setup"; + begin + if not OutlookSetup.FindFirst() then + exit(false); + exit(OutlookSetup."Email Folder" <> ''); + end; + + /// + /// Returns true when the sender of the e-document's source email can be trusted as authenticated: + /// either composite authentication passed (compauth=pass), or the message originated inside the + /// organization (see IsSenderInternal). Missing email or headers yields false. + /// + procedure IsSenderAuthenticated(EDocument: Record "E-Document"): Boolean + var + HeaderValue: Text; + begin + if TryGetSourceEmailHeader(EDocument, 'Authentication-Results', HeaderValue) then + if CompAuthPassed(HeaderValue) then + exit(true); + exit(IsSenderInternal(EDocument)); + end; + + /// + /// Returns true when the e-document's source email was stamped by Exchange as originating inside the + /// organization (X-MS-Exchange-Organization-AuthAs = Internal). This covers intra-tenant mail (e.g. + /// same onmicrosoft.com domain), which is not stamped with compauth. Exchange re-stamps this header + /// on inbound, so it cannot be spoofed by an external sender. Missing email or header yields false. + /// + procedure IsSenderInternal(EDocument: Record "E-Document"): Boolean + var + HeaderValue: Text; + begin + if TryGetSourceEmailHeader(EDocument, 'X-MS-Exchange-Organization-AuthAs', HeaderValue) then + exit(LowerCase(HeaderValue).Trim() = 'internal'); + exit(false); + end; + + local procedure TryGetSourceEmailHeader(EDocument: Record "E-Document"; HeaderName: Text; var HeaderValue: Text): Boolean + var + EmailMessage: Codeunit "Email Message"; + begin + if IsNullGuid(EDocument."Mail Message Id") then + exit(false); + if not EmailMessage.Get(EDocument."Mail Message Id") then + exit(false); + exit(EmailMessage.GetHeader(HeaderName, HeaderValue)); + end; + + /// + /// Returns true when an Authentication-Results header value indicates compauth=pass. + /// + procedure CompAuthPassed(AuthenticationResults: Text): Boolean + begin + // Case-insensitive match; tolerates surrounding tokens and a trailing reason=NNN. + exit(StrPos(LowerCase(AuthenticationResults), 'compauth=pass') > 0); + end; + + /// + /// Classifies whether a pending setup change would leave the known-senders list unused. + /// Returns None when nothing curated would be ignored (including when the list is empty). + /// + procedure ClassifyKnownSendersUnusedByChange(NewPolicy: Enum "PA Email Review Policy"; NewMonitoredFolder: Text; var KnownSendersCount: Integer): Enum "PA Setup Change Impact" + var + KnownSender: Record "PA Known Sender"; + begin + KnownSendersCount := KnownSender.Count(); + if KnownSendersCount = 0 then + exit("PA Setup Change Impact"::None); + exit(ClassifyByPolicyAndFolder(NewPolicy, NewMonitoredFolder)); + end; + + /// + /// Classifies why the known-senders list is currently unused in a saved setup, regardless of + /// whether the list has entries. Used by the Known Senders page to surface a notification. + /// + procedure ClassifyKnownSendersUnusedReason(SavedPolicy: Enum "PA Email Review Policy"; SavedMonitoredFolder: Text): Enum "PA Setup Change Impact" + begin + exit(ClassifyByPolicyAndFolder(SavedPolicy, SavedMonitoredFolder)); + end; + + local procedure ClassifyByPolicyAndFolder(Policy: Enum "PA Email Review Policy"; MonitoredFolder: Text): Enum "PA Setup Change Impact" + begin + if MonitoredFolder <> '' then + exit("PA Setup Change Impact"::KnownSendersIgnoredByFolder); + if Policy in [Policy::Always, Policy::Never] then + exit("PA Setup Change Impact"::KnownSendersIgnoredByPolicy); + exit("PA Setup Change Impact"::None); + end; + + /// + /// Returns the user-facing label for an "Email Review Policy" value, for interpolation into messages. + /// + procedure PolicyLabel(Policy: Enum "PA Email Review Policy"): Text + var + AlwaysLbl: Label 'Always'; + NeverLbl: Label 'Never'; + OnlyIfUntrustedLbl: Label 'Only if untrusted'; + begin + case Policy of + Policy::Always: + exit(AlwaysLbl); + Policy::Never: + exit(NeverLbl); + Policy::OnlyIfUntrusted: + exit(OnlyIfUntrustedLbl); + end; + exit(''); + end; + var AgentUserNameLbl: Label 'Payables Agent', Comment = 'User name of the agent.', Locked = true; AgentSummaryLbl: Label 'Monitors incoming emails for vendor invoices, matches senders to registered vendors, and creates purchase document drafts for review.'; diff --git a/src/Apps/W1/PayablesAgent/app/Setup/PayablesAgentSetup.Page.al b/src/Apps/W1/PayablesAgent/app/Setup/PayablesAgentSetup.Page.al index 6487e2dddd..02c8ae0aaa 100644 --- a/src/Apps/W1/PayablesAgent/app/Setup/PayablesAgentSetup.Page.al +++ b/src/Apps/W1/PayablesAgent/app/Setup/PayablesAgentSetup.Page.al @@ -236,7 +236,7 @@ page 3304 "Payables Agent Setup" field(MailboxFolder; TempOutlookSetup."Email Folder") { Caption = 'Folder'; - ToolTip = 'Specifies the email folder that the agent monitors. Leave blank to monitor the entire mailbox.'; + ToolTip = 'Specifies the email folder that the agent monitors. Leave blank to monitor the entire mailbox. When a folder is set and email review is ''Only if untrusted'', every email in it is treated as trusted and skips review, so the known-senders list is not consulted.'; Editable = false; trigger OnAssistEdit() @@ -309,14 +309,14 @@ page 3304 "Payables Agent Setup" Caption = 'Document processing'; group(ProcessNewTaskGroup) { - Caption = 'Review email'; - InstructionalText = 'The agent will request a review of the incoming email before creating the purchase document draft.'; + Caption = 'Email review'; + InstructionalText = 'Choose when the agent should request a human review before processing an incoming email.'; - field(ReviewEmail; Rec."Review Incoming Invoice") + field(ReviewEmailPolicy; Rec."Email Review Policy") { - ShowCaption = false; - Caption = 'Review incoming invoices'; - ToolTip = 'Specifies whether the agent should request a review before processing invoices.'; + Caption = 'Email review'; + ShowMandatory = true; + ToolTip = 'Specifies when the agent should request human review before processing an incoming email. ''Only if untrusted'' skips review for previously approved senders and any email that arrives in a configured subfolder.'; trigger OnValidate() begin @@ -324,6 +324,26 @@ page 3304 "Payables Agent Setup" CurrPage.Update(); end; } + field(ManageKnownSenders; ManageKnownSendersLbl) + { + ShowCaption = false; + StyleExpr = true; + Style = StandardAccent; + Editable = false; + ToolTip = 'Opens the list of senders the Payables Agent has previously processed.', Comment = 'Payables Agent is a term, and should not be translated.'; + + trigger OnDrillDown() + begin + Page.RunModal(Page::"PA Known Senders"); + end; + } + field(KnownSendersHint; KnownSendersHintLbl) + { + ShowCaption = false; + MultiLine = true; + Editable = false; + ToolTip = 'Explains how the known-senders list interacts with the monitored subfolder.'; + } } group(AdditionalFields) { @@ -446,10 +466,32 @@ page 3304 "Payables Agent Setup" if (CloseAction = CloseAction::Cancel) or (not SetupChanged) then exit(true); + if not ConfirmPendingChanges() then + exit(false); + ApplySetup(); exit(true); end; + local procedure ConfirmPendingChanges(): Boolean + var + Impact: Enum "PA Setup Change Impact"; + SendersCount: Integer; + begin + Impact := PayablesAgentSetup.ClassifyKnownSendersUnusedByChange(Rec."Email Review Policy", TempOutlookSetup."Email Folder", SendersCount); + if SendersCount = 0 then + exit(true); + + case Impact of + Impact::KnownSendersIgnoredByFolder: + exit(Confirm(FolderIgnoresListConfirmLbl, false, SendersCount, TempOutlookSetup."Email Folder")); + Impact::KnownSendersIgnoredByPolicy: + exit(Confirm(PolicyIgnoresListConfirmLbl, false, SendersCount, PayablesAgentSetup.PolicyLabel(Rec."Email Review Policy"))); + end; + + exit(true); + end; + local procedure ApplySetup() begin CurrPage.AgentSetupPart.Page.GetAgentSetupBuffer(TempAgentSetupBuffer); @@ -565,5 +607,9 @@ page 3304 "Payables Agent Setup" BenefitNoDisruptionLbl: Label '• No disruption to your current process'; SelectFileLbl: Label 'Select file'; PdfFileFilterLbl: Label 'PDF Files (*.pdf)|*.pdf'; + ManageKnownSendersLbl: Label 'Manage known senders'; + KnownSendersHintLbl: Label 'Add your regular senders and set the review policy for each. Approve emails automatically for senders you trust.'; + FolderIgnoresListConfirmLbl: Label 'You have %1 known senders. With subfolder ''%2'' configured, the agent will process every email there without consulting the list. The list is kept and will be used again if you remove the subfolder. Continue?', Comment = '%1 = number of known senders, %2 = folder name'; + PolicyIgnoresListConfirmLbl: Label 'You have %1 known senders. The list won''t affect processing while review is set to ''%2''. The list is kept and will be used again if you switch back to ''Only if untrusted''. Continue?', Comment = '%1 = number of known senders, %2 = review policy'; } \ No newline at end of file diff --git a/src/Apps/W1/PayablesAgent/app/Setup/PayablesAgentSetup.Table.al b/src/Apps/W1/PayablesAgent/app/Setup/PayablesAgentSetup.Table.al index 432dbbf41c..eb2d58f899 100644 --- a/src/Apps/W1/PayablesAgent/app/Setup/PayablesAgentSetup.Table.al +++ b/src/Apps/W1/PayablesAgent/app/Setup/PayablesAgentSetup.Table.al @@ -73,6 +73,11 @@ table 3303 "Payables Agent Setup" Caption = 'Use MLLM Processing'; DataClassification = SystemMetadata; } + field(12; "Email Review Policy"; Enum "PA Email Review Policy") + { + Caption = 'Email review'; + DataClassification = CustomerContent; + } } keys {