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.render_value_defs = [];
121 this.reload_mutex = new $.Mutex();
122 this.__clicked_inside = false;
123 this.__blur_timeout = null;
124 this.rendering_engine = new instance.web.form.FormRenderingEngine(this);
125 self.set({actual_mode: self.options.initial_mode});
126 this.has_been_loaded.done(function() {
127 self.on("change:actual_mode", self, self.check_actual_mode);
128 self.check_actual_mode();
129 self.on("change:actual_mode", self, self.init_pager);
132 self.on("load_record", self, self.load_record);
133 instance.web.bus.on('clear_uncommitted_changes', this, function(e) {
134 if (!this.can_be_discarded()) {
139 view_loading: function(r) {
140 return this.load_form(r);
142 destroy: function() {
143 _.each(this.get_widgets(), function(w) {
144 w.off('focused blurred');
148 this.$el.off('.formBlur');
152 this.$buttons.remove();
155 load_form: function(data) {
158 throw new Error(_t("No data provided."));
161 throw "Form view does not support multiple calls to load_form";
163 this.fields_order = [];
164 this.fields_view = data;
166 this.rendering_engine.set_fields_registry(this.fields_registry);
167 this.rendering_engine.set_tags_registry(this.tags_registry);
168 this.rendering_engine.set_widgets_registry(this.widgets_registry);
169 this.rendering_engine.set_fields_view(data);
170 var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container');
171 this.rendering_engine.render_to($dest);
173 this.$el.on('mousedown.formBlur', function () {
174 self.__clicked_inside = true;
177 this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self}));
178 if (this.options.$buttons) {
179 this.$buttons.appendTo(this.options.$buttons);
181 this.$el.find('.oe_form_buttons').replaceWith(this.$buttons);
183 this.$buttons.on('click', '.oe_form_button_create',
184 this.guard_active(this.on_button_create));
185 this.$buttons.on('click', '.oe_form_button_edit',
186 this.guard_active(this.on_button_edit));
187 this.$buttons.on('click', '.oe_form_button_save',
188 this.guard_active(this.on_button_save));
189 this.$buttons.on('click', '.oe_form_button_cancel',
190 this.guard_active(this.on_button_cancel));
191 if (this.options.footer_to_buttons) {
192 this.$el.find('footer').appendTo(this.$buttons);
195 this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar');
196 if (!this.sidebar && this.options.$sidebar) {
197 this.sidebar = new instance.web.Sidebar(this);
198 this.sidebar.appendTo(this.$sidebar);
199 if (this.fields_view.toolbar) {
200 this.sidebar.add_toolbar(this.fields_view.toolbar);
202 this.sidebar.add_items('other', _.compact([
203 self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete },
204 self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate }
208 this.has_been_loaded.resolve();
210 // Add bounce effect on button 'Edit' when click on readonly page view.
211 this.$el.find(".oe_form_group_row,.oe_form_field,label").on('click', function (e) {
212 if(self.get("actual_mode") == "view") {
213 var $button = self.options.$buttons.find(".oe_form_button_edit");
214 $button.openerpBounce();
216 instance.web.bus.trigger('click', e);
219 //bounce effect on red button when click on statusbar.
220 this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) {
221 if((self.get("actual_mode") == "view")) {
222 var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'});
223 $button.openerpBounce();
227 this.trigger('form_view_loaded', data);
230 widgetFocused: function() {
231 // Clear click flag if used to focus a widget
232 this.__clicked_inside = false;
233 if (this.__blur_timeout) {
234 clearTimeout(this.__blur_timeout);
235 this.__blur_timeout = null;
238 widgetBlurred: function() {
239 if (this.__clicked_inside) {
240 // clicked in an other section of the form (than the currently
241 // focused widget) => just ignore the blurring entirely?
242 this.__clicked_inside = false;
246 // clear timeout, if any
247 this.widgetFocused();
248 this.__blur_timeout = setTimeout(function () {
249 self.trigger('blurred');
253 do_load_state: function(state, warm) {
254 if (state.id && this.datarecord.id != state.id) {
255 if (this.dataset.get_id_index(state.id) === null) {
256 this.dataset.ids.push(state.id);
258 this.dataset.select_id(state.id);
259 this.do_show({ reload: warm });
264 * @param {Object} [options]
265 * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view".
266 * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record
267 * @return {$.Deferred}
269 do_show: function (options) {
271 options = options || {};
273 this.sidebar.$el.show();
276 this.$buttons.show();
278 this.$el.show().css({
280 filter: 'alpha(opacity = 0)'
282 this.$el.add(this.$buttons).removeClass('oe_form_dirty');
284 var shown = this.has_been_loaded;
285 if (options.reload !== false) {
286 shown = shown.then(function() {
287 if (self.dataset.index === null) {
288 // null index means we should start a new record
289 return self.on_button_new();
291 var fields = _.keys(self.fields_view.fields);
292 fields.push('display_name');
293 return self.dataset.read_index(fields, {
294 context: { 'bin_size': true, 'future_display_name' : true }
295 }).then(function(r) {
296 self.trigger('load_record', r);
300 return shown.then(function() {
301 self._actualize_mode(options.mode || self.options.initial_mode);
304 filter: 'alpha(opacity = 100)'
308 do_hide: function () {
310 this.sidebar.$el.hide();
313 this.$buttons.hide();
320 load_record: function(record) {
321 var self = this, set_values = [];
323 this.set({ 'title' : undefined });
324 this.do_warn(_t("Form"), _t("The record could not be found in the database."), true);
325 return $.Deferred().reject();
327 this.datarecord = record;
328 this._actualize_mode();
329 this.set({ 'title' : record.id ? record.display_name : _t("New") });
331 _(this.fields).each(function (field, f) {
332 field._dirty_flag = false;
333 field._inhibit_on_change_flag = true;
334 var result = field.set_value(self.datarecord[f] || false);
335 field._inhibit_on_change_flag = false;
336 set_values.push(result);
338 return $.when.apply(null, set_values).then(function() {
340 // New record: Second pass in order to trigger the onchanges
341 // respecting the fields order defined in the view
342 _.each(self.fields_order, function(field_name) {
343 if (record[field_name] !== undefined) {
344 var field = self.fields[field_name];
345 field._dirty_flag = true;
346 self.do_onchange(field);
350 self.on_form_changed();
351 self.rendering_engine.init_fields();
352 self.is_initialized.resolve();
353 self.do_update_pager(record.id == null);
355 self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
358 self.do_push_state({id:record.id});
360 self.do_push_state({});
362 self.$el.add(self.$buttons).removeClass('oe_form_dirty');
367 * Loads and sets up the default values for the model as the current
370 * @return {$.Deferred}
372 load_defaults: function () {
374 var keys = _.keys(this.fields_view.fields);
376 return this.dataset.default_get(keys).then(function(r) {
377 self.trigger('load_record', r);
380 return self.trigger('load_record', {});
382 on_form_changed: function() {
383 this.trigger("view_content_has_changed");
385 do_notify_change: function() {
386 this.$el.add(this.$buttons).addClass('oe_form_dirty');
388 execute_pager_action: function(action) {
389 if (this.can_be_discarded()) {
392 this.dataset.index = 0;
395 this.dataset.previous();
401 this.dataset.index = this.dataset.ids.length - 1;
405 this.trigger('pager_action_executed');
408 init_pager: function() {
411 this.$pager.remove();
412 if (this.get("actual_mode") === "create")
414 this.$pager = $(QWeb.render("FormView.pager", {'widget':self})).hide();
415 if (this.options.$pager) {
416 this.$pager.appendTo(this.options.$pager);
418 this.$el.find('.oe_form_pager').replaceWith(this.$pager);
420 this.$pager.on('click','a[data-pager-action]',function() {
421 var action = $(this).data('pager-action');
422 self.execute_pager_action(action);
424 this.do_update_pager();
426 do_update_pager: function(hide_index) {
427 this.$pager.toggle(this.dataset.ids.length > 1);
429 $(".oe_form_pager_state", this.$pager).html("");
431 $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
434 parse_on_change: function (on_change, widget) {
436 var onchange = _.str.trim(on_change);
437 var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
439 throw new Error(_.str.sprintf( _t("Wrong on change format: %s"), onchange ));
442 var method = call[1];
443 if (!_.str.trim(call[2])) {
444 return {method: method, args: []}
447 var argument_replacement = {
448 'False': function () {return false;},
449 'True': function () {return true;},
450 'None': function () {return null;},
451 'context': function () {
452 return new instance.web.CompoundContext(
453 self.dataset.get_context(),
454 widget.build_context() ? widget.build_context() : {});
457 var parent_fields = null;
458 var args = _.map(call[2].split(','), function (a, i) {
459 var field = _.str.trim(a);
461 // literal constant or context
462 if (field in argument_replacement) {
463 return argument_replacement[field]();
466 if (/^-?\d+(\.\d+)?$/.test(field)) {
467 return Number(field);
470 if (self.fields[field]) {
471 var value_ = self.fields[field].get_value();
472 return value_ == null ? false : value_;
475 var splitted = field.split('.');
476 if (splitted.length > 1 && _.str.trim(splitted[0]) === "parent" && self.dataset.parent_view) {
477 if (parent_fields === null) {
478 parent_fields = self.dataset.parent_view.get_fields_values();
480 var p_val = parent_fields[_.str.trim(splitted[1])];
481 if (p_val !== undefined) {
482 return p_val == null ? false : p_val;
486 var first_char = field[0], last_char = field[field.length-1];
487 if ((first_char === '"' && last_char === '"')
488 || (first_char === "'" && last_char === "'")) {
489 return field.slice(1, -1);
492 throw new Error("Could not get field with name '" + field +
493 "' for onchange '" + onchange + "'");
501 do_onchange: function(widget, processed) {
503 this.on_change_list = [{widget: widget, processed: processed}].concat(this.on_change_list);
504 return this._process_operations();
506 _process_onchange: function(on_change_obj) {
508 var widget = on_change_obj.widget;
509 var processed = on_change_obj.processed;
512 processed = processed || [];
513 processed.push(widget.name);
514 var on_change = widget.node.attrs.on_change;
516 var change_spec = self.parse_on_change(on_change, widget);
518 if (self.datarecord.id && !instance.web.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) {
519 // In case of a o2m virtual id, we should pass an empty ids list
520 ids.push(self.datarecord.id);
522 def = self.alive(new instance.web.Model(self.dataset.model).call(
523 change_spec.method, [ids].concat(change_spec.args)));
527 return def.then(function(response) {
528 if (widget.field['change_default']) {
529 var fieldname = widget.name;
531 if (response.value && (fieldname in response.value)) {
532 // Use value from onchange if onchange executed
533 value_ = response.value[fieldname];
535 // otherwise get form value for field
536 value_ = self.fields[fieldname].get_value();
538 var condition = fieldname + '=' + value_;
541 return self.alive(new instance.web.Model('ir.values').call(
542 'get_defaults', [self.model, condition]
543 )).then(function (results) {
544 if (!results.length) {
547 if (!response.value) {
550 for(var i=0; i<results.length; ++i) {
551 // [whatever, key, value]
552 var triplet = results[i];
553 response.value[triplet[1]] = triplet[2];
560 }).then(function(response) {
561 return self.on_processed_onchange(response, processed);
565 instance.webclient.crashmanager.show_message(e);
566 return $.Deferred().reject();
569 on_processed_onchange: function(result, processed) {
572 this._internal_set_values(result.value, processed);
574 if (!_.isEmpty(result.warning)) {
575 instance.web.dialog($(QWeb.render("CrashManager.warning", result.warning)), {
576 title:result.warning.title,
579 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
584 var fields = this.fields;
585 _(result.domain).each(function (domain, fieldname) {
586 var field = fields[fieldname];
587 if (!field) { return; }
588 field.node.attrs.domain = domain;
591 return $.Deferred().resolve();
594 instance.webclient.crashmanager.show_message(e);
595 return $.Deferred().reject();
598 _process_operations: function() {
600 return this.mutating_mutex.exec(function() {
602 var on_change_obj = self.on_change_list.shift();
604 return self._process_onchange(on_change_obj).then(function() {
609 _.each(self.fields, function(field) {
610 defs.push(field.commit_value());
612 var args = _.toArray(arguments);
613 return $.when.apply($, defs).then(function() {
614 if (self.on_change_list.length !== 0) {
617 var save_obj = self.save_list.pop();
619 return self._process_save(save_obj).then(function() {
620 save_obj.ret = _.toArray(arguments);
623 save_obj.error = true;
628 self.save_list.pop();
635 _internal_set_values: function(values, exclude) {
636 exclude = exclude || [];
637 for (var f in values) {
638 if (!values.hasOwnProperty(f)) { continue; }
639 var field = this.fields[f];
640 // If field is not defined in the view, just ignore it
642 var value_ = values[f];
643 if (field.get_value() != value_) {
644 field._inhibit_on_change_flag = true;
645 field.set_value(value_);
646 field._inhibit_on_change_flag = false;
647 field._dirty_flag = true;
648 if (!_.contains(exclude, field.name)) {
649 this.do_onchange(field, exclude);
654 this.on_form_changed();
656 set_values: function(values) {
658 return this.mutating_mutex.exec(function() {
659 self._internal_set_values(values);
663 * Ask the view to switch to view 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_view_mode: function() {
667 this._actualize_mode("view");
670 * Ask the view to switch to edit mode if possible. The view may not do it
671 * if the current record is not yet saved. It will then stay in create mode.
673 to_edit_mode: function() {
674 this._actualize_mode("edit");
677 * Ask the view to switch to a precise mode if possible. The view is free to
678 * not respect this command if the state of the dataset is not compatible with
679 * the new mode. For example, it is not possible to switch to edit mode if
680 * the current record is not yet saved in database.
682 * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
683 * undefined the view will test the actual mode to check if it is still consistent
684 * with the dataset state.
686 _actualize_mode: function(switch_to) {
687 var mode = switch_to || this.get("actual_mode");
688 if (! this.datarecord.id) {
690 } else if (mode === "create") {
693 this.render_value_defs = [];
694 this.set({actual_mode: mode});
696 check_actual_mode: function(source, options) {
698 if(this.get("actual_mode") === "view") {
699 self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
700 self.$buttons.find('.oe_form_buttons_edit').hide();
701 self.$buttons.find('.oe_form_buttons_view').show();
702 self.$sidebar.show();
704 self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
705 self.$buttons.find('.oe_form_buttons_edit').show();
706 self.$buttons.find('.oe_form_buttons_view').hide();
707 self.$sidebar.hide();
711 autofocus: function() {
712 if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
713 var fields_order = this.fields_order.slice(0);
714 if (this.default_focus_field) {
715 fields_order.unshift(this.default_focus_field.name);
717 for (var i = 0; i < fields_order.length; i += 1) {
718 var field = this.fields[fields_order[i]];
719 if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
720 if (field.focus() !== false) {
727 on_button_save: function(e) {
729 $(e.target).attr("disabled", true);
730 return this.save().done(function(result) {
731 self.trigger("save", result);
732 self.reload().then(function() {
734 var parent = self.ViewManager.ActionManager.getParent();
736 parent.menu.do_reload_needaction();
739 }).always(function(){
740 $(e.target).attr("disabled", false);
743 on_button_cancel: function(event) {
745 if (this.can_be_discarded()) {
746 if (this.get('actual_mode') === 'create') {
747 this.trigger('history_back');
750 $.when.apply(null, this.render_value_defs).then(function(){
751 self.trigger('load_record', self.datarecord);
755 this.trigger('on_button_cancel');
758 on_button_new: function() {
761 return $.when(this.has_been_loaded).then(function() {
762 if (self.can_be_discarded()) {
763 return self.load_defaults();
767 on_button_edit: function() {
768 return this.to_edit_mode();
770 on_button_create: function() {
771 this.dataset.index = null;
774 on_button_duplicate: function() {
776 return this.has_been_loaded.then(function() {
777 return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
778 self.record_created(new_id);
783 on_button_delete: function() {
785 var def = $.Deferred();
786 this.has_been_loaded.done(function() {
787 if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
788 self.dataset.unlink([self.datarecord.id]).done(function() {
789 if (self.dataset.size()) {
790 self.execute_pager_action('next');
792 self.do_action('history_back');
797 $.async_when().done(function () {
802 return def.promise();
804 can_be_discarded: function() {
805 if (this.$el.is('.oe_form_dirty')) {
806 if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
809 this.$el.removeClass('oe_form_dirty');
814 * Triggers saving the form's record. Chooses between creating a new
815 * record or saving an existing one depending on whether the record
816 * already has an id property.
818 * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
819 * record, should that record be inserted at the start of the dataset (by
820 * default, records are added at the end)
822 save: function(prepend_on_create) {
824 var save_obj = {prepend_on_create: prepend_on_create, ret: null};
825 this.save_list.push(save_obj);
826 return this._process_operations().then(function() {
828 return $.Deferred().reject();
829 return $.when.apply($, save_obj.ret);
831 self.$el.removeClass('oe_form_dirty');
834 _process_save: function(save_obj) {
836 var prepend_on_create = save_obj.prepend_on_create;
838 var form_invalid = false,
840 first_invalid_field = null,
841 readonly_values = {};
842 for (var f in self.fields) {
843 if (!self.fields.hasOwnProperty(f)) { continue; }
847 if (!first_invalid_field) {
848 first_invalid_field = f;
850 } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
851 // Special case 'id' field, do not save this field
852 // on 'create' : save all non readonly fields
853 // on 'edit' : save non readonly modified fields
854 if (!f.get("readonly")) {
855 values[f.name] = f.get_value();
857 readonly_values[f.name] = f.get_value();
862 self.set({'display_invalid_fields': true});
863 first_invalid_field.focus();
865 return $.Deferred().reject();
867 self.set({'display_invalid_fields': false});
869 if (!self.datarecord.id) {
871 save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
872 return self.record_created(r, prepend_on_create);
874 } else if (_.isEmpty(values)) {
875 // Not dirty, noop save
876 save_deferral = $.Deferred().resolve({}).promise();
879 save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
880 return self.record_saved(r);
883 return save_deferral;
887 return $.Deferred().reject();
890 on_invalid: function() {
891 var warnings = _(this.fields).chain()
892 .filter(function (f) { return !f.is_valid(); })
894 return _.str.sprintf('<li>%s</li>',
897 warnings.unshift('<ul>');
898 warnings.push('</ul>');
899 this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
902 * Reload the form after saving
904 * @param {Object} r result of the write function.
906 record_saved: function(r) {
907 this.trigger('record_saved', r);
909 // should not happen in the server, but may happen for internal purpose
910 return $.Deferred().reject();
915 * Updates the form' dataset to contain the new record:
917 * * Adds the newly created record to the current dataset (at the end by
919 * * Selects that record (sets the dataset's index to point to the new
921 * * Updates the pager and sidebar displays
924 * @param {Boolean} [prepend_on_create=false] adds the newly created record
925 * at the beginning of the dataset instead of the end
927 record_created: function(r, prepend_on_create) {
930 // should not happen in the server, but may happen for internal purpose
931 this.trigger('record_created', r);
932 return $.Deferred().reject();
934 this.datarecord.id = r;
935 if (!prepend_on_create) {
936 this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
937 this.dataset.index = this.dataset.ids.length - 1;
939 this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
940 this.dataset.index = 0;
942 this.do_update_pager();
944 this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
946 //openerp.log("The record has been created with id #" + this.datarecord.id);
947 return $.when(this.reload()).then(function () {
948 self.trigger('record_created', r);
949 return _.extend(r, {created: true});
953 on_action: function (action) {
954 console.debug('Executing action', action);
958 return this.reload_mutex.exec(function() {
959 if (self.dataset.index == null) {
960 self.trigger("previous_view");
961 return $.Deferred().reject().promise();
963 if (self.dataset.index == null || self.dataset.index < 0) {
964 return $.when(self.on_button_new());
966 var fields = _.keys(self.fields_view.fields);
967 fields.push('display_name');
968 return self.dataset.read_index(fields,
972 'future_display_name': true
974 check_access_rule: true
975 }).then(function(r) {
976 self.trigger('load_record', r);
978 self.do_action('history_back');
983 get_widgets: function() {
984 return _.filter(this.getChildren(), function(obj) {
985 return obj instanceof instance.web.form.FormWidget;
988 get_fields_values: function() {
990 var ids = this.get_selected_ids();
991 values["id"] = ids.length > 0 ? ids[0] : false;
992 _.each(this.fields, function(value_, key) {
993 values[key] = value_.get_value();
997 get_selected_ids: function() {
998 var id = this.dataset.ids[this.dataset.index];
999 return id ? [id] : [];
1001 recursive_save: function() {
1003 return $.when(this.save()).then(function(res) {
1004 if (self.dataset.parent_view)
1005 return self.dataset.parent_view.recursive_save();
1008 recursive_reload: function() {
1011 if (self.dataset.parent_view)
1012 pre = self.dataset.parent_view.recursive_reload();
1013 return pre.then(function() {
1014 return self.reload();
1017 is_dirty: function() {
1018 return _.any(this.fields, function (value_) {
1019 return value_._dirty_flag;
1022 is_interactible_record: function() {
1023 var id = this.datarecord.id;
1025 if (this.options.not_interactible_on_create)
1027 } else if (typeof(id) === "string") {
1028 if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
1033 sidebar_eval_context: function () {
1034 return $.when(this.build_eval_context());
1036 open_defaults_dialog: function () {
1038 var display = function (field, value) {
1039 if (field instanceof instance.web.form.FieldSelection) {
1040 return _(field.values).find(function (option) {
1041 return option[0] === value;
1043 } else if (field instanceof instance.web.form.FieldMany2One) {
1044 return field.get_displayed();
1048 var fields = _.chain(this.fields)
1049 .map(function (field) {
1050 var value = field.get_value();
1051 // ignore fields which are empty, invisible, readonly, o2m
1054 || field.get('invisible')
1055 || field.get("readonly")
1056 || field.field.type === 'one2many'
1057 || field.field.type === 'many2many'
1058 || field.field.type === 'binary'
1059 || field.password) {
1065 string: field.string,
1067 displayed: display(field, value),
1071 .sortBy(function (field) { return field.string; })
1073 var conditions = _.chain(self.fields)
1074 .filter(function (field) { return field.field.change_default; })
1075 .map(function (field) {
1076 var value = field.get_value();
1079 string: field.string,
1081 displayed: display(field, value),
1086 var d = new instance.web.Dialog(this, {
1087 title: _t("Set Default"),
1090 conditions: conditions
1093 {text: _t("Close"), click: function () { d.close(); }},
1094 {text: _t("Save default"), click: function () {
1095 var $defaults = d.$el.find('#formview_default_fields');
1096 var field_to_set = $defaults.val();
1097 if (!field_to_set) {
1098 $defaults.parent().addClass('oe_form_invalid');
1101 var condition = d.$el.find('#formview_default_conditions').val(),
1102 all_users = d.$el.find('#formview_default_all').is(':checked');
1103 new instance.web.DataSet(self, 'ir.values').call(
1107 self.fields[field_to_set].get_value(),
1111 ]).done(function () { d.close(); });
1115 d.template = 'FormView.set_default';
1118 register_field: function(field, name) {
1119 this.fields[name] = field;
1120 this.fields_order.push(name);
1121 if (JSON.parse(field.node.attrs.default_focus || "0")) {
1122 this.default_focus_field = field;
1125 field.on('focused', null, this.proxy('widgetFocused'))
1126 .on('blurred', null, this.proxy('widgetBlurred'));
1127 if (this.get_field_desc(name).translate) {
1128 this.translatable_fields.push(field);
1130 field.on('changed_value', this, function() {
1131 if (field.is_syntax_valid()) {
1132 this.trigger('field_changed:' + name);
1134 if (field._inhibit_on_change_flag) {
1137 field._dirty_flag = true;
1138 if (field.is_syntax_valid()) {
1139 this.do_onchange(field);
1140 this.on_form_changed(true);
1141 this.do_notify_change();
1145 get_field_desc: function(field_name) {
1146 return this.fields_view.fields[field_name];
1148 get_field_value: function(field_name) {
1149 return this.fields[field_name].get_value();
1151 compute_domain: function(expression) {
1152 return instance.web.form.compute_domain(expression, this.fields);
1154 _build_view_fields_values: function() {
1155 var a_dataset = this.dataset;
1156 var fields_values = this.get_fields_values();
1157 var active_id = a_dataset.ids[a_dataset.index];
1158 _.extend(fields_values, {
1159 active_id: active_id || false,
1160 active_ids: active_id ? [active_id] : [],
1161 active_model: a_dataset.model,
1164 if (a_dataset.parent_view) {
1165 fields_values.parent = a_dataset.parent_view.get_fields_values();
1167 return fields_values;
1169 build_eval_context: function() {
1170 var a_dataset = this.dataset;
1171 return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
1176 * Interface to be implemented by rendering engines for the form view.
1178 instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
1179 set_fields_view: function(fields_view) {},
1180 set_fields_registry: function(fields_registry) {},
1181 render_to: function($el) {},
1185 * Default rendering engine for the form view.
1187 * It is necessary to set the view using set_view() before usage.
1189 instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
1190 init: function(view) {
1193 set_fields_view: function(fvg) {
1195 this.version = parseFloat(this.fvg.arch.attrs.version);
1196 if (isNaN(this.version)) {
1200 set_tags_registry: function(tags_registry) {
1201 this.tags_registry = tags_registry;
1203 set_fields_registry: function(fields_registry) {
1204 this.fields_registry = fields_registry;
1206 set_widgets_registry: function(widgets_registry) {
1207 this.widgets_registry = widgets_registry;
1209 // Backward compatibility tools, current default version: v6.1
1210 process_version: function() {
1211 if (this.version < 7.0) {
1212 this.$form.find('form:first').wrapInner('<group col="4"/>');
1213 this.$form.find('page').each(function() {
1214 if (!$(this).parents('field').length) {
1215 $(this).wrapInner('<group col="4"/>');
1220 get_arch_fragment: function() {
1221 var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
1222 // IE won't allow custom button@type and will revert it to spec default : 'submit'
1223 $('button', doc).each(function() {
1224 $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
1226 // IE's html parser is also a css parser. How convenient...
1227 $('board', doc).each(function() {
1228 $(this).attr('layout', $(this).attr('style'));
1230 return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
1232 render_to: function($target) {
1234 this.$target = $target;
1236 this.$form = this.get_arch_fragment();
1238 this.process_version();
1240 this.fields_to_init = [];
1241 this.tags_to_init = [];
1242 this.widgets_to_init = [];
1244 this.process(this.$form);
1246 this.$form.appendTo(this.$target);
1248 this.to_replace = [];
1250 _.each(this.fields_to_init, function($elem) {
1251 var name = $elem.attr("name");
1252 if (!self.fvg.fields[name]) {
1253 throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
1255 var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
1257 throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
1259 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1260 var $label = self.labels[$elem.attr("name")];
1262 w.set_input_id($label.attr("for"));
1264 self.alter_field(w);
1265 self.view.register_field(w, $elem.attr("name"));
1266 self.to_replace.push([w, $elem]);
1268 _.each(this.tags_to_init, function($elem) {
1269 var tag_name = $elem[0].tagName.toLowerCase();
1270 var obj = self.tags_registry.get_object(tag_name);
1271 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1272 self.to_replace.push([w, $elem]);
1274 _.each(this.widgets_to_init, function($elem) {
1275 var widget_type = $elem.attr("type");
1276 var obj = self.widgets_registry.get_object(widget_type);
1277 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1278 self.to_replace.push([w, $elem]);
1281 init_fields: function() {
1283 _.each(this.to_replace, function(el) {
1284 defs.push(el[0].replace(el[1]));
1286 this.to_replace = [];
1287 return $.when.apply($, defs);
1289 render_element: function(template /* dictionaries */) {
1290 var dicts = [].slice.call(arguments).slice(1);
1291 var dict = _.extend.apply(_, dicts);
1292 dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
1293 return $(QWeb.render(template, dict));
1295 alter_field: function(field) {
1297 toggle_layout_debugging: function() {
1298 if (!this.$target.has('.oe_layout_debug_cell:first').length) {
1299 this.$target.find('[title]').removeAttr('title');
1300 this.$target.find('.oe_form_group_cell').each(function() {
1301 var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
1302 $(this).attr('title', text);
1305 this.$target.toggleClass('oe_layout_debugging');
1307 process: function($tag) {
1309 var tagname = $tag[0].nodeName.toLowerCase();
1310 if (this.tags_registry.contains(tagname)) {
1311 this.tags_to_init.push($tag);
1314 var fn = self['process_' + tagname];
1316 var args = [].slice.call(arguments);
1318 return fn.apply(self, args);
1320 // generic tag handling, just process children
1321 $tag.children().each(function() {
1322 self.process($(this));
1324 self.handle_common_properties($tag, $tag);
1325 $tag.removeAttr("modifiers");
1329 process_widget: function($widget) {
1330 this.widgets_to_init.push($widget);
1333 process_sheet: function($sheet) {
1334 var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
1335 this.handle_common_properties($new_sheet, $sheet);
1336 var $dst = $new_sheet.find('.oe_form_sheet');
1337 $sheet.contents().appendTo($dst);
1338 $sheet.before($new_sheet).remove();
1339 this.process($new_sheet);
1341 process_form: function($form) {
1342 if ($form.find('> sheet').length === 0) {
1343 $form.addClass('oe_form_nosheet');
1345 var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
1346 this.handle_common_properties($new_form, $form);
1347 $form.contents().appendTo($new_form);
1348 if ($form[0] === this.$form[0]) {
1349 // If root element, replace it
1350 this.$form = $new_form;
1352 $form.before($new_form).remove();
1354 this.process($new_form);
1357 * Used by direct <field> children of a <group> tag only
1358 * This method will add the implicit <label...> for every field
1361 preprocess_field: function($field) {
1363 var name = $field.attr('name'),
1364 field_colspan = parseInt($field.attr('colspan'), 10),
1365 field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
1367 if ($field.attr('nolabel') === '1')
1369 $field.attr('nolabel', '1');
1371 this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
1372 $(el).parents().each(function(unused, tag) {
1373 var name = tag.tagName.toLowerCase();
1374 if (name === "field" || name in self.tags_registry.map)
1381 var $label = $('<label/>').attr({
1383 "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
1384 "string": $field.attr('string'),
1385 "help": $field.attr('help'),
1386 "class": $field.attr('class'),
1388 $label.insertBefore($field);
1389 if (field_colspan > 1) {
1390 $field.attr('colspan', field_colspan - 1);
1394 process_field: function($field) {
1395 if ($field.parent().is('group')) {
1396 // No implicit labels for normal fields, only for <group> direct children
1397 var $label = this.preprocess_field($field);
1399 this.process($label);
1402 this.fields_to_init.push($field);
1405 process_group: function($group) {
1407 $group.children('field').each(function() {
1408 self.preprocess_field($(this));
1410 var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
1412 if ($new_group.first().is('table.oe_form_group')) {
1413 $table = $new_group;
1414 } else if ($new_group.filter('table.oe_form_group').length) {
1415 $table = $new_group.filter('table.oe_form_group').first();
1417 $table = $new_group.find('table.oe_form_group').first();
1421 cols = parseInt($group.attr('col') || 2, 10),
1425 $group.children().each(function(a,b,c) {
1426 var $child = $(this);
1427 var colspan = parseInt($child.attr('colspan') || 1, 10);
1428 var tagName = $child[0].tagName.toLowerCase();
1429 var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
1430 var newline = tagName === 'newline';
1432 // Note FME: those classes are used in layout debug mode
1433 if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
1434 $tr.addClass('oe_form_group_row_incomplete');
1436 $tr.addClass('oe_form_group_row_newline');
1443 if (!$tr || row_cols < colspan) {
1444 $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
1446 } else if (tagName==='group') {
1447 // When <group> <group/><group/> </group>, we need a spacing between the two groups
1448 $td.addClass('oe_group_right')
1450 row_cols -= colspan;
1452 // invisibility transfer
1453 var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
1454 var invisible = field_modifiers.invisible;
1455 self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
1457 $tr.append($td.append($child));
1458 children.push($child[0]);
1460 if (row_cols && $td) {
1461 $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
1463 $group.before($new_group).remove();
1465 $table.find('> tbody > tr').each(function() {
1466 var to_compute = [],
1469 $(this).children().each(function() {
1471 $child = $td.children(':first');
1472 if ($child.attr('cell-class')) {
1473 $td.addClass($child.attr('cell-class'));
1475 switch ($child[0].tagName.toLowerCase()) {
1479 if ($child.attr('for')) {
1480 $td.attr('width', '1%').addClass('oe_form_group_cell_label');
1481 row_cols-= $td.attr('colspan') || 1;
1486 var width = _.str.trim($child.attr('width') || ''),
1487 iwidth = parseInt(width, 10);
1489 if (width.substr(-1) === '%') {
1491 width = iwidth + '%';
1494 $td.css('min-width', width + 'px');
1496 $td.attr('width', width);
1497 $child.removeAttr('width');
1498 row_cols-= $td.attr('colspan') || 1;
1500 to_compute.push($td);
1506 var unit = Math.floor(total / row_cols);
1507 if (!$(this).is('.oe_form_group_row_incomplete')) {
1508 _.each(to_compute, function($td, i) {
1509 var width = parseInt($td.attr('colspan'), 10) * unit;
1510 $td.attr('width', width + '%');
1516 _.each(children, function(el) {
1517 self.process($(el));
1519 this.handle_common_properties($new_group, $group);
1522 process_notebook: function($notebook) {
1525 $notebook.find('> page').each(function() {
1526 var $page = $(this);
1527 var page_attrs = $page.getAttributes();
1528 page_attrs.id = _.uniqueId('notebook_page_');
1529 var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
1530 $page.contents().appendTo($new_page);
1531 $page.before($new_page).remove();
1532 var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
1533 page_attrs.__page = $new_page;
1534 page_attrs.__ic = ic;
1535 pages.push(page_attrs);
1537 $new_page.children().each(function() {
1538 self.process($(this));
1541 var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
1542 $notebook.contents().appendTo($new_notebook);
1543 $notebook.before($new_notebook).remove();
1544 self.process($($new_notebook.children()[0]));
1545 //tabs and invisibility handling
1546 $new_notebook.tabs();
1547 _.each(pages, function(page, i) {
1550 page.__ic.on("change:effective_invisible", null, function() {
1551 if (!page.__ic.get('effective_invisible') && page.autofocus) {
1552 $new_notebook.tabs('select', i);
1555 var current = $new_notebook.tabs("option", "selected");
1556 if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
1558 var first_visible = _.find(_.range(pages.length), function(i2) {
1559 return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
1561 if (first_visible !== undefined) {
1562 $new_notebook.tabs('select', first_visible);
1567 this.handle_common_properties($new_notebook, $notebook);
1568 return $new_notebook;
1570 process_separator: function($separator) {
1571 var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
1572 $separator.before($new_separator).remove();
1573 this.handle_common_properties($new_separator, $separator);
1574 return $new_separator;
1576 process_label: function($label) {
1577 var name = $label.attr("for"),
1578 field_orm = this.fvg.fields[name];
1580 string: $label.attr('string') || (field_orm || {}).string || '',
1581 help: $label.attr('help') || (field_orm || {}).help || '',
1582 _for: name ? _.uniqueId('oe-field-input-') : undefined,
1584 var align = parseFloat(dict.align);
1585 if (isNaN(align) || align === 1) {
1587 } else if (align === 0) {
1593 var $new_label = this.render_element('FormRenderingLabel', dict);
1594 $label.before($new_label).remove();
1595 this.handle_common_properties($new_label, $label);
1597 this.labels[name] = $new_label;
1601 handle_common_properties: function($new_element, $node) {
1602 var str_modifiers = $node.attr("modifiers") || "{}";
1603 var modifiers = JSON.parse(str_modifiers);
1605 if (modifiers.invisible !== undefined)
1606 ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
1607 $new_element.addClass($node.attr("class") || "");
1608 $new_element.attr('style', $node.attr('style'));
1609 return {invisibility_changer: ic,};
1616 If you read this documentation, it probably means that you were asked to use a form view widget outside of
1617 a form view. Before going further, you must understand that those fields were never really created for
1618 that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
1619 you to hack the system with more style.
1621 instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
1622 init: function(parent, eval_context) {
1623 this._super(parent);
1624 this.field_descs = {};
1625 this.eval_context = eval_context || {};
1627 display_invalid_fields: false,
1628 actual_mode: 'create',
1631 get_field_desc: function(field_name) {
1632 if (this.field_descs[field_name] === undefined) {
1633 this.field_descs[field_name] = {
1637 return this.field_descs[field_name];
1639 extend_field_desc: function(fields) {
1641 _.each(fields, function(v, k) {
1642 _.extend(self.get_field_desc(k), v);
1645 get_field_value: function(field_name) {
1648 set_values: function(values) {
1651 compute_domain: function(expression) {
1652 return instance.web.form.compute_domain(expression, {});
1654 build_eval_context: function() {
1655 return new instance.web.CompoundContext(this.eval_context);
1659 instance.web.form.compute_domain = function(expr, fields) {
1660 if (! (expr instanceof Array))
1663 for (var i = expr.length - 1; i >= 0; i--) {
1665 if (ex.length == 1) {
1666 var top = stack.pop();
1669 stack.push(stack.pop() || top);
1672 stack.push(stack.pop() && top);
1678 throw new Error(_.str.sprintf(
1679 _t("Unknown operator %s in domain %s"),
1680 ex, JSON.stringify(expr)));
1684 var field = fields[ex[0]];
1686 throw new Error(_.str.sprintf(
1687 _t("Unknown field %s in domain %s"),
1688 ex[0], JSON.stringify(expr)));
1690 var field_value = field.get_value ? field.get_value() : field.value;
1694 switch (op.toLowerCase()) {
1697 stack.push(_.isEqual(field_value, val));
1701 stack.push(!_.isEqual(field_value, val));
1704 stack.push(field_value < val);
1707 stack.push(field_value > val);
1710 stack.push(field_value <= val);
1713 stack.push(field_value >= val);
1716 if (!_.isArray(val)) val = [val];
1717 stack.push(_(val).contains(field_value));
1720 if (!_.isArray(val)) val = [val];
1721 stack.push(!_(val).contains(field_value));
1725 _t("Unsupported operator %s in domain %s"),
1726 op, JSON.stringify(expr));
1729 return _.all(stack, _.identity);
1732 instance.web.form.is_bin_size = function(v) {
1733 return /^\d+(\.\d*)? \w+$/.test(v);
1737 * Must be applied over an class already possessing the PropertiesMixin.
1739 * Apply the result of the "invisible" domain to this.$el.
1741 instance.web.form.InvisibilityChangerMixin = {
1742 init: function(field_manager, invisible_domain) {
1744 this._ic_field_manager = field_manager;
1745 this._ic_invisible_modifier = invisible_domain;
1746 this._ic_field_manager.on("view_content_has_changed", this, function() {
1747 var result = self._ic_invisible_modifier === undefined ? false :
1748 self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
1749 self.set({"invisible": result});
1751 this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
1752 var check = function() {
1753 if (self.get("invisible") || self.get('force_invisible')) {
1754 self.set({"effective_invisible": true});
1756 self.set({"effective_invisible": false});
1759 this.on('change:invisible', this, check);
1760 this.on('change:force_invisible', this, check);
1764 this.on("change:effective_invisible", this, this._check_visibility);
1765 this._check_visibility();
1767 _check_visibility: function() {
1768 this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
1772 instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
1773 init: function(parent, field_manager, invisible_domain, $el) {
1774 this.setParent(parent);
1775 instance.web.PropertiesMixin.init.call(this);
1776 instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
1783 Base class for all fields, custom widgets and buttons to be displayed in the form view.
1786 - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
1787 the values of the "readonly" property and the "mode" property on the field manager.
1789 instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
1791 * @constructs instance.web.form.FormWidget
1792 * @extends instance.web.Widget
1794 * @param field_manager
1797 init: function(field_manager, node) {
1798 this._super(field_manager);
1799 this.field_manager = field_manager;
1800 if (this.field_manager instanceof instance.web.FormView)
1801 this.view = this.field_manager;
1803 this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
1804 instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
1806 this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
1812 // some events to make the property "effective_readonly" sync automatically with "readonly" and
1813 // "mode" on field_manager
1815 var test_effective_readonly = function() {
1816 self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
1818 this.on("change:readonly", this, test_effective_readonly);
1819 this.field_manager.on("change:actual_mode", this, test_effective_readonly);
1820 test_effective_readonly.call(this);
1822 renderElement: function() {
1823 this.process_modifiers();
1825 this.$el.addClass(this.node.attrs["class"] || "");
1827 destroy: function() {
1829 this._super.apply(this, arguments);
1832 * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
1834 * This method is an utility method that is meant to be called by child classes.
1836 * @param {jQuery} $e jQuery object of elements to bind focus/blur on
1838 setupFocus: function ($e) {
1841 focus: function () { self.trigger('focused'); },
1842 blur: function () { self.trigger('blurred'); }
1845 process_modifiers: function() {
1847 for (var a in this.modifiers) {
1848 if (!this.modifiers.hasOwnProperty(a)) { continue; }
1849 if (!_.include(["invisible"], a)) {
1850 var val = this.field_manager.compute_domain(this.modifiers[a]);
1856 do_attach_tooltip: function(widget, trigger, options) {
1857 widget = widget || this;
1858 trigger = trigger || this.$el;
1859 options = _.extend({
1864 var template = widget.template + '.tooltip';
1865 if (!QWeb.has_template(template)) {
1866 template = 'WidgetLabel.tooltip';
1868 return QWeb.render(template, {
1869 debug: instance.session.debug,
1872 gravity: $.fn.tipsy.autoBounds(50, 'nw'),
1877 $(trigger).tipsy(options);
1880 * Builds a new context usable for operations related to fields by merging
1881 * the fields'context with the action's context.
1883 build_context: function() {
1884 // only use the model's context if there is not context on the node
1885 var v_context = this.node.attrs.context;
1887 v_context = (this.field || {}).context || {};
1890 if (v_context.__ref || true) { //TODO: remove true
1891 var fields_values = this.field_manager.build_eval_context();
1892 v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
1896 build_domain: function() {
1897 var f_domain = this.field.domain || [];
1898 var n_domain = this.node.attrs.domain || null;
1899 // if there is a domain on the node, overrides the model's domain
1900 var final_domain = n_domain !== null ? n_domain : f_domain;
1901 if (!(final_domain instanceof Array) || true) { //TODO: remove true
1902 var fields_values = this.field_manager.build_eval_context();
1903 final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
1905 return final_domain;
1909 instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
1910 template: 'WidgetButton',
1911 init: function(field_manager, node) {
1912 node.attrs.type = node.attrs['data-button-type'];
1913 this._super(field_manager, node);
1914 this.force_disabled = false;
1915 this.string = (this.node.attrs.string || '').replace(/_/g, '');
1916 if (JSON.parse(this.node.attrs.default_focus || "0")) {
1917 // TODO fme: provide enter key binding to widgets
1918 this.view.default_focus_button = this;
1920 if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
1921 this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
1925 this._super.apply(this, arguments);
1926 this.view.on('view_content_has_changed', this, this.check_disable);
1927 this.check_disable();
1928 this.$el.click(this.on_click);
1929 if (this.node.attrs.help || instance.session.debug) {
1930 this.do_attach_tooltip();
1932 this.setupFocus(this.$el);
1934 on_click: function() {
1936 this.force_disabled = true;
1937 this.check_disable();
1938 this.execute_action().always(function() {
1939 self.force_disabled = false;
1940 self.check_disable();
1943 execute_action: function() {
1945 var exec_action = function() {
1946 if (self.node.attrs.confirm) {
1947 var def = $.Deferred();
1948 var dialog = instance.web.dialog($('<div/>').text(self.node.attrs.confirm), {
1949 title: _t('Confirm'),
1952 {text: _t("Cancel"), click: function() {
1953 $(this).dialog("close");
1956 {text: _t("Ok"), click: function() {
1958 self.on_confirmed().always(function() {
1959 $(self2).dialog("close");
1964 beforeClose: function() {
1968 return def.promise();
1970 return self.on_confirmed();
1973 if (!this.node.attrs.special) {
1974 return this.view.recursive_save().then(exec_action);
1976 return exec_action();
1979 on_confirmed: function() {
1982 var context = this.build_context();
1984 return this.view.do_execute_action(
1985 _.extend({}, this.node.attrs, {context: context}),
1986 this.view.dataset, this.view.datarecord.id, function (reason) {
1987 if (!_.isObject(reason)) {
1988 self.view.recursive_reload();
1992 check_disable: function() {
1993 var disabled = (this.force_disabled || !this.view.is_interactible_record());
1994 this.$el.prop('disabled', disabled);
1995 this.$el.css('color', disabled ? 'grey' : '');
2000 * Interface to be implemented by fields.
2003 * - changed_value: triggered when the value of the field has changed. This can be due
2004 * to a user interaction or a call to set_value().
2007 instance.web.form.FieldInterface = {
2009 * Constructor takes 2 arguments:
2010 * - field_manager: Implements FieldManagerMixin
2011 * - node: the "<field>" node in json form
2013 init: function(field_manager, node) {},
2015 * Called by the form view to indicate the value of the field.
2017 * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
2018 * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
2019 * before the widget is inserted into the DOM.
2021 * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
2022 * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
2023 * also be able to handle any other format commonly used in the _defaults key on the models in the addons
2024 * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
2025 * no information is ever given to know which format is used.
2027 set_value: function(value_) {},
2029 * Get the current value of the widget.
2031 * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
2032 * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
2033 * For example if the field is marked as "required", a call to get_value() can return false.
2035 * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
2036 * return a default value according to the type of field.
2038 * This method is always assumed to perform synchronously, it can not return a promise.
2040 * If there was no user interaction to modify the value of the field, it is always assumed that
2041 * get_value() return the same semantic value than the one passed in the last call to set_value(),
2042 * although the syntax can be different. This can be the case for type of fields that have a different
2043 * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
2045 get_value: function() {},
2047 * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
2050 set_input_id: function(id) {},
2052 * Returns true if is_syntax_valid() returns true and the value is semantically
2053 * valid too according to the semantic restrictions applied to the field.
2055 is_valid: function() {},
2057 * Returns true if the field holds a value which is syntactically correct, ignoring
2058 * the potential semantic restrictions applied to the field.
2060 is_syntax_valid: function() {},
2062 * Must set the focus on the field. Return false if field is not focusable.
2064 focus: function() {},
2066 * Called when the translate button is clicked.
2068 on_translate: function() {},
2070 This method is called by the form view before reading on_change values and before saving. It tells
2071 the field to save its value before reading it using get_value(). Must return a promise.
2073 commit_value: function() {},
2077 * Abstract class for classes implementing FieldInterface.
2080 * - value: useful property to hold the value of the field. By default, set_value() and get_value()
2081 * set and retrieve the value property. Changing the value property also triggers automatically
2082 * a 'changed_value' event that inform the view to trigger on_changes.
2085 instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
2087 * @constructs instance.web.form.AbstractField
2088 * @extends instance.web.form.FormWidget
2090 * @param field_manager
2093 init: function(field_manager, node) {
2095 this._super(field_manager, node);
2096 this.name = this.node.attrs.name;
2097 this.field = this.field_manager.get_field_desc(this.name);
2098 this.widget = this.node.attrs.widget;
2099 this.string = this.node.attrs.string || this.field.string || this.name;
2100 this.options = instance.web.py_eval(this.node.attrs.options || '{}');
2101 this.set({'value': false});
2103 this.on("change:value", this, function() {
2104 this.trigger('changed_value');
2105 this._check_css_flags();
2108 renderElement: function() {
2111 if (this.field.translate && this.view) {
2112 this.$el.addClass('oe_form_field_translatable');
2113 this.$el.find('.oe_field_translate').click(this.on_translate);
2115 this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
2116 if (instance.session.debug) {
2117 this.do_attach_tooltip(this, this.$label[0] || this.$el);
2118 this.$label.off('dblclick').on('dblclick', function() {
2119 console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
2121 console.log("window.w =", window.w);
2124 if (!this.disable_utility_classes) {
2125 this.off("change:required", this, this._set_required);
2126 this.on("change:required", this, this._set_required);
2127 this._set_required();
2129 this._check_visibility();
2130 this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
2131 this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
2132 this._check_css_flags();
2135 var tmp = this._super();
2136 this.on("change:value", this, function() {
2137 if (! this.no_rerender)
2138 this.render_value();
2140 this.render_value();
2143 * Private. Do not use.
2145 _set_required: function() {
2146 this.$el.toggleClass('oe_form_required', this.get("required"));
2148 set_value: function(value_) {
2149 this.set({'value': value_});
2151 get_value: function() {
2152 return this.get('value');
2155 Utility method that all implementations should use to change the
2156 value without triggering a re-rendering.
2158 internal_set_value: function(value_) {
2159 var tmp = this.no_rerender;
2160 this.no_rerender = true;
2161 this.set({'value': value_});
2162 this.no_rerender = tmp;
2165 This method is called each time the value is modified.
2167 render_value: function() {},
2168 is_valid: function() {
2169 return this.is_syntax_valid() && !(this.get('required') && this.is_false());
2171 is_syntax_valid: function() {
2175 * Method useful to implement to ease validity testing. Must return true if the current
2176 * value is similar to false in OpenERP.
2178 is_false: function() {
2179 return this.get('value') === false;
2181 _check_css_flags: function() {
2182 if (this.field.translate) {
2183 this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
2185 if (!this.disable_utility_classes) {
2186 if (this.field_manager.get('display_invalid_fields')) {
2187 this.$el.toggleClass('oe_form_invalid', !this.is_valid());
2194 set_input_id: function(id) {
2195 this.id_for_label = id;
2197 on_translate: function() {
2199 var trans = new instance.web.DataSet(this, 'ir.translation');
2200 return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
2205 set_dimensions: function (height, width) {
2211 commit_value: function() {
2217 * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
2220 instance.web.form.ReinitializeWidgetMixin = {
2222 * Default implementation of, you should not override it, use initialize_field() instead.
2225 this.initialize_field();
2228 initialize_field: function() {
2229 this.on("change:effective_readonly", this, this.reinitialize);
2230 this.initialize_content();
2232 reinitialize: function() {
2233 this.destroy_content();
2234 this.renderElement();
2235 this.initialize_content();
2238 * Called to destroy anything that could have been created previously, called before a
2239 * re-initialization.
2241 destroy_content: function() {},
2243 * Called to initialize the content.
2245 initialize_content: function() {},
2249 * A mixin to apply on any field that has to completely re-render when its readonly state
2252 instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
2253 reinitialize: function() {
2254 instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
2255 var res = this.render_value();
2256 if (this.view && this.view.render_value_defs){
2257 this.view.render_value_defs.push(res);
2263 Some hack to make placeholders work in ie9.
2265 if ($.browser.msie && $.browser.version === "9.0") {
2266 document.addEventListener("DOMNodeInserted",function(event){
2267 var nodename = event.target.nodeName.toLowerCase();
2268 if ( nodename === "input" || nodename == "textarea" ) {
2269 $(event.target).placeholder();
2274 instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2275 template: 'FieldChar',
2276 widget_class: 'oe_form_field_char',
2278 'change input': 'store_dom_value',
2280 init: function (field_manager, node) {
2281 this._super(field_manager, node);
2282 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2284 initialize_content: function() {
2285 this.setupFocus(this.$('input'));
2287 store_dom_value: function () {
2288 if (!this.get('effective_readonly')
2289 && this.$('input').length
2290 && this.is_syntax_valid()) {
2291 this.internal_set_value(
2293 this.$('input').val()));
2296 commit_value: function () {
2297 this.store_dom_value();
2298 return this._super();
2300 render_value: function() {
2301 var show_value = this.format_value(this.get('value'), '');
2302 if (!this.get("effective_readonly")) {
2303 this.$el.find('input').val(show_value);
2305 if (this.password) {
2306 show_value = new Array(show_value.length + 1).join('*');
2308 this.$(".oe_form_char_content").text(show_value);
2311 is_syntax_valid: function() {
2312 if (!this.get("effective_readonly") && this.$("input").size() > 0) {
2314 this.parse_value(this.$('input').val(), '');
2322 parse_value: function(val, def) {
2323 return instance.web.parse_value(val, this, def);
2325 format_value: function(val, def) {
2326 return instance.web.format_value(val, this, def);
2328 is_false: function() {
2329 return this.get('value') === '' || this._super();
2332 var input = this.$('input:first')[0];
2333 return input ? input.focus() : false;
2335 set_dimensions: function (height, width) {
2336 this._super(height, width);
2337 this.$('input').css({
2344 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2345 process_modifiers: function () {
2347 this.set({ readonly: true });
2351 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2352 template: 'FieldEmail',
2353 initialize_content: function() {
2355 var $button = this.$el.find('button');
2356 $button.click(this.on_button_clicked);
2357 this.setupFocus($button);
2359 render_value: function() {
2360 if (!this.get("effective_readonly")) {
2364 .attr('href', 'mailto:' + this.get('value'))
2365 .text(this.get('value') || '');
2368 on_button_clicked: function() {
2369 if (!this.get('value') || !this.is_syntax_valid()) {
2370 this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2372 location.href = 'mailto:' + this.get('value');
2377 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2378 template: 'FieldUrl',
2379 initialize_content: function() {
2381 var $button = this.$el.find('button');
2382 $button.click(this.on_button_clicked);
2383 this.setupFocus($button);
2385 render_value: function() {
2386 if (!this.get("effective_readonly")) {
2389 var tmp = this.get('value');
2390 var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2392 tmp = "http://" + this.get('value');
2394 var text = this.get('value') ? this.node.attrs.text || tmp : '';
2395 this.$el.find('a').attr('href', tmp).text(text);
2398 on_button_clicked: function() {
2399 if (!this.get('value')) {
2400 this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2402 var url = $.trim(this.get('value'));
2403 if(/^www\./i.test(url))
2404 url = 'http://'+url;
2410 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2411 is_field_number: true,
2412 widget_class: 'oe_form_field_float',
2413 init: function (field_manager, node) {
2414 this._super(field_manager, node);
2415 this.internal_set_value(0);
2416 if (this.node.attrs.digits) {
2417 this.digits = this.node.attrs.digits;
2419 this.digits = this.field.digits;
2422 set_value: function(value_) {
2423 if (value_ === false || value_ === undefined) {
2424 // As in GTK client, floats default to 0
2427 this._super.apply(this, [value_]);
2429 focus: function () {
2430 var $input = this.$('input:first');
2431 return $input.length ? $input.select() : false;
2435 instance.web.DateTimeWidget = instance.web.Widget.extend({
2436 template: "web.datepicker",
2437 jqueryui_object: 'datetimepicker',
2438 type_of_date: "datetime",
2440 'change .oe_datepicker_master': 'change_datetime',
2441 'keypress .oe_datepicker_master': 'change_datetime',
2443 init: function(parent) {
2444 this._super(parent);
2445 this.name = parent.name;
2449 this.$input = this.$el.find('input.oe_datepicker_master');
2450 this.$input_picker = this.$el.find('input.oe_datepicker_container');
2452 $.datepicker.setDefaults({
2453 clearText: _t('Clear'),
2454 clearStatus: _t('Erase the current date'),
2455 closeText: _t('Done'),
2456 closeStatus: _t('Close without change'),
2457 prevText: _t('<Prev'),
2458 prevStatus: _t('Show the previous month'),
2459 nextText: _t('Next>'),
2460 nextStatus: _t('Show the next month'),
2461 currentText: _t('Today'),
2462 currentStatus: _t('Show the current month'),
2463 monthNames: Date.CultureInfo.monthNames,
2464 monthNamesShort: Date.CultureInfo.abbreviatedMonthNames,
2465 monthStatus: _t('Show a different month'),
2466 yearStatus: _t('Show a different year'),
2467 weekHeader: _t('Wk'),
2468 weekStatus: _t('Week of the year'),
2469 dayNames: Date.CultureInfo.dayNames,
2470 dayNamesShort: Date.CultureInfo.abbreviatedDayNames,
2471 dayNamesMin: Date.CultureInfo.shortestDayNames,
2472 dayStatus: _t('Set DD as first week day'),
2473 dateStatus: _t('Select D, M d'),
2474 firstDay: Date.CultureInfo.firstDayOfWeek,
2475 initStatus: _t('Select a date'),
2478 $.timepicker.setDefaults({
2479 timeOnlyTitle: _t('Choose Time'),
2480 timeText: _t('Time'),
2481 hourText: _t('Hour'),
2482 minuteText: _t('Minute'),
2483 secondText: _t('Second'),
2484 currentText: _t('Now'),
2485 closeText: _t('Done')
2489 onClose: this.on_picker_select,
2490 onSelect: this.on_picker_select,
2494 showButtonPanel: true,
2495 firstDay: Date.CultureInfo.firstDayOfWeek
2497 // Some clicks in the datepicker dialog are not stopped by the
2498 // datepicker and "bubble through", unexpectedly triggering the bus's
2499 // click event. Prevent that.
2500 this.picker('widget').click(function (e) { e.stopPropagation(); });
2502 this.$el.find('img.oe_datepicker_trigger').click(function() {
2503 if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
2504 self.$input.focus();
2507 self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date());
2508 self.$input_picker.show();
2509 self.picker('show');
2510 self.$input_picker.hide();
2512 this.set_readonly(false);
2513 this.set({'value': false});
2515 picker: function() {
2516 return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
2518 on_picker_select: function(text, instance_) {
2519 var date = this.picker('getDate');
2521 .val(date ? this.format_client(date) : '')
2525 set_value: function(value_) {
2526 this.set({'value': value_});
2527 this.$input.val(value_ ? this.format_client(value_) : '');
2529 get_value: function() {
2530 return this.get('value');
2532 set_value_from_ui_: function() {
2533 var value_ = this.$input.val() || false;
2534 this.set({'value': this.parse_client(value_)});
2536 set_readonly: function(readonly) {
2537 this.readonly = readonly;
2538 this.$input.prop('readonly', this.readonly);
2539 this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
2541 is_valid_: function() {
2542 var value_ = this.$input.val();
2543 if (value_ === "") {
2547 this.parse_client(value_);
2554 parse_client: function(v) {
2555 return instance.web.parse_value(v, {"widget": this.type_of_date});
2557 format_client: function(v) {
2558 return instance.web.format_value(v, {"widget": this.type_of_date});
2560 change_datetime: function(e) {
2561 if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
2562 this.set_value_from_ui_();
2563 this.trigger("datetime_changed");
2566 commit_value: function () {
2567 this.change_datetime();
2571 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2572 jqueryui_object: 'datepicker',
2573 type_of_date: "date"
2576 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2577 template: "FieldDatetime",
2578 build_widget: function() {
2579 return new instance.web.DateTimeWidget(this);
2581 destroy_content: function() {
2582 if (this.datewidget) {
2583 this.datewidget.destroy();
2584 this.datewidget = undefined;
2587 initialize_content: function() {
2588 if (!this.get("effective_readonly")) {
2589 this.datewidget = this.build_widget();
2590 this.datewidget.on('datetime_changed', this, _.bind(function() {
2591 this.internal_set_value(this.datewidget.get_value());
2593 this.datewidget.appendTo(this.$el);
2594 this.setupFocus(this.datewidget.$input);
2597 render_value: function() {
2598 if (!this.get("effective_readonly")) {
2599 this.datewidget.set_value(this.get('value'));
2601 this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2604 is_syntax_valid: function() {
2605 if (!this.get("effective_readonly") && this.datewidget) {
2606 return this.datewidget.is_valid_();
2610 is_false: function() {
2611 return this.get('value') === '' || this._super();
2614 var input = this.datewidget && this.datewidget.$input[0];
2615 return input ? input.focus() : false;
2617 set_dimensions: function (height, width) {
2618 this._super(height, width);
2619 if (!this.get("effective_readonly")) {
2620 this.datewidget.$input.css('height', height);
2625 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2626 template: "FieldDate",
2627 build_widget: function() {
2628 return new instance.web.DateWidget(this);
2632 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2633 template: 'FieldText',
2635 'keyup': function (e) {
2636 if (e.which === $.ui.keyCode.ENTER) {
2637 e.stopPropagation();
2640 'keypress': function (e) {
2641 if (e.which === $.ui.keyCode.ENTER) {
2642 e.stopPropagation();
2645 'change textarea': 'store_dom_value',
2647 initialize_content: function() {
2649 if (! this.get("effective_readonly")) {
2650 this.$textarea = this.$el.find('textarea');
2651 this.auto_sized = false;
2652 this.default_height = this.$textarea.css('height');
2653 if (this.get("effective_readonly")) {
2654 this.$textarea.attr('disabled', 'disabled');
2656 this.setupFocus(this.$textarea);
2658 this.$textarea = undefined;
2661 commit_value: function () {
2662 if (! this.get("effective_readonly") && this.$textarea) {
2663 this.store_dom_value();
2665 return this._super();
2667 store_dom_value: function () {
2668 this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2670 render_value: function() {
2671 if (! this.get("effective_readonly")) {
2672 var show_value = instance.web.format_value(this.get('value'), this, '');
2673 if (show_value === '') {
2674 this.$textarea.css('height', parseInt(this.default_height)+"px");
2676 this.$textarea.val(show_value);
2677 if (! this.auto_sized) {
2678 this.auto_sized = true;
2679 this.$textarea.autosize();
2681 this.$textarea.trigger("autosize");
2684 var txt = this.get("value") || '';
2685 this.$(".oe_form_text_content").text(txt);
2688 is_syntax_valid: function() {
2689 if (!this.get("effective_readonly") && this.$textarea) {
2691 instance.web.parse_value(this.$textarea.val(), this, '');
2699 is_false: function() {
2700 return this.get('value') === '' || this._super();
2702 focus: function($el) {
2703 var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2704 return input ? input.focus() : false;
2706 set_dimensions: function (height, width) {
2707 this._super(height, width);
2708 if (!this.get("effective_readonly") && this.$textarea) {
2709 this.$textarea.css({
2718 * FieldTextHtml Widget
2719 * Intended for FieldText widgets meant to display HTML content. This
2720 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2721 * To find more information about CLEditor configutation: go to
2722 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2724 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2725 template: 'FieldTextHtml',
2727 this._super.apply(this, arguments);
2729 initialize_content: function() {
2731 if (! this.get("effective_readonly")) {
2732 self._updating_editor = false;
2733 this.$textarea = this.$el.find('textarea');
2734 var width = ((this.node.attrs || {}).editor_width || 'calc(100% - 4px)');
2735 var height = ((this.node.attrs || {}).editor_height || 250);
2736 this.$textarea.cleditor({
2737 width: width, // width not including margins, borders or padding
2738 height: height, // height not including margins, borders or padding
2739 controls: // controls to add to the toolbar
2740 "bold italic underline strikethrough " +
2741 "| removeformat | bullets numbering | outdent " +
2742 "indent | link unlink | source",
2743 bodyStyle: // style to assign to document body contained within the editor
2744 "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2746 this.$cleditor = this.$textarea.cleditor()[0];
2747 this.$cleditor.change(function() {
2748 if (! self._updating_editor) {
2749 self.$cleditor.updateTextArea();
2750 self.internal_set_value(self.$textarea.val());
2753 if (this.field.translate) {
2754 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"/>')
2755 .click(this.on_translate);
2756 this.$cleditor.$toolbar.append($img);
2760 render_value: function() {
2761 if (! this.get("effective_readonly")) {
2762 this.$textarea.val(this.get('value') || '');
2763 this._updating_editor = true;
2764 this.$cleditor.updateFrame();
2765 this._updating_editor = false;
2767 this.$el.html(this.get('value'));
2772 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2773 template: 'FieldBoolean',
2776 this.$checkbox = $("input", this.$el);
2777 this.setupFocus(this.$checkbox);
2778 this.$el.click(_.bind(function() {
2779 this.internal_set_value(this.$checkbox.is(':checked'));
2781 var check_readonly = function() {
2782 self.$checkbox.prop('disabled', self.get("effective_readonly"));
2784 this.on("change:effective_readonly", this, check_readonly);
2785 check_readonly.call(this);
2786 this._super.apply(this, arguments);
2788 render_value: function() {
2789 this.$checkbox[0].checked = this.get('value');
2792 var input = this.$checkbox && this.$checkbox[0];
2793 return input ? input.focus() : false;
2798 The progressbar field expect a float from 0 to 100.
2800 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2801 template: 'FieldProgressBar',
2802 render_value: function() {
2803 this.$el.progressbar({
2804 value: this.get('value') || 0,
2805 disabled: this.get("effective_readonly")
2807 var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
2808 this.$('span').html(formatted_value + '%');
2813 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2814 template: 'FieldSelection',
2816 'change select': 'store_dom_value',
2818 init: function(field_manager, node) {
2820 this._super(field_manager, node);
2821 this.values = _(this.field.selection).chain()
2822 .reject(function (v) { return v[0] === false && v[1] === ''; })
2823 .unshift([false, ''])
2826 initialize_content: function() {
2827 // Flag indicating whether we're in an event chain containing a change
2828 // event on the select, in order to know what to do on keyup[RETURN]:
2829 // * If the user presses [RETURN] as part of changing the value of a
2830 // selection, we should just let the value change and not let the
2831 // event broadcast further (e.g. to validating the current state of
2832 // the form in editable list view, which would lead to saving the
2833 // current row or switching to the next one)
2834 // * If the user presses [RETURN] with a select closed (side-effect:
2835 // also if the user opened the select and pressed [RETURN] without
2836 // changing the selected value), takes the action as validating the
2838 var ischanging = false;
2839 var $select = this.$el.find('select')
2840 .change(function () { ischanging = true; })
2841 .click(function () { ischanging = false; })
2842 .keyup(function (e) {
2843 if (e.which !== 13 || !ischanging) { return; }
2844 e.stopPropagation();
2847 this.setupFocus($select);
2849 commit_value: function () {
2850 this.store_dom_value();
2851 return this._super();
2853 store_dom_value: function () {
2854 if (!this.get('effective_readonly') && this.$('select').length) {
2855 this.internal_set_value(
2856 this.values[this.$('select')[0].selectedIndex][0]);
2859 set_value: function(value_) {
2860 value_ = value_ === null ? false : value_;
2861 value_ = value_ instanceof Array ? value_[0] : value_;
2862 this._super(value_);
2864 render_value: function() {
2865 if (!this.get("effective_readonly")) {
2867 for (var i = 0, ii = this.values.length; i < ii; i++) {
2868 if (this.values[i][0] === this.get('value')) index = i;
2870 this.$el.find('select')[0].selectedIndex = index;
2873 var option = _(this.values)
2874 .detect(function (record) { return record[0] === self.get('value'); });
2875 this.$el.text(option ? option[1] : this.values[0][1]);
2879 var input = this.$('select:first')[0];
2880 return input ? input.focus() : false;
2882 set_dimensions: function (height, width) {
2883 this._super(height, width);
2884 this.$('select').css({
2891 // jquery autocomplete tweak to allow html and classnames
2893 var proto = $.ui.autocomplete.prototype,
2894 initSource = proto._initSource;
2896 function filter( array, term ) {
2897 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
2898 return $.grep( array, function(value_) {
2899 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
2904 _initSource: function() {
2905 if ( this.options.html && $.isArray(this.options.source) ) {
2906 this.source = function( request, response ) {
2907 response( filter( this.options.source, request.term ) );
2910 initSource.call( this );
2914 _renderItem: function( ul, item) {
2915 return $( "<li></li>" )
2916 .data( "item.autocomplete", item )
2917 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
2919 .addClass(item.classname);
2925 * A mixin containing some useful methods to handle completion inputs.
2927 instance.web.form.CompletionFieldMixin = {
2930 this.orderer = new instance.web.DropMisordered();
2933 * Call this method to search using a string.
2935 get_search_result: function(search_val) {
2938 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
2939 var blacklist = this.get_search_blacklist();
2940 this.last_query = search_val;
2942 return this.orderer.add(dataset.name_search(
2943 search_val, new instance.web.CompoundDomain(self.build_domain(), [["id", "not in", blacklist]]),
2944 'ilike', this.limit + 1, self.build_context())).then(function(data) {
2945 self.last_search = data;
2946 // possible selections for the m2o
2947 var values = _.map(data, function(x) {
2948 x[1] = x[1].split("\n")[0];
2950 label: _.str.escapeHTML(x[1]),
2957 // search more... if more results that max
2958 if (values.length > self.limit) {
2959 values = values.slice(0, self.limit);
2961 label: _t("Search More..."),
2962 action: function() {
2963 dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
2964 self._search_create_popup("search", data);
2967 classname: 'oe_m2o_dropdown_option'
2971 var raw_result = _(data.result).map(function(x) {return x[1];});
2972 if (search_val.length > 0 && !_.include(raw_result, search_val)) {
2974 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
2975 $('<span />').text(search_val).html()),
2976 action: function() {
2977 self._quick_create(search_val);
2979 classname: 'oe_m2o_dropdown_option'
2984 label: _t("Create and Edit..."),
2985 action: function() {
2986 self._search_create_popup("form", undefined, self._create_context(search_val));
2988 classname: 'oe_m2o_dropdown_option'
2994 get_search_blacklist: function() {
2997 _quick_create: function(name) {
2999 var slow_create = function () {
3000 self._search_create_popup("form", undefined, self._create_context(name));
3002 if (self.options.quick_create === undefined || self.options.quick_create) {
3003 new instance.web.DataSet(this, this.field.relation, self.build_context())
3004 .name_create(name).done(function(data) {
3005 if (!self.get('effective_readonly'))
3006 self.add_id(data[0]);
3007 }).fail(function(error, event) {
3008 event.preventDefault();
3014 // all search/create popup handling
3015 _search_create_popup: function(view, ids, context) {
3017 var pop = new instance.web.form.SelectCreatePopup(this);
3019 self.field.relation,
3021 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3022 initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
3024 disable_multiple_selection: true
3026 self.build_domain(),
3027 new instance.web.CompoundContext(self.build_context(), context || {})
3029 pop.on("elements_selected", self, function(element_ids) {
3030 self.add_id(element_ids[0]);
3037 add_id: function(id) {},
3038 _create_context: function(name) {
3040 var field = (this.options || {}).create_name_field;
3041 if (field === undefined)
3043 if (field !== false && name && (this.options || {}).quick_create !== false)
3044 tmp["default_" + field] = name;
3049 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3050 template: "M2ODialog",
3051 init: function(parent) {
3052 this._super(parent, {
3053 title: _.str.sprintf(_t("Add %s"), parent.string),
3059 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3060 this.$("input").val(this.getParent().last_query);
3061 this.$buttons.find(".oe_form_m2o_qc_button").click(function(){
3062 self.getParent()._quick_create(self.$("input").val());
3065 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3066 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3069 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3075 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3076 template: "FieldMany2One",
3078 'keydown input': function (e) {
3080 case $.ui.keyCode.UP:
3081 case $.ui.keyCode.DOWN:
3082 e.stopPropagation();
3086 init: function(field_manager, node) {
3087 this._super(field_manager, node);
3088 instance.web.form.CompletionFieldMixin.init.call(this);
3089 this.set({'value': false});
3090 this.display_value = {};
3091 this.last_search = [];
3092 this.floating = false;
3093 this.current_display = null;
3094 this.is_started = false;
3095 this.ignore_focusout = false;
3097 reinit_value: function(val) {
3098 this.internal_set_value(val);
3099 this.floating = false;
3100 if (this.is_started)
3101 this.render_value();
3103 initialize_field: function() {
3104 this.is_started = true;
3105 instance.web.bus.on('click', this, function() {
3106 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3107 this.$input.autocomplete("close");
3110 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3112 initialize_content: function() {
3113 if (!this.get("effective_readonly"))
3114 this.render_editable();
3116 destroy_content: function () {
3117 if (this.$drop_down) {
3118 this.$drop_down.off('click');
3119 delete this.$drop_down;
3122 this.$input.closest(".ui-dialog .ui-dialog-content").off('scroll');
3123 this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3124 'focus focusout change keydown');
3127 if (this.$follow_button) {
3128 this.$follow_button.off('blur focus click');
3129 delete this.$follow_button;
3132 destroy: function () {
3133 this.destroy_content();
3134 return this._super();
3136 init_error_displayer: function() {
3139 hide_error_displayer: function() {
3142 show_error_displayer: function() {
3143 new instance.web.form.M2ODialog(this).open();
3145 render_editable: function() {
3147 this.$input = this.$el.find("input");
3149 this.init_error_displayer();
3151 self.$input.on('focus', function() {
3152 self.hide_error_displayer();
3155 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3156 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3158 this.$follow_button.click(function(ev) {
3159 ev.preventDefault();
3160 if (!self.get('value')) {
3164 var pop = new instance.web.form.FormOpenPopup(self);
3166 self.field.relation,
3168 self.build_context(),
3170 title: _t("Open: ") + self.string
3173 pop.on('write_completed', self, function(){
3174 self.display_value = {};
3175 self.render_value();
3177 self.trigger('changed_value');
3181 // some behavior for input
3182 var input_changed = function() {
3183 if (self.current_display !== self.$input.val()) {
3184 self.current_display = self.$input.val();
3185 if (self.$input.val() === "") {
3186 self.internal_set_value(false);
3187 self.floating = false;
3189 self.floating = true;
3193 this.$input.keydown(input_changed);
3194 this.$input.change(input_changed);
3195 this.$drop_down.click(function() {
3196 if (self.$input.autocomplete("widget").is(":visible")) {
3197 self.$input.autocomplete("close");
3198 self.$input.focus();
3200 if (self.get("value") && ! self.floating) {
3201 self.$input.autocomplete("search", "");
3203 self.$input.autocomplete("search");
3208 // Autocomplete close on dialog content scroll
3209 var close_autocomplete = _.debounce(function() {
3210 if (self.$input.autocomplete("widget").is(":visible")) {
3211 self.$input.autocomplete("close");
3214 this.$input.closest(".ui-dialog .ui-dialog-content").on('scroll', this, close_autocomplete);
3216 self.ed_def = $.Deferred();
3217 self.uned_def = $.Deferred();
3219 var ed_duration = 15000;
3220 var anyoneLoosesFocus = function (e) {
3221 if (self.ignore_focusout) { return; }
3223 if (self.floating) {
3224 if (self.last_search.length > 0) {
3225 if (self.last_search[0][0] != self.get("value")) {
3226 self.display_value = {};
3227 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3228 self.reinit_value(self.last_search[0][0]);
3231 self.render_value();
3235 self.reinit_value(false);
3237 self.floating = false;
3239 if (used && self.get("value") === false && ! self.no_ed) {
3240 self.ed_def.reject();
3241 self.uned_def.reject();
3242 self.ed_def = $.Deferred();
3243 self.ed_def.done(function() {
3244 self.show_error_displayer();
3245 ignore_blur = false;
3246 self.trigger('focused');
3249 setTimeout(function() {
3250 self.ed_def.resolve();
3251 self.uned_def.reject();
3252 self.uned_def = $.Deferred();
3253 self.uned_def.done(function() {
3254 self.hide_error_displayer();
3256 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3260 self.ed_def.reject();
3263 var ignore_blur = false;
3265 focusout: anyoneLoosesFocus,
3266 focus: function () { self.trigger('focused'); },
3267 autocompleteopen: function () { ignore_blur = true; },
3268 autocompleteclose: function () { ignore_blur = false; },
3270 // autocomplete open
3271 if (ignore_blur) { return; }
3272 if (_(self.getChildren()).any(function (child) {
3273 return child instanceof instance.web.form.AbstractFormPopup;
3275 self.trigger('blurred');
3279 var isSelecting = false;
3281 this.$input.autocomplete({
3282 source: function(req, resp) {
3283 self.get_search_result(req.term).done(function(result) {
3287 select: function(event, ui) {
3291 self.display_value = {};
3292 self.display_value["" + item.id] = item.name;
3293 self.reinit_value(item.id);
3294 } else if (item.action) {
3296 // Cancel widget blurring, to avoid form blur event
3297 self.trigger('focused');
3301 focus: function(e, ui) {
3305 // disabled to solve a bug, but may cause others
3306 //close: anyoneLoosesFocus,
3310 this.$input.autocomplete("widget").openerpClass();
3311 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3312 this.$input.keyup(function(e) {
3313 if (e.which === 13) { // ENTER
3315 e.stopPropagation();
3317 isSelecting = false;
3319 this.setupFocus(this.$follow_button);
3321 render_value: function(no_recurse) {
3323 if (! this.get("value")) {
3324 this.display_string("");
3327 var display = this.display_value["" + this.get("value")];
3329 this.display_string(display);
3333 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3334 this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3336 self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
3339 self.display_value["" + self.get("value")] = data[0][1];
3340 self.render_value(true);
3344 display_string: function(str) {
3346 if (!this.get("effective_readonly")) {
3347 this.$input.val(str.split("\n")[0]);
3348 this.current_display = this.$input.val();
3349 if (this.is_false()) {
3350 this.$('.oe_m2o_cm_button').css({'display':'none'});
3352 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3355 var lines = _.escape(str).split("\n");
3359 follow = _.rest(lines).join("<br />");
3362 var $link = this.$el.find('.oe_form_uri')
3365 if (! this.options.no_open)
3366 $link.click(function () {
3368 type: 'ir.actions.act_window',
3369 res_model: self.field.relation,
3370 res_id: self.get("value"),
3371 views: [[false, 'form']],
3373 context: self.build_context().eval(),
3377 $(".oe_form_m2o_follow", this.$el).html(follow);
3380 set_value: function(value_) {
3382 if (value_ instanceof Array) {
3383 this.display_value = {};
3384 if (! this.options.always_reload) {
3385 this.display_value["" + value_[0]] = value_[1];
3389 value_ = value_ || false;
3390 this.reinit_value(value_);
3392 get_displayed: function() {
3393 return this.display_value["" + this.get("value")];
3395 add_id: function(id) {
3396 this.display_value = {};
3397 this.reinit_value(id);
3399 is_false: function() {
3400 return ! this.get("value");
3402 focus: function () {
3403 var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3404 return input ? input.focus() : false;
3406 _quick_create: function() {
3408 this.ed_def.reject();
3409 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3411 _search_create_popup: function() {
3413 this.ed_def.reject();
3414 this.ignore_focusout = true;
3415 this.reinit_value(false);
3416 var res = instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3417 this.ignore_focusout = false;
3421 set_dimensions: function (height, width) {
3422 this._super(height, width);
3423 if (!this.get("effective_readonly") && this.$input)
3424 this.$input.css('height', height);
3428 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3429 template: 'Many2OneButton',
3430 init: function(field_manager, node) {
3431 this._super.apply(this, arguments);
3434 this._super.apply(this, arguments);
3437 set_button: function() {
3440 this.$button.remove();
3443 this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3444 this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3445 this.$button.addClass('oe_link').css({'padding':'4px'});
3446 this.$el.append(this.$button);
3447 this.$button.on('click', self.on_click);
3449 on_click: function(ev) {
3451 this.popup = new instance.web.form.FormOpenPopup(this);
3452 this.popup.show_element(
3453 this.field.relation,
3455 this.build_context(),
3456 {title: this.string}
3458 this.popup.on('create_completed', self, function(r) {
3462 set_value: function(value_) {
3464 if (value_ instanceof Array) {
3467 value_ = value_ || false;
3468 this.set('value', value_);
3474 # Values: (0, 0, { fields }) create
3475 # (1, ID, { fields }) update
3476 # (2, ID) remove (delete)
3477 # (3, ID) unlink one (target id or target of relation)
3479 # (5) unlink all (only valid for one2many)
3484 'create': function (values) {
3485 return [commands.CREATE, false, values];
3487 // (1, id, {values})
3489 'update': function (id, values) {
3490 return [commands.UPDATE, id, values];
3494 'delete': function (id) {
3495 return [commands.DELETE, id, false];
3497 // (3, id[, _]) removes relation, but not linked record itself
3499 'forget': function (id) {
3500 return [commands.FORGET, id, false];
3504 'link_to': function (id) {
3505 return [commands.LINK_TO, id, false];
3509 'delete_all': function () {
3510 return [5, false, false];
3512 // (6, _, ids) replaces all linked records with provided ids
3514 'replace_with': function (ids) {
3515 return [6, false, ids];
3518 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3519 multi_selection: false,
3520 init: function(field_manager, node) {
3521 this._super(field_manager, node);
3522 lazy_build_o2m_kanban_view();
3523 this.is_loaded = $.Deferred();
3524 this.initial_is_loaded = this.is_loaded;
3525 this.form_last_update = $.Deferred();
3526 this.init_form_last_update = this.form_last_update;
3527 this.is_started = false;
3528 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
3529 this.dataset.o2m = this;
3530 this.dataset.parent_view = this.view;
3531 this.dataset.child_name = this.name;
3533 this.dataset.on('dataset_changed', this, function() {
3534 self.trigger_on_change();
3539 this._super.apply(this, arguments);
3540 this.$el.addClass('oe_form_field oe_form_field_one2many');
3545 this.is_loaded.done(function() {
3546 self.on("change:effective_readonly", self, function() {
3547 self.is_loaded = self.is_loaded.then(function() {
3548 self.viewmanager.destroy();
3549 return $.when(self.load_views()).done(function() {
3550 self.reload_current_view();
3555 this.is_started = true;
3556 this.reload_current_view();
3558 trigger_on_change: function() {
3559 this.trigger('changed_value');
3561 load_views: function() {
3564 var modes = this.node.attrs.mode;
3565 modes = !!modes ? modes.split(",") : ["tree"];
3567 _.each(modes, function(mode) {
3568 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
3569 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
3573 view_type: mode == "tree" ? "list" : mode,
3576 if (self.field.views && self.field.views[mode]) {
3577 view.embedded_view = self.field.views[mode];
3579 if(view.view_type === "list") {
3580 _.extend(view.options, {
3582 selectable: self.multi_selection,
3584 import_enabled: false,
3587 if (self.get("effective_readonly")) {
3588 _.extend(view.options, {
3593 } else if (view.view_type === "form") {
3594 if (self.get("effective_readonly")) {
3595 view.view_type = 'form';
3597 _.extend(view.options, {
3598 not_interactible_on_create: true,
3600 } else if (view.view_type === "kanban") {
3601 _.extend(view.options, {
3602 confirm_on_delete: false,
3604 if (self.get("effective_readonly")) {
3605 _.extend(view.options, {
3606 action_buttons: false,
3607 quick_creatable: false,
3609 read_only_mode: true,
3617 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
3618 this.viewmanager.o2m = self;
3619 var once = $.Deferred().done(function() {
3620 self.init_form_last_update.resolve();
3622 var def = $.Deferred().done(function() {
3623 self.initial_is_loaded.resolve();
3625 this.viewmanager.on("controller_inited", self, function(view_type, controller) {
3626 controller.o2m = self;
3627 if (view_type == "list") {
3628 if (self.get("effective_readonly")) {
3629 controller.on('edit:before', self, function (e) {
3632 _(controller.columns).find(function (column) {
3633 if (!(column instanceof instance.web.list.Handle)) {
3636 column.modifiers.invisible = true;
3640 } else if (view_type === "form") {
3641 if (self.get("effective_readonly")) {
3642 $(".oe_form_buttons", controller.$el).children().remove();
3644 controller.on("load_record", self, function(){
3647 controller.on('pager_action_executed',self,self.save_any_view);
3648 } else if (view_type == "graph") {
3649 self.reload_current_view()
3653 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
3654 $.when(self.save_any_view()).done(function() {
3655 if (n_mode === "list") {
3656 $.async_when().done(function() {
3657 self.reload_current_view();
3662 $.async_when().done(function () {
3663 self.viewmanager.appendTo(self.$el);
3667 reload_current_view: function() {
3669 return self.is_loaded = self.is_loaded.then(function() {
3670 var active_view = self.viewmanager.active_view;
3671 var view = self.viewmanager.views[active_view].controller;
3672 if(active_view === "list") {
3673 return view.reload_content();
3674 } else if (active_view === "form") {
3675 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
3676 self.dataset.index = 0;
3678 var act = function() {
3679 return view.do_show();
3681 self.form_last_update = self.form_last_update.then(act, act);
3682 return self.form_last_update;
3683 } else if (view.do_search) {
3684 return view.do_search(self.build_domain(), self.dataset.get_context(), []);
3688 set_value: function(value_) {
3689 value_ = value_ || [];
3691 this.dataset.reset_ids([]);
3692 if(value_.length >= 1 && value_[0] instanceof Array) {
3694 _.each(value_, function(command) {
3695 var obj = {values: command[2]};
3696 switch (command[0]) {
3697 case commands.CREATE:
3698 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3700 self.dataset.to_create.push(obj);
3701 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3704 case commands.UPDATE:
3705 obj['id'] = command[1];
3706 self.dataset.to_write.push(obj);
3707 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3710 case commands.DELETE:
3711 self.dataset.to_delete.push({id: command[1]});
3713 case commands.LINK_TO:
3714 ids.push(command[1]);
3716 case commands.DELETE_ALL:
3717 self.dataset.delete_all = true;
3722 this.dataset.set_ids(ids);
3723 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
3725 this.dataset.delete_all = true;
3726 _.each(value_, function(command) {
3727 var obj = {values: command};
3728 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3730 self.dataset.to_create.push(obj);
3731 self.dataset.cache.push(_.clone(obj));
3735 this.dataset.set_ids(ids);
3737 this._super(value_);
3738 this.dataset.reset_ids(value_);
3740 if (this.dataset.index === null && this.dataset.ids.length > 0) {
3741 this.dataset.index = 0;
3743 this.trigger_on_change();
3744 if (this.is_started) {
3745 return self.reload_current_view();
3750 get_value: function() {
3754 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
3755 val = val.concat(_.map(this.dataset.ids, function(id) {
3756 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
3758 return commands.create(alter_order.values);
3760 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
3762 return commands.update(alter_order.id, alter_order.values);
3764 return commands.link_to(id);
3766 return val.concat(_.map(
3767 this.dataset.to_delete, function(x) {
3768 return commands['delete'](x.id);}));
3770 commit_value: function() {
3771 return this.save_any_view();
3773 save_any_view: function() {
3774 if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
3775 this.viewmanager.views[this.viewmanager.active_view] &&
3776 this.viewmanager.views[this.viewmanager.active_view].controller) {
3777 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3778 if (this.viewmanager.active_view === "form") {
3779 if (!view.is_initialized.state() === 'resolved') {
3780 return $.when(false);
3782 return $.when(view.save());
3783 } else if (this.viewmanager.active_view === "list") {
3784 return $.when(view.ensure_saved());
3787 return $.when(false);
3789 is_false: function() {
3790 return this.dataset.ids.length == 0;
3792 is_syntax_valid: function() {
3793 if (! this.viewmanager || ! this.viewmanager.views[this.viewmanager.active_view])
3795 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3796 switch (this.viewmanager.active_view) {
3798 return _(view.fields).chain()
3804 return view.is_valid();
3810 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
3811 template: 'One2Many.viewmanager',
3812 init: function(parent, dataset, views, flags) {
3813 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
3814 this.registry = this.registry.extend({
3815 list: 'instance.web.form.One2ManyListView',
3816 form: 'instance.web.form.One2ManyFormView',
3817 kanban: 'instance.web.form.One2ManyKanbanView',
3819 this.__ignore_blur = false;
3821 switch_mode: function(mode, unused) {
3822 if (mode !== 'form') {
3823 return this._super(mode, unused);
3826 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
3827 var pop = new instance.web.form.FormOpenPopup(this);
3828 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3829 title: _t("Open: ") + self.o2m.string,
3830 create_function: function(data, options) {
3831 return self.o2m.dataset.create(data, options).done(function(r) {
3832 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3833 self.o2m.dataset.trigger("dataset_changed", r);
3836 write_function: function(id, data, options) {
3837 return self.o2m.dataset.write(id, data, {}).done(function() {
3838 self.o2m.reload_current_view();
3841 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3842 parent_view: self.o2m.view,
3843 child_name: self.o2m.name,
3844 read_function: function() {
3845 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3847 form_view_options: {'not_interactible_on_create':true},
3848 readonly: self.o2m.get("effective_readonly")
3850 pop.on("elements_selected", self, function() {
3851 self.o2m.reload_current_view();
3856 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
3857 get_context: function() {
3858 this.context = this.o2m.build_context();
3860 _.each(arguments, function(context) {
3861 self.context.add(context);
3863 return this.context;
3867 instance.web.form.One2ManyListView = instance.web.ListView.extend({
3868 _template: 'One2Many.listview',
3869 init: function (parent, dataset, view_id, options) {
3870 this._super(parent, dataset, view_id, _.extend(options || {}, {
3871 GroupsType: instance.web.form.One2ManyGroups,
3872 ListType: instance.web.form.One2ManyList
3874 this.on('edit:after', this, this.proxy('_after_edit'));
3875 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
3878 .bind('add', this.proxy("changed_records"))
3879 .bind('edit', this.proxy("changed_records"))
3880 .bind('remove', this.proxy("changed_records"));
3882 start: function () {
3883 var ret = this._super();
3885 .off('mousedown.handleButtons')
3886 .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
3889 changed_records: function () {
3890 this.o2m.trigger_on_change();
3892 is_valid: function () {
3894 if (!this.fields_view || !this.editable()){
3897 this.o2m._dirty_flag = true;
3899 return _.every(this.records.records, function(record){
3901 _.each(self.editor.form.fields, function(field){
3902 field._inhibit_on_change_flag = true;
3903 field.set_value(r.attributes[field.name]);
3904 field._inhibit_on_change_flag = false;
3906 return _.every(self.editor.form.fields, function(field){
3907 field.process_modifiers();
3908 field._check_css_flags();
3909 return field.is_valid();
3913 do_add_record: function () {
3914 if (this.editable()) {
3915 this._super.apply(this, arguments);
3918 var pop = new instance.web.form.SelectCreatePopup(this);
3920 self.o2m.field.relation,
3922 title: _t("Create: ") + self.o2m.string,
3923 initial_view: "form",
3924 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3925 create_function: function(data, options) {
3926 return self.o2m.dataset.create(data, options).done(function(r) {
3927 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3928 self.o2m.dataset.trigger("dataset_changed", r);
3931 read_function: function() {
3932 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3934 parent_view: self.o2m.view,
3935 child_name: self.o2m.name,
3936 form_view_options: {'not_interactible_on_create':true}
3938 self.o2m.build_domain(),
3939 self.o2m.build_context()
3941 pop.on("elements_selected", self, function() {
3942 self.o2m.reload_current_view();
3946 do_activate_record: function(index, id) {
3948 var pop = new instance.web.form.FormOpenPopup(self);
3949 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3950 title: _t("Open: ") + self.o2m.string,
3951 write_function: function(id, data) {
3952 return self.o2m.dataset.write(id, data, {}).done(function() {
3953 self.o2m.reload_current_view();
3956 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3957 parent_view: self.o2m.view,
3958 child_name: self.o2m.name,
3959 read_function: function() {
3960 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3962 form_view_options: {'not_interactible_on_create':true},
3963 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
3966 do_button_action: function (name, id, callback) {
3967 if (!_.isNumber(id)) {
3968 instance.webclient.notification.warn(
3969 _t("Action Button"),
3970 _t("The o2m record must be saved before an action can be used"));
3973 var parent_form = this.o2m.view;
3975 this.ensure_saved().then(function () {
3977 return parent_form.save();
3980 }).done(function () {
3981 var ds = self.o2m.dataset;
3982 var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
3983 return value.length;
3985 if (!self.o2m.options.reload_on_button && !cached_records) {
3986 self.handle_button(name, id, callback);
3988 self.handle_button(name, id, function(){
3989 self.o2m.view.reload();
3995 _after_edit: function () {
3996 this.__ignore_blur = false;
3997 this.editor.form.on('blurred', this, this._on_form_blur);
3999 // The form's blur thing may be jiggered during the edition setup,
4000 // potentially leading to the o2m instasaving the row. Cancel any
4001 // blurring triggered the edition startup here
4002 this.editor.form.widgetFocused();
4004 _before_unedit: function () {
4005 this.editor.form.off('blurred', this, this._on_form_blur);
4007 _button_down: function () {
4008 // If a button is clicked (usually some sort of action button), it's
4009 // the button's responsibility to ensure the editable list is in the
4010 // correct state -> ignore form blurring
4011 this.__ignore_blur = true;
4014 * Handles blurring of the nested form (saves the currently edited row),
4015 * unless the flag to ignore the event is set to ``true``
4017 * Makes the internal form go away
4019 _on_form_blur: function () {
4020 if (this.__ignore_blur) {
4021 this.__ignore_blur = false;
4024 // FIXME: why isn't there an API for this?
4025 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4026 this.ensure_saved();
4029 this.cancel_edition();
4031 keypress_ENTER: function () {
4032 // blurring caused by hitting the [Return] key, should skip the
4033 // autosave-on-blur and let the handler for [Return] do its thing (save
4034 // the current row *anyway*, then create a new one/edit the next one)
4035 this.__ignore_blur = true;
4036 this._super.apply(this, arguments);
4038 do_delete: function (ids) {
4039 var confirm = window.confirm;
4040 window.confirm = function () { return true; };
4042 return this._super(ids);
4044 window.confirm = confirm;
4047 reload_record: function (record) {
4048 // Evict record.id from cache to ensure it will be reloaded correctly
4049 this.dataset.evict_record(record.get('id'));
4051 return this._super(record);
4054 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4055 setup_resequence_rows: function () {
4056 if (!this.view.o2m.get('effective_readonly')) {
4057 this._super.apply(this, arguments);
4061 instance.web.form.One2ManyList = instance.web.ListView.List.extend({
4062 pad_table_to: function (count) {
4063 if (!this.view.is_action_enabled('create')) {
4066 this._super(count > 0 ? count - 1 : 0);
4069 // magical invocation of wtf does that do
4070 if (this.view.o2m.get('effective_readonly')) {
4075 var columns = _(this.columns).filter(function (column) {
4076 return column.invisible !== '1';
4078 if (this.options.selectable) { columns++; }
4079 if (this.options.deletable) { columns++; }
4081 if (!this.view.is_action_enabled('create')) {
4085 var $cell = $('<td>', {
4087 'class': 'oe_form_field_one2many_list_row_add'
4089 $('<a>', {href: '#'}).text(_t("Add an item"))
4090 .mousedown(function () {
4091 // FIXME: needs to be an official API somehow
4092 if (self.view.editor.is_editing()) {
4093 self.view.__ignore_blur = true;
4096 .click(function (e) {
4098 e.stopPropagation();
4099 // FIXME: there should also be an API for that one
4100 if (self.view.editor.form.__blur_timeout) {
4101 clearTimeout(self.view.editor.form.__blur_timeout);
4102 self.view.editor.form.__blur_timeout = false;
4104 self.view.ensure_saved().done(function () {
4105 self.view.do_add_record();
4109 var $padding = this.$current.find('tr:not([data-id]):first');
4110 var $newrow = $('<tr>').append($cell);
4111 if ($padding.length) {
4112 $padding.before($newrow);
4114 this.$current.append($newrow)
4119 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4120 form_template: 'One2Many.formview',
4121 load_form: function(data) {
4124 this.$buttons.find('button.oe_form_button_create').click(function() {
4125 self.save().done(self.on_button_new);
4128 do_notify_change: function() {
4129 if (this.dataset.parent_view) {
4130 this.dataset.parent_view.do_notify_change();
4132 this._super.apply(this, arguments);
4137 var lazy_build_o2m_kanban_view = function() {
4138 if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
4140 instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4144 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4145 template: "FieldMany2ManyTags",
4147 this._super.apply(this, arguments);
4148 instance.web.form.CompletionFieldMixin.init.call(this);
4149 this.set({"value": []});
4150 this._display_orderer = new instance.web.DropMisordered();
4151 this._drop_shown = false;
4153 initialize_content: function() {
4154 if (this.get("effective_readonly"))
4157 var ignore_blur = false;
4158 self.$text = this.$("textarea");
4159 self.$text.textext({
4160 plugins : 'tags arrow autocomplete',
4162 render: function(suggestion) {
4163 return $('<span class="text-label"/>').
4164 data('index', suggestion['index']).html(suggestion['label']);
4169 selectFromDropdown: function() {
4170 this.trigger('hideDropdown');
4171 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4172 var data = self.search_result[index];
4174 self.add_id(data.id);
4179 this.trigger('setSuggestions', {result : []});
4183 isTagAllowed: function(tag) {
4187 removeTag: function(tag) {
4188 var id = tag.data("id");
4189 self.set({"value": _.without(self.get("value"), id)});
4191 renderTag: function(stuff) {
4192 return $.fn.textext.TextExtTags.prototype.renderTag.
4193 call(this, stuff).data("id", stuff.id);
4197 itemToString: function(item) {
4202 onSetInputData: function(e, data) {
4204 this._plugins.autocomplete._suggestions = null;
4206 this.input().val(data);
4210 }).bind('getSuggestions', function(e, data) {
4212 var str = !!data ? data.query || '' : '';
4213 self.get_search_result(str).done(function(result) {
4214 self.search_result = result;
4215 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4216 return _.extend(el, {index:i});
4219 }).bind('hideDropdown', function() {
4220 self._drop_shown = false;
4221 }).bind('showDropdown', function() {
4222 self._drop_shown = true;
4224 self.tags = self.$text.textext()[0].tags();
4226 .focusin(function () {
4227 self.trigger('focused');
4228 ignore_blur = false;
4230 .focusout(function() {
4231 self.$text.trigger("setInputData", "");
4233 self.trigger('blurred');
4235 }).keydown(function(e) {
4236 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4237 self.$text.textext()[0].autocomplete().selectFromDropdown();
4241 set_value: function(value_) {
4242 value_ = value_ || [];
4243 if (value_.length >= 1 && value_[0] instanceof Array) {
4244 value_ = value_[0][2];
4246 this._super(value_);
4248 is_false: function() {
4249 return _(this.get("value")).isEmpty();
4251 get_value: function() {
4252 var tmp = [commands.replace_with(this.get("value"))];
4255 get_search_blacklist: function() {
4256 return this.get("value");
4258 render_value: function() {
4260 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4261 var values = self.get("value");
4262 var handle_names = function(data) {
4263 if (self.isDestroyed())
4266 _.each(data, function(el) {
4267 indexed[el[0]] = el;
4269 data = _.map(values, function(el) { return indexed[el]; });
4270 if (! self.get("effective_readonly")) {
4271 self.tags.containerElement().children().remove();
4272 self.$('textarea').css("padding-left", "3px");
4273 self.tags.addTags(_.map(data, function(el) {return {name: el[1], id:el[0]};}));
4275 self.$el.html(QWeb.render("FieldMany2ManyTag", {elements: data}));
4278 if (! values || values.length > 0) {
4279 return this._display_orderer.add(dataset.name_get(values)).done(handle_names);
4284 add_id: function(id) {
4285 this.set({'value': _.uniq(this.get('value').concat([id]))});
4287 focus: function () {
4288 var input = this.$text && this.$text[0];
4289 return input ? input.focus() : false;
4295 - reload_on_button: Reload the whole form view if click on a button in a list view.
4296 If you see this options, do not use it, it's basically a dirty hack to make one
4297 precise o2m to behave the way we want.
4299 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4300 multi_selection: false,
4301 init: function(field_manager, node) {
4302 this._super(field_manager, node);
4303 this.is_loaded = $.Deferred();
4304 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4305 this.dataset.m2m = this;
4307 this.dataset.on('unlink', self, function(ids) {
4308 self.dataset_changed();
4311 this.list_dm = new instance.web.DropMisordered();
4312 this.render_value_dm = new instance.web.DropMisordered();
4314 initialize_content: function() {
4317 this.$el.addClass('oe_form_field oe_form_field_many2many');
4319 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4320 'addable': this.get("effective_readonly") ? null : _t("Add"),
4321 'deletable': this.get("effective_readonly") ? false : true,
4322 'selectable': this.multi_selection,
4324 'reorderable': false,
4325 'import_enabled': false,
4327 var embedded = (this.field.views || {}).tree;
4329 this.list_view.set_embedded_view(embedded);
4331 this.list_view.m2m_field = this;
4332 var loaded = $.Deferred();
4333 this.list_view.on("list_view_loaded", this, function() {
4336 this.list_view.appendTo(this.$el);
4338 var old_def = self.is_loaded;
4339 self.is_loaded = $.Deferred().done(function() {
4342 this.list_dm.add(loaded).then(function() {
4343 self.is_loaded.resolve();
4346 destroy_content: function() {
4347 this.list_view.destroy();
4348 this.list_view = undefined;
4350 set_value: function(value_) {
4351 value_ = value_ || [];
4352 if (value_.length >= 1 && value_[0] instanceof Array) {
4353 value_ = value_[0][2];
4355 this._super(value_);
4357 get_value: function() {
4358 return [commands.replace_with(this.get('value'))];
4360 is_false: function () {
4361 return _(this.get("value")).isEmpty();
4363 render_value: function() {
4365 this.dataset.set_ids(this.get("value"));
4366 this.render_value_dm.add(this.is_loaded).then(function() {
4367 return self.list_view.reload_content();
4370 dataset_changed: function() {
4371 this.internal_set_value(this.dataset.ids);
4375 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4376 get_context: function() {
4377 this.context = this.m2m.build_context();
4378 return this.context;
4384 * @extends instance.web.ListView
4386 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4387 do_add_record: function () {
4388 var pop = new instance.web.form.SelectCreatePopup(this);
4392 title: _t("Add: ") + this.m2m_field.string,
4393 no_create: this.m2m_field.options.no_create,
4395 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4396 this.m2m_field.build_context()
4399 pop.on("elements_selected", self, function(element_ids) {
4401 _(element_ids).each(function (id) {
4402 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4403 self.dataset.set_ids(self.dataset.ids.concat([id]));
4404 self.m2m_field.dataset_changed();
4409 self.reload_content();
4413 do_activate_record: function(index, id) {
4415 var pop = new instance.web.form.FormOpenPopup(this);
4416 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4417 title: _t("Open: ") + this.m2m_field.string,
4418 readonly: this.getParent().get("effective_readonly")
4420 pop.on('write_completed', self, self.reload_content);
4422 do_button_action: function(name, id, callback) {
4424 var _sup = _.bind(this._super, this);
4425 if (! this.m2m_field.options.reload_on_button) {
4426 return _sup(name, id, callback);
4428 return this.m2m_field.view.save().then(function() {
4429 return _sup(name, id, function() {
4430 self.m2m_field.view.reload();
4435 is_action_enabled: function () { return true; },
4438 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4439 disable_utility_classes: true,
4440 init: function(field_manager, node) {
4441 this._super(field_manager, node);
4442 instance.web.form.CompletionFieldMixin.init.call(this);
4443 m2m_kanban_lazy_init();
4444 this.is_loaded = $.Deferred();
4445 this.initial_is_loaded = this.is_loaded;
4448 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4449 this.dataset.m2m = this;
4450 this.dataset.on('unlink', self, function(ids) {
4451 self.dataset_changed();
4455 this._super.apply(this, arguments);
4460 self.on("change:effective_readonly", self, function() {
4461 self.is_loaded = self.is_loaded.then(function() {
4462 self.kanban_view.destroy();
4463 return $.when(self.load_view()).done(function() {
4464 self.render_value();
4469 set_value: function(value_) {
4470 value_ = value_ || [];
4471 if (value_.length >= 1 && value_[0] instanceof Array) {
4472 value_ = value_[0][2];
4474 this._super(value_);
4476 get_value: function() {
4477 return [commands.replace_with(this.get('value'))];
4479 load_view: function() {
4481 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4482 'create_text': _t("Add"),
4483 'creatable': self.get("effective_readonly") ? false : true,
4484 'quick_creatable': self.get("effective_readonly") ? false : true,
4485 'read_only_mode': self.get("effective_readonly") ? true : false,
4486 'confirm_on_delete': false,
4488 var embedded = (this.field.views || {}).kanban;
4490 this.kanban_view.set_embedded_view(embedded);
4492 this.kanban_view.m2m = this;
4493 var loaded = $.Deferred();
4494 this.kanban_view.on("kanban_view_loaded",self,function() {
4495 self.initial_is_loaded.resolve();
4498 this.kanban_view.on('switch_mode', this, this.open_popup);
4499 $.async_when().done(function () {
4500 self.kanban_view.appendTo(self.$el);
4504 render_value: function() {
4506 this.dataset.set_ids(this.get("value"));
4507 this.is_loaded = this.is_loaded.then(function() {
4508 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
4511 dataset_changed: function() {
4512 this.set({'value': this.dataset.ids});
4514 open_popup: function(type, unused) {
4515 if (type !== "form")
4518 if (this.dataset.index === null) {
4519 var pop = new instance.web.form.SelectCreatePopup(this);
4521 this.field.relation,
4523 title: _t("Add: ") + this.string
4525 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
4526 this.build_context()
4528 pop.on("elements_selected", self, function(element_ids) {
4529 _.each(element_ids, function(one_id) {
4530 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
4531 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
4532 self.dataset_changed();
4533 self.render_value();
4538 var id = self.dataset.ids[self.dataset.index];
4539 var pop = new instance.web.form.FormOpenPopup(this);
4540 pop.show_element(self.field.relation, id, self.build_context(), {
4541 title: _t("Open: ") + self.string,
4542 write_function: function(id, data, options) {
4543 return self.dataset.write(id, data, {}).done(function() {
4544 self.render_value();
4547 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
4548 parent_view: self.view,
4549 child_name: self.name,
4550 readonly: self.get("effective_readonly")
4554 add_id: function(id) {
4555 this.quick_create.add_id(id);
4559 function m2m_kanban_lazy_init() {
4560 if (instance.web.form.Many2ManyKanbanView)
4562 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4563 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
4564 _is_quick_create_enabled: function() {
4565 return this._super() && ! this.group_by;
4568 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
4569 template: 'Many2ManyKanban.quick_create',
4572 * close_btn: If true, the widget will display a "Close" button able to trigger
4575 init: function(parent, dataset, context, buttons) {
4576 this._super(parent);
4577 this.m2m = this.getParent().view.m2m;
4578 this.m2m.quick_create = this;
4579 this._dataset = dataset;
4580 this._buttons = buttons || false;
4581 this._context = context || {};
4583 start: function () {
4585 self.$text = this.$el.find('input').css("width", "200px");
4586 self.$text.textext({
4587 plugins : 'arrow autocomplete',
4589 render: function(suggestion) {
4590 return $('<span class="text-label"/>').
4591 data('index', suggestion['index']).html(suggestion['label']);
4596 selectFromDropdown: function() {
4597 $(this).trigger('hideDropdown');
4598 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4599 var data = self.search_result[index];
4601 self.add_id(data.id);
4608 itemToString: function(item) {
4613 }).bind('getSuggestions', function(e, data) {
4615 var str = !!data ? data.query || '' : '';
4616 self.m2m.get_search_result(str).done(function(result) {
4617 self.search_result = result;
4618 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4619 return _.extend(el, {index:i});
4623 self.$text.focusout(function() {
4628 this.$text[0].focus();
4630 add_id: function(id) {
4633 self.trigger('added', id);
4634 this.m2m.dataset_changed();
4640 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
4642 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
4643 template: "AbstractFormPopup.render",
4646 * -readonly: only applicable when not in creation mode, default to false
4647 * - alternative_form_view
4654 * - form_view_options
4656 init_popup: function(model, row_id, domain, context, options) {
4657 this.row_id = row_id;
4659 this.domain = domain || [];
4660 this.context = context || {};
4661 this.options = options;
4662 _.defaults(this.options, {
4665 init_dataset: function() {
4667 this.created_elements = [];
4668 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
4669 this.dataset.read_function = this.options.read_function;
4670 this.dataset.create_function = function(data, options, sup) {
4671 var fct = self.options.create_function || sup;
4672 return fct.call(this, data, options).done(function(r) {
4673 self.trigger('create_completed saved', r);
4674 self.created_elements.push(r);
4677 this.dataset.write_function = function(id, data, options, sup) {
4678 var fct = self.options.write_function || sup;
4679 return fct.call(this, id, data, options).done(function(r) {
4680 self.trigger('write_completed saved', r);
4683 this.dataset.parent_view = this.options.parent_view;
4684 this.dataset.child_name = this.options.child_name;
4686 display_popup: function() {
4688 this.renderElement();
4689 var dialog = new instance.web.Dialog(this, {
4691 dialogClass: 'oe_act_window',
4693 self.check_exit(true);
4695 title: this.options.title || "",
4696 }, this.$el).open();
4697 this.$buttonpane = dialog.$buttons;
4700 setup_form_view: function() {
4703 this.dataset.ids = [this.row_id];
4704 this.dataset.index = 0;
4706 this.dataset.index = null;
4708 var options = _.clone(self.options.form_view_options) || {};
4709 if (this.row_id !== null) {
4710 options.initial_mode = this.options.readonly ? "view" : "edit";
4713 $buttons: this.$buttonpane,
4715 this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
4716 if (this.options.alternative_form_view) {
4717 this.view_form.set_embedded_view(this.options.alternative_form_view);
4719 this.view_form.appendTo(this.$el.find(".oe_popup_form"));
4720 this.view_form.on("form_view_loaded", self, function() {
4721 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
4722 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
4723 multi_select: multi_select,
4724 readonly: self.row_id !== null && self.options.readonly,
4726 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
4727 $snbutton.click(function() {
4728 $.when(self.view_form.save()).done(function() {
4729 self.view_form.reload_mutex.exec(function() {
4730 self.view_form.on_button_new();
4734 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
4735 $sbutton.click(function() {
4736 $.when(self.view_form.save()).done(function() {
4737 self.view_form.reload_mutex.exec(function() {
4742 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
4743 $cbutton.click(function() {
4744 self.view_form.trigger('on_button_cancel');
4747 self.view_form.do_show();
4750 select_elements: function(element_ids) {
4751 this.trigger("elements_selected", element_ids);
4753 check_exit: function(no_destroy) {
4754 if (this.created_elements.length > 0) {
4755 this.select_elements(this.created_elements);
4756 this.created_elements = [];
4758 this.trigger('closed');
4761 destroy: function () {
4762 this.trigger('closed');
4763 if (this.$el.is(":data(dialog)")) {
4764 this.$el.dialog('close');
4771 * Class to display a popup containing a form view.
4773 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
4774 show_element: function(model, row_id, context, options) {
4775 this.init_popup(model, row_id, [], context, options);
4776 _.defaults(this.options, {
4778 this.display_popup();
4782 this.init_dataset();
4783 this.setup_form_view();
4788 * Class to display a popup to display a list to search a row. It also allows
4789 * to switch to a form view to create a new row.
4791 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
4795 * - initial_view: form or search (default search)
4796 * - disable_multiple_selection
4797 * - list_view_options
4799 select_element: function(model, options, domain, context) {
4800 this.init_popup(model, null, domain, context, options);
4802 _.defaults(this.options, {
4803 initial_view: "search",
4805 this.initial_ids = this.options.initial_ids;
4806 this.display_popup();
4810 this.init_dataset();
4811 if (this.options.initial_view == "search") {
4812 instance.web.pyeval.eval_domains_and_contexts({
4814 contexts: [this.context]
4815 }).done(function (results) {
4816 var search_defaults = {};
4817 _.each(results.context, function (value_, key) {
4818 var match = /^search_default_(.*)$/.exec(key);
4820 search_defaults[match[1]] = value_;
4823 self.setup_search_view(search_defaults);
4829 setup_search_view: function(search_defaults) {
4831 if (this.searchview) {
4832 this.searchview.destroy();
4834 this.searchview = new instance.web.SearchView(this,
4835 this.dataset, false, search_defaults);
4836 this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
4837 if (self.initial_ids) {
4838 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
4839 contexts.concat(self.context), groupbys);
4840 self.initial_ids = undefined;
4842 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
4845 this.searchview.on("search_view_loaded", self, function() {
4846 self.view_list = new instance.web.form.SelectCreateListView(self,
4847 self.dataset, false,
4848 _.extend({'deletable': false,
4849 'selectable': !self.options.disable_multiple_selection,
4850 'import_enabled': false,
4851 '$buttons': self.$buttonpane,
4852 'disable_editable_mode': true,
4853 '$pager': self.$('.oe_popup_list_pager'),
4854 }, self.options.list_view_options || {}));
4855 self.view_list.on('edit:before', self, function (e) {
4858 self.view_list.popup = self;
4859 self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
4860 self.view_list.do_show();
4861 }).then(function() {
4862 self.searchview.do_search();
4864 self.view_list.on("list_view_loaded", self, function() {
4865 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
4866 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
4867 $cbutton.click(function() {
4870 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
4871 $sbutton.click(function() {
4872 self.select_elements(self.selected_ids);
4875 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
4876 $cbutton.click(function() {
4881 this.searchview.appendTo($(".oe_popup_search", self.$el));
4883 do_search: function(domains, contexts, groupbys) {
4885 instance.web.pyeval.eval_domains_and_contexts({
4886 domains: domains || [],
4887 contexts: contexts || [],
4888 group_by_seq: groupbys || []
4889 }).done(function (results) {
4890 self.view_list.do_search(results.domain, results.context, results.group_by);
4893 on_click_element: function(ids) {
4895 this.selected_ids = ids || [];
4896 if(this.selected_ids.length > 0) {
4897 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
4899 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
4902 new_object: function() {
4903 if (this.searchview) {
4904 this.searchview.hide();
4906 if (this.view_list) {
4907 this.view_list.do_hide();
4909 this.setup_form_view();
4913 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
4914 do_add_record: function () {
4915 this.popup.new_object();
4917 select_record: function(index) {
4918 this.popup.select_elements([this.dataset.ids[index]]);
4919 this.popup.destroy();
4921 do_select: function(ids, records) {
4922 this._super(ids, records);
4923 this.popup.on_click_element(ids);
4927 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4928 template: 'FieldReference',
4929 init: function(field_manager, node) {
4930 this._super(field_manager, node);
4931 this.reference_ready = true;
4933 destroy_content: function() {
4936 this.fm = undefined;
4939 initialize_content: function() {
4941 var fm = new instance.web.form.DefaultFieldManager(this);
4943 fm.extend_field_desc({
4945 selection: this.field_manager.get_field_desc(this.name).selection,
4953 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
4955 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
4957 this.selection.on("change:value", this, this.on_selection_changed);
4958 this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
4960 .on('focused', null, function () {self.trigger('focused')})
4961 .on('blurred', null, function () {self.trigger('blurred')});
4963 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
4964 name: 'Referenced Document',
4965 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
4967 this.m2o.on("change:value", this, this.data_changed);
4968 this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
4970 .on('focused', null, function () {self.trigger('focused')})
4971 .on('blurred', null, function () {self.trigger('blurred')});
4973 on_selection_changed: function() {
4974 if (this.reference_ready) {
4975 this.internal_set_value([this.selection.get_value(), false]);
4976 this.render_value();
4979 data_changed: function() {
4980 if (this.reference_ready) {
4981 this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
4984 set_value: function(val) {
4986 val = val.split(',');
4987 val[0] = val[0] || false;
4988 val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
4990 this._super(val || [false, false]);
4992 get_value: function() {
4993 return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
4995 render_value: function() {
4996 this.reference_ready = false;
4997 if (!this.get("effective_readonly")) {
4998 this.selection.set_value(this.get('value')[0]);
5000 this.m2o.field.relation = this.get('value')[0];
5001 this.m2o.set_value(this.get('value')[1]);
5002 this.m2o.$el.toggle(!!this.get('value')[0]);
5003 this.reference_ready = true;
5007 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5008 init: function(field_manager, node) {
5010 this._super(field_manager, node);
5011 this.binary_value = false;
5012 this.useFileAPI = !!window.FileReader;
5013 this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5014 if (!this.useFileAPI) {
5015 this.fileupload_id = _.uniqueId('oe_fileupload');
5016 $(window).on(this.fileupload_id, function() {
5017 var args = [].slice.call(arguments).slice(1);
5018 self.on_file_uploaded.apply(self, args);
5023 if (!this.useFileAPI) {
5024 $(window).off(this.fileupload_id);
5026 this._super.apply(this, arguments);
5028 initialize_content: function() {
5029 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5030 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5031 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5033 on_file_change: function(e) {
5035 var file_node = e.target;
5036 if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5037 if (this.useFileAPI) {
5038 var file = file_node.files[0];
5039 if (file.size > this.max_upload_size) {
5040 var msg = _t("The selected file exceed the maximum file size of %s.");
5041 instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5044 var filereader = new FileReader();
5045 filereader.readAsDataURL(file);
5046 filereader.onloadend = function(upload) {
5047 var data = upload.target.result;
5048 data = data.split(',')[1];
5049 self.on_file_uploaded(file.size, file.name, file.type, data);
5052 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5053 this.$el.find('form.oe_form_binary_form').submit();
5055 this.$el.find('.oe_form_binary_progress').show();
5056 this.$el.find('.oe_form_binary').hide();
5059 on_file_uploaded: function(size, name, content_type, file_base64) {
5060 if (size === false) {
5061 this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5062 // TODO: use openerp web crashmanager
5063 console.warn("Error while uploading file : ", name);
5065 this.filename = name;
5066 this.on_file_uploaded_and_valid.apply(this, arguments);
5068 this.$el.find('.oe_form_binary_progress').hide();
5069 this.$el.find('.oe_form_binary').show();
5071 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5073 on_save_as: function(ev) {
5074 var value = this.get('value');
5076 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5077 ev.stopPropagation();
5079 instance.web.blockUI();
5080 var c = instance.webclient.crashmanager;
5081 this.session.get_file({
5082 url: '/web/binary/saveas_ajax',
5083 data: {data: JSON.stringify({
5084 model: this.view.dataset.model,
5085 id: (this.view.datarecord.id || ''),
5087 filename_field: (this.node.attrs.filename || ''),
5088 data: instance.web.form.is_bin_size(value) ? null : value,
5089 context: this.view.dataset.get_context()
5091 complete: instance.web.unblockUI,
5092 error: c.rpc_error.bind(c)
5094 ev.stopPropagation();
5098 set_filename: function(value) {
5099 var filename = this.node.attrs.filename;
5102 tmp[filename] = value;
5103 this.field_manager.set_values(tmp);
5106 on_clear: function() {
5107 if (this.get('value') !== false) {
5108 this.binary_value = false;
5109 this.internal_set_value(false);
5115 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5116 template: 'FieldBinaryFile',
5117 initialize_content: function() {
5119 if (this.get("effective_readonly")) {
5121 this.$el.find('a').click(function(ev) {
5122 if (self.get('value')) {
5123 self.on_save_as(ev);
5129 render_value: function() {
5130 if (!this.get("effective_readonly")) {
5132 if (this.node.attrs.filename) {
5133 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5135 show_value = (this.get('value') != null && this.get('value') !== false) ? this.get('value') : '';
5137 this.$el.find('input').eq(0).val(show_value);
5139 this.$el.find('a').toggle(!!this.get('value'));
5140 if (this.get('value')) {
5141 var show_value = _t("Download")
5143 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5144 this.$el.find('a').text(show_value);
5148 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5149 this.binary_value = true;
5150 this.internal_set_value(file_base64);
5151 var show_value = name + " (" + instance.web.human_size(size) + ")";
5152 this.$el.find('input').eq(0).val(show_value);
5153 this.set_filename(name);
5155 on_clear: function() {
5156 this._super.apply(this, arguments);
5157 this.$el.find('input').eq(0).val('');
5158 this.set_filename('');
5162 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5163 template: 'FieldBinaryImage',
5164 placeholder: "/web/static/src/img/placeholder.png",
5165 render_value: function() {
5168 if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5169 url = 'data:image/png;base64,' + this.get('value');
5170 } else if (this.get('value')) {
5171 var id = JSON.stringify(this.view.datarecord.id || null);
5172 var field = this.name;
5173 if (this.options.preview_image)
5174 field = this.options.preview_image;
5175 url = this.session.url('/web/binary/image', {
5176 model: this.view.dataset.model,
5179 t: (new Date().getTime()),
5182 url = this.placeholder;
5184 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5185 this.$el.find('> img').remove();
5186 this.$el.prepend($img);
5187 $img.load(function() {
5188 if (! self.options.size)
5190 $img.css("max-width", "" + self.options.size[0] + "px");
5191 $img.css("max-height", "" + self.options.size[1] + "px");
5192 $img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px");
5193 $img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px");
5195 $img.on('error', function() {
5196 $img.attr('src', self.placeholder);
5197 instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5200 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5201 this.internal_set_value(file_base64);
5202 this.binary_value = true;
5203 this.render_value();
5204 this.set_filename(name);
5206 on_clear: function() {
5207 this._super.apply(this, arguments);
5208 this.render_value();
5209 this.set_filename('');
5211 set_value: function(value_){
5212 var changed = value_ !== this.get_value();
5213 this._super.apply(this, arguments);
5214 // By default, on binary images read, the server returns the binary size
5215 // This is possible that two images have the exact same size
5216 // Therefore we trigger the change in case the image value hasn't changed
5217 // So the image is re-rendered correctly
5219 this.trigger("change:value", this, {
5228 * Widget for (one2many field) to upload one or more file in same time and display in list.
5229 * The user can delete his files.
5230 * Options on attribute ; "blockui" {Boolean} block the UI or not
5231 * during the file is uploading
5233 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5234 template: "FieldBinaryFileUploader",
5235 init: function(field_manager, node) {
5236 this._super(field_manager, node);
5237 this.field_manager = field_manager;
5239 if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5240 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);
5242 this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5243 this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5244 $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5246 initialize_content: function() {
5247 this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5249 set_value: function(value_) {
5250 var value_ = value_ || [];
5253 _.each(value_, function(command) {
5254 if (isNaN(command) && command.id == undefined) {
5255 switch (command[0]) {
5256 case commands.CREATE:
5257 ids = ids.concat(command[2]);
5259 case commands.REPLACE_WITH:
5260 ids = ids.concat(command[2]);
5262 case commands.UPDATE:
5263 ids = ids.concat(command[2]);
5265 case commands.LINK_TO:
5266 ids = ids.concat(command[1]);
5268 case commands.DELETE:
5269 ids = _.filter(ids, function (id) { return id != command[1];});
5271 case commands.DELETE_ALL:
5281 get_value: function() {
5282 return _.map(this.get('value'), function (value) { return commands.link_to( isNaN(value) ? value.id : value ); });
5284 get_file_url: function (attachment) {
5285 return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5287 read_name_values : function () {
5289 // select the list of id for a get_name
5291 _.each(this.get('value'), function (val) {
5292 if (typeof val != 'object') {
5296 // send request for get_name
5297 if (values.length) {
5298 return this.ds_file.call('read', [values, ['id', 'name', 'datas_fname']]).done(function (datas) {
5299 _.each(datas, function (data) {
5300 data.no_unlink = true;
5301 data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5303 _.each(self.get('value'), function (val, key) {
5304 if(val == data.id) {
5305 self.get('value')[key] = data;
5311 return $.when(this.get('value'));
5314 render_value: function () {
5316 this.read_name_values().then(function (datas) {
5318 var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self}));
5319 render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5320 self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5322 // reinit input type file
5323 var $input = self.$('input.oe_form_binary_file');
5324 $input.after($input.clone(true)).remove();
5325 self.$(".oe_fileupload").show();
5329 on_file_change: function (event) {
5330 event.stopPropagation();
5332 var $target = $(event.target);
5333 if ($target.val() !== '') {
5335 var filename = $target.val().replace(/.*[\\\/]/,'');
5337 // if the files is currently uploded, don't send again
5338 if( !isNaN(_.find(this.get('value'), function (file) { return (file.filename || file.name) == filename && file.upload; } )) ) {
5343 if(this.node.attrs.blockui>0) {
5344 instance.web.blockUI();
5347 // if the files exits for this answer, delete the file before upload
5348 var files = _.filter(this.get('value'), function (file) {
5349 if((file.filename || file.name) == filename) {
5350 self.ds_file.unlink([file.id]);
5357 // TODO : unactivate send on wizard and form
5360 this.$('form.oe_form_binary_form').submit();
5361 this.$(".oe_fileupload").hide();
5363 // add file on result
5367 'filename': filename,
5372 this.set({'value': files});
5375 on_file_loaded: function (event, result) {
5376 var files = this.get('value');
5379 if(this.node.attrs.blockui>0) {
5380 instance.web.unblockUI();
5383 // TODO : activate send on wizard and form
5385 if (result.error || !result.id ) {
5386 this.do_warn( _t('Uploading Error'), result.error);
5387 files = _.filter(files, function (val) { return !val.upload; });
5389 for(var i in files){
5390 if(files[i].filename == result.filename && files[i].upload) {
5393 'name': result.name,
5394 'filename': result.filename,
5395 'url': this.get_file_url(result)
5401 this.set({'value': files});
5404 on_file_delete: function (event) {
5405 event.stopPropagation();
5406 var file_id=$(event.target).data("id");
5409 for(var i in this.get('value')){
5410 if(file_id != this.get('value')[i].id){
5411 files.push(this.get('value')[i]);
5413 else if(!this.get('value')[i].no_unlink) {
5414 this.ds_file.unlink([file_id]);
5417 this.set({'value': files});
5422 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5423 template: "FieldStatus",
5424 init: function(field_manager, node) {
5425 this._super(field_manager, node);
5426 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5427 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5428 this.set({value: false});
5429 this.selection = [];
5430 this.set("selection", []);
5431 this.selection_dm = new instance.web.DropMisordered();
5434 this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5436 this.on("change:value", this, this.get_selection);
5437 this.on("change:evaluated_selection_domain", this, this.get_selection);
5438 this.on("change:selection", this, function() {
5439 this.selection = this.get("selection");
5440 this.render_value();
5442 this.get_selection();
5443 if (this.options.clickable) {
5444 this.$el.on('click','li',this.on_click_stage);
5448 set_value: function(value_) {
5449 if (value_ instanceof Array) {
5452 this._super(value_);
5454 render_value: function() {
5456 var content = QWeb.render("FieldStatus.content", {widget: self});
5457 self.$el.html(content);
5458 var colors = JSON.parse((self.node.attrs || {}).statusbar_colors || "{}");
5459 var color = colors[self.get('value')];
5461 self.$("oe_active").css("color", color);
5464 calc_domain: function() {
5465 var d = instance.web.pyeval.eval('domain', this.build_domain());
5466 var domain = []; //if there is no domain defined, fetch all the records
5469 domain = ['|',['id', '=', this.get('value')]].concat(d);
5472 if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5473 this.set("evaluated_selection_domain", domain);
5476 /** Get the selection and render it
5477 * selection: [[identifier, value_to_display], ...]
5478 * For selection fields: this is directly given by this.field.selection
5479 * For many2one fields: perform a search on the relation of the many2one field
5481 get_selection: function() {
5485 var calculation = _.bind(function() {
5486 if (this.field.type == "many2one") {
5488 var ds = new instance.web.DataSetSearch(this, this.field.relation,
5489 self.build_context(), this.get("evaluated_selection_domain"));
5490 return ds.read_slice(['name'], {}).then(function (records) {
5491 for(var i = 0; i < records.length; i++) {
5492 selection.push([records[i].id, records[i].name]);
5496 // For field type selection filter values according to
5497 // statusbar_visible attribute of the field. For example:
5498 // statusbar_visible="draft,open".
5499 var select = this.field.selection;
5500 for(var i=0; i < select.length; i++) {
5501 var key = select[i][0];
5502 if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5503 selection.push(select[i]);
5509 this.selection_dm.add(calculation()).then(function () {
5510 if (! _.isEqual(selection, self.get("selection"))) {
5511 self.set("selection", selection);
5515 on_click_stage: function (ev) {
5517 var $li = $(ev.currentTarget);
5518 if (this.field.type == "many2one") {
5519 var val = parseInt($li.data("id"));
5522 var val = $li.data("id");
5524 if (val != self.get('value')) {
5525 this.view.recursive_save().done(function() {
5527 change[self.name] = val;
5528 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
5536 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
5537 template: "FieldMonetary",
5538 widget_class: 'oe_form_field_float oe_form_field_monetary',
5540 this._super.apply(this, arguments);
5541 this.set({"currency": false});
5542 if (this.options.currency_field) {
5543 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
5544 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
5547 this.on("change:currency", this, this.get_currency_info);
5548 this.get_currency_info();
5549 this.ci_dm = new instance.web.DropMisordered();
5552 var tmp = this._super();
5553 this.on("change:currency_info", this, this.reinitialize);
5556 get_currency_info: function() {
5558 if (this.get("currency") === false) {
5559 this.set({"currency_info": null});
5562 return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
5563 .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
5564 self.set({"currency_info": res});
5567 parse_value: function(val, def) {
5568 return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5570 format_value: function(val, def) {
5571 return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5576 * Registry of form fields, called by :js:`instance.web.FormView`.
5578 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
5579 * will substitute to the <field> tags as defined in OpenERP's views.
5581 instance.web.form.widgets = new instance.web.Registry({
5582 'char' : 'instance.web.form.FieldChar',
5583 'id' : 'instance.web.form.FieldID',
5584 'email' : 'instance.web.form.FieldEmail',
5585 'url' : 'instance.web.form.FieldUrl',
5586 'text' : 'instance.web.form.FieldText',
5587 'html' : 'instance.web.form.FieldTextHtml',
5588 'date' : 'instance.web.form.FieldDate',
5589 'datetime' : 'instance.web.form.FieldDatetime',
5590 'selection' : 'instance.web.form.FieldSelection',
5591 'many2one' : 'instance.web.form.FieldMany2One',
5592 'many2onebutton' : 'instance.web.form.Many2OneButton',
5593 'many2many' : 'instance.web.form.FieldMany2Many',
5594 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
5595 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
5596 'one2many' : 'instance.web.form.FieldOne2Many',
5597 'one2many_list' : 'instance.web.form.FieldOne2Many',
5598 'reference' : 'instance.web.form.FieldReference',
5599 'boolean' : 'instance.web.form.FieldBoolean',
5600 'float' : 'instance.web.form.FieldFloat',
5601 'integer': 'instance.web.form.FieldFloat',
5602 'float_time': 'instance.web.form.FieldFloat',
5603 'progressbar': 'instance.web.form.FieldProgressBar',
5604 'image': 'instance.web.form.FieldBinaryImage',
5605 'binary': 'instance.web.form.FieldBinaryFile',
5606 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
5607 'statusbar': 'instance.web.form.FieldStatus',
5608 'monetary': 'instance.web.form.FieldMonetary',
5612 * Registry of widgets usable in the form view that can substitute to any possible
5613 * tags defined in OpenERP's form views.
5615 * Every referenced class should extend FormWidget.
5617 instance.web.form.tags = new instance.web.Registry({
5618 'button' : 'instance.web.form.WidgetButton',
5621 instance.web.form.custom_widgets = new instance.web.Registry({
5626 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: