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 load_form: function(data) {
155 throw new Error(_t("No data provided."));
158 throw "Form view does not support multiple calls to load_form";
160 this.fields_order = [];
161 this.fields_view = data;
163 this.rendering_engine.set_fields_registry(this.fields_registry);
164 this.rendering_engine.set_tags_registry(this.tags_registry);
165 this.rendering_engine.set_widgets_registry(this.widgets_registry);
166 this.rendering_engine.set_fields_view(data);
167 var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container');
168 this.rendering_engine.render_to($dest);
170 this.$el.on('mousedown.formBlur', function () {
171 self.__clicked_inside = true;
174 this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self}));
175 if (this.options.$buttons) {
176 this.$buttons.appendTo(this.options.$buttons);
178 this.$el.find('.oe_form_buttons').replaceWith(this.$buttons);
180 this.$buttons.on('click', '.oe_form_button_create',
181 this.guard_active(this.on_button_create));
182 this.$buttons.on('click', '.oe_form_button_edit',
183 this.guard_active(this.on_button_edit));
184 this.$buttons.on('click', '.oe_form_button_save',
185 this.guard_active(this.on_button_save));
186 this.$buttons.on('click', '.oe_form_button_cancel',
187 this.guard_active(this.on_button_cancel));
188 if (this.options.footer_to_buttons) {
189 this.$el.find('footer').appendTo(this.$buttons);
192 this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar');
193 if (!this.sidebar && this.options.$sidebar) {
194 this.sidebar = new instance.web.Sidebar(this);
195 this.sidebar.appendTo(this.$sidebar);
196 if (this.fields_view.toolbar) {
197 this.sidebar.add_toolbar(this.fields_view.toolbar);
199 this.sidebar.add_items('other', _.compact([
200 self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete },
201 self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate }
205 this.has_been_loaded.resolve();
207 // Add bounce effect on button 'Edit' when click on readonly page view.
208 this.$el.find(".oe_form_group_row,.oe_form_field,label").on('click', function (e) {
209 if(self.get("actual_mode") == "view") {
210 var $button = self.options.$buttons.find(".oe_form_button_edit");
211 $button.openerpBounce();
213 instance.web.bus.trigger('click', e);
216 //bounce effect on red button when click on statusbar.
217 this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) {
218 if((self.get("actual_mode") == "view")) {
219 var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'});
220 $button.openerpBounce();
224 this.trigger('form_view_loaded', data);
227 widgetFocused: function() {
228 // Clear click flag if used to focus a widget
229 this.__clicked_inside = false;
230 if (this.__blur_timeout) {
231 clearTimeout(this.__blur_timeout);
232 this.__blur_timeout = null;
235 widgetBlurred: function() {
236 if (this.__clicked_inside) {
237 // clicked in an other section of the form (than the currently
238 // focused widget) => just ignore the blurring entirely?
239 this.__clicked_inside = false;
243 // clear timeout, if any
244 this.widgetFocused();
245 this.__blur_timeout = setTimeout(function () {
246 self.trigger('blurred');
250 do_load_state: function(state, warm) {
251 if (state.id && this.datarecord.id != state.id) {
252 if (this.dataset.get_id_index(state.id) === null) {
253 this.dataset.ids.push(state.id);
255 this.dataset.select_id(state.id);
256 this.do_show({ reload: warm });
261 * @param {Object} [options]
262 * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view".
263 * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record
264 * @return {$.Deferred}
266 do_show: function (options) {
268 options = options || {};
270 this.sidebar.$el.show();
273 this.$buttons.show();
275 this.$el.show().css({
277 filter: 'alpha(opacity = 0)'
279 this.$el.add(this.$buttons).removeClass('oe_form_dirty');
281 var shown = this.has_been_loaded;
282 if (options.reload !== false) {
283 shown = shown.then(function() {
284 if (self.dataset.index === null) {
285 // null index means we should start a new record
286 return self.on_button_new();
288 var fields = _.keys(self.fields_view.fields);
289 fields.push('display_name');
290 return self.dataset.read_index(fields, {
291 context: { 'bin_size': true, 'future_display_name' : true }
292 }).then(function(r) {
293 self.trigger('load_record', r);
297 return shown.then(function() {
298 self._actualize_mode(options.mode || self.options.initial_mode);
301 filter: 'alpha(opacity = 100)'
305 do_hide: function () {
307 this.sidebar.$el.hide();
310 this.$buttons.hide();
317 load_record: function(record) {
318 var self = this, set_values = [];
320 this.set({ 'title' : undefined });
321 this.do_warn(_t("Form"), _t("The record could not be found in the database."), true);
322 return $.Deferred().reject();
324 this.datarecord = record;
325 this._actualize_mode();
326 this.set({ 'title' : record.id ? record.display_name : _t("New") });
328 _(this.fields).each(function (field, f) {
329 field._dirty_flag = false;
330 field._inhibit_on_change_flag = true;
331 var result = field.set_value(self.datarecord[f] || false);
332 field._inhibit_on_change_flag = false;
333 set_values.push(result);
335 return $.when.apply(null, set_values).then(function() {
337 // New record: Second pass in order to trigger the onchanges
338 // respecting the fields order defined in the view
339 _.each(self.fields_order, function(field_name) {
340 if (record[field_name] !== undefined) {
341 var field = self.fields[field_name];
342 field._dirty_flag = true;
343 self.do_onchange(field);
347 self.on_form_changed();
348 self.rendering_engine.init_fields();
349 self.is_initialized.resolve();
350 self.do_update_pager(record.id == null);
352 self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
355 self.do_push_state({id:record.id});
357 self.do_push_state({});
359 self.$el.add(self.$buttons).removeClass('oe_form_dirty');
364 * Loads and sets up the default values for the model as the current
367 * @return {$.Deferred}
369 load_defaults: function () {
371 var keys = _.keys(this.fields_view.fields);
373 return this.dataset.default_get(keys).then(function(r) {
374 self.trigger('load_record', r);
377 return self.trigger('load_record', {});
379 on_form_changed: function() {
380 this.trigger("view_content_has_changed");
382 do_notify_change: function() {
383 this.$el.add(this.$buttons).addClass('oe_form_dirty');
385 execute_pager_action: function(action) {
386 if (this.can_be_discarded()) {
389 this.dataset.index = 0;
392 this.dataset.previous();
398 this.dataset.index = this.dataset.ids.length - 1;
402 this.trigger('pager_action_executed');
405 init_pager: function() {
408 this.$pager.remove();
409 if (this.get("actual_mode") === "create")
411 this.$pager = $(QWeb.render("FormView.pager", {'widget':self})).hide();
412 if (this.options.$pager) {
413 this.$pager.appendTo(this.options.$pager);
415 this.$el.find('.oe_form_pager').replaceWith(this.$pager);
417 this.$pager.on('click','a[data-pager-action]',function() {
418 var action = $(this).data('pager-action');
419 self.execute_pager_action(action);
421 this.do_update_pager();
423 do_update_pager: function(hide_index) {
424 this.$pager.toggle(this.dataset.ids.length > 1);
426 $(".oe_form_pager_state", this.$pager).html("");
428 $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
431 parse_on_change: function (on_change, widget) {
433 var onchange = _.str.trim(on_change);
434 var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
436 throw new Error(_.str.sprintf( _t("Wrong on change format: %s"), onchange ));
439 var method = call[1];
440 if (!_.str.trim(call[2])) {
441 return {method: method, args: []}
444 var argument_replacement = {
445 'False': function () {return false;},
446 'True': function () {return true;},
447 'None': function () {return null;},
448 'context': function () {
449 return new instance.web.CompoundContext(
450 self.dataset.get_context(),
451 widget.build_context() ? widget.build_context() : {});
454 var parent_fields = null;
455 var args = _.map(call[2].split(','), function (a, i) {
456 var field = _.str.trim(a);
458 // literal constant or context
459 if (field in argument_replacement) {
460 return argument_replacement[field]();
463 if (/^-?\d+(\.\d+)?$/.test(field)) {
464 return Number(field);
467 if (self.fields[field]) {
468 var value_ = self.fields[field].get_value();
469 return value_ == null ? false : value_;
472 var splitted = field.split('.');
473 if (splitted.length > 1 && _.str.trim(splitted[0]) === "parent" && self.dataset.parent_view) {
474 if (parent_fields === null) {
475 parent_fields = self.dataset.parent_view.get_fields_values();
477 var p_val = parent_fields[_.str.trim(splitted[1])];
478 if (p_val !== undefined) {
479 return p_val == null ? false : p_val;
483 var first_char = field[0], last_char = field[field.length-1];
484 if ((first_char === '"' && last_char === '"')
485 || (first_char === "'" && last_char === "'")) {
486 return field.slice(1, -1);
489 throw new Error("Could not get field with name '" + field +
490 "' for onchange '" + onchange + "'");
498 do_onchange: function(widget, processed) {
500 this.on_change_list = [{widget: widget, processed: processed}].concat(this.on_change_list);
501 return this._process_operations();
503 _process_onchange: function(on_change_obj) {
505 var widget = on_change_obj.widget;
506 var processed = on_change_obj.processed;
509 processed = processed || [];
510 processed.push(widget.name);
511 var on_change = widget.node.attrs.on_change;
513 var change_spec = self.parse_on_change(on_change, widget);
515 if (self.datarecord.id && !instance.web.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) {
516 // In case of a o2m virtual id, we should pass an empty ids list
517 ids.push(self.datarecord.id);
519 def = self.alive(new instance.web.Model(self.dataset.model).call(
520 change_spec.method, [ids].concat(change_spec.args)));
524 return def.then(function(response) {
525 if (widget.field['change_default']) {
526 var fieldname = widget.name;
528 if (response.value && (fieldname in response.value)) {
529 // Use value from onchange if onchange executed
530 value_ = response.value[fieldname];
532 // otherwise get form value for field
533 value_ = self.fields[fieldname].get_value();
535 var condition = fieldname + '=' + value_;
538 return self.alive(new instance.web.Model('ir.values').call(
539 'get_defaults', [self.model, condition]
540 )).then(function (results) {
541 if (!results.length) {
544 if (!response.value) {
547 for(var i=0; i<results.length; ++i) {
548 // [whatever, key, value]
549 var triplet = results[i];
550 response.value[triplet[1]] = triplet[2];
557 }).then(function(response) {
558 return self.on_processed_onchange(response, processed);
562 instance.webclient.crashmanager.show_message(e);
563 return $.Deferred().reject();
566 on_processed_onchange: function(result, processed) {
569 this._internal_set_values(result.value, processed);
571 if (!_.isEmpty(result.warning)) {
572 instance.web.dialog($(QWeb.render("CrashManager.warning", result.warning)), {
573 title:result.warning.title,
576 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
581 var fields = this.fields;
582 _(result.domain).each(function (domain, fieldname) {
583 var field = fields[fieldname];
584 if (!field) { return; }
585 field.node.attrs.domain = domain;
588 return $.Deferred().resolve();
591 instance.webclient.crashmanager.show_message(e);
592 return $.Deferred().reject();
595 _process_operations: function() {
597 return this.mutating_mutex.exec(function() {
599 var on_change_obj = self.on_change_list.shift();
601 return self._process_onchange(on_change_obj).then(function() {
606 _.each(self.fields, function(field) {
607 defs.push(field.commit_value());
609 var args = _.toArray(arguments);
610 return $.when.apply($, defs).then(function() {
611 if (self.on_change_list.length !== 0) {
614 var save_obj = self.save_list.pop();
616 return self._process_save(save_obj).then(function() {
617 save_obj.ret = _.toArray(arguments);
620 save_obj.error = true;
625 self.save_list.pop();
632 _internal_set_values: function(values, exclude) {
633 exclude = exclude || [];
634 for (var f in values) {
635 if (!values.hasOwnProperty(f)) { continue; }
636 var field = this.fields[f];
637 // If field is not defined in the view, just ignore it
639 var value_ = values[f];
640 if (field.get_value() != value_) {
641 field._inhibit_on_change_flag = true;
642 field.set_value(value_);
643 field._inhibit_on_change_flag = false;
644 field._dirty_flag = true;
645 if (!_.contains(exclude, field.name)) {
646 this.do_onchange(field, exclude);
651 this.on_form_changed();
653 set_values: function(values) {
655 return this.mutating_mutex.exec(function() {
656 self._internal_set_values(values);
660 * Ask the view to switch to view mode if possible. The view may not do it
661 * if the current record is not yet saved. It will then stay in create mode.
663 to_view_mode: function() {
664 this._actualize_mode("view");
667 * Ask the view to switch to edit mode if possible. The view may not do it
668 * if the current record is not yet saved. It will then stay in create mode.
670 to_edit_mode: function() {
671 this._actualize_mode("edit");
674 * Ask the view to switch to a precise mode if possible. The view is free to
675 * not respect this command if the state of the dataset is not compatible with
676 * the new mode. For example, it is not possible to switch to edit mode if
677 * the current record is not yet saved in database.
679 * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
680 * undefined the view will test the actual mode to check if it is still consistent
681 * with the dataset state.
683 _actualize_mode: function(switch_to) {
684 var mode = switch_to || this.get("actual_mode");
685 if (! this.datarecord.id) {
687 } else if (mode === "create") {
690 this.render_value_defs = [];
691 this.set({actual_mode: mode});
693 check_actual_mode: function(source, options) {
695 if(this.get("actual_mode") === "view") {
696 self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
697 self.$buttons.find('.oe_form_buttons_edit').hide();
698 self.$buttons.find('.oe_form_buttons_view').show();
699 self.$sidebar.show();
701 self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
702 self.$buttons.find('.oe_form_buttons_edit').show();
703 self.$buttons.find('.oe_form_buttons_view').hide();
704 self.$sidebar.hide();
708 autofocus: function() {
709 if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
710 var fields_order = this.fields_order.slice(0);
711 if (this.default_focus_field) {
712 fields_order.unshift(this.default_focus_field.name);
714 for (var i = 0; i < fields_order.length; i += 1) {
715 var field = this.fields[fields_order[i]];
716 if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
717 if (field.focus() !== false) {
724 on_button_save: function(e) {
726 $(e.target).attr("disabled", true);
727 return this.save().done(function(result) {
728 self.trigger("save", result);
729 self.reload().then(function() {
731 var parent = self.ViewManager.ActionManager.getParent();
733 parent.menu.do_reload_needaction();
736 }).always(function(){
737 $(e.target).attr("disabled", false);
740 on_button_cancel: function(event) {
742 if (this.can_be_discarded()) {
743 if (this.get('actual_mode') === 'create') {
744 this.trigger('history_back');
747 $.when.apply(null, this.render_value_defs).then(function(){
748 self.trigger('load_record', self.datarecord);
752 this.trigger('on_button_cancel');
755 on_button_new: function() {
758 return $.when(this.has_been_loaded).then(function() {
759 if (self.can_be_discarded()) {
760 return self.load_defaults();
764 on_button_edit: function() {
765 return this.to_edit_mode();
767 on_button_create: function() {
768 this.dataset.index = null;
771 on_button_duplicate: function() {
773 return this.has_been_loaded.then(function() {
774 return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
775 self.record_created(new_id);
780 on_button_delete: function() {
782 var def = $.Deferred();
783 this.has_been_loaded.done(function() {
784 if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
785 self.dataset.unlink([self.datarecord.id]).done(function() {
786 if (self.dataset.size()) {
787 self.execute_pager_action('next');
789 self.do_action('history_back');
794 $.async_when().done(function () {
799 return def.promise();
801 can_be_discarded: function() {
802 if (this.$el.is('.oe_form_dirty')) {
803 if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
806 this.$el.removeClass('oe_form_dirty');
811 * Triggers saving the form's record. Chooses between creating a new
812 * record or saving an existing one depending on whether the record
813 * already has an id property.
815 * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
816 * record, should that record be inserted at the start of the dataset (by
817 * default, records are added at the end)
819 save: function(prepend_on_create) {
821 var save_obj = {prepend_on_create: prepend_on_create, ret: null};
822 this.save_list.push(save_obj);
823 return this._process_operations().then(function() {
825 return $.Deferred().reject();
826 return $.when.apply($, save_obj.ret);
828 self.$el.removeClass('oe_form_dirty');
831 _process_save: function(save_obj) {
833 var prepend_on_create = save_obj.prepend_on_create;
835 var form_invalid = false,
837 first_invalid_field = null,
838 readonly_values = {};
839 for (var f in self.fields) {
840 if (!self.fields.hasOwnProperty(f)) { continue; }
844 if (!first_invalid_field) {
845 first_invalid_field = f;
847 } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
848 // Special case 'id' field, do not save this field
849 // on 'create' : save all non readonly fields
850 // on 'edit' : save non readonly modified fields
851 if (!f.get("readonly")) {
852 values[f.name] = f.get_value();
854 readonly_values[f.name] = f.get_value();
859 self.set({'display_invalid_fields': true});
860 first_invalid_field.focus();
862 return $.Deferred().reject();
864 self.set({'display_invalid_fields': false});
866 if (!self.datarecord.id) {
868 save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
869 return self.record_created(r, prepend_on_create);
871 } else if (_.isEmpty(values)) {
872 // Not dirty, noop save
873 save_deferral = $.Deferred().resolve({}).promise();
876 save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
877 return self.record_saved(r);
880 return save_deferral;
884 return $.Deferred().reject();
887 on_invalid: function() {
888 var warnings = _(this.fields).chain()
889 .filter(function (f) { return !f.is_valid(); })
891 return _.str.sprintf('<li>%s</li>',
894 warnings.unshift('<ul>');
895 warnings.push('</ul>');
896 this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
899 * Reload the form after saving
901 * @param {Object} r result of the write function.
903 record_saved: function(r) {
904 this.trigger('record_saved', r);
906 // should not happen in the server, but may happen for internal purpose
907 return $.Deferred().reject();
912 * Updates the form' dataset to contain the new record:
914 * * Adds the newly created record to the current dataset (at the end by
916 * * Selects that record (sets the dataset's index to point to the new
918 * * Updates the pager and sidebar displays
921 * @param {Boolean} [prepend_on_create=false] adds the newly created record
922 * at the beginning of the dataset instead of the end
924 record_created: function(r, prepend_on_create) {
927 // should not happen in the server, but may happen for internal purpose
928 this.trigger('record_created', r);
929 return $.Deferred().reject();
931 this.datarecord.id = r;
932 if (!prepend_on_create) {
933 this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
934 this.dataset.index = this.dataset.ids.length - 1;
936 this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
937 this.dataset.index = 0;
939 this.do_update_pager();
941 this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
943 //openerp.log("The record has been created with id #" + this.datarecord.id);
944 return $.when(this.reload()).then(function () {
945 self.trigger('record_created', r);
946 return _.extend(r, {created: true});
950 on_action: function (action) {
951 console.debug('Executing action', action);
955 return this.reload_mutex.exec(function() {
956 if (self.dataset.index == null) {
957 self.trigger("previous_view");
958 return $.Deferred().reject().promise();
960 if (self.dataset.index == null || self.dataset.index < 0) {
961 return $.when(self.on_button_new());
963 var fields = _.keys(self.fields_view.fields);
964 fields.push('display_name');
965 return self.dataset.read_index(fields,
969 'future_display_name': true
971 check_access_rule: true
972 }).then(function(r) {
973 self.trigger('load_record', r);
975 self.do_action('history_back');
980 get_widgets: function() {
981 return _.filter(this.getChildren(), function(obj) {
982 return obj instanceof instance.web.form.FormWidget;
985 get_fields_values: function() {
987 var ids = this.get_selected_ids();
988 values["id"] = ids.length > 0 ? ids[0] : false;
989 _.each(this.fields, function(value_, key) {
990 values[key] = value_.get_value();
994 get_selected_ids: function() {
995 var id = this.dataset.ids[this.dataset.index];
996 return id ? [id] : [];
998 recursive_save: function() {
1000 return $.when(this.save()).then(function(res) {
1001 if (self.dataset.parent_view)
1002 return self.dataset.parent_view.recursive_save();
1005 recursive_reload: function() {
1008 if (self.dataset.parent_view)
1009 pre = self.dataset.parent_view.recursive_reload();
1010 return pre.then(function() {
1011 return self.reload();
1014 is_dirty: function() {
1015 return _.any(this.fields, function (value_) {
1016 return value_._dirty_flag;
1019 is_interactible_record: function() {
1020 var id = this.datarecord.id;
1022 if (this.options.not_interactible_on_create)
1024 } else if (typeof(id) === "string") {
1025 if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
1030 sidebar_eval_context: function () {
1031 return $.when(this.build_eval_context());
1033 open_defaults_dialog: function () {
1035 var display = function (field, value) {
1036 if (field instanceof instance.web.form.FieldSelection) {
1037 return _(field.values).find(function (option) {
1038 return option[0] === value;
1040 } else if (field instanceof instance.web.form.FieldMany2One) {
1041 return field.get_displayed();
1045 var fields = _.chain(this.fields)
1046 .map(function (field) {
1047 var value = field.get_value();
1048 // ignore fields which are empty, invisible, readonly, o2m
1051 || field.get('invisible')
1052 || field.get("readonly")
1053 || field.field.type === 'one2many'
1054 || field.field.type === 'many2many'
1055 || field.field.type === 'binary'
1056 || field.password) {
1062 string: field.string,
1064 displayed: display(field, value),
1068 .sortBy(function (field) { return field.string; })
1070 var conditions = _.chain(self.fields)
1071 .filter(function (field) { return field.field.change_default; })
1072 .map(function (field) {
1073 var value = field.get_value();
1076 string: field.string,
1078 displayed: display(field, value),
1083 var d = new instance.web.Dialog(this, {
1084 title: _t("Set Default"),
1087 conditions: conditions
1090 {text: _t("Close"), click: function () { d.close(); }},
1091 {text: _t("Save default"), click: function () {
1092 var $defaults = d.$el.find('#formview_default_fields');
1093 var field_to_set = $defaults.val();
1094 if (!field_to_set) {
1095 $defaults.parent().addClass('oe_form_invalid');
1098 var condition = d.$el.find('#formview_default_conditions').val(),
1099 all_users = d.$el.find('#formview_default_all').is(':checked');
1100 new instance.web.DataSet(self, 'ir.values').call(
1104 self.fields[field_to_set].get_value(),
1108 ]).done(function () { d.close(); });
1112 d.template = 'FormView.set_default';
1115 register_field: function(field, name) {
1116 this.fields[name] = field;
1117 this.fields_order.push(name);
1118 if (JSON.parse(field.node.attrs.default_focus || "0")) {
1119 this.default_focus_field = field;
1122 field.on('focused', null, this.proxy('widgetFocused'))
1123 .on('blurred', null, this.proxy('widgetBlurred'));
1124 if (this.get_field_desc(name).translate) {
1125 this.translatable_fields.push(field);
1127 field.on('changed_value', this, function() {
1128 if (field.is_syntax_valid()) {
1129 this.trigger('field_changed:' + name);
1131 if (field._inhibit_on_change_flag) {
1134 field._dirty_flag = true;
1135 if (field.is_syntax_valid()) {
1136 this.do_onchange(field);
1137 this.on_form_changed(true);
1138 this.do_notify_change();
1142 get_field_desc: function(field_name) {
1143 return this.fields_view.fields[field_name];
1145 get_field_value: function(field_name) {
1146 return this.fields[field_name].get_value();
1148 compute_domain: function(expression) {
1149 return instance.web.form.compute_domain(expression, this.fields);
1151 _build_view_fields_values: function() {
1152 var a_dataset = this.dataset;
1153 var fields_values = this.get_fields_values();
1154 var active_id = a_dataset.ids[a_dataset.index];
1155 _.extend(fields_values, {
1156 active_id: active_id || false,
1157 active_ids: active_id ? [active_id] : [],
1158 active_model: a_dataset.model,
1161 if (a_dataset.parent_view) {
1162 fields_values.parent = a_dataset.parent_view.get_fields_values();
1164 return fields_values;
1166 build_eval_context: function() {
1167 var a_dataset = this.dataset;
1168 return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
1173 * Interface to be implemented by rendering engines for the form view.
1175 instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
1176 set_fields_view: function(fields_view) {},
1177 set_fields_registry: function(fields_registry) {},
1178 render_to: function($el) {},
1182 * Default rendering engine for the form view.
1184 * It is necessary to set the view using set_view() before usage.
1186 instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
1187 init: function(view) {
1190 set_fields_view: function(fvg) {
1192 this.version = parseFloat(this.fvg.arch.attrs.version);
1193 if (isNaN(this.version)) {
1197 set_tags_registry: function(tags_registry) {
1198 this.tags_registry = tags_registry;
1200 set_fields_registry: function(fields_registry) {
1201 this.fields_registry = fields_registry;
1203 set_widgets_registry: function(widgets_registry) {
1204 this.widgets_registry = widgets_registry;
1206 // Backward compatibility tools, current default version: v6.1
1207 process_version: function() {
1208 if (this.version < 7.0) {
1209 this.$form.find('form:first').wrapInner('<group col="4"/>');
1210 this.$form.find('page').each(function() {
1211 if (!$(this).parents('field').length) {
1212 $(this).wrapInner('<group col="4"/>');
1217 get_arch_fragment: function() {
1218 var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
1219 // IE won't allow custom button@type and will revert it to spec default : 'submit'
1220 $('button', doc).each(function() {
1221 $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
1223 // IE's html parser is also a css parser. How convenient...
1224 $('board', doc).each(function() {
1225 $(this).attr('layout', $(this).attr('style'));
1227 return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
1229 render_to: function($target) {
1231 this.$target = $target;
1233 this.$form = this.get_arch_fragment();
1235 this.process_version();
1237 this.fields_to_init = [];
1238 this.tags_to_init = [];
1239 this.widgets_to_init = [];
1241 this.process(this.$form);
1243 this.$form.appendTo(this.$target);
1245 this.to_replace = [];
1247 _.each(this.fields_to_init, function($elem) {
1248 var name = $elem.attr("name");
1249 if (!self.fvg.fields[name]) {
1250 throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
1252 var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
1254 throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
1256 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1257 var $label = self.labels[$elem.attr("name")];
1259 w.set_input_id($label.attr("for"));
1261 self.alter_field(w);
1262 self.view.register_field(w, $elem.attr("name"));
1263 self.to_replace.push([w, $elem]);
1265 _.each(this.tags_to_init, function($elem) {
1266 var tag_name = $elem[0].tagName.toLowerCase();
1267 var obj = self.tags_registry.get_object(tag_name);
1268 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1269 self.to_replace.push([w, $elem]);
1271 _.each(this.widgets_to_init, function($elem) {
1272 var widget_type = $elem.attr("type");
1273 var obj = self.widgets_registry.get_object(widget_type);
1274 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1275 self.to_replace.push([w, $elem]);
1278 init_fields: function() {
1280 _.each(this.to_replace, function(el) {
1281 defs.push(el[0].replace(el[1]));
1283 this.to_replace = [];
1284 return $.when.apply($, defs);
1286 render_element: function(template /* dictionaries */) {
1287 var dicts = [].slice.call(arguments).slice(1);
1288 var dict = _.extend.apply(_, dicts);
1289 dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
1290 return $(QWeb.render(template, dict));
1292 alter_field: function(field) {
1294 toggle_layout_debugging: function() {
1295 if (!this.$target.has('.oe_layout_debug_cell:first').length) {
1296 this.$target.find('[title]').removeAttr('title');
1297 this.$target.find('.oe_form_group_cell').each(function() {
1298 var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
1299 $(this).attr('title', text);
1302 this.$target.toggleClass('oe_layout_debugging');
1304 process: function($tag) {
1306 var tagname = $tag[0].nodeName.toLowerCase();
1307 if (this.tags_registry.contains(tagname)) {
1308 this.tags_to_init.push($tag);
1311 var fn = self['process_' + tagname];
1313 var args = [].slice.call(arguments);
1315 return fn.apply(self, args);
1317 // generic tag handling, just process children
1318 $tag.children().each(function() {
1319 self.process($(this));
1321 self.handle_common_properties($tag, $tag);
1322 $tag.removeAttr("modifiers");
1326 process_widget: function($widget) {
1327 this.widgets_to_init.push($widget);
1330 process_sheet: function($sheet) {
1331 var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
1332 this.handle_common_properties($new_sheet, $sheet);
1333 var $dst = $new_sheet.find('.oe_form_sheet');
1334 $sheet.contents().appendTo($dst);
1335 $sheet.before($new_sheet).remove();
1336 this.process($new_sheet);
1338 process_form: function($form) {
1339 if ($form.find('> sheet').length === 0) {
1340 $form.addClass('oe_form_nosheet');
1342 var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
1343 this.handle_common_properties($new_form, $form);
1344 $form.contents().appendTo($new_form);
1345 if ($form[0] === this.$form[0]) {
1346 // If root element, replace it
1347 this.$form = $new_form;
1349 $form.before($new_form).remove();
1351 this.process($new_form);
1354 * Used by direct <field> children of a <group> tag only
1355 * This method will add the implicit <label...> for every field
1358 preprocess_field: function($field) {
1360 var name = $field.attr('name'),
1361 field_colspan = parseInt($field.attr('colspan'), 10),
1362 field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
1364 if ($field.attr('nolabel') === '1')
1366 $field.attr('nolabel', '1');
1368 this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
1369 $(el).parents().each(function(unused, tag) {
1370 var name = tag.tagName.toLowerCase();
1371 if (name === "field" || name in self.tags_registry.map)
1378 var $label = $('<label/>').attr({
1380 "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
1381 "string": $field.attr('string'),
1382 "help": $field.attr('help'),
1383 "class": $field.attr('class'),
1385 $label.insertBefore($field);
1386 if (field_colspan > 1) {
1387 $field.attr('colspan', field_colspan - 1);
1391 process_field: function($field) {
1392 if ($field.parent().is('group')) {
1393 // No implicit labels for normal fields, only for <group> direct children
1394 var $label = this.preprocess_field($field);
1396 this.process($label);
1399 this.fields_to_init.push($field);
1402 process_group: function($group) {
1404 $group.children('field').each(function() {
1405 self.preprocess_field($(this));
1407 var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
1409 if ($new_group.first().is('table.oe_form_group')) {
1410 $table = $new_group;
1411 } else if ($new_group.filter('table.oe_form_group').length) {
1412 $table = $new_group.filter('table.oe_form_group').first();
1414 $table = $new_group.find('table.oe_form_group').first();
1418 cols = parseInt($group.attr('col') || 2, 10),
1422 $group.children().each(function(a,b,c) {
1423 var $child = $(this);
1424 var colspan = parseInt($child.attr('colspan') || 1, 10);
1425 var tagName = $child[0].tagName.toLowerCase();
1426 var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
1427 var newline = tagName === 'newline';
1429 // Note FME: those classes are used in layout debug mode
1430 if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
1431 $tr.addClass('oe_form_group_row_incomplete');
1433 $tr.addClass('oe_form_group_row_newline');
1440 if (!$tr || row_cols < colspan) {
1441 $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
1443 } else if (tagName==='group') {
1444 // When <group> <group/><group/> </group>, we need a spacing between the two groups
1445 $td.addClass('oe_group_right')
1447 row_cols -= colspan;
1449 // invisibility transfer
1450 var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
1451 var invisible = field_modifiers.invisible;
1452 self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
1454 $tr.append($td.append($child));
1455 children.push($child[0]);
1457 if (row_cols && $td) {
1458 $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
1460 $group.before($new_group).remove();
1462 $table.find('> tbody > tr').each(function() {
1463 var to_compute = [],
1466 $(this).children().each(function() {
1468 $child = $td.children(':first');
1469 if ($child.attr('cell-class')) {
1470 $td.addClass($child.attr('cell-class'));
1472 switch ($child[0].tagName.toLowerCase()) {
1476 if ($child.attr('for')) {
1477 $td.attr('width', '1%').addClass('oe_form_group_cell_label');
1478 row_cols-= $td.attr('colspan') || 1;
1483 var width = _.str.trim($child.attr('width') || ''),
1484 iwidth = parseInt(width, 10);
1486 if (width.substr(-1) === '%') {
1488 width = iwidth + '%';
1491 $td.css('min-width', width + 'px');
1493 $td.attr('width', width);
1494 $child.removeAttr('width');
1495 row_cols-= $td.attr('colspan') || 1;
1497 to_compute.push($td);
1503 var unit = Math.floor(total / row_cols);
1504 if (!$(this).is('.oe_form_group_row_incomplete')) {
1505 _.each(to_compute, function($td, i) {
1506 var width = parseInt($td.attr('colspan'), 10) * unit;
1507 $td.attr('width', width + '%');
1513 _.each(children, function(el) {
1514 self.process($(el));
1516 this.handle_common_properties($new_group, $group);
1519 process_notebook: function($notebook) {
1522 $notebook.find('> page').each(function() {
1523 var $page = $(this);
1524 var page_attrs = $page.getAttributes();
1525 page_attrs.id = _.uniqueId('notebook_page_');
1526 var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
1527 $page.contents().appendTo($new_page);
1528 $page.before($new_page).remove();
1529 var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
1530 page_attrs.__page = $new_page;
1531 page_attrs.__ic = ic;
1532 pages.push(page_attrs);
1534 $new_page.children().each(function() {
1535 self.process($(this));
1538 var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
1539 $notebook.contents().appendTo($new_notebook);
1540 $notebook.before($new_notebook).remove();
1541 self.process($($new_notebook.children()[0]));
1542 //tabs and invisibility handling
1543 $new_notebook.tabs();
1544 _.each(pages, function(page, i) {
1547 page.__ic.on("change:effective_invisible", null, function() {
1548 if (!page.__ic.get('effective_invisible') && page.autofocus) {
1549 $new_notebook.tabs('select', i);
1552 var current = $new_notebook.tabs("option", "selected");
1553 if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
1555 var first_visible = _.find(_.range(pages.length), function(i2) {
1556 return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
1558 if (first_visible !== undefined) {
1559 $new_notebook.tabs('select', first_visible);
1564 this.handle_common_properties($new_notebook, $notebook);
1565 return $new_notebook;
1567 process_separator: function($separator) {
1568 var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
1569 $separator.before($new_separator).remove();
1570 this.handle_common_properties($new_separator, $separator);
1571 return $new_separator;
1573 process_label: function($label) {
1574 var name = $label.attr("for"),
1575 field_orm = this.fvg.fields[name];
1577 string: $label.attr('string') || (field_orm || {}).string || '',
1578 help: $label.attr('help') || (field_orm || {}).help || '',
1579 _for: name ? _.uniqueId('oe-field-input-') : undefined,
1581 var align = parseFloat(dict.align);
1582 if (isNaN(align) || align === 1) {
1584 } else if (align === 0) {
1590 var $new_label = this.render_element('FormRenderingLabel', dict);
1591 $label.before($new_label).remove();
1592 this.handle_common_properties($new_label, $label);
1594 this.labels[name] = $new_label;
1598 handle_common_properties: function($new_element, $node) {
1599 var str_modifiers = $node.attr("modifiers") || "{}";
1600 var modifiers = JSON.parse(str_modifiers);
1602 if (modifiers.invisible !== undefined)
1603 ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
1604 $new_element.addClass($node.attr("class") || "");
1605 $new_element.attr('style', $node.attr('style'));
1606 return {invisibility_changer: ic,};
1613 If you read this documentation, it probably means that you were asked to use a form view widget outside of
1614 a form view. Before going further, you must understand that those fields were never really created for
1615 that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
1616 you to hack the system with more style.
1618 instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
1619 init: function(parent, eval_context) {
1620 this._super(parent);
1621 this.field_descs = {};
1622 this.eval_context = eval_context || {};
1624 display_invalid_fields: false,
1625 actual_mode: 'create',
1628 get_field_desc: function(field_name) {
1629 if (this.field_descs[field_name] === undefined) {
1630 this.field_descs[field_name] = {
1634 return this.field_descs[field_name];
1636 extend_field_desc: function(fields) {
1638 _.each(fields, function(v, k) {
1639 _.extend(self.get_field_desc(k), v);
1642 get_field_value: function(field_name) {
1645 set_values: function(values) {
1648 compute_domain: function(expression) {
1649 return instance.web.form.compute_domain(expression, {});
1651 build_eval_context: function() {
1652 return new instance.web.CompoundContext(this.eval_context);
1656 instance.web.form.compute_domain = function(expr, fields) {
1657 if (! (expr instanceof Array))
1660 for (var i = expr.length - 1; i >= 0; i--) {
1662 if (ex.length == 1) {
1663 var top = stack.pop();
1666 stack.push(stack.pop() || top);
1669 stack.push(stack.pop() && top);
1675 throw new Error(_.str.sprintf(
1676 _t("Unknown operator %s in domain %s"),
1677 ex, JSON.stringify(expr)));
1681 var field = fields[ex[0]];
1683 throw new Error(_.str.sprintf(
1684 _t("Unknown field %s in domain %s"),
1685 ex[0], JSON.stringify(expr)));
1687 var field_value = field.get_value ? field.get_value() : field.value;
1691 switch (op.toLowerCase()) {
1694 stack.push(_.isEqual(field_value, val));
1698 stack.push(!_.isEqual(field_value, val));
1701 stack.push(field_value < val);
1704 stack.push(field_value > val);
1707 stack.push(field_value <= val);
1710 stack.push(field_value >= val);
1713 if (!_.isArray(val)) val = [val];
1714 stack.push(_(val).contains(field_value));
1717 if (!_.isArray(val)) val = [val];
1718 stack.push(!_(val).contains(field_value));
1722 _t("Unsupported operator %s in domain %s"),
1723 op, JSON.stringify(expr));
1726 return _.all(stack, _.identity);
1729 instance.web.form.is_bin_size = function(v) {
1730 return /^\d+(\.\d*)? \w+$/.test(v);
1734 * Must be applied over an class already possessing the PropertiesMixin.
1736 * Apply the result of the "invisible" domain to this.$el.
1738 instance.web.form.InvisibilityChangerMixin = {
1739 init: function(field_manager, invisible_domain) {
1741 this._ic_field_manager = field_manager;
1742 this._ic_invisible_modifier = invisible_domain;
1743 this._ic_field_manager.on("view_content_has_changed", this, function() {
1744 var result = self._ic_invisible_modifier === undefined ? false :
1745 self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
1746 self.set({"invisible": result});
1748 this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
1749 var check = function() {
1750 if (self.get("invisible") || self.get('force_invisible')) {
1751 self.set({"effective_invisible": true});
1753 self.set({"effective_invisible": false});
1756 this.on('change:invisible', this, check);
1757 this.on('change:force_invisible', this, check);
1761 this.on("change:effective_invisible", this, this._check_visibility);
1762 this._check_visibility();
1764 _check_visibility: function() {
1765 this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
1769 instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
1770 init: function(parent, field_manager, invisible_domain, $el) {
1771 this.setParent(parent);
1772 instance.web.PropertiesMixin.init.call(this);
1773 instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
1780 Base class for all fields, custom widgets and buttons to be displayed in the form view.
1783 - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
1784 the values of the "readonly" property and the "mode" property on the field manager.
1786 instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
1788 * @constructs instance.web.form.FormWidget
1789 * @extends instance.web.Widget
1791 * @param field_manager
1794 init: function(field_manager, node) {
1795 this._super(field_manager);
1796 this.field_manager = field_manager;
1797 if (this.field_manager instanceof instance.web.FormView)
1798 this.view = this.field_manager;
1800 this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
1801 instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
1803 this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
1809 // some events to make the property "effective_readonly" sync automatically with "readonly" and
1810 // "mode" on field_manager
1812 var test_effective_readonly = function() {
1813 self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
1815 this.on("change:readonly", this, test_effective_readonly);
1816 this.field_manager.on("change:actual_mode", this, test_effective_readonly);
1817 test_effective_readonly.call(this);
1819 renderElement: function() {
1820 this.process_modifiers();
1822 this.$el.addClass(this.node.attrs["class"] || "");
1824 destroy: function() {
1826 this._super.apply(this, arguments);
1829 * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
1831 * This method is an utility method that is meant to be called by child classes.
1833 * @param {jQuery} $e jQuery object of elements to bind focus/blur on
1835 setupFocus: function ($e) {
1838 focus: function () { self.trigger('focused'); },
1839 blur: function () { self.trigger('blurred'); }
1842 process_modifiers: function() {
1844 for (var a in this.modifiers) {
1845 if (!this.modifiers.hasOwnProperty(a)) { continue; }
1846 if (!_.include(["invisible"], a)) {
1847 var val = this.field_manager.compute_domain(this.modifiers[a]);
1853 do_attach_tooltip: function(widget, trigger, options) {
1854 widget = widget || this;
1855 trigger = trigger || this.$el;
1856 options = _.extend({
1861 var template = widget.template + '.tooltip';
1862 if (!QWeb.has_template(template)) {
1863 template = 'WidgetLabel.tooltip';
1865 return QWeb.render(template, {
1866 debug: instance.session.debug,
1869 gravity: $.fn.tipsy.autoBounds(50, 'nw'),
1874 $(trigger).tipsy(options);
1877 * Builds a new context usable for operations related to fields by merging
1878 * the fields'context with the action's context.
1880 build_context: function() {
1881 // only use the model's context if there is not context on the node
1882 var v_context = this.node.attrs.context;
1884 v_context = (this.field || {}).context || {};
1887 if (v_context.__ref || true) { //TODO: remove true
1888 var fields_values = this.field_manager.build_eval_context();
1889 v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
1893 build_domain: function() {
1894 var f_domain = this.field.domain || [];
1895 var n_domain = this.node.attrs.domain || null;
1896 // if there is a domain on the node, overrides the model's domain
1897 var final_domain = n_domain !== null ? n_domain : f_domain;
1898 if (!(final_domain instanceof Array) || true) { //TODO: remove true
1899 var fields_values = this.field_manager.build_eval_context();
1900 final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
1902 return final_domain;
1906 instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
1907 template: 'WidgetButton',
1908 init: function(field_manager, node) {
1909 node.attrs.type = node.attrs['data-button-type'];
1910 this._super(field_manager, node);
1911 this.force_disabled = false;
1912 this.string = (this.node.attrs.string || '').replace(/_/g, '');
1913 if (JSON.parse(this.node.attrs.default_focus || "0")) {
1914 // TODO fme: provide enter key binding to widgets
1915 this.view.default_focus_button = this;
1917 if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
1918 this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
1922 this._super.apply(this, arguments);
1923 this.view.on('view_content_has_changed', this, this.check_disable);
1924 this.check_disable();
1925 this.$el.click(this.on_click);
1926 if (this.node.attrs.help || instance.session.debug) {
1927 this.do_attach_tooltip();
1929 this.setupFocus(this.$el);
1931 on_click: function() {
1933 this.force_disabled = true;
1934 this.check_disable();
1935 this.execute_action().always(function() {
1936 self.force_disabled = false;
1937 self.check_disable();
1940 execute_action: function() {
1942 var exec_action = function() {
1943 if (self.node.attrs.confirm) {
1944 var def = $.Deferred();
1945 var dialog = instance.web.dialog($('<div/>').text(self.node.attrs.confirm), {
1946 title: _t('Confirm'),
1949 {text: _t("Cancel"), click: function() {
1950 $(this).dialog("close");
1953 {text: _t("Ok"), click: function() {
1955 self.on_confirmed().always(function() {
1956 $(self2).dialog("close");
1961 beforeClose: function() {
1965 return def.promise();
1967 return self.on_confirmed();
1970 if (!this.node.attrs.special) {
1971 return this.view.recursive_save().then(exec_action);
1973 return exec_action();
1976 on_confirmed: function() {
1979 var context = this.build_context();
1981 return this.view.do_execute_action(
1982 _.extend({}, this.node.attrs, {context: context}),
1983 this.view.dataset, this.view.datarecord.id, function (reason) {
1984 if (!_.isObject(reason)) {
1985 self.view.recursive_reload();
1989 check_disable: function() {
1990 var disabled = (this.force_disabled || !this.view.is_interactible_record());
1991 this.$el.prop('disabled', disabled);
1992 this.$el.css('color', disabled ? 'grey' : '');
1997 * Interface to be implemented by fields.
2000 * - changed_value: triggered when the value of the field has changed. This can be due
2001 * to a user interaction or a call to set_value().
2004 instance.web.form.FieldInterface = {
2006 * Constructor takes 2 arguments:
2007 * - field_manager: Implements FieldManagerMixin
2008 * - node: the "<field>" node in json form
2010 init: function(field_manager, node) {},
2012 * Called by the form view to indicate the value of the field.
2014 * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
2015 * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
2016 * before the widget is inserted into the DOM.
2018 * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
2019 * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
2020 * also be able to handle any other format commonly used in the _defaults key on the models in the addons
2021 * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
2022 * no information is ever given to know which format is used.
2024 set_value: function(value_) {},
2026 * Get the current value of the widget.
2028 * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
2029 * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
2030 * For example if the field is marked as "required", a call to get_value() can return false.
2032 * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
2033 * return a default value according to the type of field.
2035 * This method is always assumed to perform synchronously, it can not return a promise.
2037 * If there was no user interaction to modify the value of the field, it is always assumed that
2038 * get_value() return the same semantic value than the one passed in the last call to set_value(),
2039 * although the syntax can be different. This can be the case for type of fields that have a different
2040 * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
2042 get_value: function() {},
2044 * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
2047 set_input_id: function(id) {},
2049 * Returns true if is_syntax_valid() returns true and the value is semantically
2050 * valid too according to the semantic restrictions applied to the field.
2052 is_valid: function() {},
2054 * Returns true if the field holds a value which is syntactically correct, ignoring
2055 * the potential semantic restrictions applied to the field.
2057 is_syntax_valid: function() {},
2059 * Must set the focus on the field. Return false if field is not focusable.
2061 focus: function() {},
2063 * Called when the translate button is clicked.
2065 on_translate: function() {},
2067 This method is called by the form view before reading on_change values and before saving. It tells
2068 the field to save its value before reading it using get_value(). Must return a promise.
2070 commit_value: function() {},
2074 * Abstract class for classes implementing FieldInterface.
2077 * - value: useful property to hold the value of the field. By default, set_value() and get_value()
2078 * set and retrieve the value property. Changing the value property also triggers automatically
2079 * a 'changed_value' event that inform the view to trigger on_changes.
2082 instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
2084 * @constructs instance.web.form.AbstractField
2085 * @extends instance.web.form.FormWidget
2087 * @param field_manager
2090 init: function(field_manager, node) {
2092 this._super(field_manager, node);
2093 this.name = this.node.attrs.name;
2094 this.field = this.field_manager.get_field_desc(this.name);
2095 this.widget = this.node.attrs.widget;
2096 this.string = this.node.attrs.string || this.field.string || this.name;
2097 this.options = instance.web.py_eval(this.node.attrs.options || '{}');
2098 this.set({'value': false});
2100 this.on("change:value", this, function() {
2101 this.trigger('changed_value');
2102 this._check_css_flags();
2105 renderElement: function() {
2108 if (this.field.translate && this.view) {
2109 this.$el.addClass('oe_form_field_translatable');
2110 this.$el.find('.oe_field_translate').click(this.on_translate);
2112 this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
2113 if (instance.session.debug) {
2114 this.do_attach_tooltip(this, this.$label[0] || this.$el);
2115 this.$label.off('dblclick').on('dblclick', function() {
2116 console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
2118 console.log("window.w =", window.w);
2121 if (!this.disable_utility_classes) {
2122 this.off("change:required", this, this._set_required);
2123 this.on("change:required", this, this._set_required);
2124 this._set_required();
2126 this._check_visibility();
2127 this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
2128 this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
2129 this._check_css_flags();
2132 var tmp = this._super();
2133 this.on("change:value", this, function() {
2134 if (! this.no_rerender)
2135 this.render_value();
2137 this.render_value();
2140 * Private. Do not use.
2142 _set_required: function() {
2143 this.$el.toggleClass('oe_form_required', this.get("required"));
2145 set_value: function(value_) {
2146 this.set({'value': value_});
2148 get_value: function() {
2149 return this.get('value');
2152 Utility method that all implementations should use to change the
2153 value without triggering a re-rendering.
2155 internal_set_value: function(value_) {
2156 var tmp = this.no_rerender;
2157 this.no_rerender = true;
2158 this.set({'value': value_});
2159 this.no_rerender = tmp;
2162 This method is called each time the value is modified.
2164 render_value: function() {},
2165 is_valid: function() {
2166 return this.is_syntax_valid() && !(this.get('required') && this.is_false());
2168 is_syntax_valid: function() {
2172 * Method useful to implement to ease validity testing. Must return true if the current
2173 * value is similar to false in OpenERP.
2175 is_false: function() {
2176 return this.get('value') === false;
2178 _check_css_flags: function() {
2179 if (this.field.translate) {
2180 this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
2182 if (!this.disable_utility_classes) {
2183 if (this.field_manager.get('display_invalid_fields')) {
2184 this.$el.toggleClass('oe_form_invalid', !this.is_valid());
2191 set_input_id: function(id) {
2192 this.id_for_label = id;
2194 on_translate: function() {
2196 var trans = new instance.web.DataSet(this, 'ir.translation');
2197 return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
2202 set_dimensions: function (height, width) {
2208 commit_value: function() {
2214 * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
2217 instance.web.form.ReinitializeWidgetMixin = {
2219 * Default implementation of, you should not override it, use initialize_field() instead.
2222 this.initialize_field();
2225 initialize_field: function() {
2226 this.on("change:effective_readonly", this, this.reinitialize);
2227 this.initialize_content();
2229 reinitialize: function() {
2230 this.destroy_content();
2231 this.renderElement();
2232 this.initialize_content();
2235 * Called to destroy anything that could have been created previously, called before a
2236 * re-initialization.
2238 destroy_content: function() {},
2240 * Called to initialize the content.
2242 initialize_content: function() {},
2246 * A mixin to apply on any field that has to completely re-render when its readonly state
2249 instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
2250 reinitialize: function() {
2251 instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
2252 var res = this.render_value();
2253 if (this.view && this.view.render_value_defs){
2254 this.view.render_value_defs.push(res);
2260 Some hack to make placeholders work in ie9.
2262 if ($.browser.msie && $.browser.version === "9.0") {
2263 document.addEventListener("DOMNodeInserted",function(event){
2264 var nodename = event.target.nodeName.toLowerCase();
2265 if ( nodename === "input" || nodename == "textarea" ) {
2266 $(event.target).placeholder();
2271 instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2272 template: 'FieldChar',
2273 widget_class: 'oe_form_field_char',
2275 'change input': 'store_dom_value',
2277 init: function (field_manager, node) {
2278 this._super(field_manager, node);
2279 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2281 initialize_content: function() {
2282 this.setupFocus(this.$('input'));
2284 store_dom_value: function () {
2285 if (!this.get('effective_readonly')
2286 && this.$('input').length
2287 && this.is_syntax_valid()) {
2288 this.internal_set_value(
2290 this.$('input').val()));
2293 commit_value: function () {
2294 this.store_dom_value();
2295 return this._super();
2297 render_value: function() {
2298 var show_value = this.format_value(this.get('value'), '');
2299 if (!this.get("effective_readonly")) {
2300 this.$el.find('input').val(show_value);
2302 if (this.password) {
2303 show_value = new Array(show_value.length + 1).join('*');
2305 this.$(".oe_form_char_content").text(show_value);
2308 is_syntax_valid: function() {
2309 if (!this.get("effective_readonly") && this.$("input").size() > 0) {
2311 this.parse_value(this.$('input').val(), '');
2319 parse_value: function(val, def) {
2320 return instance.web.parse_value(val, this, def);
2322 format_value: function(val, def) {
2323 return instance.web.format_value(val, this, def);
2325 is_false: function() {
2326 return this.get('value') === '' || this._super();
2329 var input = this.$('input:first')[0];
2330 return input ? input.focus() : false;
2332 set_dimensions: function (height, width) {
2333 this._super(height, width);
2334 this.$('input').css({
2341 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2342 process_modifiers: function () {
2344 this.set({ readonly: true });
2348 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2349 template: 'FieldEmail',
2350 initialize_content: function() {
2352 var $button = this.$el.find('button');
2353 $button.click(this.on_button_clicked);
2354 this.setupFocus($button);
2356 render_value: function() {
2357 if (!this.get("effective_readonly")) {
2361 .attr('href', 'mailto:' + this.get('value'))
2362 .text(this.get('value') || '');
2365 on_button_clicked: function() {
2366 if (!this.get('value') || !this.is_syntax_valid()) {
2367 this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2369 location.href = 'mailto:' + this.get('value');
2374 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2375 template: 'FieldUrl',
2376 initialize_content: function() {
2378 var $button = this.$el.find('button');
2379 $button.click(this.on_button_clicked);
2380 this.setupFocus($button);
2382 render_value: function() {
2383 if (!this.get("effective_readonly")) {
2386 var tmp = this.get('value');
2387 var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2389 tmp = "http://" + this.get('value');
2391 var text = this.get('value') ? this.node.attrs.text || tmp : '';
2392 this.$el.find('a').attr('href', tmp).text(text);
2395 on_button_clicked: function() {
2396 if (!this.get('value')) {
2397 this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2399 var url = $.trim(this.get('value'));
2400 if(/^www\./i.test(url))
2401 url = 'http://'+url;
2407 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2408 is_field_number: true,
2409 widget_class: 'oe_form_field_float',
2410 init: function (field_manager, node) {
2411 this._super(field_manager, node);
2412 this.internal_set_value(0);
2413 if (this.node.attrs.digits) {
2414 this.digits = this.node.attrs.digits;
2416 this.digits = this.field.digits;
2419 set_value: function(value_) {
2420 if (value_ === false || value_ === undefined) {
2421 // As in GTK client, floats default to 0
2424 this._super.apply(this, [value_]);
2426 focus: function () {
2427 var $input = this.$('input:first');
2428 return $input.length ? $input.select() : false;
2432 instance.web.DateTimeWidget = instance.web.Widget.extend({
2433 template: "web.datepicker",
2434 jqueryui_object: 'datetimepicker',
2435 type_of_date: "datetime",
2437 'change .oe_datepicker_master': 'change_datetime',
2438 'keypress .oe_datepicker_master': 'change_datetime',
2440 init: function(parent) {
2441 this._super(parent);
2442 this.name = parent.name;
2446 this.$input = this.$el.find('input.oe_datepicker_master');
2447 this.$input_picker = this.$el.find('input.oe_datepicker_container');
2449 $.datepicker.setDefaults({
2450 clearText: _t('Clear'),
2451 clearStatus: _t('Erase the current date'),
2452 closeText: _t('Done'),
2453 closeStatus: _t('Close without change'),
2454 prevText: _t('<Prev'),
2455 prevStatus: _t('Show the previous month'),
2456 nextText: _t('Next>'),
2457 nextStatus: _t('Show the next month'),
2458 currentText: _t('Today'),
2459 currentStatus: _t('Show the current month'),
2460 monthNames: Date.CultureInfo.monthNames,
2461 monthNamesShort: Date.CultureInfo.abbreviatedMonthNames,
2462 monthStatus: _t('Show a different month'),
2463 yearStatus: _t('Show a different year'),
2464 weekHeader: _t('Wk'),
2465 weekStatus: _t('Week of the year'),
2466 dayNames: Date.CultureInfo.dayNames,
2467 dayNamesShort: Date.CultureInfo.abbreviatedDayNames,
2468 dayNamesMin: Date.CultureInfo.shortestDayNames,
2469 dayStatus: _t('Set DD as first week day'),
2470 dateStatus: _t('Select D, M d'),
2471 firstDay: Date.CultureInfo.firstDayOfWeek,
2472 initStatus: _t('Select a date'),
2475 $.timepicker.setDefaults({
2476 timeOnlyTitle: _t('Choose Time'),
2477 timeText: _t('Time'),
2478 hourText: _t('Hour'),
2479 minuteText: _t('Minute'),
2480 secondText: _t('Second'),
2481 currentText: _t('Now'),
2482 closeText: _t('Done')
2486 onClose: this.on_picker_select,
2487 onSelect: this.on_picker_select,
2491 showButtonPanel: true,
2492 firstDay: Date.CultureInfo.firstDayOfWeek
2494 // Some clicks in the datepicker dialog are not stopped by the
2495 // datepicker and "bubble through", unexpectedly triggering the bus's
2496 // click event. Prevent that.
2497 this.picker('widget').click(function (e) { e.stopPropagation(); });
2499 this.$el.find('img.oe_datepicker_trigger').click(function() {
2500 if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
2501 self.$input.focus();
2504 self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date());
2505 self.$input_picker.show();
2506 self.picker('show');
2507 self.$input_picker.hide();
2509 this.set_readonly(false);
2510 this.set({'value': false});
2512 picker: function() {
2513 return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
2515 on_picker_select: function(text, instance_) {
2516 var date = this.picker('getDate');
2518 .val(date ? this.format_client(date) : '')
2522 set_value: function(value_) {
2523 this.set({'value': value_});
2524 this.$input.val(value_ ? this.format_client(value_) : '');
2526 get_value: function() {
2527 return this.get('value');
2529 set_value_from_ui_: function() {
2530 var value_ = this.$input.val() || false;
2531 this.set({'value': this.parse_client(value_)});
2533 set_readonly: function(readonly) {
2534 this.readonly = readonly;
2535 this.$input.prop('readonly', this.readonly);
2536 this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
2538 is_valid_: function() {
2539 var value_ = this.$input.val();
2540 if (value_ === "") {
2544 this.parse_client(value_);
2551 parse_client: function(v) {
2552 return instance.web.parse_value(v, {"widget": this.type_of_date});
2554 format_client: function(v) {
2555 return instance.web.format_value(v, {"widget": this.type_of_date});
2557 change_datetime: function(e) {
2558 if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
2559 this.set_value_from_ui_();
2560 this.trigger("datetime_changed");
2563 commit_value: function () {
2564 this.change_datetime();
2568 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2569 jqueryui_object: 'datepicker',
2570 type_of_date: "date"
2573 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2574 template: "FieldDatetime",
2575 build_widget: function() {
2576 return new instance.web.DateTimeWidget(this);
2578 destroy_content: function() {
2579 if (this.datewidget) {
2580 this.datewidget.destroy();
2581 this.datewidget = undefined;
2584 initialize_content: function() {
2585 if (!this.get("effective_readonly")) {
2586 this.datewidget = this.build_widget();
2587 this.datewidget.on('datetime_changed', this, _.bind(function() {
2588 this.internal_set_value(this.datewidget.get_value());
2590 this.datewidget.appendTo(this.$el);
2591 this.setupFocus(this.datewidget.$input);
2594 render_value: function() {
2595 if (!this.get("effective_readonly")) {
2596 this.datewidget.set_value(this.get('value'));
2598 this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2601 is_syntax_valid: function() {
2602 if (!this.get("effective_readonly") && this.datewidget) {
2603 return this.datewidget.is_valid_();
2607 is_false: function() {
2608 return this.get('value') === '' || this._super();
2611 var input = this.datewidget && this.datewidget.$input[0];
2612 return input ? input.focus() : false;
2614 set_dimensions: function (height, width) {
2615 this._super(height, width);
2616 if (!this.get("effective_readonly")) {
2617 this.datewidget.$input.css('height', height);
2622 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2623 template: "FieldDate",
2624 build_widget: function() {
2625 return new instance.web.DateWidget(this);
2629 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2630 template: 'FieldText',
2632 'keyup': function (e) {
2633 if (e.which === $.ui.keyCode.ENTER) {
2634 e.stopPropagation();
2637 'keypress': function (e) {
2638 if (e.which === $.ui.keyCode.ENTER) {
2639 e.stopPropagation();
2642 'change textarea': 'store_dom_value',
2644 initialize_content: function() {
2646 if (! this.get("effective_readonly")) {
2647 this.$textarea = this.$el.find('textarea');
2648 this.auto_sized = false;
2649 this.default_height = this.$textarea.css('height');
2650 if (this.get("effective_readonly")) {
2651 this.$textarea.attr('disabled', 'disabled');
2653 this.setupFocus(this.$textarea);
2655 this.$textarea = undefined;
2658 commit_value: function () {
2659 if (! this.get("effective_readonly") && this.$textarea) {
2660 this.store_dom_value();
2662 return this._super();
2664 store_dom_value: function () {
2665 this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2667 render_value: function() {
2668 if (! this.get("effective_readonly")) {
2669 var show_value = instance.web.format_value(this.get('value'), this, '');
2670 if (show_value === '') {
2671 this.$textarea.css('height', parseInt(this.default_height)+"px");
2673 this.$textarea.val(show_value);
2674 if (! this.auto_sized) {
2675 this.auto_sized = true;
2676 this.$textarea.autosize();
2678 this.$textarea.trigger("autosize");
2681 var txt = this.get("value") || '';
2682 this.$(".oe_form_text_content").text(txt);
2685 is_syntax_valid: function() {
2686 if (!this.get("effective_readonly") && this.$textarea) {
2688 instance.web.parse_value(this.$textarea.val(), this, '');
2696 is_false: function() {
2697 return this.get('value') === '' || this._super();
2699 focus: function($el) {
2700 var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2701 return input ? input.focus() : false;
2703 set_dimensions: function (height, width) {
2704 this._super(height, width);
2705 if (!this.get("effective_readonly") && this.$textarea) {
2706 this.$textarea.css({
2715 * FieldTextHtml Widget
2716 * Intended for FieldText widgets meant to display HTML content. This
2717 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2718 * To find more information about CLEditor configutation: go to
2719 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2721 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2722 template: 'FieldTextHtml',
2724 this._super.apply(this, arguments);
2726 initialize_content: function() {
2728 if (! this.get("effective_readonly")) {
2729 self._updating_editor = false;
2730 this.$textarea = this.$el.find('textarea');
2731 var width = ((this.node.attrs || {}).editor_width || 'calc(100% - 4px)');
2732 var height = ((this.node.attrs || {}).editor_height || 250);
2733 this.$textarea.cleditor({
2734 width: width, // width not including margins, borders or padding
2735 height: height, // height not including margins, borders or padding
2736 controls: // controls to add to the toolbar
2737 "bold italic underline strikethrough " +
2738 "| removeformat | bullets numbering | outdent " +
2739 "indent | link unlink | source",
2740 bodyStyle: // style to assign to document body contained within the editor
2741 "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2743 this.$cleditor = this.$textarea.cleditor()[0];
2744 this.$cleditor.change(function() {
2745 if (! self._updating_editor) {
2746 self.$cleditor.updateTextArea();
2747 self.internal_set_value(self.$textarea.val());
2750 if (this.field.translate) {
2751 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"/>')
2752 .click(this.on_translate);
2753 this.$cleditor.$toolbar.append($img);
2757 render_value: function() {
2758 if (! this.get("effective_readonly")) {
2759 this.$textarea.val(this.get('value') || '');
2760 this._updating_editor = true;
2761 this.$cleditor.updateFrame();
2762 this._updating_editor = false;
2764 this.$el.html(this.get('value'));
2769 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2770 template: 'FieldBoolean',
2773 this.$checkbox = $("input", this.$el);
2774 this.setupFocus(this.$checkbox);
2775 this.$el.click(_.bind(function() {
2776 this.internal_set_value(this.$checkbox.is(':checked'));
2778 var check_readonly = function() {
2779 self.$checkbox.prop('disabled', self.get("effective_readonly"));
2781 this.on("change:effective_readonly", this, check_readonly);
2782 check_readonly.call(this);
2783 this._super.apply(this, arguments);
2785 render_value: function() {
2786 this.$checkbox[0].checked = this.get('value');
2789 var input = this.$checkbox && this.$checkbox[0];
2790 return input ? input.focus() : false;
2795 The progressbar field expect a float from 0 to 100.
2797 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2798 template: 'FieldProgressBar',
2799 render_value: function() {
2800 this.$el.progressbar({
2801 value: this.get('value') || 0,
2802 disabled: this.get("effective_readonly")
2804 var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
2805 this.$('span').html(formatted_value + '%');
2810 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2811 template: 'FieldSelection',
2813 'change select': 'store_dom_value',
2815 init: function(field_manager, node) {
2817 this._super(field_manager, node);
2818 this.values = _(this.field.selection).chain()
2819 .reject(function (v) { return v[0] === false && v[1] === ''; })
2820 .unshift([false, ''])
2823 initialize_content: function() {
2824 // Flag indicating whether we're in an event chain containing a change
2825 // event on the select, in order to know what to do on keyup[RETURN]:
2826 // * If the user presses [RETURN] as part of changing the value of a
2827 // selection, we should just let the value change and not let the
2828 // event broadcast further (e.g. to validating the current state of
2829 // the form in editable list view, which would lead to saving the
2830 // current row or switching to the next one)
2831 // * If the user presses [RETURN] with a select closed (side-effect:
2832 // also if the user opened the select and pressed [RETURN] without
2833 // changing the selected value), takes the action as validating the
2835 var ischanging = false;
2836 var $select = this.$el.find('select')
2837 .change(function () { ischanging = true; })
2838 .click(function () { ischanging = false; })
2839 .keyup(function (e) {
2840 if (e.which !== 13 || !ischanging) { return; }
2841 e.stopPropagation();
2844 this.setupFocus($select);
2846 commit_value: function () {
2847 this.store_dom_value();
2848 return this._super();
2850 store_dom_value: function () {
2851 if (!this.get('effective_readonly') && this.$('select').length) {
2852 this.internal_set_value(
2853 this.values[this.$('select')[0].selectedIndex][0]);
2856 set_value: function(value_) {
2857 value_ = value_ === null ? false : value_;
2858 value_ = value_ instanceof Array ? value_[0] : value_;
2859 this._super(value_);
2861 render_value: function() {
2862 if (!this.get("effective_readonly")) {
2864 for (var i = 0, ii = this.values.length; i < ii; i++) {
2865 if (this.values[i][0] === this.get('value')) index = i;
2867 this.$el.find('select')[0].selectedIndex = index;
2870 var option = _(this.values)
2871 .detect(function (record) { return record[0] === self.get('value'); });
2872 this.$el.text(option ? option[1] : this.values[0][1]);
2876 var input = this.$('select:first')[0];
2877 return input ? input.focus() : false;
2879 set_dimensions: function (height, width) {
2880 this._super(height, width);
2881 this.$('select').css({
2888 // jquery autocomplete tweak to allow html and classnames
2890 var proto = $.ui.autocomplete.prototype,
2891 initSource = proto._initSource;
2893 function filter( array, term ) {
2894 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
2895 return $.grep( array, function(value_) {
2896 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
2901 _initSource: function() {
2902 if ( this.options.html && $.isArray(this.options.source) ) {
2903 this.source = function( request, response ) {
2904 response( filter( this.options.source, request.term ) );
2907 initSource.call( this );
2911 _renderItem: function( ul, item) {
2912 return $( "<li></li>" )
2913 .data( "item.autocomplete", item )
2914 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
2916 .addClass(item.classname);
2922 * A mixin containing some useful methods to handle completion inputs.
2924 instance.web.form.CompletionFieldMixin = {
2927 this.orderer = new instance.web.DropMisordered();
2930 * Call this method to search using a string.
2932 get_search_result: function(search_val) {
2935 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
2936 var blacklist = this.get_search_blacklist();
2937 this.last_query = search_val;
2939 return this.orderer.add(dataset.name_search(
2940 search_val, new instance.web.CompoundDomain(self.build_domain(), [["id", "not in", blacklist]]),
2941 'ilike', this.limit + 1, self.build_context())).then(function(data) {
2942 self.last_search = data;
2943 // possible selections for the m2o
2944 var values = _.map(data, function(x) {
2945 x[1] = x[1].split("\n")[0];
2947 label: _.str.escapeHTML(x[1]),
2954 // search more... if more results that max
2955 if (values.length > self.limit) {
2956 values = values.slice(0, self.limit);
2958 label: _t("Search More..."),
2959 action: function() {
2960 dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
2961 self._search_create_popup("search", data);
2964 classname: 'oe_m2o_dropdown_option'
2968 var raw_result = _(data.result).map(function(x) {return x[1];});
2969 if (search_val.length > 0 && !_.include(raw_result, search_val)) {
2971 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
2972 $('<span />').text(search_val).html()),
2973 action: function() {
2974 self._quick_create(search_val);
2976 classname: 'oe_m2o_dropdown_option'
2981 label: _t("Create and Edit..."),
2982 action: function() {
2983 self._search_create_popup("form", undefined, self._create_context(search_val));
2985 classname: 'oe_m2o_dropdown_option'
2991 get_search_blacklist: function() {
2994 _quick_create: function(name) {
2996 var slow_create = function () {
2997 self._search_create_popup("form", undefined, self._create_context(name));
2999 if (self.options.quick_create === undefined || self.options.quick_create) {
3000 new instance.web.DataSet(this, this.field.relation, self.build_context())
3001 .name_create(name).done(function(data) {
3002 if (!self.get('effective_readonly'))
3003 self.add_id(data[0]);
3004 }).fail(function(error, event) {
3005 event.preventDefault();
3011 // all search/create popup handling
3012 _search_create_popup: function(view, ids, context) {
3014 var pop = new instance.web.form.SelectCreatePopup(this);
3016 self.field.relation,
3018 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3019 initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
3021 disable_multiple_selection: true
3023 self.build_domain(),
3024 new instance.web.CompoundContext(self.build_context(), context || {})
3026 pop.on("elements_selected", self, function(element_ids) {
3027 self.add_id(element_ids[0]);
3034 add_id: function(id) {},
3035 _create_context: function(name) {
3037 var field = (this.options || {}).create_name_field;
3038 if (field === undefined)
3040 if (field !== false && name && (this.options || {}).quick_create !== false)
3041 tmp["default_" + field] = name;
3046 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3047 template: "M2ODialog",
3048 init: function(parent) {
3049 this._super(parent, {
3050 title: _.str.sprintf(_t("Add %s"), parent.string),
3056 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3057 this.$("input").val(this.getParent().last_query);
3058 this.$buttons.find(".oe_form_m2o_qc_button").click(function(){
3059 self.getParent()._quick_create(self.$("input").val());
3062 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3063 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3066 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3072 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3073 template: "FieldMany2One",
3075 'keydown input': function (e) {
3077 case $.ui.keyCode.UP:
3078 case $.ui.keyCode.DOWN:
3079 e.stopPropagation();
3083 init: function(field_manager, node) {
3084 this._super(field_manager, node);
3085 instance.web.form.CompletionFieldMixin.init.call(this);
3086 this.set({'value': false});
3087 this.display_value = {};
3088 this.last_search = [];
3089 this.floating = false;
3090 this.current_display = null;
3091 this.is_started = false;
3092 this.ignore_focusout = false;
3094 reinit_value: function(val) {
3095 this.internal_set_value(val);
3096 this.floating = false;
3097 if (this.is_started)
3098 this.render_value();
3100 initialize_field: function() {
3101 this.is_started = true;
3102 instance.web.bus.on('click', this, function() {
3103 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3104 this.$input.autocomplete("close");
3107 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3109 initialize_content: function() {
3110 if (!this.get("effective_readonly"))
3111 this.render_editable();
3113 destroy_content: function () {
3114 if (this.$drop_down) {
3115 this.$drop_down.off('click');
3116 delete this.$drop_down;
3119 this.$input.closest(".ui-dialog .ui-dialog-content").off('scroll');
3120 this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3121 'focus focusout change keydown');
3124 if (this.$follow_button) {
3125 this.$follow_button.off('blur focus click');
3126 delete this.$follow_button;
3129 destroy: function () {
3130 this.destroy_content();
3131 return this._super();
3133 init_error_displayer: function() {
3136 hide_error_displayer: function() {
3139 show_error_displayer: function() {
3140 new instance.web.form.M2ODialog(this).open();
3142 render_editable: function() {
3144 this.$input = this.$el.find("input");
3146 this.init_error_displayer();
3148 self.$input.on('focus', function() {
3149 self.hide_error_displayer();
3152 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3153 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3155 this.$follow_button.click(function(ev) {
3156 ev.preventDefault();
3157 if (!self.get('value')) {
3161 var pop = new instance.web.form.FormOpenPopup(self);
3163 self.field.relation,
3165 self.build_context(),
3167 title: _t("Open: ") + self.string
3170 pop.on('write_completed', self, function(){
3171 self.display_value = {};
3172 self.render_value();
3174 self.trigger('changed_value');
3178 // some behavior for input
3179 var input_changed = function() {
3180 if (self.current_display !== self.$input.val()) {
3181 self.current_display = self.$input.val();
3182 if (self.$input.val() === "") {
3183 self.internal_set_value(false);
3184 self.floating = false;
3186 self.floating = true;
3190 this.$input.keydown(input_changed);
3191 this.$input.change(input_changed);
3192 this.$drop_down.click(function() {
3193 if (self.$input.autocomplete("widget").is(":visible")) {
3194 self.$input.autocomplete("close");
3195 self.$input.focus();
3197 if (self.get("value") && ! self.floating) {
3198 self.$input.autocomplete("search", "");
3200 self.$input.autocomplete("search");
3205 // Autocomplete close on dialog content scroll
3206 var close_autocomplete = _.debounce(function() {
3207 if (self.$input.autocomplete("widget").is(":visible")) {
3208 self.$input.autocomplete("close");
3211 this.$input.closest(".ui-dialog .ui-dialog-content").on('scroll', this, close_autocomplete);
3213 self.ed_def = $.Deferred();
3214 self.uned_def = $.Deferred();
3216 var ed_duration = 15000;
3217 var anyoneLoosesFocus = function (e) {
3218 if (self.ignore_focusout) { return; }
3220 if (self.floating) {
3221 if (self.last_search.length > 0) {
3222 if (self.last_search[0][0] != self.get("value")) {
3223 self.display_value = {};
3224 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3225 self.reinit_value(self.last_search[0][0]);
3228 self.render_value();
3232 self.reinit_value(false);
3234 self.floating = false;
3236 if (used && self.get("value") === false && ! self.no_ed) {
3237 self.ed_def.reject();
3238 self.uned_def.reject();
3239 self.ed_def = $.Deferred();
3240 self.ed_def.done(function() {
3241 self.show_error_displayer();
3242 ignore_blur = false;
3243 self.trigger('focused');
3246 setTimeout(function() {
3247 self.ed_def.resolve();
3248 self.uned_def.reject();
3249 self.uned_def = $.Deferred();
3250 self.uned_def.done(function() {
3251 self.hide_error_displayer();
3253 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3257 self.ed_def.reject();
3260 var ignore_blur = false;
3262 focusout: anyoneLoosesFocus,
3263 focus: function () { self.trigger('focused'); },
3264 autocompleteopen: function () { ignore_blur = true; },
3265 autocompleteclose: function () { ignore_blur = false; },
3267 // autocomplete open
3268 if (ignore_blur) { return; }
3269 if (_(self.getChildren()).any(function (child) {
3270 return child instanceof instance.web.form.AbstractFormPopup;
3272 self.trigger('blurred');
3276 var isSelecting = false;
3278 this.$input.autocomplete({
3279 source: function(req, resp) {
3280 self.get_search_result(req.term).done(function(result) {
3284 select: function(event, ui) {
3288 self.display_value = {};
3289 self.display_value["" + item.id] = item.name;
3290 self.reinit_value(item.id);
3291 } else if (item.action) {
3293 // Cancel widget blurring, to avoid form blur event
3294 self.trigger('focused');
3298 focus: function(e, ui) {
3302 // disabled to solve a bug, but may cause others
3303 //close: anyoneLoosesFocus,
3307 this.$input.autocomplete("widget").openerpClass();
3308 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3309 this.$input.keyup(function(e) {
3310 if (e.which === 13) { // ENTER
3312 e.stopPropagation();
3314 isSelecting = false;
3316 this.setupFocus(this.$follow_button);
3318 render_value: function(no_recurse) {
3320 if (! this.get("value")) {
3321 this.display_string("");
3324 var display = this.display_value["" + this.get("value")];
3326 this.display_string(display);
3330 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3331 this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3333 self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
3336 self.display_value["" + self.get("value")] = data[0][1];
3337 self.render_value(true);
3341 display_string: function(str) {
3343 if (!this.get("effective_readonly")) {
3344 this.$input.val(str.split("\n")[0]);
3345 this.current_display = this.$input.val();
3346 if (this.is_false()) {
3347 this.$('.oe_m2o_cm_button').css({'display':'none'});
3349 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3352 var lines = _.escape(str).split("\n");
3356 follow = _.rest(lines).join("<br />");
3359 var $link = this.$el.find('.oe_form_uri')
3362 if (! this.options.no_open)
3363 $link.click(function () {
3365 type: 'ir.actions.act_window',
3366 res_model: self.field.relation,
3367 res_id: self.get("value"),
3368 views: [[false, 'form']],
3370 context: self.build_context().eval(),
3374 $(".oe_form_m2o_follow", this.$el).html(follow);
3377 set_value: function(value_) {
3379 if (value_ instanceof Array) {
3380 this.display_value = {};
3381 if (! this.options.always_reload) {
3382 this.display_value["" + value_[0]] = value_[1];
3386 value_ = value_ || false;
3387 this.reinit_value(value_);
3389 get_displayed: function() {
3390 return this.display_value["" + this.get("value")];
3392 add_id: function(id) {
3393 this.display_value = {};
3394 this.reinit_value(id);
3396 is_false: function() {
3397 return ! this.get("value");
3399 focus: function () {
3400 var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3401 return input ? input.focus() : false;
3403 _quick_create: function() {
3405 this.ed_def.reject();
3406 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3408 _search_create_popup: function() {
3410 this.ed_def.reject();
3411 this.ignore_focusout = true;
3412 this.reinit_value(false);
3413 var res = instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3414 this.ignore_focusout = false;
3418 set_dimensions: function (height, width) {
3419 this._super(height, width);
3420 if (!this.get("effective_readonly") && this.$input)
3421 this.$input.css('height', height);
3425 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3426 template: 'Many2OneButton',
3427 init: function(field_manager, node) {
3428 this._super.apply(this, arguments);
3431 this._super.apply(this, arguments);
3434 set_button: function() {
3437 this.$button.remove();
3440 this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3441 this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3442 this.$button.addClass('oe_link').css({'padding':'4px'});
3443 this.$el.append(this.$button);
3444 this.$button.on('click', self.on_click);
3446 on_click: function(ev) {
3448 this.popup = new instance.web.form.FormOpenPopup(this);
3449 this.popup.show_element(
3450 this.field.relation,
3452 this.build_context(),
3453 {title: this.string}
3455 this.popup.on('create_completed', self, function(r) {
3459 set_value: function(value_) {
3461 if (value_ instanceof Array) {
3464 value_ = value_ || false;
3465 this.set('value', value_);
3471 # Values: (0, 0, { fields }) create
3472 # (1, ID, { fields }) update
3473 # (2, ID) remove (delete)
3474 # (3, ID) unlink one (target id or target of relation)
3476 # (5) unlink all (only valid for one2many)
3481 'create': function (values) {
3482 return [commands.CREATE, false, values];
3484 // (1, id, {values})
3486 'update': function (id, values) {
3487 return [commands.UPDATE, id, values];
3491 'delete': function (id) {
3492 return [commands.DELETE, id, false];
3494 // (3, id[, _]) removes relation, but not linked record itself
3496 'forget': function (id) {
3497 return [commands.FORGET, id, false];
3501 'link_to': function (id) {
3502 return [commands.LINK_TO, id, false];
3506 'delete_all': function () {
3507 return [5, false, false];
3509 // (6, _, ids) replaces all linked records with provided ids
3511 'replace_with': function (ids) {
3512 return [6, false, ids];
3515 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3516 multi_selection: false,
3517 disable_utility_classes: true,
3518 init: function(field_manager, node) {
3519 this._super(field_manager, node);
3520 lazy_build_o2m_kanban_view();
3521 this.is_loaded = $.Deferred();
3522 this.initial_is_loaded = this.is_loaded;
3523 this.form_last_update = $.Deferred();
3524 this.init_form_last_update = this.form_last_update;
3525 this.is_started = false;
3526 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
3527 this.dataset.o2m = this;
3528 this.dataset.parent_view = this.view;
3529 this.dataset.child_name = this.name;
3531 this.dataset.on('dataset_changed', this, function() {
3532 self.trigger_on_change();
3537 this._super.apply(this, arguments);
3538 this.$el.addClass('oe_form_field oe_form_field_one2many');
3543 this.is_loaded.done(function() {
3544 self.on("change:effective_readonly", self, function() {
3545 self.is_loaded = self.is_loaded.then(function() {
3546 self.viewmanager.destroy();
3547 return $.when(self.load_views()).done(function() {
3548 self.reload_current_view();
3553 this.is_started = true;
3554 this.reload_current_view();
3556 trigger_on_change: function() {
3557 this.trigger('changed_value');
3559 load_views: function() {
3562 var modes = this.node.attrs.mode;
3563 modes = !!modes ? modes.split(",") : ["tree"];
3565 _.each(modes, function(mode) {
3566 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
3567 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
3571 view_type: mode == "tree" ? "list" : mode,
3574 if (self.field.views && self.field.views[mode]) {
3575 view.embedded_view = self.field.views[mode];
3577 if(view.view_type === "list") {
3578 _.extend(view.options, {
3580 selectable: self.multi_selection,
3582 import_enabled: false,
3585 if (self.get("effective_readonly")) {
3586 _.extend(view.options, {
3591 } else if (view.view_type === "form") {
3592 if (self.get("effective_readonly")) {
3593 view.view_type = 'form';
3595 _.extend(view.options, {
3596 not_interactible_on_create: true,
3598 } else if (view.view_type === "kanban") {
3599 _.extend(view.options, {
3600 confirm_on_delete: false,
3602 if (self.get("effective_readonly")) {
3603 _.extend(view.options, {
3604 action_buttons: false,
3605 quick_creatable: false,
3607 read_only_mode: true,
3615 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
3616 this.viewmanager.o2m = self;
3617 var once = $.Deferred().done(function() {
3618 self.init_form_last_update.resolve();
3620 var def = $.Deferred().done(function() {
3621 self.initial_is_loaded.resolve();
3623 this.viewmanager.on("controller_inited", self, function(view_type, controller) {
3624 controller.o2m = self;
3625 if (view_type == "list") {
3626 if (self.get("effective_readonly")) {
3627 controller.on('edit:before', self, function (e) {
3630 _(controller.columns).find(function (column) {
3631 if (!(column instanceof instance.web.list.Handle)) {
3634 column.modifiers.invisible = true;
3638 } else if (view_type === "form") {
3639 if (self.get("effective_readonly")) {
3640 $(".oe_form_buttons", controller.$el).children().remove();
3642 controller.on("load_record", self, function(){
3645 controller.on('pager_action_executed',self,self.save_any_view);
3646 } else if (view_type == "graph") {
3647 self.reload_current_view()
3651 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
3652 $.when(self.save_any_view()).done(function() {
3653 if (n_mode === "list") {
3654 $.async_when().done(function() {
3655 self.reload_current_view();
3660 $.async_when().done(function () {
3661 self.viewmanager.appendTo(self.$el);
3665 reload_current_view: function() {
3667 return self.is_loaded = self.is_loaded.then(function() {
3668 var active_view = self.viewmanager.active_view;
3669 var view = self.viewmanager.views[active_view].controller;
3670 if(active_view === "list") {
3671 return view.reload_content();
3672 } else if (active_view === "form") {
3673 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
3674 self.dataset.index = 0;
3676 var act = function() {
3677 return view.do_show();
3679 self.form_last_update = self.form_last_update.then(act, act);
3680 return self.form_last_update;
3681 } else if (view.do_search) {
3682 return view.do_search(self.build_domain(), self.dataset.get_context(), []);
3686 set_value: function(value_) {
3687 value_ = value_ || [];
3689 this.dataset.reset_ids([]);
3690 if(value_.length >= 1 && value_[0] instanceof Array) {
3692 _.each(value_, function(command) {
3693 var obj = {values: command[2]};
3694 switch (command[0]) {
3695 case commands.CREATE:
3696 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3698 self.dataset.to_create.push(obj);
3699 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3702 case commands.UPDATE:
3703 obj['id'] = command[1];
3704 self.dataset.to_write.push(obj);
3705 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3708 case commands.DELETE:
3709 self.dataset.to_delete.push({id: command[1]});
3711 case commands.LINK_TO:
3712 ids.push(command[1]);
3714 case commands.DELETE_ALL:
3715 self.dataset.delete_all = true;
3720 this.dataset.set_ids(ids);
3721 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
3723 this.dataset.delete_all = true;
3724 _.each(value_, function(command) {
3725 var obj = {values: command};
3726 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3728 self.dataset.to_create.push(obj);
3729 self.dataset.cache.push(_.clone(obj));
3733 this.dataset.set_ids(ids);
3735 this._super(value_);
3736 this.dataset.reset_ids(value_);
3738 if (this.dataset.index === null && this.dataset.ids.length > 0) {
3739 this.dataset.index = 0;
3741 this.trigger_on_change();
3742 if (this.is_started) {
3743 return self.reload_current_view();
3748 get_value: function() {
3752 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
3753 val = val.concat(_.map(this.dataset.ids, function(id) {
3754 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
3756 return commands.create(alter_order.values);
3758 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
3760 return commands.update(alter_order.id, alter_order.values);
3762 return commands.link_to(id);
3764 return val.concat(_.map(
3765 this.dataset.to_delete, function(x) {
3766 return commands['delete'](x.id);}));
3768 commit_value: function() {
3769 return this.save_any_view();
3771 save_any_view: function() {
3772 if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
3773 this.viewmanager.views[this.viewmanager.active_view] &&
3774 this.viewmanager.views[this.viewmanager.active_view].controller) {
3775 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3776 if (this.viewmanager.active_view === "form") {
3777 if (!view.is_initialized.state() === 'resolved') {
3778 return $.when(false);
3780 return $.when(view.save());
3781 } else if (this.viewmanager.active_view === "list") {
3782 return $.when(view.ensure_saved());
3785 return $.when(false);
3787 is_syntax_valid: function() {
3788 if (! this.viewmanager || ! this.viewmanager.views[this.viewmanager.active_view])
3790 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3791 switch (this.viewmanager.active_view) {
3793 return _(view.fields).chain()
3799 return view.is_valid();
3805 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
3806 template: 'One2Many.viewmanager',
3807 init: function(parent, dataset, views, flags) {
3808 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
3809 this.registry = this.registry.extend({
3810 list: 'instance.web.form.One2ManyListView',
3811 form: 'instance.web.form.One2ManyFormView',
3812 kanban: 'instance.web.form.One2ManyKanbanView',
3814 this.__ignore_blur = false;
3816 switch_mode: function(mode, unused) {
3817 if (mode !== 'form') {
3818 return this._super(mode, unused);
3821 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
3822 var pop = new instance.web.form.FormOpenPopup(this);
3823 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3824 title: _t("Open: ") + self.o2m.string,
3825 create_function: function(data, options) {
3826 return self.o2m.dataset.create(data, options).done(function(r) {
3827 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3828 self.o2m.dataset.trigger("dataset_changed", r);
3831 write_function: function(id, data, options) {
3832 return self.o2m.dataset.write(id, data, {}).done(function() {
3833 self.o2m.reload_current_view();
3836 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3837 parent_view: self.o2m.view,
3838 child_name: self.o2m.name,
3839 read_function: function() {
3840 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3842 form_view_options: {'not_interactible_on_create':true},
3843 readonly: self.o2m.get("effective_readonly")
3845 pop.on("elements_selected", self, function() {
3846 self.o2m.reload_current_view();
3851 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
3852 get_context: function() {
3853 this.context = this.o2m.build_context();
3854 return this.context;
3858 instance.web.form.One2ManyListView = instance.web.ListView.extend({
3859 _template: 'One2Many.listview',
3860 init: function (parent, dataset, view_id, options) {
3861 this._super(parent, dataset, view_id, _.extend(options || {}, {
3862 GroupsType: instance.web.form.One2ManyGroups,
3863 ListType: instance.web.form.One2ManyList
3865 this.on('edit:after', this, this.proxy('_after_edit'));
3866 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
3869 .bind('add', this.proxy("changed_records"))
3870 .bind('edit', this.proxy("changed_records"))
3871 .bind('remove', this.proxy("changed_records"));
3873 start: function () {
3874 var ret = this._super();
3876 .off('mousedown.handleButtons')
3877 .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
3880 changed_records: function () {
3881 this.o2m.trigger_on_change();
3883 is_valid: function () {
3884 var editor = this.editor;
3885 var form = editor.form;
3886 // If no edition is pending, the listview can not be invalid (?)
3887 if (!editor.record) {
3890 // If the form has not been modified, the view can only be valid
3891 // NB: is_dirty will also be set on defaults/onchanges/whatever?
3892 // oe_form_dirty seems to only be set on actual user actions
3893 if (!form.$el.is('.oe_form_dirty')) {
3896 this.o2m._dirty_flag = true;
3898 // Otherwise validate internal form
3899 return _(form.fields).chain()
3900 .invoke(function () {
3901 this._check_css_flags();
3902 return this.is_valid();
3907 do_add_record: function () {
3908 if (this.editable()) {
3909 this._super.apply(this, arguments);
3912 var pop = new instance.web.form.SelectCreatePopup(this);
3914 self.o2m.field.relation,
3916 title: _t("Create: ") + self.o2m.string,
3917 initial_view: "form",
3918 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3919 create_function: function(data, options) {
3920 return self.o2m.dataset.create(data, options).done(function(r) {
3921 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3922 self.o2m.dataset.trigger("dataset_changed", r);
3925 read_function: function() {
3926 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3928 parent_view: self.o2m.view,
3929 child_name: self.o2m.name,
3930 form_view_options: {'not_interactible_on_create':true}
3932 self.o2m.build_domain(),
3933 self.o2m.build_context()
3935 pop.on("elements_selected", self, function() {
3936 self.o2m.reload_current_view();
3940 do_activate_record: function(index, id) {
3942 var pop = new instance.web.form.FormOpenPopup(self);
3943 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3944 title: _t("Open: ") + self.o2m.string,
3945 write_function: function(id, data) {
3946 return self.o2m.dataset.write(id, data, {}).done(function() {
3947 self.o2m.reload_current_view();
3950 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3951 parent_view: self.o2m.view,
3952 child_name: self.o2m.name,
3953 read_function: function() {
3954 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3956 form_view_options: {'not_interactible_on_create':true},
3957 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
3960 do_button_action: function (name, id, callback) {
3961 if (!_.isNumber(id)) {
3962 instance.webclient.notification.warn(
3963 _t("Action Button"),
3964 _t("The o2m record must be saved before an action can be used"));
3967 var parent_form = this.o2m.view;
3969 this.ensure_saved().then(function () {
3971 return parent_form.save();
3974 }).done(function () {
3975 var ds = self.o2m.dataset;
3976 var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
3977 return value.length;
3979 if (!self.o2m.options.reload_on_button && !cached_records) {
3980 self.handle_button(name, id, callback);
3982 self.handle_button(name, id, function(){
3983 self.o2m.view.reload();
3989 _after_edit: function () {
3990 this.__ignore_blur = false;
3991 this.editor.form.on('blurred', this, this._on_form_blur);
3993 // The form's blur thing may be jiggered during the edition setup,
3994 // potentially leading to the o2m instasaving the row. Cancel any
3995 // blurring triggered the edition startup here
3996 this.editor.form.widgetFocused();
3998 _before_unedit: function () {
3999 this.editor.form.off('blurred', this, this._on_form_blur);
4001 _button_down: function () {
4002 // If a button is clicked (usually some sort of action button), it's
4003 // the button's responsibility to ensure the editable list is in the
4004 // correct state -> ignore form blurring
4005 this.__ignore_blur = true;
4008 * Handles blurring of the nested form (saves the currently edited row),
4009 * unless the flag to ignore the event is set to ``true``
4011 * Makes the internal form go away
4013 _on_form_blur: function () {
4014 if (this.__ignore_blur) {
4015 this.__ignore_blur = false;
4018 // FIXME: why isn't there an API for this?
4019 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4020 this.ensure_saved();
4023 this.cancel_edition();
4025 keypress_ENTER: function () {
4026 // blurring caused by hitting the [Return] key, should skip the
4027 // autosave-on-blur and let the handler for [Return] do its thing (save
4028 // the current row *anyway*, then create a new one/edit the next one)
4029 this.__ignore_blur = true;
4030 this._super.apply(this, arguments);
4032 do_delete: function (ids) {
4033 var confirm = window.confirm;
4034 window.confirm = function () { return true; };
4036 return this._super(ids);
4038 window.confirm = confirm;
4041 reload_record: function (record) {
4042 // Evict record.id from cache to ensure it will be reloaded correctly
4043 this.dataset.evict_record(record.get('id'));
4045 return this._super(record);
4048 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4049 setup_resequence_rows: function () {
4050 if (!this.view.o2m.get('effective_readonly')) {
4051 this._super.apply(this, arguments);
4055 instance.web.form.One2ManyList = instance.web.ListView.List.extend({
4056 pad_table_to: function (count) {
4057 if (!this.view.is_action_enabled('create')) {
4060 this._super(count > 0 ? count - 1 : 0);
4063 // magical invocation of wtf does that do
4064 if (this.view.o2m.get('effective_readonly')) {
4069 var columns = _(this.columns).filter(function (column) {
4070 return column.invisible !== '1';
4072 if (this.options.selectable) { columns++; }
4073 if (this.options.deletable) { columns++; }
4075 if (!this.view.is_action_enabled('create')) {
4079 var $cell = $('<td>', {
4081 'class': 'oe_form_field_one2many_list_row_add'
4083 $('<a>', {href: '#'}).text(_t("Add an item"))
4084 .mousedown(function () {
4085 // FIXME: needs to be an official API somehow
4086 if (self.view.editor.is_editing()) {
4087 self.view.__ignore_blur = true;
4090 .click(function (e) {
4092 e.stopPropagation();
4093 // FIXME: there should also be an API for that one
4094 if (self.view.editor.form.__blur_timeout) {
4095 clearTimeout(self.view.editor.form.__blur_timeout);
4096 self.view.editor.form.__blur_timeout = false;
4098 self.view.ensure_saved().done(function () {
4099 self.view.do_add_record();
4103 var $padding = this.$current.find('tr:not([data-id]):first');
4104 var $newrow = $('<tr>').append($cell);
4105 if ($padding.length) {
4106 $padding.before($newrow);
4108 this.$current.append($newrow)
4113 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4114 form_template: 'One2Many.formview',
4115 load_form: function(data) {
4118 this.$buttons.find('button.oe_form_button_create').click(function() {
4119 self.save().done(self.on_button_new);
4122 do_notify_change: function() {
4123 if (this.dataset.parent_view) {
4124 this.dataset.parent_view.do_notify_change();
4126 this._super.apply(this, arguments);
4131 var lazy_build_o2m_kanban_view = function() {
4132 if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
4134 instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4138 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4139 template: "FieldMany2ManyTags",
4141 this._super.apply(this, arguments);
4142 instance.web.form.CompletionFieldMixin.init.call(this);
4143 this.set({"value": []});
4144 this._display_orderer = new instance.web.DropMisordered();
4145 this._drop_shown = false;
4147 initialize_content: function() {
4148 if (this.get("effective_readonly"))
4151 var ignore_blur = false;
4152 self.$text = this.$("textarea");
4153 self.$text.textext({
4154 plugins : 'tags arrow autocomplete',
4156 render: function(suggestion) {
4157 return $('<span class="text-label"/>').
4158 data('index', suggestion['index']).html(suggestion['label']);
4163 selectFromDropdown: function() {
4164 this.trigger('hideDropdown');
4165 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4166 var data = self.search_result[index];
4168 self.add_id(data.id);
4173 this.trigger('setSuggestions', {result : []});
4177 isTagAllowed: function(tag) {
4181 removeTag: function(tag) {
4182 var id = tag.data("id");
4183 self.set({"value": _.without(self.get("value"), id)});
4185 renderTag: function(stuff) {
4186 return $.fn.textext.TextExtTags.prototype.renderTag.
4187 call(this, stuff).data("id", stuff.id);
4191 itemToString: function(item) {
4196 onSetInputData: function(e, data) {
4198 this._plugins.autocomplete._suggestions = null;
4200 this.input().val(data);
4204 }).bind('getSuggestions', function(e, data) {
4206 var str = !!data ? data.query || '' : '';
4207 self.get_search_result(str).done(function(result) {
4208 self.search_result = result;
4209 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4210 return _.extend(el, {index:i});
4213 }).bind('hideDropdown', function() {
4214 self._drop_shown = false;
4215 }).bind('showDropdown', function() {
4216 self._drop_shown = true;
4218 self.tags = self.$text.textext()[0].tags();
4220 .focusin(function () {
4221 self.trigger('focused');
4222 ignore_blur = false;
4224 .focusout(function() {
4225 self.$text.trigger("setInputData", "");
4227 self.trigger('blurred');
4229 }).keydown(function(e) {
4230 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4231 self.$text.textext()[0].autocomplete().selectFromDropdown();
4235 set_value: function(value_) {
4236 value_ = value_ || [];
4237 if (value_.length >= 1 && value_[0] instanceof Array) {
4238 value_ = value_[0][2];
4240 this._super(value_);
4242 is_false: function() {
4243 return _(this.get("value")).isEmpty();
4245 get_value: function() {
4246 var tmp = [commands.replace_with(this.get("value"))];
4249 get_search_blacklist: function() {
4250 return this.get("value");
4252 render_value: function() {
4254 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4255 var values = self.get("value");
4256 var handle_names = function(data) {
4257 if (self.isDestroyed())
4260 _.each(data, function(el) {
4261 indexed[el[0]] = el;
4263 data = _.map(values, function(el) { return indexed[el]; });
4264 if (! self.get("effective_readonly")) {
4265 self.tags.containerElement().children().remove();
4266 self.$('textarea').css("padding-left", "3px");
4267 self.tags.addTags(_.map(data, function(el) {return {name: el[1], id:el[0]};}));
4269 self.$el.html(QWeb.render("FieldMany2ManyTag", {elements: data}));
4272 if (! values || values.length > 0) {
4273 return this._display_orderer.add(dataset.name_get(values)).done(handle_names);
4278 add_id: function(id) {
4279 this.set({'value': _.uniq(this.get('value').concat([id]))});
4281 focus: function () {
4282 var input = this.$text && this.$text[0];
4283 return input ? input.focus() : false;
4289 - reload_on_button: Reload the whole form view if click on a button in a list view.
4290 If you see this options, do not use it, it's basically a dirty hack to make one
4291 precise o2m to behave the way we want.
4293 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4294 multi_selection: false,
4295 disable_utility_classes: true,
4296 init: function(field_manager, node) {
4297 this._super(field_manager, node);
4298 this.is_loaded = $.Deferred();
4299 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4300 this.dataset.m2m = this;
4302 this.dataset.on('unlink', self, function(ids) {
4303 self.dataset_changed();
4306 this.list_dm = new instance.web.DropMisordered();
4307 this.render_value_dm = new instance.web.DropMisordered();
4309 initialize_content: function() {
4312 this.$el.addClass('oe_form_field oe_form_field_many2many');
4314 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4315 'addable': this.get("effective_readonly") ? null : _t("Add"),
4316 'deletable': this.get("effective_readonly") ? false : true,
4317 'selectable': this.multi_selection,
4319 'reorderable': false,
4320 'import_enabled': false,
4322 var embedded = (this.field.views || {}).tree;
4324 this.list_view.set_embedded_view(embedded);
4326 this.list_view.m2m_field = this;
4327 var loaded = $.Deferred();
4328 this.list_view.on("list_view_loaded", this, function() {
4331 this.list_view.appendTo(this.$el);
4333 var old_def = self.is_loaded;
4334 self.is_loaded = $.Deferred().done(function() {
4337 this.list_dm.add(loaded).then(function() {
4338 self.is_loaded.resolve();
4341 destroy_content: function() {
4342 this.list_view.destroy();
4343 this.list_view = undefined;
4345 set_value: function(value_) {
4346 value_ = value_ || [];
4347 if (value_.length >= 1 && value_[0] instanceof Array) {
4348 value_ = value_[0][2];
4350 this._super(value_);
4352 get_value: function() {
4353 return [commands.replace_with(this.get('value'))];
4355 is_false: function () {
4356 return _(this.get("value")).isEmpty();
4358 render_value: function() {
4360 this.dataset.set_ids(this.get("value"));
4361 this.render_value_dm.add(this.is_loaded).then(function() {
4362 return self.list_view.reload_content();
4365 dataset_changed: function() {
4366 this.internal_set_value(this.dataset.ids);
4370 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4371 get_context: function() {
4372 this.context = this.m2m.build_context();
4373 return this.context;
4379 * @extends instance.web.ListView
4381 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4382 do_add_record: function () {
4383 var pop = new instance.web.form.SelectCreatePopup(this);
4387 title: _t("Add: ") + this.m2m_field.string,
4388 no_create: this.m2m_field.options.no_create,
4390 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4391 this.m2m_field.build_context()
4394 pop.on("elements_selected", self, function(element_ids) {
4396 _(element_ids).each(function (id) {
4397 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4398 self.dataset.set_ids(self.dataset.ids.concat([id]));
4399 self.m2m_field.dataset_changed();
4404 self.reload_content();
4408 do_activate_record: function(index, id) {
4410 var pop = new instance.web.form.FormOpenPopup(this);
4411 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4412 title: _t("Open: ") + this.m2m_field.string,
4413 readonly: this.getParent().get("effective_readonly")
4415 pop.on('write_completed', self, self.reload_content);
4417 do_button_action: function(name, id, callback) {
4419 var _sup = _.bind(this._super, this);
4420 if (! this.m2m_field.options.reload_on_button) {
4421 return _sup(name, id, callback);
4423 return this.m2m_field.view.save().then(function() {
4424 return _sup(name, id, function() {
4425 self.m2m_field.view.reload();
4430 is_action_enabled: function () { return true; },
4433 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4434 disable_utility_classes: true,
4435 init: function(field_manager, node) {
4436 this._super(field_manager, node);
4437 instance.web.form.CompletionFieldMixin.init.call(this);
4438 m2m_kanban_lazy_init();
4439 this.is_loaded = $.Deferred();
4440 this.initial_is_loaded = this.is_loaded;
4443 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4444 this.dataset.m2m = this;
4445 this.dataset.on('unlink', self, function(ids) {
4446 self.dataset_changed();
4450 this._super.apply(this, arguments);
4455 self.on("change:effective_readonly", self, function() {
4456 self.is_loaded = self.is_loaded.then(function() {
4457 self.kanban_view.destroy();
4458 return $.when(self.load_view()).done(function() {
4459 self.render_value();
4464 set_value: function(value_) {
4465 value_ = value_ || [];
4466 if (value_.length >= 1 && value_[0] instanceof Array) {
4467 value_ = value_[0][2];
4469 this._super(value_);
4471 get_value: function() {
4472 return [commands.replace_with(this.get('value'))];
4474 load_view: function() {
4476 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4477 'create_text': _t("Add"),
4478 'creatable': self.get("effective_readonly") ? false : true,
4479 'quick_creatable': self.get("effective_readonly") ? false : true,
4480 'read_only_mode': self.get("effective_readonly") ? true : false,
4481 'confirm_on_delete': false,
4483 var embedded = (this.field.views || {}).kanban;
4485 this.kanban_view.set_embedded_view(embedded);
4487 this.kanban_view.m2m = this;
4488 var loaded = $.Deferred();
4489 this.kanban_view.on("kanban_view_loaded",self,function() {
4490 self.initial_is_loaded.resolve();
4493 this.kanban_view.on('switch_mode', this, this.open_popup);
4494 $.async_when().done(function () {
4495 self.kanban_view.appendTo(self.$el);
4499 render_value: function() {
4501 this.dataset.set_ids(this.get("value"));
4502 this.is_loaded = this.is_loaded.then(function() {
4503 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
4506 dataset_changed: function() {
4507 this.set({'value': this.dataset.ids});
4509 open_popup: function(type, unused) {
4510 if (type !== "form")
4513 if (this.dataset.index === null) {
4514 var pop = new instance.web.form.SelectCreatePopup(this);
4516 this.field.relation,
4518 title: _t("Add: ") + this.string
4520 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
4521 this.build_context()
4523 pop.on("elements_selected", self, function(element_ids) {
4524 _.each(element_ids, function(one_id) {
4525 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
4526 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
4527 self.dataset_changed();
4528 self.render_value();
4533 var id = self.dataset.ids[self.dataset.index];
4534 var pop = new instance.web.form.FormOpenPopup(this);
4535 pop.show_element(self.field.relation, id, self.build_context(), {
4536 title: _t("Open: ") + self.string,
4537 write_function: function(id, data, options) {
4538 return self.dataset.write(id, data, {}).done(function() {
4539 self.render_value();
4542 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
4543 parent_view: self.view,
4544 child_name: self.name,
4545 readonly: self.get("effective_readonly")
4549 add_id: function(id) {
4550 this.quick_create.add_id(id);
4554 function m2m_kanban_lazy_init() {
4555 if (instance.web.form.Many2ManyKanbanView)
4557 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4558 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
4559 _is_quick_create_enabled: function() {
4560 return this._super() && ! this.group_by;
4563 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
4564 template: 'Many2ManyKanban.quick_create',
4567 * close_btn: If true, the widget will display a "Close" button able to trigger
4570 init: function(parent, dataset, context, buttons) {
4571 this._super(parent);
4572 this.m2m = this.getParent().view.m2m;
4573 this.m2m.quick_create = this;
4574 this._dataset = dataset;
4575 this._buttons = buttons || false;
4576 this._context = context || {};
4578 start: function () {
4580 self.$text = this.$el.find('input').css("width", "200px");
4581 self.$text.textext({
4582 plugins : 'arrow autocomplete',
4584 render: function(suggestion) {
4585 return $('<span class="text-label"/>').
4586 data('index', suggestion['index']).html(suggestion['label']);
4591 selectFromDropdown: function() {
4592 $(this).trigger('hideDropdown');
4593 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4594 var data = self.search_result[index];
4596 self.add_id(data.id);
4603 itemToString: function(item) {
4608 }).bind('getSuggestions', function(e, data) {
4610 var str = !!data ? data.query || '' : '';
4611 self.m2m.get_search_result(str).done(function(result) {
4612 self.search_result = result;
4613 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4614 return _.extend(el, {index:i});
4618 self.$text.focusout(function() {
4623 this.$text[0].focus();
4625 add_id: function(id) {
4628 self.trigger('added', id);
4629 this.m2m.dataset_changed();
4635 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
4637 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
4638 template: "AbstractFormPopup.render",
4641 * -readonly: only applicable when not in creation mode, default to false
4642 * - alternative_form_view
4649 * - form_view_options
4651 init_popup: function(model, row_id, domain, context, options) {
4652 this.row_id = row_id;
4654 this.domain = domain || [];
4655 this.context = context || {};
4656 this.options = options;
4657 _.defaults(this.options, {
4660 init_dataset: function() {
4662 this.created_elements = [];
4663 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
4664 this.dataset.read_function = this.options.read_function;
4665 this.dataset.create_function = function(data, options, sup) {
4666 var fct = self.options.create_function || sup;
4667 return fct.call(this, data, options).done(function(r) {
4668 self.trigger('create_completed saved', r);
4669 self.created_elements.push(r);
4672 this.dataset.write_function = function(id, data, options, sup) {
4673 var fct = self.options.write_function || sup;
4674 return fct.call(this, id, data, options).done(function(r) {
4675 self.trigger('write_completed saved', r);
4678 this.dataset.parent_view = this.options.parent_view;
4679 this.dataset.child_name = this.options.child_name;
4681 display_popup: function() {
4683 this.renderElement();
4684 var dialog = new instance.web.Dialog(this, {
4686 dialogClass: 'oe_act_window',
4688 self.check_exit(true);
4690 title: this.options.title || "",
4691 }, this.$el).open();
4692 this.$buttonpane = dialog.$buttons;
4695 setup_form_view: function() {
4698 this.dataset.ids = [this.row_id];
4699 this.dataset.index = 0;
4701 this.dataset.index = null;
4703 var options = _.clone(self.options.form_view_options) || {};
4704 if (this.row_id !== null) {
4705 options.initial_mode = this.options.readonly ? "view" : "edit";
4708 $buttons: this.$buttonpane,
4710 this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
4711 if (this.options.alternative_form_view) {
4712 this.view_form.set_embedded_view(this.options.alternative_form_view);
4714 this.view_form.appendTo(this.$el.find(".oe_popup_form"));
4715 this.view_form.on("form_view_loaded", self, function() {
4716 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
4717 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
4718 multi_select: multi_select,
4719 readonly: self.row_id !== null && self.options.readonly,
4721 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
4722 $snbutton.click(function() {
4723 $.when(self.view_form.save()).done(function() {
4724 self.view_form.reload_mutex.exec(function() {
4725 self.view_form.on_button_new();
4729 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
4730 $sbutton.click(function() {
4731 $.when(self.view_form.save()).done(function() {
4732 self.view_form.reload_mutex.exec(function() {
4737 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
4738 $cbutton.click(function() {
4739 self.view_form.trigger('on_button_cancel');
4742 self.view_form.do_show();
4745 select_elements: function(element_ids) {
4746 this.trigger("elements_selected", element_ids);
4748 check_exit: function(no_destroy) {
4749 if (this.created_elements.length > 0) {
4750 this.select_elements(this.created_elements);
4751 this.created_elements = [];
4753 this.trigger('closed');
4756 destroy: function () {
4757 this.trigger('closed');
4758 if (this.$el.is(":data(dialog)")) {
4759 this.$el.dialog('close');
4766 * Class to display a popup containing a form view.
4768 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
4769 show_element: function(model, row_id, context, options) {
4770 this.init_popup(model, row_id, [], context, options);
4771 _.defaults(this.options, {
4773 this.display_popup();
4777 this.init_dataset();
4778 this.setup_form_view();
4783 * Class to display a popup to display a list to search a row. It also allows
4784 * to switch to a form view to create a new row.
4786 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
4790 * - initial_view: form or search (default search)
4791 * - disable_multiple_selection
4792 * - list_view_options
4794 select_element: function(model, options, domain, context) {
4795 this.init_popup(model, null, domain, context, options);
4797 _.defaults(this.options, {
4798 initial_view: "search",
4800 this.initial_ids = this.options.initial_ids;
4801 this.display_popup();
4805 this.init_dataset();
4806 if (this.options.initial_view == "search") {
4807 instance.web.pyeval.eval_domains_and_contexts({
4809 contexts: [this.context]
4810 }).done(function (results) {
4811 var search_defaults = {};
4812 _.each(results.context, function (value_, key) {
4813 var match = /^search_default_(.*)$/.exec(key);
4815 search_defaults[match[1]] = value_;
4818 self.setup_search_view(search_defaults);
4824 setup_search_view: function(search_defaults) {
4826 if (this.searchview) {
4827 this.searchview.destroy();
4829 this.searchview = new instance.web.SearchView(this,
4830 this.dataset, false, search_defaults);
4831 this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
4832 if (self.initial_ids) {
4833 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
4834 contexts.concat(self.context), groupbys);
4835 self.initial_ids = undefined;
4837 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
4840 this.searchview.on("search_view_loaded", self, function() {
4841 self.view_list = new instance.web.form.SelectCreateListView(self,
4842 self.dataset, false,
4843 _.extend({'deletable': false,
4844 'selectable': !self.options.disable_multiple_selection,
4845 'import_enabled': false,
4846 '$buttons': self.$buttonpane,
4847 'disable_editable_mode': true,
4848 '$pager': self.$('.oe_popup_list_pager'),
4849 }, self.options.list_view_options || {}));
4850 self.view_list.on('edit:before', self, function (e) {
4853 self.view_list.popup = self;
4854 self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
4855 self.view_list.do_show();
4856 }).then(function() {
4857 self.searchview.do_search();
4859 self.view_list.on("list_view_loaded", self, function() {
4860 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
4861 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
4862 $cbutton.click(function() {
4865 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
4866 $sbutton.click(function() {
4867 self.select_elements(self.selected_ids);
4870 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
4871 $cbutton.click(function() {
4876 this.searchview.appendTo($(".oe_popup_search", self.$el));
4878 do_search: function(domains, contexts, groupbys) {
4880 instance.web.pyeval.eval_domains_and_contexts({
4881 domains: domains || [],
4882 contexts: contexts || [],
4883 group_by_seq: groupbys || []
4884 }).done(function (results) {
4885 self.view_list.do_search(results.domain, results.context, results.group_by);
4888 on_click_element: function(ids) {
4890 this.selected_ids = ids || [];
4891 if(this.selected_ids.length > 0) {
4892 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
4894 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
4897 new_object: function() {
4898 if (this.searchview) {
4899 this.searchview.hide();
4901 if (this.view_list) {
4902 this.view_list.do_hide();
4904 this.setup_form_view();
4908 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
4909 do_add_record: function () {
4910 this.popup.new_object();
4912 select_record: function(index) {
4913 this.popup.select_elements([this.dataset.ids[index]]);
4914 this.popup.destroy();
4916 do_select: function(ids, records) {
4917 this._super(ids, records);
4918 this.popup.on_click_element(ids);
4922 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4923 template: 'FieldReference',
4924 init: function(field_manager, node) {
4925 this._super(field_manager, node);
4926 this.reference_ready = true;
4928 destroy_content: function() {
4931 this.fm = undefined;
4934 initialize_content: function() {
4936 var fm = new instance.web.form.DefaultFieldManager(this);
4938 fm.extend_field_desc({
4940 selection: this.field_manager.get_field_desc(this.name).selection,
4948 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
4950 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
4952 this.selection.on("change:value", this, this.on_selection_changed);
4953 this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
4955 .on('focused', null, function () {self.trigger('focused')})
4956 .on('blurred', null, function () {self.trigger('blurred')});
4958 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
4959 name: 'Referenced Document',
4960 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
4962 this.m2o.on("change:value", this, this.data_changed);
4963 this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
4965 .on('focused', null, function () {self.trigger('focused')})
4966 .on('blurred', null, function () {self.trigger('blurred')});
4968 on_selection_changed: function() {
4969 if (this.reference_ready) {
4970 this.internal_set_value([this.selection.get_value(), false]);
4971 this.render_value();
4974 data_changed: function() {
4975 if (this.reference_ready) {
4976 this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
4979 set_value: function(val) {
4981 val = val.split(',');
4982 val[0] = val[0] || false;
4983 val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
4985 this._super(val || [false, false]);
4987 get_value: function() {
4988 return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
4990 render_value: function() {
4991 this.reference_ready = false;
4992 if (!this.get("effective_readonly")) {
4993 this.selection.set_value(this.get('value')[0]);
4995 this.m2o.field.relation = this.get('value')[0];
4996 this.m2o.set_value(this.get('value')[1]);
4997 this.m2o.$el.toggle(!!this.get('value')[0]);
4998 this.reference_ready = true;
5002 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5003 init: function(field_manager, node) {
5005 this._super(field_manager, node);
5006 this.binary_value = false;
5007 this.useFileAPI = !!window.FileReader;
5008 this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5009 if (!this.useFileAPI) {
5010 this.fileupload_id = _.uniqueId('oe_fileupload');
5011 $(window).on(this.fileupload_id, function() {
5012 var args = [].slice.call(arguments).slice(1);
5013 self.on_file_uploaded.apply(self, args);
5018 if (!this.useFileAPI) {
5019 $(window).off(this.fileupload_id);
5021 this._super.apply(this, arguments);
5023 initialize_content: function() {
5024 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5025 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5026 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5028 on_file_change: function(e) {
5030 var file_node = e.target;
5031 if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5032 if (this.useFileAPI) {
5033 var file = file_node.files[0];
5034 if (file.size > this.max_upload_size) {
5035 var msg = _t("The selected file exceed the maximum file size of %s.");
5036 instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5039 var filereader = new FileReader();
5040 filereader.readAsDataURL(file);
5041 filereader.onloadend = function(upload) {
5042 var data = upload.target.result;
5043 data = data.split(',')[1];
5044 self.on_file_uploaded(file.size, file.name, file.type, data);
5047 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5048 this.$el.find('form.oe_form_binary_form').submit();
5050 this.$el.find('.oe_form_binary_progress').show();
5051 this.$el.find('.oe_form_binary').hide();
5054 on_file_uploaded: function(size, name, content_type, file_base64) {
5055 if (size === false) {
5056 this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5057 // TODO: use openerp web crashmanager
5058 console.warn("Error while uploading file : ", name);
5060 this.filename = name;
5061 this.on_file_uploaded_and_valid.apply(this, arguments);
5063 this.$el.find('.oe_form_binary_progress').hide();
5064 this.$el.find('.oe_form_binary').show();
5066 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5068 on_save_as: function(ev) {
5069 var value = this.get('value');
5071 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5072 ev.stopPropagation();
5074 instance.web.blockUI();
5075 var c = instance.webclient.crashmanager;
5076 this.session.get_file({
5077 url: '/web/binary/saveas_ajax',
5078 data: {data: JSON.stringify({
5079 model: this.view.dataset.model,
5080 id: (this.view.datarecord.id || ''),
5082 filename_field: (this.node.attrs.filename || ''),
5083 data: instance.web.form.is_bin_size(value) ? null : value,
5084 context: this.view.dataset.get_context()
5086 complete: instance.web.unblockUI,
5087 error: c.rpc_error.bind(c)
5089 ev.stopPropagation();
5093 set_filename: function(value) {
5094 var filename = this.node.attrs.filename;
5097 tmp[filename] = value;
5098 this.field_manager.set_values(tmp);
5101 on_clear: function() {
5102 if (this.get('value') !== false) {
5103 this.binary_value = false;
5104 this.internal_set_value(false);
5110 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5111 template: 'FieldBinaryFile',
5112 initialize_content: function() {
5114 if (this.get("effective_readonly")) {
5116 this.$el.find('a').click(function(ev) {
5117 if (self.get('value')) {
5118 self.on_save_as(ev);
5124 render_value: function() {
5125 if (!this.get("effective_readonly")) {
5127 if (this.node.attrs.filename) {
5128 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5130 show_value = (this.get('value') != null && this.get('value') !== false) ? this.get('value') : '';
5132 this.$el.find('input').eq(0).val(show_value);
5134 this.$el.find('a').toggle(!!this.get('value'));
5135 if (this.get('value')) {
5136 var show_value = _t("Download")
5138 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5139 this.$el.find('a').text(show_value);
5143 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5144 this.binary_value = true;
5145 this.internal_set_value(file_base64);
5146 var show_value = name + " (" + instance.web.human_size(size) + ")";
5147 this.$el.find('input').eq(0).val(show_value);
5148 this.set_filename(name);
5150 on_clear: function() {
5151 this._super.apply(this, arguments);
5152 this.$el.find('input').eq(0).val('');
5153 this.set_filename('');
5157 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5158 template: 'FieldBinaryImage',
5159 placeholder: "/web/static/src/img/placeholder.png",
5160 render_value: function() {
5163 if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5164 url = 'data:image/png;base64,' + this.get('value');
5165 } else if (this.get('value')) {
5166 var id = JSON.stringify(this.view.datarecord.id || null);
5167 var field = this.name;
5168 if (this.options.preview_image)
5169 field = this.options.preview_image;
5170 url = this.session.url('/web/binary/image', {
5171 model: this.view.dataset.model,
5174 t: (new Date().getTime()),
5177 url = this.placeholder;
5179 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5180 this.$el.find('> img').remove();
5181 this.$el.prepend($img);
5182 $img.load(function() {
5183 if (! self.options.size)
5185 $img.css("max-width", "" + self.options.size[0] + "px");
5186 $img.css("max-height", "" + self.options.size[1] + "px");
5187 $img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px");
5188 $img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px");
5190 $img.on('error', function() {
5191 $img.attr('src', self.placeholder);
5192 instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5195 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5196 this.internal_set_value(file_base64);
5197 this.binary_value = true;
5198 this.render_value();
5199 this.set_filename(name);
5201 on_clear: function() {
5202 this._super.apply(this, arguments);
5203 this.render_value();
5204 this.set_filename('');
5206 set_value: function(value_){
5207 var changed = value_ !== this.get_value();
5208 this._super.apply(this, arguments);
5209 // By default, on binary images read, the server returns the binary size
5210 // This is possible that two images have the exact same size
5211 // Therefore we trigger the change in case the image value hasn't changed
5212 // So the image is re-rendered correctly
5214 this.trigger("change:value", this, {
5223 * Widget for (one2many field) to upload one or more file in same time and display in list.
5224 * The user can delete his files.
5225 * Options on attribute ; "blockui" {Boolean} block the UI or not
5226 * during the file is uploading
5228 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
5229 template: "FieldBinaryFileUploader",
5230 init: function(field_manager, node) {
5231 this._super(field_manager, node);
5232 this.field_manager = field_manager;
5234 if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5235 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);
5237 this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5238 this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5239 $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5243 this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5245 set_value: function(value_) {
5246 var value_ = value_ || [];
5249 _.each(value_, function(command) {
5250 if (isNaN(command) && command.id == undefined) {
5251 switch (command[0]) {
5252 case commands.CREATE:
5253 ids = ids.concat(command[2]);
5255 case commands.REPLACE_WITH:
5256 ids = ids.concat(command[2]);
5258 case commands.UPDATE:
5259 ids = ids.concat(command[2]);
5261 case commands.LINK_TO:
5262 ids = ids.concat(command[1]);
5264 case commands.DELETE:
5265 ids = _.filter(ids, function (id) { return id != command[1];});
5267 case commands.DELETE_ALL:
5277 get_value: function() {
5278 return _.map(this.get('value'), function (value) { return commands.link_to( isNaN(value) ? value.id : value ); });
5280 get_file_url: function (attachment) {
5281 return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5283 read_name_values : function () {
5285 // select the list of id for a get_name
5287 _.each(this.get('value'), function (val) {
5288 if (typeof val != 'object') {
5292 // send request for get_name
5293 if (values.length) {
5294 return this.ds_file.call('read', [values, ['id', 'name', 'datas_fname']]).done(function (datas) {
5295 _.each(datas, function (data) {
5296 data.no_unlink = true;
5297 data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5299 _.each(self.get('value'), function (val, key) {
5300 if(val == data.id) {
5301 self.get('value')[key] = data;
5307 return $.when(this.get('value'));
5310 render_value: function () {
5312 this.read_name_values().then(function (datas) {
5314 var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self}));
5315 render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5316 self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5318 // reinit input type file
5319 var $input = self.$('input.oe_form_binary_file');
5320 $input.after($input.clone(true)).remove();
5321 self.$(".oe_fileupload").show();
5325 on_file_change: function (event) {
5326 event.stopPropagation();
5328 var $target = $(event.target);
5329 if ($target.val() !== '') {
5331 var filename = $target.val().replace(/.*[\\\/]/,'');
5333 // if the files is currently uploded, don't send again
5334 if( !isNaN(_.find(this.get('value'), function (file) { return (file.filename || file.name) == filename && file.upload; } )) ) {
5339 if(this.node.attrs.blockui>0) {
5340 instance.web.blockUI();
5343 // if the files exits for this answer, delete the file before upload
5344 var files = _.filter(this.get('value'), function (file) {
5345 if((file.filename || file.name) == filename) {
5346 self.ds_file.unlink([file.id]);
5353 // TODO : unactivate send on wizard and form
5356 this.$('form.oe_form_binary_form').submit();
5357 this.$(".oe_fileupload").hide();
5359 // add file on result
5363 'filename': filename,
5368 this.set({'value': files});
5371 on_file_loaded: function (event, result) {
5372 var files = this.get('value');
5375 if(this.node.attrs.blockui>0) {
5376 instance.web.unblockUI();
5379 // TODO : activate send on wizard and form
5381 if (result.error || !result.id ) {
5382 this.do_warn( _t('Uploading Error'), result.error);
5383 files = _.filter(files, function (val) { return !val.upload; });
5385 for(var i in files){
5386 if(files[i].filename == result.filename && files[i].upload) {
5389 'name': result.name,
5390 'filename': result.filename,
5391 'url': this.get_file_url(result)
5397 this.set({'value': files});
5400 on_file_delete: function (event) {
5401 event.stopPropagation();
5402 var file_id=$(event.target).data("id");
5405 for(var i in this.get('value')){
5406 if(file_id != this.get('value')[i].id){
5407 files.push(this.get('value')[i]);
5409 else if(!this.get('value')[i].no_unlink) {
5410 this.ds_file.unlink([file_id]);
5413 this.set({'value': files});
5418 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5419 template: "FieldStatus",
5420 init: function(field_manager, node) {
5421 this._super(field_manager, node);
5422 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5423 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5424 this.set({value: false});
5425 this.selection = [];
5426 this.set("selection", []);
5427 this.selection_dm = new instance.web.DropMisordered();
5430 this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5432 this.on("change:value", this, this.get_selection);
5433 this.on("change:evaluated_selection_domain", this, this.get_selection);
5434 this.on("change:selection", this, function() {
5435 this.selection = this.get("selection");
5436 this.render_value();
5438 this.get_selection();
5439 if (this.options.clickable) {
5440 this.$el.on('click','li',this.on_click_stage);
5444 set_value: function(value_) {
5445 if (value_ instanceof Array) {
5448 this._super(value_);
5450 render_value: function() {
5452 var content = QWeb.render("FieldStatus.content", {widget: self});
5453 self.$el.html(content);
5454 var colors = JSON.parse((self.node.attrs || {}).statusbar_colors || "{}");
5455 var color = colors[self.get('value')];
5457 self.$("oe_active").css("color", color);
5460 calc_domain: function() {
5461 var d = instance.web.pyeval.eval('domain', this.build_domain());
5462 var domain = []; //if there is no domain defined, fetch all the records
5465 domain = ['|',['id', '=', this.get('value')]].concat(d);
5468 if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5469 this.set("evaluated_selection_domain", domain);
5472 /** Get the selection and render it
5473 * selection: [[identifier, value_to_display], ...]
5474 * For selection fields: this is directly given by this.field.selection
5475 * For many2one fields: perform a search on the relation of the many2one field
5477 get_selection: function() {
5481 var calculation = _.bind(function() {
5482 if (this.field.type == "many2one") {
5484 var ds = new instance.web.DataSetSearch(this, this.field.relation,
5485 self.build_context(), this.get("evaluated_selection_domain"));
5486 return ds.read_slice(['name'], {}).then(function (records) {
5487 for(var i = 0; i < records.length; i++) {
5488 selection.push([records[i].id, records[i].name]);
5492 // For field type selection filter values according to
5493 // statusbar_visible attribute of the field. For example:
5494 // statusbar_visible="draft,open".
5495 var select = this.field.selection;
5496 for(var i=0; i < select.length; i++) {
5497 var key = select[i][0];
5498 if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5499 selection.push(select[i]);
5505 this.selection_dm.add(calculation()).then(function () {
5506 if (! _.isEqual(selection, self.get("selection"))) {
5507 self.set("selection", selection);
5511 on_click_stage: function (ev) {
5513 var $li = $(ev.currentTarget);
5514 if (this.field.type == "many2one") {
5515 var val = parseInt($li.data("id"));
5518 var val = $li.data("id");
5520 if (val != self.get('value')) {
5521 this.view.recursive_save().done(function() {
5523 change[self.name] = val;
5524 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
5532 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
5533 template: "FieldMonetary",
5534 widget_class: 'oe_form_field_float oe_form_field_monetary',
5536 this._super.apply(this, arguments);
5537 this.set({"currency": false});
5538 if (this.options.currency_field) {
5539 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
5540 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
5543 this.on("change:currency", this, this.get_currency_info);
5544 this.get_currency_info();
5545 this.ci_dm = new instance.web.DropMisordered();
5548 var tmp = this._super();
5549 this.on("change:currency_info", this, this.reinitialize);
5552 get_currency_info: function() {
5554 if (this.get("currency") === false) {
5555 this.set({"currency_info": null});
5558 return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
5559 .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
5560 self.set({"currency_info": res});
5563 parse_value: function(val, def) {
5564 return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5566 format_value: function(val, def) {
5567 return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5572 * Registry of form fields, called by :js:`instance.web.FormView`.
5574 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
5575 * will substitute to the <field> tags as defined in OpenERP's views.
5577 instance.web.form.widgets = new instance.web.Registry({
5578 'char' : 'instance.web.form.FieldChar',
5579 'id' : 'instance.web.form.FieldID',
5580 'email' : 'instance.web.form.FieldEmail',
5581 'url' : 'instance.web.form.FieldUrl',
5582 'text' : 'instance.web.form.FieldText',
5583 'html' : 'instance.web.form.FieldTextHtml',
5584 'date' : 'instance.web.form.FieldDate',
5585 'datetime' : 'instance.web.form.FieldDatetime',
5586 'selection' : 'instance.web.form.FieldSelection',
5587 'many2one' : 'instance.web.form.FieldMany2One',
5588 'many2onebutton' : 'instance.web.form.Many2OneButton',
5589 'many2many' : 'instance.web.form.FieldMany2Many',
5590 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
5591 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
5592 'one2many' : 'instance.web.form.FieldOne2Many',
5593 'one2many_list' : 'instance.web.form.FieldOne2Many',
5594 'reference' : 'instance.web.form.FieldReference',
5595 'boolean' : 'instance.web.form.FieldBoolean',
5596 'float' : 'instance.web.form.FieldFloat',
5597 'integer': 'instance.web.form.FieldFloat',
5598 'float_time': 'instance.web.form.FieldFloat',
5599 'progressbar': 'instance.web.form.FieldProgressBar',
5600 'image': 'instance.web.form.FieldBinaryImage',
5601 'binary': 'instance.web.form.FieldBinaryFile',
5602 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
5603 'statusbar': 'instance.web.form.FieldStatus',
5604 'monetary': 'instance.web.form.FieldMonetary',
5608 * Registry of widgets usable in the form view that can substitute to any possible
5609 * tags defined in OpenERP's form views.
5611 * Every referenced class should extend FormWidget.
5613 instance.web.form.tags = new instance.web.Registry({
5614 'button' : 'instance.web.form.WidgetButton',
5617 instance.web.form.custom_widgets = new instance.web.Registry({
5622 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: