Merge pull request #56212 from mihir-kandoi/pg-controllers-selling-status-website

refactor(controllers): selling/status_updater/website raw SQL → qb/ORM (Postgres)
This commit is contained in:
Mihir Kandoi
2026-06-21 07:46:39 +05:30
committed by GitHub
5 changed files with 112 additions and 48 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

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

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)

View File

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

View File

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