[ADD] project_timesheet, sale_service: support for project sub-tasks

This commit introduces "Sub-task", a concept remotely similar to
the "delegated tasks" available in version 7.0, but covering a
more specific and narrow scope.

Activating Sub-tasks
--------------------

Sub-tasks are made available within any project by configuring
a "sub-task project" in the project settings. This feature is
considered low-level and technical, and only accessible with
"Technical Features" enabled.
The "sub-task project" may be the project itself.

Using Sub-tasks
---------------

Conceptually, a sub-task represents the allocation of a part
of the scope of its parent task. Multiple sub-task levels
are possible.

Once activated in project settings, a new "Sub-tasks" smart button
will appear on the task form, giving the option to view and
create sub-tasks. In addition, a new field "Sub-tasks Hours" will
appear next to the other computed "Hours" fields, representing
the total hours allocated to sub-tasks.
(Technically, the sum of the maximum between the planned hours
and spent hours of all sub-tasks, with spent hours always used
when a task is closed)
These sub-task hours will be dynamically subtracted from the
remaining hours of the task, and will therefore affect the
task's progress as well.

Upon creation, a sub-task will inherit the link to the
original sales order and procurement from its parent task, and
will by default be assigned to the "sub-task project" set on
the project of the parent task.

Enhancement
-----------
The kanban view for tasks will now display a color-coded label
with the number of remaining hours, whenever the percentage
of remaining hours is below 20% (orange=20%-, red=1%-).
This commit is contained in:
Thibault Francois
2016-03-01 18:54:08 +01:00
committed by Olivier Dony
parent 9523d59fe3
commit 9eac32beea
6 changed files with 150 additions and 27 deletions
+3 -3
View File
@@ -283,14 +283,14 @@
<div class="oe_clear"/>
</page>
<page string="Extra Info">
<group col="4">
<group col="2">
<group>
<group>
<field name="sequence" groups="base.group_no_one"/>
<field name="partner_id"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
<field name="displayed_image_id" groups="base.group_no_one"/>
</group>
<group col="2">
<group>
<field name="date_assign" groups="base.group_no_one"/>
<field name="date_last_stage_update" groups="base.group_no_one"/>
</group>
+75 -22
View File
@@ -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'
@@ -10,6 +10,9 @@
<div name="button_box" position="inside">
<button class="oe_stat_button" name="open_timesheets" type="object" icon="fa-calendar" string="Timesheets"/>
</div>
<field name="user_id" position="after">
<field name="subtask_project_id" groups="base.group_no_one" />
</field>
</field>
</record>
@@ -50,11 +53,34 @@
</field>
</record>
<record id="act_project_task_sub_task" model="ir.actions.act_window">
<field name="name">Sub-tasks</field>
<field name="res_model">project.task</field>
<field name="view_mode">kanban,tree,form,calendar,pivot,graph</field>
<field name="domain">[('id', 'child_of', active_id), ('id', '!=', active_id)]</field>
<field name="context">
{
'default_parent_id' : active_id,
'default_project_id' : project_id,
'default_name' : name + ':',
'default_partner_id' : partner_id,
}
</field>
<field name="search_view_id" ref="project.view_task_search_form"/>
</record>
<record model="ir.ui.view" id="view_task_form2_inherited">
<field name="name">project.task.form.inherited</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_form2" />
<field name="arch" type="xml">
<button name="toggle_active" position="after">
<button name="%(act_project_task_sub_task)d" type="action" class="oe_stat_button" icon="fa-tasks"
attrs="{'invisible' : [('subtask_project_id', '=', False)]}"
context="{'project_id': subtask_project_id, 'name': name, 'partner_id': partner_id}" >
<field string="Sub-tasks" name="subtask_count" widget="statinfo"/>
</button>
</button>
<field name="project_id" position="attributes">
<attribute name="on_change">onchange_project(project_id)</attribute>
</field>
@@ -63,6 +89,11 @@
<field name="progress" widget="progressbar"
groups="project.group_time_work_estimation_tasks"/>
</field>
<field name="partner_id" position="after">
<field name="parent_id" attrs="{'invisible' : [('parent_id', '=', False)]}" />
<field name="child_ids" invisible="1" />
<field name="subtask_project_id" invisible="1" />
</field>
<xpath expr="//notebook/page[@name='description_page']" position="after">
<page string="Timesheets" groups="project.group_tasks_work_on_tasks,project.group_time_work_estimation_tasks">
<field name="timesheet_ids" groups="project.group_tasks_work_on_tasks" context="{'default_account_id' : analytic_account_id, 'default_is_timesheet' : 1}">
@@ -77,8 +108,9 @@
</field>
<group>
<group class="oe_subtotal_footer oe_right" name="project_hours" groups="project.group_time_work_estimation_tasks">
<field name="effective_hours" widget="float_time" groups="project.group_time_work_estimation_tasks"/>
<field name="remaining_hours" widget="float_time" class="oe_subtotal_footer_separator" groups="project.group_time_work_estimation_tasks"/>
<field name="effective_hours" widget="float_time" />
<field name="children_hours" widget="float_time" attrs="{'invisible' : [('children_hours', '=', 0.0)]}" />
<field name="remaining_hours" widget="float_time" class="oe_subtotal_footer_separator" />
</group>
</group>
</page>
@@ -113,7 +145,26 @@
<a name="open_timesheets" type="object">Timesheets</a>
</div>
</xpath>
</field>
</record>
<record id="view_task_kanban_inherited_progress" model="ir.ui.view">
<field name="name">project.task.timesheet.kanban.inherited.progress</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_kanban"/>
<field name="arch" type="xml">
<templates position="before">
<field name="progress" />
<field name="remaining_hours" />
</templates>
<div class="oe_kanban_bottom_left" position="inside">
<t t-if="record.progress.raw_value &gt; 80 and record.progress.raw_value &lt; 100">
<div t-att-class="'oe_kanban_align label label-' + (record.progress.raw_value &gt;= 99 ? 'danger': 'warning')"
groups="project.group_time_work_estimation_tasks" title="Remaining hours">
<field name="remaining_hours" widget="float_time" />
</div>
</t>
</div>
</field>
</record>
</data>
@@ -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'
@@ -37,8 +37,12 @@
string="Sales Order"/>
</xpath>
<field name="company_id" position="before">
<field name="procurement_id" groups="base.group_no_one"/>
<field name="sale_line_id" string="Order Line" groups="base.group_no_one"/>
</field>
<field name="parent_id" position="attributes">
<attribute name="on_change">onchange_parent_id(parent_id)</attribute>
</field>
</field>
</record>
<record id="view_order_form_inherit_sale_service" model="ir.ui.view">
@@ -96,6 +96,10 @@
.oe_kanban_bottom_left {
float: left;
.oe_kanban_align {
display: inline-block;
vertical-align: middle;
}
}
.o_kanban_tags {