[FIX] project: follow-up the project overview removal
-- This is a follow-up of odoo/enterprise#72736 -- The project overview is a significant technical debt as it is a custom qweb view. It is quite limited: It is not possible to group the data, to filter on dates, SOs or Field Service projects. Improving this is very difficult and would require weeks of development that are not worth it. In any case, all the information provided by the project overview can be found elsewhere. The stat buttons of the report are basically duplicates of the ones from the project form view. Therefore, we are removing this report and its twin, the Project Costs and Revenues. In addition, analytic items lack context for the user to understand what generated a certain cost or revenue: there is no link to the source document; the entries are not categorized (e.g. it is not easy to understand if a cost comes from a timesheet cost, a purchase order or an expense); the billable type group by works fine for timesheets but then all the other entries are flagged as Undefined, which is not very helpful; there is no option to easily isolate costs from revenues. task-2637495 See odoo/enterprise#20740 closes odoo/odoo#77547 X-original-commit: 9bbfd3c8f948a3a947cbc0eb73ffc5d89c23d4ca Related: odoo/enterprise#21324 Signed-off-by: LTU-Odoo <IT-Ideas@users.noreply.github.com>
This commit is contained in:
committed by
Laurent Stukkens (LTU)
parent
db4e07e52e
commit
56ef461f44
@@ -661,7 +661,7 @@ class Project(models.Model):
|
||||
'user': self._get_user_values(),
|
||||
'tasks_analysis': self._get_tasks_analysis(),
|
||||
'milestones': self._get_milestones(),
|
||||
'buttons': sorted(self._get_stat_buttons(), key=lambda k: k['sequence'])
|
||||
'buttons': sorted(self._get_stat_buttons(), key=lambda k: k['sequence']),
|
||||
}
|
||||
|
||||
def _get_user_values(self):
|
||||
|
||||
@@ -7,6 +7,7 @@ from werkzeug.urls import url_encode
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import formatLang
|
||||
|
||||
STATUS_COLOR = {
|
||||
'on_track': 20, # green / success
|
||||
@@ -76,6 +77,13 @@ class ProjectUpdate(models.Model):
|
||||
update.project_id.sudo().last_update_id = update
|
||||
return update
|
||||
|
||||
def unlink(self):
|
||||
projects = self.project_id
|
||||
res = super().unlink()
|
||||
for project in projects:
|
||||
project.last_update_id = self.search([('project_id', "=", project.id)], order="date desc", limit=1)
|
||||
return res
|
||||
|
||||
# ---------------------------------
|
||||
# Build default description
|
||||
# ---------------------------------
|
||||
@@ -86,10 +94,13 @@ class ProjectUpdate(models.Model):
|
||||
|
||||
@api.model
|
||||
def _get_template_values(self, project):
|
||||
milestones = self._get_milestone_values(project)
|
||||
return {
|
||||
'user': self.env.user,
|
||||
'project': project,
|
||||
'milestones': self._get_milestone_values(project)
|
||||
'show_activities': milestones['show_section'],
|
||||
'milestones': milestones,
|
||||
'format_lang': lambda value, digits: formatLang(self.env, value, digits=digits),
|
||||
}
|
||||
|
||||
@api.model
|
||||
@@ -100,14 +111,14 @@ class ProjectUpdate(models.Model):
|
||||
'|', ('deadline', '<', fields.Date.context_today(self) + relativedelta(years=1)), ('deadline', '=', False)])._get_data_list()
|
||||
updated_milestones = self._get_last_updated_milestone(project)
|
||||
domain = [('project_id', '=', project.id)]
|
||||
if project.last_update_id.date:
|
||||
domain = expression.AND([domain, [('create_date', '>=', project.last_update_id.date)]])
|
||||
if project.last_update_id.create_date:
|
||||
domain = expression.AND([domain, [('create_date', '>', project.last_update_id.create_date)]])
|
||||
created_milestones = Milestone.search(domain)._get_data_list()
|
||||
return {
|
||||
'show_section': (list_milestones or updated_milestones or created_milestones) and True or False,
|
||||
'list': list_milestones,
|
||||
'updated': updated_milestones,
|
||||
'last_update_date': project.last_update_id.date or None,
|
||||
'last_update_date': project.last_update_id.create_date or None,
|
||||
'created': created_milestones,
|
||||
}
|
||||
|
||||
@@ -131,8 +142,8 @@ class ProjectUpdate(models.Model):
|
||||
AND mm.message_type = 'notification'
|
||||
AND pm.project_id = %(project_id)s
|
||||
"""
|
||||
if project.last_update_id.date:
|
||||
query = query + "AND mm.date <= %(last_update_date)s"
|
||||
if project.last_update_id.create_date:
|
||||
query = query + "AND mm.date > %(last_update_date)s"
|
||||
query = query + """
|
||||
WINDOW w_partition AS (
|
||||
PARTITION BY pm.id
|
||||
@@ -142,8 +153,8 @@ class ProjectUpdate(models.Model):
|
||||
LIMIT 1;
|
||||
"""
|
||||
query_params = {'project_id': project.id}
|
||||
if project.last_update_id.date:
|
||||
query_params['last_update_date'] = project.last_update_id.date
|
||||
if project.last_update_id.create_date:
|
||||
query_params['last_update_date'] = project.last_update_id.create_date
|
||||
self.env.cr.execute(query, query_params)
|
||||
results = self.env.cr.dictfetchall()
|
||||
mapped_result = {res['milestone_id']: {'new_value': res['new_value'], 'old_value': res['old_value']} for res in results}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { AddMilestone, OpenMilestone } from '@project/js/right_panel/project_utils';
|
||||
import { formatFloat } from "@web/fields/formatters";
|
||||
const { useState } = owl.hooks;
|
||||
|
||||
export default class ProjectRightPanel extends owl.Component {
|
||||
@@ -19,6 +20,10 @@ export default class ProjectRightPanel extends owl.Component {
|
||||
});
|
||||
}
|
||||
|
||||
formatFloat(value) {
|
||||
return formatFloat(value, { digits: [false, 1] });
|
||||
}
|
||||
|
||||
async willStart() {
|
||||
await super.willStart(...arguments);
|
||||
await this._loadQwebContext();
|
||||
|
||||
@@ -130,9 +130,9 @@ tour.register('project_update_tour', {
|
||||
trigger: ".o_field_widget[name=description] div[name='milestone'] ul li:contains('(12/12/2099 => 12/12/2100)')",
|
||||
run: function () {},
|
||||
}, {
|
||||
trigger: ".o_field_widget[name=description] div[name='milestone'] ul li span:contains('(due on 12/12/2022)')",
|
||||
trigger: ".o_field_widget[name=description] div[name='milestone'] ul li span:contains('(due 12/12/2022)')",
|
||||
run: function () {},
|
||||
}, {
|
||||
trigger: ".o_field_widget[name=description] div[name='milestone'] ul li span:contains('(due on 12/12/2100)')",
|
||||
trigger: ".o_field_widget[name=description] div[name='milestone'] ul li span:contains('(due 12/12/2100)')",
|
||||
run: function () {},
|
||||
}]);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<odoo>
|
||||
<template id="project.milestone_deadline">
|
||||
<t t-if="milestone['deadline']">
|
||||
(due on <t t-esc="milestone['deadline']" t-options='{"widget": "date"}'/><t t-if="not milestone['is_reached'] or not milestone['reached_date']">)</t><t t-else=""> - reached on<t t-if="milestone['reached_date'] > milestone['deadline']">
|
||||
(due <t t-esc="milestone['deadline']" t-options='{"widget": "date"}'/><t t-if="not milestone['is_reached'] or not milestone['reached_date']">)</t><t t-else=""> - reached on<t t-if="milestone['reached_date'] > milestone['deadline']">
|
||||
<font t-att-style="'color: rgb(' + str(color_level) + ', 0, 0)'"><b><t t-esc="milestone['reached_date']" t-options='{"widget": "date"}'/></b></font>)</t><t t-else="">
|
||||
<font t-att-style="'color: rgb(0, ' + str(color_level) + ', 0)'"><b><t t-esc="milestone['reached_date']" t-options='{"widget": "date"}'/></b></font>)</t></t>
|
||||
</t>
|
||||
@@ -11,18 +11,18 @@
|
||||
<template id="project_update_default_description" name="Project Update Description">
|
||||
<!--As this template is rendered in an html field, the spaces may be interpreted as nbsp while editing. -->
|
||||
<div name="summary">
|
||||
<h1>Sprint Summary</h1>
|
||||
<p>How’s this project going?</p><br/>
|
||||
<br/><h1 style="font-weight: bolder;">Sprint Summary</h1>
|
||||
<br/><p>How’s this project going?</p><br/><br/>
|
||||
</div>
|
||||
|
||||
<div name="activities" t-if="milestones['show_section']">
|
||||
<h1>Activities</h1>
|
||||
<div name="activities" t-if="show_activities">
|
||||
<h1 style="font-weight: bolder;">Activities</h1>
|
||||
</div>
|
||||
|
||||
<div name="milestone">
|
||||
<t t-if="milestones['show_section']">
|
||||
<br/>
|
||||
<h3><b><u>Milestones</u></b></h3>
|
||||
<h3 style="font-weight: bolder"><u>Milestones</u></h3>
|
||||
|
||||
<ul class="o_checklist" t-if="milestones['list']">
|
||||
<t t-foreach="milestones['list']" t-as="milestone" t-key="milestone['id']">
|
||||
@@ -35,8 +35,9 @@
|
||||
</ul>
|
||||
|
||||
<t t-if="milestones['updated']">
|
||||
<t t-if="len(milestones['updated']) > 1">The deadline of the following milestones has been updated <t t-if="milestones['last_update_date']">since the last project update (<t t-esc="milestones['last_update_date']" t-options='{"widget": "date"}'/>) </t> :</t>
|
||||
<t t-else="">The deadline of the following milestone has been updated <t t-if="milestones['last_update_date']">since the last project update (<t t-esc="milestones['last_update_date']" t-options='{"widget": "date"}'/>) </t> :</t>
|
||||
<t t-if="milestones['last_update_date']">Since <t t-esc="milestones['last_update_date']" t-options='{"widget": "date"}'/> (last project update), </t>
|
||||
<t t-if="len(milestones['updated']) > 1">the deadline of the following milestones has been updated:</t>
|
||||
<t t-else="">the deadline of the following milestone has been updated:</t>
|
||||
<ul>
|
||||
<t t-foreach="milestones['updated']" t-as="milestone" t-key="milestone['id']">
|
||||
<li>
|
||||
@@ -47,8 +48,8 @@
|
||||
</t>
|
||||
|
||||
<t t-if="milestones['created']">
|
||||
<t t-if="len(milestones['created']) > 1">The following milestones have been added <t t-if="milestones['last_update_date']">since the last project update (<t t-esc="milestones['last_update_date']" t-options='{"widget": "date"}'/>) </t>:</t>
|
||||
<t t-else="">The following milestone has been added <t t-if="milestones['last_update_date']">since the last project update (<t t-esc="milestones['last_update_date']" t-options='{"widget": "date"}'/>) </t>:</t>
|
||||
<t t-if="len(milestones['created']) > 1">The following milestones have been added:</t>
|
||||
<t t-else="">The following milestone has been added:</t>
|
||||
<ul>
|
||||
<t t-foreach="milestones['created']" t-as="milestone" t-key="milestone['id']">
|
||||
<li>
|
||||
|
||||
@@ -58,15 +58,13 @@ class Project(models.Model):
|
||||
return action_window
|
||||
|
||||
def action_create_invoice(self):
|
||||
if not self.sale_order_id and self.sale_order_id.invoice_status != 'to invoice':
|
||||
raise UserError(_("The selected Sales Order should contain something to invoice."))
|
||||
if not self.has_any_so_to_invoice:
|
||||
raise UserError(_("There is nothing to invoice in this project."))
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("sale.action_view_sale_advance_payment_inv")
|
||||
sols = self.sale_order_id.ids
|
||||
for task in self.task_ids:
|
||||
if task.sale_order_id.id not in sols:
|
||||
sols += [task.sale_order_id.id]
|
||||
so_ids = (self.sale_order_id | self.task_ids.sale_order_id).filtered(lambda so: so.invoice_status == 'to invoice').ids
|
||||
action['context'] = {
|
||||
'active_ids': sols
|
||||
'active_id': so_ids[0] if len(so_ids) == 1 else False,
|
||||
'active_ids': so_ids
|
||||
}
|
||||
return action
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ 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_is_zero
|
||||
from odoo.tools import format_amount, float_is_zero, formatLang
|
||||
|
||||
# YTI PLEASE SPLIT ME
|
||||
class Project(models.Model):
|
||||
@@ -280,6 +280,19 @@ class Project(models.Model):
|
||||
})
|
||||
return action
|
||||
|
||||
def action_view_all_rating(self):
|
||||
return {
|
||||
'name': _('Rating'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'rating.rating',
|
||||
'view_mode': 'kanban,list,graph,pivot,form',
|
||||
'view_type': 'ir.actions.act_window',
|
||||
'context': {
|
||||
'search_default_rating_last_30_days': True,
|
||||
},
|
||||
'domain': [('consumed', '=', True), ('parent_res_model', '=', 'project.project'), ('parent_res_id', '=', self.id)],
|
||||
}
|
||||
|
||||
# ----------------------------
|
||||
# Project Updates
|
||||
# ----------------------------
|
||||
@@ -290,7 +303,7 @@ class Project(models.Model):
|
||||
**panel_data,
|
||||
'analytic_account_id': self.analytic_account_id.id,
|
||||
'sold_items': self._get_sold_items(),
|
||||
'profitability_items': self._get_profitability_items()
|
||||
'profitability_items': self._get_profitability_items(),
|
||||
}
|
||||
|
||||
def _get_sale_order_lines(self):
|
||||
@@ -308,19 +321,20 @@ class Project(models.Model):
|
||||
'effective_sold': 0,
|
||||
'company_unit_name': self.env.company.timesheet_encode_uom_id.name
|
||||
}
|
||||
product_uom_unit = self.env.ref('uom.product_uom_unit')
|
||||
for sol in sols:
|
||||
name = [x[1] for x in sol.name_get()] if number_sale_orders > 1 else sol.name
|
||||
qty_delivered = int(sol.qty_delivered) if sol.qty_delivered.is_integer() else sol.qty_delivered
|
||||
product_uom_qty = int(sol.product_uom_qty) if sol.product_uom_qty.is_integer() else sol.product_uom_qty
|
||||
qty_delivered = sol.product_uom._compute_quantity(sol.qty_delivered, self.env.company.timesheet_encode_uom_id, raise_if_failure=False)
|
||||
product_uom_qty = sol.product_uom._compute_quantity(sol.product_uom_qty, self.env.company.timesheet_encode_uom_id, raise_if_failure=False)
|
||||
sold_items['data'].append({
|
||||
'name': name,
|
||||
'value': '%s / %s %s' % (qty_delivered, product_uom_qty, sol.product_uom.name),
|
||||
'value': '%s / %s %s' % (formatLang(self.env, qty_delivered, 1), formatLang(self.env, product_uom_qty, 1), sol.product_uom.name if sol.product_uom == product_uom_unit else self.env.company.timesheet_encode_uom_id.name),
|
||||
'color': 'red' if qty_delivered > product_uom_qty else 'black'
|
||||
})
|
||||
#We only want to consider hours and days for this calculation
|
||||
if sol.product_uom.category_id == self.env.company.timesheet_encode_uom_id.category_id:
|
||||
sold_items['total_sold'] += sol.product_uom._compute_quantity(sol.product_uom_qty, self.env.company.timesheet_encode_uom_id, raise_if_failure=False)
|
||||
sold_items['effective_sold'] = round(sol.product_uom._compute_quantity(self.total_timesheet_time, self.env.company.timesheet_encode_uom_id, raise_if_failure=False), 2)
|
||||
sold_items['total_sold'] += product_uom_qty
|
||||
sold_items['effective_sold'] += sol.product_uom._compute_quantity(qty_delivered, self.env.company.timesheet_encode_uom_id, raise_if_failure=False)
|
||||
remaining = sold_items['total_sold'] - sold_items['effective_sold']
|
||||
sold_items['remaining'] = {
|
||||
'value': remaining,
|
||||
|
||||
@@ -4,59 +4,59 @@
|
||||
from odoo import api, models
|
||||
from odoo.tools import float_utils, format_amount, formatLang
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
class ProjectUpdate(models.Model):
|
||||
_inherit = 'project.update'
|
||||
|
||||
@api.model
|
||||
def _get_template_values(self, project):
|
||||
template_values = super(ProjectUpdate, self)._get_template_values(project)
|
||||
services = self._get_services_values(project)
|
||||
profitability = self._get_profitability_values(project)
|
||||
show_sold = template_values['project'].allow_billable and len(services.get('data', [])) > 0
|
||||
return {
|
||||
**super(ProjectUpdate, self)._get_template_values(project),
|
||||
'services': self._get_services_values(project),
|
||||
'profitability': self._get_profitability_values(project),
|
||||
**template_values,
|
||||
'show_sold': show_sold,
|
||||
'show_profitability': bool(profitability),
|
||||
'show_activities': template_values['show_activities'] or show_sold or bool(profitability),
|
||||
'services': services,
|
||||
'profitability': profitability,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _get_project_sols(self, project):
|
||||
return self.env['sale.order.line'].search([('order_id', '=', project.sale_order_id.id), ('is_service', '=', True)])
|
||||
|
||||
@api.model
|
||||
def _get_services_values(self, project):
|
||||
if not project.allow_billable:
|
||||
return {}
|
||||
services = []
|
||||
total_sold_per_uom = defaultdict(float)
|
||||
total_effective_per_uom = defaultdict(float)
|
||||
total_sold, total_effective, total_remaining = 0, 0, 0
|
||||
sols = self._get_project_sols(project)
|
||||
sols = project._get_sale_order_lines()
|
||||
name_by_sol = dict(sols.name_get())
|
||||
product_uom_unit = self.env.ref('uom.product_uom_unit')
|
||||
for sol in sols:
|
||||
#We only want to consider hours and days for this calculation
|
||||
if sol.product_uom.category_id == self.env.company.timesheet_encode_uom_id.category_id:
|
||||
is_unit = sol.product_uom == product_uom_unit
|
||||
if sol.product_uom.category_id == self.env.company.timesheet_encode_uom_id.category_id or is_unit:
|
||||
product_uom_qty = sol.product_uom._compute_quantity(sol.product_uom_qty, self.env.company.timesheet_encode_uom_id, raise_if_failure=False)
|
||||
qty_delivered = sol.product_uom._compute_quantity(sol.qty_delivered, self.env.company.timesheet_encode_uom_id, raise_if_failure=False)
|
||||
services.append({
|
||||
'name': sol.name,
|
||||
'uom': sol.product_uom.name,
|
||||
'sold_value': sol.product_uom_qty,
|
||||
'effective_value': sol.qty_delivered,
|
||||
'remaining_value': sol.product_uom_qty - sol.qty_delivered,
|
||||
'name': name_by_sol[sol.id] if len(sols.order_id) > 1 else sol.name,
|
||||
'sold_value': product_uom_qty,
|
||||
'effective_value': qty_delivered,
|
||||
'remaining_value': product_uom_qty - qty_delivered,
|
||||
'unit': sol.product_uom.name if is_unit else self.env.company.timesheet_encode_uom_id.name,
|
||||
'is_unit': is_unit,
|
||||
'sol': sol,
|
||||
})
|
||||
total_sold_per_uom[sol.product_uom] += sol.product_uom_qty
|
||||
total_effective_per_uom[sol.product_uom] += sol.qty_delivered
|
||||
total_sold = sum(
|
||||
uom._compute_quantity(sol.product_uom_qty, self.env.company.timesheet_encode_uom_id, raise_if_failure=False)
|
||||
for uom, quantity in total_sold_per_uom.items()
|
||||
)
|
||||
total_effective = sum(
|
||||
uom._compute_quantity(sol.product_uom_qty, self.env.company.timesheet_encode_uom_id, raise_if_failure=False)
|
||||
for uom, quantity in total_effective_per_uom.items()
|
||||
)
|
||||
if sol.product_uom.category_id == self.env.company.timesheet_encode_uom_id.category_id:
|
||||
total_sold += product_uom_qty
|
||||
total_effective += qty_delivered
|
||||
total_remaining = total_sold - total_effective
|
||||
return {
|
||||
'data': services,
|
||||
'total_sold': total_sold,
|
||||
'total_effective': total_effective,
|
||||
'total_remaining': total_remaining,
|
||||
'company_unit_name': self.env.company.timesheet_encode_uom_id.name,
|
||||
}
|
||||
|
||||
@api.model
|
||||
|
||||
@@ -41,20 +41,20 @@
|
||||
<div class="o_rightpanel_title">
|
||||
<span class="o_rightpanel_left_col o_rightpanel_left_text"> Total Sold </span>
|
||||
</div>
|
||||
<span class="o_rightpanel_right_col font-weight-bold float-right"><t t-esc="sold_items.total_sold"/> <t t-esc="sold_items.company_unit_name"/></span>
|
||||
<span class="o_rightpanel_right_col font-weight-bold float-right"><t t-esc="formatFloat(sold_items.total_sold)"/> <t t-esc="sold_items.company_unit_name"/></span>
|
||||
</div>
|
||||
<div class="o_rightpanel_data">
|
||||
<div class="o_rightpanel_data_row">
|
||||
<span class="o_rightpanel_left_col o_rightpanel_left_text">Effective</span>
|
||||
<span class="o_rightpanel_right_col"><t t-esc="sold_items.effective_sold"/> <t t-esc="sold_items.company_unit_name"/></span>
|
||||
<span class="o_rightpanel_right_col"><t t-esc="formatFloat(sold_items.effective_sold)"/> <t t-esc="sold_items.company_unit_name"/></span>
|
||||
</div>
|
||||
<div class="o_rightpanel_data_row" t-if="sold_items.planned_sold > 0 and sold_items.allow_forecast">
|
||||
<span class="o_rightpanel_left_col o_rightpanel_left_text">Planned</span>
|
||||
<span class="o_rightpanel_right_col"><t t-esc="sold_items.planned_sold"/> <t t-esc="sold_items.company_unit_name"/></span>
|
||||
<span class="o_rightpanel_right_col"><t t-esc="formatFloat(sold_items.planned_sold)"/> <t t-esc="sold_items.company_unit_name"/></span>
|
||||
</div>
|
||||
<div class="o_rightpanel_data_row">
|
||||
<span class="o_rightpanel_left_col o_rightpanel_left_text">Remaining</span>
|
||||
<span class="o_rightpanel_right_col" t-attf-class="o_color_{{sold_items.remaining.color}}"><t t-esc="sold_items.remaining.value"/> <t t-esc="sold_items.company_unit_name"/></span>
|
||||
<span class="o_rightpanel_right_col" t-attf-class="o_color_{{sold_items.remaining.color}}"><t t-esc="formatFloat(sold_items.remaining['value'])"/> <t t-esc="sold_items.company_unit_name"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="action_make_billable" string="Create Sales Order" type="object" attrs="{'invisible': [('display_create_order', '=', False)]}" groups="sales_team.group_sale_salesman"/>
|
||||
<!-- To be removed in master -->
|
||||
<button name="action_make_billable" string="Create Sales Order" type="object" groups="sales_team.group_sale_salesman" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//page[@name='settings']" position="after">
|
||||
<page name="billing_employee_rate" string="Invoicing" attrs="{'invisible': ['|', ('allow_billable', '=', False), ('partner_id', '=', False)]}">
|
||||
@@ -104,7 +105,7 @@
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('o_kanban_manage_reporting')]" position="inside">
|
||||
<div role="menuitem" t-if="record.rating_active.raw_value" groups="project.group_project_manager">
|
||||
<a name="action_view_all_rating" type="object" context="{'search_default_rating_last_30_days':1}">
|
||||
<a name="action_view_all_rating" type="object">
|
||||
Customer Ratings
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -4,35 +4,37 @@
|
||||
<!--As this template is rendered in an html field, the spaces may be interpreted as nbsp while editing. -->
|
||||
<xpath expr="//div[@name='milestone']" position="before">
|
||||
<br/>
|
||||
<div t-if="project.allow_billable and 'data' in services and len(services['data']) > 0">
|
||||
<h3><b><u>Sold</u></b></h3>
|
||||
<div t-if="show_sold">
|
||||
<h3 style="font-weight: bolder"><u>Sold</u></h3>
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<thead>
|
||||
<td>Service</td>
|
||||
<td>Sold</td>
|
||||
<td>Effective</td>
|
||||
<td>Remaining</td>
|
||||
<td style="font-weight: bolder">Service</td>
|
||||
<td style="font-weight: bolder">Sold</td>
|
||||
<td style="font-weight: bolder">Effective</td>
|
||||
<td style="font-weight: bolder">Remaining</td>
|
||||
</thead>
|
||||
<tr t-foreach="services['data']" t-as="service">
|
||||
<td><t t-esc="service['name']"/></td>
|
||||
<td><t t-esc="service['sold_value']"/> <t t-esc="service['uom']"/></td>
|
||||
<td><t t-esc="service['effective_value']"/> <t t-esc="service['uom']"/></td>
|
||||
<td><t t-esc="service['remaining_value']"/> <t t-esc="service['uom']"/></td>
|
||||
<t t-set="is_unit" t-value="service['is_unit']"/>
|
||||
<td t-attf-class="#{ 'font-italic' if is_unit else ''}"><t t-esc="service['name']"/></td>
|
||||
<td t-attf-class="#{ 'font-italic' if is_unit else ''}" style="text-align: right; vertical-align: middle;"><t t-esc="format_lang(service['sold_value'], 1)"/> <t t-esc="service['unit']"/></td>
|
||||
<td t-attf-class="#{ 'font-italic' if is_unit else ''}" style="text-align: right; vertical-align: middle;"><t t-esc="format_lang(service['effective_value'], 1)"/> <t t-esc="service['unit']"/></td>
|
||||
<td t-attf-class="#{ 'font-italic' if is_unit else ''}" style="text-align: right; vertical-align: middle;"><t t-esc="format_lang(service['remaining_value'], 1)"/> <t t-esc="service['unit']"/></td>
|
||||
</tr>
|
||||
<tfoot>
|
||||
<td>Total</td>
|
||||
<td><t t-esc="services['total_sold']"/> Hours</td>
|
||||
<td><t t-esc="services['total_effective']"/> Hours</td>
|
||||
<td><t t-esc="services['total_remaining']"/> Hours</td>
|
||||
<td style="font-weight: bolder; text-align: right">Total</td>
|
||||
<td style="font-weight: bolder; text-align: right; vertical-align: middle;"><t t-esc="format_lang(services['total_sold'], 1)"/> <t t-esc="services['company_unit_name']"/></td>
|
||||
<td style="font-weight: bolder; text-align: right; vertical-align: middle;"><t t-esc="format_lang(services['total_effective'], 1)"/> <t t-esc="services['company_unit_name']"/></td>
|
||||
<td style="font-weight: bolder; text-align: right; vertical-align: middle;"><t t-esc="format_lang(services['total_remaining'], 1)"/> <t t-esc="services['company_unit_name']"/></td>
|
||||
</tfoot>
|
||||
</tbody>
|
||||
</table>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<div name="profitability" t-if="profitability">
|
||||
<div name="profitability" t-if="show_profitability">
|
||||
<t t-if="project.analytic_account_id and project.allow_billable and user.has_group('project.group_project_manager')" name="costs">
|
||||
<h3><b><u>Profitability</u></b></h3>
|
||||
<h3 style="font-weight: bolder"><u>Profitability</u></h3>
|
||||
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)">
|
||||
|
||||
Reference in New Issue
Block a user