From 46b4cf3addd848ebab559a3c09ebbbdd6f96ce94 Mon Sep 17 00:00:00 2001 From: Nishka Gosalia Date: Thu, 22 Jan 2026 20:34:22 +0530 Subject: [PATCH] fix: Added validation for quality inspection in job card --- .../doctype/job_card/job_card.py | 65 +++++++++++++++++++ .../doctype/job_card/test_job_card.py | 57 +++++++++++++++- 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index c9010ff54cc..fd2327c8841 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -23,6 +23,10 @@ from frappe.utils import ( time_diff_in_hours, ) +from erpnext.controllers.stock_controller import ( + QualityInspectionNotSubmittedError, + QualityInspectionRejectedError, +) from erpnext.manufacturing.doctype.bom.bom import add_additional_cost, get_bom_items_as_dict from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import ( get_mins_between_operations, @@ -732,6 +736,7 @@ class JobCard(Document): self.set_process_loss() def on_submit(self): + self.validate_inspection() self.validate_transfer_qty() self.validate_job_card() self.update_work_order() @@ -741,6 +746,66 @@ class JobCard(Document): self.update_work_order() self.set_transferred_qty() + def validate_inspection(self): + action_submit, action_reject = frappe.get_single_value( + "Stock Settings", + ["action_if_quality_inspection_is_not_submitted", "action_if_quality_inspection_is_rejected"], + ) + + item = self.finished_good or self.production_item + bom_inspection_required = frappe.db.get_value( + "BOM", self.semi_fg_bom or self.bom_no, "inspection_required" + ) + if bom_inspection_required: + if not self.quality_inspection: + frappe.throw( + _( + "Quality Inspection is required for the item {0} before completing the job card {1}" + ).format(get_link_to_form("Item", item), bold(self.name)) + ) + qa_status, docstatus = frappe.db.get_value( + "Quality Inspection", self.quality_inspection, ["status", "docstatus"] + ) + + if docstatus != 1: + if action_submit == "Stop": + frappe.throw( + _("Quality Inspection {0} is not submitted for the item: {1}").format( + get_link_to_form("Quality Inspection", self.quality_inspection), + get_link_to_form("Item", item), + ), + title=_("Inspection Submission"), + exc=QualityInspectionNotSubmittedError, + ) + else: + frappe.msgprint( + _("Quality Inspection {0} is not submitted for the item: {1}").format( + get_link_to_form("Quality Inspection", self.quality_inspection), + get_link_to_form("Item", item), + ), + alert=True, + indicator="orange", + ) + elif qa_status == "Rejected": + if action_reject == "Stop": + frappe.throw( + _("Quality Inspection {0} is rejected for the item: {1}").format( + get_link_to_form("Quality Inspection", self.quality_inspection), + get_link_to_form("Item", item), + ), + title=_("Inspection Rejected"), + exc=QualityInspectionRejectedError, + ) + else: + frappe.msgprint( + _("Quality Inspection {0} is rejected for the item: {1}").format( + get_link_to_form("Quality Inspection", self.quality_inspection), + get_link_to_form("Item", item), + ), + alert=True, + indicator="orange", + ) + def validate_transfer_qty(self): if ( not self.finished_good diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index e6eaea39578..028a05d5c9d 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -20,8 +20,9 @@ from erpnext.manufacturing.doctype.job_card.job_card import ( make_stock_entry as make_stock_entry_from_jc, ) from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record -from erpnext.manufacturing.doctype.work_order.work_order import WorkOrder +from erpnext.manufacturing.doctype.work_order.work_order import WorkOrder, make_work_order from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation +from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.tests.utils import ERPNextTestSuite @@ -73,6 +74,50 @@ class TestJobCard(ERPNextTestSuite): def tearDown(self): frappe.db.rollback() + def test_quality_inspection_mandatory_check(self): + from erpnext.manufacturing.doctype.operation.test_operation import make_operation + + raw = create_item("Fabric-Raw") + cut_fg = create_item("Cut-Fabric-SFG") + stitch_fg = create_item("Stitched-TShirt-SFG") + final = create_item("Finished-TShirt") + + row = {"operation": "Cutting", "workstation": "_Test Workstation 1"} + + cutting = make_operation(row) + stitching = make_operation({"operation": "Stitching", "workstation": "_Test Workstation 1"}) + ironing = make_operation({"operation": "Ironing", "workstation": "_Test Workstation 1"}) + + cut_bom = create_semi_fg_bom(cut_fg.name, raw.name, inspection_required=1) + stitch_bom = create_semi_fg_bom(stitch_fg.name, cut_fg.name, inspection_required=0) + final_bom = frappe.new_doc("BOM") + final_bom.item = final.name + final_bom.quantity = 1 + final_bom.with_operations = 1 + final_bom.track_semi_finished_goods = 1 + final_bom.append("items", {"item_code": raw.name, "qty": 1}) + final_bom.append( + "operations", {"operation": cutting.name, "workstation": "_Test Workstation 1", "bom_no": cut_bom} + ) + final_bom.append( + "operations", + {"operation": stitching.name, "workstation": "_Test Workstation 1", "bom_no": stitch_bom}, + ) + final_bom.append("operations", {"operation": ironing.name, "workstation": "_Test Workstation 1"}) + final_bom.insert() + final_bom.submit() + work_order = make_work_order(final_bom.name, final.name, 1, variant_items=[], use_multi_level_bom=0) + work_order.wip_warehouse = "Work In Progress - WP" + work_order.fg_warehouse = "Finished Goods - WP" + work_order.scrap_warehouse = "All Warehouses - WP" + for operation in work_order.operations: + operation.time_in_mins = 60 + + work_order.submit() + job_card = frappe.get_all("Job Card", filters={"work_order": work_order.name, "operation": "Cutting"}) + job_card_doc = frappe.get_doc("Job Card", job_card[0].name) + self.assertRaises(frappe.ValidationError, job_card_doc.submit) + def test_job_card_operations(self): job_cards = frappe.get_all( "Job Card", filters={"work_order": self.work_order.name}, fields=["operation_id", "name"] @@ -871,3 +916,13 @@ def make_wo_with_transfer_against_jc(): work_order.submit() return work_order + + +def create_semi_fg_bom(semi_fg_item, raw_item, inspection_required): + bom = frappe.new_doc("BOM") + bom.item = semi_fg_item + bom.quantity = 1 + bom.inspection_required = inspection_required + bom.append("items", {"item_code": raw_item, "qty": 1}) + bom.submit() + return bom.name