diff --git a/src/Apps/W1/Shopify/App/src/Order Fulfillments/Tables/ShpfyFulFillmentOrderLine.Table.al b/src/Apps/W1/Shopify/App/src/Order Fulfillments/Tables/ShpfyFulFillmentOrderLine.Table.al index 6b7188b52e..9049492a43 100644 --- a/src/Apps/W1/Shopify/App/src/Order Fulfillments/Tables/ShpfyFulFillmentOrderLine.Table.al +++ b/src/Apps/W1/Shopify/App/src/Order Fulfillments/Tables/ShpfyFulFillmentOrderLine.Table.al @@ -90,5 +90,8 @@ table 30144 "Shpfy FulFillment Order Line" key(Key4; "Shopify Order Id", "Line Item Id") { } + key(Key5; "Shopify Location Id", "Shopify Fulfillment Order Id") + { + } } } \ No newline at end of file diff --git a/src/Apps/W1/Shopify/App/src/Shipping/Codeunits/ShpfyExportShipments.Codeunit.al b/src/Apps/W1/Shopify/App/src/Shipping/Codeunits/ShpfyExportShipments.Codeunit.al index 5c022e6431..56d8fbd89a 100644 --- a/src/Apps/W1/Shopify/App/src/Shipping/Codeunits/ShpfyExportShipments.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Shipping/Codeunits/ShpfyExportShipments.Codeunit.al @@ -75,14 +75,11 @@ codeunit 30190 "Shpfy Export Shipments" internal procedure CreateFulfillmentOrderRequest(SalesShipmentHeader: Record "Sales Shipment Header"; Shop: Record "Shpfy Shop"; var AssignedFulfillmentOrderIds: Dictionary of [BigInteger, Code[20]]) Requests: List of [Text]; var SalesShipmentLine: Record "Sales Shipment Line"; - ShippingAgent: Record "Shipping Agent"; FulfillmentOrderLine: Record "Shpfy FulFillment Order Line"; TempFulfillmentOrderLine: Record "Shpfy FulFillment Order Line" temporary; - TrackingCompany: Enum "Shpfy Tracking Companies"; PrevFulfillmentOrderId: BigInteger; - IsHandled: Boolean; + PrevLocationId: BigInteger; EmptyFulfillment: Boolean; - TrackingUrl: Text; GraphQueryStart: Text; GraphQuery: TextBuilder; LineCount: Integer; @@ -90,6 +87,7 @@ codeunit 30190 "Shpfy Export Shipments" UnfulfillableOrders: List of [BigInteger]; begin Clear(PrevFulfillmentOrderId); + PrevLocationId := -1; SalesShipmentLine.Reset(); SalesShipmentLine.SetRange("Document No.", SalesShipmentHeader."No."); @@ -102,46 +100,8 @@ codeunit 30190 "Shpfy Export Shipments" until SalesShipmentLine.Next() = 0; TempFulfillmentOrderLine.Reset(); - TempFulfillmentOrderLine.SetCurrentKey("Shopify Fulfillment Order Id"); + TempFulfillmentOrderLine.SetCurrentKey("Shopify Location Id", "Shopify Fulfillment Order Id"); if TempFulfillmentOrderLine.FindSet() then begin - GraphQuery.Append('{"query": "mutation {fulfillmentCreate( fulfillment: {'); - if GetNotifyCustomer(Shop, SalesShipmentHeader, TempFulfillmentOrderLine."Shopify Location Id") then - GraphQuery.Append('notifyCustomer: true, ') - else - GraphQuery.Append('notifyCustomer: false, '); - if SalesShipmentHeader."Package Tracking No." <> '' then begin - GraphQuery.Append('trackingInfo: {'); - if SalesShipmentHeader."Shipping Agent Code" <> '' then begin - GraphQuery.Append('company: \"'); - if ShippingAgent.Get(SalesShipmentHeader."Shipping Agent Code") then - if ShippingAgent."Shpfy Tracking Company" = ShippingAgent."Shpfy Tracking Company"::" " then begin - if ShippingAgent.Name = '' then - GraphQuery.Append(ShippingAgent.Code) - else - GraphQuery.Append(ShippingAgent.Name) - end else - GraphQuery.Append(TrackingCompany.Names.Get(TrackingCompany.Ordinals.IndexOf(ShippingAgent."Shpfy Tracking Company".AsInteger()))); - GraphQuery.Append('\",'); - end; - - GraphQuery.Append('number: \"'); - GraphQuery.Append(SalesShipmentHeader."Package Tracking No."); - GraphQuery.Append('\",'); - ShippingEvents.OnBeforeRetrieveTrackingUrl(SalesShipmentHeader, TrackingUrl, IsHandled); - if not IsHandled then - if ShippingAgent."Internet Address" <> '' then - TrackingUrl := ShippingAgent.GetTrackingInternetAddr(SalesShipmentHeader."Package Tracking No."); - - if TrackingUrl <> '' then begin - GraphQuery.Append('url: \"'); - GraphQuery.Append(TrackingUrl); - GraphQuery.Append('\"'); - end; - - GraphQuery.Append('}'); - end; - GraphQuery.Append('lineItemsByFulfillmentOrder: ['); - GraphQueryStart := GraphQuery.ToText(); EmptyFulfillment := true; repeat // Skip fulfillment orders that are assigned and not accepted @@ -151,6 +111,21 @@ codeunit 30190 "Shpfy Export Shipments" if not CanFulfillOrder(TempFulfillmentOrderLine, Shop, UnfulfillableOrders) then continue; + // When location changes (or first non-skipped record), finalize the current mutation and start a new one + if PrevLocationId <> TempFulfillmentOrderLine."Shopify Location Id" then begin + if not EmptyFulfillment then begin + FinalizeFulfillmentQuery(GraphQuery); + GraphQueries.Add(GraphQuery.ToText()); + end; + GraphQuery.Clear(); + GraphQueryStart := BuildFulfillmentQueryStart(Shop, SalesShipmentHeader, TempFulfillmentOrderLine."Shopify Location Id"); + GraphQuery.Append(GraphQueryStart); + Clear(PrevFulfillmentOrderId); + LineCount := 0; + EmptyFulfillment := true; + end; + PrevLocationId := TempFulfillmentOrderLine."Shopify Location Id"; + EmptyFulfillment := false; if PrevFulfillmentOrderId <> TempFulfillmentOrderLine."Shopify Fulfillment Order Id" then begin @@ -175,23 +150,77 @@ codeunit 30190 "Shpfy Export Shipments" LineCount += 1; if LineCount = 250 then begin LineCount := 0; - GraphQuery.Append(']}]})'); - GraphQuery.Append('{fulfillment { legacyResourceId name createdAt updatedAt deliveredAt displayStatus estimatedDeliveryAt status totalQuantity location { legacyResourceId } trackingInfo { number url company } service { serviceName type } fulfillmentLineItems(first: 10) { pageInfo { endCursor hasNextPage } nodes { id quantity originalTotalSet { presentmentMoney { amount } shopMoney { amount }} lineItem { id isGiftCard }}}}, userErrors {field,message}}}"}'); + FinalizeFulfillmentQuery(GraphQuery); GraphQueries.Add(GraphQuery.ToText()); GraphQuery.Clear(); GraphQuery.Append(GraphQueryStart); Clear(PrevFulfillmentOrderId); + EmptyFulfillment := true; end; until TempFulfillmentOrderLine.Next() = 0; - GraphQuery.Append(']}]})'); - GraphQuery.Append('{fulfillment { legacyResourceId name createdAt updatedAt deliveredAt displayStatus estimatedDeliveryAt status totalQuantity location { legacyResourceId } trackingInfo { number url company } service { serviceName type } fulfillmentLineItems(first: 10) { pageInfo { endCursor hasNextPage } nodes { id quantity originalTotalSet { presentmentMoney { amount } shopMoney { amount }} lineItem { id isGiftCard }}}}, userErrors {field,message}}}"}'); - if not EmptyFulfillment then + if not EmptyFulfillment then begin + FinalizeFulfillmentQuery(GraphQuery); GraphQueries.Add(GraphQuery.ToText()); + end; end; exit(GraphQueries); end; end; + local procedure BuildFulfillmentQueryStart(Shop: Record "Shpfy Shop"; SalesShipmentHeader: Record "Sales Shipment Header"; LocationId: BigInteger): Text + var + ShippingAgent: Record "Shipping Agent"; + TrackingCompany: Enum "Shpfy Tracking Companies"; + IsHandled: Boolean; + TrackingUrl: Text; + GraphQuery: TextBuilder; + begin + GraphQuery.Append('{"query": "mutation {fulfillmentCreate( fulfillment: {'); + if GetNotifyCustomer(Shop, SalesShipmentHeader, LocationId) then + GraphQuery.Append('notifyCustomer: true, ') + else + GraphQuery.Append('notifyCustomer: false, '); + if SalesShipmentHeader."Package Tracking No." <> '' then begin + GraphQuery.Append('trackingInfo: {'); + if SalesShipmentHeader."Shipping Agent Code" <> '' then begin + GraphQuery.Append('company: \"'); + if ShippingAgent.Get(SalesShipmentHeader."Shipping Agent Code") then + if ShippingAgent."Shpfy Tracking Company" = ShippingAgent."Shpfy Tracking Company"::" " then begin + if ShippingAgent.Name = '' then + GraphQuery.Append(ShippingAgent.Code) + else + GraphQuery.Append(ShippingAgent.Name) + end else + GraphQuery.Append(TrackingCompany.Names.Get(TrackingCompany.Ordinals.IndexOf(ShippingAgent."Shpfy Tracking Company".AsInteger()))); + GraphQuery.Append('\",'); + end; + + GraphQuery.Append('number: \"'); + GraphQuery.Append(SalesShipmentHeader."Package Tracking No."); + GraphQuery.Append('\",'); + ShippingEvents.OnBeforeRetrieveTrackingUrl(SalesShipmentHeader, TrackingUrl, IsHandled); + if not IsHandled then + if ShippingAgent."Internet Address" <> '' then + TrackingUrl := ShippingAgent.GetTrackingInternetAddr(SalesShipmentHeader."Package Tracking No."); + + if TrackingUrl <> '' then begin + GraphQuery.Append('url: \"'); + GraphQuery.Append(TrackingUrl); + GraphQuery.Append('\"'); + end; + + GraphQuery.Append('}'); + end; + GraphQuery.Append('lineItemsByFulfillmentOrder: ['); + exit(GraphQuery.ToText()); + end; + + local procedure FinalizeFulfillmentQuery(var GraphQuery: TextBuilder) + begin + GraphQuery.Append(']}]})'); + GraphQuery.Append('{fulfillment { legacyResourceId name createdAt updatedAt deliveredAt displayStatus estimatedDeliveryAt status totalQuantity location { legacyResourceId } trackingInfo { number url company } service { serviceName type } fulfillmentLineItems(first: 10) { pageInfo { endCursor hasNextPage } nodes { id quantity originalTotalSet { presentmentMoney { amount } shopMoney { amount }} lineItem { id isGiftCard }}}}, userErrors {field,message}}}"}'); + end; + local procedure FindFulfillmentOrderLines(SalesShipmentHeader: Record "Sales Shipment Header"; SalesShipmentLine: Record "Sales Shipment Line"; Shop: Record "Shpfy Shop"; var FulfillmentOrderLine: Record "Shpfy FulFillment Order Line"; var TempFulfillmentOrderLine: Record "Shpfy FulFillment Order Line" temporary; var AssignedFulfillmentOrderIds: Dictionary of [BigInteger, Code[20]]) var RemainingQtyToFulfill: Decimal; diff --git a/src/Apps/W1/Shopify/Test/Shipping/ShpfyShippingHelper.Codeunit.al b/src/Apps/W1/Shopify/Test/Shipping/ShpfyShippingHelper.Codeunit.al index 0c71d5728a..ac14c954f6 100644 --- a/src/Apps/W1/Shopify/Test/Shipping/ShpfyShippingHelper.Codeunit.al +++ b/src/Apps/W1/Shopify/Test/Shipping/ShpfyShippingHelper.Codeunit.al @@ -91,6 +91,75 @@ codeunit 139559 "Shpfy Shipping Helper" exit(FulfillmentOrderHeader); end; + internal procedure CreateOrderLine(ShopifyOrderId: BigInteger; LocationId: BigInteger; DeliveryMethodType: Enum "Shpfy Delivery Method Type"; LineId: BigInteger; VariantId: BigInteger; ProductId: BigInteger; Qty: Integer) + var + OrderLine: Record "Shpfy Order Line"; + begin + Clear(OrderLine); + OrderLine."Shopify Order Id" := ShopifyOrderId; + OrderLine."Shopify Product Id" := ProductId; + OrderLine."Shopify Variant Id" := VariantId; + OrderLine."Line Id" := LineId; + OrderLine.Quantity := Qty; + OrderLine."Location Id" := LocationId; + OrderLine."Delivery Method Type" := DeliveryMethodType; + OrderLine.Insert(); + end; + + internal procedure CreateSalesShipmentLine(DocumentNo: Code[20]; ShpfyOrderLineId: BigInteger; Qty: Decimal; LineNo: Integer) + var + SalesShipmentLine: Record "Sales Shipment Line"; + Any: Codeunit Any; + begin + Any.SetDefaultSeed(); + Clear(SalesShipmentLine); + SalesShipmentLine."Document No." := DocumentNo; + SalesShipmentLine."Line No." := LineNo; + SalesShipmentLine.Type := SalesShipmentLine.Type::Item; + SalesShipmentLine."No." := CopyStr(Any.AlphanumericText(MaxStrLen(SalesShipmentLine."No.")), 1, MaxStrLen(SalesShipmentLine."No.")); + SalesShipmentLine."Shpfy Order Line Id" := ShpfyOrderLineId; + SalesShipmentLine.Quantity := Qty; + SalesShipmentLine.Insert(); + end; + + internal procedure CreateShopifyFulfillmentOrderForLocation(ShopifyOrderId: BigInteger; LocationId: BigInteger; DeliveryMethodType: Enum "Shpfy Delivery Method Type"): Record "Shpfy FulFillment Order Header" + var + OrderLine: Record "Shpfy Order Line"; + FulfillmentOrderHeader: Record "Shpfy FulFillment Order Header"; + FulfillmentOrderLine: Record "Shpfy FulFillment Order Line"; + Any: Codeunit Any; + OrderLineCount: Integer; + begin + Any.SetDefaultSeed(); + Clear(FulfillmentOrderHeader); + FulfillmentOrderHeader."Shopify Fulfillment Order Id" := Any.IntegerInRange(10000, 99999); + FulfillmentOrderHeader."Shopify Order Id" := ShopifyOrderId; + FulfillmentOrderHeader."Shopify Location Id" := LocationId; + FulfillmentOrderHeader."Delivery Method Type" := DeliveryMethodType; + FulfillmentOrderHeader.Insert(); + + OrderLine.Reset(); + OrderLine.SetRange("Shopify Order Id", ShopifyOrderId); + OrderLine.SetRange("Location Id", LocationId); + if OrderLine.FindSet() then + repeat + OrderLineCount += 1; + Clear(FulfillmentOrderLine); + FulfillmentOrderLine."Shopify Fulfillment Order Id" := FulfillmentOrderHeader."Shopify Fulfillment Order Id"; + FulfillmentOrderLine."Shopify Fulfillm. Ord. Line Id" := FulfillmentOrderHeader."Shopify Fulfillment Order Id" * 100 + OrderLineCount; + FulfillmentOrderLine."Shopify Order Id" := ShopifyOrderId; + FulfillmentOrderLine."Shopify Product Id" := OrderLine."Shopify Product Id"; + FulfillmentOrderLine."Shopify Variant Id" := OrderLine."Shopify Variant Id"; + FulfillmentOrderLine."Remaining Quantity" := OrderLine.Quantity; + FulfillmentOrderLine."Shopify Location Id" := LocationId; + FulfillmentOrderLine."Delivery Method Type" := DeliveryMethodType; + FulfillmentOrderLine."Line Item Id" := OrderLine."Line Id"; + FulfillmentOrderLine.Insert(); + until OrderLine.Next() = 0; + + exit(FulfillmentOrderHeader); + end; + internal procedure CreateRandomSalesShipment(var SalesShipmentHeader: Record "Sales Shipment Header"; ShopifyOrderId: BigInteger) var SalesShipmentLine: Record "Sales Shipment Line"; diff --git a/src/Apps/W1/Shopify/Test/Shipping/ShpfyShippingTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Shipping/ShpfyShippingTest.Codeunit.al index aed3d9d21d..784eb6e598 100644 --- a/src/Apps/W1/Shopify/Test/Shipping/ShpfyShippingTest.Codeunit.al +++ b/src/Apps/W1/Shopify/Test/Shipping/ShpfyShippingTest.Codeunit.al @@ -184,6 +184,79 @@ codeunit 139606 "Shpfy Shipping Test" LibraryAssert.AreEqual(0, FulfillmentRequests.Count, 'FulfillmentRequest count check'); end; + [Test] + procedure UnitTestExportShipmentMultipleLocations() + var + SalesShipmentHeader: Record "Sales Shipment Header"; + OrderHeader: Record "Shpfy Order Header"; + FulfillmentOrderHeaderA: Record "Shpfy FulFillment Order Header"; + FulfillmentOrderHeaderB: Record "Shpfy FulFillment Order Header"; + ExportShipments: Codeunit "Shpfy Export Shipments"; + ShippingHelper: Codeunit "Shpfy Shipping Helper"; + DeliveryMethodType: Enum "Shpfy Delivery Method Type"; + FulfillmentRequest: Text; + FulfillmentRequests: List of [Text]; + AssignedFulfillmentOrderIds: Dictionary of [BigInteger, Code[20]]; + ShopifyOrderId: BigInteger; + LocationIdA: BigInteger; + LocationIdB: BigInteger; + LineItemId: BigInteger; + VariantId: BigInteger; + ProductId: BigInteger; + begin + // [SCENARIO] A shipment spanning two Shopify locations must produce separate fulfillment requests per location + // [GIVEN] One item (qty 17) split across two locations: 10 at Location A, 7 at Location B + Initialize(); + Any.SetDefaultSeed(); + LocationIdA := Any.IntegerInRange(10000, 49999); + LocationIdB := Any.IntegerInRange(50000, 99999); + DeliveryMethodType := DeliveryMethodType::Shipping; + LineItemId := Any.IntegerInRange(10000, 99999); + VariantId := Any.IntegerInRange(10000, 99999); + ProductId := Any.IntegerInRange(10000, 99999); + + Clear(OrderHeader); + ShopifyOrderId := Any.IntegerInRange(10000, 99999); + OrderHeader."Shopify Order Id" := ShopifyOrderId; + OrderHeader.Insert(); + + // Same item split across two locations with different quantities + ShippingHelper.CreateOrderLine(ShopifyOrderId, LocationIdA, DeliveryMethodType, LineItemId, VariantId, ProductId, 10); + ShippingHelper.CreateOrderLine(ShopifyOrderId, LocationIdB, DeliveryMethodType, LineItemId + 1, VariantId, ProductId, 7); + + // Create fulfillment orders per location + FulfillmentOrderHeaderA := ShippingHelper.CreateShopifyFulfillmentOrderForLocation(ShopifyOrderId, LocationIdA, DeliveryMethodType); + FulfillmentOrderHeaderB := ShippingHelper.CreateShopifyFulfillmentOrderForLocation(ShopifyOrderId, LocationIdB, DeliveryMethodType); + + // [GIVEN] A shipment of qty 9 that spans both locations (needs items from both fulfillment orders) + Clear(SalesShipmentHeader); + SalesShipmentHeader."No." := CopyStr(Any.AlphanumericText(MaxStrLen(SalesShipmentHeader."No.")), 1, MaxStrLen(SalesShipmentHeader."No.")); + SalesShipmentHeader."Shpfy Order Id" := ShopifyOrderId; + SalesShipmentHeader."Package Tracking No." := CopyStr(Any.AlphanumericText(MaxStrLen(SalesShipmentHeader."Package Tracking No.")), 1, MaxStrLen(SalesShipmentHeader."Package Tracking No.")); + SalesShipmentHeader.Insert(); + + // Shipment line for item at Location A: ship 5 out of 10 + ShippingHelper.CreateSalesShipmentLine(SalesShipmentHeader."No.", LineItemId, 5, 10000); + // Shipment line for item at Location B: ship 4 out of 7 + ShippingHelper.CreateSalesShipmentLine(SalesShipmentHeader."No.", LineItemId + 1, 4, 20000); + + // [WHEN] Invoke the function CreateFulfillmentOrderRequest() + FulfillmentRequests := ExportShipments.CreateFulfillmentOrderRequest(SalesShipmentHeader, Shop, AssignedFulfillmentOrderIds); + + // [THEN] Two separate requests are created, one per location + LibraryAssert.AreEqual(2, FulfillmentRequests.Count, 'Should produce two fulfillment requests, one per location'); + + // [THEN] First request contains only Location A's fulfillment order + FulfillmentRequests.Get(1, FulfillmentRequest); + LibraryAssert.IsTrue(FulfillmentRequest.Contains(Format(FulfillmentOrderHeaderA."Shopify Fulfillment Order Id")), 'First request should contain Location A fulfillment order'); + LibraryAssert.IsFalse(FulfillmentRequest.Contains(Format(FulfillmentOrderHeaderB."Shopify Fulfillment Order Id")), 'First request should not contain Location B fulfillment order'); + + // [THEN] Second request contains only Location B's fulfillment order + FulfillmentRequests.Get(2, FulfillmentRequest); + LibraryAssert.IsTrue(FulfillmentRequest.Contains(Format(FulfillmentOrderHeaderB."Shopify Fulfillment Order Id")), 'Second request should contain Location B fulfillment order'); + LibraryAssert.IsFalse(FulfillmentRequest.Contains(Format(FulfillmentOrderHeaderA."Shopify Fulfillment Order Id")), 'Second request should not contain Location A fulfillment order'); + end; + local procedure Initialize() var CommunicationMgt: Codeunit "Shpfy Communication Mgt.";