Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(shopify): ability to generate invoice on fulfilment/order creation #228

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@
"column_break_22",
"section_break_25",
"sales_order_series",
"column_break_27",
"sync_delivery_note",
"delivery_note_series",
"sync_sales_invoice",
"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",
Expand Down Expand Up @@ -204,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",
Expand Down Expand Up @@ -347,12 +343,30 @@
"fieldname": "sync_new_item_as_active",
"fieldtype": "Check",
"label": "Sync New Items as Active"
},
{
"default": "0",
"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": "2022-11-01 16:09:42.685577",
"modified": "2023-02-01 15:07:02.735741",
"modified_by": "Administrator",
"module": "shopify",
"name": "Shopify Setting",
Expand Down
54 changes: 49 additions & 5 deletions ecommerce_integrations/shopify/fulfillment.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
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,
ORDER_ID_FIELD,
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


Expand All @@ -34,6 +41,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")
Expand All @@ -50,18 +59,25 @@ 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()

if shopify_order.get("note"):
dn.add_comment(text=f"Order Note: {shopify_order.get('note')}")

if setting.sync_invoice_on_delivery:
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
from ecommerce_integrations.shopify.product import get_item_code

def get_fulfillment_items(dn_items, fulfillment_items, location_id=None):
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
Expand All @@ -72,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
70 changes: 49 additions & 21 deletions ecommerce_integrations/shopify/invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,23 @@ 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()
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.")
except Exception as e:
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=nowdate()):
if so.docstatus == 1:
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
Expand All @@ -51,8 +49,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')}")
Expand All @@ -63,13 +59,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()
6 changes: 4 additions & 2 deletions ecommerce_integrations/shopify/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down Expand Up @@ -211,7 +213,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"),
)
Expand Down
2 changes: 1 addition & 1 deletion ecommerce_integrations/shopify/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down