diff --git a/project_task_code/README.rst b/project_task_code/README.rst index d11846ff2f..db62788113 100644 --- a/project_task_code/README.rst +++ b/project_task_code/README.rst @@ -42,6 +42,17 @@ This module adds a sequential code for tasks. Configuration ============= +To choose how the tasks of a project are identified, you must: + +1. Go to Project > Configuration > Projects and open a project. +2. In the Settings tab, set "Task Name Display" to: + + - "Number and Title" (default) to show the task number followed by + its title. + - "Number only" to show the task number alone. Titles are still + stored (so, for instance, tasks created from an incoming email keep + the mail subject) but they are neither displayed nor required. + To change the task code sequence, you must: 1. Activate the developer mode. @@ -94,6 +105,7 @@ Contributors - Tharathip Chaweewongphan - Ruchir Shukla - Nedas Žilinskas +- Jasmin Solanki Maintainers ----------- diff --git a/project_task_code/models/__init__.py b/project_task_code/models/__init__.py index 4e791c80b1..6486431ddd 100644 --- a/project_task_code/models/__init__.py +++ b/project_task_code/models/__init__.py @@ -1,3 +1,4 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import project_project from . import project_task diff --git a/project_task_code/models/project_project.py b/project_task_code/models/project_project.py new file mode 100644 index 0000000000..6a5fc3f7b4 --- /dev/null +++ b/project_task_code/models/project_project.py @@ -0,0 +1,21 @@ +# Copyright 2026 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ProjectProject(models.Model): + _inherit = "project.project" + + task_name_display = fields.Selection( + selection=[ + ("code_name", "Number and Title"), + ("code", "Number only"), + ], + default="code_name", + required=True, + help="How the tasks of this project are identified:\n" + "- Number and Title: the task number followed by its title.\n" + "- Number only: the task number alone; titles are kept but not " + "displayed, and are not required.", + ) diff --git a/project_task_code/models/project_task.py b/project_task_code/models/project_task.py index 3f246309d9..53845c19bd 100644 --- a/project_task_code/models/project_task.py +++ b/project_task_code/models/project_task.py @@ -15,6 +15,14 @@ class ProjectTask(models.Model): readonly=True, copy=False, ) + # Tasks of a project displaying the number alone need no title, so the + # requirement is moved to the view, where it only applies to projects + # displaying the title. + name = fields.Char(required=False) + task_name_display = fields.Selection( + related="project_id.task_name_display", + readonly=True, + ) _code_company_uniq = models.Constraint( "unique (company_id, code)", @@ -30,10 +38,24 @@ def create(self, vals_list): ) return super().create(vals_list) - @api.depends("name", "code") + def copy_data(self, default=None): + vals_list = super().copy_data(default=default) + for task, vals in zip(self, vals_list, strict=True): + # An untitled task must stay untitled, instead of getting the + # "False (copy)" title built by the standard copy. + if not task.name and not (default or {}).get("name"): + vals["name"] = False + return vals_list + + @api.depends("name", "code", "task_name_display") def _compute_display_name(self): result = super()._compute_display_name() for task in self: - if task.code and task.code != "/" and task.display_name: - task.display_name = f"[{task.code}] {task.display_name}" + if not task.code or task.code == "/": + continue + name = task.display_name + if task.task_name_display == "code" or not name: + task.display_name = task.code + else: + task.display_name = f"[{task.code}] {name}" return result diff --git a/project_task_code/readme/CONFIGURE.md b/project_task_code/readme/CONFIGURE.md index 23457fa8cc..1e2d5fbe91 100644 --- a/project_task_code/readme/CONFIGURE.md +++ b/project_task_code/readme/CONFIGURE.md @@ -1,3 +1,13 @@ +To choose how the tasks of a project are identified, you must: + +1. Go to Project \> Configuration \> Projects and open a project. +2. In the Settings tab, set "Task Name Display" to: + - "Number and Title" (default) to show the task number followed by + its title. + - "Number only" to show the task number alone. Titles are still + stored (so, for instance, tasks created from an incoming email keep + the mail subject) but they are neither displayed nor required. + To change the task code sequence, you must: 1. Activate the developer mode. diff --git a/project_task_code/readme/CONTRIBUTORS.md b/project_task_code/readme/CONTRIBUTORS.md index 534c340b75..f1dbdeac01 100644 --- a/project_task_code/readme/CONTRIBUTORS.md +++ b/project_task_code/readme/CONTRIBUTORS.md @@ -10,3 +10,4 @@ - Tharathip Chaweewongphan \<\> - Ruchir Shukla \<\> - Nedas Žilinskas \<\> +- Jasmin Solanki \<\> diff --git a/project_task_code/static/description/index.html b/project_task_code/static/description/index.html index a2a2be73fd..55c55af251 100644 --- a/project_task_code/static/description/index.html +++ b/project_task_code/static/description/index.html @@ -392,6 +392,18 @@

