Merge pull request #56105 from mihir-kandoi/pg-parity-other-class

fix(postgres): MariaDB/Postgres parity in pick-list, serial match, null ordering & link-query ranking
This commit is contained in:
Mihir Kandoi
2026-06-18 22:22:16 +05:30
committed by GitHub
5 changed files with 68 additions and 20 deletions

View File

@@ -6,7 +6,7 @@ import frappe
from frappe import _, bold
from frappe.model.document import Document
from frappe.model.mapper import map_child_doc, map_doc
from frappe.query_builder.functions import IfNull, Sum
from frappe.query_builder.functions import IfNull, Lower, Sum
from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate
from frappe.utils.nestedset import get_descendants_of
@@ -505,16 +505,20 @@ class POSInvoice(SalesInvoice):
if d.get("serial_no"):
serial_nos = get_serial_nos(d.serial_no)
for sr in serial_nos:
serial_no_exists = frappe.get_all(
"POS Invoice Item",
filters={"parent": self.return_against},
or_filters=[
["serial_no", "=", sr],
["serial_no", "like", f"{sr}\n%"],
["serial_no", "like", f"%\n{sr}"],
["serial_no", "like", f"%\n{sr}\n%"],
],
limit=1,
POI = frappe.qb.DocType("POS Invoice Item")
s = sr.lower()
serial_no_exists = (
frappe.qb.from_(POI)
.select(POI.name)
.where(POI.parent == self.return_against)
.where(
(Lower(POI.serial_no) == s)
| Lower(POI.serial_no).like(f"{s}\n%")
| Lower(POI.serial_no).like(f"%\n{s}")
| Lower(POI.serial_no).like(f"%\n{s}\n%")
)
.limit(1)
.run()
)
if not serial_no_exists:

View File

@@ -10,7 +10,7 @@ from frappe import qb, scrub
from frappe.desk.reportview import get_filters_cond, get_match_cond
from frappe.permissions import has_permission
from frappe.query_builder import Case, Criterion, DocType
from frappe.query_builder.functions import Concat, CustomFunction, Length, Locate, Substring, Sum
from frappe.query_builder.functions import Concat, CustomFunction, Length, Locate, Lower, Substring, Sum
from frappe.utils import nowdate, today, unique
from pypika import Order
@@ -313,11 +313,19 @@ def item_query(
.where(date_condition)
.where(Criterion.any(search_conditions))
.orderby(
Case().when(Locate(txt_no_percent, item.name) > 0, Locate(txt_no_percent, item.name)).else_(99999)
Case()
.when(
Locate(Lower(txt_no_percent), Lower(item.name)) > 0,
Locate(Lower(txt_no_percent), Lower(item.name)),
)
.else_(99999)
)
.orderby(
Case()
.when(Locate(txt_no_percent, item.item_name) > 0, Locate(txt_no_percent, item.item_name))
.when(
Locate(Lower(txt_no_percent), Lower(item.item_name)) > 0,
Locate(Lower(txt_no_percent), Lower(item.item_name)),
)
.else_(99999)
)
.orderby(item.idx, order=Order.desc)
@@ -406,7 +414,13 @@ def get_project_name(
# ordering
if txt:
# project_name containing search string 'txt' will be given higher precedence
q = q.orderby(ifelse(Locate(txt, proj.project_name) > 0, Locate(txt, proj.project_name), 99999))
q = q.orderby(
ifelse(
Locate(Lower(txt), Lower(proj.project_name)) > 0,
Locate(Lower(txt), Lower(proj.project_name)),
99999,
)
)
q = q.orderby(proj.idx, order=Order.desc).orderby(proj.name)
if page_len:

View File

@@ -124,13 +124,16 @@ def get_communication_details(filters):
FROM
`tabCommunication`
WHERE
recipients = %s
recipients = %s AND communication_date IS NOT NULL
ORDER BY
communication_date
LIMIT 1
""",
(d.contact_email),
)[0][0]
)
first_contact = first_contact[0][0] if first_contact else None
if not first_contact:
continue
duration = flt(date_diff(invoice[0][0], first_contact))

View File

@@ -9,7 +9,7 @@ import frappe
from frappe import _, bold
from frappe.model.document import Document
from frappe.query_builder import Case
from frappe.query_builder.functions import Coalesce, GroupConcat, Locate, Max, Replace, Sum
from frappe.query_builder.functions import Coalesce, GroupConcat, Locate, Lower, Max, Replace, Sum
from frappe.utils import cint, floor, flt, get_link_to_form
from frappe.utils.nestedset import get_descendants_of
@@ -1301,7 +1301,11 @@ def get_pending_work_orders(
& (wo.company == filters.get("company"))
& (wo.name.like(f"%{txt}%"))
)
.orderby(Case().when(Locate(txt, wo.name) > 0, Locate(txt, wo.name)).else_(99999))
.orderby(
Case()
.when(Locate(Lower(txt), Lower(wo.name)) > 0, Locate(Lower(txt), Lower(wo.name)))
.else_(99999)
)
.orderby(wo.name)
.limit(cint(page_length))
.offset(start)
@@ -1372,7 +1376,10 @@ def get_pick_list_query(doctype: Any, txt: str, searchfield: Any, start: int, pa
.where(PICK_LIST.status.isin(["Open", "Partly Delivered"]))
.where(PICK_LIST.company == filters.get("company"))
.where(SALES_ORDER.customer == filters.get("customer"))
.groupby(PICK_LIST.name)
# customer is from the joined Sales Order, not Pick List's PK, so Postgres rejects it as a bare
# select under GROUP BY pick_list.name; it is pinned to one value by the filter above, so adding
# it to the GROUP BY is valid on Postgres and identical on MariaDB.
.groupby(PICK_LIST.name, SALES_ORDER.customer)
)
if filters.get("sales_order"):

View File

@@ -1964,3 +1964,23 @@ class TestPickList(ERPNextTestSuite):
item_codes = [item.item_code for item in doc.items]
self.assertIn(item1, item_codes)
self.assertIn(item2, item_codes)
def test_get_pick_list_query_postgres_valid(self):
"""get_pick_list_query selects Sales Order.customer (a joined-table column) under
GROUP BY Pick List.name. Postgres rejects that bare column (PK functional dependency does
not cross tables), so the link query raised GroupingError. customer is pinned to one value
by the filter, so adding it to the GROUP BY is identical on MariaDB and valid on Postgres."""
from erpnext.stock.doctype.pick_list.pick_list import get_pick_list_query
warehouse = "_Test Warehouse - _TC"
item = make_item().name
make_stock_entry(item=item, to_warehouse=warehouse, qty=100, basic_rate=100)
so = make_sales_order(item_code=item, qty=5, rate=100)
pl = create_pick_list(so.name)
pl.submit()
# must run without raising on either engine (GroupingError on Postgres before the fix)
result = get_pick_list_query(
"Pick List", "", "name", 0, 20, {"company": so.company, "customer": so.customer}
)
self.assertIn(pl.name, [row["name"] for row in result])