[IMP] project,hr,sale(_timesheet): generic improvement for project

The purpose of this commit, is to make generic improvements in
project.

In this commit, following changes are made:

- add allocated hours field in project form and list view
- remaining hour widget in project kanban view displays allocated
  hours (-) timesheeted hours on task
- hide progress bar in project list view if planned hours is zero
- hide task stages if user is set
- move company field below tags	field
- project list view: rename 'Activities' to 'Next Activity'

task-2692856

closes odoo/odoo#82019

Related: odoo/upgrade#3149
Related: odoo/enterprise#23148
Signed-off-by: Laurent Stukkens (ltu) <ltu@odoo.com>
Co-authored-by: Krina Oza <kro@odoo.com>
This commit is contained in:
Anjali
2022-03-07 18:22:06 +00:00
committed by Barad Mahendra
co-authored by Krina Oza
parent 37bc99b309
commit d4252825f5
4 changed files with 77 additions and 18 deletions
+17 -12
View File
@@ -46,9 +46,8 @@ class Project(models.Model):
encode_uom_in_days = fields.Boolean(compute='_compute_encode_uom_in_days')
is_internal_project = fields.Boolean(compute='_compute_is_internal_project', search='_search_is_internal_project')
remaining_hours = fields.Float(compute='_compute_remaining_hours', string='Remaining Invoiced Time', compute_sudo=True)
has_planned_hours_tasks = fields.Boolean(compute='_compute_remaining_hours', compute_sudo=True,
help="True if any of the project's task has a set planned hours")
is_project_overtime = fields.Boolean('Project in Overtime', compute='_compute_remaining_hours', search='_search_is_project_overtime', compute_sudo=True)
allocated_hours = fields.Float(string='Allocated Hours')
def _compute_encode_uom_in_days(self):
self.encode_uom_in_days = self.env.company.timesheet_encode_uom_id == self.env.ref('uom.product_uom_day')
@@ -81,18 +80,24 @@ class Project(models.Model):
operator_new = 'not inselect'
return [('id', operator_new, (query, ()))]
@api.depends('allow_timesheets', 'task_ids.planned_hours', 'task_ids.remaining_hours')
@api.model
def _fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
result = super()._fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
if view_type in ['tree', 'form'] and self.env.company.timesheet_encode_uom_id == self.env.ref('uom.product_uom_day'):
result['arch'] = self.env['account.analytic.line']._apply_time_label(result['arch'], related_model=self._name)
return result
@api.depends('allow_timesheets', 'timesheet_ids')
def _compute_remaining_hours(self):
group_read = self.env['project.task']._read_group(
domain=[('planned_hours', '!=', False), ('project_id', 'in', self.filtered('allow_timesheets').ids),
'|', ('stage_id.fold', '=', False), ('stage_id', '=', False)],
fields=['planned_hours:sum', 'remaining_hours:sum'], groupby='project_id')
group_per_project_id = {group['project_id'][0]: group for group in group_read}
timesheets_read_group = self.env['account.analytic.line']._read_group(
[('project_id', 'in', self.ids)],
['project_id', 'unit_amount'],
['project_id'],
lazy=False)
timesheet_time_dict = {res['project_id'][0]: res['unit_amount'] for res in timesheets_read_group}
for project in self:
group = group_per_project_id.get(project.id, {})
project.remaining_hours = group.get('remaining_hours', 0)
project.has_planned_hours_tasks = bool(group.get('planned_hours', False))
project.is_project_overtime = group.get('remaining_hours', 0) < 0
project.remaining_hours = project.allocated_hours - timesheet_time_dict.get(project.id, 0)
project.is_project_overtime = project.remaining_hours < 0
@api.model
def _search_is_project_overtime(self, operator, value):
+17 -3
View File
@@ -86,6 +86,9 @@
</div>
</button>
</xpath>
<xpath expr="//div[@name='dates']" position="after">
<field name="allocated_hours" widget="timesheet_uom_no_toggle" attrs="{'invisible': [('allow_timesheets', '=', False)]}"/>
</xpath>
<xpath expr="//div[@id='rating_settings']" position="before">
<div class="col-lg-6 o_setting_box" id="timesheet_settings">
<div class="o_setting_left_pane">
@@ -250,6 +253,17 @@
</field>
</record>
<record id="project_project_view_tree_inherit_sale_project" model="ir.ui.view">
<field name="name">project.project.tree.inherit.sale.timesheet</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.view_project"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='date']" position="after">
<field name="allocated_hours" widget="timesheet_uom_no_toggle" optional="show" attrs="{'invisible' : [('allocated_hours', '=', 0)]}"/>
</xpath>
</field>
</record>
<record id="view_task_tree2_inherited" model="ir.ui.view">
<field name="name">project.task.tree.inherited</field>
<field name="model">project.task</field>
@@ -263,7 +277,7 @@
<field name="subtask_effective_hours" widget="timesheet_uom" attrs="{'invisible' : [('allow_subtasks', '=', False)]}" optional="hide"/>
<field name="total_hours_spent" widget="timesheet_uom" attrs="{'invisible' : [('allow_subtasks', '=', False)]}" optional="hide"/>
<field name="remaining_hours" widget="timesheet_uom" sum="Remaining Hours" optional="hide" decoration-danger="progress &gt;= 100" decoration-warning="progress &gt;= 80 and progress &lt; 100"/>
<field name="progress" widget="progressbar" optional="show" groups="hr_timesheet.group_hr_timesheet_user"/>
<field name="progress" widget="progressbar" optional="show" groups="hr_timesheet.group_hr_timesheet_user" attrs="{'invisible' : [('planned_hours', '=', 0)]}"/>
</field>
</field>
</record>
@@ -278,8 +292,8 @@
<field name="allow_timesheets" invisible="1"/>
<field name="timesheet_count" invisible="1"/>
<field name="remaining_hours" invisible="1"/>
<field name="has_planned_hours_tasks" invisible="1"/>
<field name="encode_uom_in_days" invisible="1"/>
<field name="allocated_hours" invisible="1"/>
</field>
<xpath expr="//div[hasclass('o_kanban_manage_view')]" position="inside">
<div role="menuitem" t-if="record.allow_timesheets.raw_value and record.timesheet_count.raw_value" groups="hr_timesheet.group_hr_timesheet_user">
@@ -291,7 +305,7 @@
<t t-set="badge" t-value="'badge-danger'" t-if="record.remaining_hours.raw_value &lt; 0"/>
<t t-set="title" t-value="'Remaining days'" t-if="record.encode_uom_in_days.raw_value"/>
<t t-set="title" t-value="'Remaining hours'" t-else=""/>
<div t-if="record.has_planned_hours_tasks.raw_value"
<div t-if="record.allocated_hours.raw_value &gt; 0"
t-attf-class="oe_kanban_align badge {{ badge }}" t-att-title="title">
<field name="remaining_hours" widget="timesheet_uom"/>
</div>
+3 -3
View File
@@ -213,7 +213,6 @@
<field name="rating_template_id" optional="hide" groups="project.group_project_rating"/>
<field name="project_ids" optional="show" widget="many2many_tags" options="{'color_field': 'color'}" />
<field name="fold" optional="show"/>
<field name="user_id" optional="show"/>
</tree>
</field>
</record>
@@ -252,6 +251,7 @@
<field name="res_model">project.task.type</field>
<field name="view_mode">tree,kanban,form</field>
<field name="view_id" ref="task_type_tree"/>
<field name="domain">[('user_id', '=', False)]</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No stages found. Let's create one!
@@ -380,6 +380,7 @@
<field name="label_tasks" string="Name of the tasks"/>
<field name="partner_id" widget="res_partner_many2one"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<group>
<field name="active" invisible="1"/>
@@ -391,7 +392,6 @@
<i class="fa fa-long-arrow-right mx-2 oe_read_only" aria-label="Arrow icon" title="Arrow" attrs="{'invisible': [('date_start', '=', False), ('date', '=', False)]}"/>
<field name="date" widget="daterange" options='{"related_start_date": "date_start"}'/>
</div>
<field name="company_id" groups="base.group_multi_company"/>
</group>
</group>
<notebook>
@@ -1204,7 +1204,7 @@
<field name="parent_id" optional="hide" attrs="{'invisible': [('allow_subtasks', '=', False)]}" groups="base.group_no_one"/>
<field name="user_ids" optional="show" widget="many2many_avatar_user" domain="[('share', '=', False), ('active', '=', True)]"/>
<field name="company_id" groups="base.group_multi_company" optional="show"/>
<field name="activity_ids" widget="list_activity" optional="show"/>
<field name="activity_ids" string="Next Activity" widget="list_activity" optional="show"/>
<field name="date_deadline" optional="hide" widget="remaining_days" attrs="{'invisible': [('is_closed', '=', True)]}"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" optional="show" context="{'project_id': project_id}"/>
<field name="rating_last_text" string="Rating" decoration-danger="rating_last_text == 'ko'"
+40
View File
@@ -54,6 +54,7 @@ class Project(models.Model):
warning_employee_rate = fields.Boolean(compute='_compute_warning_employee_rate')
partner_id = fields.Many2one(
compute='_compute_partner_id', store=True, readonly=False)
allocated_hours = fields.Float(compute='_compute_allocated_hours', store=True, readonly=False, copy=False)
@api.depends('sale_line_id', 'sale_line_employee_ids', 'allow_billable')
def _compute_pricing_type(self):
@@ -174,6 +175,45 @@ class Project(models.Model):
], limit=1)
project.sale_line_id = sol or project.sale_line_employee_ids.sale_line_id[:1] # get the first SOL containing in the employee mappings if no sol found in the search
@api.depends('sale_line_id.product_uom_qty', 'sale_line_id.product_uom')
def _compute_allocated_hours(self):
sol_ids = self._fetch_sale_order_item_ids()
sols_read_group = self.env['sale.order.line']._read_group([
('id', 'in', sol_ids), ('is_service', '=', True),
('is_downpayment', '=', False),
('product_id.service_tracking', 'in', ['task_in_project', 'project_only'])],
['project_id', 'product_uom_qty', 'product_uom'],
['project_id', 'product_uom'],
lazy=False)
uom_hour = self.env.ref('uom.product_uom_hour')
sol_qty_dict = defaultdict(list)
uom_ids = set(self.timesheet_encode_uom_id.ids)
for result in sols_read_group:
uom_id = result['product_uom'] and result['product_uom'][0]
if uom_id:
uom_ids.add(uom_id)
if result.get('project_id'):
sol_qty_dict[result['project_id'][0]].append((uom_id, result['product_uom_qty']))
uoms_dict = {uom.id: uom for uom in self.env['uom.uom'].browse(uom_ids)}
for project in self:
if not project.sale_line_id:
project.allocated_hours = 0
continue
# sale order line may be stored in a different unit of measure, so first
# we convert all of them to the reference unit
# if the sol has no product_uom_id then we take the one of the project
allocated_hours = sum([
product_uom_qty * uoms_dict.get(product_uom, project.timesheet_encode_uom_id.id).factor_inv
for product_uom, product_uom_qty in sol_qty_dict[project.id]
], 0.0)
# Now convert to the unit of measure to hours
allocated_hours *= uom_hour.factor
project.allocated_hours = allocated_hours
@api.depends('sale_line_employee_ids.sale_line_id', 'allow_billable')
def _compute_sale_order_count(self):
billable_projects = self.filtered('allow_billable')