From 1a44b849970d3e39111a99ea35db5b586b184dd3 Mon Sep 17 00:00:00 2001 From: "Bastien (bvdn)" Date: Thu, 6 Apr 2023 12:55:29 +0000 Subject: [PATCH] [IMP] project: improve various mark as done features New features developed in this PR: - Write Javascript tests for the project.task.state widget - Tests are in the project_task_state_selection.js - rewrite Research & Development demo data - Created a digest tip for task state selection (in Settings > technical > digest tips) - Tip: Use task state to keep track of the task progression - Fix Sharingview (Readonly) icons for the project.task.state widget, previously only the color bubble were displayed in readonly sharing views, now all of the icons are displayed - Display state in the Calendar view - Display the same widget as in the kanban/list views - Remove is_closed References - Adapt Kanban stages Exemples - Kanban exemples are available when creating a new stage in kanban view - improve Progressbar colors (different color for approved/done, in_progress/waiting) Task-3213526 closes odoo/odoo#117968 Related: odoo/enterprise#39714 Related: odoo/upgrade#4548 Signed-off-by: Xavier Bol (xbo) --- addons/hr_timesheet/models/project_project.py | 2 +- addons/project/data/digest_data.xml | 15 ++++ addons/project/data/project_demo.xml | 14 ++-- addons/project/models/__init__.py | 4 +- addons/project/models/project_milestone.py | 17 +++-- addons/project/models/project_project.py | 6 +- addons/project/models/project_task.py | 4 +- addons/project/report/project_report.py | 5 +- .../project_task_burndown_chart_report.py | 11 ++- ...oject_task_burndown_chart_report_views.xml | 4 +- .../project_task_state_selection.js | 13 +++- .../project_task_state_selection.xml | 10 ++- .../static/src/img/project-custom-tasks.gif | Bin 18530 -> 0 bytes .../project/static/src/img/task-state-img.png | Bin 0 -> 66542 bytes .../src/js/project_task_kanban_examples.js | 22 +++--- .../project_task_kanban_view.scss | 5 ++ .../src/xml/project_task_kanban_examples.xml | 30 +++++--- .../project_task_state_selection_tests.js | 67 ++++++++++++++++++ .../project_portal_project_task_templates.xml | 2 +- .../project_sharing_project_task_views.xml | 13 ++-- addons/project/views/project_task_views.xml | 39 +++++----- .../project_todo/views/project_task_views.xml | 14 ++-- addons/sale_project/models/project.py | 6 +- 23 files changed, 208 insertions(+), 95 deletions(-) delete mode 100644 addons/project/static/src/img/project-custom-tasks.gif create mode 100644 addons/project/static/src/img/task-state-img.png create mode 100644 addons/project/static/tests/project_task_state_selection_tests.js diff --git a/addons/hr_timesheet/models/project_project.py b/addons/hr_timesheet/models/project_project.py index 33ec9633f51..185f37af0ea 100644 --- a/addons/hr_timesheet/models/project_project.py +++ b/addons/hr_timesheet/models/project_project.py @@ -102,7 +102,7 @@ class Project(models.Model): WHERE Project.allocated_hours > 0 AND Project.allow_timesheets = TRUE AND Task.parent_id IS NULL - AND Task.is_closed IS FALSE + AND Task.state NOT IN ('1_done', '1_canceled') GROUP BY Project.id HAVING Project.allocated_hours - SUM(Task.effective_hours) < 0 """ diff --git a/addons/project/data/digest_data.xml b/addons/project/data/digest_data.xml index 1ece6d346ff..7fba15b1a85 100644 --- a/addons/project/data/digest_data.xml +++ b/addons/project/data/digest_data.xml @@ -7,6 +7,21 @@ + + Tip: Use task states to keep track of your tasks' progression + 1200 + + +
+

Tip: Use task states to keep track of your tasks' progression

+

+ Quickly check the status of tasks for approvals or change requests and identify those on hold until dependencies are resolved with the hourglass icon. +

