[IMP] project: improve project updates

This commit improves the project updates panel and the data reported
in the update description.

This commit prepares the removal of the project overview feature.

PR : #72736
See odoo/upgrade#2706

task-2545084
This commit is contained in:
Cedric Prieels (cpr)
2021-09-06 10:41:13 +02:00
committed by Laurent Stukkens (LTU)
parent 94018575c6
commit 1ec2b3c0b0
13 changed files with 351 additions and 176 deletions
+59 -1
View File
@@ -9,6 +9,7 @@ from random import randint
from odoo import api, Command, fields, models, tools, SUPERUSER_ID, _
from odoo.exceptions import UserError, ValidationError, AccessError
from odoo.tools import format_amount
from odoo.osv.expression import OR
from .project_task_recurrence import DAYS, WEEKS
@@ -137,7 +138,6 @@ class ProjectTaskType(models.Model):
else:
stage.disabled_rating_warning = False
class Project(models.Model):
_name = "project.project"
_description = "Project"
@@ -659,6 +659,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'])
}
def _get_user_values(self):
@@ -705,6 +706,63 @@ class Project(models.Model):
'data': self.milestone_ids._get_data_list(),
}
def _get_stat_buttons(self):
self.ensure_one()
buttons = [{
'icon': 'tasks',
'text': _('Tasks'),
'number': self.task_count,
'action_type': 'action',
'action': 'project.act_project_project_2_project_task_all',
'additional_context': json.dumps({
'active_id': self.id,
}),
'show': True,
'sequence': 2,
},
{
'icon': 'smile-o',
'text': _('Customer Satisfaction'),
'number': '%s %%' % (self.rating_percentage_satisfaction),
'action_type': 'object',
'action': 'action_view_all_rating',
'show': self.user_has_groups('project.group_project_rating') and self.rating_active and self.rating_percentage_satisfaction > -1,
'sequence': 5,
},
{
'icon': 'area-chart',
'text': _('Burndown Chart'),
'action_type': 'action',
'action': 'project.action_project_task_burndown_chart_report',
'additional_context': json.dumps({
'active_id': self.id,
}),
'show': self.user_has_groups('project.group_project_manager'),
'sequence': 7,
},
{
'icon': 'usd',
'text': _('Gross Margin'),
'number': format_amount(self.env, self.analytic_account_balance, self.company_id.currency_id),
'action_type': 'object',
'action': 'action_view_analytic_account_entries',
'show': self.user_has_groups('analytic.group_analytic_accounting'),
'sequence': 18,
},
{
'icon': 'users',
'text': _('Collaborators'),
'number': self.collaborator_count,
'action_type': 'action',
'action': 'project.project_collaborator_action',
'additional_context': json.dumps({
'active_id': self.id,
}),
'show': self.user_has_groups('project.group_project_manager'),
'sequence': 19,
}]
return buttons
def _get_tasks_analysis_counts(self, created=False, updated=False):
tasks = self.env['project.task'].search([('display_project_id', '=', self.id)])
open_tasks_count = created_tasks_count = updated_tasks_count = 0
+16 -63
View File
@@ -6,6 +6,7 @@ from dateutil.relativedelta import relativedelta
from werkzeug.urls import url_encode
from odoo import api, fields, models
from odoo.osv import expression
STATUS_COLOR = {
'on_track': 20, # green / success
@@ -87,55 +88,9 @@ class ProjectUpdate(models.Model):
return {
'user': self.env.user,
'project': project,
'tasks': self._get_tasks_values(project),
'milestones': self._get_milestone_values(project)
}
@api.model
def _get_tasks_values(self, project):
counts = project._get_tasks_analysis_counts(created=True)
return {
'open_tasks': counts['open_tasks_count'],
'total_tasks': counts['tasks_count'],
'created_tasks': counts['created_tasks_count'],
'closed_tasks': self._get_last_stage_changes(project=project),
'action': {
'url_default': '/web#' + url_encode({
'menu_id': self.env.ref('project.menu_projects').id,
'action': self.env.ref('project.action_project_task_burndown_chart_report').id,
'active_id': project.id
}),
'name': 'action_burndown_open_tasks',
}
}
@api.model
def _get_last_stage_changes(self, project):
query = """
SELECT DISTINCT pt.id as task_id
FROM mail_message mm
INNER JOIN mail_tracking_value mtv
ON mm.id = mtv.mail_message_id
INNER JOIN ir_model_fields imf
ON mtv.field = imf.id
AND imf.model = 'project.task'
AND imf.name = 'stage_id'
INNER JOIN project_task_type new_stage
ON mtv.new_value_integer = new_stage.id
INNER JOIN project_task pt
ON mm.res_id = pt.id
AND pt.stage_id = new_stage.id
WHERE mm.model = 'project.task'
AND mm.message_type = 'notification'
AND pt.display_project_id = %(project_id)s
AND (new_stage.fold OR new_stage.is_closed)
AND mm.date > (now() at time zone 'utc')::date - '1 month'::interval
AND pt.active
"""
self.env.cr.execute(query, {'project_id': project.id})
task_ids = [res['task_id'] for res in self.env.cr.dictfetchall()]
return self.env['project.task'].search_count([('id', 'in', task_ids)])
@api.model
def _get_milestone_values(self, project):
Milestone = self.env['project.milestone']
@@ -143,13 +98,15 @@ class ProjectUpdate(models.Model):
[('project_id', '=', project.id),
'|', ('deadline', '<', fields.Date.context_today(self) + relativedelta(years=1)), ('deadline', '=', False)])._get_data_list()
updated_milestones = self._get_last_updated_milestone(project)
created_milestones = Milestone.search(
[('project_id', '=', project.id),
('create_date', '>', fields.Datetime.now() + timedelta(days=-30))])._get_data_list()
domain = [('project_id', '=', project.id)]
if project.last_update_id.date:
domain = expression.AND([domain, [('create_date', '>=', project.last_update_id.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,
'created': created_milestones,
}
@@ -172,14 +129,21 @@ class ProjectUpdate(models.Model):
WHERE mm.model = 'project.milestone'
AND mm.message_type = 'notification'
AND pm.project_id = %(project_id)s
AND mm.date > (now() at time zone 'utc')::date - '1 month'::interval
"""
if project.last_update_id.date:
query = query + "AND mm.date <= %(last_update_date)s"
query = query + """
WINDOW w_partition AS (
PARTITION BY pm.id
ORDER BY mm.date ASC
)
ORDER BY pm.deadline ASC;
ORDER BY pm.deadline ASC
LIMIT 1;
"""
self.env.cr.execute(query, {'project_id': project.id})
query_params = {'project_id': project.id}
if project.last_update_id.date:
query_params['last_update_date'] = project.last_update_id.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}
milestones = self.env['project.milestone'].search([('id', 'in', list(mapped_result.keys()))])
@@ -188,14 +152,3 @@ class ProjectUpdate(models.Model):
'new_value': mapped_result[milestone.id]['new_value'],
'old_value': mapped_result[milestone.id]['old_value'],
} for milestone in milestones]
def action_burndown_open_tasks(self):
if not self.id or not self.project_id:
return False
action = self.env['ir.actions.act_window']._for_xml_id('project.action_project_task_burndown_chart_report')
context = {
'active_id': self.project_id.id,
'search_default_project_id': self.project_id.id,
'graph_mode': 'bar',
}
return dict(action, context=context)
@@ -11,9 +11,6 @@ export default class ProjectRightPanel extends owl.Component {
this.project_id = this.context.active_id;
this.state = useState({
data: {
tasks_analysis: {
data: []
},
milestones: {
data: []
},
@@ -48,9 +45,18 @@ export default class ProjectRightPanel extends owl.Component {
const additionalContext = JSON.parse(event.currentTarget.dataset.additional_context || "{}");
if (event.currentTarget.dataset.type === "object") {
action = await this.rpc({
model: 'project.project',
method: event.currentTarget.dataset.action,
args: [this.project_id]
// Use the call_button method in order to have an action
// with the correct view naming, i.e. list view is named
// 'list' rather than 'tree'.
route: '/web/dataset/call_button',
params: {
model: 'project.project',
method: event.currentTarget.dataset.action,
args: [this.project_id],
kwargs: {
context: this.context
}
}
});
}
this._doAction(action, {
@@ -18,6 +18,7 @@ class MilestoneComponent extends owl.Component {
});
this._dialogRef = useRef('dialog');
this._isDialogOpen = false;
this._createContext = this._createContext.bind(this);
this._onDialogSaved = this._onDialogSaved.bind(this);
}
@@ -46,6 +47,12 @@ class MilestoneComponent extends owl.Component {
}
}
_createContext() {
return Object.assign({}, {
'default_project_id': this.contextValue.active_id,
}, this.contextValue);
}
async _onDialogSaved() {
await this.__owl__.parent.willUpdateProps();
}
@@ -124,7 +131,7 @@ export class OpenMilestone extends MilestoneComponent {
args: [[this.milestone.id], !this.milestone.is_reached],
});
this.state.colorClass = this.milestone.is_deadline_exceeded ? "o_milestone_danger" : "";
this.state.checkboxIcon = this.milestone.is_reached ? "fa-check-square" : "fa-square-o";
this.state.checkboxIcon = this.milestone.is_reached ? "fa-check-square-o" : "fa-square-o";
this.write_mutex = false;
}
}
@@ -23,7 +23,7 @@ html .o_web_client > .o_action_manager > .o_action > .o_content > .o_controller_
}
.o_rightpanel {
flex: 0 0 30%;
flex: 0 0 25%;
overflow: auto;
height: 100%;
padding: $o-rightpanel-p-small $o-rightpanel-p $o-rightpanel-p*2 $o-rightpanel-p;
@@ -35,10 +35,18 @@ html .o_web_client > .o_action_manager > .o_action > .o_content > .o_controller_
padding-bottom: $o-rightpanel-p;
padding-top: $o-rightpanel-p;
&:not(:first-child){
&:nth-child(n+3){
border-top: 1px solid $gray-300;
}
&:first-child{
padding-bottom: 0px;
}
&:nth-child(2){
padding-top: 0px;
}
.o_rightpanel_header {
padding: $o-rightpanel-p-tiny 0 $o-rightpanel-p-small 0;
@@ -84,6 +92,11 @@ html .o_web_client > .o_action_manager > .o_action > .o_content > .o_controller_
float: right;
}
.o_rightpanel_center_col {
flex-grow: 1;
text-align: center;
}
.o_open_milestone {
margin-bottom: $o-rightpanel-p-tiny;
display: flex;
@@ -117,6 +130,47 @@ html .o_web_client > .o_action_manager > .o_action > .o_content > .o_controller_
.o_color_green {
color: $green;
}
.o_form_view {
.oe_button_box {
display: flex;
flex-wrap: wrap;
margin-left: -16px;
margin-right: -16px;
@media screen and (max-device-width: 768px) {
margin-top: -4px;
margin-bottom: 0px;
}
.oe_stat_button {
flex-basis: 33.333333%;
border-width: 1px;
margin-top: -1px;
margin-left: -1px;
height: 56px;
&:nth-child(3n){
border-right-width: 0px;
}
.o_stat_text {
white-space: normal;
}
}
@media screen and (max-width: 1600px) {
.oe_stat_button {
flex-basis: 50%;
&:nth-child(2n){
border-right-width: 0px;
}
}
}
}
}
}
}
@@ -43,30 +43,32 @@
<t t-name="project.ProjectRightPanel" owl="1">
<div class="o_rightpanel">
<t t-call="project.TaskSection"/>
<t t-call="project.StatButtonsSection"/>
<t t-call="project.MilestoneSection"/>
</div>
</t>
<t t-name="project.TaskSection" owl="1">
<t t-set="tasksAnalysis" t-value="state.data.tasks_analysis"/>
<div class="o_rightpanel_section" name="task_analysis">
<div class="o_rightpanel_header">
<div class="o_rightpanel_title">
Tasks Analysis
</div>
</div>
<div class="o_rightpanel_data">
<div t-foreach="tasksAnalysis.data" t-as="analysis" t-key="analysis.name" class="o_rightpanel_data_row">
<a t-if="state.data.user.is_project_user" class="btn btn-link o_rightpanel_left_col o_rightpanel_left_button" t-on-click="onProjectActionClick"
t-att-data-action="analysis.action.action"
t-att-data-additional_context="analysis.action.additional_context"
data-type="action">
<t t-esc="analysis.name"/>
</a>
<span t-else="" class="o_rightpanel_left_col o_rightpanel_left_text"><t t-esc="analysis.name"/></span>
<span class="o_rightpanel_right_col"><t t-esc="analysis.value"/></span>
<t t-name="project.StatButtonsSection" owl="1">
<div class="o_rightpanel_section">
<div class="o_form_view">
<div class="oe_button_box o_full">
<t t-foreach="state.data.buttons" t-as="button" t-key="button.order">
<button class="btn oe_stat_button" t-if="button.show"
t-on-click="onProjectActionClick"
t-att-data-action="button.action"
t-att-data-additional_context="button.additional_context"
t-att-data-type="button.action_type">
<i t-att-class="'fa fa-fw o_button_icon fa-' + button.icon"/>
<div class="o_field_widget o_stat_info">
<span class="o_stat_value">
<t t-esc="button.number"/>
</span>
<span class="o_stat_text">
<t t-esc="button.text"/>
</span>
</div>
</button>
</t>
</div>
</div>
</div>
@@ -102,11 +104,12 @@
Component="FormViewDialog"
params="{
context: context,
_createContext: _createContext,
on_saved: _onDialogSaved,
res_id: false,
res_model: 'project.milestone',
title: NEW_PROJECT_MILESTONE,
disable_multiple_selection: true,
multi_select: true,
}"
t-ref="dialog"
/>
@@ -120,8 +123,10 @@
<i class="fa" t-att-class="state.checkboxIcon"></i>
</span>
<div class="o_milestone_detail" t-on-click="onOpenMilestone">
<t t-esc="milestone.name"/>
<span class="o_rightpanel_right_col">
<span style="flex: 10%">
<t t-esc="milestone.name"/>
</span>
<span class="o_rightpanel_center_col">
<t t-esc="deadline"/>
</span>
</div>
@@ -73,9 +73,93 @@ tour.register('project_update_tour', {
extra_trigger: '.o_kanban_project_tasks',
run: "drag_and_drop .o_kanban_group:eq(1) ",
}, {
trigger: '.o_back_button',
content: 'Go back to edit the project created',
}, {
// an invisible element cannot be used as a trigger so this small hack is mandatory for the next step
trigger: '.o_kanban_record:contains("New Project")',
run: function () {
$('.o_kanban_record:contains("New Project") .o_dropdown_kanban').css('visibility', 'visible');
}
}, {
trigger: '.oe_kanban_global_click :contains("New Project") .o_dropdown_kanban',
content: 'Open the project dropdown'
}, {
trigger: '.o_kanban_record:contains("New Project") .dropdown-menu a:contains("See More")',
content: 'Start editing the project',
}, {
trigger: '.nav-item:contains("Invoicing") .nav-link',
content: 'Start the assignement of a default sale order id to the project'
}, {
trigger: '.o_form_button_edit',
content: 'Start editing the project'
}, {
trigger: 'div[name="sale_line_id"]',
content: 'Select the first sale order of the list',
run: function (actions) {
actions.text('Prepaid', this.$anchor.find('input'));
},
}, {
trigger: 'ul.ui-autocomplete > li:first-child > a',
content: 'Select the first item on the autocomplete dropdown'
}, {
trigger: '.o_form_button_save',
content: 'Save the modifications'
}, {
trigger: '.o_back_button',
content: 'Go back to the kanban view the project created',
}, {
trigger: '.oe_kanban_global_click :contains("New Project")',
content: 'Open the project'
}, {
trigger: ".o_project_updates_breadcrumb",
content: 'Open Updates'
}, {
trigger: ".oe_button_box .o_stat_text:contains('Sales Order')",
run: function () {}
}, {
trigger: ".o_rightpanel_title:eq(0):contains('Sold')",
content: 'Check the user sees Sold section',
run: function () {}
}, {
trigger: ".o_rightpanel_left_text:eq(0):contains('Prepaid Hours')",
content: 'Check the user sees Sold subsection',
run: function () {},
}, {
trigger: ".o_rightpanel_title:eq(1):contains('Total Sold')",
content: 'Check the user sees Total Sold section',
run: function () {}
}, {
trigger: ".o_rightpanel_header:eq(1) .o_rightpanel_right_col:contains('Hours')",
content: 'Check the user sees Total Sold hours title',
run: function () {},
}, {
trigger: ".o_rightpanel_section:eq(2) .o_rightpanel_data_row:contains('Effective')",
content: 'Check the user sees Total Sold subsection row',
run: function () {},
}, {
trigger: ".o_rightpanel_section:eq(2) .o_rightpanel_data_row:contains('Remaining')",
content: 'Check the user sees Total Sold subsection row',
run: function () {},
}, {
trigger: ".o_rightpanel_title:eq(2):contains('Profitability')",
content: 'Check the user sees Profitability section',
run: function () {}
}, {
trigger: ".o_rightpanel_section:eq(3) .o_rightpanel_data_row:contains('Revenues')",
content: 'Check the user sees Profitability subsection row',
run: function () {},
}, {
trigger: ".o_rightpanel_section:eq(3) .o_rightpanel_data_row:contains('Costs')",
content: 'Check the user sees Profitability subsection row',
run: function () {},
}, {
trigger: ".o_rightpanel_section:eq(3) .o_rightpanel_data_row:contains('Margin')",
content: 'Check the user sees Profitability subsection row',
run: function () {},
}, {
trigger: ".o_add_milestone a",
content: "Add a first milestone"
}, {
trigger: "input.o_field_widget[name=name]",
run: 'text New milestone'
@@ -91,22 +175,13 @@ tour.register('project_update_tour', {
run: 'text Second milestone'
}, {
trigger: "input.datetimepicker-input[name=deadline]",
run: 'text 12/12/2021'
run: 'text 12/12/2022'
}, {
trigger: ".modal-footer button"
}, {
trigger: ".o_rightpanel_left_button:eq(0)"
}, {
trigger: ".breadcrumb :contains('Tasks Analysis')",
}, {
trigger: ".o_back_button"
}, {
trigger: ".o_rightpanel_left_button:eq(1)"
}, {
trigger: ".breadcrumb :contains('Burndown Chart')",
}, {
trigger: ".o_back_button"
}, {
trigger: '.o_back_button',
content: 'Go back to edit to the list view',
trigger: ".o_open_milestone:eq(1) .o_milestone_detail span:eq(0)",
extra_trigger: ".o_add_milestone a",
run: function () {
@@ -120,25 +195,35 @@ tour.register('project_update_tour', {
}, {
trigger: ".modal-footer button"
}, {
trigger: ".o-kanban-button-new"
trigger: ".o-kanban-button-new",
content: "Create a new update"
}, {
trigger: "input.o_field_widget[name=name]",
run: 'text New update'
}, {
trigger: ".o_form_button_save"
}, {
trigger: ".o_field_widget[name=description] :contains('1 tasks are open, out of 2.')",
trigger: ".o_field_widget[name=description] h1:contains('Activities')",
run: function () {},
}, {
trigger: ".o_field_widget[name=description] :contains('2 were created')",
trigger: ".o_field_widget[name=description] h3:contains('Sold')",
run: function () {},
}, {
trigger: ".o_field_widget[name=description] td:contains('Customer Care')",
run: function () {},
}, {
trigger: ".o_field_widget[name=description] :contains('1 was closed')",
trigger: ".o_field_widget[name=description] h3:contains('Profitability')",
run: function () {},
}, {
trigger: ".o_field_widget[name=description] :contains('New milestone') :contains('(due on 12/12/2100)')",
trigger: ".o_field_widget[name=description] h3:contains('Milestones')",
run: function () {},
}, {
trigger: ".o_field_widget[name=description] :contains('New milestone') :contains('(12/12/2099 => 12/12/2100)')",
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)')",
run: function () {},
}, {
trigger: ".o_field_widget[name=description] div[name='milestone'] ul li span:contains('(due on 12/12/2100)')",
run: function () {},
}]);
@@ -26,8 +26,7 @@ class TestProjectUpdate(TestProjectCommon):
self.assertEqual(update.user_id, self.env.user, "The author is the user who created the update.")
self.assertNotEqual(len(update.description), 0, "The description should not be empty.")
self.assertTrue("Tasks" in update.description, "The description should contain 'Tasks'.")
self.assertTrue("Milestone" in update.description, "The description should contain 'Milestone'.")
self.assertTrue("Activities" in update.description, "The description should contain 'Activities'.")
self.assertEqual(update.status, 'on_track', "The status should be the default one.")
with Form(self.env['project.update'].with_context({'default_project_id': self.project_pigs.id})) as update_form:
@@ -48,11 +47,6 @@ class TestProjectUpdate(TestProjectCommon):
template_values = self.env['project.update']._get_template_values(self.project_pigs)
self.assertEqual(template_values['tasks']['open_tasks'], 2, "Open tasks should be equal to 2")
self.assertEqual(template_values['tasks']['total_tasks'], 2, "Total tasks should be equal to 2")
self.assertEqual(template_values['tasks']['created_tasks'], 2, "Created tasks should be equal to 2")
self.assertEqual(template_values['tasks']['closed_tasks'], 0, "Closed tasks should be equal to 0")
self.assertEqual(len(template_values['milestones']['list']), 2, "Milestone list length should be equal to 2")
self.assertEqual(len(template_values['milestones']['created']), 3, "Milestone created length tasks should be equal to 3")
@@ -70,7 +64,6 @@ class TestProjectUpdate(TestProjectCommon):
panel_data = self.project_pigs.get_panel_data()
self.assertEqual(len(panel_data['tasks_analysis']['data']), 2, "Panel data should contain 'tasks' entry")
self.assertEqual(len(panel_data['milestones']['data']), 3, "Panel data should contain 'milestone' entry")
self.assertFalse(panel_data['milestones']['data'][0]['is_deadline_exceeded'], "Milestone is achieved")
self.assertTrue(panel_data['milestones']['data'][1]['is_deadline_exceeded'], "Milestone is exceeded")
@@ -10,23 +10,19 @@
<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="tasks">
<h1>Tasks</h1>
<a t-if="user.has_group('project.group_project_manager')" type="object" t-att-href="tasks['action']['url_default']" t-att-name="tasks['action']['name']"><t t-esc="tasks['open_tasks']"/> tasks are open</a><t t-else=""><t t-esc="tasks['open_tasks']"/> tasks are open</t>, out of <t t-esc="tasks['total_tasks']"/>. In the last 30 days,
<ul>
<li t-if="tasks['created_tasks'] > 1"><t t-esc="tasks['created_tasks']"/> were created</li>
<li t-else=""><t t-esc="tasks['created_tasks']"/> was created</li>
<li t-if="tasks['closed_tasks'] > 1"><t t-esc="tasks['closed_tasks']"/> were closed (folded stage)</li>
<li t-else=""><t t-esc="tasks['closed_tasks']"/> was closed (folded stage)</li>
</ul>
<div name="summary">
<h1>Sprint Summary</h1>
<p>How’s this project going?</p><br/>
</div>
<hr t-if="milestones['show_section']"/>
<div name="activities" t-if="milestones['show_section']">
<h1>Activities</h1>
</div>
<div name="milestone">
<t t-if="milestones['show_section']">
<h1>Milestones</h1>
<br/>
<h3><b><u>Milestones</u></b></h3>
<ul class="o_checklist" t-if="milestones['list']">
<t t-foreach="milestones['list']" t-as="milestone" t-key="milestone['id']">
@@ -39,8 +35,8 @@
</ul>
<t t-if="milestones['updated']">
<t t-if="len(milestones['updated']) > 1">Over the last 30 days, the deadline of the following milestones has been updated :</t>
<t t-else="">Over the last 30 days, the deadline of the following milestone has been updated :</t>
<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>
<ul>
<t t-foreach="milestones['updated']" t-as="milestone" t-key="milestone['id']">
<li>
@@ -51,8 +47,8 @@
</t>
<t t-if="milestones['created']">
<t t-if="len(milestones['created']) > 1">The following milestones have been added :</t>
<t t-else="">The following milestone has been added :</t>
<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>
<ul>
<t t-foreach="milestones['created']" t-as="milestone" t-key="milestone['id']">
<li>
@@ -35,7 +35,11 @@
</group>
</group>
<separator/>
<field name="description" widget="html_with_action" nolabel="1" class="o_project_update_description"/>
<notebook>
<page string="Description" name="description">
<field name="description" widget="html_with_action" nolabel="1" class="o_project_update_description"/>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" options="{'post_refresh':True}" groups="base.group_user"/>
@@ -124,7 +128,7 @@
<group>
<group>
<field name="project_id" invisible="1"/>
<field name="name"/>
<field name="name" placeholder="E.g: Product Launch"/>
<field name="deadline"/>
<field name="is_reached"/>
</group>
+18 -18
View File
@@ -274,14 +274,6 @@
<sheet string="Project">
<div class="oe_button_box" name="button_box" groups="base.group_user">
<field name="currency_id" invisible="1"/>
<button class="oe_stat_button" type="object" name="action_view_analytic_account_entries" icon="fa-usd" attrs="{'invisible': [('analytic_account_id', '=', False)]}" groups="analytic.group_analytic_accounting">
<div class="o_form_field o_stat_info">
<span class="o_stat_text">Gross Margin</span>
<span class="o_stat_value">
<field name="analytic_account_balance" widget='monetary'/>
</span>
</div>
</button>
<button class="oe_stat_button" type="action"
name="%(act_project_project_2_project_task_all)d" icon="fa-tasks">
<field string="Tasks In Progress" name="task_count" widget="statinfo" options="{'label_field': 'label_tasks'}"/>
@@ -312,16 +304,12 @@
Burndown Chart
</span>
</button>
<button class="oe_stat_button" name="%(project.project_update_all_action)d" type="action" groups="project.group_project_manager">
<div class="pl-4">
<field name="last_update_color" invisible="1"/>
<field name="last_update_status" widget="status_with_color" options="{'color_field': 'last_update_color'}"/>
</div>
</button>
<button class="oe_stat_button o_project_not_clickable" disabled="disabled" groups="!project.group_project_manager">
<div class="pl-4">
<field name="last_update_color" invisible="1"/>
<field name="last_update_status" widget="status_with_color" options="{'color_field': 'last_update_color', 'no_quick_edit': 1}"/>
<button class="oe_stat_button" type="object" name="action_view_analytic_account_entries" icon="fa-usd" attrs="{'invisible': [('analytic_account_id', '=', False)]}" groups="analytic.group_analytic_accounting">
<div class="o_form_field o_stat_info">
<span class="o_stat_value">
<field name="analytic_account_balance"/>
</span>
<span class="o_stat_text">Gross Margin</span>
</div>
</button>
<button class="oe_stat_button" name="%(project.project_collaborator_action)d" type="action" icon="fa-users" groups="project.group_project_manager">
@@ -334,6 +322,18 @@
</span>
</div>
</button>
<button class="oe_stat_button" name="%(project.project_update_all_action)d" type="action" groups="project.group_project_manager">
<div class="pl-4">
<field name="last_update_color" invisible="1"/>
<field name="last_update_status" widget="status_with_color" options="{'color_field': 'last_update_color'}"/>
</div>
</button>
<button class="oe_stat_button o_project_not_clickable" disabled="disabled" groups="!project.group_project_manager">
<div class="pl-4">
<field name="last_update_color" invisible="1"/>
<field name="last_update_status" widget="status_with_color" options="{'color_field': 'last_update_color', 'no_quick_edit': 1}"/>
</div>
</button>
</div>
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<div class="oe_title">
@@ -32,22 +32,6 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" id="track_customer_satisfaction_setting">
<div class="o_setting_left_pane">
<field name="group_project_rating"/>
</div>
<div class="o_setting_right_pane">
<label for="group_project_rating"/>
<div class="text-muted">
Track customer satisfaction on tasks
</div>
<div class="content-group" attrs="{'invisible': [('group_project_rating', '=', False)]}">
<div class="mt8">
<button name="%(project.open_task_type_form)d" icon="fa-arrow-right" type="action" string="Set a Rating Email Template on Stages" class="btn-link"/>
</div>
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" id="recurring_tasks_setting">
<div class="o_setting_left_pane">
<field name="group_project_recurring_tasks"/>
@@ -112,6 +96,36 @@
</div>
</div>
</div>
<h2>Analytics</h2>
<div class="row mt16 o_settings_container" name="analytic">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="group_analytic_accounting"/>
</div>
<div class="o_setting_right_pane">
<label for="group_analytic_accounting" string="Profitability"/>
<div class="text-muted">
Track the costs and revenues linked to your projects
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" id="track_customer_satisfaction_setting">
<div class="o_setting_left_pane">
<field name="group_project_rating"/>
</div>
<div class="o_setting_right_pane">
<label for="group_project_rating"/>
<div class="text-muted">
Track customer satisfaction on tasks
</div>
<div class="content-group" attrs="{'invisible': [('group_project_rating', '=', False)]}">
<div class="mt8">
<button name="%(project.open_task_type_form)d" icon="fa-arrow-right" type="action" string="Set a Rating Email Template on Stages" class="btn-link"/>
</div>
</div>
</div>
</div>
</div>
</div>
</xpath>
</field>
@@ -6,7 +6,7 @@
</button>
<t t-inherit="project.ProjectRightPanel" t-inherit-mode="extension" owl="1">
<xpath expr="//t[@t-call='project.TaskSection']" position="before">
<xpath expr="//t[@t-call='project.StatButtonsSection']" position="before">
<t t-call="sale_timesheet.ProfitabilitySection"/>
</xpath>
</t>