From bdbb8481b02a856dddf2b86c7628a3c9f734259e Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 10 Jul 2026 15:20:28 +0530 Subject: [PATCH] fix(stock): link job card in stock entry created from pick list A Stock Entry created from a Pick List against a job card's Material Request never set job_card, job_card_item, fg_completed_qty or the 'Material Transfer for Manufacture' purpose, so the Job Card did not recognize the transfer and blocked submission. The WIP warehouse was also not populated. Route such pick lists through a job-card-aware branch mirroring the direct Material Request -> Stock Entry mapper, and set the purpose to 'Material Transfer for Manufacture' in the work order branch so the WO -> MR -> Pick List flow updates the work order too. --- .../doctype/job_card/test_job_card.py | 42 ++++++++++++++++++ erpnext/stock/doctype/pick_list/mapper.py | 43 ++++++++++++++++++- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index efb1636e7c1..4d8998f64db 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -665,6 +665,48 @@ class TestJobCard(ERPNextTestSuite): self.assertEqual(ste.from_bom, 1.0) self.assertEqual(ste.bom_no, work_order.bom_no) + def test_job_card_material_transfer_via_pick_list(self): + from erpnext.stock.doctype.material_request.mapper import create_pick_list + from erpnext.stock.doctype.pick_list.mapper import ( + create_stock_entry as create_stock_entry_from_pick_list, + ) + + create_bom_with_multiple_operations() + work_order = make_wo_with_transfer_against_jc() + + for item in work_order.required_items: + make_stock_entry( + item_code=item.item_code, + target=item.source_warehouse, + qty=item.required_qty * 2, + basic_rate=100, + ) + + job_card_name = frappe.db.get_value("Job Card", {"work_order": work_order.name}, "name") + job_card = frappe.get_doc("Job Card", job_card_name) + + mr = make_material_request(job_card_name) + mr.schedule_date = today() + mr.submit() + + pick_list = create_pick_list(mr.name) + pick_list.submit() + + ste = frappe.get_doc(create_stock_entry_from_pick_list(pick_list.as_dict())) + self.assertEqual(ste.purpose, "Material Transfer for Manufacture") + self.assertEqual(ste.job_card, job_card_name) + self.assertEqual(ste.work_order, work_order.name) + self.assertEqual(ste.fg_completed_qty, job_card.for_quantity) + for row in ste.items: + self.assertEqual(row.t_warehouse, job_card.wip_warehouse) + self.assertTrue(row.job_card_item) + + ste.insert() + ste.submit() + + job_card.reload() + self.assertEqual(job_card.transferred_qty, job_card.for_quantity) + def test_job_card_proccess_qty_and_completed_qty(self): from erpnext.manufacturing.doctype.routing.test_routing import ( create_routing, diff --git a/erpnext/stock/doctype/pick_list/mapper.py b/erpnext/stock/doctype/pick_list/mapper.py index df2c3f4e2c2..f9281931de5 100644 --- a/erpnext/stock/doctype/pick_list/mapper.py +++ b/erpnext/stock/doctype/pick_list/mapper.py @@ -289,15 +289,22 @@ def create_stock_entry(pick_list: str | dict): stock_entry.pick_list = pick_list.get("name") stock_entry.purpose = pick_list.get("purpose") stock_entry.company = pick_list.get("company") - stock_entry.set_stock_entry_type() - if pick_list.get("work_order"): + job_card = pick_list.get("material_request") and frappe.db.get_value( + "Material Request", pick_list.get("material_request"), "job_card" + ) + + if job_card: + stock_entry = update_stock_entry_based_on_job_card(pick_list, stock_entry, job_card) + elif pick_list.get("work_order"): stock_entry = update_stock_entry_based_on_work_order(pick_list, stock_entry) elif pick_list.get("material_request"): stock_entry = update_stock_entry_based_on_material_request(pick_list, stock_entry) else: stock_entry = update_stock_entry_items_with_no_reference(pick_list, stock_entry) + stock_entry.set_stock_entry_type() + if not stock_entry.get("items"): return frappe.msgprint(_("All picked items have already been transferred against this Pick List")) @@ -344,9 +351,41 @@ def stock_entry_exists(pick_list_name): return frappe.db.exists("Stock Entry", {"pick_list": pick_list_name}) +def update_stock_entry_based_on_job_card(pick_list, stock_entry, job_card): + job_card = frappe.db.get_value( + "Job Card", + job_card, + ["name", "work_order", "bom_no", "for_quantity", "transferred_qty", "wip_warehouse"], + as_dict=True, + ) + + stock_entry.purpose = "Material Transfer for Manufacture" + stock_entry.job_card = job_card.name + stock_entry.work_order = job_card.work_order + stock_entry.from_bom = 1 + stock_entry.bom_no = job_card.bom_no + stock_entry.fg_completed_qty = max(flt(job_card.for_quantity) - flt(job_card.transferred_qty), 0) + stock_entry.to_warehouse = job_card.wip_warehouse + + for location in pick_list.locations: + if get_pending_transfer_stock_qty(location) <= 0: + continue + item = frappe._dict() + update_common_item_properties(item, location) + item.t_warehouse = job_card.wip_warehouse + if location.material_request_item: + item.job_card_item = frappe.db.get_value( + "Material Request Item", location.material_request_item, "job_card_item" + ) + stock_entry.append("items", item) + + return stock_entry + + def update_stock_entry_based_on_work_order(pick_list, stock_entry): work_order = frappe.get_doc("Work Order", pick_list.get("work_order")) + stock_entry.purpose = "Material Transfer for Manufacture" stock_entry.work_order = work_order.name stock_entry.company = work_order.company stock_entry.from_bom = 1