1
openerp.web.form = function (instance) {
2
var _t = instance.web._t,
3
_lt = instance.web._lt;
4
var QWeb = instance.web.qweb;
7
instance.web.form = {};
10
* Interface implemented by the form view or any other object
11
* able to provide the features necessary for the fields to work.
14
* - display_invalid_fields : if true, all fields where is_valid() return true should
15
* be displayed as invalid.
16
* - actual_mode : the current mode of the field manager. Can be "view", "edit" or "create".
18
* - view_content_has_changed : when the values of the fields have changed. When
19
* this event is triggered all fields should reprocess their modifiers.
20
* - field_changed:<field_name> : when the value of a field change, an event is triggered
21
* named "field_changed:<field_name>" with <field_name> replaced by the name of the field.
22
* This event is not related to the on_change mechanism of OpenERP and is always called
23
* when the value of a field is setted or changed. This event is only triggered when the
24
* value of the field is syntactically valid, but it can be triggered when the value
25
* is sematically invalid (ie, when a required field is false). It is possible that an event
26
* about a precise field is never triggered even if that field exists in the view, in that
27
* case the value of the field is assumed to be false.
29
instance.web.form.FieldManagerMixin = {
31
* Must return the asked field as in fields_get.
33
get_field_desc: function(field_name) {},
35
* Returns the current value of a field present in the view. See the get_value() method
36
* method in FieldInterface for further information.
38
get_field_value: function(field_name) {},
40
Gives new values for the fields contained in the view. The new values could not be setted
41
right after the call to this method. Setting new values can trigger on_changes.
43
@param (dict) values A dictonnary with key = field name and value = new value.
44
@return (Deferred) Is resolved after all the values are setted.
46
set_values: function(values) {},
48
Computes an OpenERP domain.
50
@param (list) expression An OpenERP domain.
51
@return (boolean) The computed value of the domain.
53
compute_domain: function(expression) {},
55
Builds an evaluation context for the resolution of the fields' contexts. Please note
56
the field are only supposed to use this context to evualuate their own, they should not
59
@return (CompoundContext) An OpenERP context.
61
build_eval_context: function() {},
64
instance.web.views.add('form', 'instance.web.FormView');
67
* - actual_mode: always "view", "edit" or "create". Read-only property. Determines
68
* the mode used by the view.
70
instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerMixin, {
72
* Indicates that this view is not searchable, and thus that no search
73
* view should be displayed (if there is one active).
77
display_name: _lt('Form'),
80
* @constructs instance.web.FormView
81
* @extends instance.web.View
83
* @param {instance.web.Session} session the current openerp session
84
* @param {instance.web.DataSet} dataset the dataset this view will work with
85
* @param {String} view_id the identifier of the OpenERP view object
86
* @param {Object} options
87
* - resize_textareas : [true|false|max_height]
89
* @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance
91
init: function(parent, dataset, view_id, options) {
94
this.ViewManager = parent;
95
this.set_default_options(options);
96
this.dataset = dataset;
97
this.model = dataset.model;
98
this.view_id = view_id || false;
99
this.fields_view = {};
101
this.fields_order = [];
102
this.datarecord = {};
103
this.default_focus_field = null;
104
this.default_focus_button = null;
105
this.fields_registry = instance.web.form.widgets;
106
this.tags_registry = instance.web.form.tags;
107
this.widgets_registry = instance.web.form.custom_widgets;
108
this.has_been_loaded = $.Deferred();
109
this.translatable_fields = [];
110
_.defaults(this.options, {
111
"not_interactible_on_create": false,
112
"initial_mode": "view",
113
"disable_autofocus": false,
114
"footer_to_buttons": false,
116
this.is_initialized = $.Deferred();
117
this.mutating_mutex = new $.Mutex();
118
this.on_change_list = [];
120
this.reload_mutex = new $.Mutex();
121
this.__clicked_inside = false;
122
this.__blur_timeout = null;
123
this.rendering_engine = new instance.web.form.FormRenderingEngine(this);
124
self.set({actual_mode: self.options.initial_mode});
125
this.has_been_loaded.done(function() {
126
self.on("change:actual_mode", self, self.check_actual_mode);
127
self.check_actual_mode();
128
self.on("change:actual_mode", self, self.init_pager);
131
self.on("load_record", self, self.load_record);
132
instance.web.bus.on('clear_uncommitted_changes', this, function(e) {
133
if (!this.can_be_discarded()) {
138
view_loading: function(r) {
139
return this.load_form(r);
141
destroy: function() {
142
_.each(this.get_widgets(), function(w) {
143
w.off('focused blurred');
147
this.$el.off('.formBlur');
151
load_form: function(data) {
154
throw new Error(_t("No data provided."));
157
throw "Form view does not support multiple calls to load_form";
159
this.fields_order = [];
160
this.fields_view = data;
162
this.rendering_engine.set_fields_registry(this.fields_registry);
163
this.rendering_engine.set_tags_registry(this.tags_registry);
164
this.rendering_engine.set_widgets_registry(this.widgets_registry);
165
this.rendering_engine.set_fields_view(data);
166
var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container');
167
this.rendering_engine.render_to($dest);
169
this.$el.on('mousedown.formBlur', function () {
170
self.__clicked_inside = true;
173
this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self}));
174
if (this.options.$buttons) {
175
this.$buttons.appendTo(this.options.$buttons);
177
this.$el.find('.oe_form_buttons').replaceWith(this.$buttons);
179
this.$buttons.on('click', '.oe_form_button_create',
180
this.guard_active(this.on_button_create));
181
this.$buttons.on('click', '.oe_form_button_edit',
182
this.guard_active(this.on_button_edit));
183
this.$buttons.on('click', '.oe_form_button_save',
184
this.guard_active(this.on_button_save));
185
this.$buttons.on('click', '.oe_form_button_cancel',
186
this.guard_active(this.on_button_cancel));
187
if (this.options.footer_to_buttons) {
188
this.$el.find('footer').appendTo(this.$buttons);
191
this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar');
192
if (!this.sidebar && this.options.$sidebar) {
193
this.sidebar = new instance.web.Sidebar(this);
194
this.sidebar.appendTo(this.$sidebar);
195
if (this.fields_view.toolbar) {
196
this.sidebar.add_toolbar(this.fields_view.toolbar);
198
this.sidebar.add_items('other', _.compact([
199
self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete },
200
self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate }
204
this.has_been_loaded.resolve();
206
// Add bounce effect on button 'Edit' when click on readonly page view.
207
this.$el.find(".oe_form_group_row,.oe_form_field,label").on('click', function (e) {
208
if(self.get("actual_mode") == "view") {
209
var $button = self.options.$buttons.find(".oe_form_button_edit");
210
$button.openerpBounce();
212
instance.web.bus.trigger('click', e);
215
//bounce effect on red button when click on statusbar.
216
this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) {
217
if((self.get("actual_mode") == "view")) {
218
var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'});
219
$button.openerpBounce();
223
this.trigger('form_view_loaded', data);
226
widgetFocused: function() {
227
// Clear click flag if used to focus a widget
228
this.__clicked_inside = false;
229
if (this.__blur_timeout) {
230
clearTimeout(this.__blur_timeout);
231
this.__blur_timeout = null;
234
widgetBlurred: function() {
235
if (this.__clicked_inside) {
236
// clicked in an other section of the form (than the currently
237
// focused widget) => just ignore the blurring entirely?
238
this.__clicked_inside = false;
242
// clear timeout, if any
243
this.widgetFocused();
244
this.__blur_timeout = setTimeout(function () {
245
self.trigger('blurred');
249
do_load_state: function(state, warm) {
250
if (state.id && this.datarecord.id != state.id) {
251
if (this.dataset.get_id_index(state.id) === null) {
252
this.dataset.ids.push(state.id);
254
this.dataset.select_id(state.id);
255
this.do_show({ reload: warm });
260
* @param {Object} [options]
261
* @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view".
262
* @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record
263
* @return {$.Deferred}
265
do_show: function (options) {
267
options = options || {};
269
this.sidebar.$el.show();
272
this.$buttons.show();
274
this.$el.show().css({
276
filter: 'alpha(opacity = 0)'
278
this.$el.add(this.$buttons).removeClass('oe_form_dirty');
280
var shown = this.has_been_loaded;
281
if (options.reload !== false) {
282
shown = shown.then(function() {
283
if (self.dataset.index === null) {
284
// null index means we should start a new record
285
return self.on_button_new();
287
var fields = _.keys(self.fields_view.fields);
288
fields.push('display_name');
289
return self.dataset.read_index(fields, {
290
context: { 'bin_size': true, 'future_display_name' : true }
291
}).then(function(r) {
292
self.trigger('load_record', r);
296
return shown.then(function() {
297
self._actualize_mode(options.mode || self.options.initial_mode);
300
filter: 'alpha(opacity = 100)'
304
do_hide: function () {
306
this.sidebar.$el.hide();
309
this.$buttons.hide();
316
load_record: function(record) {
317
var self = this, set_values = [];
319
this.set({ 'title' : undefined });
320
this.do_warn(_t("Form"), _t("The record could not be found in the database."), true);
321
return $.Deferred().reject();
323
this.datarecord = record;
324
this._actualize_mode();
325
this.set({ 'title' : record.id ? record.display_name : _t("New") });
327
_(this.fields).each(function (field, f) {
328
field._dirty_flag = false;
329
field._inhibit_on_change_flag = true;
330
var result = field.set_value(self.datarecord[f] || false);
331
field._inhibit_on_change_flag = false;
332
set_values.push(result);
334
return $.when.apply(null, set_values).then(function() {
336
// New record: Second pass in order to trigger the onchanges
337
// respecting the fields order defined in the view
338
_.each(self.fields_order, function(field_name) {
339
if (record[field_name] !== undefined) {
340
var field = self.fields[field_name];
341
field._dirty_flag = true;
342
self.do_onchange(field);
346
self.on_form_changed();
347
self.rendering_engine.init_fields();
348
self.is_initialized.resolve();
349
self.do_update_pager(record.id == null);
351
self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
354
self.do_push_state({id:record.id});
356
self.do_push_state({});
358
self.$el.add(self.$buttons).removeClass('oe_form_dirty');
363
* Loads and sets up the default values for the model as the current
366
* @return {$.Deferred}
368
load_defaults: function () {
370
var keys = _.keys(this.fields_view.fields);
372
return this.dataset.default_get(keys).then(function(r) {
373
self.trigger('load_record', r);
376
return self.trigger('load_record', {});
378
on_form_changed: function() {
379
this.trigger("view_content_has_changed");
381
do_notify_change: function() {
382
this.$el.add(this.$buttons).addClass('oe_form_dirty');
384
execute_pager_action: function(action) {
385
if (this.can_be_discarded()) {
388
this.dataset.index = 0;
391
this.dataset.previous();
397
this.dataset.index = this.dataset.ids.length - 1;
401
this.trigger('pager_action_executed');
404
init_pager: function() {
407
this.$pager.remove();
408
if (this.get("actual_mode") === "create")
410
this.$pager = $(QWeb.render("FormView.pager", {'widget':self})).hide();
411
if (this.options.$pager) {
412
this.$pager.appendTo(this.options.$pager);
414
this.$el.find('.oe_form_pager').replaceWith(this.$pager);
416
this.$pager.on('click','a[data-pager-action]',function() {
417
var action = $(this).data('pager-action');
418
self.execute_pager_action(action);
420
this.do_update_pager();
422
do_update_pager: function(hide_index) {
423
this.$pager.toggle(this.dataset.ids.length > 1);
425
$(".oe_form_pager_state", this.$pager).html("");
427
$(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
430
parse_on_change: function (on_change, widget) {
432
var onchange = _.str.trim(on_change);
433
var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
435
throw new Error(_.str.sprintf( _t("Wrong on change format: %s"), onchange ));
438
var method = call[1];
439
if (!_.str.trim(call[2])) {
440
return {method: method, args: []}
443
var argument_replacement = {
444
'False': function () {return false;},
445
'True': function () {return true;},
446
'None': function () {return null;},
447
'context': function () {
448
return new instance.web.CompoundContext(
449
self.dataset.get_context(),
450
widget.build_context() ? widget.build_context() : {});
453
var parent_fields = null;
454
var args = _.map(call[2].split(','), function (a, i) {
455
var field = _.str.trim(a);
457
// literal constant or context
458
if (field in argument_replacement) {
459
return argument_replacement[field]();
462
if (/^-?\d+(\.\d+)?$/.test(field)) {
463
return Number(field);
466
if (self.fields[field]) {
467
var value_ = self.fields[field].get_value();
468
return value_ == null ? false : value_;
471
var splitted = field.split('.');
472
if (splitted.length > 1 && _.str.trim(splitted[0]) === "parent" && self.dataset.parent_view) {
473
if (parent_fields === null) {
474
parent_fields = self.dataset.parent_view.get_fields_values();
476
var p_val = parent_fields[_.str.trim(splitted[1])];
477
if (p_val !== undefined) {
478
return p_val == null ? false : p_val;
482
var first_char = field[0], last_char = field[field.length-1];
483
if ((first_char === '"' && last_char === '"')
484
|| (first_char === "'" && last_char === "'")) {
485
return field.slice(1, -1);
488
throw new Error("Could not get field with name '" + field +
489
"' for onchange '" + onchange + "'");
497
do_onchange: function(widget, processed) {
499
this.on_change_list = [{widget: widget, processed: processed}].concat(this.on_change_list);
500
return this._process_operations();
502
_process_onchange: function(on_change_obj) {
504
var widget = on_change_obj.widget;
505
var processed = on_change_obj.processed;
508
processed = processed || [];
509
processed.push(widget.name);
510
var on_change = widget.node.attrs.on_change;
512
var change_spec = self.parse_on_change(on_change, widget);
514
if (self.datarecord.id && !instance.web.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) {
515
// In case of a o2m virtual id, we should pass an empty ids list
516
ids.push(self.datarecord.id);
518
def = self.alive(new instance.web.Model(self.dataset.model).call(
519
change_spec.method, [ids].concat(change_spec.args)));
523
return def.then(function(response) {
524
if (widget.field['change_default']) {
525
var fieldname = widget.name;
527
if (response.value && (fieldname in response.value)) {
528
// Use value from onchange if onchange executed
529
value_ = response.value[fieldname];
531
// otherwise get form value for field
532
value_ = self.fields[fieldname].get_value();
534
var condition = fieldname + '=' + value_;
537
return self.alive(new instance.web.Model('ir.values').call(
538
'get_defaults', [self.model, condition]
539
)).then(function (results) {
540
if (!results.length) {
543
if (!response.value) {
546
for(var i=0; i<results.length; ++i) {
547
// [whatever, key, value]
548
var triplet = results[i];
549
response.value[triplet[1]] = triplet[2];
556
}).then(function(response) {
557
return self.on_processed_onchange(response, processed);
561
instance.webclient.crashmanager.show_message(e);
562
return $.Deferred().reject();
565
on_processed_onchange: function(result, processed) {
568
this._internal_set_values(result.value, processed);
570
if (!_.isEmpty(result.warning)) {
571
instance.web.dialog($(QWeb.render("CrashManager.warning", result.warning)), {
572
title:result.warning.title,
575
{text: _t("Ok"), click: function() { $(this).dialog("close"); }}
580
var fields = this.fields;
581
_(result.domain).each(function (domain, fieldname) {
582
var field = fields[fieldname];
583
if (!field) { return; }
584
field.node.attrs.domain = domain;
587
return $.Deferred().resolve();
590
instance.webclient.crashmanager.show_message(e);
591
return $.Deferred().reject();
594
_process_operations: function() {
596
return this.mutating_mutex.exec(function() {
598
var on_change_obj = self.on_change_list.shift();
600
return self._process_onchange(on_change_obj).then(function() {
605
_.each(self.fields, function(field) {
606
defs.push(field.commit_value());
608
var args = _.toArray(arguments);
609
return $.when.apply($, defs).then(function() {
610
if (self.on_change_list.length !== 0) {
613
var save_obj = self.save_list.pop();
615
return self._process_save(save_obj).then(function() {
616
save_obj.ret = _.toArray(arguments);
619
save_obj.error = true;
628
_internal_set_values: function(values, exclude) {
629
exclude = exclude || [];
630
for (var f in values) {
631
if (!values.hasOwnProperty(f)) { continue; }
632
var field = this.fields[f];
633
// If field is not defined in the view, just ignore it
635
var value_ = values[f];
636
if (field.get_value() != value_) {
637
field._inhibit_on_change_flag = true;
638
field.set_value(value_);
639
field._inhibit_on_change_flag = false;
640
field._dirty_flag = true;
641
if (!_.contains(exclude, field.name)) {
642
this.do_onchange(field, exclude);
647
this.on_form_changed();
649
set_values: function(values) {
651
return this.mutating_mutex.exec(function() {
652
self._internal_set_values(values);
656
* Ask the view to switch to view mode if possible. The view may not do it
657
* if the current record is not yet saved. It will then stay in create mode.
659
to_view_mode: function() {
660
this._actualize_mode("view");
663
* Ask the view to switch to edit mode if possible. The view may not do it
664
* if the current record is not yet saved. It will then stay in create mode.
666
to_edit_mode: function() {
667
this._actualize_mode("edit");
670
* Ask the view to switch to a precise mode if possible. The view is free to
671
* not respect this command if the state of the dataset is not compatible with
672
* the new mode. For example, it is not possible to switch to edit mode if
673
* the current record is not yet saved in database.
675
* @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
676
* undefined the view will test the actual mode to check if it is still consistent
677
* with the dataset state.
679
_actualize_mode: function(switch_to) {
680
var mode = switch_to || this.get("actual_mode");
681
if (! this.datarecord.id) {
683
} else if (mode === "create") {
686
this.set({actual_mode: mode});
688
check_actual_mode: function(source, options) {
690
if(this.get("actual_mode") === "view") {
691
self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
692
self.$buttons.find('.oe_form_buttons_edit').hide();
693
self.$buttons.find('.oe_form_buttons_view').show();
694
self.$sidebar.show();
696
self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
697
self.$buttons.find('.oe_form_buttons_edit').show();
698
self.$buttons.find('.oe_form_buttons_view').hide();
699
self.$sidebar.hide();
703
autofocus: function() {
704
if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
705
var fields_order = this.fields_order.slice(0);
706
if (this.default_focus_field) {
707
fields_order.unshift(this.default_focus_field.name);
709
for (var i = 0; i < fields_order.length; i += 1) {
710
var field = this.fields[fields_order[i]];
711
if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
712
if (field.focus() !== false) {
719
on_button_save: function() {
721
return this.save().done(function(result) {
722
self.trigger("save", result);
724
}).then(function(result) {
725
self.ViewManager.ActionManager.__parentedParent.menu.do_reload_needaction();
728
on_button_cancel: function(event) {
729
if (this.can_be_discarded()) {
730
if (this.get('actual_mode') === 'create') {
731
this.trigger('history_back');
734
this.trigger('load_record', this.datarecord);
737
this.trigger('on_button_cancel');
740
on_button_new: function() {
743
return $.when(this.has_been_loaded).then(function() {
744
if (self.can_be_discarded()) {
745
return self.load_defaults();
749
on_button_edit: function() {
750
return this.to_edit_mode();
752
on_button_create: function() {
753
this.dataset.index = null;
756
on_button_duplicate: function() {
758
return this.has_been_loaded.then(function() {
759
return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
760
self.record_created(new_id);
765
on_button_delete: function() {
767
var def = $.Deferred();
768
this.has_been_loaded.done(function() {
769
if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
770
self.dataset.unlink([self.datarecord.id]).done(function() {
771
if (self.dataset.size()) {
772
self.execute_pager_action('next');
774
self.do_action('history_back');
779
$.async_when().done(function () {
784
return def.promise();
786
can_be_discarded: function() {
787
if (this.$el.is('.oe_form_dirty')) {
788
if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
791
this.$el.removeClass('oe_form_dirty');
796
* Triggers saving the form's record. Chooses between creating a new
797
* record or saving an existing one depending on whether the record
798
* already has an id property.
800
* @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
801
* record, should that record be inserted at the start of the dataset (by
802
* default, records are added at the end)
804
save: function(prepend_on_create) {
806
var save_obj = {prepend_on_create: prepend_on_create, ret: null};
807
this.save_list.push(save_obj);
808
return this._process_operations().then(function() {
810
return $.Deferred().reject();
811
return $.when.apply($, save_obj.ret);
813
self.$el.removeClass('oe_form_dirty');
816
_process_save: function(save_obj) {
818
var prepend_on_create = save_obj.prepend_on_create;
820
var form_invalid = false,
822
first_invalid_field = null,
823
readonly_values = {};
824
for (var f in self.fields) {
825
if (!self.fields.hasOwnProperty(f)) { continue; }
829
if (!first_invalid_field) {
830
first_invalid_field = f;
832
} else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
833
// Special case 'id' field, do not save this field
834
// on 'create' : save all non readonly fields
835
// on 'edit' : save non readonly modified fields
836
if (!f.get("readonly")) {
837
values[f.name] = f.get_value();
839
readonly_values[f.name] = f.get_value();
844
self.set({'display_invalid_fields': true});
845
first_invalid_field.focus();
847
return $.Deferred().reject();
849
self.set({'display_invalid_fields': false});
851
if (!self.datarecord.id) {
853
save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
854
return self.record_created(r, prepend_on_create);
856
} else if (_.isEmpty(values)) {
857
// Not dirty, noop save
858
save_deferral = $.Deferred().resolve({}).promise();
861
save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
862
return self.record_saved(r);
865
return save_deferral;
869
return $.Deferred().reject();
872
on_invalid: function() {
873
var warnings = _(this.fields).chain()
874
.filter(function (f) { return !f.is_valid(); })
876
return _.str.sprintf('<li>%s</li>',
879
warnings.unshift('<ul>');
880
warnings.push('</ul>');
881
this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
884
* Reload the form after saving
886
* @param {Object} r result of the write function.
888
record_saved: function(r) {
891
// should not happen in the server, but may happen for internal purpose
892
this.trigger('record_saved', r);
893
return $.Deferred().reject();
895
return $.when(this.reload()).then(function () {
896
self.trigger('record_saved', r);
902
* Updates the form' dataset to contain the new record:
904
* * Adds the newly created record to the current dataset (at the end by
906
* * Selects that record (sets the dataset's index to point to the new
908
* * Updates the pager and sidebar displays
911
* @param {Boolean} [prepend_on_create=false] adds the newly created record
912
* at the beginning of the dataset instead of the end
914
record_created: function(r, prepend_on_create) {
917
// should not happen in the server, but may happen for internal purpose
918
this.trigger('record_created', r);
919
return $.Deferred().reject();
921
this.datarecord.id = r;
922
if (!prepend_on_create) {
923
this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
924
this.dataset.index = this.dataset.ids.length - 1;
926
this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
927
this.dataset.index = 0;
929
this.do_update_pager();
931
this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
933
//openerp.log("The record has been created with id #" + this.datarecord.id);
934
return $.when(this.reload()).then(function () {
935
self.trigger('record_created', r);
936
return _.extend(r, {created: true});
940
on_action: function (action) {
941
console.debug('Executing action', action);
945
return this.reload_mutex.exec(function() {
946
if (self.dataset.index == null) {
947
self.trigger("previous_view");
948
return $.Deferred().reject().promise();
950
if (self.dataset.index == null || self.dataset.index < 0) {
951
return $.when(self.on_button_new());
953
var fields = _.keys(self.fields_view.fields);
954
fields.push('display_name');
955
return self.dataset.read_index(fields,
959
'future_display_name': true
961
}).then(function(r) {
962
self.trigger('load_record', r);
967
get_widgets: function() {
968
return _.filter(this.getChildren(), function(obj) {
969
return obj instanceof instance.web.form.FormWidget;
972
get_fields_values: function() {
974
var ids = this.get_selected_ids();
975
values["id"] = ids.length > 0 ? ids[0] : false;
976
_.each(this.fields, function(value_, key) {
977
values[key] = value_.get_value();
981
get_selected_ids: function() {
982
var id = this.dataset.ids[this.dataset.index];
983
return id ? [id] : [];
985
recursive_save: function() {
987
return $.when(this.save()).then(function(res) {
988
if (self.dataset.parent_view)
989
return self.dataset.parent_view.recursive_save();
992
recursive_reload: function() {
995
if (self.dataset.parent_view)
996
pre = self.dataset.parent_view.recursive_reload();
997
return pre.then(function() {
998
return self.reload();
1001
is_dirty: function() {
1002
return _.any(this.fields, function (value_) {
1003
return value_._dirty_flag;
1006
is_interactible_record: function() {
1007
var id = this.datarecord.id;
1009
if (this.options.not_interactible_on_create)
1011
} else if (typeof(id) === "string") {
1012
if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
1017
sidebar_eval_context: function () {
1018
return $.when(this.build_eval_context());
1020
open_defaults_dialog: function () {
1022
var display = function (field, value) {
1023
if (field instanceof instance.web.form.FieldSelection) {
1024
return _(field.values).find(function (option) {
1025
return option[0] === value;
1027
} else if (field instanceof instance.web.form.FieldMany2One) {
1028
return field.get_displayed();
1032
var fields = _.chain(this.fields)
1033
.map(function (field) {
1034
var value = field.get_value();
1035
// ignore fields which are empty, invisible, readonly, o2m
1038
|| field.get('invisible')
1039
|| field.get("readonly")
1040
|| field.field.type === 'one2many'
1041
|| field.field.type === 'many2many'
1042
|| field.field.type === 'binary'
1043
|| field.password) {
1049
string: field.string,
1051
displayed: display(field, value),
1055
.sortBy(function (field) { return field.string; })
1057
var conditions = _.chain(self.fields)
1058
.filter(function (field) { return field.field.change_default; })
1059
.map(function (field) {
1060
var value = field.get_value();
1063
string: field.string,
1065
displayed: display(field, value),
1070
var d = new instance.web.Dialog(this, {
1071
title: _t("Set Default"),
1074
conditions: conditions
1077
{text: _t("Close"), click: function () { d.close(); }},
1078
{text: _t("Save default"), click: function () {
1079
var $defaults = d.$el.find('#formview_default_fields');
1080
var field_to_set = $defaults.val();
1081
if (!field_to_set) {
1082
$defaults.parent().addClass('oe_form_invalid');
1085
var condition = d.$el.find('#formview_default_conditions').val(),
1086
all_users = d.$el.find('#formview_default_all').is(':checked');
1087
new instance.web.DataSet(self, 'ir.values').call(
1091
self.fields[field_to_set].get_value(),
1095
]).done(function () { d.close(); });
1099
d.template = 'FormView.set_default';
1102
register_field: function(field, name) {
1103
this.fields[name] = field;
1104
this.fields_order.push(name);
1105
if (JSON.parse(field.node.attrs.default_focus || "0")) {
1106
this.default_focus_field = field;
1109
field.on('focused', null, this.proxy('widgetFocused'))
1110
.on('blurred', null, this.proxy('widgetBlurred'));
1111
if (this.get_field_desc(name).translate) {
1112
this.translatable_fields.push(field);
1114
field.on('changed_value', this, function() {
1115
if (field.is_syntax_valid()) {
1116
this.trigger('field_changed:' + name);
1118
if (field._inhibit_on_change_flag) {
1121
field._dirty_flag = true;
1122
if (field.is_syntax_valid()) {
1123
this.do_onchange(field);
1124
this.on_form_changed(true);
1125
this.do_notify_change();
1129
get_field_desc: function(field_name) {
1130
return this.fields_view.fields[field_name];
1132
get_field_value: function(field_name) {
1133
return this.fields[field_name].get_value();
1135
compute_domain: function(expression) {
1136
return instance.web.form.compute_domain(expression, this.fields);
1138
_build_view_fields_values: function() {
1139
var a_dataset = this.dataset;
1140
var fields_values = this.get_fields_values();
1141
var active_id = a_dataset.ids[a_dataset.index];
1142
_.extend(fields_values, {
1143
active_id: active_id || false,
1144
active_ids: active_id ? [active_id] : [],
1145
active_model: a_dataset.model,
1148
if (a_dataset.parent_view) {
1149
fields_values.parent = a_dataset.parent_view.get_fields_values();
1151
return fields_values;
1153
build_eval_context: function() {
1154
var a_dataset = this.dataset;
1155
return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
1160
* Interface to be implemented by rendering engines for the form view.
1162
instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
1163
set_fields_view: function(fields_view) {},
1164
set_fields_registry: function(fields_registry) {},
1165
render_to: function($el) {},
1169
* Default rendering engine for the form view.
1171
* It is necessary to set the view using set_view() before usage.
1173
instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
1174
init: function(view) {
1177
set_fields_view: function(fvg) {
1179
this.version = parseFloat(this.fvg.arch.attrs.version);
1180
if (isNaN(this.version)) {
1184
set_tags_registry: function(tags_registry) {
1185
this.tags_registry = tags_registry;
1187
set_fields_registry: function(fields_registry) {
1188
this.fields_registry = fields_registry;
1190
set_widgets_registry: function(widgets_registry) {
1191
this.widgets_registry = widgets_registry;
1193
// Backward compatibility tools, current default version: v6.1
1194
process_version: function() {
1195
if (this.version < 7.0) {
1196
this.$form.find('form:first').wrapInner('<group col="4"/>');
1197
this.$form.find('page').each(function() {
1198
if (!$(this).parents('field').length) {
1199
$(this).wrapInner('<group col="4"/>');
1204
get_arch_fragment: function() {
1205
var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
1206
// IE won't allow custom button@type and will revert it to spec default : 'submit'
1207
$('button', doc).each(function() {
1208
$(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
1210
// IE's html parser is also a css parser. How convenient...
1211
$('board', doc).each(function() {
1212
$(this).attr('layout', $(this).attr('style'));
1214
return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
1216
render_to: function($target) {
1218
this.$target = $target;
1220
this.$form = this.get_arch_fragment();
1222
this.process_version();
1224
this.fields_to_init = [];
1225
this.tags_to_init = [];
1226
this.widgets_to_init = [];
1228
this.process(this.$form);
1230
this.$form.appendTo(this.$target);
1232
this.to_replace = [];
1234
_.each(this.fields_to_init, function($elem) {
1235
var name = $elem.attr("name");
1236
if (!self.fvg.fields[name]) {
1237
throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
1239
var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
1241
throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
1243
var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1244
var $label = self.labels[$elem.attr("name")];
1246
w.set_input_id($label.attr("for"));
1248
self.alter_field(w);
1249
self.view.register_field(w, $elem.attr("name"));
1250
self.to_replace.push([w, $elem]);
1252
_.each(this.tags_to_init, function($elem) {
1253
var tag_name = $elem[0].tagName.toLowerCase();
1254
var obj = self.tags_registry.get_object(tag_name);
1255
var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1256
self.to_replace.push([w, $elem]);
1258
_.each(this.widgets_to_init, function($elem) {
1259
var widget_type = $elem.attr("type");
1260
var obj = self.widgets_registry.get_object(widget_type);
1261
var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1262
self.to_replace.push([w, $elem]);
1265
init_fields: function() {
1267
_.each(this.to_replace, function(el) {
1268
defs.push(el[0].replace(el[1]));
1270
this.to_replace = [];
1271
return $.when.apply($, defs);
1273
render_element: function(template /* dictionaries */) {
1274
var dicts = [].slice.call(arguments).slice(1);
1275
var dict = _.extend.apply(_, dicts);
1276
dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
1277
return $(QWeb.render(template, dict));
1279
alter_field: function(field) {
1281
toggle_layout_debugging: function() {
1282
if (!this.$target.has('.oe_layout_debug_cell:first').length) {
1283
this.$target.find('[title]').removeAttr('title');
1284
this.$target.find('.oe_form_group_cell').each(function() {
1285
var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
1286
$(this).attr('title', text);
1289
this.$target.toggleClass('oe_layout_debugging');
1291
process: function($tag) {
1293
var tagname = $tag[0].nodeName.toLowerCase();
1294
if (this.tags_registry.contains(tagname)) {
1295
this.tags_to_init.push($tag);
1298
var fn = self['process_' + tagname];
1300
var args = [].slice.call(arguments);
1302
return fn.apply(self, args);
1304
// generic tag handling, just process children
1305
$tag.children().each(function() {
1306
self.process($(this));
1308
self.handle_common_properties($tag, $tag);
1309
$tag.removeAttr("modifiers");
1313
process_widget: function($widget) {
1314
this.widgets_to_init.push($widget);
1317
process_sheet: function($sheet) {
1318
var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
1319
this.handle_common_properties($new_sheet, $sheet);
1320
var $dst = $new_sheet.find('.oe_form_sheet');
1321
$sheet.contents().appendTo($dst);
1322
$sheet.before($new_sheet).remove();
1323
this.process($new_sheet);
1325
process_form: function($form) {
1326
if ($form.find('> sheet').length === 0) {
1327
$form.addClass('oe_form_nosheet');
1329
var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
1330
this.handle_common_properties($new_form, $form);
1331
$form.contents().appendTo($new_form);
1332
if ($form[0] === this.$form[0]) {
1333
// If root element, replace it
1334
this.$form = $new_form;
1336
$form.before($new_form).remove();
1338
this.process($new_form);
1341
* Used by direct <field> children of a <group> tag only
1342
* This method will add the implicit <label...> for every field
1345
preprocess_field: function($field) {
1347
var name = $field.attr('name'),
1348
field_colspan = parseInt($field.attr('colspan'), 10),
1349
field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
1351
if ($field.attr('nolabel') === '1')
1353
$field.attr('nolabel', '1');
1355
this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
1356
$(el).parents().each(function(unused, tag) {
1357
var name = tag.tagName.toLowerCase();
1358
if (name === "field" || name in self.tags_registry.map)
1365
var $label = $('<label/>').attr({
1367
"modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
1368
"string": $field.attr('string'),
1369
"help": $field.attr('help'),
1370
"class": $field.attr('class'),
1372
$label.insertBefore($field);
1373
if (field_colspan > 1) {
1374
$field.attr('colspan', field_colspan - 1);
1378
process_field: function($field) {
1379
if ($field.parent().is('group')) {
1380
// No implicit labels for normal fields, only for <group> direct children
1381
var $label = this.preprocess_field($field);
1383
this.process($label);
1386
this.fields_to_init.push($field);
1389
process_group: function($group) {
1391
$group.children('field').each(function() {
1392
self.preprocess_field($(this));
1394
var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
1396
if ($new_group.first().is('table.oe_form_group')) {
1397
$table = $new_group;
1398
} else if ($new_group.filter('table.oe_form_group').length) {
1399
$table = $new_group.filter('table.oe_form_group').first();
1401
$table = $new_group.find('table.oe_form_group').first();
1405
cols = parseInt($group.attr('col') || 2, 10),
1409
$group.children().each(function(a,b,c) {
1410
var $child = $(this);
1411
var colspan = parseInt($child.attr('colspan') || 1, 10);
1412
var tagName = $child[0].tagName.toLowerCase();
1413
var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
1414
var newline = tagName === 'newline';
1416
// Note FME: those classes are used in layout debug mode
1417
if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
1418
$tr.addClass('oe_form_group_row_incomplete');
1420
$tr.addClass('oe_form_group_row_newline');
1427
if (!$tr || row_cols < colspan) {
1428
$tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
1430
} else if (tagName==='group') {
1431
// When <group> <group/><group/> </group>, we need a spacing between the two groups
1432
$td.addClass('oe_group_right')
1434
row_cols -= colspan;
1436
// invisibility transfer
1437
var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
1438
var invisible = field_modifiers.invisible;
1439
self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
1441
$tr.append($td.append($child));
1442
children.push($child[0]);
1444
if (row_cols && $td) {
1445
$td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
1447
$group.before($new_group).remove();
1449
$table.find('> tbody > tr').each(function() {
1450
var to_compute = [],
1453
$(this).children().each(function() {
1455
$child = $td.children(':first');
1456
if ($child.attr('cell-class')) {
1457
$td.addClass($child.attr('cell-class'));
1459
switch ($child[0].tagName.toLowerCase()) {
1463
if ($child.attr('for')) {
1464
$td.attr('width', '1%').addClass('oe_form_group_cell_label');
1465
row_cols-= $td.attr('colspan') || 1;
1470
var width = _.str.trim($child.attr('width') || ''),
1471
iwidth = parseInt(width, 10);
1473
if (width.substr(-1) === '%') {
1475
width = iwidth + '%';
1478
$td.css('min-width', width + 'px');
1480
$td.attr('width', width);
1481
$child.removeAttr('width');
1482
row_cols-= $td.attr('colspan') || 1;
1484
to_compute.push($td);
1490
var unit = Math.floor(total / row_cols);
1491
if (!$(this).is('.oe_form_group_row_incomplete')) {
1492
_.each(to_compute, function($td, i) {
1493
var width = parseInt($td.attr('colspan'), 10) * unit;
1494
$td.attr('width', width + '%');
1500
_.each(children, function(el) {
1501
self.process($(el));
1503
this.handle_common_properties($new_group, $group);
1506
process_notebook: function($notebook) {
1509
$notebook.find('> page').each(function() {
1510
var $page = $(this);
1511
var page_attrs = $page.getAttributes();
1512
page_attrs.id = _.uniqueId('notebook_page_');
1513
var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
1514
$page.contents().appendTo($new_page);
1515
$page.before($new_page).remove();
1516
var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
1517
page_attrs.__page = $new_page;
1518
page_attrs.__ic = ic;
1519
pages.push(page_attrs);
1521
$new_page.children().each(function() {
1522
self.process($(this));
1525
var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
1526
$notebook.contents().appendTo($new_notebook);
1527
$notebook.before($new_notebook).remove();
1528
self.process($($new_notebook.children()[0]));
1529
//tabs and invisibility handling
1530
$new_notebook.tabs();
1531
_.each(pages, function(page, i) {
1534
page.__ic.on("change:effective_invisible", null, function() {
1535
if (!page.__ic.get('effective_invisible') && page.autofocus) {
1536
$new_notebook.tabs('select', i);
1539
var current = $new_notebook.tabs("option", "selected");
1540
if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
1542
var first_visible = _.find(_.range(pages.length), function(i2) {
1543
return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
1545
if (first_visible !== undefined) {
1546
$new_notebook.tabs('select', first_visible);
1551
this.handle_common_properties($new_notebook, $notebook);
1552
return $new_notebook;
1554
process_separator: function($separator) {
1555
var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
1556
$separator.before($new_separator).remove();
1557
this.handle_common_properties($new_separator, $separator);
1558
return $new_separator;
1560
process_label: function($label) {
1561
var name = $label.attr("for"),
1562
field_orm = this.fvg.fields[name];
1564
string: $label.attr('string') || (field_orm || {}).string || '',
1565
help: $label.attr('help') || (field_orm || {}).help || '',
1566
_for: name ? _.uniqueId('oe-field-input-') : undefined,
1568
var align = parseFloat(dict.align);
1569
if (isNaN(align) || align === 1) {
1571
} else if (align === 0) {
1577
var $new_label = this.render_element('FormRenderingLabel', dict);
1578
$label.before($new_label).remove();
1579
this.handle_common_properties($new_label, $label);
1581
this.labels[name] = $new_label;
1585
handle_common_properties: function($new_element, $node) {
1586
var str_modifiers = $node.attr("modifiers") || "{}";
1587
var modifiers = JSON.parse(str_modifiers);
1589
if (modifiers.invisible !== undefined)
1590
ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
1591
$new_element.addClass($node.attr("class") || "");
1592
$new_element.attr('style', $node.attr('style'));
1593
return {invisibility_changer: ic,};
1600
If you read this documentation, it probably means that you were asked to use a form view widget outside of
1601
a form view. Before going further, you must understand that those fields were never really created for
1602
that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
1603
you to hack the system with more style.
1605
instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
1606
init: function(parent, eval_context) {
1607
this._super(parent);
1608
this.field_descs = {};
1609
this.eval_context = eval_context || {};
1611
display_invalid_fields: false,
1612
actual_mode: 'create',
1615
get_field_desc: function(field_name) {
1616
if (this.field_descs[field_name] === undefined) {
1617
this.field_descs[field_name] = {
1621
return this.field_descs[field_name];
1623
extend_field_desc: function(fields) {
1625
_.each(fields, function(v, k) {
1626
_.extend(self.get_field_desc(k), v);
1629
get_field_value: function(field_name) {
1632
set_values: function(values) {
1635
compute_domain: function(expression) {
1636
return instance.web.form.compute_domain(expression, {});
1638
build_eval_context: function() {
1639
return new instance.web.CompoundContext(this.eval_context);
1643
instance.web.form.compute_domain = function(expr, fields) {
1644
if (! (expr instanceof Array))
1647
for (var i = expr.length - 1; i >= 0; i--) {
1649
if (ex.length == 1) {
1650
var top = stack.pop();
1653
stack.push(stack.pop() || top);
1656
stack.push(stack.pop() && top);
1662
throw new Error(_.str.sprintf(
1663
_t("Unknown operator %s in domain %s"),
1664
ex, JSON.stringify(expr)));
1668
var field = fields[ex[0]];
1670
throw new Error(_.str.sprintf(
1671
_t("Unknown field %s in domain %s"),
1672
ex[0], JSON.stringify(expr)));
1674
var field_value = field.get_value ? field.get_value() : field.value;
1678
switch (op.toLowerCase()) {
1681
stack.push(_.isEqual(field_value, val));
1685
stack.push(!_.isEqual(field_value, val));
1688
stack.push(field_value < val);
1691
stack.push(field_value > val);
1694
stack.push(field_value <= val);
1697
stack.push(field_value >= val);
1700
if (!_.isArray(val)) val = [val];
1701
stack.push(_(val).contains(field_value));
1704
if (!_.isArray(val)) val = [val];
1705
stack.push(!_(val).contains(field_value));
1709
_t("Unsupported operator %s in domain %s"),
1710
op, JSON.stringify(expr));
1713
return _.all(stack, _.identity);
1716
instance.web.form.is_bin_size = function(v) {
1717
return /^\d+(\.\d*)? \w+$/.test(v);
1721
* Must be applied over an class already possessing the PropertiesMixin.
1723
* Apply the result of the "invisible" domain to this.$el.
1725
instance.web.form.InvisibilityChangerMixin = {
1726
init: function(field_manager, invisible_domain) {
1728
this._ic_field_manager = field_manager;
1729
this._ic_invisible_modifier = invisible_domain;
1730
this._ic_field_manager.on("view_content_has_changed", this, function() {
1731
var result = self._ic_invisible_modifier === undefined ? false :
1732
self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
1733
self.set({"invisible": result});
1735
this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
1736
var check = function() {
1737
if (self.get("invisible") || self.get('force_invisible')) {
1738
self.set({"effective_invisible": true});
1740
self.set({"effective_invisible": false});
1743
this.on('change:invisible', this, check);
1744
this.on('change:force_invisible', this, check);
1748
this.on("change:effective_invisible", this, this._check_visibility);
1749
this._check_visibility();
1751
_check_visibility: function() {
1752
this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
1756
instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
1757
init: function(parent, field_manager, invisible_domain, $el) {
1758
this.setParent(parent);
1759
instance.web.PropertiesMixin.init.call(this);
1760
instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
1767
Base class for all fields, custom widgets and buttons to be displayed in the form view.
1770
- effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
1771
the values of the "readonly" property and the "mode" property on the field manager.
1773
instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
1775
* @constructs instance.web.form.FormWidget
1776
* @extends instance.web.Widget
1778
* @param field_manager
1781
init: function(field_manager, node) {
1782
this._super(field_manager);
1783
this.field_manager = field_manager;
1784
if (this.field_manager instanceof instance.web.FormView)
1785
this.view = this.field_manager;
1787
this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
1788
instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
1790
this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
1796
// some events to make the property "effective_readonly" sync automatically with "readonly" and
1797
// "mode" on field_manager
1799
var test_effective_readonly = function() {
1800
self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
1802
this.on("change:readonly", this, test_effective_readonly);
1803
this.field_manager.on("change:actual_mode", this, test_effective_readonly);
1804
test_effective_readonly.call(this);
1806
renderElement: function() {
1807
this.process_modifiers();
1809
this.$el.addClass(this.node.attrs["class"] || "");
1811
destroy: function() {
1813
this._super.apply(this, arguments);
1816
* Sets up blur/focus forwarding from DOM elements to a widget (`this`).
1818
* This method is an utility method that is meant to be called by child classes.
1820
* @param {jQuery} $e jQuery object of elements to bind focus/blur on
1822
setupFocus: function ($e) {
1825
focus: function () { self.trigger('focused'); },
1826
blur: function () { self.trigger('blurred'); }
1829
process_modifiers: function() {
1831
for (var a in this.modifiers) {
1832
if (!this.modifiers.hasOwnProperty(a)) { continue; }
1833
if (!_.include(["invisible"], a)) {
1834
var val = this.field_manager.compute_domain(this.modifiers[a]);
1840
do_attach_tooltip: function(widget, trigger, options) {
1841
widget = widget || this;
1842
trigger = trigger || this.$el;
1843
options = _.extend({
1848
var template = widget.template + '.tooltip';
1849
if (!QWeb.has_template(template)) {
1850
template = 'WidgetLabel.tooltip';
1852
return QWeb.render(template, {
1853
debug: instance.session.debug,
1856
gravity: $.fn.tipsy.autoBounds(50, 'nw'),
1861
$(trigger).tipsy(options);
1864
* Builds a new context usable for operations related to fields by merging
1865
* the fields'context with the action's context.
1867
build_context: function() {
1868
// only use the model's context if there is not context on the node
1869
var v_context = this.node.attrs.context;
1871
v_context = (this.field || {}).context || {};
1874
if (v_context.__ref || true) { //TODO: remove true
1875
var fields_values = this.field_manager.build_eval_context();
1876
v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
1880
build_domain: function() {
1881
var f_domain = this.field.domain || [];
1882
var n_domain = this.node.attrs.domain || null;
1883
// if there is a domain on the node, overrides the model's domain
1884
var final_domain = n_domain !== null ? n_domain : f_domain;
1885
if (!(final_domain instanceof Array) || true) { //TODO: remove true
1886
var fields_values = this.field_manager.build_eval_context();
1887
final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
1889
return final_domain;
1893
instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
1894
template: 'WidgetButton',
1895
init: function(field_manager, node) {
1896
node.attrs.type = node.attrs['data-button-type'];
1897
this._super(field_manager, node);
1898
this.force_disabled = false;
1899
this.string = (this.node.attrs.string || '').replace(/_/g, '');
1900
if (JSON.parse(this.node.attrs.default_focus || "0")) {
1901
// TODO fme: provide enter key binding to widgets
1902
this.view.default_focus_button = this;
1904
if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
1905
this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
1909
this._super.apply(this, arguments);
1910
this.view.on('view_content_has_changed', this, this.check_disable);
1911
this.check_disable();
1912
this.$el.click(this.on_click);
1913
if (this.node.attrs.help || instance.session.debug) {
1914
this.do_attach_tooltip();
1916
this.setupFocus(this.$el);
1918
on_click: function() {
1920
this.force_disabled = true;
1921
this.check_disable();
1922
this.execute_action().always(function() {
1923
self.force_disabled = false;
1924
self.check_disable();
1927
execute_action: function() {
1929
var exec_action = function() {
1930
if (self.node.attrs.confirm) {
1931
var def = $.Deferred();
1932
var dialog = instance.web.dialog($('<div/>').text(self.node.attrs.confirm), {
1933
title: _t('Confirm'),
1936
{text: _t("Cancel"), click: function() {
1937
$(this).dialog("close");
1940
{text: _t("Ok"), click: function() {
1942
self.on_confirmed().always(function() {
1943
$(self2).dialog("close");
1948
beforeClose: function() {
1952
return def.promise();
1954
return self.on_confirmed();
1957
if (!this.node.attrs.special) {
1958
return this.view.recursive_save().then(exec_action);
1960
return exec_action();
1963
on_confirmed: function() {
1966
var context = this.build_context();
1968
return this.view.do_execute_action(
1969
_.extend({}, this.node.attrs, {context: context}),
1970
this.view.dataset, this.view.datarecord.id, function () {
1971
self.view.recursive_reload();
1974
check_disable: function() {
1975
var disabled = (this.force_disabled || !this.view.is_interactible_record());
1976
this.$el.prop('disabled', disabled);
1977
this.$el.css('color', disabled ? 'grey' : '');
1982
* Interface to be implemented by fields.
1985
* - changed_value: triggered when the value of the field has changed. This can be due
1986
* to a user interaction or a call to set_value().
1989
instance.web.form.FieldInterface = {
1991
* Constructor takes 2 arguments:
1992
* - field_manager: Implements FieldManagerMixin
1993
* - node: the "<field>" node in json form
1995
init: function(field_manager, node) {},
1997
* Called by the form view to indicate the value of the field.
1999
* Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
2000
* regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
2001
* before the widget is inserted into the DOM.
2003
* set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
2004
* osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
2005
* also be able to handle any other format commonly used in the _defaults key on the models in the addons
2006
* as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
2007
* no information is ever given to know which format is used.
2009
set_value: function(value_) {},
2011
* Get the current value of the widget.
2013
* Must always return a syntactically correct value to be passed to the "write" method of the osv class in
2014
* the OpenERP server, although it is not assumed to respect the constraints applied to the field.
2015
* For example if the field is marked as "required", a call to get_value() can return false.
2017
* get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
2018
* return a default value according to the type of field.
2020
* This method is always assumed to perform synchronously, it can not return a promise.
2022
* If there was no user interaction to modify the value of the field, it is always assumed that
2023
* get_value() return the same semantic value than the one passed in the last call to set_value(),
2024
* although the syntax can be different. This can be the case for type of fields that have a different
2025
* syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
2027
get_value: function() {},
2029
* Inform the current object of the id it should use to match a html <label> that exists somewhere in the
2032
set_input_id: function(id) {},
2034
* Returns true if is_syntax_valid() returns true and the value is semantically
2035
* valid too according to the semantic restrictions applied to the field.
2037
is_valid: function() {},
2039
* Returns true if the field holds a value which is syntactically correct, ignoring
2040
* the potential semantic restrictions applied to the field.
2042
is_syntax_valid: function() {},
2044
* Must set the focus on the field. Return false if field is not focusable.
2046
focus: function() {},
2048
* Called when the translate button is clicked.
2050
on_translate: function() {},
2052
This method is called by the form view before reading on_change values and before saving. It tells
2053
the field to save its value before reading it using get_value(). Must return a promise.
2055
commit_value: function() {},
2059
* Abstract class for classes implementing FieldInterface.
2062
* - value: useful property to hold the value of the field. By default, set_value() and get_value()
2063
* set and retrieve the value property. Changing the value property also triggers automatically
2064
* a 'changed_value' event that inform the view to trigger on_changes.
2067
instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
2069
* @constructs instance.web.form.AbstractField
2070
* @extends instance.web.form.FormWidget
2072
* @param field_manager
2075
init: function(field_manager, node) {
2077
this._super(field_manager, node);
2078
this.name = this.node.attrs.name;
2079
this.field = this.field_manager.get_field_desc(this.name);
2080
this.widget = this.node.attrs.widget;
2081
this.string = this.node.attrs.string || this.field.string || this.name;
2082
this.options = instance.web.py_eval(this.node.attrs.options || '{}');
2083
this.set({'value': false});
2085
this.on("change:value", this, function() {
2086
this.trigger('changed_value');
2087
this._check_css_flags();
2090
renderElement: function() {
2093
if (this.field.translate && this.view) {
2094
this.$el.addClass('oe_form_field_translatable');
2095
this.$el.find('.oe_field_translate').click(this.on_translate);
2097
this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
2098
if (instance.session.debug) {
2099
this.do_attach_tooltip(this, this.$label[0] || this.$el);
2100
this.$label.off('dblclick').on('dblclick', function() {
2101
console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
2103
console.log("window.w =", window.w);
2106
if (!this.disable_utility_classes) {
2107
this.off("change:required", this, this._set_required);
2108
this.on("change:required", this, this._set_required);
2109
this._set_required();
2111
this._check_visibility();
2112
this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
2113
this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
2114
this._check_css_flags();
2117
var tmp = this._super();
2118
this.on("change:value", this, function() {
2119
if (! this.no_rerender)
2120
this.render_value();
2122
this.render_value();
2125
* Private. Do not use.
2127
_set_required: function() {
2128
this.$el.toggleClass('oe_form_required', this.get("required"));
2130
set_value: function(value_) {
2131
this.set({'value': value_});
2133
get_value: function() {
2134
return this.get('value');
2137
Utility method that all implementations should use to change the
2138
value without triggering a re-rendering.
2140
internal_set_value: function(value_) {
2141
var tmp = this.no_rerender;
2142
this.no_rerender = true;
2143
this.set({'value': value_});
2144
this.no_rerender = tmp;
2147
This method is called each time the value is modified.
2149
render_value: function() {},
2150
is_valid: function() {
2151
return this.is_syntax_valid() && !(this.get('required') && this.is_false());
2153
is_syntax_valid: function() {
2157
* Method useful to implement to ease validity testing. Must return true if the current
2158
* value is similar to false in OpenERP.
2160
is_false: function() {
2161
return this.get('value') === false;
2163
_check_css_flags: function() {
2164
if (this.field.translate) {
2165
this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
2167
if (!this.disable_utility_classes) {
2168
if (this.field_manager.get('display_invalid_fields')) {
2169
this.$el.toggleClass('oe_form_invalid', !this.is_valid());
2176
set_input_id: function(id) {
2177
this.id_for_label = id;
2179
on_translate: function() {
2181
var trans = new instance.web.DataSet(this, 'ir.translation');
2182
return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
2187
set_dimensions: function (height, width) {
2193
commit_value: function() {
2199
* A mixin to apply on any FormWidget that has to completely re-render when its readonly state
2202
instance.web.form.ReinitializeWidgetMixin = {
2204
* Default implementation of, you should not override it, use initialize_field() instead.
2207
this.initialize_field();
2210
initialize_field: function() {
2211
this.on("change:effective_readonly", this, this.reinitialize);
2212
this.initialize_content();
2214
reinitialize: function() {
2215
this.destroy_content();
2216
this.renderElement();
2217
this.initialize_content();
2220
* Called to destroy anything that could have been created previously, called before a
2221
* re-initialization.
2223
destroy_content: function() {},
2225
* Called to initialize the content.
2227
initialize_content: function() {},
2231
* A mixin to apply on any field that has to completely re-render when its readonly state
2234
instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
2235
reinitialize: function() {
2236
instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
2237
this.render_value();
2242
Some hack to make placeholders work in ie9.
2244
if ($.browser.msie && $.browser.version === "9.0") {
2245
document.addEventListener("DOMNodeInserted",function(event){
2246
var nodename = event.target.nodeName.toLowerCase();
2247
if ( nodename === "input" || nodename == "textarea" ) {
2248
$(event.target).placeholder();
2253
instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2254
template: 'FieldChar',
2255
widget_class: 'oe_form_field_char',
2257
'change input': 'store_dom_value',
2259
init: function (field_manager, node) {
2260
this._super(field_manager, node);
2261
this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2263
initialize_content: function() {
2264
this.setupFocus(this.$('input'));
2266
store_dom_value: function () {
2267
if (!this.get('effective_readonly')
2268
&& this.$('input').length
2269
&& this.is_syntax_valid()) {
2270
this.internal_set_value(
2272
this.$('input').val()));
2275
commit_value: function () {
2276
this.store_dom_value();
2277
return this._super();
2279
render_value: function() {
2280
var show_value = this.format_value(this.get('value'), '');
2281
if (!this.get("effective_readonly")) {
2282
this.$el.find('input').val(show_value);
2284
if (this.password) {
2285
show_value = new Array(show_value.length + 1).join('*');
2287
this.$(".oe_form_char_content").text(show_value);
2290
is_syntax_valid: function() {
2291
if (!this.get("effective_readonly") && this.$("input").size() > 0) {
2293
this.parse_value(this.$('input').val(), '');
2301
parse_value: function(val, def) {
2302
return instance.web.parse_value(val, this, def);
2304
format_value: function(val, def) {
2305
return instance.web.format_value(val, this, def);
2307
is_false: function() {
2308
return this.get('value') === '' || this._super();
2311
var input = this.$('input:first')[0];
2312
return input ? input.focus() : false;
2314
set_dimensions: function (height, width) {
2315
this._super(height, width);
2316
this.$('input').css({
2323
instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2324
process_modifiers: function () {
2326
this.set({ readonly: true });
2330
instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2331
template: 'FieldEmail',
2332
initialize_content: function() {
2334
var $button = this.$el.find('button');
2335
$button.click(this.on_button_clicked);
2336
this.setupFocus($button);
2338
render_value: function() {
2339
if (!this.get("effective_readonly")) {
2343
.attr('href', 'mailto:' + this.get('value'))
2344
.text(this.get('value') || '');
2347
on_button_clicked: function() {
2348
if (!this.get('value') || !this.is_syntax_valid()) {
2349
this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2351
location.href = 'mailto:' + this.get('value');
2356
instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2357
template: 'FieldUrl',
2358
initialize_content: function() {
2360
var $button = this.$el.find('button');
2361
$button.click(this.on_button_clicked);
2362
this.setupFocus($button);
2364
render_value: function() {
2365
if (!this.get("effective_readonly")) {
2368
var tmp = this.get('value');
2369
var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2371
tmp = "http://" + this.get('value');
2373
var text = this.get('value') ? this.node.attrs.text || tmp : '';
2374
this.$el.find('a').attr('href', tmp).text(text);
2377
on_button_clicked: function() {
2378
if (!this.get('value')) {
2379
this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2381
var url = $.trim(this.get('value'));
2382
if(/^www\./i.test(url))
2383
url = 'http://'+url;
2389
instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2390
is_field_number: true,
2391
widget_class: 'oe_form_field_float',
2392
init: function (field_manager, node) {
2393
this._super(field_manager, node);
2394
this.internal_set_value(0);
2395
if (this.node.attrs.digits) {
2396
this.digits = this.node.attrs.digits;
2398
this.digits = this.field.digits;
2401
set_value: function(value_) {
2402
if (value_ === false || value_ === undefined) {
2403
// As in GTK client, floats default to 0
2406
this._super.apply(this, [value_]);
2408
focus: function () {
2409
var $input = this.$('input:first');
2410
return $input.length ? $input.select() : false;
2414
instance.web.DateTimeWidget = instance.web.Widget.extend({
2415
template: "web.datepicker",
2416
jqueryui_object: 'datetimepicker',
2417
type_of_date: "datetime",
2419
'change .oe_datepicker_master': 'change_datetime',
2421
init: function(parent) {
2422
this._super(parent);
2423
this.name = parent.name;
2427
this.$input = this.$el.find('input.oe_datepicker_master');
2428
this.$input_picker = this.$el.find('input.oe_datepicker_container');
2430
$.datepicker.setDefaults({
2431
clearText: _t('Clear'),
2432
clearStatus: _t('Erase the current date'),
2433
closeText: _t('Done'),
2434
closeStatus: _t('Close without change'),
2435
prevText: _t('<Prev'),
2436
prevStatus: _t('Show the previous month'),
2437
nextText: _t('Next>'),
2438
nextStatus: _t('Show the next month'),
2439
currentText: _t('Today'),
2440
currentStatus: _t('Show the current month'),
2441
monthNames: Date.CultureInfo.monthNames,
2442
monthNamesShort: Date.CultureInfo.abbreviatedMonthNames,
2443
monthStatus: _t('Show a different month'),
2444
yearStatus: _t('Show a different year'),
2445
weekHeader: _t('Wk'),
2446
weekStatus: _t('Week of the year'),
2447
dayNames: Date.CultureInfo.dayNames,
2448
dayNamesShort: Date.CultureInfo.abbreviatedDayNames,
2449
dayNamesMin: Date.CultureInfo.shortestDayNames,
2450
dayStatus: _t('Set DD as first week day'),
2451
dateStatus: _t('Select D, M d'),
2452
firstDay: Date.CultureInfo.firstDayOfWeek,
2453
initStatus: _t('Select a date'),
2456
$.timepicker.setDefaults({
2457
timeOnlyTitle: _t('Choose Time'),
2458
timeText: _t('Time'),
2459
hourText: _t('Hour'),
2460
minuteText: _t('Minute'),
2461
secondText: _t('Second'),
2462
currentText: _t('Now'),
2463
closeText: _t('Done')
2467
onClose: this.on_picker_select,
2468
onSelect: this.on_picker_select,
2472
showButtonPanel: true,
2473
firstDay: Date.CultureInfo.firstDayOfWeek
2475
// Some clicks in the datepicker dialog are not stopped by the
2476
// datepicker and "bubble through", unexpectedly triggering the bus's
2477
// click event. Prevent that.
2478
this.picker('widget').click(function (e) { e.stopPropagation(); });
2480
this.$el.find('img.oe_datepicker_trigger').click(function() {
2481
if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
2482
self.$input.focus();
2485
self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date());
2486
self.$input_picker.show();
2487
self.picker('show');
2488
self.$input_picker.hide();
2490
this.set_readonly(false);
2491
this.set({'value': false});
2493
picker: function() {
2494
return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
2496
on_picker_select: function(text, instance_) {
2497
var date = this.picker('getDate');
2499
.val(date ? this.format_client(date) : '')
2503
set_value: function(value_) {
2504
this.set({'value': value_});
2505
this.$input.val(value_ ? this.format_client(value_) : '');
2507
get_value: function() {
2508
return this.get('value');
2510
set_value_from_ui_: function() {
2511
var value_ = this.$input.val() || false;
2512
this.set({'value': this.parse_client(value_)});
2514
set_readonly: function(readonly) {
2515
this.readonly = readonly;
2516
this.$input.prop('readonly', this.readonly);
2517
this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
2519
is_valid_: function() {
2520
var value_ = this.$input.val();
2521
if (value_ === "") {
2525
this.parse_client(value_);
2532
parse_client: function(v) {
2533
return instance.web.parse_value(v, {"widget": this.type_of_date});
2535
format_client: function(v) {
2536
return instance.web.format_value(v, {"widget": this.type_of_date});
2538
change_datetime: function() {
2539
if (this.is_valid_()) {
2540
this.set_value_from_ui_();
2541
this.trigger("datetime_changed");
2544
commit_value: function () {
2545
this.change_datetime();
2549
instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2550
jqueryui_object: 'datepicker',
2551
type_of_date: "date"
2554
instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2555
template: "FieldDatetime",
2556
build_widget: function() {
2557
return new instance.web.DateTimeWidget(this);
2559
destroy_content: function() {
2560
if (this.datewidget) {
2561
this.datewidget.destroy();
2562
this.datewidget = undefined;
2565
initialize_content: function() {
2566
if (!this.get("effective_readonly")) {
2567
this.datewidget = this.build_widget();
2568
this.datewidget.on('datetime_changed', this, _.bind(function() {
2569
this.internal_set_value(this.datewidget.get_value());
2571
this.datewidget.appendTo(this.$el);
2572
this.setupFocus(this.datewidget.$input);
2575
render_value: function() {
2576
if (!this.get("effective_readonly")) {
2577
this.datewidget.set_value(this.get('value'));
2579
this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2582
is_syntax_valid: function() {
2583
if (!this.get("effective_readonly") && this.datewidget) {
2584
return this.datewidget.is_valid_();
2588
is_false: function() {
2589
return this.get('value') === '' || this._super();
2592
var input = this.datewidget && this.datewidget.$input[0];
2593
return input ? input.focus() : false;
2595
set_dimensions: function (height, width) {
2596
this._super(height, width);
2597
this.datewidget.$input.css('height', height);
2601
instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2602
template: "FieldDate",
2603
build_widget: function() {
2604
return new instance.web.DateWidget(this);
2608
instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2609
template: 'FieldText',
2611
'keyup': function (e) {
2612
if (e.which === $.ui.keyCode.ENTER) {
2613
e.stopPropagation();
2616
'change textarea': 'store_dom_value',
2618
initialize_content: function() {
2620
if (! this.get("effective_readonly")) {
2621
this.$textarea = this.$el.find('textarea');
2622
this.auto_sized = false;
2623
this.default_height = this.$textarea.css('height');
2624
if (this.get("effective_readonly")) {
2625
this.$textarea.attr('disabled', 'disabled');
2627
this.setupFocus(this.$textarea);
2629
this.$textarea = undefined;
2632
commit_value: function () {
2633
if (! this.get("effective_readonly") && this.$textarea) {
2634
this.store_dom_value();
2636
return this._super();
2638
store_dom_value: function () {
2639
this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2641
render_value: function() {
2642
if (! this.get("effective_readonly")) {
2643
var show_value = instance.web.format_value(this.get('value'), this, '');
2644
if (show_value === '') {
2645
this.$textarea.css('height', parseInt(this.default_height)+"px");
2647
this.$textarea.val(show_value);
2648
if (! this.auto_sized) {
2649
this.auto_sized = true;
2650
this.$textarea.autosize();
2652
this.$textarea.trigger("autosize");
2655
var txt = this.get("value") || '';
2656
this.$(".oe_form_text_content").text(txt);
2659
is_syntax_valid: function() {
2660
if (!this.get("effective_readonly") && this.$textarea) {
2662
instance.web.parse_value(this.$textarea.val(), this, '');
2670
is_false: function() {
2671
return this.get('value') === '' || this._super();
2673
focus: function($el) {
2674
var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2675
return input ? input.focus() : false;
2677
set_dimensions: function (height, width) {
2678
this._super(height, width);
2679
if (!this.get("effective_readonly") && this.$textarea) {
2680
this.$textarea.css({
2689
* FieldTextHtml Widget
2690
* Intended for FieldText widgets meant to display HTML content. This
2691
* widget will instantiate the CLEditor (see cleditor in static/src/lib)
2692
* To find more information about CLEditor configutation: go to
2693
* http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2695
instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2696
template: 'FieldTextHtml',
2698
this._super.apply(this, arguments);
2700
initialize_content: function() {
2702
if (! this.get("effective_readonly")) {
2703
self._updating_editor = false;
2704
this.$textarea = this.$el.find('textarea');
2705
var width = ((this.node.attrs || {}).editor_width || '100%');
2706
var height = ((this.node.attrs || {}).editor_height || 250);
2707
this.$textarea.cleditor({
2708
width: width, // width not including margins, borders or padding
2709
height: height, // height not including margins, borders or padding
2710
controls: // controls to add to the toolbar
2711
"bold italic underline strikethrough " +
2712
"| removeformat | bullets numbering | outdent " +
2713
"indent | link unlink | source",
2714
bodyStyle: // style to assign to document body contained within the editor
2715
"margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2717
this.$cleditor = this.$textarea.cleditor()[0];
2718
this.$cleditor.change(function() {
2719
if (! self._updating_editor) {
2720
self.$cleditor.updateTextArea();
2721
self.internal_set_value(self.$textarea.val());
2724
if (this.field.translate) {
2725
var $img = $('<img class="oe_field_translate oe_input_icon" src="/web/static/src/img/icons/terp-translate.png" width="16" height="16" border="0"/>')
2726
.click(this.on_translate);
2727
this.$cleditor.$toolbar.append($img);
2731
render_value: function() {
2732
if (! this.get("effective_readonly")) {
2733
this.$textarea.val(this.get('value') || '');
2734
this._updating_editor = true;
2735
this.$cleditor.updateFrame();
2736
this._updating_editor = false;
2738
this.$el.html(this.get('value'));
2743
instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2744
template: 'FieldBoolean',
2747
this.$checkbox = $("input", this.$el);
2748
this.setupFocus(this.$checkbox);
2749
this.$el.click(_.bind(function() {
2750
this.internal_set_value(this.$checkbox.is(':checked'));
2752
var check_readonly = function() {
2753
self.$checkbox.prop('disabled', self.get("effective_readonly"));
2755
this.on("change:effective_readonly", this, check_readonly);
2756
check_readonly.call(this);
2757
this._super.apply(this, arguments);
2759
render_value: function() {
2760
this.$checkbox[0].checked = this.get('value');
2763
var input = this.$checkbox && this.$checkbox[0];
2764
return input ? input.focus() : false;
2769
The progressbar field expect a float from 0 to 100.
2771
instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2772
template: 'FieldProgressBar',
2773
render_value: function() {
2774
this.$el.progressbar({
2775
value: this.get('value') || 0,
2776
disabled: this.get("effective_readonly")
2778
var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
2779
this.$('span').html(formatted_value + '%');
2784
instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2785
template: 'FieldSelection',
2787
'change select': 'store_dom_value',
2789
init: function(field_manager, node) {
2791
this._super(field_manager, node);
2792
this.values = _(this.field.selection).chain()
2793
.reject(function (v) { return v[0] === false && v[1] === ''; })
2794
.unshift([false, ''])
2797
initialize_content: function() {
2798
// Flag indicating whether we're in an event chain containing a change
2799
// event on the select, in order to know what to do on keyup[RETURN]:
2800
// * If the user presses [RETURN] as part of changing the value of a
2801
// selection, we should just let the value change and not let the
2802
// event broadcast further (e.g. to validating the current state of
2803
// the form in editable list view, which would lead to saving the
2804
// current row or switching to the next one)
2805
// * If the user presses [RETURN] with a select closed (side-effect:
2806
// also if the user opened the select and pressed [RETURN] without
2807
// changing the selected value), takes the action as validating the
2809
var ischanging = false;
2810
var $select = this.$el.find('select')
2811
.change(function () { ischanging = true; })
2812
.click(function () { ischanging = false; })
2813
.keyup(function (e) {
2814
if (e.which !== 13 || !ischanging) { return; }
2815
e.stopPropagation();
2818
this.setupFocus($select);
2820
commit_value: function () {
2821
this.store_dom_value();
2822
return this._super();
2824
store_dom_value: function () {
2825
if (!this.get('effective_readonly') && this.$('select').length) {
2826
this.internal_set_value(
2827
this.values[this.$('select')[0].selectedIndex][0]);
2830
set_value: function(value_) {
2831
value_ = value_ === null ? false : value_;
2832
value_ = value_ instanceof Array ? value_[0] : value_;
2833
this._super(value_);
2835
render_value: function() {
2836
if (!this.get("effective_readonly")) {
2838
for (var i = 0, ii = this.values.length; i < ii; i++) {
2839
if (this.values[i][0] === this.get('value')) index = i;
2841
this.$el.find('select')[0].selectedIndex = index;
2844
var option = _(this.values)
2845
.detect(function (record) { return record[0] === self.get('value'); });
2846
this.$el.text(option ? option[1] : this.values[0][1]);
2850
var input = this.$('select:first')[0];
2851
return input ? input.focus() : false;
2853
set_dimensions: function (height, width) {
2854
this._super(height, width);
2855
this.$('select').css({
2862
// jquery autocomplete tweak to allow html and classnames
2864
var proto = $.ui.autocomplete.prototype,
2865
initSource = proto._initSource;
2867
function filter( array, term ) {
2868
var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
2869
return $.grep( array, function(value_) {
2870
return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
2875
_initSource: function() {
2876
if ( this.options.html && $.isArray(this.options.source) ) {
2877
this.source = function( request, response ) {
2878
response( filter( this.options.source, request.term ) );
2881
initSource.call( this );
2885
_renderItem: function( ul, item) {
2886
return $( "<li></li>" )
2887
.data( "item.autocomplete", item )
2888
.append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
2890
.addClass(item.classname);
2896
* A mixin containing some useful methods to handle completion inputs.
2898
instance.web.form.CompletionFieldMixin = {
2901
this.orderer = new instance.web.DropMisordered();
2904
* Call this method to search using a string.
2906
get_search_result: function(search_val) {
2909
var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
2910
var blacklist = this.get_search_blacklist();
2911
this.last_query = search_val;
2913
return this.orderer.add(dataset.name_search(
2914
search_val, new instance.web.CompoundDomain(self.build_domain(), [["id", "not in", blacklist]]),
2915
'ilike', this.limit + 1, self.build_context())).then(function(data) {
2916
self.last_search = data;
2917
// possible selections for the m2o
2918
var values = _.map(data, function(x) {
2919
x[1] = x[1].split("\n")[0];
2921
label: _.str.escapeHTML(x[1]),
2928
// search more... if more results that max
2929
if (values.length > self.limit) {
2930
values = values.slice(0, self.limit);
2932
label: _t("Search More..."),
2933
action: function() {
2934
dataset.name_search(search_val, self.build_domain(), 'ilike', false).done(function(data) {
2935
self._search_create_popup("search", data);
2938
classname: 'oe_m2o_dropdown_option'
2942
var raw_result = _(data.result).map(function(x) {return x[1];});
2943
if (search_val.length > 0 && !_.include(raw_result, search_val)) {
2945
label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
2946
$('<span />').text(search_val).html()),
2947
action: function() {
2948
self._quick_create(search_val);
2950
classname: 'oe_m2o_dropdown_option'
2955
label: _t("Create and Edit..."),
2956
action: function() {
2957
self._search_create_popup("form", undefined, self._create_context(search_val));
2959
classname: 'oe_m2o_dropdown_option'
2965
get_search_blacklist: function() {
2968
_quick_create: function(name) {
2970
var slow_create = function () {
2971
self._search_create_popup("form", undefined, self._create_context(name));
2973
if (self.options.quick_create === undefined || self.options.quick_create) {
2974
new instance.web.DataSet(this, this.field.relation, self.build_context())
2975
.name_create(name).done(function(data) {
2976
self.add_id(data[0]);
2977
}).fail(function(error, event) {
2978
event.preventDefault();
2984
// all search/create popup handling
2985
_search_create_popup: function(view, ids, context) {
2987
var pop = new instance.web.form.SelectCreatePopup(this);
2989
self.field.relation,
2991
title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
2992
initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
2994
disable_multiple_selection: true
2996
self.build_domain(),
2997
new instance.web.CompoundContext(self.build_context(), context || {})
2999
pop.on("elements_selected", self, function(element_ids) {
3000
self.add_id(element_ids[0]);
3007
add_id: function(id) {},
3008
_create_context: function(name) {
3010
var field = (this.options || {}).create_name_field;
3011
if (field === undefined)
3013
if (field !== false && name && (this.options || {}).quick_create !== false)
3014
tmp["default_" + field] = name;
3019
instance.web.form.M2ODialog = instance.web.Dialog.extend({
3020
template: "M2ODialog",
3021
init: function(parent) {
3022
this._super(parent, {
3023
title: _.str.sprintf(_t("Add %s"), parent.string),
3029
this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3030
this.$("input").val(this.getParent().last_query);
3031
this.$buttons.find(".oe_form_m2o_qc_button").click(function(){
3032
self.getParent()._quick_create(self.$("input").val());
3035
this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3036
self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3039
this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3045
instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3046
template: "FieldMany2One",
3048
'keydown input': function (e) {
3050
case $.ui.keyCode.UP:
3051
case $.ui.keyCode.DOWN:
3052
e.stopPropagation();
3056
init: function(field_manager, node) {
3057
this._super(field_manager, node);
3058
instance.web.form.CompletionFieldMixin.init.call(this);
3059
this.set({'value': false});
3060
this.display_value = {};
3061
this.last_search = [];
3062
this.floating = false;
3063
this.current_display = null;
3064
this.is_started = false;
3066
reinit_value: function(val) {
3067
this.internal_set_value(val);
3068
this.floating = false;
3069
if (this.is_started)
3070
this.render_value();
3072
initialize_field: function() {
3073
this.is_started = true;
3074
instance.web.bus.on('click', this, function() {
3075
if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3076
this.$input.autocomplete("close");
3079
instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3081
initialize_content: function() {
3082
if (!this.get("effective_readonly"))
3083
this.render_editable();
3085
destroy_content: function () {
3086
if (this.$drop_down) {
3087
this.$drop_down.off('click');
3088
delete this.$drop_down;
3091
this.$input.closest(".ui-dialog .ui-dialog-content").off('scroll');
3092
this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3093
'focus focusout change keydown');
3096
if (this.$follow_button) {
3097
this.$follow_button.off('blur focus click');
3098
delete this.$follow_button;
3101
destroy: function () {
3102
this.destroy_content();
3103
return this._super();
3105
init_error_displayer: function() {
3108
hide_error_displayer: function() {
3111
show_error_displayer: function() {
3112
new instance.web.form.M2ODialog(this).open();
3114
render_editable: function() {
3116
this.$input = this.$el.find("input");
3118
this.init_error_displayer();
3120
self.$input.on('focus', function() {
3121
self.hide_error_displayer();
3124
this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3125
this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3127
this.$follow_button.click(function(ev) {
3128
ev.preventDefault();
3129
if (!self.get('value')) {
3133
var pop = new instance.web.form.FormOpenPopup(self);
3135
self.field.relation,
3137
self.build_context(),
3139
title: _t("Open: ") + self.string
3142
pop.on('write_completed', self, function(){
3143
self.display_value = {};
3144
self.render_value();
3146
self.view.do_onchange(self);
3150
// some behavior for input
3151
var input_changed = function() {
3152
if (self.current_display !== self.$input.val()) {
3153
self.current_display = self.$input.val();
3154
if (self.$input.val() === "") {
3155
self.internal_set_value(false);
3156
self.floating = false;
3158
self.floating = true;
3162
this.$input.keydown(input_changed);
3163
this.$input.change(input_changed);
3164
this.$drop_down.click(function() {
3165
if (self.$input.autocomplete("widget").is(":visible")) {
3166
self.$input.autocomplete("close");
3167
self.$input.focus();
3169
if (self.get("value") && ! self.floating) {
3170
self.$input.autocomplete("search", "");
3172
self.$input.autocomplete("search");
3177
// Autocomplete close on dialog content scroll
3178
var close_autocomplete = _.debounce(function() {
3179
if (self.$input.autocomplete("widget").is(":visible")) {
3180
self.$input.autocomplete("close");
3183
this.$input.closest(".ui-dialog .ui-dialog-content").on('scroll', this, close_autocomplete);
3185
self.ed_def = $.Deferred();
3186
self.uned_def = $.Deferred();
3188
var ed_duration = 15000;
3189
var anyoneLoosesFocus = function (e) {
3191
if (self.floating) {
3192
if (self.last_search.length > 0) {
3193
if (self.last_search[0][0] != self.get("value")) {
3194
self.display_value = {};
3195
self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3196
self.reinit_value(self.last_search[0][0]);
3199
self.render_value();
3203
self.reinit_value(false);
3205
self.floating = false;
3207
if (used && self.get("value") === false && ! self.no_ed) {
3208
self.ed_def.reject();
3209
self.uned_def.reject();
3210
self.ed_def = $.Deferred();
3211
self.ed_def.done(function() {
3212
self.show_error_displayer();
3213
ignore_blur = false;
3214
self.trigger('focused');
3217
setTimeout(function() {
3218
self.ed_def.resolve();
3219
self.uned_def.reject();
3220
self.uned_def = $.Deferred();
3221
self.uned_def.done(function() {
3222
self.hide_error_displayer();
3224
setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3228
self.ed_def.reject();
3231
var ignore_blur = false;
3233
focusout: anyoneLoosesFocus,
3234
focus: function () { self.trigger('focused'); },
3235
autocompleteopen: function () { ignore_blur = true; },
3236
autocompleteclose: function () { ignore_blur = false; },
3238
// autocomplete open
3239
if (ignore_blur) { return; }
3240
if (_(self.getChildren()).any(function (child) {
3241
return child instanceof instance.web.form.AbstractFormPopup;
3243
self.trigger('blurred');
3247
var isSelecting = false;
3249
this.$input.autocomplete({
3250
source: function(req, resp) {
3251
self.get_search_result(req.term).done(function(result) {
3255
select: function(event, ui) {
3259
self.display_value = {};
3260
self.display_value["" + item.id] = item.name;
3261
self.reinit_value(item.id);
3262
} else if (item.action) {
3264
// Cancel widget blurring, to avoid form blur event
3265
self.trigger('focused');
3269
focus: function(e, ui) {
3273
// disabled to solve a bug, but may cause others
3274
//close: anyoneLoosesFocus,
3278
this.$input.autocomplete("widget").openerpClass();
3279
// used to correct a bug when selecting an element by pushing 'enter' in an editable list
3280
this.$input.keyup(function(e) {
3281
if (e.which === 13) { // ENTER
3283
e.stopPropagation();
3285
isSelecting = false;
3287
this.setupFocus(this.$follow_button);
3289
render_value: function(no_recurse) {
3291
if (! this.get("value")) {
3292
this.display_string("");
3295
var display = this.display_value["" + this.get("value")];
3297
this.display_string(display);
3301
var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3302
this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3303
self.display_value["" + self.get("value")] = data[0][1];
3304
self.render_value(true);
3308
display_string: function(str) {
3310
if (!this.get("effective_readonly")) {
3311
this.$input.val(str.split("\n")[0]);
3312
this.current_display = this.$input.val();
3313
if (this.is_false()) {
3314
this.$('.oe_m2o_cm_button').css({'display':'none'});
3316
this.$('.oe_m2o_cm_button').css({'display':'inline'});
3319
var lines = _.escape(str).split("\n");
3323
follow = _.rest(lines).join("<br />");
3326
var $link = this.$el.find('.oe_form_uri')
3329
if (! this.options.no_open)
3330
$link.click(function () {
3332
type: 'ir.actions.act_window',
3333
res_model: self.field.relation,
3334
res_id: self.get("value"),
3335
views: [[false, 'form']],
3337
context: self.build_context().eval(),
3341
$(".oe_form_m2o_follow", this.$el).html(follow);
3344
set_value: function(value_) {
3346
if (value_ instanceof Array) {
3347
this.display_value = {};
3348
if (! this.options.always_reload) {
3349
this.display_value["" + value_[0]] = value_[1];
3353
value_ = value_ || false;
3354
this.reinit_value(value_);
3356
get_displayed: function() {
3357
return this.display_value["" + this.get("value")];
3359
add_id: function(id) {
3360
this.display_value = {};
3361
this.reinit_value(id);
3363
is_false: function() {
3364
return ! this.get("value");
3366
focus: function () {
3367
var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3368
return input ? input.focus() : false;
3370
_quick_create: function() {
3372
this.ed_def.reject();
3373
return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3375
_search_create_popup: function() {
3377
this.ed_def.reject();
3378
return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3380
set_dimensions: function (height, width) {
3381
this._super(height, width);
3382
this.$input.css('height', height);
3386
instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3387
template: 'Many2OneButton',
3388
init: function(field_manager, node) {
3389
this._super.apply(this, arguments);
3392
this._super.apply(this, arguments);
3395
set_button: function() {
3398
this.$button.remove();
3401
this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3402
this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3403
this.$button.addClass('oe_link').css({'padding':'4px'});
3404
this.$el.append(this.$button);
3405
this.$button.on('click', self.on_click);
3407
on_click: function(ev) {
3409
this.popup = new instance.web.form.FormOpenPopup(this);
3410
this.popup.show_element(
3411
this.field.relation,
3413
this.build_context(),
3414
{title: this.string}
3416
this.popup.on('create_completed', self, function(r) {
3420
set_value: function(value_) {
3422
if (value_ instanceof Array) {
3425
value_ = value_ || false;
3426
this.set('value', value_);
3432
# Values: (0, 0, { fields }) create
3433
# (1, ID, { fields }) update
3434
# (2, ID) remove (delete)
3435
# (3, ID) unlink one (target id or target of relation)
3437
# (5) unlink all (only valid for one2many)
3442
'create': function (values) {
3443
return [commands.CREATE, false, values];
3445
// (1, id, {values})
3447
'update': function (id, values) {
3448
return [commands.UPDATE, id, values];
3452
'delete': function (id) {
3453
return [commands.DELETE, id, false];
3455
// (3, id[, _]) removes relation, but not linked record itself
3457
'forget': function (id) {
3458
return [commands.FORGET, id, false];
3462
'link_to': function (id) {
3463
return [commands.LINK_TO, id, false];
3467
'delete_all': function () {
3468
return [5, false, false];
3470
// (6, _, ids) replaces all linked records with provided ids
3472
'replace_with': function (ids) {
3473
return [6, false, ids];
3476
instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3477
multi_selection: false,
3478
disable_utility_classes: true,
3479
init: function(field_manager, node) {
3480
this._super(field_manager, node);
3481
lazy_build_o2m_kanban_view();
3482
this.is_loaded = $.Deferred();
3483
this.initial_is_loaded = this.is_loaded;
3484
this.form_last_update = $.Deferred();
3485
this.init_form_last_update = this.form_last_update;
3486
this.is_started = false;
3487
this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
3488
this.dataset.o2m = this;
3489
this.dataset.parent_view = this.view;
3490
this.dataset.child_name = this.name;
3492
this.dataset.on('dataset_changed', this, function() {
3493
self.trigger_on_change();
3498
this._super.apply(this, arguments);
3499
this.$el.addClass('oe_form_field oe_form_field_one2many');
3504
this.is_loaded.done(function() {
3505
self.on("change:effective_readonly", self, function() {
3506
self.is_loaded = self.is_loaded.then(function() {
3507
self.viewmanager.destroy();
3508
return $.when(self.load_views()).done(function() {
3509
self.reload_current_view();
3514
this.is_started = true;
3515
this.reload_current_view();
3517
trigger_on_change: function() {
3518
this.trigger('changed_value');
3520
load_views: function() {
3523
var modes = this.node.attrs.mode;
3524
modes = !!modes ? modes.split(",") : ["tree"];
3526
_.each(modes, function(mode) {
3527
if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
3528
throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
3532
view_type: mode == "tree" ? "list" : mode,
3535
if (self.field.views && self.field.views[mode]) {
3536
view.embedded_view = self.field.views[mode];
3538
if(view.view_type === "list") {
3539
_.extend(view.options, {
3541
selectable: self.multi_selection,
3543
import_enabled: false,
3546
if (self.get("effective_readonly")) {
3547
_.extend(view.options, {
3552
} else if (view.view_type === "form") {
3553
if (self.get("effective_readonly")) {
3554
view.view_type = 'form';
3556
_.extend(view.options, {
3557
not_interactible_on_create: true,
3559
} else if (view.view_type === "kanban") {
3560
_.extend(view.options, {
3561
confirm_on_delete: false,
3563
if (self.get("effective_readonly")) {
3564
_.extend(view.options, {
3565
action_buttons: false,
3566
quick_creatable: false,
3568
read_only_mode: true,
3576
this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
3577
this.viewmanager.o2m = self;
3578
var once = $.Deferred().done(function() {
3579
self.init_form_last_update.resolve();
3581
var def = $.Deferred().done(function() {
3582
self.initial_is_loaded.resolve();
3584
this.viewmanager.on("controller_inited", self, function(view_type, controller) {
3585
controller.o2m = self;
3586
if (view_type == "list") {
3587
if (self.get("effective_readonly")) {
3588
controller.on('edit:before', self, function (e) {
3591
_(controller.columns).find(function (column) {
3592
if (!(column instanceof instance.web.list.Handle)) {
3595
column.modifiers.invisible = true;
3599
} else if (view_type === "form") {
3600
if (self.get("effective_readonly")) {
3601
$(".oe_form_buttons", controller.$el).children().remove();
3603
controller.on("load_record", self, function(){
3606
controller.on('pager_action_executed',self,self.save_any_view);
3607
} else if (view_type == "graph") {
3608
self.reload_current_view()
3612
this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
3613
$.when(self.save_any_view()).done(function() {
3614
if (n_mode === "list") {
3615
$.async_when().done(function() {
3616
self.reload_current_view();
3621
$.async_when().done(function () {
3622
self.viewmanager.appendTo(self.$el);
3626
reload_current_view: function() {
3628
return self.is_loaded = self.is_loaded.then(function() {
3629
var active_view = self.viewmanager.active_view;
3630
var view = self.viewmanager.views[active_view].controller;
3631
if(active_view === "list") {
3632
return view.reload_content();
3633
} else if (active_view === "form") {
3634
if (self.dataset.index === null && self.dataset.ids.length >= 1) {
3635
self.dataset.index = 0;
3637
var act = function() {
3638
return view.do_show();
3640
self.form_last_update = self.form_last_update.then(act, act);
3641
return self.form_last_update;
3642
} else if (view.do_search) {
3643
return view.do_search(self.build_domain(), self.dataset.get_context(), []);
3647
set_value: function(value_) {
3648
value_ = value_ || [];
3650
this.dataset.reset_ids([]);
3651
if(value_.length >= 1 && value_[0] instanceof Array) {
3653
_.each(value_, function(command) {
3654
var obj = {values: command[2]};
3655
switch (command[0]) {
3656
case commands.CREATE:
3657
obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3659
self.dataset.to_create.push(obj);
3660
self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3663
case commands.UPDATE:
3664
obj['id'] = command[1];
3665
self.dataset.to_write.push(obj);
3666
self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3669
case commands.DELETE:
3670
self.dataset.to_delete.push({id: command[1]});
3672
case commands.LINK_TO:
3673
ids.push(command[1]);
3675
case commands.DELETE_ALL:
3676
self.dataset.delete_all = true;
3681
this.dataset.set_ids(ids);
3682
} else if (value_.length >= 1 && typeof(value_[0]) === "object") {
3684
this.dataset.delete_all = true;
3685
_.each(value_, function(command) {
3686
var obj = {values: command};
3687
obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3689
self.dataset.to_create.push(obj);
3690
self.dataset.cache.push(_.clone(obj));
3694
this.dataset.set_ids(ids);
3696
this._super(value_);
3697
this.dataset.reset_ids(value_);
3699
if (this.dataset.index === null && this.dataset.ids.length > 0) {
3700
this.dataset.index = 0;
3702
this.trigger_on_change();
3703
if (this.is_started) {
3704
return self.reload_current_view();
3709
get_value: function() {
3713
var val = this.dataset.delete_all ? [commands.delete_all()] : [];
3714
val = val.concat(_.map(this.dataset.ids, function(id) {
3715
var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
3717
return commands.create(alter_order.values);
3719
alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
3721
return commands.update(alter_order.id, alter_order.values);
3723
return commands.link_to(id);
3725
return val.concat(_.map(
3726
this.dataset.to_delete, function(x) {
3727
return commands['delete'](x.id);}));
3729
commit_value: function() {
3730
return this.save_any_view();
3732
save_any_view: function() {
3733
if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
3734
this.viewmanager.views[this.viewmanager.active_view] &&
3735
this.viewmanager.views[this.viewmanager.active_view].controller) {
3736
var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3737
if (this.viewmanager.active_view === "form") {
3738
if (!view.is_initialized.state() === 'resolved') {
3739
return $.when(false);
3741
return $.when(view.save());
3742
} else if (this.viewmanager.active_view === "list") {
3743
return $.when(view.ensure_saved());
3746
return $.when(false);
3748
is_syntax_valid: function() {
3749
if (! this.viewmanager || ! this.viewmanager.views[this.viewmanager.active_view])
3751
var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3752
switch (this.viewmanager.active_view) {
3754
return _(view.fields).chain()
3760
return view.is_valid();
3766
instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
3767
template: 'One2Many.viewmanager',
3768
init: function(parent, dataset, views, flags) {
3769
this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
3770
this.registry = this.registry.extend({
3771
list: 'instance.web.form.One2ManyListView',
3772
form: 'instance.web.form.One2ManyFormView',
3773
kanban: 'instance.web.form.One2ManyKanbanView',
3775
this.__ignore_blur = false;
3777
switch_mode: function(mode, unused) {
3778
if (mode !== 'form') {
3779
return this._super(mode, unused);
3782
var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
3783
var pop = new instance.web.form.FormOpenPopup(this);
3784
pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3785
title: _t("Open: ") + self.o2m.string,
3786
create_function: function(data, options) {
3787
return self.o2m.dataset.create(data, options).done(function(r) {
3788
self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3789
self.o2m.dataset.trigger("dataset_changed", r);
3792
write_function: function(id, data, options) {
3793
return self.o2m.dataset.write(id, data, {}).done(function() {
3794
self.o2m.reload_current_view();
3797
alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3798
parent_view: self.o2m.view,
3799
child_name: self.o2m.name,
3800
read_function: function() {
3801
return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3803
form_view_options: {'not_interactible_on_create':true},
3804
readonly: self.o2m.get("effective_readonly")
3806
pop.on("elements_selected", self, function() {
3807
self.o2m.reload_current_view();
3812
instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
3813
get_context: function() {
3814
this.context = this.o2m.build_context();
3815
return this.context;
3819
instance.web.form.One2ManyListView = instance.web.ListView.extend({
3820
_template: 'One2Many.listview',
3821
init: function (parent, dataset, view_id, options) {
3822
this._super(parent, dataset, view_id, _.extend(options || {}, {
3823
GroupsType: instance.web.form.One2ManyGroups,
3824
ListType: instance.web.form.One2ManyList
3826
this.on('edit:before', this, this.proxy('_before_edit'));
3827
this.on('edit:after', this, this.proxy('_after_edit'));
3828
this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
3831
.bind('add', this.proxy("changed_records"))
3832
.bind('edit', this.proxy("changed_records"))
3833
.bind('remove', this.proxy("changed_records"));
3835
start: function () {
3836
var ret = this._super();
3838
.off('mousedown.handleButtons')
3839
.on('mousedown.handleButtons', 'table button', this.proxy('_button_down'));
3842
changed_records: function () {
3843
this.o2m.trigger_on_change();
3845
is_valid: function () {
3846
var editor = this.editor;
3847
var form = editor.form;
3848
// If no edition is pending, the listview can not be invalid (?)
3849
if (!editor.record) {
3852
// If the form has not been modified, the view can only be valid
3853
// NB: is_dirty will also be set on defaults/onchanges/whatever?
3854
// oe_form_dirty seems to only be set on actual user actions
3855
if (!form.$el.is('.oe_form_dirty')) {
3858
this.o2m._dirty_flag = true;
3860
// Otherwise validate internal form
3861
return _(form.fields).chain()
3862
.invoke(function () {
3863
this._check_css_flags();
3864
return this.is_valid();
3869
do_add_record: function () {
3870
if (this.editable()) {
3871
this._super.apply(this, arguments);
3874
var pop = new instance.web.form.SelectCreatePopup(this);
3876
self.o2m.field.relation,
3878
title: _t("Create: ") + self.o2m.string,
3879
initial_view: "form",
3880
alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3881
create_function: function(data, options) {
3882
return self.o2m.dataset.create(data, options).done(function(r) {
3883
self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3884
self.o2m.dataset.trigger("dataset_changed", r);
3887
read_function: function() {
3888
return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3890
parent_view: self.o2m.view,
3891
child_name: self.o2m.name,
3892
form_view_options: {'not_interactible_on_create':true}
3894
self.o2m.build_domain(),
3895
self.o2m.build_context()
3897
pop.on("elements_selected", self, function() {
3898
self.o2m.reload_current_view();
3902
do_activate_record: function(index, id) {
3904
var pop = new instance.web.form.FormOpenPopup(self);
3905
pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3906
title: _t("Open: ") + self.o2m.string,
3907
write_function: function(id, data) {
3908
return self.o2m.dataset.write(id, data, {}).done(function() {
3909
self.o2m.reload_current_view();
3912
alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3913
parent_view: self.o2m.view,
3914
child_name: self.o2m.name,
3915
read_function: function() {
3916
return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3918
form_view_options: {'not_interactible_on_create':true},
3919
readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
3922
do_button_action: function (name, id, callback) {
3923
if (!_.isNumber(id)) {
3924
instance.webclient.notification.warn(
3925
_t("Action Button"),
3926
_t("The o2m record must be saved before an action can be used"));
3929
var parent_form = this.o2m.view;
3931
this.ensure_saved().then(function () {
3933
return parent_form.save();
3936
}).done(function () {
3937
self.handle_button(name, id, callback);
3941
_before_edit: function () {
3942
this.__ignore_blur = false;
3943
this.editor.form.on('blurred', this, this._on_form_blur);
3945
_after_edit: function () {
3946
// The form's blur thing may be jiggered during the edition setup,
3947
// potentially leading to the o2m instasaving the row. Cancel any
3948
// blurring triggered the edition startup here
3949
this.editor.form.widgetFocused();
3951
_before_unedit: function () {
3952
this.editor.form.off('blurred', this, this._on_form_blur);
3954
_button_down: function () {
3955
// If a button is clicked (usually some sort of action button), it's
3956
// the button's responsibility to ensure the editable list is in the
3957
// correct state -> ignore form blurring
3958
this.__ignore_blur = true;
3961
* Handles blurring of the nested form (saves the currently edited row),
3962
* unless the flag to ignore the event is set to ``true``
3964
* Makes the internal form go away
3966
_on_form_blur: function () {
3967
if (this.__ignore_blur) {
3968
this.__ignore_blur = false;
3971
// FIXME: why isn't there an API for this?
3972
if (this.editor.form.$el.hasClass('oe_form_dirty')) {
3973
this.ensure_saved();
3976
this.cancel_edition();
3978
keyup_ENTER: function () {
3979
// blurring caused by hitting the [Return] key, should skip the
3980
// autosave-on-blur and let the handler for [Return] do its thing (save
3981
// the current row *anyway*, then create a new one/edit the next one)
3982
this.__ignore_blur = true;
3983
this._super.apply(this, arguments);
3985
do_delete: function (ids) {
3986
var confirm = window.confirm;
3987
window.confirm = function () { return true; };
3989
return this._super(ids);
3991
window.confirm = confirm;
3995
instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
3996
setup_resequence_rows: function () {
3997
if (!this.view.o2m.get('effective_readonly')) {
3998
this._super.apply(this, arguments);
4002
instance.web.form.One2ManyList = instance.web.ListView.List.extend({
4003
pad_table_to: function (count) {
4004
if (!this.view.is_action_enabled('create')) {
4007
this._super(count > 0 ? count - 1 : 0);
4010
// magical invocation of wtf does that do
4011
if (this.view.o2m.get('effective_readonly')) {
4016
var columns = _(this.columns).filter(function (column) {
4017
return column.invisible !== '1';
4019
if (this.options.selectable) { columns++; }
4020
if (this.options.deletable) { columns++; }
4022
if (!this.view.is_action_enabled('create')) {
4026
var $cell = $('<td>', {
4028
'class': 'oe_form_field_one2many_list_row_add'
4030
$('<a>', {href: '#'}).text(_t("Add an item"))
4031
.mousedown(function () {
4032
// FIXME: needs to be an official API somehow
4033
if (self.view.editor.is_editing()) {
4034
self.view.__ignore_blur = true;
4037
.click(function (e) {
4039
e.stopPropagation();
4040
// FIXME: there should also be an API for that one
4041
if (self.view.editor.form.__blur_timeout) {
4042
clearTimeout(self.view.editor.form.__blur_timeout);
4043
self.view.editor.form.__blur_timeout = false;
4045
self.view.ensure_saved().done(function () {
4046
self.view.do_add_record();
4050
var $padding = this.$current.find('tr:not([data-id]):first');
4051
var $newrow = $('<tr>').append($cell);
4052
if ($padding.length) {
4053
$padding.before($newrow);
4055
this.$current.append($newrow)
4060
instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4061
form_template: 'One2Many.formview',
4062
load_form: function(data) {
4065
this.$buttons.find('button.oe_form_button_create').click(function() {
4066
self.save().done(self.on_button_new);
4069
do_notify_change: function() {
4070
if (this.dataset.parent_view) {
4071
this.dataset.parent_view.do_notify_change();
4073
this._super.apply(this, arguments);
4078
var lazy_build_o2m_kanban_view = function() {
4079
if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
4081
instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4085
instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4086
template: "FieldMany2ManyTags",
4088
this._super.apply(this, arguments);
4089
instance.web.form.CompletionFieldMixin.init.call(this);
4090
this.set({"value": []});
4091
this._display_orderer = new instance.web.DropMisordered();
4092
this._drop_shown = false;
4094
initialize_content: function() {
4095
if (this.get("effective_readonly"))
4098
var ignore_blur = false;
4099
self.$text = this.$("textarea");
4100
self.$text.textext({
4101
plugins : 'tags arrow autocomplete',
4103
render: function(suggestion) {
4104
return $('<span class="text-label"/>').
4105
data('index', suggestion['index']).html(suggestion['label']);
4110
selectFromDropdown: function() {
4111
this.trigger('hideDropdown');
4112
var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4113
var data = self.search_result[index];
4115
self.add_id(data.id);
4120
this.trigger('setSuggestions', {result : []});
4124
isTagAllowed: function(tag) {
4128
removeTag: function(tag) {
4129
var id = tag.data("id");
4130
self.set({"value": _.without(self.get("value"), id)});
4132
renderTag: function(stuff) {
4133
return $.fn.textext.TextExtTags.prototype.renderTag.
4134
call(this, stuff).data("id", stuff.id);
4138
itemToString: function(item) {
4143
onSetInputData: function(e, data) {
4145
this._plugins.autocomplete._suggestions = null;
4147
this.input().val(data);
4151
}).bind('getSuggestions', function(e, data) {
4153
var str = !!data ? data.query || '' : '';
4154
self.get_search_result(str).done(function(result) {
4155
self.search_result = result;
4156
$(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4157
return _.extend(el, {index:i});
4160
}).bind('hideDropdown', function() {
4161
self._drop_shown = false;
4162
}).bind('showDropdown', function() {
4163
self._drop_shown = true;
4165
self.tags = self.$text.textext()[0].tags();
4167
.focusin(function () {
4168
self.trigger('focused');
4169
ignore_blur = false;
4171
.focusout(function() {
4172
self.$text.trigger("setInputData", "");
4174
self.trigger('blurred');
4176
}).keydown(function(e) {
4177
if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4178
self.$text.textext()[0].autocomplete().selectFromDropdown();
4182
set_value: function(value_) {
4183
value_ = value_ || [];
4184
if (value_.length >= 1 && value_[0] instanceof Array) {
4185
value_ = value_[0][2];
4187
this._super(value_);
4189
is_false: function() {
4190
return _(this.get("value")).isEmpty();
4192
get_value: function() {
4193
var tmp = [commands.replace_with(this.get("value"))];
4196
get_search_blacklist: function() {
4197
return this.get("value");
4199
render_value: function() {
4201
var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4202
var values = self.get("value");
4203
var handle_names = function(data) {
4204
if (self.isDestroyed())
4207
_.each(data, function(el) {
4208
indexed[el[0]] = el;
4210
data = _.map(values, function(el) { return indexed[el]; });
4211
if (! self.get("effective_readonly")) {
4212
self.tags.containerElement().children().remove();
4213
self.$('textarea').css("padding-left", "3px");
4214
self.tags.addTags(_.map(data, function(el) {return {name: el[1], id:el[0]};}));
4216
self.$el.html(QWeb.render("FieldMany2ManyTag", {elements: data}));
4219
if (! values || values.length > 0) {
4220
this._display_orderer.add(dataset.name_get(values)).done(handle_names);
4225
add_id: function(id) {
4226
this.set({'value': _.uniq(this.get('value').concat([id]))});
4228
focus: function () {
4229
var input = this.$text && this.$text[0];
4230
return input ? input.focus() : false;
4236
- reload_on_button: Reload the whole form view if click on a button in a list view.
4237
If you see this options, do not use it, it's basically a dirty hack to make one
4238
precise o2m to behave the way we want.
4240
instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4241
multi_selection: false,
4242
disable_utility_classes: true,
4243
init: function(field_manager, node) {
4244
this._super(field_manager, node);
4245
this.is_loaded = $.Deferred();
4246
this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4247
this.dataset.m2m = this;
4249
this.dataset.on('unlink', self, function(ids) {
4250
self.dataset_changed();
4253
this.list_dm = new instance.web.DropMisordered();
4254
this.render_value_dm = new instance.web.DropMisordered();
4256
initialize_content: function() {
4259
this.$el.addClass('oe_form_field oe_form_field_many2many');
4261
this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4262
'addable': this.get("effective_readonly") ? null : _t("Add"),
4263
'deletable': this.get("effective_readonly") ? false : true,
4264
'selectable': this.multi_selection,
4266
'reorderable': false,
4267
'import_enabled': false,
4269
var embedded = (this.field.views || {}).tree;
4271
this.list_view.set_embedded_view(embedded);
4273
this.list_view.m2m_field = this;
4274
var loaded = $.Deferred();
4275
this.list_view.on("list_view_loaded", this, function() {
4278
this.list_view.appendTo(this.$el);
4280
var old_def = self.is_loaded;
4281
self.is_loaded = $.Deferred().done(function() {
4284
this.list_dm.add(loaded).then(function() {
4285
self.is_loaded.resolve();
4288
destroy_content: function() {
4289
this.list_view.destroy();
4290
this.list_view = undefined;
4292
set_value: function(value_) {
4293
value_ = value_ || [];
4294
if (value_.length >= 1 && value_[0] instanceof Array) {
4295
value_ = value_[0][2];
4297
this._super(value_);
4299
get_value: function() {
4300
return [commands.replace_with(this.get('value'))];
4302
is_false: function () {
4303
return _(this.get("value")).isEmpty();
4305
render_value: function() {
4307
this.dataset.set_ids(this.get("value"));
4308
this.render_value_dm.add(this.is_loaded).then(function() {
4309
return self.list_view.reload_content();
4312
dataset_changed: function() {
4313
this.internal_set_value(this.dataset.ids);
4317
instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4318
get_context: function() {
4319
this.context = this.m2m.build_context();
4320
return this.context;
4326
* @extends instance.web.ListView
4328
instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4329
do_add_record: function () {
4330
var pop = new instance.web.form.SelectCreatePopup(this);
4334
title: _t("Add: ") + this.m2m_field.string
4336
new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4337
this.m2m_field.build_context()
4340
pop.on("elements_selected", self, function(element_ids) {
4342
_(element_ids).each(function (id) {
4343
if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4344
self.dataset.set_ids(self.dataset.ids.concat([id]));
4345
self.m2m_field.dataset_changed();
4350
self.reload_content();
4354
do_activate_record: function(index, id) {
4356
var pop = new instance.web.form.FormOpenPopup(this);
4357
pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4358
title: _t("Open: ") + this.m2m_field.string,
4359
readonly: this.getParent().get("effective_readonly")
4361
pop.on('write_completed', self, self.reload_content);
4363
do_button_action: function(name, id, callback) {
4365
var _sup = _.bind(this._super, this);
4366
if (! this.m2m_field.options.reload_on_button) {
4367
return _sup(name, id, callback);
4369
return this.m2m_field.view.save().then(function() {
4370
return _sup(name, id, function() {
4371
self.m2m_field.view.reload();
4376
is_action_enabled: function () { return true; },
4379
instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4380
disable_utility_classes: true,
4381
init: function(field_manager, node) {
4382
this._super(field_manager, node);
4383
instance.web.form.CompletionFieldMixin.init.call(this);
4384
m2m_kanban_lazy_init();
4385
this.is_loaded = $.Deferred();
4386
this.initial_is_loaded = this.is_loaded;
4389
this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4390
this.dataset.m2m = this;
4391
this.dataset.on('unlink', self, function(ids) {
4392
self.dataset_changed();
4396
this._super.apply(this, arguments);
4401
self.on("change:effective_readonly", self, function() {
4402
self.is_loaded = self.is_loaded.then(function() {
4403
self.kanban_view.destroy();
4404
return $.when(self.load_view()).done(function() {
4405
self.render_value();
4410
set_value: function(value_) {
4411
value_ = value_ || [];
4412
if (value_.length >= 1 && value_[0] instanceof Array) {
4413
value_ = value_[0][2];
4415
this._super(value_);
4417
get_value: function() {
4418
return [commands.replace_with(this.get('value'))];
4420
load_view: function() {
4422
this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4423
'create_text': _t("Add"),
4424
'creatable': self.get("effective_readonly") ? false : true,
4425
'quick_creatable': self.get("effective_readonly") ? false : true,
4426
'read_only_mode': self.get("effective_readonly") ? true : false,
4427
'confirm_on_delete': false,
4429
var embedded = (this.field.views || {}).kanban;
4431
this.kanban_view.set_embedded_view(embedded);
4433
this.kanban_view.m2m = this;
4434
var loaded = $.Deferred();
4435
this.kanban_view.on("kanban_view_loaded",self,function() {
4436
self.initial_is_loaded.resolve();
4439
this.kanban_view.on('switch_mode', this, this.open_popup);
4440
$.async_when().done(function () {
4441
self.kanban_view.appendTo(self.$el);
4445
render_value: function() {
4447
this.dataset.set_ids(this.get("value"));
4448
this.is_loaded = this.is_loaded.then(function() {
4449
return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
4452
dataset_changed: function() {
4453
this.set({'value': this.dataset.ids});
4455
open_popup: function(type, unused) {
4456
if (type !== "form")
4459
if (this.dataset.index === null) {
4460
var pop = new instance.web.form.SelectCreatePopup(this);
4462
this.field.relation,
4464
title: _t("Add: ") + this.string
4466
new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
4467
this.build_context()
4469
pop.on("elements_selected", self, function(element_ids) {
4470
_.each(element_ids, function(one_id) {
4471
if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
4472
self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
4473
self.dataset_changed();
4474
self.render_value();
4479
var id = self.dataset.ids[self.dataset.index];
4480
var pop = new instance.web.form.FormOpenPopup(this);
4481
pop.show_element(self.field.relation, id, self.build_context(), {
4482
title: _t("Open: ") + self.string,
4483
write_function: function(id, data, options) {
4484
return self.dataset.write(id, data, {}).done(function() {
4485
self.render_value();
4488
alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
4489
parent_view: self.view,
4490
child_name: self.name,
4491
readonly: self.get("effective_readonly")
4495
add_id: function(id) {
4496
this.quick_create.add_id(id);
4500
function m2m_kanban_lazy_init() {
4501
if (instance.web.form.Many2ManyKanbanView)
4503
instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4504
quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
4505
_is_quick_create_enabled: function() {
4506
return this._super() && ! this.group_by;
4509
instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
4510
template: 'Many2ManyKanban.quick_create',
4513
* close_btn: If true, the widget will display a "Close" button able to trigger
4516
init: function(parent, dataset, context, buttons) {
4517
this._super(parent);
4518
this.m2m = this.getParent().view.m2m;
4519
this.m2m.quick_create = this;
4520
this._dataset = dataset;
4521
this._buttons = buttons || false;
4522
this._context = context || {};
4524
start: function () {
4526
self.$text = this.$el.find('input').css("width", "200px");
4527
self.$text.textext({
4528
plugins : 'arrow autocomplete',
4530
render: function(suggestion) {
4531
return $('<span class="text-label"/>').
4532
data('index', suggestion['index']).html(suggestion['label']);
4537
selectFromDropdown: function() {
4538
$(this).trigger('hideDropdown');
4539
var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4540
var data = self.search_result[index];
4542
self.add_id(data.id);
4549
itemToString: function(item) {
4554
}).bind('getSuggestions', function(e, data) {
4556
var str = !!data ? data.query || '' : '';
4557
self.m2m.get_search_result(str).done(function(result) {
4558
self.search_result = result;
4559
$(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4560
return _.extend(el, {index:i});
4564
self.$text.focusout(function() {
4569
this.$text[0].focus();
4571
add_id: function(id) {
4574
self.trigger('added', id);
4575
this.m2m.dataset_changed();
4581
* Class with everything which is common between FormOpenPopup and SelectCreatePopup.
4583
instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
4584
template: "AbstractFormPopup.render",
4587
* -readonly: only applicable when not in creation mode, default to false
4588
* - alternative_form_view
4595
* - form_view_options
4597
init_popup: function(model, row_id, domain, context, options) {
4598
this.row_id = row_id;
4600
this.domain = domain || [];
4601
this.context = context || {};
4602
this.options = options;
4603
_.defaults(this.options, {
4606
init_dataset: function() {
4608
this.created_elements = [];
4609
this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
4610
this.dataset.read_function = this.options.read_function;
4611
this.dataset.create_function = function(data, options, sup) {
4612
var fct = self.options.create_function || sup;
4613
return fct.call(this, data, options).done(function(r) {
4614
self.trigger('create_completed saved', r);
4615
self.created_elements.push(r);
4618
this.dataset.write_function = function(id, data, options, sup) {
4619
var fct = self.options.write_function || sup;
4620
return fct.call(this, id, data, options).done(function(r) {
4621
self.trigger('write_completed saved', r);
4624
this.dataset.parent_view = this.options.parent_view;
4625
this.dataset.child_name = this.options.child_name;
4627
display_popup: function() {
4629
this.renderElement();
4630
var dialog = new instance.web.Dialog(this, {
4632
dialogClass: 'oe_act_window',
4634
self.check_exit(true);
4636
title: this.options.title || "",
4637
}, this.$el).open();
4638
this.$buttonpane = dialog.$buttons;
4641
setup_form_view: function() {
4644
this.dataset.ids = [this.row_id];
4645
this.dataset.index = 0;
4647
this.dataset.index = null;
4649
var options = _.clone(self.options.form_view_options) || {};
4650
if (this.row_id !== null) {
4651
options.initial_mode = this.options.readonly ? "view" : "edit";
4654
$buttons: this.$buttonpane,
4656
this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
4657
if (this.options.alternative_form_view) {
4658
this.view_form.set_embedded_view(this.options.alternative_form_view);
4660
this.view_form.appendTo(this.$el.find(".oe_popup_form"));
4661
this.view_form.on("form_view_loaded", self, function() {
4662
var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
4663
self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
4664
multi_select: multi_select,
4665
readonly: self.row_id !== null && self.options.readonly,
4667
var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
4668
$snbutton.click(function() {
4669
$.when(self.view_form.save()).done(function() {
4670
self.view_form.reload_mutex.exec(function() {
4671
self.view_form.on_button_new();
4675
var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
4676
$sbutton.click(function() {
4677
$.when(self.view_form.save()).done(function() {
4678
self.view_form.reload_mutex.exec(function() {
4683
var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
4684
$cbutton.click(function() {
4685
self.view_form.trigger('on_button_cancel');
4688
self.view_form.do_show();
4691
select_elements: function(element_ids) {
4692
this.trigger("elements_selected", element_ids);
4694
check_exit: function(no_destroy) {
4695
if (this.created_elements.length > 0) {
4696
this.select_elements(this.created_elements);
4697
this.created_elements = [];
4699
this.trigger('closed');
4702
destroy: function () {
4703
this.trigger('closed');
4704
if (this.$el.is(":data(dialog)")) {
4705
this.$el.dialog('close');
4712
* Class to display a popup containing a form view.
4714
instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
4715
show_element: function(model, row_id, context, options) {
4716
this.init_popup(model, row_id, [], context, options);
4717
_.defaults(this.options, {
4719
this.display_popup();
4723
this.init_dataset();
4724
this.setup_form_view();
4729
* Class to display a popup to display a list to search a row. It also allows
4730
* to switch to a form view to create a new row.
4732
instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
4736
* - initial_view: form or search (default search)
4737
* - disable_multiple_selection
4738
* - list_view_options
4740
select_element: function(model, options, domain, context) {
4741
this.init_popup(model, null, domain, context, options);
4743
_.defaults(this.options, {
4744
initial_view: "search",
4746
this.initial_ids = this.options.initial_ids;
4747
this.display_popup();
4751
this.init_dataset();
4752
if (this.options.initial_view == "search") {
4753
instance.web.pyeval.eval_domains_and_contexts({
4755
contexts: [this.context]
4756
}).done(function (results) {
4757
var search_defaults = {};
4758
_.each(results.context, function (value_, key) {
4759
var match = /^search_default_(.*)$/.exec(key);
4761
search_defaults[match[1]] = value_;
4764
self.setup_search_view(search_defaults);
4770
setup_search_view: function(search_defaults) {
4772
if (this.searchview) {
4773
this.searchview.destroy();
4775
this.searchview = new instance.web.SearchView(this,
4776
this.dataset, false, search_defaults);
4777
this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
4778
if (self.initial_ids) {
4779
self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
4780
contexts, groupbys);
4781
self.initial_ids = undefined;
4783
self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
4786
this.searchview.on("search_view_loaded", self, function() {
4787
self.view_list = new instance.web.form.SelectCreateListView(self,
4788
self.dataset, false,
4789
_.extend({'deletable': false,
4790
'selectable': !self.options.disable_multiple_selection,
4791
'import_enabled': false,
4792
'$buttons': self.$buttonpane,
4793
'disable_editable_mode': true,
4794
'$pager': self.$('.oe_popup_list_pager'),
4795
}, self.options.list_view_options || {}));
4796
self.view_list.on('edit:before', self, function (e) {
4799
self.view_list.popup = self;
4800
self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
4801
self.view_list.do_show();
4802
}).then(function() {
4803
self.searchview.do_search();
4805
self.view_list.on("list_view_loaded", self, function() {
4806
self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
4807
var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
4808
$cbutton.click(function() {
4811
var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
4812
$sbutton.click(function() {
4813
self.select_elements(self.selected_ids);
4816
var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
4817
$cbutton.click(function() {
4822
this.searchview.appendTo($(".oe_popup_search", self.$el));
4824
do_search: function(domains, contexts, groupbys) {
4826
instance.web.pyeval.eval_domains_and_contexts({
4827
domains: domains || [],
4828
contexts: contexts || [],
4829
group_by_seq: groupbys || []
4830
}).done(function (results) {
4831
self.view_list.do_search(results.domain, results.context, results.group_by);
4834
on_click_element: function(ids) {
4836
this.selected_ids = ids || [];
4837
if(this.selected_ids.length > 0) {
4838
self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
4840
self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
4843
new_object: function() {
4844
if (this.searchview) {
4845
this.searchview.hide();
4847
if (this.view_list) {
4848
this.view_list.do_hide();
4850
this.setup_form_view();
4854
instance.web.form.SelectCreateListView = instance.web.ListView.extend({
4855
do_add_record: function () {
4856
this.popup.new_object();
4858
select_record: function(index) {
4859
this.popup.select_elements([this.dataset.ids[index]]);
4860
this.popup.destroy();
4862
do_select: function(ids, records) {
4863
this._super(ids, records);
4864
this.popup.on_click_element(ids);
4868
instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4869
template: 'FieldReference',
4870
init: function(field_manager, node) {
4871
this._super(field_manager, node);
4872
this.reference_ready = true;
4874
destroy_content: function() {
4877
this.fm = undefined;
4880
initialize_content: function() {
4882
var fm = new instance.web.form.DefaultFieldManager(this);
4884
fm.extend_field_desc({
4886
selection: this.field_manager.get_field_desc(this.name).selection,
4894
this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
4896
modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
4898
this.selection.on("change:value", this, this.on_selection_changed);
4899
this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
4901
.on('focused', null, function () {self.trigger('focused')})
4902
.on('blurred', null, function () {self.trigger('blurred')});
4904
this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
4906
modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
4908
this.m2o.on("change:value", this, this.data_changed);
4909
this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
4911
.on('focused', null, function () {self.trigger('focused')})
4912
.on('blurred', null, function () {self.trigger('blurred')});
4914
on_selection_changed: function() {
4915
if (this.reference_ready) {
4916
this.internal_set_value([this.selection.get_value(), false]);
4917
this.render_value();
4920
data_changed: function() {
4921
if (this.reference_ready) {
4922
this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
4925
set_value: function(val) {
4927
val = val.split(',');
4928
val[0] = val[0] || false;
4929
val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
4931
this._super(val || [false, false]);
4933
get_value: function() {
4934
return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
4936
render_value: function() {
4937
this.reference_ready = false;
4938
if (!this.get("effective_readonly")) {
4939
this.selection.set_value(this.get('value')[0]);
4941
this.m2o.field.relation = this.get('value')[0];
4942
this.m2o.set_value(this.get('value')[1]);
4943
this.m2o.$el.toggle(!!this.get('value')[0]);
4944
this.reference_ready = true;
4948
instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4949
init: function(field_manager, node) {
4951
this._super(field_manager, node);
4952
this.binary_value = false;
4953
this.useFileAPI = !!window.FileReader;
4954
this.max_upload_size = 25 * 1024 * 1024; // 25Mo
4955
if (!this.useFileAPI) {
4956
this.fileupload_id = _.uniqueId('oe_fileupload');
4957
$(window).on(this.fileupload_id, function() {
4958
var args = [].slice.call(arguments).slice(1);
4959
self.on_file_uploaded.apply(self, args);
4964
if (!this.useFileAPI) {
4965
$(window).off(this.fileupload_id);
4967
this._super.apply(this, arguments);
4969
initialize_content: function() {
4970
this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
4971
this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
4972
this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
4974
on_file_change: function(e) {
4976
var file_node = e.target;
4977
if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
4978
if (this.useFileAPI) {
4979
var file = file_node.files[0];
4980
if (file.size > this.max_upload_size) {
4981
var msg = _t("The selected file exceed the maximum file size of %s.");
4982
instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
4985
var filereader = new FileReader();
4986
filereader.readAsDataURL(file);
4987
filereader.onloadend = function(upload) {
4988
var data = upload.target.result;
4989
data = data.split(',')[1];
4990
self.on_file_uploaded(file.size, file.name, file.type, data);
4993
this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
4994
this.$el.find('form.oe_form_binary_form').submit();
4996
this.$el.find('.oe_form_binary_progress').show();
4997
this.$el.find('.oe_form_binary').hide();
5000
on_file_uploaded: function(size, name, content_type, file_base64) {
5001
if (size === false) {
5002
this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5003
// TODO: use openerp web crashmanager
5004
console.warn("Error while uploading file : ", name);
5006
this.filename = name;
5007
this.on_file_uploaded_and_valid.apply(this, arguments);
5009
this.$el.find('.oe_form_binary_progress').hide();
5010
this.$el.find('.oe_form_binary').show();
5012
on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5014
on_save_as: function(ev) {
5015
var value = this.get('value');
5017
this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5018
ev.stopPropagation();
5020
instance.web.blockUI();
5021
var c = instance.webclient.crashmanager;
5022
this.session.get_file({
5023
url: '/web/binary/saveas_ajax',
5024
data: {data: JSON.stringify({
5025
model: this.view.dataset.model,
5026
id: (this.view.datarecord.id || ''),
5028
filename_field: (this.node.attrs.filename || ''),
5029
data: instance.web.form.is_bin_size(value) ? null : value,
5030
context: this.view.dataset.get_context()
5032
complete: instance.web.unblockUI,
5033
error: c.rpc_error.bind(c)
5035
ev.stopPropagation();
5039
set_filename: function(value) {
5040
var filename = this.node.attrs.filename;
5043
tmp[filename] = value;
5044
this.field_manager.set_values(tmp);
5047
on_clear: function() {
5048
if (this.get('value') !== false) {
5049
this.binary_value = false;
5050
this.internal_set_value(false);
5056
instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5057
template: 'FieldBinaryFile',
5058
initialize_content: function() {
5060
if (this.get("effective_readonly")) {
5062
this.$el.find('a').click(function(ev) {
5063
if (self.get('value')) {
5064
self.on_save_as(ev);
5070
render_value: function() {
5071
if (!this.get("effective_readonly")) {
5073
if (this.node.attrs.filename) {
5074
show_value = this.view.datarecord[this.node.attrs.filename] || '';
5076
show_value = (this.get('value') != null && this.get('value') !== false) ? this.get('value') : '';
5078
this.$el.find('input').eq(0).val(show_value);
5080
this.$el.find('a').toggle(!!this.get('value'));
5081
if (this.get('value')) {
5082
var show_value = _t("Download")
5084
show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5085
this.$el.find('a').text(show_value);
5089
on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5090
this.binary_value = true;
5091
this.internal_set_value(file_base64);
5092
var show_value = name + " (" + instance.web.human_size(size) + ")";
5093
this.$el.find('input').eq(0).val(show_value);
5094
this.set_filename(name);
5096
on_clear: function() {
5097
this._super.apply(this, arguments);
5098
this.$el.find('input').eq(0).val('');
5099
this.set_filename('');
5103
instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5104
template: 'FieldBinaryImage',
5105
placeholder: "/web/static/src/img/placeholder.png",
5106
render_value: function() {
5109
if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5110
url = 'data:image/png;base64,' + this.get('value');
5111
} else if (this.get('value')) {
5112
var id = JSON.stringify(this.view.datarecord.id || null);
5113
var field = this.name;
5114
if (this.options.preview_image)
5115
field = this.options.preview_image;
5116
url = this.session.url('/web/binary/image', {
5117
model: this.view.dataset.model,
5120
t: (new Date().getTime()),
5123
url = this.placeholder;
5125
var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5126
this.$el.find('> img').remove();
5127
this.$el.prepend($img);
5128
$img.load(function() {
5129
if (! self.options.size)
5131
$img.css("max-width", "" + self.options.size[0] + "px");
5132
$img.css("max-height", "" + self.options.size[1] + "px");
5133
$img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px");
5134
$img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px");
5136
$img.on('error', function() {
5137
$img.attr('src', self.placeholder);
5138
instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5141
on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5142
this.internal_set_value(file_base64);
5143
this.binary_value = true;
5144
this.render_value();
5145
this.set_filename(name);
5147
on_clear: function() {
5148
this._super.apply(this, arguments);
5149
this.render_value();
5150
this.set_filename('');
5155
* Widget for (one2many field) to upload one or more file in same time and display in list.
5156
* The user can delete his files.
5157
* Options on attribute ; "blockui" {Boolean} block the UI or not
5158
* during the file is uploading
5160
instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
5161
template: "FieldBinaryFileUploader",
5162
init: function(field_manager, node) {
5163
this._super(field_manager, node);
5164
this.field_manager = field_manager;
5166
if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5167
throw _.str.sprintf(_t("The type of the field '%s' must be a many2many field with a relation to 'ir.attachment' model."), this.field.string);
5169
this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5170
this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5171
$(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5175
this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5177
set_value: function(value_) {
5178
var value_ = value_ || [];
5181
_.each(value_, function(command) {
5182
if (isNaN(command) && command.id == undefined) {
5183
switch (command[0]) {
5184
case commands.CREATE:
5185
ids = ids.concat(command[2]);
5187
case commands.REPLACE_WITH:
5188
ids = ids.concat(command[2]);
5190
case commands.UPDATE:
5191
ids = ids.concat(command[2]);
5193
case commands.LINK_TO:
5194
ids = ids.concat(command[1]);
5196
case commands.DELETE:
5197
ids = _.filter(ids, function (id) { return id != command[1];});
5199
case commands.DELETE_ALL:
5209
get_value: function() {
5210
return _.map(this.get('value'), function (value) { return commands.link_to( isNaN(value) ? value.id : value ); });
5212
get_file_url: function (attachment) {
5213
return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5215
read_name_values : function () {
5217
// select the list of id for a get_name
5219
_.each(this.get('value'), function (val) {
5220
if (typeof val != 'object') {
5224
// send request for get_name
5225
if (values.length) {
5226
return this.ds_file.call('read', [values, ['id', 'name', 'datas_fname']]).done(function (datas) {
5227
_.each(datas, function (data) {
5228
data.no_unlink = true;
5229
data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5231
_.each(self.get('value'), function (val, key) {
5232
if(val == data.id) {
5233
self.get('value')[key] = data;
5239
return $.when(this.get('value'));
5242
render_value: function () {
5244
this.read_name_values().then(function (datas) {
5246
var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self}));
5247
render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5248
self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5250
// reinit input type file
5251
var $input = self.$('input.oe_form_binary_file');
5252
$input.after($input.clone(true)).remove();
5253
self.$(".oe_fileupload").show();
5257
on_file_change: function (event) {
5258
event.stopPropagation();
5260
var $target = $(event.target);
5261
if ($target.val() !== '') {
5263
var filename = $target.val().replace(/.*[\\\/]/,'');
5265
// if the files is currently uploded, don't send again
5266
if( !isNaN(_.find(this.get('value'), function (file) { return (file.filename || file.name) == filename && file.upload; } )) ) {
5271
if(this.node.attrs.blockui>0) {
5272
instance.web.blockUI();
5275
// if the files exits for this answer, delete the file before upload
5276
var files = _.filter(this.get('value'), function (file) {
5277
if((file.filename || file.name) == filename) {
5278
self.ds_file.unlink([file.id]);
5285
// TODO : unactivate send on wizard and form
5288
this.$('form.oe_form_binary_form').submit();
5289
this.$(".oe_fileupload").hide();
5291
// add file on result
5295
'filename': filename,
5300
this.set({'value': files});
5303
on_file_loaded: function (event, result) {
5304
var files = this.get('value');
5307
if(this.node.attrs.blockui>0) {
5308
instance.web.unblockUI();
5311
// TODO : activate send on wizard and form
5313
if (result.error || !result.id ) {
5314
this.do_warn( _t('Uploading Error'), result.error);
5315
files = _.filter(files, function (val) { return !val.upload; });
5317
for(var i in files){
5318
if(files[i].filename == result.filename && files[i].upload) {
5321
'name': result.name,
5322
'filename': result.filename,
5323
'url': this.get_file_url(result)
5329
this.set({'value': files});
5332
on_file_delete: function (event) {
5333
event.stopPropagation();
5334
var file_id=$(event.target).data("id");
5337
for(var i in this.get('value')){
5338
if(file_id != this.get('value')[i].id){
5339
files.push(this.get('value')[i]);
5341
else if(!this.get('value')[i].no_unlink) {
5342
this.ds_file.unlink([file_id]);
5345
this.set({'value': files});
5350
instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5351
template: "FieldStatus",
5352
init: function(field_manager, node) {
5353
this._super(field_manager, node);
5354
this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5355
this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5356
this.set({value: false});
5357
this.selection = [];
5358
this.set("selection", []);
5359
this.selection_dm = new instance.web.DropMisordered();
5362
this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5364
this.on("change:value", this, this.get_selection);
5365
this.on("change:evaluated_selection_domain", this, this.get_selection);
5366
this.on("change:selection", this, function() {
5367
this.selection = this.get("selection");
5368
this.render_value();
5370
this.get_selection();
5371
if (this.options.clickable) {
5372
this.$el.on('click','li',this.on_click_stage);
5374
if (this.$el.parent().is('header')) {
5375
this.$el.after('<div class="oe_clear"/>');
5379
set_value: function(value_) {
5380
if (value_ instanceof Array) {
5383
this._super(value_);
5385
render_value: function() {
5387
var content = QWeb.render("FieldStatus.content", {widget: self});
5388
self.$el.html(content);
5389
var colors = JSON.parse((self.node.attrs || {}).statusbar_colors || "{}");
5390
var color = colors[self.get('value')];
5392
self.$("oe_active").css("color", color);
5395
calc_domain: function() {
5396
var d = instance.web.pyeval.eval('domain', this.build_domain());
5397
var domain = []; //if there is no domain defined, fetch all the records
5400
domain = ['|',['id', '=', this.get('value')]].concat(d);
5403
if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5404
this.set("evaluated_selection_domain", domain);
5407
/** Get the selection and render it
5408
* selection: [[identifier, value_to_display], ...]
5409
* For selection fields: this is directly given by this.field.selection
5410
* For many2one fields: perform a search on the relation of the many2one field
5412
get_selection: function() {
5416
var calculation = _.bind(function() {
5417
if (this.field.type == "many2one") {
5419
var ds = new instance.web.DataSetSearch(this, this.field.relation,
5420
self.build_context(), this.get("evaluated_selection_domain"));
5421
return ds.read_slice(['name'], {}).then(function (records) {
5422
for(var i = 0; i < records.length; i++) {
5423
selection.push([records[i].id, records[i].name]);
5427
// For field type selection filter values according to
5428
// statusbar_visible attribute of the field. For example:
5429
// statusbar_visible="draft,open".
5430
var select = this.field.selection;
5431
for(var i=0; i < select.length; i++) {
5432
var key = select[i][0];
5433
if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5434
selection.push(select[i]);
5440
this.selection_dm.add(calculation()).then(function () {
5441
if (! _.isEqual(selection, self.get("selection"))) {
5442
self.set("selection", selection);
5446
on_click_stage: function (ev) {
5448
var $li = $(ev.currentTarget);
5449
var val = parseInt($li.data("id"));
5450
if (val != self.get('value')) {
5451
this.view.recursive_save().done(function() {
5453
change[self.name] = val;
5454
self.view.dataset.write(self.view.datarecord.id, change).done(function() {
5462
instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
5463
template: "FieldMonetary",
5464
widget_class: 'oe_form_field_float oe_form_field_monetary',
5466
this._super.apply(this, arguments);
5467
this.set({"currency": false});
5468
if (this.options.currency_field) {
5469
this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
5470
this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
5473
this.on("change:currency", this, this.get_currency_info);
5474
this.get_currency_info();
5475
this.ci_dm = new instance.web.DropMisordered();
5478
var tmp = this._super();
5479
this.on("change:currency_info", this, this.reinitialize);
5482
get_currency_info: function() {
5484
if (this.get("currency") === false) {
5485
this.set({"currency_info": null});
5488
return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
5489
.filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
5490
self.set({"currency_info": res});
5493
parse_value: function(val, def) {
5494
return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5496
format_value: function(val, def) {
5497
return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5502
* Registry of form fields, called by :js:`instance.web.FormView`.
5504
* All referenced classes must implement FieldInterface. Those represent the classes whose instances
5505
* will substitute to the <field> tags as defined in OpenERP's views.
5507
instance.web.form.widgets = new instance.web.Registry({
5508
'char' : 'instance.web.form.FieldChar',
5509
'id' : 'instance.web.form.FieldID',
5510
'email' : 'instance.web.form.FieldEmail',
5511
'url' : 'instance.web.form.FieldUrl',
5512
'text' : 'instance.web.form.FieldText',
5513
'html' : 'instance.web.form.FieldTextHtml',
5514
'date' : 'instance.web.form.FieldDate',
5515
'datetime' : 'instance.web.form.FieldDatetime',
5516
'selection' : 'instance.web.form.FieldSelection',
5517
'many2one' : 'instance.web.form.FieldMany2One',
5518
'many2onebutton' : 'instance.web.form.Many2OneButton',
5519
'many2many' : 'instance.web.form.FieldMany2Many',
5520
'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
5521
'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
5522
'one2many' : 'instance.web.form.FieldOne2Many',
5523
'one2many_list' : 'instance.web.form.FieldOne2Many',
5524
'reference' : 'instance.web.form.FieldReference',
5525
'boolean' : 'instance.web.form.FieldBoolean',
5526
'float' : 'instance.web.form.FieldFloat',
5527
'integer': 'instance.web.form.FieldFloat',
5528
'float_time': 'instance.web.form.FieldFloat',
5529
'progressbar': 'instance.web.form.FieldProgressBar',
5530
'image': 'instance.web.form.FieldBinaryImage',
5531
'binary': 'instance.web.form.FieldBinaryFile',
5532
'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
5533
'statusbar': 'instance.web.form.FieldStatus',
5534
'monetary': 'instance.web.form.FieldMonetary',
5538
* Registry of widgets usable in the form view that can substitute to any possible
5539
* tags defined in OpenERP's form views.
5541
* Every referenced class should extend FormWidget.
5543
instance.web.form.tags = new instance.web.Registry({
5544
'button' : 'instance.web.form.WidgetButton',
5547
instance.web.form.custom_widgets = new instance.web.Registry({
5552
// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: