diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py index ef03cb8f296..87d37668020 100644 --- a/addons/project/__manifest__.py +++ b/addons/project/__manifest__.py @@ -38,6 +38,7 @@ 'data/project_mail_template_data.xml', 'data/project_data.xml', 'wizard/project_delete_wizard_views.xml', + 'wizard/project_task_type_delete_views.xml', ], 'demo': ['data/project_demo.xml'], 'test': [ diff --git a/addons/project/models/project.py b/addons/project/models/project.py index ed79097320d..2e73c760ab4 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -18,6 +18,7 @@ class ProjectTaskType(models.Model): default_project_id = self.env.context.get('default_project_id') return [default_project_id] if default_project_id else None + active = fields.Boolean('Active', default=True) name = fields.Char(string='Stage Name', required=True, translate=True) description = fields.Text(translate=True) sequence = fields.Integer(default=1) @@ -49,16 +50,35 @@ class ProjectTaskType(models.Model): " * A good feedback from the customer will update the kanban state to 'ready for the new stage' (green bullet).\n" " * A medium or a bad feedback will set the kanban state to 'blocked' (red bullet).\n") - def unlink(self): - stages = self - default_project_id = self.env.context.get('default_project_id') - if default_project_id: - shared_stages = self.filtered(lambda x: len(x.project_ids) > 1 and default_project_id in x.project_ids.ids) - tasks = self.env['project.task'].with_context(active_test=False).search([('project_id', '=', default_project_id), ('stage_id', 'in', self.ids)]) - if shared_stages and not tasks: - shared_stages.write({'project_ids': [(3, default_project_id)]}) - stages = self.filtered(lambda x: x not in shared_stages) - return super(ProjectTaskType, stages).unlink() + def unlink_wizard(self, stage_view=False): + self = self.with_context(active_test=False) + # retrieves all the projects with a least 1 task in that stage + # a task can be in a stage even if the project is not assigned to the stage + readgroup = self.with_context(active_test=False).env['project.task'].read_group([('stage_id', 'in', self.ids)], ['project_id'], ['project_id']) + project_ids = list(set([project['project_id'][0] for project in readgroup] + self.project_ids.ids)) + + wizard = self.with_context(project_ids=project_ids).env['project.task.type.delete.wizard'].create({ + 'project_ids': project_ids, + 'stage_ids': self.ids + }) + + context = dict(self.env.context) + context['stage_view'] = stage_view + return { + 'name': _('Delete Stage'), + 'view_mode': 'form', + 'res_model': 'project.task.type.delete.wizard', + 'views': [(self.env.ref('project.view_project_task_type_delete_wizard').id, 'form')], + 'type': 'ir.actions.act_window', + 'res_id': wizard.id, + 'target': 'new', + 'context': context, + } + + def write(self, vals): + if 'active' in vals and not vals['active']: + self.env['project.task'].search([('stage_id', 'in', self.ids)]).write({'active': False}) + return super(ProjectTaskType, self).write(vals) class Project(models.Model): diff --git a/addons/project/security/ir.model.access.csv b/addons/project/security/ir.model.access.csv index f45dddfe5c3..53661ef75fc 100644 --- a/addons/project/security/ir.model.access.csv +++ b/addons/project/security/ir.model.access.csv @@ -23,3 +23,4 @@ access_account_analytic_account_user,account.analytic.account,analytic.model_acc access_account_analytic_account_manager,account.analytic.account,analytic.model_account_analytic_account,project.group_project_manager,1,1,1,1 access_account_analytic_line_project,account.analytic.line project,analytic.model_account_analytic_line,project.group_project_manager,1,1,1,1 access_project_delete_wizard,access_project_delete_wizard,model_project_delete_wizard,project.group_project_manager,1,1,1,1 +access_project_task_type_delete_wizard,project.task.type.delete.wizard,model_project_task_type_delete_wizard,project.group_project_manager,1,1,1,1 diff --git a/addons/project/static/src/js/project_kanban.js b/addons/project/static/src/js/project_kanban.js new file mode 100644 index 00000000000..812bfc165bc --- /dev/null +++ b/addons/project/static/src/js/project_kanban.js @@ -0,0 +1,48 @@ +odoo.define('project.project_kanban', function (require) { +'use strict'; + +var KanbanController = require('web.KanbanController'); +var KanbanView = require('web.KanbanView'); +var KanbanColumn = require('web.KanbanColumn'); +var view_registry = require('web.view_registry'); + +var ProjectKanbanController = KanbanController.extend({ + custom_events: _.extend({}, KanbanController.prototype.custom_events, { + 'kanban_column_delete_wizard': '_onDeleteColumnWizard', + }), + + _onDeleteColumnWizard: function (ev) { + ev.stopPropagation(); + const self = this; + const column_id = ev.target.id; + var state = this.model.get(this.handle, {raw: true}); + this._rpc({ + model: 'project.task.type', + method: 'unlink_wizard', + args: [column_id], + context: state.getContext(), + }).then(function (res) { + self.do_action(res); + }); + } +}); + +var ProjectKanbanView = KanbanView.extend({ + config: _.extend({}, KanbanView.prototype.config, { + Controller: ProjectKanbanController + }), +}); + +KanbanColumn.include({ + _onDeleteColumn: function (event) { + event.preventDefault(); + if (this.modelName === 'project.task') { + this.trigger_up('kanban_column_delete_wizard'); + return; + } + this._super.apply(this, arguments); + } +}); + +view_registry.add('project_kanban', ProjectKanbanView); +}); diff --git a/addons/project/views/project_assets.xml b/addons/project/views/project_assets.xml index fb203560543..347f620f386 100644 --- a/addons/project/views/project_assets.xml +++ b/addons/project/views/project_assets.xml @@ -4,6 +4,7 @@ + diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index 40231d1d83e..567276338e0 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -602,7 +602,7 @@ project.task.kanban project.task - + @@ -876,6 +876,7 @@ + @@ -884,8 +885,10 @@ project.task.type.form project.task.type -
+ + + @@ -932,7 +935,7 @@ project.task.type.tree project.task.type - + @@ -985,6 +988,15 @@ + + Delete + + + form,list + code + action = records.unlink_wizard(stage_view=True) + + diff --git a/addons/project/wizard/__init__.py b/addons/project/wizard/__init__.py index fc073c12452..063a6b88728 100644 --- a/addons/project/wizard/__init__.py +++ b/addons/project/wizard/__init__.py @@ -2,3 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import project_delete_wizard +from . import project_task_type_delete diff --git a/addons/project/wizard/project_task_type_delete.py b/addons/project/wizard/project_task_type_delete.py new file mode 100644 index 00000000000..4e8f8f4e20e --- /dev/null +++ b/addons/project/wizard/project_task_type_delete.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError + +import ast + + +class ProjectTaskTypeDelete(models.TransientModel): + _name = 'project.task.type.delete.wizard' + _description = 'Project Stage Delete Wizard' + + project_ids = fields.Many2many('project.project', domain="['|', ('active', '=', False), ('active', '=', True)]", string='Projects', ondelete='cascade') + stage_ids = fields.Many2many('project.task.type', string='Stages To Delete', ondelete='cascade') + tasks_count = fields.Integer('Number of tasks', compute='_compute_tasks_count') + stages_active = fields.Boolean(compute='_compute_stages_active') + + @api.depends('project_ids') + def _compute_tasks_count(self): + for wizard in self: + wizard.tasks_count = self.with_context(active_test=False).env['project.task'].search_count([('stage_id', 'in', wizard.stage_ids.ids)]) + + @api.depends('stage_ids') + def _compute_stages_active(self): + for wizard in self: + wizard.stages_active = all(wizard.stage_ids.mapped('active')) + + def action_archive(self): + if len(self.project_ids) <= 1: + return self.action_confirm() + + return { + 'name': _('Confirmation'), + 'view_mode': 'form', + 'res_model': 'project.task.type.delete.wizard', + 'views': [(self.env.ref('project.view_project_task_type_delete_confirmation_wizard').id, 'form')], + 'type': 'ir.actions.act_window', + 'res_id': self.id, + 'target': 'new', + 'context': self.env.context, + } + + def action_confirm(self): + tasks = self.with_context(active_test=False).env['project.task'].search([('stage_id', 'in', self.stage_ids.ids)]) + tasks.write({'active': False}) + self.stage_ids.write({'active': False}) + return self._get_action() + + def action_unlink(self): + self.stage_ids.unlink() + return self._get_action() + + def _get_action(self): + project_id = self.env.context.get('default_project_id') + + if project_id: + action = self.env.ref('project.action_view_task').read()[0] + action['domain'] = [('project_id', '=', project_id)] + action['context'] = str({ + 'pivot_row_groupby': ['user_id'], + 'default_project_id': project_id, + }) + elif self.env.context.get('stage_view'): + action = self.env.ref('project.open_task_type_form').read()[0] + else: + action = self.env.ref('project.action_view_all_task').read()[0] + + context = dict(ast.literal_eval(action.get('context')), active_test=True) + action['context'] = context + action['target'] = 'main' + return action diff --git a/addons/project/wizard/project_task_type_delete_views.xml b/addons/project/wizard/project_task_type_delete_views.xml new file mode 100644 index 00000000000..a0bc0001931 --- /dev/null +++ b/addons/project/wizard/project_task_type_delete_views.xml @@ -0,0 +1,49 @@ + + + + project.task.type.delete.wizard.form + project.task.type.delete.wizard + + + + +
+

Are you sure you want to delete those stages ?

+
+
+

You cannot delete stages containing tasks. You can either archive them or first delete all of their tasks.

+
+
+

You cannot delete stages containing tasks. You should first delete all of their tasks.

+
+
+
+ +
+
+ + + project.task.type.delete.wizard.form + project.task.type.delete.wizard + +
+
+

This will archive the stages and all the tasks they contain from the following projects:

+ + + + + +

Are you sure you want to continue?

+
+ +
+
+
+