diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 5be5d5dfdff..195aad6d74f 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -4,6 +4,7 @@ import frappe from frappe import _, bold, throw +from frappe.query_builder.functions import Sum from frappe.utils import cint, flt, get_link_to_form, nowtime from erpnext.accounts.party import render_address @@ -439,22 +440,34 @@ class SellingController(StockController): product_bundle_items[item_code] = item_code in items_with_product_bundle def get_already_delivered_qty(self, current_docname, so, so_detail): - delivered_via_dn = frappe.db.sql( - """select sum(qty) from `tabDelivery Note Item` - where so_detail = %s and docstatus = 1 - and against_sales_order = %s - and parent != %s""", - (so_detail, so, current_docname), + dn_item = frappe.qb.DocType("Delivery Note Item") + delivered_via_dn = ( + frappe.qb.from_(dn_item) + .select(Sum(dn_item.qty)) + .where( + (dn_item.so_detail == so_detail) + & (dn_item.docstatus == 1) + & (dn_item.against_sales_order == so) + & (dn_item.parent != current_docname) + ) + .run() ) - delivered_via_si = frappe.db.sql( - """select sum(si_item.qty) - from `tabSales Invoice Item` si_item, `tabSales Invoice` si - where si_item.parent = si.name and si.update_stock = 1 - and si_item.so_detail = %s and si.docstatus = 1 - and si_item.sales_order = %s - and si.name != %s""", - (so_detail, so, current_docname), + si = frappe.qb.DocType("Sales Invoice") + si_item = frappe.qb.DocType("Sales Invoice Item") + delivered_via_si = ( + frappe.qb.from_(si_item) + .inner_join(si) + .on(si_item.parent == si.name) + .select(Sum(si_item.qty)) + .where( + (si.update_stock == 1) + & (si_item.so_detail == so_detail) + & (si.docstatus == 1) + & (si_item.sales_order == so) + & (si.name != current_docname) + ) + .run() ) total_delivered_qty = (flt(delivered_via_dn[0][0]) if delivered_via_dn else 0) + ( @@ -464,14 +477,11 @@ class SellingController(StockController): return total_delivered_qty def get_so_qty_and_warehouse(self, so_detail): - so_item = frappe.db.sql( - """select qty, warehouse from `tabSales Order Item` - where name = %s and docstatus = 1""", - so_detail, - as_dict=1, + so_item = frappe.db.get_value( + "Sales Order Item", {"name": so_detail, "docstatus": 1}, ["qty", "warehouse"], as_dict=True ) - so_qty = so_item and flt(so_item[0]["qty"]) or 0.0 - so_warehouse = so_item and so_item[0]["warehouse"] or "" + so_qty = flt(so_item.qty) if so_item else 0.0 + so_warehouse = (so_item.warehouse if so_item else "") or "" return so_qty, so_warehouse def check_sales_order_on_hold_or_close(self, ref_fieldname): diff --git a/erpnext/controllers/tests/test_selling_controller.py b/erpnext/controllers/tests/test_selling_controller.py new file mode 100644 index 00000000000..16002caaed2 --- /dev/null +++ b/erpnext/controllers/tests/test_selling_controller.py @@ -0,0 +1,39 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.tests.utils import ERPNextTestSuite + + +class TestSellingControllerConversions(ERPNextTestSuite): + @staticmethod + def _cancel_and_delete(doctype, name): + if not frappe.db.exists(doctype, name): + return + doc = frappe.get_doc(doctype, name) + if doc.docstatus == 1: + doc.cancel() + frappe.delete_doc(doctype, name, force=1) + + def test_partial_delivery_updates_sales_order_status(self): + # Submitting a Delivery Note against a Sales Order calls + # SellingController.get_already_delivered_qty / get_so_qty_and_warehouse and StatusUpdater + # (per_delivered via coalesce(sum(...))) -- all converted to query builder / ORM here. + from erpnext.selling.doctype.sales_order.mapper import make_delivery_note + from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + se = make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=20, basic_rate=100) + self.addCleanup(self._cancel_and_delete, "Stock Entry", se.name) + + so = make_sales_order(qty=10) + + dn = make_delivery_note(so.name) + dn.items[0].qty = 4 + dn.insert() + dn.submit() + self.addCleanup(self._cancel_and_delete, "Delivery Note", dn.name) + + so.reload() + self.assertEqual(so.per_delivered, 40.0)