Files
tryton/work.py
T

406 lines
15 KiB
Python
Raw Normal View History

2014-12-02 12:55:41 +01:00
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
2015-09-24 15:46:08 +02:00
2015-02-09 23:02:30 +01:00
import datetime
from sql import Null
2018-05-12 00:50:28 +02:00
from trytond.model import ModelView, ModelSQL, fields, sequence_ordered, tree
2011-08-21 11:20:48 +02:00
from trytond.pyson import Eval
2010-08-19 22:16:58 +02:00
from trytond.transaction import Transaction
2011-07-03 13:19:05 +02:00
from trytond.pool import Pool
2015-09-17 11:18:56 +02:00
from trytond.tools import reduce_ids, grouped_slice
2012-09-24 22:18:06 +02:00
__all__ = ['Work']
2018-05-12 00:50:28 +02:00
class Work(sequence_ordered(), tree(separator='\\'), ModelSQL, ModelView):
2009-05-26 13:24:03 +02:00
'Work Effort'
2012-09-24 22:18:06 +02:00
__name__ = 'project.work'
2015-09-07 14:00:31 +02:00
name = fields.Char('Name', required=True, select=True)
2009-05-26 13:24:03 +02:00
type = fields.Selection([
('project', 'Project'),
('task', 'Task')
],
'Type', required=True, select=True)
2015-09-07 14:00:31 +02:00
company = fields.Many2One('company.company', 'Company', required=True,
select=True)
2008-10-22 20:11:42 +02:00
party = fields.Many2One('party.party', 'Party',
2011-08-21 11:20:48 +02:00
states={
'invisible': Eval('type') != 'project',
2009-05-26 13:24:03 +02:00
}, depends=['type'])
2008-10-22 20:11:42 +02:00
party_address = fields.Many2One('party.address', 'Contact Address',
2011-08-21 11:20:48 +02:00
domain=[('party', '=', Eval('party'))],
states={
'invisible': Eval('type') != 'project',
2009-05-26 13:24:03 +02:00
}, depends=['party', 'type'])
timesheet_works = fields.One2Many(
'timesheet.work', 'origin', 'Timesheet Works', readonly=True, size=1)
2013-03-20 22:50:59 +01:00
timesheet_available = fields.Function(
2014-01-26 14:29:25 +01:00
fields.Boolean('Available on timesheets'),
'get_timesheet_available', setter='set_timesheet_available')
timesheet_start_date = fields.Function(fields.Date('Timesheet Start',
states={
'invisible': ~Eval('timesheet_available'),
},
depends=['timesheet_available']),
'get_timesheet_date', setter='set_timesheet_date')
timesheet_end_date = fields.Function(fields.Date('Timesheet End',
states={
'invisible': ~Eval('timesheet_available'),
},
depends=['timesheet_available']),
'get_timesheet_date', setter='set_timesheet_date')
2015-02-09 23:02:30 +01:00
timesheet_duration = fields.Function(fields.TimeDelta('Duration',
2015-09-17 09:29:03 +02:00
'company_work_time',
help="Total time spent on this work and the sub-works"),
2015-09-17 11:18:56 +02:00
'get_total')
2015-02-09 23:02:30 +01:00
effort_duration = fields.TimeDelta('Effort', 'company_work_time',
2017-03-13 22:58:34 +01:00
help="Estimated Effort for this work")
2015-02-09 23:02:30 +01:00
total_effort = fields.Function(fields.TimeDelta('Total Effort',
'company_work_time',
help="Estimated total effort for this work and the sub-works"),
2015-09-17 11:18:56 +02:00
'get_total')
2015-09-24 15:46:08 +02:00
progress = fields.Float('Progress',
domain=['OR',
('progress', '=', None),
[
('progress', '>=', 0),
('progress', '<=', 1),
],
],
help='Estimated progress for this work')
total_progress = fields.Function(fields.Float('Total Progress',
2015-11-10 13:03:57 +01:00
digits=(16, 4),
2015-09-24 15:46:08 +02:00
help='Estimated total progress for this work and the sub-works',
states={
'invisible': Eval('total_progress', None) == None,
}),
'get_total')
2008-06-03 17:46:57 +02:00
comment = fields.Text('Comment')
2015-09-07 14:00:31 +02:00
parent = fields.Many2One('project.work', 'Parent',
left='left', right='right', ondelete='RESTRICT',
domain=[
('company', '=', Eval('company', -1)),
],
2017-07-10 23:55:13 +02:00
depends=['company'])
2015-09-07 14:00:31 +02:00
left = fields.Integer('Left', required=True, select=True)
right = fields.Integer('Right', required=True, select=True)
children = fields.One2Many('project.work', 'parent', 'Children',
domain=[
('company', '=', Eval('company', -1)),
],
depends=['company'])
2009-05-26 13:24:03 +02:00
state = fields.Selection([
('opened', 'Opened'),
('done', 'Done'),
], 'State', required=True, select=True)
2009-05-26 13:24:03 +02:00
2012-09-24 22:18:06 +02:00
@staticmethod
def default_type():
2009-05-26 13:24:03 +02:00
return 'task'
2015-09-07 14:00:31 +02:00
@classmethod
def default_company(cls):
return Transaction().context.get('company')
2012-09-24 22:18:06 +02:00
@staticmethod
def default_state():
2009-05-26 13:24:03 +02:00
return 'opened'
2015-09-07 14:00:31 +02:00
@classmethod
def default_left(cls):
return 0
@classmethod
def default_right(cls):
return 0
2012-09-24 22:18:06 +02:00
@classmethod
def __register__(cls, module_name):
TimesheetWork = Pool().get('timesheet.work')
cursor = Transaction().connection.cursor()
2018-07-24 22:01:08 +02:00
table_project_work = cls.__table_handler__(module_name)
2015-02-09 23:02:30 +01:00
project = cls.__table__()
timesheet = TimesheetWork.__table__()
work_exist = table_project_work.column_exist('work')
add_parent = (not table_project_work.column_exist('parent')
and work_exist)
add_company = (not table_project_work.column_exist('company')
and work_exist)
add_name = (not table_project_work.column_exist('name')
and work_exist)
2012-09-24 22:18:06 +02:00
super(Work, cls).__register__(module_name)
2015-02-09 23:02:30 +01:00
# Migration from 3.4: change effort into timedelta effort_duration
if table_project_work.column_exist('effort'):
cursor.execute(*project.select(project.id, project.effort,
where=project.effort != Null))
for id_, effort in cursor.fetchall():
duration = datetime.timedelta(hours=effort)
cursor.execute(*project.update(
[project.effort_duration],
[duration],
where=project.id == id_))
table_project_work.drop_column('effort')
2015-09-07 14:00:31 +02:00
# Migration from 3.6: add parent, company, drop required on work,
# fill name
if add_parent:
second_project = cls.__table__()
query = project.join(timesheet,
condition=project.work == timesheet.id
).join(second_project,
2015-09-16 17:42:08 +02:00
condition=timesheet.parent == second_project.work
2015-09-07 14:00:31 +02:00
).select(project.id, second_project.id)
cursor.execute(*query)
for id_, parent in cursor.fetchall():
cursor.execute(*project.update(
[project.parent],
[parent],
where=project.id == id_))
cls._rebuild_tree('parent', None, 0)
if add_company:
cursor.execute(*project.join(timesheet,
condition=project.work == timesheet.id
).select(project.id, timesheet.company))
for id_, company in cursor.fetchall():
cursor.execute(*project.update(
[project.company],
[company],
where=project.id == id_))
table_project_work.not_null_action('work', action='remove')
if add_name:
cursor.execute(*project.join(timesheet,
condition=project.work == timesheet.id
).select(project.id, timesheet.name))
for id_, name in cursor.fetchall():
cursor.execute(*project.update(
[project.name],
[name],
where=project.id == id_))
# Migration from 4.0: remove work
if work_exist:
table_project_work.drop_constraint('work_uniq')
update = Transaction().connection.cursor()
cursor.execute(*project.select(project.id, project.work,
where=project.work != Null))
for project_id, work_id in cursor:
update.execute(*timesheet.update(
[timesheet.origin, timesheet.name],
['%s,%s' % (cls.__name__, project_id), Null],
where=timesheet.id == work_id))
2018-03-15 00:45:43 +01:00
table_project_work.drop_column('work')
2012-09-24 22:18:06 +02:00
@classmethod
def __setup__(cls):
super(Work, cls).__setup__()
cls._error_messages.update({
'invalid_parent_state': ('Work "%(child)s" can not be opened '
2013-04-30 09:32:18 +02:00
'because its parent work "%(parent)s" is already done.'),
'invalid_children_state': ('Work "%(parent)s" can not be '
'done because its child work "%(child)s" is still '
2013-04-30 09:32:18 +02:00
'opened.'),
})
2018-04-05 19:19:36 +02:00
@classmethod
def index_set_field(cls, name):
index = super(Work, cls).index_set_field(name)
if name in {'timesheet_start_date', 'timesheet_end_date'}:
index = cls.index_set_field('timesheet_available') + 1
return index
@classmethod
def validate(cls, works):
super(Work, cls).validate(works)
for work in works:
work.check_state()
2012-09-24 22:18:06 +02:00
def check_state(self):
if (self.state == 'opened'
and (self.parent and self.parent.state == 'done')):
self.raise_user_error('invalid_parent_state', {
'child': self.rec_name,
'parent': self.parent.rec_name,
})
if self.state == 'done':
for child in self.children:
if child.state == 'opened':
self.raise_user_error('invalid_children_state', {
'parent': self.rec_name,
'child': child.rec_name,
})
2009-05-26 13:24:03 +02:00
@property
def effort_hours(self):
if not self.effort_duration:
return 0
return self.effort_duration.total_seconds() / 60 / 60
2015-09-24 15:46:08 +02:00
@property
def total_effort_hours(self):
if not self.total_effort:
return 0
return self.total_effort.total_seconds() / 60 / 60
2015-09-17 09:29:03 +02:00
@property
def timesheet_duration_hours(self):
if not self.timesheet_duration:
return 0
return self.timesheet_duration.total_seconds() / 60 / 60
@classmethod
def default_timesheet_available(cls):
return False
2013-03-20 22:50:59 +01:00
def get_timesheet_available(self, name):
return bool(self.timesheet_works)
@classmethod
def set_timesheet_available(cls, projects, name, value):
pool = Pool()
Timesheet = pool.get('timesheet.work')
to_create = []
to_delete = []
for project in projects:
if not project.timesheet_works and value:
to_create.append({
'origin': str(project),
'company': project.company.id,
})
elif project.timesheet_works and not value:
to_delete.extend(project.timesheet_works)
if to_create:
Timesheet.create(to_create)
if to_delete:
Timesheet.delete(to_delete)
def get_timesheet_date(self, name):
if self.timesheet_works:
func = {
'timesheet_start_date': min,
'timesheet_end_date': max,
}[name]
return func(getattr(w, name) for w in self.timesheet_works)
@classmethod
def set_timesheet_date(cls, projects, name, value):
pool = Pool()
Timesheet = pool.get('timesheet.work')
timesheets = [w for p in projects for w in p.timesheet_works]
if timesheets:
Timesheet.write(timesheets, {
name: value,
})
2013-03-20 22:50:59 +01:00
2015-09-17 09:29:03 +02:00
@classmethod
2015-09-17 11:18:56 +02:00
def sum_tree(cls, works, values, parents):
result = values.copy()
2013-08-21 18:39:32 +02:00
works = set((w.id for w in works))
2018-06-26 19:11:50 +02:00
leafs = works - set(parents.values())
2013-03-22 17:46:42 +01:00
while leafs:
for work in leafs:
works.remove(work)
2013-08-21 18:39:32 +02:00
parent = parents.get(work)
if parent in result:
result[parent] += result[work]
next_leafs = set(works)
2013-03-22 17:46:42 +01:00
for work in works:
2013-08-21 18:39:32 +02:00
parent = parents.get(work)
if not parent:
2013-03-22 17:46:42 +01:00
continue
2013-08-21 18:39:32 +02:00
if parent in next_leafs and parent in works:
next_leafs.remove(parent)
2013-03-22 17:46:42 +01:00
leafs = next_leafs
return result
2012-09-24 22:18:06 +02:00
@classmethod
2015-09-17 11:18:56 +02:00
def get_total(cls, works, names):
cursor = Transaction().connection.cursor()
2015-09-17 11:18:56 +02:00
table = cls.__table__()
2012-10-17 16:29:21 +02:00
works = cls.search([
2012-09-24 22:18:06 +02:00
('parent', 'child_of', [w.id for w in works]),
2015-09-07 14:00:31 +02:00
])
2015-09-17 11:18:56 +02:00
work_ids = [w.id for w in works]
parents = {}
for sub_ids in grouped_slice(work_ids):
where = reduce_ids(table.id, sub_ids)
cursor.execute(*table.select(table.id, table.parent,
where=where))
parents.update(cursor.fetchall())
2015-09-24 15:46:08 +02:00
if 'total_progress' in names and 'total_effort' not in names:
names = list(names)
names.append('total_effort')
2015-09-17 11:18:56 +02:00
result = {}
for name in names:
values = getattr(cls, '_get_%s' % name)(works)
result[name] = cls.sum_tree(works, values, parents)
2015-09-24 15:46:08 +02:00
if 'total_progress' in names:
2015-11-10 13:03:57 +01:00
digits = cls.total_progress.digits[1]
2015-09-24 15:46:08 +02:00
total_progress = result['total_progress']
total_effort = result['total_effort']
for work in works:
if total_effort[work.id]:
2015-11-10 13:03:57 +01:00
total_progress[work.id] = round(total_progress[work.id] /
(total_effort[work.id].total_seconds() / 60 / 60),
digits)
2015-09-24 15:46:08 +02:00
else:
total_effort[work.id] = None
2015-09-17 11:18:56 +02:00
return result
@classmethod
def _get_total_effort(cls, works):
return {w.id: w.effort_duration or datetime.timedelta() for w in works}
@classmethod
def _get_timesheet_duration(cls, works):
durations = {}
for work in works:
value = datetime.timedelta()
for timesheet_work in work.timesheet_works:
if timesheet_work.duration:
value += timesheet_work.duration
durations[work.id] = value
return durations
2009-05-26 13:24:03 +02:00
2015-09-24 15:46:08 +02:00
@classmethod
def _get_total_progress(cls, works):
return {w.id: w.effort_hours * (w.progress or 0) for w in works}
2012-09-24 22:18:06 +02:00
@classmethod
def copy(cls, project_works, default=None):
2011-03-29 16:09:59 +02:00
if default is None:
default = {}
2018-09-01 01:02:03 +02:00
else:
default = default.copy()
default.setdefault('children', None)
return super().copy(project_works, default=default)
2009-05-26 13:24:03 +02:00
2012-09-24 22:18:06 +02:00
@classmethod
def delete(cls, project_works):
TimesheetWork = Pool().get('timesheet.work')
2009-05-26 13:24:03 +02:00
# Get the timesheet works linked to the project works
timesheet_works = [
w for pw in project_works for w in pw.timesheet_works]
2009-05-26 13:24:03 +02:00
2012-09-24 22:18:06 +02:00
super(Work, cls).delete(project_works)
2009-05-26 13:24:03 +02:00
if timesheet_works:
with Transaction().set_context(_check_access=False):
TimesheetWork.delete(timesheet_works)
2014-02-20 23:29:11 +01:00
@classmethod
def search_global(cls, text):
2015-02-04 13:38:28 +01:00
for record, rec_name, icon in super(Work, cls).search_global(text):
2014-02-20 23:29:11 +01:00
icon = icon or 'tryton-project'
2015-02-04 13:38:28 +01:00
yield record, rec_name, icon