[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:
committed by
Barad Mahendra
co-authored by
Krina Oza
parent
37bc99b309
commit
d4252825f5
@@ -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):
|
||||
|
||||
@@ -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 >= 100" decoration-warning="progress >= 80 and progress < 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 < 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 > 0"
|
||||
t-attf-class="oe_kanban_align badge {{ badge }}" t-att-title="title">
|
||||
<field name="remaining_hours" widget="timesheet_uom"/>
|
||||
</div>
|
||||
|
||||
@@ -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'"
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user