details:   https://code.tryton.org/tryton/commit/76a0a505292f
branch:    default
user:      Cédric Krier <[email protected]>
date:      Thu Mar 12 18:12:50 2026 +0100
description:
        Add number to the project work efforts

        Closes #14671
diffstat:

 modules/project/CHANGELOG                           |   1 +
 modules/project/doc/design.rst                      |   4 +-
 modules/project/message.xml                         |   3 +
 modules/project/tests/scenario_project.rst          |  29 ++++++
 modules/project/tryton.cfg                          |   2 +
 modules/project/view/project_configuration_form.xml |   7 +
 modules/project/view/work_form.xml                  |   5 +-
 modules/project/view/work_list.xml                  |   1 +
 modules/project/view/work_list_children.xml         |   4 +-
 modules/project/view/work_list_simple.xml           |   1 +
 modules/project/view/work_tree.xml                  |   4 +-
 modules/project/view/work_tree_simple.xml           |   4 +-
 modules/project/work.py                             |  93 ++++++++++++++++++++-
 modules/project/work.xml                            |  59 +++++++++++++
 14 files changed, 208 insertions(+), 9 deletions(-)

diffs (394 lines):

diff -r 5f2995a96608 -r 76a0a505292f modules/project/CHANGELOG
--- a/modules/project/CHANGELOG Wed Mar 11 19:04:22 2026 +0100
+++ b/modules/project/CHANGELOG Thu Mar 12 18:12:50 2026 +0100
@@ -1,3 +1,4 @@
+* Add number to the work efforts
 * Add support for Python 3.14
 * Remove support for Python 3.9
 
diff -r 5f2995a96608 -r 76a0a505292f modules/project/doc/design.rst
--- a/modules/project/doc/design.rst    Wed Mar 11 19:04:22 2026 +0100
+++ b/modules/project/doc/design.rst    Thu Mar 12 18:12:50 2026 +0100
@@ -13,8 +13,8 @@
 The *Work Effort* concept is used to represent work that must be done for
 projects, or parts of projects.
 
-Each *Work Effort* is described by a name and a type, and there is space
-for additional comments to be added when required.
+Each *Work Effort* is described by a name, a number and a type, and there is
+space for additional comments to be added when required.
 
 The estimated time and effort needed for the work is recorded, and it is also
 possible to track the actual time and effort required.
diff -r 5f2995a96608 -r 76a0a505292f modules/project/message.xml
--- a/modules/project/message.xml       Wed Mar 11 19:04:22 2026 +0100
+++ b/modules/project/message.xml       Thu Mar 12 18:12:50 2026 +0100
@@ -3,6 +3,9 @@
 this repository contains the full copyright notices and license terms. -->
 <tryton>
     <data grouped="1">
+        <record model="ir.message" id="msg_work_number_unique">
+            <field name="text">The number on work must be unique.</field>
+        </record>
         <record model="ir.message" id="msg_work_invalid_progress_status">
             <field name="text">To set work "%(work)s" in "%(status)s" status, 
you must increase its progress up to at least %(progress)s.</field>
         </record>
diff -r 5f2995a96608 -r 76a0a505292f modules/project/tests/scenario_project.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/project/tests/scenario_project.rst        Thu Mar 12 18:12:50 
2026 +0100
@@ -0,0 +1,29 @@
+================
+Project Scenario
+================
+
+Imports::
+
+    >>> from proteus import Model
+    >>> from trytond.modules.company.tests.tools import create_company
+    >>> from trytond.tests.tools import activate_modules
+
+Activate project::
+
+    >>> config = activate_modules('project', create_company)
+
+    >>> Work = Model.get('project.work')
+
+Create a project with a task::
+
+    >>> project = Work(type='project', name="Project")
+    >>> task = project.children.new(type='task', name="Task")
+    >>> project.save()
+    >>> task, = project.children
+
+Check works have numbers::
+
+    >>> bool(project.number)
+    True
+    >>> bool(task.number)
+    True
diff -r 5f2995a96608 -r 76a0a505292f modules/project/tryton.cfg
--- a/modules/project/tryton.cfg        Wed Mar 11 19:04:22 2026 +0100
+++ b/modules/project/tryton.cfg        Thu Mar 12 18:12:50 2026 +0100
@@ -14,6 +14,8 @@
 
 [register]
 model:
