Merge pull request #57933 from frappe/mergify/bp/version-16-hotfix/pr-57687

fix(job_card): reject a completion split that cannot add up (backport #57687)
This commit is contained in:
Mihir Kandoi
2026-08-09 22:31:48 +05:30
committed by GitHub
3 changed files with 96 additions and 14 deletions

View File

@@ -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);
}
},

View File

@@ -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():

View File

@@ -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"