From 442ba48341d890ccbdebb5737af8f63891a6500e Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 21 Jun 2026 11:59:57 +0530 Subject: [PATCH] fix(manufacturing): case-insensitive batch_no filter in Cost of Poor Quality report The report's batch_no filter used an exact `==`, which is case-sensitive on Postgres -- a differently-cased batch_no missed Job Cards that MariaDB (case-insensitive collation) matches. Add a dedicated batch_no branch wrapping both sides in Lower() (keeping the exact match, not a substring like serial_no): MariaDB result is unchanged, Postgres now matches. --- .../cost_of_poor_quality_report.py | 5 ++ .../test_cost_of_poor_quality_report.py | 65 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 erpnext/manufacturing/report/cost_of_poor_quality_report/test_cost_of_poor_quality_report.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 a86df319441..0fa5983a28d 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,6 +3,8 @@ import frappe from frappe import _ +from frappe.query_builder.functions import Lower +from frappe.utils import cstr def execute(filters=None): @@ -63,6 +65,9 @@ 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 new file mode 100644 index 00000000000..1dc4b31b70e --- /dev/null +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/test_cost_of_poor_quality_report.py @@ -0,0 +1,65 @@ +# 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))