diff --git a/base_tier_validation_board/README.rst b/base_tier_validation_board/README.rst new file mode 100644 index 00000000..37413e7c --- /dev/null +++ b/base_tier_validation_board/README.rst @@ -0,0 +1,97 @@ +========================== +Tier Review Activity Board +========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:6bc34836e6b88351ca1f0343b26edf7d8951087c0e3dd7127533620b2ef42e07 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fserver--ux-lightgray.png?logo=github + :target: https://github.com/OCA/server-ux/tree/18.0/base_tier_validation_board + :alt: OCA/server-ux +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-ux-18-0/server-ux-18-0-base_tier_validation_board + :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/server-ux&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds an tier review board with form, tree, kanban, calendar, +pivot, graph and search views. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To use this module, you need to: + +1. Access to the views from menu Boards. + +From the form view of the tier review you can navigate to the origin of +the tier review. + +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 +------- + +* ForgeFlow + +Contributors +------------ + +- `ForgeFlow `__: + + - Jasmin Solanki (jasmin.solanki@forgeflow.com) + +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. + +.. |maintainer-JasminSForgeFlow| image:: https://github.com/JasminSForgeFlow.png?size=40px + :target: https://github.com/JasminSForgeFlow + :alt: JasminSForgeFlow + +Current `maintainer `__: + +|maintainer-JasminSForgeFlow| + +This module is part of the `OCA/server-ux `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/base_tier_validation_board/__init__.py b/base_tier_validation_board/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/base_tier_validation_board/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/base_tier_validation_board/__manifest__.py b/base_tier_validation_board/__manifest__.py new file mode 100644 index 00000000..87c61cde --- /dev/null +++ b/base_tier_validation_board/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2024 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + "name": "Tier Review Activity Board", + "summary": "Dashboard with kanban, list, pivot and graph views over" + " every pending/approved/rejected tier review, gated by an opt-in" + " group and per-document ACL.", + "version": "19.0.1.0.0", + "development_status": "Beta", + "maintainers": ["JasminSForgeFlow"], + "category": "Tools", + "website": "https://github.com/OCA/tier-validation", + "author": "ForgeFlow, Odoo Community Association (OCA)", + "license": "AGPL-3", + "installable": True, + "depends": ["base_tier_validation", "spreadsheet_dashboard"], + "data": ["security/groups.xml", "views/tier_review_view.xml"], +} diff --git a/base_tier_validation_board/i18n/base_tier_validation_board.pot b/base_tier_validation_board/i18n/base_tier_validation_board.pot new file mode 100644 index 00000000..8fde1b60 --- /dev/null +++ b/base_tier_validation_board/i18n/base_tier_validation_board.pot @@ -0,0 +1,55 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_tier_validation_board +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: base_tier_validation_board +#: model_terms:ir.ui.view,arch_db:base_tier_validation_board.tier_review_view_kanban +msgid "Comment: " +msgstr "" + +#. module: base_tier_validation_board +#: model_terms:ir.ui.view,arch_db:base_tier_validation_board.tier_review_view_kanban +msgid "Requsted by: " +msgstr "" + +#. module: base_tier_validation_board +#: model_terms:ir.ui.view,arch_db:base_tier_validation_board.tier_review_view_kanban +msgid "Todo by: " +msgstr "" + +#. module: base_tier_validation_board +#: model:ir.model.fields,field_description:base_tier_validation_board.field_tier_review__related_model_instance +msgid "Document" +msgstr "" + +#. module: base_tier_validation_board +#: model:ir.model.fields,field_description:base_tier_validation_board.field_tier_review__res_name +msgid "Resource Name" +msgstr "" + +#. module: base_tier_validation_board +#: model:res.groups,name:base_tier_validation_board.group_show_tier_review_board +msgid "Show tier review board" +msgstr "" + +#. module: base_tier_validation_board +#: model:ir.model,name:base_tier_validation_board.model_tier_review +msgid "Tier Review" +msgstr "" + +#. module: base_tier_validation_board +#: model:ir.actions.act_window,name:base_tier_validation_board.open_boards_tier_reviews +#: model:ir.ui.menu,name:base_tier_validation_board.board_menu_tier_reviews +msgid "Tier Reviews" +msgstr "" diff --git a/base_tier_validation_board/i18n/it.po b/base_tier_validation_board/i18n/it.po new file mode 100644 index 00000000..007fa91f --- /dev/null +++ b/base_tier_validation_board/i18n/it.po @@ -0,0 +1,58 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_tier_validation_board +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-06-26 10:45+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: base_tier_validation_board +#: model_terms:ir.ui.view,arch_db:base_tier_validation_board.tier_review_view_kanban +msgid "Comment: " +msgstr "Commento: " + +#. module: base_tier_validation_board +#: model_terms:ir.ui.view,arch_db:base_tier_validation_board.tier_review_view_kanban +msgid "Requsted by: " +msgstr "Richiesto da: " + +#. module: base_tier_validation_board +#: model_terms:ir.ui.view,arch_db:base_tier_validation_board.tier_review_view_kanban +msgid "Todo by: " +msgstr "Da fare di: " + +#. module: base_tier_validation_board +#: model:ir.model.fields,field_description:base_tier_validation_board.field_tier_review__related_model_instance +msgid "Document" +msgstr "Documento" + +#. module: base_tier_validation_board +#: model:ir.model.fields,field_description:base_tier_validation_board.field_tier_review__res_name +msgid "Resource Name" +msgstr "Nome risorsa" + +#. module: base_tier_validation_board +#: model:res.groups,name:base_tier_validation_board.group_show_tier_review_board +msgid "Show tier review board" +msgstr "Visualizza scheda revisione livelli" + +#. module: base_tier_validation_board +#: model:ir.model,name:base_tier_validation_board.model_tier_review +msgid "Tier Review" +msgstr "Revisione livello" + +#. module: base_tier_validation_board +#: model:ir.actions.act_window,name:base_tier_validation_board.open_boards_tier_reviews +#: model:ir.ui.menu,name:base_tier_validation_board.board_menu_tier_reviews +msgid "Tier Reviews" +msgstr "Revisioni livello" diff --git a/base_tier_validation_board/models/__init__.py b/base_tier_validation_board/models/__init__.py new file mode 100644 index 00000000..942b13b3 --- /dev/null +++ b/base_tier_validation_board/models/__init__.py @@ -0,0 +1,2 @@ +from . import res_users +from . import tier_review diff --git a/base_tier_validation_board/models/res_users.py b/base_tier_validation_board/models/res_users.py new file mode 100644 index 00000000..cdc53a9c --- /dev/null +++ b/base_tier_validation_board/models/res_users.py @@ -0,0 +1,35 @@ +# Copyright 2026 OCA / @bosd +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, models + + +class Users(models.Model): + _inherit = "res.users" + + @api.model + def tier_review_dashboard_action(self): + """Expose the Tier Reviews dashboard as the destination of the + systray's "Show all reviews" footer link. + + The base module ships ``tier_review_dashboard_action`` as a stub + returning ``False`` so the systray omits the link when no + dashboard module is installed. When this module is installed, + we point the link at the all-reviews kanban gated by + ``group_show_tier_review_board``. + """ + action = self.env.ref( + "base_tier_validation_board.open_boards_tier_reviews", + raise_if_not_found=False, + ) + if not action: + return False + # Only expose the link to users who can actually see the + # dashboard menu in the first place. Otherwise the link would + # take them to an action that immediately errors with an + # AccessError. + if not self.env.user.has_group( + "base_tier_validation_board.group_show_tier_review_board" + ): + return False + return action.read()[0] diff --git a/base_tier_validation_board/models/tier_review.py b/base_tier_validation_board/models/tier_review.py new file mode 100644 index 00000000..f59a417e --- /dev/null +++ b/base_tier_validation_board/models/tier_review.py @@ -0,0 +1,192 @@ +# Copyright 2024 ForgeFlow S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import api, fields, models +from odoo.tools import SQL, split_every + +OVERDUE_DAYS = 7 +# Days after which a still-pending/waiting tier review is considered +# overdue. Surfaced as ``is_overdue`` and used by the kanban "rotten" +# indicator and the "Overdue" search filter. + + +class TierReview(models.Model): + _name = "tier.review" + _inherit = ["tier.review", "mail.thread", "mail.activity.mixin"] + + @api.depends("model", "res_id") + def _compute_res_name(self): + for record in self: + if record.res_id and record.model: + record.res_name = ( + self.env[record.model].browse(record.res_id).display_name + ) + else: + record.res_name = False + + related_model_instance = fields.Reference( + selection="_selection_related_model_instance", + compute="_compute_related_model_instance", + string="Document", + ) + res_name = fields.Char( + "Resource Name", compute="_compute_res_name", compute_sudo=True + ) + model_id = fields.Many2one( + comodel_name="ir.model", + related="definition_id.model_id", + store=True, + string="Model", + help="Many2one to ir.model used by the pivot/graph views so the " + "rows display the model's human-friendly description (e.g. " + "'Journal Entry') rather than its technical name " + "('account.move').", + ) + response_days = fields.Float( + string="Response (days)", + compute="_compute_response_days", + store=True, + help="Days between the review being created and it being done. " + "Empty until the review is approved or rejected. Use this as a " + "measure in pivot/graph views to compare reviewer response time.", + ) + is_overdue = fields.Boolean( + compute="_compute_is_overdue", + search="_search_is_overdue", + help="True when this review is still pending/waiting and was " + f"created more than {OVERDUE_DAYS} days ago. Surfaced on the " + "kanban with a 'rotten' indicator.", + ) + + @api.depends("create_date", "reviewed_date") + def _compute_response_days(self): + for rec in self: + if rec.create_date and rec.reviewed_date: + rec.response_days = ( + rec.reviewed_date - rec.create_date + ).total_seconds() / 86400.0 + else: + rec.response_days = 0.0 + + @api.depends("status", "create_date") + def _compute_is_overdue(self): + cutoff = fields.Datetime.subtract(fields.Datetime.now(), days=OVERDUE_DAYS) + for rec in self: + rec.is_overdue = ( + rec.status in ("waiting", "pending") + and rec.create_date + and rec.create_date < cutoff + ) + + @api.model + def _search_is_overdue(self, operator, value): + if operator not in ("=", "!=") or not isinstance(value, bool): + return [("id", "=", False)] + cutoff = fields.Datetime.subtract(fields.Datetime.now(), days=OVERDUE_DAYS) + overdue_ids = ( + self.sudo() + .search( + [ + ("status", "in", ["waiting", "pending"]), + ("create_date", "<", cutoff), + ] + ) + .ids + ) + match = (operator == "=") == bool(value) + return [("id", "in" if match else "not in", overdue_ids)] + + @api.depends("res_id", "model") + def _compute_related_model_instance(self): + for record in self: + ref = False + if record.res_id: + ref = f"{record.model},{record.res_id}" + record.related_model_instance = ref + + @api.model + def _selection_related_model_instance(self): + # Restrict the Reference selection to models that actually carry + # tier validation. Avoids the pylint `no-search-all` warning and + # mirrors the domain used in `tier.definition.model_id`. + model_names = self.env["tier.definition"]._get_tier_validation_model_names() + if not model_names: + return [] + models = self.env["ir.model"].sudo().search([("model", "in", model_names)]) + return [(model.model, model.name) for model in models] + + def open_origin(self): + self.ensure_one() + vid = self.env[self.model].browse(self.res_id).get_formview_id() + response = { + "type": "ir.actions.act_window", + "res_model": self.model, + "view_mode": "form", + "res_id": self.res_id, + "target": "current", + "views": [(vid, "form")], + } + return response + + @api.model + def _search(self, domain, *args, **kwargs): + # Forward arbitrary kwargs to super() (the v19 ORM keeps adding + # them: ``bypass_access``, ``active_test``, ...). When the caller + # has explicitly opted out of ACL or is the superuser, skip the + # board's per-document filter -- the board filter is itself an + # ACL, so bypass_access=True should bypass it too. + if kwargs.get("bypass_access") or self.env.is_superuser(): + return super()._search(domain, *args, **kwargs) + query = super()._search(domain, *args, **kwargs) + ids = self.browse(query).ids + if not ids: + return query + + super().check_access("read") + + self.flush_model(["model", "res_id"]) + reviews_to_check = [] + for sub_ids in split_every(self.env.cr.IN_MAX, ids): + self.env.cr.execute( + SQL( + """ + SELECT DISTINCT review.id, review.model, review.res_id + FROM %(table)s review + WHERE review.id = ANY (%(ids)s) AND review.res_id != 0""", + table=SQL.identifier(self._table), + ids=list(sub_ids), + ) + ) + reviews_to_check += self.env.cr.dictfetchall() + + review_to_documents = {} + for review in reviews_to_check: + review_to_documents.setdefault(review["model"], set()).add(review["res_id"]) + + allowed_ids = set() + for doc_model, doc_ids in review_to_documents.items(): + doc_operation = "read" + DocumentModel = self.env[doc_model].with_user(self.env.uid) + right = DocumentModel.has_access(doc_operation) + if right: + valid_docs = DocumentModel.browse(doc_ids)._filtered_access( + doc_operation + ) + valid_doc_ids = set(valid_docs.ids) + allowed_ids.update( + review["id"] + for review in reviews_to_check + if review["model"] == doc_model + and review["res_id"] in valid_doc_ids + ) + + id_list = [id for id in ids if id in allowed_ids] + + return super()._search([("id", "in", id_list)], *args, **kwargs) + + # NOTE: previous Odoo versions overrode ``_read_group_raw`` here to + # re-apply the same per-document ACL the ``_search`` override above + # enforces -- needed because the read_group path used to bypass + # ``_search``. From Odoo 17 onwards ``_read_group_raw`` is gone and + # ``_read_group`` routes through ``_search`` internally, so the + # ``_search`` override already covers the read_group case and the + # explicit override is no longer required. diff --git a/base_tier_validation_board/pyproject.toml b/base_tier_validation_board/pyproject.toml new file mode 100644 index 00000000..4231d0cc --- /dev/null +++ b/base_tier_validation_board/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/base_tier_validation_board/readme/CONTRIBUTORS.md b/base_tier_validation_board/readme/CONTRIBUTORS.md new file mode 100644 index 00000000..f2649e5e --- /dev/null +++ b/base_tier_validation_board/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [ForgeFlow](https://www.forgeflow.com): + - Jasmin Solanki () diff --git a/base_tier_validation_board/readme/DESCRIPTION.md b/base_tier_validation_board/readme/DESCRIPTION.md new file mode 100644 index 00000000..c1521ec2 --- /dev/null +++ b/base_tier_validation_board/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +This module adds an tier review board with form, tree, kanban, calendar, +pivot, graph and search views. diff --git a/base_tier_validation_board/readme/USAGE.md b/base_tier_validation_board/readme/USAGE.md new file mode 100644 index 00000000..4ad5276d --- /dev/null +++ b/base_tier_validation_board/readme/USAGE.md @@ -0,0 +1,6 @@ +To use this module, you need to: + +1. Access to the views from menu Boards. + +From the form view of the tier review you can navigate to the origin of +the tier review. diff --git a/base_tier_validation_board/security/groups.xml b/base_tier_validation_board/security/groups.xml new file mode 100644 index 00000000..d0b64006 --- /dev/null +++ b/base_tier_validation_board/security/groups.xml @@ -0,0 +1,10 @@ + + + + Show tier review board + + + diff --git a/base_tier_validation_board/static/description/icon.png b/base_tier_validation_board/static/description/icon.png new file mode 100644 index 00000000..e4781126 Binary files /dev/null and b/base_tier_validation_board/static/description/icon.png differ diff --git a/base_tier_validation_board/static/description/index.html b/base_tier_validation_board/static/description/index.html new file mode 100644 index 00000000..df4296c7 --- /dev/null +++ b/base_tier_validation_board/static/description/index.html @@ -0,0 +1,439 @@ + + + + + +Tier Review Activity Board + + + +
+

