mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-20 03:47:11 +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",
|
fieldtype: "Float",
|
||||||
label: __("Completed Quantity"),
|
label: __("Completed Quantity"),
|
||||||
fieldname: "completed_qty",
|
fieldname: "completed_qty",
|
||||||
reqd: 1,
|
|
||||||
default: pending_qty,
|
default: pending_qty,
|
||||||
change() {
|
change() {
|
||||||
const dialog = frm.job_completion_dialog;
|
const dialog = frm.job_completion_dialog;
|
||||||
@@ -424,8 +423,8 @@ frappe.ui.form.on("Job Card", {
|
|||||||
frm.job_completion_dialog = frappe.prompt(
|
frm.job_completion_dialog = frappe.prompt(
|
||||||
fields,
|
fields,
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.qty <= 0) {
|
if (data.completed_qty < 0) {
|
||||||
frappe.throw(__("Quantity should be greater than 0"));
|
frappe.throw(__("Completed Quantity cannot be negative"));
|
||||||
}
|
}
|
||||||
|
|
||||||
frm.call({
|
frm.call({
|
||||||
|
|||||||
@@ -205,10 +205,12 @@ class JobCard(Document):
|
|||||||
).format(self.name)
|
).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(
|
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)
|
).format(self.name)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1013,9 +1015,10 @@ class JobCard(Document):
|
|||||||
|
|
||||||
def set_process_loss(self):
|
def set_process_loss(self):
|
||||||
precision = self.precision("total_completed_qty")
|
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
|
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 = (
|
self.process_loss_qty = (
|
||||||
flt(self.for_quantity, precision)
|
flt(self.for_quantity, precision)
|
||||||
- flt(self.total_completed_qty, precision)
|
- flt(self.total_completed_qty, precision)
|
||||||
@@ -1639,7 +1642,7 @@ class JobCard(Document):
|
|||||||
row.to_time = kwargs.to_time
|
row.to_time = kwargs.to_time
|
||||||
row.time_in_mins = time_diff_in_minutes(row.to_time, row.from_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.completed_qty = kwargs.completed_qty
|
||||||
row.db_update()
|
row.db_update()
|
||||||
else:
|
else:
|
||||||
@@ -1655,7 +1658,7 @@ class JobCard(Document):
|
|||||||
kwargs.employee = employee.get("employee")
|
kwargs.employee = employee.get("employee")
|
||||||
if kwargs.from_time and not kwargs.to_time:
|
if kwargs.from_time and not kwargs.to_time:
|
||||||
self.add_new_time_log_for_employee(kwargs)
|
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)
|
self.update_completed_qty_for_employee(kwargs)
|
||||||
update_status = True
|
update_status = True
|
||||||
else:
|
else:
|
||||||
@@ -1665,7 +1668,7 @@ class JobCard(Document):
|
|||||||
self.set_status(update_status=update_status)
|
self.set_status(update_status=update_status)
|
||||||
|
|
||||||
def add_new_time_log_for_employee(self, kwargs):
|
def add_new_time_log_for_employee(self, kwargs):
|
||||||
if kwargs.qty:
|
if kwargs.get("qty") is not None:
|
||||||
kwargs.completed_qty = kwargs.qty
|
kwargs.completed_qty = kwargs.qty
|
||||||
|
|
||||||
row = self.append("time_logs", kwargs)
|
row = self.append("time_logs", kwargs)
|
||||||
@@ -1770,6 +1773,9 @@ class JobCard(Document):
|
|||||||
frappe.throw(_("Submitted Job Card cannot be processed."))
|
frappe.throw(_("Submitted Job Card cannot be processed."))
|
||||||
|
|
||||||
def validate_complete_job_card_qty(self, kwargs):
|
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:
|
if flt(kwargs.pending_qty) and flt(kwargs.pending_qty) < 0:
|
||||||
frappe.throw(_("Pending quantity cannot be negative."))
|
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.total_completed_qty), 3)
|
||||||
self.assertEqual(flt(job_card.process_loss_qty), 0)
|
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):
|
def test_completion_qty_keeps_for_quantity_across_cycles(self):
|
||||||
work_order = make_wo_order_test_record(item="_Test FG Item 2", qty=5)
|
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 = frappe.new_doc("Job Card")
|
||||||
jc.for_quantity = 5
|
jc.for_quantity = 5
|
||||||
jc.validate_complete_job_card_qty(frappe._dict(pending_qty=3)) # within range -> passes
|
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(
|
self.assertRaises(
|
||||||
frappe.ValidationError, jc.validate_complete_job_card_qty, frappe._dict(pending_qty=-1)
|
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(
|
jc.validate_complete_job_card_qty(
|
||||||
frappe._dict(for_quantity=5, qty=3, pending_qty=2, process_loss_qty=0)
|
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(
|
self.assertRaises(
|
||||||
frappe.ValidationError,
|
frappe.ValidationError,
|
||||||
@@ -3483,6 +3534,19 @@ class TestJobCardLogic(ERPNextTestSuite):
|
|||||||
nothing_done.set_process_loss()
|
nothing_done.set_process_loss()
|
||||||
self.assertEqual(nothing_done.process_loss_qty, 0)
|
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):
|
def test_capacity_overlap_detection(self):
|
||||||
jc = frappe.new_doc("Job Card")
|
jc = frappe.new_doc("Job Card")
|
||||||
sequential = [
|
sequential = [
|
||||||
|
|||||||
Reference in New Issue
Block a user