[IMP] base_automation, web: give possibility to disable/edit failing automated actions

If an automated action raises an exception, in the traceback modal:
 - For admins, display Disable & Edit automated action buttons
   to be able to directly know with wihch automated action the error occurred,
   and to give the possibility to edit or disable it quickly,
 - For regular users, just add a paragraph to tell with which automated action the error occurred,
   so they can give this useful information to their administrator

This is specially useful for databases which have just been
upgraded to a newer version, and for which the server action
is failing because its code is no longer supported.

closes odoo/odoo#44513

X-original-commit: 8f3940c132bbbc99e47fa3f0cba0e768159d9af2
Signed-off-by: Denis Ledoux (dle) <dle@odoo.com>
This commit is contained in:
Denis Ledoux
2020-02-03 16:16:01 +00:00
parent dc1402c79b
commit 355cb45603
9 changed files with 297 additions and 11 deletions
@@ -15,6 +15,15 @@ msgstr ""
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: base_automation
#. openerp-web
#: code:addons/base_automation/static/src/xml/base_automation_error_dialog.xml:0
#, python-format
msgid ""
"\"\n"
" (ID:"
msgstr ""
#. module: base_automation
#: model:ir.model.fields,field_description:base_automation.field_base_automation__help
msgid "Action Description"
@@ -174,6 +183,24 @@ msgstr ""
msgid "Delay type"
msgstr ""
#. module: base_automation
#. openerp-web
#: code:addons/base_automation/static/src/xml/base_automation_error_dialog.xml:0
#, python-format
msgid "Disable Action"
msgstr ""
#. module: base_automation
#. openerp-web
#: code:addons/base_automation/static/src/xml/base_automation_error_dialog.xml:0
#, python-format
msgid ""
"Disabling this automated action will enable you to continue your workflow\n"
" but any data created after this could potentially be corrupted,\n"
" as you are effectively disabling a customization that may set\n"
" important and/or required fields."
msgstr ""
#. module: base_automation
#: model:ir.model.fields,field_description:base_automation.field_base_automation__display_name
msgid "Display Name"
@@ -189,6 +216,13 @@ msgstr ""
msgid "Due type"
msgstr ""
#. module: base_automation
#. openerp-web
#: code:addons/base_automation/static/src/xml/base_automation_error_dialog.xml:0
#, python-format
msgid "Edit action"
msgstr ""
#. module: base_automation
#: model:ir.model.fields,field_description:base_automation.field_base_automation__template_id
msgid "Email Template"
@@ -424,6 +458,15 @@ msgid ""
"updated.If empty, all fields are watched."
msgstr ""
#. module: base_automation
#. openerp-web
#: code:addons/base_automation/static/src/xml/base_automation_error_dialog.xml:0
#, python-format
msgid ""
"The error occurred during the execution of the automated action\n"
" \""
msgstr ""
#. module: base_automation
#: model:ir.model.fields,field_description:base_automation.field_base_automation__trigger
msgid "Trigger"
@@ -530,3 +573,18 @@ msgid ""
"Write Python code that the action will execute. Some variables are available"
" for use; help about python expression is given in the help tab."
msgstr ""
#. module: base_automation
#. openerp-web
#: code:addons/base_automation/static/src/xml/base_automation_error_dialog.xml:0
#, python-format
msgid ""
"You can ask an administrator to disable or correct this automated action."
msgstr ""
#. module: base_automation
#. openerp-web
#: code:addons/base_automation/static/src/xml/base_automation_error_dialog.xml:0
#, python-format
msgid "You can disable this automated action or edit it to solve the issue."
msgstr ""
@@ -209,6 +209,16 @@ class BaseAutomation(models.Model):
else:
return records, None
@api.model
def _add_postmortem_action(self, e):
if self.user_has_groups('base.group_user'):
e.context = {}
e.context['exception_class'] = 'base_automation'
e.context['base_automation'] = {
'id': self.id,
'name': self.name,
}
def _process(self, records, domain_post=None):
""" Process action ``self`` on the ``records`` that have not been done yet. """
# filter out the records on which self has already been done
@@ -242,7 +252,11 @@ class BaseAutomation(models.Model):
'active_id': record.id,
'domain_post': domain_post,
}
self.action_server_id.with_context(**ctx).run()
try:
self.action_server_id.with_context(**ctx).run()
except Exception as e:
self._add_postmortem_action(e)
raise e
def _check_trigger_fields(self, record):
""" Return whether any of the trigger fields has been modified on ``record``. """
@@ -367,7 +381,12 @@ class BaseAutomation(models.Model):
action_rule = self.env['base.automation'].browse(action_rule_id)
result = {}
server_action = action_rule.action_server_id.with_context(active_model=self._name, onchange_self=self)
res = server_action.run()
try:
res = server_action.run()
except Exception as e:
action_rule._add_postmortem_action(e)
raise e
if res:
if 'value' in res:
res['value'].pop('id', None)
@@ -0,0 +1,78 @@
odoo.define('base_automation.BaseAutomatioErrorDialog', function (require) {
"use strict";
const CrashManager = require('web.CrashManager');
const ErrorDialog = CrashManager.ErrorDialog;
const ErrorDialogRegistry = require('web.ErrorDialogRegistry');
const session = require('web.session');
const BaseAutomationErrorDialog = ErrorDialog.extend({
xmlDependencies: (ErrorDialog.prototype.xmlDependencies || []).concat(
['/base_automation/static/src/xml/base_automation_error_dialog.xml']
),
template: 'CrashManager.BaseAutomationError',
events: {
'click .o_disable_action_button': '_onDisableAction',
'click .o_edit_action_button': '_onEditAction',
},
/**
* Assign the `base_automation` object based on the error data,
* which is then used by the `CrashManager.BaseAutomationError` template
* and the events defined above.
* @override
* @param {Object} error
* @param {string} error.data.context.base_automation.id the ID of the failing automated action
* @param {string} error.data.context.base_automation.name the name of the failing automated action
*/
init: function (parent, options, error) {
this._super.apply(this, arguments);
this.base_automation = error.data.context.base_automation;
this.is_admin = session.is_admin;
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* This method is called when the user clicks on the 'Disable action' button
* displayed when a crash occurs in the evaluation of an automated action.
* Then, we write `active` to `False` on the automated action to disable it.
*
* @private
* @param {MouseEvent} ev
*/
_onDisableAction: function (ev) {
ev.preventDefault();
this._rpc({
model: 'base.automation',
method: 'write',
args: [[this.base_automation.id], {
active: false,
}],
}).then(this.destroy.bind(this));
},
/**
* This method is called when the user clicks on the 'Edit action' button
* displayed when a crash occurs in the evaluation of an automated action.
* Then, we redirect the user to the automated action form.
*
* @private
* @param {MouseEvent} ev
*/
_onEditAction: function (ev) {
ev.preventDefault();
this.do_action({
name: 'Automated Actions',
res_model: 'base.automation',
res_id: this.base_automation.id,
views: [[false, 'form']],
type: 'ir.actions.act_window',
view_mode: 'form',
});
},
});
ErrorDialogRegistry.add('base_automation', BaseAutomationErrorDialog);
});
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="CrashManager.BaseAutomationError" t-extend="CrashManager.error">
<t t-jquery=".alert.alert-warning" t-operation="append">
<t t-if="widget.base_automation.id">
<p>
The error occurred during the execution of the automated action
"<t t-esc="widget.base_automation.name"/>"
(ID: <t t-esc="widget.base_automation.id"/>).
<br/>
</p>
<p t-if="!widget.is_admin">
You can ask an administrator to disable or correct this automated action.
</p>
<p t-if="widget.is_admin">
You can disable this automated action or edit it to solve the issue.<br/>
Disabling this automated action will enable you to continue your workflow
but any data created after this could potentially be corrupted,
as you are effectively disabling a customization that may set
important and/or required fields.
</p>
</t>
</t>
<t t-jquery=".alert.alert-warning button" t-operation="after">
<t t-if="widget.base_automation.id &amp;&amp; widget.is_admin">
<button class="btn btn-secondary mt4 o_disable_action_button">
<i class="fa fa-ban mr8"/>Disable Action
</button>
<button class="btn btn-secondary mt4 o_edit_action_button">
<i class="fa fa-edit mr8"/>Edit action
</button>
</t>
</t>
</t>
</templates>
@@ -0,0 +1,72 @@
odoo.define('base_automation.BaseAutomatioErrorDialogTests', function (require) {
'use strict';
const CrashManager = require('web.CrashManager').CrashManager;
const session = require('web.session');
QUnit.module('base_automation', {}, function () {
QUnit.module('Error Dialog');
QUnit.test('Error due to an automated action', async function (assert) {
assert.expect(4);
let baseAutomationName = 'Test base automation error dialog';
let error = {
type: 'Odoo Client Error',
message: 'Message',
data: {
debug: 'Traceback',
context: {
exception_class: 'base_automation',
base_automation: {
id: 1,
name: baseAutomationName,
},
},
},
};
// Force the user session to be admin, to display the disable and edit action buttons,
// then reset back to the origin value after the test.
let isAdmin = session.is_admin;
session.is_admin = true;
let crashManager = new CrashManager();
let dialog = crashManager.show_error(error);
await dialog._opened;
assert.containsOnce(document.body, '.modal .o_clipboard_button');
assert.containsOnce(document.body, '.modal .o_disable_action_button');
assert.containsOnce(document.body, '.modal .o_edit_action_button');
assert.ok(dialog.$el.text().indexOf(baseAutomationName) !== -1);
session.is_admin = isAdmin;
crashManager.destroy();
});
QUnit.test('Error not due to an automated action', async function (assert) {
assert.expect(3);
let error = {
type: 'Odoo Client Error',
message: 'Message',
data: {
debug: 'Traceback',
},
};
let crashManager = new CrashManager();
let dialog = crashManager.show_error(error);
await dialog._opened;
assert.containsOnce(document.body, '.modal .o_clipboard_button');
assert.containsNone(document.body, '.modal .o_disable_action_button');
assert.containsNone(document.body, '.modal .o_edit_action_button');
crashManager.destroy();
});
});
});
@@ -123,4 +123,16 @@
<menuitem id="menu_base_automation_form"
parent="base.menu_automation" action="base_automation_act" sequence="1"/>
<template id="assets_backend" name="base_automation assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/base_automation/static/src/js/base_automation_error_dialog.js"/>
</xpath>
</template>
<template id="qunit_suite" name="base_automation_tests" inherit_id="web.qunit_suite">
<xpath expr="//t[@t-set='head']" position="inside">
<script type="text/javascript" src="/base_automation/static/tests/base_automation_error_dialog.js"/>
</xpath>
</template>
</odoo>
@@ -1,3 +1,11 @@
odoo.define('web.ErrorDialogRegistry', function (require) {
"use strict";
var Registry = require('web.Registry');
return new Registry();
});
odoo.define('web.CrashManager', function (require) {
"use strict";
@@ -5,6 +13,7 @@ const AbstractService = require('web.AbstractService');
var ajax = require('web.ajax');
var core = require('web.core');
var Dialog = require('web.Dialog');
var ErrorDialogRegistry = require('web.ErrorDialogRegistry');
var Widget = require('web.Widget');
var _t = core._t;
@@ -248,12 +257,11 @@ var CrashManager = AbstractService.extend({
if (!active) {
return;
}
var dialog = new ErrorDialog(this, {
error.traceback = error.data.debug;
var dialogClass = error.data.context && ErrorDialogRegistry.get(error.data.context.exception_class) || ErrorDialog;
var dialog = new dialogClass(this, {
title: _.str.capitalize(error.type) || _t("Odoo Error"),
}, {
message: error.message,
traceback: error.data.debug,
});
}, error);
// When the dialog opens, initialize the copy feature and destroy it when the dialog is closed
+5 -3
View File
@@ -7,9 +7,11 @@
<div t-name="CrashManager.error" class="o_dialog_error">
<div class="alert alert-warning clearfix" role="alert">
<button class="btn btn-primary float-right ml8 o_clipboard_button">
<i class="fa fa-clipboard mr8"/>Copy the full error to clipboard
</button>
<div class="float-right ml8 btn-group-vertical">
<button class="btn btn-primary o_clipboard_button">
<i class="fa fa-clipboard mr8"/>Copy the full error to clipboard
</button>
</div>
<p><b>An error occurred</b></p>
<p>Please use the copy button to report the error to your support service.</p>
</div>
+2 -1
View File
@@ -699,7 +699,8 @@ def serialize_exception(e):
"debug": traceback.format_exc(),
"message": ustr(e),
"arguments": e.args,
"exception_type": "internal_error"
"exception_type": "internal_error",
"context": getattr(e, 'context', {}),
}
if isinstance(e, odoo.exceptions.UserError):
tmp["exception_type"] = "user_error"