fix(job_card): leave the pending qty out of the job card's own output (#57686)

* fix(job_card): leave the pending qty out of the job card's own output

Pending qty is the part of a job card handed over to another job card, but the
status and the manufacturing entry still measured the card against its full
for_quantity. A card submitted with 3 completed and 2 pending was stuck at Work
In Progress with no way to change it, and its manufacturing entry was built for
the full 5.

Measure both against for_quantity minus pending qty, so the card reaches To
Manufacture on submission, its manufacturing entry covers the completed qty, and
it is Completed once that qty is manufactured.

* test(job_card): cover a job card completed with a pending qty
This commit is contained in:
Mihir Kandoi
2026-08-01 18:34:30 +05:30
committed by GitHub
parent 0e1bc58b2e
commit 970039d8ec
3 changed files with 93 additions and 5 deletions

View File

@@ -99,7 +99,8 @@ frappe.ui.form.on("Job Card", {
doc.docstatus === 1 &&
!doc.is_subcontracted &&
(doc.skip_material_transfer || doc.transferred_qty > 0) &&
flt(doc.manufactured_qty) + flt(doc.process_loss_qty) < flt(doc.for_quantity);
flt(doc.manufactured_qty) + flt(doc.process_loss_qty) <
flt(doc.for_quantity) - flt(doc.pending_qty);
if (!can_make_stock_entry) return;

View File

@@ -1310,11 +1310,17 @@ class JobCard(Document):
if self.workstation:
self.update_workstation_status()
def get_qty_to_produce(self):
"""Qty this job card is expected to produce, the pending qty is left to another job card."""
return flt(self.for_quantity) - flt(self.pending_qty)
def set_finished_good_status(self):
# Only reached for a submitted job card (docstatus == 1) with a finished good, see set_status().
if (self.manufactured_qty + self.process_loss_qty) >= self.for_quantity:
qty_to_produce = self.get_qty_to_produce()
if (self.manufactured_qty + self.process_loss_qty) >= qty_to_produce:
self.status = "Completed"
elif (self.total_completed_qty + self.process_loss_qty) >= self.for_quantity:
elif (self.total_completed_qty + self.process_loss_qty) >= qty_to_produce:
# Production is done and the card is submitted, but the finished goods have not been
# booked into stock yet (Manufacture Stock Entry pending) — distinct from active WIP.
self.status = "To Manufacture"
@@ -1344,7 +1350,7 @@ class JobCard(Document):
self.status = "Work In Progress"
if self.docstatus == 1 and (
self.for_quantity <= (self.total_completed_qty + self.process_loss_qty) or not self.items
self.get_qty_to_produce() <= (self.total_completed_qty + self.process_loss_qty) or not self.items
):
self.status = "Completed"
@@ -1752,7 +1758,7 @@ class JobCard(Document):
return ManufactureEntry(
{
"for_quantity": self.for_quantity - self.manufactured_qty,
"for_quantity": self.get_qty_to_produce() - self.manufactured_qty,
"process_loss_qty": max(self.process_loss_qty - self.get_consumed_process_loss(), 0),
"job_card": self.name,
"skip_material_transfer": self.skip_material_transfer,

View File

@@ -1358,6 +1358,87 @@ class TestJobCard(ERPNextTestSuite):
8,
)
def test_semi_fg_pending_qty_is_left_to_another_job_card(self):
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
from erpnext.stock.doctype.item.test_item import make_item
warehouse = "Stores - _TC"
rm = make_item("Pending Qty RM 1", {"is_stock_item": 1}).name
fg = make_item("Pending Qty FG 1", {"is_stock_item": 1}).name
fg_bom = frappe.new_doc(
"BOM",
company="_Test Company",
item=fg,
quantity=1,
with_operations=1,
track_semi_finished_goods=1,
)
fg_bom.append("items", {"item_code": rm, "qty": 1, "operation_row_id": 1})
operation = {
"operation": "Pending Qty Op A",
"workstation": "_Test Workstation A",
"finished_good": fg,
"finished_good_qty": 1,
"is_final_finished_good": 1,
"sequence_id": 1,
"time_in_mins": 60,
"source_warehouse": warehouse,
"fg_warehouse": warehouse,
"skip_material_transfer": 1,
}
make_workstation(operation)
make_operation(operation)
fg_bom.append("operations", operation)
fg_bom.insert()
fg_bom.submit()
work_order = make_wo_order_test_record(
item=fg,
qty=5,
source_warehouse=warehouse,
fg_warehouse=warehouse,
bom_no=fg_bom.name,
skip_transfer=1,
do_not_save=True,
)
work_order.operations[0].time_in_mins = 60
work_order.save()
work_order.submit()
make_stock_entry(item_code=rm, target=warehouse, qty=100, basic_rate=100)
job_card = self.get_first_job_card(work_order.name)
job_card.append("time_logs", {"from_time": "2024-04-01 08:00:00"})
job_card.save()
job_card.complete_job_card(
qty=3,
for_quantity=5,
pending_qty=2,
process_loss_qty=0,
end_time="2024-04-01 09:00:00",
)
job_card.reload()
self.assertEqual(flt(job_card.for_quantity), 5)
self.assertEqual(flt(job_card.pending_qty), 2)
self.assertEqual(flt(job_card.process_loss_qty), 0)
job_card.submit()
self.assertEqual(job_card.status, "To Manufacture")
manufacturing_entry = frappe.get_doc(job_card.make_stock_entry_for_semi_fg_item())
finished_item = next(row for row in manufacturing_entry.items if row.is_finished_item)
self.assertEqual(flt(finished_item.qty), 3)
manufacturing_entry.submit()
job_card.reload()
self.assertEqual(flt(job_card.manufactured_qty), 3)
self.assertEqual(job_card.status, "Completed")
def test_semi_fg_sequence_needs_previous_operations_manufactured(self):
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
from erpnext.stock.doctype.item.test_item import make_item