From c9a5b0026e46a44355a961cfd3d7a467bf05fc55 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 (cherry picked from commit 8db1eb0d27702e85db1a1cd4ed791dcc577adff8) # Conflicts: # erpnext/manufacturing/doctype/job_card/job_card.py --- .../doctype/job_card/job_card.js | 4 ++ .../doctype/job_card/job_card.py | 52 +++++++++++++++++-- .../doctype/workstation/test_workstation.py | 12 +++++ .../doctype/workstation/workstation.py | 26 +++++++++- 4 files changed, 90 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 795136d2374..f550d6fb20a 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 5ba0bed7362..538ad939ae6 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -857,9 +857,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( @@ -1259,6 +1256,13 @@ class JobCard(Document): @frappe.whitelist() def pause_job(self, **kwargs): +<<<<<<< HEAD +======= + frappe.has_permission("Job Card", "write", doc=self, throw=True) + + self.validate_docstatus() + +>>>>>>> 8db1eb0d27 (fix: allow specific methods to run) if isinstance(kwargs, dict): kwargs = frappe._dict(kwargs) @@ -1267,6 +1271,13 @@ class JobCard(Document): @frappe.whitelist() def resume_job(self, **kwargs): +<<<<<<< HEAD +======= + frappe.has_permission("Job Card", "write", doc=self, throw=True) + + self.validate_docstatus() + +>>>>>>> 8db1eb0d27 (fix: allow specific methods to run) if isinstance(kwargs, dict): kwargs = frappe._dict(kwargs) @@ -1439,6 +1450,13 @@ class JobCard(Document): @frappe.whitelist() def start_timer(self, **kwargs): +<<<<<<< HEAD +======= + frappe.has_permission("Job Card", "write", doc=self, throw=True) + + self.validate_docstatus() + +>>>>>>> 8db1eb0d27 (fix: allow specific methods to run) if isinstance(kwargs, dict): kwargs = frappe._dict(kwargs) @@ -1450,9 +1468,37 @@ class JobCard(Document): @frappe.whitelist() def complete_job_card(self, **kwargs): +<<<<<<< HEAD if isinstance(kwargs, dict): kwargs = frappe._dict(kwargs) +======= + frappe.has_permission("Job Card", "write", doc=self, throw=True) + + self.validate_docstatus() + + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + self.validate_complete_job_card_qty(kwargs) + + self.pending_qty = flt(kwargs.pending_qty) + self.process_loss_qty = flt(kwargs.process_loss_qty) + + self.add_completion_time_logs(kwargs) + + 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): +>>>>>>> 8db1eb0d27 (fix: allow specific methods to run) 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 e9ee6fb49f0..eee5b5c0638 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -516,8 +516,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) @@ -527,7 +552,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)