[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:
committed by
Laurent Stukkens (LTU)
parent
94018575c6
commit
1ec2b3c0b0
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user