From 7c3fe3e6bc33355e429a4d5d69042936acd20233 Mon Sep 17 00:00:00 2001 From: Gediminas Gaubys <56364962+GediminasGaubys@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:34:16 +0300 Subject: [PATCH] [Shopify] Connector - Export Posted Sales Invoices to Shopify as Orders (#26748) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request does not have a related issue as it's part of the delivery for development agreed directly with [@AndreiPanko](https://github.com/AndreiPanko) Here's a quick summary: ### Created a report to export Posted Sales Invoices to Shopify as Orders. Shopify Shop has a **new action "Payment Terms Mapping"** that takes the user to the **new page "Shopify Payment Terms Mapping"**. Shopify Payment Terms cannot be manually inserted or deleted, they can only be refreshed with an action that pulls them from Shopify. By default **FIXED** Payment Term is selected as primary during the synchronization of the Payment Terms. Before Posted Sales Invoices can be synced, a payment term code from Shopify needs to be mapped to the Business Central one or a primary Shopify Payment Term needs to be selected with a Boolean "Is Primary" - this payment term will be used if the Posted Sales Invoice payment terms code doesn't have a mapping to Shopify. This payment term code will also be filled during the sync of Shopify orders. A new Scope is required. To update payment terms on a draft order – write_payment_terms (read_payment_terms scope was removed because it causes strange behavior - every time a Shopify shop card is opened it states that the scope has changed and requires authorization. write_payment_terms overwrites read_payment_terms). To create a draft order - write_draft_orders. ### To export Posted Sales Invoices, here are some conditions that they need to meet: - The **new Boolean field "Posted Invoice Sync"** needs to be enabled. - The "Shopify Order Id" field on the Posted Sales Invoice needs to be 0. - Posted Sales Invoice "Bill-To Customer" has been mapped to the Shopify Customer or Shopify Companies table. - Posted Sales Invoice "Bill-To Customer" is not a customer used as a Default Customer in the Shopify Shop Card or Shopify Customer Template. - There are no lines of type non-comment (or type is populated but No. is empty). - There are no fraction quantities. - There must be a mapped Payment Terms Code or a selected Primary Payment Terms Code on the new "Shopify Payment Terms Mapping" page. - If there is at least one line of type "Item" where the selected item/variant doesn’t have a link to Shopify Product/Shopify variant and the **new Boolean field "Items must be mapped to Products"** on the Shopify Shop card is selected - the Posted Sales Invoice will not be exported. - Posted Sales Invoice currency code matches Shopify shop currency code (explanation at the bottom). If one of the previous conditions is not met, the Posted Sales Invoice "Shopify Order Id" field will be marked with -2. If the GraphQL request fails, then the field will be marked with -1. If everything goes correctly, the field will be filled with the created Shopify Order ID. Users can modify the "Shopify Order Id" field in the posted document (allowed changes from -1 or -2 to 0. The functionality works the same as in the Update Posted Shipment document.) This export can be executed with a new action that is added to Shopify Shop - "Sync Posted Sales Invoices" or just by running the report "Sync Invoices to Shopify". The draft order in Shopify is created with the values from the Posted Sales Invoice order and lines. If the Remaining Amount of the Posted Sales Invoice is 0, then the order is marked as paid, otherwise, the status will be "Payment pending" and a mapped Payment Terms code will be sent out. After that, the created draft order will be completed and the Shopify order created. All orders are automatically fulfilled. To achieve “paid” status on the Shopify order – use the Manual payment function in the Shopify Order card. Once the report is executed, the Inventory will be synced. Order sync will import created orders, then check if the order is present in the **new Shopify Invoice Header table** (this table is filled with the Shopify order ID of the exported Posted Sales Invoice) and if so: - Shopify Order is marked as processed. - A link is added to the Posted Sales Invoice (related documents). ### A few details about the mapping of the values and some additional functionality: - All non-comment type lines are exported as custom products (including Items if the new Boolean field "Items must be mapped to Products"** on the Shopify Shop card is not selected). - Taxes from the lines are sent out as a custom product with the title consisting of "VAT Calculation Type" + VAT %. - Invoice discounts from the lines and the header are added up and sent out as a discount to the whole order with the title "Invoice Discount Amount" - Posted Sales Invoice comments are exported as notes. - Once the draft order is completed, an order confirmation email will be sent out to the customer. This functionality is from Shopify itself. - If the product/variant exists in Shopify, the price, title and other information will be taken from Shopify and not the one in the Posted Sales Invoice line. This is how the draftOrderCreate mutation works. ### Out of scope functionalities: - Open orders and open invoices. - Fulfillment/Tracking Details. - Refund / partial refund status. - Weight export. **Known issues with different currencies (that's why Posted Sales Invoices with different currencies than Shopify Shop cannot be exported at the moment):** - Currently sending out different currencies than the Shopify Shop currency cannot be done because if the currency of the custom product differs from the Shopify shop's currency, the amount sent will still be accepted in the local Shopify shop's currency before being converted to the order's currency and the amount will be incorrect. For example, if the VAT amount is 100 GBP, but the Shopify shop's currency is EUR, the custom product will be exported with an amount of 100 GBP. However, this amount will be treated as 100 EUR and then converted to GBP according to the currency exchange rate in Shopify. Fixes #26819 Fixes [AB#446399](https://dynamicssmb2.visualstudio.com/1fcb79e7-ab07-432a-a3c6-6cf5a88ba4a5/_workitems/edit/446399) --------- Co-authored-by: Jesper Schulz-Wedde Co-authored-by: Tine Staric --- Apps/W1/Shopify/app/app.json | 2 +- .../app/src/Base/Pages/ShpfyShopCard.Page.al | 43 ++ .../app/src/Base/Tables/ShpfyShop.Table.al | 8 + .../ShpfyGQLDraftOrderComplete.Codeunit.al | 27 ++ .../ShpfyGQLFulfillOrder.Codeunit.al | 27 ++ .../ShpfyGQLGetFulfillments.Codeunit.al | 26 + .../ShpfyGQLPaymentTerms.Codeunit.al | 28 ++ .../Codeunits/ShpfyGQLShopLocales.Codeunit.al | 2 +- .../ShpfyGQLTranslationsRegister.Codeunit.al | 2 +- .../GraphQL/Enums/ShpfyGraphQLType.Enum.al | 20 + .../ShpfyAuthenticationMgt.Codeunit.al | 2 +- .../Codeunits/ShpfyDraftOrdersAPI.Codeunit.al | 386 +++++++++++++++ .../Codeunits/ShpfyFulfillmentAPI.Codeunit.al | 69 +++ .../ShpfyPostedInvoiceExport.Codeunit.al | 459 ++++++++++++++++++ .../ShpfyUpdateSalesInvoice.Codeunit.al | 22 + .../ShpfySalesInvoiceUpdate.PageExt.al | 44 ++ .../ShpfySyncInvoicesToShpfy.Report.al | 105 ++++ .../Tables/ShpfyInvoiceHeader.Table.al | 27 ++ .../Codeunits/ShpfyImportOrder.Codeunit.al | 24 +- .../Codeunits/ShpfyProcessOrder.Codeunit.al | 13 + .../Tables/ShpfyOrderHeader.Table.al | 10 + .../ShpfyPaymentTermsAPI.Codeunit.al | 85 ++++ .../Pages/ShpfyPaymentTermsMapping.Page.al | 75 +++ .../Tables/ShpfyPaymentTerms.Table.al | 78 +++ .../PermissionSets/ShpfyEdit.PermissionSet.al | 2 + .../PermissionSets/ShpfyRead.PermissionSet.al | 2 + 26 files changed, 1581 insertions(+), 7 deletions(-) create mode 100644 Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLDraftOrderComplete.Codeunit.al create mode 100644 Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLFulfillOrder.Codeunit.al create mode 100644 Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLGetFulfillments.Codeunit.al create mode 100644 Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLPaymentTerms.Codeunit.al create mode 100644 Apps/W1/Shopify/app/src/Invoicing/Codeunits/ShpfyDraftOrdersAPI.Codeunit.al create mode 100644 Apps/W1/Shopify/app/src/Invoicing/Codeunits/ShpfyFulfillmentAPI.Codeunit.al create mode 100644 Apps/W1/Shopify/app/src/Invoicing/Codeunits/ShpfyPostedInvoiceExport.Codeunit.al create mode 100644 Apps/W1/Shopify/app/src/Invoicing/Codeunits/ShpfyUpdateSalesInvoice.Codeunit.al create mode 100644 Apps/W1/Shopify/app/src/Invoicing/PageExt/ShpfySalesInvoiceUpdate.PageExt.al create mode 100644 Apps/W1/Shopify/app/src/Invoicing/Reports/ShpfySyncInvoicesToShpfy.Report.al create mode 100644 Apps/W1/Shopify/app/src/Invoicing/Tables/ShpfyInvoiceHeader.Table.al create mode 100644 Apps/W1/Shopify/app/src/Payments/Codeunits/ShpfyPaymentTermsAPI.Codeunit.al create mode 100644 Apps/W1/Shopify/app/src/Payments/Pages/ShpfyPaymentTermsMapping.Page.al create mode 100644 Apps/W1/Shopify/app/src/Payments/Tables/ShpfyPaymentTerms.Table.al 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,