mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-11 13:41:47 +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.selling_controller import SellingController
|
||||
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.inter_company import (
|
||||
|
||||
@@ -4,19 +4,13 @@
|
||||
|
||||
import frappe
|
||||
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 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 .mapper import (
|
||||
make_sales_invoice,
|
||||
)
|
||||
|
||||
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
|
||||
|
||||
|
||||
@@ -483,7 +477,7 @@ class DeliveryNote(SellingController):
|
||||
if not self.is_return:
|
||||
self.check_credit_limit()
|
||||
elif self.issue_credit_note:
|
||||
self.make_return_invoice()
|
||||
BillingStatusService(self).make_return_invoice()
|
||||
|
||||
for table_name in ["items", "packed_items"]:
|
||||
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
|
||||
self.update_stock_ledger()
|
||||
|
||||
self.cancel_packing_slips()
|
||||
PackingService(self).cancel_packing_slips()
|
||||
self.update_pick_list_status()
|
||||
|
||||
self.make_gl_entries_on_cancel()
|
||||
@@ -607,20 +601,7 @@ class DeliveryNote(SellingController):
|
||||
|
||||
def validate_packed_qty(self):
|
||||
"""Validate that if packed qty exists, it should be equal to 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)
|
||||
)
|
||||
)
|
||||
PackingService(self).validate_packed_qty()
|
||||
|
||||
def check_next_docstatus(self):
|
||||
submit_rv = frappe.db.sql(
|
||||
@@ -641,153 +622,14 @@ class DeliveryNote(SellingController):
|
||||
if submit_in:
|
||||
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):
|
||||
self.set_status(update=True, status=status)
|
||||
self.notify_update()
|
||||
clear_doctype_notifications(self)
|
||||
BillingStatusService(self).update_status(status)
|
||||
|
||||
def update_billing_status(self, update_modified=True):
|
||||
updated_delivery_notes = [self.name]
|
||||
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"
|
||||
)
|
||||
)
|
||||
BillingStatusService(self).update_billing_status(update_modified)
|
||||
|
||||
def has_unpacked_items(self):
|
||||
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) < 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
|
||||
return PackingService(self).has_unpacked_items()
|
||||
|
||||
|
||||
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