fix: validate Work Order qty against Production Plan (#29721) (#30003)

* fix: validate Work Order qty against Production Plan

* chore: err msg when max_qty is 0

* test: add test for overproduction

* fix: CI

(cherry picked from commit 067ede76ea0851a29e270e150f482368e0afa194)

Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index d88e10a..2359815 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -9,6 +9,7 @@
 	get_sales_orders,
 	get_warehouse_list,
 )
+from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError
 from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
 from erpnext.stock.doctype.item.test_item import create_item
 from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@@ -466,26 +467,29 @@
 		bom = make_bom(item=item, raw_materials=raw_materials)
 
 		# Create Production Plan
-		pln = create_production_plan(item_code=bom.item, planned_qty=10)
+		pln = create_production_plan(item_code=bom.item, planned_qty=5)
 
 		# All the created Work Orders
 		wo_list = []
 
-		# Create and Submit 1st Work Order for 5 qty
-		create_work_order(item, pln, 5)
+		# Create and Submit 1st Work Order for 3 qty
+		create_work_order(item, pln, 3)
+		pln.reload()
+		self.assertEqual(pln.po_items[0].ordered_qty, 3)
+
+		# Create and Submit 2nd Work Order for 2 qty
+		create_work_order(item, pln, 2)
 		pln.reload()
 		self.assertEqual(pln.po_items[0].ordered_qty, 5)
 
-		# Create and Submit 2nd Work Order for 3 qty
-		create_work_order(item, pln, 3)
-		pln.reload()
-		self.assertEqual(pln.po_items[0].ordered_qty, 8)
+		# Overproduction
+		self.assertRaises(OverProductionError, create_work_order, item=item, pln=pln, qty=2)
 
 		# Cancel 1st Work Order
 		wo1 = frappe.get_doc("Work Order", wo_list[0])
 		wo1.cancel()
 		pln.reload()
-		self.assertEqual(pln.po_items[0].ordered_qty, 3)
+		self.assertEqual(pln.po_items[0].ordered_qty, 2)
 
 		# Cancel 2nd Work Order
 		wo2 = frappe.get_doc("Work Order", wo_list[1])
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index ed6a029..e1120c7 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -636,6 +636,21 @@
 		if not self.qty > 0:
 			frappe.throw(_("Quantity to Manufacture must be greater than 0."))
 
+		if self.production_plan and self.production_plan_item:
+			qty_dict = frappe.db.get_value("Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1)
+
+			allowance_qty =flt(frappe.db.get_single_value("Manufacturing Settings",
+			"overproduction_percentage_for_work_order"))/100 * qty_dict.get("planned_qty", 0)
+
+			max_qty = qty_dict.get("planned_qty", 0) + allowance_qty - qty_dict.get("ordered_qty", 0)
+
+			if max_qty < 1:
+				frappe.throw(_("Cannot produce more item for {0}")
+				.format(self.production_item), OverProductionError)
+			elif self.qty > max_qty:
+				frappe.throw(_("Cannot produce more than {0} items for {1}")
+				.format(max_qty, self.production_item), OverProductionError)
+
 	def validate_transfer_against(self):
 		if not self.docstatus == 1:
 			# let user configure operations until they're ready to submit