From e6077c13a82449e15a7c8114ce30abf1753726e4 Mon Sep 17 00:00:00 2001 From: Brad Hover Date: Wed, 3 Jul 2024 17:27:14 -0700 Subject: [PATCH] update inventory export task (#373) --- docs/README.md | 1 + .../README.md | 6 +++--- .../script.liquid | 12 +++++++----- ...led-inventory-exports-in-shopifys-csv-format.json | 7 ++++--- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/docs/README.md b/docs/README.md index 200a9105..7c9d7de0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -994,6 +994,7 @@ This directory is built automatically. Each task's documentation is generated fr * [Auto-sort collections by inventory levels](./auto-sort-collections-by-inventory-levels) * [Auto-tag out-of-stock products](./auto-tag-out-of-stock-products) * [Auto-tag products with incoming inventory](./auto-tag-products-with-incoming-inventory) +* [Backup inventory to SFTP in Shopify CSV format](./backup-scheduled-inventory-exports-in-shopifys-csv-format) * [Create a product inventory CSV feed](./create-a-product-inventory-feed) * [Email a summary of all products and quantities ordered](./email-a-summary-of-all-products-and-quantities-ordered) * [Hide out-of-stock products](./hide-out-of-stock-products) diff --git a/docs/backup-scheduled-inventory-exports-in-shopifys-csv-format/README.md b/docs/backup-scheduled-inventory-exports-in-shopifys-csv-format/README.md index 9ec15e93..319ad343 100644 --- a/docs/backup-scheduled-inventory-exports-in-shopifys-csv-format/README.md +++ b/docs/backup-scheduled-inventory-exports-in-shopifys-csv-format/README.md @@ -1,8 +1,8 @@ # Backup inventory to SFTP in Shopify CSV format -Tags: Backups, CSV, Export, FTP +Tags: Backups, CSV, Export, FTP, Inventory -On a configurable schedule, this task generates a Shopify-friendly CSV of your inventory, and uploads it to the SFTP destination of your choice, and/or sends it via email. This is a convenient way to keep regular backups of your entire product inventory: simply import a CSV to restore your inventory to that point in time. ([Learn more about CSV imports and exports of Shopify inventory.](https://help.shopify.com/en/manual/locations/changing-quantities/exporting-or-importing-inventory)) +On a configurable schedule, this task generates a Shopify-friendly CSV of your inventory, and uploads it to the SFTP destination of your choice, and/or sends it via email. This is a convenient way to keep regular backups of your entire product inventory: simply import a CSV to restore your inventory to that point in time. ([Learn more about CSV imports and exports of Shopify inventory.](https://help.shopify.com/en/manual/products/inventory/getting-started-with-inventory/inventory-csv)) * View in the task library: [tasks.mechanic.dev/backup-scheduled-inventory-exports-in-shopifys-csv-format](https://tasks.mechanic.dev/backup-scheduled-inventory-exports-in-shopifys-csv-format) * Task JSON, for direct import: [task.json](../../tasks/backup-scheduled-inventory-exports-in-shopifys-csv-format.json) @@ -40,7 +40,7 @@ mechanic/shopify/bulk_operation ## Documentation -On a configurable schedule, this task generates a Shopify-friendly CSV of your inventory, and uploads it to the SFTP destination of your choice, and/or sends it via email. This is a convenient way to keep regular backups of your entire product inventory: simply import a CSV to restore your inventory to that point in time. ([Learn more about CSV imports and exports of Shopify inventory.](https://help.shopify.com/en/manual/locations/changing-quantities/exporting-or-importing-inventory)) +On a configurable schedule, this task generates a Shopify-friendly CSV of your inventory, and uploads it to the SFTP destination of your choice, and/or sends it via email. This is a convenient way to keep regular backups of your entire product inventory: simply import a CSV to restore your inventory to that point in time. ([Learn more about CSV imports and exports of Shopify inventory.](https://help.shopify.com/en/manual/products/inventory/getting-started-with-inventory/inventory-csv)) To only export certain products, set the "Only export products matching this query" option to a search query that works with Shopify's inventory admin area. For example, to only export products tagged "backmeup", use the search query "tag:backmeup". diff --git a/docs/backup-scheduled-inventory-exports-in-shopifys-csv-format/script.liquid b/docs/backup-scheduled-inventory-exports-in-shopifys-csv-format/script.liquid index 8164e93d..8dddaebb 100644 --- a/docs/backup-scheduled-inventory-exports-in-shopifys-csv-format/script.liquid +++ b/docs/backup-scheduled-inventory-exports-in-shopifys-csv-format/script.liquid @@ -91,7 +91,9 @@ location { name } - available + quantities(names: "available") { + quantity + } } } } @@ -123,8 +125,8 @@ {% if event.preview %} {% capture jsonl_string %} {"id":"gid:\/\/shopify\/ProductVariant\/1234567890","__typename":"ProductVariant","product":{"handle":"log","title":"Log","options":[{"name":"Size","position":1}]},"selectedOptions":[{"name":"Size","value":"Petite"}],"sku":"LOG-3","inventoryItem":{"tracked":true}} - {"id":"gid:\/\/shopify\/InventoryLevel\/1357924680?inventory_item_id=1470258369","__typename":"InventoryLevel","location":{"name":"123 Main Street"},"available":9,"__parentId":"gid:\/\/shopify\/ProductVariant\/1234567890"} - {"id":"gid:\/\/shopify\/InventoryLevel\/2468013579?inventory_item_id=1470258369","__typename":"InventoryLevel","location":{"name":"987 Alley Way"},"available":8,"__parentId":"gid:\/\/shopify\/ProductVariant\/1234567890"} + {"id":"gid:\/\/shopify\/InventoryLevel\/1357924680?inventory_item_id=1470258369","__typename":"InventoryLevel","location":{"name":"123 Main Street"},"quantities":[{"quantity":9}],"__parentId":"gid:\/\/shopify\/ProductVariant\/1234567890"} + {"id":"gid:\/\/shopify\/InventoryLevel\/2468013579?inventory_item_id=1470258369","__typename":"InventoryLevel","location":{"name":"987 Alley Way"},"quantities":[{"quantity":8}],"__parentId":"gid:\/\/shopify\/ProductVariant\/1234567890"} {% endcapture %} {% assign bulkOperation = hash %} @@ -233,12 +235,12 @@ {% endfor %} {% comment %} - -- add inventory levels by location + -- add "available" inventory levels by location {% endcomment %} {% for inventory_level in variant.inventory_levels %} {% assign location_name = inventory_level.location.name %} - {% assign variant_row[location_name] = inventory_level.available %} + {% assign variant_row[location_name] = inventory_level.quantities.first.quantity %} {% endfor %} {% comment %} diff --git a/tasks/backup-scheduled-inventory-exports-in-shopifys-csv-format.json b/tasks/backup-scheduled-inventory-exports-in-shopifys-csv-format.json index bd623565..d81cfafc 100644 --- a/tasks/backup-scheduled-inventory-exports-in-shopifys-csv-format.json +++ b/tasks/backup-scheduled-inventory-exports-in-shopifys-csv-format.json @@ -1,5 +1,5 @@ { - "docs": "On a configurable schedule, this task generates a Shopify-friendly CSV of your inventory, and uploads it to the SFTP destination of your choice, and/or sends it via email. This is a convenient way to keep regular backups of your entire product inventory: simply import a CSV to restore your inventory to that point in time. ([Learn more about CSV imports and exports of Shopify inventory.](https://help.shopify.com/en/manual/locations/changing-quantities/exporting-or-importing-inventory))\n\nTo only export certain products, set the \"Only export products matching this query\" option to a search query that works with Shopify's inventory admin area. For example, to only export products tagged \"backmeup\", use the search query \"tag:backmeup\".", + "docs": "On a configurable schedule, this task generates a Shopify-friendly CSV of your inventory, and uploads it to the SFTP destination of your choice, and/or sends it via email. This is a convenient way to keep regular backups of your entire product inventory: simply import a CSV to restore your inventory to that point in time. ([Learn more about CSV imports and exports of Shopify inventory.](https://help.shopify.com/en/manual/products/inventory/getting-started-with-inventory/inventory-csv))\n\nTo only export certain products, set the \"Only export products matching this query\" option to a search query that works with Shopify's inventory admin area. For example, to only export products tagged \"backmeup\", use the search query \"tag:backmeup\".", "halt_action_run_sequence_on_error": false, "name": "Backup inventory to SFTP in Shopify CSV format", "online_store_javascript": null, @@ -14,7 +14,7 @@ }, "order_status_javascript": null, "perform_action_runs_in_sequence": false, - "script": "{% comment %}\n Preferred option order:\n\n {{ options.only_export_products_matching_this_query }}\n {{ options.run_every_x_hours__number }}\n {{ options.sftp_host }}\n {{ options.sftp_port__number }}\n {{ options.sftp_user }}\n {{ options.sftp_password }}\n {{ options.sftp_upload_directory }}\n {{ options.send_email_export_to_this_address }}\n{% endcomment %}\n\n{% comment %}\n-- validate options\n{% endcomment %}\n{% assign export_using_sftp = false %}\n{% assign export_using_email = false %}\n\n{% if options.sftp_host != blank or options.sftp_port__number != blank or options.sftp_user != blank or options.sftp_password != blank %}\n {% if options.sftp_host == blank or options.sftp_port__number == blank or options.sftp_user == blank or options.sftp_password == blank %}\n {% error \"When exporting via SFTP, the host, port, user, and password fields are required.\" %}\n {% else %}\n {% assign export_using_sftp = true %}\n {% endif %}\n{% endif %}\n\n{% if export_using_sftp == false and options.send_email_export_to_this_address == blank %}\n {% error \"This task must be configured to export via SFTP (by filling in all of the SFTP host, port, user, and password fields), or via email, or both.\" %}\n{% endif %}\n\n{% if options.run_every_x_hours__number != blank %}\n {% assign valid_hours = array %}\n {% assign valid_hours[valid_hours.size] = 1 %}\n {% assign valid_hours[valid_hours.size] = 2 %}\n {% assign valid_hours[valid_hours.size] = 3 %}\n {% assign valid_hours[valid_hours.size] = 4 %}\n {% assign valid_hours[valid_hours.size] = 6 %}\n {% assign valid_hours[valid_hours.size] = 12 %}\n {% assign valid_hours[valid_hours.size] = 24 %}\n\n {% unless valid_hours contains options.run_every_x_hours__number %}\n {% error \"If set, 'Run interval in hours' must be 1, 2, 3, 4, 6, 12, or 24.\" %}\n {% endunless %}\n{% endif %}\n\n{% assign ok_to_run = false %}\n\n{% if event.topic == \"mechanic/user/trigger\" or event.topic == \"mechanic/scheduler/daily\" %}\n {% assign ok_to_run = true %}\n\n{% elsif event.topic == \"mechanic/scheduler/hourly\" and options.run_every_x_hours__number != blank %}\n {% assign hour_mod = \"now\" | date: \"%H\" | modulo: options.run_every_x_hours__number %}\n\n {% if event.preview or hour_mod == 0 %}\n {% assign ok_to_run = true %}\n\n {% else %}\n {% log message: \"The current hour does not fall on the configured interval; skipping\", hour_interval: options.run_every_x_hours__number, current_hour: hour_mod %}\n {% endif %}\n{% endif %}\n\n{% if ok_to_run %}\n {% capture bulk_operation_query %}\n query {\n productVariants(reverse: true, query: {{ options.only_export_products_matching_this_query | json }}) {\n edges {\n node {\n id\n __typename\n product {\n handle\n title\n options {\n name\n position\n }\n }\n selectedOptions {\n name\n value\n }\n sku\n inventoryItem {\n tracked\n inventoryLevels {\n edges {\n node {\n id\n __typename\n location {\n name\n }\n available\n }\n }\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% action \"shopify\" %}\n mutation {\n bulkOperationRunQuery(\n query: {{ bulk_operation_query | json }}\n ) {\n bulkOperation {\n id\n status\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n\n{% elsif event.topic == \"mechanic/shopify/bulk_operation\" %}\n {% if event.preview %}\n {% capture jsonl_string %}\n {\"id\":\"gid:\\/\\/shopify\\/ProductVariant\\/1234567890\",\"__typename\":\"ProductVariant\",\"product\":{\"handle\":\"log\",\"title\":\"Log\",\"options\":[{\"name\":\"Size\",\"position\":1}]},\"selectedOptions\":[{\"name\":\"Size\",\"value\":\"Petite\"}],\"sku\":\"LOG-3\",\"inventoryItem\":{\"tracked\":true}}\n {\"id\":\"gid:\\/\\/shopify\\/InventoryLevel\\/1357924680?inventory_item_id=1470258369\",\"__typename\":\"InventoryLevel\",\"location\":{\"name\":\"123 Main Street\"},\"available\":9,\"__parentId\":\"gid:\\/\\/shopify\\/ProductVariant\\/1234567890\"}\n {\"id\":\"gid:\\/\\/shopify\\/InventoryLevel\\/2468013579?inventory_item_id=1470258369\",\"__typename\":\"InventoryLevel\",\"location\":{\"name\":\"987 Alley Way\"},\"available\":8,\"__parentId\":\"gid:\\/\\/shopify\\/ProductVariant\\/1234567890\"}\n {% endcapture %}\n\n {% assign bulkOperation = hash %}\n {% assign bulkOperation[\"objects\"] = jsonl_string | parse_jsonl %}\n {% endif %}\n\n {% comment %}\n -- csv required fields, in this order\n {% endcomment %}\n\n {% assign columns = \"Handle,Title,Option1 Name,Option1 Value,Option2 Name,Option2 Value,Option3 Name,Option3 Value,SKU\" | split: \",\" %}\n\n {% comment %}\n -- add inventory locations\n {% endcomment %}\n\n {% for location in shop.locations %}\n {% if location.active %}\n {% assign columns[columns.size] = location.name %}\n {% endif %}\n {% endfor %}\n\n {% assign location_count = shop.locations.size %}\n\n {% if event.preview %}\n {% assign columns[columns.size] = \"123 Main Street\" %}\n {% assign columns[columns.size] = \"987 Alley Way\" %}\n {% endif %}\n\n {% comment %}\n -- setup 2d array required by csv filter, and add the columns as a header row\n {% endcomment %}\n\n {% assign rows = array %}\n {% assign rows[0] = columns %}\n\n {% comment %}\n -- loop through the lines (JSONL) returned by the bulk operation\n {% endcomment %}\n\n {% assign variants_by_id = hash %}\n\n {% for object in bulkOperation.objects %}\n {% case object.__typename %}\n {% when \"ProductVariant\" %}\n {% comment %}-- clone the object to allow modification --{% endcomment %}\n {% assign variant = object | json | parse_json %}\n {% assign variant[\"inventory_levels\"] = array %}\n {% assign variants_by_id[variant.id] = variant %}\n\n {% when \"InventoryLevel\" %}\n {% assign inventory_level = object %}\n {% assign variant_id = inventory_level.__parentId %}\n {% assign variant = variants_by_id[variant_id] %}\n {% assign variant[\"inventory_levels\"][variant.inventory_levels.size] = inventory_level %}\n\n {% else %}\n {% log message: \"Unexpected object type in JSONL\", object_type: object.__typename, object: object %}\n {% endcase %}\n {% endfor %}\n\n {% comment %}\n -- loop through variants_by_id to build csv rows, one row per variant\n {% endcomment %}\n\n {% for pair in variants_by_id %}\n {% assign variant = pair[1] %}\n {% assign variant_row = hash %}\n\n {% comment %}\n -- exclude untracked variants (no graphql filter for this)\n {% endcomment %}\n\n {% unless variant.inventoryItem.tracked %}\n {% continue %}\n {% endunless %}\n\n {% for column in columns %}\n {% unless forloop.rindex0 < location_count %}\n {% assign variant_row[column] = nil %}\n {% else %}\n {% assign variant_row[column] = \"not stocked\" %}\n {% endunless %}\n {% endfor %}\n\n {% assign variant_row[\"Handle\"] = variant.product.handle %}\n {% assign variant_row[\"SKU\"] = variant.sku %}\n {% assign variant_row[\"Title\"] = variant.product.title %}\n\n {% comment %}\n -- map variant selected options to the correct product option columns\n {% endcomment %}\n\n {% assign product_options = hash %}\n\n {% for option in variant.product.options %}\n {% assign product_options[option.name] = option.position %}\n {% endfor %}\n\n {% for option in variant.selectedOptions %}\n {% assign position = product_options[option.name] %}\n {% assign option_name_key = \"Option\" | append: position | append: \" Name\" %}\n {% assign option_value_key = \"Option\" | append: position | append: \" Value\" %}\n {% assign variant_row[option_name_key] = option.name %}\n {% assign variant_row[option_value_key] = option.value %}\n {% endfor %}\n\n {% comment %}\n -- add inventory levels by location\n {% endcomment %}\n\n {% for inventory_level in variant.inventory_levels %}\n {% assign location_name = inventory_level.location.name %}\n {% assign variant_row[location_name] = inventory_level.available %}\n {% endfor %}\n\n {% comment %}\n -- flatten the variant row hash into an array of values\n {% endcomment %}\n\n {% assign row = array %}\n {% for pair in variant_row %}\n {% assign row[forloop.index0] = pair[1] %}\n {% endfor %}\n\n {% comment %}\n -- add the row to 2d rows array\n {% endcomment %}\n\n {% assign rows[rows.size] = row %}\n {% endfor %}{% comment %}-- end variants loop --{% endcomment %}\n\n {% comment %}\n -- convert 2d array into csv format\n {% endcomment %}\n\n {% assign csv = rows | csv %}\n\n {% if event.preview %}\n {% action \"echo\" csv %}\n {% endif %}\n\n {% capture file_name %}inventory__{{ \"now\" | date: \"%Y-%m-%d_T%H-%M-%S_%Z\", tz: \"UTC\" }}.csv{% endcapture %}\n\n {% if export_using_sftp %}\n {% comment %}\n -- directory paths may or may not have a leading slash (if they do, they're absolute;\n -- if they don't, they're relative), but we always need a trailing slash\n {% endcomment %}\n\n {% if options.sftp_upload_directory != blank %}\n {% assign directory = options.sftp_upload_directory %}\n\n {% if directory.last != \"/\" %}\n {% assign directory = directory | append: \"/\" %}\n {% endif %}\n\n {% assign upload_path = directory | append: file_name %}\n {% endif %}\n\n {% action \"ftp\" %}\n {\n \"protocol\": \"sftp\",\n \"host\": {{ options.sftp_host | json }},\n \"port\": {{ options.sftp_port__number | json }},\n \"user\": {{ options.sftp_user | json }},\n \"password\": {{ options.sftp_password | json }},\n \"uploads\": {\n {{ upload_path | default: file_name | json }}: {{ csv | json }}\n }\n }\n {% endaction %}\n {% endif %}\n\n {% if options.send_email_export_to_this_address != blank %}\n {% action \"email\" %}\n {\n \"to\": {{ options.send_email_export_to_this_address | json }},\n \"subject\": {{ \"Inventory export for \" | append: shop.name | json }},\n \"body\": \"Please see attached. :)\",\n \"reply_to\": {{ shop.customer_email | json }},\n \"from_display_name\": {{ shop.name | json }},\n \"attachments\": {\n {{ file_name | json }}: {{ csv | json }}\n }\n }\n {% endaction %}\n {% endif %}\n{% endif %}", + "script": "{% comment %}\n Preferred option order:\n\n {{ options.only_export_products_matching_this_query }}\n {{ options.run_every_x_hours__number }}\n {{ options.sftp_host }}\n {{ options.sftp_port__number }}\n {{ options.sftp_user }}\n {{ options.sftp_password }}\n {{ options.sftp_upload_directory }}\n {{ options.send_email_export_to_this_address }}\n{% endcomment %}\n\n{% comment %}\n-- validate options\n{% endcomment %}\n{% assign export_using_sftp = false %}\n{% assign export_using_email = false %}\n\n{% if options.sftp_host != blank or options.sftp_port__number != blank or options.sftp_user != blank or options.sftp_password != blank %}\n {% if options.sftp_host == blank or options.sftp_port__number == blank or options.sftp_user == blank or options.sftp_password == blank %}\n {% error \"When exporting via SFTP, the host, port, user, and password fields are required.\" %}\n {% else %}\n {% assign export_using_sftp = true %}\n {% endif %}\n{% endif %}\n\n{% if export_using_sftp == false and options.send_email_export_to_this_address == blank %}\n {% error \"This task must be configured to export via SFTP (by filling in all of the SFTP host, port, user, and password fields), or via email, or both.\" %}\n{% endif %}\n\n{% if options.run_every_x_hours__number != blank %}\n {% assign valid_hours = array %}\n {% assign valid_hours[valid_hours.size] = 1 %}\n {% assign valid_hours[valid_hours.size] = 2 %}\n {% assign valid_hours[valid_hours.size] = 3 %}\n {% assign valid_hours[valid_hours.size] = 4 %}\n {% assign valid_hours[valid_hours.size] = 6 %}\n {% assign valid_hours[valid_hours.size] = 12 %}\n {% assign valid_hours[valid_hours.size] = 24 %}\n\n {% unless valid_hours contains options.run_every_x_hours__number %}\n {% error \"If set, 'Run interval in hours' must be 1, 2, 3, 4, 6, 12, or 24.\" %}\n {% endunless %}\n{% endif %}\n\n{% assign ok_to_run = false %}\n\n{% if event.topic == \"mechanic/user/trigger\" or event.topic == \"mechanic/scheduler/daily\" %}\n {% assign ok_to_run = true %}\n\n{% elsif event.topic == \"mechanic/scheduler/hourly\" and options.run_every_x_hours__number != blank %}\n {% assign hour_mod = \"now\" | date: \"%H\" | modulo: options.run_every_x_hours__number %}\n\n {% if event.preview or hour_mod == 0 %}\n {% assign ok_to_run = true %}\n\n {% else %}\n {% log message: \"The current hour does not fall on the configured interval; skipping\", hour_interval: options.run_every_x_hours__number, current_hour: hour_mod %}\n {% endif %}\n{% endif %}\n\n{% if ok_to_run %}\n {% capture bulk_operation_query %}\n query {\n productVariants(reverse: true, query: {{ options.only_export_products_matching_this_query | json }}) {\n edges {\n node {\n id\n __typename\n product {\n handle\n title\n options {\n name\n position\n }\n }\n selectedOptions {\n name\n value\n }\n sku\n inventoryItem {\n tracked\n inventoryLevels {\n edges {\n node {\n id\n __typename\n location {\n name\n }\n quantities(names: \"available\") {\n quantity\n }\n }\n }\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% action \"shopify\" %}\n mutation {\n bulkOperationRunQuery(\n query: {{ bulk_operation_query | json }}\n ) {\n bulkOperation {\n id\n status\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n\n{% elsif event.topic == \"mechanic/shopify/bulk_operation\" %}\n {% if event.preview %}\n {% capture jsonl_string %}\n {\"id\":\"gid:\\/\\/shopify\\/ProductVariant\\/1234567890\",\"__typename\":\"ProductVariant\",\"product\":{\"handle\":\"log\",\"title\":\"Log\",\"options\":[{\"name\":\"Size\",\"position\":1}]},\"selectedOptions\":[{\"name\":\"Size\",\"value\":\"Petite\"}],\"sku\":\"LOG-3\",\"inventoryItem\":{\"tracked\":true}}\n {\"id\":\"gid:\\/\\/shopify\\/InventoryLevel\\/1357924680?inventory_item_id=1470258369\",\"__typename\":\"InventoryLevel\",\"location\":{\"name\":\"123 Main Street\"},\"quantities\":[{\"quantity\":9}],\"__parentId\":\"gid:\\/\\/shopify\\/ProductVariant\\/1234567890\"}\n {\"id\":\"gid:\\/\\/shopify\\/InventoryLevel\\/2468013579?inventory_item_id=1470258369\",\"__typename\":\"InventoryLevel\",\"location\":{\"name\":\"987 Alley Way\"},\"quantities\":[{\"quantity\":8}],\"__parentId\":\"gid:\\/\\/shopify\\/ProductVariant\\/1234567890\"}\n {% endcapture %}\n\n {% assign bulkOperation = hash %}\n {% assign bulkOperation[\"objects\"] = jsonl_string | parse_jsonl %}\n {% endif %}\n\n {% comment %}\n -- csv required fields, in this order\n {% endcomment %}\n\n {% assign columns = \"Handle,Title,Option1 Name,Option1 Value,Option2 Name,Option2 Value,Option3 Name,Option3 Value,SKU\" | split: \",\" %}\n\n {% comment %}\n -- add inventory locations\n {% endcomment %}\n\n {% for location in shop.locations %}\n {% if location.active %}\n {% assign columns[columns.size] = location.name %}\n {% endif %}\n {% endfor %}\n\n {% assign location_count = shop.locations.size %}\n\n {% if event.preview %}\n {% assign columns[columns.size] = \"123 Main Street\" %}\n {% assign columns[columns.size] = \"987 Alley Way\" %}\n {% endif %}\n\n {% comment %}\n -- setup 2d array required by csv filter, and add the columns as a header row\n {% endcomment %}\n\n {% assign rows = array %}\n {% assign rows[0] = columns %}\n\n {% comment %}\n -- loop through the lines (JSONL) returned by the bulk operation\n {% endcomment %}\n\n {% assign variants_by_id = hash %}\n\n {% for object in bulkOperation.objects %}\n {% case object.__typename %}\n {% when \"ProductVariant\" %}\n {% comment %}-- clone the object to allow modification --{% endcomment %}\n {% assign variant = object | json | parse_json %}\n {% assign variant[\"inventory_levels\"] = array %}\n {% assign variants_by_id[variant.id] = variant %}\n\n {% when \"InventoryLevel\" %}\n {% assign inventory_level = object %}\n {% assign variant_id = inventory_level.__parentId %}\n {% assign variant = variants_by_id[variant_id] %}\n {% assign variant[\"inventory_levels\"][variant.inventory_levels.size] = inventory_level %}\n\n {% else %}\n {% log message: \"Unexpected object type in JSONL\", object_type: object.__typename, object: object %}\n {% endcase %}\n {% endfor %}\n\n {% comment %}\n -- loop through variants_by_id to build csv rows, one row per variant\n {% endcomment %}\n\n {% for pair in variants_by_id %}\n {% assign variant = pair[1] %}\n {% assign variant_row = hash %}\n\n {% comment %}\n -- exclude untracked variants (no graphql filter for this)\n {% endcomment %}\n\n {% unless variant.inventoryItem.tracked %}\n {% continue %}\n {% endunless %}\n\n {% for column in columns %}\n {% unless forloop.rindex0 < location_count %}\n {% assign variant_row[column] = nil %}\n {% else %}\n {% assign variant_row[column] = \"not stocked\" %}\n {% endunless %}\n {% endfor %}\n\n {% assign variant_row[\"Handle\"] = variant.product.handle %}\n {% assign variant_row[\"SKU\"] = variant.sku %}\n {% assign variant_row[\"Title\"] = variant.product.title %}\n\n {% comment %}\n -- map variant selected options to the correct product option columns\n {% endcomment %}\n\n {% assign product_options = hash %}\n\n {% for option in variant.product.options %}\n {% assign product_options[option.name] = option.position %}\n {% endfor %}\n\n {% for option in variant.selectedOptions %}\n {% assign position = product_options[option.name] %}\n {% assign option_name_key = \"Option\" | append: position | append: \" Name\" %}\n {% assign option_value_key = \"Option\" | append: position | append: \" Value\" %}\n {% assign variant_row[option_name_key] = option.name %}\n {% assign variant_row[option_value_key] = option.value %}\n {% endfor %}\n\n {% comment %}\n -- add \"available\" inventory levels by location\n {% endcomment %}\n\n {% for inventory_level in variant.inventory_levels %}\n {% assign location_name = inventory_level.location.name %}\n {% assign variant_row[location_name] = inventory_level.quantities.first.quantity %}\n {% endfor %}\n\n {% comment %}\n -- flatten the variant row hash into an array of values\n {% endcomment %}\n\n {% assign row = array %}\n {% for pair in variant_row %}\n {% assign row[forloop.index0] = pair[1] %}\n {% endfor %}\n\n {% comment %}\n -- add the row to 2d rows array\n {% endcomment %}\n\n {% assign rows[rows.size] = row %}\n {% endfor %}{% comment %}-- end variants loop --{% endcomment %}\n\n {% comment %}\n -- convert 2d array into csv format\n {% endcomment %}\n\n {% assign csv = rows | csv %}\n\n {% if event.preview %}\n {% action \"echo\" csv %}\n {% endif %}\n\n {% capture file_name %}inventory__{{ \"now\" | date: \"%Y-%m-%d_T%H-%M-%S_%Z\", tz: \"UTC\" }}.csv{% endcapture %}\n\n {% if export_using_sftp %}\n {% comment %}\n -- directory paths may or may not have a leading slash (if they do, they're absolute;\n -- if they don't, they're relative), but we always need a trailing slash\n {% endcomment %}\n\n {% if options.sftp_upload_directory != blank %}\n {% assign directory = options.sftp_upload_directory %}\n\n {% if directory.last != \"/\" %}\n {% assign directory = directory | append: \"/\" %}\n {% endif %}\n\n {% assign upload_path = directory | append: file_name %}\n {% endif %}\n\n {% action \"ftp\" %}\n {\n \"protocol\": \"sftp\",\n \"host\": {{ options.sftp_host | json }},\n \"port\": {{ options.sftp_port__number | json }},\n \"user\": {{ options.sftp_user | json }},\n \"password\": {{ options.sftp_password | json }},\n \"uploads\": {\n {{ upload_path | default: file_name | json }}: {{ csv | json }}\n }\n }\n {% endaction %}\n {% endif %}\n\n {% if options.send_email_export_to_this_address != blank %}\n {% action \"email\" %}\n {\n \"to\": {{ options.send_email_export_to_this_address | json }},\n \"subject\": {{ \"Inventory export for \" | append: shop.name | json }},\n \"body\": \"Please see attached. :)\",\n \"reply_to\": {{ shop.customer_email | json }},\n \"from_display_name\": {{ shop.name | json }},\n \"attachments\": {\n {{ file_name | json }}: {{ csv | json }}\n }\n }\n {% endaction %}\n {% endif %}\n{% endif %}\n", "subscriptions": [ "mechanic/user/trigger", "mechanic/shopify/bulk_operation" @@ -24,6 +24,7 @@ "Backups", "CSV", "Export", - "FTP" + "FTP", + "Inventory" ] }