354 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			354 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from __future__ import unicode_literals
 | |
| import frappe
 | |
| from frappe import _
 | |
| import json
 | |
| from frappe.utils import cstr, cint, nowdate, getdate, flt, get_request_session, get_datetime
 | |
| 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
 | |
| from erpnext.erpnext_integrations.doctype.shopify_settings.shopify_settings import get_shopify_url, get_header
 | |
| 
 | |
| @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, old_order_sync=False):
 | |
| 	frappe.set_user('Administrator')
 | |
| 	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, old_order_sync=old_order_sync)
 | |
| 		except Exception as e:
 | |
| 			make_shopify_log(status="Error", exception=e)
 | |
| 
 | |
| 		else:
 | |
| 			make_shopify_log(status="Success")
 | |
| 
 | |
| def prepare_sales_invoice(order, request_id=None):
 | |
| 	frappe.set_user('Administrator')
 | |
| 	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 as e:
 | |
| 		make_shopify_log(status="Error", exception=e, rollback=True)
 | |
| 
 | |
| def prepare_delivery_note(order, request_id=None):
 | |
| 	frappe.set_user('Administrator')
 | |
| 	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 as e:
 | |
| 		make_shopify_log(status="Error", exception=e, rollback=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, old_order_sync=False, 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, old_order_sync=old_order_sync)
 | |
| 
 | |
| 		if order.get("fulfillments") and not old_order_sync:
 | |
| 			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, getdate(shopify_order.get('created_at')))
 | |
| 
 | |
| 		if not items:
 | |
| 			message = 'Following items exists in the shopify order but relevant records were not found in the shopify Product master'
 | |
| 			message += "\n" + ", ".join(product_not_exists)
 | |
| 
 | |
| 			make_shopify_log(status="Error", exception=message, rollback=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"),
 | |
| 			"shopify_order_number": shopify_order.get("name"),
 | |
| 			"customer": customer or shopify_settings.default_customer,
 | |
| 			"transaction_date": getdate(shopify_order.get("created_at")) or nowdate(),
 | |
| 			"delivery_date": getdate(shopify_order.get("created_at")) or 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, old_order_sync=False):
 | |
| 	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):
 | |
| 
 | |
| 		if old_order_sync:
 | |
| 			posting_date = getdate(shopify_order.get('created_at'))
 | |
| 		else:
 | |
| 			posting_date = nowdate()
 | |
| 
 | |
| 		si = make_sales_invoice(so.name, ignore_permissions=True)
 | |
| 		si.shopify_order_id = shopify_order.get("id")
 | |
| 		si.shopify_order_number = shopify_order.get("name")
 | |
| 		si.set_posting_time = 1
 | |
| 		si.posting_date = posting_date
 | |
| 		si.due_date = posting_date
 | |
| 		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.insert(ignore_mandatory=True)
 | |
| 		si.submit()
 | |
| 		make_payament_entry_against_sales_invoice(si, shopify_settings, posting_date)
 | |
| 		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, posting_date=None):
 | |
| 	from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
 | |
| 	payment_entry = get_payment_entry(doc.doctype, doc.name, bank_account=shopify_settings.cash_bank_account)
 | |
| 	payment_entry.flags.ignore_mandatory = True
 | |
| 	payment_entry.reference_no = doc.name
 | |
| 	payment_entry.posting_date = posting_date or nowdate()
 | |
| 	payment_entry.reference_date = posting_date or nowdate()
 | |
| 	payment_entry.insert(ignore_permissions=True)
 | |
| 	payment_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_order_number = shopify_order.get("name")
 | |
| 			dn.set_posting_time = 1
 | |
| 			dn.posting_date = getdate(fulfillment.get("created_at"))
 | |
| 			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()
 | |
| 			dn.submit()
 | |
| 			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, delivery_date):
 | |
| 	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": delivery_date,
 | |
| 				"qty": shopify_item.get("quantity"),
 | |
| 				"stock_uom": shopify_item.get("uom") or _("Nos"),
 | |
| 				"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):
 | |
| 	"""Shipping lines represents the shipping details,
 | |
| 		each such shipping detail consists of a list of tax_lines"""
 | |
| 	for shipping_charge in shipping_lines:
 | |
| 		if shipping_charge.get("price"):
 | |
| 			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
 | |
| 			})
 | |
| 
 | |
| 		for tax in shipping_charge.get("tax_lines"):
 | |
| 			taxes.append({
 | |
| 				"charge_type": _("Actual"),
 | |
| 				"account_head": get_tax_account_head(tax),
 | |
| 				"description": tax["title"],
 | |
| 				"tax_amount": tax["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
 | |
| 
 | |
| @frappe.whitelist(allow_guest=True)
 | |
| def sync_old_orders():
 | |
| 	frappe.set_user('Administrator')
 | |
| 	shopify_settings = frappe.get_doc('Shopify Settings')
 | |
| 
 | |
| 	if not shopify_settings.sync_missing_orders:
 | |
| 		return
 | |
| 
 | |
| 	url = get_url(shopify_settings)
 | |
| 	session = get_request_session()
 | |
| 
 | |
| 	try:
 | |
| 		res = session.get(url, headers=get_header(shopify_settings))
 | |
| 		res.raise_for_status()
 | |
| 		orders = res.json()["orders"]
 | |
| 
 | |
| 		for order in orders:
 | |
| 			if is_sync_complete(shopify_settings, order):
 | |
| 				stop_sync(shopify_settings)
 | |
| 				return
 | |
| 
 | |
| 			sync_sales_order(order=order, old_order_sync=True)
 | |
| 			last_order_id = order.get('id')
 | |
| 
 | |
| 		if last_order_id:
 | |
| 			shopify_settings.load_from_db()
 | |
| 			shopify_settings.last_order_id = last_order_id
 | |
| 			shopify_settings.save()
 | |
| 			frappe.db.commit()
 | |
| 
 | |
| 	except Exception as e:
 | |
| 		raise e
 | |
| 
 | |
| def stop_sync(shopify_settings):
 | |
| 	shopify_settings.sync_missing_orders = 0
 | |
| 	shopify_settings.last_order_id = ''
 | |
| 	shopify_settings.save()
 | |
| 	frappe.db.commit()
 | |
| 
 | |
| def get_url(shopify_settings):
 | |
| 	last_order_id = shopify_settings.last_order_id
 | |
| 
 | |
| 	if not last_order_id:
 | |
| 		if shopify_settings.sync_based_on == 'Date':
 | |
| 			url = get_shopify_url("admin/api/2021-04/orders.json?limit=250&created_at_min={0}&since_id=0".format(
 | |
| 				get_datetime(shopify_settings.from_date)), shopify_settings)
 | |
| 		else:
 | |
| 			url = get_shopify_url("admin/api/2021-04/orders.json?limit=250&since_id={0}".format(
 | |
| 				shopify_settings.from_order_id), shopify_settings)
 | |
| 	else:
 | |
| 		url = get_shopify_url("admin/api/2021-04/orders.json?limit=250&since_id={0}".format(last_order_id), shopify_settings)
 | |
| 
 | |
| 	return url
 | |
| 
 | |
| def is_sync_complete(shopify_settings, order):
 | |
| 	if shopify_settings.sync_based_on == 'Date':
 | |
| 		return getdate(shopify_settings.to_date) < getdate(order.get('created_at'))
 | |
| 	else:
 | |
| 		return cstr(order.get('id')) == cstr(shopify_settings.to_order_id)
 | |
| 
 |