From ebe826dd41f0e97a5f0322e9a732e4a435ce66d2 Mon Sep 17 00:00:00 2001 From: Andrius Andrulevicius Date: Thu, 12 Feb 2026 15:09:35 +0200 Subject: [PATCH 1/2] Adds option to use Shopify order number as document no. Introduces a configurable setting to use the Shopify order number as the document number for Sales Orders and Sales Invoices, with validation to prevent invalid characters. Updates UI, logic, and tests to support the new option and ensure correct propagation and behavior. --- .../App/src/Base/Pages/ShpfyShopCard.Page.al | 5 + .../App/src/Base/Tables/ShpfyShop.Table.al | 5 + .../Codeunits/ShpfyImportOrder.Codeunit.al | 1 + .../Codeunits/ShpfyProcessOrder.Codeunit.al | 7 + .../Order handling/Pages/ShpfyOrder.Page.al | 6 + .../Tables/ShpfyOrderHeader.Table.al | 7 + .../ShpfyOrdersAPITest.Codeunit.al | 239 ++++++++++++++++++ 7 files changed, 270 insertions(+) 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..73f805afa6 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,11 @@ page 30101 "Shpfy Shop Card" { ApplicationArea = All; } + field(UseShopifyOrderNo; Rec."Use Shopify Order No.") + { + ApplicationArea = All; + 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(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..9329303b6a 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,12 @@ 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; + ToolTip = 'Specifies whether the Shopify order number is used as the document number for this specific order.'; + } 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..8a8f604640 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,13 @@ 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; + InitValue = false; + 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; } From e3d3854a9b095fba75799ba06afd04b77090d2f4 Mon Sep 17 00:00:00 2001 From: Andrius Andrulevicius Date: Thu, 12 Feb 2026 15:54:25 +0200 Subject: [PATCH 2/2] Removes redundant tooltips and default value from order fields Cleans up UI by eliminating unnecessary tooltip descriptions and the explicit default value assignment for the Shopify order number usage option. Simplifies maintenance and reduces duplication, as field intent is already clear from context. --- src/Apps/W1/Shopify/App/src/Base/Pages/ShpfyShopCard.Page.al | 1 - .../W1/Shopify/App/src/Order handling/Pages/ShpfyOrder.Page.al | 1 - .../App/src/Order handling/Tables/ShpfyOrderHeader.Table.al | 1 - 3 files changed, 3 deletions(-) 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 73f805afa6..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 @@ -534,7 +534,6 @@ page 30101 "Shpfy Shop Card" field(UseShopifyOrderNo; Rec."Use Shopify Order No.") { ApplicationArea = All; - 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(ArchiveProcessOrders; Rec."Archive Processed Orders") { 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 9329303b6a..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 @@ -91,7 +91,6 @@ page 30113 "Shpfy Order" { ApplicationArea = All; Editable = not Rec.Processed; - ToolTip = 'Specifies whether the Shopify order number is used as the document number for this specific order.'; } field(Closed; Rec.Closed) { 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 8a8f604640..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 @@ -693,7 +693,6 @@ table 30118 "Shpfy Order Header" { Caption = 'Use Shopify Order No.'; DataClassification = SystemMetadata; - InitValue = false; ToolTip = 'Specifies whether the Shopify order number is used as the document number for this specific order.'; } field(500; "Shop Code"; Code[20])