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

@ -25,6 +25,11 @@ def get_data():
{
"type": "doctype",
"name": "Woocommerce Settings"
},
{
"type": "doctype",
"name": "Shopify Settings",
"description": _("Connect Shopify with ERPNext"),
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -0,0 +1,42 @@
# Shopify Integration
The Shopify Connector pulls the orders from Shopify and creates Sales Order against them in ERPNext.
While creating the sales order if Customer or Item is missing in ERPNext the system will create new Customer/Item by pulling respective details from Shopify.
### How to Setup Connector?
#### Create A Private App in Shopify
1. Click on Apps in menu bar
<img class="screenshot" alt="Menu Section" src="{{docs_base_url}}/assets/img/erpnext_integrations/app_menu.png">
2. Click on **Manage private apps** to create private app
<img class="screenshot" alt="Manage Private Apps" src="{{docs_base_url}}/assets/img/erpnext_integrations/manage_private_apps.png">
3. Fill up the details and create app. The each app has its own API key, Password and Shared secret
<img class="screenshot" alt="App Details" src="{{docs_base_url}}/assets/img/erpnext_integrations/app_details.png">
#### Setting Up Shopify on ERPNext:-
Once you have created a Private App on Shopify, setup App Credentials and other details in ERPNext.
1. Select App Type as Private and Fill-up API key, Password and Shared Secret from Shopify's Private App.
<img class="screenshot" alt="Setup Private App Credentials" src="{{docs_base_url}}/assets/img/erpnext_integrations/app_details.png">
2. Setup Customer, Company and Inventory configurations
<img class="screenshot" alt="ERP Configurations" src="{{docs_base_url}}/assets/img/erpnext_integrations/erp_configurations.png">
3. Setup Sync Configurations.
The system pulls Orders from Shopify and creates Sales Order in ERPNext. You can configure ERPNext system to capture payment and fulfilments against orders.
<img class="screenshot" alt="Sync Configure" src="{{docs_base_url}}/assets/img/erpnext_integrations/sync_config.png">
4. Setup Tax Mapper.
Prepare tax and shipping charges mapper for each tax and shipping charge you apply in Shopify
<img class="screenshot" alt="Taxes and Shipping Charges" src="{{docs_base_url}}/assets/img/erpnext_integrations/tax_config.png">
After setting up all the configurations, enable the Shopify sync and save the settings. This will register the API's to Shopify and the system will start Order sync between Shopify and ERPNext.
### Note:
The connector won't handle Order cancellation. If you cancelled any order in Shopify then manually you have to cancel respective Sales Order and other documents in ERPNext.

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

View File

@ -540,3 +540,4 @@ erpnext.patches.v11_0.create_department_records_for_each_company
erpnext.patches.v11_0.make_location_from_warehouse
erpnext.patches.v11_0.make_asset_finance_book_against_old_entries
erpnext.patches.v11_0.check_buying_selling_in_currency_exchange
erpnext.patches.v11_0.refactor_erpnext_shopify

View File

@ -0,0 +1,28 @@
from __future__ import unicode_literals
import frappe
from frappe.installer import remove_from_installed_apps
def execute():
frappe.reload_doc('erpnext_integrations', 'doctype', 'shopify_settings')
frappe.reload_doc('erpnext_integrations', 'doctype', 'shopify_tax_account')
frappe.reload_doc('erpnext_integrations', 'doctype', 'shopify_log')
if 'erpnext_shopify' in frappe.get_installed_apps():
remove_from_installed_apps('erpnext_shopify')
frappe.db.sql('delete from `tabDesktop Icon` where app="erpnext_shopify" ')
frappe.delete_doc("Module Def", 'erpnext_shopify')
frappe.db.commit()
frappe.db.sql("truncate `tabShopify Log`")
setup_app_type()
def setup_app_type():
shopify_settings = frappe.get_doc("Shopify Settings")
shopify_settings.app_type = 'Private'
shopify_settings.update_price_in_erpnext_price_list = 0 if shopify_settings.push_prices_to_shopify else 1
shopify_settings.flags.ignore_mandatory = True
shopify_settings.ignore_permissions = True
shopify_settings.save()