mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-19 19:37:56 +00:00
fix(stock): allow zero completed quantity and handle process loss in job cards (#59104)
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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."))
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user