[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:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 && 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'] > 0" style="color: rgb(0, 128, 0)">
|
||||
<b><t t-esc="profitability['margin_formatted']"/></b>
|
||||
</font>
|
||||
<font t-elif="profitability['margin'] < 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>
|
||||
Reference in New Issue
Block a user