diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index a19d11adf5d..073446e91ca 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -251,6 +251,7 @@ frappe.ui.form.on("Job Card", { change() { const dialog = frm.job_completion_dialog; dialog.set_value("completed_qty", dialog.get_value("for_quantity")); + dialog.set_value("pending_qty", 0); dialog.set_value("process_loss_qty", 0); }, }, @@ -262,8 +263,21 @@ frappe.ui.form.on("Job Card", { default: pending_qty, change() { const dialog = frm.job_completion_dialog; - const remaining = dialog.get_value("for_quantity") - dialog.get_value("completed_qty"); - if (remaining > 0 && remaining != dialog.get_value("pending_qty")) { + const remaining = + dialog.get_value("for_quantity") - + dialog.get_value("completed_qty") - + dialog.get_value("process_loss_qty"); + + if (remaining < 0) { + const max_completed_qty = + flt(dialog.get_value("for_quantity")) - flt(dialog.get_value("process_loss_qty")); + dialog.set_value("completed_qty", max_completed_qty); + frappe.throw( + __("Completed Quantity cannot be greater than {0}", [max_completed_qty]) + ); + } + + if (remaining != dialog.get_value("pending_qty")) { dialog.set_value("pending_qty", remaining); } }, @@ -279,7 +293,18 @@ frappe.ui.form.on("Job Card", { dialog.get_value("for_quantity") - dialog.get_value("completed_qty") - dialog.get_value("pending_qty"); - if (process_loss_qty >= 0 && process_loss_qty != dialog.get_value("process_loss_qty")) { + + if (process_loss_qty < 0) { + dialog.set_value("pending_qty", 0); + frappe.throw( + __("Pending Quantity cannot be greater than {0}", [ + flt(dialog.get_value("for_quantity")) - + flt(dialog.get_value("completed_qty")), + ]) + ); + } + + if (process_loss_qty != dialog.get_value("process_loss_qty")) { dialog.set_value("process_loss_qty", process_loss_qty); } }, @@ -294,7 +319,18 @@ frappe.ui.form.on("Job Card", { dialog.get_value("for_quantity") - dialog.get_value("completed_qty") - dialog.get_value("process_loss_qty"); - if (remaining >= 0 && remaining != dialog.get_value("pending_qty")) { + + if (remaining < 0) { + dialog.set_value("process_loss_qty", 0); + frappe.throw( + __("Process Loss Quantity cannot be greater than {0}", [ + flt(dialog.get_value("for_quantity")) - + flt(dialog.get_value("completed_qty")), + ]) + ); + } + + if (remaining != dialog.get_value("pending_qty")) { dialog.set_value("pending_qty", remaining); } }, diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index a06529d0699..069dcdb7299 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -895,22 +895,21 @@ class JobCard(Document): ) precision = self.precision("total_completed_qty") - total_completed_qty = flt( + accounted_qty = flt( flt(self.total_completed_qty, precision) + flt(self.process_loss_qty, precision) + flt(self.pending_qty, precision) ) - if self.for_quantity and flt(total_completed_qty, precision) != flt(self.for_quantity, precision): - total_completed_qty_label = bold(_("Total Completed Qty")) - qty_to_manufacture = bold(_("Qty to Manufacture")) - + if self.for_quantity and flt(accounted_qty, precision) != flt(self.for_quantity, precision): frappe.throw( - _("The {0} ({1}) must be equal to {2} ({3})").format( - total_completed_qty_label, - bold(flt(total_completed_qty, precision)), - qty_to_manufacture, - bold(self.for_quantity), + _( + "Total Completed Qty ({0}), Process Loss Qty ({1}) and Pending Qty ({2}) must add up to the Qty to Manufacture ({3})." + ).format( + bold(flt(self.total_completed_qty, precision)), + bold(flt(self.process_loss_qty, precision)), + bold(flt(self.pending_qty, precision)), + bold(flt(self.for_quantity, precision)), ) ) @@ -1605,6 +1604,8 @@ class JobCard(Document): if flt(kwargs.pending_qty) and flt(kwargs.pending_qty) > self.for_quantity: frappe.throw(_("Pending quantity cannot be greater than the for quantity.")) + self.validate_completion_qty_split(kwargs) + self.pending_qty = flt(kwargs.pending_qty) self.process_loss_qty = flt(kwargs.process_loss_qty) @@ -1633,6 +1634,31 @@ class JobCard(Document): _("Job Card {0} has been completed").format(get_link_to_form("Job Card", self.name)) ) + def validate_completion_qty_split(self, kwargs): + if not flt(kwargs.for_quantity): + return + + precision = self.precision("total_completed_qty") + accounted_qty = flt( + flt(kwargs.qty, precision) + + flt(kwargs.pending_qty, precision) + + flt(kwargs.process_loss_qty, precision) + ) + + if flt(accounted_qty, precision) == flt(kwargs.for_quantity, precision): + return + + frappe.throw( + _( + "Completed Quantity ({0}), Pending Quantity ({1}) and Process Loss Quantity ({2}) must add up to the Qty to Manufacture ({3})." + ).format( + bold(flt(kwargs.qty, precision)), + bold(flt(kwargs.pending_qty, precision)), + bold(flt(kwargs.process_loss_qty, precision)), + bold(flt(kwargs.for_quantity, precision)), + ) + ) + @frappe.whitelist() def make_stock_entry_for_semi_fg_item(self, auto_submit: bool = False): def get_consumed_process_loss(): diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 934c33decd5..55b2a848469 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -2114,6 +2114,26 @@ class TestJobCard(ERPNextTestSuite): self.assertEqual(s.additional_costs[2].amount, 480) self.assertEqual(s.additional_costs[3].amount, 480) + def test_completion_qty_split_must_add_up(self): + jc = frappe.new_doc("Job Card") + jc.for_quantity = 5 + + jc.validate_completion_qty_split( + frappe._dict(for_quantity=5, qty=3, pending_qty=2, process_loss_qty=0) + ) + + self.assertRaises( + frappe.ValidationError, + jc.validate_completion_qty_split, + frappe._dict(for_quantity=3, qty=3, pending_qty=2, process_loss_qty=0), + ) + + self.assertRaises( + frappe.ValidationError, + jc.validate_completion_qty_split, + frappe._dict(for_quantity=1, qty=0.3334, pending_qty=0.3334, process_loss_qty=0.3334), + ) + def create_bom_with_multiple_operations(): "Create a BOM with multiple operations and Material Transfer against Job Card"