Add number to the project work efforts

Closes #14671
This commit is contained in:
Cédric Krier
2026-03-12 18:12:50 +01:00
parent 3b292c46b7
commit 75159112f6
14 changed files with 208 additions and 9 deletions
+1
View File
@@ -1,3 +1,4 @@
* Add number to the work efforts
* Add support for Python 3.14
* Remove support for Python 3.9
+2 -2
View File
@@ -13,8 +13,8 @@ Work Effort
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.
+3
View File
@@ -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>
@@ -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
+2
View File
@@ -14,6 +14,8 @@ xml:
[register]
model:
work.Configuration
work.ConfigurationSequence
# Before Work because status default value is read from WorkStatus
work.WorkStatus
work.Work
@@ -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>
+4 -1
View File
@@ -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"/>
+1
View File
@@ -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"/>
+3 -1
View File
@@ -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"/>
@@ -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"/>
+3 -1
View File
@@ -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"/>
+3 -1
View File
@@ -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"/>
+90 -3
View File
@@ -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 @@ class Work(
],
"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 @@ class Work(
@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'
@@ -403,6 +457,38 @@ class Work(
language = party.lang.code if party and party.lang else None
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()
@@ -411,6 +497,7 @@ class Work(
default = {}
else:
default = default.copy()
default.setdefault('number')
default.setdefault('progress', None)
default.setdefault(
'status', lambda data: WorkStatus.get_default_status(data['type']))
+59
View File
@@ -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 @@ this repository contains the full copyright notices and license terms. -->
</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"/>