From 2b1a477fc8bc3cca30bb8feda31929b7a5961073 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 23 Jun 2026 22:07:48 +0530 Subject: [PATCH] Revert "fix: case-insensitive matching match MariaDB on Postgres" --- .../cost_of_poor_quality_report.py | 5 -- .../test_cost_of_poor_quality_report.py | 65 ------------------- .../serial_and_batch_bundle.py | 5 +- .../test_serial_and_batch_bundle.py | 25 ------- erpnext/utilities/product.py | 8 +-- erpnext/utilities/test_product.py | 25 ------- 6 files changed, 4 insertions(+), 129 deletions(-) delete mode 100644 erpnext/manufacturing/report/cost_of_poor_quality_report/test_cost_of_poor_quality_report.py delete mode 100644 erpnext/utilities/test_product.py diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py index 0fa5983a28d..a86df319441 100644 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py @@ -3,8 +3,6 @@ import frappe from frappe import _ -from frappe.query_builder.functions import Lower -from frappe.utils import cstr def execute(filters=None): @@ -65,9 +63,6 @@ def append_filters(query, report_filters, operations, job_card): if report_filters.get(field): if field == "serial_no": query = query.where(job_card[field].like(f"%{report_filters.get(field)}%")) - elif field == "batch_no": - # Lower() both sides: exact match stays case-insensitive on Postgres as it is on MariaDB - query = query.where(Lower(job_card[field]) == cstr(report_filters.get(field)).lower()) elif field == "operation": query = query.where(job_card[field].isin(operations)) else: diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/test_cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/test_cost_of_poor_quality_report.py deleted file mode 100644 index 1dc4b31b70e..00000000000 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/test_cost_of_poor_quality_report.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import frappe -from frappe.utils import add_to_date, now - -from erpnext.manufacturing.doctype.job_card.mapper import make_corrective_job_card -from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record -from erpnext.manufacturing.report.cost_of_poor_quality_report.cost_of_poor_quality_report import execute -from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -from erpnext.tests.utils import ERPNextTestSuite - - -class TestCostOfPoorQualityReport(ERPNextTestSuite): - def setUp(self): - self.load_test_records("BOM") - # BOM with operations for _Test FG Item 2, so submitting the work order creates Job Cards - bom = frappe.copy_doc(self.globalTestRecords["BOM"][2]) - bom.set_rate_of_sub_assembly_item_based_on_bom = 0 - bom.rm_cost_as_per = "Valuation Rate" - bom.items[0].uom = "_Test UOM 1" - bom.items[0].conversion_factor = 5 - bom.insert(ignore_if_duplicate=True) - - def test_batch_no_filter_is_case_insensitive(self): - # The report's batch_no filter used an exact `==`, which is case-sensitive on Postgres -- a - # differently-cased batch_no would miss job cards that MariaDB (case-insensitive collation) - # matches. Lower() both sides keeps MariaDB unchanged and makes Postgres match too. - wo = make_wo_order_test_record(item="_Test FG Item 2", qty=2, transfer_material_against="Work Order") - for item in wo.required_items: - make_stock_entry( - item_code=item.item_code, - target=item.source_warehouse, - qty=item.required_qty * 2, - basic_rate=100, - ) - - job_card = frappe.get_last_doc("Job Card", {"work_order": wo.name}) - job_card.append( - "time_logs", {"from_time": now(), "to_time": add_to_date(now(), hours=1), "completed_qty": 2} - ) - job_card.submit() - - corrective_op = frappe.get_doc( - doctype="Operation", is_corrective_operation=1, name=frappe.generate_hash() - ).insert() - corrective_jc = make_corrective_job_card( - job_card.name, operation=corrective_op.name, for_operation=job_card.operation - ) - corrective_jc.hour_rate = 100 - corrective_jc.insert() - corrective_jc.append( - "time_logs", - { - "from_time": add_to_date(now(), hours=2), - "to_time": add_to_date(now(), hours=2, minutes=30), - "completed_qty": 2, - }, - ) - corrective_jc.submit() - # store an uppercase batch_no; the report is then filtered with a lowercase value - corrective_jc.db_set("batch_no", "TESTCOPQBATCH") - - _columns, data = execute(frappe._dict({"batch_no": "testcopqbatch"})) - self.assertTrue(any(row.get("name") == corrective_jc.name for row in data)) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 1d9ed5a2af7..cd706b800a5 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -11,7 +11,7 @@ import frappe.query_builder from frappe import _, _dict, bold from frappe.model.document import Document from frappe.model.naming import make_autoname -from frappe.query_builder.functions import Concat_ws, Lower, Max, Sum +from frappe.query_builder.functions import Concat_ws, Max, Sum from frappe.utils import ( cint, cstr, @@ -3389,8 +3389,7 @@ def get_stock_ledgers_for_serial_nos(kwargs): query.left_join(serial_batch_entry) .on(stock_ledger_entry.serial_and_batch_bundle == serial_batch_entry.parent) .where( - # Lower() both sides so serial-no matching is case-insensitive on Postgres as on MariaDB - Lower(serial_batch_entry.serial_no).isin([sn.lower() for sn in serial_nos]) + serial_batch_entry.serial_no.isin(serial_nos) | Concat_ws("", "\n", stock_ledger_entry.serial_no, "\n").regexp(regex_pattern) ) .distinct() diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index 2c6aac7d4ce..95857f8ab52 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -80,31 +80,6 @@ class TestSerialandBatchBundle(ERPNextTestSuite): self.assertFalse(bundle_doc.name.startswith("SABB-")) - def test_get_stock_ledgers_for_serial_nos_is_case_insensitive(self): - # get_stock_ledgers_for_serial_nos matches Serial and Batch Entry.serial_no with isin(), which is - # case-sensitive on Postgres -- a differently-cased serial no would miss entries that MariaDB - # (case-insensitive collation) matches. Lower() both sides keeps MariaDB unchanged and makes - # Postgres match too. - from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt - from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( - get_stock_ledgers_for_serial_nos, - ) - - item_code = "Test SBB Case-insensitive Serial Item" - make_item( - item_code, - {"has_serial_no": 1, "serial_no_series": "TESTCISER-.#####", "is_stock_item": 1}, - ) - pr = make_purchase_receipt(item_code=item_code, warehouse="_Test Warehouse - _TC", qty=1, rate=100) - bundle = pr.items[0].serial_and_batch_bundle - serial_no = get_serial_nos_from_bundle(bundle)[0] - - # query with a lowercased serial no; the stored Serial and Batch Entry value is uppercase - rows = get_stock_ledgers_for_serial_nos( - frappe._dict({"item_code": item_code, "serial_nos": [serial_no.lower()]}) - ) - self.assertTrue(any(row.serial_and_batch_bundle == bundle for row in rows)) - def test_inward_outward_serial_valuation(self): from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt diff --git a/erpnext/utilities/product.py b/erpnext/utilities/product.py index e19769fa95b..282a9a6f461 100644 --- a/erpnext/utilities/product.py +++ b/erpnext/utilities/product.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt import frappe -from frappe.query_builder.functions import Lower from frappe.utils import cint, cstr, flt, fmt_money from erpnext.accounts.doctype.pricing_rule.pricing_rule import get_pricing_rule_for_item @@ -134,12 +133,9 @@ def get_item_codes_by_attributes(attribute_filters, template_item_code=None): frappe.qb.from_(iva) .select(iva.parent) # attribute_value is a varchar column; cast values to str so postgres doesn't choke on - # `varchar = numeric` for numeric attributes (stored values are strings on both backends). - # Lower() both sides so matching is case-insensitive on Postgres too, matching MariaDB's - # default collation (MariaDB result is unchanged -- it already matches case-insensitively). + # `varchar = numeric` for numeric attributes (stored values are strings on both backends) .where( - (Lower(iva.attribute) == cstr(attribute).lower()) - & (Lower(iva.attribute_value).isin([cstr(v).lower() for v in attribute_values])) + (iva.attribute == attribute) & (iva.attribute_value.isin([cstr(v) for v in attribute_values])) ) .where(iva.parent.isin(item_subquery)) .groupby(iva.parent) diff --git a/erpnext/utilities/test_product.py b/erpnext/utilities/test_product.py deleted file mode 100644 index 19c529ef638..00000000000 --- a/erpnext/utilities/test_product.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import frappe - -from erpnext.controllers.item_variant import create_variant -from erpnext.tests.utils import ERPNextTestSuite -from erpnext.utilities.product import get_item_codes_by_attributes - - -class TestProduct(ERPNextTestSuite): - def test_get_item_codes_by_attributes_is_case_insensitive(self): - # get_item_codes_by_attributes matches Item Variant Attribute values. A raw equality is - # case-sensitive on Postgres, so a differently-cased filter value would miss variants that - # MariaDB (case-insensitive collation) matches. Lower() both sides keeps MariaDB unchanged and - # makes Postgres match too. - template = "_Test Variant Item" - variant = create_variant(template, {"Test Size": "Small"}) - if not frappe.db.exists("Item", variant.name): - variant.insert() - self.addCleanup(frappe.delete_doc, "Item", variant.name, force=True) - - # stored attribute value is "Small"; query with a different case - matches = get_item_codes_by_attributes({"Test Size": ["small"]}, template) - self.assertIn(variant.name, matches)