From 1ec2b3c0b04ca30b96585611390c8b6b0d294ea0 Mon Sep 17 00:00:00 2001 From: "Cedric Prieels (cpr)" Date: Thu, 2 Sep 2021 12:49:40 +0200 Subject: [PATCH 01/11] [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 --- addons/project/models/project.py | 60 ++++++++- addons/project/models/project_update.py | 79 +++-------- .../src/js/right_panel/project_right_panel.js | 18 ++- .../src/js/right_panel/project_utils.js | 9 +- .../static/src/scss/project_rightpanel.scss | 58 ++++++++- .../static/src/xml/project_templates.xml | 51 ++++---- .../tests/tours/project_update_tour_tests.js | 123 +++++++++++++++--- .../project/tests/test_project_update_flow.py | 9 +- .../views/project_update_templates.xml | 28 ++-- addons/project/views/project_update_views.xml | 8 +- addons/project/views/project_views.xml | 36 ++--- .../views/res_config_settings_views.xml | 46 ++++--- .../static/src/xml/sale_project_templates.xml | 2 +- 13 files changed, 351 insertions(+), 176 deletions(-) diff --git a/addons/project/models/project.py b/addons/project/models/project.py index b8895e7a484..02415e1c66f 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -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 diff --git a/addons/project/models/project_update.py b/addons/project/models/project_update.py index 886a96d89d8..ebcc9ff62ac 100644 --- a/addons/project/models/project_update.py +++ b/addons/project/models/project_update.py @@ -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) diff --git a/addons/project/static/src/js/right_panel/project_right_panel.js b/addons/project/static/src/js/right_panel/project_right_panel.js index 7ecf7288e6e..f47cb60a287 100644 --- a/addons/project/static/src/js/right_panel/project_right_panel.js +++ b/addons/project/static/src/js/right_panel/project_right_panel.js @@ -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, { diff --git a/addons/project/static/src/js/right_panel/project_utils.js b/addons/project/static/src/js/right_panel/project_utils.js index 719dfd06dfb..a1959945c23 100644 --- a/addons/project/static/src/js/right_panel/project_utils.js +++ b/addons/project/static/src/js/right_panel/project_utils.js @@ -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; } } diff --git a/addons/project/static/src/scss/project_rightpanel.scss b/addons/project/static/src/scss/project_rightpanel.scss index 4755f7e9f2c..75126215e29 100644 --- a/addons/project/static/src/scss/project_rightpanel.scss +++ b/addons/project/static/src/scss/project_rightpanel.scss @@ -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; + } + } + } + + } + } } } diff --git a/addons/project/static/src/xml/project_templates.xml b/addons/project/static/src/xml/project_templates.xml index 5a43c2f4b6a..55aad92746c 100644 --- a/addons/project/static/src/xml/project_templates.xml +++ b/addons/project/static/src/xml/project_templates.xml @@ -43,30 +43,32 @@
- +
- - -
-
-
- Tasks Analysis -
-
- -
-
- - - - - + +
+
+
+ + +
@@ -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 @@
- - + + + +
diff --git a/addons/project/static/tests/tours/project_update_tour_tests.js b/addons/project/static/tests/tours/project_update_tour_tests.js index 5227a4e960b..ae9c3fe0ccf 100644 --- a/addons/project/static/tests/tours/project_update_tour_tests.js +++ b/addons/project/static/tests/tours/project_update_tour_tests.js @@ -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 () {}, }]); diff --git a/addons/project/tests/test_project_update_flow.py b/addons/project/tests/test_project_update_flow.py index 18179a572dd..4435f265197 100644 --- a/addons/project/tests/test_project_update_flow.py +++ b/addons/project/tests/test_project_update_flow.py @@ -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") diff --git a/addons/project/views/project_update_templates.xml b/addons/project/views/project_update_templates.xml index 459b1e56ec6..42a21c0565a 100644 --- a/addons/project/views/project_update_templates.xml +++ b/addons/project/views/project_update_templates.xml @@ -10,23 +10,19 @@