Refactor shopify (#13588)
* move shopify settings to ERPNext
* [fix] setup webhook for order and validate webhook request
* validate webhook request
* sync customer and item if not exists while syncing order from shopify
* pull item from shopify and minor fixes
* fix method naming, mechanisim to register and withdraw webhooks, patch
* minor fix
* minor fixes
* [Patch][fix] use remove_from_installed_apps instead of remove app
* log exceptions
* add shopify logging for failed requests
* minor fixes
* fix test case
* codecy fixes
* check shared secret exists
* Test Case fixes
* UX fixes and patch fixes
* Documentation
* fixes
* [fix] dump webhooks request data into doctype
* Provision to resync the order if error occured in sync
* [fix] set default host
diff --git a/erpnext/config/erpnext_integrations.py b/erpnext/config/erpnext_integrations.py
index 88c4779..e27b7cd 100644
--- a/erpnext/config/erpnext_integrations.py
+++ b/erpnext/config/erpnext_integrations.py
@@ -25,6 +25,11 @@
{
"type": "doctype",
"name": "Woocommerce Settings"
+ },
+ {
+ "type": "doctype",
+ "name": "Shopify Settings",
+ "description": _("Connect Shopify with ERPNext"),
}
]
}
diff --git a/erpnext/docs/assets/img/erpnext_integrations/app_details.png b/erpnext/docs/assets/img/erpnext_integrations/app_details.png
new file mode 100644
index 0000000..3492eaa
--- /dev/null
+++ b/erpnext/docs/assets/img/erpnext_integrations/app_details.png
Binary files differ
diff --git a/erpnext/docs/assets/img/erpnext_integrations/erp_configurations.png b/erpnext/docs/assets/img/erpnext_integrations/erp_configurations.png
new file mode 100644
index 0000000..12ff52d
--- /dev/null
+++ b/erpnext/docs/assets/img/erpnext_integrations/erp_configurations.png
Binary files differ
diff --git a/erpnext/docs/assets/img/erpnext_integrations/manage_private_apps.png b/erpnext/docs/assets/img/erpnext_integrations/manage_private_apps.png
new file mode 100644
index 0000000..62fbc73
--- /dev/null
+++ b/erpnext/docs/assets/img/erpnext_integrations/manage_private_apps.png
Binary files differ
diff --git a/erpnext/docs/assets/img/erpnext_integrations/menu_bar.png b/erpnext/docs/assets/img/erpnext_integrations/menu_bar.png
new file mode 100644
index 0000000..d69d0f7
--- /dev/null
+++ b/erpnext/docs/assets/img/erpnext_integrations/menu_bar.png
Binary files differ
diff --git a/erpnext/docs/assets/img/erpnext_integrations/sync_config.png b/erpnext/docs/assets/img/erpnext_integrations/sync_config.png
new file mode 100644
index 0000000..a6611c9
--- /dev/null
+++ b/erpnext/docs/assets/img/erpnext_integrations/sync_config.png
Binary files differ
diff --git a/erpnext/docs/assets/img/erpnext_integrations/tax_config.png b/erpnext/docs/assets/img/erpnext_integrations/tax_config.png
new file mode 100644
index 0000000..3c6cc55
--- /dev/null
+++ b/erpnext/docs/assets/img/erpnext_integrations/tax_config.png
Binary files differ
diff --git a/erpnext/docs/user/manual/en/erpnext_integration/shopify_integration.md b/erpnext/docs/user/manual/en/erpnext_integration/shopify_integration.md
new file mode 100644
index 0000000..391bf2b
--- /dev/null
+++ b/erpnext/docs/user/manual/en/erpnext_integration/shopify_integration.md
@@ -0,0 +1,42 @@
+# Shopify Integration
+
+The Shopify Connector pulls the orders from Shopify and creates Sales Order against them in ERPNext.
+
+While creating the sales order if Customer or Item is missing in ERPNext the system will create new Customer/Item by pulling respective details from Shopify.
+
+### How to Setup Connector?
+
+#### Create A Private App in Shopify
+
+1. Click on Apps in menu bar
+<img class="screenshot" alt="Menu Section" src="{{docs_base_url}}/assets/img/erpnext_integrations/app_menu.png">
+
+2. Click on **Manage private apps** to create private app
+<img class="screenshot" alt="Manage Private Apps" src="{{docs_base_url}}/assets/img/erpnext_integrations/manage_private_apps.png">
+
+3. Fill up the details and create app. The each app has its own API key, Password and Shared secret
+<img class="screenshot" alt="App Details" src="{{docs_base_url}}/assets/img/erpnext_integrations/app_details.png">
+
+
+#### Setting Up Shopify on ERPNext:-
+Once you have created a Private App on Shopify, setup App Credentials and other details in ERPNext.
+
+1. Select App Type as Private and Fill-up API key, Password and Shared Secret from Shopify's Private App.
+<img class="screenshot" alt="Setup Private App Credentials" src="{{docs_base_url}}/assets/img/erpnext_integrations/app_details.png">
+
+2. Setup Customer, Company and Inventory configurations
+<img class="screenshot" alt="ERP Configurations" src="{{docs_base_url}}/assets/img/erpnext_integrations/erp_configurations.png">
+
+3. Setup Sync Configurations.
+ The system pulls Orders from Shopify and creates Sales Order in ERPNext. You can configure ERPNext system to capture payment and fulfilments against orders.
+<img class="screenshot" alt="Sync Configure" src="{{docs_base_url}}/assets/img/erpnext_integrations/sync_config.png">
+
+4. Setup Tax Mapper.
+ Prepare tax and shipping charges mapper for each tax and shipping charge you apply in Shopify
+<img class="screenshot" alt="Taxes and Shipping Charges" src="{{docs_base_url}}/assets/img/erpnext_integrations/tax_config.png">
+
+
+After setting up all the configurations, enable the Shopify sync and save the settings. This will register the API's to Shopify and the system will start Order sync between Shopify and ERPNext.
+
+### Note:
+The connector won't handle Order cancellation. If you cancelled any order in Shopify then manually you have to cancel respective Sales Order and other documents in ERPNext.
diff --git a/erpnext/erpnext_integrations/connectors/shopify_connection.py b/erpnext/erpnext_integrations/connectors/shopify_connection.py
new file mode 100644
index 0000000..88078ab
--- /dev/null
+++ b/erpnext/erpnext_integrations/connectors/shopify_connection.py
@@ -0,0 +1,257 @@
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+import json
+from frappe.utils import cstr, cint, nowdate, flt
+from erpnext.erpnext_integrations.utils import validate_webhooks_request
+from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note, make_sales_invoice
+from erpnext.erpnext_integrations.doctype.shopify_settings.sync_product import sync_item_from_shopify
+from erpnext.erpnext_integrations.doctype.shopify_settings.sync_customer import create_customer
+from erpnext.erpnext_integrations.doctype.shopify_log.shopify_log import make_shopify_log, dump_request_data
+
+@frappe.whitelist(allow_guest=True)
+@validate_webhooks_request("Shopify Settings", 'X-Shopify-Hmac-Sha256', secret_key='shared_secret')
+def store_request_data(order=None, event=None):
+ if frappe.request:
+ order = json.loads(frappe.request.data)
+ event = frappe.request.headers.get('X-Shopify-Topic')
+
+ dump_request_data(order, event)
+
+def sync_sales_order(order, request_id=None):
+ shopify_settings = frappe.get_doc("Shopify Settings")
+ frappe.flags.request_id = request_id
+
+ if not frappe.db.get_value("Sales Order", filters={"shopify_order_id": cstr(order['id'])}):
+ try:
+ validate_customer(order, shopify_settings)
+ validate_item(order, shopify_settings)
+ create_order(order, shopify_settings)
+ except Exception as e:
+ make_shopify_log(status="Error", message=e.message, exception=False)
+ else:
+ make_shopify_log(status="Success")
+
+def prepare_sales_invoice(order, request_id=None):
+ shopify_settings = frappe.get_doc("Shopify Settings")
+ frappe.flags.request_id = request_id
+
+ try:
+ sales_order = get_sales_order(cstr(order['id']))
+ if sales_order:
+ create_sales_invoice(order, shopify_settings, sales_order)
+ make_shopify_log(status="Success")
+ except Exception:
+ make_shopify_log(status="Error", exception=True)
+
+def prepare_delivery_note(order, request_id=None):
+ shopify_settings = frappe.get_doc("Shopify Settings")
+ frappe.flags.request_id = request_id
+
+ try:
+ sales_order = get_sales_order(cstr(order['id']))
+ if sales_order:
+ create_delivery_note(order, shopify_settings, sales_order)
+ make_shopify_log(status="Success")
+ except Exception:
+ make_shopify_log(status="Error", exception=True)
+
+def get_sales_order(shopify_order_id):
+ sales_order = frappe.db.get_value("Sales Order", filters={"shopify_order_id": shopify_order_id})
+ if sales_order:
+ so = frappe.get_doc("Sales Order", sales_order)
+ return so
+
+def validate_customer(order, shopify_settings):
+ customer_id = order.get("customer", {}).get("id")
+ if customer_id:
+ if not frappe.db.get_value("Customer", {"shopify_customer_id": customer_id}, "name"):
+ create_customer(order.get("customer"), shopify_settings)
+
+def validate_item(order, shopify_settings):
+ for item in order.get("line_items"):
+ if item.get("product_id") and not frappe.db.get_value("Item", {"shopify_product_id": item.get("product_id")}, "name"):
+ sync_item_from_shopify(shopify_settings, item)
+
+def create_order(order, shopify_settings, company=None):
+ so = create_sales_order(order, shopify_settings, company)
+ if so:
+ if order.get("financial_status") == "paid":
+ create_sales_invoice(order, shopify_settings, so)
+
+ if order.get("fulfillments"):
+ create_delivery_note(order, shopify_settings, so)
+
+def create_sales_order(shopify_order, shopify_settings, company=None):
+ product_not_exists = []
+ customer = frappe.db.get_value("Customer", {"shopify_customer_id": shopify_order.get("customer", {}).get("id")}, "name")
+ so = frappe.db.get_value("Sales Order", {"shopify_order_id": shopify_order.get("id")}, "name")
+
+ if not so:
+ items = get_order_items(shopify_order.get("line_items"), shopify_settings)
+
+ if not items:
+ message = 'Following items are exists in order but relevant record not found in Product master'
+ message += "\n" + ", ".join(product_not_exists)
+
+ make_shopify_log(status="Error", message=message, exception=True)
+
+ return ''
+
+ so = frappe.get_doc({
+ "doctype": "Sales Order",
+ "naming_series": shopify_settings.sales_order_series or "SO-Shopify-",
+ "shopify_order_id": shopify_order.get("id"),
+ "customer": customer or shopify_settings.default_customer,
+ "delivery_date": nowdate(),
+ "company": shopify_settings.company,
+ "selling_price_list": shopify_settings.price_list,
+ "ignore_pricing_rule": 1,
+ "items": items,
+ "taxes": get_order_taxes(shopify_order, shopify_settings),
+ "apply_discount_on": "Grand Total",
+ "discount_amount": get_discounted_amount(shopify_order),
+ })
+
+ if company:
+ so.update({
+ "company": company,
+ "status": "Draft"
+ })
+ so.flags.ignore_mandatory = True
+ so.save(ignore_permissions=True)
+ so.submit()
+
+ else:
+ so = frappe.get_doc("Sales Order", so)
+
+ frappe.db.commit()
+ return so
+
+def create_sales_invoice(shopify_order, shopify_settings, so):
+ if not frappe.db.get_value("Sales Invoice", {"shopify_order_id": shopify_order.get("id")}, "name")\
+ and so.docstatus==1 and not so.per_billed and cint(shopify_settings.sync_sales_invoice):
+
+ si = make_sales_invoice(so.name, ignore_permissions=True)
+ si.shopify_order_id = shopify_order.get("id")
+ si.naming_series = shopify_settings.sales_invoice_series or "SI-Shopify-"
+ si.flags.ignore_mandatory = True
+ set_cost_center(si.items, shopify_settings.cost_center)
+ si.submit()
+ make_payament_entry_against_sales_invoice(si, shopify_settings)
+ frappe.db.commit()
+
+def set_cost_center(items, cost_center):
+ for item in items:
+ item.cost_center = cost_center
+
+def make_payament_entry_against_sales_invoice(doc, shopify_settings):
+ from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+ payemnt_entry = get_payment_entry(doc.doctype, doc.name, bank_account=shopify_settings.cash_bank_account)
+ payemnt_entry.flags.ignore_mandatory = True
+ payemnt_entry.reference_no = doc.name
+ payemnt_entry.reference_date = nowdate()
+ payemnt_entry.submit()
+
+def create_delivery_note(shopify_order, shopify_settings, so):
+ if not cint(shopify_settings.sync_delivery_note):
+ return
+
+ for fulfillment in shopify_order.get("fulfillments"):
+ if not frappe.db.get_value("Delivery Note", {"shopify_fulfillment_id": fulfillment.get("id")}, "name")\
+ and so.docstatus==1:
+
+ dn = make_delivery_note(so.name)
+ dn.shopify_order_id = fulfillment.get("order_id")
+ dn.shopify_fulfillment_id = fulfillment.get("id")
+ dn.naming_series = shopify_settings.delivery_note_series or "DN-Shopify-"
+ dn.items = get_fulfillment_items(dn.items, fulfillment.get("line_items"), shopify_settings)
+ dn.flags.ignore_mandatory = True
+ dn.save()
+ frappe.db.commit()
+
+def get_fulfillment_items(dn_items, fulfillment_items, shopify_settings):
+ return [dn_item.update({"qty": item.get("quantity")}) for item in fulfillment_items for dn_item in dn_items\
+ if get_item_code(item) == dn_item.item_code]
+
+def get_discounted_amount(order):
+ discounted_amount = 0.0
+ for discount in order.get("discount_codes"):
+ discounted_amount += flt(discount.get("amount"))
+ return discounted_amount
+
+def get_order_items(order_items, shopify_settings):
+ items = []
+ all_product_exists = True
+ product_not_exists = []
+
+ for shopify_item in order_items:
+ if not shopify_item.get('product_exists'):
+ all_product_exists = False
+ product_not_exists.append({'title':shopify_item.get('title'),
+ 'shopify_order_id': shopify_item.get('id')})
+ continue
+
+ if all_product_exists:
+ item_code = get_item_code(shopify_item)
+ items.append({
+ "item_code": item_code,
+ "item_name": shopify_item.get("name"),
+ "rate": shopify_item.get("price"),
+ "delivery_date": nowdate(),
+ "qty": shopify_item.get("quantity"),
+ "stock_uom": shopify_item.get("sku"),
+ "warehouse": shopify_settings.warehouse
+ })
+ else:
+ items = []
+
+ return items
+
+def get_item_code(shopify_item):
+ item_code = frappe.db.get_value("Item", {"shopify_variant_id": shopify_item.get("variant_id")}, "item_code")
+ if not item_code:
+ item_code = frappe.db.get_value("Item", {"shopify_product_id": shopify_item.get("product_id")}, "item_code")
+ if not item_code:
+ item_code = frappe.db.get_value("Item", {"item_name": shopify_item.get("title")}, "item_code")
+
+ return item_code
+
+def get_order_taxes(shopify_order, shopify_settings):
+ taxes = []
+ for tax in shopify_order.get("tax_lines"):
+ taxes.append({
+ "charge_type": _("On Net Total"),
+ "account_head": get_tax_account_head(tax),
+ "description": "{0} - {1}%".format(tax.get("title"), tax.get("rate") * 100.0),
+ "rate": tax.get("rate") * 100.00,
+ "included_in_print_rate": 1 if shopify_order.get("taxes_included") else 0,
+ "cost_center": shopify_settings.cost_center
+ })
+
+ taxes = update_taxes_with_shipping_lines(taxes, shopify_order.get("shipping_lines"), shopify_settings)
+
+ return taxes
+
+def update_taxes_with_shipping_lines(taxes, shipping_lines, shopify_settings):
+ for shipping_charge in shipping_lines:
+ taxes.append({
+ "charge_type": _("Actual"),
+ "account_head": get_tax_account_head(shipping_charge),
+ "description": shipping_charge["title"],
+ "tax_amount": shipping_charge["price"],
+ "cost_center": shopify_settings.cost_center
+ })
+
+ return taxes
+
+def get_tax_account_head(tax):
+ tax_title = tax.get("title").encode("utf-8")
+
+ tax_account = frappe.db.get_value("Shopify Tax Account", \
+ {"parent": "Shopify Settings", "shopify_tax": tax_title}, "tax_account")
+
+ if not tax_account:
+ frappe.throw("Tax Account not specified for Shopify Tax {0}".format(tax.get("title")))
+
+ return tax_account
diff --git a/erpnext/erpnext_integrations/doctype/shopify_log/__init__.py b/erpnext/erpnext_integrations/doctype/shopify_log/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_log/__init__.py
diff --git a/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.js b/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.js
new file mode 100644
index 0000000..d3fe7d2b
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.js
@@ -0,0 +1,22 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Shopify Log', {
+ refresh: function(frm) {
+ if (frm.doc.request_data && frm.doc.status=='Error'){
+ frm.add_custom_button('Resync', function() {
+ frappe.call({
+ method:"erpnext.erpnext_integrations.doctype.shopify_log.shopify_log.resync",
+ args:{
+ method:frm.doc.method,
+ name: frm.doc.name,
+ request_data: frm.doc.request_data
+ },
+ callback: function(r){
+ frappe.msgprint(__("Order rescheduled for sync"))
+ }
+ })
+ }).addClass('btn-primary');
+ }
+ }
+});
diff --git a/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.json b/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.json
new file mode 100644
index 0000000..ab373ee
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.json
@@ -0,0 +1,268 @@
+{
+ "allow_copy": 0,
+ "allow_guest_to_view": 0,
+ "allow_import": 0,
+ "allow_rename": 0,
+ "beta": 0,
+ "creation": "2016-03-14 10:02:06.227184",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "System",
+ "editable_grid": 0,
+ "fields": [
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Title",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "default": "Queued",
+ "fieldname": "status",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Status",
+ "length": 0,
+ "no_copy": 0,
+ "options": "",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "method",
+ "fieldtype": "Small Text",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Method",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "message",
+ "fieldtype": "Code",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Message",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "traceback",
+ "fieldtype": "Code",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Traceback",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "request_data",
+ "fieldtype": "Code",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Request Data",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ }
+ ],
+ "has_web_view": 0,
+ "hide_heading": 0,
+ "hide_toolbar": 0,
+ "idx": 0,
+ "image_view": 0,
+ "in_create": 1,
+ "is_submittable": 0,
+ "issingle": 0,
+ "istable": 0,
+ "max_attachments": 0,
+ "modified": "2018-04-20 16:23:36.862381",
+ "modified_by": "Administrator",
+ "module": "ERPNext Integrations",
+ "name": "Shopify Log",
+ "name_case": "",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "amend": 0,
+ "cancel": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Administrator",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 0,
+ "write": 1
+ },
+ {
+ "amend": 0,
+ "cancel": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 0,
+ "write": 1
+ }
+ ],
+ "quick_entry": 0,
+ "read_only": 0,
+ "read_only_onload": 0,
+ "show_name_in_global_search": 0,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "title",
+ "track_changes": 0,
+ "track_seen": 0
+}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.py b/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.py
new file mode 100644
index 0000000..e6e6579
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import json
+from frappe.model.document import Document
+from erpnext.erpnext_integrations.utils import get_webhook_address
+
+class ShopifyLog(Document):
+ pass
+
+
+def make_shopify_log(status="Queued", message=None, exception=False):
+ # if name not provided by log calling method then fetch existing queued state log
+ if not frappe.flags.request_id:
+ return
+
+ log = frappe.get_doc("Shopify Log", frappe.flags.request_id)
+
+ if exception:
+ frappe.db.rollback()
+ log = frappe.get_doc({"doctype":"Shopify Log"}).insert(ignore_permissions=True)
+
+ log.message = message if message else ''
+ log.traceback = frappe.get_traceback()
+ log.status = status
+ log.save(ignore_permissions=True)
+ frappe.db.commit()
+
+def dump_request_data(data, event="create/order"):
+ event_mapper = {
+ "orders/create": get_webhook_address(connector_name='shopify_connection', method="sync_sales_order", exclude_uri=True),
+ "orders/paid" : get_webhook_address(connector_name='shopify_connection', method="prepare_sales_invoice", exclude_uri=True),
+ "orders/fulfilled": get_webhook_address(connector_name='shopify_connection', method="prepare_delivery_note", exclude_uri=True)
+ }
+
+ log = frappe.get_doc({
+ "doctype": "Shopify Log",
+ "request_data": json.dumps(data, indent=1),
+ "method": event_mapper[event]
+ }).insert(ignore_permissions=True)
+
+ frappe.db.commit()
+ frappe.enqueue(method=event_mapper[event], queue='short', timeout=300, async=True,
+ **{"order": data, "request_id": log.name})
+
+@frappe.whitelist()
+def resync(method, name, request_data):
+ frappe.db.set_value("Shopify Log", name, "status", "Queued", update_modified=False)
+ frappe.enqueue(method=method, queue='short', timeout=300, async=True,
+ **{"order": json.loads(request_data), "request_id": name})
diff --git a/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log_list.js b/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log_list.js
new file mode 100644
index 0000000..0913ce4
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log_list.js
@@ -0,0 +1,12 @@
+frappe.listview_settings['Shopify Log'] = {
+ add_fields: ["status"],
+ get_indicator: function(doc) {
+ if(doc.status==="Success"){
+ return [__("Success"), "green", "status,=,Success"];
+ } else if(doc.status ==="Error"){
+ return [__("Error"), "red", "status,=,Error"];
+ } else if(doc.status ==="Queued"){
+ return [__("Queued"), "orange", "status,=,Queued"];
+ }
+ }
+}
diff --git a/erpnext/erpnext_integrations/doctype/shopify_log/test_shopify_log.js b/erpnext/erpnext_integrations/doctype/shopify_log/test_shopify_log.js
new file mode 100644
index 0000000..d22b6d5
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_log/test_shopify_log.js
@@ -0,0 +1,23 @@
+/* eslint-disable */
+// rename this file from _test_[name] to test_[name] to activate
+// and remove above this line
+
+QUnit.test("test: Shopify Log", function (assert) {
+ let done = assert.async();
+
+ // number of asserts
+ assert.expect(1);
+
+ frappe.run_serially([
+ // insert a new Shopify Log
+ () => frappe.tests.make('Shopify Log', [
+ // values to be set
+ {key: 'value'}
+ ]),
+ () => {
+ assert.equal(cur_frm.doc.key, 'value');
+ },
+ () => done()
+ ]);
+
+});
diff --git a/erpnext/erpnext_integrations/doctype/shopify_log/test_shopify_log.py b/erpnext/erpnext_integrations/doctype/shopify_log/test_shopify_log.py
new file mode 100644
index 0000000..5892e1d
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_log/test_shopify_log.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+
+# test_records = frappe.get_test_records('Shopify Log')
+
+class TestShopifyLog(unittest.TestCase):
+ pass
diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/__init__.py b/erpnext/erpnext_integrations/doctype/shopify_settings/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_settings/__init__.py
diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js
new file mode 100644
index 0000000..1574795
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js
@@ -0,0 +1,90 @@
+// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+// License: GNU General Public License v3. See license.txt
+
+frappe.provide("erpnext_integrations.shopify_settings");
+
+frappe.ui.form.on("Shopify Settings", "onload", function(frm){
+ frappe.call({
+ method:"erpnext.erpnext_integrations.doctype.shopify_settings.shopify_settings.get_series",
+ callback:function(r){
+ $.each(r.message, function(key, value){
+ set_field_options(key, value);
+ });
+ }
+ });
+ erpnext_integrations.shopify_settings.setup_queries(frm);
+})
+
+frappe.ui.form.on("Shopify Settings", "app_type", function(frm) {
+ frm.toggle_reqd("api_key", (frm.doc.app_type == "Private"));
+ frm.toggle_reqd("password", (frm.doc.app_type == "Private"));
+})
+
+frappe.ui.form.on("Shopify Settings", "refresh", function(frm){
+ if(!frm.doc.__islocal && frm.doc.enable_shopify === 1){
+ frm.toggle_reqd("price_list", true);
+ frm.toggle_reqd("warehouse", true);
+ frm.toggle_reqd("taxes", true);
+ frm.toggle_reqd("company", true);
+ frm.toggle_reqd("cost_center", true);
+ frm.toggle_reqd("cash_bank_account", true);
+ frm.toggle_reqd("sales_order_series", true);
+ frm.toggle_reqd("customer_group", true);
+ frm.toggle_reqd("shared_secret", true);
+
+ frm.toggle_reqd("sales_invoice_series", frm.doc.sync_sales_invoice);
+ frm.toggle_reqd("delivery_note_series", frm.doc.sync_delivery_note);
+
+ }
+})
+
+$.extend(erpnext_integrations.shopify_settings, {
+ setup_queries: function(frm) {
+ frm.fields_dict["warehouse"].get_query = function(doc) {
+ return {
+ filters:{
+ "company": doc.company,
+ "is_group": "No"
+ }
+ }
+ }
+
+ frm.fields_dict["taxes"].grid.get_field("tax_account").get_query = function(doc){
+ return {
+ "query": "erpnext.controllers.queries.tax_account_query",
+ "filters": {
+ "account_type": ["Tax", "Chargeable", "Expense Account"],
+ "company": doc.company
+ }
+ }
+ }
+
+ frm.fields_dict["cash_bank_account"].get_query = function(doc) {
+ return {
+ filters: [
+ ["Account", "account_type", "in", ["Cash", "Bank"]],
+ ["Account", "root_type", "=", "Asset"],
+ ["Account", "is_group", "=",0],
+ ["Account", "company", "=", doc.company]
+ ]
+ }
+ }
+
+ frm.fields_dict["cost_center"].get_query = function(doc) {
+ return {
+ filters:{
+ "company": doc.company,
+ "is_group": "No"
+ }
+ }
+ }
+
+ frm.fields_dict["price_list"].get_query = function() {
+ return {
+ filters:{
+ "selling": 1
+ }
+ }
+ }
+ }
+})
diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json
new file mode 100644
index 0000000..cf1c0ad
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json
@@ -0,0 +1,1257 @@
+{
+ "allow_copy": 0,
+ "allow_guest_to_view": 0,
+ "allow_import": 0,
+ "allow_rename": 0,
+ "beta": 0,
+ "creation": "2015-05-18 05:21:07.270859",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "System",
+ "editable_grid": 0,
+ "fields": [
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "status_html",
+ "fieldtype": "HTML",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "status html",
+ "length": 0,
+ "no_copy": 0,
+ "options": "",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "default": "1",
+ "fieldname": "enable_shopify",
+ "fieldtype": "Check",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Enable Shopify",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "default": "Public",
+ "fieldname": "app_type",
+ "fieldtype": "Select",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "App Type",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Public\nPrivate",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "last_sync_datetime",
+ "fieldtype": "Datetime",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Last Sync Datetime",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "section_break_2",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "description": "eg: frappe.myshopify.com",
+ "fieldname": "shopify_url",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Shop URL",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "depends_on": "eval:doc.app_type==\"Private\"",
+ "fieldname": "api_key",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "API Key",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "depends_on": "eval:doc.app_type==\"Private\"",
+ "fieldname": "password",
+ "fieldtype": "Password",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Password",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "shared_secret",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Shared secret",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "access_token",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Access Token",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 1,
+ "columns": 0,
+ "fieldname": "section_break_38",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Webhooks Details",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "webhooks",
+ "fieldtype": "Table",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Webhooks",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Shopify Webhook Detail",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "section_break_15",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Customer Settings",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "description": "If Shopify not contains a customer in Order, then while syncing Orders, the system will consider default customer for order",
+ "fieldname": "default_customer",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Default Customer",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Customer",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "column_break_19",
+ "fieldtype": "Column Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "default": "",
+ "description": "Customer Group will set to selected group while syncing customers from Shopify",
+ "fieldname": "customer_group",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Customer Group",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Customer Group",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "company_dependent_settings",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "For Company",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Company",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "description": "Cash Account will used for Sales Invoice creation",
+ "fieldname": "cash_bank_account",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Cash/Bank Account",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Account",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "column_break_20",
+ "fieldtype": "Column Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Cost Center",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Cost Center",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "erp_settings",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "description": "",
+ "fieldname": "price_list",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Price List",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Price List",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "update_price_in_erpnext_price_list",
+ "fieldtype": "Check",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Update Price from Shopify To ERPNext Price List",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "column_break_26",
+ "fieldtype": "Column Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "default": "",
+ "depends_on": "",
+ "description": "Default Warehouse to to create Sales Order and Delivery Note",
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Warehouse",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Warehouse",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "section_break_25",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "sales_order_series",
+ "fieldtype": "Select",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Sales Order Series",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "column_break_27",
+ "fieldtype": "Column Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "description": "",
+ "fieldname": "sync_delivery_note",
+ "fieldtype": "Check",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Import Delivery Notes from Shopify on Shipment",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "depends_on": "eval:doc.sync_delivery_note==1",
+ "fieldname": "delivery_note_series",
+ "fieldtype": "Select",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Delivery Note Series",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "description": "",
+ "fieldname": "sync_sales_invoice",
+ "fieldtype": "Check",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Import Sales Invoice from Shopify if Payment is marked",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "depends_on": "eval:doc.sync_sales_invoice==1",
+ "fieldname": "sales_invoice_series",
+ "fieldtype": "Select",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Sales Invoice Series",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "section_break_22",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "html_16",
+ "fieldtype": "HTML",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "length": 0,
+ "no_copy": 0,
+ "options": "Map Shopify Taxes / Shipping Charges to ERPNext Account",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "taxes",
+ "fieldtype": "Table",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Shopify Tax Account",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Shopify Tax Account",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ }
+ ],
+ "has_web_view": 0,
+ "hide_heading": 0,
+ "hide_toolbar": 0,
+ "idx": 0,
+ "image_view": 0,
+ "in_create": 0,
+ "is_submittable": 0,
+ "issingle": 1,
+ "istable": 0,
+ "max_attachments": 0,
+ "modified": "2018-04-11 19:04:53.396557",
+ "modified_by": "Administrator",
+ "module": "ERPNext Integrations",
+ "name": "Shopify Settings",
+ "name_case": "",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "amend": 0,
+ "cancel": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 0,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 0,
+ "role": "System Manager",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 0,
+ "write": 1
+ }
+ ],
+ "quick_entry": 0,
+ "read_only": 0,
+ "read_only_onload": 0,
+ "show_name_in_global_search": 0,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 0,
+ "track_seen": 0
+}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py
new file mode 100644
index 0000000..fd4f498
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py
@@ -0,0 +1,136 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import json
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import get_request_session
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+from erpnext.erpnext_integrations.utils import get_webhook_address
+from erpnext.erpnext_integrations.doctype.shopify_log.shopify_log import make_shopify_log
+
+class ShopifySettings(Document):
+ def validate(self):
+ if self.enable_shopify == 1:
+ setup_custom_fields()
+ self.validate_access_credentials()
+ self.register_webhooks()
+ else:
+ self.unregister_webhooks()
+
+ self.validate_app_type()
+
+ def validate_access_credentials(self):
+ if self.app_type == "Private":
+ if not (self.get_password(raise_exception=False) and self.api_key and self.shopify_url):
+ frappe.msgprint(_("Missing value for Password, API Key or Shopify URL"), raise_exception=frappe.ValidationError)
+
+ else:
+ if not (self.access_token and self.shopify_url):
+ frappe.msgprint(_("Access token or Shopify URL missing"), raise_exception=frappe.ValidationError)
+
+ def validate_app_type(self):
+ if self.app_type == "Public":
+ frappe.throw(_("Support for public app is deprecated. Please setup private app, for more details refer user manual"))
+
+ def register_webhooks(self):
+ webhooks = ["orders/create", "orders/paid", "orders/fulfilled"]
+
+ url = get_shopify_url('admin/webhooks.json', self)
+ created_webhooks = [d.method for d in self.webhooks]
+
+ for method in webhooks:
+ if method in created_webhooks:
+ continue
+
+ session = get_request_session()
+ try:
+ d = session.post(url, data=json.dumps({
+ "webhook": {
+ "topic": method,
+ "address": get_webhook_address(connector_name='shopify_connection', method='store_request_data'),
+ "format": "json"
+ }
+ }), headers=get_header(self))
+ d.raise_for_status()
+ self.update_webhook_table(method, d.json())
+ except Exception as e:
+ make_shopify_log(status="Warning", method="register_webhooks",
+ message=e.message, exception=False)
+
+ def unregister_webhooks(self):
+ session = get_request_session()
+ deleted_webhooks = []
+
+ for d in self.webhooks:
+ url = get_shopify_url('admin/webhooks/{0}.json'.format(d.webhook_id), self)
+ try:
+ res = session.delete(url, headers=get_header(self))
+ res.raise_for_status()
+ deleted_webhooks.append(d)
+ except Exception as e:
+ frappe.log_error(message=frappe.get_traceback(), title=e.message[:140])
+
+ for d in deleted_webhooks:
+ self.remove(d)
+
+ def update_webhook_table(self, method, res):
+ self.append("webhooks", {
+ "webhook_id": res['webhook']['id'],
+ "method": method
+ })
+
+def get_shopify_url(path, settings):
+ if settings.app_type == "Private":
+ return 'https://{}:{}@{}/{}'.format(settings.api_key, settings.get_password('password'), settings.shopify_url, path)
+ else:
+ return 'https://{}/{}'.format(settings.shopify_url, path)
+
+def get_header(settings):
+ header = {'Content-Type': 'application/json'}
+
+ if settings.app_type == "Private":
+ return header
+ else:
+ header["X-Shopify-Access-Token"] = settings.access_token
+ return header
+
+@frappe.whitelist()
+def get_series():
+ return {
+ "sales_order_series" : frappe.get_meta("Sales Order").get_options("naming_series") or "SO-Shopify-",
+ "sales_invoice_series" : frappe.get_meta("Sales Invoice").get_options("naming_series") or "SI-Shopify-",
+ "delivery_note_series" : frappe.get_meta("Delivery Note").get_options("naming_series") or "DN-Shopify-"
+ }
+
+def setup_custom_fields():
+ custom_fields = {
+ "Customer": [dict(fieldname='shopify_customer_id', label='Shopify Customer Id',
+ fieldtype='Data', insert_after='series', read_only=1, print_hide=1)],
+ "Address": [dict(fieldname='shopify_address_id', label='Shopify Address Id',
+ fieldtype='Data', insert_after='fax', read_only=1, print_hide=1)],
+ "Item": [
+ dict(fieldname='shopify_variant_id', label='Shopify Variant Id',
+ fieldtype='Data', insert_after='item_code', read_only=1, print_hide=1),
+ dict(fieldname='shopify_product_id', label='Shopify Product Id',
+ fieldtype='Data', insert_after='item_code', read_only=1, print_hide=1),
+ dict(fieldname='shopify_description', label='Shopify Description',
+ fieldtype='Text Editor', insert_after='description', read_only=1, print_hide=1)
+ ],
+ "Sales Order": [dict(fieldname='shopify_order_id', label='Shopify Order Id',
+ fieldtype='Data', insert_after='title', read_only=1, print_hide=1)],
+ "Delivery Note":[
+ dict(fieldname='shopify_order_id', label='Shopify Order Id',
+ fieldtype='Data', insert_after='title', read_only=1, print_hide=1),
+ dict(fieldname='shopify_fulfillment_id', label='Shopify Fulfillment Id',
+ fieldtype='Data', insert_after='title', read_only=1, print_hide=1)
+ ],
+ "Sales Invoice": [dict(fieldname='shopify_order_id', label='Shopify Order Id',
+ fieldtype='Data', insert_after='title', read_only=1, print_hide=1)]
+ }
+
+ create_custom_fields(custom_fields)
+
diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/sync_customer.py b/erpnext/erpnext_integrations/doctype/shopify_settings/sync_customer.py
new file mode 100644
index 0000000..02e1fc9
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_settings/sync_customer.py
@@ -0,0 +1,68 @@
+import frappe
+from frappe import _
+
+def create_customer(shopify_customer, shopify_settings):
+ import frappe.utils.nestedset
+
+ cust_name = (shopify_customer.get("first_name") + " " + (shopify_customer.get("last_name") \
+ and shopify_customer.get("last_name") or "")) if shopify_customer.get("first_name")\
+ else shopify_customer.get("email")
+
+ try:
+ customer = frappe.get_doc({
+ "doctype": "Customer",
+ "name": shopify_customer.get("id"),
+ "customer_name" : cust_name,
+ "shopify_customer_id": shopify_customer.get("id"),
+ "sync_with_shopify": 1,
+ "customer_group": shopify_settings.customer_group,
+ "territory": frappe.utils.nestedset.get_root_of("Territory"),
+ "customer_type": _("Individual")
+ })
+ customer.flags.ignore_mandatory = True
+ customer.insert()
+
+ if customer:
+ create_customer_address(customer, shopify_customer)
+
+ frappe.db.commit()
+
+ except Exception as e:
+ raise e
+
+def create_customer_address(customer, shopify_customer):
+ if not shopify_customer.get("addresses"):
+ return
+
+ for i, address in enumerate(shopify_customer.get("addresses")):
+ address_title, address_type = get_address_title_and_type(customer.customer_name, i)
+ try :
+ frappe.get_doc({
+ "doctype": "Address",
+ "shopify_address_id": address.get("id"),
+ "address_title": address_title,
+ "address_type": address_type,
+ "address_line1": address.get("address1") or "Address 1",
+ "address_line2": address.get("address2"),
+ "city": address.get("city") or "City",
+ "state": address.get("province"),
+ "pincode": address.get("zip"),
+ "country": address.get("country"),
+ "phone": address.get("phone"),
+ "email_id": shopify_customer.get("email"),
+ "links": [{
+ "link_doctype": "Customer",
+ "link_name": customer.name
+ }]
+ }).insert(ignore_mandatory=True)
+
+ except Exception as e:
+ raise e
+
+def get_address_title_and_type(customer_name, index):
+ address_type = _("Billing")
+ address_title = customer_name
+ if frappe.db.get_value("Address", "{0}-{1}".format(customer_name.strip(), address_type)):
+ address_title = "{0}-{1}".format(customer_name.strip(), index)
+
+ return address_title, address_type
diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/sync_product.py b/erpnext/erpnext_integrations/doctype/shopify_settings/sync_product.py
new file mode 100644
index 0000000..55f47a6
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_settings/sync_product.py
@@ -0,0 +1,302 @@
+import frappe
+from frappe import _
+from frappe.utils import cstr, cint, get_request_session
+from erpnext.erpnext_integrations.doctype.shopify_settings.shopify_settings import get_shopify_url, get_header
+
+shopify_variants_attr_list = ["option1", "option2", "option3"]
+
+def sync_item_from_shopify(shopify_settings, item):
+ url = get_shopify_url("/admin/products/{0}.json".format(item.get("product_id")), shopify_settings)
+ session = get_request_session()
+
+ try:
+ res = session.get(url, headers=get_header(shopify_settings))
+ res.raise_for_status()
+
+ shopify_item = res.json()["product"]
+ make_item(shopify_settings.warehouse, shopify_item)
+ except Exception as e:
+ raise e
+
+def make_item(warehouse, shopify_item):
+ add_item_weight(shopify_item)
+
+ if has_variants(shopify_item):
+ attributes = create_attribute(shopify_item)
+ create_item(shopify_item, warehouse, 1, attributes)
+ create_item_variants(shopify_item, warehouse, attributes, shopify_variants_attr_list)
+
+ else:
+ shopify_item["variant_id"] = shopify_item['variants'][0]["id"]
+ create_item(shopify_item, warehouse)
+
+def add_item_weight(shopify_item):
+ shopify_item["weight"] = shopify_item['variants'][0]["weight"]
+ shopify_item["weight_unit"] = shopify_item['variants'][0]["weight_unit"]
+
+def has_variants(shopify_item):
+ if len(shopify_item.get("options")) >= 1 and "Default Title" not in shopify_item.get("options")[0]["values"]:
+ return True
+ return False
+
+def create_attribute(shopify_item):
+ attribute = []
+ # shopify item dict
+ for attr in shopify_item.get('options'):
+ if not frappe.db.get_value("Item Attribute", attr.get("name"), "name"):
+ frappe.get_doc({
+ "doctype": "Item Attribute",
+ "attribute_name": attr.get("name"),
+ "item_attribute_values": [
+ {
+ "attribute_value": attr_value,
+ "abbr":attr_value
+ }
+ for attr_value in attr.get("values")
+ ]
+ }).insert()
+ attribute.append({"attribute": attr.get("name")})
+
+ else:
+ # check for attribute values
+ item_attr = frappe.get_doc("Item Attribute", attr.get("name"))
+ if not item_attr.numeric_values:
+ set_new_attribute_values(item_attr, attr.get("values"))
+ item_attr.save()
+ attribute.append({"attribute": attr.get("name")})
+
+ else:
+ attribute.append({
+ "attribute": attr.get("name"),
+ "from_range": item_attr.get("from_range"),
+ "to_range": item_attr.get("to_range"),
+ "increment": item_attr.get("increment"),
+ "numeric_values": item_attr.get("numeric_values")
+ })
+
+ return attribute
+
+def set_new_attribute_values(item_attr, values):
+ for attr_value in values:
+ if not any((d.abbr.lower() == attr_value.lower() or d.attribute_value.lower() == attr_value.lower())\
+ for d in item_attr.item_attribute_values):
+ item_attr.append("item_attribute_values", {
+ "attribute_value": attr_value,
+ "abbr": attr_value
+ })
+
+def create_item(shopify_item, warehouse, has_variant=0, attributes=None,variant_of=None):
+ item_dict = {
+ "doctype": "Item",
+ "shopify_product_id": shopify_item.get("id"),
+ "shopify_variant_id": shopify_item.get("variant_id"),
+ "variant_of": variant_of,
+ "sync_with_shopify": 1,
+ "is_stock_item": 1,
+ "item_code": cstr(shopify_item.get("item_code")) or cstr(shopify_item.get("id")),
+ "item_name": shopify_item.get("title", '').strip(),
+ "description": shopify_item.get("body_html") or shopify_item.get("title"),
+ "shopify_description": shopify_item.get("body_html") or shopify_item.get("title"),
+ "item_group": get_item_group(shopify_item.get("product_type")),
+ "has_variants": has_variant,
+ "attributes":attributes or [],
+ "stock_uom": shopify_item.get("uom") or _("Nos"),
+ "stock_keeping_unit": shopify_item.get("sku") or get_sku(shopify_item),
+ "default_warehouse": warehouse,
+ "image": get_item_image(shopify_item),
+ "weight_uom": shopify_item.get("weight_unit"),
+ "weight_per_unit": shopify_item.get("weight"),
+ "default_supplier": get_supplier(shopify_item)
+ }
+
+ if not is_item_exists(item_dict, attributes, variant_of=variant_of):
+ item_details = get_item_details(shopify_item)
+ name = ''
+
+ if not item_details:
+ new_item = frappe.get_doc(item_dict)
+ new_item.insert()
+ name = new_item.name
+
+ if not name:
+ name = item_details.name
+
+ if not has_variant:
+ add_to_price_list(shopify_item, name)
+
+ frappe.db.commit()
+
+def create_item_variants(shopify_item, warehouse, attributes, shopify_variants_attr_list):
+ template_item = frappe.db.get_value("Item", filters={"shopify_product_id": shopify_item.get("id")},
+ fieldname=["name", "stock_uom"], as_dict=True)
+
+ if template_item:
+ for variant in shopify_item.get("variants"):
+ shopify_item_variant = {
+ "id" : variant.get("id"),
+ "item_code": variant.get("id"),
+ "title": variant.get("title"),
+ "product_type": shopify_item.get("product_type"),
+ "sku": variant.get("sku"),
+ "uom": template_item.stock_uom or _("Nos"),
+ "item_price": variant.get("price"),
+ "variant_id": variant.get("id"),
+ "weight_unit": variant.get("weight_unit"),
+ "weight": variant.get("weight")
+ }
+
+ for i, variant_attr in enumerate(shopify_variants_attr_list):
+ if variant.get(variant_attr):
+ attributes[i].update({"attribute_value": get_attribute_value(variant.get(variant_attr), attributes[i])})
+ create_item(shopify_item_variant, warehouse, 0, attributes, template_item.name)
+
+def get_attribute_value(variant_attr_val, attribute):
+ attribute_value = frappe.db.sql("""select attribute_value from `tabItem Attribute Value`
+ where parent = %s and (abbr = %s or attribute_value = %s)""", (attribute["attribute"], variant_attr_val,
+ variant_attr_val), as_list=1)
+ return attribute_value[0][0] if len(attribute_value)>0 else cint(variant_attr_val)
+
+def get_item_group(product_type=None):
+ import frappe.utils.nestedset
+ parent_item_group = frappe.utils.nestedset.get_root_of("Item Group")
+
+ if product_type:
+ if not frappe.db.get_value("Item Group", product_type, "name"):
+ item_group = frappe.get_doc({
+ "doctype": "Item Group",
+ "item_group_name": product_type,
+ "parent_item_group": parent_item_group,
+ "is_group": "No"
+ }).insert()
+ return item_group.name
+ else:
+ return product_type
+ else:
+ return parent_item_group
+
+
+def get_sku(item):
+ if item.get("variants"):
+ return item.get("variants")[0].get("sku")
+ return ""
+
+def add_to_price_list(item, name):
+ shopify_settings = frappe.db.get_value("Shopify Settings", None, ["price_list", "update_price_in_erpnext_price_list"], as_dict=1)
+ if not shopify_settings.update_price_in_erpnext_price_list:
+ return
+
+ item_price_name = frappe.db.get_value("Item Price",
+ {"item_code": name, "price_list": shopify_settings.price_list}, "name")
+
+ if not item_price_name:
+ frappe.get_doc({
+ "doctype": "Item Price",
+ "price_list": shopify_settings.price_list,
+ "item_code": name,
+ "price_list_rate": item.get("item_price") or item.get("variants")[0].get("price")
+ }).insert()
+ else:
+ item_rate = frappe.get_doc("Item Price", item_price_name)
+ item_rate.price_list_rate = item.get("item_price") or item.get("variants")[0].get("price")
+ item_rate.save()
+
+def get_item_image(shopify_item):
+ if shopify_item.get("image"):
+ return shopify_item.get("image").get("src")
+ return None
+
+def get_supplier(shopify_item):
+ if shopify_item.get("vendor"):
+ supplier = frappe.db.sql("""select name from tabSupplier
+ where name = %s or shopify_supplier_id = %s """, (shopify_item.get("vendor"),
+ shopify_item.get("vendor").lower()), as_list=1)
+
+ if not supplier:
+ supplier = frappe.get_doc({
+ "doctype": "Supplier",
+ "supplier_name": shopify_item.get("vendor"),
+ "shopify_supplier_id": shopify_item.get("vendor").lower(),
+ "supplier_type": get_supplier_type()
+ }).insert()
+ return supplier.name
+ else:
+ return shopify_item.get("vendor")
+ else:
+ return ""
+
+def get_supplier_type():
+ supplier_type = frappe.db.get_value("Supplier Type", _("Shopify Supplier"))
+ if not supplier_type:
+ supplier_type = frappe.get_doc({
+ "doctype": "Supplier Type",
+ "supplier_type": _("Shopify Supplier")
+ }).insert()
+ return supplier_type.name
+ return supplier_type
+
+def get_item_details(shopify_item):
+ item_details = {}
+
+ item_details = frappe.db.get_value("Item", {"shopify_product_id": shopify_item.get("id")},
+ ["name", "stock_uom", "item_name"], as_dict=1)
+
+ if item_details:
+ return item_details
+
+ else:
+ item_details = frappe.db.get_value("Item", {"shopify_variant_id": shopify_item.get("id")},
+ ["name", "stock_uom", "item_name"], as_dict=1)
+ return item_details
+
+def is_item_exists(shopify_item, attributes=None, variant_of=None):
+ if variant_of:
+ name = variant_of
+ else:
+ name = frappe.db.get_value("Item", {"item_name": shopify_item.get("item_name")})
+
+ if name:
+ item = frappe.get_doc("Item", name)
+ item.flags.ignore_mandatory=True
+
+ if not variant_of and not item.shopify_product_id:
+ item.shopify_product_id = shopify_item.get("shopify_product_id")
+ item.shopify_variant_id = shopify_item.get("shopify_variant_id")
+ item.save()
+ return True
+
+ if item.shopify_product_id and attributes and attributes[0].get("attribute_value"):
+ if not variant_of:
+ variant_of = frappe.db.get_value("Item",
+ {"shopify_product_id": item.shopify_product_id}, "variant_of")
+
+ # create conditions for all item attributes,
+ # as we are putting condition basis on OR it will fetch all items matching either of conditions
+ # thus comparing matching conditions with len(attributes)
+ # which will give exact matching variant item.
+
+ conditions = ["(iv.attribute='{0}' and iv.attribute_value = '{1}')"\
+ .format(attr.get("attribute"), attr.get("attribute_value")) for attr in attributes]
+
+ conditions = "( {0} ) and iv.parent = it.name ) = {1}".format(" or ".join(conditions), len(attributes))
+
+ parent = frappe.db.sql(""" select * from tabItem it where
+ ( select count(*) from `tabItem Variant Attribute` iv
+ where {conditions} and it.variant_of = %s """.format(conditions=conditions) ,
+ variant_of, as_list=1)
+
+ if parent:
+ variant = frappe.get_doc("Item", parent[0][0])
+ variant.flags.ignore_mandatory = True
+
+ variant.shopify_product_id = shopify_item.get("shopify_product_id")
+ variant.shopify_variant_id = shopify_item.get("shopify_variant_id")
+ variant.save()
+ return False
+
+ if item.shopify_product_id and item.shopify_product_id != shopify_item.get("shopify_product_id"):
+ return False
+
+ return True
+
+ else:
+ return False
diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/test_data/shopify_customer.json b/erpnext/erpnext_integrations/doctype/shopify_settings/test_data/shopify_customer.json
new file mode 100644
index 0000000..e91ce9a
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_settings/test_data/shopify_customer.json
@@ -0,0 +1,59 @@
+{
+ "customer": {
+ "id": 2324518599,
+ "email": "andrew@wyatt.co.in",
+ "accepts_marketing": false,
+ "created_at": "2016-01-20T17:18:35+05:30",
+ "updated_at": "2016-01-20T17:22:23+05:30",
+ "first_name": "Andrew",
+ "last_name": "Wyatt",
+ "orders_count": 0,
+ "state": "disabled",
+ "total_spent": "0.00",
+ "last_order_id": null,
+ "note": "",
+ "verified_email": true,
+ "multipass_identifier": null,
+ "tax_exempt": false,
+ "tags": "",
+ "last_order_name": null,
+ "default_address": {
+ "id": 2476804295,
+ "first_name": "Andrew",
+ "last_name": "Wyatt",
+ "company": "Wyatt Inc.",
+ "address1": "B-11, Betahouse",
+ "address2": "Street 11, Sector 52",
+ "city": "Manhattan",
+ "province": "New York",
+ "country": "United States",
+ "zip": "10027",
+ "phone": "145-112211",
+ "name": "Andrew Wyatt",
+ "province_code": "NY",
+ "country_code": "US",
+ "country_name": "United States",
+ "default": true
+ },
+ "addresses": [
+ {
+ "id": 2476804295,
+ "first_name": "Andrew",
+ "last_name": "Wyatt",
+ "company": "Wyatt Inc.",
+ "address1": "B-11, Betahouse",
+ "address2": "Street 11, Sector 52",
+ "city": "Manhattan",
+ "province": "New York",
+ "country": "United States",
+ "zip": "10027",
+ "phone": "145-112211",
+ "name": "Andrew Wyatt",
+ "province_code": "NY",
+ "country_code": "US",
+ "country_name": "United States",
+ "default": true
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/test_data/shopify_item.json b/erpnext/erpnext_integrations/doctype/shopify_settings/test_data/shopify_item.json
new file mode 100644
index 0000000..296dede
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_settings/test_data/shopify_item.json
@@ -0,0 +1,125 @@
+{
+ "product": {
+ "id": 4059739520,
+ "title": "Shopify Test Item",
+ "body_html": "<div>Hold back Spin Medallion-Set of 2</div>\n<div></div>\n<div>Finish: Plated/ Powder Coated</div>\n<div>Material: Iron</div>\n<div>Color Finish: Satin Silver, Brown Oil Rubbed, Roman Bronze</div>\n<div>Qty: 1 Set</div>",
+ "vendor": "Boa casa",
+ "product_type": "Curtain Accessories",
+ "created_at": "2016-01-18T17:16:37+05:30",
+ "handle": "1001624-01",
+ "updated_at": "2016-01-20T17:26:44+05:30",
+ "published_at": "2016-01-18T17:16:37+05:30",
+ "template_suffix": null,
+ "published_scope": "global",
+ "tags": "Category_Curtain Accessories, Type_Holdback",
+ "variants": [{
+ "id": 13917612359,
+ "product_id": 4059739520,
+ "title": "Test BALCK Item",
+ "price": "499.00",
+ "sku": "",
+ "position": 1,
+ "grams": 0,
+ "inventory_policy": "continue",
+ "compare_at_price": null,
+ "fulfillment_service": "manual",
+ "inventory_management": "shopify",
+ "option1": "BLACK",
+ "option2": null,
+ "option3": null,
+ "created_at": "2016-01-18T17:16:37+05:30",
+ "updated_at": "2016-01-20T17:26:44+05:30",
+ "requires_shipping": true,
+ "taxable": true,
+ "barcode": "",
+ "inventory_quantity": -1,
+ "old_inventory_quantity": -1,
+ "image_id": 8539321735,
+ "weight": 0,
+ "weight_unit": "kg"
+ }, {
+ "id": 13917612423,
+ "product_id": 4059739520,
+ "title": "Test BLUE Item",
+ "price": "499.00",
+ "sku": "",
+ "position": 2,
+ "grams": 0,
+ "inventory_policy": "continue",
+ "compare_at_price": null,
+ "fulfillment_service": "manual",
+ "inventory_management": "shopify",
+ "option1": "BLUE",
+ "option2": null,
+ "option3": null,
+ "created_at": "2016-01-18T17:16:37+05:30",
+ "updated_at": "2016-01-20T17:26:44+05:30",
+ "requires_shipping": true,
+ "taxable": true,
+ "barcode": "",
+ "inventory_quantity": -1,
+ "old_inventory_quantity": -1,
+ "image_id": null,
+ "weight": 0,
+ "weight_unit": "kg"
+ }, {
+ "id": 13917612487,
+ "product_id": 4059739520,
+ "title": "Test White Item",
+ "price": "499.00",
+ "sku": "",
+ "position": 3,
+ "grams": 0,
+ "inventory_policy": "continue",
+ "compare_at_price": null,
+ "fulfillment_service": "manual",
+ "inventory_management": "shopify",
+ "option1": "White",
+ "option2": null,
+ "option3": null,
+ "created_at": "2016-01-18T17:16:37+05:30",
+ "updated_at": "2016-01-18T17:16:37+05:30",
+ "requires_shipping": true,
+ "taxable": true,
+ "barcode": "",
+ "inventory_quantity": 0,
+ "old_inventory_quantity": 0,
+ "image_id": null,
+ "weight": 0,
+ "weight_unit": "kg"
+ }],
+ "options": [{
+ "id": 4985027399,
+ "product_id": 4059739520,
+ "name": "Colour",
+ "position": 1,
+ "values": [
+ "BLACK",
+ "BLUE",
+ "White"
+ ]
+ }],
+ "images": [{
+ "id": 8539321735,
+ "product_id": 4059739520,
+ "position": 1,
+ "created_at": "2016-01-18T17:16:37+05:30",
+ "updated_at": "2016-01-18T17:16:37+05:30",
+ "src": "https://cdn.shopify.com/s/files/1/1123/0654/products/2015-12-17_6.png?v=1453117597",
+ "variant_ids": [
+ 13917612359
+ ]
+ }],
+ "image": {
+ "id": 8539321735,
+ "product_id": 4059739520,
+ "position": 1,
+ "created_at": "2016-01-18T17:16:37+05:30",
+ "updated_at": "2016-01-18T17:16:37+05:30",
+ "src": "https://cdn.shopify.com/s/files/1/1123/0654/products/2015-12-17_6.png?v=1453117597",
+ "variant_ids": [
+ 13917612359
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/test_data/shopify_order.json b/erpnext/erpnext_integrations/doctype/shopify_settings/test_data/shopify_order.json
new file mode 100644
index 0000000..988a2f0
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_settings/test_data/shopify_order.json
@@ -0,0 +1,270 @@
+{
+ "order": {
+ "id": 2414345735,
+ "email": "andrew@wyatt.co.in",
+ "closed_at": null,
+ "created_at": "2016-01-20T17:26:39+05:30",
+ "updated_at": "2016-01-20T17:27:15+05:30",
+ "number": 5,
+ "note": "",
+ "token": "660fed25987517b733644a8c9ec7c8e0",
+ "gateway": "manual",
+ "test": false,
+ "total_price": "1018.00",
+ "subtotal_price": "998.00",
+ "total_weight": 0,
+ "total_tax": "0.00",
+ "taxes_included": false,
+ "currency": "INR",
+ "financial_status": "paid",
+ "confirmed": true,
+ "total_discounts": "0.00",
+ "total_line_items_price": "998.00",
+ "cart_token": null,
+ "buyer_accepts_marketing": false,
+ "name": "#1005",
+ "referring_site": null,
+ "landing_site": null,
+ "cancelled_at": null,
+ "cancel_reason": null,
+ "total_price_usd": "15.02",
+ "checkout_token": null,
+ "reference": null,
+ "user_id": 55391175,
+ "location_id": null,
+ "source_identifier": null,
+ "source_url": null,
+ "processed_at": "2016-01-20T17:26:39+05:30",
+ "device_id": null,
+ "browser_ip": null,
+ "landing_site_ref": null,
+ "order_number": 1005,
+ "discount_codes": [],
+ "note_attributes": [],
+ "payment_gateway_names": [
+ "manual"
+ ],
+ "processing_method": "manual",
+ "checkout_id": null,
+ "source_name": "shopify_draft_order",
+ "fulfillment_status": "fulfilled",
+ "tax_lines": [],
+ "tags": "",
+ "contact_email": "andrew@wyatt.co.in",
+ "line_items": [
+ {
+ "id": 4125768135,
+ "variant_id": 13917612359,
+ "title": "Shopify Test Item",
+ "quantity": 1,
+ "price": "499.00",
+ "grams": 0,
+ "sku": "",
+ "variant_title": "Roman BALCK 1",
+ "vendor": "Boa casa",
+ "fulfillment_service": "manual",
+ "product_id": 4059739527,
+ "requires_shipping": true,
+ "taxable": true,
+ "gift_card": false,
+ "name": "Roman BALCK 1",
+ "variant_inventory_management": "shopify",
+ "properties": [],
+ "product_exists": true,
+ "fulfillable_quantity": 0,
+ "total_discount": "0.00",
+ "fulfillment_status": "fulfilled",
+ "tax_lines": []
+ },
+ {
+ "id": 4125768199,
+ "variant_id": 13917612423,
+ "title": "Shopify Test Item",
+ "quantity": 1,
+ "price": "499.00",
+ "grams": 0,
+ "sku": "",
+ "variant_title": "Satin BLUE 1",
+ "vendor": "Boa casa",
+ "fulfillment_service": "manual",
+ "product_id": 4059739527,
+ "requires_shipping": true,
+ "taxable": true,
+ "gift_card": false,
+ "name": "Satin BLUE 1",
+ "variant_inventory_management": "shopify",
+ "properties": [],
+ "product_exists": true,
+ "fulfillable_quantity": 0,
+ "total_discount": "0.00",
+ "fulfillment_status": "fulfilled",
+ "tax_lines": []
+ }
+ ],
+ "shipping_lines": [
+ {
+ "id": 2108906247,
+ "title": "International Shipping",
+ "price": "20.00",
+ "code": "International Shipping",
+ "source": "shopify",
+ "phone": null,
+ "tax_lines": []
+ }
+ ],
+ "billing_address": {
+ "first_name": "Andrew",
+ "address1": "B-11, Betahouse",
+ "phone": "145-112211",
+ "city": "Manhattan",
+ "zip": "10027",
+ "province": "New York",
+ "country": "United States",
+ "last_name": "Wyatt",
+ "address2": "Street 11, Sector 52",
+ "company": "Wyatt Inc.",
+ "latitude": 40.8138912,
+ "longitude": -73.96243270000001,
+ "name": "Andrew Wyatt",
+ "country_code": "US",
+ "province_code": "NY"
+ },
+ "shipping_address": {
+ "first_name": "Andrew",
+ "address1": "B-11, Betahouse",
+ "phone": "145-112211",
+ "city": "Manhattan",
+ "zip": "10027",
+ "province": "New York",
+ "country": "United States",
+ "last_name": "Wyatt",
+ "address2": "Street 11, Sector 52",
+ "company": "Wyatt Inc.",
+ "latitude": 40.8138912,
+ "longitude": -73.96243270000001,
+ "name": "Andrew Wyatt",
+ "country_code": "US",
+ "province_code": "NY"
+ },
+ "fulfillments": [
+ {
+ "id": 1849629255,
+ "order_id": 2414345735,
+ "status": "success",
+ "created_at": "2016-01-20T17:27:15+05:30",
+ "service": "manual",
+ "updated_at": "2016-01-20T17:27:15+05:30",
+ "tracking_company": null,
+ "tracking_number": null,
+ "tracking_numbers": [],
+ "tracking_url": null,
+ "tracking_urls": [],
+ "receipt": {},
+ "line_items": [
+ {
+ "id": 4125768199,
+ "variant_id": 13917612423,
+ "title": "1001624/01",
+ "quantity": 1,
+ "price": "499.00",
+ "grams": 0,
+ "sku": "",
+ "variant_title": "Satin Silver",
+ "vendor": "Boa casa",
+ "fulfillment_service": "manual",
+ "product_id": 4059739527,
+ "requires_shipping": true,
+ "taxable": true,
+ "gift_card": false,
+ "name": "1001624/01 - Satin Silver",
+ "variant_inventory_management": "shopify",
+ "properties": [],
+ "product_exists": true,
+ "fulfillable_quantity": 0,
+ "total_discount": "0.00",
+ "fulfillment_status": "fulfilled",
+ "tax_lines": []
+ }
+ ]
+ },
+ {
+ "id": 1849628167,
+ "order_id": 2414345735,
+ "status": "success",
+ "created_at": "2016-01-20T17:26:58+05:30",
+ "service": "manual",
+ "updated_at": "2016-01-20T17:26:58+05:30",
+ "tracking_company": null,
+ "tracking_number": null,
+ "tracking_numbers": [],
+ "tracking_url": null,
+ "tracking_urls": [],
+ "receipt": {},
+ "line_items": [
+ {
+ "id": 4125768135,
+ "variant_id": 13917612359,
+ "title": "1001624/01",
+ "quantity": 1,
+ "price": "499.00",
+ "grams": 0,
+ "sku": "",
+ "variant_title": "Roman Bronze",
+ "vendor": "Boa casa",
+ "fulfillment_service": "manual",
+ "product_id": 4059739527,
+ "requires_shipping": true,
+ "taxable": true,
+ "gift_card": false,
+ "name": "1001624/01 - Roman Bronze",
+ "variant_inventory_management": "shopify",
+ "properties": [],
+ "product_exists": true,
+ "fulfillable_quantity": 0,
+ "total_discount": "0.00",
+ "fulfillment_status": "fulfilled",
+ "tax_lines": []
+ }
+ ]
+ }
+ ],
+ "refunds": [],
+ "customer": {
+ "id": 2324518599,
+ "email": "andrew@wyatt.co.in",
+ "accepts_marketing": false,
+ "created_at": "2016-01-20T17:18:35+05:30",
+ "updated_at": "2016-01-20T17:26:39+05:30",
+ "first_name": "Andrew",
+ "last_name": "Wyatt",
+ "orders_count": 1,
+ "state": "disabled",
+ "total_spent": "1018.00",
+ "last_order_id": 2414345735,
+ "note": "",
+ "verified_email": true,
+ "multipass_identifier": null,
+ "tax_exempt": false,
+ "tags": "",
+ "last_order_name": "#1005",
+ "default_address": {
+ "id": 2476804295,
+ "first_name": "Andrew",
+ "last_name": "Wyatt",
+ "company": "Wyatt Inc.",
+ "address1": "B-11, Betahouse",
+ "address2": "Street 11, Sector 52",
+ "city": "Manhattan",
+ "province": "New York",
+ "country": "United States",
+ "zip": "10027",
+ "phone": "145-112211",
+ "name": "Andrew Wyatt",
+ "province_code": "NY",
+ "country_code": "US",
+ "country_name": "United States",
+ "default": true
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.js b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.js
new file mode 100644
index 0000000..b2f82d5
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.js
@@ -0,0 +1,23 @@
+/* eslint-disable */
+// rename this file from _test_[name] to test_[name] to activate
+// and remove above this line
+
+QUnit.test("test: Shopify Settings", function (assert) {
+ let done = assert.async();
+
+ // number of asserts
+ assert.expect(1);
+
+ frappe.run_serially([
+ // insert a new Shopify Settings
+ () => frappe.tests.make('Shopify Settings', [
+ // values to be set
+ {key: 'value'}
+ ]),
+ () => {
+ assert.equal(cur_frm.doc.key, 'value');
+ },
+ () => done()
+ ]);
+
+});
diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py
new file mode 100644
index 0000000..cd1ab16
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+import frappe
+
+import unittest, os, json
+from frappe.utils import cstr
+from frappe.utils.fixtures import sync_fixtures
+from erpnext.erpnext_integrations.connectors.shopify_connection import create_order
+from erpnext.erpnext_integrations.doctype.shopify_settings.sync_product import make_item
+from erpnext.erpnext_integrations.doctype.shopify_settings.sync_customer import create_customer
+
+class ShopifySettings(unittest.TestCase):
+ def setUp(self):
+ frappe.set_user("Administrator")
+ sync_fixtures("erpnext_shopify")
+ frappe.reload_doctype("Customer")
+ frappe.reload_doctype("Sales Order")
+ frappe.reload_doctype("Delivery Note")
+ frappe.reload_doctype("Sales Invoice")
+
+ self.setup_shopify()
+
+ def setup_shopify(self):
+ shopify_settings = frappe.get_doc("Shopify Settings")
+ shopify_settings.taxes = []
+
+ shopify_settings.update({
+ "app_type": "Private",
+ "shopify_url": "test.myshopify.com",
+ "api_key": "17702c7c4452b9c5d235240b6e7a39da",
+ "password": "17702c7c4452b9c5d235240b6e7a39da",
+ "shared_secret": "17702c7c4452b9c5d235240b6e7a39da",
+ "price_list": "_Test Price List",
+ "warehouse": "_Test Warehouse - _TC",
+ "cash_bank_account": "Cash - _TC",
+ "customer_group": "_Test Customer Group",
+ "cost_center": "Main - _TC",
+ "taxes": [
+ {
+ "shopify_tax": "International Shipping",
+ "tax_account":"Legal Expenses - _TC"
+ }
+ ],
+ "enable_shopify": 0,
+ "sales_order_series": "SO-",
+ "sync_sales_invoice": 1,
+ "sales_invoice_series": "SINV-",
+ "sync_delivery_note": 1,
+ "delivery_note_series": "DN-"
+ }).save(ignore_permissions=True)
+
+ self.shopify_settings = shopify_settings
+
+ def test_order(self):
+ ### Create Customer ###
+ with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_customer.json")) as shopify_customer:
+ shopify_customer = json.load(shopify_customer)
+ create_customer(shopify_customer.get("customer"), self.shopify_settings)
+
+ ### Create Item ###
+ with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_item.json")) as shopify_item:
+ shopify_item = json.load(shopify_item)
+ make_item("_Test Warehouse - _TC", shopify_item.get("product"))
+
+
+ ### Create Order ###
+ with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_order.json")) as shopify_order:
+ shopify_order = json.load(shopify_order)
+
+ create_order(shopify_order.get("order"), self.shopify_settings, "_Test Company")
+
+ sales_order = frappe.get_doc("Sales Order", {"shopify_order_id": cstr(shopify_order.get("order").get("id"))})
+
+ self.assertEqual(cstr(shopify_order.get("order").get("id")), sales_order.shopify_order_id)
+
+ #check for customer
+ shopify_order_customer_id = cstr(shopify_order.get("order").get("customer").get("id"))
+ sales_order_customer_id = frappe.get_value("Customer", sales_order.customer, "shopify_customer_id")
+
+ self.assertEqual(shopify_order_customer_id, sales_order_customer_id)
+
+ #check sales invoice
+ sales_invoice = frappe.get_doc("Sales Invoice", {"shopify_order_id": sales_order.shopify_order_id})
+ self.assertEqual(sales_invoice.rounded_total, sales_order.rounded_total)
+
+ #check delivery note
+ delivery_note_count = frappe.db.sql("""select count(*) from `tabDelivery Note`
+ where shopify_order_id = %s""", sales_order.shopify_order_id)[0][0]
+
+ self.assertEqual(delivery_note_count, len(shopify_order.get("order").get("fulfillments")))
diff --git a/erpnext/erpnext_integrations/doctype/shopify_tax_account/__init__.py b/erpnext/erpnext_integrations/doctype/shopify_tax_account/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_tax_account/__init__.py
diff --git a/erpnext/erpnext_integrations/doctype/shopify_tax_account/shopify_tax_account.json b/erpnext/erpnext_integrations/doctype/shopify_tax_account/shopify_tax_account.json
new file mode 100644
index 0000000..63c674c
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_tax_account/shopify_tax_account.json
@@ -0,0 +1,133 @@
+{
+ "allow_copy": 0,
+ "allow_guest_to_view": 0,
+ "allow_import": 0,
+ "allow_rename": 0,
+ "beta": 0,
+ "creation": "2015-10-05 16:55:20.455371",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "",
+ "editable_grid": 0,
+ "fields": [
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "shopify_tax",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Shopify Tax/Shipping Title",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "tax_account",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "ERPNext Account",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Account",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ }
+ ],
+ "has_web_view": 0,
+ "hide_heading": 0,
+ "hide_toolbar": 0,
+ "idx": 0,
+ "image_view": 0,
+ "in_create": 0,
+ "is_submittable": 0,
+ "issingle": 0,
+ "istable": 1,
+ "max_attachments": 0,
+ "modified": "2018-04-09 11:36:49.272815",
+ "modified_by": "Administrator",
+ "module": "ERPNext Integrations",
+ "name": "Shopify Tax Account",
+ "name_case": "",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 0,
+ "read_only": 0,
+ "read_only_onload": 0,
+ "show_name_in_global_search": 0,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 0,
+ "track_seen": 0
+}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/shopify_tax_account/shopify_tax_account.py b/erpnext/erpnext_integrations/doctype/shopify_tax_account/shopify_tax_account.py
new file mode 100644
index 0000000..74c13c0
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_tax_account/shopify_tax_account.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.model.document import Document
+
+class ShopifyTaxAccount(Document):
+ pass
diff --git a/erpnext/erpnext_integrations/doctype/shopify_webhook_detail/__init__.py b/erpnext/erpnext_integrations/doctype/shopify_webhook_detail/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_webhook_detail/__init__.py
diff --git a/erpnext/erpnext_integrations/doctype/shopify_webhook_detail/shopify_webhook_detail.json b/erpnext/erpnext_integrations/doctype/shopify_webhook_detail/shopify_webhook_detail.json
new file mode 100644
index 0000000..e47ecdc
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_webhook_detail/shopify_webhook_detail.json
@@ -0,0 +1,103 @@
+{
+ "allow_copy": 0,
+ "allow_guest_to_view": 0,
+ "allow_import": 0,
+ "allow_rename": 0,
+ "beta": 0,
+ "creation": "2018-04-10 17:06:22.697427",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "",
+ "editable_grid": 0,
+ "engine": "InnoDB",
+ "fields": [
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "webhook_id",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Webhook ID",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "method",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Method",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ }
+ ],
+ "has_web_view": 0,
+ "hide_heading": 0,
+ "hide_toolbar": 0,
+ "idx": 0,
+ "image_view": 0,
+ "in_create": 0,
+ "is_submittable": 0,
+ "issingle": 0,
+ "istable": 1,
+ "max_attachments": 0,
+ "modified": "2018-04-11 12:43:09.456449",
+ "modified_by": "Administrator",
+ "module": "ERPNext Integrations",
+ "name": "Shopify Webhook Detail",
+ "name_case": "",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "read_only": 0,
+ "read_only_onload": 0,
+ "show_name_in_global_search": 0,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1,
+ "track_seen": 0
+}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/shopify_webhook_detail/shopify_webhook_detail.py b/erpnext/erpnext_integrations/doctype/shopify_webhook_detail/shopify_webhook_detail.py
new file mode 100644
index 0000000..e127989
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/shopify_webhook_detail/shopify_webhook_detail.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.model.document import Document
+
+class ShopifyWebhookDetail(Document):
+ pass
diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py
new file mode 100644
index 0000000..2c03686
--- /dev/null
+++ b/erpnext/erpnext_integrations/utils.py
@@ -0,0 +1,42 @@
+import frappe
+from frappe import _
+import base64, hashlib, hmac
+from six.moves.urllib.parse import urlparse
+
+def validate_webhooks_request(doctype, hmac_key, secret_key='secret'):
+ def innerfn(fn):
+ settings = frappe.get_doc(doctype)
+
+ if frappe.request and settings and settings.get(secret_key) and not frappe.flags.in_test:
+ sig = base64.b64encode(
+ hmac.new(
+ settings.get(secret_key).encode('utf8'),
+ frappe.request.data,
+ hashlib.sha256
+ ).digest()
+ )
+
+ if frappe.request.data and \
+ frappe.get_request_header(hmac_key) and \
+ not sig == bytes(frappe.get_request_header(hmac_key).encode()):
+ frappe.throw(_("Unverified Webhook Data"))
+ frappe.set_user(settings.modified_by)
+
+ return fn
+
+ return innerfn
+
+def get_webhook_address(connector_name, method, exclude_uri=False):
+ endpoint = "erpnext.erpnext_integrations.connectors.{0}.{1}".format(connector_name, method)
+
+ if exclude_uri:
+ return endpoint
+
+ try:
+ url = frappe.request.url
+ except RuntimeError:
+ url = "http://localhost:8000"
+
+ server_url = '{uri.scheme}://{uri.netloc}/api/method/{endpoint}'.format(uri=urlparse(url), endpoint=endpoint)
+
+ return server_url
\ No newline at end of file
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index a8c6cea..f1d3677 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -540,3 +540,4 @@
erpnext.patches.v11_0.make_location_from_warehouse
erpnext.patches.v11_0.make_asset_finance_book_against_old_entries
erpnext.patches.v11_0.check_buying_selling_in_currency_exchange
+erpnext.patches.v11_0.refactor_erpnext_shopify
diff --git a/erpnext/patches/v11_0/refactor_erpnext_shopify.py b/erpnext/patches/v11_0/refactor_erpnext_shopify.py
new file mode 100644
index 0000000..c8d4de8
--- /dev/null
+++ b/erpnext/patches/v11_0/refactor_erpnext_shopify.py
@@ -0,0 +1,28 @@
+from __future__ import unicode_literals
+import frappe
+from frappe.installer import remove_from_installed_apps
+
+def execute():
+ frappe.reload_doc('erpnext_integrations', 'doctype', 'shopify_settings')
+ frappe.reload_doc('erpnext_integrations', 'doctype', 'shopify_tax_account')
+ frappe.reload_doc('erpnext_integrations', 'doctype', 'shopify_log')
+
+ if 'erpnext_shopify' in frappe.get_installed_apps():
+ remove_from_installed_apps('erpnext_shopify')
+
+ frappe.db.sql('delete from `tabDesktop Icon` where app="erpnext_shopify" ')
+ frappe.delete_doc("Module Def", 'erpnext_shopify')
+
+ frappe.db.commit()
+
+ frappe.db.sql("truncate `tabShopify Log`")
+
+ setup_app_type()
+
+def setup_app_type():
+ shopify_settings = frappe.get_doc("Shopify Settings")
+ shopify_settings.app_type = 'Private'
+ shopify_settings.update_price_in_erpnext_price_list = 0 if shopify_settings.push_prices_to_shopify else 1
+ shopify_settings.flags.ignore_mandatory = True
+ shopify_settings.ignore_permissions = True
+ shopify_settings.save()