diff --git a/src/Layers/ES/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al b/src/Layers/ES/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al index c5c16255d50..698a211b6e0 100644 --- a/src/Layers/ES/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al +++ b/src/Layers/ES/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al @@ -45,7 +45,10 @@ using System.Utilities; codeunit 6620 "Copy Document Mgt." { - Permissions = TableData "Cust. Ledger Entry" = rim; + Permissions = TableData "Cust. Ledger Entry" = rim, + tabledata "Assembly Header" = rim, + tabledata "Assembly Line" = rim, + tabledata "Assemble-to-Order Link" = rim; trigger OnRun() begin diff --git a/src/Layers/IT/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al b/src/Layers/IT/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al index 6d9acab7830..0dfee713261 100644 --- a/src/Layers/IT/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al +++ b/src/Layers/IT/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al @@ -46,6 +46,11 @@ using System.Utilities; codeunit 6620 "Copy Document Mgt." { + Permissions = + tabledata "Assembly Header" = rim, + tabledata "Assembly Line" = rim, + tabledata "Assemble-to-Order Link" = rim; + trigger OnRun() begin end; diff --git a/src/Layers/NA/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al b/src/Layers/NA/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al index 929f2f01eb6..445088fe526 100644 --- a/src/Layers/NA/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al +++ b/src/Layers/NA/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al @@ -47,6 +47,11 @@ using System.Utilities; codeunit 6620 "Copy Document Mgt." { + Permissions = + tabledata "Assembly Header" = rim, + tabledata "Assembly Line" = rim, + tabledata "Assemble-to-Order Link" = rim; + trigger OnRun() begin end; diff --git a/src/Layers/NL/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al b/src/Layers/NL/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al index 0e2eb10a44e..3a5fd95f144 100644 --- a/src/Layers/NL/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al +++ b/src/Layers/NL/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al @@ -46,6 +46,11 @@ using System.Utilities; codeunit 6620 "Copy Document Mgt." { + Permissions = + tabledata "Assembly Header" = rim, + tabledata "Assembly Line" = rim, + tabledata "Assemble-to-Order Link" = rim; + trigger OnRun() begin end; diff --git a/src/Layers/RU/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al b/src/Layers/RU/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al index 09eaee8d287..39b9bb69820 100644 --- a/src/Layers/RU/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al +++ b/src/Layers/RU/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al @@ -46,6 +46,11 @@ using System.Utilities; codeunit 6620 "Copy Document Mgt." { + Permissions = + tabledata "Assembly Header" = rim, + tabledata "Assembly Line" = rim, + tabledata "Assemble-to-Order Link" = rim; + trigger OnRun() begin end; diff --git a/src/Layers/W1/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al b/src/Layers/W1/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al index 7a9981ee716..6adfab8eb9f 100644 --- a/src/Layers/W1/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al +++ b/src/Layers/W1/BaseApp/Utilities/CopyDocumentMgt.Codeunit.al @@ -46,6 +46,11 @@ using System.Utilities; codeunit 6620 "Copy Document Mgt." { + Permissions = + tabledata "Assembly Header" = rim, + tabledata "Assembly Line" = rim, + tabledata "Assemble-to-Order Link" = rim; + trigger OnRun() begin end; @@ -438,6 +443,7 @@ codeunit 6620 "Copy Document Mgt." FromDocType.AsInteger(), FromDocNo, ToSalesHeader, FromDocOccurrenceNo, FromDocVersionNo, IncludeHeader, RecalculateLines, MoveNegLines); end; + procedure CopySalesDocSalesLine(FromSalesHeader: Record "Sales Header"; var ToSalesHeader: Record "Sales Header"; var LinesNotCopied: Integer; NextLineNo: Integer) var AssemblyHeader: Record "Assembly Header"; diff --git a/src/Layers/W1/Tests/SCM/SCMCopyDocumentMgt.Codeunit.al b/src/Layers/W1/Tests/SCM/SCMCopyDocumentMgt.Codeunit.al index b071ed726db..4ec4ab363a3 100644 --- a/src/Layers/W1/Tests/SCM/SCMCopyDocumentMgt.Codeunit.al +++ b/src/Layers/W1/Tests/SCM/SCMCopyDocumentMgt.Codeunit.al @@ -27,6 +27,7 @@ codeunit 137212 "SCM Copy Document Mgt." LibrarySetupStorage: Codeunit "Library - Setup Storage"; LibraryRandom: Codeunit "Library - Random"; LibraryWarehouse: Codeunit "Library - Warehouse"; + LibraryE2EPlanPermissions: Codeunit "Library - E2E Plan Permissions"; IsInitialized: Boolean; MsgCorrectedInvoiceNo: Label 'have a Corrected Invoice No. Do you want to continue?'; WrongDimensionsCopiedErr: Label 'Wrong dimensions in copied document'; @@ -1726,6 +1727,211 @@ codeunit 137212 "SCM Copy Document Mgt." ReturnSalesLine.TestField(Quantity, TotalQty); end; + [Test] + [HandlerFunctions('MessageHandler')] + [Scope('OnPrem')] + procedure CopySalesQuoteWithATOAsTeamMember() + var + Item: Record Item; + AssemblyItem: Record Item; + FromSalesHeader: Record "Sales Header"; + FromSalesLine: Record "Sales Line"; + ToSalesHeader: Record "Sales Header"; + ToSalesLine: Record "Sales Line"; + AssemblyHeader: Record "Assembly Header"; + LibraryLowerPermissions: Codeunit "Library - Lower Permissions"; + Qty: Decimal; + begin + // [FEATURE] [Assemble-to-Order] [Copy Document] [Permissions] + // [SCENARIO] AB#630947: A user restricted to the D365 Team Member permission set can copy a sales quote whose + // line has an assemble-to-order link, and the copy reproduces the ATO link on the target line. + Initialize(); + + // [GIVEN] Assemble-to-order item with a resource component (resource-only BOM avoids the + // component availability engine that would read Prod. Order tables outside the fix under test) + LibraryInventory.CreateItem(Item); + Item.Validate("Replenishment System", Item."Replenishment System"::Assembly); + Item.Validate("Assembly Policy", Item."Assembly Policy"::"Assemble-to-Order"); + Item.Modify(true); + LibraryAssembly.CreateAssemblyList( + AssemblyItem."Costing Method"::Standard, Item."No.", true, 0, 1, 0, LibraryRandom.RandInt(5), '', ''); + + // [GIVEN] Source sales quote with the ATO item; validating the item auto-creates the ATO link + Qty := LibraryRandom.RandIntInRange(2, 10); + LibrarySales.CreateSalesDocumentWithItem( + FromSalesHeader, FromSalesLine, FromSalesHeader."Document Type"::Quote, LibrarySales.CreateCustomerNo(), + Item."No.", Qty, '', WorkDate()); + + // [GIVEN] Empty target sales quote created while the user still has full permissions + CreateEmptySalesHeader(ToSalesHeader, ToSalesHeader."Document Type"::Quote); + Commit(); + + // [GIVEN] User is now restricted to the D365 Team Member plan (regression harness for AB#630947) + LibraryE2EPlanPermissions.SetTeamMemberPlan(); + + // [WHEN] Copy Document is executed for the source quote into the target quote + CopyDocumentMgt.SetProperties(true, false, false, false, true, false, false); + CopyDocumentMgt.CopySalesDoc("Sales Document Type From"::Quote, FromSalesHeader."No.", ToSalesHeader); + + // [THEN] The copied line exists on the target quote with the same quantity and Qty. to Assemble to Order... + LibraryLowerPermissions.SetO365Full(); + ToSalesLine.SetRange("Document Type", ToSalesHeader."Document Type"); + ToSalesLine.SetRange("Document No.", ToSalesHeader."No."); + ToSalesLine.SetRange(Type, ToSalesLine.Type::Item); + ToSalesLine.SetRange("No.", Item."No."); + Assert.RecordCount(ToSalesLine, 1); + ToSalesLine.FindFirst(); + ToSalesLine.TestField(Quantity, Qty); + ToSalesLine.TestField("Qty. to Assemble to Order", Qty); + + // [THEN] ...and an Assembly Header is linked to the copied sales line + Assert.IsTrue( + ToSalesLine.AsmToOrderExists(AssemblyHeader), + 'Assemble-to-Order link should exist for the copied sales line.'); + end; + + [Test] + [HandlerFunctions('MessageHandler')] + [Scope('OnPrem')] + procedure CopySalesQuoteWithItemComponentATOAsTeamMember() + var + Item: Record Item; + AssemblyItem: Record Item; + FromSalesHeader: Record "Sales Header"; + FromSalesLine: Record "Sales Line"; + ToSalesHeader: Record "Sales Header"; + ToSalesLine: Record "Sales Line"; + AssemblyHeader: Record "Assembly Header"; + AssemblyLine: Record "Assembly Line"; + LibraryLowerPermissions: Codeunit "Library - Lower Permissions"; + Qty: Decimal; + begin + // [FEATURE] [Assemble-to-Order] [Copy Document] [Permissions] + // [SCENARIO] AB#630947: A user restricted to the D365 Team Member permission set can copy a sales quote whose + // line has an assemble-to-order link built from an item component, and the copy reproduces the ATO link and + // the item assembly line on the target line. + Initialize(); + + // [GIVEN] Assemble-to-order item with a single item component (typical ATO BOM that reads item data) + LibraryInventory.CreateItem(Item); + Item.Validate("Replenishment System", Item."Replenishment System"::Assembly); + Item.Validate("Assembly Policy", Item."Assembly Policy"::"Assemble-to-Order"); + Item.Modify(true); + LibraryAssembly.CreateAssemblyList( + AssemblyItem."Costing Method"::Standard, Item."No.", true, 1, 0, 0, LibraryRandom.RandInt(5), '', ''); + + // [GIVEN] Source sales quote with the ATO item; validating the item auto-creates the ATO link + Qty := LibraryRandom.RandIntInRange(2, 10); + LibrarySales.CreateSalesDocumentWithItem( + FromSalesHeader, FromSalesLine, FromSalesHeader."Document Type"::Quote, LibrarySales.CreateCustomerNo(), + Item."No.", Qty, '', WorkDate()); + + // [GIVEN] Empty target sales quote created while the user still has full permissions + CreateEmptySalesHeader(ToSalesHeader, ToSalesHeader."Document Type"::Quote); + Commit(); + + // [GIVEN] User is now restricted to the D365 Team Member plan (regression harness for AB#630947) + LibraryE2EPlanPermissions.SetTeamMemberPlan(); + + // [WHEN] Copy Document is executed for the source quote into the target quote + CopyDocumentMgt.SetProperties(true, false, false, false, true, false, false); + CopyDocumentMgt.CopySalesDoc("Sales Document Type From"::Quote, FromSalesHeader."No.", ToSalesHeader); + + // [THEN] The copied line exists on the target quote with the same quantity and Qty. to Assemble to Order... + LibraryLowerPermissions.SetO365Full(); + ToSalesLine.SetRange("Document Type", ToSalesHeader."Document Type"); + ToSalesLine.SetRange("Document No.", ToSalesHeader."No."); + ToSalesLine.SetRange(Type, ToSalesLine.Type::Item); + ToSalesLine.SetRange("No.", Item."No."); + Assert.RecordCount(ToSalesLine, 1); + ToSalesLine.FindFirst(); + ToSalesLine.TestField(Quantity, Qty); + ToSalesLine.TestField("Qty. to Assemble to Order", Qty); + + // [THEN] ...an Assembly Header is linked to the copied sales line... + Assert.IsTrue( + ToSalesLine.AsmToOrderExists(AssemblyHeader), + 'Assemble-to-Order link should exist for the copied sales line.'); + + // [THEN] ...and the linked assembly order reproduces the item component line + AssemblyLine.SetRange("Document Type", AssemblyHeader."Document Type"); + AssemblyLine.SetRange("Document No.", AssemblyHeader."No."); + AssemblyLine.SetRange(Type, AssemblyLine.Type::Item); + Assert.RecordCount(AssemblyLine, 1); + end; + + [Test] + [HandlerFunctions('MessageHandler')] + [Scope('OnPrem')] + procedure CopyArchivedSalesQuoteWithATOAsTeamMember() + var + Item: Record Item; + AssemblyItem: Record Item; + FromSalesHeader: Record "Sales Header"; + FromSalesLine: Record "Sales Line"; + SalesHeaderArchive: Record "Sales Header Archive"; + ToSalesHeader: Record "Sales Header"; + ToSalesLine: Record "Sales Line"; + AssemblyHeader: Record "Assembly Header"; + ArchiveManagement: Codeunit ArchiveManagement; + LibraryLowerPermissions: Codeunit "Library - Lower Permissions"; + Qty: Decimal; + begin + // [FEATURE] [Assemble-to-Order] [Copy Document] [Permissions] [Archive] + // [SCENARIO] AB#630947: A user restricted to the D365 Team Member permission set can copy an archived sales quote + // whose line has an assemble-to-order link, exercising the separately elevated CopySalesDocSalesLineArchive path, + // and the copy reproduces the ATO link on the target line. + Initialize(); + + // [GIVEN] Assemble-to-order item with a resource component + LibraryInventory.CreateItem(Item); + Item.Validate("Replenishment System", Item."Replenishment System"::Assembly); + Item.Validate("Assembly Policy", Item."Assembly Policy"::"Assemble-to-Order"); + Item.Modify(true); + LibraryAssembly.CreateAssemblyList( + AssemblyItem."Costing Method"::Standard, Item."No.", true, 0, 1, 0, LibraryRandom.RandInt(5), '', ''); + + // [GIVEN] Source sales quote with the ATO item; validating the item auto-creates the ATO link + Qty := LibraryRandom.RandIntInRange(2, 10); + LibrarySales.CreateSalesDocumentWithItem( + FromSalesHeader, FromSalesLine, FromSalesHeader."Document Type"::Quote, LibrarySales.CreateCustomerNo(), + Item."No.", Qty, '', WorkDate()); + + // [GIVEN] The source quote is archived + ArchiveManagement.StoreSalesDocument(FromSalesHeader, false); + SalesHeaderArchive.Get(FromSalesHeader."Document Type", FromSalesHeader."No.", 1, 1); + + // [GIVEN] Empty target sales quote created while the user still has full permissions + CreateEmptySalesHeader(ToSalesHeader, ToSalesHeader."Document Type"::Quote); + Commit(); + + // [GIVEN] User is now restricted to the D365 Team Member plan (regression harness for AB#630947) + LibraryE2EPlanPermissions.SetTeamMemberPlan(); + + // [WHEN] Copy Document is executed for the archived quote into the target quote + // Recalculate lines is required: the sales line archive does not persist "Qty. to Assemble to Order", + // so the ATO quantity is re-derived from the item's assembly policy during the copy. + CopyDocumentMgt.SetProperties(true, true, false, false, true, false, false); + CopyDocumentMgt.SetArchDocVal(SalesHeaderArchive."Doc. No. Occurrence", SalesHeaderArchive."Version No."); + CopyDocumentMgt.CopySalesDoc("Sales Document Type From"::"Arch. Quote", SalesHeaderArchive."No.", ToSalesHeader); + + // [THEN] The copied line exists on the target quote with the same quantity and Qty. to Assemble to Order... + LibraryLowerPermissions.SetO365Full(); + ToSalesLine.SetRange("Document Type", ToSalesHeader."Document Type"); + ToSalesLine.SetRange("Document No.", ToSalesHeader."No."); + ToSalesLine.SetRange(Type, ToSalesLine.Type::Item); + ToSalesLine.SetRange("No.", Item."No."); + Assert.RecordCount(ToSalesLine, 1); + ToSalesLine.FindFirst(); + ToSalesLine.TestField(Quantity, Qty); + ToSalesLine.TestField("Qty. to Assemble to Order", Qty); + + // [THEN] ...and an Assembly Header is linked to the copied sales line + Assert.IsTrue( + ToSalesLine.AsmToOrderExists(AssemblyHeader), + 'Assemble-to-Order link should exist for the copied sales line.'); + end; + local procedure Initialize() var LibraryERMCountryData: Codeunit "Library - ERM Country Data";