mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-24 05:47:15 +00:00
fix(stock): allow zero completed quantity and handle process loss in job cards (#59104)
(cherry picked from commit 1d8ce1ee8c)
This commit is contained in:
@@ -264,7 +264,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;
|
||||
@@ -389,8 +388,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({
|
||||
|
||||
@@ -194,10 +194,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)
|
||||
)
|
||||
|
||||
@@ -951,9 +953,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)
|
||||
@@ -1543,7 +1546,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:
|
||||
@@ -1559,13 +1562,13 @@ class JobCard(Document):
|
||||
for employee in kwargs.employees:
|
||||
kwargs.employee = employee.get("employee")
|
||||
if kwargs.from_time and not kwargs.to_time:
|
||||
if kwargs.qty:
|
||||
if kwargs.get("qty") is not None:
|
||||
kwargs.completed_qty = kwargs.qty
|
||||
|
||||
row = self.append("time_logs", kwargs)
|
||||
row.db_update()
|
||||
self.db_set("status", "Work In Progress")
|
||||
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:
|
||||
update_status = True
|
||||
for row in self.time_logs:
|
||||
if row.employee != kwargs.employee:
|
||||
@@ -1659,6 +1662,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."))
|
||||
|
||||
|
||||
@@ -1136,6 +1136,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)
|
||||
|
||||
@@ -2975,6 +3019,12 @@ class TestJobCard(ERPNextTestSuite):
|
||||
jc.validate_completion_qty_split(
|
||||
frappe._dict(for_quantity=5, qty=3, pending_qty=2, process_loss_qty=0)
|
||||
)
|
||||
jc.validate_completion_qty_split(
|
||||
frappe._dict(for_quantity=5, qty=0, pending_qty=0, process_loss_qty=5)
|
||||
)
|
||||
jc.validate_completion_qty_split(
|
||||
frappe._dict(for_quantity=5, qty=0, pending_qty=5, process_loss_qty=0)
|
||||
)
|
||||
|
||||
self.assertRaises(
|
||||
frappe.ValidationError,
|
||||
@@ -2988,6 +3038,31 @@ class TestJobCard(ERPNextTestSuite):
|
||||
frappe._dict(for_quantity=1, qty=0.3334, pending_qty=0.3334, process_loss_qty=0.3334),
|
||||
)
|
||||
|
||||
def test_complete_job_card_qty_guards(self):
|
||||
jc = frappe.new_doc("Job Card")
|
||||
jc.for_quantity = 5
|
||||
self.assertRaises(frappe.ValidationError, jc.validate_complete_job_card_qty, frappe._dict(qty=-1))
|
||||
|
||||
def test_set_process_loss(self):
|
||||
nothing_done = frappe.new_doc("Job Card")
|
||||
nothing_done.for_quantity = 10
|
||||
nothing_done.total_completed_qty = 0
|
||||
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 create_bom_with_multiple_operations():
|
||||
"Create a BOM with multiple operations and Material Transfer against Job Card"
|
||||
|
||||
Reference in New Issue
Block a user