diff --git a/erpnext/stock/doctype/stock_entry/services/disassemble.py b/erpnext/stock/doctype/stock_entry/services/disassemble.py index 84e07a24f75..3c94ea5771f 100644 --- a/erpnext/stock/doctype/stock_entry/services/disassemble.py +++ b/erpnext/stock/doctype/stock_entry/services/disassemble.py @@ -2,7 +2,7 @@ from collections import defaultdict import frappe from frappe import _ -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import Max, Min, NullIf, Sum from frappe.utils import flt from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -348,12 +348,40 @@ class DisassembleStockEntry(BaseStockEntry): .run(as_dict=True) ) + # Aggregating across all Manufacture entries of the work order, one row per item_code. + # The non-grouped columns are constant per item_code in practice (an item plays one role with + # one uom/warehouse across the WO's manufacture entries); Max() keeps the GROUP BY valid on + # postgres while returning the value MySQL picked arbitrarily, preserving the one-row-per-item + # shape the disassembly expects. return ( - query.select(Sum(SED.qty).as_("qty"), Sum(SED.transfer_qty).as_("transfer_qty"), *common_fields) + query.select( + Sum(SED.qty).as_("qty"), + Sum(SED.transfer_qty).as_("transfer_qty"), + SED.item_code, + Max(SED.item_name).as_("item_name"), + Max(SED.description).as_("description"), + Max(SED.stock_uom).as_("stock_uom"), + Max(SED.uom).as_("uom"), + # qty-weighted average so consolidating an item across manufacture entries at different + # valuation rates values the summed qty correctly (Max would bias the rate high). + # Manufacture rows always carry positive qty, so NullIf only guards a theoretical /0. + (Sum(SED.basic_rate * SED.qty) / NullIf(Sum(SED.qty), 0)).as_("basic_rate"), + Max(SED.conversion_factor).as_("conversion_factor"), + Max(SED.is_finished_item).as_("is_finished_item"), + Max(SED.secondary_item_type).as_("secondary_item_type"), + Max(SED.is_legacy_scrap_item).as_("is_legacy_scrap_item"), + Max(SED.bom_secondary_item).as_("bom_secondary_item"), + Max(SED.batch_no).as_("batch_no"), + Max(SED.serial_no).as_("serial_no"), + Max(SED.use_serial_batch_fields).as_("use_serial_batch_fields"), + Max(SED.s_warehouse).as_("s_warehouse"), + Max(SED.t_warehouse).as_("t_warehouse"), + Max(SED.bom_no).as_("bom_no"), + ) .where(SE.purpose == "Manufacture") .where(SE.work_order == self.doc.work_order) .groupby(SED.item_code) - .orderby(SED.idx) + .orderby(Min(SED.idx)) .run(as_dict=True) ) diff --git a/erpnext/stock/doctype/stock_entry/services/manufacturing.py b/erpnext/stock/doctype/stock_entry/services/manufacturing.py index f61b667380c..b3455c40b51 100644 --- a/erpnext/stock/doctype/stock_entry/services/manufacturing.py +++ b/erpnext/stock/doctype/stock_entry/services/manufacturing.py @@ -3,7 +3,7 @@ from collections import defaultdict import frappe from frappe import _, bold -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import Max, Min, Sum from frappe.utils import ceil, cint, flt, get_link_to_form from erpnext.manufacturing.doctype.bom.bom import add_additional_cost @@ -1005,11 +1005,14 @@ def get_secondary_items_from_job_card(work_order, jc_name=None): .select( Sum(job_card_secondary_item.stock_qty).as_("stock_qty"), job_card_secondary_item.item_code, - job_card_secondary_item.item_name, - job_card_secondary_item.description, - job_card_secondary_item.stock_uom, + # non-grouped columns are item attributes / the secondary-item BOM link, constant per + # grouped (item_code, secondary_item_type) -> Max() keeps the GROUP BY valid on postgres + # while returning the value MySQL picked arbitrarily. + Max(job_card_secondary_item.item_name).as_("item_name"), + Max(job_card_secondary_item.description).as_("description"), + Max(job_card_secondary_item.stock_uom).as_("stock_uom"), job_card_secondary_item.secondary_item_type, - job_card_secondary_item.bom_secondary_item, + Max(job_card_secondary_item.bom_secondary_item).as_("bom_secondary_item"), ) .join(job_card_secondary_item) .on(job_card_secondary_item.parent == job_card.name) @@ -1019,7 +1022,7 @@ def get_secondary_items_from_job_card(work_order, jc_name=None): & (job_card.docstatus == 1) ) .groupby(job_card_secondary_item.item_code, job_card_secondary_item.secondary_item_type) - .orderby(job_card_secondary_item.idx) + .orderby(Min(job_card_secondary_item.idx)) ) if jc_name: