fix(postgres): satisfy strict GROUP BY in production plan bom explosion

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mihir Kandoi
2026-06-17 14:05:53 +05:30
parent 93021a9d45
commit 85191d1cac

View File

@@ -4,7 +4,7 @@
"""BOM explosion helpers for Production Plan material planning."""
import frappe
from frappe.query_builder.functions import IfNull, Sum
from frappe.query_builder.functions import IfNull, Max, Min, Sum
from erpnext.manufacturing.doctype.production_plan.services.planning_queries import get_uom_conversion_factor
@@ -38,22 +38,25 @@ def _exploded_items_query(company, bom_no, include_non_stock_items, planned_qty)
def _exploded_item_columns(bei, bom, item, item_default, item_uom, planned_qty):
# only item_code/stock_uom are grouped; the rest are functionally dependent on the grouped item
# or arbitrary per BOM Item on MySQL -> Max() keeps the GROUP BY valid on postgres with the same
# value MySQL picked.
return [
(IfNull(Sum(bei.stock_qty / IfNull(bom.quantity, 1)), 0) * planned_qty).as_("qty"),
item.item_name,
item.name.as_("item_code"),
bei.description,
Max(item.item_name).as_("item_name"),
Max(item.name).as_("item_code"),
Max(bei.description).as_("description"),
bei.stock_uom,
item.min_order_qty,
bei.source_warehouse,
item.default_material_request_type,
item.min_order_qty,
item_default.default_warehouse,
item.purchase_uom,
item_uom.conversion_factor,
item.safety_stock,
bom.item.as_("main_bom_item"),
bom.name.as_("main_bom"),
Max(item.min_order_qty).as_("min_order_qty"),
Max(bei.source_warehouse).as_("source_warehouse"),
Max(item.default_material_request_type).as_("default_material_request_type"),
Max(item.min_order_qty).as_("min_order_qty"),
Max(item_default.default_warehouse).as_("default_warehouse"),
Max(item.purchase_uom).as_("purchase_uom"),
Max(item_uom.conversion_factor).as_("conversion_factor"),
Max(item.safety_stock).as_("safety_stock"),
Max(bom.item).as_("main_bom_item"),
Max(bom.name).as_("main_bom"),
]
@@ -106,30 +109,34 @@ def _subitems_query(company, bom_no, include_non_stock_items, parent_qty, planne
.select(*_subitem_columns(bom_item, bom, item, item_default, item_uom, parent_qty, planned_qty))
.where(_subitem_filter(bom_item, bom, item, bom_no, include_non_stock_items))
.groupby(bom_item.item_code)
.orderby(bom_item.idx)
# idx is not grouped; Min() preserves the original ordering and is valid on postgres
.orderby(Min(bom_item.idx))
).run(as_dict=True)
def _subitem_columns(bom_item, bom, item, item_default, item_uom, parent_qty, planned_qty):
qty = IfNull(parent_qty * Sum(bom_item.stock_qty / IfNull(bom.quantity, 1)) * planned_qty, 0).as_("qty")
# only item_code is grouped; the rest are functionally dependent on the grouped item (item
# attributes) or arbitrary per BOM Item on MySQL -> Max() keeps the GROUP BY valid on postgres
# while returning the same value MySQL picked.
return [
bom_item.item_code,
item.default_material_request_type,
item.item_name,
Max(item.default_material_request_type).as_("default_material_request_type"),
Max(item.item_name).as_("item_name"),
qty,
item.is_sub_contracted_item.as_("is_sub_contracted"),
bom_item.source_warehouse,
item.default_bom.as_("default_bom"),
bom_item.description.as_("description"),
bom_item.stock_uom.as_("stock_uom"),
item.min_order_qty.as_("min_order_qty"),
item.safety_stock.as_("safety_stock"),
item_default.default_warehouse,
item.purchase_uom,
item_uom.conversion_factor,
bom.item.as_("main_bom_item"),
bom.name.as_("main_bom"),
bom_item.is_phantom_item,
Max(item.is_sub_contracted_item).as_("is_sub_contracted"),
Max(bom_item.source_warehouse).as_("source_warehouse"),
Max(item.default_bom).as_("default_bom"),
Max(bom_item.description).as_("description"),
Max(bom_item.stock_uom).as_("stock_uom"),
Max(item.min_order_qty).as_("min_order_qty"),
Max(item.safety_stock).as_("safety_stock"),
Max(item_default.default_warehouse).as_("default_warehouse"),
Max(item.purchase_uom).as_("purchase_uom"),
Max(item_uom.conversion_factor).as_("conversion_factor"),
Max(bom.item).as_("main_bom_item"),
Max(bom.name).as_("main_bom"),
Max(bom_item.is_phantom_item).as_("is_phantom_item"),
]