Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions project_task_code/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -94,6 +105,7 @@ Contributors
- Tharathip Chaweewongphan <tharathipc@ecosoft.co.th>
- Ruchir Shukla <ruchir@bizzappdev.com>
- Nedas Žilinskas <nedas.zilinskas@avoin.systems>
- Jasmin Solanki <jasmin.solanki@forgeflow.com>

Maintainers
-----------
Expand Down
1 change: 1 addition & 0 deletions project_task_code/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from . import project_project
from . import project_task
21 changes: 21 additions & 0 deletions project_task_code/models/project_project.py
Original file line number Diff line number Diff line change
@@ -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.",
)
28 changes: 25 additions & 3 deletions project_task_code/models/project_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand All @@ -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
10 changes: 10 additions & 0 deletions project_task_code/readme/CONFIGURE.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
1 change: 1 addition & 0 deletions project_task_code/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
- Tharathip Chaweewongphan \<<tharathipc@ecosoft.co.th>\>
- Ruchir Shukla \<<ruchir@bizzappdev.com>\>
- Nedas Žilinskas \<<nedas.zilinskas@avoin.systems>\>
- Jasmin Solanki \<<jasmin.solanki@forgeflow.com>\>
13 changes: 13 additions & 0 deletions project_task_code/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,18 @@ <h1>Sequential Code for Tasks</h1>
</div>
<div class="section" id="configuration">
<h2><a class="toc-backref" href="#toc-entry-1">Configuration</a></h2>
<p>To choose how the tasks of a project are identified, you must:</p>
<ol class="arabic simple">
<li>Go to Project &gt; Configuration &gt; Projects and open a project.</li>
<li>In the Settings tab, set “Task Name Display” to:<ul>
<li>“Number and Title” (default) to show the task number followed by
its title.</li>
<li>“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.</li>
</ul>
</li>
</ol>
<p>To change the task code sequence, you must:</p>
<ol class="arabic simple">
<li>Activate the developer mode.</li>
Expand Down Expand Up @@ -441,6 +453,7 @@ <h3><a class="toc-backref" href="#toc-entry-6">Contributors</a></h3>
<li>Tharathip Chaweewongphan &lt;<a class="reference external" href="mailto:tharathipc&#64;ecosoft.co.th">tharathipc&#64;ecosoft.co.th</a>&gt;</li>
<li>Ruchir Shukla &lt;<a class="reference external" href="mailto:ruchir&#64;bizzappdev.com">ruchir&#64;bizzappdev.com</a>&gt;</li>
<li>Nedas Žilinskas &lt;<a class="reference external" href="mailto:nedas.zilinskas&#64;avoin.systems">nedas.zilinskas&#64;avoin.systems</a>&gt;</li>
<li>Jasmin Solanki &lt;<a class="reference external" href="mailto:jasmin.solanki&#64;forgeflow.com">jasmin.solanki&#64;forgeflow.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
Expand Down
45 changes: 45 additions & 0 deletions project_task_code/tests/test_project_task_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
31 changes: 30 additions & 1 deletion project_task_code/views/project_view.xml
Original file line number Diff line number Diff line change
@@ -1,13 +1,36 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="project_project_task_name_display_form_view" model="ir.ui.view">
<field name="name">project.project.form.task.name.display</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.edit_project" />
<field name="arch" type="xml">
<group name="extra_settings" position="inside">
<field name="task_name_display" widget="radio" />
</group>
</field>
</record>
<record id="project_task_code_form_view" model="ir.ui.view">
<field name="name">project.task.code.form</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_form2" />
<field name="arch" type="xml">
<field name="name" position="before">
<field name="task_name_display" invisible="1" />
<field name="code" class="oe_inline" />
<span class="oe_inline"> - </span>
<span
class="oe_inline"
invisible="task_name_display == 'code'"
> - </span>
</field>
<!--
Projects displaying the number alone hide the title, so it
cannot be required there. The field itself is not required at
model level for that reason.
-->
<field name="name" position="attributes">
<attribute name="invisible">task_name_display == 'code'</attribute>
<attribute name="required">task_name_display != 'code'</attribute>
</field>
</field>
</record>
Expand Down Expand Up @@ -36,6 +59,12 @@
<xpath expr="//div[@name='task_code']" position="inside">
<xpath expr="//field[@name='name']" position="move" />
</xpath>
<xpath
expr="//div[@name='task_code']/field[@name='name']"
position="attributes"
>
<attribute name="invisible">task_name_display == 'code'</attribute>
</xpath>
</field>
</record>
<record id="project_task_code_search_view" model="ir.ui.view">
Expand Down
Loading