diff --git a/erpnext/config/erpnext_integrations.py b/erpnext/config/erpnext_integrations.py
index 88c47791870..e27b7cd04f9 100644
--- a/erpnext/config/erpnext_integrations.py
+++ b/erpnext/config/erpnext_integrations.py
@@ -25,6 +25,11 @@ def get_data():
{
"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 00000000000..3492eaa0635
Binary files /dev/null and b/erpnext/docs/assets/img/erpnext_integrations/app_details.png 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 00000000000..12ff52d2495
Binary files /dev/null and b/erpnext/docs/assets/img/erpnext_integrations/erp_configurations.png 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 00000000000..62fbc734db2
Binary files /dev/null and b/erpnext/docs/assets/img/erpnext_integrations/manage_private_apps.png 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 00000000000..d69d0f7d09a
Binary files /dev/null and b/erpnext/docs/assets/img/erpnext_integrations/menu_bar.png 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 00000000000..a6611c94420
Binary files /dev/null and b/erpnext/docs/assets/img/erpnext_integrations/sync_config.png 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 00000000000..3c6cc55ab87
Binary files /dev/null and b/erpnext/docs/assets/img/erpnext_integrations/tax_config.png 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 00000000000..391bf2b71c8
--- /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
+
+
+2. Click on **Manage private apps** to create private app
+
+
+3. Fill up the details and create app. The each app has its own API key, Password and Shared secret
+
+
+
+#### 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.
+
+
+2. Setup Customer, Company and Inventory configurations
+
+
+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.
+
+
+4. Setup Tax Mapper.
+ Prepare tax and shipping charges mapper for each tax and shipping charge you apply in Shopify
+
+
+
+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 00000000000..88078ab74f6
--- /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 00000000000..e69de29bb2d
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 00000000000..d3fe7d2b4d6
--- /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 00000000000..ab373eedb4e
--- /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 00000000000..e6e6579084c
--- /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 00000000000..0913ce4ef3c
--- /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 00000000000..d22b6d52402
--- /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 00000000000..5892e1d6c4e
--- /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 00000000000..e69de29bb2d
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 00000000000..1574795dfad
--- /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 00000000000..cf1c0adc112
--- /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 00000000000..fd4f4986b25
--- /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 00000000000..02e1fc9a692
--- /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 00000000000..55f47a600e3
--- /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 00000000000..e91ce9abf81
--- /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 00000000000..296dede7868
--- /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": "