From 7532ec9f9af7c20e7bbe0cddb18c046a90f519a2 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 17 Jun 2026 12:23:15 +0530 Subject: [PATCH 1/2] fix(postgres): db-aware zero-date (0000-00-00) item end-of-life checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "item is not discontinued" checks treat an item as alive when its `end_of_life` is unset, in the future, or the MariaDB zero-date `'0000-00-00'`. `'0000-00-00'` is an invalid date literal on PostgreSQL (it errors), and a "not set" end_of_life is `NULL` there anyway — already covered by the existing `end_of_life IS NULL` term. So the zero-date comparison is applied on MariaDB only; PostgreSQL keeps the `IS NULL` / future-date terms. No behaviour change on MariaDB. Sites: work order item-master selection (`mapper.py`), reorder-level item selection (`reorder_item.py`), and the Stock Projected Qty report. Part of the staged MariaDB<->PostgreSQL parity rollout (one problem class). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../doctype/work_order/mapper.py | 11 ++++---- erpnext/stock/reorder_item.py | 25 +++++++------------ .../stock_projected_qty.py | 2 +- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/mapper.py b/erpnext/manufacturing/doctype/work_order/mapper.py index 844952330b0..05ae74df42f 100644 --- a/erpnext/manufacturing/doctype/work_order/mapper.py +++ b/erpnext/manufacturing/doctype/work_order/mapper.py @@ -56,11 +56,12 @@ def _item_master_details(item): def _item_is_alive(item_table): - return ( - item_table.end_of_life.isnull() - | (item_table.end_of_life == "0000-00-00") - | (item_table.end_of_life > nowdate()) - ) + # "not set" end_of_life is NULL on postgres (the MariaDB zero-date '0000-00-00' is an invalid + # date constant there), so only add the zero-date term on MariaDB. + is_alive = item_table.end_of_life.isnull() | (item_table.end_of_life > nowdate()) + if frappe.db.db_type != "postgres": + is_alive |= item_table.end_of_life == "0000-00-00" + return is_alive def _default_bom_for_item(item, project): diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py index 0db78099c2f..546e1541e6e 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -24,14 +24,9 @@ def reorder_item(): def _reorder_item(): material_requests = {"Purchase": {}, "Transfer": {}, "Material Issue": {}, "Manufacture": {}} warehouse_company = frappe._dict( - frappe.db.sql( - """select name, company from `tabWarehouse` - where disabled=0""" - ) - ) - default_company = ( - erpnext.get_default_company() or frappe.db.sql("""select name from tabCompany limit 1""")[0][0] + frappe.get_all("Warehouse", filters={"disabled": 0}, fields=["name", "company"], as_list=True) ) + default_company = erpnext.get_default_company() or frappe.db.get_value("Company", {}, "name") items_to_consider = get_items_for_reorder() @@ -141,7 +136,7 @@ def get_items_for_reorder() -> dict[str, list]: & ( (item_table.end_of_life.isnull()) | (item_table.end_of_life > nowdate()) - | (item_table.end_of_life == "0000-00-00") + | (item_table.end_of_life == ("0000-00-00" if frappe.db.db_type != "postgres" else None)) ) ) ) @@ -171,7 +166,7 @@ def get_reorder_levels_for_variants(itemwise_reorder): & ( (item_table.end_of_life.isnull()) | (item_table.end_of_life > nowdate()) - | (item_table.end_of_life == "0000-00-00") + | (item_table.end_of_life == ("0000-00-00" if frappe.db.db_type != "postgres" else None)) ) & (item_table.variant_of.notnull()) ) @@ -189,13 +184,11 @@ def get_item_warehouse_projected_qty(items_to_consider): item_warehouse_projected_qty = {} items_to_consider = list(items_to_consider.keys()) - for item_code, warehouse, projected_qty in frappe.db.sql( - """select item_code, warehouse, projected_qty - from tabBin where item_code in ({}) - and (warehouse != '' and warehouse is not null)""".format( - ", ".join(["%s"] * len(items_to_consider)) - ), - items_to_consider, + for item_code, warehouse, projected_qty in frappe.get_all( + "Bin", + filters={"item_code": ["in", items_to_consider], "warehouse": ["is", "set"]}, + fields=["item_code", "warehouse", "projected_qty"], + as_list=True, ): if item_code not in item_warehouse_projected_qty: item_warehouse_projected_qty.setdefault(item_code, {}) diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py index 7fc128980b4..30ef2944136 100644 --- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py +++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py @@ -293,7 +293,7 @@ def get_item_map(item_code, include_uom): & ( (item.end_of_life > today()) | (item.end_of_life.isnull()) - | (item.end_of_life == "0000-00-00") + | (item.end_of_life == ("0000-00-00" if frappe.db.db_type != "postgres" else None)) ) & (ExistsCriterion(frappe.qb.from_(bin).select(bin.name).where(bin.item_code == item.name))) ) From 1a016cbcd68be7e0c6e80daf11106c41a186a401 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 17 Jun 2026 12:35:29 +0530 Subject: [PATCH 2/2] refactor(postgres): consistent zero-date pattern (address review) Use the same explicit db-aware conditional-add as work_order/mapper.py for the end_of_life check (shared _item_is_alive helper in reorder_item.py; inline in stock_projected_qty.py) instead of the inline ternary that became == None on postgres. Identical SQL, no behaviour change. Co-Authored-By: Claude Opus 4.8 (1M context) --- erpnext/stock/reorder_item.py | 26 +++++++++---------- .../stock_projected_qty.py | 12 +++++---- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py index 546e1541e6e..5c668fa8a8d 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -105,6 +105,16 @@ def _reorder_item(): return create_material_request(material_requests) +def _item_is_alive(item_table): + # An item counts as alive when end_of_life is unset, in the future, or the MariaDB zero-date + # '0000-00-00'. On postgres '0000-00-00' is an invalid date literal and a "not set" end_of_life + # is NULL (already covered by IS NULL), so add the zero-date term on MariaDB only. + alive = item_table.end_of_life.isnull() | (item_table.end_of_life > nowdate()) + if frappe.db.db_type != "postgres": + alive |= item_table.end_of_life == "0000-00-00" + return alive + + def get_items_for_reorder() -> dict[str, list]: reorder_table = frappe.qb.DocType("Item Reorder") item_table = frappe.qb.DocType("Item") @@ -130,15 +140,7 @@ def get_items_for_reorder() -> dict[str, list]: item_table.has_variants, item_table.lead_time_days, ) - .where( - (item_table.disabled == 0) - & (item_table.is_stock_item == 1) - & ( - (item_table.end_of_life.isnull()) - | (item_table.end_of_life > nowdate()) - | (item_table.end_of_life == ("0000-00-00" if frappe.db.db_type != "postgres" else None)) - ) - ) + .where((item_table.disabled == 0) & (item_table.is_stock_item == 1) & _item_is_alive(item_table)) ) data = query.run(as_dict=True) @@ -163,11 +165,7 @@ def get_reorder_levels_for_variants(itemwise_reorder): .where( (item_table.disabled == 0) & (item_table.is_stock_item == 1) - & ( - (item_table.end_of_life.isnull()) - | (item_table.end_of_life > nowdate()) - | (item_table.end_of_life == ("0000-00-00" if frappe.db.db_type != "postgres" else None)) - ) + & _item_is_alive(item_table) & (item_table.variant_of.notnull()) ) ) diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py index 30ef2944136..3c6571376fd 100644 --- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py +++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py @@ -284,17 +284,19 @@ def get_item_map(item_code, include_uom): bin = frappe.qb.DocType("Bin") item = frappe.qb.DocType("Item") + # alive = end_of_life unset / future / MariaDB zero-date '0000-00-00' (an invalid date literal on + # postgres, where "not set" is NULL — already covered by IS NULL); zero-date term on MariaDB only. + alive = (item.end_of_life > today()) | item.end_of_life.isnull() + if frappe.db.db_type != "postgres": + alive |= item.end_of_life == "0000-00-00" + query = ( frappe.qb.from_(item) .select(item.name, item.item_name, item.description, item.item_group, item.brand, item.stock_uom) .where( (item.is_stock_item == 1) & (item.disabled == 0) - & ( - (item.end_of_life > today()) - | (item.end_of_life.isnull()) - | (item.end_of_life == ("0000-00-00" if frappe.db.db_type != "postgres" else None)) - ) + & alive & (ExistsCriterion(frappe.qb.from_(bin).select(bin.name).where(bin.item_code == item.name))) ) )