Send client email via server

issue9449
review292001002
This commit is contained in:
Cédric Krier committed 2020-09-17 17:18:02 +02:00
1 parent 5b674ad987
commit d9242e3a6b
10 files changed
+454 -159

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
* Support e-mail template
* Always encode CSV export in UTF-8
* Use tempusdominus and Popper for date time picker
* Support PYSON comparison of date and datetime
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>

After

Width:  |  Height:  |  Size: 175 B

-7
View File
@@ -132,8 +132,6 @@
params.name = action.report_name;
params.data = data;
params.direct_print = action.direct_print;
params.email_print = action.email_print;
params.email = action.email;
params.context = context;
Sao.Action.exec_report(params);
return;
@@ -183,15 +181,10 @@
if (!attributes.context) {
attributes.context = {};
}
if (!attributes.email) {
attributes.email = {};
}
var data = jQuery.extend({}, attributes.data);
var context = jQuery.extend({}, Sao.Session.current_session.context);
jQuery.extend(context, attributes.context);
context.direct_print = attributes.direct_print;
context.email_print = attributes.email_print;
context.email = attributes.email;
var prm = Sao.rpc({
'method': 'report.' + attributes.name + '.execute',
+144 -3
View File
@@ -133,7 +133,7 @@
keys.forEach(function(k, i) {
jQuery('<div/>', {
'class': 'checkbox'
'class': 'radio'
}).append(jQuery('<label/>')
.text(' ' + k)
.prepend(jQuery('<input/>', {
@@ -150,7 +150,7 @@
'type': 'button'
}).text(Sao.i18n.gettext('Cancel')).click(function() {
dialog.modal.modal('hide');
prm.fail();
prm.reject();
}).appendTo(dialog.footer);
jQuery('<button/>', {
'class': 'btn btn-primary',
@@ -2723,6 +2723,7 @@
'tryton-remove',
'tryton-save',
'tryton-search',
'tryton-send',
'tryton-star-border',
'tryton-star',
'tryton-switch',
@@ -3435,7 +3436,9 @@
this.input.focus();
}.bind(this)).prependTo(this.menu);
}, this);
if (!this.input.val()) {
if (!this.input.val() || (
!this.menu.find('li.completion').length &&
!this.menu.find('li.action').length)) {
if (this.dropdown.hasClass('open')) {
this.menu.dropdown('toggle');
}
@@ -3770,4 +3773,142 @@
return colors;
};
Sao.common.richtext_toolbar = function() {
var toolbar = jQuery('<div/>', {
'class': 'btn-toolbar',
'role': 'toolbar',
});
var button_apply_command = function(evt) {
document.execCommand(evt.data);
};
var add_buttons = function(buttons) {
var group = jQuery('<div/>', {
'class': 'btn-group',
'role': 'group'
}).appendTo(toolbar);
for (var i in buttons) {
var properties = buttons[i];
var button = jQuery('<button/>', {
'class': 'btn btn-default',
'type': 'button'
}).append(Sao.common.ICONFACTORY.get_icon_img(
'tryton-format-' + properties.icon)
).appendTo(group);
button.click(properties.command, button_apply_command);
}
};
add_buttons([
{
'icon': 'bold',
'command': 'bold'
}, {
'icon': 'italic',
'command': 'italic'
}, {
'icon': 'underline',
'command': 'underline'
}]);
var selections = [
{
'heading': Sao.i18n.gettext('Font'),
'options': ['Normal', 'Serif', 'Sans', 'Monospace'], // XXX
'command': 'fontname'
}, {
'heading': Sao.i18n.gettext('Size'),
'options': [1, 2, 3, 4, 5, 6, 7],
'command': 'fontsize'
}];
var add_option = function(dropdown, properties) {
return function(option) {
dropdown.append(jQuery('<li/>').append(jQuery('<a/>', {
'href': '#'
}).text(option).click(function(evt) {
evt.preventDefault();
document.execCommand(properties.command, false, option);
})));
};
};
for (var i in selections) {
var properties = selections[i];
var group = jQuery('<div/>', {
'class': 'btn-group',
'role': 'group'
}).appendTo(toolbar);
var button = jQuery('<button/>', {
'class': 'btn btn-default dropdown-toggle',
'type': 'button',
'data-toggle': 'dropdown',
'aria-expanded': false,
'aria-haspopup': true
}).append(properties.heading)
.append(jQuery('<span/>', {
'class': 'caret'
})).appendTo(group);
var dropdown = jQuery('<ul/>', {
'class': 'dropdown-menu'
}).appendTo(group);
properties.options.forEach(add_option(dropdown, properties));
}
add_buttons([
{
'icon': 'align-left',
'command': Sao.i18n.rtl? 'justifyRight' : 'justifyLeft',
}, {
'icon': 'align-center',
'command': 'justifyCenter'
}, {
'icon': 'align-right',
'command': Sao.i18n.rtl? 'justifyLeft': 'justifyRight',
}, {
'icon': 'align-justify',
'command': 'justifyFull'
}]);
// TODO backColor
[['foreColor', '#000000']].forEach(
function(e) {
var command = e[0];
var color = e[1];
jQuery('<input/>', {
'class': 'btn btn-default',
'type': 'color'
}).appendTo(toolbar)
.change(function() {
document.execCommand(command, false, jQuery(this).val());
}).focusin(function() {
document.execCommand(command, false, jQuery(this).val());
}).val(color);
});
return toolbar;
};
Sao.common.richtext_normalize = function(html) {
var el = jQuery('<div/>').html(html);
// TODO order attributes
el.find('div').each(function(i, el) {
el = jQuery(el);
// Not all browsers respect the styleWithCSS
if (el.css('text-align')) {
// Remove browser specific prefix
var align = el.css('text-align').split('-').pop();
el.attr('align', align);
el.css('text-align', '');
}
// Some browsers set start as default align
if (el.attr('align') == 'start') {
if (Sao.i18n.rtl) {
el.attr('align', 'right');
} else {
el.attr('align', 'left');
}
}
});
return el.html();
};
}());
+4 -4
View File
@@ -488,8 +488,6 @@ var Sao = {};
try {
attributes.data = loads(params.data || '{}');
attributes.direct_print = loads(params.direct_print || 'false');
attributes.email_print = loads(params.email_print || 'false');
attributes.email = loads(params.email || 'null');
attributes.name = loads(params.name || '""');
attributes.window = loads(params.window || 'false');
attributes.context = loads(params.context || '{}');
@@ -512,8 +510,6 @@ var Sao = {};
try {
attributes.data = loads(params.data || '{}');
attributes.direct_print = loads(params.direct_print || 'false');
attributes.email_print = loads(params.email_print || 'false');
attributes.email = loads(params.email || 'null');
attributes.context = loads(params.context || '{}');
} catch (e) {
return;
@@ -1014,6 +1010,10 @@ var Sao = {};
shortcut: 'ctrl+p',
label: Sao.i18n.gettext('Print'),
id: 'print',
}, {
shortcut: 'ctrl+shift+e',
label: Sao.i18n.gettext('E-Mail'),
id: 'email',
}, {
shortcut: 'alt+shift+tab',
label: Sao.i18n.gettext('Previous tab'),
+12
View File
@@ -681,6 +681,18 @@ input.column-boolean {
}
}
.email {
.email-richtext {
min-height: 10em;
}
input[type='file'] {
display: inline-block;
}
.close {
float: none;
}
}
.bootstrap-datetimepicker-widget.dropdown-menu {
z-index: 2000;
}
+38 -3
View File
@@ -91,6 +91,11 @@
id: 'print',
icon: 'tryton-print',
label: Sao.i18n.gettext('Print'),
}, {
id: 'email',
icon: 'tryton-email',
label: Sao.i18n.gettext('E-Mail...'),
tooltip: Sao.i18n.gettext('Send an e-mail using the record'),
}, null, {
id: 'export',
icon: 'tryton-export',
@@ -522,7 +527,7 @@
'role': 'menu',
'aria-labelledby': menu_action[0]
}))
.appendTo(toolbar.find('.btn-toolbar > .btn-group').last());
.insertBefore(toolbar.find('button#email'));
buttons[menu_action[0]] = button;
var dropdown = button
.on('show.bs.dropdown', function() {
@@ -643,8 +648,6 @@
.selected_records.map(function(record) {
return record.id;
});
exec_action = Sao.Action.evaluate(exec_action,
menu_action[0], screen.current_record);
var data = {
model: screen.model_name,
id: record_id,
@@ -1173,6 +1176,38 @@
this.refresh_resources(true);
}.bind(this));
},
email: function() {
function is_report(action) {
return action.type == 'ir.action.report';
}
if (!this.buttons.email.hasClass('disabled')) {
var record = this.screen.current_record;
if (!record || (record.id < 0)) {
return;
}
var title = this.title.text();
this.screen.model.execute(
'view_toolbar_get', [], this.screen.context)
.then(function(toolbars) {
var prints = toolbars.print.filter(is_report);
var emails = {};
for (var i = 0; i < toolbars.emails.length; i++) {
var email = toolbars.emails[i];
emails[email.name] = email.id;
}
record.rec_name().then(function(rec_name) {
function email(template) {
new Sao.Window.Email(
title + ': ' + rec_name, record,
prints, template);
}
Sao.common.selection(
Sao.i18n.gettext("Template"), emails, true)
.then(email, email);
});
});
}
},
refresh_resources: function(reload) {
var record = this.screen.current_record;
if (record) {
+3
View File
@@ -187,6 +187,9 @@ select.form-control {
.checkbox-inline {
label {
padding-left: 25px;
input {
margin-right: 4px;
}
}
input[type="radio"],
+9 -142
View File
@@ -2100,7 +2100,10 @@ function eval_pyson(value){
'class': this.class_ + ' panel panel-default'
});
if (parseInt(attributes.toolbar || '1', 10)) {
this.toolbar = this.get_toolbar().appendTo(this.el);
this.toolbar = Sao.common.richtext_toolbar().appendTo(
jQuery('<div/>', {
'class': 'panel-heading',
}).appendTo(this.el));
}
this.input = this.labelled = jQuery('<div/>', {
'class': 'richtext mousetrap',
@@ -2122,122 +2125,6 @@ function eval_pyson(value){
button.click(this.translate.bind(this));
}
},
get_toolbar: function() {
var i, properties, button;
var toolbar = jQuery('<div/>', {
'class': 'btn-toolbar',
'role': 'toolbar'
}).appendTo(jQuery('<div/>', {
'class': 'panel-heading'
}));
var button_apply_command = function(evt) {
document.execCommand(evt.data);
};
var add_buttons = function(buttons) {
var group = jQuery('<div/>', {
'class': 'btn-group',
'role': 'group'
}).appendTo(toolbar);
for (i in buttons) {
properties = buttons[i];
button = jQuery('<button/>', {
'class': 'btn btn-default',
'type': 'button'
}).append(Sao.common.ICONFACTORY.get_icon_img(
'tryton-format-' + properties.icon)
).appendTo(group);
button.click(properties.command, button_apply_command);
}
};
add_buttons([
{
'icon': 'bold',
'command': 'bold'
}, {
'icon': 'italic',
'command': 'italic'
}, {
'icon': 'underline',
'command': 'underline'
}]);
var selections = [
{
'heading': Sao.i18n.gettext('Font'),
'options': ['Normal', 'Serif', 'Sans', 'Monospace'], // XXX
'command': 'fontname'
}, {
'heading': Sao.i18n.gettext('Size'),
'options': [1, 2, 3, 4, 5, 6, 7],
'command': 'fontsize'
}];
var add_option = function(dropdown, properties) {
return function(option) {
dropdown.append(jQuery('<li/>').append(jQuery('<a/>', {
'href': '#'
}).text(option).click(function(evt) {
evt.preventDefault();
document.execCommand(properties.command, false, option);
})));
};
};
for (i in selections) {
properties = selections[i];
var group = jQuery('<div/>', {
'class': 'btn-group',
'role': 'group'
}).appendTo(toolbar);
button = jQuery('<button/>', {
'class': 'btn btn-default dropdown-toggle',
'type': 'button',
'data-toggle': 'dropdown',
'aria-expanded': false,
'aria-haspopup': true
}).append(properties.heading)
.append(jQuery('<span/>', {
'class': 'caret'
})).appendTo(group);
var dropdown = jQuery('<ul/>', {
'class': 'dropdown-menu'
}).appendTo(group);
properties.options.forEach(add_option(dropdown, properties));
}
add_buttons([
{
'icon': 'align-left',
'command': Sao.i18n.rtl? 'justifyRight' : 'justifyLeft',
}, {
'icon': 'align-center',
'command': 'justifyCenter'
}, {
'icon': 'align-right',
'command': Sao.i18n.rtl? 'justifyLeft': 'justifyRight',
}, {
'icon': 'align-justify',
'command': 'justifyFull'
}]);
// TODO backColor
[['foreColor', '#000000']].forEach(
function(e) {
var command = e[0];
var color = e[1];
jQuery('<input/>', {
'class': 'btn btn-default',
'type': 'color'
}).appendTo(toolbar)
.change(function() {
document.execCommand(command, false, jQuery(this).val());
}).focusin(function() {
document.execCommand(command, false, jQuery(this).val());
}).val(color);
});
return toolbar;
},
focus_out: function() {
// Let browser set the next focus before testing
// if it moved out of the widget
@@ -2277,31 +2164,8 @@ function eval_pyson(value){
this.field.set_client(this.record, value);
},
_normalize_markup: function(content) {
var el = jQuery('<div/>').html(
return Sao.common.richtext_normalize(
Sao.HtmlSanitizer.sanitize(content || ''));
this._normalize(el);
return el.html();
},
_normalize: function(el) {
// TODO order attributes
el.find('div').each(function(i, el) {
el = jQuery(el);
// Not all browsers respect the styleWithCSS
if (el.css('text-align')) {
// Remove browser specific prefix
var align = el.css('text-align').split('-').pop();
el.attr('align', align);
el.css('text-align', '');
}
// Some browsers set start as default align
if (el.attr('align') == 'start') {
if (Sao.i18n.rtl) {
el.attr('align', 'right');
} else {
el.attr('align', 'left');
}
}
});
},
get modified() {
if (this.record && this.field) {
@@ -2323,7 +2187,10 @@ function eval_pyson(value){
'class': this.class_ + ' panel panel-default',
});
if (parseInt(this.attributes.toolbar || '1', 10)) {
this.get_toolbar().appendTo(widget);
Sao.common.richtext_toolbar().appendTo(
jQuery('<div/>', {
'class': 'panel-heading',
}).appendTo(this.el));
}
var input = jQuery('<div/>', {
'class': 'richtext mousetrap',
+242
View File
@@ -1972,4 +1972,246 @@
return row;
};
Sao.Window.EmailEntry = Sao.class_(Sao.common.InputCompletion, {
init: function(el, session) {
this.session = session;
Sao.Window.EmailEntry._super.init.call(
this, el,
this._email_source,
this._email_match_selected,
this._email_format);
},
_email_match_selected: function(value) {
this.input.val(value[2]);
},
_email_source: function(text) {
if (this.input[0].selectionStart < this.input.val().length) {
return jQuery.when([]);
}
return Sao.rpc({
'method': 'model.ir.email.complete',
'params': [text, Sao.config.limit, {}],
}, this.session);
},
_email_format: function(value) {
return value[1];
},
});
Sao.Window.Email = Sao.class_(Object, {
init: function(name, record, prints, template) {
this.record = record;
this.dialog = new Sao.Dialog(
Sao.i18n.gettext('E-mail %1', name), 'email', 'lg');
this.el = this.dialog.modal;
this.dialog.content.addClass('form-horizontal');
var body = this.dialog.body;
function add_group(name, label, required) {
var group = jQuery('<div/>', {
'class': 'form-group',
}).appendTo(body);
jQuery('<label/>', {
'class': 'control-label col-sm-1',
'text': label,
'for': 'email-' + name,
}).appendTo(group);
var input = jQuery('<input/>', {
'type': 'text',
'class':'form-control input-sm',
'id': 'email-' + name,
}).appendTo(jQuery('<div/>', {
'class': 'col-sm-11',
}).appendTo(group));
if (required) {
input.attr('required', true);
}
return input;
}
this.to = add_group('to', Sao.i18n.gettext('To:'), true);
this.cc = add_group('cc', Sao.i18n.gettext('Cc:'));
this.bcc = add_group('bcc', Sao.i18n.gettext('Bcc:'));
[this.to, this.cc, this.bcc].forEach(function(input) {
new Sao.Window.EmailEntry(input, this.record.model.session);
}.bind(this));
this.subject = add_group(
'subject', Sao.i18n.gettext('Subject:'), true);
var panel = jQuery('<div/>', {
'class': 'panel panel-default',
}).appendTo(body
).append(jQuery('<div/>', {
'class': 'panel-heading',
}).append(Sao.common.richtext_toolbar()));
this.body = jQuery('<div>', {
'class': 'email-richtext form-control input-sm mousetrap',
'contenteditable': true,
'spellcheck': true,
'id': 'email-body',
}).appendTo(jQuery('<div/>', {
'class': 'panel-body',
}).appendTo(panel));
var print_frame = jQuery('<div/>', {
'class': 'col-md-6',
}).appendTo(body);
jQuery('<label/>', {
'text': Sao.i18n.gettext("Reports"),
}).appendTo(print_frame);
this.print_actions = {};
for (var i = 0; i < prints.length; i++) {
var print = prints[i];
var print_check = jQuery('<input/>', {
'type': 'checkbox',
});
jQuery('<div/>', {
'class': 'checkbox',
}).append(jQuery('<label/>'
).text(Sao.i18n.gettext(print.name)
).prepend(print_check)).appendTo(print_frame);
this.print_actions[print.id] = print_check;
}
this.files = jQuery('<div/>', {
'class': 'col-md-6',
}).appendTo(body);
jQuery('<label/>', {
'text': Sao.i18n.gettext("Attachments"),
}).appendTo(this.files);
this._add_file_button();
jQuery('<button/>', {
'class': 'btn btn-link',
'type': 'button',
}).text(' ' + Sao.i18n.gettext('Cancel')).prepend(
Sao.common.ICONFACTORY.get_icon_img('tryton-cancel')
).click(function() {
this.response('RESPONSE_CANCEL');
}.bind(this)).appendTo(this.dialog.footer);
jQuery('<button/>', {
'class': 'btn btn-primary',
'type': 'submit',
}).text(' ' + Sao.i18n.gettext('Send')).prepend(
Sao.common.ICONFACTORY.get_icon_img('tryton-send')
).appendTo(this.dialog.footer);
this.dialog.content.submit(function(e) {
e.preventDefault();
this.response('RESPONSE_OK');
}.bind(this));
this._fill_with(template);
this.el.modal('show');
this.el.on('hidden.bs.modal', function() {
jQuery(this).remove();
});
},
_add_file_button: function() {
var row = jQuery('<div/>').appendTo(this.files);
var file = jQuery('<input/>', {
'type': 'file',
}).appendTo(row);
var button = jQuery('<a/>', {
'class': 'close',
'title': Sao.i18n.gettext("Remove attachment"),
}).append(jQuery('<span/>', {
'aria-hidden': true,
'text': 'x',
})).append(jQuery('<span/>', {
'class': 'sr-only',
}).text(Sao.i18n.gettext("Remove")));
button.hide();
button.appendTo(row);
file.on('change', function() {
button.show();
this._add_file_button();
}.bind(this));
button.click(function() {
row.remove();
});
},
get_files: function() {
var prms = [];
var files = [];
this.files.find('input[type=file]').each(function(i, input) {
if (input.files.length) {
var dfd = jQuery.Deferred();
prms.push(dfd);
Sao.common.get_file_data(
input.files[0], function(data, filename) {
files.push([filename, data]);
dfd.resolve();
});
}
});
return jQuery.when.apply(jQuery, prms).then(function() {
return files;
});
},
_fill_with: function(template) {
var prm;
if (template) {
prm = Sao.rpc({
'method': 'model.ir.email.template.get',
'params': [template, this.record.id, {}],
}, this.record.model.session);
} else {
prm = Sao.rpc({
'method': 'model.ir.email.template.get_default',
'params': [this.record.model.name, this.record.id, {}],
}, this.record.model.session);
}
prm.then(function(values) {
this.to.val((values.to || []).join(', '));
this.cc.val((values.cc || []).join(', '));
this.bcc.val((values.bcc || []).join(', '));
this.subject.val(values.subject || '');
this.body.html(Sao.HtmlSanitizer.sanitize(values.body || ''));
var print_ids = (values.reports || []);
for (var print_id in this.print_actions) {
var check = this.print_actions[print_id];
check.prop(
'checked', ~print_ids.indexOf(parseInt(print_id, 10)));
}
}.bind(this));
},
response: function(response_id) {
if (response_id == 'RESPONSE_OK') {
var to = this.to.val();
var cc = this.cc.val();
var bcc = this.bcc.val();
var subject = this.subject.val();
var body = Sao.common.richtext_normalize(this.body.html());
var reports = [];
for (var id in this.print_actions) {
var check = this.print_actions[id];
if (check.prop('checked')) {
reports.push(id);
}
}
var record = this.record;
this.get_files().then(function(attachments) {
return Sao.rpc({
'method': 'model.ir.email.send',
'params': [
to, cc, bcc, subject, body,
attachments,
[record.model.name, record.id],
reports, {}],
}, record.model.session);
}).then(function() {
this.destroy();
}.bind(this));
} else {
this.destroy();
}
},
destroy: function() {
this.el.modal('hide');
},
});
}());