diff --git a/src/Apps/W1/ExpenseAgent/app/src/Common/Extensions/ExpenseSpendRequest.TableExt.al b/src/Apps/W1/ExpenseAgent/app/src/Common/Extensions/ExpenseSpendRequest.TableExt.al index 3fc56366082..b76de879706 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/Common/Extensions/ExpenseSpendRequest.TableExt.al +++ b/src/Apps/W1/ExpenseAgent/app/src/Common/Extensions/ExpenseSpendRequest.TableExt.al @@ -107,6 +107,13 @@ tableextension 6908 "Expense Spend Request" extends "Spend Request" TestStatusOpen(); end; } + field(6910; "Requested For Name"; Text[100]) + { + Caption = 'Requested For Name'; + FieldClass = FlowField; + CalcFormula = lookup("Expense User".Name where("No." = field("Requested For"))); + ToolTip = 'Specifies the name of the expense user for whom the spend request is being created.'; + } field(6911; "Actual Start Date and Time"; DateTime) { Caption = 'Actual Start Date and Time'; diff --git a/src/Apps/W1/ExpenseAgent/app/src/Setup/Codeunits/ExpenseEventSubscriber.Codeunit.al b/src/Apps/W1/ExpenseAgent/app/src/Setup/Codeunits/ExpenseEventSubscriber.Codeunit.al index 4bf92c33a99..a1a4832769b 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/Setup/Codeunits/ExpenseEventSubscriber.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/app/src/Setup/Codeunits/ExpenseEventSubscriber.Codeunit.al @@ -329,6 +329,9 @@ codeunit 6908 "Expense Event Subscriber" exit; SpendRequest.Status := SpendRequest.Status::Approved; + SpendRequest."Approved/Rejected At" := CurrentDateTime(); + SpendRequest."Approved/Rejected by User ID" := UserSecurityId(); + SpendRequest."Approved/Rejected by User Name" := CopyStr(UserId(), 1, MaxStrLen(SpendRequest."Approved/Rejected by User Name")); SpendRequest.Modify(); end; } \ No newline at end of file diff --git a/src/Apps/W1/ExpenseAgent/app/src/Travel Request/Pages/TravelRequestCard.Page.al b/src/Apps/W1/ExpenseAgent/app/src/Travel Request/Pages/TravelRequestCard.Page.al index eeb88cc66d0..e324a1273ac 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/Travel Request/Pages/TravelRequestCard.Page.al +++ b/src/Apps/W1/ExpenseAgent/app/src/Travel Request/Pages/TravelRequestCard.Page.al @@ -29,6 +29,7 @@ page 7129 "Travel Request Card" field("No."; Rec."No.") { ToolTip = 'Specifies the number of the travel request.'; + Editable = Rec."No." = ''; trigger OnAssistEdit() begin @@ -38,11 +39,18 @@ page 7129 "Travel Request Card" field("Requested For"; Rec."Requested For") { ToolTip = 'Specifies the expense user for whom the travel request is being created.'; + Editable = Rec.Status = Rec.Status::Open; + } + field("Requested For Name"; Rec."Requested For Name") + { + ToolTip = 'Specifies the name of the expense user for whom the travel request is being created.'; + Editable = Rec.Status = Rec.Status::Open; } field(Purpose; Rec.Purpose) { MultiLine = true; ToolTip = 'Specifies the purpose of the travel request.'; + Editable = Rec.Status = Rec.Status::Open; } field(Status; Rec.Status) { @@ -93,6 +101,7 @@ page 7129 "Travel Request Card" { Importance = Additional; ToolTip = 'Specifies the code for Shortcut Dimension 1, which is one of two global dimension codes that you set up.'; + Editable = Rec.Status = Rec.Status::Open; } field("Shortcut Dimension 2 Code"; Rec."Shortcut Dimension 2 Code") { @@ -115,21 +124,25 @@ page 7129 "Travel Request Card" { Importance = Promoted; ToolTip = 'Specifies the expected start date of the travel.'; + Editable = Rec.Status = Rec.Status::Open; } field("Expected End Date"; Rec."Expected End Date") { Importance = Promoted; ToolTip = 'Specifies the expected end date of the travel.'; + Editable = Rec.Status = Rec.Status::Open; } field("Actual Start Date and Time"; Rec."Actual Start Date and Time") { Importance = Additional; ToolTip = 'Specifies the actual start date and time of the travel.'; + Editable = Rec.Status = Rec.Status::Open; } field("Actual End Date and Time"; Rec."Actual End Date and Time") { Importance = Additional; ToolTip = 'Specifies the actual end date and time of the travel.'; + Editable = Rec.Status = Rec.Status::Open; } } group("Travel Details") @@ -141,33 +154,40 @@ page 7129 "Travel Request Card" MultiLine = true; Importance = Additional; ToolTip = 'Specifies the business justification for the travel.'; + Editable = Rec.Status = Rec.Status::Open; } field("International Travel"; Rec."International Travel") { Importance = Additional; ToolTip = 'Specifies whether the travel is international.'; + Editable = Rec.Status = Rec.Status::Open; } field("Origin Country"; Rec."Origin Country/Region Code") { ToolTip = 'Specifies the origin country for the travel.'; + Editable = Rec.Status = Rec.Status::Open; } field("Destination Country"; Rec."Dest. Country/Region Code") { ToolTip = 'Specifies the destination country for the travel.'; + Editable = Rec.Status = Rec.Status::Open; } field(Restrictions; Rec.Restrictions) { Importance = Additional; ToolTip = 'Specifies any travel restrictions that apply.'; + Editable = Rec.Status = Rec.Status::Open; } field("Travel Policy Acknowledgment"; Rec."Travel Policy Acknowledgment") { ToolTip = 'Specifies whether the travel policy has been acknowledged.'; + Editable = Rec.Status = Rec.Status::Open; } field("Per Diem Included"; Rec."Per Diem Included") { Importance = Additional; ToolTip = 'Specifies whether per diem is included in the travel request.'; + Editable = Rec.Status = Rec.Status::Open; } } group(Approval) diff --git a/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al index 358149c1de7..743c11e74a1 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al @@ -9,6 +9,7 @@ using Microsoft.Finance.GeneralLedger.Preview; using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Finance.SpendRequest; using Microsoft.HumanResources.Employee; +using System.Security.AccessControl; codeunit 148339 "Spend Request Test" { @@ -51,6 +52,13 @@ codeunit 148339 "Spend Request Test" CategoryClearedMsg: Label 'The expense category should be cleared when the line is not a Category line.'; MixedTypesMsg: Label 'Category and Lump Sum lines should coexist on the same travel request.'; CategoryLineOnlyErr: Label 'You can select an %1 only when %2 is %3.', Locked = true; + ApproverUserIdRecordedMsg: Label 'The approving user should be recorded on the travel request.'; + ApproverUserNameRecordedMsg: Label 'The approving user name should be recorded on the travel request.'; + ApproverDateTimeRecordedMsg: Label 'The approval date and time should be recorded on the travel request.'; + RequestedForNameMirrorsMsg: Label 'The requested-for name should mirror the expense user name.'; + RequestedForNameClearedMsg: Label 'The requested-for name should be cleared when the requester is removed.'; + RequesterFieldsEditableMsg: Label 'The requester fields and purpose should be editable while the travel request is open.'; + RequesterFieldsReadOnlyMsg: Label 'The requester fields and purpose should be read-only once the travel request leaves the open status.'; [Test] [HandlerFunctions('SpendReqConfirmHandler')] @@ -344,6 +352,29 @@ codeunit 148339 "Spend Request Test" Assert.AreEqual(SpendRequest.Status::Approved, SpendRequest.Status, SpendReqApprovedMsg); end; + [Test] + procedure ReleaseSpendReqRecordsApproverInfoWhenAgentDisabled() + var + SpendRequest: Record "Spend Request"; + ExpenseUser: Record "Expense User"; + ReleaseSpendRequest: Codeunit "Release Spend Request"; + begin + // [SCENARIO 650348] Auto-approving a travel request records approver audit fields. + Initialize(); + + // [GIVEN] A releasable spend request with every prerequisite satisfied. + CreateReleasableSpendRequest(SpendRequest, ExpenseUser); + + // [WHEN] The spend request is Released. + ReleaseSpendRequest.Release(SpendRequest); + + // [THEN] Approver user id, user name, and timestamp are recorded. + SpendRequest.Get(SpendRequest."No."); + Assert.AreEqual(UserSecurityId(), SpendRequest."Approved/Rejected by User ID", ApproverUserIdRecordedMsg); + Assert.AreEqual(GetExpectedApproverName(), SpendRequest."Approved/Rejected by User Name", ApproverUserNameRecordedMsg); + Assert.AreNotEqual(0DT, SpendRequest."Approved/Rejected At", ApproverDateTimeRecordedMsg); + end; + [Test] procedure ReleaseSpendReqStaysReleasedWhenAgentEnabled() var @@ -553,6 +584,93 @@ codeunit 148339 "Spend Request Test" Assert.RecordCount(Traveler, 1); end; + [Test] + [HandlerFunctions('SpendReqConfirmHandler')] + procedure RequestedForNameMirrorsExpenseUserName() + var + SpendRequest: Record "Spend Request"; + ExpenseUser: Record "Expense User"; + begin + // [SCENARIO 650348] "Requested For Name" mirrors the requester and is cleared when the requester is removed. + Initialize(); + + // [GIVEN] An expense user and an open travel request. + LibraryExpense.CreateExpenseUser(ExpenseUser); + LibraryExpense.CreateSpendRequest(SpendRequest); + + // [WHEN] The expense user is set as requester. + SpendRequest.Validate("Requested For", ExpenseUser."No."); + SpendRequest.Modify(true); + + // [THEN] The denormalized requester name is populated. + SpendRequest.CalcFields("Requested For Name"); + Assert.AreEqual(ExpenseUser.Name, SpendRequest."Requested For Name", RequestedForNameMirrorsMsg); + + // [WHEN] The requester is removed. + SpendRequest.Validate("Requested For", ''); + SpendRequest.Modify(true); + + // [THEN] The denormalized requester name is cleared. + SpendRequest.CalcFields("Requested For Name"); + Assert.AreEqual('', SpendRequest."Requested For Name", RequestedForNameClearedMsg); + end; + + [Test] + procedure PurposeCannotBeEditedWhenRequestNotOpen() + var + SpendRequest: Record "Spend Request"; + ExpenseUser: Record "Expense User"; + begin + // [SCENARIO 650348] The purpose cannot be changed once the travel request leaves the Open status. + Initialize(); + + // [GIVEN] An approved (non-open) travel request. + CreateReleasableSpendRequest(SpendRequest, ExpenseUser); + LibraryExpense.SetSpendRequestStatus(SpendRequest, SpendRequest.Status::Approved); + + // [WHEN] The purpose is changed. + asserterror SpendRequest.Validate(Purpose, 'Updated purpose after approval'); + + // [THEN] It fails because the request must be open to be edited. + Assert.ExpectedError(StatusNotOpenErr); + end; + + [Test] + procedure TravelRequestIdentityFieldsEditableOnlyWhenOpen() + var + SpendRequest: Record "Spend Request"; + ExpenseUser: Record "Expense User"; + TravelRequestCard: TestPage "Travel Request Card"; + begin + // [SCENARIO 650348] The requester and purpose fields on the travel request card are editable only while the request is Open. + Initialize(); + + // [GIVEN] An open travel request. + CreateReleasableSpendRequest(SpendRequest, ExpenseUser); + + // [WHEN] The card is opened while the request is Open. + TravelRequestCard.OpenEdit(); + TravelRequestCard.GoToRecord(SpendRequest); + + // [THEN] The requester fields and purpose are editable. + Assert.IsTrue(TravelRequestCard."Requested For".Editable(), RequesterFieldsEditableMsg); + Assert.IsTrue(TravelRequestCard.Purpose.Editable(), RequesterFieldsEditableMsg); + TravelRequestCard.Close(); + + // [GIVEN] The request is approved and therefore no longer open. + LibraryExpense.SetSpendRequestStatus(SpendRequest, SpendRequest.Status::Approved); + + // [WHEN] The card is reopened. + TravelRequestCard.OpenEdit(); + TravelRequestCard.GoToRecord(SpendRequest); + + // [THEN] The requester fields and purpose are read-only. + Assert.IsFalse(TravelRequestCard."Requested For".Editable(), RequesterFieldsReadOnlyMsg); + Assert.IsFalse(TravelRequestCard."Requested For Name".Editable(), RequesterFieldsReadOnlyMsg); + Assert.IsFalse(TravelRequestCard.Purpose.Editable(), RequesterFieldsReadOnlyMsg); + TravelRequestCard.Close(); + end; + [Test] procedure AddDuplicateTravelerFails() var @@ -1096,6 +1214,16 @@ codeunit 148339 "Spend Request Test" Traveler.DeleteAll(); end; + local procedure GetExpectedApproverName(): Code[50] + var + User: Record User; + begin + if User.ReadPermission() then + if User.Get(UserSecurityId()) then + exit(CopyStr(User."User Name", 1, 50)); + exit(CopyStr(UserId(), 1, 50)); + end; + [PageHandler] procedure SpendReqGLPostingPreviewHandler(var GLPostingPreview: TestPage "G/L Posting Preview") begin diff --git a/src/Layers/W1/BaseApp/Finance/SpendRequest/SpendRequest.Table.al b/src/Layers/W1/BaseApp/Finance/SpendRequest/SpendRequest.Table.al index bfce60e9179..94b1510c74c 100644 --- a/src/Layers/W1/BaseApp/Finance/SpendRequest/SpendRequest.Table.al +++ b/src/Layers/W1/BaseApp/Finance/SpendRequest/SpendRequest.Table.al @@ -83,6 +83,11 @@ table 6840 "Spend Request" { Caption = 'Purpose'; ToolTip = 'Specifies the purpose of the spend request.'; + + trigger OnValidate() + begin + TestStatusOpen(); + end; } field(9; "Currency Code"; Code[10]) {