From 3be0c7801afd0ed16fcd1a986c842437d2355c26 Mon Sep 17 00:00:00 2001 From: Pandiyan P Date: Wed, 9 Sep 2026 18:05:03 +0530 Subject: [PATCH] fix(manufacturing): handle empty raw materials in workstation (#58927) --- .../manufacturing/doctype/job_card/mapper.py | 3 + .../doctype/workstation/test_workstation.py | 77 +++++++++++++++++++ .../doctype/workstation/workstation.py | 4 +- .../js/templates/shop_floor_template.html | 4 +- 4 files changed, 84 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/mapper.py b/erpnext/manufacturing/doctype/job_card/mapper.py index 42cf4c91e5b..1753be76bbd 100644 --- a/erpnext/manufacturing/doctype/job_card/mapper.py +++ b/erpnext/manufacturing/doctype/job_card/mapper.py @@ -102,6 +102,9 @@ def make_stock_entry(source_name: str, target_doc: str | dict | Document | None target.qty = pending_rm_qty def set_missing_values(source, target): + if not source.items: + frappe.throw(_("This Job Card has no raw materials to transfer.")) + if source.finished_good and not source.target_warehouse: frappe.throw(_("Please set the Target Warehouse in the Job Card")) diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py index 4c8e740e71e..4ca34c9ab85 100644 --- a/erpnext/manufacturing/doctype/workstation/test_workstation.py +++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py @@ -3,18 +3,95 @@ import frappe from frappe import _ +from erpnext.manufacturing.doctype.job_card.mapper import make_stock_entry from erpnext.manufacturing.doctype.operation.test_operation import make_operation from erpnext.manufacturing.doctype.routing.test_routing import create_routing, setup_bom from erpnext.manufacturing.doctype.workstation.workstation import ( NotInWorkingHoursError, WorkstationHolidayError, check_if_within_operating_hours, + get_raw_materials, update_job_card, ) from erpnext.tests.utils import ERPNextTestSuite class TestWorkstation(ERPNextTestSuite): + def test_get_raw_materials_without_items(self): + for skip_transfer, backflush_from_wip in ((0, 0), (1, 0), (1, 1)): + with self.subTest(skip_transfer=skip_transfer, backflush_from_wip=backflush_from_wip): + job_card = frappe.get_doc( + { + "doctype": "Job Card", + "company": "_Test Company", + "skip_material_transfer": skip_transfer, + "backflush_from_wip_warehouse": backflush_from_wip, + "wip_warehouse": "_Test Warehouse 1 - _TC", + } + ).insert(ignore_mandatory=True) + + for method in (get_raw_materials, make_stock_entry): + with self.subTest(method=method.__name__): + with self.assertRaisesRegex( + frappe.ValidationError, "This Job Card has no raw materials to transfer" + ): + method(job_card.name) + + job_card.reload() + self.assertFalse(job_card.items) + self.assertFalse(frappe.db.exists("Stock Entry", {"job_card": job_card.name})) + + def test_get_raw_materials_availability(self): + for skip_transfer, backflush_from_wip, transferred_qty in ( + (0, 0, 2), + (0, 0, 5), + (1, 0, 0), + (1, 1, 0), + ): + with self.subTest( + skip_transfer=skip_transfer, + backflush_from_wip=backflush_from_wip, + transferred_qty=transferred_qty, + ): + job_card = frappe.get_doc( + { + "doctype": "Job Card", + "company": "_Test Company", + "skip_material_transfer": skip_transfer, + "backflush_from_wip_warehouse": backflush_from_wip, + "wip_warehouse": "_Test Warehouse 1 - _TC", + "items": [ + { + "item_code": "_Test Item", + "source_warehouse": "_Test Warehouse - _TC", + "required_qty": 5, + "transferred_qty": transferred_qty, + }, + ], + } + ).insert(ignore_mandatory=True) + + materials = get_raw_materials(job_card.name) + + self.assertEqual(len(materials), 1) + material = materials[0] + warehouse = "_Test Warehouse 1 - _TC" if backflush_from_wip else "_Test Warehouse - _TC" + stock_qty = ( + frappe.db.get_value( + "Bin", {"item_code": "_Test Item", "warehouse": warehouse}, "actual_qty" + ) + or 0 + ) + self.assertEqual(material.item_code, "_Test Item") + self.assertEqual(material.required_qty, 5) + self.assertEqual(material.transferred_qty, transferred_qty) + self.assertEqual(material.warehouse, warehouse) + self.assertEqual(material.stock_qty, stock_qty) + self.assertEqual( + material.material_availability_status, + int(stock_qty >= 5) if skip_transfer else int(transferred_qty >= 5), + ) + def test_update_job_card_rejects_disallowed_method(self): # The whitelisted update_job_card endpoint must only run an allowlisted set of Job Card # methods. An arbitrary method name must be rejected (PermissionError) before the document diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index efd11a18d05..88e8ac179b8 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -287,8 +287,8 @@ def get_raw_materials(job_card: str): filters={"name": job_card}, ) - if not raw_materials: - return [] + if not raw_materials or not raw_materials[0].item_code: + frappe.throw(_("This Job Card has no raw materials to transfer.")) for row in raw_materials: warehouse = row.source_warehouse diff --git a/erpnext/public/js/templates/shop_floor_template.html b/erpnext/public/js/templates/shop_floor_template.html index 0adc8432629..09360ee873a 100644 --- a/erpnext/public/js/templates/shop_floor_template.html +++ b/erpnext/public/js/templates/shop_floor_template.html @@ -727,7 +727,7 @@ - {% } else { %} + {% } else if (slot.materials && slot.materials.length) { %} @@ -990,7 +990,7 @@ - {% } else { %} + {% } else if (jc.materials && jc.materials.length) { %}