[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:
Cedric Prieels (cpr)
2021-09-30 20:56:12 +00:00
committed by Laurent Stukkens (LTU)
parent db4e07e52e
commit 56ef461f44
11 changed files with 116 additions and 84 deletions
+1 -1
View File
@@ -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):
+19 -8
View File
@@ -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'] &gt; 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'] &gt; 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>
+5 -7
View File
@@ -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
+21 -7
View File
@@ -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,
+27 -27
View File
@@ -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'] &gt; 0" style="color: rgb(0, 128, 0)">