[IMP] sale_timesheet: extend project update

This commit adds information in project update based on sale_timesheet models.

In the Project Right Panel, a profitability section is added with data
from the profitability report.
In the project update description a profitability section is also added
with data from account analytic lines or profitability report.

PR : #68899

task-2393768
This commit is contained in:
Thibault Libioulle
2021-06-01 12:16:27 +02:00
parent ebe8418f26
commit 129b841d6c
7 changed files with 246 additions and 0 deletions
+1
View File
@@ -22,6 +22,7 @@ have real delivered quantities in sales orders.
'views/sale_order_views.xml',
'views/product_views.xml',
'views/project_task_views.xml',
'views/project_update_templates.xml',
'views/hr_timesheet_views.xml',
'views/res_config_settings_views.xml',
'views/hr_timesheet_templates.xml',
@@ -1685,5 +1685,23 @@
<field name="amount">-30.0</field>
</record>
<record id="project_update_1" model="project.update" context="{'default_project_id': ref('project.project_project_1')}">
<field name="name">Weekly review</field>
<field name="user_id" eval="ref('base.user_demo')"/>
<field name="progress" eval="50"/>
<field name="status">on_track</field>
</record>
<record id="project_update_2" model="project.update" context="{'default_project_id': ref('project.project_project_2')}">
<field name="name">Weekly review</field>
<field name="user_id" eval="ref('base.user_admin')"/>
<field name="progress" eval="35"/>
<field name="status">on_hold</field>
</record>
<record id="project_update_3" model="project.update" context="{'default_project_id': ref('sale_timesheet.project_support')}">
<field name="name">Review of the situation</field>
<field name="user_id" eval="ref('base.user_admin')"/>
<field name="progress" eval="30"/>
<field name="status">at_risk</field>
</record>
</data>
</odoo>
+1
View File
@@ -6,5 +6,6 @@ from . import account_move
from . import product
from . import project
from . import project_overview
from . import project_update
from . import sale_order
from . import project_sale_line_employee_map
+80
View File
@@ -6,6 +6,8 @@ from collections import defaultdict
from odoo import api, fields, models, _
from odoo.osv import expression
from odoo.exceptions import ValidationError, UserError
from odoo.tools import format_amount, float_round, float_is_zero
from odoo.tools.misc import formatLang
# YTI PLEASE SPLIT ME
@@ -251,6 +253,84 @@ class Project(models.Model):
}
return action_window
# ----------------------------
# Project Updates
# ----------------------------
def get_panel_data(self):
panel_data = super(Project, self).get_panel_data()
return {
**panel_data,
'profitability_items': self._get_profitability_items()
}
def _get_profitability_items(self):
if not self.user_has_groups('project.group_project_manager'):
return {'data': []}
profitability = self.get_profitability_common()
data = []
if self.allow_timesheets and self.user_has_groups('hr_timesheet.group_hr_timesheet_user'):
data += [{
'name': _("Timesheets"),
'value': self.env.ref('sale_timesheet.project_profitability_timesheet_panel')._render({
'timesheet_unit_amount': float_round(profitability['timesheet_unit_amount'], precision_digits=2),
'timesheet_uom': self.env.company._timesheet_uom_text(),
'is_timesheet_uom_hour': self.env.company._is_timesheet_hour_uom(),
'percentage_billable': formatLang(self.env, profitability['timesheet_percentage_billable'], digits=0),
}, engine='ir.qweb'),
}]
if self.allow_billable:
margin_color = False
if not float_is_zero(profitability['margin'], precision_digits=0):
margin_color = profitability['margin'] > 0 and 'green' or 'red'
data += [{
'name': _("Revenues"),
'value': format_amount(self.env, profitability['revenues'], self.env.company.currency_id)
}, {
'name': _("Costs"),
'value': format_amount(self.env, profitability['costs'], self.env.company.currency_id)
}, {
'name': _("Margin"),
'color': margin_color,
'value': format_amount(self.env, profitability['margin'], self.env.company.currency_id)
}]
return {
'action': self.allow_billable and self.allow_timesheets and "action_view_timesheet",
'allow_billable': self.allow_billable,
'data': data,
}
def get_profitability_common(self):
self.ensure_one()
profitability = self.env['project.profitability.report'].read_group(
[('project_id', '=', self.id)],
['project_id',
'timesheet_unit_amount',
'amount_untaxed_to_invoice',
'amount_untaxed_invoiced',
'expense_amount_untaxed_to_invoice',
'expense_amount_untaxed_invoiced',
'other_revenues',
'expense_cost',
'timesheet_cost',
'margin'],
['project_id'])
timesheets = self.env['account.analytic.line'].read_group([('project_id', '=', self.id)], ['so_line', 'unit_amount'], ['so_line'])
timesheet_billable = timesheet_non_billable = 0.0
for timesheet in timesheets:
if timesheet['so_line']:
timesheet_billable += timesheet['unit_amount']
else:
timesheet_non_billable += timesheet['unit_amount']
return {
'costs': profitability and profitability[0]['timesheet_cost'] + profitability[0]['expense_cost'] or 0.0,
'margin': profitability and profitability[0]['margin'] or 0.0,
'revenues': profitability and (profitability[0]['amount_untaxed_invoiced'] + profitability[0]['amount_untaxed_to_invoice'] +
profitability[0]['expense_amount_untaxed_invoiced'] + profitability[0]['expense_amount_untaxed_to_invoice'] +
profitability[0]['other_revenues']) or 0.0,
'timesheet_unit_amount': profitability and self._convert_project_uom_to_timesheet_encode_uom(profitability[0]['timesheet_unit_amount']) or 0.0,
'timesheet_percentage_billable': timesheet_billable and timesheet_billable / (timesheet_billable + timesheet_non_billable) * 100 or 0.0,
}
class ProjectTask(models.Model):
_inherit = "project.task"
@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models
from odoo.tools import float_utils, format_amount
from odoo.tools.misc import formatLang
class ProjectUpdate(models.Model):
_inherit = 'project.update'
@api.model
def _get_template_values(self, project):
return {
**super(ProjectUpdate, self)._get_template_values(project),
'profitability': self._get_profitability_values(project),
}
@api.model
def _get_profitability_values(self, project):
if not (self.user_has_groups('project.group_project_manager') and (project.analytic_account_id and project.allow_billable or project.allow_timesheets)):
return {}
profitability = project.get_profitability_common()
start_of_month = fields.Date.context_today(self) + relativedelta(day=1)
timesheets_this_month = self.env['project.profitability.report'].read_group(
[('project_id', '=', project.id),
('line_date', '>=', start_of_month)],
['project_id',
'timesheet_unit_amount'],
['project_id'])
timesheets_previous_month = self.env['project.profitability.report'].read_group(
[('project_id', '=', project.id),
('line_date', '>=', start_of_month + relativedelta(months=-1, day=1)),
('line_date', '<', start_of_month)
],
['project_id',
'timesheet_unit_amount'],
['project_id'])
timesheet_unit_amount = timesheets_this_month and timesheets_this_month[0]['timesheet_unit_amount'] or 0.0
previous_timesheet_unit_amount = timesheets_previous_month and timesheets_previous_month[0]['timesheet_unit_amount'] or 0.0
return {
'allow_timesheets': project.allow_timesheets,
'analytic_account_id': project.analytic_account_id,
'month': start_of_month.strftime('%B %Y'),
'previous_month': (start_of_month + relativedelta(months=-1, day=1)).strftime('%B'),
'is_timesheet_uom_hour': self.env.company._is_timesheet_hour_uom(),
'timesheet_uom': self.env.company._timesheet_uom_text(),
'timesheet_unit_amount': formatLang(self.env, project._convert_project_uom_to_timesheet_encode_uom(timesheet_unit_amount), digits=0),
'previous_timesheet_unit_amount': formatLang(self.env, project._convert_project_uom_to_timesheet_encode_uom(previous_timesheet_unit_amount), digits=0),
'timesheet_trend': formatLang(self.env,
previous_timesheet_unit_amount > 0 and ((timesheet_unit_amount / previous_timesheet_unit_amount) - 1) * 100 or 0.0,
digits=0),
'costs': format_amount(self.env, -profitability['costs'], self.env.company.currency_id),
'revenues': format_amount(self.env, profitability['revenues'], self.env.company.currency_id),
'margin': profitability['margin'],
'margin_formatted': format_amount(self.env, profitability['margin'], self.env.company.currency_id),
'margin_percentage': formatLang(self.env,
not float_utils.float_is_zero(profitability['costs'], precision_digits=2) and -(profitability['margin'] / profitability['costs']) * 100 or 0.0,
digits=0),
'billing_rate': formatLang(self.env,
not float_utils.float_is_zero(profitability['costs'], precision_digits=2) and -(profitability['revenues'] / profitability['costs']) * 100 or 0.0,
digits=0),
}
@@ -5,4 +5,39 @@
Create Sales Order
</button>
<t t-inherit="project.ProjectRightPanel" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('o_rightpanel_section')][1]" position="before">
<t t-if="state.data.user.is_project_manager &amp;&amp; state.data.profitability_items.data.length > 0">
<t t-set="profitability_items" t-value="state.data.profitability_items"/>
<div class="o_rightpanel_section">
<div class="o_rightpanel_header">
<div class="o_rightpanel_title">
<t t-if="profitability_items.allow_billable">Profitability</t>
<t t-else="">Recorded Hours</t>
</div>
<span class="o_rightpanel_button" t-if="profitability_items.action">
<a t-on-click="onProjectActionClick"
t-att-data-action="profitability_items.action"
data-type="object">
See Overview
</a>
</span>
</div>
<div class="o_rightpanel_data">
<div t-foreach="profitability_items.data" t-as="profitability" t-key="profitability.name" class="o_rightpanel_data_row">
<span class="o_rightpanel_left_col o_rightpanel_left_text"><t t-esc="profitability.name"/></span>
<t t-if="profitability.color">
<span t-attf-class="o_rightpanel_right_col o_color_{{profitability.color}}"><b><t t-esc="profitability.value"/></b></span>
</t>
<t t-else="">
<span class="o_rightpanel_right_col"><t t-esc="profitability.value"/></span>
</t>
</div>
</div>
</div>
</t>
</xpath>
</t>
</templates>
@@ -0,0 +1,47 @@
<?xml version="1.0"?>
<odoo>
<template id="project_update_default_description" inherit_id="hr_timesheet.project_update_default_description">
<xpath expr="//div[@name='milestone']" position="after">
<hr t-if="profitability"/>
<div name="profitability" t-if="profitability">
<h1>Profitability</h1>
<t t-if="project.allow_timesheets and user.has_group('hr_timesheet.group_hr_timesheet_user')">
In <t t-esc="profitability['month']"/>,
<b>
<t t-if="profitability['is_timesheet_uom_hour']" t-esc="profitability['timesheet_unit_amount']"/>
<t t-else="" t-esc="profitability['timesheet_unit_amount']"/>
<t t-esc="profitability['timesheet_uom']"/>
</b> have been recorded on the project vs.
<t t-if="profitability['is_timesheet_uom_hour']" t-esc="profitability['previous_timesheet_unit_amount']"/>
<t t-else="" t-esc="profitability['previous_timesheet_unit_amount']"/>
<t t-esc="profitability['timesheet_uom']"/>
in <t t-esc="profitability['previous_month']"/> (<t t-esc="profitability['timesheet_trend']"/> %).
</t>
<t t-if="project.analytic_account_id and project.allow_billable and user.has_group('project.group_project_manager')">
The cost of the project is now at <t t-esc="profitability['costs']"/>, for a revenue of <t t-esc="profitability['revenues']"/>, leading to a
<span>
<font t-if="profitability['margin'] &gt; 0" style="color: rgb(0, 128, 0)">
<b><t t-esc="profitability['margin_formatted']"/></b>
</font>
<font t-elif="profitability['margin'] &lt; 0" style="color: rgb(128, 0, 0)">
<b><t t-esc="profitability['margin_formatted']"/></b>
</font>
<t t-else="" t-esc="profitability['margin_formatted']"/>
</span> margin (<t t-esc="profitability['margin_percentage']"/> %), and a <b><t t-esc="profitability['billing_rate']"/> % billing rate</b>.
</t>
</div>
</xpath>
</template>
<template id="project_profitability_timesheet_panel" name="Timesheet Profitability">
<t t-if="is_timesheet_uom_hour" t-esc="timesheet_unit_amount" t-options='{"widget": "float_time"}'/>
<t t-else="" t-esc="timesheet_unit_amount"/>
<t t-if="not is_timesheet_uom_hour">
<t t-esc="timesheet_uom"/>
</t>
(<t t-esc="percentage_billable"/> % billable)
</template>
</odoo>