diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 95c25c497bf..a87a1bf4e99 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -281,10 +281,10 @@ class StatusUpdater(Document): # get unique transactions to update for d in self.get_all_children(): - if hasattr(d, "qty") and d.qty < 0 and not self.get("is_return"): + if hasattr(d, "qty") and flt(d.qty) < 0 and not self.get("is_return"): frappe.throw(_("For an item {0}, quantity must be positive number").format(d.item_code)) - if hasattr(d, "qty") and d.qty > 0 and self.get("is_return"): + if hasattr(d, "qty") and flt(d.qty) > 0 and self.get("is_return"): frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code)) if ( diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 7c032be36e4..a0e4d248af8 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1647,12 +1647,12 @@ def add_non_stock_items_cost(stock_entry, work_order, expense_account, job_card= ) -def add_operating_cost_component_wise( - stock_entry, work_order=None, consumed_operating_cost=None, op_expense_account=None, job_card=None -): +def add_operating_cost_component_wise(stock_entry, work_order=None, op_expense_account=None, job_card=None): if not work_order: return False + from erpnext.stock.doctype.stock_entry.stock_entry import get_consumed_operating_cost + cost_added = False for row in work_order.operations: if job_card and job_card.operation_id != row.name: @@ -1670,18 +1670,32 @@ def add_operating_cost_component_wise( }, ) + consumed_operating_cost = ( + get_consumed_operating_cost(work_order.name, stock_entry.bom_no, row.name) or [] + ) for wc in workstation_cost: expense_account = ( get_component_account(wc.operating_component, stock_entry.company) or op_expense_account ) + consumed_op_cost = next( + ( + cost + for cost in consumed_operating_cost + if cost.get("operating_component") == wc.operating_component + ), + {}, + ) actual_cp_operating_cost = flt( - flt(wc.operating_cost) * flt(flt(row.actual_operation_time) / 60.0) - consumed_operating_cost, + flt(wc.operating_cost) * flt(flt(row.actual_operation_time) / 60.0) + - flt(consumed_op_cost.get("consumed_cost")), row.precision("actual_operating_cost"), ) - per_unit_cost = flt(actual_cp_operating_cost) / flt(row.completed_qty - work_order.produced_qty) + remaining_qty = row.completed_qty - consumed_op_cost.get("consumed_qty", 0) + per_unit_cost = actual_cp_operating_cost / (remaining_qty or 1) + operating_cost = per_unit_cost * stock_entry.fg_completed_qty - if per_unit_cost: + if actual_cp_operating_cost: stock_entry.append( "additional_costs", { @@ -1689,8 +1703,14 @@ def add_operating_cost_component_wise( "description": _("{0} Operating Cost for operation {1}").format( wc.operating_component, row.operation ), - "amount": per_unit_cost * flt(stock_entry.fg_completed_qty), + "amount": flt( + min(operating_cost, actual_cp_operating_cost), + frappe.get_precision("Landed Cost Taxes and Charges", "amount"), + ), "has_operating_cost": 1, + "operation_id": row.name, + "operating_component": wc.operating_component, + "qty": min(remaining_qty, stock_entry.fg_completed_qty), }, ) @@ -1708,17 +1728,15 @@ def get_component_account(parent, company): def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_card=None): from erpnext.stock.doctype.stock_entry.stock_entry import ( - get_consumed_operating_cost, - get_operating_cost_per_unit, + get_remaining_operating_cost, ) - operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no) + remaining_operating_cost = get_remaining_operating_cost(work_order, stock_entry.bom_no) - if operating_cost_per_unit: + if remaining_operating_cost: cost_added = add_operating_cost_component_wise( stock_entry, work_order, - get_consumed_operating_cost(work_order.name, stock_entry.bom_no), expense_account, job_card=job_card, ) @@ -1729,7 +1747,10 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_ { "expense_account": expense_account, "description": _("Operating Cost as per Work Order / BOM"), - "amount": operating_cost_per_unit * flt(stock_entry.fg_completed_qty), + "amount": flt( + remaining_operating_cost * stock_entry.fg_completed_qty, + frappe.get_precision("Landed Cost Taxes and Charges", "amount"), + ), "has_operating_cost": 1, }, ) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 21f0adbdd74..1e4af027c30 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_bulk_edit": 1, "autoname": "naming_series:", "creation": "2026-03-31 21:06:16.282931", "doctype": "DocType", @@ -155,6 +156,7 @@ "fieldname": "wip_warehouse", "fieldtype": "Link", "label": "WIP Warehouse", + "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]", "mandatory_depends_on": "eval:!doc.finished_good || doc.skip_material_transfer === 0 || (doc.skip_material_transfer && doc.backflush_from_wip_warehouse)", "options": "Warehouse" }, @@ -506,6 +508,7 @@ "fieldname": "target_warehouse", "fieldtype": "Link", "label": "Target Warehouse", + "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]", "mandatory_depends_on": "eval:doc.track_semi_finished_goods", "options": "Warehouse" }, @@ -518,6 +521,7 @@ "fieldname": "source_warehouse", "fieldtype": "Link", "label": "Source Warehouse", + "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]", "options": "Warehouse" }, { @@ -627,7 +631,7 @@ "grid_page_length": 50, "is_submittable": 1, "links": [], - "modified": "2026-03-31 21:06:48.987740", + "modified": "2026-05-12 12:17:17.750857", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index a6ac3f0a79a..7f1f50748b8 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -1081,6 +1081,243 @@ class TestJobCard(ERPNextTestSuite): self.assertEqual(s.items[3].item_code, "_Test Item") self.assertEqual(s.items[3].transfer_qty, 2) + @ERPNextTestSuite.change_settings( + "Manufacturing Settings", {"overproduction_percentage_for_work_order": 100} + ) + def test_operating_cost_with_overproduction(self): + from erpnext.manufacturing.doctype.routing.test_routing import ( + create_routing, + setup_bom, + setup_operations, + ) + from erpnext.manufacturing.doctype.work_order.work_order import make_job_card + from erpnext.manufacturing.doctype.work_order.work_order import ( + make_stock_entry as make_stock_entry_for_wo, + ) + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + workstation = make_workstation( + workstation_name="Test Workstation for Overproduction", hour_rate_rent=10, hour_rate_labour=10 + ) + operations = [ + {"operation": "Test Operation 1", "workstation": workstation.name, "time_in_mins": 30}, + {"operation": "Test Operation 2", "workstation": workstation.name, "time_in_mins": 30}, + ] + warehouse = create_warehouse("Test Warehouse for Overproduction") + setup_operations(operations) + + fg = make_item("Test FG for Overproduction", {"stock_uom": "Nos", "is_stock_item": 1}) + rm = make_item("Test RM for Overproduction", {"stock_uom": "Nos", "is_stock_item": 1}) + + routing_doc = create_routing(routing_name="Testing Route", operations=operations) + bom_doc = setup_bom( + item_code=fg.name, + routing=routing_doc.name, + raw_materials=[rm.name], + source_warehouse=warehouse, + ) + + for row in bom_doc.items: + make_stock_entry( + item_code=row.item_code, + target=row.source_warehouse, + qty=100, + basic_rate=100, + ) + + wo_doc = make_wo_order_test_record( + production_item=fg.name, + bom_no=bom_doc.name, + qty=10, + skip_transfer=1, + source_warehouse=warehouse, + ) + + first_operation = frappe.get_all( + "Job Card", + filters={"work_order": wo_doc.name, "sequence_id": 1}, + fields=["name"], + order_by="sequence_id", + limit=1, + )[0].name + + jc = frappe.get_doc("Job Card", first_operation) + from_time = add_to_date(now(), days=1) + for _ in jc.scheduled_time_logs: + jc.append( + "time_logs", + { + "from_time": from_time, + "to_time": add_to_date(from_time, days=1), + "completed_qty": 4, + }, + ) + jc.for_quantity = 4 + jc.save() + jc.submit() + + second_operation = frappe.get_all( + "Job Card", + filters={"work_order": wo_doc.name, "sequence_id": 2}, + fields=["name"], + order_by="sequence_id", + limit=1, + )[0].name + + jc = frappe.get_doc("Job Card", second_operation) + from_time = add_to_date(now(), days=2) + for _ in jc.scheduled_time_logs: + jc.append( + "time_logs", + { + "from_time": from_time, + "to_time": add_to_date(from_time, days=2), + "completed_qty": 4, + }, + ) + jc.for_quantity = 4 + jc.save() + jc.submit() + + s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 6)) # overproduction + s.submit() + + self.assertEqual(s.additional_costs[0].amount, 240) + self.assertEqual(s.additional_costs[1].amount, 240) + self.assertEqual(s.additional_costs[2].amount, 480) + self.assertEqual(s.additional_costs[3].amount, 480) + + make_job_card( + wo_doc.name, + [ + { + "name": wo_doc.operations[0].name, + "operation": "Test Operation 1", + "qty": 2, + "pending_qty": 2, + } + ], + ) + + job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name}) + from_time = add_to_date(now(), days=4) + job_card.append( + "time_logs", + { + "from_time": from_time, + "to_time": add_to_date(from_time, days=1), + "completed_qty": 2, + }, + ) + job_card.for_quantity = 2 + job_card.save() + job_card.submit() + + make_job_card( + wo_doc.name, + [ + { + "name": wo_doc.operations[1].name, + "operation": "Test Operation 2", + "qty": 2, + "pending_qty": 2, + } + ], + ) + + job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name}) + from_time = add_to_date(now(), days=5) + job_card.append( + "time_logs", + { + "from_time": from_time, + "to_time": add_to_date(from_time, days=2), + "completed_qty": 2, + }, + ) + job_card.for_quantity = 2 + job_card.save() + job_card.submit() + + s2 = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 1)) + s2.submit() + + self.assertEqual(s2.additional_costs[0].amount, 120) + self.assertEqual(s2.additional_costs[1].amount, 120) + self.assertEqual(s2.additional_costs[2].amount, 240) + self.assertEqual(s2.additional_costs[3].amount, 240) + + make_job_card( + wo_doc.name, + [ + { + "name": wo_doc.operations[0].name, + "operation": "Test Operation 1", + "qty": 2, + "pending_qty": 2, + } + ], + ) + + job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name}) + from_time = add_to_date(now(), days=7) + job_card.append( + "time_logs", + { + "from_time": from_time, + "to_time": add_to_date(from_time, days=1), + "completed_qty": 2, + }, + ) + job_card.for_quantity = 2 + job_card.save() + job_card.submit() + + make_job_card( + wo_doc.name, + [ + { + "name": wo_doc.operations[1].name, + "operation": "Test Operation 2", + "qty": 2, + "pending_qty": 2, + } + ], + ) + + job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name}) + from_time = add_to_date(now(), days=8) + job_card.append( + "time_logs", + { + "from_time": from_time, + "to_time": add_to_date(from_time, days=2), + "completed_qty": 2, + }, + ) + job_card.for_quantity = 2 + job_card.save() + job_card.submit() + + s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 2)) + s.submit() + + self.assertEqual(s.additional_costs[0].amount, 240) + self.assertEqual(s.additional_costs[1].amount, 240) + self.assertEqual(s.additional_costs[2].amount, 480) + self.assertEqual(s.additional_costs[3].amount, 480) + + s2.cancel() + + s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 3)) + s.submit() + + self.assertEqual(s.additional_costs[0].amount, 240) + self.assertEqual(s.additional_costs[1].amount, 240) + self.assertEqual(s.additional_costs[2].amount, 480) + self.assertEqual(s.additional_costs[3].amount, 480) + def create_bom_with_multiple_operations(): "Create a BOM with multiple operations and Material Transfer against Job Card" diff --git a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json index 69a7428f218..c8dd6403321 100644 --- a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json +++ b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_bulk_edit": 1, "creation": "2018-07-09 17:20:44.737289", "doctype": "DocType", "editable_grid": 1, @@ -34,6 +35,7 @@ "ignore_user_permissions": 1, "in_list_view": 1, "label": "Source Warehouse", + "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]", "options": "Warehouse" }, { @@ -113,7 +115,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-12-04 14:30:19.472294", + "modified": "2026-05-12 12:22:18.506904", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card Item", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index d26f6040f4c..5131b30c889 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -12,21 +12,10 @@ frappe.ui.form.on("Work Order", { frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"]; // Set query for warehouses - frm.set_query("wip_warehouse", function () { - return { - filters: { - company: frm.doc.company, - }, - }; - }); - - frm.set_query("source_warehouse", function () { - return { - filters: { - company: frm.doc.company, - }, - }; - }); + frm.events.set_company_filters(frm, "wip_warehouse"); + frm.events.set_company_filters(frm, "source_warehouse"); + frm.events.set_company_filters(frm, "fg_warehouse"); + frm.events.set_company_filters(frm, "scrap_warehouse"); frm.set_query("source_warehouse", "required_items", function () { return { @@ -44,24 +33,6 @@ frappe.ui.form.on("Work Order", { }; }); - frm.set_query("fg_warehouse", function () { - return { - filters: { - company: frm.doc.company, - is_group: 0, - }, - }; - }); - - frm.set_query("scrap_warehouse", function () { - return { - filters: { - company: frm.doc.company, - is_group: 0, - }, - }; - }); - // Set query for BOM frm.set_query("bom_no", function () { if (frm.doc.production_item) { @@ -118,6 +89,16 @@ frappe.ui.form.on("Work Order", { }); }, + set_company_filters(frm, fieldname) { + frm.set_query(fieldname, () => { + return { + filters: { + company: frm.doc.company, + }, + }; + }); + }, + onload: function (frm) { if (!frm.doc.status) frm.doc.status = "Draft"; @@ -348,7 +329,7 @@ frappe.ui.form.on("Work Order", { { fieldtype: "Data", fieldname: "name", - label: __("Operation Id"), + label: __("Operation ID"), }, { fieldtype: "Float", @@ -425,6 +406,7 @@ frappe.ui.form.on("Work Order", { if (pending_qty) { dialog.fields_dict.operations.df.data.push({ + __checked: 1, name: data.name, operation: data.operation, workstation: data.workstation, diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 2d5145141ae..9dc57a3b99c 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_bulk_edit": 1, "allow_import": 1, "autoname": "naming_series:", "creation": "2025-04-09 12:09:40.634472", @@ -266,6 +267,7 @@ "fieldname": "wip_warehouse", "fieldtype": "Link", "label": "Work-in-Progress Warehouse", + "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]", "mandatory_depends_on": "eval:(!doc.skip_transfer || doc.from_wip_warehouse) && !doc.track_semi_finished_goods", "options": "Warehouse" }, @@ -274,6 +276,7 @@ "fieldname": "fg_warehouse", "fieldtype": "Link", "label": "Target Warehouse", + "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]", "options": "Warehouse", "read_only_depends_on": "subcontracting_inward_order" }, @@ -286,6 +289,7 @@ "fieldname": "scrap_warehouse", "fieldtype": "Link", "label": "Scrap Warehouse", + "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]", "options": "Warehouse" }, { @@ -513,6 +517,7 @@ "fieldname": "source_warehouse", "fieldtype": "Link", "label": "Source Warehouse", + "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]", "options": "Warehouse", "read_only_depends_on": "eval:doc.subcontracting_inward_order" }, @@ -706,7 +711,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2026-04-17 13:42:12.374055", + "modified": "2026-05-19 12:20:38.102403", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json index e5f322ca367..dec4934ea4f 100644 --- a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json +++ b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_bulk_edit": 1, "creation": "2016-04-18 07:38:26.314642", "doctype": "DocType", "editable_grid": 1, @@ -53,6 +54,7 @@ "ignore_user_permissions": 1, "in_list_view": 1, "label": "Source Warehouse", + "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]", "options": "Warehouse", "read_only_depends_on": "eval:parent.subcontracting_inward_order && doc.is_customer_provided_item" }, @@ -207,7 +209,7 @@ "grid_page_length": 50, "istable": 1, "links": [], - "modified": "2025-12-02 11:16:05.081613", + "modified": "2026-05-12 12:05:16.687866", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Item", diff --git a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json index 1bbafc08446..6d638b6e59b 100644 --- a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json +++ b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_bulk_edit": 1, "creation": "2014-07-11 11:51:00.453717", "doctype": "DocType", "editable_grid": 1, @@ -13,7 +14,10 @@ "amount", "base_amount", "has_corrective_cost", - "has_operating_cost" + "has_operating_cost", + "operation_id", + "qty", + "operating_component" ], "fields": [ { @@ -78,13 +82,38 @@ "fieldtype": "Check", "label": "Has Operating Cost", "read_only": 1 + }, + { + "fieldname": "operation_id", + "fieldtype": "Data", + "hidden": 1, + "label": "Operation ID", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "qty", + "fieldtype": "Float", + "hidden": 1, + "label": "Qty", + "no_copy": 1, + "non_negative": 1, + "read_only": 1 + }, + { + "fieldname": "operating_component", + "fieldtype": "Data", + "hidden": 1, + "label": "Operating Component", + "no_copy": 1, + "read_only": 1 } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-07-16 15:27:59.175530", + "modified": "2026-05-19 12:21:07.953801", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Taxes and Charges", diff --git a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.py b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.py index a4fb129a7ae..e6a9c0535cf 100644 --- a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.py +++ b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.py @@ -22,9 +22,12 @@ class LandedCostTaxesandCharges(Document): expense_account: DF.Link | None has_corrective_cost: DF.Check has_operating_cost: DF.Check + operating_component: DF.Data | None + operation_id: DF.Data | None parent: DF.Data parentfield: DF.Data parenttype: DF.Data + qty: DF.Float # end: auto-generated types pass diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 2951b7272b3..f10f7db755c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -10,7 +10,7 @@ from frappe import _, bold from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc from frappe.query_builder import DocType -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import Max, Sum from frappe.utils import ( cint, comma_or, @@ -3875,28 +3875,33 @@ def get_work_order_details(work_order: str, company: str): } -def get_consumed_operating_cost(wo_name, bom_no): +def get_consumed_operating_cost(wo_name, bom_no, operation_id): table = frappe.qb.DocType("Stock Entry") child_table = frappe.qb.DocType("Landed Cost Taxes and Charges") query = ( frappe.qb.from_(child_table) .join(table) .on(child_table.parent == table.name) - .select(Sum(child_table.amount).as_("consumed_cost")) + .select( + Sum(child_table.amount).as_("consumed_cost"), + Sum(child_table.qty).as_("consumed_qty"), + child_table.operating_component, + ) .where( (table.docstatus == 1) & (table.work_order == wo_name) & (table.purpose == "Manufacture") & (table.bom_no == bom_no) & (child_table.has_operating_cost == 1) + & (child_table.operation_id == operation_id) ) + .groupby(child_table.operation_id, child_table.operating_component) ) - cost = query.run(pluck="consumed_cost") - return cost[0] if cost and cost[0] else 0 + return query.run(as_dict=True) -def get_operating_cost_per_unit(work_order=None, bom_no=None): - operating_cost_per_unit = 0 +def get_remaining_operating_cost(work_order=None, bom_no=None): + remaining_operating_cost = 0 if work_order: if ( bom_no @@ -3911,23 +3916,23 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None): bom_no = work_order.bom_no for d in work_order.get("operations"): + consumed_op_cost = get_consumed_operating_cost(work_order.name, bom_no, d.name) or [] + cost = 0 + for row in consumed_op_cost: + cost += flt(row.consumed_cost) + if flt(d.completed_qty): - if not (remaining_qty := flt(d.completed_qty - work_order.produced_qty)): - continue - operating_cost_per_unit += ( - flt(d.actual_operating_cost - get_consumed_operating_cost(work_order.name, bom_no)) - / remaining_qty - ) + remaining_operating_cost += flt(d.actual_operating_cost - cost) elif work_order.qty: - operating_cost_per_unit += flt(d.planned_operating_cost) / flt(work_order.qty) + remaining_operating_cost += flt(d.planned_operating_cost) / flt(work_order.qty) # Get operating cost from BOM if not found in work_order. - if not operating_cost_per_unit and bom_no: + if not remaining_operating_cost and bom_no: bom = frappe.db.get_value("BOM", bom_no, ["operating_cost", "quantity"], as_dict=1) if bom.quantity: - operating_cost_per_unit = flt(bom.operating_cost) / flt(bom.quantity) + remaining_operating_cost = flt(bom.operating_cost) / flt(bom.quantity) - return operating_cost_per_unit + return remaining_operating_cost def get_used_alternative_items(