summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorKevin Baptiste <kba@odoo.com>2020-02-26 12:17:28 +0000
committerKevin Baptiste <kba@odoo.com>2020-04-22 08:29:51 +0000
commit53954ad93efba227efb2cb129ea1b95253a18f2a (patch)
tree167a83cd42baf10ea0623394741ad9b5aad63a05 /addons
parent12acf49f1520a9792b3de077cd50583ede39c572 (diff)
[IMP] project: ease stage deletion
Before this commit, deleting a stage with tasks, the user received an error. With this commit, when deleting a stage containing tasks, a wizard offers the choice to move the tasks to another stage. closes odoo/odoo#46410 Taskid: 1869030 Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
Diffstat (limited to 'addons')
-rw-r--r--addons/project/__manifest__.py1
-rw-r--r--addons/project/models/project.py40
-rw-r--r--addons/project/security/ir.model.access.csv1
-rw-r--r--addons/project/static/src/js/project_kanban.js48
-rw-r--r--addons/project/views/project_assets.xml1
-rw-r--r--addons/project/views/project_views.xml18
-rw-r--r--addons/project/wizard/__init__.py1
-rw-r--r--addons/project/wizard/project_task_type_delete.py72
-rw-r--r--addons/project/wizard/project_task_type_delete_views.xml49
9 files changed, 218 insertions, 13 deletions
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 @@
<xpath expr="." position="inside">
<link rel="stylesheet" href="/project/static/src/css/project.css"/>
<script type="text/javascript" src="/project/static/src/js/project.js"></script>
+ <script type="text/javascript" src="/project/static/src/js/project_kanban.js"></script>
<script type="text/javascript" src="/project/static/src/js/project_task_kanban_examples.js"></script>
<script type="text/javascript" src="/project/static/src/js/tours/project.js"></script>
<link rel="stylesheet" type="text/scss" href="/project/static/src/scss/project_dashboard.scss"/>
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 @@
<field name="name">project.task.kanban</field>
<field name="model">project.task</field>
<field name="arch" type="xml">
- <kanban default_group_by="stage_id" class="o_kanban_small_column o_kanban_project_tasks" on_create="quick_create" quick_create_view="project.quick_create_task_form" examples="project">
+ <kanban default_group_by="stage_id" class="o_kanban_small_column o_kanban_project_tasks" on_create="quick_create" quick_create_view="project.quick_create_task_form" examples="project" js_class="project_kanban">
<field name="color"/>
<field name="priority"/>
<field name="stage_id" options='{"group_by_tooltip": {"description": "Description"}}'/>
@@ -876,6 +876,7 @@
<field name="arch" type="xml">
<search string="Tasks Stages">
<field name="name" string="Tasks Stages"/>
+ <filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
</search>
</field>
</record>
@@ -884,8 +885,10 @@
<field name="name">project.task.type.form</field>
<field name="model">project.task.type</field>
<field name="arch" type="xml">
- <form string="Task Stage">
+ <form string="Task Stage" delete="0">
+ <field name="active" invisible="1" />
<sheet>
+ <widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}" />
<group>
<group>
<field name="name"/>
@@ -932,7 +935,7 @@
<field name="name">project.task.type.tree</field>
<field name="model">project.task.type</field>
<field name="arch" type="xml">
- <tree string="Task Stage">
+ <tree string="Task Stage" delete="0">
<field name="sequence" widget="handle" groups="base.group_no_one"/>
<field name="name"/>
<field name="fold"/>
@@ -985,6 +988,15 @@
</field>
</record>
+ <record id="unlink_task_type_action" model="ir.actions.server">
+ <field name="name">Delete</field>
+ <field name="model_id" ref="project.model_project_task_type"/>
+ <field name="binding_model_id" ref="project.model_project_task_type"/>
+ <field name="binding_view_types">form,list</field>
+ <field name="state">code</field>
+ <field name="code">action = records.unlink_wizard(stage_view=True)</field>
+ </record>
+
<menuitem id="menu_tasks_config" name="GTD" parent="menu_project_config" sequence="2"/>
<menuitem action="open_task_type_form" id="menu_project_config_project" name="Stages" parent="menu_project_config" sequence="3" groups="base.group_no_one"/>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="view_project_task_type_delete_wizard" model="ir.ui.view">
+ <field name="name">project.task.type.delete.wizard.form</field>
+ <field name="model">project.task.type.delete.wizard</field>
+ <field name="arch" type="xml">
+ <form string="Delete Stage">
+ <field name="tasks_count" invisible="1" />
+ <field name="stages_active" invisible="1" />
+ <div attrs="{'invisible': [('tasks_count', '>', 0)]}">
+ <p>Are you sure you want to delete those stages ?</p>
+ </div>
+ <div attrs="{'invisible': ['|', ('stages_active', '=', False), ('tasks_count', '=', 0)]}">
+ <p>You cannot delete stages containing tasks. You can either archive them or first delete all of their tasks.</p>
+ </div>
+ <div attrs="{'invisible': ['|', ('stages_active', '=', True), ('tasks_count', '=', 0)]}">
+ <p>You cannot delete stages containing tasks. You should first delete all of their tasks.</p>
+ </div>
+ <footer>
+ <button string="Archive Stages" type="object" name="action_archive" class="btn btn-primary" attrs="{'invisible': ['|', ('stages_active', '=', False), ('tasks_count', '=', 0)]}" />
+ <button string="Delete" type="object" name="action_unlink" class="btn btn-primary" attrs="{'invisible': [('tasks_count', '>', 0)]}" />
+ <button string="Discard" special="cancel" />
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <record id="view_project_task_type_delete_confirmation_wizard" model="ir.ui.view">
+ <field name="name">project.task.type.delete.wizard.form</field>
+ <field name="model">project.task.type.delete.wizard</field>
+ <field name="arch" type="xml">
+ <form string="Delete Stage">
+ <div>
+ <p>This will archive the stages and all the tasks they contain from the following projects:</p>
+ <field name="project_ids" readonly="1">
+ <tree>
+ <field name="name"/>
+ </tree>
+ </field>
+ <p>Are you sure you want to continue?</p>
+ </div>
+ <footer>
+ <button string="Confirm" type="object" name="action_confirm" class="btn btn-primary"/>
+ <button string="Discard" special="cancel" />
+ </footer>
+ </form>
+ </field>
+ </record>
+</odoo>