-
Notifications
You must be signed in to change notification settings - Fork 35
/
Copy pathbackup-scheduled-inventory-exports-in-shopifys-csv-format.json
30 lines (30 loc) · 12.6 KB
/
backup-scheduled-inventory-exports-in-shopifys-csv-format.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"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,
"options": {
"only_export_products_matching_this_query": null,
"run_every_x_hours__number": null,
"sftp_host__required": null,
"sftp_port__required_number": null,
"sftp_user__required": null,
"sftp_password__required": null,
"sftp_upload_directory": null
},
"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 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"
],
"subscriptions_template": "mechanic/user/trigger\n{% if options.run_every_x_hours__number == 24 %}\n mechanic/scheduler/daily\n{% elsif options.run_every_x_hours__number %}\n mechanic/scheduler/hourly\n{% endif %}\nmechanic/shopify/bulk_operation",
"tags": [
"Backups",
"CSV",
"Export",
"FTP",
"Inventory"
]
}