+    work.Configuration
+    work.ConfigurationSequence
     # Before Work because status default value is read from WorkStatus
     work.WorkStatus
     work.Work
diff -r 5f2995a96608 -r 76a0a505292f 
modules/project/view/project_configuration_form.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/project/view/project_configuration_form.xml       Thu Mar 12 
18:12:50 2026 +0100
@@ -0,0 +1,7 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<form>
+    <label name="work_sequence"/>
+    <field name="work_sequence"/>
+</form>
diff -r 5f2995a96608 -r 76a0a505292f modules/project/view/work_form.xml
--- a/modules/project/view/work_form.xml        Wed Mar 11 19:04:22 2026 +0100
+++ b/modules/project/view/work_form.xml        Thu Mar 12 18:12:50 2026 +0100
@@ -3,9 +3,12 @@
 this repository contains the full copyright notices and license terms. -->
 <form col="6">
     <label name="name"/>
-    <field name="name" colspan="3"/>
+    <field name="name"/>
     <label name="parent"/>
     <field name="parent"/>
+    <label name="number"/>
+    <field name="number"/>
+
     <label name="type"/>
     <field name="type"/>
     <label name="company"/>
diff -r 5f2995a96608 -r 76a0a505292f modules/project/view/work_list.xml
--- a/modules/project/view/work_list.xml        Wed Mar 11 19:04:22 2026 +0100
+++ b/modules/project/view/work_list.xml        Thu Mar 12 18:12:50 2026 +0100
@@ -3,6 +3,7 @@
 this repository contains the full copyright notices and license terms. -->
 <tree sequence="sequence">
     <field name="company" expand="1" optional="1"/>
+    <field name="number"/>
     <field name="rec_name" expand="1"/>
     <field name="timesheet_duration" optional="0"/>
     <field name="total_effort" optional="0"/>
diff -r 5f2995a96608 -r 76a0a505292f modules/project/view/work_list_children.xml
--- a/modules/project/view/work_list_children.xml       Wed Mar 11 19:04:22 
2026 +0100
+++ b/modules/project/view/work_list_children.xml       Thu Mar 12 18:12:50 
2026 +0100
@@ -2,7 +2,9 @@
 <!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
 this repository contains the full copyright notices and license terms. -->
 <tree sequence="sequence">
-    <field name="name" expand="1"/>
+    <field name="name" expand="1">
+        <prefix name="number"/>
+    </field>
     <field name="timesheet_duration" optional="0"/>
     <field name="total_effort" optional="0"/>
     <field name="type" optional="1"/>
diff -r 5f2995a96608 -r 76a0a505292f modules/project/view/work_list_simple.xml
--- a/modules/project/view/work_list_simple.xml Wed Mar 11 19:04:22 2026 +0100
+++ b/modules/project/view/work_list_simple.xml Thu Mar 12 18:12:50 2026 +0100
@@ -3,6 +3,7 @@
 this repository contains the full copyright notices and license terms. -->
 <tree>
     <field name="company" expand="1" optional="1"/>
+    <field name="number"/>
     <field name="rec_name" expand="1"/>
     <field name="type" optional="1"/>
     <field name="status" optional="0"/>
diff -r 5f2995a96608 -r 76a0a505292f modules/project/view/work_tree.xml
--- a/modules/project/view/work_tree.xml        Wed Mar 11 19:04:22 2026 +0100
+++ b/modules/project/view/work_tree.xml        Thu Mar 12 18:12:50 2026 +0100
@@ -2,7 +2,9 @@
 <!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
 this repository contains the full copyright notices and license terms. -->
 <tree sequence="sequence">
-    <field name="name" expand="1"/>
+    <field name="name" expand="1">
+        <prefix name="number"/>
+    </field>
     <field name="company" expand="1" optional="1"/>
     <field name="timesheet_duration" optional="0"/>
     <field name="total_effort" optional="0"/>
diff -r 5f2995a96608 -r 76a0a505292f modules/project/view/work_tree_simple.xml
--- a/modules/project/view/work_tree_simple.xml Wed Mar 11 19:04:22 2026 +0100
+++ b/modules/project/view/work_tree_simple.xml Thu Mar 12 18:12:50 2026 +0100
@@ -2,7 +2,9 @@
 <!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
 this repository contains the full copyright notices and license terms. -->
 <tree sequence="sequence">
