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/status_updater.py b/erpnext/controllers/status_updater.py index 1b0ee5cf6b7..ccff198fd89 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -5,6 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document +from frappe.query_builder.functions import Sum from frappe.utils import comma_or, flt, get_link_to_form, getdate, now, nowdate, safe_div @@ -554,7 +555,7 @@ class StatusUpdater(Document): args["second_source_extra_cond"] = "" args["second_source_condition"] = frappe.db.sql( - """ select ifnull((select sum({second_source_field}) + """ select coalesce((select sum({second_source_field}) from `tab{second_source_dt}` where `{second_join_field}`=%(detail_id)s and (`tab{second_source_dt}`.docstatus=1) @@ -569,7 +570,7 @@ class StatusUpdater(Document): args["source_dt_value"] = ( frappe.db.sql( """ - (select ifnull(sum({source_field}), 0) + (select coalesce(sum({source_field}), 0) from `tab{source_dt}` where `{join_field}`=%(detail_id)s and (docstatus=1 {cond}) {extra_cond}) """.format(**args), @@ -684,18 +685,10 @@ class StatusUpdater(Document): if not ref_docs: return - zero_amount_refdocs = frappe.db.sql_list( - f""" - SELECT - name - from - `tab{ref_dt}` - where - docstatus = 1 - and base_net_total = 0 - and name in %(ref_docs)s - """, - {"ref_docs": ref_docs}, + zero_amount_refdocs = frappe.get_all( + ref_dt, + filters={"docstatus": 1, "base_net_total": 0, "name": ["in", ref_docs]}, + pluck="name", ) if zero_amount_refdocs: @@ -703,20 +696,20 @@ class StatusUpdater(Document): def update_billing_status(self, zero_amount_refdoc, ref_dt, ref_fieldname): for ref_dn in zero_amount_refdoc: + ref_item = frappe.qb.DocType(f"{ref_dt} Item") ref_doc_qty = flt( - frappe.db.sql( - """select ifnull(sum(qty), 0) from `tab{} Item` - where parent={}""".format(ref_dt, "%s"), - (ref_dn), - )[0][0] + frappe.qb.from_(ref_item) + .select(Sum(ref_item.qty)) + .where(ref_item.parent == ref_dn) + .run()[0][0] ) + doc_item = frappe.qb.DocType(f"{self.doctype} Item") billed_qty = flt( - frappe.db.sql( - """select ifnull(sum(qty), 0) - from `tab{} Item` where {}={} and docstatus=1""".format(self.doctype, ref_fieldname, "%s"), - (ref_dn), - )[0][0] + frappe.qb.from_(doc_item) + .select(Sum(doc_item.qty)) + .where((doc_item[ref_fieldname] == ref_dn) & (doc_item.docstatus == 1)) + .run()[0][0] ) per_billed = safe_div(min(ref_doc_qty, billed_qty), ref_doc_qty) * 100 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) diff --git a/erpnext/controllers/tests/test_website_list_for_contact.py b/erpnext/controllers/tests/test_website_list_for_contact.py new file mode 100644 index 00000000000..7d4a80aeec1 --- /dev/null +++ b/erpnext/controllers/tests/test_website_list_for_contact.py @@ -0,0 +1,19 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import json + +from erpnext.tests.utils import ERPNextTestSuite + + +class TestWebsiteListForContact(ERPNextTestSuite): + def test_get_list_context_currency_symbols(self): + # get_list_context builds the enabled-currency symbol map via frappe.get_all (converted from + # raw SQL). Exercises that query and asserts a known enabled currency is present. + from erpnext.controllers.website_list_for_contact import get_list_context + + context = get_list_context() + + symbols = json.loads(context["currency_symbols"]) + self.assertIsInstance(symbols, dict) + self.assertIn("USD", symbols) diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py index 7552b226285..07798305200 100644 --- a/erpnext/controllers/website_list_for_contact.py +++ b/erpnext/controllers/website_list_for_contact.py @@ -17,9 +17,12 @@ def get_list_context(context=None): "currency": frappe.db.get_default("currency"), "currency_symbols": json.dumps( dict( - frappe.db.sql( - """select name, symbol - from tabCurrency where enabled=1""" + frappe.get_all( + "Currency", + filters={"enabled": 1}, + fields=["name", "symbol"], + as_list=True, + limit_page_length=0, # all enabled currencies are needed for the symbol map ) ) ),