diff options
| author | Kevin Baptiste <kba@odoo.com> | 2020-02-26 12:17:28 +0000 |
|---|---|---|
| committer | Kevin Baptiste <kba@odoo.com> | 2020-04-22 08:29:51 +0000 |
| commit | 53954ad93efba227efb2cb129ea1b95253a18f2a (patch) | |
| tree | 167a83cd42baf10ea0623394741ad9b5aad63a05 /addons/project | |
| parent | 12acf49f1520a9792b3de077cd50583ede39c572 (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/project')
| -rw-r--r-- | addons/project/__manifest__.py | 1 | ||||
| -rw-r--r-- | addons/project/models/project.py | 40 | ||||
| -rw-r--r-- | addons/project/security/ir.model.access.csv | 1 | ||||
| -rw-r--r-- | addons/project/static/src/js/project_kanban.js | 48 | ||||
| -rw-r--r-- | addons/project/views/project_assets.xml | 1 | ||||
| -rw-r--r-- | addons/project/views/project_views.xml | 18 | ||||
| -rw-r--r-- | addons/project/wizard/__init__.py | 1 | ||||
| -rw-r--r-- | addons/project/wizard/project_task_type_delete.py | 72 | ||||
| -rw-r--r-- | addons/project/wizard/project_task_type_delete_views.xml | 49 |
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> |
