fix(stock): link job card in stock entry created from pick list (backport #57031)

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.
This commit is contained in:
Mihir Kandoi
2026-07-10 17:46:57 +05:30
parent 67c85ef0af
commit 755b9ccbc3
2 changed files with 99 additions and 2 deletions

View File

@@ -662,6 +662,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.material_request import create_pick_list
from erpnext.stock.doctype.pick_list.pick_list 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,

View File

@@ -1585,15 +1585,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"))
@@ -1684,9 +1691,57 @@ 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", "semi_fg_bom", "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.semi_fg_bom or 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
job_card_items = get_job_card_items_by_material_request_item(pick_list)
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
item.job_card_item = job_card_items.get(location.material_request_item)
stock_entry.append("items", item)
return stock_entry
def get_job_card_items_by_material_request_item(pick_list):
material_request_items = [
location.material_request_item for location in pick_list.locations if location.material_request_item
]
if not material_request_items:
return {}
return dict(
frappe.get_all(
"Material Request Item",
filters={"name": ["in", material_request_items]},
fields=["name", "job_card_item"],
as_list=True,
)
)
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