fix(stock): allow zero completed quantity and handle process loss in job cards (#59104)

This commit is contained in:
Afsal Syed
2026-09-16 20:14:32 +05:30
committed by GitHub
parent 4b858ec0af
commit 1d8ce1ee8c
3 changed files with 78 additions and 9 deletions

View File

@@ -299,7 +299,6 @@ frappe.ui.form.on("Job Card", {
fieldtype: "Float",
label: __("Completed Quantity"),
fieldname: "completed_qty",
reqd: 1,
default: pending_qty,
change() {
const dialog = frm.job_completion_dialog;
@@ -424,8 +423,8 @@ frappe.ui.form.on("Job Card", {
frm.job_completion_dialog = frappe.prompt(
fields,
(data) => {
if (data.qty <= 0) {
frappe.throw(__("Quantity should be greater than 0"));
if (data.completed_qty < 0) {
frappe.throw(__("Completed Quantity cannot be negative"));
}
frm.call({

View File

@@ -205,10 +205,12 @@ class JobCard(Document):
).format(self.name)
)
if self.docstatus == 1 and not self.total_completed_qty:
if self.docstatus == 1 and not (
self.total_completed_qty or self.process_loss_qty or self.pending_qty
):
frappe.throw(
_(
"Total Completed Qty is required for Job Card {0}, please start and complete the job card before submission"
"Completed, Process Loss or Pending Qty is required for Job Card {0}, please start and complete the job card before submission"
).format(self.name)
)
@@ -1013,9 +1015,10 @@ class JobCard(Document):
def set_process_loss(self):
precision = self.precision("total_completed_qty")
should_set_process_loss = self.total_completed_qty or self.process_loss_qty
self.process_loss_qty = 0.0
if self.total_completed_qty and self.for_quantity > self.total_completed_qty:
if should_set_process_loss and self.for_quantity > self.total_completed_qty:
self.process_loss_qty = (
flt(self.for_quantity, precision)
- flt(self.total_completed_qty, precision)
@@ -1639,7 +1642,7 @@ class JobCard(Document):
row.to_time = kwargs.to_time
row.time_in_mins = time_diff_in_minutes(row.to_time, row.from_time)
if kwargs.completed_qty:
if kwargs.get("completed_qty") is not None:
row.completed_qty = kwargs.completed_qty
row.db_update()
else:
@@ -1655,7 +1658,7 @@ class JobCard(Document):
kwargs.employee = employee.get("employee")
if kwargs.from_time and not kwargs.to_time:
self.add_new_time_log_for_employee(kwargs)
elif not kwargs.from_time and not kwargs.to_time and kwargs.completed_qty:
elif not kwargs.from_time and not kwargs.to_time and kwargs.get("completed_qty") is not None:
self.update_completed_qty_for_employee(kwargs)
update_status = True
else:
@@ -1665,7 +1668,7 @@ class JobCard(Document):
self.set_status(update_status=update_status)
def add_new_time_log_for_employee(self, kwargs):
if kwargs.qty:
if kwargs.get("qty") is not None:
kwargs.completed_qty = kwargs.qty
row = self.append("time_logs", kwargs)
@@ -1770,6 +1773,9 @@ class JobCard(Document):
frappe.throw(_("Submitted Job Card cannot be processed."))
def validate_complete_job_card_qty(self, kwargs):
if flt(kwargs.qty) < 0:
frappe.throw(_("Completed quantity cannot be negative."))
if flt(kwargs.pending_qty) and flt(kwargs.pending_qty) < 0:
frappe.throw(_("Pending quantity cannot be negative."))

View File

@@ -1330,6 +1330,50 @@ class TestJobCard(ERPNextTestSuite):
self.assertEqual(flt(job_card.total_completed_qty), 3)
self.assertEqual(flt(job_card.process_loss_qty), 0)
def test_completion_allows_zero_completed_qty(self):
work_order = make_wo_order_test_record(item="_Test FG Item 2", qty=5)
job_card = self.get_first_job_card(work_order.name)
job_card.append("time_logs", {"from_time": "2024-03-01 08:00:00"})
job_card.save()
job_card.complete_job_card(
qty=0,
for_quantity=5,
pending_qty=0,
process_loss_qty=5,
end_time="2024-03-01 09:00:00",
)
job_card.reload()
self.assertEqual(flt(job_card.total_completed_qty), 0)
self.assertEqual(flt(job_card.process_loss_qty), 5)
job_card.submit()
self.assertEqual(job_card.docstatus, 1)
def test_completion_overwrites_existing_completed_qty_with_zero(self):
work_order = make_wo_order_test_record(item="_Test FG Item 2", qty=5)
job_card = self.get_first_job_card(work_order.name)
job_card.append("time_logs", {"from_time": "2024-03-01 08:00:00", "completed_qty": 5})
job_card.complete_job_card(
qty=0,
for_quantity=5,
pending_qty=0,
process_loss_qty=5,
end_time="2024-03-01 09:00:00",
)
job_card.reload()
self.assertEqual(flt(job_card.total_completed_qty), 0)
self.assertEqual(flt(job_card.process_loss_qty), 5)
self.assertEqual(flt(job_card.time_logs[0].completed_qty), 0)
job_card.submit()
self.assertEqual(job_card.docstatus, 1)
def test_completion_qty_keeps_for_quantity_across_cycles(self):
work_order = make_wo_order_test_record(item="_Test FG Item 2", qty=5)
@@ -3424,6 +3468,7 @@ class TestJobCardLogic(ERPNextTestSuite):
jc = frappe.new_doc("Job Card")
jc.for_quantity = 5
jc.validate_complete_job_card_qty(frappe._dict(pending_qty=3)) # within range -> passes
self.assertRaises(frappe.ValidationError, jc.validate_complete_job_card_qty, frappe._dict(qty=-1))
self.assertRaises(
frappe.ValidationError, jc.validate_complete_job_card_qty, frappe._dict(pending_qty=-1)
)
@@ -3449,6 +3494,12 @@ class TestJobCardLogic(ERPNextTestSuite):
jc.validate_complete_job_card_qty(
frappe._dict(for_quantity=5, qty=3, pending_qty=2, process_loss_qty=0)
)
jc.validate_complete_job_card_qty(
frappe._dict(for_quantity=5, qty=0, pending_qty=0, process_loss_qty=5)
)
jc.validate_complete_job_card_qty(
frappe._dict(for_quantity=5, qty=0, pending_qty=5, process_loss_qty=0)
)
self.assertRaises(
frappe.ValidationError,
@@ -3483,6 +3534,19 @@ class TestJobCardLogic(ERPNextTestSuite):
nothing_done.set_process_loss()
self.assertEqual(nothing_done.process_loss_qty, 0)
all_process_loss = frappe.new_doc("Job Card")
all_process_loss.for_quantity = 10
all_process_loss.process_loss_qty = 10
all_process_loss.set_process_loss()
self.assertEqual(all_process_loss.process_loss_qty, 10)
def test_zero_completed_qty_is_valid_for_semi_finished_goods(self):
jc = frappe.new_doc("Job Card")
jc.docstatus = 1
jc.track_semi_finished_goods = 1
jc.process_loss_qty = 5
jc.validate_semi_finished_goods()
def test_capacity_overlap_detection(self):
jc = frappe.new_doc("Job Card")
sequential = [