diff --git a/addons/project/project_view.xml b/addons/project/project_view.xml index 1a9fb973655..c97e5e35471 100644 --- a/addons/project/project_view.xml +++ b/addons/project/project_view.xml @@ -283,14 +283,14 @@
- - + + - + diff --git a/addons/project_timesheet/project_timesheet.py b/addons/project_timesheet/project_timesheet.py index 54a4a2a3b84..65f840275ca 100644 --- a/addons/project_timesheet/project_timesheet.py +++ b/addons/project_timesheet/project_timesheet.py @@ -13,6 +13,13 @@ from openerp.exceptions import UserError class project_project(osv.osv): _inherit = 'project.project' + _columns = { + 'subtask_project_id': fields.many2one( + 'project.project', string='Sub-task Project', + help="Choosing a sub-tasks project will both enable sub-tasks and set their default project (possibly the project itself)", + ondelete="restrict"), + } + def open_timesheets(self, cr, uid, ids, context=None): """ open Timesheets view """ mod_obj = self.pool.get('ir.model.data') @@ -49,28 +56,63 @@ class task(osv.osv): # Compute: effective_hours, total_hours, progress def _hours_get(self, cr, uid, ids, field_names, args, context=None): + def _get_child_hour(child, res): + if child.stage_id and child.stage_id.fold: + return res[child.id]['effective_hours'] + res[child.id]['children_hours'] + else: + return max(child.planned_hours, res[child.id]['effective_hours'] + res[child.id]['children_hours']) + res = {} - for task in self.browse(cr, uid, ids, context=context): - res[task.id] = {'effective_hours': 0.0, 'remaining_hours': task.planned_hours, 'progress': 0.0, 'total_hours': task.planned_hours, 'delay_hours': 0.0} - tasks_data = self.pool['account.analytic.line'].read_group(cr, uid, [('task_id', 'in', ids)], ['task_id','unit_amount'], ['task_id'], context=context) - for data in tasks_data: - task = self.browse(cr, uid, data['task_id'][0], context=context) - res[data['task_id'][0]] = {'effective_hours': data.get('unit_amount', 0.0), 'remaining_hours': task.planned_hours - data.get('unit_amount', 0.0)} - res[data['task_id'][0]]['total_hours'] = res[data['task_id'][0]]['remaining_hours'] + data.get('unit_amount', 0.0) - res[data['task_id'][0]]['delay_hours'] = res[data['task_id'][0]]['total_hours'] - task.planned_hours - res[data['task_id'][0]]['progress'] = 0.0 - if (task.planned_hours > 0.0 and data.get('unit_amount', 0.0)): - res[data['task_id'][0]]['progress'] = round(min(100.0 * data.get('unit_amount', 0.0) / task.planned_hours, 99.99),2) + task_to_read = self.search(cr, uid, [('id', 'child_of', ids)]) + task_to_sort = {} + for task in self.browse(cr, uid, task_to_read, context=context): + res[task.id] = { + 'effective_hours': 0.0, + 'remaining_hours': task.planned_hours, + 'progress': 0.0, + 'total_hours': task.planned_hours, + 'delay_hours': 0.0, + 'children_hours' : 0.0 + } + task_to_sort[task.id] = [child.id for child in task.child_ids] + + tasks_data = self.pool['account.analytic.line'].read_group( + cr, uid, [('task_id', 'in', task_to_read)], ['task_id','unit_amount'], ['task_id'], context=context) + tasks_data_dict = {data['task_id'][0] : data for data in tasks_data} + for task_id in tools.topological_sort(task_to_sort): + task = self.browse(cr, uid, task_id, context=context) + values = { + 'effective_hours': tasks_data_dict.get(task_id, {}).get('unit_amount', 0.0), + 'children_hours' : sum([_get_child_hour(child, res) for child in task.child_ids]) + } + values['remaining_hours'] = task.planned_hours - values['effective_hours'] - values['children_hours'] + values['total_hours'] = values['remaining_hours'] + values['effective_hours'] + values['delay_hours'] = values['total_hours'] - task.planned_hours + values['progress'] = 0.0 # TDE CHECK: if task.state in ('done','cancelled'): if task.stage_id and task.stage_id.fold: - res[data['task_id'][0]]['progress'] = 100.0 + values['progress'] = 100.0 + elif task.planned_hours > 0.0: + values['progress'] = round(min(100.0 * (values['effective_hours'] + values['children_hours']) / task.planned_hours, 99.99),2) + res[task.id] = values + return {k : val for k, val in res.iteritems() if k in ids} #Filter only needed result + + def _get_subtask_count(self, cr, uid, ids, field_names, args, context=None): + res = dict.fromkeys(ids, 0) + for task_id in ids: + res[task_id] = (self.search_count( + cr, uid, [('id', 'child_of', task_id)], context=context) - 1) # don't count task itself return res - def _get_task(self, cr, uid, id, context=None): + def _get_task(self, cr, uid, ids, context=None): res = [] - for line in self.pool.get('account.analytic.line').search_read(cr,uid,[('task_id', '!=', False),('id','in',id)], context=context): + for line in self.pool['account.analytic.line'].search_read( + cr, uid, [('task_id', '!=', False), ('id', 'in', ids)], context=context): res.append(line['task_id'][0]) - return res + return self.pool['project.task'].search(cr, uid, [('id', 'parent_of', res)]) + + def _get_parent_tasks(self, cr, uid, ids, context=None): + return self.pool['project.task'].search(cr, uid, [('id', 'parent_of', ids)]) def _get_total_hours(self): return super(task, self)._get_total_hours() + self.effective_hours @@ -78,38 +120,50 @@ class task(osv.osv): _columns = { 'remaining_hours': fields.function(_hours_get, string='Remaining Hours', multi='line_id', help="Total remaining time, can be re-estimated periodically by the assignee of the task.", store = { - 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['timesheet_ids', 'remaining_hours', 'planned_hours'], 10), + 'project.task': (_get_parent_tasks, ['timesheet_ids', 'remaining_hours', 'planned_hours', 'parent_id', 'stage_id'], 10), + 'account.analytic.line': (_get_task, ['task_id', 'unit_amount'], 10), + }), + 'children_hours': fields.function(_hours_get, string='Sub-tasks Hours', type='float', multi='line_id', + help="Sum of the planned hours of all sub-tasks (when a sub-task is closed or its spent hours exceed its planned hours, spent hours are counted instead)", + store = { + 'project.task': (_get_parent_tasks, ['timesheet_ids', 'remaining_hours', 'planned_hours', 'parent_id', 'stage_id'], 10), 'account.analytic.line': (_get_task, ['task_id', 'unit_amount'], 10), }), 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='line_id', help="Computed using the sum of the task work done.", store = { - 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['timesheet_ids', 'remaining_hours', 'planned_hours'], 10), + 'project.task': (_get_parent_tasks, ['timesheet_ids', 'remaining_hours', 'planned_hours', 'parent_id', 'stage_id'], 10), 'account.analytic.line': (_get_task, ['task_id', 'unit_amount'], 10), }), 'total_hours': fields.function(_hours_get, string='Total', multi='line_id', help="Computed as: Time Spent + Remaining Time.", store = { - 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['timesheet_ids', 'remaining_hours', 'planned_hours'], 10), + 'project.task': (_get_parent_tasks, ['timesheet_ids', 'remaining_hours', 'planned_hours', 'parent_id', 'stage_id'], 10), 'account.analytic.line': (_get_task, ['task_id', 'unit_amount'], 10), }), 'progress': fields.function(_hours_get, string='Working Time Progress (%)', multi='line_id', group_operator="avg", help="If the task has a progress of 99.99% you should close the task if it's finished or reevaluate the time", store = { - 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['timesheet_ids', 'remaining_hours', 'planned_hours', 'state', 'stage_id'], 10), + 'project.task': (_get_parent_tasks, ['timesheet_ids', 'remaining_hours', 'planned_hours', 'parent_id', 'state', 'stage_id'], 10), 'account.analytic.line': (_get_task, ['task_id', 'unit_amount'], 10), }), 'delay_hours': fields.function(_hours_get, string='Delay Hours', multi='line_id', help="Computed as difference between planned hours by the project manager and the total hours of the task.", store = { - 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['timesheet_ids', 'remaining_hours', 'planned_hours'], 10), + 'project.task': (_get_parent_tasks, ['timesheet_ids', 'remaining_hours', 'planned_hours','parent_id', 'stage_id'], 10), 'account.analytic.line': (_get_task, ['task_id', 'unit_amount'], 10), - }), + }), 'timesheet_ids': fields.one2many('account.analytic.line', 'task_id', 'Timesheets'), 'analytic_account_id': fields.related('project_id', 'analytic_account_id', type='many2one', relation='account.analytic.account', string='Analytic Account', store=True), + 'parent_id' : fields.many2one('project.task', string='Parent Task', select=True), + 'child_ids' : fields.one2many('project.task', 'parent_id', string="Sub-tasks"), + 'subtask_project_id': fields.related('project_id', 'subtask_project_id', type='many2one', relation='project.project', string='Sub-task Project'), + 'subtask_count' : fields.function(_get_subtask_count, type='integer', string="Sub-task count"), } _defaults = { 'progress': 0, } + _constraints = [(osv.osv._check_recursion, 'Circular references are not permitted between tasks and sub-tasks', ['parent_id'])] + def _prepare_delegate_values(self, cr, uid, ids, delegate_data, context=None): vals = super(task, self)._prepare_delegate_values(cr, uid, ids, delegate_data, context) for task in self.browse(cr, uid, ids, context=context): @@ -125,7 +179,6 @@ class task(osv.osv): project = self.pool['project.project'].browse(cr, uid, project_id, context=context) return result - class res_partner(osv.osv): _inherit = 'res.partner' diff --git a/addons/project_timesheet/project_timesheet_view.xml b/addons/project_timesheet/project_timesheet_view.xml index d3f1ffaed30..c8d9ef4ce84 100644 --- a/addons/project_timesheet/project_timesheet_view.xml +++ b/addons/project_timesheet/project_timesheet_view.xml @@ -10,6 +10,9 @@
+ + + @@ -50,11 +53,34 @@ + + Sub-tasks + project.task + kanban,tree,form,calendar,pivot,graph + [('id', 'child_of', active_id), ('id', '!=', active_id)] + + { + 'default_parent_id' : active_id, + 'default_project_id' : project_id, + 'default_name' : name + ':', + 'default_partner_id' : partner_id, + } + + + + project.task.form.inherited project.task + + onchange_project(project_id) @@ -63,6 +89,11 @@ + + + + + @@ -77,8 +108,9 @@ - - + + + @@ -113,7 +145,26 @@ Timesheets
+ + + + project.task.timesheet.kanban.inherited.progress + project.task + + + + + + +
+ +
+ +
+
+
diff --git a/addons/sale_service/models/sale_service.py b/addons/sale_service/models/sale_service.py index bcaabaaf20e..e6343e940dc 100644 --- a/addons/sale_service/models/sale_service.py +++ b/addons/sale_service/models/sale_service.py @@ -118,6 +118,17 @@ class project_task(osv.osv): "context": {"create": False, "show_sale": True}, } + def onchange_parent_id(self, cr, uid, ids, parent_id, context=None): + if not parent_id: + return {'value' : {'procurement_id': False, 'sale_line_id': False }} + parent_task = self.browse(cr, uid, parent_id, context=context) + return { + 'value' : { + 'procurement_id' : parent_task.procurement_id.id, + 'sale_line_id' : parent_task.sale_line_id.id, + } + } + class ProductProduct(models.Model): _inherit = 'product.product' diff --git a/addons/sale_service/views/sale_service_view.xml b/addons/sale_service/views/sale_service_view.xml index e097b02d92b..3a49020d835 100644 --- a/addons/sale_service/views/sale_service_view.xml +++ b/addons/sale_service/views/sale_service_view.xml @@ -37,8 +37,12 @@ string="Sales Order"/> + + + onchange_parent_id(parent_id) + diff --git a/addons/web_kanban/static/src/less/kanban_view.less b/addons/web_kanban/static/src/less/kanban_view.less index fbbcc92365f..ddd7f6374d1 100644 --- a/addons/web_kanban/static/src/less/kanban_view.less +++ b/addons/web_kanban/static/src/less/kanban_view.less @@ -96,6 +96,10 @@ .oe_kanban_bottom_left { float: left; + .oe_kanban_align { + display: inline-block; + vertical-align: middle; + } } .o_kanban_tags {