From 8db1eb0d27702e85db1a1cd4ed791dcc577adff8 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 8 Jun 2026 15:41:30 +0530 Subject: [PATCH] fix: allow specific methods to run --- .../doctype/job_card/job_card.js | 4 +++ .../doctype/job_card/job_card.py | 18 ++++++++++--- .../doctype/workstation/test_workstation.py | 12 +++++++++ .../doctype/workstation/workstation.py | 26 ++++++++++++++++++- 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 51441e87430..5457e0a93dc 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -577,6 +577,10 @@ frappe.ui.form.on("Job Card", { const wrapper = $(frm.fields_dict["job_card_dashboard"].wrapper); wrapper.empty(); + if (frm.doc.docstatus !== 0) { + return; + } + const { doc } = frm; const { time_logs, status } = doc; diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 802b68d3ba7..8639a6769cd 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -905,9 +905,6 @@ class JobCard(Document): ) def validate_job_card(self): - if self.track_semi_finished_goods: - return - if self.work_order and frappe.get_cached_value("Work Order", self.work_order, "status") == "Stopped": frappe.throw( _("Transaction not allowed against stopped Work Order {0}").format( @@ -1328,6 +1325,8 @@ class JobCard(Document): def pause_job(self, **kwargs): frappe.has_permission("Job Card", "write", doc=self, throw=True) + self.validate_docstatus() + if isinstance(kwargs, dict): kwargs = frappe._dict(kwargs) @@ -1338,6 +1337,8 @@ class JobCard(Document): def resume_job(self, **kwargs): frappe.has_permission("Job Card", "write", doc=self, throw=True) + self.validate_docstatus() + if isinstance(kwargs, dict): kwargs = frappe._dict(kwargs) @@ -1515,6 +1516,8 @@ class JobCard(Document): def start_timer(self, **kwargs): frappe.has_permission("Job Card", "write", doc=self, throw=True) + self.validate_docstatus() + if isinstance(kwargs, dict): kwargs = frappe._dict(kwargs) @@ -1528,6 +1531,8 @@ class JobCard(Document): def complete_job_card(self, **kwargs): frappe.has_permission("Job Card", "write", doc=self, throw=True) + self.validate_docstatus() + if isinstance(kwargs, dict): kwargs = frappe._dict(kwargs) @@ -1541,6 +1546,13 @@ class JobCard(Document): if kwargs.auto_submit: self.auto_submit_job_card(kwargs.auto_submit) + def validate_docstatus(self): + if self.docstatus == 2: + frappe.throw(_("Cancelled Job Card cannot be processed.")) + + if self.docstatus == 1: + frappe.throw(_("Submitted Job Card cannot be processed.")) + def validate_complete_job_card_qty(self, kwargs): if flt(kwargs.pending_qty) and flt(kwargs.pending_qty) < 0: frappe.throw(_("Pending quantity cannot be negative.")) diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py index 424e467e39f..21dcc771213 100644 --- a/erpnext/manufacturing/doctype/workstation/test_workstation.py +++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py @@ -9,11 +9,23 @@ from erpnext.manufacturing.doctype.workstation.workstation import ( NotInWorkingHoursError, WorkstationHolidayError, check_if_within_operating_hours, + update_job_card, ) from erpnext.tests.utils import ERPNextTestSuite class TestWorkstation(ERPNextTestSuite): + 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 + # is even loaded, so this needs no Job Card to exist. + self.assertRaises( + frappe.PermissionError, + update_job_card, + "NON-EXISTENT-JOB-CARD", + "delete", + ) + def test_validate_timings(self): check_if_within_operating_hours( "_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00" diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index 60c50631847..a6c320b7606 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -517,8 +517,33 @@ def get_color_map(): } +ALLOWED_JOB_CARD_METHODS = frozenset( + { + "start_timer", + "pause_job", + "resume_job", + "complete_job_card", + } +) + + @frappe.whitelist() def update_job_card(job_card: str, method: str, **kwargs): + if method not in ALLOWED_JOB_CARD_METHODS: + frappe.throw( + _("Method {0} is not allowed to be run on a Job Card.").format(bold(method)), + frappe.PermissionError, + title=_("Not Allowed"), + ) + + frappe.has_permission("Job Card", "read", throw=True) + + doc = frappe.get_doc("Job Card", job_card) + + # These methods mutate the Job Card, but frappe.get_doc does not enforce permissions — + # require write access before running anything. + frappe.has_permission("Job Card", "write", doc=doc, throw=True) + if isinstance(kwargs, dict): kwargs = frappe._dict(kwargs) @@ -528,7 +553,6 @@ def update_job_card(job_card: str, method: str, **kwargs): if kwargs.qty and isinstance(kwargs.qty, str): kwargs.qty = flt(kwargs.qty) - doc = frappe.get_doc("Job Card", job_card) doc.run_method(method, **kwargs)