From 935746e752c289bedb16c9b0a8c9141b6cff3b31 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 16 Jun 2026 23:28:53 +0530 Subject: [PATCH] Revert "refactor(selling, buying): make raw SQL portable to PostgreSQL (parity rollout 1/9)" --- .../purchase_order/test_purchase_order.py | 14 +- .../supplier_scorecard_variable.py | 13 +- .../procurement_tracker.py | 4 +- .../purchase_order_analysis.py | 4 +- .../requested_items_to_order_and_receive.py | 20 ++- erpnext/selling/doctype/customer/customer.py | 10 +- .../selling/doctype/quotation/quotation.py | 44 ++---- .../doctype/quotation/test_quotation.py | 10 +- .../doctype/sales_order/test_sales_order.py | 25 +-- .../page/point_of_sale/point_of_sale.py | 149 ++++++++---------- .../selling/page/sales_funnel/sales_funnel.py | 57 +++---- .../available_stock_for_packing_items.py | 93 +++++------ .../customer_acquisition_and_loyalty.py | 23 +-- .../customer_credit_balance.py | 21 ++- .../inactive_customers/inactive_customers.py | 11 +- .../payment_terms_status_for_sales_order.py | 17 +- .../pending_so_items_for_purchase_request.py | 40 +++-- .../report/sales_analytics/sales_analytics.py | 27 ++-- .../sales_order_analysis.py | 135 ++++++++-------- 19 files changed, 319 insertions(+), 398 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 39c618e975e..fe406d6ad3a 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -477,8 +477,10 @@ class TestPurchaseOrder(ERPNextTestSuite): item_doc.save() else: # update valid from - frappe.db.set_value( - "Item Tax", {"parent": item, "item_tax_template": tax_template}, "valid_from", nowdate() + frappe.db.sql( + """UPDATE `tabItem Tax` set valid_from = CURRENT_DATE + where parent = %(item)s and item_tax_template = %(tax)s""", + {"item": item, "tax": tax_template}, ) po = create_purchase_order(item_code=item, qty=1, do_not_save=1) @@ -525,8 +527,10 @@ class TestPurchaseOrder(ERPNextTestSuite): self.assertEqual(po.taxes[1].total, 840) # teardown - frappe.db.set_value( - "Item Tax", {"parent": item, "item_tax_template": tax_template}, "valid_from", None + frappe.db.sql( + """UPDATE `tabItem Tax` set valid_from = NULL + where parent = %(item)s and item_tax_template = %(tax)s""", + {"item": item, "tax": tax_template}, ) po.cancel() po.delete() @@ -648,7 +652,7 @@ class TestPurchaseOrder(ERPNextTestSuite): def test_purchase_order_on_hold(self): po = create_purchase_order(item_code="_Test Product Bundle Item") - po.db_set("status", "On Hold") + po.db_set("Status", "On Hold") pi = make_pi_from_po(po.name) pr = make_purchase_receipt(po.name) self.assertRaises(frappe.ValidationError, pr.submit) diff --git a/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py b/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py index 99b780375fa..d7edd57b18a 100644 --- a/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py +++ b/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py @@ -7,7 +7,7 @@ import sys import frappe from frappe import _ from frappe.model.document import Document -from frappe.query_builder.functions import DateDiff, Sum +from frappe.query_builder.functions import Sum from frappe.utils import getdate @@ -68,7 +68,7 @@ def get_item_workdays(scorecard): frappe.qb.from_(PO_Item) .join(PO) .on(PO_Item.parent == PO.name) - .select(Sum(DateDiff(scorecard.end_date, PO_Item.schedule_date) * (PO_Item.qty))) + .select(Sum(frappe.qb.fn.DATEDIFF(scorecard.end_date, PO_Item.schedule_date) * (PO_Item.qty))) .where(PO.supplier == scorecard.supplier) .where(PO_Item.received_qty < PO_Item.qty) .where(PO_Item.schedule_date[scorecard.start_date : scorecard.end_date]) # Équivalent du BETWEEN @@ -153,7 +153,7 @@ def get_total_days_late(scorecard): .on(PR_Item.purchase_order_item == PO_Item.name) .join(PO) .on(PO_Item.parent == PO.name) - .select(Sum(DateDiff(PR.posting_date, PO_Item.schedule_date) * PR_Item.qty)) + .select(Sum(frappe.qb.fn.DATEDIFF(PR.posting_date, PO_Item.schedule_date) * PR_Item.qty)) .where(PO.supplier == scorecard.supplier) .where(PO_Item.schedule_date[scorecard.start_date : scorecard.end_date]) .where(PO_Item.schedule_date < PR.posting_date) @@ -170,7 +170,10 @@ def get_total_days_late(scorecard): .join(PO) .on(PO_Item.parent == PO.name) .select( - Sum(DateDiff(scorecard.end_date, PO_Item.schedule_date) * (PO_Item.qty - PO_Item.received_qty)) + Sum( + frappe.qb.fn.DATEDIFF(scorecard.end_date, PO_Item.schedule_date) + * (PO_Item.qty - PO_Item.received_qty) + ) ) .where(PO.supplier == scorecard.supplier) .where(PO_Item.received_qty < PO_Item.qty) @@ -527,7 +530,7 @@ def get_rfq_response_days(scorecard): .on(sq_item.request_for_quotation_item == rfq_item.name) .join(sq) .on(sq_item.parent == sq.name) - .select(frappe.qb.fn.Sum(DateDiff(sq.transaction_date, rfq.transaction_date))) + .select(frappe.qb.fn.Sum(frappe.qb.fn.Datediff(sq.transaction_date, rfq.transaction_date))) .where(rfq_sup.supplier == scorecard.supplier) .where(sq.supplier == scorecard.supplier) .where(rfq.transaction_date[scorecard.start_date : scorecard.end_date]) diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py index fd30ddf6884..10169c554fb 100644 --- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py @@ -305,9 +305,7 @@ def get_po_entries(filters): & (parent.name == child.parent) & (parent.status.notin(("Closed", "Completed", "Cancelled"))) ) - # This is one row per PO item; the selected child.* columns are only functionally dependent - # on the child PK, which postgres requires in the GROUP BY (MariaDB allows omitting it). - .groupby(parent.name, child.material_request_item, child.name) + .groupby(parent.name, child.material_request_item) ) query = apply_filters_on_query(filters, parent, child, query) diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py index 5522aac1044..b6bf1d9f8da 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py @@ -71,9 +71,7 @@ def get_data(filters): po_item.name, ) .where((po_item.parent == po.name) & (po.status.notin(("Stopped", "On Hold"))) & (po.docstatus == 1)) - # the selected po.* columns need the Purchase Order PK grouped on postgres; po.name is 1:1 - # with the grouped po_item.name, so groups are unchanged. - .groupby(po_item.name, po.name) + .groupby(po_item.name) .orderby(po.transaction_date) ) diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py index b6f9bb13795..55189a722a3 100644 --- a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py +++ b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py @@ -6,7 +6,7 @@ import copy import frappe from frappe import _ -from frappe.query_builder.functions import Coalesce, Max, Sum +from frappe.query_builder.functions import Coalesce, Sum from frappe.utils import cint, date_diff, flt, getdate @@ -44,15 +44,13 @@ def get_data(filters): .on(mr_item.parent == mr.name) .select( mr.name.as_("material_request"), - # non-grouped columns are constant per grouped mr.name / item_code -> Max() keeps the - # GROUP BY valid on postgres while returning the same value MySQL picked. - Max(mr.transaction_date).as_("date"), - Max(mr_item.schedule_date).as_("required_date"), + mr.transaction_date.as_("date"), + mr_item.schedule_date.as_("required_date"), mr_item.item_code.as_("item_code"), Sum(Coalesce(mr_item.qty, 0)).as_("qty"), Sum(Coalesce(mr_item.stock_qty, 0)).as_("stock_qty"), - Max(Coalesce(mr_item.uom, "")).as_("uom"), - Max(Coalesce(mr_item.stock_uom, "")).as_("stock_uom"), + Coalesce(mr_item.uom, "").as_("uom"), + Coalesce(mr_item.stock_uom, "").as_("stock_uom"), Sum(Coalesce(mr_item.ordered_qty, 0)).as_("ordered_qty"), Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"), (Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.received_qty, 0))).as_( @@ -60,9 +58,9 @@ def get_data(filters): ), Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"), (Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.ordered_qty, 0))).as_("qty_to_order"), - Max(mr_item.item_name).as_("item_name"), - Max(mr_item.description).as_("description"), - Max(mr.company).as_("company"), + mr_item.item_name, + mr_item.description, + mr.company, ) .where( (mr.material_request_type == "Purchase") @@ -74,7 +72,7 @@ def get_data(filters): query = get_conditions(filters, query, mr, mr_item) # add conditional conditions - query = query.groupby(mr.name, mr_item.item_code).orderby(Max(mr.transaction_date), Max(mr.schedule_date)) + query = query.groupby(mr.name, mr_item.item_code).orderby(mr.transaction_date, mr.schedule_date) data = query.run(as_dict=True) return data diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index c8dbfb797e5..a79bc9e935a 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -15,7 +15,7 @@ from frappe.model.document import Document from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options from frappe.model.utils.rename_doc import update_linked_doctypes from frappe.query_builder import CustomFunction, Field, functions -from frappe.query_builder.functions import Cast, Coalesce, Max +from frappe.query_builder.functions import Cast, Coalesce, Max, Substring from frappe.utils import cint, cstr, flt, get_formatted_email, today from frappe.utils.user import get_users_with_role @@ -128,11 +128,9 @@ class Customer(TransactionBase): Customer = frappe.qb.DocType("Customer") if frappe.db.db_type == "postgres": - # Postgres: take the token after the last space (mirrors MariaDB - # SUBSTRING_INDEX(name, ' ', -1)) and cast to int. (pypika's Substring is start/length, - # not a regex, so it can't be used here; UNSIGNED also doesn't exist on postgres.) - regexp_replace = CustomFunction("regexp_replace", ["source", "pattern", "replacement"]) - extracted_part = regexp_replace(Customer.name, "^.* ", "") + # Postgres: extract trailing digits (e.g. "Customer - 3") and cast to int. + # NOTE: PostgreSQL is strict about types; MySQL's UNSIGNED cast does not exist. + extracted_part = Substring(Customer.name, r"\d+$") casted_part = Cast(extracted_part, "INTEGER") else: # MariaDB/MySQL: keep existing behavior. diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 8eaf33c084d..b2d6e7838a1 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -6,7 +6,6 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import getdate, nowdate -from pypika.terms import ExistsCriterion from erpnext.controllers.selling_controller import SellingController @@ -359,31 +358,22 @@ def get_list_context(context=None): def set_expired_status(): - quotation = frappe.qb.DocType("Quotation") - so = frappe.qb.DocType("Sales Order") - so_item = frappe.qb.DocType("Sales Order Item") + # filter out submitted non expired quotations whose validity has been ended + cond = "`tabQuotation`.docstatus = 1 and `tabQuotation`.status NOT IN ('Expired', 'Lost') and `tabQuotation`.valid_till < %s" + # check if those QUO have SO against it + so_against_quo = """ + SELECT + so.name FROM `tabSales Order` so, `tabSales Order Item` so_item + WHERE + so_item.docstatus = 1 and so.docstatus = 1 + and so_item.parent = so.name + and so_item.prevdoc_docname = `tabQuotation`.name""" - # submitted Sales Orders raised against the quotation (correlated to the quotation being updated) - so_against_quo = ( - frappe.qb.from_(so) - .from_(so_item) - .select(so.name) - .where( - (so_item.docstatus == 1) - & (so.docstatus == 1) - & (so_item.parent == so.name) - & (so_item.prevdoc_docname == quotation.name) - ) + # if not exists any SO, set status as Expired + frappe.db.multisql( + { + "mariadb": f"""UPDATE `tabQuotation` SET `tabQuotation`.status = 'Expired' WHERE {cond} and not exists({so_against_quo})""", + "postgres": f"""UPDATE `tabQuotation` SET status = 'Expired' FROM `tabSales Order`, `tabSales Order Item` WHERE {cond} and not exists({so_against_quo})""", + }, + (nowdate()), ) - - # expire submitted, non-expired/lost quotations whose validity has ended and that have no SO - ( - frappe.qb.update(quotation) - .set(quotation.status, "Expired") - .where( - (quotation.docstatus == 1) - & (quotation.status.notin(["Expired", "Lost"])) - & (quotation.valid_till < nowdate()) - & ExistsCriterion(so_against_quo).negate() - ) - ).run() diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 57e2317cfea..fd2b40d3a18 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -1082,19 +1082,13 @@ class TestQuotation(ERPNextTestSuite): @ERPNextTestSuite.change_settings("Accounts Settings", {"allow_pegged_currencies_exchange_rates": True}) def test_make_quotation_qar_to_inr(self): - # Seed the pegged base rate in cache so the conversion is deterministic and doesn't depend - # on the external exchange-rate API (flaky/unreachable in CI) or on a record left by another - # test. get_exchange_rate reads this key, applies the QAR peg (/3.64); the assertion below - # reads the same key. - cache = frappe.cache() - key = "currency_exchange_rate_{}:{}:{}".format("2026-01-01", "QAR", "INR") - cache.setex(name=key, time=21600, value=flt(218.4)) - quotation = make_quotation( currency="QAR", transaction_date="2026-01-01", ) + cache = frappe.cache() + key = "currency_exchange_rate_{}:{}:{}".format("2026-01-01", "QAR", "INR") value = cache.get(key) expected_rate = flt(value) / 3.64 diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index f74cd1af124..45d3933a017 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -907,8 +907,10 @@ class TestSalesOrder(ERPNextTestSuite): item_doc.save() else: # update valid from - frappe.db.set_value( - "Item Tax", {"parent": item, "item_tax_template": tax_template}, "valid_from", nowdate() + frappe.db.sql( + """UPDATE `tabItem Tax` set valid_from = CURRENT_DATE + where parent = %(item)s and item_tax_template = %(tax)s""", + {"item": item, "tax": tax_template}, ) so = make_sales_order(item_code=item, qty=1, do_not_save=1) @@ -958,8 +960,10 @@ class TestSalesOrder(ERPNextTestSuite): self.assertEqual(so.taxes[1].total, 480) # teardown - frappe.db.set_value( - "Item Tax", {"parent": item, "item_tax_template": tax_template}, "valid_from", None + frappe.db.sql( + """UPDATE `tabItem Tax` set valid_from = NULL + where parent = %(item)s and item_tax_template = %(tax)s""", + {"item": item, "tax": tax_template}, ) so.cancel() so.delete() @@ -1553,12 +1557,11 @@ class TestSalesOrder(ERPNextTestSuite): # Check if Work Orders were raised for item in so_item_name: - wo_qty = frappe.get_all( - "Work Order", - filters={"sales_order": so.name, "sales_order_item": item}, - fields=[{"SUM": "qty", "as": "qty"}], + wo_qty = frappe.db.sql( + "select sum(qty) from `tabWork Order` where sales_order=%s and sales_order_item=%s", + (so.name, item), ) - self.assertEqual(wo_qty[0].qty, so_item_name.get(item)) + self.assertEqual(wo_qty[0][0], so_item_name.get(item)) def test_advance_payment_entry_unlink_against_sales_order(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry @@ -1737,7 +1740,9 @@ class TestSalesOrder(ERPNextTestSuite): mr_dict["include_exploded_items"] = 0 mr_dict["ignore_existing_ordered_qty"] = 1 make_raw_material_request(mr_dict, so.company, so.name) - mr = frappe.get_all("Material Request", fields=["name"], order_by="creation desc", limit=1)[0] + mr = frappe.db.sql( + """select name from `tabMaterial Request` ORDER BY creation DESC LIMIT 1""", as_dict=1 + )[0] mr_doc = frappe.get_doc("Material Request", mr.get("name")) self.assertEqual(mr_doc.items[0].sales_order, so.name) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index e81f9eb051f..df675272c68 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -5,7 +5,7 @@ import json import frappe -from frappe.query_builder import Criterion, DocType, Order +from frappe.query_builder import DocType, Order from frappe.utils import cint, get_datetime from frappe.utils.nestedset import get_root_of @@ -155,55 +155,50 @@ def get_items( if not frappe.db.exists("Item Group", item_group): item_group = get_root_of("Item Group") + condition = get_conditions(search_term) + condition += get_item_group_condition(pos_profile) + lft, rgt = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"]) - item = frappe.qb.DocType("Item") - item_group_dt = frappe.qb.DocType("Item Group") + bin_join_selection, bin_join_condition = "", "" + if hide_unavailable_items: + bin_join_selection = "LEFT JOIN `tabBin` bin ON bin.item_code = item.name" + bin_join_condition = "AND (item.is_stock_item = 0 OR (item.is_stock_item = 1 AND bin.warehouse = %(warehouse)s AND bin.actual_qty > 0))" - item_group_subquery = ( - frappe.qb.from_(item_group_dt) - .select(item_group_dt.name) - .where((item_group_dt.lft >= lft) & (item_group_dt.rgt <= rgt)) - ) - - query = ( - frappe.qb.from_(item) - .select( - item.name.as_("item_code"), + items_data = frappe.db.sql( + """ + SELECT + item.name AS item_code, item.item_name, item.description, item.stock_uom, - item.image.as_("item_image"), + item.image AS item_image, item.is_stock_item, - item.sales_uom, - ) - .where( - (item.disabled == 0) - & (item.has_variants == 0) - & (item.is_sales_item == 1) - & (item.is_fixed_asset == 0) - & (item.item_group.isin(item_group_subquery)) - & get_conditions(search_term, item) - ) - ) - - item_group_condition = get_item_group_condition(pos_profile, item) - if item_group_condition is not None: - query = query.where(item_group_condition) - - if hide_unavailable_items: - bin = frappe.qb.DocType("Bin") - query = ( - query.left_join(bin) - .on(bin.item_code == item.name) - .where( - (item.is_stock_item == 0) - | ((item.is_stock_item == 1) & (bin.warehouse == warehouse) & (bin.actual_qty > 0)) - ) - ) - - items_data = ( - query.orderby(item.name, order=Order.asc).limit(cint(page_length)).offset(cint(start)).run(as_dict=1) + item.sales_uom + FROM + `tabItem` item {bin_join_selection} + WHERE + item.disabled = 0 + AND item.has_variants = 0 + AND item.is_sales_item = 1 + AND item.is_fixed_asset = 0 + AND item.item_group in (SELECT name FROM `tabItem Group` WHERE lft >= {lft} AND rgt <= {rgt}) + AND {condition} + {bin_join_condition} + ORDER BY + item.name asc + LIMIT + {page_length} offset {start}""".format( + start=cint(start), + page_length=cint(page_length), + lft=cint(lft), + rgt=cint(rgt), + condition=condition, + bin_join_selection=bin_join_selection, + bin_join_condition=bin_join_condition, + ), + {"warehouse": warehouse}, + as_dict=1, ) # return (empty) list if there are no results @@ -274,62 +269,56 @@ def search_for_serial_or_batch_or_barcode_number(search_value: str) -> dict[str, return scan_barcode(search_value) -def get_conditions(search_term, item=None): - if item is None: - item = frappe.qb.DocType("Item") +def get_conditions(search_term): + condition = "(" + condition += """item.name like {search_term} + or item.item_name like {search_term}""".format(search_term=frappe.db.escape("%" + search_term + "%")) + condition += add_search_fields_condition(search_term) + condition += ")" - pattern = f"%{search_term}%" - conditions = [item.name.like(pattern), item.item_name.like(pattern)] - conditions += add_search_fields_condition(search_term, item) - - return Criterion.any(conditions) + return condition -def add_search_fields_condition(search_term, item=None): - if item is None: - item = frappe.qb.DocType("Item") - - pattern = f"%{search_term}%" - conditions = [] +def add_search_fields_condition(search_term): + condition = "" search_fields = frappe.get_all("POS Search Fields", fields=["fieldname"]) - for field in search_fields: - if not field.get("fieldname"): - continue - conditions.append(item[field["fieldname"]].like(pattern)) - - return conditions + if search_fields: + for field in search_fields: + if not field.get("fieldname"): + continue + condition += " or item.`{}` like {}".format( + field["fieldname"], frappe.db.escape("%" + search_term + "%") + ) + return condition -def get_item_group_condition(pos_profile, item=None): - if item is None: - item = frappe.qb.DocType("Item") - +def get_item_group_condition(pos_profile): + cond = "and 1=1" item_groups = get_item_groups(pos_profile) if item_groups: - return item.item_group.isin(item_groups) + cond = "and item.item_group in (%s)" % (", ".join(["%s"] * len(item_groups))) - return None + return cond % tuple(item_groups) @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def item_group_query(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict): + item_groups = [] + cond = "1=1" pos_profile = filters.get("pos_profile") - item_filters = [["name", "like", f"%{txt}%"]] if pos_profile: item_groups = get_item_groups(pos_profile) - if item_groups: - item_filters.append(["name", "in", item_groups]) - return frappe.get_all( - "Item Group", - filters=item_filters, - fields=["name"], - distinct=True, - limit_start=start, - limit_page_length=page_len, - as_list=True, + if item_groups: + cond = "name in (%s)" % (", ".join(["%s"] * len(item_groups))) + cond = cond % tuple(item_groups) + + return frappe.db.sql( + f""" select distinct name from `tabItem Group` + where {cond} and (name like %(txt)s) limit {page_len} offset {start}""", + {"txt": "%%%s%%" % txt}, ) diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.py b/erpnext/selling/page/sales_funnel/sales_funnel.py index 6ce192e95e2..11fd698c27b 100644 --- a/erpnext/selling/page/sales_funnel/sales_funnel.py +++ b/erpnext/selling/page/sales_funnel/sales_funnel.py @@ -5,7 +5,6 @@ from itertools import groupby import frappe from frappe import _ -from frappe.query_builder.functions import Count, Date from frappe.utils import flt from erpnext.accounts.report.utils import convert @@ -23,47 +22,33 @@ def validate_filters(from_date, to_date, company): def get_funnel_data(from_date: str, to_date: str, company: str): validate_filters(from_date, to_date, company) - lead = frappe.qb.DocType("Lead") - active_leads = ( - frappe.qb.from_(lead) - .select(Count("*")) - .where(Date(lead.creation).between(from_date, to_date) & (lead.company == company)) - .run() + active_leads = frappe.db.sql( + """select count(*) from `tabLead` + where (date(`creation`) between %s and %s) + and company=%s""", + (from_date, to_date, company), )[0][0] - opportunity = frappe.qb.DocType("Opportunity") - opportunities = ( - frappe.qb.from_(opportunity) - .select(Count("*")) - .where( - Date(opportunity.creation).between(from_date, to_date) - & (opportunity.opportunity_from == "Lead") - & (opportunity.company == company) - ) - .run() + opportunities = frappe.db.sql( + """select count(*) from `tabOpportunity` + where (date(`creation`) between %s and %s) + and opportunity_from='Lead' and company=%s""", + (from_date, to_date, company), )[0][0] - quotation = frappe.qb.DocType("Quotation") - quotations = ( - frappe.qb.from_(quotation) - .select(Count("*")) - .where( - (quotation.docstatus == 1) - & Date(quotation.creation).between(from_date, to_date) - & ((quotation.opportunity != "") | (quotation.quotation_to == "Lead")) - & (quotation.company == company) - ) - .run() + quotations = frappe.db.sql( + """select count(*) from `tabQuotation` + where docstatus = 1 and (date(`creation`) between %s and %s) + and (opportunity!="" or quotation_to="Lead") and company=%s""", + (from_date, to_date, company), )[0][0] - customer = frappe.qb.DocType("Customer") - converted = ( - frappe.qb.from_(customer) - .inner_join(lead) - .on(lead.name == customer.lead_name) - .select(Count("*")) - .where(Date(customer.creation).between(from_date, to_date) & (lead.company == company)) - .run() + converted = frappe.db.sql( + """select count(*) from `tabCustomer` + JOIN `tabLead` ON `tabLead`.name = `tabCustomer`.lead_name + WHERE (date(`tabCustomer`.creation) between %s and %s) + and `tabLead`.company=%s""", + (from_date, to_date, company), )[0][0] return [ diff --git a/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py b/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py index 0c93800bd8b..b312abd4607 100644 --- a/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py +++ b/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py @@ -12,7 +12,7 @@ def execute(filters=None): columns = get_columns() iwq_map = get_item_warehouse_quantity_map() - item_map = get_item_details(list(iwq_map.keys())) + item_map = get_item_details() data = [] for sbom, warehouse in iwq_map.items(): total = 0 @@ -53,67 +53,48 @@ def get_columns(): return columns -def get_item_details(item_codes): - # only the bundle items actually shown in the report need detail lookup, not the whole catalogue - if not item_codes: - return {} +def get_item_details(): item_map = {} - for item in frappe.get_all( - "Item", - filters={"name": ["in", item_codes]}, - fields=["name", "item_name", "description", "stock_uom"], + for item in frappe.db.sql( + """SELECT name, item_name, description, stock_uom + from `tabItem`""", + as_dict=1, ): item_map.setdefault(item.name, item) return item_map def get_item_warehouse_quantity_map(): - # Components of every active product bundle: (bundle item code, component item, qty per bundle) - pb = frappe.qb.DocType("Product Bundle") - pbi = frappe.qb.DocType("Product Bundle Item") - bundle_components = ( - frappe.qb.from_(pbi) - .inner_join(pb) - .on(pbi.parent == pb.name) - .select(pb.new_item_code.as_("parent"), pbi.item_code, pbi.qty) - .where((pb.is_active == 1) & (pb.docstatus == 1)) - .run(as_dict=True) - ) - - if not bundle_components: - return {} - - component_items = list({c.item_code for c in bundle_components}) - - bin_projected = { - (b.item_code, b.warehouse): flt(b.projected_qty) - for b in frappe.get_all( - "Bin", - filters={"item_code": ["in", component_items]}, - fields=["item_code", "warehouse", "projected_qty"], - ) - } - - # Only warehouses that hold at least one component can yield a non-zero packable qty; a warehouse - # missing any component yields MIN()=0 and is dropped below, so scanning every warehouse in the - # system is wasted work. Scope the loop to warehouses present in the Bin result. - bin_warehouses = {wh for (_, wh) in bin_projected} - - # For each (bundle, warehouse) the number of complete bundles that can be packed is the - # MIN over components of (component projected_qty in that warehouse / component qty per bundle). - # A component with no Bin in a warehouse contributes 0 (the original UNION ALL/NOT EXISTS branch). - packable_qty = {} - for component in bundle_components: - if not component.qty: - continue - for warehouse in bin_warehouses: - qty = bin_projected.get((component.item_code, warehouse), 0) / flt(component.qty) - key = (component.parent, warehouse) - packable_qty[key] = min(packable_qty[key], qty) if key in packable_qty else qty - + query = """SELECT parent, warehouse, MIN(qty) AS qty + FROM (SELECT b.parent, bi.item_code, bi.warehouse, + sum(bi.projected_qty) / b.qty AS qty + FROM tabBin AS bi, (SELECT pb.new_item_code as parent, b.item_code, b.qty, w.name + FROM `tabProduct Bundle Item` b, `tabWarehouse` w, + `tabProduct Bundle` pb + where b.parent = pb.name + and pb.is_active = 1 and pb.docstatus = 1) AS b + WHERE bi.item_code = b.item_code + AND bi.warehouse = b.name + GROUP BY b.parent, b.item_code, bi.warehouse + UNION ALL + SELECT b.parent, b.item_code, b.name, 0 AS qty + FROM (SELECT pb.new_item_code as parent, b.item_code, b.qty, w.name + FROM `tabProduct Bundle Item` b, `tabWarehouse` w, + `tabProduct Bundle` pb + where b.parent = pb.name + and pb.is_active = 1 and pb.docstatus = 1) AS b + WHERE NOT EXISTS(SELECT * + FROM `tabBin` AS bi + WHERE bi.item_code = b.item_code + AND bi.warehouse = b.name)) AS r + GROUP BY parent, warehouse + HAVING MIN(qty) != 0""" + result = frappe.db.sql(query, as_dict=1) + last_sbom = "" sbom_map = {} - for (parent, warehouse), qty in packable_qty.items(): - if qty != 0: # HAVING MIN(qty) != 0 - sbom_map.setdefault(parent, {})[warehouse] = qty - + for line in result: + if line.get("parent") != last_sbom: + last_sbom = line.get("parent") + actual_dict = sbom_map.setdefault(last_sbom, {}) + actual_dict.setdefault(line.get("warehouse"), line.get("qty")) return sbom_map diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py index 89493e57510..dcdd2525d8e 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py @@ -112,8 +112,8 @@ def get_data_by_territory(filters, common_columns): customers_in = get_customer_stats(filters, tree_view=True) territory_dict = {} - for t in frappe.get_all( - "Territory", fields=["name", "lft", "parent_territory", "is_group"], order_by="lft" + for t in frappe.db.sql( + """SELECT name, lft, parent_territory, is_group FROM `tabTerritory` ORDER BY lft""", as_dict=1 ): territory_dict.update({t.name: {"parent": t.parent_territory, "is_group": t.is_group}}) @@ -155,18 +155,19 @@ def get_data_by_territory(filters, common_columns): def get_customer_stats(filters, tree_view=False): """Calculates number of new and repeated customers and revenue.""" + company_condition = "" + if filters.get("company"): + company_condition = " and company=%(company)s" + customers = [] customers_in = {} - si_filters = {"docstatus": 1, "posting_date": ["<=", filters.get("to_date")]} - if filters.get("company"): - si_filters["company"] = filters.get("company") - - for si in frappe.get_all( - "Sales Invoice", - filters=si_filters, - fields=["territory", "posting_date", "customer", "base_grand_total"], - order_by="posting_date", + for si in frappe.db.sql( + f"""select territory, posting_date, customer, base_grand_total from `tabSales Invoice` + where docstatus=1 and posting_date <= %(to_date)s + {company_condition} order by posting_date""", + filters, + as_dict=1, ): key = si.territory if tree_view else si.posting_date.strftime("%Y-%m") new_or_repeat = "new" if si.customer not in customers else "repeat" diff --git a/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py b/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py index 4d3172c177f..6813060d414 100644 --- a/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py +++ b/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py @@ -77,18 +77,17 @@ def get_columns(customer_naming_type): def get_details(filters): - c = frappe.qb.DocType("Customer") - ccl = frappe.qb.DocType("Customer Credit Limit") - query = ( - frappe.qb.from_(c) - .inner_join(ccl) - .on(c.name == ccl.parent) - .select(c.name, c.customer_name, ccl.bypass_credit_limit_check, c.is_frozen, c.disabled) - .where(ccl.company == filters.get("company")) - ) + sql_query = """SELECT + c.name, c.customer_name, + ccl.bypass_credit_limit_check, + c.is_frozen, c.disabled + FROM `tabCustomer` c, `tabCustomer Credit Limit` ccl + WHERE + c.name = ccl.parent + AND ccl.company = %(company)s""" # customer filter is optional. if filters.get("customer"): - query = query.where(c.name == filters.get("customer")) + sql_query += " AND c.name = %(customer)s" - return query.run(as_dict=1) + return frappe.db.sql(sql_query, filters, as_dict=1) diff --git a/erpnext/selling/report/inactive_customers/inactive_customers.py b/erpnext/selling/report/inactive_customers/inactive_customers.py index 2dedb346601..8c7c7b99a32 100644 --- a/erpnext/selling/report/inactive_customers/inactive_customers.py +++ b/erpnext/selling/report/inactive_customers/inactive_customers.py @@ -4,8 +4,8 @@ import frappe from frappe import _ -from frappe.query_builder import Case -from frappe.query_builder.functions import Count, CurDate, DateDiff, Max, Sum +from frappe.query_builder import Case, CustomFunction +from frappe.query_builder.functions import Count, Max, Sum from frappe.utils import cint @@ -37,6 +37,9 @@ def get_sales_details(doctype): customer = frappe.qb.DocType("Customer") sales_doctype = frappe.qb.DocType(doctype) + date_diff = CustomFunction("DATEDIFF", ["d1", "d2"]) + current_date = CustomFunction("CURRENT_DATE", []) + if doctype == "Sales Order": total_considered = Sum( Case() @@ -52,9 +55,7 @@ def get_sales_details(doctype): date_col = sales_doctype.posting_date last_order_date = Max(date_col) - # DateDiff is cross-database (DATEDIFF on MariaDB, date subtraction on postgres); CurDate() - # renders the bare CURRENT_DATE keyword. Yields the integer number of days. - days_since_last_order = DateDiff(CurDate(), last_order_date) + days_since_last_order = date_diff(current_date(), last_order_date) return ( frappe.qb.from_(customer) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index 1f9c2ceb442..eb2b7cc21d8 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -3,8 +3,7 @@ import frappe from frappe import _, qb, query_builder -from frappe.query_builder import Criterion -from frappe.query_builder.functions import Max +from frappe.query_builder import Criterion, functions from frappe.utils.dateutils import getdate @@ -186,6 +185,9 @@ def get_so_with_invoices(filters): conditions = get_conditions(filters) filter_criterions = build_filter_criterions(filters) + datediff = query_builder.CustomFunction("DATEDIFF", ["cur_date", "due_date"]) + ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"]) + query_so = ( qb.from_(so) .join(soi) @@ -197,8 +199,7 @@ def get_so_with_invoices(filters): .select( so.customer, so.transaction_date.as_("submitted"), - # CASE + a Python date is portable; MySQL's IF()/DATEDIFF()/CURDATE() don't exist on postgres - query_builder.Case().when(ps.due_date < getdate(), "Overdue").else_("Unpaid").as_("status"), + ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"), ps.payment_term, ps.description, ps.due_date, @@ -229,13 +230,7 @@ def get_so_with_invoices(filters): .on(si.name == sii.parent) .inner_join(soi) .on(soi.name == sii.so_detail) - .select( - # grouped by the invoice (sii.parent); sales_order is arbitrary per invoice on MySQL and - # base_grand_total is constant per invoice -> Max() keeps the GROUP BY postgres-valid. - Max(sii.sales_order).as_("sales_order"), - sii.parent.as_("invoice"), - Max(si.base_grand_total).as_("invoice_amount"), - ) + .select(sii.sales_order, sii.parent.as_("invoice"), si.base_grand_total.as_("invoice_amount")) .where((sii.sales_order.isin([x.name for x in sorders])) & (si.docstatus == 1)) .groupby(sii.parent) ) diff --git a/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py b/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py index 7d7ea42209f..82143a11eea 100644 --- a/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py +++ b/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py @@ -4,7 +4,6 @@ import frappe from frappe import _ -from frappe.query_builder.functions import Max, Sum from frappe.utils import flt @@ -50,28 +49,27 @@ def get_columns(): def get_data(): - so = frappe.qb.DocType("Sales Order") - so_item = frappe.qb.DocType("Sales Order Item") - sales_order_entry = ( - frappe.qb.from_(so) - .inner_join(so_item) - .on(so.name == so_item.parent) - .select( + sales_order_entry = frappe.db.sql( + """ + SELECT so_item.item_code, - # non-grouped columns are constant per grouped so.name / item_code -> Max() keeps the - # GROUP BY valid on postgres while returning the same value MySQL picked. - Max(so_item.item_name).as_("item_name"), - Max(so_item.description).as_("description"), + so_item.item_name, + so_item.description, so.name, - Max(so.transaction_date).as_("transaction_date"), - Max(so.customer).as_("customer"), - Max(so.territory).as_("territory"), - Sum(so_item.qty).as_("total_qty"), - Max(so.company).as_("company"), - ) - .where((so.docstatus == 1) & so.status.notin(["Closed", "Completed", "Cancelled"])) - .groupby(so.name, so_item.item_code) - .run(as_dict=1) + so.transaction_date, + so.customer, + so.territory, + sum(so_item.qty) as total_qty, + so.company + FROM `tabSales Order` so, `tabSales Order Item` so_item + WHERE + so.docstatus = 1 + and so.name = so_item.parent + and so.status not in ('Closed','Completed','Cancelled') + GROUP BY + so.name,so_item.item_code + """, + as_dict=1, ) sales_orders = [row.name for row in sales_order_entry] diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index 93f1abe8222..e36690b4384 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -510,10 +510,10 @@ class Analytics: self.depth_map = frappe._dict() - self.group_entries = frappe.get_all( - self.filters.tree_type, - fields=["name", "lft", "rgt", f"{parent} as parent"], - order_by="lft", + self.group_entries = frappe.db.sql( + f"""select name, lft, rgt , {parent} as parent + from `tab{self.filters.tree_type}` order by lft""", + as_dict=1, ) for d in self.group_entries: @@ -528,19 +528,14 @@ class Analytics: if not frappe.db.exists("DocType", self.filters.doc_type): frappe.throw(_("Invalid Document Type {0}").format(self.filters.doc_type)) - order_types = frappe.get_all( - self.filters.doc_type, - filters={"order_type": ["is", "set"]}, - pluck="order_type", - distinct=True, - order_by="order_type", + self.group_entries = frappe.db.sql( + f""" select * from (select "Order Types" as name, 0 as lft, + 2 as rgt, '' as parent union select distinct order_type as name, 1 as lft, 1 as rgt, "Order Types" as parent + from `tab{self.filters.doc_type}` where ifnull(order_type, '') != '') as b order by lft, name + """, + as_dict=1, ) - self.group_entries = [frappe._dict(name="Order Types", lft=0, rgt=2, parent="")] - self.group_entries += [ - frappe._dict(name=order_type, lft=1, rgt=1, parent="Order Types") for order_type in order_types - ] - for d in self.group_entries: if d.parent: self.depth_map.setdefault(d.name, self.depth_map.get(d.parent) + 1) @@ -549,7 +544,7 @@ class Analytics: def get_supplier_parent_child_map(self): self.parent_child_map = frappe._dict( - frappe.get_all("Supplier", fields=["name", "supplier_group"], as_list=True) + frappe.db.sql(""" select name, supplier_group from `tabSupplier`""") ) def get_chart_data(self): diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index 93c7dcc2554..90c33c323ce 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -6,9 +6,9 @@ from collections import OrderedDict import frappe from frappe import _, qb -from frappe.query_builder import Case, CustomFunction -from frappe.query_builder.functions import Coalesce, DateDiff, Max, Sum -from frappe.utils import date_diff, flt, getdate, nowdate +from frappe.query_builder import CustomFunction +from frappe.query_builder.functions import Max +from frappe.utils import date_diff, flt, getdate def execute(filters=None): @@ -18,7 +18,8 @@ def execute(filters=None): validate_filters(filters) columns = get_columns(filters) - data = get_data(filters) + conditions = get_conditions(filters) + data = get_data(conditions, filters) so_elapsed_time = get_so_elapsed_time(data) if not data: @@ -38,66 +39,64 @@ def validate_filters(filters): frappe.throw(_("To Date cannot be before From Date.")) -def get_data(filters): - so = qb.DocType("Sales Order") - soi = qb.DocType("Sales Order Item") - sii = qb.DocType("Sales Invoice Item") +def get_conditions(filters): + conditions = "" + if filters.get("from_date") and filters.get("to_date"): + conditions += " and so.transaction_date between %(from_date)s and %(to_date)s" - # Use the application's today (nowdate, System Settings timezone) rather than the database - # server's CURRENT_DATE: the two differ by a day when the DB server runs in a different timezone - # (e.g. UTC DB + IST app near midnight), which made delay_days non-deterministic on postgres CI. - # DateDiff is cross-database: DATEDIFF() on MariaDB, date subtraction on postgres; it casts the - # string date to a date on postgres. delivery_date is functionally dependent on the grouped - # soi.name primary key, so this is valid under both. - delay = DateDiff(nowdate(), soi.delivery_date) - conversion_rate = Coalesce(so.conversion_rate, 1) + if filters.get("company"): + conditions += " and so.company = %(company)s" - query = ( - qb.from_(so) - .join(soi) - .on(soi.parent == so.name) - .left_join(sii) - .on((sii.so_detail == soi.name) & (sii.docstatus == 1)) - .select( - so.transaction_date.as_("date"), - soi.delivery_date.as_("delivery_date"), - so.name.as_("sales_order"), - so.status, - so.customer, - soi.item_code, - delay.as_("delay_days"), - Case().when(so.status.isin(["Completed", "To Bill"]), 0).else_(delay).as_("delay"), - soi.qty, - soi.delivered_qty, - (soi.qty - soi.delivered_qty).as_("pending_qty"), - Coalesce(Sum(sii.qty), 0).as_("billed_qty"), - soi.base_amount.as_("amount"), - (soi.delivered_qty * soi.base_rate).as_("delivered_qty_amount"), - (soi.billed_amt * conversion_rate).as_("billed_amount"), - (soi.base_amount - (soi.billed_amt * conversion_rate)).as_("pending_amount"), - soi.warehouse.as_("warehouse"), - so.company, - soi.name, - soi.description.as_("description"), - ) - .where((so.status.notin(["Stopped", "On Hold"])) & (so.docstatus == 1)) - .groupby(soi.name, so.name) - .orderby(so.transaction_date) - .orderby(soi.item_code) + if filters.get("sales_order"): + conditions += " and so.name in %(sales_order)s" + + if filters.get("status"): + conditions += " and so.status in %(status)s" + + if filters.get("warehouse"): + conditions += " and soi.warehouse = %(warehouse)s" + + return conditions + + +def get_data(conditions, filters): + data = frappe.db.sql( + f""" + SELECT + so.transaction_date as date, + soi.delivery_date as delivery_date, + so.name as sales_order, + so.status, so.customer, soi.item_code, + DATEDIFF(CURRENT_DATE, soi.delivery_date) as delay_days, + IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay, + soi.qty, soi.delivered_qty, + (soi.qty - soi.delivered_qty) AS pending_qty, + IFNULL(SUM(sii.qty), 0) as billed_qty, + soi.base_amount as amount, + (soi.delivered_qty * soi.base_rate) as delivered_qty_amount, + (soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount, + (soi.base_amount - (soi.billed_amt * IFNULL(so.conversion_rate, 1))) as pending_amount, + soi.warehouse as warehouse, + so.company, soi.name, + soi.description as description + FROM + `tabSales Order` so, + `tabSales Order Item` soi + LEFT JOIN `tabSales Invoice Item` sii + ON sii.so_detail = soi.name and sii.docstatus = 1 + WHERE + soi.parent = so.name + and so.status not in ('Stopped', 'On Hold') + and so.docstatus = 1 + {conditions} + GROUP BY soi.name + ORDER BY so.transaction_date ASC, soi.item_code ASC + """, + filters, + as_dict=1, ) - if filters.get("from_date") and filters.get("to_date"): - query = query.where(so.transaction_date[filters.get("from_date") : filters.get("to_date")]) - if filters.get("company"): - query = query.where(so.company == filters.get("company")) - if filters.get("sales_order"): - query = query.where(so.name.isin(filters.get("sales_order"))) - if filters.get("status"): - query = query.where(so.status.isin(filters.get("status"))) - if filters.get("warehouse"): - query = query.where(soi.warehouse == filters.get("warehouse")) - - return query.run(as_dict=True) + return data def get_so_elapsed_time(data): @@ -113,17 +112,7 @@ def get_so_elapsed_time(data): dn = qb.DocType("Delivery Note") dni = qb.DocType("Delivery Note Item") - # TO_SECONDS is MariaDB-only. On postgres, subtracting dates yields days, so multiply - # by 86400 for the equivalent second delta. so.transaction_date is neither aggregated nor - # in the GROUP BY, but it is selectable under postgres' strict GROUP BY because it is - # functionally dependent on the grouped so.name (a doctype's `name` is always the PK). - if frappe.db.db_type == "postgres": - elapsed_seconds = ((Max(dn.posting_date) - so.transaction_date) * 86400).as_("elapsed_seconds") - else: - to_seconds = CustomFunction("TO_SECONDS", ["date"]) - elapsed_seconds = (to_seconds(Max(dn.posting_date)) - to_seconds(so.transaction_date)).as_( - "elapsed_seconds" - ) + to_seconds = CustomFunction("TO_SECONDS", ["date"]) query = ( qb.from_(so) @@ -136,11 +125,11 @@ def get_so_elapsed_time(data): .select( so.name.as_("sales_order"), soi.item_code.as_("so_item_code"), - elapsed_seconds, + (to_seconds(Max(dn.posting_date)) - to_seconds(so.transaction_date)).as_("elapsed_seconds"), ) .where((so.name.isin(sales_orders)) & (dn.docstatus == 1)) .orderby(so.name, soi.name) - .groupby(soi.name, so.name) + .groupby(soi.name) ) dn_elapsed_time = query.run(as_dict=True)