+ +
+
+
+ Tip: Create tasks from incoming emails 1300 diff --git a/addons/project/data/project_demo.xml b/addons/project/data/project_demo.xml index 764fb7ddd36..22bd574fc3e 100644 --- a/addons/project/data/project_demo.xml +++ b/addons/project/data/project_demo.xml @@ -725,6 +725,7 @@ 0 Customer analysis + Architecture + 1_done 7 @@ -771,9 +772,10 @@ Basic outline + 1_done + - project.task @@ -818,10 +820,10 @@ 1 Planning and budget - + 1_done + 6 - @@ -916,7 +918,6 @@ Send it ASAP, its urgent. Social network integration Facebook and Twitter integration - 1_canceled @@ -979,10 +980,11 @@ Send it ASAP, its urgent. New portal system 0 + 1_done - + project.task @@ -1060,7 +1062,7 @@ Send it ASAP, its urgent. Document management - 1_done + 1_canceled diff --git a/addons/project/models/__init__.py b/addons/project/models/__init__.py index 229858414cc..2421fc3c272 100644 --- a/addons/project/models/__init__.py +++ b/addons/project/models/__init__.py @@ -3,11 +3,11 @@ from . import account_analytic_account from . import mail_message -from . import project_milestone from . import project_project_stage from . import project_task_recurrence -# `project_task_stage_personal` has to be loaded before `project` +# `project_task_stage_personal` has to be loaded before `project_project` and `project_milestone` from . import project_task_stage_personal +from . import project_milestone from . import project_project from . import project_task from . import project_task_type diff --git a/addons/project/models/project_milestone.py b/addons/project/models/project_milestone.py index b60f5e435ff..1c9bb0e157d 100644 --- a/addons/project/models/project_milestone.py +++ b/addons/project/models/project_milestone.py @@ -5,6 +5,8 @@ from collections import defaultdict from odoo import api, fields, models +from .project_task import CLOSED_STATES + class ProjectMilestone(models.Model): _name = 'project.milestone' _description = "Project Milestone" @@ -47,10 +49,10 @@ class ProjectMilestone(models.Model): @api.depends('task_ids.milestone_id') def _compute_task_count(self): all_and_done_task_count_per_milestone = { - milestone.id: (count, is_closed_list.count(True)) - for milestone, count, is_closed_list in self.env['project.task']._read_group( + milestone.id: (count, sum(state in CLOSED_STATES for state in state_list)) + for milestone, count, state_list in self.env['project.task']._read_group( [('milestone_id', 'in', self.ids), ('allow_milestones', '=', True)], - ['milestone_id'], ['__count', 'is_closed:array_agg'], + ['milestone_id'], ['__count', 'state:array_agg'], ) } for milestone in self: @@ -59,19 +61,20 @@ class ProjectMilestone(models.Model): def _compute_can_be_marked_as_done(self): if not any(self._ids): for milestone in self: - milestone.can_be_marked_as_done = not milestone.is_reached and all(milestone.task_ids.is_closed) + milestone.can_be_marked_as_done = not milestone.is_reached and all(milestone.task_ids.mapped(lambda t: t.state in CLOSED_STATES)) return + unreached_milestones = self.filtered(lambda milestone: not milestone.is_reached) (self - unreached_milestones).can_be_marked_as_done = False task_read_group = self.env['project.task']._read_group( [('milestone_id', 'in', unreached_milestones.ids)], - ['milestone_id', 'is_closed'], + ['milestone_id', 'state'], ['__count'], ) task_count_per_milestones = defaultdict(lambda: (0, 0)) - for milestone, is_closed, count in task_read_group: + for milestone, state, count in task_read_group: opened_task_count, closed_task_count = task_count_per_milestones[milestone.id] - if is_closed: + if state in CLOSED_STATES: closed_task_count += count else: opened_task_count += count diff --git a/addons/project/models/project_project.py b/addons/project/models/project_project.py index 04bafdebcc4..e2ee58991be 100644 --- a/addons/project/models/project_project.py +++ b/addons/project/models/project_project.py @@ -11,7 +11,7 @@ from odoo.addons.rating.models import rating_data from odoo.tools.misc import get_lang from .project_update import STATUS_COLOR - +from .project_task import CLOSED_STATES class Project(models.Model): _name = "project.project" @@ -55,7 +55,7 @@ class Project(models.Model): for project, count in self.env['project.task'].with_context( active_test=any(project.active for project in self) )._read_group( - [('is_closed', '=', False), ('project_id', 'in', self.ids)], + [('state', 'not in', list(CLOSED_STATES)), ('project_id', 'in', self.ids)], ['project_id'], ['__count'], ) @@ -128,7 +128,7 @@ class Project(models.Model): type_ids = fields.Many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', string='Tasks Stages') task_count = fields.Integer(compute='_compute_task_count', string="Task Count") task_ids = fields.One2many('project.task', 'project_id', string='Tasks', - domain=[('is_closed', '=', False)]) + domain=[('state', 'not in', list(CLOSED_STATES))]) color = fields.Integer(string='Color Index') user_id = fields.Many2one('res.users', string='Project Manager', default=lambda self: self.env.user, tracking=True) alias_enabled = fields.Boolean(string='Use Email Alias', compute='_compute_alias_enabled', readonly=False) diff --git a/addons/project/models/project_task.py b/addons/project/models/project_task.py index 3901dcb7514..fe75eedc0a8 100644 --- a/addons/project/models/project_task.py +++ b/addons/project/models/project_task.py @@ -50,7 +50,6 @@ PROJECT_TASK_WRITABLE_FIELDS = { 'date_last_stage_update', 'tag_ids', 'sequence', - 'is_closed', 'stage_id', 'child_ids', 'parent_id', @@ -133,7 +132,7 @@ class Task(models.Model): ('03_approved', 'Approved'), *CLOSED_STATES.items(), ('04_waiting_normal', 'Waiting'), - ], string='Status', copy=False, default='01_in_progress', required=True, compute='_compute_state', inverse='_inverse_state', readonly=False, store=True, recursive=True, tracking=True) + ], string='State', copy=False, default='01_in_progress', required=True, compute='_compute_state', inverse='_inverse_state', readonly=False, store=True, recursive=True, tracking=True) create_date = fields.Datetime("Created On", readonly=True) write_date = fields.Datetime("Last Updated On", readonly=True) @@ -190,7 +189,6 @@ class Task(models.Model): # In the domain of displayed_image_id, we couln't use attachment_ids because a one2many is represented as a list of commands so we used res_model & res_id displayed_image_id = fields.Many2one('ir.attachment', domain="[('res_model', '=', 'project.task'), ('res_id', '=', id), ('mimetype', 'ilike', 'image')]", string='Cover Image') - is_closed = fields.Boolean(compute='_compute_is_closed', string="Closed State", store=True, index=True) parent_id = fields.Many2one('project.task', string='Parent Task', index=True) child_ids = fields.One2many('project.task', 'parent_id', string="Sub-tasks", domain="[('recurring_task', '=', False)]") subtask_count = fields.Integer("Sub-task Count", compute='_compute_subtask_count') diff --git a/addons/project/report/project_report.py b/addons/project/report/project_report.py index 94110e99218..2be1f410d38 100644 --- a/addons/project/report/project_report.py +++ b/addons/project/report/project_report.py @@ -42,11 +42,10 @@ class ReportProjectTaskUser(models.Model): ('03_approved', 'Approved'), ('1_canceled', 'Canceled'), ('02_changes_requested', 'Changes Requested'), - ], string='Status', readonly=True) + ], string='State', readonly=True) company_id = fields.Many2one('res.company', string='Company', readonly=True) partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) stage_id = fields.Many2one('project.task.type', string='Stage', readonly=True) - is_closed = fields.Boolean("Closing Stage", readonly=True, help="Folded in Kanban stages are closing stages.") task_id = fields.Many2one('project.task', string='Tasks', readonly=True) active = fields.Boolean(readonly=True) tag_ids = fields.Many2many('project.tags', relation='project_tags_project_task_rel', @@ -81,7 +80,6 @@ class ReportProjectTaskUser(models.Model): t.partner_id, t.parent_id, t.stage_id, - t.is_closed, t.state, t.milestone_id, CASE WHEN pm.id IS NOT NULL THEN true ELSE false END as has_late_and_unreached_milestone, @@ -113,7 +111,6 @@ class ReportProjectTaskUser(models.Model): t.partner_id, t.parent_id, t.stage_id, - t.is_closed, t.state, t.rating_last_value, t.working_days_close, diff --git a/addons/project/report/project_task_burndown_chart_report.py b/addons/project/report/project_task_burndown_chart_report.py index 8c0d010e0fb..121988e7a16 100644 --- a/addons/project/report/project_task_burndown_chart_report.py +++ b/addons/project/report/project_task_burndown_chart_report.py @@ -17,7 +17,14 @@ class ReportProjectTaskBurndownChart(models.AbstractModel): date_assign = fields.Datetime(string='Assignment Date', readonly=True) date_deadline = fields.Date(string='Deadline', readonly=True) date_last_stage_update = fields.Date(string='Last Stage Update', readonly=True) - is_closed = fields.Boolean("Closing Stage", readonly=True) + state = fields.Selection([ + ('01_in_progress', 'In Progress'), + ('1_done', 'Done'), + ('04_waiting_normal', 'Waiting'), + ('03_approved', 'Approved'), + ('1_canceled', 'Canceled'), + ('02_changes_requested', 'Changes Requested'), + ], string='State', readonly=True) milestone_id = fields.Many2one('project.milestone', readonly=True) partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) project_id = fields.Many2one('project.project', readonly=True) @@ -38,7 +45,7 @@ class ReportProjectTaskBurndownChart(models.AbstractModel): 'date_assign', 'date_deadline', 'date_last_stage_update', - 'is_closed', + 'state', 'milestone_id', 'partner_id', 'project_id', diff --git a/addons/project/report/project_task_burndown_chart_report_views.xml b/addons/project/report/project_task_burndown_chart_report_views.xml index a9385dc76e0..9fe7386aab8 100644 --- a/addons/project/report/project_task_burndown_chart_report_views.xml +++ b/addons/project/report/project_task_burndown_chart_report_views.xml @@ -21,8 +21,8 @@ - - + + diff --git a/addons/project/static/src/components/project_task_state_selection/project_task_state_selection.js b/addons/project/static/src/components/project_task_state_selection/project_task_state_selection.js index 91bb4cae414..ecd2c59c657 100644 --- a/addons/project/static/src/components/project_task_state_selection/project_task_state_selection.js +++ b/addons/project/static/src/components/project_task_state_selection/project_task_state_selection.js @@ -6,6 +6,7 @@ import { } from "@web/views/fields/state_selection/state_selection_field"; import { sprintf } from "@web/core/utils/strings"; import { useCommand } from "@web/core/commands/command_hook"; +import { formatSelection } from "@web/views/fields/formatters"; import { registry } from "@web/core/registry"; @@ -92,6 +93,14 @@ export class ProjectTaskStateSelection extends StateSelectionField { return this.options; } + get label() { + const fullSelection = [...this.options]; + fullSelection.push(["04_waiting_normal", "Waiting"]); + return formatSelection(this.currentValue, { + selection: fullSelection, + }); + } + stateIcon(value) { return this.icons[value] || ""; } @@ -122,14 +131,14 @@ export class ProjectTaskStateSelection extends StateSelectionField { } getDropdownPosition() { - if (this.isView(['kanban', 'list']) || this.env.isSmall) { + if (this.isView(['kanban', 'list', 'calendar']) || this.env.isSmall) { return ''; } return 'bottom-end'; } getTogglerClass(currentValue) { - if (this.isView(['kanban', 'list']) || this.env.isSmall) { + if (this.isView(['kanban', 'list', 'calendar']) || this.env.isSmall) { return 'btn btn-link d-flex p-0'; } return 'o_state_button btn rounded-pill ' + this.colorButton[currentValue]; diff --git a/addons/project/static/src/components/project_task_state_selection/project_task_state_selection.xml b/addons/project/static/src/components/project_task_state_selection/project_task_state_selection.xml index a8e50fbad33..054fee72969 100644 --- a/addons/project/static/src/components/project_task_state_selection/project_task_state_selection.xml +++ b/addons/project/static/src/components/project_task_state_selection/project_task_state_selection.xml @@ -7,9 +7,13 @@ {{ stateIcon(currentValue) }} {{ statusColor(currentValue) }} + + cursor: default; + label + - + @@ -18,7 +22,7 @@ -