diff --git a/src/Apps/W1/Shopify/App/src/Base/Pages/ShpfyShopCard.Page.al b/src/Apps/W1/Shopify/App/src/Base/Pages/ShpfyShopCard.Page.al index 1d494cf0df..40d6fdd078 100644 --- a/src/Apps/W1/Shopify/App/src/Base/Pages/ShpfyShopCard.Page.al +++ b/src/Apps/W1/Shopify/App/src/Base/Pages/ShpfyShopCard.Page.al @@ -531,6 +531,10 @@ page 30101 "Shpfy Shop Card" { ApplicationArea = All; } + field(UseShopifyOrderNo; Rec."Use Shopify Order No.") + { + ApplicationArea = All; + } field(ArchiveProcessOrders; Rec."Archive Processed Orders") { ApplicationArea = All; diff --git a/src/Apps/W1/Shopify/App/src/Base/Tables/ShpfyShop.Table.al b/src/Apps/W1/Shopify/App/src/Base/Tables/ShpfyShop.Table.al index 123de2a5e5..b1be1c6497 100644 --- a/src/Apps/W1/Shopify/App/src/Base/Tables/ShpfyShop.Table.al +++ b/src/Apps/W1/Shopify/App/src/Base/Tables/ShpfyShop.Table.al @@ -776,6 +776,11 @@ table 30102 "Shpfy Shop" Caption = 'Currency Handling'; InitValue = "Shop Currency"; } + field(136; "Use Shopify Order No."; Boolean) + { + Caption = 'Use Shopify Order No.'; + ToolTip = 'Specifies whether the Shopify order number is used as the document number on the created Sales Order or Sales Invoice. The number series must have Allow Manual Nos. enabled.'; + } field(200; "Shop Id"; Integer) { DataClassification = SystemMetadata; diff --git a/src/Apps/W1/Shopify/App/src/Order handling/Codeunits/ShpfyImportOrder.Codeunit.al b/src/Apps/W1/Shopify/App/src/Order handling/Codeunits/ShpfyImportOrder.Codeunit.al index a0f70c1bae..d8b252d038 100644 --- a/src/Apps/W1/Shopify/App/src/Order handling/Codeunits/ShpfyImportOrder.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Order handling/Codeunits/ShpfyImportOrder.Codeunit.al @@ -367,6 +367,7 @@ codeunit 30161 "Shpfy Import Order" exit(false); OrderHeader."Shopify Order Id" := OrderId; OrderHeader."Shop Code" := Shop.Code; + OrderHeader."Use Shopify Order No." := Shop."Use Shopify Order No."; ICountyFromJson := Shop."County Source"; OrderHeaderRecordRef.GetTable(OrderHeader); diff --git a/src/Apps/W1/Shopify/App/src/Order handling/Codeunits/ShpfyProcessOrder.Codeunit.al b/src/Apps/W1/Shopify/App/src/Order handling/Codeunits/ShpfyProcessOrder.Codeunit.al index a4da7cca5d..7df67e45cc 100644 --- a/src/Apps/W1/Shopify/App/src/Order handling/Codeunits/ShpfyProcessOrder.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Order handling/Codeunits/ShpfyProcessOrder.Codeunit.al @@ -68,6 +68,8 @@ codeunit 30166 "Shpfy Process Order" DocLinkToBCDoc: Record "Shpfy Doc. Link To Doc."; OrdersAPI: Codeunit "Shpfy Orders API"; BCDocumentTypeConvert: Codeunit "Shpfy BC Document Type Convert"; + InvalidCharTok: Label '@', Locked = true; + InvalidShopifyOrderErr: Label '%1 cannot start with %2.', Comment = '%1 = Shopify Order No. field caption, %2 = Invalid Character'; IsHandled: Boolean; begin OrderEvents.OnBeforeCreateSalesHeader(ShopifyOrderHeader, SalesHeader, LastCreatedDocumentId, IsHandled); @@ -79,6 +81,11 @@ codeunit 30166 "Shpfy Process Order" SalesHeader.Validate("Document Type", SalesHeader."Document Type"::Invoice) else SalesHeader.Validate("Document Type", SalesHeader."Document Type"::Order); + if ShopifyOrderHeader."Use Shopify Order No." and (ShopifyOrderHeader."Shopify Order No." <> '') then begin + if ShopifyOrderHeader."Shopify Order No.".StartsWith(InvalidCharTok) then + Error(InvalidShopifyOrderErr, ShopifyOrderHeader.FieldCaption("Shopify Order No."), InvalidCharTok); + SalesHeader.Validate("No.", CopyStr(ShopifyOrderHeader."Shopify Order No.", 1, MaxStrLen(SalesHeader."No."))); + end; SalesHeader.Insert(true); LastCreatedDocumentId := SalesHeader.SystemId; SalesHeader.Validate("Sell-to Customer No.", ShopifyOrderHeader."Sell-to Customer No."); diff --git a/src/Apps/W1/Shopify/App/src/Order handling/Pages/ShpfyOrder.Page.al b/src/Apps/W1/Shopify/App/src/Order handling/Pages/ShpfyOrder.Page.al index 7eeec660b1..c131e7ab3f 100644 --- a/src/Apps/W1/Shopify/App/src/Order handling/Pages/ShpfyOrder.Page.al +++ b/src/Apps/W1/Shopify/App/src/Order handling/Pages/ShpfyOrder.Page.al @@ -87,6 +87,11 @@ page 30113 "Shpfy Order" ApplicationArea = All; ToolTip = 'Specifies the purchase order number that is associated with the Shopify order.'; } + field(UseShopifyOrderNo; Rec."Use Shopify Order No.") + { + ApplicationArea = All; + Editable = not Rec.Processed; + } field(Closed; Rec.Closed) { ApplicationArea = All; diff --git a/src/Apps/W1/Shopify/App/src/Order handling/Tables/ShpfyOrderHeader.Table.al b/src/Apps/W1/Shopify/App/src/Order handling/Tables/ShpfyOrderHeader.Table.al index effc1419c9..f1e6327fec 100644 --- a/src/Apps/W1/Shopify/App/src/Order handling/Tables/ShpfyOrderHeader.Table.al +++ b/src/Apps/W1/Shopify/App/src/Order handling/Tables/ShpfyOrderHeader.Table.al @@ -689,6 +689,12 @@ table 30118 "Shpfy Order Header" FieldClass = FlowField; CalcFormula = exist("Shpfy Order Tax Line" where("Parent Id" = field("Shopify Order Id"), "Channel Liable" = const(true))); } + field(135; "Use Shopify Order No."; Boolean) + { + Caption = 'Use Shopify Order No.'; + DataClassification = SystemMetadata; + ToolTip = 'Specifies whether the Shopify order number is used as the document number for this specific order.'; + } field(500; "Shop Code"; Code[20]) { Caption = 'Shop Code'; diff --git a/src/Apps/W1/Shopify/Test/Order Handling/ShpfyOrdersAPITest.Codeunit.al b/src/Apps/W1/Shopify/Test/Order Handling/ShpfyOrdersAPITest.Codeunit.al index f657c9f8d1..cfaafbfa49 100644 --- a/src/Apps/W1/Shopify/Test/Order Handling/ShpfyOrdersAPITest.Codeunit.al +++ b/src/Apps/W1/Shopify/Test/Order Handling/ShpfyOrdersAPITest.Codeunit.al @@ -8,11 +8,13 @@ namespace Microsoft.Integration.Shopify.Test; using Microsoft.Finance.Currency; using Microsoft.Finance.SalesTax; using Microsoft.Foundation.Address; +using Microsoft.Foundation.NoSeries; using Microsoft.Integration.Shopify; using Microsoft.Inventory.Item; using Microsoft.Inventory.Journal; using Microsoft.Sales.Customer; using Microsoft.Sales.Document; +using Microsoft.Sales.Setup; using System.TestLibraries.Utilities; codeunit 139608 "Shpfy Orders API Test" @@ -1272,6 +1274,219 @@ codeunit 139608 "Shpfy Orders API Test" LibraryAssert.AreEqual(ExpectedChannelLiable, OrderHeader."Channel Liable Taxes", StrSubstNo(OrderHeaderChannelLiableMismatchTxt, ScenarioName)); end; + [Test] + procedure UnitTestImportOrderPropagatesUseShopifyOrderNo() + var + Shop: Record "Shpfy Shop"; + OrderHeader: Record "Shpfy Order Header"; + CommunicationMgt: Codeunit "Shpfy Communication Mgt."; + ImportOrder: Codeunit "Shpfy Import Order"; + OrderHandlingHelper: Codeunit "Shpfy Order Handling Helper"; + begin + // [SCENARIO] When Shop."Use Shopify Order No." is true, importing an order propagates the setting to OrderHeader. + Initialize(); + + // [GIVEN] Shopify Shop with "Use Shopify Order No." enabled + Shop := CommunicationMgt.GetShopRecord(); + Shop."Customer Mapping Type" := "Shpfy Customer Mapping"::"By EMail/Phone"; + Shop."Use Shopify Order No." := true; + if not Shop.Modify() then + Shop.Insert(); + ImportOrder.SetShop(Shop.Code); + + // [WHEN] Shopify order is imported + OrderHandlingHelper.ImportShopifyOrder(Shop, OrderHeader, ImportOrder, false); + + // [THEN] OrderHeader."Use Shopify Order No." = true + LibraryAssert.IsTrue(OrderHeader."Use Shopify Order No.", 'OrderHeader."Use Shopify Order No." should be true when Shop setting is enabled'); + end; + + [Test] + procedure UnitTestImportOrderPropagatesUseShopifyOrderNoDisabled() + var + Shop: Record "Shpfy Shop"; + OrderHeader: Record "Shpfy Order Header"; + CommunicationMgt: Codeunit "Shpfy Communication Mgt."; + ImportOrder: Codeunit "Shpfy Import Order"; + OrderHandlingHelper: Codeunit "Shpfy Order Handling Helper"; + begin + // [SCENARIO] When Shop."Use Shopify Order No." is false, importing an order propagates the setting to OrderHeader. + Initialize(); + + // [GIVEN] Shopify Shop with "Use Shopify Order No." disabled + Shop := CommunicationMgt.GetShopRecord(); + Shop."Customer Mapping Type" := "Shpfy Customer Mapping"::"By EMail/Phone"; + Shop."Use Shopify Order No." := false; + if not Shop.Modify() then + Shop.Insert(); + ImportOrder.SetShop(Shop.Code); + + // [WHEN] Shopify order is imported + OrderHandlingHelper.ImportShopifyOrder(Shop, OrderHeader, ImportOrder, false); + + // [THEN] OrderHeader."Use Shopify Order No." = false + LibraryAssert.IsFalse(OrderHeader."Use Shopify Order No.", 'OrderHeader."Use Shopify Order No." should be false when Shop setting is disabled'); + end; + + [Test] + procedure UnitTestCreateSalesOrderWithShopifyOrderNo() + var + Shop: Record "Shpfy Shop"; + OrderHeader: Record "Shpfy Order Header"; + SalesHeader: Record "Sales Header"; + CommunicationMgt: Codeunit "Shpfy Communication Mgt."; + ImportOrder: Codeunit "Shpfy Import Order"; + ProcessOrders: Codeunit "Shpfy Process Orders"; + OrderHandlingHelper: Codeunit "Shpfy Order Handling Helper"; + begin + // [SCENARIO] When "Use Shopify Order No." is enabled, the created Sales Order uses the Shopify order number as its document number. + Initialize(); + + // [GIVEN] Shopify Shop + Shop := CommunicationMgt.GetShopRecord(); + Shop."Customer Mapping Type" := "Shpfy Customer Mapping"::"By EMail/Phone"; + if not Shop.Modify() then + Shop.Insert(); + ImportOrder.SetShop(Shop.Code); + + // [GIVEN] Sales Order number series allows manual entry + SetManualNosOnOrderNoSeries(); + + // [GIVEN] Imported Shopify order with "Use Shopify Order No." enabled + OrderHandlingHelper.ImportShopifyOrder(Shop, OrderHeader, ImportOrder, false); + OrderHeader."Use Shopify Order No." := true; + OrderHeader.Modify(); + Commit(); + + // [WHEN] Order is processed + ProcessOrders.ProcessShopifyOrder(OrderHeader); + OrderHeader.GetBySystemId(OrderHeader.SystemId); + + // [THEN] Sales document is created with Shopify Order No. as document number + SalesHeader.SetRange("Shpfy Order Id", OrderHeader."Shopify Order Id"); + LibraryAssert.IsTrue(SalesHeader.FindLast(), 'Sales document is created from Shopify order'); + LibraryAssert.AreEqual(OrderHeader."Shopify Order No.", SalesHeader."No.", 'Sales document number should equal Shopify Order No.'); + end; + + [Test] + procedure UnitTestCreateSalesOrderWithoutShopifyOrderNo() + var + Shop: Record "Shpfy Shop"; + OrderHeader: Record "Shpfy Order Header"; + SalesHeader: Record "Sales Header"; + CommunicationMgt: Codeunit "Shpfy Communication Mgt."; + ImportOrder: Codeunit "Shpfy Import Order"; + ProcessOrders: Codeunit "Shpfy Process Orders"; + OrderHandlingHelper: Codeunit "Shpfy Order Handling Helper"; + begin + // [SCENARIO] When "Use Shopify Order No." is disabled, the created Sales Order uses the standard number series. + Initialize(); + + // [GIVEN] Shopify Shop + Shop := CommunicationMgt.GetShopRecord(); + Shop."Customer Mapping Type" := "Shpfy Customer Mapping"::"By EMail/Phone"; + if not Shop.Modify() then + Shop.Insert(); + ImportOrder.SetShop(Shop.Code); + + // [GIVEN] Imported Shopify order with "Use Shopify Order No." disabled + OrderHandlingHelper.ImportShopifyOrder(Shop, OrderHeader, ImportOrder, false); + OrderHeader."Use Shopify Order No." := false; + OrderHeader.Modify(); + Commit(); + + // [WHEN] Order is processed + ProcessOrders.ProcessShopifyOrder(OrderHeader); + OrderHeader.GetBySystemId(OrderHeader.SystemId); + + // [THEN] Sales document is created with a number from the number series, not the Shopify Order No. + SalesHeader.SetRange("Shpfy Order Id", OrderHeader."Shopify Order Id"); + LibraryAssert.IsTrue(SalesHeader.FindLast(), 'Sales document is created from Shopify order'); + LibraryAssert.AreNotEqual(OrderHeader."Shopify Order No.", SalesHeader."No.", 'Sales document number should not equal Shopify Order No. when feature is disabled'); + end; + + [Test] + procedure UnitTestCreateSalesOrderWithShopifyOrderNoInvalidChar() + var + Shop: Record "Shpfy Shop"; + OrderHeader: Record "Shpfy Order Header"; + CommunicationMgt: Codeunit "Shpfy Communication Mgt."; + ImportOrder: Codeunit "Shpfy Import Order"; + ProcessOrders: Codeunit "Shpfy Process Orders"; + OrderHandlingHelper: Codeunit "Shpfy Order Handling Helper"; + begin + // [SCENARIO] When "Use Shopify Order No." is enabled and the Shopify Order No. starts with "@", processing fails with an error. + Initialize(); + + // [GIVEN] Shopify Shop + Shop := CommunicationMgt.GetShopRecord(); + Shop."Customer Mapping Type" := "Shpfy Customer Mapping"::"By EMail/Phone"; + if not Shop.Modify() then + Shop.Insert(); + ImportOrder.SetShop(Shop.Code); + + // [GIVEN] Sales Order number series allows manual entry + SetManualNosOnOrderNoSeries(); + + // [GIVEN] Imported Shopify order with invalid Shopify Order No. starting with "@" + OrderHandlingHelper.ImportShopifyOrder(Shop, OrderHeader, ImportOrder, false); + OrderHeader."Use Shopify Order No." := true; + OrderHeader."Shopify Order No." := '@INVALID123'; + OrderHeader.Modify(); + Commit(); + + // [WHEN] Order is processed + ProcessOrders.ProcessShopifyOrder(OrderHeader); + OrderHeader.GetBySystemId(OrderHeader.SystemId); + + // [THEN] Order has an error because Shopify Order No. starts with "@" + LibraryAssert.IsTrue(OrderHeader."Has Error", 'Order should have an error when Shopify Order No. starts with @'); + LibraryAssert.IsTrue(OrderHeader."Error Message".Contains('@'), 'Error message should mention the invalid character @'); + end; + + [Test] + procedure UnitTestCreateSalesInvoiceWithShopifyOrderNo() + var + Shop: Record "Shpfy Shop"; + OrderHeader: Record "Shpfy Order Header"; + SalesHeader: Record "Sales Header"; + CommunicationMgt: Codeunit "Shpfy Communication Mgt."; + ImportOrder: Codeunit "Shpfy Import Order"; + ProcessOrders: Codeunit "Shpfy Process Orders"; + OrderHandlingHelper: Codeunit "Shpfy Order Handling Helper"; + begin + // [SCENARIO] When "Use Shopify Order No." is enabled and a fulfilled order creates an invoice, the Sales Invoice uses the Shopify order number. + Initialize(); + + // [GIVEN] Shopify Shop with "Create Invoices From Orders" enabled + Shop := CommunicationMgt.GetShopRecord(); + Shop."Customer Mapping Type" := "Shpfy Customer Mapping"::"By EMail/Phone"; + Shop."Create Invoices From Orders" := true; + if not Shop.Modify() then + Shop.Insert(); + ImportOrder.SetShop(Shop.Code); + + // [GIVEN] Sales Invoice number series allows manual entry + SetManualNosOnInvoiceNoSeries(); + + // [GIVEN] Imported fulfilled Shopify order with "Use Shopify Order No." enabled + OrderHandlingHelper.ImportShopifyOrder(Shop, OrderHeader, ImportOrder, false); + OrderHeader."Use Shopify Order No." := true; + OrderHeader."Fulfillment Status" := "Shpfy Order Fulfill. Status"::Fulfilled; + OrderHeader.Modify(); + Commit(); + + // [WHEN] Order is processed + ProcessOrders.ProcessShopifyOrder(OrderHeader); + OrderHeader.GetBySystemId(OrderHeader.SystemId); + + // [THEN] Sales Invoice is created with the Shopify Order No. as document number + SalesHeader.SetRange("Shpfy Order Id", OrderHeader."Shopify Order Id"); + LibraryAssert.IsTrue(SalesHeader.FindLast(), 'Sales document is created from Shopify order'); + LibraryAssert.AreEqual(SalesHeader."Document Type", SalesHeader."Document Type"::Invoice, 'Sales document should be an Invoice for fulfilled orders'); + LibraryAssert.AreEqual(OrderHeader."Shopify Order No.", SalesHeader."No.", 'Sales Invoice number should equal Shopify Order No.'); + end; + local procedure CreateTaxArea(var TaxArea: Record "Tax Area"; var ShopifyTaxArea: Record "Shpfy Tax Area"; Shop: Record "Shpfy Shop") var ShopifyCustomerTemplate: Record "Shpfy Customer Template"; @@ -1485,4 +1700,28 @@ codeunit 139608 "Shpfy Orders API Test" JTaxLines.Add(JTaxLine); JOrder.Add('taxLines', JTaxLines); end; + + local procedure SetManualNosOnOrderNoSeries() + var + SalesReceivablesSetup: Record "Sales & Receivables Setup"; + NoSeries: Record "No. Series"; + begin + SalesReceivablesSetup.Get(); + if NoSeries.Get(SalesReceivablesSetup."Order Nos.") then begin + NoSeries."Manual Nos." := true; + NoSeries.Modify(); + end; + end; + + local procedure SetManualNosOnInvoiceNoSeries() + var + SalesReceivablesSetup: Record "Sales & Receivables Setup"; + NoSeries: Record "No. Series"; + begin + SalesReceivablesSetup.Get(); + if NoSeries.Get(SalesReceivablesSetup."Invoice Nos.") then begin + NoSeries."Manual Nos." := true; + NoSeries.Modify(); + end; + end; }