diff --git a/Apps/W1/Shopify/app/app.json b/Apps/W1/Shopify/app/app.json index 3ddb14510c..ab77786c2e 100644 --- a/Apps/W1/Shopify/app/app.json +++ b/Apps/W1/Shopify/app/app.json @@ -21,7 +21,7 @@ "idRanges": [ { "from": 30100, - "to": 30360 + "to": 30370 } ], "internalsVisibleTo": [ diff --git a/Apps/W1/Shopify/app/src/Base/Pages/ShpfyShopCard.Page.al b/Apps/W1/Shopify/app/src/Base/Pages/ShpfyShopCard.Page.al index 4a9f745590..edc57c53c5 100644 --- a/Apps/W1/Shopify/app/src/Base/Pages/ShpfyShopCard.Page.al +++ b/Apps/W1/Shopify/app/src/Base/Pages/ShpfyShopCard.Page.al @@ -245,6 +245,11 @@ page 30101 "Shpfy Shop Card" ApplicationArea = All; ToolTip = 'Specifies the status of a product in Shopify via the sync when an item is removed in Shopify or an item is blocked in Business Central.'; } + field("Items Mapped to Products"; Rec."Items Mapped to Products") + { + ApplicationArea = All; + ToolTip = 'Specifies if only the items that are mapped to Shopify products/Shopify variants are synchronized from Posted Sales Invoices to Shopify.'; + } } group(PriceSynchronization) { @@ -517,6 +522,11 @@ page 30101 "Shpfy Shop Card" end; } #endif + field("Posted Invoice Sync"; Rec."Posted Invoice Sync") + { + ApplicationArea = All; + ToolTip = 'Specifies whether the posted sales invoices can be synchronized to Shopify.'; + } } group(ReturnsAndRefunds) { @@ -650,6 +660,19 @@ page 30101 "Shpfy Shop Card" RunPageLink = "Shop Code" = field(Code); ToolTip = 'Maps the Shopify payment methods to the related payment methods and prioritize them.'; } + action(PaymentTerms) + { + ApplicationArea = All; + Caption = 'Payment Terms Mapping'; + Image = SuggestPayment; + Promoted = true; + PromotedCategory = Category4; + PromotedIsBig = true; + PromotedOnly = true; + RunObject = page "Shpfy Payment Terms Mapping"; + RunPageLink = "Shop Code" = field(Code); + ToolTip = 'Maps the Shopify payment terms to the related payment terms and prioritize them.'; + } action(Orders) { ApplicationArea = All; @@ -986,6 +1009,26 @@ page 30101 "Shpfy Shop Card" Report.Run(Report::"Shpfy Sync Shipm. to Shopify"); end; } + action(SyncPostedSalesInvoices) + { + ApplicationArea = All; + Ellipsis = true; + Caption = 'Sync Posted Sales Invoices'; + Image = Export; + Promoted = true; + PromotedCategory = Category5; + PromotedIsBig = true; + PromotedOnly = true; + ToolTip = 'Synchronize posted sales invoices to Shopify. Synchronization will be performed only if the Posted Invoice Sync field is enabled in the Shopify shop.'; + + trigger OnAction(); + var + ExportInvoicetoShpfy: Report "Shpfy Sync Invoices to Shpfy"; + begin + ExportInvoicetoShpfy.SetShop(Rec.Code); + ExportInvoicetoShpfy.Run(); + end; + } action(SyncDisputes) { ApplicationArea = All; diff --git a/Apps/W1/Shopify/app/src/Base/Tables/ShpfyShop.Table.al b/Apps/W1/Shopify/app/src/Base/Tables/ShpfyShop.Table.al index 05bf12e5ad..c90443c731 100644 --- a/Apps/W1/Shopify/app/src/Base/Tables/ShpfyShop.Table.al +++ b/Apps/W1/Shopify/app/src/Base/Tables/ShpfyShop.Table.al @@ -787,6 +787,14 @@ table 30102 "Shpfy Shop" { DataClassification = SystemMetadata; } + field(201; "Items Mapped to Products"; Boolean) + { + Caption = 'Items Must be Mapped to Products'; + } + field(202; "Posted Invoice Sync"; Boolean) + { + Caption = 'Posted Invoice Sync'; + } } keys diff --git a/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLDraftOrderComplete.Codeunit.al b/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLDraftOrderComplete.Codeunit.al new file mode 100644 index 0000000000..921394524d --- /dev/null +++ b/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLDraftOrderComplete.Codeunit.al @@ -0,0 +1,27 @@ +namespace Microsoft.Integration.Shopify; + +/// +/// Codeunit Shpfy GQL DraftOrderComplete (ID 30341) implements Interface Shpfy IGraphQL. +/// +codeunit 30341 "Shpfy GQL DraftOrderComplete" implements "Shpfy IGraphQL" +{ + Access = Internal; + + /// + /// GetGraphQL. + /// + /// Return value of type Text. + internal procedure GetGraphQL(): Text + begin + exit('{"query": "mutation {draftOrderComplete(id: \"gid://shopify/DraftOrder/{{DraftOrderId}}\") { draftOrder { order { legacyResourceId, name }} userErrors { field, message }}}"}'); + end; + + /// + /// GetExpectedCost. + /// + /// Return value of type Integer. + internal procedure GetExpectedCost(): Integer + begin + exit(11); + end; +} diff --git a/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLFulfillOrder.Codeunit.al b/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLFulfillOrder.Codeunit.al new file mode 100644 index 0000000000..8e85b8b2a6 --- /dev/null +++ b/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLFulfillOrder.Codeunit.al @@ -0,0 +1,27 @@ +namespace Microsoft.Integration.Shopify; + +/// +/// Codeunit Shpfy GQL Fulfill Order (ID 30355) implements Interface Shpfy IGraphQL. +/// +codeunit 30355 "Shpfy GQL Fulfill Order" implements "Shpfy IGraphQL" +{ + Access = Internal; + + /// + /// GetGraphQL. + /// + /// Return value of type Text. + internal procedure GetGraphQL(): Text + begin + exit('{"query": "mutation { fulfillmentCreateV2 ( fulfillment: { lineItemsByFulfillmentOrder: [{ fulfillmentOrderId: \"gid://shopify/FulfillmentOrder/{{FulfillmentOrderId}}\" }] }) { fulfillment {id, status} userErrors {field, message}}}"}'); + end; + + /// + /// GetExpectedCost. + /// + /// Return value of type Integer. + internal procedure GetExpectedCost(): Integer + begin + exit(10); + end; +} diff --git a/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLGetFulfillments.Codeunit.al b/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLGetFulfillments.Codeunit.al new file mode 100644 index 0000000000..adb8d1562e --- /dev/null +++ b/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLGetFulfillments.Codeunit.al @@ -0,0 +1,26 @@ +/// +/// Codeunit Shpfy GQL Get Fulfillments (ID 30356) implements Interface Shpfy IGraphQL. +/// +codeunit 30356 "Shpfy GQL Get Fulfillments" implements "Shpfy IGraphQL" +{ + Access = Internal; + + /// + /// GetGraphQL. + /// + /// Return value of type Text. + internal procedure GetGraphQL(): Text + begin + exit('{"query": "{order (id: \"gid://shopify/Order/{{OrderId}}\") { fulfillmentOrders ( first: {{NumberOfOrders}}) { nodes { id }}}}"}'); + end; + + /// + /// GetExpectedCost. + /// + /// Return value of type Integer. + internal procedure GetExpectedCost(): Integer + begin + exit(6); + end; +} + diff --git a/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLPaymentTerms.Codeunit.al b/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLPaymentTerms.Codeunit.al new file mode 100644 index 0000000000..21fc47a25f --- /dev/null +++ b/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLPaymentTerms.Codeunit.al @@ -0,0 +1,28 @@ + +namespace Microsoft.Integration.Shopify; + +/// +/// Codeunit Shpfy GQL Payment Terms (ID 30357) implements Interface Shpfy IGraphQL. +/// +codeunit 30357 "Shpfy GQL Payment Terms" implements "Shpfy IGraphQL" +{ + Access = Internal; + + /// + /// GetGraphQL. + /// + /// Return value of type Text. + internal procedure GetGraphQL(): Text + begin + exit('{"query": "{paymentTermsTemplates{id name paymentTermsType dueInDays description translatedName}}"}'); + end; + + /// + /// GetExpectedCost. + /// + /// Return value of type Integer. + internal procedure GetExpectedCost(): Integer + begin + exit(1); + end; +} diff --git a/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLShopLocales.Codeunit.al b/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLShopLocales.Codeunit.al index 004ba743cb..27a145ce33 100644 --- a/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLShopLocales.Codeunit.al +++ b/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLShopLocales.Codeunit.al @@ -1,6 +1,6 @@ namespace Microsoft.Integration.Shopify; -codeunit 30168 "Shpfy GQL ShopLocales" implements "Shpfy IGraphQL" +codeunit 30358 "Shpfy GQL ShopLocales" implements "Shpfy IGraphQL" { internal procedure GetGraphQL(): Text diff --git a/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLTranslationsRegister.Codeunit.al b/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLTranslationsRegister.Codeunit.al index 4b571c629a..e7b4167fae 100644 --- a/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLTranslationsRegister.Codeunit.al +++ b/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLTranslationsRegister.Codeunit.al @@ -1,6 +1,6 @@ namespace Microsoft.Integration.Shopify; -codeunit 30159 "Shpfy GQL TranslationsRegister" implements "Shpfy IGraphQL" +codeunit 30359 "Shpfy GQL TranslationsRegister" implements "Shpfy IGraphQL" { internal procedure GetGraphQL(): Text diff --git a/Apps/W1/Shopify/app/src/GraphQL/Enums/ShpfyGraphQLType.Enum.al b/Apps/W1/Shopify/app/src/GraphQL/Enums/ShpfyGraphQLType.Enum.al index 7659605415..76c143c034 100644 --- a/Apps/W1/Shopify/app/src/GraphQL/Enums/ShpfyGraphQLType.Enum.al +++ b/Apps/W1/Shopify/app/src/GraphQL/Enums/ShpfyGraphQLType.Enum.al @@ -405,6 +405,26 @@ enum 30111 "Shpfy GraphQL Type" implements "Shpfy IGraphQL" Caption = 'Get Order Transactions'; Implementation = "Shpfy IGraphQL" = "Shpfy GQL OrderTransactions"; } + value(80; DraftOrderComplete) + { + Caption = 'Draft Order Complete'; + Implementation = "Shpfy IGraphQL" = "Shpfy GQL DraftOrderComplete"; + } + value(81; FulfillOrder) + { + Caption = 'Fulfill Order'; + Implementation = "Shpfy IGraphQL" = "Shpfy GQL Fulfill Order"; + } + value(82; GetPaymentTerms) + { + Caption = 'Get Payment Terms'; + Implementation = "Shpfy IGraphQL" = "Shpfy GQL Payment Terms"; + } + value(83; GetFulfillmentOrderIds) + { + Caption = 'Get Fulfillments'; + Implementation = "Shpfy IGraphQL" = "Shpfy GQL Get Fulfillments"; + } value(85; ProductVariantDelete) { Caption = 'Product Variant Delete'; diff --git a/Apps/W1/Shopify/app/src/Integration/Codeunits/ShpfyAuthenticationMgt.Codeunit.al b/Apps/W1/Shopify/app/src/Integration/Codeunits/ShpfyAuthenticationMgt.Codeunit.al index 7a0c1ed557..3770859591 100644 --- a/Apps/W1/Shopify/app/src/Integration/Codeunits/ShpfyAuthenticationMgt.Codeunit.al +++ b/Apps/W1/Shopify/app/src/Integration/Codeunits/ShpfyAuthenticationMgt.Codeunit.al @@ -15,7 +15,7 @@ codeunit 30199 "Shpfy Authentication Mgt." var // https://shopify.dev/api/usage/access-scopes - ScopeTxt: Label 'write_orders,read_all_orders,write_assigned_fulfillment_orders,read_checkouts,write_customers,read_discounts,write_files,write_merchant_managed_fulfillment_orders,write_fulfillments,write_inventory,read_locations,read_payment_terms,write_products,write_shipping,read_shopify_payments_disputes,read_shopify_payments_payouts,write_returns,write_translations,write_third_party_fulfillment_orders,write_order_edits,write_companies,write_publications,read_locales', Locked = true; + ScopeTxt: Label 'write_orders,read_all_orders,write_assigned_fulfillment_orders,read_checkouts,write_customers,read_discounts,write_files,write_merchant_managed_fulfillment_orders,write_fulfillments,write_inventory,read_locations,write_products,write_shipping,read_shopify_payments_disputes,read_shopify_payments_payouts,write_returns,write_translations,write_third_party_fulfillment_orders,write_order_edits,write_companies,write_publications,read_payment_terms,write_payment_terms,write_draft_orders,read_locales', Locked = true; ShopifyAPIKeyAKVSecretNameLbl: Label 'ShopifyApiKey', Locked = true; ShopifyAPISecretAKVSecretNameLbl: Label 'ShopifyApiSecret', Locked = true; MissingAPIKeyTelemetryTxt: Label 'The api key has not been initialized.', Locked = true; diff --git a/Apps/W1/Shopify/app/src/Invoicing/Codeunits/ShpfyDraftOrdersAPI.Codeunit.al b/Apps/W1/Shopify/app/src/Invoicing/Codeunits/ShpfyDraftOrdersAPI.Codeunit.al new file mode 100644 index 0000000000..0e67fc2d40 --- /dev/null +++ b/Apps/W1/Shopify/app/src/Invoicing/Codeunits/ShpfyDraftOrdersAPI.Codeunit.al @@ -0,0 +1,386 @@ +namespace Microsoft.Integration.Shopify; + +using Microsoft.Sales.Comment; +using Microsoft.Sales.History; +using Microsoft.Finance.Currency; +using Microsoft.Inventory.Item.Attribute; +using Microsoft.Inventory.Item; + +/// +/// Codeunit Draft Orders API (ID 30159). +/// +codeunit 30159 "Shpfy Draft Orders API" +{ + Access = Internal; + + var + ShpfyShop: Record "Shpfy Shop"; + ShpfyCommunicationMgt: Codeunit "Shpfy Communication Mgt."; + ShpfyJsonHelper: Codeunit "Shpfy Json Helper"; + + /// + /// Creates a draft order in shopify by constructing and sending a graphQL request. + /// + /// Header information for a shopify order. + /// Line items for a shopify order. + /// Tax lines for a shopify order. + /// Unique id of the created draft order in shopify. + internal procedure CreateDraftOrder( + var TempShpfyOrderHeader: Record "Shpfy Order Header" temporary; + var TempShpfyOrderLine: Record "Shpfy Order Line" temporary; + var ShpfyOrderTaxLines: Dictionary of [Text, Decimal] + ): BigInteger + var + DraftOrderId: BigInteger; + GraphQuery: TextBuilder; + begin + GraphQuery := CreateDraftOrderGQLRequest(TempShpfyOrderHeader, TempShpfyOrderLine, ShpfyOrderTaxLines); + DraftOrderId := SendDraftOrderGraphQLRequest(GraphQuery); + exit(DraftOrderId); + end; + + /// + /// Completes a draft order in shopify by converting it to an order. + /// + /// Draft order id that needs to be completed. + /// Json response of a created order in shopify. + internal procedure CompleteDraftOrder(DraftOrderId: BigInteger): JsonToken + var + GraphQLType: Enum "Shpfy GraphQL Type"; + Parameters: Dictionary of [Text, Text]; + JResponse: JsonToken; + begin + GraphQLType := "Shpfy GraphQL Type"::DraftOrderComplete; + Parameters.Add('DraftOrderId', Format(DraftOrderId)); + JResponse := ShpfyCommunicationMgt.ExecuteGraphQL(GraphQLType, Parameters); + exit(JResponse); + end; + + /// + /// Sets a global shopify shop to be used for draft orders api functionality. + /// + /// Shopify shop code to be set. + internal procedure SetShop(ShopCode: Code[20]) + begin + Clear(ShpfyShop); + ShpfyShop.Get(ShopCode); + ShpfyCommunicationMgt.SetShop(ShpfyShop); + end; + + local procedure CreateDraftOrderGQLRequest( + var TempShpfyOrderHeader: Record "Shpfy Order Header" temporary; + var TempShpfyOrderLine: Record "Shpfy Order Line" temporary; + var ShpfyOrderTaxLines: Dictionary of [Text, Decimal] + ): TextBuilder + var + GraphQuery: TextBuilder; + begin + GraphQuery.Append('{"query":"mutation {draftOrderCreate(input: {'); + if TempShpfyOrderHeader.Email <> '' then begin + GraphQuery.Append('email: \"'); + GraphQuery.Append(ShpfyCommunicationMgt.EscapeGrapQLData(TempShpfyOrderHeader.Email)); + GraphQuery.Append('\"'); + end; + if TempShpfyOrderHeader."Phone No." <> '' then begin + GraphQuery.Append('phone: \"'); + GraphQuery.Append(ShpfyCommunicationMgt.EscapeGrapQLData(TempShpfyOrderHeader."Phone No.")); + GraphQuery.Append('\"'); + end; + if TempShpfyOrderHeader."Currency Code" <> '' then begin + GraphQuery.Append('presentmentCurrencyCode: '); + GraphQuery.Append(Format(GetISOCode(TempShpfyOrderHeader."Currency Code"))); + end; + if TempShpfyOrderHeader."Discount Amount" <> 0 then + AddDiscountAmountToGraphQuery(GraphQuery, TempShpfyOrderHeader."Discount Amount", 'Invoice Discount Amount'); + + GraphQuery.Append(', taxExempt: true'); + + AddShippingAddressToGraphQuery(GraphQuery, TempShpfyOrderHeader); + AddBillingAddressToGraphQuery(GraphQuery, TempShpfyOrderHeader); + AddNote(GraphQuery, TempShpfyOrderHeader); + if TempShpfyOrderHeader.Unpaid then + AddPaymentTerms(GraphQuery, TempShpfyOrderHeader); + + AddLineItemsToGraphQuery(GraphQuery, TempShpfyOrderHeader, TempShpfyOrderLine, ShpfyOrderTaxLines); + + GraphQuery.Append('}) {draftOrder { legacyResourceId } userErrors {field, message}}'); + GraphQuery.Append('}"}'); + + exit(GraphQuery); + end; + + local procedure SendDraftOrderGraphQLRequest(GraphQuery: TextBuilder): BigInteger + var + DraftOrderId: BigInteger; + JResponse: JsonToken; + begin + JResponse := ShpfyCommunicationMgt.ExecuteGraphQL(GraphQuery.ToText()); + DraftOrderId := ShpfyJsonHelper.GetValueAsBigInteger(JResponse, 'data.draftOrderCreate.draftOrder.legacyResourceId'); + exit(DraftOrderId); + end; + + local procedure AddShippingAddressToGraphQuery(var GraphQuery: TextBuilder; var TempShpfyOrderHeader: Record "Shpfy Order Header" temporary) + begin + GraphQuery.Append(', shippingAddress: {'); + if TempShpfyOrderHeader."Ship-to Address" <> '' then begin + GraphQuery.Append('address1: \"'); + GraphQuery.Append(ShpfyCommunicationMgt.EscapeGrapQLData(TempShpfyOrderHeader."Ship-to Address")); + GraphQuery.Append('\"'); + end; + if TempShpfyOrderHeader."Ship-to Address 2" <> '' then begin + GraphQuery.Append(', address2: \"'); + GraphQuery.Append(ShpfyCommunicationMgt.EscapeGrapQLData(TempShpfyOrderHeader."Ship-to Address 2")); + GraphQuery.Append('\"'); + end; + if TempShpfyOrderHeader."Ship-to City" <> '' then begin + GraphQuery.Append(', city: \"'); + GraphQuery.Append(ShpfyCommunicationMgt.EscapeGrapQLData(TempShpfyOrderHeader."Ship-to City")); + GraphQuery.Append('\"'); + end; + if TempShpfyOrderHeader."Ship-to Country/Region Code" <> '' then begin + GraphQuery.Append(', countryCode: '); + GraphQuery.Append(TempShpfyOrderHeader."Ship-to Country/Region Code"); + end; + if TempShpfyOrderHeader."Ship-to Post Code" <> '' then begin + GraphQuery.Append(', zip: \"'); + GraphQuery.Append(ShpfyCommunicationMgt.EscapeGrapQLData(TempShpfyOrderHeader."Ship-to Post Code")); + GraphQuery.Append('\"'); + end; + if TempShpfyOrderHeader."Ship-to Name" <> '' then begin + GraphQuery.Append(', firstName: \"'); + GraphQuery.Append(ShpfyCommunicationMgt.EscapeGrapQLData(TempShpfyOrderHeader."Ship-to Name")); + GraphQuery.Append('\"'); + end; + if TempShpfyOrderHeader."Ship-to Name 2" <> '' then begin + GraphQuery.Append(', lastName: \"'); + GraphQuery.Append(ShpfyCommunicationMgt.EscapeGrapQLData(TempShpfyOrderHeader."Ship-to Name 2")); + GraphQuery.Append('\"'); + end; + GraphQuery.Append('}'); + end; + + local procedure AddBillingAddressToGraphQuery(var GraphQuery: TextBuilder; var TempShpfyOrderHeader: Record "Shpfy Order Header" temporary) + begin + GraphQuery.Append(', billingAddress: {'); + if TempShpfyOrderHeader."Bill-to Address" <> '' then begin + GraphQuery.Append('address1: \"'); + GraphQuery.Append(ShpfyCommunicationMgt.EscapeGrapQLData(TempShpfyOrderHeader."Bill-to Address")); + GraphQuery.Append('\"'); + end; + if TempShpfyOrderHeader."Bill-to Address 2" <> '' then begin + GraphQuery.Append(', address2: \"'); + GraphQuery.Append(ShpfyCommunicationMgt.EscapeGrapQLData(TempShpfyOrderHeader."Bill-to Address 2")); + GraphQuery.Append('\"'); + end; + if TempShpfyOrderHeader."Bill-to City" <> '' then begin + GraphQuery.Append(', city: \"'); + GraphQuery.Append(ShpfyCommunicationMgt.EscapeGrapQLData(TempShpfyOrderHeader."Bill-to City")); + GraphQuery.Append('\"'); + end; + if TempShpfyOrderHeader."Bill-to Country/Region Code" <> '' then begin + GraphQuery.Append(', countryCode: '); + GraphQuery.Append(TempShpfyOrderHeader."Bill-to Country/Region Code"); + end; + if TempShpfyOrderHeader."Bill-to Post Code" <> '' then begin + GraphQuery.Append(', zip: \"'); + GraphQuery.Append(ShpfyCommunicationMgt.EscapeGrapQLData(TempShpfyOrderHeader."Bill-to Post Code")); + GraphQuery.Append('\"'); + end; + if TempShpfyOrderHeader."Bill-to Name" <> '' then begin + GraphQuery.Append(', firstName: \"'); + GraphQuery.Append(ShpfyCommunicationMgt.EscapeGrapQLData(TempShpfyOrderHeader."Bill-to Name")); + GraphQuery.Append('\"'); + end; + if TempShpfyOrderHeader."Bill-to Name 2" <> '' then begin + GraphQuery.Append(', lastName: \"'); + GraphQuery.Append(ShpfyCommunicationMgt.EscapeGrapQLData(TempShpfyOrderHeader."Bill-to Name 2")); + GraphQuery.Append('\"'); + end; + GraphQuery.Append('}'); + end; + + local procedure AddLineItemsToGraphQuery( + var GraphQuery: TextBuilder; + var TempShpfyOrderHeader: Record "Shpfy Order Header" temporary; + var TempShpfyOrderLine: Record "Shpfy Order Line" temporary; + var ShpfyOrderTaxLines: Dictionary of [Text, Decimal] + ) + var + TaxTitle: Text; + begin + TempShpfyOrderLine.SetRange("Shopify Order Id", TempShpfyOrderHeader."Shopify Order Id"); + if TempShpfyOrderLine.FindSet(false) then begin + GraphQuery.Append(', lineItems: ['); + repeat + GraphQuery.Append('{'); + GraphQuery.Append('title: \"'); + GraphQuery.Append(ShpfyCommunicationMgt.EscapeGrapQLData(TempShpfyOrderLine.Description)); + GraphQuery.Append('\"'); + + if TempShpfyOrderLine."Shopify Variant Id" <> 0 then begin + GraphQuery.Append(', variantId: \"gid://shopify/ProductVariant/'); + GraphQuery.Append(Format(TempShpfyOrderLine."Shopify Variant Id")); + GraphQuery.Append('\"'); + + AddItemAttributes(GraphQuery, TempShpfyOrderLine."Item No."); + end; + + GraphQuery.Append(', quantity: '); + GraphQuery.Append(Format(TempShpfyOrderLine.Quantity, 0, 9)); + + GraphQuery.Append(', originalUnitPrice :'); + GraphQuery.Append(Format(TempShpfyOrderLine."Unit Price", 0, 9)); + + GraphQuery.Append('},'); + until TempShpfyOrderLine.Next() = 0; + + foreach TaxTitle in ShpfyOrderTaxLines.Keys() do begin + GraphQuery.Append('{'); + GraphQuery.Append('title: \"'); + GraphQuery.Append(ShpfyCommunicationMgt.EscapeGrapQLData(TaxTitle)); + GraphQuery.Append('\"'); + + GraphQuery.Append(', quantity: '); + GraphQuery.Append(Format(1, 0, 9)); + + GraphQuery.Append(', originalUnitPrice: '); + GraphQuery.Append(Format(ShpfyOrderTaxLines.Get(TaxTitle), 0, 9)); + + GraphQuery.Append('},'); + end; + GraphQuery.Remove(GraphQuery.Length(), 1); + end; + GraphQuery.Append(']'); + end; + + local procedure AddDiscountAmountToGraphQuery(var GraphQuery: TextBuilder; DiscountAmount: Decimal; DiscountTitle: Text) + begin + GraphQuery.Append(', appliedDiscount: {'); + GraphQuery.Append('description: \"'); + GraphQuery.Append(ShpfyCommunicationMgt.EscapeGrapQLData(DiscountTitle)); + GraphQuery.Append('\"'); + + GraphQuery.Append(', value: '); + GraphQuery.Append(Format(DiscountAmount, 0, 9)); + + GraphQuery.Append(', valueType: '); + GraphQuery.Append('FIXED_AMOUNT'); + + GraphQuery.Append(', title: \"'); + GraphQuery.Append(ShpfyCommunicationMgt.EscapeGrapQLData(DiscountTitle)); + GraphQuery.Append('\"'); + + GraphQuery.Append('}'); + end; + + local procedure AddNote(var GraphQuery: TextBuilder; var TempShpfyOrderHeader: Record "Shpfy Order Header" temporary) + var + SalesCommentLine: Record "Sales Comment Line"; + NotesTextBuilder: TextBuilder; + begin + SalesCommentLine.SetRange("Document Type", SalesCommentLine."Document Type"::"Posted Invoice"); + SalesCommentLine.SetRange("No.", TempShpfyOrderHeader."Sales Invoice No."); + + if SalesCommentLine.FindSet() then begin + GraphQuery.Append(', note: \"'); + repeat + NotesTextBuilder.Append(SalesCommentLine.Comment + '\n'); + until SalesCommentLine.Next() = 0; + GraphQuery.Append(NotesTextBuilder.ToText()); + GraphQuery.Append('\"'); + end; + end; + + local procedure AddPaymentTerms(var GraphQuery: TextBuilder; var TempShpfyOrderHeader: Record "Shpfy Order Header" temporary) + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + ShpfyPaymentTerms: Record "Shpfy Payment Terms"; + DueAtDateTime: DateTime; + IssuedAtDateTime: DateTime; + begin + if not ShopifyPaymentTermsExists(ShpfyPaymentTerms, TempShpfyOrderHeader, SalesInvoiceHeader) then + exit; + + GraphQuery.Append(', paymentTerms: {'); + GraphQuery.Append('paymentTermsTemplateId: \"gid://shopify/PaymentTermsTemplate/'); + GraphQuery.Append(Format(ShpfyPaymentTerms.Id)); + GraphQuery.Append('\"'); + + Evaluate(IssuedAtDateTime, Format(SalesInvoiceHeader."Document Date")); + Evaluate(DueAtDateTime, Format(SalesInvoiceHeader."Due Date")); + + GraphQuery.Append(', paymentSchedules: {'); + if ShpfyPaymentTerms.Type = 'FIXED' then begin + GraphQuery.Append('dueAt: \"'); + GraphQuery.Append(ShpfyCommunicationMgt.EscapeGrapQLData(Format(DueAtDateTime, 0, 9))); + GraphQuery.Append('\"'); + end else + if ShpfyPaymentTerms.Type = 'NET' then begin + GraphQuery.Append(', issuedAt: \"'); + GraphQuery.Append(ShpfyCommunicationMgt.EscapeGrapQLData(Format(IssuedAtDateTime, 0, 9))); + GraphQuery.Append('\"'); + end; + + GraphQuery.Append('}}'); + end; + + local procedure ShopifyPaymentTermsExists( + var ShpfyPaymentTerms: Record "Shpfy Payment Terms"; + var TempShpfyOrderHeader: Record "Shpfy Order Header" temporary; + var SalesInvoiceHeader: Record "Sales Invoice Header" + ): Boolean + begin + SalesInvoiceHeader.Get(TempShpfyOrderHeader."Sales Invoice No."); + ShpfyPaymentTerms.SetRange("Payment Terms Code", SalesInvoiceHeader."Payment Terms Code"); + ShpfyPaymentTerms.SetRange("Shop Code", ShpfyShop.Code); + + if not ShpfyPaymentTerms.FindFirst() then begin + ShpfyPaymentTerms.SetRange("Payment Terms Code"); + ShpfyPaymentTerms.SetRange("Is Primary", true); + + if not ShpfyPaymentTerms.FindFirst() then + exit(false); + end; + + exit(true); + end; + + local procedure GetISOCode(CurrencyCode: Code[10]): Code[3] + var + Currency: Record Currency; + begin + Currency.Get(CurrencyCode); + exit(Currency."ISO Code"); + end; + + local procedure AddItemAttributes(var GraphQuery: TextBuilder; ItemNo: Code[20]) + var + Item: Record Item; + ItemAttribute: Record "Item Attribute"; + ItemAttributeValue: Record "Item Attribute Value"; + ItemAttributeValueMapping: Record "Item Attribute Value Mapping"; + begin + Item.Get(ItemNo); + ItemAttributeValueMapping.SetRange("Table ID", Database::Item); + ItemAttributeValueMapping.SetRange("No.", ItemNo); + if ItemAttributeValueMapping.FindSet() then begin + GraphQuery.Append(', customAttributes: ['); + repeat + ItemAttribute.Get(ItemAttributeValueMapping."Item Attribute ID"); + ItemAttributeValue.Get(ItemAttribute.ID, ItemAttributeValueMapping."Item Attribute Value ID"); + + GraphQuery.Append('{'); + GraphQuery.Append('key: \"'); + GraphQuery.Append(ShpfyCommunicationMgt.EscapeGrapQLData(Format(ItemAttribute.Name))); + GraphQuery.Append('\"'); + + GraphQuery.Append(', value: \"'); + GraphQuery.Append(ShpfyCommunicationMgt.EscapeGrapQLData(Format(ItemAttributeValue.Value))); + GraphQuery.Append('\"'); + GraphQuery.Append('},') + until ItemAttributeValueMapping.Next() = 0; + GraphQuery.Append(']'); + end; + + end; +} \ No newline at end of file diff --git a/Apps/W1/Shopify/app/src/Invoicing/Codeunits/ShpfyFulfillmentAPI.Codeunit.al b/Apps/W1/Shopify/app/src/Invoicing/Codeunits/ShpfyFulfillmentAPI.Codeunit.al new file mode 100644 index 0000000000..e63b44d222 --- /dev/null +++ b/Apps/W1/Shopify/app/src/Invoicing/Codeunits/ShpfyFulfillmentAPI.Codeunit.al @@ -0,0 +1,69 @@ +namespace Microsoft.Integration.Shopify; + +/// +/// Codeunit Shpfy Fulfillment API (ID 30361). +/// +codeunit 30361 "Shpfy Fulfillment API" +{ + Access = Internal; + + var + ShpfyCommunicationMgt: Codeunit "Shpfy Communication Mgt."; + + /// + /// Creates a fulfillment for a provided fulfillment order id. + /// + /// Fulfillment order id. + internal procedure CreateFulfillment(FulfillmentOrderId: BigInteger) + var + JResponse: JsonToken; + GraphQLType: Enum "Shpfy GraphQL Type"; + Parameters: Dictionary of [Text, Text]; + begin + GraphQLType := "Shpfy GraphQL Type"::FulfillOrder; + Parameters.Add('FulfillmentOrderId', Format(FulfillmentOrderId)); + JResponse := ShpfyCommunicationMgt.ExecuteGraphQL(GraphQLType, Parameters); + end; + + /// + /// Gets fulfillment order ids for a provided shopify order id. + /// + /// Shopify order id to get fulfillments from. + /// Number of fulfillment orders to get. + /// List of fulfillment order ids. + internal procedure GetFulfillmentOrderIds(OrderId: Text; NumberOfLines: Integer) FulfillmentOrderList: List of [BigInteger] + var + GraphQLType: Enum "Shpfy GraphQL Type"; + Parameters: Dictionary of [Text, Text]; + JFulfillments: JsonToken; + begin + GraphQLType := "Shpfy GraphQL Type"::GetFulfillmentOrderIds; + Parameters.Add('OrderId', OrderId); + Parameters.Add('NumberOfOrders', Format(NumberOfLines)); + JFulfillments := ShpfyCommunicationMgt.ExecuteGraphQL(GraphQLType, Parameters); + FulfillmentOrderList := ParseFulfillmentOrders(JFulfillments); + + exit(FulfillmentOrderList); + end; + + /// + /// Sets a global shopify shop to be used for fulfillment api functionality. + /// + /// Shopify shop code to be set. + internal procedure SetShop(ShopCode: Code[20]) + begin + ShpfyCommunicationMgt.SetShop(ShopCode); + end; + + local procedure ParseFulfillmentOrders(JFulfillments: JsonToken) FulfillmentOrderList: List of [BigInteger] + var + ShpfyJsonHelper: Codeunit "Shpfy Json Helper"; + JArray: JsonArray; + JToken: JsonToken; + begin + JArray := ShpfyJsonHelper.GetJsonArray(JFulfillments, 'data.order.fulfillmentOrders.nodes'); + + foreach JToken in JArray do + FulfillmentOrderList.Add(ShpfyCommunicationMgt.GetIdOfGId(ShpfyJsonHelper.GetValueAsText(JToken, 'id'))); + end; +} \ No newline at end of file diff --git a/Apps/W1/Shopify/app/src/Invoicing/Codeunits/ShpfyPostedInvoiceExport.Codeunit.al b/Apps/W1/Shopify/app/src/Invoicing/Codeunits/ShpfyPostedInvoiceExport.Codeunit.al new file mode 100644 index 0000000000..bcd6981e3e --- /dev/null +++ b/Apps/W1/Shopify/app/src/Invoicing/Codeunits/ShpfyPostedInvoiceExport.Codeunit.al @@ -0,0 +1,459 @@ +namespace Microsoft.Integration.Shopify; + +using Microsoft.Sales.History; +using Microsoft.Finance.GeneralLedger.Setup; + +/// +/// Codeunit Shpfy Posted Invoice Export" (ID 30362). +/// +codeunit 30362 "Shpfy Posted Invoice Export" +{ + Access = Internal; + TableNo = "Sales Invoice Header"; + Permissions = tabledata "Sales Invoice Header" = m; + + var + ShpfyShop: Record "Shpfy Shop"; + ShpfyDraftOrdersAPI: Codeunit "Shpfy Draft Orders API"; + ShpfyFulfillmentAPI: Codeunit "Shpfy Fulfillment API"; + ShpfyJsonHelper: Codeunit "Shpfy Json Helper"; + + trigger OnRun() + begin + ExportPostedSalesInvoiceToShopify(Rec); + end; + + /// + /// Sets a global shopify shop to be used for posted invoice export. + /// + /// Shopify shop code to be set. + internal procedure SetShop(NewShopCode: Code[20]) + begin + ShpfyShop.Get(NewShopCode); + ShpfyDraftOrdersAPI.SetShop(ShpfyShop.Code); + ShpfyFulfillmentAPI.SetShop(ShpfyShop.Code); + end; + + /// + /// Exports provided posted sales invoice to shopify. + /// + /// + /// If the posted sales invoice isn't exportable, the shopify order id is set to -2. + /// If shopify order creation fails, the id is set to -1. + /// + /// Posted sales invoice to be exported. + internal procedure ExportPostedSalesInvoiceToShopify(SalesInvoiceHeader: Record "Sales Invoice Header") + var + TempShpfyOrderHeader: Record "Shpfy Order Header" temporary; + TempShpfyOrderLine: Record "Shpfy Order Line" temporary; + DraftOrderId: BigInteger; + ShpfyOrderTaxLines: Dictionary of [Text, Decimal]; + FulfillmentOrderIds: List of [BigInteger]; + JResponse: JsonToken; + OrderId: BigInteger; + OrderNo: Text; + begin + if not IsInvoiceExportable(SalesInvoiceHeader) then begin + SetSalesInvoiceShopifyOrderInformation(SalesInvoiceHeader, -2, ''); + exit; + end; + + MapPostedSalesInvoiceData(SalesInvoiceHeader, TempShpfyOrderHeader, TempShpfyOrderLine, ShpfyOrderTaxLines); + + DraftOrderId := ShpfyDraftOrdersAPI.CreateDraftOrder(TempShpfyOrderHeader, TempShpfyOrderLine, ShpfyOrderTaxLines); + JResponse := ShpfyDraftOrdersAPI.CompleteDraftOrder(DraftOrderId); + + if IsSuccess(JResponse) then begin + OrderId := ShpfyJsonHelper.GetValueAsBigInteger(JResponse, 'data.draftOrderComplete.draftOrder.order.legacyResourceId'); + OrderNo := ShpfyJsonHelper.GetValueAsText(JResponse, 'data.draftOrderComplete.draftOrder.order.name'); + + FulfillmentOrderIds := ShpfyFulfillmentAPI.GetFulfillmentOrderIds(Format(OrderId), GetNumberOfLines(TempShpfyOrderLine, ShpfyOrderTaxLines)); + CreateFulfillmentsForShopifyOrder(FulfillmentOrderIds); + CreateShpfyInvoiceHeader(OrderId); + SetSalesInvoiceShopifyOrderInformation(SalesInvoiceHeader, OrderId, Format(OrderNo)); + AddDocumentLinkToBCDocument(SalesInvoiceHeader); + end else + SetSalesInvoiceShopifyOrderInformation(SalesInvoiceHeader, -1, ''); + end; + + local procedure CreateFulfillmentsForShopifyOrder(FulfillmentOrderIds: List of [BigInteger]) + var + FulfillmentOrderId: BigInteger; + begin + foreach FulfillmentOrderId in FulfillmentOrderIds do + ShpfyFulfillmentAPI.CreateFulfillment(FulfillmentOrderId); + end; + + local procedure IsInvoiceExportable(SalesInvoiceHeader: Record "Sales Invoice Header"): Boolean + var + ShpfyCompany: Record "Shpfy Company"; + ShpfyCustomer: Record "Shpfy Customer"; + begin + ShpfyCompany.SetRange("Customer No.", SalesInvoiceHeader."Bill-to Customer No."); + if ShpfyCompany.IsEmpty() then begin + ShpfyCustomer.SetRange("Customer No.", SalesInvoiceHeader."Bill-to Customer No."); + if ShpfyCustomer.IsEmpty() then + exit(false); + end; + + if not CurrencyCodeMatch(SalesInvoiceHeader) then + exit(false); + + if not ShopifyPaymentTermsExists(SalesInvoiceHeader."Payment Terms Code") then + exit(false); + + if ShpfyShop."Default Customer No." = SalesInvoiceHeader."Bill-to Customer No." then + exit(false); + + if CheckCustomerTemplates(SalesInvoiceHeader."Bill-to Customer No.") then + exit(false); + + if not CheckSalesInvoiceHeaderLines(SalesInvoiceHeader) then + exit(false); + + exit(true); + end; + + local procedure CurrencyCodeMatch(SalesInvoiceHeader: Record "Sales Invoice Header"): Boolean + var + GeneralLedgerSetup: Record "General Ledger Setup"; + ShopifyLocalCurrencyCode: Code[10]; + begin + GeneralLedgerSetup.Get(); + + if ShpfyShop."Currency Code" = '' then + ShopifyLocalCurrencyCode := GeneralLedgerSetup."LCY Code" + else + ShopifyLocalCurrencyCode := ShpfyShop."Currency Code"; + + if SalesInvoiceHeader."Currency Code" = '' then + exit(ShopifyLocalCurrencyCode = GeneralLedgerSetup."LCY Code") + else + exit(ShopifyLocalCurrencyCode = SalesInvoiceHeader."Currency Code"); + end; + + local procedure ShopifyPaymentTermsExists(PaymentTermsCode: Code[10]): Boolean + var + ShpfyPaymentTerms: Record "Shpfy Payment Terms"; + begin + ShpfyPaymentTerms.SetRange("Payment Terms Code", PaymentTermsCode); + ShpfyPaymentTerms.SetRange("Shop Code", ShpfyShop.Code); + + if not ShpfyPaymentTerms.FindFirst() then begin + ShpfyPaymentTerms.SetRange("Payment Terms Code"); + ShpfyPaymentTerms.SetRange("Is Primary", true); + + if not ShpfyPaymentTerms.FindFirst() then + exit(false); + end; + + exit(true); + end; + + local procedure CheckCustomerTemplates(CustomerNo: Code[20]): Boolean + var + ShpfyCustomerTemplate: Record "Shpfy Customer Template"; + begin + ShpfyCustomerTemplate.SetRange("Default Customer No.", CustomerNo); + ShpfyCustomerTemplate.SetRange("Shop Code", ShpfyShop.Code); + exit(not ShpfyCustomerTemplate.IsEmpty()); + end; + + local procedure CheckSalesInvoiceHeaderLines(SalesInvoiceHeader: Record "Sales Invoice Header"): Boolean + var + SalesInvoiceLine: Record "Sales Invoice Line"; + begin + SalesInvoiceLine.SetFilter(Type, '<>%1', SalesInvoiceLine.Type::" "); + if SalesInvoiceLine.IsEmpty() then + exit(false); + + SalesInvoiceLine.Reset(); + + SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); + SalesInvoiceLine.SetRange(Type, SalesInvoiceLine.Type::Item); + if SalesInvoiceLine.FindSet() then + repeat + if (SalesInvoiceLine.Quantity <> 0) and (SalesInvoiceLine.Quantity <> Round(SalesInvoiceLine.Quantity, 1)) then + exit(false); + + if ShpfyShop."Items Mapped to Products" then + if not ItemIsMappedToShopifyProduct(SalesInvoiceLine) then + exit(false); + + if (SalesInvoiceLine.Type <> SalesInvoiceLine.Type::" ") and (SalesInvoiceLine."No." = '') then + exit(false); + until SalesInvoiceLine.Next() = 0; + + exit(true); + end; + + local procedure SetSalesInvoiceShopifyOrderInformation(var SalesInvoiceHeader: Record "Sales Invoice Header"; OrderId: BigInteger; OrderNo: Code[50]) + begin + SalesInvoiceHeader.Validate("Shpfy Order Id", OrderId); + SalesInvoiceHeader.Validate("Shpfy Order No.", OrderNo); + SalesInvoiceHeader.Modify(true); + end; + + local procedure ItemIsMappedToShopifyProduct(SalesInvoiceLine: Record "Sales Invoice Line"): Boolean + var + ShpfyProduct: Record "Shpfy Product"; + ShpfyVariant: Record "Shpfy Variant"; + begin + ShpfyProduct.SetRange("Item No.", SalesInvoiceLine."No."); + if ShpfyProduct.IsEmpty() then + exit(false); + + if ShpfyShop."UoM as Variant" then begin + if not ProductVariantExists(SalesInvoiceLine."Unit of Measure Code", SalesInvoiceLine) then + exit(false); + end else begin + ShpfyVariant.SetRange("Item No.", SalesInvoiceLine."No."); + ShpfyVariant.SetRange("Variant Code", SalesInvoiceLine."Variant Code"); + ShpfyVariant.SetRange("Shop Code", ShpfyShop.Code); + if ShpfyVariant.IsEmpty() then + exit(false); + end; + + exit(true); + end; + + local procedure ProductVariantExists(UnitOfMeasure: Code[10]; SalesInvoiceLine: Record "Sales Invoice Line"): Boolean + var + ShpfyVariant: Record "Shpfy Variant"; + begin + ShpfyVariant.SetRange("Item No.", SalesInvoiceLine."No."); + ShpfyVariant.SetRange("Shop Code", ShpfyShop.Code); + ShpfyVariant.SetRange("Variant Code", SalesInvoiceLine."Variant Code"); + if ShpfyVariant.FindSet() then + repeat + case ShpfyVariant."UoM Option Id" of + 1: + if ShpfyVariant."Option 1 Value" = UnitOfMeasure then + exit(true); + 2: + if ShpfyVariant."Option 2 Value" = UnitOfMeasure then + exit(true); + 3: + if ShpfyVariant."Option 3 Value" = UnitOfMeasure then + exit(true); + end; + until ShpfyVariant.Next() = 0; + end; + + local procedure MapPostedSalesInvoiceData( + SalesInvoiceHeader: Record "Sales Invoice Header"; + var TempShpfyOrderHeader: Record "Shpfy Order Header" temporary; + var TempShpfyOrderLine: Record "Shpfy Order Line" temporary; + var ShpfyOrderTaxLines: Dictionary of [Text, Decimal] + ) + var + InvoiceLine: Record "Sales Invoice Line"; + begin + MapSalesInvoiceHeader(SalesInvoiceHeader, TempShpfyOrderHeader); + + InvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); + if InvoiceLine.FindSet() then + repeat + MapSalesInvoiceLine(InvoiceLine, TempShpfyOrderHeader, TempShpfyOrderLine, ShpfyOrderTaxLines); + until InvoiceLine.Next() = 0; + end; + + local procedure MapSalesInvoiceHeader( + SalesInvoiceHeader: Record "Sales Invoice Header"; + var TempShpfyOrderHeader: Record "Shpfy Order Header" temporary + ) + begin + TempShpfyOrderHeader.Init(); + TempShpfyOrderHeader."Sales Invoice No." := SalesInvoiceHeader."No."; + TempShpfyOrderHeader."Sales Order No." := SalesInvoiceHeader."Order No."; + TempShpfyOrderHeader."Created At" := SalesInvoiceHeader.SystemCreatedAt; + TempShpfyOrderHeader.Confirmed := true; + TempShpfyOrderHeader."Updated At" := SalesInvoiceHeader.SystemModifiedAt; + TempShpfyOrderHeader."Currency Code" := MapCurrencyCode(SalesInvoiceHeader); + TempShpfyOrderHeader."Document Date" := SalesInvoiceHeader."Document Date"; + SalesInvoiceHeader.CalcFields(Amount, "Amount Including VAT", "Invoice Discount Amount"); + TempShpfyOrderHeader."VAT Amount" := SalesInvoiceHeader."Amount Including VAT" - SalesInvoiceHeader.Amount; + TempShpfyOrderHeader."Discount Amount" := SalesInvoiceHeader."Invoice Discount Amount"; + TempShpfyOrderHeader."Fulfillment Status" := Enum::"Shpfy Order Fulfill. Status"::Fulfilled; + TempShpfyOrderHeader."Shop Code" := ShpfyShop.Code; + TempShpfyOrderHeader.Unpaid := IsInvoiceUnpaid(SalesInvoiceHeader); + + MapBillToInformation(TempShpfyOrderHeader, SalesInvoiceHeader); + MapShipToInformation(TempShpfyOrderHeader, SalesInvoiceHeader); + + TempShpfyOrderHeader.Insert(false); + end; + + local procedure MapCurrencyCode(SalesInvoiceHeader: Record "Sales Invoice Header"): Code[10] + var + GeneralLedgerSetup: Record "General Ledger Setup"; + begin + if SalesInvoiceHeader."Currency Code" <> '' then + exit(SalesInvoiceHeader."Currency Code"); + + GeneralLedgerSetup.Get(); + exit(GeneralLedgerSetup."LCY Code"); + end; + + local procedure MapBillToInformation( + var TempShpfyOrderHeader: Record "Shpfy Order Header" temporary; + SalesInvoiceHeader: Record "Sales Invoice Header" + ) + var + ShpfyCustomer: Record "Shpfy Customer"; + begin + TempShpfyOrderHeader."Bill-to Name" := CopyStr(SalesInvoiceHeader."Bill-to Name", 1, MaxStrLen(TempShpfyOrderHeader."Bill-to Name")); + TempShpfyOrderHeader."Bill-to Name 2" := SalesInvoiceHeader."Bill-to Name 2"; + TempShpfyOrderHeader."Bill-to Address" := SalesInvoiceHeader."Bill-to Address"; + TempShpfyOrderHeader."Bill-to Address 2" := SalesInvoiceHeader."Bill-to Address 2"; + TempShpfyOrderHeader."Bill-to Post Code" := SalesInvoiceHeader."Bill-to Post Code"; + TempShpfyOrderHeader."Bill-to City" := SalesInvoiceHeader."Bill-to City"; + TempShpfyOrderHeader."Bill-to County" := SalesInvoiceHeader."Bill-to County"; + TempShpfyOrderHeader."Bill-to Country/Region Code" := SalesInvoiceHeader."Bill-to Country/Region Code"; + TempShpfyOrderHeader."Bill-to Customer No." := SalesInvoiceHeader."Bill-to Customer No."; + + ShpfyCustomer.SetRange("Customer No.", SalesInvoiceHeader."Bill-to Customer No."); + if ShpfyCustomer.FindFirst() then begin + TempShpfyOrderHeader.Email := CopyStr(ShpfyCustomer.Email, 1, MaxStrLen(TempShpfyOrderHeader.Email)); + TempShpfyOrderHeader."Phone No." := ShpfyCustomer."Phone No."; + end; + end; + + local procedure MapShipToInformation( + var TempShpfyOrderHeader: Record "Shpfy Order Header" temporary; + SalesInvoiceHeader: Record "Sales Invoice Header" + ) + begin + TempShpfyOrderHeader."Ship-to Name" := CopyStr(SalesInvoiceHeader."Ship-to Name", 1, MaxStrLen(TempShpfyOrderHeader."Ship-to Name")); + TempShpfyOrderHeader."Ship-to Name 2" := SalesInvoiceHeader."Ship-to Name 2"; + TempShpfyOrderHeader."Ship-to Address" := SalesInvoiceHeader."Ship-to Address"; + TempShpfyOrderHeader."Ship-to Address 2" := SalesInvoiceHeader."Ship-to Address 2"; + TempShpfyOrderHeader."Ship-to Post Code" := SalesInvoiceHeader."Ship-to Post Code"; + TempShpfyOrderHeader."Ship-to City" := SalesInvoiceHeader."Ship-to City"; + TempShpfyOrderHeader."Ship-to County" := SalesInvoiceHeader."Ship-to County"; + TempShpfyOrderHeader."Ship-to Country/Region Code" := SalesInvoiceHeader."Ship-to Country/Region Code"; + end; + + local procedure IsInvoiceUnpaid(SalesInvoiceHeader: Record "Sales Invoice Header"): Boolean + begin + SalesInvoiceHeader.CalcFields("Remaining Amount"); + exit(SalesInvoiceHeader."Remaining Amount" <> 0); + end; + + local procedure MapSalesInvoiceLine( + SalesInvoiceLine: Record "Sales Invoice Line"; + var TempShpfyOrderHeader: Record "Shpfy Order Header" temporary; + var TempShpfyOrderLine: Record "Shpfy Order Line" temporary; + var ShpfyOrderTaxLines: Dictionary of [Text, Decimal] +): BigInteger + var + ShpfyVariant: Record "Shpfy Variant"; + begin + TempShpfyOrderLine.Init(); + TempShpfyOrderLine."Line Id" := SalesInvoiceLine."Line No."; + TempShpfyOrderLine.Description := SalesInvoiceLine.Description; + TempShpfyOrderLine.Quantity := SalesInvoiceLine.Quantity; + TempShpfyOrderLine."Item No." := SalesInvoiceLine."No."; + TempShpfyOrderLine."Variant Code" := SalesInvoiceLine."Variant Code"; + TempShpfyOrderLine."Gift Card" := false; + TempShpfyOrderLine.Taxable := false; + TempShpfyOrderLine."Unit Price" := SalesInvoiceLine."Unit Price"; + TempShpfyOrderHeader."Discount Amount" += SalesInvoiceLine."Line Discount Amount"; + TempShpfyOrderHeader.Modify(false); + + if ShpfyShop."UoM as Variant" then + MapUOMProductVariants(SalesInvoiceLine, TempShpfyOrderLine) + else begin + ShpfyVariant.SetRange("Shop Code", ShpfyShop.Code); + ShpfyVariant.SetRange("Item No.", SalesInvoiceLine."No."); + ShpfyVariant.SetRange("Variant Code", SalesInvoiceLine."Variant Code"); + if ShpfyVariant.FindFirst() then begin + TempShpfyOrderLine."Shopify Product Id" := ShpfyVariant."Product Id"; + TempShpfyOrderLine."Shopify Variant Id" := ShpfyVariant.Id; + end; + end; + + MapTaxLine(SalesInvoiceLine, ShpfyOrderTaxLines); + + TempShpfyOrderLine.Insert(false); + end; + + local procedure MapUOMProductVariants(SalesInvoiceLine: Record "Sales Invoice Line"; var TempShpfyOrderLine: Record "Shpfy Order Line" temporary) + var + ShpfyVariant: Record "Shpfy Variant"; + begin + ShpfyVariant.SetRange("Shop Code", ShpfyShop.Code); + ShpfyVariant.SetRange("Item No.", SalesInvoiceLine."No."); + ShpfyVariant.SetRange("Variant Code", SalesInvoiceLine."Variant Code"); + if ShpfyVariant.FindSet() then + repeat + case ShpfyVariant."UoM Option Id" of + 1: + if ShpfyVariant."Option 1 Value" = SalesInvoiceLine."Unit of Measure Code" then begin + TempShpfyOrderLine."Shopify Product Id" := ShpfyVariant."Product Id"; + TempShpfyOrderLine."Shopify Variant Id" := ShpfyVariant.Id; + exit; + end; + 2: + if ShpfyVariant."Option 2 Value" = SalesInvoiceLine."Unit of Measure Code" then begin + TempShpfyOrderLine."Shopify Product Id" := ShpfyVariant."Product Id"; + TempShpfyOrderLine."Shopify Variant Id" := ShpfyVariant.Id; + exit; + end; + 3: + if ShpfyVariant."Option 3 Value" = SalesInvoiceLine."Unit of Measure Code" then begin + TempShpfyOrderLine."Shopify Product Id" := ShpfyVariant."Product Id"; + TempShpfyOrderLine."Shopify Variant Id" := ShpfyVariant.Id; + exit; + end; + end; + until ShpfyVariant.Next() = 0; + end; + + local procedure MapTaxLine(var SalesInvoiceLine: Record "Sales Invoice Line" temporary; var ShpfyOrderTaxLines: Dictionary of [Text, Decimal]) + var + VATAmount: Decimal; + TaxLineTok: Label '%1 - %2%', Comment = '%1 = VAT Calculation Type, %2 = VAT %', Locked = true; + TaxTitle: Text; + begin + VATAmount := SalesInvoiceLine."Amount Including VAT" - SalesInvoiceLine."VAT Base Amount"; + + TaxTitle := StrSubstNo(TaxLineTok, Format(SalesInvoiceLine."VAT Calculation Type"), Format(SalesInvoiceLine."VAT %")); + if ShpfyOrderTaxLines.ContainsKey(TaxTitle) then + ShpfyOrderTaxLines.Set(TaxTitle, ShpfyOrderTaxLines.Get(TaxTitle) + VATAmount) + else + ShpfyOrderTaxLines.Add(TaxTitle, VATAmount); + end; + + local procedure IsSuccess(JsonTokenResponse: JsonToken): Boolean + begin + exit(ShpfyJsonHelper.GetJsonArray(JsonTokenResponse, 'data.draftOrderComplete.userErrors').Count() = 0); + end; + + local procedure CreateShpfyInvoiceHeader(OrderId: BigInteger) + var + ShpfyInvoiceHeader: Record "Shpfy Invoice Header"; + begin + ShpfyInvoiceHeader.Init(); + ShpfyInvoiceHeader.Validate("Shopify Order Id", OrderId); + ShpfyInvoiceHeader.Insert(true); + end; + + local procedure AddDocumentLinkToBCDocument(SalesInvoiceHeader: Record "Sales Invoice Header") + var + DocLinkToBCDoc: Record "Shpfy Doc. Link To Doc."; + BCDocumentTypeConvert: Codeunit "Shpfy BC Document Type Convert"; + begin + DocLinkToBCDoc.Init(); + DocLinkToBCDoc."Shopify Document Type" := "Shpfy Shop Document Type"::"Shopify Shop Order"; + DocLinkToBCDoc."Shopify Document Id" := SalesInvoiceHeader."Shpfy Order Id"; + DocLinkToBCDoc."Document Type" := BCDocumentTypeConvert.Convert(SalesInvoiceHeader); + DocLinkToBCDoc."Document No." := SalesInvoiceHeader."No."; + DocLinkToBCDoc.Insert(true); + end; + + local procedure GetNumberOfLines(var TempShpfyOrderLine: Record "Shpfy Order Line" temporary; var ShpfyOrderTaxLines: Dictionary of [Text, Decimal]): Integer + begin + exit(ShpfyOrderTaxLines.Count() + TempShpfyOrderLine.Count()); + end; +} \ No newline at end of file diff --git a/Apps/W1/Shopify/app/src/Invoicing/Codeunits/ShpfyUpdateSalesInvoice.Codeunit.al b/Apps/W1/Shopify/app/src/Invoicing/Codeunits/ShpfyUpdateSalesInvoice.Codeunit.al new file mode 100644 index 0000000000..cf03c116ea --- /dev/null +++ b/Apps/W1/Shopify/app/src/Invoicing/Codeunits/ShpfyUpdateSalesInvoice.Codeunit.al @@ -0,0 +1,22 @@ +namespace Microsoft.Integration.Shopify; + +using Microsoft.Sales.History; + +codeunit 30364 "Shpfy Update Sales Invoice" +{ + Access = Internal; + + [EventSubscriber(ObjectType::Page, Page::"Posted Sales Inv. - Update", 'OnAfterRecordChanged', '', false, false)] + local procedure CheckShopifyOrderIdOnAfterRecordChanged(var SalesInvoiceHeader: Record "Sales Invoice Header"; xSalesInvoiceHeader: Record "Sales Invoice Header"; var IsChanged: Boolean) + begin + if IsChanged then + exit; + IsChanged := SalesInvoiceHeader."Shpfy Order Id" <> xSalesInvoiceHeader."Shpfy Order Id"; + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales Inv. Header - Edit", 'OnOnRunOnBeforeTestFieldNo', '', false, false)] + local procedure SetShopifyOrderIdOnBeforeSalesShptHeaderModify(var SalesInvoiceHeader: Record "Sales Invoice Header"; SalesInvoiceHeaderRec: Record "Sales Invoice Header") + begin + SalesInvoiceHeader."Shpfy Order Id" := SalesInvoiceHeaderRec."Shpfy Order Id"; + end; +} \ No newline at end of file diff --git a/Apps/W1/Shopify/app/src/Invoicing/PageExt/ShpfySalesInvoiceUpdate.PageExt.al b/Apps/W1/Shopify/app/src/Invoicing/PageExt/ShpfySalesInvoiceUpdate.PageExt.al new file mode 100644 index 0000000000..d9b090faa4 --- /dev/null +++ b/Apps/W1/Shopify/app/src/Invoicing/PageExt/ShpfySalesInvoiceUpdate.PageExt.al @@ -0,0 +1,44 @@ +namespace Microsoft.Integration.Shopify; + +using Microsoft.Sales.History; + +/// +/// PageExtension Shpfy Sales Invoice Update (ID 30125) extends Record Posted Sales Inv. - Update. +/// +pageextension 30125 "Shpfy Sales Invoice Update" extends "Posted Sales Inv. - Update" +{ + layout + { + addlast(content) + { + group(Shopify) + { + Caption = 'Shopify'; + Visible = ShopifyTabVisible; + + field("Shpfy Order Id"; Rec."Shpfy Order Id") + { + ApplicationArea = Basic, Suite; + Caption = 'Shopify Order Id'; + Editable = (Rec."Shpfy Order Id" = 0) or (Rec."Shpfy Order Id" = -1) or (Rec."Shpfy Order Id" = -2); + ToolTip = 'Specifies the Shopify Order ID. Helps track the status of invoices within Shopify, with 0 indicating readiness to synchronize, -1 indicating an error, and -2 indicating that the shipment is skipped.'; + + trigger OnValidate() + begin + if not (Rec."Shpfy Order Id" in [0, -1, -2]) then + Error(ValueNotAllowedErr); + end; + } + } + } + } + + trigger OnAfterGetRecord() + begin + ShopifyTabVisible := Rec."Shpfy Order Id" <> 0; + end; + + var + ShopifyTabVisible: Boolean; + ValueNotAllowedErr: Label 'Allowed values are 0, -1 or -2. 0 indicates readiness to synchronize, -1 indicates an error, and -2 indicates that the invoice is skipped.'; +} \ No newline at end of file diff --git a/Apps/W1/Shopify/app/src/Invoicing/Reports/ShpfySyncInvoicesToShpfy.Report.al b/Apps/W1/Shopify/app/src/Invoicing/Reports/ShpfySyncInvoicesToShpfy.Report.al new file mode 100644 index 0000000000..38e086d885 --- /dev/null +++ b/Apps/W1/Shopify/app/src/Invoicing/Reports/ShpfySyncInvoicesToShpfy.Report.al @@ -0,0 +1,105 @@ +namespace Microsoft.Integration.Shopify; + +using Microsoft.Sales.History; + +/// +/// Report Shpfy Sync Invoices to Shpfy (ID 30119). +/// +report 30119 "Shpfy Sync Invoices to Shpfy" +{ + ApplicationArea = All; + Caption = 'Sync Invoices to Shopify'; + ProcessingOnly = true; + UsageCategory = Tasks; + + dataset + { + dataitem(SalesInvoiceHeader; "Sales Invoice Header") + { + RequestFilterFields = "No.", "Posting Date"; + trigger OnPreDataItem() + var + ShopCodeNotSetErr: Label 'Shopify Shop Code is empty.'; + PostedInvoiceSyncNotSetErr: Label 'Posted Invoice Sync is not enabled for this shop.'; + begin + if ShopCode = '' then + Error(ShopCodeNotSetErr); + + ShpfyShop.Get(ShopCode); + + if not ShpfyShop."Posted Invoice Sync" then + Error(PostedInvoiceSyncNotSetErr); + + ShpfyPostedInvoiceExport.SetShop(ShopCode); + SetRange("Shpfy Order Id", 0); + + if GuiAllowed then begin + CurrSalesInvoiceHeaderNo := SalesInvoiceHeader."No."; + ProcessDialog.Open(ProcessMsg, CurrSalesInvoiceHeaderNo); + ProcessDialog.Update(); + end; + end; + + trigger OnAfterGetRecord() + begin + if GuiAllowed then begin + CurrSalesInvoiceHeaderNo := SalesInvoiceHeader."No."; + ProcessDialog.Update(); + end; + + ShpfyPostedInvoiceExport.Run(SalesInvoiceHeader); + end; + + trigger OnPostDataItem() + var + ShpfyBackgroundSyncs: Codeunit "Shpfy Background Syncs"; + begin + if GuiAllowed then + ProcessDialog.Close(); + + ShpfyBackgroundSyncs.InventorySync(ShopCode); + end; + } + } + requestpage + { + SaveValues = true; + layout + { + area(Content) + { + group(ShopFilter) + { + Caption = 'Options'; + field(Shop; ShopCode) + { + ApplicationArea = All; + Caption = 'Shop Code'; + Lookup = true; + LookupPageId = "Shpfy Shops"; + TableRelation = "Shpfy Shop"; + ToolTip = 'Specifies the Shopify Shop to which the invoice will be exported.'; + ShowMandatory = true; + } + } + } + } + } + + var + ShpfyShop: Record "Shpfy Shop"; + ShpfyPostedInvoiceExport: Codeunit "Shpfy Posted Invoice Export"; + ShopCode: Code[20]; + CurrSalesInvoiceHeaderNo: Code[20]; + ProcessDialog: Dialog; + ProcessMsg: Label 'Synchronizing Posted Sales Invoice #1####################', Comment = '#1 = Posted Sales Invoice No.'; + + /// + /// Sets a global shopify shop code to be used. + /// + /// Shopify shop code to be set. + internal procedure SetShop(NewShopCode: Code[20]) + begin + ShopCode := NewShopCode; + end; +} \ No newline at end of file diff --git a/Apps/W1/Shopify/app/src/Invoicing/Tables/ShpfyInvoiceHeader.Table.al b/Apps/W1/Shopify/app/src/Invoicing/Tables/ShpfyInvoiceHeader.Table.al new file mode 100644 index 0000000000..001b9f99e0 --- /dev/null +++ b/Apps/W1/Shopify/app/src/Invoicing/Tables/ShpfyInvoiceHeader.Table.al @@ -0,0 +1,27 @@ +namespace Microsoft.Integration.Shopify; + +/// +/// Table Shpfy Invoice Header (ID 30161). +/// +table 30161 "Shpfy Invoice Header" +{ + Caption = 'Shopify Invoice Header'; + DataClassification = CustomerContent; + Access = Internal; + + fields + { + field(1; "Shopify Order Id"; BigInteger) + { + Caption = 'Shopify Order Id'; + } + } + + keys + { + key(PK; "Shopify Order Id") + { + Clustered = true; + } + } +} \ No newline at end of file diff --git a/Apps/W1/Shopify/app/src/Order handling/Codeunits/ShpfyImportOrder.Codeunit.al b/Apps/W1/Shopify/app/src/Order handling/Codeunits/ShpfyImportOrder.Codeunit.al index 69a398c2fd..c4e67f7c00 100644 --- a/Apps/W1/Shopify/app/src/Order handling/Codeunits/ShpfyImportOrder.Codeunit.al +++ b/Apps/W1/Shopify/app/src/Order handling/Codeunits/ShpfyImportOrder.Codeunit.al @@ -116,6 +116,22 @@ codeunit 30161 "Shpfy Import Order" if CheckToCloseOrder(OrderHeader) then CloseOrder(OrderHeader); + + if ShopifyInvoiceExists(OrderHeader) then + MarkAsProcessed(OrderHeader); + end; + + local procedure ShopifyInvoiceExists(OrderHeader: Record "Shpfy Order Header"): Boolean + var + ShpfyInvoiceHeader: Record "Shpfy Invoice Header"; + begin + exit(ShpfyInvoiceHeader.Get(OrderHeader."Shopify Order Id")); + end; + + local procedure MarkAsProcessed(OrderHeader: Record "Shpfy Order Header") + begin + OrderHeader.Validate(Processed, true); + OrderHeader.Modify() end; local procedure InsertOrderLinesAndRelatedRecords(var TempOrderLine: Record "Shpfy Order Line" temporary; var DataCaptureDict: Dictionary of [BigInteger, JsonToken]; var Redundancy: Integer) @@ -502,6 +518,8 @@ codeunit 30161 "Shpfy Import Order" JsonHelper.GetValueIntoField(JOrder, 'currentTotalPriceSet.shopMoney.amount', OrderHeaderRecordRef, OrderHeader.FieldNo("Current Total Amount")); JsonHelper.GetValueIntoField(JOrder, 'currentSubtotalLineItemsQuantity', OrderHeaderRecordRef, OrderHeader.FieldNo("Current Total Items Quantity")); JsonHelper.GetValueIntoField(Jorder, 'poNumber', OrderHeaderRecordRef, OrderHeader.FieldNo("PO Number")); + JsonHelper.GetValueIntoField(JOrder, 'paymentTerms.paymentTermsType', OrderHeaderRecordRef, OrderHeader.FieldNo("Payment Terms Type")); + JsonHelper.GetValueIntoField(JOrder, 'paymentTerms.paymentTermsName', OrderHeaderRecordRef, OrderHeader.FieldNo("Payment Terms Name")); OrderHeaderRecordRef.SetTable(OrderHeader); if JsonHelper.GetJsonObject(JOrder, JObject, 'purchasingEntity') then if JsonHelper.GetJsonObject(JOrder, JObject, 'purchasingEntity.company') then @@ -620,7 +638,7 @@ codeunit 30161 "Shpfy Import Order" OrderAttribute.Value := CopyStr(JsonHelper.GetValueAsText(JToken, 'value', MaxStrLen(OrderAttribute.Value)), 1, MaxStrLen(OrderAttribute.Value)) else #endif - OrderAttribute."Attribute Value" := CopyStr(JsonHelper.GetValueAsText(JToken, 'value', MaxStrLen(OrderAttribute."Attribute Value")), 1, MaxStrLen(OrderAttribute."Attribute Value")); + OrderAttribute."Attribute Value" := CopyStr(JsonHelper.GetValueAsText(JToken, 'value', MaxStrLen(OrderAttribute."Attribute Value")), 1, MaxStrLen(OrderAttribute."Attribute Value")); OrderAttribute.Insert(); end; end; @@ -632,7 +650,7 @@ codeunit 30161 "Shpfy Import Order" begin OrderLineAttribute.SetRange("Order Id", ShopifyOrderId); OrderLineAttribute.SetRange("Order Line Id", OrderLineId); - if not OrderLineAttribute.IsEmpty then + if not OrderLineAttribute.IsEmpty() then OrderLineAttribute.DeleteAll(); foreach JToken in JCustomAttributtes do begin Clear(OrderLineAttribute); @@ -723,7 +741,7 @@ codeunit 30161 "Shpfy Import Order" JToken: JsonToken; begin OrderTaxLine.SetRange("Parent Id", ParentId); - if not OrderTaxLine.IsEmpty then + if not OrderTaxLine.IsEmpty() then OrderTaxLine.DeleteAll(); foreach JToken in JTaxLines do begin RecordRef.Open(Database::"Shpfy Order Tax Line"); diff --git a/Apps/W1/Shopify/app/src/Order handling/Codeunits/ShpfyProcessOrder.Codeunit.al b/Apps/W1/Shopify/app/src/Order handling/Codeunits/ShpfyProcessOrder.Codeunit.al index c2dadeb483..2d027d83bb 100644 --- a/Apps/W1/Shopify/app/src/Order handling/Codeunits/ShpfyProcessOrder.Codeunit.al +++ b/Apps/W1/Shopify/app/src/Order handling/Codeunits/ShpfyProcessOrder.Codeunit.al @@ -125,6 +125,8 @@ codeunit 30166 "Shpfy Process Order" end; if ShopifyOrderHeader."Payment Method Code" <> '' then SalesHeader.Validate("Payment Method Code", ShopifyOrderHeader."Payment Method Code"); + if ShopifyOrderHeader."Payment Terms Type" <> '' then + UpdatePaymentTerms(SalesHeader, ShopifyOrderHeader."Payment Terms Type", ShopifyOrderHeader."Payment Terms Name"); SalesHeader.Modify(true); @@ -148,6 +150,17 @@ codeunit 30166 "Shpfy Process Order" OrderEvents.OnAfterCreateSalesHeader(ShopifyOrderHeader, SalesHeader); end; + local procedure UpdatePaymentTerms(var SalesHeader: Record "Sales Header"; PaymentTermsType: Code[20]; PaymentTermsName: Text[50]) + var + ShpfyPaymentTerms: Record "Shpfy Payment Terms"; + begin + ShpfyPaymentTerms.SetRange(Type, PaymentTermsType); + ShpfyPaymentTerms.SetRange("Shop Code", ShopifyShop.Code); + ShpfyPaymentTerms.SetRange(Name, PaymentTermsName); + if ShpfyPaymentTerms.FindFirst() then + SalesHeader.Validate("Payment Terms Code", ShpfyPaymentTerms."Payment Terms Code"); + end; + local procedure ApplyGlobalDiscounts(OrderHeader: Record "Shpfy Order Header"; var SalesHeader: Record "Sales Header") var OrderLine: Record "Shpfy Order Line"; diff --git a/Apps/W1/Shopify/app/src/Order handling/Tables/ShpfyOrderHeader.Table.al b/Apps/W1/Shopify/app/src/Order handling/Tables/ShpfyOrderHeader.Table.al index e31860f410..3e54e0be84 100644 --- a/Apps/W1/Shopify/app/src/Order handling/Tables/ShpfyOrderHeader.Table.al +++ b/Apps/W1/Shopify/app/src/Order handling/Tables/ShpfyOrderHeader.Table.al @@ -751,6 +751,16 @@ table 30118 "Shpfy Order Header" Caption = 'Shipping Agent Service Code'; TableRelation = "Shipping Agent Services".Code where("Shipping Agent Code" = field("Shipping Agent Code")); } + field(1030; "Payment Terms Type"; Code[20]) + { + DataClassification = CustomerContent; + Caption = 'Payment Terms Type'; + } + field(1040; "Payment Terms Name"; Text[50]) + { + DataClassification = CustomerContent; + Caption = 'Payment Terms Name'; + } } keys { diff --git a/Apps/W1/Shopify/app/src/Payments/Codeunits/ShpfyPaymentTermsAPI.Codeunit.al b/Apps/W1/Shopify/app/src/Payments/Codeunits/ShpfyPaymentTermsAPI.Codeunit.al new file mode 100644 index 0000000000..143c9b5e5d --- /dev/null +++ b/Apps/W1/Shopify/app/src/Payments/Codeunits/ShpfyPaymentTermsAPI.Codeunit.al @@ -0,0 +1,85 @@ +namespace Microsoft.Integration.Shopify; + +/// +/// Codeunit Shpfy Payment Terms API (ID 30360). +/// +codeunit 30360 "Shpfy Payment Terms API" +{ + Access = Internal; + + var + CommunicationMgt: Codeunit "Shpfy Communication Mgt."; + JsonHelper: Codeunit "Shpfy Json Helper"; + ShopCode: Code[20]; + + /// + /// Synchronizes payment terms from shopify, ensuring that the payment terms are up-to-date with those defined in the shopify store. + /// + /// Shopify shop code to be used. + internal procedure PullPaymentTermsCodes() + var + GraphQLType: Enum "Shpfy GraphQL Type"; + JTemplates: JsonArray; + JTemplate: JsonToken; + JResponse: JsonToken; + begin + GraphQLType := GraphQLType::GetPaymentTerms; + JResponse := CommunicationMgt.ExecuteGraphQL(GraphQLType); + + JsonHelper.GetJsonArray(JResponse, JTemplates, 'data.paymentTermsTemplates'); + foreach JTemplate in JTemplates do + UpdatePaymentTerms(JTemplate); + end; + + /// + /// Sets a global shopify shop to be used form payment terms api functionality. + /// + /// Shopify shop code to be set. + internal procedure SetShop(NewShopCode: Code[20]) + begin + ShopCode := NewShopCode; + CommunicationMgt.SetShop(NewShopCode); + end; + + local procedure UpdatePaymentTerms(JTemplate: JsonToken) + var + ShpfyPaymentTerms: Record "Shpfy Payment Terms"; + PaymentTermRecordRef: RecordRef; + Id: BigInteger; + IsNew: Boolean; + begin + Id := CommunicationMgt.GetIdOfGId(JsonHelper.GetValueAsText(JTemplate, 'id')); + IsNew := not ShpfyPaymentTerms.Get(ShopCode, Id); + + if IsNew then begin + Clear(ShpfyPaymentTerms); + ShpfyPaymentTerms.Id := Id; + ShpfyPaymentTerms."Shop Code" := ShopCode; + end; + + PaymentTermRecordRef.GetTable(ShpfyPaymentTerms); + JsonHelper.GetValueIntoField(JTemplate, 'name', PaymentTermRecordRef, ShpfyPaymentTerms.FieldNo(Name)); + JsonHelper.GetValueIntoField(JTemplate, 'paymentTermsType', PaymentTermRecordRef, ShpfyPaymentTerms.FieldNo(Type)); + JsonHelper.GetValueIntoField(JTemplate, 'dueInDays', PaymentTermRecordRef, ShpfyPaymentTerms.FieldNo("Due In Days")); + JsonHelper.GetValueIntoField(JTemplate, 'description', PaymentTermRecordRef, ShpfyPaymentTerms.FieldNo(Description)); + PaymentTermRecordRef.SetTable(ShpfyPaymentTerms); + + if ShpfyPaymentTerms.Type = 'FIXED' then + if ShouldBeMarkedAsPrimary() then + ShpfyPaymentTerms.Validate("Is Primary", true); + + if IsNew then + ShpfyPaymentTerms.Insert(true) + else + ShpfyPaymentTerms.Modify(true); + end; + + local procedure ShouldBeMarkedAsPrimary(): Boolean + var + ShpfyPaymentTerms: Record "Shpfy Payment Terms"; + begin + ShpfyPaymentTerms.SetRange("Shop Code", ShopCode); + ShpfyPaymentTerms.SetRange("Is Primary", true); + exit(ShpfyPaymentTerms.IsEmpty()); + end; +} \ No newline at end of file diff --git a/Apps/W1/Shopify/app/src/Payments/Pages/ShpfyPaymentTermsMapping.Page.al b/Apps/W1/Shopify/app/src/Payments/Pages/ShpfyPaymentTermsMapping.Page.al new file mode 100644 index 0000000000..bcf888a04d --- /dev/null +++ b/Apps/W1/Shopify/app/src/Payments/Pages/ShpfyPaymentTermsMapping.Page.al @@ -0,0 +1,75 @@ +namespace Microsoft.Integration.Shopify; + +/// +/// Page Shpfy Payment Terms Mapping (ID 30162). +/// +page 30162 "Shpfy Payment Terms Mapping" +{ + PageType = List; + ApplicationArea = All; + UsageCategory = Lists; + SourceTable = "Shpfy Payment Terms"; + Caption = 'Shopify Payment Terms Mapping'; + + layout + { + area(Content) + { + repeater(General) + { + field(Type; Rec.Type) + { + ToolTip = 'Specifies the value of the Type field.'; + } + field(Name; Rec.Name) + { + ToolTip = 'Specifies the value of the Name field.'; + } + field(Description; Rec.Description) + { + ToolTip = 'Specifies the value of the Description field.'; + } + field("Payment Terms Code"; Rec."Payment Terms Code") + { + ToolTip = 'Specifies the value of the Payment Terms Code field.'; + } + field("Is Primary"; Rec."Is Primary") + { + ToolTip = 'Specifies the value of the Is Primary field.'; + } + } + } + } + + actions + { + area(Promoted) + { + group(Category_Process) + { + Caption = 'Process'; + ShowAs = Standard; + + actionref(PromotedRefresh; Refresh) { } + } + } + area(Processing) + { + action(Refresh) + { + ApplicationArea = All; + Caption = 'Refresh'; + Image = Refresh; + ToolTip = 'Refreshes the list of Shopify Payment Terms.'; + + trigger OnAction() + var + ShpfyPaymentTermAPI: Codeunit "Shpfy Payment Terms API"; + begin + ShpfyPaymentTermAPI.SetShop(CopyStr(Rec.GetFilter("Shop Code"), 1, 20)); + ShpfyPaymentTermAPI.PullPaymentTermsCodes(); + end; + } + } + } +} \ No newline at end of file diff --git a/Apps/W1/Shopify/app/src/Payments/Tables/ShpfyPaymentTerms.Table.al b/Apps/W1/Shopify/app/src/Payments/Tables/ShpfyPaymentTerms.Table.al new file mode 100644 index 0000000000..fffe926394 --- /dev/null +++ b/Apps/W1/Shopify/app/src/Payments/Tables/ShpfyPaymentTerms.Table.al @@ -0,0 +1,78 @@ +namespace Microsoft.Integration.Shopify; + +using Microsoft.Foundation.PaymentTerms; + +/// +/// Table Shpfy Payment Terms (ID 30158). +/// +table 30158 "Shpfy Payment Terms" +{ + Caption = 'Payment Terms'; + DataClassification = CustomerContent; + Access = Internal; + + fields + { + field(1; "Shop Code"; Code[20]) + { + Caption = 'Shop Code'; + TableRelation = "Shpfy Shop"; + Editable = false; + } + field(2; Id; BigInteger) + { + Caption = 'ID'; + Editable = false; + } + field(20; Name; Text[50]) + { + Caption = 'Name'; + Editable = false; + } + field(30; "Due In Days"; Integer) + { + Caption = 'Due In Days'; + Editable = false; + } + field(40; Description; Text[50]) + { + Caption = 'Description'; + Editable = false; + } + field(50; Type; Code[20]) + { + Caption = 'Type'; + Editable = false; + } + field(60; "Is Primary"; Boolean) + { + Caption = 'Is Primary'; + + trigger OnValidate() + var + ShpfyPaymentTerms: Record "Shpfy Payment Terms"; + PrimaryPaymentTermsExistsErr: Label 'Primary payment terms already exist for this shop.'; + begin + ShpfyPaymentTerms.SetRange("Shop Code", Rec."Shop Code"); + ShpfyPaymentTerms.SetRange("Is Primary", true); + ShpfyPaymentTerms.SetFilter(Id, '<>%1', Rec.Id); + + if not ShpfyPaymentTerms.IsEmpty() then + Error(PrimaryPaymentTermsExistsErr); + end; + } + field(70; "Payment Terms Code"; Code[10]) + { + TableRelation = "Payment Terms"; + Caption = 'Payment Terms Code'; + } + } + + keys + { + key(PK; "Shop Code", Id) + { + Clustered = true; + } + } +} \ No newline at end of file diff --git a/Apps/W1/Shopify/app/src/PermissionSets/ShpfyEdit.PermissionSet.al b/Apps/W1/Shopify/app/src/PermissionSets/ShpfyEdit.PermissionSet.al index 19913bb1aa..d8296da1f9 100644 --- a/Apps/W1/Shopify/app/src/PermissionSets/ShpfyEdit.PermissionSet.al +++ b/Apps/W1/Shopify/app/src/PermissionSets/ShpfyEdit.PermissionSet.al @@ -32,6 +32,7 @@ permissionset 30102 "Shpfy - Edit" tabledata "Shpfy Gift Card" = IMD, tabledata "Shpfy Initial Import Line" = imd, tabledata "Shpfy Inventory Item" = IMD, + tabledata "Shpfy Invoice Header" = IMD, tabledata "Shpfy Language" = IMD, tabledata "Shpfy Log Entry" = IMD, tabledata "Shpfy Metafield" = IMD, @@ -52,6 +53,7 @@ permissionset 30102 "Shpfy - Edit" tabledata "Shpfy Order Tax Line" = IMD, tabledata "Shpfy Order Transaction" = IMD, tabledata "Shpfy Payment Method Mapping" = IMD, + tabledata "Shpfy Payment Terms" = IMD, tabledata "Shpfy Payment Transaction" = IMD, tabledata "Shpfy Payout" = IMD, tabledata "Shpfy Product" = IMD, diff --git a/Apps/W1/Shopify/app/src/PermissionSets/ShpfyRead.PermissionSet.al b/Apps/W1/Shopify/app/src/PermissionSets/ShpfyRead.PermissionSet.al index 80680e13c7..2fab53a2b8 100644 --- a/Apps/W1/Shopify/app/src/PermissionSets/ShpfyRead.PermissionSet.al +++ b/Apps/W1/Shopify/app/src/PermissionSets/ShpfyRead.PermissionSet.al @@ -32,6 +32,7 @@ permissionset 30100 "Shpfy - Read" tabledata "Shpfy Gift Card" = R, tabledata "Shpfy Initial Import Line" = r, tabledata "Shpfy Inventory Item" = R, + tabledata "Shpfy Invoice Header" = R, tabledata "Shpfy Language" = R, tabledata "Shpfy Log Entry" = R, tabledata "Shpfy Metafield" = R, @@ -48,6 +49,7 @@ permissionset 30100 "Shpfy - Read" tabledata "Shpfy Order Tax Line" = R, tabledata "Shpfy Order Transaction" = R, tabledata "Shpfy Payment Method Mapping" = R, + tabledata "Shpfy Payment Terms" = R, tabledata "Shpfy Payment Transaction" = R, tabledata "Shpfy Payout" = R, tabledata "Shpfy Product" = R,