Sequential Code for Tasks

Configuration

+

To choose how the tasks of a project are identified, you must:

+
    +
  1. Go to Project > Configuration > Projects and open a project.
  2. +
  3. In the Settings tab, set “Task Name Display” to:
      +
    • “Number and Title” (default) to show the task number followed by +its title.
    • +
    • “Number only” to show the task number alone. Titles are still +stored (so, for instance, tasks created from an incoming email keep +the mail subject) but they are neither displayed nor required.
    • +
    +
  4. +

To change the task code sequence, you must:

  1. Activate the developer mode.
  2. @@ -441,6 +453,7 @@

    Contributors

  3. Tharathip Chaweewongphan <tharathipc@ecosoft.co.th>
  4. Ruchir Shukla <ruchir@bizzappdev.com>
  5. Nedas Žilinskas <nedas.zilinskas@avoin.systems>
  6. +
  7. Jasmin Solanki <jasmin.solanki@forgeflow.com>
diff --git a/project_task_code/tests/test_project_task_code.py b/project_task_code/tests/test_project_task_code.py index 08b57df1e7..9ba39fe54b 100644 --- a/project_task_code/tests/test_project_task_code.py +++ b/project_task_code/tests/test_project_task_code.py @@ -65,6 +65,51 @@ def test_name_search(self): f"Task with code {project_task.code} should not be in the results", ) + def test_display_name_code_only_project(self): + project = self.env["project.project"].create( + {"name": "Code only", "task_name_display": "code"} + ) + task = self.project_task_model.create( + {"name": "Testing task code", "project_id": project.id} + ) + self.assertEqual(task.display_name, task.code) + + def test_task_without_name_on_code_only_project(self): + project = self.env["project.project"].create( + {"name": "Code only", "task_name_display": "code"} + ) + task = self.project_task_model.create({"project_id": project.id}) + self.assertFalse(task.name) + self.assertEqual(task.display_name, task.code) + + def test_duplicated_task_without_name_stays_untitled(self): + project = self.env["project.project"].create( + {"name": "Code only", "task_name_display": "code"} + ) + task = self.project_task_model.create({"project_id": project.id}) + copied = task.copy() + self.assertNotEqual(copied.code, task.code) + self.assertFalse(copied.name) + self.assertEqual(copied.display_name, copied.code) + + def test_duplicated_task_with_name_keeps_the_copy_suffix(self): + task = self.project_task_model.create({"name": "Testing task code"}) + copied = task.copy() + self.assertIn("(copy)", copied.name) + + def test_display_name_falls_back_to_code_without_name(self): + project = self.env["project.project"].create({"name": "With titles"}) + self.assertEqual(project.task_name_display, "code_name") + task = self.project_task_model.create({"project_id": project.id}) + self.assertEqual(task.display_name, task.code) + + def test_display_name_code_and_name_project(self): + project = self.env["project.project"].create({"name": "With titles"}) + task = self.project_task_model.create( + {"name": "Testing task code", "project_id": project.id} + ) + self.assertEqual(task.display_name, f"[{task.code}] Testing task code") + def test_name_get_on_create(self): number_next = self.task_sequence.number_next_actual code = self.task_sequence.get_next_char(number_next) diff --git a/project_task_code/views/project_view.xml b/project_task_code/views/project_view.xml index 4fb98ce175..e9f97428e7 100644 --- a/project_task_code/views/project_view.xml +++ b/project_task_code/views/project_view.xml @@ -1,13 +1,36 @@ + + project.project.form.task.name.display + project.project + + + + + + + project.task.code.form project.task + - - + - + + + + task_name_display == 'code' + task_name_display != 'code' @@ -36,6 +59,12 @@ + + task_name_display == 'code' +