-    <field name="name" expand="1"/>
+    <field name="name" expand="1">
+        <prefix name="number"/>
+    </field>
     <field name="company" expand="1" optional="1"/>
     <field name="type" optional="1"/>
     <field name="status" optional="0"/>
diff -r 5f2995a96608 -r 76a0a505292f modules/project/work.py
--- a/modules/project/work.py   Wed Mar 11 19:04:22 2026 +0100
+++ b/modules/project/work.py   Thu Mar 12 18:12:50 2026 +0100
@@ -4,17 +4,58 @@
 import datetime
 from collections import defaultdict
 
+from sql import Null
+from sql.functions import CharLength
+
 from trytond.cache import Cache
 from trytond.i18n import gettext
 from trytond.model import (
-    ChatMixin, DeactivableMixin, Index, ModelSQL, ModelView, fields,
-    sequence_ordered, sum_tree, tree)
+    ChatMixin, DeactivableMixin, Index, ModelSingleton, ModelSQL, ModelView,
+    Unique, ValueMixin, fields, sequence_ordered, sum_tree, tree)
+from trytond.modules.company.model import CompanyMultiValueMixin
 from trytond.pool import Pool
-from trytond.pyson import Bool, Eval, If, PYSONEncoder, TimeDelta
+from trytond.pyson import Bool, Eval, Id, If, PYSONEncoder, TimeDelta
+from trytond.tools import is_full_text, lstrip_wildcard
 from trytond.transaction import Transaction
 
 from .exceptions import WorkProgressValidationError
 
+_work_sequence = fields.Many2One(
+    'ir.sequence', "Work Effort Sequence", required=True,
+    domain=[
+        ('sequence_type', '=', Id('project', 'sequence_type_work')),
+        ],
+    help="Used to generate the work number.")
+
+
+class Configuration(
+        ModelSingleton, ModelSQL, ModelView, CompanyMultiValueMixin):
+    __name__ = 'project.configuration'
+
+    work_sequence = fields.MultiValue(_work_sequence)
+
+    @classmethod
+    def multivalue_model(cls, field):
+        pool = Pool()
+        if field == 'work_sequence':
+            return pool.get('project.configuration.sequence')
+        return super().multivalue_model(field)
+
+    @classmethod
+    def default_work_sequence(cls, **pattern):
+        pool = Pool()
+        ModelData = pool.get('ir.model.data')
+        try:
+            return ModelData.get_id('project', 'sequence_work')
+        except KeyError:
+            return None
+
+
+class ConfigurationSequence(ModelSQL, ValueMixin):
+    __name__ = 'project.configuration.sequence'
+
+    work_sequence = _work_sequence
+
 
 class WorkStatus(DeactivableMixin, sequence_ordered(), ModelSQL, ModelView):
     __name__ = 'project.work.status'
@@ -113,6 +154,7 @@
             ],
         "Type", required=True)
     company = fields.Many2One('company.company', "Company", required=True)
