From 297153264b694324011c71bee49a83fbc0439b8c Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 23 Jun 2026 17:49:12 +0530 Subject: [PATCH 1/6] fix(accounts): guard last-GLE exchange-rate division against a zero divisor (Postgres) calculate_exchange_rate_using_last_gle divides (debit - credit) by (debit_in_account_currency - credit_in_account_currency). The GL row is re-selected by (voucher_type, voucher_no, account) ordered by posting_date WITHOUT the "(debit_in_account_currency > 0) | (credit_in_account_currency > 0)" filter the first query used, so the chosen row can have equal/zero account- currency amounts, making the divisor 0. MariaDB returns NULL for x/0 (the caller maps it via `or 0.0`); PostgreSQL raises `division by zero` and aborts. Wrapping the divisor in NullIf(divisor, 0) yields NULL on both engines, so MariaDB output is unchanged and PostgreSQL no longer errors. --- .../exchange_rate_revaluation/exchange_rate_revaluation.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index a5d5b5b3be1..d9ddf9290c5 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -605,7 +605,10 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party): last_exchange_rate = ( qb.from_(gl) - .select((gl.debit - gl.credit) / (gl.debit_in_account_currency - gl.credit_in_account_currency)) + .select( + (gl.debit - gl.credit) + / NullIf(gl.debit_in_account_currency - gl.credit_in_account_currency, 0) + ) .where( (gl.voucher_type == voucher_type) & (gl.voucher_no == voucher_no) & (gl.account == account) ) From affd2fd95d11444042b7a8cb0f1b91ce1e9d13f0 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 23 Jun 2026 17:49:26 +0530 Subject: [PATCH 2/6] fix(manufacturing): guard average bin valuation-rate division against a zero divisor (Postgres) _get_avg_valuation_rate_from_bins divides Sum(stock_value) by Sum(actual_qty). The `Count(name) > 0` guard only proves a Bin row exists; Sum(actual_qty) can still be 0 (stock depleted, or per-warehouse quantities cancelling out), and the outer IfNull catches only NULL, not a 0 divisor. MariaDB returns NULL for x/0 (then IfNull -> 0.0); PostgreSQL raises `division by zero` and aborts BOM costing. Wrapping the divisor in NullIf(Sum(actual_qty), 0) keeps the identical 0.0 result on MariaDB and avoids the error on PostgreSQL. --- erpnext/manufacturing/doctype/bom/bom.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 90d898f4751..061fbbf43e7 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -9,7 +9,7 @@ import frappe from frappe import _, bold from frappe.model.document import Document from frappe.query_builder import Field -from frappe.query_builder.functions import Count, IfNull, Max, Min, Sum +from frappe.query_builder.functions import Count, IfNull, Max, Min, NullIf, Sum from frappe.utils import cint, cstr, flt, get_link_to_form, parse_json from frappe.website.website_generator import WebsiteGenerator @@ -1124,7 +1124,8 @@ def _get_avg_valuation_rate_from_bins(item_code, company, data): .select( Case() .when( - Count(bin_table.name) > 0, IfNull(Sum(bin_table.stock_value) / Sum(bin_table.actual_qty), 0.0) + Count(bin_table.name) > 0, + IfNull(Sum(bin_table.stock_value) / NullIf(Sum(bin_table.actual_qty), 0), 0.0), ) .else_(None) .as_("valuation_rate") From d48396cb117c999511755178709782ab7bdc2e64 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 23 Jun 2026 17:49:27 +0530 Subject: [PATCH 3/6] fix(selling): guard lost-value ratio against a zero total (Postgres) The Lost Quotations report's lost-value ratio divides Sum(base_net_total) by total_value, a scalar Sum(base_net_total) subquery over the same lost quotations. If every lost quotation in the period is zero-amount, total_value is 0 while the grouped query still returns rows. MariaDB returns NULL for x/0; PostgreSQL raises `division by zero` and aborts the report. Wrapping the divisor in NullIf(total_value, 0) yields the same NULL column on MariaDB and no error on PostgreSQL. (The sibling count ratio divides by Count >= 1 in any returned row and is unaffected.) --- erpnext/selling/report/lost_quotations/lost_quotations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/report/lost_quotations/lost_quotations.py b/erpnext/selling/report/lost_quotations/lost_quotations.py index c3bcd54cfd3..799dbec5878 100644 --- a/erpnext/selling/report/lost_quotations/lost_quotations.py +++ b/erpnext/selling/report/lost_quotations/lost_quotations.py @@ -6,7 +6,7 @@ from typing import Literal import frappe from frappe import _ from frappe.model.docstatus import DocStatus -from frappe.query_builder.functions import Coalesce, Count, Round, Sum +from frappe.query_builder.functions import Coalesce, Count, NullIf, Round, Sum from frappe.utils.data import get_timespan_date_range @@ -86,7 +86,7 @@ def get_data(company: str, from_date: str, to_date: str, group_by: Literal["Lost # `* 100.0` before dividing: count/count is integer division on Postgres (truncates to 0) Round((Count(q.name).distinct() * 100.0 / total_quotations), 2), Sum(q.base_net_total), - Round((Sum(q.base_net_total) / total_value * 100), 2), + Round((Sum(q.base_net_total) / NullIf(total_value, 0) * 100), 2), ) .left_join(dimension) .on(dimension.parent == q.name) From 334f1cc6f0f7e59210e1440da08809cd369ac617 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 23 Jun 2026 18:41:22 +0530 Subject: [PATCH 4/6] fix(stock): guard incorrect-serial valuation-rate division against a zero qty (Postgres) The Incorrect Serial No Valuation report computes stock_value_difference / actual_qty for every matching Stock Ledger Entry. A valuation-only Stock Reconciliation of serialized/batched stock writes an SLE with actual_qty = 0 and a non-zero stock_value_difference, and the or_filters (serial_no / serial_and_batch_bundle set) do not exclude it. MariaDB returns NULL for x/0; PostgreSQL raises `division by zero` and aborts the report. Using the get_all nested NULLIF form {"DIV": ["stock_value_difference", {"NULLIF": ["actual_qty", 0]}]} yields NULL on both engines, leaving MariaDB output unchanged. --- .../incorrect_serial_no_valuation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py index 101b6a21461..c7a256c7c57 100644 --- a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py +++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py @@ -115,7 +115,7 @@ def get_stock_ledger_entries(report_filters): "posting_time", "company", "warehouse", - {"DIV": ["stock_value_difference", "actual_qty"], "as": "valuation_rate"}, + {"DIV": ["stock_value_difference", {"NULLIF": ["actual_qty", 0]}], "as": "valuation_rate"}, ] filters = {"is_cancelled": 0} From 727f8d0967d6d8fe170ed282a38bdbff257fcf86 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 23 Jun 2026 18:41:44 +0530 Subject: [PATCH 5/6] fix(stock): guard production-plan received-qty division against a zero divisor (Postgres) update_received_qty_if_from_pp divides received_qty by (qty / fg_item_qty) over Purchase Order Items. Both qty and fg_item_qty are Float with no non-zero constraint, so a zero qty (or fg_item_qty) drives the divisor to 0. MariaDB returns NULL for x/0 (dropped by the surrounding Sum); PostgreSQL raises `division by zero` and aborts the Purchase Receipt submit/cancel. Wrapping both divisors in NullIf(..., 0) makes the zero row contribute NULL on both engines, leaving MariaDB output unchanged. --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index a99c536dd5b..fbb9a38150c 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -383,7 +383,7 @@ class PurchaseReceipt(BuyingController): self.update_received_qty_if_from_pp() def update_received_qty_if_from_pp(self): - from frappe.query_builder.functions import Coalesce, Sum + from frappe.query_builder.functions import Coalesce, NullIf, Sum items_from_po = [item.purchase_order_item for item in self.items if item.purchase_order_item] if items_from_po: @@ -404,7 +404,9 @@ class PurchaseReceipt(BuyingController): frappe.qb.from_(table) .select( table.production_plan_sub_assembly_item, - Sum(table.received_qty / (table.qty / table.fg_item_qty)).as_("received_qty"), + Sum(table.received_qty / NullIf(table.qty / NullIf(table.fg_item_qty, 0), 0)).as_( + "received_qty" + ), ) .where(table.production_plan_sub_assembly_item.isin(result)) .groupby(table.production_plan_sub_assembly_item) From 07a86b33e6f7e34a5b4bfda3f12a60d0c970a604 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 23 Jun 2026 18:51:18 +0530 Subject: [PATCH 6/6] fix(stock): guard non-stock valuation-rate division against a zero divisor (Postgres) The non-stock-item valuation rate divides Sum(base_net_amount) by Sum(qty * conversion_factor) over Purchase Invoice Items. A line with qty 0 zeroes the divisor. MariaDB returns NULL for x/0 (the caller maps it via `or 0.0`); PostgreSQL raises `division by zero` and aborts. Wrap the divisor in NullIf(Sum(qty * conversion_factor), 0): unchanged on MariaDB, valid on Postgres. --- erpnext/stock/get_item_details.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 1025bbf5355..b11868347ea 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -11,7 +11,7 @@ from frappe.model import child_table_fields, default_fields from frappe.model.document import Document from frappe.model.meta import get_field_precision from frappe.model.utils import get_fetch_values -from frappe.query_builder.functions import IfNull, Sum +from frappe.query_builder.functions import IfNull, NullIf, Sum from frappe.utils import add_days, add_months, cint, cstr, flt, get_link_to_form, getdate, parse_json import erpnext @@ -1734,7 +1734,7 @@ def get_valuation_rate(item_code: str, company: str, warehouse: str | None = Non pi_item = frappe.qb.DocType("Purchase Invoice Item") valuation_rate = ( frappe.qb.from_(pi_item) - .select(Sum(pi_item.base_net_amount) / Sum(pi_item.qty * pi_item.conversion_factor)) + .select(Sum(pi_item.base_net_amount) / NullIf(Sum(pi_item.qty * pi_item.conversion_factor), 0)) .where((pi_item.docstatus == 1) & (pi_item.item_code == item_code)) ).run()