Skip to content

Commit

Permalink
feat: add webhook for order edit events [CU-866966341] (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
Alchez authored Jan 25, 2023
1 parent 8061f5b commit 608a2d8
Show file tree
Hide file tree
Showing 5 changed files with 287 additions and 133 deletions.
262 changes: 187 additions & 75 deletions shopify_integration/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,22 @@
import frappe
from frappe.utils import flt, getdate, nowdate

from shopify_integration.shopify_integration.doctype.shopify_log.shopify_log import make_shopify_log
from shopify_integration.shopify_integration.doctype.shopify_log.shopify_log import (
make_shopify_log,
)
from shopify_integration.utils import get_shopify_document, get_tax_account_head

if TYPE_CHECKING:
from erpnext.selling.doctype.sales_order.sales_order import SalesOrder
from shopify import LineItem, Order
from shopify_integration.shopify_integration.doctype.shopify_settings.shopify_settings import ShopifySettings
from shopify_integration.shopify_integration.doctype.shopify_settings.shopify_settings import (
ShopifySettings,
)


def create_shopify_documents(shop_name: str, order_id: str, log_id: str = str()):
def create_shopify_documents(
shop_name: str, order_id: str, log_id: str = str(), amended_from: str = str()
):
"""
Create the following from a Shopify order:
Expand All @@ -26,6 +32,7 @@ def create_shopify_documents(shop_name: str, order_id: str, log_id: str = str())
order_id (str): The Shopify order ID.
log_id (str, optional): The ID of an existing Shopify Log. Defaults
to an empty string.
amended_from (str, optional): The name of the original cancelled Sales Order.
"""

from shopify_integration.fulfilments import create_shopify_delivery
Expand All @@ -38,7 +45,7 @@ def create_shopify_documents(shop_name: str, order_id: str, log_id: str = str())
if not order:
return

sales_order = create_shopify_order(shop_name, order, log_id)
sales_order = create_shopify_order(shop_name, order, log_id, amended_from)
if sales_order:
create_shopify_invoice(shop_name, order, sales_order, log_id)
create_shopify_delivery(shop_name, order, sales_order, log_id)
Expand All @@ -62,7 +69,12 @@ def get_shopify_order(shop_name: str, order_id: str, log_id: str = str()):
return order


def create_shopify_order(shop_name: str, shopify_order: "Order", log_id: str = str()):
def create_shopify_order(
shop_name: str,
shopify_order: "Order",
log_id: str = str(),
amended_from: str = str(),
):
"""
Create a Sales Order document for a Shopify order.
Expand All @@ -71,6 +83,7 @@ def create_shopify_order(shop_name: str, shopify_order: "Order", log_id: str = s
shopify_order (Order): The Shopify order data.
log_id (str, optional): The ID of an existing Shopify Log. Defaults
to an empty string.
amended_from (str, optional): The name of the original cancelled Sales Order.
Returns:
SalesOrder: The created Sales Order document, if any, otherwise None.
Expand All @@ -81,56 +94,103 @@ def create_shopify_order(shop_name: str, shopify_order: "Order", log_id: str = s

frappe.flags.log_id = log_id

existing_so = get_shopify_document(shop_name=shop_name, doctype="Sales Order", order=shopify_order)
if existing_so:
if existing_so := get_shopify_document(
shop_name=shop_name, doctype="Sales Order", order=shopify_order
):
existing_so: "SalesOrder"
make_shopify_log(shop_name, status="Skipped", response_data=shopify_order.to_dict())
make_shopify_log(
shop_name, status="Skipped", response_data=shopify_order.to_dict()
)
return existing_so

try:
validate_customer(shop_name, shopify_order)
validate_items(shop_name, shopify_order)
sales_order = create_sales_order(shop_name, shopify_order)
sales_order = create_sales_order(
shop_name, shopify_order, amended_from=amended_from
)
except Exception as e:
make_shopify_log(shop_name, status="Error", response_data=shopify_order.to_dict(), exception=e)
make_shopify_log(
shop_name,
status="Error",
response_data=shopify_order.to_dict(),
exception=e,
)
else:
make_shopify_log(shop_name, status="Success", response_data=shopify_order.to_dict())
make_shopify_log(
shop_name, status="Success", response_data=shopify_order.to_dict()
)
return sales_order


def create_sales_order(shop_name: str, shopify_order: "Order"):
def update_shopify_order(shop_name: str, order_id: str, log_id: str = str()):
"""
Webhook endpoint to process changes in a Shopify order.
Instead of updating the existing documents, cancel them and create a new series of
sales documents for the Shopify order.
Args:
shop_name (str): The name of the Shopify configuration for the store.
order_id (str): The Shopify order ID.
log_id (str, optional): The ID of an existing Shopify Log. Defaults
to an empty string.
"""

if existing_so := get_shopify_document(
shop_name=shop_name, doctype="Sales Order", order_id=order_id
):
cancel_shopify_order(shop_name, order_id, log_id)
create_shopify_documents(
shop_name, order_id, log_id, amended_from=existing_so.name
)


def create_sales_order(
shop_name: str, shopify_order: "Order", *, amended_from: str = str()
):
"""
Helper function to create a Sales Order document for a Shopify order.
Args:
shop_name (str): The name of the Shopify configuration for the store.
shopify_order (Order): The Shopify order data.
amended_from (str, optional): The name of the original cancelled Sales Order.
Returns:
SalesOrder: The created Sales Order document, if any, otherwise None.
"""

shopify_settings: "ShopifySettings" = frappe.get_doc("Shopify Settings", shop_name)
shopify_customer = shopify_order.attributes.get("customer", frappe._dict())
customer = frappe.db.get_value("Customer", {"shopify_customer_id": shopify_customer.id}, "name")

sales_order: "SalesOrder" = frappe.get_doc({
"doctype": "Sales Order",
"naming_series": shopify_settings.sales_order_series or "SO-Shopify-",
"shopify_settings": shopify_settings.name,
"shopify_order_id": shopify_order.id,
"shopify_order_number": shopify_order.attributes.get("order_number"),
"customer": customer or shopify_settings.default_customer,
"transaction_date": getdate(shopify_order.attributes.get("created_at")),
"delivery_date": getdate(shopify_order.attributes.get("created_at")),
"company": shopify_settings.company,
"selling_price_list": shopify_settings.price_list,
"ignore_pricing_rule": 1,
"items": get_order_items(shopify_order.attributes.get("line_items", []), shopify_settings),
"taxes": get_order_taxes(shopify_order, shopify_settings),
"apply_discount_on": "Grand Total",
"discount_amount": flt(shopify_order.attributes.get("total_discounts")),
})
customer = frappe.db.get_value(
"Customer", {"shopify_customer_id": shopify_customer.id}, "name"
)

sales_order: "SalesOrder" = frappe.get_doc(
{
"doctype": "Sales Order",
"naming_series": shopify_settings.sales_order_series or "SO-Shopify-",
"shopify_settings": shopify_settings.name,
"shopify_order_id": shopify_order.id,
"shopify_order_number": shopify_order.attributes.get("order_number"),
"customer": customer or shopify_settings.default_customer,
"transaction_date": getdate(shopify_order.attributes.get("created_at")),
"delivery_date": getdate(shopify_order.attributes.get("created_at")),
"company": shopify_settings.company,
"selling_price_list": shopify_settings.price_list,
"ignore_pricing_rule": 1,
"items": get_order_items(
shopify_order.attributes.get("line_items", []), shopify_settings
),
"taxes": get_order_taxes(shopify_order, shopify_settings),
"apply_discount_on": "Grand Total",
"discount_amount": flt(
shopify_order.attributes.get("current_total_discounts")
),
"amended_from": amended_from,
}
)

sales_order.flags.ignore_mandatory = True
sales_order.save(ignore_permissions=True)
Expand All @@ -142,59 +202,97 @@ def create_sales_order(shop_name: str, shopify_order: "Order"):
def get_order_items(
shopify_order_items: List["LineItem"], shopify_settings: "ShopifySettings"
):
from shopify_integration.products import get_item_code

items = []
for shopify_item in shopify_order_items:
item_code = get_item_code(shopify_item)
item_group = (
frappe.db.get_value("Item", item_code, "item_group")
or shopify_settings.item_group
)

items.append({
"item_code": item_code,
"item_name": shopify_item.attributes.get("name"),
"item_group": item_group,
"rate": shopify_item.attributes.get("price"),
"delivery_date": nowdate(),
"qty": shopify_item.attributes.get("quantity"),
"stock_uom": shopify_item.attributes.get("uom")
or frappe.db.get_single_value("Stock Settings", "stock_uom"),
"conversion_factor": 1,
"warehouse": shopify_settings.warehouse,
})
items.append(get_order_item(shopify_item, shopify_settings))
return items


def get_order_item(shopify_item: "LineItem", shopify_settings: "ShopifySettings"):
from shopify_integration.products import get_item_code

item_code = get_item_code(shopify_item)
item_group = (
frappe.db.get_value("Item", item_code, "item_group")
or shopify_settings.item_group
)

stock_uom = shopify_item.attributes.get("uom") or frappe.db.get_single_value(
"Stock Settings", "stock_uom"
)

return {
"shopify_order_item_id": str(shopify_item.id),
"item_code": item_code,
"item_name": shopify_item.attributes.get("name"),
"item_group": item_group,
"rate": flt(shopify_item.attributes.get("price")),
# TODO: if items with discounts are edited, Shopify's API doesn't have an easy
# way to get the discount amount
# "discount_amount": flt(shopify_item.attributes.get("total_discount")),
"delivery_date": nowdate(),
"qty": flt(shopify_item.attributes.get("fulfillable_quantity")),
"stock_uom": stock_uom,
"conversion_factor": 1,
"warehouse": shopify_settings.warehouse,
}


def get_order_taxes(shopify_order: "Order", shopify_settings: "ShopifySettings"):
taxes = []

# add shipping charges
for shipping in shopify_order.attributes.get("shipping_lines"):
if shipping.attributes.get("price"):
taxes.append({
"charge_type": "Actual",
"account_head": get_tax_account_head(shopify_settings.name, "shipping"),
"description": shipping.attributes.get("title"),
"tax_amount": shipping.attributes.get("price"),
"cost_center": shopify_settings.cost_center
})
taxes.append(
{
"charge_type": "Actual",
"account_head": get_tax_account_head(
shopify_settings.name, "shipping"
),
"description": shipping.attributes.get("title"),
"tax_amount": shipping.attributes.get("price"),
"cost_center": shopify_settings.cost_center,
}
)

# add additional taxes and fees
for tax in shopify_order.attributes.get("tax_lines"):
tax_description = (
f'{tax.attributes.get("title")} - {tax.attributes.get("rate") * 100.0}%'
title = tax.attributes.get("title")
if rate := tax.attributes.get("rate"):
tax_description = f"{title} - {rate * 100.0}%"
else:
tax_description = title

taxes.append(
{
"charge_type": "Actual",
"account_head": get_tax_account_head(shopify_settings.name, "tax"),
"description": tax_description,
"tax_amount": flt(tax.attributes.get("price")),
"cost_center": shopify_settings.cost_center,
"included_in_print_rate": shopify_order.attributes.get(
"taxes_included"
),
}
)

taxes.append({
"charge_type": "Actual",
"account_head": get_tax_account_head(shopify_settings.name, "tax"),
"description": tax_description,
"tax_amount": tax.attributes.get("price"),
"cost_center": shopify_settings.cost_center,
"included_in_print_rate": shopify_order.attributes.get("taxes_included")
})
# TODO: Shopify's API doesn't have a clear way to identify changes in taxes
# from orders being edited. Instead of calculating the difference, we'll
# just add a tax line for the difference
total_taxes = sum(tax.get("tax_amount") for tax in taxes)
order_taxes = flt(shopify_order.attributes.get("current_total_tax"))
difference = order_taxes - total_taxes
if difference:
taxes.append(
{
"charge_type": "Actual",
"account_head": get_tax_account_head(shopify_settings.name, "tax"),
"description": "Tax Difference from Order Edits",
"tax_amount": difference,
"cost_center": shopify_settings.cost_center,
}
)

return taxes

Expand Down Expand Up @@ -230,16 +328,30 @@ def cancel_shopify_order(shop_name: str, order_id: str, log_id: str = str()):
doc.flags.ignore_links = True
doc.cancel()
except Exception as e:
make_shopify_log(shop_name, status="Error", response_data=order.to_dict(),
exception=e, rollback=True)
make_shopify_log(
shop_name,
status="Error",
response_data=order.to_dict(),
exception=e,
rollback=True,
)

# update the financial status in all linked Shopify Payouts
payout_transactions = frappe.get_all("Shopify Payout Transaction",
payout_transactions = frappe.get_all(
"Shopify Payout Transaction",
filters={
frappe.scrub(doctype): doc.name,
"source_order_financial_status": ["!=", order.attributes.get("financial_status")]
})
"source_order_financial_status": [
"!=",
order.attributes.get("financial_status"),
],
},
)

for transaction in payout_transactions:
frappe.db.set_value("Shopify Payout Transaction", transaction.name,
"source_order_financial_status", frappe.unscrub(order.attributes.get("financial_status")))
frappe.db.set_value(
"Shopify Payout Transaction",
transaction.name,
"source_order_financial_status",
frappe.unscrub(order.attributes.get("financial_status")),
)
Loading

0 comments on commit 608a2d8

Please sign in to comment.