mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-01 07:37:04 +00:00
Merge pull request #55685 from nabinhait/dn-services
refactor(stock): extract Delivery Note services
This commit is contained in:
@@ -26,7 +26,9 @@ from erpnext.accounts.utils import update_voucher_outstanding
|
|||||||
from erpnext.controllers.accounts_controller import validate_account_head
|
from erpnext.controllers.accounts_controller import validate_account_head
|
||||||
from erpnext.controllers.selling_controller import SellingController
|
from erpnext.controllers.selling_controller import SellingController
|
||||||
from erpnext.setup.doctype.company.company import update_company_current_month_sales
|
from erpnext.setup.doctype.company.company import update_company_current_month_sales
|
||||||
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
|
from erpnext.stock.doctype.delivery_note.services.billing_status import (
|
||||||
|
update_billed_amount_based_on_so,
|
||||||
|
)
|
||||||
|
|
||||||
from .services.fixed_assets import FixedAssetService
|
from .services.fixed_assets import FixedAssetService
|
||||||
from .services.inter_company import (
|
from .services.inter_company import (
|
||||||
|
|||||||
@@ -4,19 +4,13 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.desk.notifications import clear_doctype_notifications
|
|
||||||
from frappe.model.document import Document
|
|
||||||
from frappe.query_builder import DocType
|
|
||||||
from frappe.query_builder.functions import Abs, Sum
|
|
||||||
from frappe.utils import cint, flt
|
from frappe.utils import cint, flt
|
||||||
|
|
||||||
from erpnext.controllers.selling_controller import SellingController
|
from erpnext.controllers.selling_controller import SellingController
|
||||||
|
from erpnext.stock.doctype.delivery_note.services.billing_status import BillingStatusService
|
||||||
|
from erpnext.stock.doctype.delivery_note.services.packing import PackingService
|
||||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||||
|
|
||||||
from .mapper import (
|
|
||||||
make_sales_invoice,
|
|
||||||
)
|
|
||||||
|
|
||||||
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
|
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
|
||||||
|
|
||||||
|
|
||||||
@@ -483,7 +477,7 @@ class DeliveryNote(SellingController):
|
|||||||
if not self.is_return:
|
if not self.is_return:
|
||||||
self.check_credit_limit()
|
self.check_credit_limit()
|
||||||
elif self.issue_credit_note:
|
elif self.issue_credit_note:
|
||||||
self.make_return_invoice()
|
BillingStatusService(self).make_return_invoice()
|
||||||
|
|
||||||
for table_name in ["items", "packed_items"]:
|
for table_name in ["items", "packed_items"]:
|
||||||
if not self.get(table_name):
|
if not self.get(table_name):
|
||||||
@@ -516,7 +510,7 @@ class DeliveryNote(SellingController):
|
|||||||
# because updating reserved qty in bin depends upon updated delivered qty in SO
|
# because updating reserved qty in bin depends upon updated delivered qty in SO
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
|
|
||||||
self.cancel_packing_slips()
|
PackingService(self).cancel_packing_slips()
|
||||||
self.update_pick_list_status()
|
self.update_pick_list_status()
|
||||||
|
|
||||||
self.make_gl_entries_on_cancel()
|
self.make_gl_entries_on_cancel()
|
||||||
@@ -607,20 +601,7 @@ class DeliveryNote(SellingController):
|
|||||||
|
|
||||||
def validate_packed_qty(self):
|
def validate_packed_qty(self):
|
||||||
"""Validate that if packed qty exists, it should be equal to qty"""
|
"""Validate that if packed qty exists, it should be equal to qty"""
|
||||||
|
PackingService(self).validate_packed_qty()
|
||||||
if frappe.db.exists("Packing Slip", {"docstatus": 1, "delivery_note": self.name}):
|
|
||||||
product_bundle_list = self.get_product_bundle_list()
|
|
||||||
for item in self.items + self.packed_items:
|
|
||||||
if (
|
|
||||||
item.item_code not in product_bundle_list
|
|
||||||
and flt(item.packed_qty)
|
|
||||||
and flt(item.packed_qty) != flt(item.qty)
|
|
||||||
):
|
|
||||||
frappe.throw(
|
|
||||||
_("Row {0}: Packed Qty must be equal to {1} Qty.").format(
|
|
||||||
item.idx, frappe.bold(item.doctype)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def check_next_docstatus(self):
|
def check_next_docstatus(self):
|
||||||
submit_rv = frappe.db.sql(
|
submit_rv = frappe.db.sql(
|
||||||
@@ -641,153 +622,14 @@ class DeliveryNote(SellingController):
|
|||||||
if submit_in:
|
if submit_in:
|
||||||
frappe.throw(_("Installation Note {0} has already been submitted").format(submit_in[0][0]))
|
frappe.throw(_("Installation Note {0} has already been submitted").format(submit_in[0][0]))
|
||||||
|
|
||||||
def cancel_packing_slips(self):
|
|
||||||
"""
|
|
||||||
Cancel submitted packing slips related to this delivery note
|
|
||||||
"""
|
|
||||||
res = frappe.db.sql(
|
|
||||||
"""SELECT name FROM `tabPacking Slip` WHERE delivery_note = %s
|
|
||||||
AND docstatus = 1""",
|
|
||||||
self.name,
|
|
||||||
)
|
|
||||||
|
|
||||||
if res:
|
|
||||||
for r in res:
|
|
||||||
ps = frappe.get_doc("Packing Slip", r[0])
|
|
||||||
ps.cancel()
|
|
||||||
frappe.msgprint(_("Packing Slip(s) cancelled"))
|
|
||||||
|
|
||||||
def update_status(self, status):
|
def update_status(self, status):
|
||||||
self.set_status(update=True, status=status)
|
BillingStatusService(self).update_status(status)
|
||||||
self.notify_update()
|
|
||||||
clear_doctype_notifications(self)
|
|
||||||
|
|
||||||
def update_billing_status(self, update_modified=True):
|
def update_billing_status(self, update_modified=True):
|
||||||
updated_delivery_notes = [self.name]
|
BillingStatusService(self).update_billing_status(update_modified)
|
||||||
for d in self.get("items"):
|
|
||||||
if d.si_detail and not d.so_detail:
|
|
||||||
d.db_set("billed_amt", d.amount, update_modified=update_modified)
|
|
||||||
elif d.so_detail:
|
|
||||||
updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
|
|
||||||
|
|
||||||
for dn in set(updated_delivery_notes):
|
|
||||||
dn_doc = self if (dn == self.name) else frappe.get_lazy_doc("Delivery Note", dn)
|
|
||||||
dn_doc.update_billing_percentage(update_modified=update_modified)
|
|
||||||
|
|
||||||
self.load_from_db()
|
|
||||||
|
|
||||||
def make_return_invoice(self):
|
|
||||||
try:
|
|
||||||
return_invoice = make_sales_invoice(self.name)
|
|
||||||
return_invoice.is_return = True
|
|
||||||
return_invoice.save()
|
|
||||||
return_invoice.submit()
|
|
||||||
|
|
||||||
credit_note_link = frappe.utils.get_link_to_form("Sales Invoice", return_invoice.name)
|
|
||||||
|
|
||||||
frappe.msgprint(_("Credit Note {0} has been created automatically").format(credit_note_link))
|
|
||||||
except Exception:
|
|
||||||
frappe.throw(
|
|
||||||
_(
|
|
||||||
"Could not create Credit Note automatically, please uncheck 'Issue Credit Note' and submit again"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def has_unpacked_items(self):
|
def has_unpacked_items(self):
|
||||||
product_bundle_list = self.get_product_bundle_list()
|
return PackingService(self).has_unpacked_items()
|
||||||
|
|
||||||
for item in self.items + self.packed_items:
|
|
||||||
if item.item_code not in product_bundle_list and flt(item.packed_qty) < flt(item.qty):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_product_bundle_list(self):
|
|
||||||
items_list = [item.item_code for item in self.items]
|
|
||||||
return frappe.db.get_all(
|
|
||||||
"Product Bundle",
|
|
||||||
filters={"new_item_code": ["in", items_list], "disabled": 0},
|
|
||||||
pluck="name",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def update_billed_amount_based_on_so(so_detail, update_modified=True):
|
|
||||||
# Billed against Sales Order directly
|
|
||||||
si = frappe.qb.DocType("Sales Invoice").as_("si")
|
|
||||||
si_item = frappe.qb.DocType("Sales Invoice Item").as_("si_item")
|
|
||||||
sum_amount = Sum(si_item.amount).as_("amount")
|
|
||||||
|
|
||||||
billed_against_so = (
|
|
||||||
frappe.qb.from_(si_item)
|
|
||||||
.join(si)
|
|
||||||
.on(si.name == si_item.parent)
|
|
||||||
.select(sum_amount)
|
|
||||||
.where(
|
|
||||||
(si_item.so_detail == so_detail)
|
|
||||||
& ((si_item.dn_detail.isnull()) | (si_item.dn_detail == ""))
|
|
||||||
& (si_item.docstatus == 1)
|
|
||||||
& (si.update_stock == 0)
|
|
||||||
)
|
|
||||||
.run()
|
|
||||||
)
|
|
||||||
billed_against_so = billed_against_so and billed_against_so[0][0] or 0
|
|
||||||
|
|
||||||
# Get all Delivery Note Item rows against the Sales Order Item row
|
|
||||||
dn = frappe.qb.DocType("Delivery Note").as_("dn")
|
|
||||||
dn_item = frappe.qb.DocType("Delivery Note Item").as_("dn_item")
|
|
||||||
|
|
||||||
dn_details = (
|
|
||||||
frappe.qb.from_(dn)
|
|
||||||
.from_(dn_item)
|
|
||||||
.select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent)
|
|
||||||
.where(
|
|
||||||
(dn.name == dn_item.parent)
|
|
||||||
& (dn_item.so_detail == so_detail)
|
|
||||||
& (dn.docstatus == 1)
|
|
||||||
& (dn.is_return == 0)
|
|
||||||
)
|
|
||||||
.orderby(dn.posting_date, dn.posting_time, dn.name)
|
|
||||||
.run(as_dict=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
updated_dn = []
|
|
||||||
for dnd in dn_details:
|
|
||||||
billed_amt_against_dn = 0
|
|
||||||
|
|
||||||
# If delivered against Sales Invoice
|
|
||||||
if dnd.si_detail:
|
|
||||||
billed_amt_against_dn = flt(dnd.amount)
|
|
||||||
billed_against_so -= billed_amt_against_dn
|
|
||||||
else:
|
|
||||||
# Get billed amount directly against Delivery Note
|
|
||||||
billed_amt_against_dn = frappe.db.sql(
|
|
||||||
"""select sum(amount) from `tabSales Invoice Item`
|
|
||||||
where dn_detail=%s and docstatus=1""",
|
|
||||||
dnd.name,
|
|
||||||
)
|
|
||||||
billed_amt_against_dn = billed_amt_against_dn and billed_amt_against_dn[0][0] or 0
|
|
||||||
|
|
||||||
# Distribute billed amount directly against SO between DNs based on FIFO
|
|
||||||
if billed_against_so and billed_amt_against_dn < dnd.amount:
|
|
||||||
pending_to_bill = flt(dnd.amount) - billed_amt_against_dn
|
|
||||||
if pending_to_bill <= billed_against_so:
|
|
||||||
billed_amt_against_dn += pending_to_bill
|
|
||||||
billed_against_so -= pending_to_bill
|
|
||||||
else:
|
|
||||||
billed_amt_against_dn += billed_against_so
|
|
||||||
billed_against_so = 0
|
|
||||||
|
|
||||||
frappe.db.set_value(
|
|
||||||
"Delivery Note Item",
|
|
||||||
dnd.name,
|
|
||||||
"billed_amt",
|
|
||||||
billed_amt_against_dn,
|
|
||||||
update_modified=update_modified,
|
|
||||||
)
|
|
||||||
|
|
||||||
updated_dn.append(dnd.parent)
|
|
||||||
|
|
||||||
return updated_dn
|
|
||||||
|
|
||||||
|
|
||||||
def get_list_context(context=None):
|
def get_list_context(context=None):
|
||||||
|
|||||||
134
erpnext/stock/doctype/delivery_note/services/billing_status.py
Normal file
134
erpnext/stock/doctype/delivery_note/services/billing_status.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
"""Billing status tracking and return invoicing for Delivery Note."""
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.desk.notifications import clear_doctype_notifications
|
||||||
|
from frappe.query_builder.functions import Sum
|
||||||
|
from frappe.utils import flt
|
||||||
|
|
||||||
|
|
||||||
|
class BillingStatusService:
|
||||||
|
def __init__(self, doc):
|
||||||
|
self.doc = doc
|
||||||
|
|
||||||
|
def update_status(self, status: str) -> None:
|
||||||
|
doc = self.doc
|
||||||
|
doc.set_status(update=True, status=status)
|
||||||
|
doc.notify_update()
|
||||||
|
clear_doctype_notifications(doc)
|
||||||
|
|
||||||
|
def update_billing_status(self, update_modified: bool = True) -> None:
|
||||||
|
doc = self.doc
|
||||||
|
updated_delivery_notes = [doc.name]
|
||||||
|
for d in doc.get("items"):
|
||||||
|
if d.si_detail and not d.so_detail:
|
||||||
|
d.db_set("billed_amt", d.amount, update_modified=update_modified)
|
||||||
|
elif d.so_detail:
|
||||||
|
updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
|
||||||
|
|
||||||
|
for dn in set(updated_delivery_notes):
|
||||||
|
dn_doc = doc if (dn == doc.name) else frappe.get_lazy_doc("Delivery Note", dn)
|
||||||
|
dn_doc.update_billing_percentage(update_modified=update_modified)
|
||||||
|
|
||||||
|
doc.load_from_db()
|
||||||
|
|
||||||
|
def make_return_invoice(self) -> None:
|
||||||
|
from erpnext.stock.doctype.delivery_note.mapper import make_sales_invoice
|
||||||
|
|
||||||
|
try:
|
||||||
|
return_invoice = make_sales_invoice(self.doc.name)
|
||||||
|
return_invoice.is_return = True
|
||||||
|
return_invoice.save()
|
||||||
|
return_invoice.submit()
|
||||||
|
|
||||||
|
credit_note_link = frappe.utils.get_link_to_form("Sales Invoice", return_invoice.name)
|
||||||
|
|
||||||
|
frappe.msgprint(_("Credit Note {0} has been created automatically").format(credit_note_link))
|
||||||
|
except Exception:
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Could not create Credit Note automatically, please uncheck 'Issue Credit Note' and submit again"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_billed_amount_based_on_so(so_detail: str, update_modified: bool = True) -> list[str]:
|
||||||
|
# Billed against Sales Order directly
|
||||||
|
si = frappe.qb.DocType("Sales Invoice").as_("si")
|
||||||
|
si_item = frappe.qb.DocType("Sales Invoice Item").as_("si_item")
|
||||||
|
sum_amount = Sum(si_item.amount).as_("amount")
|
||||||
|
|
||||||
|
billed_against_so = (
|
||||||
|
frappe.qb.from_(si_item)
|
||||||
|
.join(si)
|
||||||
|
.on(si.name == si_item.parent)
|
||||||
|
.select(sum_amount)
|
||||||
|
.where(
|
||||||
|
(si_item.so_detail == so_detail)
|
||||||
|
& ((si_item.dn_detail.isnull()) | (si_item.dn_detail == ""))
|
||||||
|
& (si_item.docstatus == 1)
|
||||||
|
& (si.update_stock == 0)
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
)
|
||||||
|
billed_against_so = billed_against_so and billed_against_so[0][0] or 0
|
||||||
|
|
||||||
|
# Get all Delivery Note Item rows against the Sales Order Item row
|
||||||
|
dn = frappe.qb.DocType("Delivery Note").as_("dn")
|
||||||
|
dn_item = frappe.qb.DocType("Delivery Note Item").as_("dn_item")
|
||||||
|
|
||||||
|
dn_details = (
|
||||||
|
frappe.qb.from_(dn)
|
||||||
|
.from_(dn_item)
|
||||||
|
.select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent)
|
||||||
|
.where(
|
||||||
|
(dn.name == dn_item.parent)
|
||||||
|
& (dn_item.so_detail == so_detail)
|
||||||
|
& (dn.docstatus == 1)
|
||||||
|
& (dn.is_return == 0)
|
||||||
|
)
|
||||||
|
.orderby(dn.posting_date, dn.posting_time, dn.name)
|
||||||
|
.run(as_dict=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_dn = []
|
||||||
|
for dnd in dn_details:
|
||||||
|
billed_amt_against_dn = 0
|
||||||
|
|
||||||
|
# If delivered against Sales Invoice
|
||||||
|
if dnd.si_detail:
|
||||||
|
billed_amt_against_dn = flt(dnd.amount)
|
||||||
|
billed_against_so -= billed_amt_against_dn
|
||||||
|
else:
|
||||||
|
# Get billed amount directly against Delivery Note
|
||||||
|
billed_amt_against_dn = frappe.db.sql(
|
||||||
|
"""select sum(amount) from `tabSales Invoice Item`
|
||||||
|
where dn_detail=%s and docstatus=1""",
|
||||||
|
dnd.name,
|
||||||
|
)
|
||||||
|
billed_amt_against_dn = billed_amt_against_dn and billed_amt_against_dn[0][0] or 0
|
||||||
|
|
||||||
|
# Distribute billed amount directly against SO between DNs based on FIFO
|
||||||
|
if billed_against_so and billed_amt_against_dn < dnd.amount:
|
||||||
|
pending_to_bill = flt(dnd.amount) - billed_amt_against_dn
|
||||||
|
if pending_to_bill <= billed_against_so:
|
||||||
|
billed_amt_against_dn += pending_to_bill
|
||||||
|
billed_against_so -= pending_to_bill
|
||||||
|
else:
|
||||||
|
billed_amt_against_dn += billed_against_so
|
||||||
|
billed_against_so = 0
|
||||||
|
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Delivery Note Item",
|
||||||
|
dnd.name,
|
||||||
|
"billed_amt",
|
||||||
|
billed_amt_against_dn,
|
||||||
|
update_modified=update_modified,
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_dn.append(dnd.parent)
|
||||||
|
|
||||||
|
return updated_dn
|
||||||
63
erpnext/stock/doctype/delivery_note/services/packing.py
Normal file
63
erpnext/stock/doctype/delivery_note/services/packing.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
"""Packing slip and product bundle handling for Delivery Note."""
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import flt
|
||||||
|
|
||||||
|
|
||||||
|
class PackingService:
|
||||||
|
def __init__(self, doc):
|
||||||
|
self.doc = doc
|
||||||
|
|
||||||
|
def validate_packed_qty(self) -> None:
|
||||||
|
"""Validate that if packed qty exists, it should be equal to qty"""
|
||||||
|
doc = self.doc
|
||||||
|
|
||||||
|
if frappe.db.exists("Packing Slip", {"docstatus": 1, "delivery_note": doc.name}):
|
||||||
|
product_bundle_list = self.get_product_bundle_list()
|
||||||
|
for item in doc.items + doc.packed_items:
|
||||||
|
if (
|
||||||
|
item.item_code not in product_bundle_list
|
||||||
|
and flt(item.packed_qty)
|
||||||
|
and flt(item.packed_qty) != flt(item.qty)
|
||||||
|
):
|
||||||
|
frappe.throw(
|
||||||
|
_("Row {0}: Packed Qty must be equal to {1} Qty.").format(
|
||||||
|
item.idx, frappe.bold(item.doctype)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def has_unpacked_items(self) -> bool:
|
||||||
|
doc = self.doc
|
||||||
|
product_bundle_list = self.get_product_bundle_list()
|
||||||
|
|
||||||
|
for item in doc.items + doc.packed_items:
|
||||||
|
if item.item_code not in product_bundle_list and flt(item.packed_qty) < flt(item.qty):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_product_bundle_list(self) -> list[str]:
|
||||||
|
items_list = [item.item_code for item in self.doc.items]
|
||||||
|
return frappe.db.get_all(
|
||||||
|
"Product Bundle",
|
||||||
|
filters={"new_item_code": ["in", items_list], "disabled": 0},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
def cancel_packing_slips(self) -> None:
|
||||||
|
"""Cancel submitted packing slips related to this delivery note"""
|
||||||
|
res = frappe.db.sql(
|
||||||
|
"""SELECT name FROM `tabPacking Slip` WHERE delivery_note = %s
|
||||||
|
AND docstatus = 1""",
|
||||||
|
self.doc.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
if res:
|
||||||
|
for r in res:
|
||||||
|
ps = frappe.get_doc("Packing Slip", r[0])
|
||||||
|
ps.cancel()
|
||||||
|
frappe.msgprint(_("Packing Slip(s) cancelled"))
|
||||||
Reference in New Issue
Block a user