From 20e6a6e149362b579ae4b9f76d588d4b7fd3f61e Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 23 Jun 2026 19:38:25 +0530 Subject: [PATCH 1/3] fix(selling): make POS item-price NULL ordering match across engines (Postgres) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit POS get_items keeps Item Price rows with a NULL valid_from (open-ended base price) alongside dated rows, orders by valid_from DESC, then picks the first matching UOM positionally via next()/[0]. MariaDB sorts NULL last for DESC, so a dated override wins; PostgreSQL defaults to NULLS FIRST for DESC, so the NULL-valid_from base price wins instead — the POS shows a different price_list_rate/currency on the two engines for an item that has both an undated standing price and a dated price. Coalesce(valid_from, "1900-01-01") in the ORDER BY forces the NULL row to sort last on both engines. MariaDB already placed it last for DESC, so its output is unchanged; PostgreSQL now picks the same dated override. --- erpnext/selling/page/point_of_sale/point_of_sale.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 83e7bac3fef..fff8c5ab6a3 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -6,6 +6,7 @@ import json import frappe from frappe.query_builder import Criterion, DocType, Order +from frappe.query_builder.functions import Coalesce from frappe.utils import cint, get_datetime from frappe.utils.nestedset import get_root_of @@ -231,7 +232,10 @@ def get_items( .where(ItemPrice.selling == 1) .where((ItemPrice.valid_from <= current_date) | (ItemPrice.valid_from.isnull())) .where((ItemPrice.valid_upto >= current_date) | (ItemPrice.valid_upto.isnull())) - .orderby(ItemPrice.valid_from, order=Order.desc) + # Coalesce so a NULL valid_from (open-ended base price) sorts last under DESC on both + # engines: MariaDB already sorts NULL last for DESC, Postgres defaults to NULLS FIRST, which + # would otherwise make the base price win the positional pick over a dated override. + .orderby(Coalesce(ItemPrice.valid_from, "1900-01-01"), order=Order.desc) ).run(as_dict=True) stock_uom_price = next((d for d in item_prices if d.get("uom") == item.stock_uom), {}) From 3859919263bc3ca23a40b5f2e4af0ea568c44e3b Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 23 Jun 2026 19:38:27 +0530 Subject: [PATCH 2/3] fix(stock): guard traceability qty division against a zero divisor (Postgres) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get_materials divides stock_entry_detail.qty by a CASE that returns fg_completed_qty when it is > 0 and otherwise the injected sabb_data.qty. The code explicitly anticipates fg_completed_qty <= 0 (the else branch), and neither fg_completed_qty nor sabb_data.qty is constrained non-zero, so the divisor can be 0. MariaDB returns NULL for x/0; PostgreSQL raises `division by zero` and aborts the report. Wrapping the CASE in NullIf(..., 0) makes the divisor NULL instead of 0 — unchanged on MariaDB, valid on Postgres. --- .../serial_no_and_batch_traceability.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py b/erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py index 493313ed9e6..69c34f25cd9 100644 --- a/erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py +++ b/erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py @@ -4,6 +4,7 @@ import frappe from frappe import _ from frappe.query_builder import Case +from frappe.query_builder.functions import NullIf def execute(filters: dict | None = None): @@ -300,9 +301,12 @@ class ReportData: ( ( stock_entry_detail.qty - / Case() - .when(stock_entry.fg_completed_qty > 0, stock_entry.fg_completed_qty) - .else_(sabb_data.qty) + / NullIf( + Case() + .when(stock_entry.fg_completed_qty > 0, stock_entry.fg_completed_qty) + .else_(sabb_data.qty), + 0, + ) ) * sabb_data.qty ).as_("qty"), From 8960e3ff4a9f54a3d3c877c737f0d2a8dad83bb5 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 23 Jun 2026 19:38:27 +0530 Subject: [PATCH 3/3] fix(accounts): cast non-text Account fields for LIKE filters (Postgres) Financial Report Template calculation_formula filters are user-authored and only validated for field existence + operator membership, not that a like/ilike operator targets a text field. A filter such as ["is_group", "like", "1"] builds `is_group ILIKE '%1%'`; PostgreSQL has no LIKE/ILIKE operator for a smallint/int/numeric column (`operator does not exist: smallint ~~* unknown`) and aborts the report, while MariaDB implicitly casts the numeric column to text and matches. For like-family operators, cast a numeric/Check Account field to varchar (`Cast_(field, "varchar")`), reproducing MariaDB's implicit numeric->text coercion on both engines. Text-field filters (the normal account_name/ account_number case) are left untouched, so MariaDB output is unchanged. --- .../financial_report_engine.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py b/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py index 47c7e2e6366..6d44796fb1c 100644 --- a/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py +++ b/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py @@ -12,8 +12,9 @@ from typing import Any, Union import frappe from frappe import _ from frappe.database.operator_map import OPERATOR_MAP +from frappe.model import numeric_fieldtypes from frappe.query_builder import Case -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import Cast_, Sum from frappe.utils import cstr, date_diff, flt, getdate from frappe.utils.xlsxutils import XLSXMetadata, XLSXStyleBuilder from pypika.terms import Bracket, LiteralValue @@ -864,8 +865,15 @@ class FilterExpressionParser: field = getattr(table, field_name, None) operator_fn = OPERATOR_MAP.get(operator.casefold()) - if "like" in operator.casefold() and "%" not in value: - value = f"%{value}%" + if "like" in operator.casefold(): + if "%" not in value: + value = f"%{value}%" + # Postgres has no LIKE/ILIKE operator for non-text columns; MariaDB implicitly casts + # the numeric column to text. Cast a numeric/Check Account field to varchar so the + # match runs on both engines and reproduces MariaDB's result. + meta_field = frappe.get_meta("Account").get_field(field_name) + if meta_field and meta_field.fieldtype in numeric_fieldtypes: + field = Cast_(field, "varchar") return operator_fn(field, value)