diff --git a/shopify_integration/payouts.py b/shopify_integration/payouts.py index d41fd5a..1380a14 100644 --- a/shopify_integration/payouts.py +++ b/shopify_integration/payouts.py @@ -10,14 +10,22 @@ from shopify_integration.fulfilments import create_shopify_delivery from shopify_integration.invoices import create_shopify_invoice from shopify_integration.orders import create_shopify_order -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 if TYPE_CHECKING: - from erpnext.selling.doctype.sales_order.sales_order import SalesOrder from shopify import Order, Payouts, Transactions - from shopify_integration.shopify_integration.doctype.shopify_settings.shopify_settings import ShopifySettings - from shopify_integration.shopify_integration.doctype.shopify_payout.shopify_payout import ShopifyPayout + + from erpnext.selling.doctype.sales_order.sales_order import SalesOrder + + from shopify_integration.shopify_integration.doctype.shopify_settings.shopify_settings import ( + ShopifySettings, + ) + from shopify_integration.shopify_integration.doctype.shopify_payout.shopify_payout import ( + ShopifyPayout, + ) def sync_all_payouts(): @@ -34,10 +42,10 @@ def create_shopify_payouts(shop_name: str, start_date: str = str()): """ Pull the latest payouts from Shopify and do the following: - - Create missing Sales Orders, Sales Invoices and Delivery Notes, - if enabled in Shopify Settings - - Create a Shopify Payout document with info on all transactions - - Update any invoices with fees accrued for each payout transaction + - Create missing Sales Orders, Sales Invoices and Delivery Notes, + if enabled in Shopify Settings + - Create a Shopify Payout document with info on all transactions + - Update any invoices with fees accrued for each payout transaction Args: shop_name (str): The name of the Shopify configuration for the store. @@ -45,8 +53,7 @@ def create_shopify_payouts(shop_name: str, start_date: str = str()): """ shopify_settings: "ShopifySettings" = frappe.get_doc("Shopify Settings", shop_name) - - payouts = get_payouts(shopify_settings, start_date) + payouts: List["Payouts"] = get_payouts(shopify_settings, start_date) if not payouts: return @@ -54,29 +61,47 @@ def create_shopify_payouts(shop_name: str, start_date: str = str()): if frappe.db.exists("Shopify Payout", {"payout_id": payout.id}): continue - payout_order_ids = [] - try: - payout_transactions: List["Transactions"] = shopify_settings.get_payout_transactions( - payout_id=payout.id - ) - except Exception as e: - make_shopify_log( - status="Payout Transactions Error", - response_data=payout.to_dict(), - exception=e - ) - else: - payout_order_ids = [transaction.source_order_id for transaction in payout_transactions - if transaction.source_order_id] - - create_missing_orders(shopify_settings, payout_order_ids) - payout_doc: "ShopifyPayout" = create_shopify_payout(shopify_settings, payout) - payout_doc.update_invoice_fees() + frappe.enqueue( + method="shopify_integration.payouts.create_shopify_payout", + queue="long", + timeout=3600, + is_async=True, + **{"shop_name": shop_name, "payout_id": payout.id}, + ) shopify_settings.last_sync_datetime = now() shopify_settings.save() +def create_shopify_payout(shop_name: str, payout_id: str): + shopify_settings: "ShopifySettings" = frappe.get_doc("Shopify Settings", shop_name) + payouts: List["Payouts"] = shopify_settings.get_payouts(payout_id) + payout = payouts[0] + + payout_order_ids = [] + try: + payout_transactions: List[ + "Transactions" + ] = shopify_settings.get_payout_transactions(payout_id=payout_id) + except Exception as e: + make_shopify_log( + shop_name=shop_name, + status="Payout Transactions Error", + response_data=payout.to_dict(), + exception=e, + ) + else: + payout_order_ids = [ + transaction.source_order_id + for transaction in payout_transactions + if transaction.source_order_id + ] + + create_missing_orders(shopify_settings, payout_order_ids) + payout_doc: "ShopifyPayout" = _create_shopify_payout(shopify_settings, payout) + payout_doc.update_invoice_fees() + + def get_payouts(shopify_settings: "ShopifySettings", start_date: str = str()): """ Request Shopify API for the latest payouts @@ -91,23 +116,27 @@ def get_payouts(shopify_settings: "ShopifySettings", start_date: str = str()): kwargs = {} if start_date: - kwargs['date_min'] = start_date + kwargs["date_min"] = start_date elif shopify_settings.last_sync_datetime: - kwargs['date_min'] = shopify_settings.last_sync_datetime + kwargs["date_min"] = shopify_settings.last_sync_datetime else: # default to first day of current month for first sync - kwargs['date_min'] = get_datetime_str(get_first_day(today())) + kwargs["date_min"] = get_datetime_str(get_first_day(today())) try: payouts = shopify_settings.get_payouts(**kwargs) except Exception as e: - make_shopify_log(status="Payout Error", exception=e, rollback=True) + make_shopify_log( + shopify_settings.name, status="Payout Error", exception=e, rollback=True + ) return [] else: return payouts -def create_missing_orders(shopify_settings: "ShopifySettings", shopify_order_ids: List[str]): +def create_missing_orders( + shopify_settings: "ShopifySettings", shopify_order_ids: List[str] +): """ Create missing Sales Orders, Sales Invoices and Delivery Notes, if enabled in Shopify Settings. @@ -117,21 +146,30 @@ def create_missing_orders(shopify_settings: "ShopifySettings", shopify_order_ids """ for shopify_order_id in shopify_order_ids: - sales_order = get_shopify_document(shop_name=shopify_settings.name, - doctype="Sales Order", order_id=shopify_order_id) - sales_invoice = get_shopify_document(shop_name=shopify_settings.name, - doctype="Sales Invoice", order_id=shopify_order_id) - delivery_note = get_shopify_document(shop_name=shopify_settings.name, - doctype="Delivery Note", order_id=shopify_order_id) + sales_order = get_shopify_document( + shop_name=shopify_settings.name, + doctype="Sales Order", + order_id=shopify_order_id, + ) + sales_invoice = get_shopify_document( + shop_name=shopify_settings.name, + doctype="Sales Invoice", + order_id=shopify_order_id, + ) + delivery_note = get_shopify_document( + shop_name=shopify_settings.name, + doctype="Delivery Note", + order_id=shopify_order_id, + ) if all([sales_order, sales_invoice, delivery_note]): continue - orders = shopify_settings.get_orders(shopify_order_id) + orders: List["Order"] = shopify_settings.get_orders(shopify_order_id) if not orders: continue - order: "Order" = orders[0] + order = orders[0] # create an order, invoice and delivery, if missing if not sales_order: @@ -146,7 +184,7 @@ def create_missing_orders(shopify_settings: "ShopifySettings", shopify_order_ids create_shopify_delivery(shopify_settings.name, order, sales_order) -def create_shopify_payout(shopify_settings: "ShopifySettings", payout: "Payouts"): +def _create_shopify_payout(shopify_settings: "ShopifySettings", payout: "Payouts"): """ Create a Shopify Payout document from Shopify's Payout information. @@ -159,22 +197,31 @@ def create_shopify_payout(shopify_settings: "ShopifySettings", payout: "Payouts" """ payout_doc: "ShopifyPayout" = frappe.new_doc("Shopify Payout") - payout_doc.update({ - "shop_name": shopify_settings.name, - "company": shopify_settings.company, - "payout_id": payout.id, - "payout_date": getdate(payout.date), - "status": frappe.unscrub(payout.status), - "amount": flt(payout.amount), - "currency": payout.currency, - **payout.summary.to_dict() # unpack the payout amounts and fees from the summary - }) + payout_doc.update( + { + "shop_name": shopify_settings.name, + "company": shopify_settings.company, + "payout_id": payout.id, + "payout_date": getdate(payout.date), + "status": frappe.unscrub(payout.status), + "amount": flt(payout.amount), + "currency": payout.currency, + **payout.summary.to_dict(), # unpack the payout amounts and fees from the summary + } + ) try: - payout_transactions: List["Transactions"] = shopify_settings.get_payout_transactions(payout_id=payout.id) + payout_transactions: List[ + "Transactions" + ] = shopify_settings.get_payout_transactions(payout_id=payout.id) except Exception as e: payout_doc.save(ignore_permissions=True) - make_shopify_log(status="Payout Transactions Error", response_data=payout.to_dict(), exception=e) + make_shopify_log( + shopify_settings.name, + status="Payout Transactions Error", + response_data=payout.to_dict(), + exception=e, + ) return payout_doc payout_doc.set("transactions", []) @@ -183,39 +230,61 @@ def create_shopify_payout(shopify_settings: "ShopifySettings", payout: "Payouts" order_financial_status = sales_order = sales_invoice = delivery_note = None if shopify_order_id: - orders = shopify_settings.get_orders(shopify_order_id) + orders = shopify_settings.get_orders( + shopify_order_id, fields="financial_status" + ) if not orders: continue order = orders[0] order_financial_status = frappe.unscrub(order.financial_status) - sales_order = get_shopify_document(shop_name=shopify_settings.name, - doctype="Sales Order", order_id=shopify_order_id) - sales_invoice = get_shopify_document(shop_name=shopify_settings.name, - doctype="Sales Invoice", order_id=shopify_order_id) - delivery_note = get_shopify_document(shop_name=shopify_settings.name, - doctype="Delivery Note", order_id=shopify_order_id) - - total_amount = -flt(transaction.amount) if transaction.type == "payout" else flt(transaction.amount) - net_amount = -flt(transaction.net) if transaction.type == "payout" else flt(transaction.net) - - payout_doc.append("transactions", { - "transaction_id": transaction.id, - "transaction_type": frappe.unscrub(transaction.type), - "processed_at": getdate(transaction.processed_at), - "total_amount": total_amount, - "fee": flt(transaction.fee), - "net_amount": net_amount, - "currency": transaction.currency, - "sales_order": sales_order.name if sales_order else None, - "sales_invoice": sales_invoice.name if sales_invoice else None, - "delivery_note": delivery_note.name if delivery_note else None, - "source_id": transaction.source_id, - "source_type": frappe.unscrub(transaction.source_type or ""), - "source_order_financial_status": order_financial_status, - "source_order_id": shopify_order_id, - "source_order_transaction_id": transaction.source_order_transaction_id, - }) + sales_order = get_shopify_document( + shop_name=shopify_settings.name, + doctype="Sales Order", + order_id=shopify_order_id, + ) + sales_invoice = get_shopify_document( + shop_name=shopify_settings.name, + doctype="Sales Invoice", + order_id=shopify_order_id, + ) + delivery_note = get_shopify_document( + shop_name=shopify_settings.name, + doctype="Delivery Note", + order_id=shopify_order_id, + ) + + total_amount = ( + -flt(transaction.amount) + if transaction.type == "payout" + else flt(transaction.amount) + ) + net_amount = ( + -flt(transaction.net) + if transaction.type == "payout" + else flt(transaction.net) + ) + + payout_doc.append( + "transactions", + { + "transaction_id": transaction.id, + "transaction_type": frappe.unscrub(transaction.type), + "processed_at": getdate(transaction.processed_at), + "total_amount": total_amount, + "fee": flt(transaction.fee), + "net_amount": net_amount, + "currency": transaction.currency, + "sales_order": sales_order.name if sales_order else None, + "sales_invoice": sales_invoice.name if sales_invoice else None, + "delivery_note": delivery_note.name if delivery_note else None, + "source_id": transaction.source_id, + "source_type": frappe.unscrub(transaction.source_type or ""), + "source_order_financial_status": order_financial_status, + "source_order_id": shopify_order_id, + "source_order_transaction_id": transaction.source_order_transaction_id, + }, + ) payout_doc.save(ignore_permissions=True) frappe.db.commit() diff --git a/shopify_integration/products.py b/shopify_integration/products.py index 24b3a12..45bb021 100644 --- a/shopify_integration/products.py +++ b/shopify_integration/products.py @@ -3,17 +3,22 @@ from shopify import Product, Variant import frappe -from erpnext import get_default_company from frappe import _ from frappe.utils import cint, cstr -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, +) if TYPE_CHECKING: + from shopify import LineItem, Option, Order + from erpnext.stock.doctype.item.item import Item from erpnext.stock.doctype.item_attribute.item_attribute import ItemAttribute - from shopify import LineItem, Option, 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, + ) SHOPIFY_VARIANTS_ATTR_LIST = ["option1", "option2", "option3"] @@ -126,16 +131,11 @@ def make_item( if isinstance(shopify_item, Product): attributes = create_product_attributes(shopify_item) - sync_item( - shopify_settings=shopify_settings, - shopify_item=shopify_item, - attributes=attributes - ) - sync_item_variants( - shopify_settings=shopify_settings, - shopify_item=shopify_item, - attributes=attributes - ) + if attributes: + sync_item(shopify_settings, shopify_item, attributes) + sync_item_variants(shopify_settings, shopify_item, attributes) + else: + sync_item(shopify_settings, shopify_item) def make_item_by_title(shopify_settings: "ShopifySettings", line_item_title: str): @@ -148,11 +148,11 @@ def make_item_by_title(shopify_settings: "ShopifySettings", line_item_title: str "shopify_description": line_item_title, "item_group": shopify_settings.item_group, "stock_uom": frappe.db.get_single_value("Stock Settings", "stock_uom"), - "default_warehouse": shopify_settings.warehouse, "integration_doctype": "Shopify Settings", "integration_doc": shopify_settings.name, "item_defaults": [{ - "company": get_default_company() + "company": shopify_settings.company, + "default_warehouse": shopify_settings.warehouse, }] } @@ -205,9 +205,12 @@ def has_variants(shopify_item: Product): def update_item_attribute_values(item_attr: "ItemAttribute", values: List[str]): - existing_attribute_values = [ - attr.attribute_value.lower() for attr in item_attr.item_attribute_values - ] + if item_attr.is_new(): + existing_attribute_values = [] + else: + existing_attribute_values = [ + attr.attribute_value.lower() for attr in item_attr.item_attribute_values + ] for attr_value in values: if attr_value.lower() not in existing_attribute_values: @@ -239,6 +242,9 @@ def sync_item( Defaults to False. """ + if not attributes: + attributes = [] + item_title = shopify_item.attributes.get("title", "").strip() item_description = shopify_item.attributes.get("body_html") or item_title item_has_variants = has_variants(shopify_item) @@ -279,12 +285,16 @@ def sync_item( "stock_uom": WEIGHT_UOM_MAP.get(shopify_item.attributes.get("uom")) or frappe.db.get_single_value("Stock Settings", "stock_uom"), "shopify_sku": shopify_item.attributes.get("sku"), - "default_warehouse": shopify_settings.warehouse, "weight_uom": WEIGHT_UOM_MAP.get(shopify_item.attributes.get("weight_unit")), "weight_per_unit": shopify_item.attributes.get("weight"), "integration_doctype": "Shopify Settings", "integration_doc": shopify_settings.name, - "item_defaults": [{"company": get_default_company()}], + "item_defaults": [ + { + "company": shopify_settings.company, + "default_warehouse": shopify_settings.warehouse, + } + ], } if not is_item_exists(item_data, attributes, variant_of=variant_of): @@ -378,48 +388,50 @@ def sync_item_variants( if not product_id: return - template_item = frappe.db.get_value("Item", + template_item = frappe.db.get_value( + "Item", filters={"shopify_product_id": product_id}, fieldname=["name", "stock_uom"], - as_dict=True) + as_dict=True, + ) if template_item: + variant: Variant for variant in shopify_item.attributes.get("variants", []): - variant: Variant - for index, variant_attr in enumerate(SHOPIFY_VARIANTS_ATTR_LIST): - if variant.attributes.get(variant_attr): - attributes[index].update({ - "attribute_value": get_attribute_value( - variant.attributes.get(variant_attr), - attributes[index] - ) - }) + if index < len(attributes) and variant.attributes.get(variant_attr): + attributes[index].update( + { + "attribute_value": get_attribute_value( + variant.attributes.get(variant_attr), + attributes[index], + ) + } + ) sync_item( shopify_settings=shopify_settings, shopify_item=variant, attributes=attributes, - variant_of=template_item.name + variant_of=template_item.name, ) -def get_attribute_value(variant_attr_val: str, attribute: Dict): - 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 +def get_attribute_value(variant_attr_val: str, attribute: Dict = None): + if not attribute or not attribute.get("attribute"): + return cint(variant_attr_val) + + attribute_values = frappe.get_all( + "Item Attribute Value", + filters={"parent": attribute.get("attribute")}, + or_filters={"abbr": variant_attr_val, "attribute_value": variant_attr_val}, + pluck="attribute_value", ) - return attribute_value[0][0] if len(attribute_value) > 0 else cint(variant_attr_val) + if not attribute_values: + return cint(variant_attr_val) + + return attribute_values[0] def get_item_group(product_type: str = str()): diff --git a/shopify_integration/shopify_integration/doctype/shopify_payout/shopify_payout.py b/shopify_integration/shopify_integration/doctype/shopify_payout/shopify_payout.py index 263bd14..e2d5e51 100644 --- a/shopify_integration/shopify_integration/doctype/shopify_payout/shopify_payout.py +++ b/shopify_integration/shopify_integration/doctype/shopify_payout/shopify_payout.py @@ -71,7 +71,10 @@ def update_cancelled_shopify_orders(self): if not transaction.source_order_id: continue - shopify_orders = settings.get_orders(cint(transaction.source_order_id)) + shopify_orders = settings.get_orders( + cint(transaction.source_order_id), fields="cancelled_at" + ) + if not shopify_orders: continue