Merge branch 'develop' into fg_based_operating_cost
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index 4dd8205..4304193 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -65,7 +65,21 @@
 		});
 	},
 
-	onload_post_render(frm) {
+	validate: function(frm) {
+		if (frm.doc.fg_based_operating_cost && frm.doc.with_operations) {
+			frappe.throw({message: __("Please check either with operations or FG Based Operating Cost."), title: __("Mandatory")});
+		}
+	},
+
+	with_operations: function(frm) {
+		frm.set_df_property("fg_based_operating_cost", "hidden", frm.doc.with_operations ? 1 : 0);
+	},
+
+	fg_based_operating_cost: function(frm) {
+		frm.set_df_property("with_operations", "hidden", frm.doc.fg_based_operating_cost ? 1 : 0);
+	},
+
+	onload_post_render: function(frm) {
 		frm.get_field("items").grid.set_multiple_add("item_code", "qty");
 	},
 
@@ -532,18 +546,25 @@
 };
 
 erpnext.bom.calculate_op_cost = function(doc) {
-	var op = doc.operations || [];
 	doc.operating_cost = 0.0;
 	doc.base_operating_cost = 0.0;
 
-	for(var i=0;i<op.length;i++) {
-		var operating_cost = flt(flt(op[i].hour_rate) * flt(op[i].time_in_mins) / 60, 2);
-		var base_operating_cost = flt(operating_cost * doc.conversion_rate, 2);
-		frappe.model.set_value('BOM Operation',op[i].name, "operating_cost", operating_cost);
-		frappe.model.set_value('BOM Operation',op[i].name, "base_operating_cost", base_operating_cost);
+	if(doc.with_operations) {
+		doc.operations.forEach((item) => {
+			let operating_cost = flt(flt(item.hour_rate) * flt(item.time_in_mins) / 60, 2);
+			let base_operating_cost = flt(operating_cost * doc.conversion_rate, 2);
+			frappe.model.set_value('BOM Operation',item.name, {
+				"operating_cost": operating_cost,
+				"base_operating_cost": base_operating_cost
+			});
 
-		doc.operating_cost += operating_cost;
-		doc.base_operating_cost += base_operating_cost;
+			doc.operating_cost += operating_cost;
+			doc.base_operating_cost += base_operating_cost;
+		});
+	} else if(doc.fg_based_operating_cost) {
+		let total_operating_cost = doc.quantity * flt(doc.operating_cost_per_bom_quantity);
+		doc.operating_cost = total_operating_cost;
+		doc.base_operating_cost = flt(total_operating_cost * doc.conversion_rate, 2);
 	}
 	refresh_field(['operating_cost', 'base_operating_cost']);
 };
diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json
index c31b69f..c2b331f 100644
--- a/erpnext/manufacturing/doctype/bom/bom.json
+++ b/erpnext/manufacturing/doctype/bom/bom.json
@@ -33,6 +33,9 @@
   "column_break_23",
   "transfer_material_against",
   "routing",
+  "fg_based_operating_cost",
+  "fg_based_section_section",
+  "operating_cost_per_bom_quantity",
   "operations_section",
   "operations",
   "materials_section",
@@ -575,7 +578,26 @@
   {
    "fieldname": "scrap_items_section",
    "fieldtype": "Section Break",
+   "hide_border": 1,
    "label": "Scrap Items"
+  },
+  {
+   "default": "0",
+   "fieldname": "fg_based_operating_cost",
+   "fieldtype": "Check",
+   "label": "FG based Operating Cost"
+  },
+  {
+   "depends_on": "fg_based_operating_cost",
+   "fieldname": "fg_based_section_section",
+   "fieldtype": "Section Break",
+   "label": "FG Based Operating Cost Section"
+  },
+  {
+   "depends_on": "fg_based_operating_cost",
+   "fieldname": "operating_cost_per_bom_quantity",
+   "fieldtype": "Currency",
+   "label": "Operating Cost Per BOM Quantity"
   }
  ],
  "icon": "fa fa-sitemap",
@@ -583,7 +605,7 @@
  "image_field": "image",
  "is_submittable": 1,
  "links": [],
- "modified": "2023-01-03 18:42:27.732107",
+ "modified": "2023-01-10 07:47:08.652616",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "BOM",
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 53af28d..8ab79e6 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -614,18 +614,26 @@
 		"""Update workstation rate and calculates totals"""
 		self.operating_cost = 0
 		self.base_operating_cost = 0
