[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%-).
Notes
-----------
This is a forward port of 43c42b304d
Since hr_timesheet has been migrated to the new API, the forward port changes mostly consists in migrating the sub-tasks system to the new API as well.
This commit is contained in:
committed by
Tanguy Charlier
parent
7c49782fea
commit
e6ac7a7d25
@@ -2,6 +2,14 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from openerp import models, fields, api
|
||||
from openerp.osv import osv
|
||||
|
||||
|
||||
class Project(models.Model):
|
||||
_inherit = "project.project"
|
||||
|
||||
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")
|
||||
|
||||
|
||||
class Task(models.Model):
|
||||
@@ -10,18 +18,31 @@ class Task(models.Model):
|
||||
def _get_total_hours(self):
|
||||
return super(Task, self)._get_total_hours() + self.effective_hours
|
||||
|
||||
@api.depends('timesheet_ids', 'timesheet_ids.unit_amount', 'planned_hours')
|
||||
@api.multi
|
||||
def _get_subtask_count(self):
|
||||
for task in self:
|
||||
task.subtask_count = self.search_count([('id', 'child_of', task.id)]) - 1 # Don't count task itself
|
||||
|
||||
@api.depends('timesheet_ids.unit_amount', 'planned_hours', 'child_ids.stage_id',
|
||||
'child_ids.planned_hours', 'child_ids.effective_hours', 'child_ids.children_hours')
|
||||
def _hours_get(self):
|
||||
for task in self:
|
||||
|
||||
for child_task in task.child_ids:
|
||||
if child_task.stage_id and child_task.stage_id.fold:
|
||||
task.children_hours += child_task.effective_hours + child_task.children_hours
|
||||
else:
|
||||
task.children_hours += max(child_task.planned_hours, child_task.effective_hours + child_task.children_hours)
|
||||
|
||||
task.effective_hours = sum(task.timesheet_ids.mapped('unit_amount'))
|
||||
task.remaining_hours = task.planned_hours - task.effective_hours
|
||||
task.remaining_hours = task.planned_hours - task.effective_hours - task.children_hours
|
||||
task.total_hours = max(task.planned_hours, task.effective_hours)
|
||||
task.delay_hours = max(-task.remaining_hours, 0.0)
|
||||
|
||||
if task.stage_id and task.stage_id.fold:
|
||||
task.progress = 100.0
|
||||
elif (task.planned_hours > 0.0 and task.effective_hours):
|
||||
task.progress = round(min(100.0 * task.effective_hours / task.planned_hours, 99.99), 2)
|
||||
task.progress = round(min(100.0 * (task.effective_hours + task.children_hours) / task.planned_hours, 99.99), 2)
|
||||
else:
|
||||
task.progress = 0.0
|
||||
|
||||
@@ -30,4 +51,12 @@ class Task(models.Model):
|
||||
total_hours = fields.Float(compute='_hours_get', store=True, string='Total', help="Computed as: Time Spent + Remaining Time.")
|
||||
progress = fields.Float(compute='_hours_get', store=True, string='Progress (%)', 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", default=0.0)
|
||||
delay_hours = fields.Float(compute='_hours_get', store=True, string='Delay Hours', help="Computed as difference between planned hours by the project manager and the total hours of the task.")
|
||||
children_hours = fields.Float(compute='_hours_get', store=True, string='Sub-tasks Hours', 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)")
|
||||
timesheet_ids = fields.One2many('account.analytic.line', 'task_id', 'Timesheets')
|
||||
|
||||
parent_id = fields.Many2one('project.task', string='Parent Task')
|
||||
child_ids = fields.One2many('project.task', 'parent_id', string="Sub-tasks")
|
||||
subtask_project_id = fields.Many2one('project.project', related="project_id.subtask_project_id", string='Sub-task Project')
|
||||
subtask_count = fields.Integer(compute='_get_subtask_count', type='integer', string="Sub-task count")
|
||||
|
||||
_constraints = [(osv.osv._check_recursion, 'Circular references are not permitted between tasks and sub-tasks', ['parent_id'])]
|
||||
|
||||
@@ -28,21 +28,52 @@
|
||||
<div name="button_box" position="inside">
|
||||
<button class="oe_stat_button" name="%(act_hr_timesheet_line_by_project)d" type="action" 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>
|
||||
|
||||
<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="groups_id" eval="[(4, ref('base.group_user'))]"/>
|
||||
<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="tag_ids" position="after">
|
||||
<field name="progress" widget="progressbar"/>
|
||||
</field>
|
||||
<field name="user_id" position="after">
|
||||
<field name="planned_hours" widget="float_time"/>
|
||||
</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">
|
||||
<field name="timesheet_ids" context="{'default_project_id': project_id}">
|
||||
@@ -57,7 +88,8 @@
|
||||
<group>
|
||||
<group class="oe_subtotal_footer oe_right" name="project_hours">
|
||||
<field name="effective_hours" widget="float_time" />
|
||||
<field name="remaining_hours" widget="float_time" class="oe_subtotal_footer_separator" readonly="1" />
|
||||
<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" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
@@ -96,5 +128,26 @@
|
||||
|
||||
</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 > 80 and record.progress.raw_value < 100">
|
||||
<div t-att-class="'oe_kanban_align label label-' + (record.progress.raw_value >= 99 ? 'danger': 'warning')"
|
||||
title="Remaining hours">
|
||||
<field name="remaining_hours" widget="float_time" />
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
||||
@@ -281,14 +281,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>
|
||||
|
||||
@@ -111,6 +111,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'
|
||||
|
||||
@@ -24,8 +24,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>
|
||||
</data>
|
||||
|
||||
@@ -96,6 +96,10 @@
|
||||
|
||||
.oe_kanban_bottom_left {
|
||||
float: left;
|
||||
.oe_kanban_align {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.o_kanban_tags {
|
||||
|
||||
Reference in New Issue
Block a user