From e8bdaecac3250e1bf542bd32b1991924957ae9cb Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 25 Jan 2023 12:32:45 +0000 Subject: [PATCH 1/4] feat(shopify): COD - create invoice on delivery --- .../shopify_setting/shopify_setting.json | 9 ++- ecommerce_integrations/shopify/fulfillment.py | 9 +++ ecommerce_integrations/shopify/invoice.py | 69 +++++++++++++------ 3 files changed, 65 insertions(+), 22 deletions(-) diff --git a/ecommerce_integrations/shopify/doctype/shopify_setting/shopify_setting.json b/ecommerce_integrations/shopify/doctype/shopify_setting/shopify_setting.json index d6534057..1d600cf1 100644 --- a/ecommerce_integrations/shopify/doctype/shopify_setting/shopify_setting.json +++ b/ecommerce_integrations/shopify/doctype/shopify_setting/shopify_setting.json @@ -31,6 +31,7 @@ "sync_delivery_note", "delivery_note_series", "sync_sales_invoice", + "sync_cod_invoices", "sales_invoice_series", "section_break_22", "html_16", @@ -347,12 +348,18 @@ "fieldname": "sync_new_item_as_active", "fieldtype": "Check", "label": "Sync New Items as Active" + }, + { + "default": "0", + "fieldname": "sync_cod_invoices", + "fieldtype": "Check", + "label": "Import Sales Invoice from Shopify if Fulfillment is created" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-11-01 16:09:42.685577", + "modified": "2023-01-25 17:59:13.943796", "modified_by": "Administrator", "module": "shopify", "name": "Shopify Setting", diff --git a/ecommerce_integrations/shopify/fulfillment.py b/ecommerce_integrations/shopify/fulfillment.py index e8cbb453..dcc456da 100644 --- a/ecommerce_integrations/shopify/fulfillment.py +++ b/ecommerce_integrations/shopify/fulfillment.py @@ -34,6 +34,8 @@ def create_delivery_note(shopify_order, setting, so): if not cint(setting.sync_delivery_note): return + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice + for fulfillment in shopify_order.get("fulfillments"): if ( not frappe.db.get_value("Delivery Note", {FULLFILLMENT_ID_FIELD: fulfillment.get("id")}, "name") @@ -57,6 +59,13 @@ def create_delivery_note(shopify_order, setting, so): if shopify_order.get("note"): dn.add_comment(text=f"Order Note: {shopify_order.get('note')}") + if setting.sync_cod_invoices: + inv = make_sales_invoice(dn.name) + if inv.items: + setattr(inv, ORDER_ID_FIELD, fulfillment.get("order_id")) + setattr(inv, ORDER_NUMBER_FIELD, shopify_order.get("name")) + inv.submit() + def get_fulfillment_items(dn_items, fulfillment_items, location_id=None): # local import to avoid circular imports diff --git a/ecommerce_integrations/shopify/invoice.py b/ecommerce_integrations/shopify/invoice.py index 26afb825..6a3c998e 100644 --- a/ecommerce_integrations/shopify/invoice.py +++ b/ecommerce_integrations/shopify/invoice.py @@ -22,7 +22,10 @@ def prepare_sales_invoice(payload, request_id=None): try: sales_order = get_sales_order(cstr(order["id"])) if sales_order: - create_sales_invoice(order, setting, sales_order) + payment = order.get("payment_terms", {}).get("payment_schedules", []) + posting_date = getdate(payment[0]["completed_at"]) if payment else nowdate() + create_sales_invoice(order, setting, sales_order, posting_date) + make_payment_entry_against_sales_invoice(cstr(order["id"]), setting, posting_date) create_shopify_log(status="Success") else: create_shopify_log(status="Invalid", message="Sales Order not found for syncing sales invoice.") @@ -30,17 +33,11 @@ def prepare_sales_invoice(payload, request_id=None): create_shopify_log(status="Error", exception=e, rollback=True) -def create_sales_invoice(shopify_order, setting, so): - if ( - not frappe.db.get_value("Sales Invoice", {ORDER_ID_FIELD: shopify_order.get("id")}, "name") - and so.docstatus == 1 - and not so.per_billed - and cint(setting.sync_sales_invoice) - ): - - posting_date = getdate(shopify_order.get("created_at")) or nowdate() - +def create_sales_invoice(shopify_order, setting, so, posting_date): + if so.docstatus == 1 and cint(setting.sync_sales_invoice): sales_invoice = make_sales_invoice(so.name, ignore_permissions=True) + if not sales_invoice.items: + return sales_invoice.set(ORDER_ID_FIELD, str(shopify_order.get("id"))) sales_invoice.set(ORDER_NUMBER_FIELD, shopify_order.get("name")) sales_invoice.set_posting_time = 1 @@ -51,8 +48,6 @@ def create_sales_invoice(shopify_order, setting, so): set_cost_center(sales_invoice.items, setting.cost_center) sales_invoice.insert(ignore_mandatory=True) sales_invoice.submit() - if sales_invoice.grand_total > 0: - make_payament_entry_against_sales_invoice(sales_invoice, setting, posting_date) if shopify_order.get("note"): sales_invoice.add_comment(text=f"Order Note: {shopify_order.get('note')}") @@ -63,13 +58,45 @@ def set_cost_center(items, cost_center): item.cost_center = cost_center -def make_payament_entry_against_sales_invoice(doc, setting, posting_date=None): +def make_payment_entry_against_sales_invoice(order_id, setting, 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=setting.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() + invoices = frappe.db.get_all( + "Sales Invoice", + filters={ORDER_ID_FIELD: order_id, "docstatus": 1}, + fields=["name", "due_date", "grand_total", "outstanding_amount"], + ) + + if not invoices: + frappe.throw(frappe._("Invoices not synced to mark payment.")) + + payment_entry = None + + for inv in invoices: + if not payment_entry: + payment_entry = get_payment_entry( + "Sales Invoice", inv.name, bank_account=setting.cash_bank_account + ) + continue + + payment_entry.append( + "references", + { + "reference_doctype": "Sales Invoice", + "reference_name": inv.name, + "bill_no": "", + "due_date": inv.due_date, + "total_amount": inv.grand_total, + "outstanding_amount": inv.outstanding_amount, + "allocated_amount": inv.outstanding_amount, + }, + ) + payment_entry.paid_amount += inv.outstanding_amount + + if payment_entry: + payment_entry.flags.ignore_mandatory = True + payment_entry.reference_no = order_id + 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() From cc8797e9566a1dddabe43f8962c9abc2a22e8eaf Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 25 Jan 2023 13:10:01 +0000 Subject: [PATCH 2/4] fix(shopify): recalculate tax on dn creation --- ecommerce_integrations/shopify/fulfillment.py | 45 ++++++++++++++++--- ecommerce_integrations/shopify/order.py | 2 +- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/ecommerce_integrations/shopify/fulfillment.py b/ecommerce_integrations/shopify/fulfillment.py index dcc456da..4a4405a5 100644 --- a/ecommerce_integrations/shopify/fulfillment.py +++ b/ecommerce_integrations/shopify/fulfillment.py @@ -1,6 +1,8 @@ +import json + import frappe from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note -from frappe.utils import cint, cstr, getdate +from frappe.utils import cint, cstr, flt, getdate from ecommerce_integrations.shopify.constants import ( FULLFILLMENT_ID_FIELD, @@ -8,7 +10,12 @@ ORDER_NUMBER_FIELD, SETTING_DOCTYPE, ) -from ecommerce_integrations.shopify.order import get_sales_order +from ecommerce_integrations.shopify.order import ( + get_sales_order, + get_tax_account_description, + get_tax_account_head, +) +from ecommerce_integrations.shopify.product import get_item_code from ecommerce_integrations.shopify.utils import create_shopify_log @@ -52,6 +59,9 @@ def create_delivery_note(shopify_order, setting, so): dn.items = get_fulfillment_items( dn.items, fulfillment.get("line_items"), fulfillment.get("location_id") ) + dn.taxes = [] + for tax in get_dn_taxes(fulfillment, setting): + dn.append("taxes", tax) dn.flags.ignore_mandatory = True dn.save() dn.submit() @@ -68,9 +78,6 @@ def create_delivery_note(shopify_order, setting, so): def get_fulfillment_items(dn_items, fulfillment_items, location_id=None): - # local import to avoid circular imports - from ecommerce_integrations.shopify.product import get_item_code - setting = frappe.get_cached_doc(SETTING_DOCTYPE) wh_map = setting.get_integration_to_erpnext_wh_mapping() warehouse = wh_map.get(str(location_id)) or setting.warehouse @@ -81,3 +88,31 @@ def get_fulfillment_items(dn_items, fulfillment_items, location_id=None): for dn_item in dn_items if get_item_code(item) == dn_item.item_code ] + + +def get_dn_taxes(fulfillment, setting): + taxes = [] + line_items = fulfillment.get("line_items") + + for line_item in line_items: + item_code = get_item_code(line_item) + for tax in line_item.get("tax_lines"): + tax_amt = ( + flt(tax.get("rate", 0)) * flt(line_item.get("quantity", 0)) * flt(line_item.get("price", 0)) + ) + taxes.append( + { + "charge_type": "Actual", + "account_head": get_tax_account_head(tax), + "description": ( + f"{get_tax_account_description(tax) or tax.get('title')} - {tax.get('rate') * 100.0:.2f}%" + ), + "tax_amount": flt(tax_amt), + "included_in_print_rate": 0, + "cost_center": setting.cost_center, + "item_wise_tax_detail": json.dumps({item_code: [flt(tax.get("rate")) * 100, flt(tax_amt)]}), + "dont_recompute_tax": 1, + } + ) + + return taxes diff --git a/ecommerce_integrations/shopify/order.py b/ecommerce_integrations/shopify/order.py index ddaa73d4..4b10391c 100644 --- a/ecommerce_integrations/shopify/order.py +++ b/ecommerce_integrations/shopify/order.py @@ -211,7 +211,7 @@ def get_order_taxes(shopify_order, setting): taxes = update_taxes_with_shipping_lines( taxes, - shopify_order.get("shipping_lines"), + shopify_order.get("shipping_lines", []), setting, taxes_inclusive=shopify_order.get("taxes_included"), ) From 921e77cc394dc1328a4b0b59b3786fb18cee7a87 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 1 Feb 2023 09:46:23 +0000 Subject: [PATCH 3/4] feat: option to generate invoice on order creation --- .../shopify_setting/shopify_setting.json | 35 +++++++++++-------- ecommerce_integrations/shopify/fulfillment.py | 2 +- ecommerce_integrations/shopify/invoice.py | 9 ++--- ecommerce_integrations/shopify/order.py | 4 ++- ecommerce_integrations/shopify/tests/utils.py | 2 +- 5 files changed, 31 insertions(+), 21 deletions(-) diff --git a/ecommerce_integrations/shopify/doctype/shopify_setting/shopify_setting.json b/ecommerce_integrations/shopify/doctype/shopify_setting/shopify_setting.json index 1d600cf1..2db17a2c 100644 --- a/ecommerce_integrations/shopify/doctype/shopify_setting/shopify_setting.json +++ b/ecommerce_integrations/shopify/doctype/shopify_setting/shopify_setting.json @@ -27,12 +27,13 @@ "column_break_22", "section_break_25", "sales_order_series", - "column_break_27", - "sync_delivery_note", "delivery_note_series", - "sync_sales_invoice", - "sync_cod_invoices", "sales_invoice_series", + "column_break_27", + "sync_delivery_note", + "sync_sales_invoice_on_payment", + "sync_invoice_on_delivery", + "sync_invoice_on_order", "section_break_22", "html_16", "taxes", @@ -205,17 +206,11 @@ "mandatory_depends_on": "eval:doc.sync_delivery_note" }, { - "default": "0", - "fieldname": "sync_sales_invoice", - "fieldtype": "Check", - "label": "Import Sales Invoice from Shopify if Payment is marked" - }, - { - "depends_on": "eval:doc.sync_sales_invoice==1", + "depends_on": "eval:doc.sync_sales_invoice_on_payment || doc.sync_invoice_on_delivery || doc.sync_invoice_on_order", "fieldname": "sales_invoice_series", "fieldtype": "Select", "label": "Sales Invoice Series", - "mandatory_depends_on": "eval:doc.sync_sales_invoice" + "mandatory_depends_on": "eval:doc.sync_sales_invoice_on_payment || doc.sync_invoice_on_delivery || doc.sync_invoice_on_order" }, { "fieldname": "section_break_22", @@ -351,15 +346,27 @@ }, { "default": "0", - "fieldname": "sync_cod_invoices", + "fieldname": "sync_sales_invoice_on_payment", + "fieldtype": "Check", + "label": "Import Sales Invoice from Shopify if Payment is marked" + }, + { + "default": "0", + "fieldname": "sync_invoice_on_delivery", "fieldtype": "Check", "label": "Import Sales Invoice from Shopify if Fulfillment is created" + }, + { + "default": "0", + "fieldname": "sync_invoice_on_order", + "fieldtype": "Check", + "label": "Import Sales Invoice from Shopify on Order creation" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-01-25 17:59:13.943796", + "modified": "2023-02-01 15:07:02.735741", "modified_by": "Administrator", "module": "shopify", "name": "Shopify Setting", diff --git a/ecommerce_integrations/shopify/fulfillment.py b/ecommerce_integrations/shopify/fulfillment.py index 4a4405a5..5709d842 100644 --- a/ecommerce_integrations/shopify/fulfillment.py +++ b/ecommerce_integrations/shopify/fulfillment.py @@ -69,7 +69,7 @@ def create_delivery_note(shopify_order, setting, so): if shopify_order.get("note"): dn.add_comment(text=f"Order Note: {shopify_order.get('note')}") - if setting.sync_cod_invoices: + if setting.sync_invoice_on_delivery: inv = make_sales_invoice(dn.name) if inv.items: setattr(inv, ORDER_ID_FIELD, fulfillment.get("order_id")) diff --git a/ecommerce_integrations/shopify/invoice.py b/ecommerce_integrations/shopify/invoice.py index 6a3c998e..2a194298 100644 --- a/ecommerce_integrations/shopify/invoice.py +++ b/ecommerce_integrations/shopify/invoice.py @@ -24,8 +24,9 @@ def prepare_sales_invoice(payload, request_id=None): if sales_order: payment = order.get("payment_terms", {}).get("payment_schedules", []) posting_date = getdate(payment[0]["completed_at"]) if payment else nowdate() - create_sales_invoice(order, setting, sales_order, posting_date) - make_payment_entry_against_sales_invoice(cstr(order["id"]), setting, posting_date) + if cint(setting.sync_sales_invoice_on_payment): + create_sales_invoice(order, setting, sales_order, posting_date) + make_payment_entry_against_sales_invoice(cstr(order["id"]), setting, posting_date) create_shopify_log(status="Success") else: create_shopify_log(status="Invalid", message="Sales Order not found for syncing sales invoice.") @@ -33,8 +34,8 @@ def prepare_sales_invoice(payload, request_id=None): create_shopify_log(status="Error", exception=e, rollback=True) -def create_sales_invoice(shopify_order, setting, so, posting_date): - if so.docstatus == 1 and cint(setting.sync_sales_invoice): +def create_sales_invoice(shopify_order, setting, so, posting_date=nowdate()): + if so.docstatus == 1: sales_invoice = make_sales_invoice(so.name, ignore_permissions=True) if not sales_invoice.items: return diff --git a/ecommerce_integrations/shopify/order.py b/ecommerce_integrations/shopify/order.py index 4b10391c..e4bd1baa 100644 --- a/ecommerce_integrations/shopify/order.py +++ b/ecommerce_integrations/shopify/order.py @@ -60,7 +60,9 @@ def create_order(order, setting, company=None): so = create_sales_order(order, setting, company) if so: - if order.get("financial_status") == "paid": + if cint(setting.sync_invoice_on_order): + create_sales_invoice(order, setting, so) + elif order.get("financial_status") == "paid" and cint(setting.sync_sales_invoice_on_payment): create_sales_invoice(order, setting, so) if order.get("fulfillments"): diff --git a/ecommerce_integrations/shopify/tests/utils.py b/ecommerce_integrations/shopify/tests/utils.py index fd4f075c..465bce47 100644 --- a/ecommerce_integrations/shopify/tests/utils.py +++ b/ecommerce_integrations/shopify/tests/utils.py @@ -59,7 +59,7 @@ def setUpClass(cls): "sales_order_series": "SAL-ORD-.YYYY.-", "sync_delivery_note": 1, "delivery_note_series": "MAT-DN-.YYYY.-", - "sync_sales_invoice": 1, + "sync_sales_invoice_on_payment": 1, "sales_invoice_series": "SINV-.YY.-", "upload_erpnext_items": 1, "update_shopify_item_on_update": 1, From 9c531accc3af27ef2fe5fb9787d88fd36f8d0c7b Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 1 Feb 2023 10:59:48 +0000 Subject: [PATCH 4/4] fix(shopify): payment record issue --- ecommerce_integrations/shopify/invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce_integrations/shopify/invoice.py b/ecommerce_integrations/shopify/invoice.py index 2a194298..9881d13e 100644 --- a/ecommerce_integrations/shopify/invoice.py +++ b/ecommerce_integrations/shopify/invoice.py @@ -26,7 +26,7 @@ def prepare_sales_invoice(payload, request_id=None): posting_date = getdate(payment[0]["completed_at"]) if payment else nowdate() if cint(setting.sync_sales_invoice_on_payment): create_sales_invoice(order, setting, sales_order, posting_date) - make_payment_entry_against_sales_invoice(cstr(order["id"]), setting, posting_date) + make_payment_entry_against_sales_invoice(cstr(order["id"]), setting, posting_date) create_shopify_log(status="Success") else: create_shopify_log(status="Invalid", message="Sales Order not found for syncing sales invoice.")