+    number = fields.Char("Number", readonly=True)
     party = fields.Many2One('party.party', 'Party',
         states={
             'invisible': Eval('type') != 'project',
@@ -191,13 +233,25 @@
 
     @classmethod
     def __setup__(cls):
+        cls.number.search_unaccented = False
         cls.path.search_unaccented = False
         super().__setup__()
         t = cls.__table__()
+        cls._sql_constraints += [
+            ('number_unique', Unique(t, t.number),
+                'project.msg_work_number_unique'),
+            ]
         cls._sql_indexes.update({
+                Index(t, (t.number, Index.Equality(cardinality='high'))),
+                Index(t, (t.number, Index.Similarity(cardinality='high'))),
                 Index(t, (t.path, Index.Similarity(begin=True))),
                 })
 
+    @classmethod
+    def order_number(cls, tables):
+        table, _ = tables[None]
+        return [table.number != Null, CharLength(table.number), table.number]
+
     @staticmethod
     def default_type():
         return 'task'
@@ -404,6 +458,38 @@
         return language
 
     @classmethod
+    def search_rec_name(cls, name, clause):
+        _, operator, value = clause
+        if operator.startswith('!') or operator.startswith('not '):
+            bool_op = 'AND'
+        else:
+            bool_op = 'OR'
+        code_value = value
+        if operator.endswith('like') and is_full_text(value):
+            code_value = lstrip_wildcard(value)
+        domain = [bool_op,
+            ('number', operator, code_value),
+            ('name', operator, value),
+            ]
+        return domain
+
+    @classmethod
+    def _number_sequence(cls, **pattern):
+        pool = Pool()
+        Configuration = pool.get('project.configuration')
+        config = Configuration(1)
+        return config.get_multivalue('work_sequence', **pattern)
+
+    @classmethod
+    def preprocess_values(cls, mode, values):
+        values = super().preprocess_values(mode, values)
+        if mode == 'create':
+            if not values.get('number'):
+                if sequence := cls._number_sequence():
+                    values['number'] = sequence.get()
+        return values
+
+    @classmethod
     def copy(cls, project_works, default=None):
         pool = Pool()
         WorkStatus = pool.get('project.work.status')
@@ -411,6 +497,7 @@
             default = {}
         else:
             default = default.copy()
+        default.setdefault('number')
         default.setdefault('progress', None)
         default.setdefault(
             'status', lambda data: WorkStatus.get_default_status(data['type']))
diff -r 5f2995a96608 -r 76a0a505292f modules/project/work.xml
--- a/modules/project/work.xml  Wed Mar 11 19:04:22 2026 +0100
+++ b/modules/project/work.xml  Thu Mar 12 18:12:50 2026 +0100
@@ -3,6 +3,56 @@
 this repository contains the full copyright notices and license terms. -->
 <tryton>
     <data>
+        <record model="ir.ui.view" id="project_configuration_view_form">
+            <field name="model">project.configuration</field>
+            <field name="type">form</field>
+            <field name="name">project_configuration_form</field>
+        </record>
+
+        <record model="ir.action.act_window" id="act_project_configuration">
+            <field name="name">Configuration</field>
+            <field name="res_model">project.configuration</field>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_project_configuration_view1">
+            <field name="sequence" eval="10"/>
+            <field name="view" ref="project_configuration_view_form"/>
+            <field name="act_window" ref="act_project_configuration"/>
+        </record>
+        <menuitem
+            parent="menu_configuration"
+            action="act_project_configuration"
+            sequence="10"
+            id="menu_project_configuration"
+            icon="tryton-list"/>
+
+        <record model="ir.model.access" id="access_project_configuration">
+            <field name="model">project.configuration</field>
+            <field name="perm_read" eval="True"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+        <record model="ir.model.access" 
id="access_project_configuration_party_admin">
+            <field name="model">project.configuration</field>
+            <field name="group" ref="group_project_admin"/>
+            <field name="perm_read" eval="True"/>
+            <field name="perm_write" eval="True"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+
+        <record model="ir.sequence.type" id="sequence_type_work">
+            <field name="name">Project Work Effort</field>
+        </record>
+        <record model="ir.sequence.type-res.group" 
id="sequence_type_work_group_admin">
+            <field name="sequence_type" ref="sequence_type_work"/>
+            <field name="group" ref="res.group_admin"/>
+        </record>
+        <record model="ir.sequence.type-res.group" 
id="sequence_type_work_group_project_admin">
+            <field name="sequence_type" ref="sequence_type_work"/>
+            <field name="group" ref="group_project_admin"/>
+        </record>
+
         <record model="ir.ui.view" id="work_status_view_list">
             <field name="model">project.work.status</field>
             <field name="type">tree</field>
@@ -261,6 +311,15 @@
 
     </data>
     <data noupdate="1">
+        <record model="ir.sequence" id="sequence_work">
+            <field name="name">Work Effort</field>
+            <field name="sequence_type" ref="sequence_type_work"/>
+        </record>
+
+        <record model="project.configuration.sequence" 
id="project_configuration_sequence">
+            <field name="work_sequence" ref="sequence_work"/>
+        </record>
+
         <record model="project.work.status" id="work_open_status">
             <field name="name">Open</field>
             <field name="default" eval="True"/>

Reply via email to