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
This commit is contained in:
Saurabh
2018-05-16 11:33:47 +05:30
committed by Rushabh Mehta
parent ad08d4ce96
commit d60c0f2292
36 changed files with 3443 additions and 0 deletions

View File

@@ -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

View File

@@ -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');
}
}
});

View File

@@ -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
}

View File

@@ -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})

View File

@@ -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"];
}
}
}

View File

@@ -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()
]);
});

View File

@@ -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

View File

@@ -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
}
}
}
}
})

File diff suppressed because it is too large Load Diff

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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
}
]
}
}

View File

@@ -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
]
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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()
]);
});

View File

@@ -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")))

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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