refactor(controllers): convert SellingController delivered-qty lookups to qb/ORM

get_already_delivered_qty used two raw frappe.db.sql sums (Delivery Note
Item, and Sales Invoice Item joined to Sales Invoice) and
get_so_qty_and_warehouse used a raw select. Convert to frappe.qb (Sum) and
frappe.db.get_value. Engine-portable and MariaDB-identical.

Adds a test (Sales Order -> partial Delivery Note) that asserts
per_delivered, exercising get_already_delivered_qty / get_so_qty_and_warehouse
(and the StatusUpdater percentage path) on both engines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mihir Kandoi
2026-06-21 05:10:17 +05:30
parent 4255059846
commit f793027800
2 changed files with 70 additions and 21 deletions

View File

@@ -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):

View File

@@ -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)