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"/>