-		for d in self.get("operations"):
-			if d.workstation:
-				self.update_rate_and_time(d, update_hour_rate)
+		if self.get("with_operations"):
+			for d in self.get("operations"):
+				if d.workstation:
+					self.update_rate_and_time(d, update_hour_rate)
 
-			operating_cost = d.operating_cost
-			base_operating_cost = d.base_operating_cost
-			if d.set_cost_based_on_bom_qty:
-				operating_cost = flt(d.cost_per_unit) * flt(self.quantity)
-				base_operating_cost = flt(d.base_cost_per_unit) * flt(self.quantity)
+				operating_cost = d.operating_cost
+				base_operating_cost = d.base_operating_cost
+				if d.set_cost_based_on_bom_qty:
+					operating_cost = flt(d.cost_per_unit) * flt(self.quantity)
+					base_operating_cost = flt(d.base_cost_per_unit) * flt(self.quantity)
 
-			self.operating_cost += flt(operating_cost)
-			self.base_operating_cost += flt(base_operating_cost)
+				self.operating_cost += flt(operating_cost)
+				self.base_operating_cost += flt(base_operating_cost)
+
+		elif self.get("fg_based_operating_cost"):
+			total_operating_cost = flt(self.get("quantity")) * flt(
+				self.get("operating_cost_per_bom_quantity")
+			)
+			self.operating_cost = total_operating_cost
+			self.base_operating_cost = flt(total_operating_cost * self.conversion_rate, 2)
 
 	def update_rate_and_time(self, row, update_hour_rate=False):
 		if not row.hour_rate or update_hour_rate:
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 16f5c79..d60feb2 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -202,6 +202,33 @@
 
 		self.assertEqual(bom.items[0].rate, 20)
 
+	def test_bom_cost_with_fg_based_operating_cost(self):
+		bom = frappe.copy_doc(test_records[4])
+		bom.insert()
+
+		raw_material_cost = 0.0
+		op_cost = 0.0
+
+		op_cost = bom.quantity * bom.operating_cost_per_bom_quantity
+
+		for row in bom.items:
+			raw_material_cost += row.amount
+
+		base_raw_material_cost = raw_material_cost * flt(
+			bom.conversion_rate, bom.precision("conversion_rate")
+		)
+		base_op_cost = op_cost * flt(bom.conversion_rate, bom.precision("conversion_rate"))
+
+		# test amounts in selected currency, almostEqual checks for 7 digits by default
+		self.assertAlmostEqual(bom.operating_cost, op_cost)
+		self.assertAlmostEqual(bom.raw_material_cost, raw_material_cost)
+		self.assertAlmostEqual(bom.total_cost, raw_material_cost + op_cost)
+
+		# test amounts in selected currency
+		self.assertAlmostEqual(bom.base_operating_cost, base_op_cost)
+		self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
+		self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
+
 	def test_subcontractor_sourced_item(self):
 		item_code = "_Test Subcontracted FG Item 1"
 		set_backflush_based_on("Material Transferred for Subcontract")
diff --git a/erpnext/manufacturing/doctype/bom/test_records.json b/erpnext/manufacturing/doctype/bom/test_records.json
index 507d319..e9cbdfe 100644
--- a/erpnext/manufacturing/doctype/bom/test_records.json
+++ b/erpnext/manufacturing/doctype/bom/test_records.json
@@ -162,5 +162,31 @@
   "item": "_Test Variant Item",
   "quantity": 1.0,
   "with_operations": 1
+ },
+ {
+  "items": [
+   {
+    "amount": 5000.0,
+    "doctype": "BOM Item",
+    "item_code": "_Test Item",
+    "parentfield": "items",
+    "qty": 2.0,
+    "rate": 3000.0,
+    "uom": "_Test UOM",
+    "stock_uom": "_Test UOM",
+    "source_warehouse": "_Test Warehouse - _TC",
+    "include_item_in_manufacturing": 1
+   }
+  ],
+  "docstatus": 1,
+  "doctype": "BOM",
+  "is_active": 1,
+  "is_default": 1,
+  "currency": "USD",
+  "item": "_Test Variant Item",
+  "quantity": 1.0,
+  "with_operations": 0,
+  "fg_based_operating_cost": 1,
+  "operating_cost_per_bom_quantity": 140
  }
 ]