From 808b2e298432dfec7558da3262eff292b12b9027 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 13:08:33 +0530 Subject: [PATCH 1/3] fix: require material transfer before job card start and completion When the work order transfers material against Job Card, the Start Job and Complete Job actions (and the whitelisted start_timer and complete_job_card methods behind them) accepted work before any Material Transfer for Manufacture existed; the transfer gate only fired on job card submission. Run validate_transfer_qty on both actions, and drop the finished_good escape in materials_ready so the dashboard hides the buttons while transfer is pending. Job cards that skip material transfer, corrective job cards, and work orders transferring against Work Order are exempt, as on submit. --- erpnext/manufacturing/doctype/job_card/job_card.js | 3 +-- erpnext/manufacturing/doctype/job_card/job_card.py | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index dedc7c20966..8948cf6eeb3 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -597,8 +597,7 @@ frappe.ui.form.on("Job Card", { const has_remaining_qty = doc.for_quantity + doc.process_loss_qty > doc.total_completed_qty; const pending_transfer = has_items && doc.items.some((row) => flt(row.transferred_qty) < flt(row.required_qty)); - const materials_ready = - doc.skip_material_transfer || !pending_transfer || !doc.finished_good || !has_items; + const materials_ready = doc.skip_material_transfer || !pending_transfer; let last_row = {}; const has_sub_ops_or_pending_qty = doc.sub_operations?.length || doc.pending_qty > 0; diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 1271f1b6117..bc52de3f836 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -1692,6 +1692,7 @@ class JobCard(Document): frappe.has_permission("Job Card", "write", doc=self, throw=True) self.validate_docstatus() + self.validate_transfer_qty() if isinstance(kwargs, dict): kwargs = frappe._dict(kwargs) @@ -1707,6 +1708,7 @@ class JobCard(Document): frappe.has_permission("Job Card", "write", doc=self, throw=True) self.validate_docstatus() + self.validate_transfer_qty() if isinstance(kwargs, dict): kwargs = frappe._dict(kwargs) From c95705dc64489069d4b7042700f5a7b0b4620168 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 13:14:38 +0530 Subject: [PATCH 2/3] test: job card start and completion blocked until material transfer --- .../doctype/job_card/test_job_card.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index ec95b935450..0db5e537131 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -383,6 +383,31 @@ class TestJobCard(ERPNextTestSuite): # JC is Completed with excess transfer self.assertEqual(job_card.status, "Completed") + def test_job_card_actions_blocked_until_material_transfer(self): + "Start and Complete must wait for the transfer when RMs move against Job Card." + self.transfer_material_against = "Job Card" + self.source_warehouse = "Stores - _TC" + + self.generate_required_stock(self.work_order) + job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name}) + + self.assertRaises(frappe.ValidationError, job_card.start_timer, start_time=now()) + self.assertRaises(frappe.ValidationError, job_card.complete_job_card, qty=2, for_quantity=2) + + transfer_entry = make_stock_entry_from_jc(job_card.name) + transfer_entry.insert() + transfer_entry.submit() + + job_card.reload() + job_card.append("time_logs", {"from_time": "2024-03-01 08:00:00"}) + job_card.save() + job_card.complete_job_card( + qty=2, for_quantity=2, pending_qty=0, process_loss_qty=0, end_time="2024-03-01 09:00:00" + ) + + job_card.reload() + self.assertEqual(flt(job_card.total_completed_qty), 2) + @ERPNextTestSuite.change_settings("Manufacturing Settings", {"job_card_excess_transfer": 0}) def test_job_card_excess_material_transfer_block(self): self.transfer_material_against = "Job Card" From e9533495fcf52257e620d6fd0a5f3e30c3b7f7a3 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 13:18:55 +0530 Subject: [PATCH 3/3] fix: keep job card actions visible for corrective job cards Corrective job cards regenerate required items but are exempt from the transfer gate on the server; mirror that exemption in materials_ready. --- erpnext/manufacturing/doctype/job_card/job_card.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 8948cf6eeb3..130eda01d6b 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -597,7 +597,7 @@ frappe.ui.form.on("Job Card", { const has_remaining_qty = doc.for_quantity + doc.process_loss_qty > doc.total_completed_qty; const pending_transfer = has_items && doc.items.some((row) => flt(row.transferred_qty) < flt(row.required_qty)); - const materials_ready = doc.skip_material_transfer || !pending_transfer; + const materials_ready = doc.skip_material_transfer || doc.is_corrective_job_card || !pending_transfer; let last_row = {}; const has_sub_ops_or_pending_qty = doc.sub_operations?.length || doc.pending_qty > 0;