From bb8d0fce23e09b389dfdc9f37e845fd49edfc0fc Mon Sep 17 00:00:00 2001
From: Morita Shinnosuke
Date: Fri, 24 Jul 2026 02:34:05 +0000
Subject: [PATCH 1/2] [6788][ADD] maintenance_request_done_readonly
---
maintenance_request_done_readonly/README.rst | 90 ++++
maintenance_request_done_readonly/__init__.py | 1 +
.../__manifest__.py | 16 +
.../models/__init__.py | 2 +
.../models/maintenance_request.py | 65 +++
.../models/res_config_settings.py | 38 ++
.../pyproject.toml | 3 +
.../readme/CONTRIBUTORS.md | 2 +
.../readme/DESCRIPTION.md | 13 +
.../static/description/index.html | 435 ++++++++++++++++++
.../tests/__init__.py | 1 +
.../test_maintenance_request_done_readonly.py | 112 +++++
.../views/res_config_settings_views.xml | 26 ++
13 files changed, 804 insertions(+)
create mode 100644 maintenance_request_done_readonly/README.rst
create mode 100644 maintenance_request_done_readonly/__init__.py
create mode 100644 maintenance_request_done_readonly/__manifest__.py
create mode 100644 maintenance_request_done_readonly/models/__init__.py
create mode 100644 maintenance_request_done_readonly/models/maintenance_request.py
create mode 100644 maintenance_request_done_readonly/models/res_config_settings.py
create mode 100644 maintenance_request_done_readonly/pyproject.toml
create mode 100644 maintenance_request_done_readonly/readme/CONTRIBUTORS.md
create mode 100644 maintenance_request_done_readonly/readme/DESCRIPTION.md
create mode 100644 maintenance_request_done_readonly/static/description/index.html
create mode 100644 maintenance_request_done_readonly/tests/__init__.py
create mode 100644 maintenance_request_done_readonly/tests/test_maintenance_request_done_readonly.py
create mode 100644 maintenance_request_done_readonly/views/res_config_settings_views.xml
diff --git a/maintenance_request_done_readonly/README.rst b/maintenance_request_done_readonly/README.rst
new file mode 100644
index 0000000..6fe2503
--- /dev/null
+++ b/maintenance_request_done_readonly/README.rst
@@ -0,0 +1,90 @@
+==================================
+Maintenance Request Done Read-only
+==================================
+
+..
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! source digest: sha256:878934f590ae604ed42cdc4a74c23b91b357b91fe17730356a8e08983b8fa828
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
+ :target: https://odoo-community.org/page/development-status
+ :alt: Beta
+.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
+ :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
+ :alt: License: AGPL-3
+.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fmaintenance-lightgray.png?logo=github
+ :target: https://github.com/OCA/maintenance/tree/18.0/maintenance_request_done_readonly
+ :alt: OCA/maintenance
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+ :target: https://translation.odoo-community.org/projects/maintenance-18-0/maintenance-18-0-maintenance_request_done_readonly
+ :alt: Translate me on Weblate
+.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/maintenance&target_branch=18.0
+ :alt: Try me on Runboat
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+This module makes completed maintenance requests read-only.
+
+Once a request reaches a done stage, it can no longer be edited or
+reopened: any attempt to change its fields (including moving it back out
+of the done stage) is blocked. Only members of the *Equipment Manager*
+group keep full access to completed requests.
+
+The restriction is enforced on write, so every field is locked by
+default, without having to enumerate them. Completing a request and its
+follow-up (commenting, following, scheduling activities) keep working.
+
+If some fields should stay editable after completion, an administrator
+can select them in *Settings > Maintenance > Completed Requests*.
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Bug Tracker
+===========
+
+Bugs are tracked on `GitHub Issues `_.
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us to smash it by providing a detailed and welcomed
+`feedback `_.
+
+Do not contact contributors directly about support or help with technical issues.
+
+Credits
+=======
+
+Authors
+-------
+
+* Quartile
+
+Contributors
+------------
+
+- `Quartile `__:
+
+ - Shinnosuke Morita
+
+Maintainers
+-----------
+
+This module is maintained by the OCA.
+
+.. image:: https://odoo-community.org/logo.png
+ :alt: Odoo Community Association
+ :target: https://odoo-community.org
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
+This module is part of the `OCA/maintenance `_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/maintenance_request_done_readonly/__init__.py b/maintenance_request_done_readonly/__init__.py
new file mode 100644
index 0000000..0650744
--- /dev/null
+++ b/maintenance_request_done_readonly/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/maintenance_request_done_readonly/__manifest__.py b/maintenance_request_done_readonly/__manifest__.py
new file mode 100644
index 0000000..9052737
--- /dev/null
+++ b/maintenance_request_done_readonly/__manifest__.py
@@ -0,0 +1,16 @@
+# Copyright 2026 Quartile (https://www.quartile.co)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+{
+ "name": "Maintenance Request Done Read-only",
+ "summary": "Make completed maintenance requests read-only except for managers",
+ "version": "18.0.1.0.0",
+ "license": "AGPL-3",
+ "author": "Quartile, Odoo Community Association (OCA)",
+ "website": "https://github.com/OCA/maintenance",
+ "depends": ["maintenance"],
+ "data": [
+ "views/res_config_settings_views.xml",
+ ],
+ "installable": True,
+}
diff --git a/maintenance_request_done_readonly/models/__init__.py b/maintenance_request_done_readonly/models/__init__.py
new file mode 100644
index 0000000..0dcc3e8
--- /dev/null
+++ b/maintenance_request_done_readonly/models/__init__.py
@@ -0,0 +1,2 @@
+from . import maintenance_request
+from . import res_config_settings
diff --git a/maintenance_request_done_readonly/models/maintenance_request.py b/maintenance_request_done_readonly/models/maintenance_request.py
new file mode 100644
index 0000000..93e0321
--- /dev/null
+++ b/maintenance_request_done_readonly/models/maintenance_request.py
@@ -0,0 +1,65 @@
+# Copyright 2026 Quartile (https://www.quartile.co)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import _, models
+from odoo.exceptions import UserError
+
+
+class MaintenanceRequest(models.Model):
+ _inherit = "maintenance.request"
+
+ def _writable_fields_when_done(self):
+ """Fields a non-manager may still write on a completed (done) request.
+
+ Everything else is locked by default (allow-list approach), so business
+ fields - including those added by other modules - are protected without
+ listing them. The set is made of:
+
+ * mail/activity bookkeeping, so that commenting, following and
+ scheduling activities keep working on a completed request;
+ * the fields an administrator selected in the maintenance settings
+ (stored in the ``maintenance_request_done_readonly.editable_fields``
+ system parameter).
+
+ Completion side effects (e.g. ``close_date``) are not listed here: they
+ are handled through the ``mnt_done_bypass_lock`` context set below, so
+ they stay protected against direct edits unless explicitly selected.
+ """
+ allowed = {
+ "message_main_attachment_id",
+ "message_follower_ids",
+ "message_ids",
+ "activity_ids",
+ }
+ names = (
+ self.env["ir.config_parameter"]
+ .sudo()
+ .get_param("maintenance_request_done_readonly.editable_fields", "")
+ )
+ return allowed | {name for name in names.split(",") if name}
+
+ def write(self, vals):
+ # A completed (done) request can only be edited by a maintenance
+ # manager. Checked against the pre-write state, so completing a request
+ # (moving it to a done stage) is still allowed for everyone. The bypass
+ # context lets the internal write cascade triggered by completion (e.g.
+ # close_date) through, while direct edits of those fields stay blocked.
+ if (
+ not self.env.user.has_group("maintenance.group_equipment_manager")
+ and (set(vals) - self._writable_fields_when_done())
+ and not self.env.context.get("mnt_done_bypass_lock")
+ ):
+ locked = self.filtered("done")
+ if locked:
+ raise UserError(
+ _(
+ "'%s' is completed and can only be edited by a "
+ "maintenance manager.",
+ locked[0].display_name,
+ )
+ )
+ records = self
+ stage_id = vals.get("stage_id")
+ if stage_id and self.env["maintenance.stage"].browse(stage_id).done:
+ records = self.with_context(mnt_done_bypass_lock=True)
+ return super(MaintenanceRequest, records).write(vals)
diff --git a/maintenance_request_done_readonly/models/res_config_settings.py b/maintenance_request_done_readonly/models/res_config_settings.py
new file mode 100644
index 0000000..249d36b
--- /dev/null
+++ b/maintenance_request_done_readonly/models/res_config_settings.py
@@ -0,0 +1,38 @@
+# Copyright 2026 Quartile (https://www.quartile.co)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import fields, models
+
+
+class ResConfigSettings(models.TransientModel):
+ _inherit = "res.config.settings"
+
+ maintenance_done_editable_field_ids = fields.Many2many(
+ "ir.model.fields",
+ string="Editable fields on completed requests",
+ domain=[("model", "=", "maintenance.request"), ("store", "=", True)],
+ help="Fields that non-managers may still edit after a maintenance "
+ "request is completed. Other fields stay locked.",
+ )
+
+ def get_values(self):
+ res = super().get_values()
+ names = (
+ self.env["ir.config_parameter"]
+ .sudo()
+ .get_param("maintenance_request_done_readonly.editable_fields", "")
+ )
+ name_list = [name for name in names.split(",") if name]
+ field_recs = self.env["ir.model.fields"].search(
+ [("model", "=", "maintenance.request"), ("name", "in", name_list)]
+ )
+ res["maintenance_done_editable_field_ids"] = [(6, 0, field_recs.ids)]
+ return res
+
+ def set_values(self):
+ res = super().set_values()
+ names = ",".join(self.maintenance_done_editable_field_ids.mapped("name"))
+ self.env["ir.config_parameter"].sudo().set_param(
+ "maintenance_request_done_readonly.editable_fields", names
+ )
+ return res
diff --git a/maintenance_request_done_readonly/pyproject.toml b/maintenance_request_done_readonly/pyproject.toml
new file mode 100644
index 0000000..4231d0c
--- /dev/null
+++ b/maintenance_request_done_readonly/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["whool"]
+build-backend = "whool.buildapi"
diff --git a/maintenance_request_done_readonly/readme/CONTRIBUTORS.md b/maintenance_request_done_readonly/readme/CONTRIBUTORS.md
new file mode 100644
index 0000000..260160c
--- /dev/null
+++ b/maintenance_request_done_readonly/readme/CONTRIBUTORS.md
@@ -0,0 +1,2 @@
+- [Quartile](https://www.quartile.co):
+ - Shinnosuke Morita
diff --git a/maintenance_request_done_readonly/readme/DESCRIPTION.md b/maintenance_request_done_readonly/readme/DESCRIPTION.md
new file mode 100644
index 0000000..b372001
--- /dev/null
+++ b/maintenance_request_done_readonly/readme/DESCRIPTION.md
@@ -0,0 +1,13 @@
+This module makes completed maintenance requests read-only.
+
+Once a request reaches a done stage, it can no longer be edited or reopened:
+any attempt to change its fields (including moving it back out of the done
+stage) is blocked. Only members of the *Equipment Manager* group keep full
+access to completed requests.
+
+The restriction is enforced on write, so every field is locked by default,
+without having to enumerate them. Completing a request and its follow-up
+(commenting, following, scheduling activities) keep working.
+
+If some fields should stay editable after completion, an administrator can
+select them in *Settings > Maintenance > Completed Requests*.
diff --git a/maintenance_request_done_readonly/static/description/index.html b/maintenance_request_done_readonly/static/description/index.html
new file mode 100644
index 0000000..ad9a1fa
--- /dev/null
+++ b/maintenance_request_done_readonly/static/description/index.html
@@ -0,0 +1,435 @@
+
+
+
+
+
+Maintenance Request Done Read-only
+
+
+
+
+
Maintenance Request Done Read-only
+
+
+

+
This module makes completed maintenance requests read-only.
+
Once a request reaches a done stage, it can no longer be edited or
+reopened: any attempt to change its fields (including moving it back out
+of the done stage) is blocked. Only members of the Equipment Manager
+group keep full access to completed requests.
+
The restriction is enforced on write, so every field is locked by
+default, without having to enumerate them. Completing a request and its
+follow-up (commenting, following, scheduling activities) keep working.
+
If some fields should stay editable after completion, an administrator
+can select them in Settings > Maintenance > Completed Requests.
+
Table of contents
+
+
+
+
Bugs are tracked on GitHub Issues.
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us to smash it by providing a detailed and welcomed
+feedback.
+
Do not contact contributors directly about support or help with technical issues.
+
+
+
+
+
+
+
+
This module is maintained by the OCA.
+
+
+
+
OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
This module is part of the OCA/maintenance project on GitHub.
+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
+
+
+
+
+
diff --git a/maintenance_request_done_readonly/tests/__init__.py b/maintenance_request_done_readonly/tests/__init__.py
new file mode 100644
index 0000000..43931db
--- /dev/null
+++ b/maintenance_request_done_readonly/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_maintenance_request_done_readonly
diff --git a/maintenance_request_done_readonly/tests/test_maintenance_request_done_readonly.py b/maintenance_request_done_readonly/tests/test_maintenance_request_done_readonly.py
new file mode 100644
index 0000000..eff6430
--- /dev/null
+++ b/maintenance_request_done_readonly/tests/test_maintenance_request_done_readonly.py
@@ -0,0 +1,112 @@
+# Copyright 2026 Quartile (https://www.quartile.co)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo.exceptions import UserError
+from odoo.tests.common import TransactionCase, new_test_user
+
+
+class TestMaintenanceRequestDoneReadonly(TransactionCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.done_stage = cls.env["maintenance.stage"].search(
+ [("done", "=", True)], limit=1
+ )
+ cls.open_stage = cls.env["maintenance.stage"].search(
+ [("done", "=", False)], order="sequence", limit=1
+ )
+ cls.manager = new_test_user(
+ cls.env,
+ login="mrl_manager",
+ groups="maintenance.group_equipment_manager",
+ )
+ cls.user = new_test_user(
+ cls.env,
+ login="mrl_user",
+ groups="base.group_user",
+ )
+ cls.equipment = cls.env["maintenance.equipment"].create(
+ {"name": "Test Equipment"}
+ )
+
+ def _new_request(self, user):
+ # A regular user (base.group_user) may only access maintenance requests
+ # that satisfy maintenance's record rule "equipment_request_rule_user":
+ # they must be the owner, a follower, or the assigned technician. We set
+ # ``user_id`` (technician) rather than ``owner_user_id`` on purpose: when
+ # hr_maintenance is installed (as in CI, where the whole repo is
+ # installed together), it redefines ``owner_user_id`` as a read-only
+ # computed field, so a value passed here is ignored and the user would
+ # fail the rule on their own create. ``user_id`` stays a plain writable
+ # field and keeps the rule satisfied in both cases.
+ return (
+ self.env["maintenance.request"]
+ .with_user(user)
+ .create(
+ {
+ "name": "Test Request",
+ "equipment_id": self.equipment.id,
+ "user_id": user.id,
+ "stage_id": self.open_stage.id,
+ }
+ )
+ )
+
+ def test_regular_user_can_complete(self):
+ """Completing a request (moving it to a done stage) is allowed for a
+ regular user and must not be blocked by the lock guard."""
+ request = self._new_request(self.user)
+ request.with_user(self.user).write({"stage_id": self.done_stage.id})
+ self.assertTrue(request.done)
+
+ def test_regular_user_cannot_edit_completed(self):
+ request = self._new_request(self.user)
+ request.with_user(self.user).write({"stage_id": self.done_stage.id})
+ with self.assertRaises(UserError):
+ request.with_user(self.user).write({"name": "Changed"})
+
+ def test_regular_user_cannot_reopen_completed(self):
+ request = self._new_request(self.user)
+ request.with_user(self.user).write({"stage_id": self.done_stage.id})
+ with self.assertRaises(UserError):
+ request.with_user(self.user).write({"stage_id": self.open_stage.id})
+
+ def test_regular_user_can_edit_open(self):
+ request = self._new_request(self.user)
+ request.with_user(self.user).write({"name": "Still editable"})
+ self.assertEqual(request.name, "Still editable")
+
+ def test_manager_can_edit_completed(self):
+ request = self._new_request(self.user)
+ request.with_user(self.user).write({"stage_id": self.done_stage.id})
+ request.with_user(self.manager).write({"name": "Corrected by manager"})
+ self.assertEqual(request.name, "Corrected by manager")
+
+ def test_regular_user_cannot_edit_close_date_when_done(self):
+ """close_date is written internally on completion (bypass), but a
+ direct edit by a regular user on a completed request must be blocked."""
+ request = self._new_request(self.user)
+ request.with_user(self.user).write({"stage_id": self.done_stage.id})
+ with self.assertRaises(UserError):
+ request.with_user(self.user).write({"close_date": "2026-01-01"})
+
+ def test_configured_field_writable_when_done(self):
+ """A field selected in the settings becomes writable again on a
+ completed request, even for a regular user."""
+ self.env["ir.config_parameter"].sudo().set_param(
+ "maintenance_request_done_readonly.editable_fields", "close_date"
+ )
+ request = self._new_request(self.user)
+ request.with_user(self.user).write({"stage_id": self.done_stage.id})
+ request.with_user(self.user).write({"close_date": "2026-01-01"})
+ self.assertEqual(str(request.close_date), "2026-01-01")
+
+ def test_allowed_field_writable_when_completed(self):
+ """Allow-listed technical fields stay writable on a completed request
+ even for a regular user (e.g. chatter posting)."""
+ request = self._new_request(self.user)
+ request.with_user(self.user).write({"stage_id": self.done_stage.id})
+ # message_post writes message_main_attachment_id / message_ids, which
+ # are allow-listed, so it must not be blocked by the lock guard.
+ request.with_user(self.user).message_post(body="Still can comment")
+ self.assertTrue(request.message_ids)
diff --git a/maintenance_request_done_readonly/views/res_config_settings_views.xml b/maintenance_request_done_readonly/views/res_config_settings_views.xml
new file mode 100644
index 0000000..14d92fb
--- /dev/null
+++ b/maintenance_request_done_readonly/views/res_config_settings_views.xml
@@ -0,0 +1,26 @@
+
+
+
+
+ res.config.settings.view.form.inherit.maintenance.done.readonly
+ res.config.settings
+
+
+
+
+
+
+
+
+
+
+
+
From bc20921b3f2ccd59cb3aa65a224f6693fa3f7cd2 Mon Sep 17 00:00:00 2001
From: Morita Shinnosuke
Date: Wed, 5 Aug 2026 07:50:00 +0000
Subject: [PATCH 2/2] fixup!
---
maintenance_request_done_readonly/README.rst | 13 ++--
.../__manifest__.py | 5 +-
.../data/ir_config_parameter.xml | 7 ++
.../models/__init__.py | 1 -
.../models/maintenance_request.py | 67 ++++++++-----------
.../models/res_config_settings.py | 38 -----------
.../readme/DESCRIPTION.md | 12 ++--
.../security/res_groups.xml | 18 +++++
.../static/description/index.html | 13 ++--
.../test_maintenance_request_done_readonly.py | 22 ++++++
.../views/res_config_settings_views.xml | 26 -------
11 files changed, 104 insertions(+), 118 deletions(-)
create mode 100644 maintenance_request_done_readonly/data/ir_config_parameter.xml
delete mode 100644 maintenance_request_done_readonly/models/res_config_settings.py
create mode 100644 maintenance_request_done_readonly/security/res_groups.xml
delete mode 100644 maintenance_request_done_readonly/views/res_config_settings_views.xml
diff --git a/maintenance_request_done_readonly/README.rst b/maintenance_request_done_readonly/README.rst
index 6fe2503..37fc979 100644
--- a/maintenance_request_done_readonly/README.rst
+++ b/maintenance_request_done_readonly/README.rst
@@ -32,15 +32,20 @@ This module makes completed maintenance requests read-only.
Once a request reaches a done stage, it can no longer be edited or
reopened: any attempt to change its fields (including moving it back out
-of the done stage) is blocked. Only members of the *Equipment Manager*
-group keep full access to completed requests.
+of the done stage) is blocked. Only members of the *Maintenance: Edit
+Completed Requests* group keep full access to completed requests.
+*Equipment Manager* users get that group by default, so it can also be
+granted to users who should be able to correct completed requests
+without being maintenance managers.
The restriction is enforced on write, so every field is locked by
default, without having to enumerate them. Completing a request and its
follow-up (commenting, following, scheduling activities) keep working.
-If some fields should stay editable after completion, an administrator
-can select them in *Settings > Maintenance > Completed Requests*.
+If some fields should stay editable after completion, edit the
+``maintenance_request_done_readonly.editable_fields`` system parameter
+(*Settings > Technical > Parameters > System Parameters*, created on
+install) and set a comma-separated list of their technical names.
**Table of contents**
diff --git a/maintenance_request_done_readonly/__manifest__.py b/maintenance_request_done_readonly/__manifest__.py
index 9052737..f5dee07 100644
--- a/maintenance_request_done_readonly/__manifest__.py
+++ b/maintenance_request_done_readonly/__manifest__.py
@@ -3,14 +3,15 @@
{
"name": "Maintenance Request Done Read-only",
- "summary": "Make completed maintenance requests read-only except for managers",
+ "summary": "Restrict editing of completed maintenance requests to an allowed group",
"version": "18.0.1.0.0",
"license": "AGPL-3",
"author": "Quartile, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/maintenance",
"depends": ["maintenance"],
"data": [
- "views/res_config_settings_views.xml",
+ "security/res_groups.xml",
+ "data/ir_config_parameter.xml",
],
"installable": True,
}
diff --git a/maintenance_request_done_readonly/data/ir_config_parameter.xml b/maintenance_request_done_readonly/data/ir_config_parameter.xml
new file mode 100644
index 0000000..24357f7
--- /dev/null
+++ b/maintenance_request_done_readonly/data/ir_config_parameter.xml
@@ -0,0 +1,7 @@
+
+
+
+ maintenance_request_done_readonly.editable_fields
+
+
+
diff --git a/maintenance_request_done_readonly/models/__init__.py b/maintenance_request_done_readonly/models/__init__.py
index 0dcc3e8..df8c9b5 100644
--- a/maintenance_request_done_readonly/models/__init__.py
+++ b/maintenance_request_done_readonly/models/__init__.py
@@ -1,2 +1 @@
from . import maintenance_request
-from . import res_config_settings
diff --git a/maintenance_request_done_readonly/models/maintenance_request.py b/maintenance_request_done_readonly/models/maintenance_request.py
index 93e0321..34ed098 100644
--- a/maintenance_request_done_readonly/models/maintenance_request.py
+++ b/maintenance_request_done_readonly/models/maintenance_request.py
@@ -9,57 +9,46 @@ class MaintenanceRequest(models.Model):
_inherit = "maintenance.request"
def _writable_fields_when_done(self):
- """Fields a non-manager may still write on a completed (done) request.
+ """Fields a user without the edit group may still write when completed.
- Everything else is locked by default (allow-list approach), so business
- fields - including those added by other modules - are protected without
- listing them. The set is made of:
-
- * mail/activity bookkeeping, so that commenting, following and
- scheduling activities keep working on a completed request;
- * the fields an administrator selected in the maintenance settings
- (stored in the ``maintenance_request_done_readonly.editable_fields``
- system parameter).
-
- Completion side effects (e.g. ``close_date``) are not listed here: they
- are handled through the ``mnt_done_bypass_lock`` context set below, so
- they stay protected against direct edits unless explicitly selected.
+ ``message_main_attachment_id`` is written by the chatter when a file is
+ attached, so it is always allowed to keep commenting with attachments
+ working. Extra fields come from the
+ ``maintenance_request_done_readonly.editable_fields`` system parameter;
+ every other field is locked.
"""
- allowed = {
- "message_main_attachment_id",
- "message_follower_ids",
- "message_ids",
- "activity_ids",
- }
names = (
self.env["ir.config_parameter"]
.sudo()
.get_param("maintenance_request_done_readonly.editable_fields", "")
)
- return allowed | {name for name in names.split(",") if name}
+ return {"message_main_attachment_id"} | {
+ name.strip() for name in names.split(",") if name.strip()
+ }
def write(self, vals):
- # A completed (done) request can only be edited by a maintenance
- # manager. Checked against the pre-write state, so completing a request
- # (moving it to a done stage) is still allowed for everyone. The bypass
- # context lets the internal write cascade triggered by completion (e.g.
- # close_date) through, while direct edits of those fields stay blocked.
+ # A completed (done) request can only be edited by users in the edit
+ # group. Checked pre-write, so everyone can still complete a request
+ # (move it to done).
if (
- not self.env.user.has_group("maintenance.group_equipment_manager")
+ not self.env.context.get("mnt_done_bypass_lock")
+ and not self.env.user.has_group(
+ "maintenance_request_done_readonly."
+ "group_maintenance_request_edit_done"
+ )
and (set(vals) - self._writable_fields_when_done())
- and not self.env.context.get("mnt_done_bypass_lock")
+ and (locked := self.filtered("done"))
):
- locked = self.filtered("done")
- if locked:
- raise UserError(
- _(
- "'%s' is completed and can only be edited by a "
- "maintenance manager.",
- locked[0].display_name,
- )
+ raise UserError(
+ _(
+ "'%s' is completed and can only be edited by users in the "
+ "'Maintenance: Edit Completed Requests' group.",
+ locked[0].display_name,
)
- records = self
+ )
+ # When completing a request, let the internal close_date write (done by
+ # core maintenance) bypass the lock above so completion isn't blocked.
stage_id = vals.get("stage_id")
if stage_id and self.env["maintenance.stage"].browse(stage_id).done:
- records = self.with_context(mnt_done_bypass_lock=True)
- return super(MaintenanceRequest, records).write(vals)
+ self = self.with_context(mnt_done_bypass_lock=True)
+ return super().write(vals)
diff --git a/maintenance_request_done_readonly/models/res_config_settings.py b/maintenance_request_done_readonly/models/res_config_settings.py
deleted file mode 100644
index 249d36b..0000000
--- a/maintenance_request_done_readonly/models/res_config_settings.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# Copyright 2026 Quartile (https://www.quartile.co)
-# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-
-from odoo import fields, models
-
-
-class ResConfigSettings(models.TransientModel):
- _inherit = "res.config.settings"
-
- maintenance_done_editable_field_ids = fields.Many2many(
- "ir.model.fields",
- string="Editable fields on completed requests",
- domain=[("model", "=", "maintenance.request"), ("store", "=", True)],
- help="Fields that non-managers may still edit after a maintenance "
- "request is completed. Other fields stay locked.",
- )
-
- def get_values(self):
- res = super().get_values()
- names = (
- self.env["ir.config_parameter"]
- .sudo()
- .get_param("maintenance_request_done_readonly.editable_fields", "")
- )
- name_list = [name for name in names.split(",") if name]
- field_recs = self.env["ir.model.fields"].search(
- [("model", "=", "maintenance.request"), ("name", "in", name_list)]
- )
- res["maintenance_done_editable_field_ids"] = [(6, 0, field_recs.ids)]
- return res
-
- def set_values(self):
- res = super().set_values()
- names = ",".join(self.maintenance_done_editable_field_ids.mapped("name"))
- self.env["ir.config_parameter"].sudo().set_param(
- "maintenance_request_done_readonly.editable_fields", names
- )
- return res
diff --git a/maintenance_request_done_readonly/readme/DESCRIPTION.md b/maintenance_request_done_readonly/readme/DESCRIPTION.md
index b372001..6646062 100644
--- a/maintenance_request_done_readonly/readme/DESCRIPTION.md
+++ b/maintenance_request_done_readonly/readme/DESCRIPTION.md
@@ -2,12 +2,16 @@ This module makes completed maintenance requests read-only.
Once a request reaches a done stage, it can no longer be edited or reopened:
any attempt to change its fields (including moving it back out of the done
-stage) is blocked. Only members of the *Equipment Manager* group keep full
-access to completed requests.
+stage) is blocked. Only members of the *Maintenance: Edit Completed Requests*
+group keep full access to completed requests. *Equipment Manager* users get
+that group by default, so it can also be granted to users who should be able
+to correct completed requests without being maintenance managers.
The restriction is enforced on write, so every field is locked by default,
without having to enumerate them. Completing a request and its follow-up
(commenting, following, scheduling activities) keep working.
-If some fields should stay editable after completion, an administrator can
-select them in *Settings > Maintenance > Completed Requests*.
+If some fields should stay editable after completion, edit the
+``maintenance_request_done_readonly.editable_fields`` system parameter
+(*Settings > Technical > Parameters > System Parameters*, created on install)
+and set a comma-separated list of their technical names.
diff --git a/maintenance_request_done_readonly/security/res_groups.xml b/maintenance_request_done_readonly/security/res_groups.xml
new file mode 100644
index 0000000..3c750c7
--- /dev/null
+++ b/maintenance_request_done_readonly/security/res_groups.xml
@@ -0,0 +1,18 @@
+
+
+
+ Maintenance: Edit Completed Requests
+
+
+ The user will be able to edit completed maintenance requests.
+
+
+
+
+
+
diff --git a/maintenance_request_done_readonly/static/description/index.html b/maintenance_request_done_readonly/static/description/index.html
index ad9a1fa..381a447 100644
--- a/maintenance_request_done_readonly/static/description/index.html
+++ b/maintenance_request_done_readonly/static/description/index.html
@@ -373,13 +373,18 @@ Maintenance Request Done Read-only
This module makes completed maintenance requests read-only.
Once a request reaches a done stage, it can no longer be edited or
reopened: any attempt to change its fields (including moving it back out
-of the done stage) is blocked. Only members of the Equipment Manager
-group keep full access to completed requests.
+of the done stage) is blocked. Only members of the Maintenance: Edit
+Completed Requests group keep full access to completed requests.
+Equipment Manager users get that group by default, so it can also be
+granted to users who should be able to correct completed requests
+without being maintenance managers.
The restriction is enforced on write, so every field is locked by
default, without having to enumerate them. Completing a request and its
follow-up (commenting, following, scheduling activities) keep working.
-If some fields should stay editable after completion, an administrator
-can select them in Settings > Maintenance > Completed Requests.
+If some fields should stay editable after completion, edit the
+maintenance_request_done_readonly.editable_fields system parameter
+(Settings > Technical > Parameters > System Parameters, created on
+install) and set a comma-separated list of their technical names.
Table of contents
diff --git a/maintenance_request_done_readonly/tests/test_maintenance_request_done_readonly.py b/maintenance_request_done_readonly/tests/test_maintenance_request_done_readonly.py
index eff6430..baebf70 100644
--- a/maintenance_request_done_readonly/tests/test_maintenance_request_done_readonly.py
+++ b/maintenance_request_done_readonly/tests/test_maintenance_request_done_readonly.py
@@ -25,6 +25,12 @@ def setUpClass(cls):
login="mrl_user",
groups="base.group_user",
)
+ cls.editor = new_test_user(
+ cls.env,
+ login="mrl_editor",
+ groups="base.group_user,"
+ "maintenance_request_done_readonly.group_maintenance_request_edit_done",
+ )
cls.equipment = cls.env["maintenance.equipment"].create(
{"name": "Test Equipment"}
)
@@ -77,11 +83,27 @@ def test_regular_user_can_edit_open(self):
self.assertEqual(request.name, "Still editable")
def test_manager_can_edit_completed(self):
+ """Equipment managers imply the edit group, so they keep full access."""
+ self.assertTrue(
+ self.manager.has_group(
+ "maintenance_request_done_readonly."
+ "group_maintenance_request_edit_done"
+ )
+ )
request = self._new_request(self.user)
request.with_user(self.user).write({"stage_id": self.done_stage.id})
request.with_user(self.manager).write({"name": "Corrected by manager"})
self.assertEqual(request.name, "Corrected by manager")
+ def test_editor_can_edit_completed(self):
+ """A user granted the edit group, but not a manager, can still correct a
+ completed request."""
+ self.assertFalse(self.editor.has_group("maintenance.group_equipment_manager"))
+ request = self._new_request(self.editor)
+ request.with_user(self.editor).write({"stage_id": self.done_stage.id})
+ request.with_user(self.editor).write({"name": "Corrected by editor"})
+ self.assertEqual(request.name, "Corrected by editor")
+
def test_regular_user_cannot_edit_close_date_when_done(self):
"""close_date is written internally on completion (bypass), but a
direct edit by a regular user on a completed request must be blocked."""
diff --git a/maintenance_request_done_readonly/views/res_config_settings_views.xml b/maintenance_request_done_readonly/views/res_config_settings_views.xml
deleted file mode 100644
index 14d92fb..0000000
--- a/maintenance_request_done_readonly/views/res_config_settings_views.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
- res.config.settings.view.form.inherit.maintenance.done.readonly
- res.config.settings
-
-
-
-
-
-
-
-
-
-
-
-