From 61dcf865c85741f62b681111f38944a790ba1c92 Mon Sep 17 00:00:00 2001 From: justaszaksauskas Date: Wed, 17 Dec 2025 17:46:29 +0200 Subject: [PATCH 01/21] Added Item Attributes enum and extensions for Shpfy Product Sync --- .../Enums/ShpfyInclInProductSync.Enum.al | 24 +++++++++++++++++++ .../ShpfyItemAttributes.PageExt.al | 23 ++++++++++++++++++ .../ShpfyItemAttribute.TableExt.al | 20 ++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 src/Apps/W1/Shopify/App/src/Products/Enums/ShpfyInclInProductSync.Enum.al create mode 100644 src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemAttributes.PageExt.al create mode 100644 src/Apps/W1/Shopify/App/src/Products/Table Extensions/ShpfyItemAttribute.TableExt.al diff --git a/src/Apps/W1/Shopify/App/src/Products/Enums/ShpfyInclInProductSync.Enum.al b/src/Apps/W1/Shopify/App/src/Products/Enums/ShpfyInclInProductSync.Enum.al new file mode 100644 index 0000000000..ecf72608d9 --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Products/Enums/ShpfyInclInProductSync.Enum.al @@ -0,0 +1,24 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace Microsoft.Integration.Shopify; + +/// +/// Enum Shpfy Incl. in Product Sync (ID 30179). +/// +enum 30179 "Shpfy Incl. in Product Sync" +{ + Caption = 'Incl. in Product Sync'; + Extensible = false; + + value(0; " ") + { + Caption = ' '; + } + value(1; "As Option") + { + Caption = 'As Option'; + } +} diff --git a/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemAttributes.PageExt.al b/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemAttributes.PageExt.al new file mode 100644 index 0000000000..fffac64079 --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemAttributes.PageExt.al @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace Microsoft.Integration.Shopify; + +using Microsoft.Inventory.Item.Attribute; + +pageextension 30127 "Shpfy Item Attributes" extends "Item Attributes" +{ + layout + { + addlast(Control1) + { + field("Shpfy Incl. in Product Sync"; Rec."Shpfy Incl. in Product Sync") + { + ApplicationArea = All; + ToolTip = 'Specifies whether to include this item attribute in product synchronization to Shopify. Select "As Option" to export the attribute as a Shopify Product Option.'; + } + } + } +} diff --git a/src/Apps/W1/Shopify/App/src/Products/Table Extensions/ShpfyItemAttribute.TableExt.al b/src/Apps/W1/Shopify/App/src/Products/Table Extensions/ShpfyItemAttribute.TableExt.al new file mode 100644 index 0000000000..5d745786fd --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Products/Table Extensions/ShpfyItemAttribute.TableExt.al @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace Microsoft.Integration.Shopify; + +using Microsoft.Inventory.Item.Attribute; + +tableextension 30112 "Shpfy Item Attribute" extends "Item Attribute" +{ + fields + { + field(30100; "Shpfy Incl. in Product Sync"; Enum "Shpfy Incl. in Product Sync") + { + Caption = 'Incl. in Product Sync'; + DataClassification = CustomerContent; + } + } +} From d1915b2cf8f8eddf620ae05123d3a6bd89b1dd0c Mon Sep 17 00:00:00 2001 From: jzaksauskas Date: Tue, 13 Jan 2026 12:46:31 +0200 Subject: [PATCH 02/21] Added functionality to collect and assign product options for the items with variants --- .../Codeunits/ShpfyCreateProduct.Codeunit.al | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al index f690c24b19..08a2de589b 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al @@ -6,6 +6,7 @@ namespace Microsoft.Integration.Shopify; using Microsoft.Inventory.Item; +using Microsoft.Inventory.Item.Attribute; using Microsoft.Inventory.Item.Catalog; /// @@ -16,8 +17,12 @@ codeunit 30174 "Shpfy Create Product" Access = Internal; Permissions = tabledata Item = r, + tabledata "Item Attribute" = r, + tabledata "Item Attribute Value" = r, + tabledata "Item Attribute Value Mapping" = r, tabledata "Item Reference" = r, tabledata "Item Unit of Measure" = r, + tabledata "Item Var. Attr. Value Mapping" = r, tabledata "Item Variant" = r; TableNo = Item; @@ -31,6 +36,7 @@ codeunit 30174 "Shpfy Create Product" Getlocations: Boolean; ProductId: BigInteger; ItemVariantIsBlockedLbl: Label 'Item variant is blocked or sales blocked.'; + TooManyAttributesAsOptionErr: Label 'Item %1 has %2 attributes marked as "As Option". Shopify supports a maximum of 3 product options.', Comment = '%1 = Item No., %2 = Number of attributes'; trigger OnRun() var @@ -165,6 +171,7 @@ codeunit 30174 "Shpfy Create Product" CreateTempShopifyVariantFromItem(Item, TempShopifyVariant); TempShopifyProduct.Insert(false); + FillOptionsForAllVariants(Item."No.", TempShopifyVariant); Events.OnAfterCreateTempShopifyProduct(Item, TempShopifyProduct, TempShopifyVariant, TempShopifyTag); end; @@ -240,6 +247,107 @@ codeunit 30174 "Shpfy Create Product" TempShopifyVariant.Insert(false); end; + local procedure FillOptionsForAllVariants(ItemNo: Code[20]; var TempShopifyVariant: Record "Shpfy Variant" temporary) + var + ItemAttribute: Record "Item Attribute"; + ItemAttributeValueMapping: Record "Item Attribute Value Mapping"; + ItemVariant: Record "Item Variant"; + ItemAttributeIds: List of [Integer]; + VariantCode: Code[10]; + begin + // Skip if UoM as Variant is enabled + if Shop."UoM as Variant" then + exit; + + // Step 1: Get Item Attributes defined at Item level with "As Option" (these define the schema) + ItemAttributeValueMapping.SetRange("Table ID", Database::Item); + ItemAttributeValueMapping.SetRange("No.", ItemNo); + if ItemAttributeValueMapping.FindSet() then + repeat + if ItemAttribute.Get(ItemAttributeValueMapping."Item Attribute ID") then + if (not ItemAttribute.Blocked) and (ItemAttribute."Shpfy Incl. in Product Sync" = ItemAttribute."Shpfy Incl. in Product Sync"::"As Option") then + if not ItemAttributeIds.Contains(ItemAttribute.ID) then + ItemAttributeIds.Add(ItemAttribute.ID); + until ItemAttributeValueMapping.Next() = 0; + + // Step 2.1: Checks - Exit if no attributes are defined with "As Option" + if ItemAttributeIds.Count() = 0 then + exit; + + // Step 2.2: Checks - Error if more than 3 attributes are defined with "As Option" + if ItemAttributeIds.Count() > 3 then + Error(TooManyAttributesAsOptionErr, ItemNo, ItemAttributeIds.Count()); + + // Step 3: For each variant, fill in the option values + if TempShopifyVariant.FindSet() then + repeat + // Get the variant code from Item Variant SystemId + VariantCode := ''; + if not IsNullGuid(TempShopifyVariant."Item Variant SystemId") then + if ItemVariant.GetBySystemId(TempShopifyVariant."Item Variant SystemId") then + VariantCode := ItemVariant.Code; + + FillOptionsFromItemAttributes(ItemNo, VariantCode, ItemAttributeIds, TempShopifyVariant); + TempShopifyVariant.Modify(false); + until TempShopifyVariant.Next() = 0; + end; + + local procedure FillOptionsFromItemAttributes(ItemNo: Code[20]; VariantCode: Code[10]; ItemAttributeIds: List of [Integer]; var TempShopifyVariant: Record "Shpfy Variant" temporary) + var + ItemAttribute: Record "Item Attribute"; + ItemAttributeValue: Record "Item Attribute Value"; + ItemAttributeValueMapping: Record "Item Attribute Value Mapping"; + ItemVarAttrValueMapping: Record "Item Var. Attr. Value Mapping"; + OptionIndex: Integer; + AttributeId: Integer; + begin + // For each attribute defined at Item level, get the value from Item Variant (if exists) or Item + OptionIndex := 1; + foreach AttributeId in ItemAttributeIds do + if ItemAttribute.Get(AttributeId) then begin + // Get the Option Value from Item Variant Attribute Value Mapping or Item Attribute Value Mapping + if VariantCode <> '' then begin + // Try to get value from Item Variant + ItemVarAttrValueMapping.SetRange("Item No.", ItemNo); + ItemVarAttrValueMapping.SetRange("Variant Code", VariantCode); + ItemVarAttrValueMapping.SetRange("Item Attribute ID", AttributeId); + if ItemVarAttrValueMapping.FindFirst() then + if ItemAttributeValue.Get(ItemVarAttrValueMapping."Item Attribute ID", ItemVarAttrValueMapping."Item Attribute Value ID") then + AssignOptionValue(TempShopifyVariant, OptionIndex, ItemAttribute.Name, ItemAttributeValue.Value); + end else begin + // Get value from Item Attribute Value Mapping (no variant) + ItemAttributeValueMapping.SetRange("Table ID", Database::Item); + ItemAttributeValueMapping.SetRange("No.", ItemNo); + ItemAttributeValueMapping.SetRange("Item Attribute ID", AttributeId); + if ItemAttributeValueMapping.FindFirst() then + if ItemAttributeValue.Get(ItemAttributeValueMapping."Item Attribute ID", ItemAttributeValueMapping."Item Attribute Value ID") then + AssignOptionValue(TempShopifyVariant, OptionIndex, ItemAttribute.Name, ItemAttributeValue.Value); + end; + OptionIndex += 1; + end; + end; + + local procedure AssignOptionValue(var TempShopifyVariant: Record "Shpfy Variant" temporary; OptionIndex: Integer; AttributeName: Text[250]; AttributeValue: Text[250]) + begin + case OptionIndex of + 1: + begin + TempShopifyVariant."Option 1 Name" := CopyStr(AttributeName, 1, MaxStrLen(TempShopifyVariant."Option 1 Name")); + TempShopifyVariant."Option 1 Value" := CopyStr(AttributeValue, 1, MaxStrLen(TempShopifyVariant."Option 1 Value")); + end; + 2: + begin + TempShopifyVariant."Option 2 Name" := CopyStr(AttributeName, 1, MaxStrLen(TempShopifyVariant."Option 2 Name")); + TempShopifyVariant."Option 2 Value" := CopyStr(AttributeValue, 1, MaxStrLen(TempShopifyVariant."Option 2 Value")); + end; + 3: + begin + TempShopifyVariant."Option 3 Name" := CopyStr(AttributeName, 1, MaxStrLen(TempShopifyVariant."Option 3 Name")); + TempShopifyVariant."Option 3 Value" := CopyStr(AttributeValue, 1, MaxStrLen(TempShopifyVariant."Option 3 Value")); + end; + end; + end; + /// /// Set Shop. /// From afaecf4698521104657a223241da5af189083334 Mon Sep 17 00:00:00 2001 From: jzaksauskas Date: Wed, 14 Jan 2026 09:30:03 +0200 Subject: [PATCH 03/21] Added functionality to check Item Attr Count and the duplicates --- .../Codeunits/ShpfyCreateProduct.Codeunit.al | 110 +++++++++++++----- 1 file changed, 81 insertions(+), 29 deletions(-) diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al index 08a2de589b..d2f99dcffb 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al @@ -36,7 +36,6 @@ codeunit 30174 "Shpfy Create Product" Getlocations: Boolean; ProductId: BigInteger; ItemVariantIsBlockedLbl: Label 'Item variant is blocked or sales blocked.'; - TooManyAttributesAsOptionErr: Label 'Item %1 has %2 attributes marked as "As Option". Shopify supports a maximum of 3 product options.', Comment = '%1 = Item No., %2 = Number of attributes'; trigger OnRun() var @@ -63,6 +62,9 @@ codeunit 30174 "Shpfy Create Product" TempShopifyVariant: Record "Shpfy Variant" temporary; TempShopifyTag: Record "Shpfy Tag" temporary; begin + if CheckProductOptionsInaccuraciesExists(Item) then + exit; + CreateTempProduct(Item, TempShopifyProduct, TempShopifyVariant, TempShopifyTag); if not VariantApi.FindShopifyProductVariant(TempShopifyProduct, TempShopifyVariant) then ProductId := ProductApi.CreateProduct(TempShopifyProduct, TempShopifyVariant, TempShopifyTag) @@ -171,7 +173,7 @@ codeunit 30174 "Shpfy Create Product" CreateTempShopifyVariantFromItem(Item, TempShopifyVariant); TempShopifyProduct.Insert(false); - FillOptionsForAllVariants(Item."No.", TempShopifyVariant); + FillProductOptionsForShopifyVariants(Item, TempShopifyVariant); Events.OnAfterCreateTempShopifyProduct(Item, TempShopifyProduct, TempShopifyVariant, TempShopifyTag); end; @@ -247,52 +249,90 @@ codeunit 30174 "Shpfy Create Product" TempShopifyVariant.Insert(false); end; - local procedure FillOptionsForAllVariants(ItemNo: Code[20]; var TempShopifyVariant: Record "Shpfy Variant" temporary) + local procedure CheckProductOptionsInaccuraciesExists(var Item: Record Item): Boolean + var + SkippedRecord: Codeunit "Shpfy Skipped Record"; + ItemAttributeIds: List of [Integer]; + TooManyAttributesAsOptionErr: Label 'Item %1 has %2 attributes marked as "As Option". Shopify supports a maximum of 3 product options.', Comment = '%1 = Item No., %2 = Number of attributes'; + DuplicateOptionCombinationErr: Label 'Item %1 has duplicate item variant attribute value combinations. Each variant must have a unique combination of option values.', Comment = '%1 = Item No.'; + begin + if Shop."UoM as Variant" then + exit(false); + + GetItemAttributeIDsMarkedAsOption(Item, ItemAttributeIds); + + if ItemAttributeIds.Count() > 3 then begin + SkippedRecord.LogSkippedRecord(Item.RecordId, StrSubstNo(TooManyAttributesAsOptionErr, Item."No.", ItemAttributeIds.Count()), Shop); + exit(true); + end; + + if CheckProductOptionDuplicatesExists(Item, ItemAttributeIds) then begin + SkippedRecord.LogSkippedRecord(Item.RecordId, StrSubstNo(DuplicateOptionCombinationErr, Item."No."), Shop); + exit(true); + end; + end; + + local procedure CheckProductOptionDuplicatesExists(Item: Record Item; ItemAttributeIds: List of [Integer]): Boolean + var + ItemAttributeValue: Record "Item Attribute Value"; + ItemVariant: Record "Item Variant"; + ItemVarAttrValueMapping: Record "Item Var. Attr. Value Mapping"; + VariantCombinations: Dictionary of [Text, Code[10]]; + CombinationKey: Text; + VariantCode: Code[10]; + AttributeId: Integer; + CombinationKeyTok: Label '%1:%2|', Locked = true, Comment = '%1 = Attribute ID, %2 = Attribute Value'; + begin + ItemVariant.SetRange("Item No.", Item."No."); + if ItemVariant.FindSet() then + repeat + VariantCode := ItemVariant.Code; + + CombinationKey := ''; + foreach AttributeId in ItemAttributeIds do begin + ItemVarAttrValueMapping.SetRange("Item No.", Item."No."); + ItemVarAttrValueMapping.SetRange("Variant Code", VariantCode); + ItemVarAttrValueMapping.SetRange("Item Attribute ID", AttributeId); + if ItemVarAttrValueMapping.FindFirst() then + if ItemAttributeValue.Get(ItemVarAttrValueMapping."Item Attribute ID", ItemVarAttrValueMapping."Item Attribute Value ID") then + CombinationKey += StrSubstNo(CombinationKeyTok, ItemAttributeValue."Attribute ID", ItemAttributeValue.Value); + end; + + if CombinationKey <> '' then + if VariantCombinations.ContainsKey(CombinationKey) then + exit(true) + else + VariantCombinations.Add(CombinationKey, VariantCode); + until ItemVariant.Next() = 0; + end; + + local procedure FillProductOptionsForShopifyVariants(Item: Record Item; var TempShopifyVariant: Record "Shpfy Variant" temporary) var - ItemAttribute: Record "Item Attribute"; - ItemAttributeValueMapping: Record "Item Attribute Value Mapping"; ItemVariant: Record "Item Variant"; ItemAttributeIds: List of [Integer]; VariantCode: Code[10]; begin - // Skip if UoM as Variant is enabled if Shop."UoM as Variant" then exit; - // Step 1: Get Item Attributes defined at Item level with "As Option" (these define the schema) - ItemAttributeValueMapping.SetRange("Table ID", Database::Item); - ItemAttributeValueMapping.SetRange("No.", ItemNo); - if ItemAttributeValueMapping.FindSet() then - repeat - if ItemAttribute.Get(ItemAttributeValueMapping."Item Attribute ID") then - if (not ItemAttribute.Blocked) and (ItemAttribute."Shpfy Incl. in Product Sync" = ItemAttribute."Shpfy Incl. in Product Sync"::"As Option") then - if not ItemAttributeIds.Contains(ItemAttribute.ID) then - ItemAttributeIds.Add(ItemAttribute.ID); - until ItemAttributeValueMapping.Next() = 0; + GetItemAttributeIDsMarkedAsOption(Item, ItemAttributeIds); - // Step 2.1: Checks - Exit if no attributes are defined with "As Option" if ItemAttributeIds.Count() = 0 then exit; - // Step 2.2: Checks - Error if more than 3 attributes are defined with "As Option" - if ItemAttributeIds.Count() > 3 then - Error(TooManyAttributesAsOptionErr, ItemNo, ItemAttributeIds.Count()); - - // Step 3: For each variant, fill in the option values if TempShopifyVariant.FindSet() then repeat - // Get the variant code from Item Variant SystemId VariantCode := ''; if not IsNullGuid(TempShopifyVariant."Item Variant SystemId") then if ItemVariant.GetBySystemId(TempShopifyVariant."Item Variant SystemId") then VariantCode := ItemVariant.Code; - FillOptionsFromItemAttributes(ItemNo, VariantCode, ItemAttributeIds, TempShopifyVariant); + FillProductOptionsFromItemAttributes(Item."No.", VariantCode, ItemAttributeIds, TempShopifyVariant); TempShopifyVariant.Modify(false); until TempShopifyVariant.Next() = 0; end; - local procedure FillOptionsFromItemAttributes(ItemNo: Code[20]; VariantCode: Code[10]; ItemAttributeIds: List of [Integer]; var TempShopifyVariant: Record "Shpfy Variant" temporary) + local procedure FillProductOptionsFromItemAttributes(ItemNo: Code[20]; VariantCode: Code[10]; ItemAttributeIds: List of [Integer]; var TempShopifyVariant: Record "Shpfy Variant" temporary) var ItemAttribute: Record "Item Attribute"; ItemAttributeValue: Record "Item Attribute Value"; @@ -301,13 +341,10 @@ codeunit 30174 "Shpfy Create Product" OptionIndex: Integer; AttributeId: Integer; begin - // For each attribute defined at Item level, get the value from Item Variant (if exists) or Item OptionIndex := 1; foreach AttributeId in ItemAttributeIds do if ItemAttribute.Get(AttributeId) then begin - // Get the Option Value from Item Variant Attribute Value Mapping or Item Attribute Value Mapping if VariantCode <> '' then begin - // Try to get value from Item Variant ItemVarAttrValueMapping.SetRange("Item No.", ItemNo); ItemVarAttrValueMapping.SetRange("Variant Code", VariantCode); ItemVarAttrValueMapping.SetRange("Item Attribute ID", AttributeId); @@ -315,7 +352,6 @@ codeunit 30174 "Shpfy Create Product" if ItemAttributeValue.Get(ItemVarAttrValueMapping."Item Attribute ID", ItemVarAttrValueMapping."Item Attribute Value ID") then AssignOptionValue(TempShopifyVariant, OptionIndex, ItemAttribute.Name, ItemAttributeValue.Value); end else begin - // Get value from Item Attribute Value Mapping (no variant) ItemAttributeValueMapping.SetRange("Table ID", Database::Item); ItemAttributeValueMapping.SetRange("No.", ItemNo); ItemAttributeValueMapping.SetRange("Item Attribute ID", AttributeId); @@ -348,6 +384,22 @@ codeunit 30174 "Shpfy Create Product" end; end; + local procedure GetItemAttributeIDsMarkedAsOption(Item: Record Item; var ItemAttributeIds: List of [Integer]) + var + ItemAttribute: Record "Item Attribute"; + ItemAttributeValueMapping: Record "Item Attribute Value Mapping"; + begin + ItemAttributeValueMapping.SetRange("Table ID", Database::Item); + ItemAttributeValueMapping.SetRange("No.", Item."No."); + if ItemAttributeValueMapping.FindSet() then + repeat + if ItemAttribute.Get(ItemAttributeValueMapping."Item Attribute ID") then + if (not ItemAttribute.Blocked) and (ItemAttribute."Shpfy Incl. in Product Sync" = ItemAttribute."Shpfy Incl. in Product Sync"::"As Option") then + if not ItemAttributeIds.Contains(ItemAttribute.ID) then + ItemAttributeIds.Add(ItemAttribute.ID); + until ItemAttributeValueMapping.Next() = 0; + end; + /// /// Set Shop. /// From bafe0eae003d5af6de5a39a36daddfc4669d9d24 Mon Sep 17 00:00:00 2001 From: jzaksauskas Date: Wed, 14 Jan 2026 13:39:29 +0200 Subject: [PATCH 04/21] Added checks for missing attribute values and limits on product options in Shopify product creation --- .../Codeunits/ShpfyCreateProduct.Codeunit.al | 87 +++++++++++++++---- 1 file changed, 69 insertions(+), 18 deletions(-) diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al index d2f99dcffb..a35dd99f1d 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al @@ -253,6 +253,7 @@ codeunit 30174 "Shpfy Create Product" var SkippedRecord: Codeunit "Shpfy Skipped Record"; ItemAttributeIds: List of [Integer]; + SkippedReason: Text[250]; TooManyAttributesAsOptionErr: Label 'Item %1 has %2 attributes marked as "As Option". Shopify supports a maximum of 3 product options.', Comment = '%1 = Item No., %2 = Number of attributes'; DuplicateOptionCombinationErr: Label 'Item %1 has duplicate item variant attribute value combinations. Each variant must have a unique combination of option values.', Comment = '%1 = Item No.'; begin @@ -261,11 +262,19 @@ codeunit 30174 "Shpfy Create Product" GetItemAttributeIDsMarkedAsOption(Item, ItemAttributeIds); + if ItemAttributeIds.Count() = 0 then + exit(false); + if ItemAttributeIds.Count() > 3 then begin SkippedRecord.LogSkippedRecord(Item.RecordId, StrSubstNo(TooManyAttributesAsOptionErr, Item."No.", ItemAttributeIds.Count()), Shop); exit(true); end; + if CheckItemVariantsMissingAttributeValues(Item, ItemAttributeIds, SkippedReason) then begin + SkippedRecord.LogSkippedRecord(Item.RecordId, SkippedReason, Shop); + exit(true); + end; + if CheckProductOptionDuplicatesExists(Item, ItemAttributeIds) then begin SkippedRecord.LogSkippedRecord(Item.RecordId, StrSubstNo(DuplicateOptionCombinationErr, Item."No."), Shop); exit(true); @@ -284,26 +293,68 @@ codeunit 30174 "Shpfy Create Product" CombinationKeyTok: Label '%1:%2|', Locked = true, Comment = '%1 = Attribute ID, %2 = Attribute Value'; begin ItemVariant.SetRange("Item No.", Item."No."); - if ItemVariant.FindSet() then - repeat - VariantCode := ItemVariant.Code; + if not ItemVariant.FindSet() then + exit(false); - CombinationKey := ''; - foreach AttributeId in ItemAttributeIds do begin - ItemVarAttrValueMapping.SetRange("Item No.", Item."No."); - ItemVarAttrValueMapping.SetRange("Variant Code", VariantCode); - ItemVarAttrValueMapping.SetRange("Item Attribute ID", AttributeId); - if ItemVarAttrValueMapping.FindFirst() then - if ItemAttributeValue.Get(ItemVarAttrValueMapping."Item Attribute ID", ItemVarAttrValueMapping."Item Attribute Value ID") then - CombinationKey += StrSubstNo(CombinationKeyTok, ItemAttributeValue."Attribute ID", ItemAttributeValue.Value); - end; + repeat + VariantCode := ItemVariant.Code; + + CombinationKey := ''; + foreach AttributeId in ItemAttributeIds do begin + ItemVarAttrValueMapping.SetRange("Item No.", Item."No."); + ItemVarAttrValueMapping.SetRange("Variant Code", VariantCode); + ItemVarAttrValueMapping.SetRange("Item Attribute ID", AttributeId); + if ItemVarAttrValueMapping.FindFirst() then + if ItemAttributeValue.Get(ItemVarAttrValueMapping."Item Attribute ID", ItemVarAttrValueMapping."Item Attribute Value ID") then + CombinationKey += StrSubstNo(CombinationKeyTok, ItemAttributeValue."Attribute ID", ItemAttributeValue.Value); + end; - if CombinationKey <> '' then - if VariantCombinations.ContainsKey(CombinationKey) then - exit(true) - else - VariantCombinations.Add(CombinationKey, VariantCode); - until ItemVariant.Next() = 0; + if CombinationKey <> '' then + if VariantCombinations.ContainsKey(CombinationKey) then + exit(true) + else + VariantCombinations.Add(CombinationKey, VariantCode); + until ItemVariant.Next() = 0; + end; + + local procedure CheckItemVariantsMissingAttributeValues(Item: Record Item; ItemAttributeIds: List of [Integer]; var SkippedReason: Text[250]): Boolean + var + ItemAttribute: Record "Item Attribute"; + ItemVariant: Record "Item Variant"; + ItemVarAttrValueMapping: Record "Item Var. Attr. Value Mapping"; + ItemAttributeValue: Record "Item Attribute Value"; + AttributeId: Integer; + MissingVariantCode: Code[10]; + MissingAttributeName: Text[250]; + MissingAttributeErr: Label 'Item %1 Variant %2 is missing an attribute "%3". All item variants must have must have item attributes marked as "As Option".', Comment = '%1 = Item No., %2 = Variant Code, %3 = Attribute Name'; + MissingAttributeValueErr: Label 'Item %1 Variant %2 is missing a value for attribute "%3". All item variants must have values for attributes marked as "As Option".', Comment = '%1 = Item No., %2 = Variant Code, %3 = Attribute Name'; + begin + ItemVariant.SetRange("Item No.", Item."No."); + if not ItemVariant.FindSet() then + exit(false); + + repeat + foreach AttributeId in ItemAttributeIds do begin + ItemVarAttrValueMapping.SetRange("Item No.", Item."No."); + ItemVarAttrValueMapping.SetRange("Variant Code", ItemVariant.Code); + ItemVarAttrValueMapping.SetRange("Item Attribute ID", AttributeId); + if not ItemVarAttrValueMapping.FindFirst() then begin + MissingVariantCode := ItemVariant.Code; + if ItemAttribute.Get(AttributeId) then + MissingAttributeName := ItemAttribute.Name; + SkippedReason := StrSubstNo(MissingAttributeErr, Item."No.", MissingVariantCode, MissingAttributeName); + exit(true); + end else + if ItemAttributeValue.Get(ItemVarAttrValueMapping."Item Attribute ID", ItemVarAttrValueMapping."Item Attribute Value ID") then + if ItemAttributeValue.Value = '' then begin + MissingVariantCode := ItemVariant.Code; + if ItemAttribute.Get(AttributeId) then + MissingAttributeName := ItemAttribute.Name; + SkippedReason := StrSubstNo(MissingAttributeValueErr, Item."No.", MissingVariantCode, MissingAttributeName); + exit(true); + end; + end; + until ItemVariant.Next() = 0; end; local procedure FillProductOptionsForShopifyVariants(Item: Record Item; var TempShopifyVariant: Record "Shpfy Variant" temporary) From 66ae67e37c88539449567d10346ebae7af0cab63 Mon Sep 17 00:00:00 2001 From: jzaksauskas Date: Wed, 14 Jan 2026 16:32:28 +0200 Subject: [PATCH 05/21] Refactor product option checks and improve attribute validation for Shopify product creation --- .../Codeunits/ShpfyCreateProduct.Codeunit.al | 78 ++++++++++++------- 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al index a35dd99f1d..20d5991581 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al @@ -62,7 +62,7 @@ codeunit 30174 "Shpfy Create Product" TempShopifyVariant: Record "Shpfy Variant" temporary; TempShopifyTag: Record "Shpfy Tag" temporary; begin - if CheckProductOptionsInaccuraciesExists(Item) then + if not CheckItemAttributesCompatibleForProductOptions(Item) then exit; CreateTempProduct(Item, TempShopifyProduct, TempShopifyVariant, TempShopifyTag); @@ -172,8 +172,8 @@ codeunit 30174 "Shpfy Create Product" end else CreateTempShopifyVariantFromItem(Item, TempShopifyVariant); + FillProductOptionsForShopifyVariants(Item, TempShopifyVariant, TempShopifyProduct); TempShopifyProduct.Insert(false); - FillProductOptionsForShopifyVariants(Item, TempShopifyVariant); Events.OnAfterCreateTempShopifyProduct(Item, TempShopifyProduct, TempShopifyVariant, TempShopifyTag); end; @@ -249,7 +249,7 @@ codeunit 30174 "Shpfy Create Product" TempShopifyVariant.Insert(false); end; - local procedure CheckProductOptionsInaccuraciesExists(var Item: Record Item): Boolean + internal procedure CheckItemAttributesCompatibleForProductOptions(Item: Record Item): Boolean var SkippedRecord: Codeunit "Shpfy Skipped Record"; ItemAttributeIds: List of [Integer]; @@ -258,27 +258,29 @@ codeunit 30174 "Shpfy Create Product" DuplicateOptionCombinationErr: Label 'Item %1 has duplicate item variant attribute value combinations. Each variant must have a unique combination of option values.', Comment = '%1 = Item No.'; begin if Shop."UoM as Variant" then - exit(false); + exit(true); GetItemAttributeIDsMarkedAsOption(Item, ItemAttributeIds); if ItemAttributeIds.Count() = 0 then - exit(false); + exit(true); if ItemAttributeIds.Count() > 3 then begin SkippedRecord.LogSkippedRecord(Item.RecordId, StrSubstNo(TooManyAttributesAsOptionErr, Item."No.", ItemAttributeIds.Count()), Shop); - exit(true); + exit(false); end; - if CheckItemVariantsMissingAttributeValues(Item, ItemAttributeIds, SkippedReason) then begin + if CheckMissingItemAttributeValues(Item, ItemAttributeIds, SkippedReason) then begin SkippedRecord.LogSkippedRecord(Item.RecordId, SkippedReason, Shop); - exit(true); + exit(false); end; if CheckProductOptionDuplicatesExists(Item, ItemAttributeIds) then begin SkippedRecord.LogSkippedRecord(Item.RecordId, StrSubstNo(DuplicateOptionCombinationErr, Item."No."), Shop); - exit(true); + exit(false); end; + + exit(true); end; local procedure CheckProductOptionDuplicatesExists(Item: Record Item; ItemAttributeIds: List of [Integer]): Boolean @@ -317,47 +319,61 @@ codeunit 30174 "Shpfy Create Product" until ItemVariant.Next() = 0; end; - local procedure CheckItemVariantsMissingAttributeValues(Item: Record Item; ItemAttributeIds: List of [Integer]; var SkippedReason: Text[250]): Boolean + local procedure CheckMissingItemAttributeValues(Item: Record Item; ItemAttributeIds: List of [Integer]; var SkippedReason: Text[250]): Boolean var ItemAttribute: Record "Item Attribute"; ItemVariant: Record "Item Variant"; + ItemAttributeValueMapping: Record "Item Attribute Value Mapping"; ItemVarAttrValueMapping: Record "Item Var. Attr. Value Mapping"; ItemAttributeValue: Record "Item Attribute Value"; AttributeId: Integer; MissingVariantCode: Code[10]; MissingAttributeName: Text[250]; MissingAttributeErr: Label 'Item %1 Variant %2 is missing an attribute "%3". All item variants must have must have item attributes marked as "As Option".', Comment = '%1 = Item No., %2 = Variant Code, %3 = Attribute Name'; - MissingAttributeValueErr: Label 'Item %1 Variant %2 is missing a value for attribute "%3". All item variants must have values for attributes marked as "As Option".', Comment = '%1 = Item No., %2 = Variant Code, %3 = Attribute Name'; + MissingItemAttributeValueErr: Label 'Item %1 is missing a value for attribute "%2". Item must have values for attributes marked as "As Option".', Comment = '%1 = Item No., %2 = Attribute Name'; + MissingItemVarAttributeValueErr: Label 'Item %1 Variant %2 is missing a value for attribute "%3". All item variants must have values for attributes marked as "As Option".', Comment = '%1 = Item No., %2 = Variant Code, %3 = Attribute Name'; begin ItemVariant.SetRange("Item No.", Item."No."); - if not ItemVariant.FindSet() then - exit(false); - - repeat + if ItemVariant.FindSet() then + repeat + foreach AttributeId in ItemAttributeIds do begin + ItemVarAttrValueMapping.SetRange("Item No.", Item."No."); + ItemVarAttrValueMapping.SetRange("Variant Code", ItemVariant.Code); + ItemVarAttrValueMapping.SetRange("Item Attribute ID", AttributeId); + if not ItemVarAttrValueMapping.FindFirst() then begin + MissingVariantCode := ItemVariant.Code; + if ItemAttribute.Get(AttributeId) then + MissingAttributeName := ItemAttribute.Name; + SkippedReason := StrSubstNo(MissingAttributeErr, Item."No.", MissingVariantCode, MissingAttributeName); + exit(true); + end else + if ItemAttributeValue.Get(ItemVarAttrValueMapping."Item Attribute ID", ItemVarAttrValueMapping."Item Attribute Value ID") then + if ItemAttributeValue.Value = '' then begin + MissingVariantCode := ItemVariant.Code; + if ItemAttribute.Get(AttributeId) then + MissingAttributeName := ItemAttribute.Name; + SkippedReason := StrSubstNo(MissingItemVarAttributeValueErr, Item."No.", MissingVariantCode, MissingAttributeName); + exit(true); + end; + end; + until ItemVariant.Next() = 0 + else foreach AttributeId in ItemAttributeIds do begin - ItemVarAttrValueMapping.SetRange("Item No.", Item."No."); - ItemVarAttrValueMapping.SetRange("Variant Code", ItemVariant.Code); - ItemVarAttrValueMapping.SetRange("Item Attribute ID", AttributeId); - if not ItemVarAttrValueMapping.FindFirst() then begin - MissingVariantCode := ItemVariant.Code; - if ItemAttribute.Get(AttributeId) then - MissingAttributeName := ItemAttribute.Name; - SkippedReason := StrSubstNo(MissingAttributeErr, Item."No.", MissingVariantCode, MissingAttributeName); - exit(true); - end else - if ItemAttributeValue.Get(ItemVarAttrValueMapping."Item Attribute ID", ItemVarAttrValueMapping."Item Attribute Value ID") then + ItemAttributeValueMapping.SetRange("Table ID", Database::Item); + ItemAttributeValueMapping.SetRange("No.", Item."No."); + ItemAttributeValueMapping.SetRange("Item Attribute ID", AttributeId); + if ItemAttributeValueMapping.FindFirst() then + if ItemAttributeValue.Get(ItemAttributeValueMapping."Item Attribute ID", ItemAttributeValueMapping."Item Attribute Value ID") then if ItemAttributeValue.Value = '' then begin - MissingVariantCode := ItemVariant.Code; if ItemAttribute.Get(AttributeId) then MissingAttributeName := ItemAttribute.Name; - SkippedReason := StrSubstNo(MissingAttributeValueErr, Item."No.", MissingVariantCode, MissingAttributeName); + SkippedReason := StrSubstNo(MissingItemAttributeValueErr, Item."No.", MissingAttributeName); exit(true); end; end; - until ItemVariant.Next() = 0; end; - local procedure FillProductOptionsForShopifyVariants(Item: Record Item; var TempShopifyVariant: Record "Shpfy Variant" temporary) + local procedure FillProductOptionsForShopifyVariants(Item: Record Item; var TempShopifyVariant: Record "Shpfy Variant" temporary; var TempShopifyProduct: Record "Shpfy Product" temporary) var ItemVariant: Record "Item Variant"; ItemAttributeIds: List of [Integer]; @@ -381,6 +397,8 @@ codeunit 30174 "Shpfy Create Product" FillProductOptionsFromItemAttributes(Item."No.", VariantCode, ItemAttributeIds, TempShopifyVariant); TempShopifyVariant.Modify(false); until TempShopifyVariant.Next() = 0; + + TempShopifyProduct."Has Variants" := true; end; local procedure FillProductOptionsFromItemAttributes(ItemNo: Code[20]; VariantCode: Code[10]; ItemAttributeIds: List of [Integer]; var TempShopifyVariant: Record "Shpfy Variant" temporary) From 92e5590bfce895634f2954e02416291c98e921d0 Mon Sep 17 00:00:00 2001 From: jzaksauskas Date: Wed, 14 Jan 2026 16:35:13 +0200 Subject: [PATCH 06/21] Add compatibility check for item attributes before adding to Shopify --- .../src/Products/Page Extensions/ShpfyItemCard.PageExt.al | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemCard.PageExt.al b/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemCard.PageExt.al index 495e86c364..fa664103df 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemCard.PageExt.al +++ b/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemCard.PageExt.al @@ -48,10 +48,15 @@ pageextension 30119 "Shpfy Item Card" extends "Item Card" trigger OnAction() var Shop: Record "Shpfy Shop"; + CreateProduct: Codeunit "Shpfy Create Product"; SyncProducts: Codeunit "Shpfy Sync Products"; begin if SyncProducts.ConfirmAddItemToShopify(Rec, Shop) then begin + if not CreateProduct.CheckItemAttributesCompatibleForProductOptions(Rec) then + exit; + SyncProducts.AddItemToShopify(Rec, Shop); + if Confirm(ViewInShopifyLbl) then Hyperlink(SyncProducts.GetProductUrl(Rec, Shop.Code)); CurrPage.Update(false); From 4d2291bf5a83584b56dfcb011c885cc83c4d6e8c Mon Sep 17 00:00:00 2001 From: jzaksauskas Date: Thu, 15 Jan 2026 14:04:50 +0200 Subject: [PATCH 07/21] Renamed procedure and added documentations for internal procedures --- .../Codeunits/ShpfyCreateProduct.Codeunit.al | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al index 20d5991581..fbc3b36e69 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al @@ -249,6 +249,12 @@ codeunit 30174 "Shpfy Create Product" TempShopifyVariant.Insert(false); end; + + /// + /// Checks if item attributes marked as "As Option" are compatible to be used as product options in Shopify. + /// + /// The item to check. + /// True if the item attributes are compatible, false otherwise. internal procedure CheckItemAttributesCompatibleForProductOptions(Item: Record Item): Boolean var SkippedRecord: Codeunit "Shpfy Skipped Record"; @@ -419,20 +425,27 @@ codeunit 30174 "Shpfy Create Product" ItemVarAttrValueMapping.SetRange("Item Attribute ID", AttributeId); if ItemVarAttrValueMapping.FindFirst() then if ItemAttributeValue.Get(ItemVarAttrValueMapping."Item Attribute ID", ItemVarAttrValueMapping."Item Attribute Value ID") then - AssignOptionValue(TempShopifyVariant, OptionIndex, ItemAttribute.Name, ItemAttributeValue.Value); + AssignProductOptionValues(TempShopifyVariant, OptionIndex, ItemAttribute.Name, ItemAttributeValue.Value); end else begin ItemAttributeValueMapping.SetRange("Table ID", Database::Item); ItemAttributeValueMapping.SetRange("No.", ItemNo); ItemAttributeValueMapping.SetRange("Item Attribute ID", AttributeId); if ItemAttributeValueMapping.FindFirst() then if ItemAttributeValue.Get(ItemAttributeValueMapping."Item Attribute ID", ItemAttributeValueMapping."Item Attribute Value ID") then - AssignOptionValue(TempShopifyVariant, OptionIndex, ItemAttribute.Name, ItemAttributeValue.Value); + AssignProductOptionValues(TempShopifyVariant, OptionIndex, ItemAttribute.Name, ItemAttributeValue.Value); end; OptionIndex += 1; end; end; - local procedure AssignOptionValue(var TempShopifyVariant: Record "Shpfy Variant" temporary; OptionIndex: Integer; AttributeName: Text[250]; AttributeValue: Text[250]) + /// + /// Assigns product option values to the temporary Shopify variant based on the option index. + /// + /// Parameter of type Record "Shpfy Variant" temporary. + /// Parameter of type Integer. + /// Parameter of type Text[250]. + /// Parameter of type Text[250]. + internal procedure AssignProductOptionValues(var TempShopifyVariant: Record "Shpfy Variant" temporary; OptionIndex: Integer; AttributeName: Text[250]; AttributeValue: Text[250]) begin case OptionIndex of 1: From 7f897f5be5d494728abe47999fa9bcd1f941f674 Mon Sep 17 00:00:00 2001 From: jzaksauskas Date: Thu, 15 Jan 2026 14:06:16 +0200 Subject: [PATCH 08/21] Added functionality for product variant creation based on Item Attributes (checks for existing product options and ensuring unique combinations) --- .../ShpfyCreateItemAsVariant.Codeunit.al | 124 +++++++++++++++++- 1 file changed, 117 insertions(+), 7 deletions(-) diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al index 6ecec599a4..2a989fd9be 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al @@ -5,6 +5,7 @@ namespace Microsoft.Integration.Shopify; +using Microsoft.Inventory.Item.Attribute; using Microsoft.Inventory.Item; codeunit 30343 "Shpfy Create Item As Variant" @@ -34,6 +35,11 @@ codeunit 30343 "Shpfy Create Item As Variant" internal procedure CreateVariantFromItem(var Item: Record "Item") var TempShopifyVariant: Record "Shpfy Variant" temporary; + ProductOptions: Dictionary of [Integer, Text]; + ExistingProductOptionValues: Dictionary of [Text, Text]; + ProductOptionIndex: Integer; + VariantOptionTok: Label 'Variant', Locked = true; + TitleOptionTok: Label 'Title', Locked = true; begin if Item.SystemId = ShopifyProduct."Item SystemId" then exit; @@ -41,14 +47,26 @@ codeunit 30343 "Shpfy Create Item As Variant" CreateProduct.CreateTempShopifyVariantFromItem(Item, TempShopifyVariant); TempShopifyVariant.Title := Item."No."; - if not ShopifyProduct."Has Variants" and (OptionName = 'Title') then begin + if not ShopifyProduct."Has Variants" and (OptionName = TitleOptionTok) then begin // Shopify automatically deletes the default variant (Title) when adding a new one so first we need to update the default variant to have a different name (Variant) - UpdateProductOption('Variant'); - TempShopifyVariant."Option 1 Name" := 'Variant'; + UpdateProductOption(VariantOptionTok); + TempShopifyVariant."Option 1 Name" := VariantOptionTok; end else TempShopifyVariant."Option 1 Name" := CopyStr(OptionName, 1, MaxStrLen(TempShopifyVariant."Option 1 Name")); TempShopifyVariant."Option 1 Value" := Item."No."; + + if ShopifyProduct."Has Variants" and (OptionName <> VariantOptionTok) then begin + CollectExistingProductVariantOptions(ProductOptions, ExistingProductOptionValues); + + for ProductOptionIndex := 1 to ProductOptions.Count() do + if not AssignProductOptionValuesToTempProductVariant(TempShopifyVariant, Item, ProductOptions, ProductOptionIndex) then + exit; + + if not CheckProdOptionsCombinationUnique(TempShopifyVariant, ExistingProductOptionValues, Item) then + exit; + end; + Events.OnAfterCreateTempShopifyVariant(Item, TempShopifyVariant); TempShopifyVariant.Modify(); @@ -58,6 +76,102 @@ codeunit 30343 "Shpfy Create Item As Variant" end; end; + local procedure CollectExistingProductVariantOptions(var ProductOptions: Dictionary of [Integer, Text]; var ExistingProductOptionValues: Dictionary of [Text, Text]) + var + ShopifyVariant: Record "Shpfy Variant"; + ItemAttribute: Record "Item Attribute"; + CombinationKey: Text; + begin + ShopifyVariant.SetRange("Product Id", ShopifyProduct.Id); + if ShopifyVariant.FindSet() then + repeat + CombinationKey := BuildCombinationKey( + ShopifyVariant."Option 1 Name", ShopifyVariant."Option 1 Value", + ShopifyVariant."Option 2 Name", ShopifyVariant."Option 2 Value", + ShopifyVariant."Option 3 Name", ShopifyVariant."Option 3 Value"); + + if not ExistingProductOptionValues.ContainsKey(CombinationKey) then + ExistingProductOptionValues.Add(CombinationKey, ShopifyVariant."Variant Code"); + until ShopifyVariant.Next() = 0; + + if ShopifyVariant."Option 1 Name" <> '' then begin + ItemAttribute.SetRange(Name, ShopifyVariant."Option 1 Name"); + if ItemAttribute.FindFirst() then + ProductOptions.Add(ItemAttribute.ID, ShopifyVariant."Option 1 Name"); + end; + if ShopifyVariant."Option 2 Name" <> '' then begin + ItemAttribute.SetRange(Name, ShopifyVariant."Option 2 Name"); + if ItemAttribute.FindFirst() then + ProductOptions.Add(ItemAttribute.ID, ShopifyVariant."Option 2 Name"); + end; + if ShopifyVariant."Option 3 Name" <> '' then begin + ItemAttribute.SetRange(Name, ShopifyVariant."Option 3 Name"); + if ItemAttribute.FindFirst() then + ProductOptions.Add(ItemAttribute.ID, ShopifyVariant."Option 3 Name"); + end; + end; + + local procedure BuildCombinationKey(Option1Name: Text; Option1Value: Text; Option2Name: Text; Option2Value: Text; Option3Name: Text; Option3Value: Text): Text + var + CombinationKey: Text; + KeyPartTok: Label '%1:%2|', Locked = true; + begin + if Option1Name <> '' then + CombinationKey += StrSubstNo(KeyPartTok, Option1Name, Option1Value); + if Option2Name <> '' then + CombinationKey += StrSubstNo(KeyPartTok, Option2Name, Option2Value); + if Option3Name <> '' then + CombinationKey += StrSubstNo(KeyPartTok, Option3Name, Option3Value); + + exit(CombinationKey); + end; + + local procedure AssignProductOptionValuesToTempProductVariant(var TempShopifyVariant: Record "Shpfy Variant" temporary; Item: Record "Item"; ProductOptions: Dictionary of [Integer, Text]; ProductOptionIndex: Integer): Boolean + var + ItemAttributeValueMapping: Record "Item Attribute Value Mapping"; + ItemAttributeValue: Record "Item Attribute Value"; + SkippedRecord: Codeunit "Shpfy Skipped Record"; + ItemWithoutRequiredAttributeErr: Label 'Item %1 cannot be added as a product variant because it does not have required attributes.', Comment = '%1 = Item No.'; + ItemWithoutRequiredAttributeValueErr: Label 'Item %1 cannot be added as a product variant because it does not have a value for the required attributes.', Comment = '%1 = Item No.'; + begin + ItemAttributeValueMapping.SetRange("Table ID", Database::Item); + ItemAttributeValueMapping.SetRange("No.", Item."No."); + ItemAttributeValueMapping.SetRange("Item Attribute ID", ProductOptions.Keys.Get(ProductOptionIndex)); + if ItemAttributeValueMapping.FindFirst() then begin + if ItemAttributeValue.Get(ItemAttributeValueMapping."Item Attribute ID", ItemAttributeValueMapping."Item Attribute Value ID") then begin + ItemAttributeValue.CalcFields("Attribute Name"); + CreateProduct.AssignProductOptionValues(TempShopifyVariant, ProductOptionIndex, ItemAttributeValue."Attribute Name", ItemAttributeValue.Value); + end else begin + SkippedRecord.LogSkippedRecord(Item.RecordId(), StrSubstNo(ItemWithoutRequiredAttributeValueErr, Item."No."), Shop); + exit(false); + end; + end else begin + SkippedRecord.LogSkippedRecord(Item.RecordId(), StrSubstNo(ItemWithoutRequiredAttributeErr, Item."No."), Shop); + exit(false); + end; + + exit(true); + end; + + local procedure CheckProdOptionsCombinationUnique(var TempShopifyVariant: Record "Shpfy Variant" temporary; ExistingProductOptionValues: Dictionary of [Text, Text]; Item: Record "Item"): Boolean + var + SkippedRecord: Codeunit "Shpfy Skipped Record"; + CombinationKey: Text; + DuplicateCombinationErr: Label 'Item %1 cannot be added as a product variant because another variant already has the same option values.', Comment = '%1 = Item No.'; + begin + CombinationKey := BuildCombinationKey( + TempShopifyVariant."Option 1 Name", TempShopifyVariant."Option 1 Value", + TempShopifyVariant."Option 2 Name", TempShopifyVariant."Option 2 Value", + TempShopifyVariant."Option 3 Name", TempShopifyVariant."Option 3 Value"); + + if ExistingProductOptionValues.ContainsKey(CombinationKey) then begin + SkippedRecord.LogSkippedRecord(Item.RecordId(), StrSubstNo(DuplicateCombinationErr, Item."No."), Shop); + exit(false); + end; + + exit(true); + end; + /// /// Checks if items can be added as variants to the product. The items cannot be added as variants if: /// - The product has more than one option. @@ -67,7 +181,6 @@ codeunit 30343 "Shpfy Create Item As Variant" var CommunicationMgt: Codeunit "Shpfy Communication Mgt."; Options: Dictionary of [Text, Text]; - MultipleOptionsErr: Label 'The product has more than one option. Items cannot be added as variants to a product with multiple options.'; UOMAsVariantEnabledErr: Label 'Items cannot be added as variants to a product with the "%1" setting enabled for this store.', Comment = '%1 - UoM as Variant field caption'; begin if Shop."UoM as Variant" then @@ -75,9 +188,6 @@ codeunit 30343 "Shpfy Create Item As Variant" Options := ProductApi.GetProductOptions(ShopifyProduct.Id); - if Options.Count > 1 then - Error(MultipleOptionsErr); - OptionId := CommunicationMgt.GetIdOfGId(Options.Keys.Get(1)); OptionName := Options.Values.Get(1); end; From fa943e547bdd11e852eb8edfc340329167a0068e Mon Sep 17 00:00:00 2001 From: jzaksauskas Date: Fri, 16 Jan 2026 10:03:51 +0200 Subject: [PATCH 09/21] Rename few procedures --- .../Codeunits/ShpfyCreateItemAsVariant.Codeunit.al | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al index 2a989fd9be..9d2b156963 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al @@ -57,13 +57,13 @@ codeunit 30343 "Shpfy Create Item As Variant" if ShopifyProduct."Has Variants" and (OptionName <> VariantOptionTok) then begin - CollectExistingProductVariantOptions(ProductOptions, ExistingProductOptionValues); + CollectExistingProductVariantOptionValues(ProductOptions, ExistingProductOptionValues); for ProductOptionIndex := 1 to ProductOptions.Count() do if not AssignProductOptionValuesToTempProductVariant(TempShopifyVariant, Item, ProductOptions, ProductOptionIndex) then exit; - if not CheckProdOptionsCombinationUnique(TempShopifyVariant, ExistingProductOptionValues, Item) then + if not CheckProductOptionsCombinationUnique(TempShopifyVariant, ExistingProductOptionValues, Item) then exit; end; @@ -76,7 +76,8 @@ codeunit 30343 "Shpfy Create Item As Variant" end; end; - local procedure CollectExistingProductVariantOptions(var ProductOptions: Dictionary of [Integer, Text]; var ExistingProductOptionValues: Dictionary of [Text, Text]) + #region Shopify Product Options as Item/Variant Attributes + local procedure CollectExistingProductVariantOptionValues(var ProductOptions: Dictionary of [Integer, Text]; var ExistingProductOptionValues: Dictionary of [Text, Text]) var ShopifyVariant: Record "Shpfy Variant"; ItemAttribute: Record "Item Attribute"; @@ -130,6 +131,7 @@ codeunit 30343 "Shpfy Create Item As Variant" var ItemAttributeValueMapping: Record "Item Attribute Value Mapping"; ItemAttributeValue: Record "Item Attribute Value"; + ProductExport: Codeunit "Shpfy Product Export"; SkippedRecord: Codeunit "Shpfy Skipped Record"; ItemWithoutRequiredAttributeErr: Label 'Item %1 cannot be added as a product variant because it does not have required attributes.', Comment = '%1 = Item No.'; ItemWithoutRequiredAttributeValueErr: Label 'Item %1 cannot be added as a product variant because it does not have a value for the required attributes.', Comment = '%1 = Item No.'; @@ -140,7 +142,7 @@ codeunit 30343 "Shpfy Create Item As Variant" if ItemAttributeValueMapping.FindFirst() then begin if ItemAttributeValue.Get(ItemAttributeValueMapping."Item Attribute ID", ItemAttributeValueMapping."Item Attribute Value ID") then begin ItemAttributeValue.CalcFields("Attribute Name"); - CreateProduct.AssignProductOptionValues(TempShopifyVariant, ProductOptionIndex, ItemAttributeValue."Attribute Name", ItemAttributeValue.Value); + ProductExport.AssignProductOptionValues(TempShopifyVariant, ProductOptionIndex, ItemAttributeValue."Attribute Name", ItemAttributeValue.Value); end else begin SkippedRecord.LogSkippedRecord(Item.RecordId(), StrSubstNo(ItemWithoutRequiredAttributeValueErr, Item."No."), Shop); exit(false); @@ -153,7 +155,7 @@ codeunit 30343 "Shpfy Create Item As Variant" exit(true); end; - local procedure CheckProdOptionsCombinationUnique(var TempShopifyVariant: Record "Shpfy Variant" temporary; ExistingProductOptionValues: Dictionary of [Text, Text]; Item: Record "Item"): Boolean + local procedure CheckProductOptionsCombinationUnique(var TempShopifyVariant: Record "Shpfy Variant" temporary; ExistingProductOptionValues: Dictionary of [Text, Text]; Item: Record "Item"): Boolean var SkippedRecord: Codeunit "Shpfy Skipped Record"; CombinationKey: Text; @@ -171,6 +173,7 @@ codeunit 30343 "Shpfy Create Item As Variant" exit(true); end; + #endregion /// /// Checks if items can be added as variants to the product. The items cannot be added as variants if: From 8b9258aa481e5fd42f9f00e1ca6a6d56de7bc5ca Mon Sep 17 00:00:00 2001 From: jzaksauskas Date: Fri, 16 Jan 2026 10:06:03 +0200 Subject: [PATCH 10/21] Moved Product Options Handling procedures from Create Product to Export Product codeunit. updated references --- .../Codeunits/ShpfyCreateProduct.Codeunit.al | 237 +---------------- .../Codeunits/ShpfyProductExport.Codeunit.al | 248 +++++++++++++++++- .../Page Extensions/ShpfyItemCard.PageExt.al | 4 +- 3 files changed, 246 insertions(+), 243 deletions(-) diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al index fbc3b36e69..22ef81ac18 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al @@ -62,7 +62,7 @@ codeunit 30174 "Shpfy Create Product" TempShopifyVariant: Record "Shpfy Variant" temporary; TempShopifyTag: Record "Shpfy Tag" temporary; begin - if not CheckItemAttributesCompatibleForProductOptions(Item) then + if not ProductExport.CheckItemAttributesCompatibleForProductOptions(Item) then exit; CreateTempProduct(Item, TempShopifyProduct, TempShopifyVariant, TempShopifyTag); @@ -172,7 +172,7 @@ codeunit 30174 "Shpfy Create Product" end else CreateTempShopifyVariantFromItem(Item, TempShopifyVariant); - FillProductOptionsForShopifyVariants(Item, TempShopifyVariant, TempShopifyProduct); + ProductExport.FillProductOptionsForShopifyVariants(Item, TempShopifyVariant, TempShopifyProduct); TempShopifyProduct.Insert(false); Events.OnAfterCreateTempShopifyProduct(Item, TempShopifyProduct, TempShopifyVariant, TempShopifyTag); end; @@ -249,239 +249,6 @@ codeunit 30174 "Shpfy Create Product" TempShopifyVariant.Insert(false); end; - - /// - /// Checks if item attributes marked as "As Option" are compatible to be used as product options in Shopify. - /// - /// The item to check. - /// True if the item attributes are compatible, false otherwise. - internal procedure CheckItemAttributesCompatibleForProductOptions(Item: Record Item): Boolean - var - SkippedRecord: Codeunit "Shpfy Skipped Record"; - ItemAttributeIds: List of [Integer]; - SkippedReason: Text[250]; - TooManyAttributesAsOptionErr: Label 'Item %1 has %2 attributes marked as "As Option". Shopify supports a maximum of 3 product options.', Comment = '%1 = Item No., %2 = Number of attributes'; - DuplicateOptionCombinationErr: Label 'Item %1 has duplicate item variant attribute value combinations. Each variant must have a unique combination of option values.', Comment = '%1 = Item No.'; - begin - if Shop."UoM as Variant" then - exit(true); - - GetItemAttributeIDsMarkedAsOption(Item, ItemAttributeIds); - - if ItemAttributeIds.Count() = 0 then - exit(true); - - if ItemAttributeIds.Count() > 3 then begin - SkippedRecord.LogSkippedRecord(Item.RecordId, StrSubstNo(TooManyAttributesAsOptionErr, Item."No.", ItemAttributeIds.Count()), Shop); - exit(false); - end; - - if CheckMissingItemAttributeValues(Item, ItemAttributeIds, SkippedReason) then begin - SkippedRecord.LogSkippedRecord(Item.RecordId, SkippedReason, Shop); - exit(false); - end; - - if CheckProductOptionDuplicatesExists(Item, ItemAttributeIds) then begin - SkippedRecord.LogSkippedRecord(Item.RecordId, StrSubstNo(DuplicateOptionCombinationErr, Item."No."), Shop); - exit(false); - end; - - exit(true); - end; - - local procedure CheckProductOptionDuplicatesExists(Item: Record Item; ItemAttributeIds: List of [Integer]): Boolean - var - ItemAttributeValue: Record "Item Attribute Value"; - ItemVariant: Record "Item Variant"; - ItemVarAttrValueMapping: Record "Item Var. Attr. Value Mapping"; - VariantCombinations: Dictionary of [Text, Code[10]]; - CombinationKey: Text; - VariantCode: Code[10]; - AttributeId: Integer; - CombinationKeyTok: Label '%1:%2|', Locked = true, Comment = '%1 = Attribute ID, %2 = Attribute Value'; - begin - ItemVariant.SetRange("Item No.", Item."No."); - if not ItemVariant.FindSet() then - exit(false); - - repeat - VariantCode := ItemVariant.Code; - - CombinationKey := ''; - foreach AttributeId in ItemAttributeIds do begin - ItemVarAttrValueMapping.SetRange("Item No.", Item."No."); - ItemVarAttrValueMapping.SetRange("Variant Code", VariantCode); - ItemVarAttrValueMapping.SetRange("Item Attribute ID", AttributeId); - if ItemVarAttrValueMapping.FindFirst() then - if ItemAttributeValue.Get(ItemVarAttrValueMapping."Item Attribute ID", ItemVarAttrValueMapping."Item Attribute Value ID") then - CombinationKey += StrSubstNo(CombinationKeyTok, ItemAttributeValue."Attribute ID", ItemAttributeValue.Value); - end; - - if CombinationKey <> '' then - if VariantCombinations.ContainsKey(CombinationKey) then - exit(true) - else - VariantCombinations.Add(CombinationKey, VariantCode); - until ItemVariant.Next() = 0; - end; - - local procedure CheckMissingItemAttributeValues(Item: Record Item; ItemAttributeIds: List of [Integer]; var SkippedReason: Text[250]): Boolean - var - ItemAttribute: Record "Item Attribute"; - ItemVariant: Record "Item Variant"; - ItemAttributeValueMapping: Record "Item Attribute Value Mapping"; - ItemVarAttrValueMapping: Record "Item Var. Attr. Value Mapping"; - ItemAttributeValue: Record "Item Attribute Value"; - AttributeId: Integer; - MissingVariantCode: Code[10]; - MissingAttributeName: Text[250]; - MissingAttributeErr: Label 'Item %1 Variant %2 is missing an attribute "%3". All item variants must have must have item attributes marked as "As Option".', Comment = '%1 = Item No., %2 = Variant Code, %3 = Attribute Name'; - MissingItemAttributeValueErr: Label 'Item %1 is missing a value for attribute "%2". Item must have values for attributes marked as "As Option".', Comment = '%1 = Item No., %2 = Attribute Name'; - MissingItemVarAttributeValueErr: Label 'Item %1 Variant %2 is missing a value for attribute "%3". All item variants must have values for attributes marked as "As Option".', Comment = '%1 = Item No., %2 = Variant Code, %3 = Attribute Name'; - begin - ItemVariant.SetRange("Item No.", Item."No."); - if ItemVariant.FindSet() then - repeat - foreach AttributeId in ItemAttributeIds do begin - ItemVarAttrValueMapping.SetRange("Item No.", Item."No."); - ItemVarAttrValueMapping.SetRange("Variant Code", ItemVariant.Code); - ItemVarAttrValueMapping.SetRange("Item Attribute ID", AttributeId); - if not ItemVarAttrValueMapping.FindFirst() then begin - MissingVariantCode := ItemVariant.Code; - if ItemAttribute.Get(AttributeId) then - MissingAttributeName := ItemAttribute.Name; - SkippedReason := StrSubstNo(MissingAttributeErr, Item."No.", MissingVariantCode, MissingAttributeName); - exit(true); - end else - if ItemAttributeValue.Get(ItemVarAttrValueMapping."Item Attribute ID", ItemVarAttrValueMapping."Item Attribute Value ID") then - if ItemAttributeValue.Value = '' then begin - MissingVariantCode := ItemVariant.Code; - if ItemAttribute.Get(AttributeId) then - MissingAttributeName := ItemAttribute.Name; - SkippedReason := StrSubstNo(MissingItemVarAttributeValueErr, Item."No.", MissingVariantCode, MissingAttributeName); - exit(true); - end; - end; - until ItemVariant.Next() = 0 - else - foreach AttributeId in ItemAttributeIds do begin - ItemAttributeValueMapping.SetRange("Table ID", Database::Item); - ItemAttributeValueMapping.SetRange("No.", Item."No."); - ItemAttributeValueMapping.SetRange("Item Attribute ID", AttributeId); - if ItemAttributeValueMapping.FindFirst() then - if ItemAttributeValue.Get(ItemAttributeValueMapping."Item Attribute ID", ItemAttributeValueMapping."Item Attribute Value ID") then - if ItemAttributeValue.Value = '' then begin - if ItemAttribute.Get(AttributeId) then - MissingAttributeName := ItemAttribute.Name; - SkippedReason := StrSubstNo(MissingItemAttributeValueErr, Item."No.", MissingAttributeName); - exit(true); - end; - end; - end; - - local procedure FillProductOptionsForShopifyVariants(Item: Record Item; var TempShopifyVariant: Record "Shpfy Variant" temporary; var TempShopifyProduct: Record "Shpfy Product" temporary) - var - ItemVariant: Record "Item Variant"; - ItemAttributeIds: List of [Integer]; - VariantCode: Code[10]; - begin - if Shop."UoM as Variant" then - exit; - - GetItemAttributeIDsMarkedAsOption(Item, ItemAttributeIds); - - if ItemAttributeIds.Count() = 0 then - exit; - - if TempShopifyVariant.FindSet() then - repeat - VariantCode := ''; - if not IsNullGuid(TempShopifyVariant."Item Variant SystemId") then - if ItemVariant.GetBySystemId(TempShopifyVariant."Item Variant SystemId") then - VariantCode := ItemVariant.Code; - - FillProductOptionsFromItemAttributes(Item."No.", VariantCode, ItemAttributeIds, TempShopifyVariant); - TempShopifyVariant.Modify(false); - until TempShopifyVariant.Next() = 0; - - TempShopifyProduct."Has Variants" := true; - end; - - local procedure FillProductOptionsFromItemAttributes(ItemNo: Code[20]; VariantCode: Code[10]; ItemAttributeIds: List of [Integer]; var TempShopifyVariant: Record "Shpfy Variant" temporary) - var - ItemAttribute: Record "Item Attribute"; - ItemAttributeValue: Record "Item Attribute Value"; - ItemAttributeValueMapping: Record "Item Attribute Value Mapping"; - ItemVarAttrValueMapping: Record "Item Var. Attr. Value Mapping"; - OptionIndex: Integer; - AttributeId: Integer; - begin - OptionIndex := 1; - foreach AttributeId in ItemAttributeIds do - if ItemAttribute.Get(AttributeId) then begin - if VariantCode <> '' then begin - ItemVarAttrValueMapping.SetRange("Item No.", ItemNo); - ItemVarAttrValueMapping.SetRange("Variant Code", VariantCode); - ItemVarAttrValueMapping.SetRange("Item Attribute ID", AttributeId); - if ItemVarAttrValueMapping.FindFirst() then - if ItemAttributeValue.Get(ItemVarAttrValueMapping."Item Attribute ID", ItemVarAttrValueMapping."Item Attribute Value ID") then - AssignProductOptionValues(TempShopifyVariant, OptionIndex, ItemAttribute.Name, ItemAttributeValue.Value); - end else begin - ItemAttributeValueMapping.SetRange("Table ID", Database::Item); - ItemAttributeValueMapping.SetRange("No.", ItemNo); - ItemAttributeValueMapping.SetRange("Item Attribute ID", AttributeId); - if ItemAttributeValueMapping.FindFirst() then - if ItemAttributeValue.Get(ItemAttributeValueMapping."Item Attribute ID", ItemAttributeValueMapping."Item Attribute Value ID") then - AssignProductOptionValues(TempShopifyVariant, OptionIndex, ItemAttribute.Name, ItemAttributeValue.Value); - end; - OptionIndex += 1; - end; - end; - - /// - /// Assigns product option values to the temporary Shopify variant based on the option index. - /// - /// Parameter of type Record "Shpfy Variant" temporary. - /// Parameter of type Integer. - /// Parameter of type Text[250]. - /// Parameter of type Text[250]. - internal procedure AssignProductOptionValues(var TempShopifyVariant: Record "Shpfy Variant" temporary; OptionIndex: Integer; AttributeName: Text[250]; AttributeValue: Text[250]) - begin - case OptionIndex of - 1: - begin - TempShopifyVariant."Option 1 Name" := CopyStr(AttributeName, 1, MaxStrLen(TempShopifyVariant."Option 1 Name")); - TempShopifyVariant."Option 1 Value" := CopyStr(AttributeValue, 1, MaxStrLen(TempShopifyVariant."Option 1 Value")); - end; - 2: - begin - TempShopifyVariant."Option 2 Name" := CopyStr(AttributeName, 1, MaxStrLen(TempShopifyVariant."Option 2 Name")); - TempShopifyVariant."Option 2 Value" := CopyStr(AttributeValue, 1, MaxStrLen(TempShopifyVariant."Option 2 Value")); - end; - 3: - begin - TempShopifyVariant."Option 3 Name" := CopyStr(AttributeName, 1, MaxStrLen(TempShopifyVariant."Option 3 Name")); - TempShopifyVariant."Option 3 Value" := CopyStr(AttributeValue, 1, MaxStrLen(TempShopifyVariant."Option 3 Value")); - end; - end; - end; - - local procedure GetItemAttributeIDsMarkedAsOption(Item: Record Item; var ItemAttributeIds: List of [Integer]) - var - ItemAttribute: Record "Item Attribute"; - ItemAttributeValueMapping: Record "Item Attribute Value Mapping"; - begin - ItemAttributeValueMapping.SetRange("Table ID", Database::Item); - ItemAttributeValueMapping.SetRange("No.", Item."No."); - if ItemAttributeValueMapping.FindSet() then - repeat - if ItemAttribute.Get(ItemAttributeValueMapping."Item Attribute ID") then - if (not ItemAttribute.Blocked) and (ItemAttribute."Shpfy Incl. in Product Sync" = ItemAttribute."Shpfy Incl. in Product Sync"::"As Option") then - if not ItemAttributeIds.Contains(ItemAttribute.ID) then - ItemAttributeIds.Add(ItemAttribute.ID); - until ItemAttributeValueMapping.Next() = 0; - end; - /// /// Set Shop. /// diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al index 3312f04bdb..846924b111 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al @@ -242,7 +242,7 @@ codeunit 30178 "Shpfy Product Export" /// /// Parameter of type Record Item. /// Parameter of type Record "Item Variant". - local procedure CreateProductVariant(ProductId: BigInteger; Item: Record Item; ItemVariant: Record "Item Variant") + local procedure CreateProductVariant(ProductId: BigInteger; Item: Record Item; ItemVariant: Record "Item Variant"; TempShopifyProduct: Record "Shpfy Product" temporary) var TempShopifyVariant: Record "Shpfy Variant" temporary; begin @@ -255,7 +255,14 @@ codeunit 30178 "Shpfy Product Export" exit; Clear(TempShopifyVariant); FillInProductVariantData(TempShopifyVariant, Item, ItemVariant); + + if not CheckItemAttributesCompatibleForProductOptions(Item) then + exit; + TempShopifyVariant.Insert(false); + + FillProductOptionsForShopifyVariants(Item, TempShopifyVariant, TempShopifyProduct); + VariantApi.AddProductVariant(TempShopifyVariant, ProductId, "Shpfy Variant Create Strategy"::DEFAULT); end; @@ -421,10 +428,6 @@ codeunit 30178 "Shpfy Product Export" ShopifyVariant.Weight := Item."Gross Weight"; if ShopifyVariant."Option 1 Name" = '' then ShopifyVariant."Option 1 Name" := 'Variant'; - if ItemAsVariant then - ShopifyVariant."Option 1 Value" := Item."No." - else - ShopifyVariant."Option 1 Value" := ItemVariant.Code; ShopifyVariant."Shop Code" := Shop.Code; ShopifyVariant."Item SystemId" := Item.SystemId; ShopifyVariant."Item Variant SystemId" := ItemVariant.SystemId; @@ -704,7 +707,7 @@ codeunit 30178 "Shpfy Product Export" if ShopifyVariant.FindFirst() then UpdateProductVariant(ShopifyVariant, Item, ItemVariant, TempCurrVariant) else - CreateProductVariant(ProductId, Item, ItemVariant); + CreateProductVariant(ProductId, Item, ItemVariant, TempShopifyProduct); until ItemVariant.Next() = 0 else begin Clear(ShopifyVariant); @@ -910,4 +913,237 @@ codeunit 30178 "Shpfy Product Export" until ShopifyLanguage.Next() = 0; end; #endregion + + #region Shopify Product Options as Item/Variant Attributes + /// + /// Checks if item attributes marked as "As Option" are compatible to be used as product options in Shopify. + /// + /// The item to check. + /// True if the item attributes are compatible, false otherwise. + internal procedure CheckItemAttributesCompatibleForProductOptions(Item: Record Item): Boolean + var + ItemAttributeIds: List of [Integer]; + SkippedReason: Text[250]; + TooManyAttributesAsOptionErr: Label 'Item %1 has %2 attributes marked as "As Option". Shopify supports a maximum of 3 product options.', Comment = '%1 = Item No., %2 = Number of attributes'; + DuplicateOptionCombinationErr: Label 'Item %1 has duplicate item variant attribute value combinations. Each variant must have a unique combination of option values.', Comment = '%1 = Item No.'; + begin + if Shop."UoM as Variant" then + exit(true); + + GetItemAttributeIDsMarkedAsOption(Item, ItemAttributeIds); + + if ItemAttributeIds.Count() = 0 then + exit(true); + + if ItemAttributeIds.Count() > 3 then begin + SkippedRecord.LogSkippedRecord(Item.RecordId, StrSubstNo(TooManyAttributesAsOptionErr, Item."No.", ItemAttributeIds.Count()), Shop); + exit(false); + end; + + if CheckMissingItemAttributeValues(Item, ItemAttributeIds, SkippedReason) then begin + SkippedRecord.LogSkippedRecord(Item.RecordId, SkippedReason, Shop); + exit(false); + end; + + if CheckProductOptionDuplicatesExists(Item, ItemAttributeIds) then begin + SkippedRecord.LogSkippedRecord(Item.RecordId, StrSubstNo(DuplicateOptionCombinationErr, Item."No."), Shop); + exit(false); + end; + + exit(true); + end; + + local procedure GetItemAttributeIDsMarkedAsOption(Item: Record Item; var ItemAttributeIds: List of [Integer]) + var + ItemAttribute: Record "Item Attribute"; + ItemAttributeValueMapping: Record "Item Attribute Value Mapping"; + begin + ItemAttributeValueMapping.SetRange("Table ID", Database::Item); + ItemAttributeValueMapping.SetRange("No.", Item."No."); + if ItemAttributeValueMapping.FindSet() then + repeat + if ItemAttribute.Get(ItemAttributeValueMapping."Item Attribute ID") then + if (not ItemAttribute.Blocked) and (ItemAttribute."Shpfy Incl. in Product Sync" = ItemAttribute."Shpfy Incl. in Product Sync"::"As Option") then + if not ItemAttributeIds.Contains(ItemAttribute.ID) then + ItemAttributeIds.Add(ItemAttribute.ID); + until ItemAttributeValueMapping.Next() = 0; + end; + + local procedure CheckProductOptionDuplicatesExists(Item: Record Item; ItemAttributeIds: List of [Integer]): Boolean + var + ItemAttributeValue: Record "Item Attribute Value"; + ItemVariant: Record "Item Variant"; + ItemVarAttrValueMapping: Record "Item Var. Attr. Value Mapping"; + VariantCombinations: Dictionary of [Text, Code[10]]; + CombinationKey: Text; + VariantCode: Code[10]; + AttributeId: Integer; + CombinationKeyTok: Label '%1:%2|', Locked = true, Comment = '%1 = Attribute ID, %2 = Attribute Value'; + begin + ItemVariant.SetRange("Item No.", Item."No."); + if not ItemVariant.FindSet() then + exit(false); + + repeat + VariantCode := ItemVariant.Code; + + CombinationKey := ''; + foreach AttributeId in ItemAttributeIds do begin + ItemVarAttrValueMapping.SetRange("Item No.", Item."No."); + ItemVarAttrValueMapping.SetRange("Variant Code", VariantCode); + ItemVarAttrValueMapping.SetRange("Item Attribute ID", AttributeId); + if ItemVarAttrValueMapping.FindFirst() then + if ItemAttributeValue.Get(ItemVarAttrValueMapping."Item Attribute ID", ItemVarAttrValueMapping."Item Attribute Value ID") then + CombinationKey += StrSubstNo(CombinationKeyTok, ItemAttributeValue."Attribute ID", ItemAttributeValue.Value); + end; + + if CombinationKey <> '' then + if VariantCombinations.ContainsKey(CombinationKey) then + exit(true) + else + VariantCombinations.Add(CombinationKey, VariantCode); + until ItemVariant.Next() = 0; + end; + + local procedure CheckMissingItemAttributeValues(Item: Record Item; ItemAttributeIds: List of [Integer]; var SkippedReason: Text[250]): Boolean + var + ItemAttribute: Record "Item Attribute"; + ItemVariant: Record "Item Variant"; + ItemAttributeValueMapping: Record "Item Attribute Value Mapping"; + ItemVarAttrValueMapping: Record "Item Var. Attr. Value Mapping"; + ItemAttributeValue: Record "Item Attribute Value"; + AttributeId: Integer; + MissingVariantCode: Code[10]; + MissingAttributeName: Text[250]; + MissingAttributeErr: Label 'Item %1 Variant %2 is missing an attribute "%3". All item variants must have must have item attributes marked as "As Option".', Comment = '%1 = Item No., %2 = Variant Code, %3 = Attribute Name'; + MissingItemAttributeValueErr: Label 'Item %1 is missing a value for attribute "%2". Item must have values for attributes marked as "As Option".', Comment = '%1 = Item No., %2 = Attribute Name'; + MissingItemVarAttributeValueErr: Label 'Item %1 Variant %2 is missing a value for attribute "%3". All item variants must have values for attributes marked as "As Option".', Comment = '%1 = Item No., %2 = Variant Code, %3 = Attribute Name'; + begin + ItemVariant.SetRange("Item No.", Item."No."); + if ItemVariant.FindSet() then + repeat + foreach AttributeId in ItemAttributeIds do begin + ItemVarAttrValueMapping.SetRange("Item No.", Item."No."); + ItemVarAttrValueMapping.SetRange("Variant Code", ItemVariant.Code); + ItemVarAttrValueMapping.SetRange("Item Attribute ID", AttributeId); + if not ItemVarAttrValueMapping.FindFirst() then begin + MissingVariantCode := ItemVariant.Code; + if ItemAttribute.Get(AttributeId) then + MissingAttributeName := ItemAttribute.Name; + SkippedReason := StrSubstNo(MissingAttributeErr, Item."No.", MissingVariantCode, MissingAttributeName); + exit(true); + end else + if ItemAttributeValue.Get(ItemVarAttrValueMapping."Item Attribute ID", ItemVarAttrValueMapping."Item Attribute Value ID") then + if ItemAttributeValue.Value = '' then begin + MissingVariantCode := ItemVariant.Code; + if ItemAttribute.Get(AttributeId) then + MissingAttributeName := ItemAttribute.Name; + SkippedReason := StrSubstNo(MissingItemVarAttributeValueErr, Item."No.", MissingVariantCode, MissingAttributeName); + exit(true); + end; + end; + until ItemVariant.Next() = 0 + else + foreach AttributeId in ItemAttributeIds do begin + ItemAttributeValueMapping.SetRange("Table ID", Database::Item); + ItemAttributeValueMapping.SetRange("No.", Item."No."); + ItemAttributeValueMapping.SetRange("Item Attribute ID", AttributeId); + if ItemAttributeValueMapping.FindFirst() then + if ItemAttributeValue.Get(ItemAttributeValueMapping."Item Attribute ID", ItemAttributeValueMapping."Item Attribute Value ID") then + if ItemAttributeValue.Value = '' then begin + if ItemAttribute.Get(AttributeId) then + MissingAttributeName := ItemAttribute.Name; + SkippedReason := StrSubstNo(MissingItemAttributeValueErr, Item."No.", MissingAttributeName); + exit(true); + end; + end; + end; + + internal procedure FillProductOptionsForShopifyVariants(Item: Record Item; var TempShopifyVariant: Record "Shpfy Variant" temporary; var TempShopifyProduct: Record "Shpfy Product" temporary) + var + ItemVariant: Record "Item Variant"; + ItemAttributeIds: List of [Integer]; + VariantCode: Code[10]; + begin + if Shop."UoM as Variant" then + exit; + + GetItemAttributeIDsMarkedAsOption(Item, ItemAttributeIds); + + if ItemAttributeIds.Count() = 0 then + exit; + + if TempShopifyVariant.FindSet() then + repeat + VariantCode := ''; + if not IsNullGuid(TempShopifyVariant."Item Variant SystemId") then + if ItemVariant.GetBySystemId(TempShopifyVariant."Item Variant SystemId") then + VariantCode := ItemVariant.Code; + + FillProductOptionsFromItemAttributes(Item."No.", VariantCode, ItemAttributeIds, TempShopifyVariant); + TempShopifyVariant.Modify(false); + until TempShopifyVariant.Next() = 0; + + TempShopifyProduct."Has Variants" := true; + end; + + local procedure FillProductOptionsFromItemAttributes(ItemNo: Code[20]; VariantCode: Code[10]; ItemAttributeIds: List of [Integer]; var TempShopifyVariant: Record "Shpfy Variant" temporary) + var + ItemAttribute: Record "Item Attribute"; + ItemAttributeValue: Record "Item Attribute Value"; + ItemAttributeValueMapping: Record "Item Attribute Value Mapping"; + ItemVarAttrValueMapping: Record "Item Var. Attr. Value Mapping"; + OptionIndex: Integer; + AttributeId: Integer; + begin + OptionIndex := 1; + foreach AttributeId in ItemAttributeIds do + if ItemAttribute.Get(AttributeId) then begin + if VariantCode <> '' then begin + ItemVarAttrValueMapping.SetRange("Item No.", ItemNo); + ItemVarAttrValueMapping.SetRange("Variant Code", VariantCode); + ItemVarAttrValueMapping.SetRange("Item Attribute ID", AttributeId); + if ItemVarAttrValueMapping.FindFirst() then + if ItemAttributeValue.Get(ItemVarAttrValueMapping."Item Attribute ID", ItemVarAttrValueMapping."Item Attribute Value ID") then + AssignProductOptionValues(TempShopifyVariant, OptionIndex, ItemAttribute.Name, ItemAttributeValue.Value); + end else begin + ItemAttributeValueMapping.SetRange("Table ID", Database::Item); + ItemAttributeValueMapping.SetRange("No.", ItemNo); + ItemAttributeValueMapping.SetRange("Item Attribute ID", AttributeId); + if ItemAttributeValueMapping.FindFirst() then + if ItemAttributeValue.Get(ItemAttributeValueMapping."Item Attribute ID", ItemAttributeValueMapping."Item Attribute Value ID") then + AssignProductOptionValues(TempShopifyVariant, OptionIndex, ItemAttribute.Name, ItemAttributeValue.Value); + end; + OptionIndex += 1; + end; + end; + + /// + /// Assigns product option values to the temporary Shopify variant based on the option index. + /// + /// Parameter of type Record "Shpfy Variant" temporary. + /// Parameter of type Integer. + /// Parameter of type Text[250]. + /// Parameter of type Text[250]. + internal procedure AssignProductOptionValues(var TempShopifyVariant: Record "Shpfy Variant" temporary; OptionIndex: Integer; AttributeName: Text[250]; AttributeValue: Text[250]) + begin + case OptionIndex of + 1: + begin + TempShopifyVariant."Option 1 Name" := CopyStr(AttributeName, 1, MaxStrLen(TempShopifyVariant."Option 1 Name")); + TempShopifyVariant."Option 1 Value" := CopyStr(AttributeValue, 1, MaxStrLen(TempShopifyVariant."Option 1 Value")); + end; + 2: + begin + TempShopifyVariant."Option 2 Name" := CopyStr(AttributeName, 1, MaxStrLen(TempShopifyVariant."Option 2 Name")); + TempShopifyVariant."Option 2 Value" := CopyStr(AttributeValue, 1, MaxStrLen(TempShopifyVariant."Option 2 Value")); + end; + 3: + begin + TempShopifyVariant."Option 3 Name" := CopyStr(AttributeName, 1, MaxStrLen(TempShopifyVariant."Option 3 Name")); + TempShopifyVariant."Option 3 Value" := CopyStr(AttributeValue, 1, MaxStrLen(TempShopifyVariant."Option 3 Value")); + end; + end; + end; + #endregion } diff --git a/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemCard.PageExt.al b/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemCard.PageExt.al index fa664103df..e068deeecc 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemCard.PageExt.al +++ b/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemCard.PageExt.al @@ -48,11 +48,11 @@ pageextension 30119 "Shpfy Item Card" extends "Item Card" trigger OnAction() var Shop: Record "Shpfy Shop"; - CreateProduct: Codeunit "Shpfy Create Product"; + ProductExport: Codeunit "Shpfy Product Export"; SyncProducts: Codeunit "Shpfy Sync Products"; begin if SyncProducts.ConfirmAddItemToShopify(Rec, Shop) then begin - if not CreateProduct.CheckItemAttributesCompatibleForProductOptions(Rec) then + if not ProductExport.CheckItemAttributesCompatibleForProductOptions(Rec) then exit; SyncProducts.AddItemToShopify(Rec, Shop); From 3703f98be3af75adf66410c4416d28afebc9b2d3 Mon Sep 17 00:00:00 2001 From: jzaksauskas Date: Fri, 16 Jan 2026 10:54:30 +0200 Subject: [PATCH 11/21] Add validation to prevent UoM as Variant usage if Item Attributes As Porduct Options utilized --- .../Shopify/App/src/Base/Tables/ShpfyShop.Table.al | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 081b40ecda..6524d158e4 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 @@ -5,6 +5,7 @@ namespace Microsoft.Integration.Shopify; +using Microsoft.Inventory.Item.Attribute; using Microsoft.Finance.Currency; using Microsoft.Finance.GeneralLedger.Account; using Microsoft.Finance.GeneralLedger.Setup; @@ -300,6 +301,9 @@ table 30102 "Shpfy Shop" trigger OnValidate() begin + if "UoM as Variant" then + VerifyNoItemAttributesAsOptions(); + if "UoM as Variant" and ("Option Name for UoM" = '') then "Option Name for UoM" := 'Unit of Measure'; end; @@ -1090,4 +1094,13 @@ table 30102 "Shpfy Shop" end; #pragma warning restore AL0432 #endif + + local procedure VerifyNoItemAttributesAsOptions() + var + ItemAttribute: Record "Item Attribute"; + begin + ItemAttribute.SetRange("Shpfy Incl. in Product Sync", "Shpfy Incl. in Product Sync"::"As Option"); + if not ItemAttribute.IsEmpty() then + Error('UoM as Variant is unavailable due to existing Item Attributes marked as “As Option” which are utilized for Shopify Product Options.'); + end; } From 095ab61375fb7cf4a058b13456429978bb1ececc Mon Sep 17 00:00:00 2001 From: jzaksauskas Date: Fri, 16 Jan 2026 10:55:15 +0200 Subject: [PATCH 12/21] Adjusted codeunit permissions --- .../App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al | 4 ---- .../App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al index 22ef81ac18..13c3648b3a 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al @@ -17,12 +17,8 @@ codeunit 30174 "Shpfy Create Product" Access = Internal; Permissions = tabledata Item = r, - tabledata "Item Attribute" = r, - tabledata "Item Attribute Value" = r, - tabledata "Item Attribute Value Mapping" = r, tabledata "Item Reference" = r, tabledata "Item Unit of Measure" = r, - tabledata "Item Var. Attr. Value Mapping" = r, tabledata "Item Variant" = r; TableNo = Item; diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al index 846924b111..a1fb8fd7ea 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al @@ -27,6 +27,7 @@ codeunit 30178 "Shpfy Product Export" tabledata "Item Attribute Translation" = r, tabledata "Item Attribute Value" = r, tabledata "Item Attribute Value Mapping" = r, + tabledata "Item Var. Attr. Value Mapping" = r, tabledata "Item Category" = r, tabledata "Item Reference" = r, tabledata "Item Unit of Measure" = rim, From 3ad6d973f9976571d36cbe86f2eb797ffe042b59 Mon Sep 17 00:00:00 2001 From: jzaksauskas Date: Fri, 16 Jan 2026 11:03:37 +0200 Subject: [PATCH 13/21] Added FillProductOptionsForShopifyVariants procedure documentation --- .../src/Products/Codeunits/ShpfyProductExport.Codeunit.al | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al index a1fb8fd7ea..e2013371b9 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al @@ -1060,6 +1060,12 @@ codeunit 30178 "Shpfy Product Export" end; end; + /// + /// Fills product options for Shopify variants based on item attributes marked as "As Option". + /// + /// The item to process. + /// Parameter of Shopify Variants to fill. + /// Parameter of Shopify Product. internal procedure FillProductOptionsForShopifyVariants(Item: Record Item; var TempShopifyVariant: Record "Shpfy Variant" temporary; var TempShopifyProduct: Record "Shpfy Product" temporary) var ItemVariant: Record "Item Variant"; From ffcc7f0e0343a79ac45f37d89b2186fd41ccf824 Mon Sep 17 00:00:00 2001 From: jzaksauskas Date: Fri, 16 Jan 2026 16:33:29 +0200 Subject: [PATCH 14/21] Refactor product option handling. Moded procedure to Product Export codedunit --- .../ShpfyCreateItemAsVariant.Codeunit.al | 116 +---------- .../Codeunits/ShpfyProductExport.Codeunit.al | 180 ++++++++++++++++-- 2 files changed, 170 insertions(+), 126 deletions(-) diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al index 9d2b156963..6e56e02124 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al @@ -5,7 +5,6 @@ namespace Microsoft.Integration.Shopify; -using Microsoft.Inventory.Item.Attribute; using Microsoft.Inventory.Item; codeunit 30343 "Shpfy Create Item As Variant" @@ -35,9 +34,7 @@ codeunit 30343 "Shpfy Create Item As Variant" internal procedure CreateVariantFromItem(var Item: Record "Item") var TempShopifyVariant: Record "Shpfy Variant" temporary; - ProductOptions: Dictionary of [Integer, Text]; - ExistingProductOptionValues: Dictionary of [Text, Text]; - ProductOptionIndex: Integer; + ProductExport: Codeunit "Shpfy Product Export"; VariantOptionTok: Label 'Variant', Locked = true; TitleOptionTok: Label 'Title', Locked = true; begin @@ -56,16 +53,8 @@ codeunit 30343 "Shpfy Create Item As Variant" TempShopifyVariant."Option 1 Value" := Item."No."; - if ShopifyProduct."Has Variants" and (OptionName <> VariantOptionTok) then begin - CollectExistingProductVariantOptionValues(ProductOptions, ExistingProductOptionValues); - - for ProductOptionIndex := 1 to ProductOptions.Count() do - if not AssignProductOptionValuesToTempProductVariant(TempShopifyVariant, Item, ProductOptions, ProductOptionIndex) then - exit; - - if not CheckProductOptionsCombinationUnique(TempShopifyVariant, ExistingProductOptionValues, Item) then - exit; - end; + if ShopifyProduct."Has Variants" and (OptionName <> VariantOptionTok) then + ProductExport.ValidateItemAttributesAndAssignProductOptionsForNewVariant(TempShopifyVariant, Item, '', ShopifyProduct.Id); Events.OnAfterCreateTempShopifyVariant(Item, TempShopifyVariant); TempShopifyVariant.Modify(); @@ -76,105 +65,6 @@ codeunit 30343 "Shpfy Create Item As Variant" end; end; - #region Shopify Product Options as Item/Variant Attributes - local procedure CollectExistingProductVariantOptionValues(var ProductOptions: Dictionary of [Integer, Text]; var ExistingProductOptionValues: Dictionary of [Text, Text]) - var - ShopifyVariant: Record "Shpfy Variant"; - ItemAttribute: Record "Item Attribute"; - CombinationKey: Text; - begin - ShopifyVariant.SetRange("Product Id", ShopifyProduct.Id); - if ShopifyVariant.FindSet() then - repeat - CombinationKey := BuildCombinationKey( - ShopifyVariant."Option 1 Name", ShopifyVariant."Option 1 Value", - ShopifyVariant."Option 2 Name", ShopifyVariant."Option 2 Value", - ShopifyVariant."Option 3 Name", ShopifyVariant."Option 3 Value"); - - if not ExistingProductOptionValues.ContainsKey(CombinationKey) then - ExistingProductOptionValues.Add(CombinationKey, ShopifyVariant."Variant Code"); - until ShopifyVariant.Next() = 0; - - if ShopifyVariant."Option 1 Name" <> '' then begin - ItemAttribute.SetRange(Name, ShopifyVariant."Option 1 Name"); - if ItemAttribute.FindFirst() then - ProductOptions.Add(ItemAttribute.ID, ShopifyVariant."Option 1 Name"); - end; - if ShopifyVariant."Option 2 Name" <> '' then begin - ItemAttribute.SetRange(Name, ShopifyVariant."Option 2 Name"); - if ItemAttribute.FindFirst() then - ProductOptions.Add(ItemAttribute.ID, ShopifyVariant."Option 2 Name"); - end; - if ShopifyVariant."Option 3 Name" <> '' then begin - ItemAttribute.SetRange(Name, ShopifyVariant."Option 3 Name"); - if ItemAttribute.FindFirst() then - ProductOptions.Add(ItemAttribute.ID, ShopifyVariant."Option 3 Name"); - end; - end; - - local procedure BuildCombinationKey(Option1Name: Text; Option1Value: Text; Option2Name: Text; Option2Value: Text; Option3Name: Text; Option3Value: Text): Text - var - CombinationKey: Text; - KeyPartTok: Label '%1:%2|', Locked = true; - begin - if Option1Name <> '' then - CombinationKey += StrSubstNo(KeyPartTok, Option1Name, Option1Value); - if Option2Name <> '' then - CombinationKey += StrSubstNo(KeyPartTok, Option2Name, Option2Value); - if Option3Name <> '' then - CombinationKey += StrSubstNo(KeyPartTok, Option3Name, Option3Value); - - exit(CombinationKey); - end; - - local procedure AssignProductOptionValuesToTempProductVariant(var TempShopifyVariant: Record "Shpfy Variant" temporary; Item: Record "Item"; ProductOptions: Dictionary of [Integer, Text]; ProductOptionIndex: Integer): Boolean - var - ItemAttributeValueMapping: Record "Item Attribute Value Mapping"; - ItemAttributeValue: Record "Item Attribute Value"; - ProductExport: Codeunit "Shpfy Product Export"; - SkippedRecord: Codeunit "Shpfy Skipped Record"; - ItemWithoutRequiredAttributeErr: Label 'Item %1 cannot be added as a product variant because it does not have required attributes.', Comment = '%1 = Item No.'; - ItemWithoutRequiredAttributeValueErr: Label 'Item %1 cannot be added as a product variant because it does not have a value for the required attributes.', Comment = '%1 = Item No.'; - begin - ItemAttributeValueMapping.SetRange("Table ID", Database::Item); - ItemAttributeValueMapping.SetRange("No.", Item."No."); - ItemAttributeValueMapping.SetRange("Item Attribute ID", ProductOptions.Keys.Get(ProductOptionIndex)); - if ItemAttributeValueMapping.FindFirst() then begin - if ItemAttributeValue.Get(ItemAttributeValueMapping."Item Attribute ID", ItemAttributeValueMapping."Item Attribute Value ID") then begin - ItemAttributeValue.CalcFields("Attribute Name"); - ProductExport.AssignProductOptionValues(TempShopifyVariant, ProductOptionIndex, ItemAttributeValue."Attribute Name", ItemAttributeValue.Value); - end else begin - SkippedRecord.LogSkippedRecord(Item.RecordId(), StrSubstNo(ItemWithoutRequiredAttributeValueErr, Item."No."), Shop); - exit(false); - end; - end else begin - SkippedRecord.LogSkippedRecord(Item.RecordId(), StrSubstNo(ItemWithoutRequiredAttributeErr, Item."No."), Shop); - exit(false); - end; - - exit(true); - end; - - local procedure CheckProductOptionsCombinationUnique(var TempShopifyVariant: Record "Shpfy Variant" temporary; ExistingProductOptionValues: Dictionary of [Text, Text]; Item: Record "Item"): Boolean - var - SkippedRecord: Codeunit "Shpfy Skipped Record"; - CombinationKey: Text; - DuplicateCombinationErr: Label 'Item %1 cannot be added as a product variant because another variant already has the same option values.', Comment = '%1 = Item No.'; - begin - CombinationKey := BuildCombinationKey( - TempShopifyVariant."Option 1 Name", TempShopifyVariant."Option 1 Value", - TempShopifyVariant."Option 2 Name", TempShopifyVariant."Option 2 Value", - TempShopifyVariant."Option 3 Name", TempShopifyVariant."Option 3 Value"); - - if ExistingProductOptionValues.ContainsKey(CombinationKey) then begin - SkippedRecord.LogSkippedRecord(Item.RecordId(), StrSubstNo(DuplicateCombinationErr, Item."No."), Shop); - exit(false); - end; - - exit(true); - end; - #endregion - /// /// Checks if items can be added as variants to the product. The items cannot be added as variants if: /// - The product has more than one option. diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al index e2013371b9..e254b0ea71 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al @@ -257,13 +257,10 @@ codeunit 30178 "Shpfy Product Export" Clear(TempShopifyVariant); FillInProductVariantData(TempShopifyVariant, Item, ItemVariant); - if not CheckItemAttributesCompatibleForProductOptions(Item) then + if not ValidateItemAttributesAndAssignProductOptionsForNewVariant(TempShopifyVariant, Item, ItemVariant.Code, TempShopifyProduct.Id) then exit; TempShopifyVariant.Insert(false); - - FillProductOptionsForShopifyVariants(Item, TempShopifyVariant, TempShopifyProduct); - VariantApi.AddProductVariant(TempShopifyVariant, ProductId, "Shpfy Variant Create Strategy"::DEFAULT); end; @@ -429,6 +426,11 @@ codeunit 30178 "Shpfy Product Export" ShopifyVariant.Weight := Item."Gross Weight"; if ShopifyVariant."Option 1 Name" = '' then ShopifyVariant."Option 1 Name" := 'Variant'; + if ShopifyVariant."Option 1 Name" = 'Variant' then + if ItemAsVariant then + ShopifyVariant."Option 1 Value" := Item."No." + else + ShopifyVariant."Option 1 Value" := ItemVariant.Code; ShopifyVariant."Shop Code" := Shop.Code; ShopifyVariant."Item SystemId" := Item.SystemId; ShopifyVariant."Item Variant SystemId" := ItemVariant.SystemId; @@ -917,7 +919,7 @@ codeunit 30178 "Shpfy Product Export" #region Shopify Product Options as Item/Variant Attributes /// - /// Checks if item attributes marked as "As Option" are compatible to be used as product options in Shopify. + /// Checks if item/item variant attributes marked as "As Option" are compatible to be used as product options in Shopify. /// /// The item to check. /// True if the item attributes are compatible, false otherwise. @@ -1125,14 +1127,7 @@ codeunit 30178 "Shpfy Product Export" end; end; - /// - /// Assigns product option values to the temporary Shopify variant based on the option index. - /// - /// Parameter of type Record "Shpfy Variant" temporary. - /// Parameter of type Integer. - /// Parameter of type Text[250]. - /// Parameter of type Text[250]. - internal procedure AssignProductOptionValues(var TempShopifyVariant: Record "Shpfy Variant" temporary; OptionIndex: Integer; AttributeName: Text[250]; AttributeValue: Text[250]) + local procedure AssignProductOptionValues(var TempShopifyVariant: Record "Shpfy Variant" temporary; OptionIndex: Integer; AttributeName: Text[250]; AttributeValue: Text[250]) begin case OptionIndex of 1: @@ -1152,5 +1147,164 @@ codeunit 30178 "Shpfy Product Export" end; end; end; + + + /// + /// Validates item attributes and prepares temporary Shopify variant by assigning product option values from Item/Item Variant. + /// + /// Parameter of type Record "Shpfy Variant" temporary. + /// Parameter of type Record Item. + /// Parameter of type Record "Item Variant". + /// Parameter of type BigInteger. + internal procedure ValidateItemAttributesAndAssignProductOptionsForNewVariant(var TempShopifyVariant: Record "Shpfy Variant" temporary; Item: Record Item; ItemVariantCode: Code[10]; ShopifyProductId: BigInteger): Boolean + var + ItemAttributeIds: List of [Integer]; + ProductOptions: Dictionary of [Integer, Text]; + ExistingProductOptionValues: Dictionary of [Text, Text]; + ProductOptionIndex: Integer; + begin + if Shop."UoM as Variant" then + exit(true); + + GetItemAttributeIDsMarkedAsOption(Item, ItemAttributeIds); + + if ItemAttributeIds.Count() = 0 then + exit(true); + + CollectExistingProductVariantOptionValues(ProductOptions, ExistingProductOptionValues, ShopifyProductId); + + for ProductOptionIndex := 1 to ProductOptions.Count() do + if not AssignProductOptionValuesToTempProductVariant(TempShopifyVariant, Item, ItemVariantCode, ProductOptions, ProductOptionIndex) then + exit(false); + + if not CheckProductOptionsCombinationUnique(TempShopifyVariant, ExistingProductOptionValues, Item, ItemVariantCode) then + exit(false); + + exit(true); + end; + + local procedure CollectExistingProductVariantOptionValues(var ProductOptions: Dictionary of [Integer, Text]; var ExistingProductOptionValues: Dictionary of [Text, Text]; ShopifyProductId: BigInteger) + var + ShopifyVariant: Record "Shpfy Variant"; + ItemAttribute: Record "Item Attribute"; + CombinationKey: Text; + begin + ShopifyVariant.SetAutoCalcFields("Variant Code"); + ShopifyVariant.SetRange("Product Id", ShopifyProductId); + if ShopifyVariant.FindSet() then + repeat + CombinationKey := BuildCombinationKey( + ShopifyVariant."Option 1 Name", ShopifyVariant."Option 1 Value", + ShopifyVariant."Option 2 Name", ShopifyVariant."Option 2 Value", + ShopifyVariant."Option 3 Name", ShopifyVariant."Option 3 Value"); + + if not ExistingProductOptionValues.ContainsKey(CombinationKey) then + ExistingProductOptionValues.Add(CombinationKey, ShopifyVariant."Variant Code"); + until ShopifyVariant.Next() = 0; + + if ShopifyVariant."Option 1 Name" <> '' then begin + ItemAttribute.SetRange(Name, ShopifyVariant."Option 1 Name"); + if ItemAttribute.FindFirst() then + ProductOptions.Add(ItemAttribute.ID, ShopifyVariant."Option 1 Name"); + end; + if ShopifyVariant."Option 2 Name" <> '' then begin + ItemAttribute.SetRange(Name, ShopifyVariant."Option 2 Name"); + if ItemAttribute.FindFirst() then + ProductOptions.Add(ItemAttribute.ID, ShopifyVariant."Option 2 Name"); + end; + if ShopifyVariant."Option 3 Name" <> '' then begin + ItemAttribute.SetRange(Name, ShopifyVariant."Option 3 Name"); + if ItemAttribute.FindFirst() then + ProductOptions.Add(ItemAttribute.ID, ShopifyVariant."Option 3 Name"); + end; + end; + + local procedure BuildCombinationKey(Option1Name: Text; Option1Value: Text; Option2Name: Text; Option2Value: Text; Option3Name: Text; Option3Value: Text): Text + var + CombinationKey: Text; + KeyPartTok: Label '%1:%2|', Locked = true, Comment = '%1 = Option Name, %2 = Option Value'; + begin + if Option1Name <> '' then + CombinationKey += StrSubstNo(KeyPartTok, Option1Name, Option1Value); + if Option2Name <> '' then + CombinationKey += StrSubstNo(KeyPartTok, Option2Name, Option2Value); + if Option3Name <> '' then + CombinationKey += StrSubstNo(KeyPartTok, Option3Name, Option3Value); + + exit(CombinationKey); + end; + + local procedure AssignProductOptionValuesToTempProductVariant(var TempShopifyVariant: Record "Shpfy Variant" temporary; Item: Record "Item"; ItemVariantCode: Code[10]; ProductOptions: Dictionary of [Integer, Text]; ProductOptionIndex: Integer): Boolean + var + ItemVariant: Record "Item Variant"; + ItemAttributeValueMapping: Record "Item Attribute Value Mapping"; + ItemVarAttrValueMapping: Record "Item Var. Attr. Value Mapping"; + ItemAttributeValue: Record "Item Attribute Value"; + ItemWithoutRequiredAttributeErr: Label 'Item %1 cannot be added as a product variant because it does not have required attributes.', Comment = '%1 = Item No.'; + ItemWithoutRequiredAttributeValueErr: Label 'Item %1 cannot be added as a product variant because it does not have a value for the required attributes.', Comment = '%1 = Item No.'; + ItemVariantWithoutRequiredAttributeErr: Label 'Item Variant %1 cannot be added as a product variant because it does not have required attributes.', Comment = '%1 = Item No.'; + ItemVariantWithoutRequiredAttributeValueErr: Label 'Item Variant%1 cannot be added as a product variant because it does not have a value for the required attributes.', Comment = '%1 = Item No.'; + begin + ItemVariant.SetRange("Item No.", Item."No."); + ItemVariant.SetRange("Code", ItemVariantCode); + if ItemVariant.FindFirst() then begin + ItemVarAttrValueMapping.SetRange("Item No.", Item."No."); + ItemVarAttrValueMapping.SetRange("Variant Code", ItemVariant."Code"); + ItemVarAttrValueMapping.SetRange("Item Attribute ID", ProductOptions.Keys.Get(ProductOptionIndex)); + if ItemVarAttrValueMapping.FindFirst() then begin + if ItemAttributeValue.Get(ItemVarAttrValueMapping."Item Attribute ID", ItemVarAttrValueMapping."Item Attribute Value ID") then begin + ItemAttributeValue.CalcFields("Attribute Name"); + AssignProductOptionValues(TempShopifyVariant, ProductOptionIndex, ItemAttributeValue."Attribute Name", ItemAttributeValue.Value); + end else begin + SkippedRecord.LogSkippedRecord(Item.RecordId(), StrSubstNo(ItemVariantWithoutRequiredAttributeValueErr, Item."No."), Shop); + exit(false); + end; + end else begin + SkippedRecord.LogSkippedRecord(Item.RecordId(), StrSubstNo(ItemVariantWithoutRequiredAttributeErr, Item."No."), Shop); + exit(false); + end; + end else begin + ItemAttributeValueMapping.SetRange("Table ID", Database::Item); + ItemAttributeValueMapping.SetRange("No.", Item."No."); + ItemAttributeValueMapping.SetRange("Item Attribute ID", ProductOptions.Keys.Get(ProductOptionIndex)); + if ItemAttributeValueMapping.FindFirst() then begin + if ItemAttributeValue.Get(ItemAttributeValueMapping."Item Attribute ID", ItemAttributeValueMapping."Item Attribute Value ID") then begin + ItemAttributeValue.CalcFields("Attribute Name"); + AssignProductOptionValues(TempShopifyVariant, ProductOptionIndex, ItemAttributeValue."Attribute Name", ItemAttributeValue.Value); + end else begin + SkippedRecord.LogSkippedRecord(Item.RecordId(), StrSubstNo(ItemWithoutRequiredAttributeValueErr, Item."No."), Shop); + exit(false); + end; + end else begin + SkippedRecord.LogSkippedRecord(Item.RecordId(), StrSubstNo(ItemWithoutRequiredAttributeErr, Item."No."), Shop); + exit(false); + end; + end; + + exit(true); + end; + + local procedure CheckProductOptionsCombinationUnique(var TempShopifyVariant: Record "Shpfy Variant" temporary; ExistingProductOptionValues: Dictionary of [Text, Text]; Item: Record "Item"; ItemVariantCode: Code[10]): Boolean + var + ItemVariant: Record "Item Variant"; + CombinationKey: Text; + DuplicateItemCombinationErr: Label 'Item %1 cannot be added as a product variant because another variant already has the same option values.', Comment = '%1 = Item No.'; + DuplicateItemVarCombinationErr: Label 'Item %1 cannot be added as a product variant because another variant already has the same option values.', Comment = '%1 = Item No.'; + begin + CombinationKey := BuildCombinationKey( + TempShopifyVariant."Option 1 Name", TempShopifyVariant."Option 1 Value", + TempShopifyVariant."Option 2 Name", TempShopifyVariant."Option 2 Value", + TempShopifyVariant."Option 3 Name", TempShopifyVariant."Option 3 Value"); + + if ExistingProductOptionValues.ContainsKey(CombinationKey) then begin + if ItemVariant.Get(Item."No.", ItemVariantCode) then + SkippedRecord.LogSkippedRecord(ItemVariant.RecordId(), StrSubstNo(DuplicateItemVarCombinationErr, Item."No."), Shop) + else + SkippedRecord.LogSkippedRecord(Item.RecordId(), StrSubstNo(DuplicateItemCombinationErr, Item."No."), Shop); + exit(false); + end; + + exit(true); + end; #endregion } From bdf1698fb41c3cabd6ab300eb1ff1b4f24f33a47 Mon Sep 17 00:00:00 2001 From: jzaksauskas Date: Fri, 16 Jan 2026 16:44:45 +0200 Subject: [PATCH 15/21] Renamed procedure --- .../Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al | 2 +- .../App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al | 1 - .../App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al index 6e56e02124..7bb3ce779d 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al @@ -54,7 +54,7 @@ codeunit 30343 "Shpfy Create Item As Variant" if ShopifyProduct."Has Variants" and (OptionName <> VariantOptionTok) then - ProductExport.ValidateItemAttributesAndAssignProductOptionsForNewVariant(TempShopifyVariant, Item, '', ShopifyProduct.Id); + ProductExport.ValidateItemAttributesAsProductOptionsForNewVariant(TempShopifyVariant, Item, '', ShopifyProduct.Id); Events.OnAfterCreateTempShopifyVariant(Item, TempShopifyVariant); TempShopifyVariant.Modify(); diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al index 13c3648b3a..3495495a37 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al @@ -6,7 +6,6 @@ namespace Microsoft.Integration.Shopify; using Microsoft.Inventory.Item; -using Microsoft.Inventory.Item.Attribute; using Microsoft.Inventory.Item.Catalog; /// diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al index e254b0ea71..6e5b739ad9 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al @@ -257,7 +257,7 @@ codeunit 30178 "Shpfy Product Export" Clear(TempShopifyVariant); FillInProductVariantData(TempShopifyVariant, Item, ItemVariant); - if not ValidateItemAttributesAndAssignProductOptionsForNewVariant(TempShopifyVariant, Item, ItemVariant.Code, TempShopifyProduct.Id) then + if not ValidateItemAttributesAsProductOptionsForNewVariant(TempShopifyVariant, Item, ItemVariant.Code, TempShopifyProduct.Id) then exit; TempShopifyVariant.Insert(false); @@ -1156,7 +1156,7 @@ codeunit 30178 "Shpfy Product Export" /// Parameter of type Record Item. /// Parameter of type Record "Item Variant". /// Parameter of type BigInteger. - internal procedure ValidateItemAttributesAndAssignProductOptionsForNewVariant(var TempShopifyVariant: Record "Shpfy Variant" temporary; Item: Record Item; ItemVariantCode: Code[10]; ShopifyProductId: BigInteger): Boolean + internal procedure ValidateItemAttributesAsProductOptionsForNewVariant(var TempShopifyVariant: Record "Shpfy Variant" temporary; Item: Record Item; ItemVariantCode: Code[10]; ShopifyProductId: BigInteger): Boolean var ItemAttributeIds: List of [Integer]; ProductOptions: Dictionary of [Integer, Text]; From cb45c2837be5fbb299878e45f718ebc9da1eebfd Mon Sep 17 00:00:00 2001 From: jzaksauskas Date: Fri, 16 Jan 2026 17:20:28 +0200 Subject: [PATCH 16/21] Added SetShop --- .../Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al | 4 +++- .../App/src/Products/Page Extensions/ShpfyItemCard.PageExt.al | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al index 7bb3ce779d..cda6a4141d 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al @@ -53,8 +53,10 @@ codeunit 30343 "Shpfy Create Item As Variant" TempShopifyVariant."Option 1 Value" := Item."No."; - if ShopifyProduct."Has Variants" and (OptionName <> VariantOptionTok) then + if ShopifyProduct."Has Variants" and (OptionName <> VariantOptionTok) then begin + ProductExport.SetShop(Shop); ProductExport.ValidateItemAttributesAsProductOptionsForNewVariant(TempShopifyVariant, Item, '', ShopifyProduct.Id); + end; Events.OnAfterCreateTempShopifyVariant(Item, TempShopifyVariant); TempShopifyVariant.Modify(); diff --git a/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemCard.PageExt.al b/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemCard.PageExt.al index e068deeecc..94b8c2b7f3 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemCard.PageExt.al +++ b/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemCard.PageExt.al @@ -52,6 +52,7 @@ pageextension 30119 "Shpfy Item Card" extends "Item Card" SyncProducts: Codeunit "Shpfy Sync Products"; begin if SyncProducts.ConfirmAddItemToShopify(Rec, Shop) then begin + ProductExport.SetShop(Shop); if not ProductExport.CheckItemAttributesCompatibleForProductOptions(Rec) then exit; From d07364ac52e7c9a5149b59e9d22de6591d64bb8f Mon Sep 17 00:00:00 2001 From: jzaksauskas Date: Tue, 20 Jan 2026 08:46:46 +0200 Subject: [PATCH 17/21] Fixed hardcoded label and moved tooltip to table ext --- src/Apps/W1/Shopify/App/src/Base/Tables/ShpfyShop.Table.al | 3 ++- .../Products/Page Extensions/ShpfyItemAttributes.PageExt.al | 1 - .../Products/Table Extensions/ShpfyItemAttribute.TableExt.al | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) 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 6524d158e4..9a9851a3d9 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 @@ -1098,9 +1098,10 @@ table 30102 "Shpfy Shop" local procedure VerifyNoItemAttributesAsOptions() var ItemAttribute: Record "Item Attribute"; + UoMVariantUnavailableErr: Label 'UoM as Variant is unavailable due to existing Item Attributes marked as “As Option” which are utilized for Shopify Product Options.'; begin ItemAttribute.SetRange("Shpfy Incl. in Product Sync", "Shpfy Incl. in Product Sync"::"As Option"); if not ItemAttribute.IsEmpty() then - Error('UoM as Variant is unavailable due to existing Item Attributes marked as “As Option” which are utilized for Shopify Product Options.'); + Error(UoMVariantUnavailableErr); end; } diff --git a/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemAttributes.PageExt.al b/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemAttributes.PageExt.al index fffac64079..d6c0a33d1a 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemAttributes.PageExt.al +++ b/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemAttributes.PageExt.al @@ -16,7 +16,6 @@ pageextension 30127 "Shpfy Item Attributes" extends "Item Attributes" field("Shpfy Incl. in Product Sync"; Rec."Shpfy Incl. in Product Sync") { ApplicationArea = All; - ToolTip = 'Specifies whether to include this item attribute in product synchronization to Shopify. Select "As Option" to export the attribute as a Shopify Product Option.'; } } } diff --git a/src/Apps/W1/Shopify/App/src/Products/Table Extensions/ShpfyItemAttribute.TableExt.al b/src/Apps/W1/Shopify/App/src/Products/Table Extensions/ShpfyItemAttribute.TableExt.al index 5d745786fd..077d0be0ca 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Table Extensions/ShpfyItemAttribute.TableExt.al +++ b/src/Apps/W1/Shopify/App/src/Products/Table Extensions/ShpfyItemAttribute.TableExt.al @@ -15,6 +15,7 @@ tableextension 30112 "Shpfy Item Attribute" extends "Item Attribute" { Caption = 'Incl. in Product Sync'; DataClassification = CustomerContent; + ToolTip = 'Specifies whether to include this item attribute in product synchronization to Shopify. Select "As Option" to export the attribute as a Shopify Product Option.'; } } } From 2ecc413b914345297dfb01317fbb64c2c8cfa0f8 Mon Sep 17 00:00:00 2001 From: jzaksauskas Date: Wed, 21 Jan 2026 09:45:54 +0200 Subject: [PATCH 18/21] Automated tests for Item attributes as Product Options --- .../ShpfyItemAttrAsOptionTest.Codeunit.al | 614 ++++++++++++++++++ 1 file changed, 614 insertions(+) create mode 100644 src/Apps/W1/Shopify/Test/Products/ShpfyItemAttrAsOptionTest.Codeunit.al diff --git a/src/Apps/W1/Shopify/Test/Products/ShpfyItemAttrAsOptionTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Products/ShpfyItemAttrAsOptionTest.Codeunit.al new file mode 100644 index 0000000000..579aa9f4d2 --- /dev/null +++ b/src/Apps/W1/Shopify/Test/Products/ShpfyItemAttrAsOptionTest.Codeunit.al @@ -0,0 +1,614 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace Microsoft.Integration.Shopify.Test; + +using Microsoft.Integration.Shopify; +using Microsoft.Inventory.Item; +using Microsoft.Inventory.Item.Attribute; +using System.TestLibraries.Utilities; + +/// +/// Codeunit Shpfy Item Attr As Option Test (ID 139540). +/// Tests for 'Item Attributes As Shopify Product Options' functionality. +/// +codeunit 139540 "Shpfy Item Attr As Option Test" +{ + Subtype = Test; + TestType = IntegrationTest; + TestPermissions = Disabled; + + var + Shop: Record "Shpfy Shop"; + Any: Codeunit Any; + LibraryVariableStorage: Codeunit "Library - Variable Storage"; + LibraryAssert: Codeunit "Library Assert"; + LibraryInventory: Codeunit "Library - Inventory"; + InitializeTest: Codeunit "Shpfy Initialize Test"; + IsInitialized: Boolean; + + trigger OnRun() + begin + IsInitialized := false; + end; + + #region UoM as Variant validation Tests + [Test] + procedure UnitTestValidateUoMAsVariantWhenAsOptionAttributesExist() + var + ItemAttribute: Record "Item Attribute"; + FailureMessageErr: Label 'UoM as Variant is unavailable due to existing Item Attributes marked as “As Option” which are utilized for Shopify Product Options.'; + begin + // [SCENARIO] Enabling 'UoM as Variant' fails when 'As Option' Item Attributes exist + + // [GIVEN] Shopify Shop is created, and UoM as Variant is false + Initialize(); + + // [GIVEN] Some Item Attributes are marked 'As Option' + CreateItemAttributeAsOption(ItemAttribute); + + // [WHEN] User tries to validate 'UoM as Variant' to true + asserterror Shop.Validate("UoM as Variant", true); + + // [THEN] Error is raised about unavailability + LibraryAssert.IsTrue(GetLastErrorText().Contains(FailureMessageErr), 'Expected error was not raised.'); + end; + #endregion + + #region No Variants, No As Option Attributes + [Test] + procedure UnitTestExportItemWithoutVariantsAndWithoutAsOptionAttributes() + var + Item: Record Item; + TempShopifyProduct: Record "Shpfy Product" temporary; + TempShopifyVariant: Record "Shpfy Variant" temporary; + TempTag: Record "Shpfy Tag" temporary; + CreateProduct: Codeunit "Shpfy Create Product"; + begin + // [SCENARIO] Exporting Item without variants and without 'As Option' attributes creates Shopify product variant with no options + + // [GIVEN] Shopify Shop is created + Initialize(); + CreateProduct.SetShop(Shop); + + // [GIVEN] Item is created without Item variants and without 'As Option' Item Attributes + CreateItem(Item); + + // [WHEN] Create Temp Product from Item + CreateProduct.CreateTempProduct(Item, TempShopifyProduct, TempShopifyVariant, TempTag); + + // [THEN] Product variant is created without Option Names and Option values + VerifyVariantHasNoOptions(TempShopifyVariant); + end; + + [Test] + procedure UnitTestAddItemAsVariantToProductWithoutAsOptionAttributes() + var + Item: Record Item; + ShpfyVariant: Record "Shpfy Variant"; + CreateItemAsVariant: Codeunit "Shpfy Create Item As Variant"; + CreateItemAsVariantSub: Codeunit "Shpfy CreateItemAsVariantSub"; + ParentProductId: BigInteger; + VariantId: BigInteger; + begin + // [SCENARIO] Adding Item as variant to product without 'As Option' attributes creates variant with 'Variant' option + + // [GIVEN] Shopify Shop is created + Initialize(); + + // [GIVEN] Product exists without As Option Attributes + ParentProductId := CreateShopifyProductWithoutAsOptionAttributes(); + + // [GIVEN] Item is created without Item variants and without 'As Option' Item Attributes + CreateItem(Item); + + // [WHEN] Add Item as Shopify Variant + BindSubscription(CreateItemAsVariantSub); + CreateItemAsVariant.SetParentProduct(ParentProductId); + CreateItemAsVariant.CheckProductAndShopSettings(); + CreateItemAsVariant.CreateVariantFromItem(Item); + VariantId := CreateItemAsVariantSub.GetNewVariantId(); + UnbindSubscription(CreateItemAsVariantSub); + + // [THEN] New Variant is created with Option 1 Name 'Variant', Option 1 Value '' + VerifyVariantCreatedWithItemNo(ShpfyVariant, VariantId, Item."No."); + end; + + #endregion + + #region No Variants, 2 As Option Attributes + [Test] + procedure UnitTestExportItemWithoutVariantsAndWith2AsOptionAttributes() + var + Item: Record Item; + TempShopifyProduct: Record "Shpfy Product" temporary; + TempShopifyVariant: Record "Shpfy Variant" temporary; + TempTag: Record "Shpfy Tag" temporary; + CreateProduct: Codeunit "Shpfy Create Product"; + begin + // [SCENARIO] Exporting Item without variants but with 2 'As Option' attributes creates variant with 2 options + + // [GIVEN] Shopify Shop is created + Initialize(); + CreateProduct.SetShop(Shop); + + // [GIVEN] Item is created without Item variants but with 2 'As Option' Item Attributes + Item := CreateItemWithAsOptionAttributes(2); + + // [WHEN] Create Temp Product from Item + CreateProduct.CreateTempProduct(Item, TempShopifyProduct, TempShopifyVariant, TempTag); + + // [THEN] Product variant is created with Item attributes as options + VerifyVariantHas2Options(TempShopifyVariant); + + // [THEN] Product should be marked as having variants + VerifyProductHasVariants(TempShopifyProduct); + end; + + [Test] + procedure UnitTestValidateItemAttributesForNewVariantMissingAttributes() + var + Item: Record Item; + ItemAttribute: Record "Item Attribute"; + TempShopifyVariant: Record "Shpfy Variant" temporary; + ProductExport: Codeunit "Shpfy Product Export"; + ValidationResult: Boolean; + ExpFailureMessageErr: Label 'cannot be added as a product variant because it does not have required attributes.'; + ParentProductId: BigInteger; + begin + // [SCENARIO] Adding Item as variant fails when Item is missing required 'As Option' attributes + + // [GIVEN] Shopify Shop is created + Initialize(); + ProductExport.SetShop(Shop); + + // [GIVEN] Item is created without Item variants but with 2 'As Option' Item Attributes + Item := CreateItemWithAsOptionAttributes(2); + + // [GIVEN] Product exists with 'As Option' Attributes + ParentProductId := CreateShopifyProductWithAsOptionAttributesAndValues(CopyStr(LibraryVariableStorage.PeekText(2), 1, 250), CopyStr(LibraryVariableStorage.PeekText(4), 1, 250), CopyStr(LibraryVariableStorage.PeekText(6), 1, 250), CopyStr(LibraryVariableStorage.PeekText(8), 1, 250)); + + // [GIVEN] Item is created without Item variants and without all required 'As Option' Item Attributes + LibraryInventory.CreateItemAttribute(ItemAttribute, ItemAttribute.Type::Text, ''); + Item := CreateItemWithSpecificAsOptionAttributes(LibraryVariableStorage.PeekInteger(1), LibraryVariableStorage.PeekInteger(3), CopyStr(LibraryVariableStorage.PeekText(4), 1, 250), ItemAttribute.ID, 0, GenerateRandomAttributeValue()); + + // [WHEN] Validate item attributes for new variant + TempShopifyVariant.Init(); + ValidationResult := ProductExport.ValidateItemAttributesAsProductOptionsForNewVariant(TempShopifyVariant, Item, '', ParentProductId); + + // [THEN] Returns false (variant should not be created) and skipped entry is logged + VerifyItemAttributesValidationForNewVariantFailed(ValidationResult); + VerifySkippedEntryExists(Item.RecordId, ExpFailureMessageErr); + end; + + [Test] + procedure UnitTestValidateItemAttributesForNewVariantDuplicateCombination() + var + Item: Record Item; + TempShopifyVariant: Record "Shpfy Variant" temporary; + ProductExport: Codeunit "Shpfy Product Export"; + ValidationResult: Boolean; + ExpFailureMessageErr: Label 'cannot be added as a product variant because another variant already has the same option values.'; + ParentProductId: BigInteger; + begin + // [SCENARIO] Adding Item as variant fails when option value combination already exists + + // [GIVEN] Shopify Shop is created + Initialize(); + ProductExport.SetShop(Shop); + + // [GIVEN] Item is created without Item variants but with 2 'As Option' Item Attributes + Item := CreateItemWithAsOptionAttributes(2); + + // [GIVEN] Product exists with 'As Option' Attributes + ParentProductId := CreateShopifyProductWithAsOptionAttributesAndValues( + CopyStr(LibraryVariableStorage.PeekText(2), 1, 250), + CopyStr(LibraryVariableStorage.PeekText(4), 1, 250), + CopyStr(LibraryVariableStorage.PeekText(6), 1, 250), + CopyStr(LibraryVariableStorage.PeekText(8), 1, 250)); + + // [GIVEN] Item is created with the same 'As Option' Item Attribute values as existing variant + Item := CreateItemWithSpecificAsOptionAttributes( + LibraryVariableStorage.PeekInteger(1), + LibraryVariableStorage.PeekInteger(3), + CopyStr(LibraryVariableStorage.PeekText(4), 1, 250), + LibraryVariableStorage.PeekInteger(5), + LibraryVariableStorage.PeekInteger(7), + CopyStr(LibraryVariableStorage.PeekText(8), 1, 250)); + + // [WHEN] Validate item attributes for new variant + TempShopifyVariant.Init(); + ValidationResult := ProductExport.ValidateItemAttributesAsProductOptionsForNewVariant(TempShopifyVariant, Item, '', ParentProductId); + + // [THEN] Returns false (variant should not be created) and skipped entry is logged + VerifyItemAttributesValidationForNewVariantFailed(ValidationResult); + VerifySkippedEntryExists(Item.RecordId, ExpFailureMessageErr); + end; + + #endregion + + #region 2 Variants, No As Option Attributes + [Test] + procedure UnitTestExportItemWith2VariantsAndWithoutAsOptionAttributes() + var + Item: Record Item; + TempShopifyProduct: Record "Shpfy Product" temporary; + TempShopifyVariant: Record "Shpfy Variant" temporary; + TempTag: Record "Shpfy Tag" temporary; + CreateProduct: Codeunit "Shpfy Create Product"; + ExpOptionNameTok: Label 'Variant', Locked = true; + begin + // [SCENARIO] Exporting Item with 2 variants and without 'As Option' attributes creates 2 variants with 'Variant' option name + + // [GIVEN] Shopify Shop is created + Initialize(); + CreateProduct.SetShop(Shop); + + // [GIVEN] Item is created with 2 Item variants and without 'As Option' Item Attributes + CreateItemWithVariants(Item, 2); + + // [WHEN] Create Temp Product from Item + CreateProduct.CreateTempProduct(Item, TempShopifyProduct, TempShopifyVariant, TempTag); + + // [THEN] 2 Product variants are created (Option 1 Name for both 'Variant') + VerifyVariantsCreatedWithOptionName(TempShopifyVariant, 2, ExpOptionNameTok); + end; + + #endregion + + #region 2 Variants, 3 As Option Attributes + [Test] + procedure UnitTestExportItemWith2VariantsAnd3AsOptionAttributesDifferentCombinations() + var + Item: Record Item; + TempShopifyProduct: Record "Shpfy Product" temporary; + TempShopifyVariant: Record "Shpfy Variant" temporary; + TempTag: Record "Shpfy Tag" temporary; + CreateProduct: Codeunit "Shpfy Create Product"; + begin + // [SCENARIO] Exporting Item with 2 variants and 3 'As Option' attributes creates 2 variants with unique option combinations + + // [GIVEN] Shopify Shop is created + Initialize(); + CreateProduct.SetShop(Shop); + + // [GIVEN] Item is created with 2 Item variants and 3 'As Option' Item Attributes with different value combinations + Item := CreateItemWithVariantsAndAsOptionAttributes(2, 3, false); + + // [WHEN] Create Temp Product from Item + CreateProduct.CreateTempProduct(Item, TempShopifyProduct, TempShopifyVariant, TempTag); + + // [THEN] 2 Product variants are created with different Item attribute combinations + VerifyVariantsCreatedWith3Options(TempShopifyVariant, 2); + + // [THEN] Product should be marked as having variants + VerifyProductHasVariants(TempShopifyProduct); + end; + + [Test] + procedure UnitTestExportItemWith2VariantsAnd3AsOptionAttributesDuplicateCombinations() + var + Item: Record Item; + ProductExport: Codeunit "Shpfy Product Export"; + CompatibilityCheckResult: Boolean; + ExpFailureMessageErr: Label 'duplicate item variant attribute value combinations'; + begin + // [SCENARIO] Exporting Item with duplicate option value combinations fails with skipped entry + + // [GIVEN] Shopify Shop is created + Initialize(); + ProductExport.SetShop(Shop); + + // [GIVEN] Item is created with 2 Item variants and 3 'As Option' Item Attributes with duplicate value combinations + Item := CreateItemWithVariantsAndAsOptionAttributes(2, 3, true); + + // [WHEN] Check item attributes compatible for product options + CompatibilityCheckResult := ProductExport.CheckItemAttributesCompatibleForProductOptions(Item); + + // [THEN] Returns false (product should not be created) and skipped entry is logged + VerifyResultOfCompatibilityCheck(CompatibilityCheckResult); + VerifySkippedEntryExists(Item.RecordId, ExpFailureMessageErr); + end; + + [Test] + procedure UnitTestExportItemWithMoreThan3AsOptionAttributes() + var + Item: Record Item; + ProductExport: Codeunit "Shpfy Product Export"; + CompatibilityCheckResult: Boolean; + ExpFailureMessageErr: Label 'maximum of 3 product options'; + begin + // [SCENARIO] Exporting Item with more than 3 'As Option' attributes fails due to Shopify limit + + // [GIVEN] Shopify Shop is created + Initialize(); + ProductExport.SetShop(Shop); + + // [GIVEN] Item is created without Item variants but with 4 'As Option' Item Attributes (exceeds Shopify limit of 3) + Item := CreateItemWithAsOptionAttributes(4); + + // [WHEN] Check item attributes compatible for product options + CompatibilityCheckResult := ProductExport.CheckItemAttributesCompatibleForProductOptions(Item); + + // [THEN] Returns false and skipped entry is logged about too many attributes + VerifyResultOfCompatibilityCheck(CompatibilityCheckResult); + VerifySkippedEntryExists(Item.RecordId, ExpFailureMessageErr); + end; + #endregion + + #region Helper Procedures + local procedure Initialize() + begin + LibraryVariableStorage.Clear(); + Any.SetDefaultSeed(); + if IsInitialized then + exit; + + Shop := InitializeTest.CreateShop(); + Shop."UoM as Variant" := false; + Shop.Modify(); + Commit(); + IsInitialized := true; + end; + + local procedure CreateItemAttributeAsOption(var ItemAttribute: Record "Item Attribute") + begin + LibraryInventory.CreateItemAttribute(ItemAttribute, ItemAttribute.Type::Text, ''); + ItemAttribute."Shpfy Incl. in Product Sync" := "Shpfy Incl. in Product Sync"::"As Option"; + ItemAttribute.Modify(true); + end; + + local procedure GenerateRandomAttributeValue(): Text[250] + begin + exit(CopyStr(Any.AlphanumericText(50), 1, 250)); + end; + + local procedure CreateItem(var Item: Record Item) + var + ProductInitTest: Codeunit "Shpfy Product Init Test"; + begin + Item := ProductInitTest.CreateItem(); + end; + + local procedure CreateItemWithVariants(var Item: Record Item; NumberOfVariants: Integer) + var + ItemVariant: Record "Item Variant"; + Index: Integer; + begin + CreateItem(Item); + + for Index := 1 to NumberOfVariants do begin + ItemVariant.Init(); + ItemVariant."Item No." := Item."No."; + ItemVariant.Code := CopyStr(Any.AlphabeticText(MaxStrLen(ItemVariant.Code)), 1, MaxStrLen(ItemVariant.Code)); + ItemVariant.Description := CopyStr(Any.AlphabeticText(50), 1, MaxStrLen(ItemVariant.Description)); + ItemVariant.Insert(true); + end; + end; + + local procedure CreateItemWithAsOptionAttributes(NumberOfAsOptionAttributes: Integer) Item: Record Item + var + ItemAttribute: Record "Item Attribute"; + ItemAttributeValue: Record "Item Attribute Value"; + Index: Integer; + begin + CreateItem(Item); + + for Index := 1 to NumberOfAsOptionAttributes do begin + CreateItemAttributeMappedToItem(Item, ItemAttribute, ItemAttributeValue); + + LibraryVariableStorage.Enqueue(ItemAttribute.ID); + LibraryVariableStorage.Enqueue(ItemAttribute.Name); + LibraryVariableStorage.Enqueue(ItemAttributeValue.ID); + LibraryVariableStorage.Enqueue(ItemAttributeValue.Value); + end; + end; + + local procedure CreateItemWithVariantsAndAsOptionAttributes(NumberOfVariants: Integer; NumberOfAsOptionAttributes: Integer; CreateDuplicateCombinations: Boolean) Item: Record Item + var + ItemAttribute: Record "Item Attribute"; + ItemAttributeValue: Record "Item Attribute Value"; + FirstAttributeValue: Record "Item Attribute Value"; + ItemVariant: Record "Item Variant"; + IsFirstVariant: Boolean; + Index: Integer; + begin + CreateItemWithVariants(Item, NumberOfVariants); + + for Index := 1 to NumberOfAsOptionAttributes do begin + CreateItemAttributeAsOption(ItemAttribute); + IsFirstVariant := true; + + ItemVariant.SetRange("Item No.", Item."No."); + if ItemVariant.FindSet() then + repeat + if CreateDuplicateCombinations then begin + if IsFirstVariant then begin + LibraryInventory.CreateItemAttributeValue(ItemAttributeValue, ItemAttribute.ID, GenerateRandomAttributeValue()); + FirstAttributeValue := ItemAttributeValue; + IsFirstVariant := false; + end else + ItemAttributeValue := FirstAttributeValue; + end else + LibraryInventory.CreateItemAttributeValue(ItemAttributeValue, ItemAttribute.ID, GenerateRandomAttributeValue()); + + LibraryInventory.CreateItemVariantAttributeValueMapping(Item."No.", ItemVariant.Code, ItemAttribute.ID, ItemAttributeValue.ID, Database::Item, Item."No."); + + LibraryVariableStorage.Enqueue(ItemAttribute.Name); + LibraryVariableStorage.Enqueue(ItemAttributeValue.Value); + until ItemVariant.Next() = 0; + + LibraryInventory.CreateItemAttributeValueMapping(Database::Item, Item."No.", ItemAttribute.ID, ItemAttributeValue.ID); + end; + end; + + local procedure CreateItemWithSpecificAsOptionAttributes(ItemAttributeID1: Integer; ItemAttributeValueID1: Integer; ItemAttributeValue1: Text[250]; ItemAttributeID2: Integer; ItemAttributeValueID2: Integer; ItemAttributeValue2: Text[250]) Item: Record Item + var + ItemAttributeValue: Record "Item Attribute Value"; + begin + CreateItem(Item); + + if not ItemAttributeValue.Get(ItemAttributeID1, ItemAttributeValueID1) then begin + LibraryInventory.CreateItemAttributeValue(ItemAttributeValue, ItemAttributeID1, ItemAttributeValue1); + ItemAttributeValueID1 := ItemAttributeValue.ID; + end; + + LibraryInventory.CreateItemAttributeValueMapping(Database::Item, Item."No.", ItemAttributeID1, ItemAttributeValueID1); + + if not ItemAttributeValue.Get(ItemAttributeID2, ItemAttributeValueID2) then begin + LibraryInventory.CreateItemAttributeValue(ItemAttributeValue, ItemAttributeID2, ItemAttributeValue2); + ItemAttributeValueID2 := ItemAttributeValue.ID; + end; + LibraryInventory.CreateItemAttributeValueMapping(Database::Item, Item."No.", ItemAttributeID2, ItemAttributeValueID2); + end; + + local procedure CreateShopifyProductWithoutAsOptionAttributes(): BigInteger + var + ShopifyProduct: Record "Shpfy Product"; + ShopifyVariant: Record "Shpfy Variant"; + begin + ShopifyProduct.Init(); + ShopifyProduct.Id := Any.IntegerInRange(10000, 99999); + ShopifyProduct."Shop Code" := Shop.Code; + ShopifyProduct.Title := CopyStr(Any.AlphabeticText(50), 1, MaxStrLen(ShopifyProduct.Title)); + ShopifyProduct.Insert(true); + + ShopifyVariant.Init(); + ShopifyVariant.Id := Any.IntegerInRange(10000, 99999); + ShopifyVariant."Product Id" := ShopifyProduct.Id; + ShopifyVariant."Shop Code" := Shop.Code; + ShopifyVariant."Option 1 Name" := 'Variant'; + ShopifyVariant."Option 1 Value" := 'Default'; + ShopifyVariant.Insert(true); + + exit(ShopifyProduct.Id); + end; + + local procedure CreateShopifyProductWithAsOptionAttributesAndValues(ItemAttributeName1: Text[250]; ItemAttributeValue1: Text[250]; ItemAttributeName2: Text[250]; ItemAttributeValue2: Text[250]): BigInteger + var + ShopifyProduct: Record "Shpfy Product"; + ShopifyVariant: Record "Shpfy Variant"; + begin + ShopifyProduct.Init(); + ShopifyProduct.Id := Any.IntegerInRange(10000, 99999); + ShopifyProduct."Shop Code" := Shop.Code; + ShopifyProduct.Title := CopyStr(Any.AlphabeticText(50), 1, MaxStrLen(ShopifyProduct.Title)); + ShopifyProduct."Has Variants" := true; + ShopifyProduct.Insert(true); + + ShopifyVariant.Init(); + ShopifyVariant.Id := Any.IntegerInRange(10000, 99999); + ShopifyVariant."Product Id" := ShopifyProduct.Id; + ShopifyVariant."Shop Code" := Shop.Code; + ShopifyVariant."Option 1 Name" := CopyStr(ItemAttributeName1, 1, MaxStrLen(ShopifyVariant."Option 1 Name")); + ShopifyVariant."Option 1 Value" := CopyStr(ItemAttributeValue1, 1, MaxStrLen(ShopifyVariant."Option 1 Value")); + ShopifyVariant."Option 2 Name" := CopyStr(ItemAttributeName2, 1, MaxStrLen(ShopifyVariant."Option 2 Name")); + ShopifyVariant."Option 2 Value" := CopyStr(ItemAttributeValue2, 1, MaxStrLen(ShopifyVariant."Option 2 Value")); + ShopifyVariant.Insert(true); + + exit(ShopifyProduct.Id); + end; + + local procedure VerifyVariantHasNoOptions(var TempShopifyVariant: Record "Shpfy Variant" temporary) + begin + LibraryAssert.RecordIsNotEmpty(TempShopifyVariant); + LibraryAssert.AreEqual('', TempShopifyVariant."Option 1 Name", 'Option 1 Name should be empty'); + LibraryAssert.AreEqual('', TempShopifyVariant."Option 1 Value", 'Option 1 Value should be empty'); + LibraryAssert.AreEqual('', TempShopifyVariant."Option 2 Name", 'Option 2 Name should be empty'); + LibraryAssert.AreEqual('', TempShopifyVariant."Option 2 Value", 'Option 2 Value should be empty'); + end; + + local procedure VerifyVariantCreatedWithItemNo(var ShpfyVariant: Record "Shpfy Variant"; VariantId: BigInteger; ItemNo: Code[20]) + begin + LibraryAssert.IsTrue(ShpfyVariant.Get(VariantId), 'Variant should be created'); + LibraryAssert.AreEqual('Variant', ShpfyVariant."Option 1 Name", 'Option 1 Name should be ''Variant'''); + LibraryAssert.AreEqual(ItemNo, ShpfyVariant."Option 1 Value", 'Option 1 Value should be Item No.'); + end; + + local procedure VerifyVariantHas2Options(var TempShopifyVariant: Record "Shpfy Variant" temporary) + begin + LibraryAssert.RecordIsNotEmpty(TempShopifyVariant); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(2), TempShopifyVariant."Option 1 Name", 'Option 1 Name should not be empty'); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(4), TempShopifyVariant."Option 1 Value", 'Option 1 Value should not be empty'); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(6), TempShopifyVariant."Option 2 Name", 'Option 2 Name should not be empty'); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(8), TempShopifyVariant."Option 2 Value", 'Option 2 Value should not be empty'); + end; + + local procedure VerifyProductHasVariants(var TempShopifyProduct: Record "Shpfy Product" temporary) + begin + LibraryAssert.IsTrue(TempShopifyProduct."Has Variants", 'Product should be marked as having variants'); + end; + + local procedure VerifyItemAttributesValidationForNewVariantFailed(ValidationResult: Boolean) + begin + LibraryAssert.IsFalse(ValidationResult, 'Validation result was incorrect.'); + end; + + local procedure VerifyVariantsCreatedWithOptionName(var TempShopifyVariant: Record "Shpfy Variant" temporary; ExpectedCount: Integer; ExpectedOption1Name: Text) + begin + LibraryAssert.AreEqual(ExpectedCount, TempShopifyVariant.Count(), Format(ExpectedCount) + ' variants should be created'); + TempShopifyVariant.FindSet(); + repeat + LibraryAssert.AreEqual(ExpectedOption1Name, TempShopifyVariant."Option 1 Name", 'Option 1 Name should be ''' + ExpectedOption1Name + ''''); + LibraryAssert.AreNotEqual('', TempShopifyVariant."Option 1 Value", 'Option 1 Value should not be empty'); + until TempShopifyVariant.Next() = 0; + end; + + local procedure VerifyVariantsCreatedWith3Options(var TempShopifyVariant: Record "Shpfy Variant" temporary; ExpectedCount: Integer) + var + Option1NameMismatchLbl: Label 'Option 1 Name has incorrect value.'; + Option1ValueMismatchLbl: Label 'Option 1 Value has incorrect value.'; + Option2NameMismatchLbl: Label 'Option 2 Name has incorrect value.'; + Option2ValueMismatchLbl: Label 'Option 2 Value has incorrect value.'; + Option3NameMismatchLbl: Label 'Option 3 Name has incorrect value.'; + Option3ValueMismatchLbl: Label 'Option 3 Value has incorrect value.'; + ItemVariantNumber: Integer; + begin + LibraryAssert.AreEqual(ExpectedCount, TempShopifyVariant.Count(), Format(ExpectedCount) + ' variants should be created'); + TempShopifyVariant.FindSet(); + repeat + ItemVariantNumber += 1; + if ItemVariantNumber = 1 then begin + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(1), TempShopifyVariant."Option 1 Name", Option1NameMismatchLbl); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(2), TempShopifyVariant."Option 1 Value", Option1ValueMismatchLbl); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(5), TempShopifyVariant."Option 2 Name", Option2NameMismatchLbl); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(6), TempShopifyVariant."Option 2 Value", Option2ValueMismatchLbl); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(9), TempShopifyVariant."Option 3 Name", Option3NameMismatchLbl); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(10), TempShopifyVariant."Option 3 Value", Option3ValueMismatchLbl); + end; + if ItemVariantNumber = 2 then begin + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(3), TempShopifyVariant."Option 1 Name", Option1NameMismatchLbl); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(4), TempShopifyVariant."Option 1 Value", Option1ValueMismatchLbl); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(7), TempShopifyVariant."Option 2 Name", Option2NameMismatchLbl); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(8), TempShopifyVariant."Option 2 Value", Option2ValueMismatchLbl); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(11), TempShopifyVariant."Option 3 Name", Option3NameMismatchLbl); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(12), TempShopifyVariant."Option 3 Value", Option3ValueMismatchLbl); + end; + until TempShopifyVariant.Next() = 0; + end; + + local procedure VerifyResultOfCompatibilityCheck(CompatibilityCheckResult: Boolean) + begin + LibraryAssert.IsFalse(CompatibilityCheckResult, 'Incorrect result of compatibility check.'); + end; + + local procedure VerifySkippedEntryExists(ExpectedRecordId: RecordId; ExpFailureMessage: Text) + var + SkippedRecord: Record "Shpfy Skipped Record"; + begin + SkippedRecord.SetRange("Record ID", ExpectedRecordId); + SkippedRecord.SetFilter("Skipped Reason", '*' + ExpFailureMessage + '*'); + LibraryAssert.RecordIsNotEmpty(SkippedRecord); + end; + + local procedure CreateItemAttributeMappedToItem(var Item: Record Item; var ItemAttribute: Record "Item Attribute"; var ItemAttributeValue: Record "Item Attribute Value") + begin + CreateItemAttributeAsOption(ItemAttribute); + + LibraryInventory.CreateItemAttributeValue(ItemAttributeValue, ItemAttribute.ID, GenerateRandomAttributeValue()); + LibraryInventory.CreateItemAttributeValueMapping(Database::Item, Item."No.", ItemAttribute.ID, ItemAttributeValue.ID); + end; + #endregion +} \ No newline at end of file From ee826c4c6abbb046d7aa66f53b9c4c6964813c3d Mon Sep 17 00:00:00 2001 From: jzaksauskas Date: Wed, 21 Jan 2026 11:03:47 +0200 Subject: [PATCH 19/21] Removed test scenario of checking restriction to have only 1 product option --- .../ShpfyCreateItemVariantTest.Codeunit.al | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/src/Apps/W1/Shopify/Test/Products/ShpfyCreateItemVariantTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Products/ShpfyCreateItemVariantTest.Codeunit.al index ac3e8d7436..683babaf26 100644 --- a/src/Apps/W1/Shopify/Test/Products/ShpfyCreateItemVariantTest.Codeunit.al +++ b/src/Apps/W1/Shopify/Test/Products/ShpfyCreateItemVariantTest.Codeunit.al @@ -140,36 +140,6 @@ codeunit 139632 "Shpfy Create Item Variant Test" LibraryAssert.AreEqual(1, Options.Count(), 'Options not returned'); end; - [Test] - procedure UnitTestCreateVariantFromProductWithMultipleOptions() - var - Item: Record "Item"; - ShpfyProductInitTest: Codeunit "Shpfy Product Init Test"; - CreateItemAsVariant: Codeunit "Shpfy Create Item As Variant"; - CreateItemAsVariantSub: Codeunit "Shpfy CreateItemAsVariantSub"; - ProductId: BigInteger; - begin - // [SCENARIO] Create a variant from a product with multiple options - Initialize(); - - // [GIVEN] Item - Item := ShpfyProductInitTest.CreateItem(Shop."Item Templ. Code", Any.DecimalInRange(10, 100, 2), Any.DecimalInRange(100, 500, 2)); - // [GIVEN] Shopify product - ProductId := CreateShopifyProduct(Item.SystemId); - - // [GIVEN] Multiple options for the product in Shopify - CreateItemAsVariantSub.SetMultipleOptions(true); - - // [WHEN] Invoke ProductAPI.CheckProductAndShopSettings - BindSubscription(CreateItemAsVariantSub); - CreateItemAsVariant.SetParentProduct(ProductId); - asserterror CreateItemAsVariant.CheckProductAndShopSettings(); - UnbindSubscription(CreateItemAsVariantSub); - - // [THEN] Error is thrown - LibraryAssert.ExpectedError('The product has more than one option. Items cannot be added as variants to a product with multiple options.'); - end; - [Test] procedure UnitTestCreateVariantFromSameItem() var From 545db1017def11556aaa1313c33681fd13fab5c5 Mon Sep 17 00:00:00 2001 From: jzaksauskas Date: Wed, 21 Jan 2026 13:29:51 +0200 Subject: [PATCH 20/21] Added additioanal tests for Item Attributes as Prod. Options --- .../VariantCreatedFromItemResponse.txt | 1 + .../ShpfyItemAttrAsOptionTest.Codeunit.al | 234 +++++++++++++----- 2 files changed, 176 insertions(+), 59 deletions(-) create mode 100644 src/Apps/W1/Shopify/Test/.resources/Products/VariantCreatedFromItemResponse.txt diff --git a/src/Apps/W1/Shopify/Test/.resources/Products/VariantCreatedFromItemResponse.txt b/src/Apps/W1/Shopify/Test/.resources/Products/VariantCreatedFromItemResponse.txt new file mode 100644 index 0000000000..b40a1d8ff4 --- /dev/null +++ b/src/Apps/W1/Shopify/Test/.resources/Products/VariantCreatedFromItemResponse.txt @@ -0,0 +1 @@ +{"data":{"productVariantsBulkCreate":{"productVariants":[{"legacyResourceId":"55278024982862","createdAt":"2026-01-21T10:17:50Z","updatedAt":"2026-01-21T10:17:50Z"}],"userErrors":[]}},"extensions":{"cost":{"requestedQueryCost":10,"actualQueryCost":10,"throttleStatus":{"maximumAvailable":2000.0,"currentlyAvailable":1990,"restoreRate":100.0}}}} \ No newline at end of file diff --git a/src/Apps/W1/Shopify/Test/Products/ShpfyItemAttrAsOptionTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Products/ShpfyItemAttrAsOptionTest.Codeunit.al index 579aa9f4d2..9d9914f82a 100644 --- a/src/Apps/W1/Shopify/Test/Products/ShpfyItemAttrAsOptionTest.Codeunit.al +++ b/src/Apps/W1/Shopify/Test/Products/ShpfyItemAttrAsOptionTest.Codeunit.al @@ -18,16 +18,20 @@ codeunit 139540 "Shpfy Item Attr As Option Test" { Subtype = Test; TestType = IntegrationTest; + TestHttpRequestPolicy = BlockOutboundRequests; TestPermissions = Disabled; var Shop: Record "Shpfy Shop"; Any: Codeunit Any; LibraryVariableStorage: Codeunit "Library - Variable Storage"; + HttpHandlerParams: Codeunit "Library - Variable Storage"; + OutboundHttpRequests: Codeunit "Library - Variable Storage"; LibraryAssert: Codeunit "Library Assert"; LibraryInventory: Codeunit "Library - Inventory"; InitializeTest: Codeunit "Shpfy Initialize Test"; IsInitialized: Boolean; + UnexpectedAPICallsErr: Label 'More than expected API calls to Shopify detected.'; trigger OnRun() begin @@ -40,6 +44,7 @@ codeunit 139540 "Shpfy Item Attr As Option Test" var ItemAttribute: Record "Item Attribute"; FailureMessageErr: Label 'UoM as Variant is unavailable due to existing Item Attributes marked as “As Option” which are utilized for Shopify Product Options.'; + ExpectedErrorNotRaisedErr: Label 'Expected error was not raised.'; begin // [SCENARIO] Enabling 'UoM as Variant' fails when 'As Option' Item Attributes exist @@ -53,7 +58,7 @@ codeunit 139540 "Shpfy Item Attr As Option Test" asserterror Shop.Validate("UoM as Variant", true); // [THEN] Error is raised about unavailability - LibraryAssert.IsTrue(GetLastErrorText().Contains(FailureMessageErr), 'Expected error was not raised.'); + LibraryAssert.IsTrue(GetLastErrorText().Contains(FailureMessageErr), ExpectedErrorNotRaisedErr); end; #endregion @@ -84,19 +89,20 @@ codeunit 139540 "Shpfy Item Attr As Option Test" end; [Test] + [HandlerFunctions('HttpSubmitHandler_GetProductOptions')] procedure UnitTestAddItemAsVariantToProductWithoutAsOptionAttributes() var Item: Record Item; - ShpfyVariant: Record "Shpfy Variant"; CreateItemAsVariant: Codeunit "Shpfy Create Item As Variant"; - CreateItemAsVariantSub: Codeunit "Shpfy CreateItemAsVariantSub"; ParentProductId: BigInteger; - VariantId: BigInteger; + ProdOptionNameTok: Label 'Variant', Locked = true; begin // [SCENARIO] Adding Item as variant to product without 'As Option' attributes creates variant with 'Variant' option // [GIVEN] Shopify Shop is created Initialize(); + RegisterOutboundHttpRequests(); + HttpHandlerParams.Enqueue(ProdOptionNameTok); // [GIVEN] Product exists without As Option Attributes ParentProductId := CreateShopifyProductWithoutAsOptionAttributes(); @@ -105,15 +111,12 @@ codeunit 139540 "Shpfy Item Attr As Option Test" CreateItem(Item); // [WHEN] Add Item as Shopify Variant - BindSubscription(CreateItemAsVariantSub); CreateItemAsVariant.SetParentProduct(ParentProductId); CreateItemAsVariant.CheckProductAndShopSettings(); CreateItemAsVariant.CreateVariantFromItem(Item); - VariantId := CreateItemAsVariantSub.GetNewVariantId(); - UnbindSubscription(CreateItemAsVariantSub); // [THEN] New Variant is created with Option 1 Name 'Variant', Option 1 Value '' - VerifyVariantCreatedWithItemNo(ShpfyVariant, VariantId, Item."No."); + VerifyVariantCreatedWithItemNo(ParentProductId, Item."No."); end; #endregion @@ -203,20 +206,10 @@ codeunit 139540 "Shpfy Item Attr As Option Test" Item := CreateItemWithAsOptionAttributes(2); // [GIVEN] Product exists with 'As Option' Attributes - ParentProductId := CreateShopifyProductWithAsOptionAttributesAndValues( - CopyStr(LibraryVariableStorage.PeekText(2), 1, 250), - CopyStr(LibraryVariableStorage.PeekText(4), 1, 250), - CopyStr(LibraryVariableStorage.PeekText(6), 1, 250), - CopyStr(LibraryVariableStorage.PeekText(8), 1, 250)); + ParentProductId := CreateShopifyProductWithAsOptionAttributesAndValues(CopyStr(LibraryVariableStorage.PeekText(2), 1, 250), CopyStr(LibraryVariableStorage.PeekText(4), 1, 250), CopyStr(LibraryVariableStorage.PeekText(6), 1, 250), CopyStr(LibraryVariableStorage.PeekText(8), 1, 250)); // [GIVEN] Item is created with the same 'As Option' Item Attribute values as existing variant - Item := CreateItemWithSpecificAsOptionAttributes( - LibraryVariableStorage.PeekInteger(1), - LibraryVariableStorage.PeekInteger(3), - CopyStr(LibraryVariableStorage.PeekText(4), 1, 250), - LibraryVariableStorage.PeekInteger(5), - LibraryVariableStorage.PeekInteger(7), - CopyStr(LibraryVariableStorage.PeekText(8), 1, 250)); + Item := CreateItemWithSpecificAsOptionAttributes(LibraryVariableStorage.PeekInteger(1), LibraryVariableStorage.PeekInteger(3), CopyStr(LibraryVariableStorage.PeekText(4), 1, 250), LibraryVariableStorage.PeekInteger(5), LibraryVariableStorage.PeekInteger(7), CopyStr(LibraryVariableStorage.PeekText(8), 1, 250)); // [WHEN] Validate item attributes for new variant TempShopifyVariant.Init(); @@ -227,6 +220,41 @@ codeunit 139540 "Shpfy Item Attr As Option Test" VerifySkippedEntryExists(Item.RecordId, ExpFailureMessageErr); end; + [Test] + [HandlerFunctions('HttpSubmitHandler_GetProductOptions')] + procedure UnitTestAddItemAsVariantToProductWithAsOptionAttributes() + var + Item: Record Item; + ProductExport: Codeunit "Shpfy Product Export"; + CreateItemAsVariant: Codeunit "Shpfy Create Item As Variant"; + ParentProductId: BigInteger; + begin + // [SCENARIO] Item variant is successfully created for the product when Item attributes differs + + // [GIVEN] Shopify Shop is created + Initialize(); + ProductExport.SetShop(Shop); + RegisterOutboundHttpRequests(); + + // [GIVEN] Item is created without Item variants but with 2 'As Option' Item Attributes + Item := CreateItemWithAsOptionAttributes(2); + HttpHandlerParams.Enqueue(LibraryVariableStorage.PeekText(2)); + + // [GIVEN] Product exists with 'As Option' Attributes + ParentProductId := CreateShopifyProductWithAsOptionAttributesAndValues(CopyStr(LibraryVariableStorage.PeekText(2), 1, 250), CopyStr(LibraryVariableStorage.PeekText(4), 1, 250), CopyStr(LibraryVariableStorage.PeekText(6), 1, 250), CopyStr(LibraryVariableStorage.PeekText(8), 1, 250)); + + // [GIVEN] Item is created without Item variants and with all required 'As Option' Item Attributes + Item := CreateItemWithSpecificAsOptionAttributes(LibraryVariableStorage.PeekInteger(1), LibraryVariableStorage.PeekInteger(3), CopyStr(LibraryVariableStorage.PeekText(4), 1, 250), LibraryVariableStorage.PeekInteger(5), LibraryVariableStorage.PeekInteger(7), GenerateRandomAttributeValue()); + + // [WHEN] Add Item as Shopify Variant + CreateItemAsVariant.SetParentProduct(ParentProductId); + CreateItemAsVariant.CheckProductAndShopSettings(); + CreateItemAsVariant.CreateVariantFromItem(Item); + + // [THEN] New Variant is created with new combination of product options + VerifyVariantCreatedWithCorrectOptionValues(ParentProductId); + end; + #endregion #region 2 Variants, No As Option Attributes @@ -340,15 +368,25 @@ codeunit 139540 "Shpfy Item Attr As Option Test" #region Helper Procedures local procedure Initialize() + var + CommunicationMgt: Codeunit "Shpfy Communication Mgt."; + LibraryRandom: Codeunit "Library - Random"; + AccessToken: SecretText; begin LibraryVariableStorage.Clear(); + OutboundHttpRequests.Clear(); + HttpHandlerParams.Clear(); Any.SetDefaultSeed(); if IsInitialized then exit; Shop := InitializeTest.CreateShop(); - Shop."UoM as Variant" := false; - Shop.Modify(); + + // Disable Event Mocking + CommunicationMgt.SetTestInProgress(false); + //Register Shopify Access Token + AccessToken := LibraryRandom.RandText(20); + InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken); Commit(); IsInitialized := true; end; @@ -368,8 +406,10 @@ codeunit 139540 "Shpfy Item Attr As Option Test" local procedure CreateItem(var Item: Record Item) var ProductInitTest: Codeunit "Shpfy Product Init Test"; + ItemTemplateCode: Code[20]; begin - Item := ProductInitTest.CreateItem(); + ItemTemplateCode := Shop."Item Templ. Code"; + Item := ProductInitTest.CreateItem(ItemTemplateCode, Any.DecimalInRange(10, 100, 2), Any.DecimalInRange(100, 500, 2), false); end; local procedure CreateItemWithVariants(var Item: Record Item; NumberOfVariants: Integer) @@ -512,58 +552,99 @@ codeunit 139540 "Shpfy Item Attr As Option Test" end; local procedure VerifyVariantHasNoOptions(var TempShopifyVariant: Record "Shpfy Variant" temporary) + var + EmptyOptionName1Lbl: Label 'Option 1 Name should be empty'; + EmptyOptionName2Lbl: Label 'Option 2 Name should be empty'; + EmptyOptionValue1Lbl: Label 'Option 1 Value should be empty'; + EmptyOptionValue2Lbl: Label 'Option 2 Value should be empty'; begin LibraryAssert.RecordIsNotEmpty(TempShopifyVariant); - LibraryAssert.AreEqual('', TempShopifyVariant."Option 1 Name", 'Option 1 Name should be empty'); - LibraryAssert.AreEqual('', TempShopifyVariant."Option 1 Value", 'Option 1 Value should be empty'); - LibraryAssert.AreEqual('', TempShopifyVariant."Option 2 Name", 'Option 2 Name should be empty'); - LibraryAssert.AreEqual('', TempShopifyVariant."Option 2 Value", 'Option 2 Value should be empty'); + LibraryAssert.AreEqual('', TempShopifyVariant."Option 1 Name", EmptyOptionName1Lbl); + LibraryAssert.AreEqual('', TempShopifyVariant."Option 1 Value", EmptyOptionValue1Lbl); + LibraryAssert.AreEqual('', TempShopifyVariant."Option 2 Name", EmptyOptionName2Lbl); + LibraryAssert.AreEqual('', TempShopifyVariant."Option 2 Value", EmptyOptionValue2Lbl); + end; + + local procedure VerifyVariantCreatedWithItemNo(ParentProductId: BigInteger; ItemNo: Code[20]) + var + ShpfyVariant: Record "Shpfy Variant"; + Option1NameMismatchMsg: Label 'Option 1 Name should be ''Variant'''; + Option1ValueShouldBeItemNoMsg: Label 'Option 1 Value should be Item No.'; + begin + ShpfyVariant.SetRange("Product Id", ParentProductId); + ShpfyVariant.SetRange("Shop Code", Shop.Code); + ShpfyVariant.FindLast(); + LibraryAssert.AreEqual(HttpHandlerParams.PeekText(1), ShpfyVariant."Option 1 Name", Option1NameMismatchMsg); + LibraryAssert.AreEqual(ItemNo, ShpfyVariant."Option 1 Value", Option1ValueShouldBeItemNoMsg); end; - local procedure VerifyVariantCreatedWithItemNo(var ShpfyVariant: Record "Shpfy Variant"; VariantId: BigInteger; ItemNo: Code[20]) + local procedure VerifyVariantCreatedWithCorrectOptionValues(ParentProductId: BigInteger) + var + ShpfyVariant: Record "Shpfy Variant"; + Option1NameMismatchMsg: Label 'Incorrect Option 1 Name'; + Option1ValueMismatchMsg: Label 'Incorrect Option 1 Value'; + Option2NameMismatchMsg: Label 'Incorrect Option 2 Name'; + Option2ValueMismatchMsg: Label 'Incorrect Option 2 Value'; begin - LibraryAssert.IsTrue(ShpfyVariant.Get(VariantId), 'Variant should be created'); - LibraryAssert.AreEqual('Variant', ShpfyVariant."Option 1 Name", 'Option 1 Name should be ''Variant'''); - LibraryAssert.AreEqual(ItemNo, ShpfyVariant."Option 1 Value", 'Option 1 Value should be Item No.'); + ShpfyVariant.SetRange("Product Id", ParentProductId); + ShpfyVariant.SetRange("Shop Code", Shop.Code); + ShpfyVariant.FindLast(); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(2), ShpfyVariant."Option 1 Name", Option1NameMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(4), ShpfyVariant."Option 1 Value", Option1ValueMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(6), ShpfyVariant."Option 2 Name", Option2NameMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(8), ShpfyVariant."Option 2 Value", Option2ValueMismatchMsg); end; local procedure VerifyVariantHas2Options(var TempShopifyVariant: Record "Shpfy Variant" temporary) + var + Option1NameMismatchMsg: Label 'Incorrect Option 1 Name'; + Option1ValueMismatchMsg: Label 'Incorrect Option 1 Value'; + Option2NameMismatchMsg: Label 'Incorrect Option 2 Name'; + Option2ValueMismatchMsg: Label 'Incorrect Option 2 Value'; begin LibraryAssert.RecordIsNotEmpty(TempShopifyVariant); - LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(2), TempShopifyVariant."Option 1 Name", 'Option 1 Name should not be empty'); - LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(4), TempShopifyVariant."Option 1 Value", 'Option 1 Value should not be empty'); - LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(6), TempShopifyVariant."Option 2 Name", 'Option 2 Name should not be empty'); - LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(8), TempShopifyVariant."Option 2 Value", 'Option 2 Value should not be empty'); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(2), TempShopifyVariant."Option 1 Name", Option1NameMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(4), TempShopifyVariant."Option 1 Value", Option1ValueMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(6), TempShopifyVariant."Option 2 Name", Option2NameMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(8), TempShopifyVariant."Option 2 Value", Option2ValueMismatchMsg); end; local procedure VerifyProductHasVariants(var TempShopifyProduct: Record "Shpfy Product" temporary) + var + ProductHasVariantsMsg: Label 'Product should be marked as having variants'; begin - LibraryAssert.IsTrue(TempShopifyProduct."Has Variants", 'Product should be marked as having variants'); + LibraryAssert.IsTrue(TempShopifyProduct."Has Variants", ProductHasVariantsMsg); end; local procedure VerifyItemAttributesValidationForNewVariantFailed(ValidationResult: Boolean) + var + IncorrectValidationResultMsg: Label 'Validation result was incorrect.'; begin - LibraryAssert.IsFalse(ValidationResult, 'Validation result was incorrect.'); + LibraryAssert.IsFalse(ValidationResult, IncorrectValidationResultMsg); end; local procedure VerifyVariantsCreatedWithOptionName(var TempShopifyVariant: Record "Shpfy Variant" temporary; ExpectedCount: Integer; ExpectedOption1Name: Text) + var + Option1ValueNotEmptyMsg: Label 'Option 1 Value should not be empty'; + Option1NameAssertionMsg: Label 'Incorrect Option 1 Name'; + IncorrectVariantCountMsg: Label 'Incorect count of variants has been be created'; begin - LibraryAssert.AreEqual(ExpectedCount, TempShopifyVariant.Count(), Format(ExpectedCount) + ' variants should be created'); + LibraryAssert.AreEqual(ExpectedCount, TempShopifyVariant.Count(), IncorrectVariantCountMsg); TempShopifyVariant.FindSet(); repeat - LibraryAssert.AreEqual(ExpectedOption1Name, TempShopifyVariant."Option 1 Name", 'Option 1 Name should be ''' + ExpectedOption1Name + ''''); - LibraryAssert.AreNotEqual('', TempShopifyVariant."Option 1 Value", 'Option 1 Value should not be empty'); + LibraryAssert.AreEqual(ExpectedOption1Name, TempShopifyVariant."Option 1 Name", Option1NameAssertionMsg); + LibraryAssert.AreNotEqual('', TempShopifyVariant."Option 1 Value", Option1ValueNotEmptyMsg); until TempShopifyVariant.Next() = 0; end; local procedure VerifyVariantsCreatedWith3Options(var TempShopifyVariant: Record "Shpfy Variant" temporary; ExpectedCount: Integer) var - Option1NameMismatchLbl: Label 'Option 1 Name has incorrect value.'; - Option1ValueMismatchLbl: Label 'Option 1 Value has incorrect value.'; - Option2NameMismatchLbl: Label 'Option 2 Name has incorrect value.'; - Option2ValueMismatchLbl: Label 'Option 2 Value has incorrect value.'; - Option3NameMismatchLbl: Label 'Option 3 Name has incorrect value.'; - Option3ValueMismatchLbl: Label 'Option 3 Value has incorrect value.'; + Option1NameMismatchMsg: Label 'Option 1 Name has incorrect value.'; + Option1ValueMismatchMsg: Label 'Option 1 Value has incorrect value.'; + Option2NameMismatchMsg: Label 'Option 2 Name has incorrect value.'; + Option2ValueMismatchMsg: Label 'Option 2 Value has incorrect value.'; + Option3NameMismatchMsg: Label 'Option 3 Name has incorrect value.'; + Option3ValueMismatchMsg: Label 'Option 3 Value has incorrect value.'; ItemVariantNumber: Integer; begin LibraryAssert.AreEqual(ExpectedCount, TempShopifyVariant.Count(), Format(ExpectedCount) + ' variants should be created'); @@ -571,27 +652,29 @@ codeunit 139540 "Shpfy Item Attr As Option Test" repeat ItemVariantNumber += 1; if ItemVariantNumber = 1 then begin - LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(1), TempShopifyVariant."Option 1 Name", Option1NameMismatchLbl); - LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(2), TempShopifyVariant."Option 1 Value", Option1ValueMismatchLbl); - LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(5), TempShopifyVariant."Option 2 Name", Option2NameMismatchLbl); - LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(6), TempShopifyVariant."Option 2 Value", Option2ValueMismatchLbl); - LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(9), TempShopifyVariant."Option 3 Name", Option3NameMismatchLbl); - LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(10), TempShopifyVariant."Option 3 Value", Option3ValueMismatchLbl); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(1), TempShopifyVariant."Option 1 Name", Option1NameMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(2), TempShopifyVariant."Option 1 Value", Option1ValueMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(5), TempShopifyVariant."Option 2 Name", Option2NameMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(6), TempShopifyVariant."Option 2 Value", Option2ValueMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(9), TempShopifyVariant."Option 3 Name", Option3NameMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(10), TempShopifyVariant."Option 3 Value", Option3ValueMismatchMsg); end; if ItemVariantNumber = 2 then begin - LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(3), TempShopifyVariant."Option 1 Name", Option1NameMismatchLbl); - LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(4), TempShopifyVariant."Option 1 Value", Option1ValueMismatchLbl); - LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(7), TempShopifyVariant."Option 2 Name", Option2NameMismatchLbl); - LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(8), TempShopifyVariant."Option 2 Value", Option2ValueMismatchLbl); - LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(11), TempShopifyVariant."Option 3 Name", Option3NameMismatchLbl); - LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(12), TempShopifyVariant."Option 3 Value", Option3ValueMismatchLbl); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(3), TempShopifyVariant."Option 1 Name", Option1NameMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(4), TempShopifyVariant."Option 1 Value", Option1ValueMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(7), TempShopifyVariant."Option 2 Name", Option2NameMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(8), TempShopifyVariant."Option 2 Value", Option2ValueMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(11), TempShopifyVariant."Option 3 Name", Option3NameMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(12), TempShopifyVariant."Option 3 Value", Option3ValueMismatchMsg); end; until TempShopifyVariant.Next() = 0; end; local procedure VerifyResultOfCompatibilityCheck(CompatibilityCheckResult: Boolean) + var + IncorrectResultMsg: Label 'Incorrect result of compatibility check.'; begin - LibraryAssert.IsFalse(CompatibilityCheckResult, 'Incorrect result of compatibility check.'); + LibraryAssert.IsFalse(CompatibilityCheckResult, IncorrectResultMsg); end; local procedure VerifySkippedEntryExists(ExpectedRecordId: RecordId; ExpFailureMessage: Text) @@ -610,5 +693,38 @@ codeunit 139540 "Shpfy Item Attr As Option Test" LibraryInventory.CreateItemAttributeValue(ItemAttributeValue, ItemAttribute.ID, GenerateRandomAttributeValue()); LibraryInventory.CreateItemAttributeValueMapping(Database::Item, Item."No.", ItemAttribute.ID, ItemAttributeValue.ID); end; + + local procedure RegisterOutboundHttpRequests() + var + GqlProductOptionsLbl: Label 'GQL Product Options'; + VariantCreatedFromItemResponseLbl: Label 'GQL Prod. Variant Creation Response'; + begin + OutboundHttpRequests.Enqueue(GqlProductOptionsLbl); + OutboundHttpRequests.Enqueue(VariantCreatedFromItemResponseLbl); + end; #endregion + + [HttpClientHandler] + internal procedure HttpSubmitHandler_GetProductOptions(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean + var + ResponseText: Text; + ProductOptionsResponseTok: Label 'Products/ProductOptionsResponse.txt', Locked = true; + VariantCreatedFromItemResponseTok: Label 'Products/VariantCreatedFromItemResponse.txt', Locked = true; + begin + if not InitializeTest.VerifyRequestUrl(Request.Path, Shop."Shopify URL") then + exit(true); + + case OutboundHttpRequests.Length() of + 2: + ResponseText := StrSubstNo(NavApp.GetResourceAsText(ProductOptionsResponseTok, TextEncoding::UTF8), HttpHandlerParams.PeekText(1)); + 1: + ResponseText := NavApp.GetResourceAsText(VariantCreatedFromItemResponseTok, TextEncoding::UTF8); + 0: + Error(UnexpectedAPICallsErr); + end; + + Response.Content.WriteFrom(ResponseText); + OutboundHttpRequests.DequeueText(); + exit(false); + end; } \ No newline at end of file From 0170a9870dea65227a81673e9cbac1a2cdc789f6 Mon Sep 17 00:00:00 2001 From: jzaksauskas Date: Thu, 22 Jan 2026 10:32:38 +0200 Subject: [PATCH 21/21] Fix label formatting for item variant error messages and add caption locking in enum --- .../App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al | 2 +- .../App/src/Products/Enums/ShpfyInclInProductSync.Enum.al | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al index 875429af02..5d5035f769 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al @@ -1247,7 +1247,7 @@ codeunit 30178 "Shpfy Product Export" ItemWithoutRequiredAttributeErr: Label 'Item %1 cannot be added as a product variant because it does not have required attributes.', Comment = '%1 = Item No.'; ItemWithoutRequiredAttributeValueErr: Label 'Item %1 cannot be added as a product variant because it does not have a value for the required attributes.', Comment = '%1 = Item No.'; ItemVariantWithoutRequiredAttributeErr: Label 'Item Variant %1 cannot be added as a product variant because it does not have required attributes.', Comment = '%1 = Item No.'; - ItemVariantWithoutRequiredAttributeValueErr: Label 'Item Variant%1 cannot be added as a product variant because it does not have a value for the required attributes.', Comment = '%1 = Item No.'; + ItemVariantWithoutRequiredAttributeValueErr: Label 'Item Variant %1 cannot be added as a product variant because it does not have a value for the required attributes.', Comment = '%1 = Item No.'; begin ItemVariant.SetRange("Item No.", Item."No."); ItemVariant.SetRange("Code", ItemVariantCode); diff --git a/src/Apps/W1/Shopify/App/src/Products/Enums/ShpfyInclInProductSync.Enum.al b/src/Apps/W1/Shopify/App/src/Products/Enums/ShpfyInclInProductSync.Enum.al index ecf72608d9..e9087b5b15 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Enums/ShpfyInclInProductSync.Enum.al +++ b/src/Apps/W1/Shopify/App/src/Products/Enums/ShpfyInclInProductSync.Enum.al @@ -15,7 +15,7 @@ enum 30179 "Shpfy Incl. in Product Sync" value(0; " ") { - Caption = ' '; + Caption = ' ', Locked = true; } value(1; "As Option") {