Tier Review Activity Board

+ + +

Beta License: AGPL-3 OCA/server-ux Translate me on Weblate Try me on Runboat

+

This module adds an tier review board with form, tree, kanban, calendar, +pivot, graph and search views.

+

Table of contents

+ +
+

Usage

+

To use this module, you need to:

+
    +
  1. Access to the views from menu Boards.
  2. +
+

From the form view of the tier review you can navigate to the origin of +the tier review.

+
+
+

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

+
    +
  • ForgeFlow
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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.

+

Current maintainer:

+

JasminSForgeFlow

+

This module is part of the OCA/server-ux project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/base_tier_validation_board/tests/__init__.py b/base_tier_validation_board/tests/__init__.py new file mode 100644 index 00000000..19cb3f59 --- /dev/null +++ b/base_tier_validation_board/tests/__init__.py @@ -0,0 +1 @@ +from . import test_tier_validation_board diff --git a/base_tier_validation_board/tests/test_tier_validation_board.py b/base_tier_validation_board/tests/test_tier_validation_board.py new file mode 100644 index 00000000..e5fba77d --- /dev/null +++ b/base_tier_validation_board/tests/test_tier_validation_board.py @@ -0,0 +1,157 @@ +# Copyright 2026 OCA / @bosd +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from freezegun import freeze_time + +from odoo import fields +from odoo.fields import Domain +from odoo.tests.common import tagged + +from odoo.addons.base_tier_validation.tests.common import CommonTierValidation + + +@tagged("post_install", "-at_install") +class TierValidationBoard(CommonTierValidation): + def setUp(self): + super().setUp() + # Trigger a review on the standard test fixture so the board + # always has at least one record to render. + self.test_record.with_user(self.test_user_2.id).request_validation() + self.review = self.env["tier.review"].search( + Domain("res_id", "=", self.test_record.id) + & Domain("model", "=", self.test_record._name), + limit=1, + ) + self.assertTrue(self.review) + + def test_related_model_instance_compute(self): + """The board's Reference + Char computes resolve cleanly off a + real tier review.""" + review = self.review + self.assertEqual(review.res_name, self.test_record.display_name) + self.assertEqual(review.related_model_instance._name, self.test_record._name) + self.assertEqual(review.related_model_instance.id, self.test_record.id) + + def test_model_id_related(self): + """The `model_id` related field resolves to the validated model's + `ir.model` record so pivot/graph views can display its + human-friendly description instead of the technical name.""" + self.assertEqual(self.review.model_id, self.tester_model) + self.assertEqual(self.review.model_id.model, self.test_record._name) + + def test_search_is_overdue_returns_id_in_domain(self): + """`_search_is_overdue` returns a domain shape the ORM can use -- + either ``[("id", "in", ids)]`` or ``[("id", "not in", ids)]`` + depending on the operator/value pair, and never raises.""" + Model = self.env["tier.review"] + for value in (True, False): + for operator in ("=", "!="): + domain = Model._search_is_overdue(operator, value) + self.assertEqual(len(domain), 1) + cond = domain[0] + self.assertEqual(cond[0], "id") + self.assertIn(cond[1], ("in", "not in", "=")) + # Bad operator -> match nothing (does not raise). + bad = Model._search_is_overdue("ilike", "garbage") + self.assertEqual(bad, [("id", "=", False)]) + + def test_selection_related_model_instance(self): + """The Reference selection only exposes tier-validated models, + not every model in the DB.""" + selection = self.env["tier.review"]._selection_related_model_instance() + models = {m for m, _name in selection} + expected = set(self.env["tier.definition"]._get_tier_validation_model_names()) + self.assertEqual(models, expected) + # Sanity: at least the standard tester model is in there. + self.assertIn(self.test_record._name, models) + + def test_open_origin(self): + """The board's `Open` button returns an act_window pointing at + the underlying validated record.""" + action = self.review.open_origin() + self.assertEqual(action["res_model"], self.test_record._name) + self.assertEqual(action["res_id"], self.test_record.id) + self.assertEqual(action["view_mode"], "form") + + def test_response_days_is_zero_until_reviewed(self): + """`response_days` only populates once the review is approved + or rejected; it stays 0 while pending/waiting.""" + self.assertEqual(self.review.response_days, 0.0) + # Force-set a reviewed_date so the compute fires deterministically. + self.review.write( + { + "reviewed_date": fields.Datetime.add(self.review.create_date, days=2), + "status": "approved", + "done_by": self.test_user_1.id, + } + ) + self.assertAlmostEqual(self.review.response_days, 2.0, places=2) + + def test_is_overdue_compute(self): + """`is_overdue` flips True for waiting/pending reviews older + than the configured threshold, and resets when the review is + completed. + + Uses ``freeze_time`` to move "now" past the threshold instead + of rewriting create_date, keeping the test independent of any + ORM-side cache invariants around the create_date magic field. + """ + # Fresh review: not overdue. + self.assertFalse(self.review.is_overdue) + # Move "now" 14 days forward -- the review's real create_date + # is now well past the 7-day overdue threshold. + later = fields.Datetime.add(fields.Datetime.now(), days=14) + with freeze_time(later): + self.review.invalidate_recordset(["is_overdue"]) + self.assertTrue(self.review.is_overdue) + # Once approved, the review is no longer "overdue" -- even + # if it took forever, it's done. + self.review.write({"status": "approved", "done_by": self.test_user_1.id}) + self.assertFalse(self.review.is_overdue) + + def test_tier_review_dashboard_action_for_authorised_user(self): + """Users in the board group get the dashboard action back from + the systray hook so the "Show all reviews" footer link can open + it. Users without the group get `False` -- without that gate + clicking the link would fail with an AccessError.""" + group = self.env.ref("base_tier_validation_board.group_show_tier_review_board") + admin = self.env.ref("base.user_admin") + # Admin is already in the group via the security/groups.xml + # default; assert and read the action. + self.assertIn(admin, group.user_ids) + action = self.env["res.users"].with_user(admin).tier_review_dashboard_action() + self.assertTrue(action) + self.assertEqual(action.get("res_model"), "tier.review") + # test_user_2 is a plain employee and not in the dashboard group + # -> the hook returns False so the systray omits the link. + self.assertNotIn(self.test_user_2, group.user_ids) + no_action = ( + self.env["res.users"] + .with_user(self.test_user_2) + .tier_review_dashboard_action() + ) + self.assertFalse(no_action) + + def test_search_filters_by_user_acl(self): + """The `_search` override hides reviews whose underlying record + the current user cannot read. The standard tester model has a + public ACL so test_user_2 sees everything; revoke that ACL and + the review should drop out of the board for them.""" + # Sanity: test_user_2 can see the review with the default ACL. + visible = ( + self.env["tier.review"] + .with_user(self.test_user_2) + .search(Domain("id", "=", self.review.id)) + ) + self.assertEqual(visible, self.review) + # Restrict the model's ACL to admins; test_user_2 loses access. + self.env["ir.model.access"].search( + Domain("model_id", "=", self.tester_model.id) + ).write({"group_id": self.env.ref("base.group_system").id}) + self.env["ir.model.access"].call_cache_clearing_methods() + hidden = ( + self.env["tier.review"] + .with_user(self.test_user_2) + .search(Domain("id", "=", self.review.id)) + ) + self.assertFalse(hidden) diff --git a/base_tier_validation_board/views/tier_review_view.xml b/base_tier_validation_board/views/tier_review_view.xml new file mode 100644 index 00000000..7f309ba1 --- /dev/null +++ b/base_tier_validation_board/views/tier_review_view.xml @@ -0,0 +1,374 @@ + + + + tier.review.boards.view.form + tier.review + +
+ + + + + + + + + + + + + + + + + +
+
+
+ + + tier.review.boards.view.kanban + tier.review + + + + + + + + + + + + + + +
+
+ + + + + + +
+
+
+
+ Requested by + + +
+
+ To review + +
+
+ Todo by: + +
+
+ Done by + + + on + +
+
+ Comment: + +
+
+
+
+ Requested +
+
+ +
+
+
+
+
+
+
+ + + tier.review.boards.view.pivot + tier.review + + + + + + + + + + + tier.review.boards.view.graph + tier.review + + + + + + + + + + tier.review.boards.view.search + tier.review + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tier Reviews + tier.review + kanban,list,form,pivot,graph + [] + {'create': False} + + + + + + tier.review.boards.view.stats.pivot + tier.review + + + + + + + + + + + tier.review.boards.view.stats.graph + tier.review + + + + + + + + + + Review Statistics + tier.review + pivot,graph + [("done_by", "!=", False), ("status", "in", ("approved", "rejected"))] + {} + + + <p class="o_view_nocontent_smiling_face"> + No completed reviews to chart yet. + </p><p> + Statistics are computed across approved and rejected reviews. + The default *Avg. days* measure shows the average response + time per reviewer. Use the *Group by* menu in the search bar + to slice by Model, Definition or Requested by. + </p> + + + + My Tier Reviews + tier.review + list,kanban,form,pivot,graph + [("reviewer_ids", "in", uid), ("status", "in", ("pending", "waiting"))] + {"search_default_my_reviews": 1, "create": False} + + + + + + + + +