From 00e126eef770f4c29791b77779e18a016e670c65 Mon Sep 17 00:00:00 2001 From: Bertrand Chenal Date: Tue, 26 May 2009 13:24:03 +0200 Subject: [PATCH] Improvements on work, project and task: * work: - added sequence - added One2Many to timesheet lines * project / task: - use type instead of project_type - added children, effort and total_effort - handling of the parent field at the project.work level --- work.py | 226 +++++++++++++++++++++++++++++++++++++++++++++++++++---- work.xml | 205 +++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 374 insertions(+), 57 deletions(-) diff --git a/work.py b/work.py index 9ff46327f1..766532dc5c 100644 --- a/work.py +++ b/work.py @@ -2,52 +2,246 @@ #this repository contains the full copyright notices and license terms. "Project" from trytond.model import ModelView, ModelSQL, fields +from trytond.model.modelstorage import OPERATORS import copy class TimesheetWork(ModelSQL, ModelView): _name = 'timesheet.work' + timesheet_lines = fields.One2Many('timesheet.line', 'work', + 'Timesheet Lines', + depends=['timesheet_available', 'active'], + states={ + 'invisible': 'not bool(timesheet_available)', + 'readonly': '''not active''' + }) + sequence = fields.Integer('Sequence') + def __init__(self): super(TimesheetWork, self).__init__() + self._order.insert(0, ('sequence', 'ASC')) + self.parent = copy.copy(self.parent) if not self.parent.context: - self.parent.context = "{'project_type': locals().get('project_type')}" - elif 'project_type' not in self.parent.context: + self.parent.context = "{'type': locals().get('type')}" + elif 'type' not in self.parent.context: sep = '' if self.parent.context[-2] != ',': sep = ',' self.parent.context = self.parent.context[:-1] + \ - sep + "'project_type': locals().get('project_type')" + \ + sep + "'type': locals().get('type')" + \ self.parent.context[-1:] + self._reset_columns() TimesheetWork() class Work(ModelSQL, ModelView): - 'Work' + 'Work Effort' _name = 'project.work' _description = __doc__ _inherits = {'timesheet.work': 'work'} - work = fields.Many2One('timesheet.work', 'Work', - required=True) - project_type = fields.Selection([ - ('project', 'Project'), - ], 'Type', required=True, select=1) + work = fields.Many2One('timesheet.work', 'Work', required=True, + ondelete='CASCADE') + type = fields.Selection([ + ('project', 'Project'), + ('task', 'Task') + ], + 'Type', required=True, select=1, + states={ + 'invisible': 'context.get("type", False)', + }) party = fields.Many2One('party.party', 'Party', states={ - 'invisible': "project_type != 'project'", - }) + 'invisible': "type!= 'project'" + }, depends=['type']) party_address = fields.Many2One('party.address', 'Contact Address', - domain="[('party', '=', party)]", + domain="["\ + "('party', '=', party)"\ + "]", states={ - 'invisible': "project_type != 'project'", - }) + 'invisible': "type != 'project'" + }, depends=['party', 'type']) + effort = fields.Float("Effort", + states={ + 'invisible': "type != 'task'" + }, depends=['type']) + total_effort = fields.Function('get_total_effort', type='float', + string='Total Effort', + states={ + 'invisible': "type != 'project'" + }, depends=['type']) comment = fields.Text('Comment') + parent = fields.Function('get_parent', fnct_inv='set_parent', + fnct_search='search_parent', string='Parent', + relation='project.work', type="many2one") + children = fields.One2Many('project.work', 'parent', 'Children') + state = fields.Selection([ + ('opened', 'Opened'), + ('done', 'Done'), + ], 'State', + states={ + 'invisible': "type != 'task'", + 'required': "type == 'task'", + }, select=1, depends=['type']) + + def default_type(self, cursor, user, context=None): + if context.get('type') == 'project': + return 'project' + return 'task' + + def default_state(self, cursor, user, context=None): + return 'opened' + + def __init__(self): + super(Work, self).__init__() + self._sql_constraints += [ + ('work_uniq', 'UNIQUE(work)', 'There should be only one '\ + 'timesheet work by task/project!'), + ] + self._order.insert(0, ('sequence', 'ASC')) + + def get_parent(self, cursor, user, ids, name, arg, context=None): + res = dict.fromkeys(ids, None) + cursor.execute( + 'SELECT child.id, parent.id '\ + 'FROM project_work child '\ + 'JOIN timesheet_work twc on (twc.id = child.work) '\ + 'JOIN timesheet_work twp on (twc.parent = twp.id) '\ + 'JOIN project_work parent on (twp.id = parent.work) '\ + 'WHERE child.id in (' + ','.join('%s' for i in ids) + ') ', + ids) + res.update(dict(cursor.fetchall())) + return res + + def set_parent(self, cursor, user, id, name, value, arg, context=None): + timesheet_work_obj = self.pool.get('timesheet.work') + if value: + child_project_work, parent_project_work = self.browse( + cursor, user, [id, value], context=context) + child_timesheet_work_id = child_project_work.work.id + parent_timesheet_work_id = parent_project_work.work.id + else: + child_project_work = self.browse( cursor, user, id, context=context) + child_timesheet_work_id = child_project_work.work.id + parent_timesheet_work_id = False + + timesheet_work_obj.write(cursor, user, child_timesheet_work_id, { + 'parent': parent_timesheet_work_id + }, + context=context) + + def search_parent(self, cursor, user, name, domain=None, context=None): + ids = domain[0][2] + clauses = [] + args = [] + for d in domain: + if d[1] in ('in', 'not in'): + clause = 'parent.id ' + d[1] + \ + '(' + ','.join('%s' for i in d[2]) + ')' + clauses.append(clause) + args.extend(d[2]) + + elif d[1] in ('child_of', 'not child_of'): + raise Exception('Domain not implemented') + + elif d[1] in ('=', '!=') and not d[2]: + op = d[1] == '=' and 'is' or 'is not' + clauses.append('twc.parent %s null' % op) + + elif d[1] in OPERATORS: + clauses.append('parent.id %s %%s' % d[1]) + args.append(d[2]) + + query = \ + 'SELECT child.id '\ + 'FROM project_work child '\ + 'JOIN timesheet_work twc on (twc.id = child.work) '\ + 'LEFT JOIN timesheet_work twp on (twc.parent = twp.id) '\ + 'LEFT JOIN project_work parent on (twp.id = parent.work) ' + + if clauses: + query = query + 'WHERE ' + ' AND '.join(clauses) + + cursor.execute(query, args) + + res = [('id', 'in', [x[0] for x in cursor.fetchall()])] + return res + + def get_total_effort(self, cursor, user, ids, name, arg, context=None): + timesheet_work_obj = self.pool.get('timesheet.work') + + projects = self.browse(cursor, user, ids, context=context) + w2p = dict((p.work.id, p.id) for p in projects) + + query_ids, args_ids = timesheet_work_obj.search(cursor, user, [ + ('parent', 'child_of', w2p.keys()), + ], context=context, query_string=True) + + cursor.execute( + 'SELECT task.work, ' \ + 'SUM(CASE WHEN task.type = \'task\' '\ + 'AND task.effort is not null '\ + 'THEN task.effort '\ + 'ELSE 0 END) '\ + 'FROM project_work task ' \ + 'WHERE task.work IN (' + query_ids + ') ' \ + 'GROUP BY task.work', args_ids) + work_effort = dict(cursor.fetchall()) + + works = timesheet_work_obj.browse( + cursor, user, work_effort.keys(), context=context) + id2work = {} + leafs = set() + for work in works: + id2work[work.id] = work + if not work.children: + leafs.add(work.id) + + while leafs: + parents = set() + for work_id in leafs: + work = id2work[work_id] + if work.parent and work.parent.id in work_effort: + work_effort[work.parent.id] += work_effort[work_id] + parents.add(work.parent.id) + leafs = parents + + return dict((w2p[w], e) for w,e in work_effort.iteritems() if w in w2p) + + + def delete(self, cursor, user, ids, context=None): + timesheet_work_obj = self.pool.get('timesheet.work') + + if isinstance(ids, (int, long)): + ids = [ids] + + # Get the timesheet works linked to the project works + project_works = self.browse(cursor, user, ids, context=context) + timesheet_work_ids = [pw.work.id for pw in project_works] + + res = super(Work, self).delete(cursor, user, ids, context=context) + + # check if timesheet works are linked to project works + project_work_ids = self.search(cursor, user, [ + ('work', 'in', timesheet_work_ids) + ], + context=context) + if project_work_ids: + project_works = self.browse( + cursor, user, project_work_ids, context=context) + + keep_ids = set(pw.work.id for pw in project_works) + timesheet_work_ids = [ + i for i in timesheet_work_ids if i not in keep_ids] + + timesheet_work_obj.delete( + cursor, user, timesheet_work_ids, context=context) + return res - def default_project_type(self, cursor, user, context=None): - return 'project' Work() + diff --git a/work.xml b/work.xml index a56d55533b..65cef18b52 100644 --- a/work.xml +++ b/work.xml @@ -3,56 +3,119 @@ this repository contains the full copyright notices and license terms. --> + + project.work + tree + children + + + + + + + + + + ]]> + + + + project.work tree - - + + + + ]]> + + + project.work + tree + + + + + + + + ]]> + + project.work form - - Works + Works Efforts project.work form @@ -71,6 +134,90 @@ this repository contains the full copyright notices and license terms. --> + + Works + project.work + tree + [('parent', '=', False)] + + + + + + + + + + Projects + project.work + form + [('type', '=', 'project')] + {'type': 'project'} + + + + + + + + + + + + + + + Tasks + project.work + form + [('type', '=', 'task')] + {'type': 'task'} + + + + + + + + + + + + + + + Tasks + project.work + form + [('parent', '=', active_id), ('type', '=', 'task')] + + + form_relate + project.work,0 + + + + + + + + + + + + + @@ -87,29 +234,5 @@ this repository contains the full copyright notices and license terms. --> - - - - Projects - project.work - form - [('project_type', '=', 'project')] - - - - - - - - - - - - -