', {
'class': 'dict-row'
}).appendTo(body));
this.wid_text = jQuery('
', {
'type': 'text',
'class': 'form-control input-sm',
'placeholder': Sao.i18n.gettext('Search'),
'name': attributes.name,
}).appendTo(group);
if (!attributes.completion || attributes.completion == '1') {
this.wid_completion = Sao.common.get_completion(
group,
this._update_completion.bind(this),
this._completion_match_selected.bind(this));
this.wid_text.completion = this.wid_completion;
}
this.but_add = jQuery('
', {
'class': 'btn btn-default btn-sm',
'type': 'button',
'aria-label': Sao.i18n.gettext("Add"),
'title': Sao.i18n.gettext("Add"),
}).append(Sao.common.ICONFACTORY.get_icon_img('tryton-add')
).appendTo(jQuery('
', {
'class': 'input-group-btn'
}).appendTo(group));
this.but_add.click(this.add.bind(this));
this._readonly = false;
this._record_id = null;
this._popup = false;
},
_required_el: function() {
return this.wid_text;
},
_invalid_el: function() {
return this.wid_text;
},
add: function() {
var context = this.field.get_context(this.record);
var value = this.wid_text.val();
var domain = this.field.get_domain(this.record);
if (this._popup) {
return;
} else {
this._popup = true;
}
const callback = result => {
if (!jQuery.isEmptyObject(result)) {
var ids = result.map(function(e) {
return e[0];
});
this.add_new_keys(ids);
}
this.wid_text.val('');
this._popup = false;
};
var parser = new Sao.common.DomainParser();
new Sao.Window.Search(this.schema_model,
callback, {
sel_multi: true,
context: context,
domain: domain,
new_: false,
search_filter: parser.quote(value),
title: this.attributes.string
});
},
add_new_keys: function(ids) {
var field = this.field;
field.add_new_keys(ids, this.record)
.then(new_names => {
this.send_modified();
var value = this.field.get_client(this.record);
for (const key of new_names) {
value[key] = null;
}
this.field.set_client(this.record, value);
this._display().then(() => {
this.fields[new_names[0]].input.focus();
});
});
},
remove: function(key, modified=true) {
delete this.fields[key];
this.rows[key].remove();
delete this.rows[key];
if (modified) {
this.send_modified();
this.set_value(this.record, this.field);
}
},
set_value: function() {
this.field.set_client(this.record, this.get_value());
},
get_value: function() {
var value = {};
for (var key in this.fields) {
var widget = this.fields[key];
value[key] = widget.get_value();
}
return value;
},
get modified() {
if (this.record && this.field) {
var value = this.field.get_client(this.record);
for (var key in this.fields) {
var widget = this.fields[key];
if (widget.modified(value)) {
return true;
}
}
}
return false;
},
set_readonly: function(readonly) {
Sao.View.Form.Dict._super.set_readonly.call(this, readonly);
this._set_button_sensitive();
for (var key in this.fields) {
var widget = this.fields[key];
widget.set_readonly(readonly);
}
this.wid_text.prop('disabled', readonly || !this.record);
},
_set_button_sensitive: function() {
var record = this.record;
var create = this.attributes.create;
if (create === undefined) {
create = 1;
} else if (typeof create == 'string') {
create = Boolean(parseInt(create, 10));
}
var delete_ = this.attributes['delete'];
if (delete_ === undefined) {
delete_ = 1;
} else if (typeof delete_ == 'string') {
delete_ = Boolean(parseInt(delete_, 10));
}
this.but_add.prop('disabled', this._readonly || !create || !record);
for (var key in this.fields) {
var button = this.fields[key].button;
button.prop('disabled', this._readonly || !delete_ || !record);
}
},
add_line: function(key, position) {
var field, row;
var key_schema = this.field.keys[key];
this.fields[key] = field = new (
this.get_entries(key_schema.type))(key, this);
this.rows[key] = row = jQuery('
', {
'class': 'dict-row'
});
var text = key_schema.string + Sao.i18n.gettext(':');
var label = jQuery('
', {
'class': 'dict-label control-label'
}).appendTo(row));
field.el.appendTo(row);
label.uniqueId();
field.labelled.uniqueId();
field.labelled.attr('aria-labelledby', label.attr('id'));
label.attr('for', field.labelled.attr('id'));
field.button.click(() => {
this.remove(key, true);
});
var previous = null;
if (position > 0) {
previous = this.container.children().eq(position - 1);
}
if (previous) {
previous.after(row);
} else {
this.container.prepend(row);
}
},
display: function() {
this._display();
},
_display: function() {
Sao.View.Form.Dict._super.display.call(this);
var record = this.record;
var field = this.field;
if (!field) {
return;
}
var record_id = record ? record.id : null;
var key;
if (record_id != this._record_id) {
for (key in this.fields) {
this.remove(key, false);
}
this._record_id = record_id;
}
var value = field.get_client(record);
var new_key_names = Object.keys(value).filter(
e => !this.field.keys[e]);
var prm;
if (!jQuery.isEmptyObject(new_key_names)) {
prm = field.add_keys(new_key_names, record);
} else {
prm = jQuery.when();
}
prm.then(() => {
var i, len, key;
var keys = Object.keys(value)
.filter(function(key) {
return field.keys[key];
})
.sort(function(key1, key2) {
var seq1 = field.keys[key1].sequence;
var seq2 = field.keys[key2].sequence;
if (seq1 < seq2) {
return -1;
} else if (seq1 > seq2) {
return 1;
} else {
return 0;
}
});
// We remove first the old keys in order to keep the order
// inserting the new ones
var removed_key_names = Object.keys(this.fields).filter(
function(e) {
return !(e in value);
});
for (i = 0, len = removed_key_names.length; i < len; i++) {
key = removed_key_names[i];
this.remove(key, false);
}
var decoder = new Sao.PYSON.Decoder();
var inversion = new Sao.common.DomainInversion();
for (i = 0, len = keys.length; i < len; i++) {
key = keys[i];
var val = value[key];
if (!this.fields[key]) {
this.add_line(key, i);
}
var widget = this.fields[key];
widget.set_value(val);
widget.set_readonly(this._readonly);
var key_domain = (decoder.decode(field.keys[key].domain ||
'null'));
if (key_domain !== null) {
if (!inversion.eval_domain(key_domain, value)) {
widget.el.addClass('has-error');
} else {
widget.el.removeClass('has-error');
}
}
}
});
this._set_button_sensitive();
return prm;
},
_update_completion: function(text) {
if (this.wid_text.prop('disabled')) {
return jQuery.when();
}
if (!this.record) {
return jQuery.when();
}
return Sao.common.update_completion(
this.wid_text, this.record, this.field, this.schema_model);
},
_completion_match_selected: function(value) {
this.add_new_keys([value.id]);
this.wid_text.val('');
},
get_entries: function(type) {
switch (type) {
case 'char':
return Sao.View.Form.Dict.Char;
case 'color':
return Sao.View.Form.Dict.Color;
case 'boolean':
return Sao.View.Form.Dict.Boolean;
case 'selection':
return Sao.View.Form.Dict.Selection;
case 'multiselection':
return Sao.View.Form.Dict.MultiSelection;
case 'integer':
return Sao.View.Form.Dict.Integer;
case 'float':
return Sao.View.Form.Dict.Float;
case 'numeric':
return Sao.View.Form.Dict.Numeric;
case 'date':
return Sao.View.Form.Dict.Date;
case 'datetime':
return Sao.View.Form.Dict.DateTime;
}
}
});
Sao.View.Form.Dict.Entry = Sao.class_(Object, {
init: function(name, parent_widget) {
this.name = name;
this.definition = parent_widget.field.keys[name];
this.parent_widget = parent_widget;
this.create_widget();
if (this.definition.help) {
this.el.attr('title', this.definition.help);
}
},
create_widget: function() {
this.el = jQuery('
', {
'class': this.class_
});
var group = jQuery('
', {
'class': 'input-group input-group-sm'
}).appendTo(this.el);
this.input = this.labelled = jQuery('
', {
'type': 'text',
'class': 'form-control input-sm mousetrap',
'name': this.name,
}).appendTo(group);
this.button = jQuery('
', {
'class': 'btn btn-default',
'type': 'button',
'arial-label': Sao.i18n.gettext("Remove"),
'title': Sao.i18n.gettext("Remove"),
}).append(Sao.common.ICONFACTORY.get_icon_img('tryton-remove')
).appendTo(jQuery('
', {
'class': 'input-group-btn'
}).appendTo(group));
this.el.on('keydown',
this.parent_widget.send_modified.bind(this.parent_widget));
this.el.change(
this.parent_widget.focus_out.bind(this.parent_widget));
},
modified: function(value) {
return (JSON.stringify(this.get_value()) !=
JSON.stringify(value[this.name]));
},
get_value: function() {
return this.input.val();
},
set_value: function(value) {
this.input.val(value || '');
},
set_readonly: function(readonly) {
this._readonly = readonly;
this.input.prop('readonly', readonly);
}
});
Sao.View.Form.Dict.Char = Sao.class_(Sao.View.Form.Dict.Entry, {
class_: 'dict-char',
modified: function(value) {
return (JSON.stringify(this.get_value()) !=
JSON.stringify(value[this.name] || ""));
}
});
Sao.View.Form.Dict.Color = Sao.class_(Sao.View.Form.Dict.Char, {
class_: 'dict-color',
create_widget: function() {
Sao.View.Form.Dict.Color._super.create_widget.call(this);
this.color_input = jQuery('
', {
'class': 'btn btn-default btn-sm',
'type': 'color',
'title': Sao.i18n.gettext(
'Select a color for "%1"', this.definition.string),
}).prependTo(this.el.find('.input-group-btn'));
this.color_input.change(this.set_color.bind(this));
},
set_color: function() {
this.input.val(this.color_input.val());
this.parent_widget.focus_out();
},
set_value: function(value) {
Sao.View.Form.Dict.Color._super.set_value.call(this, value);
this.color_input.val(value || '');
},
set_readonly: function(readonly) {
Sao.View.Form.Dict.Color._super.set_readonly.call(this, readonly);
this.color_input.prop('disabled', readonly);
},
});
Sao.View.Form.Dict.Boolean = Sao.class_(Sao.View.Form.Dict.Entry, {
class_: 'dict-boolean',
create_widget: function() {
Sao.View.Form.Dict.Boolean._super.create_widget.call(this);
this.input.attr('type', 'checkbox');
this.input.change(
this.parent_widget.focus_out.bind(this.parent_widget));
},
get_value: function() {
return this.input.prop('checked');
},
set_readonly: function(readonly) {
this._readonly = readonly;
this.input.prop('disabled', readonly);
},
set_value: function(value) {
this.input.prop('checked', Boolean(value));
}
});
Sao.View.Form.Dict.SelectionEntry = Sao.class_(Sao.View.Form.Dict.Entry, {
create_widget: function() {
Sao.View.Form.Dict.SelectionEntry._super.create_widget.call(this);
var select = jQuery('
', {
'class': 'form-control input-sm mousetrap',
'name': this.name,
});
select.change(
this.parent_widget.focus_out.bind(this.parent_widget));
this.input.replaceWith(select);
this.input = this.labelled = select;
var selection = jQuery.extend([], this.definition.selection);
if (this.definition.sort === undefined || this.definition.sort) {
selection.sort(function(a, b) {
return a[1].localeCompare(b[1]);
});
}
for (const e of selection) {
select.append(jQuery('
', {
'value': JSON.stringify(e[0]),
'text': e[1],
'title': this.definition.help_selection[e[0]],
}));
}
},
set_readonly: function(readonly) {
this._readonly = readonly;
this.input.prop('disabled', readonly);
}
});
Sao.View.Form.Dict.Selection = Sao.class_(
Sao.View.Form.Dict.SelectionEntry, {
class_: 'dict-selection',
create_widget: function() {
Sao.View.Form.Dict.Selection._super.create_widget.call(this);
this.input.prepend(jQuery('
', {
'value': JSON.stringify(null),
'text': '',
}));
},
get_value: function() {
return JSON.parse(this.input.val());
},
set_value: function(value) {
this.input.val(JSON.stringify(value));
var title = this.definition.help_selection[value] || null;
if (this.definition.help && title) {
title = this.definition.help + '\n' + title;
}
this.input.attr('title', title);
},
});
Sao.View.Form.Dict.MultiSelection = Sao.class_(
Sao.View.Form.Dict.SelectionEntry, {
class_: 'dict-multiselection',
create_widget: function() {
Sao.View.Form.Dict.MultiSelection._super
.create_widget.call(this);
this.input.prop('multiple', true);
this.input.on('mousedown', 'option', (evt) => {
evt.preventDefault();
var scroll = this.input.get(0).scrollTop;
evt.target.selected = !evt.target.selected;
this.input.trigger('change');
setTimeout(() => this.input.get(0).scrollTop = scroll, 0);
}).mousemove(evt => evt.preventDefault());
var widget_help = this.definition.help;
if (widget_help) {
this.input.children().each(function() {
var option = jQuery(this);
var help = option.attr('title');
if (help) {
help = widget_help + '\n' + help;
option.attr('title', help);
}
});
}
},
get_value: function() {
var value = this.input.val();
if (value && value.length) {
return value.map(function(e) { return JSON.parse(e); });
} else {
return null;
}
},
set_value: function(value) {
if (value) {
value = value.map(function(e) { return JSON.stringify(e); });
}
this.input.val(value);
}
});
Sao.View.Form.Dict.Integer = Sao.class_(Sao.View.Form.Dict.Entry, {
class_: 'dict-integer',
create_widget: function() {
Sao.View.Form.Dict.Integer._super.create_widget.call(this);
this.input_text = this.labelled = integer_input(this.input);
},
get_value: function() {
var value = parseInt(this.input.val(), 10);
if (isNaN(value)) {
return null;
}
return value;
},
set_value: function(value, options) {
if ((typeof(value) == 'number') ||
(value instanceof Sao.Decimal)) {
this.input.val(value);
this.input_text.val(value.toLocaleString(
Sao.i18n.BC47(Sao.i18n.getlang()), options));
} else {
this.input.val('');
this.input_text.val('');
}
},
set_readonly: function(readonly) {
Sao.View.Form.Dict.Integer._super.set_readonly.call(this, readonly);
this.input_text.prop('readonly', readonly);
},
});
Sao.View.Form.Dict.Float = Sao.class_(Sao.View.Form.Dict.Integer, {
class_: 'dict-float',
get digits() {
var record = this.parent_widget.record;
if (record) {
return record.expr_eval(this.definition.digits);
} else {
return null;
}
},
get_value: function() {
var value = this.input.val();
if (!value && (value !== 0)) {
return null;
}
value = Number(value);
if (isNaN(value)) {
return null;
}
return value;
},
set_value: function(value) {
var step = 'any',
options = {};
var max, min;
var digits = this.digits;
if (digits) {
if (digits[1] !== null) {
step = Math.pow(10, -digits[1]).toFixed(digits[1]);
options.minimumFractionDigits = digits[1];
options.maximumFractionDigits = digits[1];
}
if (digits[0] !== null) {
max = '9'.repeat(digits[0]);
if (digits[1] !== null) {
max += '.' + '9'.repeat(digits[1]);
} else {
max += 1;
}
min = '-' + max;
}
}
this.input.attr('step', step);
this.input.attr('max', max);
this.input.attr('min', min);
Sao.View.Form.Dict.Float._super.set_value.call(this, value, options);
},
});
Sao.View.Form.Dict.Numeric = Sao.class_(Sao.View.Form.Dict.Float, {
class_: 'dict-numeric',
get_value: function() {
var value = this.input.val();
if (!value && (value !== 0)) {
return null;
}
value = new Sao.Decimal(value);
if (isNaN(value.valueOf())) {
return null;
}
return value;
}
});
Sao.View.Form.Dict.Date = Sao.class_(Sao.View.Form.Dict.Entry, {
class_: 'dict-date',
format: '%x',
_input: 'date',
_input_format: '%Y-%m-%d',
_format: Sao.common.format_date,
_parse: Sao.common.parse_date,
create_widget: function() {
Sao.View.Form.Dict.Date._super.create_widget.call(this);
this.input_date = jQuery('
', {
'type': this._input,
'role': 'button',
'tabindex': -1,
});
this.input_date.click(() => {
var value = this.get_value();
value = this._format(this._input_format, value);
this.input_date.val(value);
});
this.input_date.change(() => {
var value = this.input_date.val();
if (value) {
value = this._parse(this._input_format, value);
value = this._format(this.format, value);
this.input.val(value).change();
this.input.focus();
}
});
if (this.input_date[0].type == this._input) {
var group = jQuery('
', {
'class': 'input-icon input-icon-secondary',
}).prependTo(this.input.parent());
this.input.appendTo(group);
var icon = jQuery('
', {
'class': 'icon-input icon-secondary',
'aria-label': Sao.i18n.gettext("Open the calendar"),
'title': Sao.i18n.gettext("Open the calendar"),
}).appendTo(group);
this.input_date.appendTo(icon);
Sao.common.ICONFACTORY.get_icon_img('tryton-date')
.appendTo(icon);
}
var mousetrap = new Mousetrap(this.el[0]);
mousetrap.bind('enter', (e, combo) => {
var value = this._parse(this.format, this.input.val());
value = this._format(this.format, value);
this.input.val(value).change();
});
mousetrap.bind('=', (e, combo) => {
e.preventDefault();
this.input.val(this._format(this.format, moment())).change();
});
Sao.common.DATE_OPERATORS.forEach(operator => {
mousetrap.bind(operator[0], (e, combo) => {
e.preventDefault();
var date = this.get_value() || Sao.DateTime();
date.add(operator[1]);
this.input.val(this._format(this.format, date)).change();
});
});
},
get_value: function() {
return this._parse(this.format, this.input.val());
},
set_value: function(value) {
if (value && (value.isDate || value.isDateTime)) {
value = this._format(this.format, value);
} else {
value = '';
}
this.input.val(value);
},
});
Sao.View.Form.Dict.DateTime = Sao.class_(Sao.View.Form.Dict.Date, {
class_: 'dict-datetime',
format: '%x %X',
_input: 'datetime-local',
_input_format: '%Y-%m-%dT%H:%M:%S',
_format: Sao.common.format_datetime,
_parse: Sao.common.parse_datetime,
});
Sao.View.Form.PYSON = Sao.class_(Sao.View.Form.Char, {
class_: 'form-pyson',
init: function(view, attributes) {
Sao.View.Form.PYSON._super.init.call(this, view, attributes);
this.encoder = new Sao.PYSON.Encoder({});
this.decoder = new Sao.PYSON.Decoder({}, true);
this.el.keyup(this.validate_pyson.bind(this));
this.icon = jQuery('
', {
'class': 'icon form-control-feedback',
}).appendTo(this.group);
this.group.addClass('has-feedback');
},
display: function() {
Sao.View.Form.PYSON._super.display.call(this);
this.validate_pyson();
},
get_encoded_value: function() {
var value = this.input.val();
if (!value) {
return value;
}
try {
return this.encoder.encode(eval_pyson(value));
}
catch (err) {
return null;
}
},
set_value: function() {
// avoid modification because different encoding
var value = this.get_encoded_value();
var record = this.record;
var field = this.field;
var previous = field.get_client(record);
if (value && previous && Sao.common.compare(
value, this.encoder.encode(this.decoder.decode(previous)))) {
value = previous;
}
field.set_client(record, value);
},
get_client_value: function() {
var value = Sao.View.Form.PYSON._super.get_client_value.call(this);
if (value) {
value = Sao.PYSON.toString(this.decoder.decode(value));
}
return value;
},
validate_pyson: function() {
var icon = 'ok';
if (this.get_encoded_value() === null) {
icon = 'error';
}
Sao.common.ICONFACTORY.get_icon_url('tryton-' + icon)
.then(url => {
this.icon.attr('src', url);
});
},
focus_out: function() {
this.validate_pyson();
Sao.View.Form.PYSON._super.focus_out.call(this);
}
});
Sao.View.FormXMLViewParser.WIDGETS = {
'binary': Sao.View.Form.Binary,
'boolean': Sao.View.Form.Boolean,
'callto': Sao.View.Form.CallTo,
'char': Sao.View.Form.Char,
'color': Sao.View.Form.Color,
'date': Sao.View.Form.Date,
'datetime': Sao.View.Form.DateTime,
'dict': Sao.View.Form.Dict,
'document': Sao.View.Form.Document,
'email': Sao.View.Form.Email,
'float': Sao.View.Form.Float,
'html': Sao.View.Form.HTML,
'image': Sao.View.Form.Image,
'integer': Sao.View.Form.Integer,
'many2many': Sao.View.Form.Many2Many,
'many2one': Sao.View.Form.Many2One,
'multiselection': Sao.View.Form.MultiSelection,
'numeric': Sao.View.Form.Float,
'one2many': Sao.View.Form.One2Many,
'one2one': Sao.View.Form.One2One,
'password': Sao.View.Form.Password,
'progressbar': Sao.View.Form.ProgressBar,
'pyson': Sao.View.Form.PYSON,
'reference': Sao.View.Form.Reference,
'richtext': Sao.View.Form.RichText,
'selection': Sao.View.Form.Selection,
'sip': Sao.View.Form.SIP,
'text': Sao.View.Form.Text,
'time': Sao.View.Form.Time,
'timedelta': Sao.View.Form.TimeDelta,
'timestamp': Sao.View.Form.DateTime,
'url': Sao.View.Form.URL,
};
}());
/* This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. */
(function() {
'use strict';
if ('IntersectionObserver' in window) {
var moreObserver = new IntersectionObserver(function(entries, observer) {
for (const entry of entries) {
if (entry.isIntersecting) {
jQuery(entry.target).trigger('click');
}
}
}, {
rootMargin: '0px 0px 50px 0px',
});
}
Sao.View.TreeXMLViewParser = Sao.class_(Sao.View.XMLViewParser, {
_parse_tree: function(node, attributes) {
for (const child of node.childNodes) {
this.parse(child);
}
},
_parse_field: function(node, attributes) {
var name = attributes.name;
var ColumnFactory = Sao.View.TreeXMLViewParser.WIDGETS[
attributes.widget];
var column = new ColumnFactory(this.view.screen.model, attributes);
if (!this.view.widgets[name]) {
this.view.widgets[name] = [];
}
column.tree = this.view;
this.view.widgets[name].push(column);
if ('symbol' in attributes) {
column.suffixes.push(
new Sao.View.Tree.Symbol(attributes, 1));
}
if (~['url', 'email', 'callto', 'sip'
].indexOf(attributes.widget)) {
column.prefixes.push(
new Sao.View.Tree.Affix(attributes, attributes.widget));
}
if ('icon' in attributes) {
column.prefixes.push(new Sao.View.Tree.Affix(attributes));
}
for (const affix of node.childNodes) {
let affix_attributes = {};
for (const attribute of affix.attributes) {
affix_attributes[attribute.name] = attribute.value;
}
if (!affix_attributes.name) {
affix_attributes.name = name;
}
let list;
if (affix.tagName == 'prefix') {
list = column.prefixes;
} else {
list = column.suffixes;
}
list.push(new Sao.View.Tree.Affix(affix_attributes));
}
if ('symbol' in attributes) {
column.prefixes.push(
new Sao.View.Tree.Symbol(attributes, 0));
}
if (!this.view.attributes.sequence &&
!this.view.children_field &&
this.field_attrs[name].sortable !== false){
column.sortable = true;
}
this.view.columns.push(column);
if (attributes.optional && (name !== this.exclude_field)) {
Sao.setdefault(
this.view.optionals, column.attributes.name, []).push(column);
}
if (parseInt(attributes.sum || '0', 10)) {
var sum = jQuery('
', {
'text': attributes.string,
});
var aggregate = jQuery('
', {
'class': 'value',
});
this.view.sum_widgets.set(column, [sum, aggregate]);
}
},
_parse_button: function(node, attributes) {
let button;
if (parseInt(attributes.multiple || '0', 10)) {
button = new Sao.View.Tree.ButtonMultiple(attributes);
button.el.click(
button, this.view.button_clicked.bind(this.view));
this.view.footer.append(button.el);
} else {
button = new Sao.View.Tree.ButtonColumn(
this.view, attributes);
this.view.columns.push(button);
}
this.view.state_widgets.push(button);
}
});
Sao.View.Tree = Sao.class_(Sao.View, {
view_type: 'tree',
xml_parser: Sao.View.TreeXMLViewParser,
draggable: false,
display_size: null,
init: function(view_id, screen, xml, children_field) {
this.children_field = children_field;
this.optionals = {};
this.sum_widgets = new Map();
this.columns = [];
this.selection_mode = (screen.attributes.selection_mode ||
Sao.common.SELECTION_MULTIPLE);
this.el = jQuery('
', {
'class': 'tree-container',
})
// Prevent Chrome based browser to compute a min-content
// such that only this table has scrollbar if needed
.css('display', 'grid');
this.scrollbar = jQuery('
', {
'class': 'scrollbar responsive',
}).appendTo(this.el));
this.treeview = jQuery('
', {
'class': 'treeview responsive'
}).appendTo(this.el);
// Synchronize both scrollbars
this.treeview.scroll(() => {
this.scrollbar.parent().scrollLeft(this.treeview.scrollLeft());
});
this.scrollbar.parent().scroll(() => {
this.treeview.scrollLeft(this.scrollbar.parent().scrollLeft());
});
this.footer = jQuery('
', {
'class': 'tree-footer',
}).appendTo(this.el);
this.expanded = new Set();
Sao.View.Tree._super.init.call(this, view_id, screen, xml);
// Table of records
this.rows = [];
this.edited_row = null;
this.table = jQuery('
', {
'class': 'tree table table-hover table-condensed'
});
if (this.editable) {
this.table.addClass('table-bordered');
}
this.treeview.append(this.table);
this.colgroup = jQuery('
', {
'class': 'selection-state',
}).appendTo(this.colgroup);
if (this.selection_mode == Sao.common.SELECTION_NONE) {
col.css('width', 0);
}
this.thead = jQuery('
', {
'class': 'selection-state'
});
this.selection = jQuery('
', {
'type': 'checkbox',
});
this.selection.change(this.selection_changed.bind(this));
th.append(this.selection);
tr.append(th);
this.thead.append(tr);
this.tfoot = null;
var sum_row;
if (this.sum_widgets.size) {
sum_row = jQuery('
');
this.tfoot.append(sum_row);
// insert before thead to not hide drop-down from thead
this.table.prepend(this.tfoot);
}
if (this.children_field) {
this.expander = jQuery('
', {
'tabindex': 0,
'class': 'icon',
}));
this.update_expander('more');
this.expander.on('click keypress',
Sao.common.click_press(this.unfold.bind(this)));
Sao.common.ICONFACTORY.get_icon_url(
'tryton-unfold-' + this.expander.action)
.then(url => {
this.expander.children().attr('src', url);
});
}
let idx = 2;
for (const column of this.columns) {
col = jQuery('
', {
'class': column.attributes.widget,
}).appendTo(this.colgroup);
th = jQuery('
', {
'class': column.attributes.widget,
});
th.uniqueId();
th.get(0).dataset.column = idx;
var label = jQuery('
')
.text(column.attributes.string)
.attr('title', column.attributes.string);
if (this.editable) {
if (column.attributes.required) {
label.addClass('required');
}
if (!column.attributes.readonly) {
label.addClass('editable');
}
}
if (column.attributes.help) {
label.attr('title', column.attributes.help);
}
if (column.sortable) {
var arrow = jQuery('
', {
'class': 'icon',
});
label.append(arrow);
column.arrow = arrow;
th.click(column, (e) => {
if (is_resizing) {
e.stopImmediatePropagation();
return;
}
this.sort_model(e);
});
label.addClass('sortable');
}
tr.append(th.append(label));
let resizer = jQuery('
', {
'class': 'resizer',
'draggable': true,
}).appendTo(th);
let is_resizing = false;
resizer.on('mousedown', (event) => {
is_resizing = true;
let th = event.target.parentNode;
let headers = th.parentNode.childNodes;
let cols = this.colgroup[0].childNodes;
for (let i = 0; i < headers.length; i++) {
let header = headers[i];
let col = cols[i];
if (header == th) {
break;
}
if (col.style.display != 'none') {
col.style.width = `${header.offsetWidth}px`;
}
}
let startX = event.pageX;
let originalWidth = th.offsetWidth;
function on_mouse_move(e) {
let col = cols[th.dataset.column];
let width_offset = e.pageX - startX;
if (Sao.i18n.rtl) {
width_offset *= -1;
}
let width = Number(originalWidth) + width_offset;
col.style.width = `${width}px`;
}
function on_mouse_up() {
document.removeEventListener('mousemove', on_mouse_move);
document.removeEventListener('mouseup', on_mouse_up);
setTimeout(() => is_resizing = false, 0);
}
document.addEventListener('mousemove', on_mouse_move);
document.addEventListener('mouseup', on_mouse_up);
this.table.addClass('table-bordered');
event.preventDefault();
});
column.header = th;
column.col = col;
column.footers = [];
if (this.sum_widgets.size) {
var total_cell = jQuery('
', {
'class': column.class_,
});
if (this.sum_widgets.has(column)) {
var sum_label = this.sum_widgets.get(column)[0];
var sum_value = this.sum_widgets.get(column)[1];
total_cell.append(sum_label);
total_cell.append(sum_value);
}
sum_row.append(total_cell);
column.footers.push(total_cell);
}
this._set_column_width(column);
idx += 1;
}
this.tbody = jQuery('
');
this.table.append(this.tbody);
this.colgroup.prepend(jQuery('
', {
'class': 'tree-menu',
}));
this.thead.children().prepend(jQuery('
', {
'class': 'tree-menu',
}));
if (this.tfoot) {
this.tfoot.children().prepend(jQuery('
'));
}
this.set_drag_and_drop();
th = this.thead.find('th').first();
if (!jQuery.isEmptyObject(this.optionals)) {
th.addClass('optional');
}
var menu = jQuery('
', {
'class': 'dropdown-menu',
}).click(function(evt) {
evt.stopImmediatePropagation();
});
var dropdown = jQuery('
', {
'data-toggle': 'dropdown',
'aria-haspopup': true,
'aria-expanded': false,
'title': Sao.i18n.gettext("Menu"),
}).append(Sao.common.ICONFACTORY.get_icon_img('tryton-menu'))
.click(menu, this.tree_menu.bind(this))
).append(menu);
dropdown.on('hide.bs.dropdown', () => {
this.save_optional(true);
Sao.common.set_overflow(th, 'hide');
});
dropdown.on('show.bs.dropdown', () => {
Sao.common.set_overflow(th, 'show');
});
th.append(dropdown);
},
tree_menu: function(evt) {
const toggle = evt => {
let columns = evt.data;
let visible = jQuery(evt.delegateTarget).prop('checked');
columns.forEach(c => c.set_visible(visible));
this.save_optional();
this.display();
this.update_visible();
};
var menu = evt.data;
menu.empty();
for (let columns of Object.values(this.optionals)) {
let visible = columns.some(c => c.get_visible());
let string = [...new Set(columns.map(c => c.attributes.string))]
.join(' / ');
menu.append(jQuery('
', {
'role': 'presentation',
}).append(jQuery('
', {
'type': 'checkbox',
'checked': visible,
}).change(columns, toggle))
.append('Â ' + string)))));
}
if (!jQuery.isEmptyObject(this.optionals)) {
menu.append(jQuery('
', {
'role': 'separator',
'class': 'divider hidden-xs',
}));
}
menu.append(jQuery('
', {
'role': 'presentation',
'class': (
this.selected_records.length || !navigator.clipboard?
'' : 'disabled'),
}).append(jQuery('
', {
'role': 'menuitem',
'href': '#',
'tabindex': -1,
}).text(' ' + Sao.i18n.gettext("Copy Selected Rows"))
.prepend(
Sao.common.ICONFACTORY.get_icon_img('tryton-copy', {
'aria-hidden': 'true',
}))
.click(evt => {
evt.preventDefault();
this.on_copy().then(() => {
menu.dropdown('toggle');
});
})));
menu.append(jQuery('
', {
'role': 'menuitem',
'href': '#',
'tabindex': -1,
}).text(' ' + Sao.i18n.gettext("Reset Column Widths"))
.prepend(
Sao.common.ICONFACTORY.get_icon_img('tryton-refresh', {
'aria-hidden': 'true',
}))
.click((evt) => {
evt.preventDefault();
let TreeWidth = new Sao.Model('ir.ui.view_tree_width');
TreeWidth.execute(
'reset_width',
[this.screen.model_name, window.screen.width],
{});
for (let column of this.columns) {
if (column.col.data('default-width')) {
column.col.css(
'width', column.col.data('default-width'));
}
}
delete Sao.Screen.tree_column_width[this.screen.model_name];
menu.dropdown('toggle');
})));
},
save_optional: function(store=true) {
if (jQuery.isEmptyObject(this.optionals)) {
return;
}
var fields = {};
for (let [name, columns] of Object.entries(this.optionals)) {
fields[name] = columns.every(c => !c.get_visible());
}
if (store) {
var tree_optional_model = new Sao.Model(
'ir.ui.view_tree_optional');
tree_optional_model.execute('set_optional', [
this.view_id, fields], {});
}
Sao.Screen.tree_column_optional[this.view_id] = fields;
},
_set_column_width: function(column) {
let default_width = {
'integer': 8,
'selection': 9,
'reference': 20,
'one2many': 5,
'many2many': 5,
'boolean': 3,
'binary': 20,
}[column.attributes.widget] || 10;
if (column.attributes.symbol) {
default_width += 2;
}
var factor = 1;
if (column.attributes.expand) {
factor += parseInt(column.attributes.expand, 10);
}
default_width = default_width * 100 * factor + '%';
column.col.data('default-width', default_width);
let tree_column_width = (
Sao.Screen.tree_column_width[this.screen.model_name] || {});
let width = tree_column_width[name];
if (width || column.attributes.width) {
if (!width) {
width = column.attributes.width;
}
column.col.data('custom-width', `${width}px`);
} else {
width = default_width;
}
column.col.css('width', width);
},
save_width: function() {
var widths = {};
for (let column of this.columns) {
if (!column.get_visible() || !column.attributes.name ||
column instanceof Sao.View.Tree.ButtonColumn) {
continue;
}
// Use the DOM element to retrieve the exact style set
var width = column.col[0].style.width;
let custom_width = column.col.data('custom-width');
if (width.endsWith('px') && (width != custom_width)) {
widths[column.attributes.name] = Number(width.slice(0, -2));
}
}
if (!jQuery.isEmptyObject(widths)) {
let model_name = this.screen.model_name;
let TreeWidth = new Sao.Model('ir.ui.view_tree_width');
TreeWidth.execute(
'set_width',
[model_name, widths, window.screen.width],
{});
if (Object.prototype.hasOwnProperty.call(
Sao.Screen.tree_column_width, model_name)) {
Object.assign(
Sao.Screen.tree_column_width[model_name],
widths);
} else {
Sao.Screen.tree_column_width[model_name] = widths;
}
}
},
reset: function() {
this.display_size = null;
},
get editable() {
return (parseInt(this.attributes.editable || 0, 10) &&
!this.screen.attributes.readonly);
},
get creatable() {
if (this.editable) {
if (this.attributes.creatable) {
return Boolean(parseInt(this.attributes.creatable, 10));
} else {
return true;
}
} else {
return false;
}
},
unfold: function() {
if (!this.expander) {
return;
}
var action, unfold;
if (this.expander.action == 'more') {
unfold = function(row) {
if (row.is_selected() && !row.is_expanded()) {
row.expand_row();
}
row.rows.forEach(unfold);
};
action = 'less';
} else {
unfold = function(row) {
if (row.is_selected() && row.is_expanded()) {
row.collapse_row();
}
row.rows.forEach(unfold);
};
action = 'more';
}
this.rows.forEach(unfold);
this.update_expander(action);
},
update_expander: function(action) {
if (!this.expander) {
return;
}
if (action) {
this.expander.action = action;
}
Sao.common.ICONFACTORY.get_icon_url(
'tryton-unfold-' + this.expander.action)
.then(url => {
this.expander.children().attr('src', url);
});
if (jQuery.isEmptyObject(this.selected_records)) {
this.expander.css('visibility', 'hidden');
} else {
this.expander.css('visibility', 'visible');
}
},
sort_model: function(e){
var column = e.data;
var arrow = column.arrow;
for (const col of this.columns) {
if (col.arrow){
if (col != column && col.arrow.attr('src')) {
col.arrow.attr('src', '');
}
}
}
this.screen.order = this.screen.default_order;
if (arrow.data('order') == 'ASC') {
arrow.data('order', 'DESC');
Sao.common.ICONFACTORY.get_icon_url('tryton-arrow-up')
.then(function(url) {
arrow.attr('src', url);
});
this.screen.order = [[column.attributes.name, 'DESC']];
} else if (arrow.data('order') == 'DESC') {
arrow.data('order', '');
arrow.attr('src', '');
} else {
arrow.data('order', 'ASC');
Sao.common.ICONFACTORY.get_icon_url('tryton-arrow-down')
.then(function(url) {
arrow.attr('src', url);
});
this.screen.order = [[column.attributes.name, 'ASC']];
}
var unsaved_records = [];
for (const unsaved_record of this.group) {
if (unsaved_record.id < 0) {
unsaved_records = unsaved_record.group;
}
}
var search_string = this.screen.screen_container.get_text();
if ((!jQuery.isEmptyObject(unsaved_records)) ||
(this.screen.search_count == this.group.length) ||
(this.group.parent)) {
this.screen.search_filter(search_string, true).then(
ids => {
this.group.sort(function(a, b) {
a = ids.indexOf(a.id);
a = a < 0 ? ids.length : a;
b = ids.indexOf(b.id);
b = b < 0 ? ids.length : b;
if (a < b) {
return -1;
} else if (a > b) {
return 1;
} else {
return 0;
}
});
this.screen.display();
});
} else {
this.screen.search_filter(search_string);
}
},
update_arrow: function() {
var order = this.screen.order,
name = null,
direction = null,
icon = '';
if (order && (order.length == 1)) {
name = order[0][0];
direction = order[0][1];
if (direction) {
direction = direction.trim().split(' ', 1)[0];
icon = {
'ASC': 'tryton-arrow-down',
'DESC': 'tryton-arrow-up',
}[direction];
}
}
this.columns.forEach(function(col) {
var arrow = col.arrow;
if (arrow) {
if (col.attributes.name != name) {
arrow.data('order', '');
arrow.attr('src', '');
} else {
arrow.data('order', direction);
Sao.common.ICONFACTORY.get_icon_url(icon)
.then(function(url) {
arrow.attr('src', url);
});
}
}
});
},
on_copy: function() {
var data = [];
this.selected_records.forEach((record) => {
var values = [];
this.columns.forEach((col) => {
if (!col.get_visible() || !col.attributes.name ||
col instanceof Sao.View.Tree.ButtonColumn) {
return;
}
var text;
if (!record.is_loaded(col.attributes.name)) {
try {
record.load(col.attributes.name, false, false);
text = col.get_textual_value(record);
} catch (e) {
Sao.Logger.error(
"Error loading " +
`${this.attributes.name} for ${record}`);
text = Sao.i18n.gettext('#ERROR');
}
} else {
text = col.get_textual_value(record);
}
values.push('"' + String(text).replace('"', '""') + '"');
});
data.push(values.join('\t'));
});
return navigator.clipboard.writeText(data.join('\n'))
.catch(error => {
alert(Sao.i18n.gettext(
"Failed to copy text to clipboard: %1", error));
});
},
_add_drag_n_drop: function() {
Sortable.create(this.tbody[0], {
handle: '.draggable-handle',
ghostClass: 'dragged-row'
});
this.tbody.on('dragstart', this.drag_data_get.bind(this));
this.tbody.on('drop', this.drag_data_received.bind(this));
},
set_drag_and_drop: function() {
var dnd = false;
var children, parent_name;
if (this.children_field) {
children = this.screen.model.fields[this.children_field];
if (children) {
parent_name = children.description.relation_field;
dnd = Boolean(this.widgets[parent_name]);
}
} else if (this.attributes.sequence) {
dnd = true;
}
if (this.screen.readonly) {
dnd = false;
}
this.draggable = dnd;
if (dnd) {
this.thead.find('th').first().addClass('draggable-handle');
this._add_drag_n_drop();
}
},
drag_data_get: function(evt) {
var row_position = 0;
var row_leaves = [];
var set_dragged_row = function(row) {
if (row.el[0] === evt.target) {
evt.originalEvent.dataTransfer.setData('path', row.path);
evt.originalEvent.dataTransfer.setData(
'position', row_position);
}
if (row.rows.length === 0) {
row_leaves.push(row);
}
row_position += 1;
row.rows.forEach(set_dragged_row.bind(this));
};
this.rows.forEach(set_dragged_row.bind(this));
},
drag_data_received: function(evt) {
var dataTransfer = evt.originalEvent.dataTransfer;
var origin_path = dataTransfer.getData('path').split('.');
if (origin_path.length === 0) {
return ;
}
var row = this;
while (origin_path.length > 0) {
row = row.rows[origin_path[0]];
origin_path = origin_path.slice(1);
}
var record = row.record;
var parent_row = null;
var dest_position;
if ((evt.ctrlKey || evt.metaKey) && this.children_field) {
parent_row = this._find_row(row.el.prev());
dest_position = (parent_row || this).rows.length;
} else {
var sibling_row;
if (evt.shiftKey) {
sibling_row = this._find_row(row.el.prev());
if (sibling_row) {
parent_row = sibling_row.parent_;
dest_position = (
(parent_row || this).rows.indexOf(sibling_row) + 1);
} else {
parent_row = null;
dest_position = 0;
}
} else {
sibling_row = this._find_row(row.el.next());
if (sibling_row) {
parent_row = sibling_row.parent_;
dest_position = (
(parent_row || this).rows.indexOf(sibling_row));
} else {
parent_row = null;
dest_position = this.rows.length;
}
}
}
var current_row = parent_row;
while (current_row && (current_row != row)) {
current_row = current_row.parent_;
}
if (current_row) {
// There is a recursion cancel the drop
// by moving the row at its previous place
var original_position = dataTransfer.getData('position');
var successor = jQuery(
this.tbody.children()[original_position]);
successor.before(row.el);
return;
}
var previous_row = row;
var move_child = function(child_row) {
previous_row.el.after(child_row.el);
previous_row = child_row;
child_row.rows.forEach(move_child);
};
row.rows.forEach(move_child);
var dest_group;
var origin_group, origin_position;
origin_group = record.group;
origin_position = row.group_position;
if (parent_row) {
dest_group = parent_row.record.children_group(
this.children_field);
} else {
dest_group = this.group;
}
var origin_rows, dest_rows;
if (row.parent_) {
origin_rows = row.parent_.rows;
} else {
origin_rows = this.rows;
}
if (parent_row) {
dest_rows = parent_row.rows;
} else {
dest_rows = this.rows;
}
if (origin_group === dest_group) {
if (origin_position < dest_position) {
dest_position -= 1;
}
origin_group.splice(origin_position, 1);
origin_group.splice(dest_position, 0, record);
origin_group.record_modified();
} else {
origin_group.remove(record, true, true);
// Don't remove record from previous group
// as the new parent will change the parent
// This prevents concurrency conflict
origin_group.record_removed.splice(
origin_group.record_removed.indexOf(record));
dest_group.add(record, dest_position);
if (!record.parent_name) {
record.modified_fields[origin_group.parent_name] = true;
record._values[origin_group.parent_name] = null;
} else {
record.modified_fields[origin_group.parent_name] = true;
}
}
dest_rows.splice(dest_position, 0, row);
origin_rows.splice(origin_position, 1);
row.parent_ = parent_row;
row.record.group = dest_group;
for (const r of dest_rows.slice(dest_position)) {
r.reset_path();
}
for (const r of origin_rows.slice(origin_position)) {
r.reset_path();
}
var selected = this.get_selected_paths();
row.redraw(selected);
var child_redraw = function(child_row) {
child_row.redraw(selected);
child_row.rows.forEach(child_redraw);
};
row.rows.forEach(child_redraw);
if (this.attributes.sequence) {
row.record.group.set_sequence(
this.attributes.sequence, this.screen.new_position);
}
},
get_fields: function() {
return Object.keys(this.widgets);
},
get_buttons: function() {
var buttons = [];
for (const widget of this.state_widgets) {
if ((widget instanceof Sao.View.Tree.ButtonColumn) ||
(widget instanceof Sao.View.Tree.ButtonMultiple)) {
buttons.push(widget);
}
}
return buttons;
},
button_clicked: function(event) {
var button = event.data;
button.el.prop('disabled', true); // state will be reset at display
this.screen.button(button.attributes);
},
display: function(selected, expanded) {
if ((this.display_size === null) && this.screen.group.length) {
if (this.screen.group.parent) {
this.display_size = 0;
} else {
this.display_size = Sao.config.display_size;
}
}
if (!this.display_size &&
(!jQuery.isEmptyObject(selected) ||
!jQuery.isEmptyObject(expanded))) {
this.display_size = Sao.config.display_size;
}
let display_size = this.display_size || 0;
var current_record = this.record;
if (jQuery.isEmptyObject(selected) && current_record) {
selected = this.get_selected_paths();
if (this.selection.prop('checked') &&
!this.selection.prop('indeterminate')) {
for (const record of this.screen.group.slice(
this.rows.length, display_size)) {
selected.push([record.id]);
}
} else {
var current_path = current_record.get_path(this.group);
current_path = current_path.map(function(e) {
return e[1];
});
if (!Sao.common.contains(selected, current_path)) {
selected = [current_path];
}
}
}
expanded = expanded || this.get_expanded_paths();
if (this.selection_mode == Sao.common.SELECTION_MULTIPLE) {
this.selection.show();
} else {
this.selection.hide();
}
const group_records = (group, root) => {
var records = [];
for (const record of group) {
records.push(record);
let path = root.concat([record.id]);
if (Sao.common.contains(expanded, path)) {
const children = record.field_get_client(
this.children_field);
Array.prototype.push.apply(
records, group_records(children, path));
}
}
return records;
};
const row_records = rows => {
var records = [];
for (const row of rows) {
records.push(row.record);
if (row.is_expanded()) {
Array.prototype.push.apply(
records, row_records(row.rows));
}
}
return records;
};
var min_display_size = Math.min(
this.group.length, display_size);
if (this.children_field) {
if (!Sao.common.compare(
group_records(this.group.slice(0, min_display_size), []),
row_records(this.rows))) {
this.construct();
}
} else if ((min_display_size > this.rows.length) &&
Sao.common.compare(
this.group.slice(0, this.rows.length),
row_records(this.rows))) {
this.construct(true);
} else if ((min_display_size != this.rows.length) ||
!Sao.common.compare(
this.group.slice(0, this.rows.length),
row_records(this.rows))){
this.construct();
}
// Set column visibility depending on attributes and domain
// start at 2 because of checkbox and option headers
var visible_columns = 2;
var domain = [];
if (!jQuery.isEmptyObject(this.screen.domain)) {
domain.push(this.screen.domain);
}
var tab_domain = this.screen.screen_container.get_tab_domain();
if (!jQuery.isEmptyObject(tab_domain)) {
domain.push(tab_domain);
}
var inversion = new Sao.common.DomainInversion();
domain = inversion.simplify(domain);
var decoder = new Sao.PYSON.Decoder(this.screen.context);
var min_width = [];
var tree_column_optional = (
Sao.Screen.tree_column_optional[this.view_id] || {});
for (const column of this.columns) {
visible_columns += 1;
var name = column.attributes.name;
if (!name) {
return;
}
var optional;
if ((column.attributes.optional) &&
Object.prototype.hasOwnProperty.call(
tree_column_optional, name)) {
optional = tree_column_optional[name];
} else {
optional = Boolean(parseInt(
column.attributes.optional || '0', 10));
}
var invisible = decoder.decode(
column.attributes.tree_invisible || '0');
if (invisible || optional) {
visible_columns -= 1;
column.set_visible(false);
} else if (name === this.screen.exclude_field) {
visible_columns -= 1;
column.set_visible(false);
} else if (name in this.screen.model.fields) {
const field = this.screen.model.fields[name];
var inv_domain = inversion.domain_inversion(domain, name);
if (typeof inv_domain != 'boolean') {
inv_domain = inversion.simplify(inv_domain);
}
var unique = inversion.unique_value(
inv_domain, field._single_value)[0];
if (unique && jQuery.isEmptyObject(this.children_field)) {
visible_columns -= 1;
column.set_visible(false);
} else {
column.set_visible(true);
}
}
if (!column.get_visible()) {
if (!column.col.data('hidden-width')) {
column.col.data('hidden-width', column.col.css('width'))
}
column.col.css('width', 0);
column.col.hide();
} else if (!column.col.hasClass('draggable-handle') &&
!column.col.hasClass('optional') &&
!column.col.hasClass('selection-state') &&
!column.col.hasClass('favorite')) {
if (column.col.data('hidden-width')) {
column.col.css(
'width', column.col.data('hidden-width'));
column.col.removeData('hidden-width');
}
let width;
if (column.col.data('custom-width')) {
width = column.col.data('custom-width');
} else {
width = column.col.data('default-width');
}
if (width && width.endsWith('%')) {
width = parseInt(width.slice(0, -1), 10) / 100;
if (column.attributes.expand) {
width /= parseInt(column.attributes.expand, 10);
}
width = `${width}em`;
}
min_width.push(width);
column.col.show();
}
}
this.table.find('thead > tr > th .resizer').show();
this.table.find('thead > tr > th:visible:last .resizer').hide();
if (this.children_field) {
this.columns.every(column => {
if (column.col.hasClass('draggable-handle') ||
column.header.hasClass('invisible')) {
return true;
} else {
if (this.expander.parent()[0] !== column.header[0]) {
column.header.prepend(this.expander);
}
return false;
}
});
}
this.table.css('min-width', 'calc(' + min_width.join(' + ') + ')');
this.scrollbar.css('min-width', this.table.css('min-width'));
if (!this.table.hasClass('no-responsive') &
(this.columns.filter(function(c) {
return c.get_visible();
}).length > 1)) {
this.table.addClass('responsive');
this.table.addClass('responsive-header');
} else {
this.table.removeClass('responsive');
this.table.removeClass('responsive-header');
}
this.update_arrow();
return this.redraw(selected, expanded).then(() => {
var tbody = this.table.children('tbody');
if (!tbody.length) {
this.table.append(this.tbody);
} else if (tbody !== this.tbody) {
tbody.replaceWith(this.tbody);
}
this.tbody.append(this.rows.filter(function(row) {
return !row.el.parent().length;
}).map(function(row) {
return row.el;
}));
this.update_selection(); // update after new rows has been added
this.update_visible();
if ((display_size < this.group.length) &&
(!this.tbody.children().last().hasClass('more-row'))) {
var more_row = jQuery('
', {
'class': 'more-row',
});
var more_cell = jQuery('
', {
'colspan': visible_columns,
});
var more_button = jQuery('
', {
'class': 'btn btn-default btn-block',
'type': 'button',
'title': Sao.i18n.gettext("More"),
}).text(Sao.i18n.gettext('More')
).one('click', () => {
this.tbody.find('tr.more-row').remove();
var height = this.table.height();
this.display_size += Sao.config.display_size;
this.display();
height -= this.treeview.height();
height -= 50;
if (this.tfoot) {
height -= this.tfoot.height();
}
this.treeview[0].scroll({
'top': height,
});
});
more_cell.append(more_button);
more_row.append(more_cell);
this.tbody.append(more_row);
if (moreObserver) {
window.setTimeout(
() => moreObserver.observe(more_button[0]));
}
}
}).done(
Sao.common.debounce(this.update_with_selection.bind(this), 250));
},
construct: function(extend) {
if (!extend) {
this.rows = [];
// The new tbody is added to the DOM
// after the rows have been rendered
// to minimize browser reflow
this.tbody = jQuery('
');
if (this.draggable) {
this._add_drag_n_drop();
}
this.edited_row = null;
} else {
this.tbody.find('tr.more-row').remove();
}
// The rows are added to tbody after being rendered
// to minimize browser reflow
for (const record of this.group.slice(
this.rows.length, this.display_size || 0)) {
var RowBuilder;
if (this.editable) {
RowBuilder = Sao.View.Tree.RowEditable;
} else {
RowBuilder = Sao.View.Tree.Row;
}
this.rows.push(new RowBuilder(this, record, this.rows.length));
}
},
redraw: function(selected, expanded) {
return redraw_async(this.rows, selected, expanded);
},
switch_: function(path) {
this.screen.row_activate();
},
select_changed: function(record) {
if (this.edited_row) {
record = this.edited_row.record;
this.edited_row.set_selection(true);
}
this.record = record;
// TODO update_children
},
update_visible: function() {
var offset = 2; // selection-state + tree-menu
var thead_visible = this.thead.is(':visible');
var to_hide = jQuery();
var to_show = jQuery();
for (var i = 0; i < this.columns.length; i++) {
var column = this.columns[i];
// CSS is 1-indexed
var selector = `tr > td:nth-child(${i + offset + 1})`;
if ((thead_visible && column.header.is(':hidden')) ||
(column.header.css('display') == 'none')) {
to_hide = to_hide.add(this.tbody.find(selector));
} else {
to_show = to_show.add(this.tbody.find(selector));
}
}
to_hide.addClass('invisible').hide();
to_show.removeClass('invisible').show();
},
update_with_selection: function() {
let selected_records = this.selected_records;
for (let widget of this.state_widgets) {
if (widget instanceof Sao.View.Tree.ButtonMultiple) {
widget.set_state(selected_records);
}
}
let records_ids = selected_records.map(function(record){
return record.id;
});
for (const [column, sum_widget] of this.sum_widgets) {
var name = column.attributes.name;
var aggregate = '-';
var sum_label = sum_widget[0];
var sum_value = sum_widget[1];
var sum_ = null;
var selected_sum = null;
var loaded = true;
var digit = 0;
var field = this.screen.model.fields[name];
var i, record;
for (i=0; i < this.group.length; i++) {
record = this.group[i];
if (!record.get_loaded([name]) && record.id >=0){
loaded = false;
break;
}
var value = field.get(record);
if (value && value.isTimeDelta) {
value = value.asSeconds();
}
if (value !== null){
if (sum_ === null){
sum_ = value;
}else {
sum_ += value;
}
if (~records_ids.indexOf(record.id) ||
!selected_records){
if (selected_sum === null){
selected_sum = value;
}else {
selected_sum += value;
}
}
if (field.digits) {
var fdigits = field.digits(record);
if (fdigits && digit !== null){
digit = Math.max(fdigits[1], digit);
} else {
digit = null;
}
}
}
}
if (loaded) {
if (field.description.type == 'timedelta'){
var converter = field.converter(this.group);
selected_sum = Sao.common.timedelta.format(
Sao.TimeDelta(null, selected_sum), converter);
sum_ = Sao.common.timedelta.format(
Sao.TimeDelta(null, sum_), converter);
} else if (digit !== null){
var options = {};
options.minimumFractionDigits = digit;
options.maximumFractionDigits = digit;
selected_sum = (selected_sum || 0).toLocaleString(
Sao.i18n.BC47(Sao.i18n.getlang()), options);
sum_ = (sum_ || 0).toLocaleString(
Sao.i18n.BC47(Sao.i18n.getlang()), options);
} else {
selected_sum = (selected_sum || 0).toLocaleString(
Sao.i18n.BC47(Sao.i18n.getlang()));
sum_ = (sum_ || 0).toLocaleString(
Sao.i18n.BC47(Sao.i18n.getlang()));
}
aggregate = selected_sum + '\n' + sum_;
}
sum_value.text(aggregate);
sum_value.parent().attr(
'title', sum_label.text() + '\n' + sum_value.text());
}
},
get selected_records() {
if (this.selection_mode == Sao.common.SELECTION_NONE) {
return [];
}
var records = [];
var add_record = function(row) {
if (row.is_selected()) {
records.push(row.record);
}
row.rows.forEach(add_record);
};
this.rows.forEach(add_record);
if (this.selection.prop('checked') &&
!this.selection.prop('indeterminate')) {
for (const record of this.group.slice(this.rows.length)) {
records.push(record);
}
}
return records;
},
get listed_records() {
if (!this.children_field) {
return this.group.slice();
}
const get_listed_records = start => {
var records = [];
var row = this.find_row(start);
var children_rows = row ? row.rows : this.rows;
for (var idx = 0, len = this.n_children(row);
idx < len; idx++) {
var path = start.concat([idx]);
row = children_rows[idx];
if (row) {
var record = row.record;
records.push(record);
if (row.is_expanded()) {
records = records.concat(get_listed_records(path));
}
}
}
return records;
};
return get_listed_records([]).concat(this.group.slice(this.rows.length));
},
get_listed_paths: function() {
if (!this.children_field) {
return this.group.map(function(record) {
return [record.id];
});
}
const get_listed_paths = (start, start_path) => {
var paths = [];
var row = this.find_row(start);
var children_rows = row ? row.rows : this.rows;
for (var idx = 0, len = this.n_children(row);
idx < len; idx++) {
var path = start.concat([idx]);
row = children_rows[idx];
if (row) {
var record = row.record;
var id_path = start_path.concat([record.id]);
paths.push(id_path);
if (row.is_expanded()) {
paths = paths.concat(get_listed_paths(path, id_path));
}
}
}
return paths;
};
return get_listed_paths([], []).concat(
this.group.slice(this.rows.length).map(function(record) {
return [record.id];
}));
},
select_records: function(from, to) {
if (!from && to) {
from = this.rows[0].record;
}
if (from && to) {
var from_idx = from.get_index_path(this.screen.group);
var to_idx = to.get_index_path(this.screen.group);
var max_len = Math.min(from_idx.length, to_idx.length);
var tmp;
for (var i=0; i < max_len; i++) {
if (from_idx[i] > to_idx[i]) {
tmp = from;
from = to;
to = tmp;
break;
}
}
if (!tmp && (from_idx.length > to_idx.length)) {
tmp = from;
from = to;
to = tmp;
}
}
var value = this.rows[0].record === from;
var select_record = function(row) {
var record = row.record;
if (record === from) {
value = true;
}
row.set_selection(value);
if (record === to) {
value = false;
}
row.rows.forEach(select_record);
};
this.rows.forEach(select_record);
},
selection_changed: function() {
var value = this.selection.prop('checked');
var set_checked = function(row) {
row.set_selection(value);
row.rows.forEach(set_checked);
};
this.rows.forEach(set_checked);
if (value && this.rows[0]) {
this.select_changed(this.rows[0].record);
} else {
this.select_changed(null);
}
this.update_expander(value? 'more' : null);
this.update_with_selection();
},
update_selection: function() {
this.update_with_selection();
var selected_records = this.selected_records;
this.selection.prop('indeterminate', false);
if (jQuery.isEmptyObject(selected_records)) {
this.selection.prop('checked', false);
} else if (
this.rows.every((row) => row.is_selected()) &&
(selected_records.length >= this.tbody.children().length)) {
this.selection.prop('checked', true);
} else {
this.selection.prop('indeterminate', true);
// Set checked to go first unchecked after first click
this.selection.prop('checked', true);
}
this.update_expander();
},
get_selected_paths: function() {
var selected_paths = [];
function get_selected(row, path) {
var i, r, len, r_path;
for (i = 0, len = row.rows.length; i < len; i++) {
r = row.rows[i];
r_path = path.concat([r.record.id]);
if (r.is_selected()) {
selected_paths.push(r_path);
}
get_selected(r, r_path);
}
}
get_selected(this, []);
return selected_paths;
},
get_expanded_paths: function(
starting_path=[], starting_id_path=[]) {
var id_path, id_paths, row, children_rows, path;
id_paths = [];
row = this.find_row(starting_path);
children_rows = row ? row.rows : this.rows;
for (var path_idx = 0, len = this.n_children(row) ;
path_idx < len ; path_idx++) {
path = starting_path.concat([path_idx]);
row = children_rows[path_idx];
if (row && row.is_expanded()) {
id_path = starting_id_path.concat(row.record.id);
id_paths.push(id_path);
id_paths = id_paths.concat(this.get_expanded_paths(path,
id_path));
}
}
return id_paths;
},
find_row: function(path) {
var index;
var row = null;
var group = this.rows;
for (var i=0, len=path.length; i < len; i++) {
index = path[i];
if (!group || index >= group.length) {
return null;
}
row = group[index];
group = row.rows;
if (!this.children_field) {
break;
}
}
return row;
},
n_children: function(row) {
if (!row || !this.children_field) {
return this.rows.length;
}
if (row.record.is_loaded(this.children_field)) {
return row.record.field_get_client(this.children_field).length;
} else {
return 0;
}
},
set_cursor: function(new_, reset_view) {
var path, row, column;
var td, prm;
if (!this.record) {
return;
}
path = this.record.get_index_path(this.group);
if (this.rows.length <= path[0]) {
this.display_size = this.group.length;
this.display();
}
if (path.length > 1) {
prm = this.rows[path[0]].expand_to_path(
path.slice(1),
[this.record.get_path(this.group).map(function(value) {
return value[1];
})]);
}
const focus = () => {
row = this.find_row(path);
if (row) {
Sao.common.scrollIntoViewIfNeeded(row.el);
column = row.next_column(null, new_);
if (column !== null) {
td = row._get_column_td(column);
if (this.editable && new_) {
td.trigger('click');
}
var child = Sao.common.find_focusable_child(td);
if (child) {
child.focus();
}
}
}
};
if (prm) {
prm.then(focus);
} else {
focus();
}
},
save_row: function() {
var i, prm, edited_row = this.edited_row;
if (!this.editable || !this.edited_row) {
return jQuery.when();
}
if (!this.edited_row.record.validate(
this.get_fields(), false, false, true)) {
var focused = false;
var invalid_fields = this.edited_row.record.invalid_fields();
for (i = 0; i < this.columns.length; i++) {
var col = this.columns[i];
if (col.attributes.name in invalid_fields) {
var td = this.edited_row._get_column_td(i);
var editable_el = this.edited_row.get_editable_el(td);
var widget = editable_el.data('widget');
widget.display(this.edited_row.record, col.field);
if (!focused) {
widget.focus();
focused = true;
}
}
}
return;
}
if (!this.group.parent) {
prm = this.edited_row.record.save();
} else if (this.screen.attributes.pre_validate) {
prm = this.record.pre_validate();
} else {
prm = jQuery.when();
}
prm.fail(() => {
if (this.edited_row != edited_row) {
this.edit_row(null);
edited_row.set_selection(true);
edited_row.selection_changed();
this.edit_row(edited_row);
}
});
return prm;
},
edit_row: function(row) {
if (!this.editable || this.edited_row == row) {
return;
}
if (this.edited_row) {
this.edited_row.unset_editable();
}
if (row) {
row.set_editable();
}
this.edited_row = row;
},
_find_row: function(tr) {
var row = null;
var find_row = function(r) {
if (r.el[0] == tr[0]) {
row = r;
return;
}
r.rows.forEach(find_row);
};
this.rows.forEach(find_row);
return row;
}
});
function redraw_async(rows, selected, expanded) {
var dfd= jQuery.Deferred(),
i = 0;
var redraw = function() {
for (; i < rows.length; i++) {
var row = rows[i];
var record = row.record;
var field_name;
for (var j=0; j < row.tree.columns.length; j++) {
var column = row.tree.columns[j];
if (column.type == 'field') {
field_name = column.attributes.name;
break;
}
}
if (field_name && !record.is_loaded(field_name)) {
// Prefetch the first field to prevent promises in
// Cell.render
record.load(field_name, true, false).done(redraw);
return;
} else {
row.redraw(selected, expanded);
}
}
dfd.resolve();
};
redraw();
return dfd.promise();
}
Sao.View.Tree.Row = Sao.class_(Object, {
init: function(tree, record, pos, parent) {
this.tree = tree;
this.current_column = null;
this.rows = [];
this.record = record;
this.parent_ = parent;
this.children_field = tree.children_field;
this.expander = null;
this._group_position = null;
this._path = null;
this._drawed_record = null;
this.el = jQuery('
');
this.el.on('click', this.select_row.bind(this));
this._construct();
},
get group_position() {
if (this._group_position === null) {
this._group_position = this.record.group.indexOf(this.record);
}
return this._group_position;
},
get path() {
if (!this._path) {
var path;
if (this.parent_) {
path = jQuery.extend([], this.parent_.path.split('.'));
} else {
path = [];
}
path.push(this.group_position);
this._path = path.join('.');
}
return this._path;
},
reset_path: function() {
this._group_position = null;
this._path = null;
for (const row of this.rows) {
row.reset_path();
}
},
is_expanded: function() {
return this.tree.expanded.has(this);
},
get_id_path: function() {
if (!this.parent_) {
return [this.record.id];
}
return this.parent_.get_id_path().concat([this.record.id]);
},
_construct: function() {
var td;
this.tree.el.uniqueId();
td = jQuery('
');
if (this.tree.draggable) {
td.addClass('draggable-handle');
td.append(Sao.common.ICONFACTORY.get_icon_img('tryton-drag'));
}
if (!jQuery.isEmptyObject(this.tree.optionals)) {
td.addClass('optional');
}
this.el.append(td);
td = jQuery('
', {
'class': 'selection-state',
}).click(event_ => {
event_.stopPropagation();
this.selection.click();
});
this.el.append(td);
this.selection = jQuery('
', {
'type': 'checkbox',
'name': 'tree-selection-' + this.tree.el.attr('id'),
});
this.selection.click(function(event_) {
event_.stopPropagation();
});
this.selection.change(this.selection_changed.bind(this));
td.append(this.selection);
const on_click = event_ => {
if (this.expander && !this.is_expanded() &&
(this.tree.n_children(this) <= Sao.config.limit)) {
this.toggle_row();
}
this.select_column(event_.data.index);
};
if (this.children_field) {
this.expander = jQuery('
', {
'tabindex': 0,
'class': 'icon',
});
this.expander.children().html(' ');
this.expander.on('click keypress',
Sao.common.click_press(this.toggle_row.bind(this)));
this.expander.dblclick((evt) => {
// prevent calling switch_row on td
evt.preventDefault();
evt.stopImmediatePropagation();
});
}
for (var i = 0; i < this.tree.columns.length; i++) {
var column = this.tree.columns[i];
if (column instanceof Sao.View.Tree.ButtonColumn) {
td = jQuery('
', {
'data-title': column.attributes.string +
Sao.i18n.gettext(': ')
}).append(jQuery('
', { // For responsive min-height
'aria-hidden': true
}));
}
td.attr('headers', column.header.attr('id'));
td.on('click keypress', {'index': i}, on_click);
if (!this.tree.editable) {
td.dblclick(this.switch_row.bind(this));
} else {
if (column.attributes.required) {
td.addClass('required');
}
if (!column.attributes.readonly) {
td.addClass('editable');
}
}
var cell = jQuery('
', {
'class': 'cell',
});
td.append(cell);
if (column.prefixes) {
for (let j = 0; j < column.prefixes.length; j++) {
cell.append(jQuery('
', {
'class': 'prefix'
}));
}
}
cell.append(jQuery('
', {
'class': 'widget'
}));
if (column.suffixes) {
for (let j = 0; j < column.suffixes.length; j++) {
cell.append(jQuery('
', {
'class': 'suffix'
}));
}
}
this.el.append(td);
}
},
_get_column_td: function(column_index, row) {
row = row || this.el;
// take into account the selection and option column
return jQuery(row.children()[column_index + 2]);
},
redraw: function(selected, expanded) {
selected = selected || [];
expanded = expanded || [];
switch(this.tree.selection_mode) {
case Sao.common.SELECTION_NONE:
this.selection.hide();
break;
case Sao.common.SELECTION_SINGLE:
this.selection.attr('type', 'radio');
this.selection.show();
break;
case Sao.common.SELECTION_MULTIPLE:
this.selection.attr('type', 'checkbox');
this.selection.show();
break;
}
function apply_visual(el, visual) {
for (const name of ['muted', 'success', 'warning', 'danger']) {
var klass = name == 'muted' ? 'text-muted' : name;
if (name == visual) {
el.addClass(klass);
} else {
el.removeClass(klass);
}
}
}
if (this._drawed_record !== this.record.identity) {
for (var i = 0; i < this.tree.columns.length; i++) {
var column = this.tree.columns[i];
var td = this._get_column_td(i);
var cell = td.find('.cell');
var item;
if (column.prefixes) {
for (var j = 0; j < column.prefixes.length; j++) {
var prefix = column.prefixes[j];
var prefix_el = jQuery(cell.children('.prefix')[j]);
item = prefix_el.children();
if (item.length) {
prefix.render(this.record, item);
} else {
prefix_el.empty().append(prefix.render(this.record));
}
}
}
var widget = cell.children('.widget');
item = widget.children();
if (item.length) {
column.render(this.record, item);
} else {
widget.empty().append(column.render(this.record));
}
if (column.suffixes) {
for (var k = 0; k < column.suffixes.length; k++) {
var suffix = column.suffixes[k];
var suffix_el = jQuery(cell.children('.suffix')[k]);
item = suffix_el.children();
if (item.length) {
suffix.render(this.record, item);
} else {
suffix_el.empty().append(suffix.render(this.record));
}
}
}
let visual = this.record.expr_eval(column.attributes.visual);
if (this.record.deleted || this.record.removed) {
visual = 'muted';
}
apply_visual(td, visual);
}
}
if (this.children_field) {
this.tree.columns.every((column, i) => {
if (column.col.hasClass('draggable-handle') ||
column.header.hasClass('invisible')) {
return true;
} else {
var td = this._get_column_td(i);
var cell = td.find('.cell');
if (this.expander.parent()[0] !== cell[0]) {
cell.prepend(this.expander);
}
return false;
}
});
}
this._drawed_record = this.record.identity;
var row_id_path = this.get_id_path();
this.set_selection(Sao.common.contains(selected, row_id_path));
if (selected.length && Sao.common.compare(selected[0], row_id_path)) {
Sao.common.scrollIntoViewIfNeeded(this.el);
}
if (this.children_field) {
var depth = this.path.split('.').length;
var margin = 'margin-left';
if (Sao.i18n.rtl) {
margin = 'margin-right';
}
this.expander.children().css(margin, (depth - 1) + 'em');
const update_expander = () => {
var length = this.record.field_get_client(
this.children_field).length;
if (length && (
this.is_expanded() ||
Sao.common.contains(expanded, row_id_path))) {
this.expander.css('visibility', 'visible');
this.tree.expanded.add(this);
this.expand_children(selected, expanded);
this.update_expander(true);
} else {
this.expander.css('visibility',
length ? 'visible' : 'hidden');
this.update_expander(false);
}
};
if (!this.record.is_loaded(this.children_field)) {
this.record.load(this.children_field, true, false)
.done(update_expander);
} else {
update_expander();
}
}
let visual = this.record.expr_eval(this.tree.attributes.visual);
if (this.record.deleted || this.record.removed) {
if (this.record.deleted) {
this.el.css('text-decoration', 'line-through');
}
visual = 'muted';
} else {
this.el.css('text-decoration', 'inherit');
}
apply_visual(this.el, visual);
},
toggle_row: function() {
if (this.is_expanded()) {
this.collapse_row();
} else {
this.expand_row();
}
return false;
},
expand_row: function() {
if (this.tree.n_children(this) > Sao.config.limit) {
this.tree.record = this.record;
this.tree.screen.switch_view('form');
} else {
this.update_expander(true);
this.tree.expanded.add(this);
this.expand_children();
}
},
collapse_row: function() {
for (const row of this.rows) {
if (row.exception) {
row.record.cancel();
}
}
this.update_expander(false);
this.tree.expanded.delete(this);
this.collapse_children();
},
update_expander: function(expanded) {
var icon;
if (expanded) {
icon = 'tryton-arrow-down';
} else {
icon = 'tryton-arrow-right';
}
Sao.common.ICONFACTORY.get_icon_url(icon)
.then(url => {
this.expander.children().attr('src', url);
});
},
collapse_children: function() {
for (const row of this.rows) {
row.collapse_children();
row.el.remove();
}
this.rows = [];
},
expand_children: function(selected, expanded) {
return this.record.load(
this.children_field, true, false).done(() => {
if (this.rows.length === 0) {
var children = this.record.field_get_client(
this.children_field);
children.forEach((record, pos, group) => {
// The rows are added to the tbody after being rendered
// to minimize browser reflow
this.rows.push(new this.Class(
this.tree, record, pos, this));
});
}
redraw_async(this.rows, selected, expanded).then(() => {
this.el.after(this.rows.filter(function(row) {
return !row.el.parent().length;
}).map(function(row) {
return row.el;
}));
this.tree.update_selection();
this.tree.update_visible();
});
});
},
switch_row: function() {
Sao.common.clear_selection();
if (this.tree.selection_mode != Sao.common.SELECTION_NONE) {
this.set_selection(true);
this.selection_changed();
if (!this.is_selected()) {
return;
}
}
this.tree.switch_(this.path);
},
select_column: function(index) {
},
select_row: function(event_) {
if (this.tree.selection_mode == Sao.common.SELECTION_NONE) {
this.tree.select_changed(this.record);
this.switch_row();
} else {
var current_record;
if (event_.shiftKey &&
this.tree.selection_mode != Sao.common.SELECTION_SINGLE) {
current_record = this.tree.screen.current_record;
this.tree.select_records(current_record, this.record);
} else {
let selected = this.is_selected();
if (!(event_.ctrlKey || event_.metaKey) ||
this.tree.selection_mode ==
Sao.common.SELECTION_SINGLE) {
this.tree.select_records(null, null);
}
this.set_selection(!selected);
}
if (event_.shiftKey || event_.ctrlKey || event_.metaKey) {
Sao.common.clear_selection();
}
this.selection_changed();
if (current_record) {
// Keep original current record with shift select
this.tree.screen.current_record = current_record;
}
}
},
is_selected: function() {
if (this.tree.selection_mode == Sao.common.SELECTION_NONE) {
return false;
}
return this.selection.prop('checked');
},
set_selection: function(value) {
if (this.tree.selection_mode == Sao.common.SELECTION_NONE) {
return;
}
this.selection.prop('checked', value);
if (value) {
this.el.addClass('selected');
} else {
this.el.removeClass('selected');
}
if (!value) {
this.tree.selection.prop('checked', false);
}
},
selection_changed: function() {
var is_selected = this.is_selected();
if (this.tree.selection_mode == Sao.common.SELECTION_SINGLE) {
this.tree.select_records(null, null);
}
this.set_selection(is_selected);
if (is_selected) {
this.tree.select_changed(this.record);
if (this.expander) {
if (this.is_expanded()) {
this.tree.update_expander('less');
} else if (this.expander.css('visibility') == 'visible') {
this.tree.update_expander('more');
}
}
} else {
this.tree.select_changed(
this.tree.selected_records[0] || null);
}
this.tree.update_selection();
},
expand_to_path: function(path, selected) {
if (path.length &&
this.record.field_get_client(this.children_field).length) {
this.expander.css('visibility', 'visible');
this.tree.expanded.add(this);
this.update_expander(true);
return this.expand_children(selected).done(() => {
return this.rows[path[0]].expand_to_path(path.slice(1), selected);
});
}
},
next_column: function(path, editable, sign) {
var i, readonly, invisible;
var column, column_index, state_attrs;
sign = sign || 1;
if ((path === null) && (sign > 0)) {
path = -1;
} else if (path === null) {
path = 0;
}
column_index = 0;
for (i = 0; i < this.tree.columns.length; i++) {
column_index = ((path + (sign * (i + 1))) %
this.tree.columns.length);
// javascript modulo returns negative number for negative
// numbers
if (column_index < 0) {
column_index += this.tree.columns.length;
}
column = this.tree.columns[column_index];
if (!column.field) {
continue;
}
state_attrs = column.field.get_state_attrs(this.record);
invisible = state_attrs.invisible;
if (column.header.is(':hidden')) {
invisible = true;
}
if (editable) {
var EditableBuilder = Sao.View.EditableTree.WIDGETS[
column.attributes.widget];
readonly = (column.attributes.readonly ||
state_attrs.readonly ||
!EditableBuilder);
} else {
readonly = false;
}
if (!(invisible || readonly)) {
return column_index;
}
}
}
});
Sao.View.Tree.RowEditable = Sao.class_(Sao.View.Tree.Row, {
init: function(tree, record, pos, parent) {
Sao.View.Tree.RowEditable._super.init.call(this, tree, record, pos,
parent);
this.edited_column = null;
this.el.on('keypress', event_ => {
if ((event_.which == Sao.common.RETURN_KEYCODE) &&
(this.tree.edited_row != this)) {
event_.preventDefault();
this.tree.edit_row(this);
}
});
},
redraw: function(selected, expanded) {
var i, cell, widget;
Sao.View.Tree.RowEditable._super.redraw.call(this, selected,
expanded);
const display_callback = widget => {
var record = this.record;
return function() {
var field = record.model.fields[widget.field_name];
field.set_state(record);
widget.display(record, field);
};
};
// The autocompletion widget do not call display thus we have to
// call it when redrawing the row
for (i = 0; i < this.tree.columns.length; i++) {
var column = this.tree.columns[i];
cell = this._get_column_td(i).children('.cell');
widget = jQuery(cell.children('.widget-editable')).data('widget');
if (widget) {
var callback = display_callback(widget);
if (!this.record.is_loaded(column.attributes.name)) {
this.record.load(column.attributes.name, true, false)
.done(callback);
} else {
callback();
}
}
}
},
select_column: function(index) {
this.edited_column = index;
},
select_row: function(event_) {
var body, listener;
event_.stopPropagation();
if (this.tree.edited_row &&
(event_.currentTarget == this.tree.edited_row.el[0])) {
return;
}
var current_record = this.tree.screen.current_record;
if ((this.record != current_record) &&
current_record && !current_record.validate(
this.tree.get_fields(), false, false, true)) {
return;
}
body = jQuery(document.body);
if (body.hasClass('modal-open')) {
listener = this.tree.el.parents('.modal').last();
} else {
listener = this.tree.el.parents('.tab-pane').last();
}
const handler = event_ => {
if ((event_.currentTarget == body[0]) &&
body.hasClass('modal-open')) {
return;
}
if (!this.tree.save_row()) {
event_.preventDefault();
event_.stopPropagation();
return;
}
listener.off('click.sao.editabletree');
this.tree.edit_row(null);
return true;
};
if (!handler(event_)) {
return;
}
listener.on('click.sao.editabletree', handler);
// do not call super when editing row because the selection must
// not be changed
if (!event_.shiftKey && !(event_.ctrlKey || event_.metaKey) &&
(this.record === current_record)) {
this.tree.edit_row(this);
} else {
Sao.View.Tree.RowEditable._super.select_row.call(this, event_);
}
},
unset_editable: function() {
this.tree.columns.forEach((col, idx) => {
var td = this._get_column_td(idx);
var static_el = this.get_static_el(td);
static_el.empty().append(col.render(this.record)).show();
this.get_editable_el(td)
.empty()
.data('widget', null)
.hide()
.parents('.treeview td').addBack().removeClass('edited');
});
},
set_editable: function() {
var focus_widget = null;
for (var i = 0, len=this.tree.columns.length; i < len; i++) {
var td = this._get_column_td(i);
var col = this.tree.columns[i];
if (!col.field) {
continue;
}
var EditableBuilder = Sao.View.EditableTree.WIDGETS[
col.attributes.widget];
if (!col.attributes.readonly && EditableBuilder) {
var widget = new EditableBuilder(
this.tree, col.attributes);
widget.el.on('keydown', this.key_press.bind(this));
var editable_el = this.get_editable_el(td);
editable_el.append(widget.el);
editable_el.data('widget', widget);
widget.display(this.record, col.field);
var static_el = this.get_static_el(td);
static_el.hide();
editable_el.show();
editable_el.parents('.treeview td').addBack()
.addClass('edited');
if (this.edited_column == i) {
focus_widget = widget;
}
}
}
if (focus_widget) {
focus_widget.focus();
}
},
get_static_el: function(td) {
td = td || this.get_active_td();
return td.find('.widget');
},
get_editable_el: function(td) {
td = td || this.get_active_td();
var editable = td.find('.widget-editable');
if (!editable.length) {
editable = jQuery('
', {
'class': 'widget-editable'
}).insertAfter(td.find('.widget'));
}
return editable;
},
get_active_td: function() {
return this._get_column_td(this.edited_column);
},
key_press: function(event_) {
var next_column, next_idx, i;
if (((event_.which != Sao.common.TAB_KEYCODE) &&
(event_.which != Sao.common.UP_KEYCODE) &&
(event_.which != Sao.common.DOWN_KEYCODE) &&
(event_.which != Sao.common.ESC_KEYCODE) &&
(event_.which != Sao.common.RETURN_KEYCODE)) ||
jQuery(event_.currentTarget)
.find('.dropdown-menu:visible').length) {
return;
}
event_.preventDefault();
var td = this._get_column_td(this.edited_column);
var editable_el = this.get_editable_el(td);
var widget = editable_el.data('widget');
widget.focus_out();
var column = this.tree.columns[this.edited_column];
if (column.field.validate(this.record)) {
if (event_.which == Sao.common.TAB_KEYCODE) {
var sign = 1;
if (event_.shiftKey) {
sign = -1;
}
next_idx = this.next_column(this.edited_column, true, sign);
if (next_idx !== null) {
this.edited_column = next_idx;
td = this._get_column_td(next_idx);
editable_el = this.get_editable_el(td);
widget = editable_el.data('widget');
widget.focus();
}
} else if (event_.which == Sao.common.UP_KEYCODE ||
event_.which == Sao.common.DOWN_KEYCODE ||
event_.which == Sao.common.RETURN_KEYCODE) {
next_column = this.edited_column;
if (!this.record.validate(this.tree.get_fields())) {
var invalid_fields =
this.record.invalid_fields();
for (i = 0; i < this.tree.columns.length; i++) {
var col = this.tree.columns[i];
if (col.attributes.name in invalid_fields) {
next_column = i;
break;
}
}
this._get_column_td(next_column)
.find(':input,[tabindex=0]')
.filter(':visible')
.first()
.focus();
} else {
var prm = jQuery.when();
if (!this.tree.screen.group.parent) {
prm = this.record.save();
} else if (this.tree.screen.attributes.pre_validate) {
prm = this.record.pre_validate();
}
prm.fail(function() {
widget.focus();
});
var next_row;
if (event_.which == Sao.common.UP_KEYCODE) {
next_row = this.el.prev('tr');
} else if (event_.which == Sao.common.DOWN_KEYCODE) {
next_row = this.el.next('tr');
} else {
if (this.tree.screen.new_position == -1) {
next_row = this.el.next('tr');
} else {
next_row = this.el.prev('tr');
}
}
if (!next_row.length &&
((event_.which == Sao.common.RETURN_KEYCODE) ||
((event_.which == Sao.common.UP_KEYCODE) &&
(this.tree.screen.new_position == 0)) ||
((event_.which == Sao.common.DOWN_KEYCODE) &&
(this.tree.screen.new_position == -1)))) {
var model = this.tree.screen.group;
var access = Sao.common.MODELACCESS.get(
this.tree.screen.model_name);
var limit = ((this.tree.screen.size_limit !== null) &&
(model.length >= this.tree.screen.size_limit));
if (this.tree.creatable && access.create && !limit) {
prm.then(() => this.tree.screen.new_())
.then(record => {
var sequence = this.tree.attributes.sequence;
if (sequence) {
record.group.set_sequence(
sequence, this.tree.screen.new_position);
}
});
}
} else {
prm.then(() => {
this._get_column_td(
next_column, next_row)
.trigger('click')
.trigger('click')
.find(':input,[tabindex=0]')
.filter(':visible')
.first()
.focus();
});
}
}
} else if (event_.which == Sao.common.ESC_KEYCODE) {
this.tree.edit_row(null);
this.get_static_el().show().find('[tabindex=0]').focus();
}
} else {
widget.display(this.record, column.field);
}
}
});
Sao.View.Tree.Affix = Sao.class_(Object, {
init: function(attributes, protocol) {
this.attributes = attributes;
this.protocol = protocol || null;
this.icon = attributes.icon;
if (this.protocol && !this.icon) {
this.icon = 'tryton-public';
}
},
get_cell: function() {
var cell;
if (this.protocol) {
cell = jQuery('
', {
'target': '_blank',
'rel': 'noreferrer noopener',
});
cell.append(jQuery('
![]()
'));
cell.click({'cell': cell}, this.clicked.bind(this));
} else if (this.icon) {
cell = jQuery('
![]()
');
} else {
cell = jQuery('
');
cell.attr('tabindex', 0);
}
cell.addClass('column-affix');
return cell;
},
render: function(record, cell) {
if (!cell) {
cell = this.get_cell();
}
const render = () => {
var value;
var field = record.model.fields[this.attributes.name];
field.set_state(record, ['invisible']);
var invisible = field.get_state_attrs(record).invisible;
if (invisible) {
cell.hide();
} else {
cell.show();
}
if (this.protocol) {
value = field.get(record);
if (!jQuery.isEmptyObject(value)) {
switch (this.protocol) {
case 'email':
value = 'mailto:' + value;
break;
case 'callto':
value = 'callto:' + value;
break;
case 'sip':
value = 'sip:' + value;
break;
}
}
cell.attr('href', value);
}
if (this.icon) {
if (this.icon in record.model.fields) {
var icon_field = record.model.fields[this.icon];
value = icon_field.get_client(record);
}
else {
value = this.icon;
}
var img_tag;
if (cell.children('img').length) {
img_tag = cell.children('img');
} else {
img_tag = cell;
}
if (this.attributes.icon_type == 'url') {
if (value) {
if (this.attributes.url_size) {
var url = new URL(value, window.location);
url.searchParams.set(
this.attributes.url_size, 20);
value = url.href;
}
img_tag.attr('src', value);
} else {
img_tag.removeAttr('src');
}
} else if (this.attributes.icon_type == 'color') {
img_tag.attr('src', Sao.common.image_url());
// clean previous color if the new one is not valid
img_tag.css('background-color', '');
img_tag.css('background-color', value);
img_tag.toggle(Boolean(value));
} else {
Sao.common.ICONFACTORY.get_icon_url(value)
.done(url => {
if (url) {
img_tag.attr('src', url);
} else {
img_tag.removeAttr('src');
}
});
}
switch (this.attributes.border) {
case 'rounded':
img_tag.addClass('img-rounded');
break;
case 'circle':
img_tag.addClass('img-circle');
break;
default:
break;
}
} else {
value = this.attributes.string || '';
if (!value) {
value = field.get_client(record) || '';
}
cell.text(value);
}
};
let prm = jQuery.when();
if (!record.is_loaded(this.attributes.name)) {
prm = prm.then(() => record.load(this.attributes.name, true, false));
}
if (this.icon && (this.icon in record.model.fields)) {
prm = prm.then(() => record.load(this.icon, true, false));
}
prm.done(render);
return cell;
},
clicked: function(event) {
event.stopPropagation(); // prevent edition
}
});
Sao.View.Tree.Symbol = Sao.class_(Object, {
class_: 'column-symbol',
init: function(attributes, position) {
this.attributes = attributes;
this.position = position;
},
get_cell: function() {
var cell = jQuery('
', {
'class': this.class_,
'tabindex': 0
});
return cell;
},
render: function(record, cell) {
if (!cell) {
cell = this.get_cell();
}
const render = () => {
var field = record.model.fields[this.attributes.name];
field.set_state(record, ['invisible']);
var invisible = field.get_state_attrs(record).invisible;
if (invisible) {
cell.text('');
cell.hide();
return;
}
var result = field.get_symbol(record, this.attributes.symbol);
var symbol = result[0],
position = result[1];
if (Math.round(position) === this.position) {
cell.text(symbol);
cell.show();
} else {
cell.text('');
cell.hide();
}
};
if (!record.is_loaded(this.attributes.name)) {
record.load(this.attributes.name, true, false).done(render);
} else {
render();
}
return cell;
},
});
Sao.View.Tree.CharColumn = Sao.class_(Object, {
class_: 'column-char',
init: function(model, attributes) {
this.type = 'field';
this.model = model;
this.field = model.fields[attributes.name];
this.tree = null;
this.attributes = attributes;
this.prefixes = [];
this.suffixes = [];
this.header = null;
this.footers = [];
},
get field_name() {
return this.attributes.name;
},
get model_name() {
return this.model.name;
},
get_cell: function() {
var cell = jQuery('
', {
'class': this.class_,
'tabindex': 0
});
return cell;
},
get_textual_value: function(record) {
return this.field.get_client(record);
},
update_text: function(cell, record) {
var text = this.get_textual_value(record);
cell.text(text).attr('title', text);
},
render: function(record, cell) {
if (!cell) {
cell = this.get_cell();
}
const render = () => {
this.update_text(cell, record);
this.field.set_state(record);
var state_attrs = this.field.get_state_attrs(record);
if (state_attrs.invisible) {
cell.hide();
} else {
cell.show();
}
};
const render_error = () => {
var text = Sao.i18n.gettext('#ERROR');
cell.text(text).attr('title', text);
};
if (!record.is_loaded(this.attributes.name)) {
record.load(this.attributes.name, true, false)
.done(render)
.fail(render_error);
} else {
if (record.exception) {
render_error();
} else {
render();
}
}
return cell;
},
set_visible: function(visible) {
var cells = this.footers.slice();
cells.push(this.header);
for (const cell of cells) {
if (visible) {
cell.show();
cell.removeClass('invisible');
} else {
cell.hide();
cell.addClass('invisible');
}
}
},
get_visible: function() {
return !this.header.hasClass('invisible');
},
});
Sao.View.Tree.TextColum = Sao.class_(Sao.View.Tree.CharColumn, {
class_: 'column-text'
});
Sao.View.Tree.IntegerColumn = Sao.class_(Sao.View.Tree.CharColumn, {
class_: 'column-integer',
init: function(model, attributes) {
Sao.View.Tree.IntegerColumn._super.init.call(this, model, attributes);
this.factor = Number(attributes.factor || 1);
this.grouping = Boolean(Number(attributes.grouping || 1));
},
get_cell: function() {
return Sao.View.Tree.IntegerColumn._super.get_cell.call(this);
},
get_textual_value: function(record) {
return this.field.get_client(record, this.factor, this.grouping);
},
});
Sao.View.Tree.FloatColumn = Sao.class_(Sao.View.Tree.IntegerColumn, {
class_: 'column-float'
});
Sao.View.Tree.BooleanColumn = Sao.class_(Sao.View.Tree.CharColumn, {
class_: 'column-boolean',
get_cell: function() {
return jQuery('
', {
'type': 'checkbox',
'class': this.class_,
'tabindex': 0
});
},
update_text: function(cell, record) {
cell.prop('checked', this.field.get(record));
},
render: function(record, cell) {
var new_cell = !cell;
cell = Sao.View.Tree.BooleanColumn._super.render.call(
this, record, cell);
var disabled = true;
if (this.tree.editable) {
if (new_cell) {
cell.on('click', null,
{record: record, cell:cell},
this.clicked.bind(this));
}
var state_attrs = this.field.get_state_attrs(record);
disabled = this.attributes.readonly || state_attrs.readonly;
}
cell.prop('disabled', disabled);
return cell;
},
clicked: function(evt) {
var record = evt.data.record;
var cell = evt.data.cell;
var current_record = this.tree.screen.current_record;
var fields = this.tree.get_fields();
if (!current_record || current_record.validate(
fields, false, false)) {
var value = cell.prop('checked');
this.field.set_client(record, value);
} else {
evt.preventDefault();
}
}
});
Sao.View.Tree.Many2OneColumn = Sao.class_(Sao.View.Tree.CharColumn, {
class_: 'column-many2one',
get_cell: function() {
var cell = Sao.View.Tree.Many2OneColumn._super.get_cell.call(this);
cell.append(jQuery('
', {
'href': '#',
}));
return cell;
},
update_text: function(cell, record) {
cell = cell.children('a');
cell.unbind('click');
Sao.View.Tree.Many2OneColumn._super.update_text.call(this, cell, record);
let view_ids = (this.attributes.view_ids || '').split(',');
if (!jQuery.isEmptyObject(view_ids)) {
// Remove the first tree view as mode is form only
view_ids.shift();
}
cell.click(event => {
event.preventDefault();
event.stopPropagation();
var params = {};
params.model = this.attributes.relation;
params.res_id = this.field.get(record);
params.mode = ['form'];
params.view_ids = view_ids;
params.name = this.attributes.string;
params.context = this.field.get_context(record);
Sao.Tab.create(params);
});
}
});
Sao.View.Tree.One2OneColumn = Sao.class_(Sao.View.Tree.Many2OneColumn, {
class_: 'column-one2one'
});
Sao.View.Tree.SelectionColumn = Sao.class_(Sao.View.Tree.CharColumn, {
class_: 'column-selection',
init: function(model, attributes) {
Sao.View.Tree.SelectionColumn._super.init.call(this, model,
attributes);
Sao.common.selection_mixin.init.call(this);
if (!this.tree || this.tree.editable) {
this.init_selection();
}
},
init_selection: function(key) {
Sao.common.selection_mixin.init_selection.call(this, key);
},
update_selection: function(record, callback) {
if (!this.selection) {
this.init_selection();
}
Sao.common.selection_mixin.update_selection.call(this, record,
this.field, callback);
},
get_textual_value: function(record) {
var related = this.field.name + ':string';
if (!this.tree.editable && related in record._values) {
return record._values[related];
} else {
var value = this.field.get(record);
for (const option of this.selection) {
if (option[0] === value) {
return option[1];
}
}
return value;
}
},
update_text: function(cell, record) {
if (!this.tree.editable &&
(this.field.name + ':string' in record._values)) {
var text_value = this.get_textual_value(record);
cell.text(text_value).attr('title', text_value);
} else {
this.update_selection(record, () => {
var value = this.field.get(record);
var prm, text, found = false;
for (const option of this.selection) {
if (option[0] === value) {
found = true;
text = option[1];
break;
}
}
if (!found) {
prm = Sao.common.selection_mixin.get_inactive_selection
.call(this, value).then(function(inactive) {
return inactive[1];
});
} else {
prm = jQuery.when(text);
}
prm.done(text_value => {
cell.text(text_value).attr('title', text_value);
});
});
}
}
});
Sao.View.Tree.MultiSelectionColumn = Sao.class_(Sao.View.Tree.CharColumn, {
class_: 'column-multiselection',
init: function(model, attributes) {
Sao.View.Tree.MultiSelectionColumn._super.init.call(
this, model, attributes);
Sao.common.selection_mixin.init.call(this);
this.init_selection();
},
init_selection: function(key) {
Sao.common.selection_mixin.init_selection.call(this, key);
},
update_selection: function(record, callback) {
Sao.common.selection_mixin.update_selection.call(this, record,
this.field, callback);
},
get_textual_value: function(record) {
var related = this.field.name + ':string';
if (!this.tree.editable && related in record._values) {
return record._values[related];
} else {
var values = this.field.get_eval(record).map(value => {
for (const option of this.selection) {
if (option[0] === value) {
return option[1];
}
}
return '';
});
return values.join(';');
}
},
update_text: function(cell, record) {
if (!this.tree.editable &&
(this.field_name + ':string' in record._values)) {
var text_value = this.get_textual_value(record);
cell.text(text_value).attr('title', text_value);
} else {
this.update_selection(record, () => {
var text_value = this.get_textual_value(record);
cell.text(text_value).attr('title', text_value);
});
}
},
});
Sao.View.Tree.ReferenceColumn = Sao.class_(Sao.View.Tree.CharColumn, {
class_: 'column-reference',
init: function(model, attributes) {
Sao.View.Tree.ReferenceColumn._super.init.call(this, model,
attributes);
Sao.common.selection_mixin.init.call(this);
this.init_selection();
},
init_selection: function(key) {
Sao.common.selection_mixin.init_selection.call(this, key);
},
update_selection: function(record, callback) {
Sao.common.selection_mixin.update_selection.call(this, record,
this.field, callback);
},
get_cell: function() {
var cell = Sao.View.Tree.ReferenceColumn._super
.get_cell.call(this);
cell.append(jQuery('
', {
'href': '#',
}));
return cell;
},
get_textual_value: function(record) {
var value = Sao.View.Tree.ReferenceColumn._super
.get_textual_value.call(this, record);
var model, name, text;
if (!value) {
model = '';
name = '';
} else {
model = value[0];
name = value[1];
}
if (model) {
for (const option of this.selection) {
if (option[0] === model) {
model = option[1];
break;
}
}
text = model + ',' + name;
} else {
text = name;
}
return text;
},
update_text: function(cell, record) {
cell = cell.children('a');
cell.unbind('click');
this.update_selection(record, () => {
var text = this.get_textual_value(record);
cell.text(text).attr('title', text);
cell.click(event => {
event.stopPropagation();
var value = this.field.get(record);
if (value) {
var model = value.split(',')[0];
var id = parseInt(value.split(',')[1], 10);
var params = {
model: model,
res_id: id,
mode: ['form'],
name: this.attributes.string,
context: this.field.get_context(record),
};
Sao.Tab.create(params);
}
});
});
}
});
Sao.View.Tree.DictColumn = Sao.class_(Sao.View.Tree.CharColumn, {
class_: 'column-dict',
get_textual_value: function(record) {
return '(' + Object.keys(this.field.get_client(record)).length + ')';
},
});
Sao.View.Tree.DateColumn = Sao.class_(Sao.View.Tree.CharColumn, {
class_: 'column-date',
get_textual_value: function(record) {
var value = this.field.get_client(record);
var date_format = this.field.date_format(record);
return Sao.common.format_date(date_format, value);
}
});
Sao.View.Tree.TimeColumn = Sao.class_(Sao.View.Tree.CharColumn, {
class_: 'column-time',
get_textual_value: function(record) {
var value = this.field.get_client(record);
return Sao.common.format_time(
this.field.time_format(record), value);
}
});
Sao.View.Tree.TimeDeltaColumn = Sao.class_(Sao.View.Tree.CharColumn, {
class_: 'column-timedelta'
});
Sao.View.Tree.One2ManyColumn = Sao.class_(Sao.View.Tree.CharColumn, {
class_: 'column-one2many',
get_textual_value: function(record) {
return '( ' + this.field.get_client(record).length + ' )';
}
});
Sao.View.Tree.Many2ManyColumn = Sao.class_(Sao.View.Tree.One2ManyColumn, {
class_: 'column-many2many'
});
Sao.View.Tree.BinaryColumn = Sao.class_(Sao.View.Tree.CharColumn, {
class_: 'column-binary',
init: function(model, attributes) {
Sao.View.Tree.BinaryColumn._super.init.call(this, model, attributes);
this.filename = attributes.filename || null;
},
get_cell: function() {
var cell = Sao.View.Tree.BinaryColumn._super.get_cell.call(this);
jQuery('
').appendTo(cell);
return cell;
},
get_textual_value: function(record) {
var size;
if (this.field.get_size) {
size = this.field.get_size(record);
} else {
size = this.field.get(record).length;
}
return size? Sao.common.humanize(size, 'B') : '';
},
update_text: function(cell, record) {
var text = this.get_textual_value(record);
cell.children('span').text(text).attr('title', text);
var button = cell.children('button');
if (!button.length) {
button = jQuery('
', {
'class': 'btn btn-default btn-sm',
'type': 'button',
'title': Sao.i18n.gettext("Save As..."),
}).append(Sao.common.ICONFACTORY.get_icon_img('tryton-download')
).appendTo(cell)
.click(record, event => {
// Prevent editable tree to start edition
event.stopPropagation();
this.save_as(event.data);
});
}
if (!text) {
button.hide();
} else {
button.show();
}
},
save_as: function(record) {
var filename;
var filename_field = record.model.fields[this.filename];
if (filename_field) {
filename = filename_field.get_client(record);
}
var prm;
if (this.field.get_data) {
prm = this.field.get_data(record);
} else {
prm = jQuery.when(this.field.get(record));
}
prm.done(data => {
Sao.common.download_file(data, filename);
});
},
});
Sao.View.Tree.ImageColumn = Sao.class_(Sao.View.Tree.CharColumn, {
class_: 'column-image',
get_cell: function() {
var cell = jQuery('
![]()
', {
'class': this.class_ + ' center-block',
'tabindex': 0
});
this.height = parseInt(this.attributes.height || 100, 10);
this.width = parseInt(this.attributes.width || 300, 10);
cell.css('max-height', this.height);
cell.css('max-width', this.width);
cell.css('height', 'auto');
cell.css('width', 'auto');
return cell;
},
render: function(record, cell) {
if (!cell) {
cell = this.get_cell();
}
const render = () => {
const set_src = data => {
cell.attr('src', Sao.common.image_url(data));
};
var value = this.field.get_client(record);
if (value) {
if (value > Sao.config.image_max_size) {
set_src(null);
} else {
this.field.get_data(record).done(set_src);
}
} else {
set_src(null);
}
};
const render_error = () => {
cell.attr('src', null);
};
if (!record.is_loaded(this.attributes.name)) {
record.load(this.attributes.name, true, false)
.done(render)
.fail(render_error);
} else {
if (render.exception) {
render_error();
} else {
render();
}
}
return cell;
}
});
Sao.View.Tree.URLColumn = Sao.class_(Sao.View.Tree.CharColumn, {
class_: 'column-url',
render: function(record, cell) {
cell = Sao.View.Tree.URLColumn._super.render.call(
this, record, cell);
this.field.set_state(record);
var state_attrs = this.field.get_state_attrs(record);
if (state_attrs.readonly) {
cell.hide();
} else {
cell.show();
}
return cell;
}
});
Sao.View.Tree.ProgressBar = Sao.class_(Sao.View.Tree.CharColumn, {
class_: 'column-progressbar',
get_cell: function() {
var cell = jQuery('
', {
'class': this.class_ + ' progress',
'tabindex': 0
});
var progressbar = jQuery('
', {
'class': 'progress-bar',
'role': 'progressbar',
'aria-valuemin': 0,
'aria-valuemax': 100
}).appendTo(cell);
progressbar.css('min-width: 2em');
return cell;
},
get_textual_value: function(record) {
var text = this.field.get_client(record, 100);
if (text) {
text = Sao.i18n.gettext('%1%', text);
}
return text;
},
update_text: function(cell, record) {
var text = this.get_textual_value(record);
var value = this.field.get(record) || 0;
var progressbar = cell.find('.progress-bar');
progressbar.attr('aria-valuenow', value * 100);
progressbar.css('width', value * 100 + '%');
progressbar.text(text).attr('title', text);
}
});
Sao.View.Tree.ButtonColumn = Sao.class_(Object, {
init: function(view, attributes) {
this.view = view;
this.type = 'button';
this.attributes = attributes;
this.footers = [];
},
render: function(record, el) {
var button = new Sao.common.Button(this.attributes, el, 'btn-sm');
if (!el) {
button.el.click(
[record, button], this.button_clicked.bind(this));
}
jQuery.map(this.view.screen.model.fields,
function(field, name) {
if ((field.description.loading || 'eager') ==
'eager') {
return name;
} else {
return undefined;
}
});
button.set_state(record);
return button.el;
},
set_visible: function(visible) {
var cells = this.footers.slice();
cells.push(this.header);
for (const cell of cells) {
if (visible) {
cell.show();
cell.removeClass('invisible');
} else {
cell.hide();
cell.addClass('invisible');
}
}
},
get_visible: function() {
return !this.header.hasClass('invisible');
},
button_clicked: function(event) {
var record = event.data[0];
var button = event.data[1];
if (record != this.view.screen.current_record) {
// Need to raise the event to get the record selected
return true;
}
var states = record.expr_eval(this.attributes.states || {});
if (states.invisible || states.readonly) {
return;
}
event.stopImmediatePropagation();
button.el.prop('disabled', true); // state will be reset at display
var row = this.view.rows.find(function(row) {
return row.record == record;
});
if (row) {
row._drawed_record = null; // force redraw the row
}
this.view.screen.button(this.attributes);
}
});
Sao.View.Tree.ButtonMultiple = Sao.class_(Sao.common.Button, {
set_state: function(records) {
if (!records.length) {
this.el.hide();
this.el.prop('disabled', true);
this.set_icon(null);
return;
}
let states = {
'invisible': false,
'readonly': false,
};
let icons = new Set();
for (let record of records) {
let r_states = record.expr_eval(this.attributes.states || {});
states['invisible'] |= r_states.invisible;
states['readonly'] |= r_states.readonly;
icons.add(r_states.icon || this.attributes.icon);
}
if (states.invisible) {
this.el.hide();
} else {
this.el.show();
}
this.el.prop('disabled', Boolean(states.readonly));
if (icons.size == 1) {
this.set_icon(icons.values().next().value);
} else {
this.set_icon(null);
}
if ((this.attributes.type === undefined) ||
(this.attributes.type === 'class')) {
for (let record of records) {
let parent = record.group.parent;
while (parent) {
if (parent.modified) {
this.el.prop('disabled', true);
break;
}
parent = parent.group.parent;
}
}
}
},
});
Sao.View.TreeXMLViewParser.WIDGETS = {
'binary': Sao.View.Tree.BinaryColumn,
'boolean': Sao.View.Tree.BooleanColumn,
'callto': Sao.View.Tree.URLColumn,
'char': Sao.View.Tree.CharColumn,
'date': Sao.View.Tree.DateColumn,
'dict': Sao.View.Tree.DictColumn,
'email': Sao.View.Tree.URLColumn,
'float': Sao.View.Tree.FloatColumn,
'image': Sao.View.Tree.ImageColumn,
'integer': Sao.View.Tree.IntegerColumn,
'many2many': Sao.View.Tree.Many2ManyColumn,
'many2one': Sao.View.Tree.Many2OneColumn,
'numeric': Sao.View.Tree.FloatColumn,
'one2many': Sao.View.Tree.One2ManyColumn,
'one2one': Sao.View.Tree.One2OneColumn,
'progressbar': Sao.View.Tree.ProgressBar,
'reference': Sao.View.Tree.ReferenceColumn,
'selection': Sao.View.Tree.SelectionColumn,
'multiselection': Sao.View.Tree.MultiSelectionColumn,
'sip': Sao.View.Tree.URLColumn,
'text': Sao.View.Tree.TextColum,
'time': Sao.View.Tree.TimeColumn,
'timedelta': Sao.View.Tree.TimeDeltaColumn,
'url': Sao.View.Tree.URLColumn,
};
Sao.View.EditableTree = {};
Sao.View.EditableTree.editable_mixin = function(widget) {
var key_press = function(event_) {
if ((event_.which == Sao.common.TAB_KEYCODE) ||
(event_.which == Sao.common.UP_KEYCODE) ||
(event_.which == Sao.common.DOWN_KEYCODE) ||
(event_.which == Sao.common.ESC_KEYCODE) ||
(event_.which == Sao.common.RETURN_KEYCODE)) {
this.focus_out();
}
};
widget.el.on('keydown', key_press.bind(widget));
};
Sao.View.EditableTree.Char = Sao.class_(Sao.View.Form.Char, {
class_: 'editabletree-char',
init: function(view, attributes) {
Sao.View.EditableTree.Char._super.init.call(
this, view, attributes);
Sao.View.EditableTree.editable_mixin(this);
}
});
Sao.View.EditableTree.URL = Sao.class_(Sao.View.EditableTree.Char, {
class_: 'editable-url',
set_readonly: function(readonly) {
Sao.View.EditableTree.URL._super.set_readonly.call(this, readonly);
if (readonly) {
this.input.hide();
} else {
this.input.show();
}
},
});
Sao.View.EditableTree.Date = Sao.class_(Sao.View.Form.Date, {
class_: 'editabletree-date',
init: function(view, attributes) {
Sao.View.EditableTree.Date._super.init.call(
this, view, attributes);
Sao.View.EditableTree.editable_mixin(this);
}
});
Sao.View.EditableTree.Time = Sao.class_(Sao.View.Form.Time, {
class_: 'editabletree-time',
init: function(view, attributes) {
Sao.View.EditableTree.Time._super.init.call(
this, view, attributes);
Sao.View.EditableTree.editable_mixin(this);
}
});
Sao.View.EditableTree.TimeDelta = Sao.class_(Sao.View.Form.TimeDelta, {
class_: 'editabletree-timedelta',
init: function(view, attributes) {
Sao.View.EditableTree.TimeDelta._super.init.call(
this, view, attributes);
Sao.View.EditableTree.editable_mixin(this);
}
});
Sao.View.EditableTree.Integer = Sao.class_(Sao.View.Form.Integer, {
class_: 'editabletree-integer',
init: function(view, attributes) {
attributes = jQuery.extend({}, attributes);
delete attributes.symbol;
Sao.View.EditableTree.Integer._super.init.call(
this, view, attributes);
Sao.View.EditableTree.editable_mixin(this);
},
get width() {
return null;
},
});
Sao.View.EditableTree.Float = Sao.class_(Sao.View.Form.Float, {
class_: 'editabletree-float',
init: function(view, attributes) {
attributes = jQuery.extend({}, attributes);
delete attributes.symbol;
Sao.View.EditableTree.Float._super.init.call(
this, view, attributes);
Sao.View.EditableTree.editable_mixin(this);
},
get width() {
return null;
},
});
Sao.View.EditableTree.Selection = Sao.class_(Sao.View.Form.Selection, {
class_: 'editabletree-selection',
init: function(view, attributes) {
Sao.View.EditableTree.Selection._super.init.call(
this, view, attributes);
Sao.View.EditableTree.editable_mixin(this);
}
});
Sao.View.EditableTree.Boolean = Sao.class_(Sao.View.Form.Boolean, {
class_: 'editabletree-boolean',
init: function(view, attributes) {
Sao.View.EditableTree.Boolean._super.init.call(
this, view, attributes);
Sao.View.EditableTree.editable_mixin(this);
}
});
Sao.View.EditableTree.Many2One = Sao.class_(Sao.View.Form.Many2One, {
class_: 'editabletree-many2one',
init: function(view, attributes) {
Sao.View.EditableTree.Many2One._super.init.call(
this, view, attributes);
},
});
Sao.View.EditableTree.Reference = Sao.class_(Sao.View.Form.Reference, {
class_: 'editabletree-reference',
init: function(view, attributes) {
Sao.View.EditableTree.Reference._super.init.call(
this, view, attributes);
},
});
Sao.View.EditableTree.One2One = Sao.class_(Sao.View.Form.One2One, {
class_: 'editabletree-one2one',
init: function(view, attributes) {
Sao.View.EditableTree.One2One._super.init.call(
this, view, attributes);
},
});
Sao.View.EditableTree.One2Many = Sao.class_(Sao.View.EditableTree.Char, {
class_: 'editabletree-one2many',
init: function(view, attributes) {
Sao.View.EditableTree.One2Many._super.init.call(
this, view, attributes);
},
display: function(record, field) {
if (record) {
this.el.val('(' + field.get_client(record).length + ')');
} else {
this.el.val('');
}
},
key_press: function(event_) {
// TODO: remove when key_press is implemented
if (event_.which == Sao.common.TAB_KEYCODE) {
this.focus_out();
}
},
set_value: function(record, field) {
}
});
Sao.View.EditableTree.Binary = Sao.class_(Sao.View.Form.Binary, {
class_: 'editabletree-binary',
init: function(view, attributes) {
Sao.View.EditableTree.Binary._super.init.call(
this, view, attributes);
Sao.View.EditableTree.editable_mixin(this);
}
});
Sao.View.EditableTree.WIDGETS = {
'binary': Sao.View.EditableTree.Binary,
'boolean': Sao.View.EditableTree.Boolean,
'callto': Sao.View.EditableTree.URL,
'char': Sao.View.EditableTree.Char,
'date': Sao.View.EditableTree.Date,
'email': Sao.View.EditableTree.URL,
'float': Sao.View.EditableTree.Float,
'integer': Sao.View.EditableTree.Integer,
'many2many': Sao.View.EditableTree.Many2Many,
'many2one': Sao.View.EditableTree.Many2One,
'numeric': Sao.View.EditableTree.Float,
'one2many': Sao.View.EditableTree.One2Many,
'one2one': Sao.View.EditableTree.One2One,
'reference': Sao.View.EditableTree.Reference,
'selection': Sao.View.EditableTree.Selection,
'sip': Sao.View.EditableTree.URL,
'text': Sao.View.EditableTree.Char,
'time': Sao.View.EditableTree.Time,
'timedelta': Sao.View.EditableTree.TimeDelta,
'url': Sao.View.EditableTree.URL,
};
}());
/* This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. */
(function() {
'use strict';
Sao.View.GraphXMLViewParser = Sao.class_(Sao.View.XMLViewParser, {
init: function(view, exclude_field, fields) {
Sao.View.GraphXMLViewParser._super.init.call(
this, view, exclude_field, fields);
this._xfield = null;
this._yfields = [];
},
_node_attributes: function(node) {
var node_attrs = {};
for (const attribute of node.attributes) {
node_attrs[attribute.name] = attribute.value;
}
if (node_attrs.name) {
if (!node_attrs.string && (node_attrs.name != '#')) {
var field = this.field_attrs[node_attrs.name];
node_attrs.string = field.string;
}
}
return node_attrs;
},
_parse_graph: function(node, attributes) {
for (const child of node.childNodes) {
this.parse(child);
}
var Widget = Sao.View.GraphXMLViewParser.WIDGETS[
attributes.type || 'vbar'];
var widget = new Widget(this.view, this._xfield, this._yfields);
this.view.el.append(widget.el);
this.view.widgets.root = widget;
},
_parse_x: function(node, attributes) {
for (const child of node.children) {
this._xfield = this._node_attributes(child);
}
},
_parse_y: function(node, attributes) {
for (const child of node.children) {
this._yfields.push(this._node_attributes(child));
}
}
});
Sao.View.Graph = Sao.class_(Sao.View, {
editable: false,
view_type: 'graph',
xml_parser: Sao.View.GraphXMLViewParser,
init: function(view_id, screen, xml, children_field) {
this.el = jQuery('
', {
'class': 'graph'
});
Sao.View.Graph._super.init.call(this, view_id, screen, xml);
},
display: function() {
return this.widgets.root.display(this.group);
}
});
Sao.View.Graph.Chart = Sao.class_(Object, {
_chart_type: undefined,
init: function(view, xfield, yfields) {
this.view = view;
this.xfield = xfield;
this.yfields = yfields;
this.el = jQuery('
');
this.el.uniqueId();
},
update_data: function(group) {
var data = {};
var record, yfield, key;
var i, len, j, y_len;
this.ids = {};
data.columns = [['labels']];
data.names = {};
var key2columns = {};
var fields2load = [this.xfield.name];
for (i = 0, len = this.yfields.length; i < len; i++) {
yfield = this.yfields[i];
key = yfield.key || yfield.name;
data.columns.push([key]);
data.names[key] = yfield.string;
key2columns[key] = i + 1;
fields2load.push(yfield.name);
}
var prms = [];
const set_data = index => {
return () => {
record = group[index];
var x = record.field_get_client(this.xfield.name);
// c3 does not support moment
if (x && (x.isDate || x.isDateTime)) {
x = x.toString();
}
var pos = data.columns[0].indexOf(x);
if (pos < 0) {
pos = data.columns[0].push(x) - 1;
}
this._add_id(x, record.id);
var column;
for (j = 0, y_len = this.yfields.length; j < y_len; j++) {
yfield = this.yfields[j];
key = yfield.key || yfield.name;
column = data.columns[key2columns[key]];
if (column[pos] === undefined) {
column[pos] = null;
}
if (yfield.domain) {
var ctx = jQuery.extend({},
Sao.Session.current_session.context);
ctx.context = ctx;
ctx._user = Sao.Session.current_session.user_id;
for (var field in group.model.fields) {
ctx[field] = record.field_get(field);
}
var decoder = new Sao.PYSON.Decoder(ctx);
if (!decoder.decode(yfield.domain)) {
continue;
}
}
if (!column[pos]) {
column[pos] = 0;
}
if (yfield.name == '#') {
column[pos] += 1;
} else {
var value = record.field_get(yfield.name);
if (value && value.isTimeDelta) {
value = value.asSeconds();
}
column[pos] += value || 0;
}
}
};
};
var r_prms = [];
for (i = 0, len = group.length; i < len; i++) {
record = group[i];
for (const fname of fields2load) {
prms.push(record.load(fname));
}
r_prms.push(
jQuery.when.apply(jQuery, prms).then(set_data(i)));
}
return jQuery.when.apply(jQuery, r_prms).then(function() {
return data;
});
},
_add_id: function(key, id) {
if (!(key in this.ids)) {
this.ids[key] = [];
}
this.ids[key].push(id);
},
display: function(group) {
var update_prm = this.update_data(group);
update_prm.done(data => {
c3.generate(this._c3_config(data));
});
return update_prm;
},
_c3_config: function(data) {
var c3_config = {};
c3_config.bindto = '#' + this.el.attr('id');
c3_config.data = data;
c3_config.data.type = this._chart_type;
c3_config.data.x = 'labels';
c3_config.data.onclick = this.action.bind(this);
var type = this.view.screen.model.fields[this.xfield.name]
.description.type;
if ((type == 'date') || (type == 'datetime')) {
var format_func, date_format, time_format;
date_format = Sao.common.date_format(
this.view.screen.context.date_format);
time_format = '%X';
if (type == 'datetime') {
c3_config.data.xFormat = '%Y-%m-%d %H:%M:%S';
format_func = function(dt) {
return Sao.common.format_datetime(
date_format + ' ' + time_format, moment(dt));
};
} else {
c3_config.data.xFormat = '%Y-%m-%d';
format_func = function(dt) {
return Sao.common.format_date(date_format, moment(dt));
};
}
c3_config.axis = {
x: {
type: 'timeseries',
tick: {
format: format_func,
}
}
};
} else {
c3_config.axis = {
x: {
type: 'category',
}
};
}
let keys = this._data_keys(data);
var color = this.view.attributes.color || Sao.config.graph_color;
var rgb = Sao.common.hex2rgb(
Sao.common.COLOR_SCHEMES[color] || color);
var maxcolor = Math.max.apply(null, rgb);
var colors = Sao.common.generateColorscheme(
color, keys, maxcolor / (keys.length || 1));
for (let i = 0; i < this.yfields.length; i++) {
let yfield = this.yfields[i];
if (yfield.color) {
colors[yfield.key || yfield.name] = yfield.color;
}
}
c3_config.data.color = function(color, column) {
// column is an object when called for legend
var key = column.id || column;
return colors[key] || color;
};
return c3_config;
},
_data_keys: function(data) {
let keys = [];
for (let i = 0; i < this.yfields.length; i++) {
let yfield = this.yfields[i];
keys.push(yfield.key || yfield.name);
}
return keys;
},
action: function(data, element) {
var ids = this.ids[this._action_key(data)];
var ctx = jQuery.extend({}, this.view.screen.group.local_context);
delete ctx.active_ids;
delete ctx.active_id;
Sao.Action.exec_keyword('graph_open', {
model: this.view.screen.model_name,
id: ids[0],
ids: ids
}, ctx, false);
},
_action_key: function(data) {
var x = data.x;
var type = this.view.screen.model.fields[this.xfield.name]
.description.type;
if (x && (type == 'datetime')) {
x = Sao.DateTime(x).toString();
} else if (x && (type == 'date')) {
x = Sao.Date(x).toString();
}
return x;
}
});
Sao.View.Graph.VerticalBar = Sao.class_(Sao.View.Graph.Chart, {
_chart_type: 'bar'
});
Sao.View.Graph.HorizontalBar = Sao.class_(Sao.View.Graph.Chart, {
_chart_type: 'bar',
_c3_config: function(data) {
var config = Sao.View.Graph.HorizontalBar._super._c3_config
.call(this, data);
config.axis.rotated = true;
return config;
}
});
Sao.View.Graph.Line = Sao.class_(Sao.View.Graph.Chart, {
_chart_type: 'line',
_c3_config: function(data) {
var config = Sao.View.Graph.Line._super._c3_config
.call(this, data);
config.line = {
connectNull: true,
};
return config;
}
});
Sao.View.Graph.Pie = Sao.class_(Sao.View.Graph.Chart, {
_chart_type: 'pie',
_c3_config: function(data) {
var config = Sao.View.Graph.Pie._super._c3_config.call(this, data);
var pie_columns = [], pie_names = {};
var i, len;
var labels, values;
for (i = 0, len = data.columns.length; i < len; i++) {
if (data.columns[i][0] == 'labels') {
labels = data.columns[i].slice(1);
} else {
values = data.columns[i].slice(1);
}
}
// Pie chart do not support axis definition.
delete config.axis;
delete config.data.x;
var format_func;
var type = this.view.screen.model.fields[this.xfield.name]
.description.type;
if ((type == 'date') || (type == 'datetime')) {
var date_format = Sao.common.date_format(
this.view.screen.context.date_format);
var datetime_format = date_format + ' %X';
if (type == 'datetime') {
format_func = function(dt) {
return Sao.common.format_datetime(datetime_format, dt);
};
} else {
format_func = function(dt) {
return Sao.common.format_date(date_format, dt);
};
}
}
var label;
for (i = 0, len = labels.length; i < len; i++) {
label = labels[i];
if (format_func) {
label = format_func(label);
}
pie_columns.push([i, values[i]]);
pie_names[i] = label;
}
config.data.columns = pie_columns;
config.data.names = pie_names;
config.data.order = null;
return config;
},
_data_keys: function(data) {
let keys = [];
for (let i = 0; i < data.columns[1].length - 1; i++) {
keys.push(i);
};
return keys;
},
_add_id: function(key, id) {
var type = this.xfield.type;
if ((type == 'date') || (type == 'datetime')) {
var date_format = Sao.common.date_format(
this.view.screen.context.date_format);
var datetime_format = date_format + ' %X';
if (type == 'datetime') {
key = Sao.common.format_datetime(datetime_format, key);
} else {
key = Sao.common.format_date(date_format, key);
}
}
Sao.View.Graph.Pie._super._add_id.call(this, key, id);
},
_action_key: function(data) {
// data.name is the label used for the x axis
return data.name;
}
});
Sao.View.GraphXMLViewParser.WIDGETS = {
'hbar': Sao.View.Graph.HorizontalBar,
'line': Sao.View.Graph.Line,
'pie': Sao.View.Graph.Pie,
'vbar': Sao.View.Graph.VerticalBar,
};
}());
/* This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. */
(function() {
'use strict';
function set_calendar_height(el) {
var height = window.innerHeight - 15;
if (el.parents('.modal-body').length) {
var modal_height = parseInt(
el.parents('.modal-body').css('max-height'), 10);
if (isNaN(modal_height)) {
height -= 200;
} else {
height = modal_height;
}
}
height -= el.find('.fc-toolbar').height();
el.parents('.panel-body').each(function(i, panel) {
panel = jQuery(panel);
height -= parseInt(panel.css('padding-top'), 10);
height -= parseInt(panel.css('padding-bottom'), 10);
});
el.parents('.panel').each(function(i, panel) {
panel = jQuery(panel);
var lengths = panel.css('box-shadow').match(/\d+px/g);
if (lengths && lengths.length) {
lengths = lengths.map(function(length) {
length = parseInt(length, 10);
return isNaN(length) ? 0 : length;
});
height -= Math.max.apply(null, lengths);
}
});
height -= el[0].getBoundingClientRect().y;
el.fullCalendar('option', 'contentHeight', height);
}
jQuery(window).resize(function() {
jQuery('.calendar').each(function(i, el) {
set_calendar_height(jQuery(el));
});
});
Sao.View.CalendarXMLViewParser = Sao.class_(Sao.View.XMLViewParser, {
_parse_calendar: function(node, attributes) {
for (const child of node.childNodes) {
this.parse(child);
}
var view_week;
if (this.view.screen.model.fields[attributes.dtstart]
.description.type == "datetime") {
view_week = 'agendaWeek';
} else {
view_week = 'basicWeek';
}
var view_day;
if (this.view.screen.model.fields[attributes.dtstart]
.description.type == "datetime") {
view_day = 'agendaDay';
} else {
view_day = 'basicDay';
}
var defaultview = 'month';
if (attributes.mode == 'week') {
defaultview = view_week;
}
if (attributes.mode == 'day') {
defaultview = view_day;
}
var header = {
left: 'today prev,next',
center: 'title',
right: 'month,' + view_week + ',' + view_day,
};
if (Sao.i18n.rtl) {
var header_rtl = jQuery.extend({}, header);
header_rtl.left = header.right;
header_rtl.right = header.left;
header = header_rtl;
}
this.view.el.fullCalendar({
defaultView: defaultview,
header: header,
timeFormat: 'H:mm',
scrollTime: (
this.view.screen.context.calendar_scroll_time ||
Sao.Time(6)).toString(),
events: this.view.get_events.bind(this.view),
locale: Sao.i18n.getlang().slice(0, 2),
isRTL: Sao.i18n.rtl,
themeSystem: 'bootstrap3',
bootstrapGlyphicons: {
'prev': 'chevron-' + (Sao.i18n.rtl? 'right' : 'left'),
'next': 'chevron-' + (Sao.i18n.rtl? 'left' : 'right'),
},
buttonTextOverride: {
'today': Sao.i18n.gettext("Today"),
'month': Sao.i18n.gettext("Month"),
'week': Sao.i18n.gettext("Week"),
'day': Sao.i18n.gettext("Day"),
},
eventRender: this.view.event_render.bind(this.view),
eventResize: this.view.event_resize.bind(this.view),
eventDrop: this.view.event_drop.bind(this.view),
eventClick: this.view.event_click.bind(this.view),
dayClick: this.view.day_click.bind(this.view),
});
if (attributes.height !== undefined) {
this.view.el.css('min-height', attributes.height + 'px');
}
if (attributes.width !== undefined) {
this.view.el.css('min-width', attributes.width + 'px');
}
},
_parse_field: function(node, attributes) {
this.view.fields.push(attributes.name);
},
});
Sao.View.Calendar = Sao.class_(Sao.View, {
/* Fullcalendar works with utc date, the default week start day depends on
the user language, the events dates are handled by moment object. */
editable: false,
creatable: false,
view_type: 'calendar',
xml_parser: Sao.View.CalendarXMLViewParser,
init: function(view_id, screen, xml) {
// Used to check if the events are still processing
this.processing = true;
this.fields = [];
this.el = jQuery('
', {
'class': 'calendar'
});
Sao.View.Calendar._super.init.call(this, view_id, screen, xml);
//this.el.fullCalendar('changeView', defaultview);
},
get_colors: function(record) {
var colors = {};
colors.text_color = Sao.config.calendar_colors[0];
if (this.attributes.color) {
colors.text_color = record.field_get(
this.attributes.color);
}
colors.background_color = Sao.config.calendar_colors[1];
if (this.attributes.background_color) {
colors.background_color = record.field_get(
this.attributes.background_color);
}
return colors;
},
display: function() {
this.el.fullCalendar('render');
set_calendar_height(this.el);
// Don't refetch events from server when get_events is processing
if (!this.processing) {
this.el.fullCalendar('refetchEvents');
}
},
insert_event: function(record) {
var description_fields = jQuery.extend([], this.fields);
var title_field = description_fields.shift();
var title = this.screen.model.fields[title_field].get_client(
record);
var field_start = record.model.fields[this.attributes.dtstart];
var date_start = field_start.get_client(record);
field_start.set_state(record);
var date_end = null;
var field_end;
if (this.attributes.dtend) {
field_end = record.model.fields[this.attributes.dtend];
date_end = field_end.get_client(record);
field_end.set_state(record);
}
var model_access = Sao.common.MODELACCESS.get(
this.screen.model_name);
var editable = (
parseInt(this.attributes.editable || 1, 10) &&
model_access.write);
var description = [];
for (const field of description_fields) {
description.push(
this.screen.model.fields[field].get_client( record));
}
description = description.join('\n');
if (date_start) {
var allDay = date_start.isDate &&
(!date_end || date_end.isDate);
if (allDay && date_end && !date_end.isSame(date_start) &&
this.screen.current_view.view_type == "calendar") {
// Add one day to allday event that last more than one day.
// http://github.com/fullcalendar/fullcalendar/issues/2909
date_end.add(1, 'day');
}
// Skip invalid event
if (date_end && date_start > date_end) {
return;
}
var event_editable = (
editable &&
!field_start.get_state_attrs(record).readonly &&
(!date_end || !field_end.get_state_attrs(record).readonly));
var colors = this.get_colors(record);
var values = {
title: title,
start: date_start,
end: date_end,
allDay: allDay,
editable: event_editable,
color: colors.background_color,
textColor: colors.text_color,
record: record,
description: description
};
this.events.push(values);
}
},
get_events: function(start, end, timezone, callback) {
this.processing = true;
this.start = Sao.DateTime(start.utc());
this.end = Sao.DateTime(end.utc());
var prm = jQuery.when();
if (this.screen.current_view &&
(this.screen.current_view.view_type != 'form')) {
var search_string = this.screen.screen_container.get_text();
prm = this.screen.search_filter(search_string);
}
this.events = [];
var promisses = [];
prm.then(() => {
this.group.forEach(record => {
var record_promisses = [];
for (const name of this.fields) {
record_promisses.push(record.load(name));
}
var prm = jQuery.when.apply(jQuery, record_promisses).then(
() => {
this.insert_event(record);
});
promisses.push(prm);
});
return jQuery.when.apply(jQuery, promisses).then(() => {
callback(this.events);
}).always(() => {
this.processing = false;
});
});
},
event_click: function(calEvent, jsEvent, view) {
// Prevent opening the wrong event while the calendar event clicked
// when loading
if (!this.clicked_event) {
this.clicked_event = true;
this.screen.current_record = calEvent.record;
this.screen.switch_view().always(() => {
this.clicked_event = false;
});
}
},
event_drop: function(event, delta, revertFunc, jsEvent, ui, view) {
var dtstart = this.attributes.dtstart;
var dtend = this.attributes.dtend;
var record = event.record;
var previous_start = record.field_get(dtstart);
var previous_end = previous_start;
if (dtend) {
previous_end = record.field_get(dtend);
}
var new_start = event.start;
var new_end = event.end;
if (new_end == previous_start || !new_end) {
new_end = new_start;
}
if (previous_start.isDateTime) {
new_end = Sao.DateTime(new_end.format()).utc();
new_start = Sao.DateTime(new_start.format()).utc();
} else if (!previous_start.isSame(previous_end)) {
// Remove the day that was added at the event end.
new_end.subtract(1, 'day');
this.el.fullCalendar('refetchEvents');
}
if (previous_start <= new_start) {
if (dtend) {
record.field_set_client(dtend, new_end);
}
record.field_set_client(dtstart, new_start);
} else {
record.field_set_client(dtstart, new_start);
if (dtend) {
record.field_set_client(dtend, new_end);
}
}
record.save();
},
event_resize: function(event, delta, revertFunc, jsEvent, ui, view) {
var dtend = this.attributes.dtend;
var record = event.record;
var previous_end = record.field_get(dtend);
var new_end = event.end;
if (previous_end.isDateTime === true) {
new_end = Sao.DateTime(new_end.format()).utc();
} else {
// Remove the day that was added at the event end.
new_end.subtract(1, 'day');
this.el.fullCalendar('refetchEvents');
}
if (new_end == previous_end || !new_end) {
new_end = previous_end;
}
record.field_set_client(dtend, new_end);
record.save();
},
event_render: function(event, element, view) {
// The description field is added in the calendar events and the
// event time is not shown in week view.
if (this.screen.model.fields.date &&
this.screen.view_name == 'calendar') {
element.find('.fc-time').remove();
}
element.find('.fc-content')
.append(jQuery('
', {'class': 'fc-description'})
.text(event.description));
element.css('white-space', 'pre')
.css('overflow', 'hidden')
.css('text-overflow', 'ellipsis')
.attr('title', [event.title, event.description]
.filter(function(e) {
return e;
}).join('\n'));
},
day_click: function(date, jsEvent, view){
var model_access = Sao.common.MODELACCESS.get(
this.screen.model_name);
if (parseInt(this.attributes.editable || 1, 10) &&
model_access.create) {
// Set the calendar date to the clicked date
this.el.fullCalendar('gotoDate', date);
this.screen.current_record = null;
this.screen.new_();
}
},
current_domain: function() {
if (!this.start && !this.end) {
return [['id', '=', -1]];
}
var start = Sao.DateTime(this.start);
var end = Sao.DateTime(this.end);
var dtstart = this.attributes.dtstart;
var dtend = this.attributes.dtend || dtstart;
var fields = this.screen.model.fields;
if (fields[dtstart].description.type == 'date') {
start = start.todate();
}
if (fields[dtend].description.type == 'date') {
end = end.todate();
}
return [
[dtstart, '!=', null],
[dtend, '!=', null],
['OR',
['AND', [dtstart, '>=', start], [dtstart, '<', end]],
['AND', [dtend, '>=', start], [dtend, '<', end]],
['AND', [dtstart, '<', start], [dtend, '>', end]],
],
];
},
get_displayed_period: function(){
var DatesPeriod = [];
if (this.start && this.end) {
DatesPeriod.push(this.start, this.end);
}
return DatesPeriod;
},
set_default_date: function(record, selected_date){
var dtstart = this.attributes.dtstart;
var field = record.model.fields[dtstart];
if (field instanceof Sao.field.DateTime) {
selected_date = Sao.DateTime(selected_date);
} else if (field instanceof Sao.field.Date) {
selected_date = Sao.Date(selected_date);
}
field.set(record, selected_date);
record.on_change([dtstart]);
record.on_change_with([dtstart]);
},
get_selected_date: function(){
return this.el.fullCalendar('getDate');
},
get listed_records() {
return this.events.map(e => e.record);
},
});
}());
/* This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. */
(function() {
'use strict';
Sao.View.ListGroupViewForm = Sao.class_(Sao.View.Form, {
get record() {
return this._record;
},
set record(value) {
this._record = value;
},
button_clicked: function(event) {
if (Sao.common.compare(this.screen.selected_records, [this.record])) {
Sao.View.ListGroupViewForm._super.button_clicked.call(this, event);
}
}
});
Sao.View.ListForm = Sao.class_(Sao.View, {
editable: true,
creatable: true,
view_type: 'list-form',
init: function(view_id, screen, xml) {
Sao.View.ListForm._super.init.call(this, view_id, screen, xml);
if (this.attributes.creatable) {
this.creatable = Boolean(parseInt(this.attributes.creatable, 10));
}
this.form_xml = xml;
this.el = jQuery('
', {
'class': 'list-group list-form'
});
this._view_forms = [];
},
display: function(selected_nodes) {
var record, view_form, view_form_frame, to_delete;
var deferreds = [];
var new_elements = [];
var selected = new Set();
if (!jQuery.isEmptyObject(selected_nodes)) {
for (const id_path of selected_nodes) {
selected.add(id_path[0]);
}
}
for (var i = 0; i < this.group.length; i++) {
record = this.group[i];
view_form = this._view_forms[i];
if (!view_form) {
view_form_frame = this._create_form(record);
new_elements.push(view_form_frame);
view_form = this._view_forms[this._view_forms.length - 1];
} else {
view_form_frame = view_form.el.parent();
view_form.record = record;
}
if (~this.group.record_deleted.indexOf(record) ||
~this.group.record_removed.indexOf(record)) {
view_form_frame.addClass('disabled');
} else {
view_form_frame.removeClass('disabled');
}
if ((this.record === record) || selected.has(record.id)) {
view_form_frame.addClass('list-group-item-selected');
} else {
view_form_frame.removeClass('list-group-item-selected');
}
deferreds.push(view_form.display());
}
if (new_elements.length > 0) {
this.el.append(new_elements);
}
to_delete = this._view_forms.splice(this.group.length);
jQuery(to_delete.map(function (vf) { return vf.el[0]; }))
.parent().detach();
return jQuery.when.apply(jQuery, deferreds);
},
get_selected_paths: function() {
var paths = [];
var view_form_frame;
for (const form of this._view_forms) {
view_form_frame = form.el.parent();
if (view_form_frame.hasClass('list-group-item-selected')) {
paths.push([form.record.id]);
}
}
return paths;
},
_create_form: function(record) {
var view_form = new Sao.View.ListGroupViewForm(
this.view_id, this.screen, this.form_xml);
view_form.record = record;
this._view_forms.push(view_form);
var frame = jQuery('
', {
'class': 'list-group-item list-form-item'
});
frame.append(view_form.el);
frame.click(
this._view_forms.length - 1, this._select_row.bind(this));
return frame;
},
get selected_records() {
var records = [];
for (const view_form of this._view_forms) {
const frame = view_form.el.parent();
if (frame.hasClass('list-group-item-selected')) {
records.push(view_form.record);
}
}
return records;
},
get listed_records() {
return this.group.slice();
},
set_cursor: function(new_, reset_view) {
if (new_) {
this.el.animate({
scrollTop: this.el[0].scrollHeight
});
}
},
select_records: function(from, to) {
jQuery(this._view_forms.map(function (vf) { return vf.el[0]; }))
.parent().removeClass('list-group-item-selected');
if ((from === null) && (to === null)) {
return;
}
if (!from) {
from = 0;
}
if (!to) {
to = 0;
}
if (to < from) {
var tmp = from;
from = to;
to = tmp;
}
for (const form of this._view_forms.slice(from, to + 1)) {
form.el.parent().addClass('list-group-item-selected');
}
},
_select_row: function(event_) {
var next_form_idx = event_.data;
var next_view_form = this._view_forms[next_form_idx];
var prm = jQuery.when();
if (this.record && (next_view_form.record != this.record)) {
if (!this.screen.group.parent) {
if (!this.record.validate(
this.get_fields(), false, false, false)) {
prm = jQuery.Deferred().reject();
} else {
prm = this.record.save();
}
} else if (this.screen.attributes.pre_validate) {
prm = this.record.pre_validate();
}
}
prm.done(() => {
var current_view_form;
if (event_.shiftKey) {
let i = 0;
for (const other_view_form of this._view_forms) {
if (other_view_form.record === this.record) {
current_view_form = other_view_form;
break;
}
i++;
}
this.select_records(i, next_form_idx);
} else {
let selected = next_view_form.el.parent().hasClass(
'list-group-item-selected')
if (!(event_.ctrlKey || event_.metaKey)) {
this.select_records(null, null);
}
if (selected) {
next_view_form.el.parent()
.removeClass('list-group-item-selected');
this.record = null;
} else {
next_view_form.el.parent()
.addClass('list-group-item-selected');
this.record = next_view_form.record;
}
}
if (current_view_form) {
this.record = current_view_form.record;
}
});
}
});
}());
/* This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. */
(function() {
'use strict';
Sao.Action = {
report_blob_url: undefined
};
Sao.Action.exec_action = function(action, data, context) {
if (!context) {
context = {};
} else {
context = jQuery.extend({}, context);
}
var session = Sao.Session.current_session;
if (data === undefined) {
data = {};
} else {
data = jQuery.extend({}, data);
}
delete context.active_id;
delete context.active_ids;
delete context.active_model;
function add_name_suffix(name, context){
if (!data.model || !data.ids) {
return jQuery.when(name);
}
var max_records = 5;
var ids = data.ids.filter(function(id){
return id >= 0;
}).slice(0, max_records);
if (!ids.length) {
return jQuery.when(name);
}
return Sao.rpc({
'method': 'model.' + data.model + '.read',
'params': [ids, ['rec_name'], context]
}, Sao.Session.current_session).then(function(result) {
var name_suffix = result.map(function(record){
return record.rec_name;
}).join(Sao.i18n.gettext(', '));
if (data.ids.length > ids.length) {
name_suffix += Sao.i18n.gettext(',...');
}
if (name_suffix) {
return Sao.i18n.gettext('%1 (%2)', name, name_suffix);
} else {
return name;
}
});
}
data.action_id = action.id;
var params = {
'icon': action['icon.rec_name'] || '',
};
var name_prm;
switch (action.type) {
case 'ir.action.act_window':
if (!jQuery.isEmptyObject(action.views)) {
params.view_ids = [];
params.mode = [];
for (const view of action.views) {
params.view_ids.push(view[0]);
params.mode.push(view[1]);
}
} else if (!jQuery.isEmptyObject(action.view_id)) {
params.view_ids = [action.view_id[0]];
}
if (action.pyson_domain === undefined) {
action.pyson_domain = '[]';
}
var ctx = {
active_model: data.model || null,
active_id: data.id || null,
active_ids: data.ids || [],
};
ctx = jQuery.extend(ctx, session.context);
ctx._user = session.user_id;
var decoder = new Sao.PYSON.Decoder(ctx);
params.context = jQuery.extend(
{}, context,
decoder.decode( action.pyson_context || '{}'));
ctx = jQuery.extend(ctx, params.context);
ctx.context = ctx;
decoder = new Sao.PYSON.Decoder(ctx);
params.domain = decoder.decode(action.pyson_domain);
params.order = decoder.decode(action.pyson_order);
params.search_value = decoder.decode(
action.pyson_search_value || '[]');
params.tab_domain = [];
for (const element of action.domains) {
params.tab_domain.push(
[element[0], decoder.decode(element[1]), element[2]]);
}
name_prm = jQuery.when(action.name);
params.model = action.res_model || data.res_model;
params.res_id = action.res_id || data.res_id;
params.context_model = action.context_model;
params.context_domain = action.context_domain;
if ((action.limit !== undefined) && (action.limit !== null)) {
params.limit = action.limit;
}
if (action.keyword) {
name_prm = add_name_suffix(action.name, params.context);
}
return name_prm.then(function(name) {
params.name = name;
return Sao.Tab.create(params);
});
case 'ir.action.wizard':
params.action = action.wiz_name;
params.data = data;
params.context = context;
params.window = action.window;
name_prm = jQuery.when(action.name);
if ((action.keyword || 'form_action') === 'form_action') {
name_prm = add_name_suffix(action.name, context);
}
return name_prm.then(function(name) {
params.name = name;
return Sao.Wizard.create(params);
});
case 'ir.action.report':
params.name = action.report_name;
params.data = data;
params.direct_print = action.direct_print;
params.context = context;
return Sao.Action.exec_report(params);
case 'ir.action.url':
window.open(action.url, '_blank', 'noreferrer,noopener');
return jQuery.when();
}
};
Sao.Action.exec_keyword = function(
keyword, data, context, warning=true, alwaysask=false) {
var model_id = data.id;
var args = {
'method': 'model.' + 'ir.action.keyword.get_keyword',
'params': [keyword, [data.model, model_id], {}]
};
var prm = Sao.rpc(args, Sao.Session.current_session);
var exec_action = function(actions) {
var keyact = {};
for (var i in actions) {
var action = actions[i];
keyact[action.name.split(' / ').pop()] = action;
}
var prm = Sao.common.selection(
Sao.i18n.gettext('Select your action'),
keyact, alwaysask);
return prm.then(function(action) {
Sao.Action.exec_action(action, data, context);
}, function() {
if (jQuery.isEmptyObject(keyact) && warning) {
alert(Sao.i18n.gettext('No action defined.'));
}
});
};
return prm.pipe(exec_action);
};
Sao.Action.exec_report = function(attributes) {
if (!attributes.context) {
attributes.context = {};
}
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;
var prm = Sao.rpc({
'method': 'report.' + attributes.name + '.execute',
'params': [data.ids || [], data, context]
}, Sao.Session.current_session);
return prm.done(function(result) {
var report_type = result[0];
var data = result[1];
// TODO direct print
var name = result[3];
var file_name = name + '.' + report_type;
Sao.common.download_file(data, file_name);
});
};
Sao.Action.execute = function(action, data, context, keyword) {
if (typeof action == 'number') {
action = Sao.rpc({
'method': 'model.ir.action.get_action_value',
'params': [action, context],
}, Sao.Session.current_session, false);
}
if (keyword) {
var keywords = {
'ir.action.report': 'form_report',
'ir.action.wizard': 'form_action',
'ir.action.act_window': 'form_relate'
};
if (!action.keyword) {
action.keyword = keywords[action.type];
}
}
return Sao.Action.exec_action(action, data, context);
};
Sao.Action.evaluate = function(action, atype, record) {
action = jQuery.extend({}, action);
var email = {};
if ('pyson_email' in action) {
email = record.expr_eval(action.pyson_email);
if (jQuery.isEmptyObject(email)) {
email = {};
}
}
if (!('subject' in email)) {
email.subject = action.name.replace(/_/g, '');
}
action.email = email;
return action;
};
}());
/* This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. */
(function() {
'use strict';
var ENCODINGS = ["866", "ansi_x3.4-1968", "arabic", "ascii",
"asmo-708", "big5", "big5-hkscs", "chinese", "cn-big5", "cp1250",
"cp1251", "cp1252", "cp1253", "cp1254", "cp1255", "cp1256",
"cp1257", "cp1258", "cp819", "cp866", "csbig5", "cseuckr",
"cseucpkdfmtjapanese", "csgb2312", "csibm866", "csiso2022jp",
"csiso2022kr", "csiso58gb231280", "csiso88596e", "csiso88596i",
"csiso88598e", "csiso88598i", "csisolatin1", "csisolatin2",
"csisolatin3", "csisolatin4", "csisolatin5", "csisolatin6",
"csisolatin9", "csisolatinarabic", "csisolatincyrillic",
"csisolatingreek", "csisolatinhebrew", "cskoi8r", "csksc56011987",
"csmacintosh", "csshiftjis", "cyrillic", "dos-874", "ecma-114",
"ecma-118", "elot_928", "euc-jp", "euc-kr", "gb18030", "gb2312",
"gb_2312", "gb_2312-80", "gbk", "greek", "greek8", "hebrew",
"hz-gb-2312", "ibm819", "ibm866", "iso-2022-cn", "iso-2022-cn-ext",
"iso-2022-jp", "iso-2022-kr", "iso-8859-1", "iso-8859-10",
"iso-8859-11", "iso-8859-13", "iso-8859-14", "iso-8859-15",
"iso-8859-16", "iso-8859-2", "iso-8859-3", "iso-8859-4",
"iso-8859-5", "iso-8859-6", "iso-8859-6-e", "iso-8859-6-i",
"iso-8859-7", "iso-8859-8", "iso-8859-8-e", "iso-8859-8-i",
"iso-8859-9", "iso-ir-100", "iso-ir-101", "iso-ir-109",
"iso-ir-110", "iso-ir-126", "iso-ir-127", "iso-ir-138",
"iso-ir-144", "iso-ir-148", "iso-ir-149", "iso-ir-157", "iso-ir-58",
"iso8859-1", "iso8859-10", "iso8859-11", "iso8859-13", "iso8859-14",
"iso8859-15", "iso8859-2", "iso8859-3", "iso8859-4", "iso8859-5",
"iso8859-6", "iso8859-7", "iso8859-8", "iso8859-9", "iso88591",
"iso885910", "iso885911", "iso885913", "iso885914", "iso885915",
"iso88592", "iso88593", "iso88594", "iso88595", "iso88596",
"iso88597", "iso88598", "iso88599", "iso_8859-1", "iso_8859-15",
"iso_8859-1:1987", "iso_8859-2", "iso_8859-2:1987", "iso_8859-3",
"iso_8859-3:1988", "iso_8859-4", "iso_8859-4:1988", "iso_8859-5",
"iso_8859-5:1988", "iso_8859-6", "iso_8859-6:1987", "iso_8859-7",
"iso_8859-7:1987", "iso_8859-8", "iso_8859-8:1988", "iso_8859-9",
"iso_8859-9:1989", "koi", "koi8", "koi8-r", "koi8-ru", "koi8-u",
"koi8_r", "korean", "ks_c_5601-1987", "ks_c_5601-1989", "ksc5601",
"ksc_5601", "l1", "l2", "l3", "l4", "l5", "l6", "l9", "latin1",
"latin2", "latin3", "latin4", "latin5", "latin6", "logical", "mac",
"macintosh", "ms932", "ms_kanji", "shift-jis", "shift_jis", "sjis",
"sun_eu_greek", "tis-620", "unicode-1-1-utf-8", "us-ascii",
"utf-16", "utf-16be", "utf-16le", "utf-8", "utf8", "visual",
"windows-1250", "windows-1251", "windows-1252", "windows-1253",
"windows-1254", "windows-1255", "windows-1256", "windows-1257",
"windows-1258", "windows-31j", "windows-874", "windows-949",
"x-cp1250", "x-cp1251", "x-cp1252", "x-cp1253", "x-cp1254",
"x-cp1255", "x-cp1256", "x-cp1257", "x-cp1258", "x-euc-jp", "x-gbk",
"x-mac-cyrillic", "x-mac-roman", "x-mac-ukrainian", "x-sjis",
"x-user-defined", "x-x-big5"];
Sao.Window = {};
Sao.Window.InfoBar = Sao.class_(Object, {
init: function() {
this.el = jQuery('
', {
'class': 'infobar',
});
this.__messages = new Set();
},
add: function(message, type, kind) {
kind = kind || null;
if (!message) {
return;
}
var key = JSON.stringify([message, type]);
if (!this.__messages.has(key)) {
var infobar = jQuery('
', {
'class': 'alert alert-dismissible alert-' + (
type || 'error'),
'role': 'alert',
}).append(jQuery('
', {
'type': 'button',
'class': 'close',
'aria-label': Sao.i18n.gettext("Close"),
'title': Sao.i18n.gettext("Close"),
'data-dismiss': 'alert',
}).append(jQuery('
', {
'aria-hidden': true,
}).append('×'))
).append(jQuery('
')
.css('white-space','pre-wrap')
.text(message))
.on('close.bs.alert',
null, key, this.__response.bind(this));
this.el.append(infobar);
infobar.data('kind', kind);
}
},
__response: function(evt) {
this.__messages.add(evt.data);
},
refresh: function(kind) {
kind = kind || null;
this.el.children().each((i, el) => {
el = jQuery(el);
if (el.data('kind') === kind) {
el.remove();
}
});
},
clear: function() {
let kinds = new Set();
this.el.children().each(
(i, el) => kinds.add(jQuery(el).data('kind')));
kinds.forEach(kind => {
this.refresh(kind);
});
this.__messages.clear();
},
});
Sao.Window.Form = Sao.class_(Object, {
init: function(screen, callback, kwargs) {
kwargs = kwargs || {};
this._position = undefined;
this._length = 0;
this.screen = screen;
this.callback = callback;
this.many = kwargs.many || 0;
this.domain = kwargs.domain || null;
this.context = kwargs.context || null;
this.save_current = kwargs.save_current;
var title_prm = jQuery.when(kwargs.title || '').then(
title => {
if (screen.breadcrumb.length) {
var breadcrumb = jQuery.extend([], screen.breadcrumb);
if (title) {
breadcrumb.push(title);
}
this.title = breadcrumb.slice(-3, -1).map(function(x) {
return Sao.common.ellipsize(x, 30);
}).concat(breadcrumb.slice(-1)).join(' › ');
if (breadcrumb.length > 3) {
this.title = '... › ' + this.title;
}
} else {
if (!title) {
title = Sao.common.MODELNAME.get(this.screen.model_name);
}
this.title = title;
}
var revision = this.screen.context._datetime;
var label;
if (revision &&
Sao.common.MODELHISTORY.contains(this.screen.model_name)) {
var date_format = Sao.common.date_format(
this.screen.context.date_format);
var time_format = '%H:%M:%S.%f';
var revision_label = ' @ ' + Sao.common.format_datetime(
date_format + ' ' + time_format, revision);
label = Sao.common.ellipsize(
this.title, 80 - revision_label.length) +
revision_label;
title = this.title + revision_label;
} else {
label = Sao.common.ellipsize(this.title, 80);
}
return label;
});
this.prev_view = screen.current_view;
this.screen.screen_container.alternate_view = true;
this.info_bar = new Sao.Window.InfoBar();
var view_type = kwargs.view_type || 'form';
this.switch_prm = this.screen.switch_view(view_type)
.done(() => {
if (kwargs.new_ &&
(this.screen.current_view.view_type == view_type)) {
this.screen.new_(undefined, kwargs.defaults);
}
});
var dialog = new Sao.Dialog('', 'window-form', 'lg', false);
this.dialog = dialog;
this.el = dialog.modal;
this.el.on('keydown', e => {
if (e.which == Sao.common.ESC_KEYCODE) {
e.preventDefault();
this.response('RESPONSE_CANCEL');
}
});
var readonly = this.screen.group.readonly;
this.but_ok = null;
this.but_new = null;
this._initial_value = null;
this.view_type = view_type;
if ((view_type == 'form') && !readonly) {
let label;
if (kwargs.new_) {
label = Sao.i18n.gettext('Delete');
} else {
label = Sao.i18n.gettext("Discard changes");
var record = this.screen.current_record;
this._initial_value = record.get_on_change_value();
if (record.group.parent &&
record.model.fields[record.group.parent_name]) {
var parent_field = record.model.fields[
record.group.parent_name];
this._initial_value[record.group.parent_name] = (
parent_field.get_eval(record));
}
}
dialog.footer.append(jQuery('
', {
'class': 'btn btn-link',
'type': 'button',
'title': label,
'id': 'delete_',
}).text(label).click(() => {
this.response('RESPONSE_CANCEL');
}));
}
if (kwargs.new_ && this.many) {
let label;
if (this.save_current && !readonly) {
label = Sao.i18n.gettext("Save and New");
} else {
label = Sao.i18n.gettext("Add and New");
}
dialog.footer.append(jQuery('
', {
'class': 'btn btn-default',
'type': 'button',
'title': label,
'id': 'new_',
}).text(label).click(() => {
this.response('RESPONSE_ACCEPT');
}));
}
if (this.save_current && !readonly) {
let label = Sao.i18n.gettext("Save");
this.but_ok = jQuery('
', {
'class': 'btn btn-primary',
'type': 'submit',
'title': label,
'id': 'save',
}).text(label).appendTo(dialog.footer);
} else {
let label;
if (readonly || (view_type == 'tree')) {
label = Sao.i18n.gettext("Close");
} else if (kwargs.new_) {
label = Sao.i18n.gettext("Add");
} else {
label = Sao.i18n.gettext("Apply changes");
}
this.but_ok = jQuery('
', {
'class': 'btn btn-primary',
'type': 'submit',
'title': label,
'id': 'save',
}).text(label).appendTo(dialog.footer);
}
dialog.content.submit(e => {
e.preventDefault();
this.response('RESPONSE_OK');
});
if (view_type == 'tree') {
var menu = jQuery('
', {
'class': 'window-form-toolbar'
}).appendTo(dialog.body);
var group = jQuery('
', {
'class': 'input-group input-group-sm'
}).appendTo(menu);
this.wid_text = jQuery('
', {
type: 'input'
}).appendTo(menu);
this.wid_text.hide();
var buttons = jQuery('
', {
'class': 'input-group-btn'
}).appendTo(group);
var access = Sao.common.MODELACCESS.get(this.screen.model_name);
var disable_during = function(callback) {
return function(evt) {
var button = jQuery(evt.target);
button.prop('disabled', true);
(callback(evt) || jQuery.when())
.always(function() {
button.prop('disabled', false);
});
};
};
this.but_switch = jQuery('
', {
'class': 'btn btn-default btn-sm',
'type': 'button',
'aria-label': Sao.i18n.gettext("Switch"),
'title': Sao.i18n.gettext("Switch"),
'id': 'switch_',
}).append(Sao.common.ICONFACTORY.get_icon_img('tryton-switch')
).appendTo(buttons);
this.but_switch.click(
disable_during(this.switch_.bind(this)));
this.but_previous = jQuery('
', {
'class': 'btn btn-default btn-sm',
'type': 'button',
'aria-label': Sao.i18n.gettext("Previous"),
'title': Sao.i18n.gettext("Previous"),
'id': 'previous',
}).append(Sao.common.ICONFACTORY.get_icon_img('tryton-back')
).appendTo(buttons);
this.but_previous.click(
disable_during(this.previous.bind(this)));
this.label = jQuery('
', {
'class': 'badge'
}).appendTo(jQuery('
', {
'class': 'btn hidden-xs',
}).appendTo(buttons));
this.but_next = jQuery('
', {
'class': 'btn btn-default btn-sm',
'type': 'button',
'aria-label': Sao.i18n.gettext("Next"),
'title': Sao.i18n.gettext("Next"),
'id': 'next',
}).append(Sao.common.ICONFACTORY.get_icon_img('tryton-forward')
).appendTo(buttons);
this.but_next.click(disable_during(this.next.bind(this)));
if (this.domain) {
this.wid_text.show();
this.but_add = jQuery('
', {
'class': 'btn btn-default btn-sm',
'type': 'button',
'aria-label': Sao.i18n.gettext("Add"),
'title': Sao.i18n.gettext("Add"),
}).append(Sao.common.ICONFACTORY.get_icon_img('tryton-add')
).appendTo(buttons);
this.but_add.click(disable_during(this.add.bind(this)));
this.but_add.prop('disabled', !access.read || readonly);
this.but_remove = jQuery('
', {
'class': 'btn btn-default btn-sm',
'type': 'button',
'aria-label': Sao.i18n.gettext("Remove"),
'title': Sao.i18n.gettext("Remove"),
}).append(Sao.common.ICONFACTORY.get_icon_img('tryton-remove')
).appendTo(buttons);
this.but_remove.click(
disable_during(this.remove.bind(this)));
this.but_remove.prop('disabled', !access.read || readonly);
}
this.but_new = jQuery('
', {
'class': 'btn btn-default btn-sm',
'type': 'button',
'aria-label': Sao.i18n.gettext("New"),
'title': Sao.i18n.gettext("New"),
'id': 'new_',
}).append(Sao.common.ICONFACTORY.get_icon_img('tryton-create')
).appendTo(buttons);
this.but_new.click(disable_during(this.new_.bind(this)));
this.but_new.prop('disabled', !access.create || readonly);
this.but_del = jQuery('
', {
'class': 'btn btn-default btn-sm',
'type': 'button',
'aria-label': Sao.i18n.gettext("Delete"),
'title': Sao.i18n.gettext("Delete"),
'id': 'delete_',
}).append(Sao.common.ICONFACTORY.get_icon_img('tryton-delete')
).appendTo(buttons);
this.but_del.click(disable_during(this.delete_.bind(this)));
this.but_del.prop('disabled', !access['delete'] || readonly);
this.but_undel = jQuery('
', {
'class': 'btn btn-default btn-sm',
'type': 'button',
'aria-label': Sao.i18n.gettext("Undelete"),
'title': Sao.i18n.gettext("Undelete"),
}).append(Sao.common.ICONFACTORY.get_icon_img('tryton-undo')
).appendTo(buttons);
this.but_undel.click(disable_during(this.undelete.bind(this)));
this.but_undel.prop('disabled', !access['delete'] || readonly);
}
var content = jQuery('
').appendTo(dialog.body);
dialog.body.append(this.info_bar.el);
this.screen.windows.push(this);
this.switch_prm.done(() => {
if (this.screen.current_view.view_type != view_type) {
this.destroy();
} else {
title_prm.done(dialog.add_title.bind(dialog));
content.append(this.screen.screen_container.alternate_viewport);
this.el.modal('show');
}
});
this.el.on('shown.bs.modal', event => {
this.screen.display().done(() => {
this.screen.set_cursor();
});
});
this.el.on('hidden.bs.modal', function(event) {
jQuery(this).remove();
});
},
record_message: function(position, size) {
this._position = position;
this._length = size;
let name = '_';
if (position) {
let selected = this.screen.selected_records.length;
name = ' ' + position;
if (selected > 1) {
name += '#' + selected;
}
}
let message = name + '/' + Sao.common.humanize(size);
if (this.label) {
this.label.text(message).attr('title', message);
}
this._set_button_sensitive();
},
record_modified: function() {
this.info_bar.refresh();
this._set_button_sensitive();
},
_set_button_sensitive: function() {
if (this.view_type != 'tree') {
return;
}
let access = Sao.common.MODELACCESS.get(this.screen.model_name);
let first = false,
last = false;
if (typeof this._position == 'number') {
first = this._position <= 1;
last = this._position >= this._length;
}
var deletable =
this.screen.deletable &&
this.screen.selected_records.some((r) => !r.deleted && !r.removed);
let undeletable =
this.screen.selected_records.some((r) => r.deleted || r.removed);
let view_type = this.screen.current_view.view_type;
let has_views = this.screen.number_of_views > 1;
let readonly = this.screen.group.readonly;
this.but_switch.prop(
'disabled',
!((this._position || (view_type == 'form')) && has_views));
this.but_new.prop(
'disabled',
readonly ||
!access.create);
this.but_del.prop(
'disabled',
readonly ||
!access.delete ||
!deletable);
this.but_undel.prop(
'disabled',
readonly ||
!undeletable ||
(typeof this._position != 'number'));
this.but_next.prop(
'disabled',
!this._length ||
last);
this.but_previous.prop(
'disabled',
!this._length ||
first);
if (this.domain) {
this.but_add.prop(
'disabled',
readonly ||
!access.write_access ||
!access.readonly);
this.wid_text.prop('disabled', this.but_add.prop('disabled'));
this.but_remove.prop(
'disabled',
readonly ||
(typeof this._position != 'number') ||
!access.write ||
!access.read);
}
},
add: function() {
var domain = jQuery.extend([], this.domain);
var model_name = this.screen.model_name;
var value = this.wid_text.val();
const callback = result => {
var prm = jQuery.when();
if (!jQuery.isEmptyObject(result)) {
var ids = [];
for (const record of result) {
ids.push(record[0]);
}
this.screen.group.load(ids, true, -1, null);
prm = this.screen.display();
}
prm.done(() => {
this.screen.set_cursor();
});
this.entry.val('');
};
var parser = new Sao.common.DomainParser();
new Sao.Window.Search(model_name, callback, {
sel_multi: true,
context: this.context,
domain: domain,
search_filter: parser.quote(value)
});
},
remove: function() {
this.screen.remove(false, true, false);
},
new_: function() {
this.screen.new_();
this._initial_value = null;
this.many -= 1;
if (this.many == 0) {
this.but_new.addClass('disabled');
}
},
delete_: function() {
this.screen.remove(false, false, false);
},
undelete: function() {
this.screen.unremove();
},
previous: function() {
return this.screen.display_previous();
},
next: function() {
return this.screen.display_next();
},
switch_: function() {
return this.screen.switch_view();
},
response: function(response_id) {
var result;
this.screen.current_view.set_value();
var readonly = this.screen.group.readonly;
if (~['RESPONSE_OK', 'RESPONSE_ACCEPT'].indexOf(response_id) &&
!readonly &&
this.screen.current_record) {
jQuery.when(this.screen.current_record.validate()).then(validate => {
if (validate && this.screen.attributes.pre_validate) {
return this.screen.current_record.pre_validate().then(
() => true, () => false);
}
return validate;
}).then(validate => {
var closing_prm = jQuery.Deferred();
if (validate && this.save_current) {
this.screen.save_current().then(closing_prm.resolve,
closing_prm.reject);
} else if (validate &&
this.screen.current_view.view_type == 'form') {
var view = this.screen.current_view;
var validate_prms = [];
for (var name in view.widgets) {
var widget = view.widgets[name];
if (widget.screen &&
widget.screen.attributes.pre_validate) {
var record = widget.screen.current_record;
if (record) {
validate_prms.push(record.pre_validate());
}
}
}
jQuery.when.apply(jQuery, validate_prms).then(
closing_prm.resolve, closing_prm.reject);
} else if (!validate) {
this.info_bar.add(
this.screen.invalid_message(), 'danger');
closing_prm.reject();
} else {
this.info_bar.clear();
closing_prm.resolve();
}
closing_prm.fail(() => {
this.screen.display().done(() => {
this.screen.set_cursor();
});
});
// TODO Add support for many
closing_prm.done(() => {
if (response_id == 'RESPONSE_ACCEPT') {
this.screen.new_();
this.screen.current_view.display().done(() => {
this.screen.set_cursor();
});
this.many -= 1;
if (this.many === 0) {
this.but_new.prop('disabled', true);
}
} else {
result = true;
if (this.callback) {
this.callback(result);
}
this.destroy();
}
});
});
return;
}
var cancel_prm = null;
if (response_id == 'RESPONSE_CANCEL' &&
!readonly &&
this.screen.current_record) {
result = false;
var record = this.screen.current_record;
var added = record.modified_fields.id;
if ((record.id < 0) || this.save_current) {
cancel_prm = this.screen.cancel_current(
this._initial_value);
} else if (record.modified) {
record.cancel();
cancel_prm = record.reload().then(() => {
this.screen.display();
});
}
if (added) {
record.modified_fields.id = added;
}
} else {
result = (response_id != 'RESPONSE_CANCEL') && !readonly;
}
(cancel_prm || jQuery.when()).then(() => {
if (this.callback) {
this.callback(result);
}
this.destroy();
});
},
destroy: function() {
this.screen.windows.splice(this.screen.windows.indexOf(this), 1);
this.screen.screen_container.alternate_view = false;
this.screen.screen_container.alternate_viewport.children()
.detach();
if (this.prev_view) {
// Empty when opening from Many2One
this.screen.switch_view(this.prev_view.view_type);
}
this.el.modal('hide');
}
});
Sao.Window.Attachment = Sao.class_(Sao.Window.Form, {
init: function(record, callback) {
this.resource = record.model.name + ',' + record.id;
this.attachment_callback = callback;
var context = jQuery.extend({}, record.get_context());
var screen = new Sao.Screen('ir.attachment', {
domain: [['resource', '=', this.resource]],
mode: ['tree', 'form'],
context: context,
});
var title = record.rec_name().then(function(rec_name) {
return Sao.i18n.gettext('Attachments (%1)', rec_name);
});
Sao.Window.Attachment._super.init.call(this, screen, this.callback,
{view_type: 'tree', title: title});
this.switch_prm = this.switch_prm.then(function() {
return screen.search_filter();
});
},
callback: function(result) {
var prm = jQuery.when();
if (result) {
prm = this.screen.save_current();
}
if (this.attachment_callback) {
prm.always(this.attachment_callback.bind(this));
}
},
add_data: function(data, filename) {
var screen = this.screen;
this.switch_prm.then(function() {
screen.new_().then(function(record) {
var data_field = record.model.fields.data;
record.field_set_client(
data_field.description.filename, filename);
record.field_set_client('data', data);
screen.display();
});
});
},
add_uri: function(uri) {
var screen = this.screen;
this.switch_prm.then(function() {
screen.current_record = null;
screen.switch_view('form').then(function() {
screen.new_().then(function(record) {
record.field_set_client('link', uri);
record.field_set_client('type', 'link');
screen.display();
});
});
});
},
add_text: function(text) {
var screen = this.screen;
this.switch_prm.then(function() {
screen.current_record = null;
screen.switch_view('form').then(function() {
screen.new_().then(function(record) {
record.field_set_client('description', text);
screen.display();
});
});
});
},
});
Sao.Window.Attachment.get_attachments = function(record) {
var prm;
if (record && (record.id >= 0)) {
var context = record.get_context();
prm = Sao.rpc({
'method': 'model.ir.attachment.search_read',
'params': [
[['resource', '=', record.model.name + ',' + record.id]],
0, 20, null, ['rec_name', 'name', 'type', 'link'],
context],
}, record.model.session);
} else {
prm = jQuery.when([]);
}
var partial = function(callback, attachment, context, session) {
return function() {
return callback(attachment, context, session);
};
};
return prm.then(function(attachments) {
return attachments.map(function(attachment) {
var name = attachment.rec_name;
if (attachment.type == 'link') {
return [name, attachment.link];
} else {
var callback = Sao.Window.Attachment[
'open_' + attachment.type];
return [name, partial(
callback, attachment, context, record.model.session)];
}
});
});
};
Sao.Window.Attachment.open_data = function(attachment, context, session) {
Sao.rpc({
'method': 'model.ir.attachment.read',
'params': [
[attachment.id], ['data'], context],
}, session).then(function(values) {
Sao.common.download_file(values[0].data, attachment.name);
});
};
Sao.Window.Note = Sao.class_(Sao.Window.Form, {
init: function(record, callback) {
this.resource = record.model.name + ',' + record.id;
this.note_callback = callback;
var context = jQuery.extend({}, record.get_context());
var screen = new Sao.Screen('ir.note', {
domain: [['resource', '=', this.resource]],
mode: ['tree', 'form'],
context: context,
});
var title = record.rec_name().then(function(rec_name) {
return Sao.i18n.gettext('Notes (%1)', rec_name);
});
Sao.Window.Note._super.init.call(this, screen, this.callback,
{view_type: 'tree', title: title});
this.switch_prm = this.switch_prm.then(function() {
return screen.search_filter();
});
},
callback: function(result) {
var prm = jQuery.when();
if (result) {
var unread = this.screen.group.model.fields.unread;
for (const record of this.screen.group) {
if (record.get_loaded() || record.id < 0) {
if (!record.modified_fields.unread) {
unread.set_client(record, false);
}
}
}
prm = this.screen.save_current();
}
if (this.note_callback) {
prm.always(this.note_callback.bind(this));
}
}
});
Sao.Window.Log = Sao.class_(Sao.Window.Form, {
init: function(record) {
this.resource = record.model.name + ',' + record.id;
var title = record.rec_name().then(function(rec_name) {
return Sao.i18n.gettext("Logs (%1)", rec_name);
});
var context = jQuery.extend({}, record.get_context());
var form = jQuery('
', {
'class': 'form',
});
var table = jQuery('
', {
'class': 'form-container responsive responsive-noheader',
}).appendTo(form);
var body = jQuery('
').appendTo(table);
record.model.execute(
'read', [[record.id],
['create_uid.rec_name', 'create_date',
'write_uid.rec_name', 'write_date',
'xml_id']], context
).then(([log]) => {
var row, cell, label, input;
row = jQuery('
|
').appendTo(body);
cell = jQuery('
| ').css('text-align', 'end').appendTo(row);
label = jQuery('
', {
'class': 'form-label',
'text': Sao.i18n.gettext("Model:"),
}).appendTo(cell);
label.uniqueId();
cell = jQuery('
| ').appendTo(row);
input = jQuery('
', {
'readonly': true,
'class': 'form-control input-sm',
'aria-labelledby': label.attr('id'),
}).val(record.model.name).appendTo(cell);
input.uniqueId();
cell = jQuery('
| ').css('text-align', 'end').appendTo(row);
label = jQuery('
', {
'class': 'form-label',
'text': Sao.i18n.gettext("ID:"),
}).appendTo(cell);
label.uniqueId();
cell = jQuery('
| ').appendTo(row);
input = jQuery('
', {
'type': 'integer',
'readonly': true,
'class': 'form-control input-sm',
'aria-labelledby': label.attr('id'),
}).val(record.id).appendTo(cell);
input.css('text-align', 'end');
input.uniqueId();
if (log.xml_id) {
const [module, xml_id] = log.xml_id.split('.', 2);
row = jQuery('
|
').appendTo(body);
cell = jQuery('
| ').css('text-align', 'end').appendTo(row);
label = jQuery('
', {
'class': 'form-label',
'text': Sao.i18n.gettext("Module:"),
}).appendTo(cell);
label.uniqueId();
cell = jQuery('
| ').appendTo(row);
input = jQuery('
', {
'readonly': true,
'class': 'form-control input-sm',
'aria-labelledby': label.attr('id'),
}).val(module).appendTo(cell);
input.uniqueId();
cell = jQuery('
| ').css('text-align', 'end').appendTo(row);
label = jQuery('
', {
'class': 'form-label',
'text': Sao.i18n.gettext("XML ID:"),
}).appendTo(cell);
label.uniqueId();
cell = jQuery('
| ').appendTo(row);
input = jQuery('
', {
'readonly': true,
'class': 'form-control input-sm',
'aria-labelledby': label.attr('id'),
}).val(xml_id).appendTo(cell);
input.uniqueId();
}
[['create_uid.', Sao.i18n.gettext("Created by:"),
'create_date', Sao.i18n.gettext("Created at:")],
['write_uid.', Sao.i18n.gettext("Last Modified by:"),
'write_date', Sao.i18n.gettext("Last Modified at:")],
].forEach(([user, user_label, date, date_label]) => {
row = jQuery('
|
').appendTo(body);
cell = jQuery('
| ').css('text-align', 'end').appendTo(row);
label = jQuery('
', {
'class': 'form-label',
'text': user_label,
}).appendTo(cell);
label.uniqueId();
cell = jQuery('
| ').appendTo(row);
user = log[user];
if (user) {
user = user.rec_name;
}
input = jQuery('
', {
'readonly': true,
'class': 'form-control input-sm',
'aria-labelledby': label.attr('id'),
}).val(user || '').appendTo(cell);
input.css('width', '50ch');
input.uniqueId();
cell = jQuery('
| ').css('text-align', 'end').appendTo(row);
label = jQuery('
', {
'class': 'form-label',
'text': date_label,
}).appendTo(cell);
label.uniqueId();
cell = jQuery('
| ').appendTo(row);
date = log[date];
if (date) {
date = Sao.common.format_datetime(
Sao.common.date_format() + ' %H:%M:%S', date);
} else {
date = '';
}
input = jQuery('
', {
'readonly': true,
'class': 'form-control input-sm',
'aria-labelledby': label.attr('id'),
}).val(date).appendTo(cell);
input.css('width', date.length + 'ch');
input.uniqueId();
});
});
var screen = new Sao.Screen('ir.model.log', {
domain: [['resource', '=', this.resource]],
mode: ['tree', 'form'],
context: context,
});
Sao.Window.Log._super.init.call(
this, screen, null, {view_type: 'tree', title: title});
this.switch_prm = this.switch_prm.then(function() {
return screen.search_filter();
});
this.dialog.body.prepend(form);
},
});
Sao.Window.Search = Sao.class_(Object, {
init: function(model, callback, kwargs) {
kwargs = kwargs || {};
var views_preload = kwargs.views_preload || {};
this.model_name = model;
this.domain = kwargs.domain || [];
this.context = kwargs.context || {};
this.order = kwargs.order || null;
this.view_ids = kwargs.view_ids;
this.views_preload = views_preload;
this.sel_multi = kwargs.sel_multi;
this.callback = callback;
var title = kwargs.title;
if (!title) {
title = Sao.common.MODELNAME.get(model);
}
this.title = title;
this.exclude_field = kwargs.exclude_field || null;
var dialog = new Sao.Dialog(Sao.i18n.gettext(
'Search %1', this.title), '', 'lg', false);
this.el = dialog.modal;
this.el.on('keydown', e => {
if (e.which == Sao.common.ESC_KEYCODE) {
e.preventDefault();
this.response('RESPONSE_CANCEL');
}
});
jQuery('
', {
'class': 'btn btn-link',
'type': 'button',
'title': Sao.i18n.gettext("Cancel"),
}).text(Sao.i18n.gettext('Cancel')).click(() => {
this.response('RESPONSE_CANCEL');
}).appendTo(dialog.footer);
jQuery('
', {
'class': 'btn btn-default',
'type': 'button',
'title': Sao.i18n.gettext("Find"),
}).text(Sao.i18n.gettext('Find')).click(() => {
this.response('RESPONSE_APPLY');
}).appendTo(dialog.footer);
if (kwargs.new_ && Sao.common.MODELACCESS.get(model).create) {
jQuery('
', {
'class': 'btn btn-default',
'type': 'button',
'title': Sao.i18n.gettext("New"),
'id': 'new_',
}).text(Sao.i18n.gettext('New')).click(() => {
this.response('RESPONSE_ACCEPT');
}).appendTo(dialog.footer);
}
jQuery('
', {
'class': 'btn btn-primary',
'type': 'submit',
'title': Sao.i18n.gettext("OK"),
}).text(Sao.i18n.gettext('OK')).appendTo(dialog.footer);
dialog.content.submit(e => {
e.preventDefault();
this.response('RESPONSE_OK');
});
this.screen = new Sao.Screen(model, {
mode: ['tree'],
context: this.context,
domain: this.domain,
order: this.order,
view_ids: kwargs.view_ids,
views_preload: views_preload,
row_activate: this.activate.bind(this),
readonly: true,
breadcrumb: [this.title],
});
this.screen.load_next_view().done(() => {
this.screen.switch_view().done(() => {
if (!this.sel_multi) {
this.screen.current_view.selection_mode = (
Sao.common.SELECTION_SINGLE);
} else {
this.screen.current_view.selection_mode = (
Sao.common.SELECTION_MULTIPLE);
}
dialog.body.append(this.screen.screen_container.el);
this.el.modal('show');
this.screen.display();
if (kwargs.search_filter !== undefined) {
this.screen.search_filter(kwargs.search_filter);
}
});
});
this.el.on('shown.bs.modal', () => {
this.screen.screen_container.search_entry.focus();
});
this.el.on('hidden.bs.modal', function(event) {
jQuery(this).remove();
});
},
activate: function() {
this.response('RESPONSE_OK');
},
response: function(response_id) {
var records;
var value = [];
if (response_id == 'RESPONSE_OK') {
records = this.screen.current_view.selected_records;
} else if (response_id == 'RESPONSE_APPLY') {
this.screen.search_filter(
this.screen.screen_container.get_text());
return;
} else if (response_id == 'RESPONSE_ACCEPT') {
var view_ids = jQuery.extend([], this.view_ids);
if (!jQuery.isEmptyObject(view_ids)) {
// Remove the first tree view as mode is form only
view_ids.shift();
}
var screen = new Sao.Screen(this.model_name, {
domain: this.domain,
context: this.context,
order: this.order,
mode: ['form'],
view_ids: view_ids,
views_preload: this.views_preload,
exclude_field: this.exclude_field,
});
var callback = function(result) {
if (result) {
var record = screen.current_record;
this.callback([[record.id,
record._values.rec_name || '']]);
} else {
this.callback(null);
}
};
this.el.modal('hide');
new Sao.Window.Form(screen, callback.bind(this), {
new_: true,
save_current: true,
});
return;
}
if (records) {
var index, record;
for (index in records) {
record = records[index];
value.push([record.id, record._values.rec_name || '']);
}
}
this.callback(value);
this.el.modal('hide');
}
});
Sao.Window.Preferences = Sao.class_(Object, {
init: function(callback) {
var dialog = new Sao.Dialog('Preferences', '', 'lg');
this.el = dialog.modal;
jQuery('
', {
'class': 'btn btn-link',
'type': 'button',
'title': Sao.i18n.gettext("Cancel"),
}).text(Sao.i18n.gettext('Cancel')).click(() => {
this.response('RESPONSE_CANCEL');
}).appendTo(dialog.footer);
jQuery('
', {
'class': 'btn btn-primary',
'type': 'submit',
'title': Sao.i18n.gettext("OK"),
}).text(Sao.i18n.gettext('OK')).appendTo(dialog.footer);
dialog.content.submit(e => {
e.preventDefault();
this.response('RESPONSE_OK');
});
this.screen = new Sao.Screen('res.user', {
mode: []
});
// Reset readonly set automaticly by MODELACCESS
this.screen.attributes.readonly = false;
this.screen.group.readonly = false;
this.screen.group.skip_model_access = true;
const set_view = view => {
this.screen.add_view(view);
this.screen.switch_view().done(() => {
this.screen.new_(false);
this.screen.model.execute('get_preferences', [false], {})
.then(set_preferences, this.destroy);
});
};
const set_preferences = preferences => {
this.screen.current_record.cancel();
this.screen.current_record.set(preferences);
this.screen.current_record.id =
this.screen.model.session.user_id;
this.screen.current_record.validate(null, true);
this.screen.display(true);
dialog.body.append(this.screen.screen_container.el);
this.el.modal('show');
};
this.el.on('hidden.bs.modal', function(event) {
callback();
jQuery(this).remove();
});
this.screen.model.execute('get_preferences_fields_view', [], {})
.then(set_view, this.destroy);
},
response: function(response_id) {
if (response_id == 'RESPONSE_OK') {
if (this.screen.current_record.validate()) {
var values = jQuery.extend({}, this.screen.get());
return this.screen.model.execute(
'set_preferences', [values], {})
.then(() => this.destroy());
}
return;
}
this.destroy();
},
destroy: function() {
this.el.modal('hide');
}
});
Sao.Window.Revision = Sao.class_(Object, {
init: function(revisions, revision, callback) {
this.callback = callback;
var dialog = new Sao.Dialog(
Sao.i18n.gettext('Revision'), '', 'lg');
this.el = dialog.modal;
jQuery('
', {
'class': 'btn btn-link',
'type': 'button',
'title': Sao.i18n.gettext("Cancel"),
}).text(Sao.i18n.gettext('Cancel')).click(() => {
this.response('RESPONSE_CANCEL');
}).appendTo(dialog.footer);
jQuery('
', {
'class': 'btn btn-primary',
'type': 'submit',
'title': Sao.i18n.gettext("OK"),
}).text(Sao.i18n.gettext('OK')).appendTo(dialog.footer);
dialog.content.submit(e => {
e.preventDefault();
this.response('RESPONSE_OK');
});
var group = jQuery('
', {
'class': 'form-group'
}).appendTo(dialog.body);
jQuery('
', {
'for': 'revision',
'text': 'Revision'
}).appendTo(group);
this.select = jQuery('
', {
'class': 'form-control',
id: 'revision',
'placeholder': Sao.i18n.gettext('Revision')
}).appendTo(group);
var date_format = Sao.common.date_format();
var time_format = '%H:%M:%S.%f';
this.select.append(jQuery('
', {
value: null,
text: ''
}));
for (let rev of revisions) {
var name = rev[2];
rev = rev[0];
this.select.append(jQuery('
', {
value: rev.valueOf(),
text: Sao.common.format_datetime(
date_format + ' ' + time_format, rev) + ' ' + name,
}));
}
if (revision) {
this.select.val(revision.valueOf());
}
this.el.modal('show');
this.el.on('hidden.bs.modal', function(event) {
jQuery(this).remove();
});
},
response: function(response_id) {
var revision = null;
if (response_id == 'RESPONSE_OK') {
revision = this.select.val();
if (revision) {
revision = Sao.DateTime(parseInt(revision, 10));
}
}
this.el.modal('hide');
this.callback(revision);
}
});
Sao.Window.CSV = Sao.class_(Object, {
init: function(title) {
this.languages = Sao.rpc({
'method': 'model.ir.lang.search_read',
'params': [
[['translatable', '=', true]],
0, null, null, ['code', 'name'],
{},
],
}, this.session, false);
this.dialog = new Sao.Dialog(title, 'csv', 'lg');
this.el = this.dialog.modal;
this.fields = {};
this.fields_model = {};
var row_fields = jQuery('
', {
'class': 'row'
}).appendTo(this.dialog.body);
var column_fields_all = jQuery('
', {
'class': 'col-md-5',
}).append(jQuery('
', {
'class': 'panel panel-default',
}).append(jQuery('
', {
'class': 'panel-heading',
}).append(jQuery('
', {
'class': 'panel-title',
'text': Sao.i18n.gettext('All Fields')
})))).appendTo(row_fields);
this.fields_all = jQuery('
', {
'class': 'list-unstyled column-fields panel-body'
}).css('cursor', 'pointer')
.appendTo(column_fields_all.find('.panel'));
this.model_populate(this._get_fields(this.screen.model_name));
this.view_populate(this.fields_model, this.fields_all);
this.column_buttons = jQuery('
', {
'class': 'col-md-2'
}).appendTo(row_fields);
jQuery('
', {
'class': 'btn btn-default btn-block',
'type': 'button',
'title': Sao.i18n.gettext("Add"),
}).text(' ' + Sao.i18n.gettext('Add')).prepend(
Sao.common.ICONFACTORY.get_icon_img('tryton-add')
).click(() => {
this.fields_all.find('.bg-primary').each((i, el_field) => {
this.sig_sel_add(el_field);
});
})
.appendTo(this.column_buttons);
jQuery('
', {
'class': 'btn btn-default btn-block',
'type': 'button',
'title': Sao.i18n.gettext("Remove"),
}).text(' ' + Sao.i18n.gettext('Remove')).prepend(
Sao.common.ICONFACTORY.get_icon_img('tryton-remove')
).click(() => {
// sig_unsel
this.fields_selected.children('li.bg-primary').remove();
})
.appendTo(this.column_buttons);
jQuery('
', {
'class': 'btn btn-default btn-block',
'type': 'button',
'title': Sao.i18n.gettext("Clear"),
}).text(' ' + Sao.i18n.gettext('Clear')).prepend(
Sao.common.ICONFACTORY.get_icon_img('tryton-clear')
).click(() => {
this.fields_selected.empty();
})
.appendTo(this.column_buttons);
jQuery('
').appendTo(this.column_buttons);
var column_fields_selected = jQuery('
', {
'class': 'col-md-5',
}).append(jQuery('
', {
'class': 'panel panel-default',
}).append(jQuery('
', {
'class': 'panel-heading',
}).append(jQuery('
', {
'class': 'panel-title',
'text': Sao.i18n.gettext('Fields Selected')
})))).appendTo(row_fields);
this.fields_selected = jQuery('
', {
'class': 'list-unstyled column-fields panel-body',
}).css('cursor', 'pointer')
.appendTo(column_fields_selected.find('.panel'));
this.chooser_form = jQuery('
', {
'class': 'form-inline'
}).appendTo(this.dialog.body);
var row_csv_param = jQuery('
', {
}).appendTo(this.dialog.body);
jQuery('
', {
'text': Sao.i18n.gettext('CSV Parameters')
}).append(jQuery('
', {
'class': 'caret',
}).html(' '))
.css('cursor', 'pointer')
.on('click', () => {
this.expander_csv.collapse('toggle');
}).appendTo(row_csv_param);
this.expander_csv = jQuery('
', {
'id': 'expander_csv',
'class': 'collapse form-inline'
}).appendTo(row_csv_param);
var delimiter_label = jQuery('
', {
'text': Sao.i18n.gettext('Delimiter:'),
'class': 'control-label',
'for': 'input-delimiter'
});
var separator = ',';
if (navigator.platform &&
navigator.platform.slice(0, 3) == 'Win') {
separator = ';';
}
this.el_csv_delimiter = jQuery('
', {
'type': 'text',
'class': 'form-control',
'id': 'input-delimiter',
'size': '1',
'maxlength': '1',
'value': separator
});
jQuery('
', {
'class': 'form-group'
}).append(delimiter_label)
.append(this.el_csv_delimiter)
.appendTo(this.expander_csv);
this.expander_csv.append(' ');
var quotechar_label = jQuery('
', {
'text': Sao.i18n.gettext('Quote Char:'),
'class': 'control-label',
'for': 'input-quotechar'
});
this.el_csv_quotechar = jQuery('
', {
'type': 'text',
'class': 'form-control',
'id': 'input-quotechar',
'size': '1',
'maxlength': '1',
'value': '"',
});
jQuery('
', {
'class': 'form-group'
}).append(quotechar_label)
.append(this.el_csv_quotechar)
.appendTo(this.expander_csv);
this.expander_csv.append(' ');
this.el.modal('show');
this.el.on('hidden.bs.modal', function() {
jQuery(this).remove();
});
},
_get_fields: function(model) {
return Sao.rpc({
'method': 'model.' + model + '.fields_get',
'params': [this.screen.context],
}, this.session, false);
},
on_row_expanded: function(node) {
var container_view = jQuery('
').css('list-style', 'none')
.insertAfter(node.view);
this.children_expand(node);
this.view_populate(node.children, container_view);
},
destroy: function() {
this.el.modal('hide');
}
});
Sao.Window.Import = Sao.class_(Sao.Window.CSV, {
init: function(name, screen) {
this.name = name;
this.screen = screen;
this.session = Sao.Session.current_session;
this.fields_data = {}; // Ask before Removing this.
this.fields_invert = {};
Sao.Window.Import._super.init.call(this,
Sao.i18n.gettext('CSV Import: %1', name));
jQuery('
', {
'class': 'btn btn-link',
'type': 'button',
'title': Sao.i18n.gettext("Cancel"),
}).text(Sao.i18n.gettext("Cancel")).on('click', (evt) => {
this.response(evt, 'RESPONSE_CANCEL');
}).appendTo(this.dialog.footer);
jQuery('
', {
'class': 'btn btn-primary',
'type': 'submit',
'title': Sao.i18n.gettext("Import"),
}).text(Sao.i18n.gettext("Import")).on('click', (evt) => {
this.response(evt, 'RESPONSE_OK');
}).appendTo(this.dialog.footer);
jQuery('
', {
'class': 'btn btn-default btn-block',
'type': 'button',
'title': Sao.i18n.gettext("Auto-Detect"),
}).text(' ' + Sao.i18n.gettext('Auto-Detect')).prepend(
Sao.common.ICONFACTORY.get_icon_img('tryton-search')
).click(() => {
this.autodetect();
})
.appendTo(this.column_buttons);
var chooser_label = jQuery('
', {
'text': Sao.i18n.gettext('File to Import'),
'class': 'col-sm-6 control-label',
'for': 'input-csv-file'
});
this.file_input = jQuery('
', {
'type': 'file',
'id': 'input-csv-file'
});
jQuery('
', {
'class': 'form-group'
}).append(chooser_label).append(jQuery('
', {
'class': 'col-sm-6'
}).append(this.file_input))
.appendTo(this.chooser_form);
var encoding_label = jQuery('
', {
'text': Sao.i18n.gettext('Encoding:'),
'class': 'control-label',
'for': 'input-encoding'
});
this.el_csv_encoding = jQuery('
', {
'class': 'form-control',
'id': 'input-encoding'
});
for (const encoding of ENCODINGS) {
jQuery('
', {
'val': encoding,
}).append(encoding).appendTo(this.el_csv_encoding);
}
var enc = 'utf-8';
if (navigator.platform &&
navigator.platform.slice(0, 3) == 'Win') {
enc = 'cp1252';
}
this.el_csv_encoding.children('option[value="' + enc + '"]')
.attr('selected', 'selected');
jQuery('
', {
'class': 'form-group'
}).append(encoding_label)
.append(this.el_csv_encoding)
.appendTo(this.expander_csv);
this.expander_csv.append(' ');
var skip_label = jQuery('
', {
'text': Sao.i18n.gettext('Lines to Skip:'),
'class': 'control-label',
'for': 'input-skip'
});
this.el_csv_skip = jQuery('
', {
'type': 'number',
'class': 'form-control',
'id': 'input-skip',
'value': '0'
});
jQuery('
', {
'class': 'form-group'
}).append(skip_label)
.append(this.el_csv_skip)
.appendTo(this.expander_csv);
this.expander_csv.append(' ');
Sortable.create(this.fields_selected.get(0), {
handle: '.draggable-handle',
ghostClass: 'dragged-row'
});
},
sig_sel_add: function(el_field) {
el_field = jQuery(el_field);
this._add_node(el_field.attr('field'), el_field.attr('name'));
},
_add_node: function(field, name) {
jQuery('
', {
'field': field,
'class': 'draggable-handle',
}).text(name).prepend(
Sao.common.ICONFACTORY.get_icon_img('tryton-drag')
).click(function(evt) {
const node = jQuery(evt.target);
if (evt.ctrlKey || evt.metaKey) {
node.toggleClass('bg-primary');
} else {
node.addClass('bg-primary');
node.siblings().removeClass('bg-primary');
}
}).appendTo(this.fields_selected);
},
view_populate: function (parent_node, parent_view) {
var fields_order = Object.keys(parent_node).sort(function(a,b) {
if (parent_node[b].string < parent_node[a].string) {
return -1;
}
else {
return 1;
}
}).reverse();
fields_order.forEach(field => {
var name = parent_node[field].string || field;
var node = jQuery('
', {
'field': parent_node[field].field,
'name': parent_node[field].name
}).text(name).click(e => {
if (e.ctrlKey || e.metaKey) {
node.toggleClass('bg-primary');
} else {
this.fields_all.find('li').removeClass('bg-primary');
node.addClass('bg-primary');
}
}).appendTo(parent_view);
parent_node[field].view = node;
var expander_icon = Sao.common.ICONFACTORY
.get_icon_img('tryton-arrow-right')
.data('expanded', false)
.click(e => {
e.stopPropagation();
var icon;
var expanded = expander_icon.data('expanded');
expander_icon.data('expanded', !expanded);
if (expanded) {
icon = 'tryton-arrow-right';
node.next('ul').remove();
} else {
icon = 'tryton-arrow-down';
this.on_row_expanded(parent_node[field]);
}
Sao.common.ICONFACTORY.get_icon_url(icon)
.then(function(url) {
expander_icon.attr('src', url);
});
}).prependTo(node);
expander_icon.css(
'visibility',
parent_node[field].children ? 'visible' : 'hidden');
});
},
model_populate: function (fields, parent_node, prefix_field,
prefix_name) {
parent_node = parent_node || this.fields_model;
prefix_field = prefix_field || '';
prefix_name = prefix_name || '';
for (const field_name of Object.keys(fields)) {
const field = fields[field_name];
if(!field.readonly || field_name == 'id') {
var name = field.string || field_name;
name = prefix_name + name;
// Only One2Many can be nested for import
var relation;
if (field.type == 'one2many') {
relation = field.relation;
} else {
relation = null;
}
var node = {
name: name,
field: prefix_field + field_name,
relation: relation,
string: field.string
};
parent_node[field_name] = node;
this.fields[prefix_field + field_name] = node;
this.fields_invert[name] = prefix_field + field_name;
if (relation) {
node.children = {};
} else if (field.translate) {
node.children = {};
for (const language of this.languages) {
const l_field_name = (
`${field_name}:lang=${language.code}`);
const l_name = (
prefix_name + name + ` (${language.name})`);
const l_node = {
name: l_name,
field: prefix_field + l_field_name,
relation: null,
string: language.name,
};
node.children[l_field_name] = l_node;
this.fields[prefix_field + l_field_name] = l_node;
this.fields_invert[l_name] = (
prefix_field + l_field_name);
}
}
}
}
},
children_expand: function(node) {
if (jQuery.isEmptyObject(node.children) && node.relation) {
this.model_populate(
this._get_fields(node.relation), node.children,
node.field + '/', node.name + '/');
}
},
autodetect: function() {
var fname = this.file_input.val();
if(!fname) {
Sao.common.message.run(
Sao.i18n.gettext('You must select an import file first.'));
return;
}
this.fields_selected.empty();
this.el_csv_skip.val(1);
Papa.parse(this.file_input[0].files[0], {
delimiter: this.el_csv_delimiter.val(),
quoteChar: this.el_csv_quotechar.val(),
preview: 1,
encoding: this.el_csv_encoding.val(),
error: (err, file, inputElem, reason) => {
Sao.common.warning.run(
reason,
Sao.i18n.gettext("Detection failed"));
},
complete: results => {
results.data[0].every(word => {
if (!(word in this.fields_invert) && !(word in this.fields)) {
var fields = this.fields_model;
var prefix = '';
var parents = word.split('/').slice(0, -1);
this._traverse(fields, prefix, parents, 0);
}
return this._auto_select(word);
});
}
});
},
_auto_select: function(word) {
var name,field;
if(word in this.fields_invert) {
name = word;
field = this.fields_invert[word];
}
else if (word in this.fields) {
name = this.fields[word].name;
field = [word];
}
else {
Sao.common.warning.run(
Sao.i18n.gettext('Unknown column header "%1"', word),
Sao.i18n.gettext('Error'));
return false;
}
this._add_node(field, name);
return true;
},
_traverse: function(fields, prefix, parents, i) {
var field, item;
var names = Object.keys(fields);
for (item = 0; item
{
fields.push(field_el.getAttribute('field'));
});
var fname = this.file_input.val();
if(fname) {
this.import_csv(fname, fields).then(() => {
this.destroy();
}, () => {
button.prop('disabled', false);
});
} else {
this.destroy();
}
}
else {
this.destroy();
}
},
import_csv: function(fname, fields) {
var skip = this.el_csv_skip.val();
var encoding = this.el_csv_encoding.val();
var prm = jQuery.Deferred();
Papa.parse(this.file_input[0].files[0], {
delimiter: this.el_csv_delimiter.val(),
quoteChar: this.el_csv_quotechar.val(),
encoding: encoding,
error: (err, file, inputElem, reason) => {
Sao.common.warning.run(
reason,
Sao.i18n.gettext("Import failed"))
.always(prm.reject);
},
complete: results => {
var data = results.data.slice(skip, results.data.length - 1);
Sao.rpc({
'method': 'model.' + this.screen.model_name +
'.import_data',
'params': [fields, data, this.screen.context]
}, this.session).then(count => {
return Sao.common.message.run(
Sao.i18n.ngettext('%1 record imported',
'%1 records imported', count, count));
}).then(prm.resolve, prm.reject);
}
});
return prm.promise();
}
});
Sao.Window.Export = Sao.class_(Sao.Window.CSV, {
init: function(name, screen, names) {
this.name = name;
this.screen = screen;
this.session = Sao.Session.current_session;
Sao.Window.Export._super.init.call(this,
Sao.i18n.gettext('CSV Export: %1',name));
jQuery('', {
'class': 'btn btn-link',
'type': 'button',
'title': Sao.i18n.gettext("Close"),
}).text(Sao.i18n.gettext("Close")).on('click', (evt) => {
this.response(evt, 'RESPONSE_CLOSE');
}).appendTo(this.dialog.footer);
jQuery('', {
'class': 'btn btn-primary',
'type': 'submit',
'title': Sao.i18n.gettext("Save As..."),
}).text(Sao.i18n.gettext("Save As...")).on('click', (evt) => {
this.response(evt, 'RESPONSE_OK');
}).appendTo(this.dialog.footer);
this.info_bar = new Sao.Window.InfoBar();
this.dialog.body.append(this.info_bar.el);
var fields = this.screen.model.fields;
for (const name of names) {
var type = fields[name].description.type;
if (type == 'selection') {
this.sel_field(name + '.translated');
} else if (type == 'reference') {
this.sel_field(name + '.translated');
this.sel_field(name + '/rec_name');
} else {
this.sel_field(name);
}
}
this.predef_exports = {};
this.fill_predefwin();
jQuery('', {
'class': 'btn btn-default btn-block',
'type': 'button',
'title': Sao.i18n.gettext("Save Export"),
}).text(' ' + Sao.i18n.gettext('Save Export')).prepend(
Sao.common.ICONFACTORY.get_icon_img('tryton-save')
).click(() => {
this.addreplace_predef();
})
.appendTo(this.column_buttons);
this.button_url = jQuery('', {
'class': 'btn btn-default btn-block',
'target': '_blank',
'rel': 'noreferrer noopener',
'title': Sao.i18n.gettext("URL Export"),
}).text(' ' + Sao.i18n.gettext("URL Export")).prepend(
Sao.common.ICONFACTORY.get_icon_img('tryton-public')
)
.appendTo(this.column_buttons);
this.dialog.body.on('change click', this.set_url.bind(this));
jQuery('', {
'class': 'btn btn-default btn-block',
'type': 'button',
'title': Sao.i18n.gettext("Delete Export"),
}).text(' ' + Sao.i18n.gettext('Delete Export')).prepend(
Sao.common.ICONFACTORY.get_icon_img('tryton-delete')
).click(() => {
this.remove_predef();
})
.appendTo(this.column_buttons);
var predefined_exports_column = jQuery('', {
'class': 'panel panel-default',
}).append(jQuery('', {
'class': 'panel-heading',
}).append(jQuery('', {
'class': 'panel-title',
'text': Sao.i18n.gettext('Predefined Exports')
}))).appendTo(this.column_buttons);
this.predef_exports_list = jQuery('', {
'class': 'list-unstyled predef-exports panel-body'
}).css('cursor', 'pointer')
.appendTo(predefined_exports_column);
var selected = !this.screen_is_tree && this.screen_has_selected
this.selected_records = jQuery('', {
'class': 'form-control',
'id': 'input-records',
}).append(jQuery('', {
'val': true,
'selected': selected,
}).text(Sao.i18n.gettext("Selected Records")))
.append(jQuery('', {
'val': false,
'selected': !selected,
}).text(Sao.i18n.gettext("Listed Records")));
this.ignore_search_limit = jQuery('', {
'type': 'checkbox',
});
this.selected_records.change(() => {
this.ignore_search_limit.parents('.form-group').first().toggle(
!JSON.parse(this.selected_records.val()) &&
!this.screen_is_tree);
});
jQuery('', {
'class': 'form-group',
}).appendTo(this.chooser_form)
.append(jQuery('', {
'text': Sao.i18n.gettext("Export:"),
'class': 'control-label',
'for': 'input-records',
})).append(this.selected_records);
jQuery('', {
'class': 'form-group',
}).appendTo(this.chooser_form)
.append(jQuery('', {
'class': 'checkbox',
}).append(jQuery('', {
'text': ' ' + Sao.i18n.gettext("Ignore search limit")
}).prepend(this.ignore_search_limit)))
.toggle(!selected && !this.screen_is_tree);
this.el_csv_locale = jQuery('', {
'type': 'checkbox',
'checked': 'checked',
});
jQuery('', {
'class': 'checkbox',
}).append(jQuery('', {
'text': ' ' + Sao.i18n.gettext("Use locale format"),
}).prepend(this.el_csv_locale)).appendTo(this.expander_csv);
this.expander_csv.append(' ');
this.el_add_field_names = jQuery('', {
'type': 'checkbox',
'checked': 'checked'
});
jQuery('', {
'class': 'checkbox',
}).append(jQuery('', {
'text': ' '+Sao.i18n.gettext('Add Field Names')
}).prepend(this.el_add_field_names)).appendTo(this.expander_csv);
this.expander_csv.append(' ');
this.set_url();
Sortable.create(this.fields_selected.get(0), {
handle: '.draggable-handle',
ghostClass: 'dragged-row'
});
},
get context() {
return this.screen.context;
},
get screen_is_tree() {
return Boolean(
this.screen.current_view &&
(this.screen.current_view.view_type == 'tree') &&
this.screen.current_view.children_field);
},
get screen_has_selected() {
return Boolean(this.screen.selected_records.length);
},
view_populate: function(parent_node, parent_view) {
var names = Object.keys(parent_node).sort(function(a, b) {
if (parent_node[b].string < parent_node[a].string) {
return -1;
}
else {
return 1;
}
}).reverse();
names.forEach(name => {
var path = parent_node[name].path;
var node = jQuery('', {
'path': path
}).text(parent_node[name].string).click(e => {
if (e.ctrlKey || e.metaKey) {
node.toggleClass('bg-primary');
} else {
this.fields_all.find('li')
.removeClass('bg-primary');
node.addClass('bg-primary');
}
}).appendTo(parent_view);
parent_node[name].view = node;
var expander_icon = Sao.common.ICONFACTORY
.get_icon_img('tryton-arrow-right')
.data('expanded', false)
.click(e => {
e.stopPropagation();
var icon;
var expanded = expander_icon.data('expanded');
expander_icon.data('expanded', !expanded);
if (expanded) {
icon = 'tryton-arrow-right';
node.next('ul').remove();
} else {
icon = 'tryton-arrow-down';
this.on_row_expanded(parent_node[name]);
}
Sao.common.ICONFACTORY.get_icon_url(icon)
.then(function(url) {
expander_icon.attr('src', url);
});
}).prependTo(node);
expander_icon.css(
'visibility',
parent_node[name].children ? 'visible' : 'hidden');
});
},
model_populate: function(fields, parent_node, prefix_field,
prefix_name) {
parent_node = parent_node || this.fields_model;
prefix_field = prefix_field || '';
prefix_name = prefix_name || '';
for (const name of Object.keys(fields)) {
var field = fields[name];
var string = field.string || name;
var items = [{ name: name, field: field, string: string }];
if (field.type == 'selection') {
items.push({
name: name+'.translated',
field: field,
string: Sao.i18n.gettext('%1 (string)', string)
});
} else if (field.type == 'reference') {
items.push({
name: name + '.translated',
field: field,
string: Sao.i18n.gettext("%1 (model name)", string),
});
items.push({
name: name + '/rec_name',
field: field,
string: Sao.i18n.gettext("%1/Record Name", string),
});
}
for (const item of items) {
var path = prefix_field + item.name;
var long_string = prefix_name + item.string;
var node = {
path: path,
string: item.string,
long_string: long_string,
relation: item.field.relation
};
parent_node[item.name] = node;
this.fields[path] = node;
// Insert relation only to real field
if (item.name.indexOf('.') == -1) {
if (item.field.relation) {
node.children = {};
} else if (item.field.translate) {
node.children = {};
for (const language of this.languages) {
const l_path = `${path}:lang=${language.code}`;
const l_string = (
`${long_string} (${language.name})`);
const l_node = {
path: l_path,
string: language.name,
long_string: l_string,
relation: null,
};
node.children[language.code] = l_node;
this.fields[l_path] = l_node;
}
}
}
}
}
},
children_expand: function(node) {
if (jQuery.isEmptyObject(node.children) && node.relation) {
this.model_populate(
this._get_fields(node.relation), node.children,
node.path + '/', node.long_string + '/');
}
},
sig_sel_add: function(el_field) {
el_field = jQuery(el_field);
var name = el_field.attr('path');
this.sel_field(name);
},
fill_predefwin: function() {
Sao.rpc({
'method': 'model.ir.export.get',
'params': [
this.screen.model_name, [
'name', 'header', 'records', 'ignore_search_limit',
'export_fields.name'],
this.context,
],
}, this.session).done(exports => {
for (const export_ of exports) {
this.predef_exports[export_.id] = {
'fields': export_['export_fields.'].map(
field => field.name),
'values': export_,
};
this.add_to_predef(export_.id, export_.name);
this.predef_exports_list.children('li').first().focus();
}
});
},
add_to_predef: function(id, name) {
var node = jQuery('', {
'text': name,
'export_id': id,
'tabindex': 0
}).on('keypress', function(e) {
var keyCode = (e.keyCode ? e.keyCode : e.which);
if(keyCode == 13 || keyCode == 32) {
node.click();
}
}).click(event => {
node.toggleClass('bg-primary')
.siblings().removeClass('bg-primary');
if (node.hasClass('bg-primary')) {
this.sel_predef(node.attr('export_id'));
}
});
this.predef_exports_list.append(node);
},
addreplace_predef: function() {
var fields = [];
var selected_fields = this.fields_selected.children('li');
for (const field of selected_fields) {
fields.push(field.getAttribute('path'));
}
if(fields.length === 0) {
return;
}
var pref_id;
const save = name => {
var prm;
var values = {
'header': this.el_add_field_names.is(':checked'),
'records': (
JSON.parse(this.selected_records.val()) ?
'selected' : 'listed'),
'ignore_search_limit': this.ignore_search_limit.is(
':checked'),
};
if (!pref_id) {
values.name = name;
values.resource = this.screen.model_name;
values.export_fields = fields.map(f => ({'name': f}));
prm = Sao.rpc({
method: 'model.ir.export.set',
params: [values, this.context],
}, this.session);
} else {
prm = Sao.rpc({
method: 'model.ir.export.update',
params: [pref_id, values, fields, this.context],
}, this.session).then(() => pref_id);
}
return prm.then(pref_id => {
this.session.cache.clear(
'model.' + this.screen.model_name + '.view_toolbar_get');
this.predef_exports[pref_id] = {
'fields': fields,
'values': values,
};
if (selection.length === 0) {
this.add_to_predef(pref_id, name);
}
});
};
var selection = this.predef_exports_list.children('li.bg-primary');
if (selection.length === 0) {
pref_id = null;
Sao.common.ask.run(
Sao.i18n.gettext('What is the name of this export?'),
'export')
.then(save);
}
else {
pref_id = selection.attr('export_id');
Sao.common.sur.run(
Sao.i18n.gettext(
'Override %1 definition?', selection.text()))
.then(save);
}
},
remove_predef: function() {
var selection = this.predef_exports_list.children('li.bg-primary');
if (selection.length === 0) {
return;
}
var export_id = jQuery(selection).attr('export_id');
Sao.rpc({
'method': 'model.ir.export.unset',
'params': [export_id, this.context]
}, this.session).then(() => {
this.session.cache.clear(
'model.' + this.screen.model_name + '.view_toolbar_get');
delete this.predef_exports[export_id];
selection.remove();
});
},
sel_predef: function(export_id) {
this.fields_selected.empty();
const export_ = this.predef_exports[export_id];
for (const name of export_.fields) {
if (!(name in this.fields)) {
var fields = this.fields_model;
var prefix = '';
var parents = name.split('/').slice(0, -1);
this._traverse(fields, prefix, parents, 0);
}
if(!(name in this.fields)) {
return;
}
this.sel_field(name);
}
this.el_add_field_names.prop('checked', export_.values.header);
this.selected_records.val(
JSON.stringify(export_.values.records == 'selected'));
this.selected_records.change();
this.ignore_search_limit.prop(
'checked', export_.values.ignore_search_limit);
},
_traverse: function(fields, prefix, parents, i) {
var field, item;
var names = Object.keys(fields);
for (item = 0; item < names.length; item++) {
field = fields[names[item]];
if (field.path == (prefix + parents[i])) {
this.children_expand(field);
prefix += parents[i] + '/';
if (field.children) {
this._traverse(field.children, prefix, parents, ++i);
}
break;
}
}
},
sel_field: function(name) {
var long_string = this.fields[name].long_string;
var node = jQuery('', {
'path': name,
'class': 'draggable-handle',
}).text(long_string).click(function(e) {
if (e.ctrlKey || e.metaKey) {
node.toggleClass('bg-primary');
} else {
jQuery(e.target).addClass('bg-primary')
.siblings().removeClass('bg-primary');
}
}).prepend(
Sao.common.ICONFACTORY.get_icon_img('tryton-drag')
).appendTo(this.fields_selected);
},
response: function(evt, response_id) {
evt.preventDefault();
let button = jQuery(evt.currentTarget);
button.prop('disabled', true);
this.info_bar.clear();
if(response_id == 'RESPONSE_OK') {
var fields = [];
this.fields_selected.children('li').each(function(i, field) {
fields.push(field.getAttribute('path'));
});
var header = this.el_add_field_names.is(':checked');
var prm, ids, paths;
if (JSON.parse(this.selected_records.val())) {
ids = this.screen.selected_records.map(function(r) {
return r.id;
});
paths = this.screen.selected_paths;
prm = Sao.rpc({
'method': (
'model.' + this.screen.model_name +
'.export_data'),
'params': [ids, fields, header, this.context]
}, this.session);
} else if (this.screen_is_tree) {
ids = this.screen.listed_records.map(function(r) {
return r.id;
});
paths = this.screen.listed_paths;
prm = Sao.rpc({
'method': (
'model.' + this.screen.model_name +
'.export_data'),
'params': [ids, fields, header, this.context]
}, this.session);
} else {
var domain = this.screen.search_domain(
this.screen.screen_container.get_text());
var offset, limit;
if (this.ignore_search_limit.prop('checked')) {
offset = 0;
limit = null;
} else {
offset = this.screen.offset;
limit = this.screen.limit;
}
prm = Sao.rpc({
'method': (
'model.' + this.screen.model_name +
'.export_data_domain'),
'params': [
domain, fields, offset, limit, this.screen.order,
header, this.context],
}, this.session);
}
prm.then(data => {
this.export_csv(data, paths, header);
}).always(() => {
button.prop('disabled', false);
});
} else {
this.destroy();
}
},
export_csv: function(data, paths, header=false) {
var locale_format = this.el_csv_locale.prop('checked');
var unparse_obj = {};
unparse_obj.data = data.map(function(row, i) {
var indent = paths && paths[i] ? paths[i].length -1 : 0;
return Sao.Window.Export.format_row(
row, indent, locale_format);
});
var csv = Papa.unparse(unparse_obj, {
quoteChar: this.el_csv_quotechar.val(),
delimiter: this.el_csv_delimiter.val()
});
if (navigator.platform &&
navigator.platform.slice(0, 3) == 'Win') {
csv = Sao.BOM_UTF8 + csv;
}
Sao.common.download_file(
csv, this.name + '.csv', {type: 'text/csv;charset=utf-8'});
var size = data.length;
if (header) {
size -= 1;
}
this.info_bar.add(
Sao.i18n.ngettext(
"%1 record saved", "%1 records saved", size, size), 'info');
},
set_url: function() {
var path = [this.session.database, 'data', this.screen.model_name];
var query_string = [];
var domain;
if (JSON.parse(this.selected_records.val())) {
domain = this.screen.current_view.selected_records.map(function(r) {
return r.id;
});
} else {
domain = this.screen.search_domain(
this.screen.screen_container.get_text());
if (!this.ignore_search_limit.prop('checked') &&
this.screen.limit !== null) {
query_string.push(['s', this.screen.limit.toString()]);
query_string.push(
['p', Math.floor(
this.screen.offset / this.screen.limit).toString()]);
}
if (this.screen.order) {
for (const expr of this.screen.order) {
query_string.push(['o', expr.map(function(e) {
return e;
}).join(',')]);
}
}
}
query_string.splice(
0, 0, ['d', JSON.stringify(Sao.rpc.prepareObject(domain))]);
if (!jQuery.isEmptyObject(this.screen.local_context)) {
query_string.push(
['c', JSON.stringify(Sao.rpc.prepareObject(
this.screen.local_context))]);
}
this.fields_selected.children('li').each(function(i, field) {
query_string.push(['f', field.getAttribute('path')]);
});
query_string.push(['dl', this.el_csv_delimiter.val()]);
query_string.push(['qc', this.el_csv_quotechar.val()]);
if (!this.el_add_field_names.is(':checked')) {
query_string.push(['h', '0']);
}
if (this.el_csv_locale.prop('checked')) {
query_string.push(['loc', '1']);
}
query_string = query_string.map(function(param) {
return param.map(encodeURIComponent).join('=');
}).join('&');
this.button_url.attr('href', '/' + path.join('/') + '?' + query_string);
},
});
Sao.Window.Export.format_row = function(
line, indent=0, locale_format=true) {
var row = [];
line.forEach(function(val, i) {
if (locale_format) {
if (val.isDateTime) {
val = val.local().format(
Sao.common.date_format() + ' ' +
Sao.common.moment_format('%X'));
} else if (val.isDate) {
val = val.format(Sao.common.date_format());
} else if (val.isTimeDelta) {
val = Sao.common.timedelta.format(
val, {'s': 1, 'm': 60, 'h': 60 * 60});
} else if (!isNaN(Number(val))) {
val = val.toLocaleString(
Sao.i18n.BC47(Sao.i18n.getlang()), {
'minimumFractionDigits': 0,
'maximumFractionDigits': 20,
});
}
} else if (val.isDateTime) {
val = val.utc();
} else if (val.isTimeDelta) {
val = val.asSeconds();
} else if (typeof(val) == 'boolean') {
val += 0;
}
if ((i === 0) && indent && (typeof(val) == 'string')) {
val = ' '.repeat(indent) + val;
}
if (val instanceof Uint8Array) {
val = Sao.common.btoa(val);
}
row.push(val);
});
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)
.fail(function() {
Sao.Logger.warn("Unable to complete email entry");
});
},
_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('Email %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('', {
'class': 'form-group',
}).appendTo(body);
jQuery('', {
'class': 'control-label col-sm-1',
'text': label,
'for': 'email-' + name,
}).appendTo(group);
var input = jQuery('', {
'type': 'text',
'class':'form-control input-sm',
'id': 'email-' + name,
}).appendTo(jQuery('', {
'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:'));
for (const input of [this.to, this.cc, this.bcc]) {
new Sao.Window.EmailEntry(input, this.record.model.session);
}
this.subject = add_group(
'subject', Sao.i18n.gettext('Subject:'), true);
var panel = jQuery('', {
'class': 'panel panel-default',
}).appendTo(body
).append(jQuery('', {
'class': 'panel-heading',
}).append(Sao.common.richtext_toolbar()));
this.body = jQuery('', {
'class': 'email-richtext form-control input-sm mousetrap',
'contenteditable': true,
'spellcheck': true,
'id': 'email-body',
}).appendTo(jQuery('
', {
'class': 'panel-body',
}).appendTo(panel));
var print_frame = jQuery('
', {
'class': 'col-md-4',
}).appendTo(body);
jQuery('
', {
'text': Sao.i18n.gettext("Reports"),
}).appendTo(print_frame);
this.print_actions = {};
for (const print of prints) {
var print_check = jQuery('
', {
'type': 'checkbox',
});
jQuery('
', {
'class': 'checkbox',
}).append(jQuery('
'
).text(Sao.i18n.gettext(print.name)
).prepend(print_check)).appendTo(print_frame);
this.print_actions[print.id] = print_check;
}
var attachment_frame = jQuery('
', {
'class': 'col-md-4',
}).appendTo(body);
jQuery('
', {
'text': Sao.i18n.gettext("Attachments"),
}).appendTo(attachment_frame);
this.attachments = jQuery('
', {
'class': 'form-control input-sm',
'name': 'attachments',
'multiple': true,
}).appendTo(attachment_frame);
Sao.rpc({
'method': 'model.ir.attachment.search_read',
'params': [
[
['resource', '=', record.model.name + ',' + record.id],
['OR',
['data', '!=', null],
['file_id', '!=', null],
],
],
0, null, null, ['rec_name'], record.get_context()],
}, record.model.session).then(attachments => {
for (const attachment of attachments) {
this.attachments.append(jQuery('
', {
'value': JSON.stringify(attachment.id),
'text': attachment.rec_name,
}));
}
})
.fail(function() {
Sao.Logger.error(
"Could not fetch attachment for", record);
});
this.files = jQuery('
', {
'class': 'col-md-4',
}).appendTo(body);
jQuery('
', {
'text': Sao.i18n.gettext("Files"),
}).appendTo(this.files);
this._add_file_button();
jQuery('
', {
'class': 'btn btn-link',
'type': 'button',
'title': Sao.i18n.gettext("Cancel"),
}).text(' ' + Sao.i18n.gettext('Cancel')).prepend(
Sao.common.ICONFACTORY.get_icon_img('tryton-cancel')
).click(() => {
this.response('RESPONSE_CANCEL');
}).appendTo(this.dialog.footer);
jQuery('
', {
'class': 'btn btn-primary',
'type': 'submit',
'title': Sao.i18n.gettext("Send"),
}).text(' ' + Sao.i18n.gettext('Send')).prepend(
Sao.common.ICONFACTORY.get_icon_img('tryton-send')
).appendTo(this.dialog.footer);
this.dialog.content.submit(e => {
e.preventDefault();
this.response('RESPONSE_OK');
});
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('
').appendTo(this.files);
var file = jQuery('
', {
'type': 'file',
}).appendTo(row);
var button = jQuery('
', {
'class': 'close',
'title': Sao.i18n.gettext("Remove File"),
}).append(jQuery('
', {
'aria-hidden': true,
'text': 'x',
})).append(jQuery('
', {
'class': 'sr-only',
}).text(Sao.i18n.gettext("Remove")));
button.hide();
button.appendTo(row);
file.on('change', () => {
button.show();
this._add_file_button();
});
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;
});
},
get_attachments: function() {
var attachments = this.attachments.val();
if (attachments) {
return attachments.map(function(e) { return JSON.parse(e); });
}
return [];
},
_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(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)));
}
});
},
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 attachments = this.get_attachments();
var record = this.record;
this.get_files().then(function(files) {
return Sao.rpc({
'method': 'model.ir.email.send',
'params': [
to, cc, bcc, subject, body,
files,
[record.model.name, record.id],
reports,
attachments,
{}],
}, record.model.session);
}).then(() => {
this.destroy();
});
} else {
this.destroy();
}
},
destroy: function() {
this.el.modal('hide');
},
});
Sao.Window.CodeScanner = Sao.class_(Object, {
init: function(callback, loop=false) {
this.callback = callback;
this.loop = loop;
this.submitting = false;
this.dialog = new Sao.Dialog(
Sao.i18n.gettext("Code Scanner"), 'code-scanner', 'md');
this.el = this.dialog.modal;
this.input = jQuery('
', {
'type': 'text',
'class': 'form-control input-sm mousetrap',
'aria-label': Sao.i18n.gettext("Code"),
'placeholder': Sao.i18n.gettext("Code"),
}).appendTo(jQuery('
', {
'class': 'col-sm-12',
}).appendTo(this.dialog.body));
var sound_btn = jQuery('
', {
'class': 'btn btn-default pull-right',
'type': 'button',
'title': Sao.i18n.gettext("Toggle Sound"),
}).prependTo(this.dialog.header);
var sound_icon = jQuery('
![]()
', {
'class': 'icon',
}).appendTo(sound_btn);
var sound_set_state = function(state) {
var prm;
if (state) {
sound_btn.addClass('active');
sound_btn.attr('aria-pressed', state);
prm = Sao.common.ICONFACTORY.get_icon_url('tryton-sound-on')
.done(url => {
sound_icon.attr('src', url);
});
} else {
sound_btn.removeClass('active');
sound_btn.attr('aria-pressed', state);
prm = Sao.common.ICONFACTORY.get_icon_url('tryton-sound-off')
.done(url => {
sound_icon.attr('src', url);
});
}
localStorage.setItem('sao_code_scanner_sound', state);
return prm;
};
var sound_state = JSON.parse(
localStorage.getItem('sao_code_scanner_sound'));
if (sound_state === null) {
sound_state = true;
}
var sound_prm = sound_set_state(sound_state);
sound_btn.click(
() => sound_set_state(!sound_btn.hasClass('active')));
jQuery('
', {
'class': 'btn btn-link',
'type': 'button',
'title': Sao.i18n.gettext("Close"),
}).text(' ' + Sao.i18n.gettext("Close")).prepend(
Sao.common.ICONFACTORY.get_icon_img('tryton-close')
).click(() => {
this.response('RESPONSE_CLOSE');
}).appendTo(this.dialog.footer);
jQuery('
', {
'class': 'btn btn-primary',
'type': 'submit',
'title': Sao.i18n.gettext("OK"),
}).text(' ' + Sao.i18n.gettext("OK")).prepend(
Sao.common.ICONFACTORY.get_icon_img('tryton-ok')
).appendTo(this.dialog.footer);
this.dialog.content.submit(e => {
e.preventDefault();
this.response('RESPONSE_OK');
});
this.el.on('shown.bs.modal', () => {
this.input.on('blur', this._keep_focus);
this.input.focus();
});
this.el.on('hidden.bs.modal', function() {
jQuery(this).remove();
});
// show modal after sound icons have been set
// because they can trigger a renew session modal
sound_prm.then(() => this.el.modal('show'));
},
_play_sound: function(sound) {
if (JSON.parse(localStorage.getItem('sao_code_scanner_sound'))) {
Sao.common.play_sound(sound);
}
},
_keep_focus: function() {
setTimeout(() => this.focus(), 1);
},
response: function(response_id) {
if (this.submitting) return;
if (response_id == 'RESPONSE_OK') {
var code = this.input.val();
this.input.val(''); // clear input to prevent multiple calls
if (code) {
this.submitting = true;
this.input.off('blur');
return this.callback(code)
.always(() => this.submitting = false)
.then((modified) => {
this._play_sound('success');
if (!this.loop || !modified) {
this.destroy();
}
this.input.on('blur', this._keep_focus);
this.input.focus();
}, (error) => {
this._play_sound('danger');
if (error[0] == 'UserError') {
Sao.common.warning.run(error[1][1], error[1][0]);
} else {
Sao.common.error.run(error[0], error[1]);
}
this.destroy();
});
}
}
if (!this.loop || response_id != 'RESPONSE_OK') {
this.destroy();
}
},
destroy: function() {
this.el.modal('hide');
},
});
}());
/* This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. */
(function() {
'use strict';
Sao.Wizard = Sao.class_(Object, {
init: function(name) {
this.widget = jQuery('
', {
'class': 'wizard'
});
this.name = name || Sao.i18n.gettext('Wizard');
this.action_id = null;
this.id = null;
this.ids = null;
this.action = null;
this.context = null;
this.states = {};
this.session_id = null;
this.start_state = null;
this.end_state = null;
this.screen = null;
this.screen_state = null;
this.state = null;
this.session = Sao.Session.current_session;
this.__prm = jQuery.Deferred();
this.__processing = false;
this.__waiting_response = false;
this.info_bar = new Sao.Window.InfoBar();
},
run: function(attributes) {
this.action = attributes.action;
this.action_id = attributes.data.action_id;
this.id = attributes.data.id;
this.ids = attributes.data.ids;
this.model = attributes.data.model;
this.context = jQuery.extend({}, attributes.context);
this.context.active_id = this.id;
this.context.active_ids = this.ids;
this.context.active_model = this.model;
this.context.action_id = this.action_id;
Sao.rpc({
'method': 'wizard.' + this.action + '.create',
'params': [this.session.context]
}, this.session).then(result => {
this.session_id = result[0];
this.start_state = this.state = result[1];
this.end_state = result[2];
return this.process();
}, () => {
this.destroy();
});
return this.__prm.promise();
},
process: function() {
if (this.__processing || this.__waiting_response) {
return jQuery.when();
}
var process = function() {
if (this.state == this.end_state) {
return this.end();
}
var ctx = jQuery.extend({}, this.context);
var data = {};
if (this.screen) {
data[this.screen_state] = this.screen.get_on_change_value();
}
return Sao.rpc({
'method': 'wizard.' + this.action + '.execute',
'params': [this.session_id, data, this.state, ctx]
}, this.session).then(result => {
var prms = [];
if (result.view) {
this.clean();
var view = result.view;
this.update(view.fields_view, view.buttons);
prms.push(this.screen.new_(false).then(() => {
this.screen.current_record.set_default(
view.defaults || {})
.then(() => {
this.screen.current_record.set(
view.values || {});
this.update_buttons();
this.screen.set_cursor();
});
}));
this.screen_state = view.state;
this.__waiting_response = true;
} else {
this.state = this.end_state;
}
const execute_actions = () => {
var prms = [];
if (result.actions) {
for (const action of result.actions) {
var context = jQuery.extend({}, this.context);
// Remove wizard keys added by run
delete context.active_id;
delete context.active_ids;
delete context.active_model;
delete context.action_id;
prms.push(Sao.Action.execute(
action[0], action[1], context));
}
}
return jQuery.when.apply(jQuery, prms);
};
if (this.state == this.end_state) {
prms.push(this.end().then(execute_actions));
} else {
prms.push(execute_actions());
}
this.__processing = false;
return jQuery.when.apply(jQuery, prms);
}, result => {
if (!result || !this.screen) {
this.state = this.end_state;
this.end();
}
this.__processing = false;
});
};
return process.call(this);
},
destroy: function(action) {
// TODO
},
end: function() {
return Sao.rpc({
'method': 'wizard.' + this.action + '.delete',
'params': [this.session_id, this.session.context]
}, this.session).then(action => {
this.destroy(action);
this.__prm.resolve();
})
.fail(() => {
Sao.Logger.warn(
"Unable to delete session %s of wizard %s",
this.session_id, this.action);
this.__prm.reject();
});
},
clean: function() {
this.widget.empty();
this.states = {};
},
response: function(definition) {
this.__waiting_response = false;
this.screen.current_view.set_value();
if (definition.validate && !this.screen.current_record.validate(
null, null, null)) {
this.screen.display(true);
this.info_bar.add(this.screen.invalid_message(), 'danger');
return;
}
this.info_bar.clear();
this.state = definition.state;
this.process();
},
_get_button: function(definition) {
var style = 'btn-default';
if (definition.default) {
style = 'btn-primary';
} else if (definition.state == this.end_state) {
style = 'btn-link';
}
var button = new Sao.common.Button(
definition, undefined, undefined, style);
this.states[definition.state] = button;
return button;
},
record_modified: function() {
this.update_buttons();
this.info_bar.refresh();
},
update_buttons: function() {
var record = this.screen.current_record;
for (var state in this.states) {
var button = this.states[state];
button.set_state(record);
}
},
update: function(view, buttons) {
for (const button of buttons) {
this._get_button(button);
}
if (this.screen) {
this.screen.windows.splice(
this.screen.windows.indexOf(this), 1);
}
this.screen = new Sao.Screen(view.model,
{mode: [], context: this.context});
this.screen.add_view(view);
this.screen.switch_view();
this.screen.windows.push(this);
this.header.append(jQuery('
', {
'class': 'model-title',
'title': this.name,
}).text(Sao.common.ellipsize(this.name, 80)));
this.widget.append(this.screen.screen_container.el);
}
});
Sao.Wizard.create = function(attributes) {
var win;
if (attributes.window) {
win = new Sao.Wizard.Form(attributes.name);
var tab = new Sao.Tab.Wizard(win);
Sao.Tab.add(tab);
} else {
win = new Sao.Wizard.Dialog(attributes.name);
}
return win.run(attributes);
};
Sao.Wizard.Form = Sao.class_(Sao.Wizard, {
init: function(name) {
Sao.Wizard.Form._super.init.call(this, name);
this.tab = null; // Filled by Sao.Tab.Wizard
this.header = jQuery('
', {
'class': 'modal-header',
});
this.form = jQuery('
', {
'class': 'wizard-form',
}).append(this.header).append(this.widget);
this.footer = jQuery('
', {
'class': 'modal-footer'
}).appendTo(this.form);
},
clean: function() {
Sao.Wizard.Form._super.clean.call(this);
this.header.empty();
this.footer.empty();
},
_get_button: function(definition) {
var button = Sao.Wizard.Form._super._get_button.call(this,
definition);
this.footer.append(button.el);
button.el.click(() => {
this.response(definition);
});
return button;
},
destroy: function(action) {
Sao.Wizard.Form._super.destroy.call(this, action);
switch (action) {
case 'reload menu':
Sao.Session.current_session.reload_context()
.then(function() {
Sao.menu();
});
break;
case 'reload context':
Sao.Session.current_session.reload_context();
break;
}
},
end: function() {
return Sao.Wizard.Form._super.end.call(this).always(
() => this.tab.close());
}
});
Sao.Wizard.Dialog = Sao.class_(Sao.Wizard, { // TODO nomodal
init: function(name) {
Sao.Wizard.Dialog._super.init.call(this, name);
var dialog = new Sao.Dialog(name, 'wizard-dialog', 'md', false);
this.dialog = dialog.modal;
this.header = dialog.header;
this.content = dialog.content;
this.footer = dialog.footer;
this.dialog.on('keydown', e => {
if (e.which == Sao.common.ESC_KEYCODE) {
e.preventDefault();
if (this.end_state in this.states) {
this.response(this.states[this.end_state].attributes);
}
}
});
dialog.body.append(this.widget).append(this.info_bar.el);
},
clean: function() {
Sao.Wizard.Dialog._super.clean.call(this);
this.header.empty();
this.footer.empty();
},
_get_button: function(definition) {
var button = Sao.Wizard.Dialog._super._get_button.call(this,
definition);
this.footer.append(button.el);
if (definition['default']) {
this.content.unbind('submit');
this.content.submit(e => {
this.response(definition);
e.preventDefault();
});
button.el.attr('type', 'submit');
} else {
button.el.click(() => {
this.response(definition);
});
}
return button;
},
update: function(view, buttons) {
this.content.unbind('submit');
Sao.Wizard.Dialog._super.update.call(this, view, buttons);
this.show();
},
destroy: function(action) {
Sao.Wizard.Dialog._super.destroy.call(this, action);
const destroy = () => {
this.dialog.remove();
var dialog = jQuery('.wizard-dialog').filter(':visible')[0];
var is_menu = false;
var screen;
if (!dialog) {
dialog = Sao.Tab.tabs.get_current();
}
if (!dialog ||
!this.model ||
(Sao.main_menu_screen &&
(Sao.main_menu_screen.model_name == this.model))) {
is_menu = true;
screen = Sao.main_menu_screen;
} else {
screen = dialog.screen;
}
if (screen) {
var prm = jQuery.when();
if (screen.current_record && !is_menu) {
var ids;
if (screen.model_name == this.model) {
ids = this.ids;
} else {
// Wizard run form a children record so reload
// parent record
ids = [screen.current_record.id];
}
prm = screen.reload(ids, true);
}
if (action) {
prm.then(function() {
screen.client_action(action);
});
}
}
};
if ((this.dialog.data('bs.modal') || {}).isShown) {
this.dialog.on('hidden.bs.modal', destroy);
this.dialog.modal('hide');
} else {
destroy();
}
},
show: function() {
var view = this.screen.current_view;
var expand;
if (view.view_type == 'form') {
expand = false;
var fields = view.get_fields();
for (const name of fields) {
var widgets = view.widgets[name];
for (const widget of widgets) {
if (widget.expand) {
expand = true;
break;
}
}
if (expand) {
break;
}
}
} else {
expand = true;
}
if (expand) {
this.dialog.find('.modal-dialog')
.removeClass('modal-md modal-sm')
.addClass('modal-lg');
} else {
this.dialog.find('.modal-dialog')
.removeClass('modal-lg modal-sm')
.addClass('modal-md');
}
this.dialog.modal('show');
},
hide: function() {
this.dialog.modal('hide');
},
state_changed: function() {
this.process();
}
});
}());
/* This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. */
(function() {
'use strict';
Sao.View.BoardXMLViewParser = Sao.class_(Sao.View.FormXMLViewParser, {
_parse_board: function(node, attributes) {
var container = new Sao.View.Form.Container(
Number(node.getAttribute('col') || 4));
this.view.containers.push(container);
this.view.el.append(container.el);
this.parse_child(node, container);
if (this._containers.length > 0) {
throw 'AssertionError';
}
},
_parse_action: function(node, attributes) {
var action;
if (attributes.yexpand === undefined) {
attributes.yexpand = true;
}
if (attributes.yfill === undefined) {
attributes.yfill = true;
}
action = new Sao.View.Board.Action(this.view, attributes);
this.view.actions.push(action);
this.container.add(action, attributes);
},
});
Sao.View.Board = Sao.class_(Object, {
xml_parser: Sao.View.BoardXMLViewParser,
init: function(xml, context) {
this.context = context;
this.actions = [];
this.containers = [];
this.state_widgets = [];
this.el = jQuery('
', {
'class': 'board'
});
new this.xml_parser(this, null, {}).parse(xml.children()[0]);
var actions_prms = [];
for (const action of this.actions) {
actions_prms.push(action.action_prm);
}
this.actions_prms = jQuery.when.apply(jQuery, actions_prms);
},
reload: function() {
var i;
for (i = 0; i < this.actions.length; i++) {
this.actions[i].display();
}
var promesses = [];
for (let state_widget of this.state_widgets.toReversed()) {
var prm = state_widget.set_state(null);
if (prm) {
promesses.push(prm);
}
}
for (const container of this.containers) {
container.set_grid_template();
}
// re-set the grid templates for the StateWidget that are
// asynchronous
jQuery.when.apply(jQuery, promesses).done(() => {
for (const container of this.containers) {
container.set_grid_template();
}
});
},
active_changed: function(event_action) {
for (const action of this.actions) {
if (action !== event_action) {
action.update_domain(this.actions);
}
}
},
});
Sao.View.Board.Action = Sao.class_(Object, {
init: function(view, attributes) {
var session = Sao.Session.current_session;
this.name = attributes.name;
this.view = view;
this.el = jQuery('
', {
'class': 'board-action panel panel-default',
});
this.title = jQuery('
', {
'class': 'panel-heading',
});
this.el.append(this.title);
this.body = jQuery('
', {
'class': 'panel-body',
});
this.el.append(this.body);
var act_window = new Sao.Model('ir.action.act_window');
this.action_prm = act_window.execute('get', [this.name], {});
this.action_prm.done(action => {
var params = {};
this.action = action;
params.view_ids = [];
params.mode = null;
if (!jQuery.isEmptyObject(action.views)) {
params.view_ids = [];
params.mode = [];
for (const view of action.views) {
params.view_ids.push(view[0]);
params.mode.push(view[1]);
}
} else if (!jQuery.isEmptyObject(action.view_id)) {
params.view_ids = [action.view_id[0]];
}
if (!('pyson_domain' in this.action)) {
this.action.pyson_domain = '[]';
}
var ctx = {};
ctx = jQuery.extend(ctx, session.context);
ctx._user = session.user_id;
var decoder = new Sao.PYSON.Decoder(ctx);
params.context = jQuery.extend(
{}, this.view.context,
decoder.decode(action.pyson_context || '{}'));
ctx = jQuery.extend(ctx, params.context);
ctx.context = ctx;
decoder = new Sao.PYSON.Decoder(ctx);
params.order = decoder.decode(action.pyson_order);
params.search_value = decoder.decode(
action.pyson_search_value || '[]');
params.tab_domain = [];
for (const element of action.domains) {
params.tab_domain.push(
[element[0], decoder.decode(element[1]), element[2]]);
}
params.context_model = action.context_model;
params.context_domain = action.context_domain;
if (action.limit !== null) {
params.limit = action.limit;
} else {
params.limit = Sao.config.limit;
}
this.context = ctx;
this.domain = [];
this.update_domain([]);
params.row_activate = this.row_activate.bind(this);
this.screen = new Sao.Screen(this.action.res_model,
params);
if (attributes.string) {
this.title.text(attributes.string);
} else {
this.title.text(this.action.name);
}
this.screen.switch_view().done(() => {
this.body.append(this.screen.screen_container.el);
this.screen.search_filter();
});
});
},
row_activate: function() {
if (!this.screen.current_record) {
return;
}
if (this.screen.current_view.view_type == 'tree' &&
(this.screen.current_view.attributes.keyword_open == 1)) {
const record_ids = this.screen.current_view.selected_records.map(
function(record) { return record.id; });
Sao.Action.exec_keyword('tree_open', {
model: this.screen.model_name,
id: this.screen.current_record.id,
ids: record_ids
}, jQuery.extend({}, this.screen.group._context), false);
} else {
new Sao.Window.Form(this.screen, result => {
if (result) {
this.screen.current_record.save();
} else {
this.screen.current_record.cancel();
}
}, {
'title': this.title.text(),
});
}
},
display: function() {
this.screen.search_filter(this.screen.screen_container.get_text());
},
record_message: function() {
this.view.active_changed(this);
},
get active() {
if (this.screen && this.screen.current_record) {
return {
'active_id': this.screen.current_record.id,
'active_ids': this.screen.selected_records.map(
function(r) {
return r.id;
}),
};
} else {
return {
'active_id': null,
'active_ids': [],
};
}
},
update_domain: function(actions) {
const domain_ctx = jQuery.extend({}, this.context);
domain_ctx._actions = {};
for (var i = 0, len = actions.length; i < len; i++) {
domain_ctx._actions[actions[i].name] = actions[i].active;
}
const decoder = new Sao.PYSON.Decoder(domain_ctx);
const new_domain = decoder.decode(this.action.pyson_domain);
if (Sao.common.compare(this.domain, new_domain)) {
return;
}
this.domain.splice(0, this.domain.length);
jQuery.extend(this.domain, new_domain);
if (this.screen) {
this.display();
}
}
});
}());
/* This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. */
(function() {
'use strict';
Sao.Bus = {};
// Bus Identifier
Sao.Bus.id = Sao.common.uuid4();
Sao.Bus.channel_actions = {};
Sao.Bus.request = null;
Sao.Bus.last_message = undefined;
Sao.Bus.listening = false;
Sao.Bus.listen = function(last_message, wait) {
wait = wait || 1;
last_message = last_message || Sao.Bus.last_message;
var session = Sao.Session.current_session;
if (!session || !session.bus_url_host) {
return;
}
Sao.Bus.listening = true;
let channels = Object.keys(Sao.Bus.channel_actions);
let url = new URL(`${session.database}/bus`, session.bus_url_host);
Sao.Bus.last_message = last_message;
Sao.Bus.request = jQuery.ajax({
headers: {
Authorization: 'Session ' + session.get_auth(),
},
contentType: 'application/json',
data: JSON.stringify({
last_message: last_message,
channels: channels,
}),
dataType: 'json',
url: url,
type: 'POST',
timeout: Sao.config.bus_timeout,
});
Sao.Bus.request.done(function(response) {
if (Sao.Session.current_session != session) {
return;
}
if (response.message) {
last_message = response.message.message_id;
Sao.Logger.info(
"poll channels %s with last message",
Sao.Bus.channels, last_message);
Sao.Bus.handle(response.channel, response.message);
}
Sao.Bus.listen(last_message, 1);
});
Sao.Bus.request.fail(function(response, status, error) {
if (Sao.Session.current_session != session) {
return;
}
if ((error == "abort") || (error === "timeout")) {
Sao.Bus.listen(last_message, 1);
} else if (response.status == 501) {
Sao.Logger.info("Bus not supported");
Sao.Bus.listening = false;
} else {
Sao.Logger.error(
"An exception occured while connection to the bus. " +
"Sleeping for %s seconds",
wait, error);
Sao.Bus.listening = false;
window.setTimeout(
Sao.Bus.listen,
Math.min(wait * 1000, Sao.config.bus_timeout),
last_message, wait * 2);
}
});
};
Sao.Bus.handle = function(channel, message) {
let actions = Sao.Bus.channel_actions[channel] || [];
for (let callback of actions) {
callback(message);
}
};
Sao.Bus.register = function(channel, func) {
if (!Object.hasOwn(Sao.Bus.channel_actions, channel)) {
Sao.Bus.channel_actions[channel] = [];
}
Sao.Bus.channel_actions[channel].push(func);
if (Sao.Bus.request) {
Sao.Bus.request.abort();
}
};
Sao.Bus.unregister = function(channel, func) {
if (Object.hasOwn(Sao.Bus.channel_actions, channel)) {
let actions = Sao.Bus.channel_actions[channel];
let func_idx = actions.indexOf(func);
if (func_idx != -1) {
actions.splice(func_idx, 1);
}
}
if ((Sao.Bus.channel_actions[channel] || []).length == 0) {
delete Sao.Bus.channel_actions[channel];
}
}
let popup_notification = function(message) {
if (message.type != 'notification') {
return;
}
try {
if (Notification.permission != "granted") {
return;
}
} catch (e) {
Sao.Logger.error(e.message, e.stack);
return;
}
new Notification(message.title, {
body: message.body || '',
});
}
Sao.Bus.register(`client:${Sao.Bus.id}`, popup_notification);
}());
/* This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. */
(function() {
'use strict';
class _Chat {
constructor(record) {
this.notify = this.notify.bind(this);
this.record = record;
Sao.Bus.register(`chat:${this.record}`, this.notify);
this.el = this.__build();
}
unregister() {
Sao.Bus.unregister(`chat:${this.record}`, this.notify);
}
send_message(message, internal) {
return Sao.rpc({
'method': 'model.ir.chat.channel.post',
'params': [this.record, message, internal ? 'internal' : 'public', {}],
}, Sao.Session.current_session)
}
get_messages() {
return Sao.rpc({
'method': 'model.ir.chat.channel.get',
'params': [this.record, {}],
}, Sao.Session.current_session);
}
notify(message) {
this.refresh();
}
refresh() {
let prm = this.get_messages();
prm.done((posts) => {
this._messages.empty();
for (let post of posts) {
this._messages.append(this.create_message(post));
}
let messages = this._messages[this._messages.length - 1];
messages.scrollTop = messages.scrollHeight;
});
}
__build() {
let el = jQuery('
', {
'class': 'chat',
});
let btn_group = jQuery('
', {
'class': 'btn-group',
'role': 'group',
}).appendTo(el);
let subscribe_btn = jQuery('
', {
'class': 'btn btn-default pull-right',
'type': 'button',
'title': Sao.i18n.gettext("Toggle notification"),
}).append(Sao.common.ICONFACTORY.get_icon_img(
'tryton-notification'))
.appendTo(btn_group);
Sao.rpc({
'method': 'model.ir.chat.channel.get_followers',
'params': [this.record, {}],
}, Sao.Session.current_session).then((followers) => {
set_subscribe_state(~followers.users.indexOf(
Sao.Session.current_session.login));
})
let set_subscribe_state = (subscribed) => {
let img;
if (subscribed) {
img = 'tryton-notification-on';
subscribe_btn.addClass('active').off().click(unsubscribe);
} else {
img = 'tryton-notification-off';
subscribe_btn.removeClass('active').off().click(subscribe);
}
subscribe_btn.html(Sao.common.ICONFACTORY.get_icon_img(img));
}
let subscribe = () => {
let session = Sao.Session.current_session;
Sao.rpc({
'method': 'model.ir.chat.channel.subscribe',
'params': [this.record, {}],
}, session).then(() => {
set_subscribe_state(true);
})
};
let unsubscribe = () => {
let session = Sao.Session.current_session;
Sao.rpc({
'method': 'model.ir.chat.channel.unsubscribe',
'params': [this.record, {}],
}, session).then(() => {
set_subscribe_state(false);
})
};
this._messages = jQuery('
', {
'class': 'chat-messages',
}).appendTo(jQuery('
', {
'class': 'chat-messages-outer',
}).appendTo(el));
let input = jQuery('
', {
'class': 'input-sm form-control',
'placeholder': Sao.i18n.gettext("Enter a message"),
});
let submit = jQuery('
', {
'class': 'btn btn-block btn-default',
'type': 'submit',
'aria-label': Sao.i18n.gettext("Submit the message"),
'title': Sao.i18n.gettext("Send"),
'text': Sao.i18n.gettext("Send"),
});
let internal = jQuery('
', {
'type': 'checkbox',
});
jQuery('
').appendTo(el);
let form = jQuery('
')
.append(jQuery('
', {
'class': 'form-group',
}).append(input))
.append(jQuery('
', {
'class': 'checkbox',
}).append(jQuery('
')
.append(internal)
.append(Sao.i18n.gettext("Make this an internal message"))))
.append(submit)
.appendTo(el);
let send = (evt) => {
evt.preventDefault();
submit.prop('disabled', true);
input.prop('disabled', true);
this.send_message(
input.val(),
internal.prop('checked')
).done(() => {
input.val('');
submit.prop('disabled', false);
input.prop('disabled', false);
input.trigger('focus');
internal.prop('checked', false);
if (!Sao.Bus.listening) {
this.refresh();
}
});
};
form.submit(send);
input.keypress((evt) => {
if ((evt.which == Sao.common.RETURN_KEYCODE) && evt.ctrlKey) {
evt.preventDefault();
form.submit();
}
});
return el;
}
create_message(message) {
let avatar_size = 32;
let timestamp = Sao.common.format_datetime(
Sao.common.date_format() + ' %X', message.timestamp);
let avatar_url = '';
if (message.avatar_url) {
let url = new URL(message.avatar_url, window.location);
url.searchParams.set('s', avatar_size);
avatar_url = url.href;
}
return jQuery('
', {
'class': 'media chat-message',
}).append(jQuery('
', {
'class': 'media-left',
}).append(jQuery('
![]()
', {
'class': 'media-object img-circle chat-avatar',
'src': avatar_url,
'alt': message.author,
'style': `width: ${avatar_size}px; height: ${avatar_size}px;`,
}))).append(jQuery('
', {
'class': 'media-body well well-sm',
}).append(jQuery('
', {
'class': 'media-heading',
}).text(message.author)
.append(jQuery('
', {
'class': 'text-muted pull-right',
}).text(timestamp)))
.append(jQuery('
', {
'class': `chat-content chat-content-${message.audience}`,
}).text(message.content)));
}
}
Sao.Chat = _Chat;
}());
/* This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. */
(function() {
'use strict';
class _NotificationMenu {
constructor() {
this.notify = this.notify.bind(this);
this.el = jQuery('
', {
'class': 'notification-menu dropdown-menu',
'role': 'menu',
});
this.el.on('show.bs.dropdown', () => {
this.fill();
this.indicator.hide();
});
this.indicator = jQuery('
', {
'class': 'notification-badge',
});
let indicator_observer = new MutationObserver(() => {
let indicators = jQuery('.notification-badge').not(this.indicator);
indicators.text(this.indicator.text())
indicators.toggle(this.indicator.css('display') !== 'none');
});
indicator_observer.observe(
this.indicator.get(0), {
characterData: true,
attributes: true,
attributeFilter: ['style'],
});
jQuery(document).ready(() => {
// Wait for the DOM to be ready to let the observer find the
// other indicators
this.indicator.hide();
});
}
fill() {
let Notification = new Sao.Model('res.notification');
Notification.execute('get', []).done((notifications) => {
this.el.empty();
for (let notification of notifications) {
let notification_item = jQuery('
', {
}).append(jQuery('
', {
'class': 'notification-label',
'text': notification.label,
'title': notification.label,
})).append(jQuery('
', {
'class': 'notification-description',
'text': notification.description,
'title': notification.description,
}));
let link = jQuery('
', {
'role': 'menuitem',
'href': '#',
}).click((evt) => {
evt.preventDefault();
this.open(notification)
}).append(notification_item);
let li = jQuery('
', {
'class': 'notification-item',
'role': 'presentation',
});
let img = jQuery('
![]()
', {
'class': 'icon',
});
link.prepend(img);
Sao.common.ICONFACTORY.get_icon_url(
notification.icon || 'tryton-notification')
.then(url => {
img.attr('src', url);
// Append only when the url is known to prevent
// layout shifts
li.append(link);
});
if (notification.unread) {
li.addClass('notification-unread');
}
this.el.append(li)
}
this.el.append(
jQuery('
', {
'role': 'presentation',
'class': 'notification-item notification-action',
}).append(jQuery('
', {
'role': 'menuitem',
'href': '#',
'title': Sao.i18n.gettext("All Notifications..."),
}).append(jQuery('
', {
'class': 'caret',
})).click((evt) => {
evt.preventDefault();
let params = {
context: jQuery.extend({}, Sao.Session.current_session.context),
domain: [['user', '=', Sao.Session.current_session.user_id]],
};
params.model = 'res.notification';
Sao.Tab.create(params).done(() => {
this.indicator.hide();
});
}))
);
if (notifications.length > 0) {
this.el.append(
jQuery('
', {
'role': 'separator',
'class': 'divider',
}));
}
let preferences_img = jQuery('
![]()
', {
'class': 'icon',
});
let preferences = jQuery('
', {
'class': 'notification-item',
'role': 'presentation',
}).append(
jQuery('
', {
'role': 'menuitem',
'href': '#',
'text': Sao.i18n.gettext("Preferences..."),
}).prepend(preferences_img
).click((evt) => {
evt.preventDefault();
Sao.preferences();
}));
Sao.common.ICONFACTORY.get_icon_url('tryton-launch')
.then(url => {
preferences_img.attr('src', url);
});
this.el.append(preferences);
});
}
open(notification) {
let prms = [];
if (notification.model && notification.records) {
let params = {
context: jQuery.extend({}, Sao.Session.current_session.context),
domain: [['id', 'in', notification.records]],
};
if (notification.records.length == 1) {
params['res_id'] = notification.records[0];
params['mode'] = ['form', 'tree'];
}
params.model = notification.model;
prms.push(Sao.Tab.create(params));
}
if (notification.action) {
prms.push(Sao.Action.execute(notification.action));
}
jQuery.when.apply(jQuery, prms).done(() => {
if (notification.unread) {
let Notification = new Sao.Model('res.notification');
Notification.execute('mark_read', [[notification.id]]);
}
});
}
_update(count) {
if (count > 0) {
if (count < 10) {
this.indicator.text(count);
} else {
// Let's keep the text short
this.indicator.text('9+');
}
this.indicator.show();
} else {
this.indicator.hide();
}
}
notify(message) {
if (message.type == 'user-notification') {
this._update(message.count);
try {
if (Notification.permission == "granted") {
message.content.forEach((body) => {
new Notification(
Sao.config.title, {
'body': body,
});
});
}
} catch (e) {
Sao.Logger.error(e.message, e.stack);
}
}
}
count() {
let Notification = new Sao.Model('res.notification');
Notification.execute('get_count', [])
.done((count) => {
this._update(count);
});
}
}
Sao.NotificationMenu = new _NotificationMenu();
}());
/* This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. */
(function() {
'use strict';
var Translate = {};
Translate.translate_view = function(data) {
var model = data.model;
Sao.Tab.create({
model: 'ir.translation',
domain: [['model', '=', model]],
mode: ['tree', 'form'],
name: Sao.i18n.gettext('Translate view'),
});
};
Translate.get_plugins = function(model) {
var access = Sao.common.MODELACCESS.get('ir.translation');
if (access.read && access.write) {
return [
[Sao.i18n.gettext('Translate view'), Translate.translate_view],
];
} else {
return [];
}
};
Sao.Plugins.push(Translate);
}());
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
(function () {
'use strict';
var tag_whitelist = {
B: true,
BODY: true,
BR: true,
DIV: true,
FONT: true,
I: true,
U: true,
};
var attribute_whitelist = {
align: true,
color: true,
face: true,
size: true,
};
Sao.HtmlSanitizer = {};
Sao.HtmlSanitizer.sanitize = function(input) {
input = input.trim();
// to save performance and not create iframe
if (input == "") return "";
// firefox "bogus node" workaround
if (input == "
") return "";
var iframe = document.createElement('iframe');
if (iframe.sandbox === undefined) {
// Browser does not support sandboxed iframes
Sao.Logger.warn("Your browser do not support sandboxed iframes," +
" unable to sanitize HTML.");
return input;
}
iframe.sandbox = 'allow-same-origin';
iframe.style.display = 'none';
// necessary so the iframe contains a document
document.body.appendChild(iframe);
var iframedoc = (iframe.contentDocument ||
iframe.contentWindow.document);
// null in IE
if (iframedoc.body == null) {
iframedoc.write("");
}
iframedoc.body.innerHTML = input;
function make_sanitized_copy(node) {
var new_node;
if (node.nodeType == Node.TEXT_NODE) {
new_node = node.cloneNode(true);
} else if (node.nodeType == Node.ELEMENT_NODE &&
tag_whitelist[node.tagName]) {
//remove useless empty tags
if ((node.tagName != "BR") && node.innerHTML.trim() == "") {
return document.createDocumentFragment();
}
new_node = iframedoc.createElement(node.tagName);
for (var i = 0; i < node.attributes.length; i++) {
var attr = node.attributes[i];
if (attribute_whitelist[attr.name]) {
new_node.setAttribute(attr.name, attr.value);
}
}
for (i = 0; i < node.childNodes.length; i++) {
var sub_copy = make_sanitized_copy(node.childNodes[i]);
new_node.appendChild(sub_copy, false);
}
} else {
new_node = document.createDocumentFragment();
if (node.tagName != 'SCRIPT') {
new_node.textContent = node.textContent;
}
}
return new_node;
}
var result_element = make_sanitized_copy(iframedoc.body);
document.body.removeChild(iframe);
// replace is just for cleaner code
return result_element.innerHTML
.replace(/
]*>(\S)/g, "
\n$1")
.replace(/div>