3 var instance = openerp;
4 var _t = instance.web._t,
5 _lt = instance.web._lt;
6 var QWeb = instance.web.qweb;
9 instance.web.form = {};
12 * Interface implemented by the form view or any other object
13 * able to provide the features necessary for the fields to work.
16 * - display_invalid_fields : if true, all fields where is_valid() return true should
17 * be displayed as invalid.
18 * - actual_mode : the current mode of the field manager. Can be "view", "edit" or "create".
20 * - view_content_has_changed : when the values of the fields have changed. When
21 * this event is triggered all fields should reprocess their modifiers.
22 * - field_changed:<field_name> : when the value of a field change, an event is triggered
23 * named "field_changed:<field_name>" with <field_name> replaced by the name of the field.
24 * This event is not related to the on_change mechanism of OpenERP and is always called
25 * when the value of a field is setted or changed. This event is only triggered when the
26 * value of the field is syntactically valid, but it can be triggered when the value
27 * is sematically invalid (ie, when a required field is false). It is possible that an event
28 * about a precise field is never triggered even if that field exists in the view, in that
29 * case the value of the field is assumed to be false.
31 instance.web.form.FieldManagerMixin = {
33 * Must return the asked field as in fields_get.
35 get_field_desc: function(field_name) {},
37 * Returns the current value of a field present in the view. See the get_value() method
38 * method in FieldInterface for further information.
40 get_field_value: function(field_name) {},
42 Gives new values for the fields contained in the view. The new values could not be setted
43 right after the call to this method. Setting new values can trigger on_changes.
45 @param (dict) values A dictonnary with key = field name and value = new value.
46 @return (Deferred) Is resolved after all the values are setted.
48 set_values: function(values) {},
50 Computes an OpenERP domain.
52 @param (list) expression An OpenERP domain.
53 @return (boolean) The computed value of the domain.
55 compute_domain: function(expression) {},
57 Builds an evaluation context for the resolution of the fields' contexts. Please note
58 the field are only supposed to use this context to evualuate their own, they should not
61 @return (CompoundContext) An OpenERP context.
63 build_eval_context: function() {},
66 instance.web.views.add('form', 'instance.web.FormView');
69 * - actual_mode: always "view", "edit" or "create". Read-only property. Determines
70 * the mode used by the view.
72 instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerMixin, {
74 * Indicates that this view is not searchable, and thus that no search
75 * view should be displayed (if there is one active).
79 display_name: _lt('Form'),
82 * @constructs instance.web.FormView
83 * @extends instance.web.View
85 * @param {instance.web.Session} session the current openerp session
86 * @param {instance.web.DataSet} dataset the dataset this view will work with
87 * @param {String} view_id the identifier of the OpenERP view object
88 * @param {Object} options
89 * - resize_textareas : [true|false|max_height]
91 * @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance
93 init: function(parent, dataset, view_id, options) {
96 this.ViewManager = parent;
97 this.set_default_options(options);
98 this.dataset = dataset;
99 this.model = dataset.model;
100 this.view_id = view_id || false;
101 this.fields_view = {};
103 this.fields_order = [];
104 this.datarecord = {};
105 this.default_focus_field = null;
106 this.default_focus_button = null;
107 this.fields_registry = instance.web.form.widgets;
108 this.tags_registry = instance.web.form.tags;
109 this.widgets_registry = instance.web.form.custom_widgets;
110 this.has_been_loaded = $.Deferred();
111 this.translatable_fields = [];
112 _.defaults(this.options, {
113 "not_interactible_on_create": false,
114 "initial_mode": "view",
115 "disable_autofocus": false,
116 "footer_to_buttons": false,
118 this.is_initialized = $.Deferred();
119 this.mutating_mutex = new $.Mutex();
120 this.on_change_list = [];
122 this.reload_mutex = new $.Mutex();
123 this.__clicked_inside = false;
124 this.__blur_timeout = null;
125 this.rendering_engine = new instance.web.form.FormRenderingEngine(this);
126 self.set({actual_mode: self.options.initial_mode});
127 this.has_been_loaded.done(function() {
128 self.on("change:actual_mode", self, self.check_actual_mode);
129 self.check_actual_mode();
130 self.on("change:actual_mode", self, self.init_pager);
133 self.on("load_record", self, self.load_record);
134 instance.web.bus.on('clear_uncommitted_changes', this, function(e) {
135 if (!this.can_be_discarded()) {
140 view_loading: function(r) {
141 return this.load_form(r);
143 destroy: function() {
144 _.each(this.get_widgets(), function(w) {
145 w.off('focused blurred');
149 this.$el.off('.formBlur');
153 load_form: function(data) {
156 throw new Error(_t("No data provided."));
159 throw "Form view does not support multiple calls to load_form";
161 this.fields_order = [];
162 this.fields_view = data;
164 this.rendering_engine.set_fields_registry(this.fields_registry);
165 this.rendering_engine.set_tags_registry(this.tags_registry);
166 this.rendering_engine.set_widgets_registry(this.widgets_registry);
167 this.rendering_engine.set_fields_view(data);
168 var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container');
169 this.rendering_engine.render_to($dest);
171 this.$el.on('mousedown.formBlur', function () {
172 self.__clicked_inside = true;
175 this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self}));
176 if (this.options.$buttons) {
177 this.$buttons.appendTo(this.options.$buttons);
179 this.$el.find('.oe_form_buttons').replaceWith(this.$buttons);
181 this.$buttons.on('click', '.oe_form_button_create',
182 this.guard_active(this.on_button_create));
183 this.$buttons.on('click', '.oe_form_button_edit',
184 this.guard_active(this.on_button_edit));
185 this.$buttons.on('click', '.oe_form_button_save',
186 this.guard_active(this.on_button_save));
187 this.$buttons.on('click', '.oe_form_button_cancel',
188 this.guard_active(this.on_button_cancel));
189 if (this.options.footer_to_buttons) {
190 this.$el.find('footer').appendTo(this.$buttons);
193 this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar');
194 if (!this.sidebar && this.options.$sidebar) {
195 this.sidebar = new instance.web.Sidebar(this);
196 this.sidebar.appendTo(this.$sidebar);
197 if (this.fields_view.toolbar) {
198 this.sidebar.add_toolbar(this.fields_view.toolbar);
200 this.sidebar.add_items('other', _.compact([
201 self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete },
202 self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate }
206 this.has_been_loaded.resolve();
208 // Add bounce effect on button 'Edit' when click on readonly page view.
209 this.$el.find(".oe_form_group_row,.oe_form_field,label,h1,.oe_title,.oe_notebook_page, .oe_list_content").on('click', function (e) {
210 if(self.get("actual_mode") == "view") {
211 var $button = self.options.$buttons.find(".oe_form_button_edit");
212 $button.openerpBounce();
214 instance.web.bus.trigger('click', e);
217 //bounce effect on red button when click on statusbar.
218 this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) {
219 if((self.get("actual_mode") == "view")) {
220 var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'});
221 $button.openerpBounce();
225 this.trigger('form_view_loaded', data);
228 widgetFocused: function() {
229 // Clear click flag if used to focus a widget
230 this.__clicked_inside = false;
231 if (this.__blur_timeout) {
232 clearTimeout(this.__blur_timeout);
233 this.__blur_timeout = null;
236 widgetBlurred: function() {
237 if (this.__clicked_inside) {
238 // clicked in an other section of the form (than the currently
239 // focused widget) => just ignore the blurring entirely?
240 this.__clicked_inside = false;
244 // clear timeout, if any
245 this.widgetFocused();
246 this.__blur_timeout = setTimeout(function () {
247 self.trigger('blurred');
251 do_load_state: function(state, warm) {
252 if (state.id && this.datarecord.id != state.id) {
253 if (this.dataset.get_id_index(state.id) === null) {
254 this.dataset.ids.push(state.id);
256 this.dataset.select_id(state.id);
257 this.do_show({ reload: warm });
262 * @param {Object} [options]
263 * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view".
264 * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record
265 * @return {$.Deferred}
267 do_show: function (options) {
269 options = options || {};
271 this.sidebar.$el.show();
274 this.$buttons.show();
276 this.$el.show().css({
278 filter: 'alpha(opacity = 0)'
280 this.$el.add(this.$buttons).removeClass('oe_form_dirty');
282 var shown = this.has_been_loaded;
283 if (options.reload !== false) {
284 shown = shown.then(function() {
285 if (self.dataset.index === null) {
286 // null index means we should start a new record
287 return self.on_button_new();
289 var fields = _.keys(self.fields_view.fields);
290 fields.push('display_name');
291 return self.dataset.read_index(fields, {
292 context: { 'bin_size': true, 'future_display_name' : true }
293 }).then(function(r) {
294 self.trigger('load_record', r);
298 return shown.then(function() {
299 self._actualize_mode(options.mode || self.options.initial_mode);
302 filter: 'alpha(opacity = 100)'
306 do_hide: function () {
308 this.sidebar.$el.hide();
311 this.$buttons.hide();
318 load_record: function(record) {
319 var self = this, set_values = [];
321 this.set({ 'title' : undefined });
322 this.do_warn(_t("Form"), _t("The record could not be found in the database."), true);
323 return $.Deferred().reject();
325 this.datarecord = record;
326 this._actualize_mode();
327 this.set({ 'title' : record.id ? record.display_name : _t("New") });
329 _(this.fields).each(function (field, f) {
330 field._dirty_flag = false;
331 field._inhibit_on_change_flag = true;
332 var result = field.set_value(self.datarecord[f] || false);
333 field._inhibit_on_change_flag = false;
334 set_values.push(result);
336 return $.when.apply(null, set_values).then(function() {
338 // New record: Second pass in order to trigger the onchanges
339 // respecting the fields order defined in the view
340 _.each(self.fields_order, function(field_name) {
341 if (record[field_name] !== undefined) {
342 var field = self.fields[field_name];
343 field._dirty_flag = true;
344 self.do_onchange(field);
348 self.on_form_changed();
349 self.rendering_engine.init_fields();
350 self.is_initialized.resolve();
351 self.do_update_pager(record.id === null || record.id === undefined);
353 self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
356 self.do_push_state({id:record.id});
358 self.do_push_state({});
360 self.$el.add(self.$buttons).removeClass('oe_form_dirty');
365 * Loads and sets up the default values for the model as the current
368 * @return {$.Deferred}
370 load_defaults: function () {
372 var keys = _.keys(this.fields_view.fields);
374 return this.dataset.default_get(keys).then(function(r) {
375 self.trigger('load_record', r);
378 return self.trigger('load_record', {});
380 on_form_changed: function() {
381 this.trigger("view_content_has_changed");
383 do_notify_change: function() {
384 this.$el.add(this.$buttons).addClass('oe_form_dirty');
386 execute_pager_action: function(action) {
387 if (this.can_be_discarded()) {
390 this.dataset.index = 0;
393 this.dataset.previous();
399 this.dataset.index = this.dataset.ids.length - 1;
402 var def = this.reload();
403 this.trigger('pager_action_executed');
408 init_pager: function() {
411 this.$pager.remove();
412 if (this.get("actual_mode") === "create")
414 this.$pager = $(QWeb.render("FormView.pager", {'widget':self})).hide();
415 if (this.options.$pager) {
416 this.$pager.appendTo(this.options.$pager);
418 this.$el.find('.oe_form_pager').replaceWith(this.$pager);
420 this.$pager.on('click','a[data-pager-action]',function() {
422 if ($el.attr("disabled"))
424 var action = $el.data('pager-action');
425 var def = $.when(self.execute_pager_action(action));
426 $el.attr("disabled");
427 def.always(function() {
428 $el.removeAttr("disabled");
431 this.do_update_pager();
433 do_update_pager: function(hide_index) {
434 this.$pager.toggle(this.dataset.ids.length > 1);
436 $(".oe_form_pager_state", this.$pager).html("");
438 $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
441 parse_on_change: function (on_change, widget) {
443 var onchange = _.str.trim(on_change);
444 var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
446 throw new Error(_.str.sprintf( _t("Wrong on change format: %s"), onchange ));
449 var method = call[1];
450 if (!_.str.trim(call[2])) {
451 return {method: method, args: []};
454 var argument_replacement = {
455 'False': function () {return false;},
456 'True': function () {return true;},
457 'None': function () {return null;},
458 'context': function () {
459 return new instance.web.CompoundContext(
460 self.dataset.get_context(),
461 widget.build_context() ? widget.build_context() : {});
464 var parent_fields = null;
465 var args = _.map(call[2].split(','), function (a, i) {
466 var field = _.str.trim(a);
468 // literal constant or context
469 if (field in argument_replacement) {
470 return argument_replacement[field]();
473 if (/^-?\d+(\.\d+)?$/.test(field)) {
474 return Number(field);
477 if (self.fields[field]) {
478 var value_ = self.fields[field].get_value();
479 return value_ === null || value_ === undefined ? false : value_;
482 var splitted = field.split('.');
483 if (splitted.length > 1 && _.str.trim(splitted[0]) === "parent" && self.dataset.parent_view) {
484 if (parent_fields === null) {
485 parent_fields = self.dataset.parent_view.get_fields_values();
487 var p_val = parent_fields[_.str.trim(splitted[1])];
488 if (p_val !== undefined) {
489 return p_val === null || p_val === undefined ? false : p_val;
493 var first_char = field[0], last_char = field[field.length-1];
494 if ((first_char === '"' && last_char === '"')
495 || (first_char === "'" && last_char === "'")) {
496 return field.slice(1, -1);
499 throw new Error("Could not get field with name '" + field +
500 "' for onchange '" + onchange + "'");
508 do_onchange: function(widget, processed) {
510 this.on_change_list = [{widget: widget, processed: processed}].concat(this.on_change_list);
511 return this._process_operations();
513 _process_onchange: function(on_change_obj) {
515 var widget = on_change_obj.widget;
516 var processed = on_change_obj.processed;
519 processed = processed || [];
520 processed.push(widget.name);
521 var on_change = widget.node.attrs.on_change;
523 var change_spec = self.parse_on_change(on_change, widget);
525 if (self.datarecord.id && !instance.web.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) {
526 // In case of a o2m virtual id, we should pass an empty ids list
527 ids.push(self.datarecord.id);
529 def = self.alive(new instance.web.Model(self.dataset.model).call(
530 change_spec.method, [ids].concat(change_spec.args)));
534 return def.then(function(response) {
535 if (widget.field['change_default']) {
536 var fieldname = widget.name;
538 if (response.value && (fieldname in response.value)) {
539 // Use value from onchange if onchange executed
540 value_ = response.value[fieldname];
542 // otherwise get form value for field
543 value_ = self.fields[fieldname].get_value();
545 var condition = fieldname + '=' + value_;
548 return self.alive(new instance.web.Model('ir.values').call(
549 'get_defaults', [self.model, condition]
550 )).then(function (results) {
551 if (!results.length) {
554 if (!response.value) {
557 for(var i=0; i<results.length; ++i) {
558 // [whatever, key, value]
559 var triplet = results[i];
560 response.value[triplet[1]] = triplet[2];
567 }).then(function(response) {
568 return self.on_processed_onchange(response, processed);
572 instance.webclient.crashmanager.show_message(e);
573 return $.Deferred().reject();
576 on_processed_onchange: function(result, processed) {
578 var fields = this.fields;
579 _(result.domain).each(function (domain, fieldname) {
580 var field = fields[fieldname];
581 if (!field) { return; }
582 field.node.attrs.domain = domain;
586 this._internal_set_values(result.value, processed);
588 if (!_.isEmpty(result.warning)) {
589 new instance.web.Dialog(this, {
591 title:result.warning.title,
593 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
595 }, QWeb.render("CrashManager.warning", result.warning)).open();
598 return $.Deferred().resolve();
601 instance.webclient.crashmanager.show_message(e);
602 return $.Deferred().reject();
605 _process_operations: function() {
607 return this.mutating_mutex.exec(function() {
609 var on_change_obj = self.on_change_list.shift();
611 return self._process_onchange(on_change_obj).then(function() {
616 _.each(self.fields, function(field) {
617 defs.push(field.commit_value());
619 var args = _.toArray(arguments);
620 return $.when.apply($, defs).then(function() {
621 if (self.on_change_list.length !== 0) {
624 var save_obj = self.save_list.pop();
626 return self._process_save(save_obj).then(function() {
627 save_obj.ret = _.toArray(arguments);
630 save_obj.error = true;
635 self.save_list.pop();
642 _internal_set_values: function(values, exclude) {
643 exclude = exclude || [];
644 for (var f in values) {
645 if (!values.hasOwnProperty(f)) { continue; }
646 var field = this.fields[f];
647 // If field is not defined in the view, just ignore it
649 var value_ = values[f];
650 if (field.get_value() != value_) {
651 field._inhibit_on_change_flag = true;
652 field.set_value(value_);
653 field._inhibit_on_change_flag = false;
654 field._dirty_flag = true;
655 if (!_.contains(exclude, field.name)) {
656 this.do_onchange(field, exclude);
661 this.on_form_changed();
663 set_values: function(values) {
665 return this.mutating_mutex.exec(function() {
666 self._internal_set_values(values);
670 * Ask the view to switch to view mode if possible. The view may not do it
671 * if the current record is not yet saved. It will then stay in create mode.
673 to_view_mode: function() {
674 this._actualize_mode("view");
677 * Ask the view to switch to edit mode if possible. The view may not do it
678 * if the current record is not yet saved. It will then stay in create mode.
680 to_edit_mode: function() {
681 this._actualize_mode("edit");
684 * Ask the view to switch to a precise mode if possible. The view is free to
685 * not respect this command if the state of the dataset is not compatible with
686 * the new mode. For example, it is not possible to switch to edit mode if
687 * the current record is not yet saved in database.
689 * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
690 * undefined the view will test the actual mode to check if it is still consistent
691 * with the dataset state.
693 _actualize_mode: function(switch_to) {
694 var mode = switch_to || this.get("actual_mode");
695 if (! this.datarecord.id) {
697 } else if (mode === "create") {
700 this.set({actual_mode: mode});
702 check_actual_mode: function(source, options) {
704 if(this.get("actual_mode") === "view") {
705 self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
706 self.$buttons.find('.oe_form_buttons_edit').hide();
707 self.$buttons.find('.oe_form_buttons_view').show();
708 self.$sidebar.show();
710 self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
711 self.$buttons.find('.oe_form_buttons_edit').show();
712 self.$buttons.find('.oe_form_buttons_view').hide();
713 self.$sidebar.hide();
717 autofocus: function() {
718 if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
719 var fields_order = this.fields_order.slice(0);
720 if (this.default_focus_field) {
721 fields_order.unshift(this.default_focus_field.name);
723 for (var i = 0; i < fields_order.length; i += 1) {
724 var field = this.fields[fields_order[i]];
725 if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
726 if (field.focus() !== false) {
733 on_button_save: function(e) {
735 $(e.target).attr("disabled", true);
736 return this.save().done(function(result) {
737 self.trigger("save", result);
738 self.reload().then(function() {
740 var parent = self.ViewManager.ActionManager.getParent();
742 parent.menu.do_reload_needaction();
745 }).always(function(){
746 $(e.target).attr("disabled", false);
749 on_button_cancel: function(event) {
750 if (this.can_be_discarded()) {
751 if (this.get('actual_mode') === 'create') {
752 this.trigger('history_back');
755 this.trigger('load_record', this.datarecord);
758 this.trigger('on_button_cancel');
761 on_button_new: function() {
764 return $.when(this.has_been_loaded).then(function() {
765 if (self.can_be_discarded()) {
766 return self.load_defaults();
770 on_button_edit: function() {
771 return this.to_edit_mode();
773 on_button_create: function() {
774 this.dataset.index = null;
777 on_button_duplicate: function() {
779 return this.has_been_loaded.then(function() {
780 return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
781 self.record_created(new_id);
786 on_button_delete: function() {
788 var def = $.Deferred();
789 this.has_been_loaded.done(function() {
790 if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
791 self.dataset.unlink([self.datarecord.id]).done(function() {
792 if (self.dataset.size()) {
793 self.execute_pager_action('next');
795 self.do_action('history_back');
800 $.async_when().done(function () {
805 return def.promise();
807 can_be_discarded: function() {
808 if (this.$el.is('.oe_form_dirty')) {
809 if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
812 this.$el.removeClass('oe_form_dirty');
817 * Triggers saving the form's record. Chooses between creating a new
818 * record or saving an existing one depending on whether the record
819 * already has an id property.
821 * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
822 * record, should that record be inserted at the start of the dataset (by
823 * default, records are added at the end)
825 save: function(prepend_on_create) {
827 var save_obj = {prepend_on_create: prepend_on_create, ret: null};
828 this.save_list.push(save_obj);
829 return this._process_operations().then(function() {
831 return $.Deferred().reject();
832 return $.when.apply($, save_obj.ret);
834 self.$el.removeClass('oe_form_dirty');
837 _process_save: function(save_obj) {
839 var prepend_on_create = save_obj.prepend_on_create;
841 var form_invalid = false,
843 first_invalid_field = null,
844 readonly_values = {};
845 for (var f in self.fields) {
846 if (!self.fields.hasOwnProperty(f)) { continue; }
850 if (!first_invalid_field) {
851 first_invalid_field = f;
853 } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
854 // Special case 'id' field, do not save this field
855 // on 'create' : save all non readonly fields
856 // on 'edit' : save non readonly modified fields
857 if (!f.get("readonly")) {
858 values[f.name] = f.get_value();
860 readonly_values[f.name] = f.get_value();
865 self.set({'display_invalid_fields': true});
866 first_invalid_field.focus();
868 return $.Deferred().reject();
870 self.set({'display_invalid_fields': false});
872 if (!self.datarecord.id) {
874 save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
875 return self.record_created(r, prepend_on_create);
877 } else if (_.isEmpty(values)) {
878 // Not dirty, noop save
879 save_deferral = $.Deferred().resolve({}).promise();
882 save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
883 return self.record_saved(r);
886 return save_deferral;
890 return $.Deferred().reject();
893 on_invalid: function() {
894 var warnings = _(this.fields).chain()
895 .filter(function (f) { return !f.is_valid(); })
897 return _.str.sprintf('<li>%s</li>',
900 warnings.unshift('<ul>');
901 warnings.push('</ul>');
902 this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
905 * Reload the form after saving
907 * @param {Object} r result of the write function.
909 record_saved: function(r) {
910 this.trigger('record_saved', r);
912 // should not happen in the server, but may happen for internal purpose
913 return $.Deferred().reject();
918 * Updates the form' dataset to contain the new record:
920 * * Adds the newly created record to the current dataset (at the end by
922 * * Selects that record (sets the dataset's index to point to the new
924 * * Updates the pager and sidebar displays
927 * @param {Boolean} [prepend_on_create=false] adds the newly created record
928 * at the beginning of the dataset instead of the end
930 record_created: function(r, prepend_on_create) {
933 // should not happen in the server, but may happen for internal purpose
934 this.trigger('record_created', r);
935 return $.Deferred().reject();
937 this.datarecord.id = r;
938 if (!prepend_on_create) {
939 this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
940 this.dataset.index = this.dataset.ids.length - 1;
942 this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
943 this.dataset.index = 0;
945 this.do_update_pager();
947 this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
949 //openerp.log("The record has been created with id #" + this.datarecord.id);
950 return $.when(this.reload()).then(function () {
951 self.trigger('record_created', r);
952 return _.extend(r, {created: true});
956 on_action: function (action) {
957 console.debug('Executing action', action);
961 return this.reload_mutex.exec(function() {
962 if (self.dataset.index === null || self.dataset.index === undefined) {
963 self.trigger("previous_view");
964 return $.Deferred().reject().promise();
966 if (self.dataset.index < 0) {
967 return $.when(self.on_button_new());
969 var fields = _.keys(self.fields_view.fields);
970 fields.push('display_name');
971 return self.dataset.read_index(fields,
975 'future_display_name': true
977 check_access_rule: true
978 }).then(function(r) {
979 self.trigger('load_record', r);
981 self.do_action('history_back');
986 get_widgets: function() {
987 return _.filter(this.getChildren(), function(obj) {
988 return obj instanceof instance.web.form.FormWidget;
991 get_fields_values: function() {
993 var ids = this.get_selected_ids();
994 values["id"] = ids.length > 0 ? ids[0] : false;
995 _.each(this.fields, function(value_, key) {
996 values[key] = value_.get_value();
1000 get_selected_ids: function() {
1001 var id = this.dataset.ids[this.dataset.index];
1002 return id ? [id] : [];
1004 recursive_save: function() {
1006 return $.when(this.save()).then(function(res) {
1007 if (self.dataset.parent_view)
1008 return self.dataset.parent_view.recursive_save();
1011 recursive_reload: function() {
1014 if (self.dataset.parent_view)
1015 pre = self.dataset.parent_view.recursive_reload();
1016 return pre.then(function() {
1017 return self.reload();
1020 is_dirty: function() {
1021 return _.any(this.fields, function (value_) {
1022 return value_._dirty_flag;
1025 is_interactible_record: function() {
1026 var id = this.datarecord.id;
1028 if (this.options.not_interactible_on_create)
1030 } else if (typeof(id) === "string") {
1031 if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
1036 sidebar_eval_context: function () {
1037 return $.when(this.build_eval_context());
1039 open_defaults_dialog: function () {
1041 var display = function (field, value) {
1042 if (!value) { return value; }
1043 if (field instanceof instance.web.form.FieldSelection) {
1044 return _(field.get('values')).find(function (option) {
1045 return option[0] === value;
1047 } else if (field instanceof instance.web.form.FieldMany2One) {
1048 return field.get_displayed();
1052 var fields = _.chain(this.fields)
1053 .map(function (field) {
1054 var value = field.get_value();
1055 // ignore fields which are empty, invisible, readonly, o2m
1058 || field.get('invisible')
1059 || field.get("readonly")
1060 || field.field.type === 'one2many'
1061 || field.field.type === 'many2many'
1062 || field.field.type === 'binary'
1063 || field.password) {
1069 string: field.string,
1071 displayed: display(field, value),
1075 .sortBy(function (field) { return field.string; })
1077 var conditions = _.chain(self.fields)
1078 .filter(function (field) { return field.field.change_default; })
1079 .map(function (field) {
1080 var value = field.get_value();
1083 string: field.string,
1085 displayed: display(field, value),
1089 var d = new instance.web.Dialog(this, {
1090 title: _t("Set Default"),
1093 conditions: conditions
1096 {text: _t("Close"), click: function () { d.close(); }},
1097 {text: _t("Save default"), click: function () {
1098 var $defaults = d.$el.find('#formview_default_fields');
1099 var field_to_set = $defaults.val();
1100 if (!field_to_set) {
1101 $defaults.parent().addClass('oe_form_invalid');
1104 var condition = d.$el.find('#formview_default_conditions').val(),
1105 all_users = d.$el.find('#formview_default_all').is(':checked');
1106 new instance.web.DataSet(self, 'ir.values').call(
1110 self.fields[field_to_set].get_value(),
1114 ]).done(function () { d.close(); });
1118 d.template = 'FormView.set_default';
1121 register_field: function(field, name) {
1122 this.fields[name] = field;
1123 this.fields_order.push(name);
1124 if (JSON.parse(field.node.attrs.default_focus || "0")) {
1125 this.default_focus_field = field;
1128 field.on('focused', null, this.proxy('widgetFocused'))
1129 .on('blurred', null, this.proxy('widgetBlurred'));
1130 if (this.get_field_desc(name).translate) {
1131 this.translatable_fields.push(field);
1133 field.on('changed_value', this, function() {
1134 if (field.is_syntax_valid()) {
1135 this.trigger('field_changed:' + name);
1137 if (field._inhibit_on_change_flag) {
1140 field._dirty_flag = true;
1141 if (field.is_syntax_valid()) {
1142 this.do_onchange(field);
1143 this.on_form_changed(true);
1144 this.do_notify_change();
1148 get_field_desc: function(field_name) {
1149 return this.fields_view.fields[field_name];
1151 get_field_value: function(field_name) {
1152 return this.fields[field_name].get_value();
1154 compute_domain: function(expression) {
1155 return instance.web.form.compute_domain(expression, this.fields);
1157 _build_view_fields_values: function() {
1158 var a_dataset = this.dataset;
1159 var fields_values = this.get_fields_values();
1160 var active_id = a_dataset.ids[a_dataset.index];
1161 _.extend(fields_values, {
1162 active_id: active_id || false,
1163 active_ids: active_id ? [active_id] : [],
1164 active_model: a_dataset.model,
1167 if (a_dataset.parent_view) {
1168 fields_values.parent = a_dataset.parent_view.get_fields_values();
1170 return fields_values;
1172 build_eval_context: function() {
1173 var a_dataset = this.dataset;
1174 return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
1179 * Interface to be implemented by rendering engines for the form view.
1181 instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
1182 set_fields_view: function(fields_view) {},
1183 set_fields_registry: function(fields_registry) {},
1184 render_to: function($el) {},
1188 * Default rendering engine for the form view.
1190 * It is necessary to set the view using set_view() before usage.
1192 instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
1193 init: function(view) {
1196 set_fields_view: function(fvg) {
1198 this.version = parseFloat(this.fvg.arch.attrs.version);
1199 if (isNaN(this.version)) {
1203 set_tags_registry: function(tags_registry) {
1204 this.tags_registry = tags_registry;
1206 set_fields_registry: function(fields_registry) {
1207 this.fields_registry = fields_registry;
1209 set_widgets_registry: function(widgets_registry) {
1210 this.widgets_registry = widgets_registry;
1212 // Backward compatibility tools, current default version: v6.1
1213 process_version: function() {
1214 if (this.version < 7.0) {
1215 this.$form.find('form:first').wrapInner('<group col="4"/>');
1216 this.$form.find('page').each(function() {
1217 if (!$(this).parents('field').length) {
1218 $(this).wrapInner('<group col="4"/>');
1223 get_arch_fragment: function() {
1224 var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
1225 // IE won't allow custom button@type and will revert it to spec default : 'submit'
1226 $('button', doc).each(function() {
1227 $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
1229 // IE's html parser is also a css parser. How convenient...
1230 $('board', doc).each(function() {
1231 $(this).attr('layout', $(this).attr('style'));
1233 return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
1235 render_to: function($target) {
1237 this.$target = $target;
1239 this.$form = this.get_arch_fragment();
1241 this.process_version();
1243 this.fields_to_init = [];
1244 this.tags_to_init = [];
1245 this.widgets_to_init = [];
1247 this.process(this.$form);
1249 this.$form.appendTo(this.$target);
1251 this.to_replace = [];
1253 _.each(this.fields_to_init, function($elem) {
1254 var name = $elem.attr("name");
1255 if (!self.fvg.fields[name]) {
1256 throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
1258 var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
1260 throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
1262 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1263 var $label = self.labels[$elem.attr("name")];
1265 w.set_input_id($label.attr("for"));
1267 self.alter_field(w);
1268 self.view.register_field(w, $elem.attr("name"));
1269 self.to_replace.push([w, $elem]);
1271 _.each(this.tags_to_init, function($elem) {
1272 var tag_name = $elem[0].tagName.toLowerCase();
1273 var obj = self.tags_registry.get_object(tag_name);
1274 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1275 self.to_replace.push([w, $elem]);
1277 _.each(this.widgets_to_init, function($elem) {
1278 var widget_type = $elem.attr("type");
1279 var obj = self.widgets_registry.get_object(widget_type);
1280 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1281 self.to_replace.push([w, $elem]);
1284 init_fields: function() {
1286 _.each(this.to_replace, function(el) {
1287 defs.push(el[0].replace(el[1]));
1288 if (el[1].children().length) {
1289 el[0].$el.append(el[1].children());
1292 this.to_replace = [];
1293 return $.when.apply($, defs);
1295 render_element: function(template /* dictionaries */) {
1296 var dicts = [].slice.call(arguments).slice(1);
1297 var dict = _.extend.apply(_, dicts);
1298 dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
1299 return $(QWeb.render(template, dict));
1301 alter_field: function(field) {
1303 toggle_layout_debugging: function() {
1304 if (!this.$target.has('.oe_layout_debug_cell:first').length) {
1305 this.$target.find('[title]').removeAttr('title');
1306 this.$target.find('.oe_form_group_cell').each(function() {
1307 var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
1308 $(this).attr('title', text);
1311 this.$target.toggleClass('oe_layout_debugging');
1313 process: function($tag) {
1315 var tagname = $tag[0].nodeName.toLowerCase();
1316 if (this.tags_registry.contains(tagname)) {
1317 this.tags_to_init.push($tag);
1318 return (tagname === 'button') ? this.process_button($tag) : $tag;
1320 var fn = self['process_' + tagname];
1322 var args = [].slice.call(arguments);
1324 return fn.apply(self, args);
1326 // generic tag handling, just process children
1327 $tag.children().each(function() {
1328 self.process($(this));
1330 self.handle_common_properties($tag, $tag);
1331 $tag.removeAttr("modifiers");
1335 process_button: function ($button) {
1337 $button.children().each(function() {
1338 self.process($(this));
1342 process_widget: function($widget) {
1343 this.widgets_to_init.push($widget);
1346 process_sheet: function($sheet) {
1347 var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
1348 this.handle_common_properties($new_sheet, $sheet);
1349 var $dst = $new_sheet.find('.oe_form_sheet');
1350 $sheet.contents().appendTo($dst);
1351 $sheet.before($new_sheet).remove();
1352 this.process($new_sheet);
1354 process_form: function($form) {
1355 if ($form.find('> sheet').length === 0) {
1356 $form.addClass('oe_form_nosheet');
1358 var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
1359 this.handle_common_properties($new_form, $form);
1360 $form.contents().appendTo($new_form);
1361 if ($form[0] === this.$form[0]) {
1362 // If root element, replace it
1363 this.$form = $new_form;
1365 $form.before($new_form).remove();
1367 this.process($new_form);
1370 * Used by direct <field> children of a <group> tag only
1371 * This method will add the implicit <label...> for every field
1374 preprocess_field: function($field) {
1376 var name = $field.attr('name'),
1377 field_colspan = parseInt($field.attr('colspan'), 10),
1378 field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
1380 if ($field.attr('nolabel') === '1')
1382 $field.attr('nolabel', '1');
1384 this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
1385 $(el).parents().each(function(unused, tag) {
1386 var name = tag.tagName.toLowerCase();
1387 if (name === "field" || name in self.tags_registry.map)
1394 var $label = $('<label/>').attr({
1396 "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
1397 "string": $field.attr('string'),
1398 "help": $field.attr('help'),
1399 "class": $field.attr('class'),
1401 $label.insertBefore($field);
1402 if (field_colspan > 1) {
1403 $field.attr('colspan', field_colspan - 1);
1407 process_field: function($field) {
1408 if ($field.parent().is('group')) {
1409 // No implicit labels for normal fields, only for <group> direct children
1410 var $label = this.preprocess_field($field);
1412 this.process($label);
1415 this.fields_to_init.push($field);
1418 process_group: function($group) {
1420 $group.children('field').each(function() {
1421 self.preprocess_field($(this));
1423 var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
1425 if ($new_group.first().is('table.oe_form_group')) {
1426 $table = $new_group;
1427 } else if ($new_group.filter('table.oe_form_group').length) {
1428 $table = $new_group.filter('table.oe_form_group').first();
1430 $table = $new_group.find('table.oe_form_group').first();
1434 cols = parseInt($group.attr('col') || 2, 10),
1438 $group.children().each(function(a,b,c) {
1439 var $child = $(this);
1440 var colspan = parseInt($child.attr('colspan') || 1, 10);
1441 var tagName = $child[0].tagName.toLowerCase();
1442 var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
1443 var newline = tagName === 'newline';
1445 // Note FME: those classes are used in layout debug mode
1446 if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
1447 $tr.addClass('oe_form_group_row_incomplete');
1449 $tr.addClass('oe_form_group_row_newline');
1456 if (!$tr || row_cols < colspan) {
1457 $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
1459 } else if (tagName==='group') {
1460 // When <group> <group/><group/> </group>, we need a spacing between the two groups
1461 $td.addClass('oe_group_right');
1463 row_cols -= colspan;
1465 // invisibility transfer
1466 var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
1467 var invisible = field_modifiers.invisible;
1468 self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
1470 $tr.append($td.append($child));
1471 children.push($child[0]);
1473 if (row_cols && $td) {
1474 $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
1476 $group.before($new_group).remove();
1478 $table.find('> tbody > tr').each(function() {
1479 var to_compute = [],
1482 $(this).children().each(function() {
1484 $child = $td.children(':first');
1485 if ($child.attr('cell-class')) {
1486 $td.addClass($child.attr('cell-class'));
1488 switch ($child[0].tagName.toLowerCase()) {
1492 if ($child.attr('for')) {
1493 $td.attr('width', '1%').addClass('oe_form_group_cell_label');
1494 row_cols-= $td.attr('colspan') || 1;
1499 var width = _.str.trim($child.attr('width') || ''),
1500 iwidth = parseInt(width, 10);
1502 if (width.substr(-1) === '%') {
1504 width = iwidth + '%';
1507 $td.css('min-width', width + 'px');
1509 $td.attr('width', width);
1510 $child.removeAttr('width');
1511 row_cols-= $td.attr('colspan') || 1;
1513 to_compute.push($td);
1519 var unit = Math.floor(total / row_cols);
1520 if (!$(this).is('.oe_form_group_row_incomplete')) {
1521 _.each(to_compute, function($td, i) {
1522 var width = parseInt($td.attr('colspan'), 10) * unit;
1523 $td.attr('width', width + '%');
1529 _.each(children, function(el) {
1530 self.process($(el));
1532 this.handle_common_properties($new_group, $group);
1535 process_notebook: function($notebook) {
1538 $notebook.find('> page').each(function() {
1539 var $page = $(this);
1540 var page_attrs = $page.getAttributes();
1541 page_attrs.id = _.uniqueId('notebook_page_');
1542 var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
1543 $page.contents().appendTo($new_page);
1544 $page.before($new_page).remove();
1545 var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
1546 page_attrs.__page = $new_page;
1547 page_attrs.__ic = ic;
1548 pages.push(page_attrs);
1550 $new_page.children().each(function() {
1551 self.process($(this));
1554 var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
1555 $notebook.contents().appendTo($new_notebook);
1556 $notebook.before($new_notebook).remove();
1557 self.process($($new_notebook.children()[0]));
1558 //tabs and invisibility handling
1559 $new_notebook.tabs();
1560 _.each(pages, function(page, i) {
1563 page.__ic.on("change:effective_invisible", null, function() {
1564 if (!page.__ic.get('effective_invisible') && page.autofocus) {
1565 $new_notebook.tabs('select', i);
1568 var current = $new_notebook.tabs("option", "selected");
1569 if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
1571 var first_visible = _.find(_.range(pages.length), function(i2) {
1572 return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
1574 if (first_visible !== undefined) {
1575 $new_notebook.tabs('select', first_visible);
1580 this.handle_common_properties($new_notebook, $notebook);
1581 return $new_notebook;
1583 process_separator: function($separator) {
1584 var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
1585 $separator.before($new_separator).remove();
1586 this.handle_common_properties($new_separator, $separator);
1587 return $new_separator;
1589 process_label: function($label) {
1590 var name = $label.attr("for"),
1591 field_orm = this.fvg.fields[name];
1593 string: $label.attr('string') || (field_orm || {}).string || '',
1594 help: $label.attr('help') || (field_orm || {}).help || '',
1595 _for: name ? _.uniqueId('oe-field-input-') : undefined,
1597 var align = parseFloat(dict.align);
1598 if (isNaN(align) || align === 1) {
1600 } else if (align === 0) {
1606 var $new_label = this.render_element('FormRenderingLabel', dict);
1607 $label.before($new_label).remove();
1608 this.handle_common_properties($new_label, $label);
1610 this.labels[name] = $new_label;
1614 handle_common_properties: function($new_element, $node) {
1615 var str_modifiers = $node.attr("modifiers") || "{}";
1616 var modifiers = JSON.parse(str_modifiers);
1618 if (modifiers.invisible !== undefined)
1619 ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
1620 $new_element.addClass($node.attr("class") || "");
1621 $new_element.attr('style', $node.attr('style'));
1622 return {invisibility_changer: ic,};
1629 If you read this documentation, it probably means that you were asked to use a form view widget outside of
1630 a form view. Before going further, you must understand that those fields were never really created for
1631 that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
1632 you to hack the system with more style.
1634 instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
1635 init: function(parent, eval_context) {
1636 this._super(parent);
1637 this.field_descs = {};
1638 this.eval_context = eval_context || {};
1640 display_invalid_fields: false,
1641 actual_mode: 'create',
1644 get_field_desc: function(field_name) {
1645 if (this.field_descs[field_name] === undefined) {
1646 this.field_descs[field_name] = {
1650 return this.field_descs[field_name];
1652 extend_field_desc: function(fields) {
1654 _.each(fields, function(v, k) {
1655 _.extend(self.get_field_desc(k), v);
1658 get_field_value: function(field_name) {
1661 set_values: function(values) {
1664 compute_domain: function(expression) {
1665 return instance.web.form.compute_domain(expression, {});
1667 build_eval_context: function() {
1668 return new instance.web.CompoundContext(this.eval_context);
1672 instance.web.form.compute_domain = function(expr, fields) {
1673 if (! (expr instanceof Array))
1676 for (var i = expr.length - 1; i >= 0; i--) {
1678 if (ex.length == 1) {
1679 var top = stack.pop();
1682 stack.push(stack.pop() || top);
1685 stack.push(stack.pop() && top);
1691 throw new Error(_.str.sprintf(
1692 _t("Unknown operator %s in domain %s"),
1693 ex, JSON.stringify(expr)));
1697 var field = fields[ex[0]];
1699 throw new Error(_.str.sprintf(
1700 _t("Unknown field %s in domain %s"),
1701 ex[0], JSON.stringify(expr)));
1703 var field_value = field.get_value ? field.get_value() : field.value;
1707 switch (op.toLowerCase()) {
1710 stack.push(_.isEqual(field_value, val));
1714 stack.push(!_.isEqual(field_value, val));
1717 stack.push(field_value < val);
1720 stack.push(field_value > val);
1723 stack.push(field_value <= val);
1726 stack.push(field_value >= val);
1729 if (!_.isArray(val)) val = [val];
1730 stack.push(_(val).contains(field_value));
1733 if (!_.isArray(val)) val = [val];
1734 stack.push(!_(val).contains(field_value));
1738 _t("Unsupported operator %s in domain %s"),
1739 op, JSON.stringify(expr));
1742 return _.all(stack, _.identity);
1745 instance.web.form.is_bin_size = function(v) {
1746 return (/^\d+(\.\d*)? \w+$/).test(v);
1750 * Must be applied over an class already possessing the PropertiesMixin.
1752 * Apply the result of the "invisible" domain to this.$el.
1754 instance.web.form.InvisibilityChangerMixin = {
1755 init: function(field_manager, invisible_domain) {
1757 this._ic_field_manager = field_manager;
1758 this._ic_invisible_modifier = invisible_domain;
1759 this._ic_field_manager.on("view_content_has_changed", this, function() {
1760 var result = self._ic_invisible_modifier === undefined ? false :
1761 self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
1762 self.set({"invisible": result});
1764 this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
1765 var check = function() {
1766 if (self.get("invisible") || self.get('force_invisible')) {
1767 self.set({"effective_invisible": true});
1769 self.set({"effective_invisible": false});
1772 this.on('change:invisible', this, check);
1773 this.on('change:force_invisible', this, check);
1777 this.on("change:effective_invisible", this, this._check_visibility);
1778 this._check_visibility();
1780 _check_visibility: function() {
1781 this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
1785 instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
1786 init: function(parent, field_manager, invisible_domain, $el) {
1787 this.setParent(parent);
1788 instance.web.PropertiesMixin.init.call(this);
1789 instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
1796 Base class for all fields, custom widgets and buttons to be displayed in the form view.
1799 - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
1800 the values of the "readonly" property and the "mode" property on the field manager.
1802 instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
1804 * @constructs instance.web.form.FormWidget
1805 * @extends instance.web.Widget
1807 * @param field_manager
1810 init: function(field_manager, node) {
1811 this._super(field_manager);
1812 this.field_manager = field_manager;
1813 if (this.field_manager instanceof instance.web.FormView)
1814 this.view = this.field_manager;
1816 this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
1817 instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
1819 this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
1825 // some events to make the property "effective_readonly" sync automatically with "readonly" and
1826 // "mode" on field_manager
1828 var test_effective_readonly = function() {
1829 self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
1831 this.on("change:readonly", this, test_effective_readonly);
1832 this.field_manager.on("change:actual_mode", this, test_effective_readonly);
1833 test_effective_readonly.call(this);
1835 renderElement: function() {
1836 this.process_modifiers();
1838 this.$el.addClass(this.node.attrs["class"] || "");
1840 destroy: function() {
1841 $.fn.tooltip('destroy');
1842 this._super.apply(this, arguments);
1845 * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
1847 * This method is an utility method that is meant to be called by child classes.
1849 * @param {jQuery} $e jQuery object of elements to bind focus/blur on
1851 setupFocus: function ($e) {
1854 focus: function () { self.trigger('focused'); },
1855 blur: function () { self.trigger('blurred'); }
1858 process_modifiers: function() {
1860 for (var a in this.modifiers) {
1861 if (!this.modifiers.hasOwnProperty(a)) { continue; }
1862 if (!_.include(["invisible"], a)) {
1863 var val = this.field_manager.compute_domain(this.modifiers[a]);
1869 do_attach_tooltip: function(widget, trigger, options) {
1870 widget = widget || this;
1871 trigger = trigger || this.$el;
1872 var container = 'body';
1873 /*TODO: need to be refactor
1874 in the case we can find the view form in the parent,
1875 attach the element to it (to prevent tooltip to keep showing
1876 when switching view) or if we have a modal currently showing,
1877 attach tooltip to the modal to prevent the tooltip to show in the body in the
1878 case we close the modal too fast*/
1879 if ($(trigger).parents('.oe_view_manager_view_form').length > 0){
1880 container = $(trigger).parents('.oe_view_manager_view_form');
1883 if (window.$('.modal.in').length>0){
1884 container = window.$('.modal.in:last()');
1887 options = _.extend({
1888 delay: { show: 500, hide: 0 },
1890 container: container,
1892 var template = widget.template + '.tooltip';
1893 if (!QWeb.has_template(template)) {
1894 template = 'WidgetLabel.tooltip';
1896 return QWeb.render(template, {
1897 debug: instance.session.debug,
1902 //only show tooltip if we are in debug or if we have a help to show, otherwise it will display
1904 if (instance.session.debug || widget.node.attrs.help || (widget.field && widget.field.help)){
1905 $(trigger).tooltip(options);
1909 * Builds a new context usable for operations related to fields by merging
1910 * the fields'context with the action's context.
1912 build_context: function() {
1913 // only use the model's context if there is not context on the node
1914 var v_context = this.node.attrs.context;
1916 v_context = (this.field || {}).context || {};
1919 if (v_context.__ref || true) { //TODO: remove true
1920 var fields_values = this.field_manager.build_eval_context();
1921 v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
1925 build_domain: function() {
1926 var f_domain = this.field.domain || [];
1927 var n_domain = this.node.attrs.domain || null;
1928 // if there is a domain on the node, overrides the model's domain
1929 var final_domain = n_domain !== null ? n_domain : f_domain;
1930 if (!(final_domain instanceof Array) || true) { //TODO: remove true
1931 var fields_values = this.field_manager.build_eval_context();
1932 final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
1934 return final_domain;
1938 instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
1939 template: 'WidgetButton',
1940 init: function(field_manager, node) {
1941 node.attrs.type = node.attrs['data-button-type'];
1942 this.is_stat_button = /\boe_stat_button\b/.test(node.attrs['class']);
1943 this.icon_class = node.attrs.icon && "stat_button_icon fa " + node.attrs.icon + " fa-fw";
1944 this._super(field_manager, node);
1945 this.force_disabled = false;
1946 this.string = (this.node.attrs.string || '').replace(/_/g, '');
1947 if (JSON.parse(this.node.attrs.default_focus || "0")) {
1948 // TODO fme: provide enter key binding to widgets
1949 this.view.default_focus_button = this;
1951 if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
1952 this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
1956 this._super.apply(this, arguments);
1957 this.view.on('view_content_has_changed', this, this.check_disable);
1958 this.check_disable();
1959 this.$el.click(this.on_click);
1960 if (this.node.attrs.help || instance.session.debug) {
1961 this.do_attach_tooltip();
1963 this.setupFocus(this.$el);
1965 on_click: function() {
1967 this.force_disabled = true;
1968 this.check_disable();
1969 this.execute_action().always(function() {
1970 self.force_disabled = false;
1971 self.check_disable();
1974 execute_action: function() {
1976 var exec_action = function() {
1977 if (self.node.attrs.confirm) {
1978 var def = $.Deferred();
1979 var dialog = new instance.web.Dialog(this, {
1980 title: _t('Confirm'),
1982 {text: _t("Cancel"), click: function() {
1983 this.parents('.modal').modal('hide');
1986 {text: _t("Ok"), click: function() {
1988 self.on_confirmed().always(function() {
1989 self2.parents('.modal').modal('hide');
1994 }, $('<div/>').text(self.node.attrs.confirm)).open();
1995 dialog.on("closing", null, function() {def.resolve();});
1996 return def.promise();
1998 return self.on_confirmed();
2001 if (!this.node.attrs.special) {
2002 return this.view.recursive_save().then(exec_action);
2004 return exec_action();
2007 on_confirmed: function() {
2010 var context = this.build_context();
2011 return this.view.do_execute_action(
2012 _.extend({}, this.node.attrs, {context: context}),
2013 this.view.dataset, this.view.datarecord.id, function (reason) {
2014 if (!_.isObject(reason)) {
2015 self.view.recursive_reload();
2019 check_disable: function() {
2020 var disabled = (this.force_disabled || !this.view.is_interactible_record());
2021 this.$el.prop('disabled', disabled);
2022 this.$el.css('color', disabled ? 'grey' : '');
2027 * Interface to be implemented by fields.
2030 * - changed_value: triggered when the value of the field has changed. This can be due
2031 * to a user interaction or a call to set_value().
2034 instance.web.form.FieldInterface = {
2036 * Constructor takes 2 arguments:
2037 * - field_manager: Implements FieldManagerMixin
2038 * - node: the "<field>" node in json form
2040 init: function(field_manager, node) {},
2042 * Called by the form view to indicate the value of the field.
2044 * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
2045 * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
2046 * before the widget is inserted into the DOM.
2048 * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
2049 * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
2050 * also be able to handle any other format commonly used in the _defaults key on the models in the addons
2051 * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
2052 * no information is ever given to know which format is used.
2054 set_value: function(value_) {},
2056 * Get the current value of the widget.
2058 * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
2059 * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
2060 * For example if the field is marked as "required", a call to get_value() can return false.
2062 * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
2063 * return a default value according to the type of field.
2065 * This method is always assumed to perform synchronously, it can not return a promise.
2067 * If there was no user interaction to modify the value of the field, it is always assumed that
2068 * get_value() return the same semantic value than the one passed in the last call to set_value(),
2069 * although the syntax can be different. This can be the case for type of fields that have a different
2070 * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
2072 get_value: function() {},
2074 * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
2077 set_input_id: function(id) {},
2079 * Returns true if is_syntax_valid() returns true and the value is semantically
2080 * valid too according to the semantic restrictions applied to the field.
2082 is_valid: function() {},
2084 * Returns true if the field holds a value which is syntactically correct, ignoring
2085 * the potential semantic restrictions applied to the field.
2087 is_syntax_valid: function() {},
2089 * Must set the focus on the field. Return false if field is not focusable.
2091 focus: function() {},
2093 * Called when the translate button is clicked.
2095 on_translate: function() {},
2097 This method is called by the form view before reading on_change values and before saving. It tells
2098 the field to save its value before reading it using get_value(). Must return a promise.
2100 commit_value: function() {},
2104 * Abstract class for classes implementing FieldInterface.
2107 * - value: useful property to hold the value of the field. By default, set_value() and get_value()
2108 * set and retrieve the value property. Changing the value property also triggers automatically
2109 * a 'changed_value' event that inform the view to trigger on_changes.
2112 instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
2114 * @constructs instance.web.form.AbstractField
2115 * @extends instance.web.form.FormWidget
2117 * @param field_manager
2120 init: function(field_manager, node) {
2122 this._super(field_manager, node);
2123 this.name = this.node.attrs.name;
2124 this.field = this.field_manager.get_field_desc(this.name);
2125 this.widget = this.node.attrs.widget;
2126 this.string = this.node.attrs.string || this.field.string || this.name;
2127 this.options = instance.web.py_eval(this.node.attrs.options || '{}');
2128 this.set({'value': false});
2130 this.on("change:value", this, function() {
2131 this.trigger('changed_value');
2132 this._check_css_flags();
2135 renderElement: function() {
2138 if (this.field.translate && this.view) {
2139 this.$el.addClass('oe_form_field_translatable');
2140 this.$el.find('.oe_field_translate').click(this.on_translate);
2142 this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
2143 this.do_attach_tooltip(this, this.$label[0] || this.$el);
2144 if (instance.session.debug) {
2145 this.$label.off('dblclick').on('dblclick', function() {
2146 console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
2148 console.log("window.w =", window.w);
2151 if (!this.disable_utility_classes) {
2152 this.off("change:required", this, this._set_required);
2153 this.on("change:required", this, this._set_required);
2154 this._set_required();
2156 this._check_visibility();
2157 this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
2158 this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
2159 this._check_css_flags();
2162 var tmp = this._super();
2163 this.on("change:value", this, function() {
2164 if (! this.no_rerender)
2165 this.render_value();
2167 this.render_value();
2170 * Private. Do not use.
2172 _set_required: function() {
2173 this.$el.toggleClass('oe_form_required', this.get("required"));
2175 set_value: function(value_) {
2176 this.set({'value': value_});
2178 get_value: function() {
2179 return this.get('value');
2182 Utility method that all implementations should use to change the
2183 value without triggering a re-rendering.
2185 internal_set_value: function(value_) {
2186 var tmp = this.no_rerender;
2187 this.no_rerender = true;
2188 this.set({'value': value_});
2189 this.no_rerender = tmp;
2192 This method is called each time the value is modified.
2194 render_value: function() {},
2195 is_valid: function() {
2196 return this.is_syntax_valid() && !(this.get('required') && this.is_false());
2198 is_syntax_valid: function() {
2202 * Method useful to implement to ease validity testing. Must return true if the current
2203 * value is similar to false in OpenERP.
2205 is_false: function() {
2206 return this.get('value') === false;
2208 _check_css_flags: function() {
2209 if (this.field.translate) {
2210 this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
2212 if (!this.disable_utility_classes) {
2213 if (this.field_manager.get('display_invalid_fields')) {
2214 this.$el.toggleClass('oe_form_invalid', !this.is_valid());
2221 set_input_id: function(id) {
2222 this.id_for_label = id;
2224 on_translate: function() {
2226 var trans = new instance.web.DataSet(this, 'ir.translation');
2227 return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
2232 set_dimensions: function (height, width) {
2238 commit_value: function() {
2244 * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
2247 instance.web.form.ReinitializeWidgetMixin = {
2249 * Default implementation of, you should not override it, use initialize_field() instead.
2252 this.initialize_field();
2255 initialize_field: function() {
2256 this.on("change:effective_readonly", this, this.reinitialize);
2257 this.initialize_content();
2259 reinitialize: function() {
2260 this.destroy_content();
2261 this.renderElement();
2262 this.initialize_content();
2265 * Called to destroy anything that could have been created previously, called before a
2266 * re-initialization.
2268 destroy_content: function() {},
2270 * Called to initialize the content.
2272 initialize_content: function() {},
2276 * A mixin to apply on any field that has to completely re-render when its readonly state
2279 instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
2280 reinitialize: function() {
2281 instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
2282 this.render_value();
2287 Some hack to make placeholders work in ie9.
2289 if (!('placeholder' in document.createElement('input'))) {
2290 document.addEventListener("DOMNodeInserted",function(event){
2291 var nodename = event.target.nodeName.toLowerCase();
2292 if ( nodename === "input" || nodename == "textarea" ) {
2293 $(event.target).placeholder();
2298 instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2299 template: 'FieldChar',
2300 widget_class: 'oe_form_field_char',
2302 'change input': 'store_dom_value',
2304 init: function (field_manager, node) {
2305 this._super(field_manager, node);
2306 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2308 initialize_content: function() {
2309 this.setupFocus(this.$('input'));
2311 store_dom_value: function () {
2312 if (!this.get('effective_readonly')
2313 && this.$('input').length
2314 && this.is_syntax_valid()) {
2315 this.internal_set_value(
2317 this.$('input').val()));
2320 commit_value: function () {
2321 this.store_dom_value();
2322 return this._super();
2324 render_value: function() {
2325 var show_value = this.format_value(this.get('value'), '');
2326 if (!this.get("effective_readonly")) {
2327 this.$el.find('input').val(show_value);
2329 if (this.password) {
2330 show_value = new Array(show_value.length + 1).join('*');
2332 this.$(".oe_form_char_content").text(show_value);
2335 is_syntax_valid: function() {
2336 if (!this.get("effective_readonly") && this.$("input").size() > 0) {
2338 this.parse_value(this.$('input').val(), '');
2346 parse_value: function(val, def) {
2347 return instance.web.parse_value(val, this, def);
2349 format_value: function(val, def) {
2350 return instance.web.format_value(val, this, def);
2352 is_false: function() {
2353 return this.get('value') === '' || this._super();
2356 var input = this.$('input:first')[0];
2357 return input ? input.focus() : false;
2359 set_dimensions: function (height, width) {
2360 this._super(height, width);
2361 this.$('input').css({
2368 instance.web.form.KanbanSelection = instance.web.form.FieldChar.extend({
2369 init: function (field_manager, node) {
2370 this._super(field_manager, node);
2372 prepare_dropdown_selection: function() {
2375 var selection = self.field.selection || [];
2376 _.map(selection, function(res) {
2380 'state_name': res[1],
2382 if (res[0] == 'normal') { value['state_class'] = 'oe_kanban_status'; }
2383 else if (res[0] == 'done') { value['state_class'] = 'oe_kanban_status oe_kanban_status_green'; }
2384 else { value['state_class'] = 'oe_kanban_status oe_kanban_status_red'; }
2389 render_value: function() {
2391 this.record_id = self.view.datarecord.id;
2392 this.states = self.prepare_dropdown_selection();;
2393 this.$el.html(QWeb.render("KanbanSelection", {'widget': self}));
2394 this.$el.find('.oe_legend').click(self.do_action.bind(self));
2396 do_action: function(e) {
2398 var li = $(e.target).closest( "li" );
2401 value[self.name] = String(li.data('value'));
2402 self.record_id = self.view.datarecord.id;
2403 if (self.record_id) {
2404 return self.view.dataset._model.call('write', [[self.record_id], value, self.view.dataset.get_context()]).done(self.reload_record.bind(self));
2406 return self.view.on_button_save().done(function(result) {
2408 self.view.dataset._model.call('write', [[result], value, self.view.dataset.get_context()]).done(self.reload_record.bind(self));
2414 reload_record: function() {
2419 instance.web.form.Priority = instance.web.form.FieldChar.extend({
2420 init: function (field_manager, node) {
2421 this._super(field_manager, node);
2423 prepare_priority: function() {
2425 var selection = this.field.selection || [];
2426 var init_value = selection && selection[0][0] || 0;
2427 var data = _.map(selection.slice(1), function(element, index) {
2429 'value': element[0],
2431 'click_value': element[0],
2433 if (index == 0 && self.get('value') == element[0]) {
2434 value['click_value'] = init_value;
2440 render_value: function() {
2442 this.record_id = self.view.datarecord.id;
2443 this.priorities = self.prepare_priority();
2444 this.$el.html(QWeb.render("Priority", {'widget': this}));
2445 this.$el.find('.oe_legend').click(self.do_action.bind(self));
2447 do_action: function(e) {
2449 var li = $(e.target).closest( "li" );
2452 value[self.name] = String(li.data('value'));
2453 if (self.record_id) {
2454 return self.view.dataset._model.call('write', [[self.record_id], value, self.view.dataset.get_context()]).done(self.reload_record.bind(self));
2456 return self.view.on_button_save().done(function(result) {
2458 self.view.dataset._model.call('write', [[result], value, self.view.dataset.get_context()]).done(self.reload_record.bind(self));
2464 reload_record: function() {
2469 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2470 process_modifiers: function () {
2472 this.set({ readonly: true });
2476 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2477 template: 'FieldEmail',
2478 initialize_content: function() {
2480 var $button = this.$el.find('button');
2481 $button.click(this.on_button_clicked);
2482 this.setupFocus($button);
2484 render_value: function() {
2485 if (!this.get("effective_readonly")) {
2489 .attr('href', 'mailto:' + this.get('value'))
2490 .text(this.get('value') || '');
2493 on_button_clicked: function() {
2494 if (!this.get('value') || !this.is_syntax_valid()) {
2495 this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2497 location.href = 'mailto:' + this.get('value');
2502 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2503 template: 'FieldUrl',
2504 initialize_content: function() {
2506 var $button = this.$el.find('button');
2507 $button.click(this.on_button_clicked);
2508 this.setupFocus($button);
2510 render_value: function() {
2511 if (!this.get("effective_readonly")) {
2514 var tmp = this.get('value');
2515 var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2517 tmp = "http://" + this.get('value');
2519 var text = this.get('value') ? this.node.attrs.text || tmp : '';
2520 this.$el.find('a').attr('href', tmp).text(text);
2523 on_button_clicked: function() {
2524 if (!this.get('value')) {
2525 this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2527 var url = $.trim(this.get('value'));
2528 if(/^www\./i.test(url))
2529 url = 'http://'+url;
2535 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2536 is_field_number: true,
2537 widget_class: 'oe_form_field_float',
2538 init: function (field_manager, node) {
2539 this._super(field_manager, node);
2540 this.internal_set_value(0);
2541 if (this.node.attrs.digits) {
2542 this.digits = this.node.attrs.digits;
2544 this.digits = this.field.digits;
2547 set_value: function(value_) {
2548 if (value_ === false || value_ === undefined) {
2549 // As in GTK client, floats default to 0
2552 this._super.apply(this, [value_]);
2554 focus: function () {
2555 var $input = this.$('input:first');
2556 return $input.length ? $input.select() : false;
2560 instance.web.form.FieldCharDomain = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2561 init: function(field_manager, node) {
2562 this._super.apply(this, arguments);
2566 this._super.apply(this, arguments);
2567 this.on("change:effective_readonly", this, function () {
2568 this.display_field();
2569 this.render_value();
2571 this.display_field();
2572 return this._super();
2574 render_value: function() {
2575 this.$('button.select_records').css('visibility', this.get('effective_readonly') ? 'hidden': '');
2577 set_value: function(value_) {
2579 this.set('value', value_ || false);
2580 this.display_field();
2582 display_field: function() {
2584 this.$el.html(instance.web.qweb.render("FieldCharDomain", {widget: this}));
2585 if (this.get('value')) {
2586 var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2587 var domain = instance.web.pyeval.eval('domain', this.get('value'));
2588 var ds = new instance.web.DataSetStatic(self, model, self.build_context());
2589 ds.call('search_count', [domain]).then(function (results) {
2590 $('.oe_domain_count', self.$el).text(results + ' records selected');
2591 $('button span', self.$el).text(' Change selection');
2594 $('.oe_domain_count', this.$el).text('0 record selected');
2595 $('button span', this.$el).text(' Select records');
2597 this.$('.select_records').on('click', self.on_click);
2599 on_click: function(ev) {
2600 event.preventDefault();
2602 var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2603 this.pop = new instance.web.form.SelectCreatePopup(this);
2604 this.pop.select_element(
2605 model, {title: 'Select records...'},
2606 [], this.build_context());
2607 this.pop.on("elements_selected", self, function(element_ids) {
2608 if (this.pop.$('input.oe_list_record_selector').prop('checked')) {
2609 var search_data = this.pop.searchview.build_search_data();
2610 var domain_done = instance.web.pyeval.eval_domains_and_contexts({
2611 domains: search_data.domains,
2612 contexts: search_data.contexts,
2613 group_by_seq: search_data.groupbys || []
2614 }).then(function (results) {
2615 return results.domain;
2619 var domain = [["id", "in", element_ids]];
2620 var domain_done = $.Deferred().resolve(domain);
2622 $.when(domain_done).then(function (domain) {
2623 var domain = self.pop.dataset.domain.concat(domain || []);
2624 self.set_value(domain);
2630 instance.web.DateTimeWidget = instance.web.Widget.extend({
2631 template: "web.datepicker",
2632 jqueryui_object: 'datetimepicker',
2633 type_of_date: "datetime",
2635 'change .oe_datepicker_master': 'change_datetime',
2636 'keypress .oe_datepicker_master': 'change_datetime',
2638 init: function(parent) {
2639 this._super(parent);
2640 this.name = parent.name;
2644 this.$input = this.$el.find('input.oe_datepicker_master');
2645 this.$input_picker = this.$el.find('input.oe_datepicker_container');
2647 $.datepicker.setDefaults({
2648 clearText: _t('Clear'),
2649 clearStatus: _t('Erase the current date'),
2650 closeText: _t('Done'),
2651 closeStatus: _t('Close without change'),
2652 prevText: _t('<Prev'),
2653 prevStatus: _t('Show the previous month'),
2654 nextText: _t('Next>'),
2655 nextStatus: _t('Show the next month'),
2656 currentText: _t('Today'),
2657 currentStatus: _t('Show the current month'),
2658 monthNames: Date.CultureInfo.monthNames,
2659 monthNamesShort: Date.CultureInfo.abbreviatedMonthNames,
2660 monthStatus: _t('Show a different month'),
2661 yearStatus: _t('Show a different year'),
2662 weekHeader: _t('Wk'),
2663 weekStatus: _t('Week of the year'),
2664 dayNames: Date.CultureInfo.dayNames,
2665 dayNamesShort: Date.CultureInfo.abbreviatedDayNames,
2666 dayNamesMin: Date.CultureInfo.shortestDayNames,
2667 dayStatus: _t('Set DD as first week day'),
2668 dateStatus: _t('Select D, M d'),
2669 firstDay: Date.CultureInfo.firstDayOfWeek,
2670 initStatus: _t('Select a date'),
2673 $.timepicker.setDefaults({
2674 timeOnlyTitle: _t('Choose Time'),
2675 timeText: _t('Time'),
2676 hourText: _t('Hour'),
2677 minuteText: _t('Minute'),
2678 secondText: _t('Second'),
2679 currentText: _t('Now'),
2680 closeText: _t('Done')
2684 onClose: this.on_picker_select,
2685 onSelect: this.on_picker_select,
2689 showButtonPanel: true,
2690 firstDay: Date.CultureInfo.firstDayOfWeek
2692 // Some clicks in the datepicker dialog are not stopped by the
2693 // datepicker and "bubble through", unexpectedly triggering the bus's
2694 // click event. Prevent that.
2695 this.picker('widget').click(function (e) { e.stopPropagation(); });
2697 this.$el.find('img.oe_datepicker_trigger').click(function() {
2698 if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
2699 self.$input.focus();
2702 self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date());
2703 self.$input_picker.show();
2704 self.picker('show');
2705 self.$input_picker.hide();
2707 this.set_readonly(false);
2708 this.set({'value': false});
2710 picker: function() {
2711 return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
2713 on_picker_select: function(text, instance_) {
2714 var date = this.picker('getDate');
2716 .val(date ? this.format_client(date) : '')
2720 set_value: function(value_) {
2721 this.set({'value': value_});
2722 this.$input.val(value_ ? this.format_client(value_) : '');
2724 get_value: function() {
2725 return this.get('value');
2727 set_value_from_ui_: function() {
2728 var value_ = this.$input.val() || false;
2729 this.set({'value': this.parse_client(value_)});
2731 set_readonly: function(readonly) {
2732 this.readonly = readonly;
2733 this.$input.prop('readonly', this.readonly);
2734 this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
2736 is_valid_: function() {
2737 var value_ = this.$input.val();
2738 if (value_ === "") {
2742 this.parse_client(value_);
2749 parse_client: function(v) {
2750 return instance.web.parse_value(v, {"widget": this.type_of_date});
2752 format_client: function(v) {
2753 return instance.web.format_value(v, {"widget": this.type_of_date});
2755 change_datetime: function(e) {
2756 if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
2757 this.set_value_from_ui_();
2758 this.trigger("datetime_changed");
2761 commit_value: function () {
2762 this.change_datetime();
2766 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2767 jqueryui_object: 'datepicker',
2768 type_of_date: "date"
2771 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2772 template: "FieldDatetime",
2773 build_widget: function() {
2774 return new instance.web.DateTimeWidget(this);
2776 destroy_content: function() {
2777 if (this.datewidget) {
2778 this.datewidget.destroy();
2779 this.datewidget = undefined;
2782 initialize_content: function() {
2783 if (!this.get("effective_readonly")) {
2784 this.datewidget = this.build_widget();
2785 this.datewidget.on('datetime_changed', this, _.bind(function() {
2786 this.internal_set_value(this.datewidget.get_value());
2788 this.datewidget.appendTo(this.$el);
2789 this.setupFocus(this.datewidget.$input);
2792 render_value: function() {
2793 if (!this.get("effective_readonly")) {
2794 this.datewidget.set_value(this.get('value'));
2796 this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2799 is_syntax_valid: function() {
2800 if (!this.get("effective_readonly") && this.datewidget) {
2801 return this.datewidget.is_valid_();
2805 is_false: function() {
2806 return this.get('value') === '' || this._super();
2809 var input = this.datewidget && this.datewidget.$input[0];
2810 return input ? input.focus() : false;
2812 set_dimensions: function (height, width) {
2813 this._super(height, width);
2814 if (!this.get("effective_readonly")) {
2815 this.datewidget.$input.css('height', height);
2820 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2821 template: "FieldDate",
2822 build_widget: function() {
2823 return new instance.web.DateWidget(this);
2827 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2828 template: 'FieldText',
2830 'keyup': function (e) {
2831 if (e.which === $.ui.keyCode.ENTER) {
2832 e.stopPropagation();
2835 'keypress': function (e) {
2836 if (e.which === $.ui.keyCode.ENTER) {
2837 e.stopPropagation();
2840 'change textarea': 'store_dom_value',
2842 initialize_content: function() {
2844 if (! this.get("effective_readonly")) {
2845 this.$textarea = this.$el.find('textarea');
2846 this.auto_sized = false;
2847 this.default_height = this.$textarea.css('height');
2848 if (this.get("effective_readonly")) {
2849 this.$textarea.attr('disabled', 'disabled');
2851 this.setupFocus(this.$textarea);
2853 this.$textarea = undefined;
2856 commit_value: function () {
2857 if (! this.get("effective_readonly") && this.$textarea) {
2858 this.store_dom_value();
2860 return this._super();
2862 store_dom_value: function () {
2863 this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2865 render_value: function() {
2866 if (! this.get("effective_readonly")) {
2867 var show_value = instance.web.format_value(this.get('value'), this, '');
2868 if (show_value === '') {
2869 this.$textarea.css('height', parseInt(this.default_height, 10)+"px");
2871 this.$textarea.val(show_value);
2872 if (! this.auto_sized) {
2873 this.auto_sized = true;
2874 this.$textarea.autosize();
2876 this.$textarea.trigger("autosize");
2879 var txt = this.get("value") || '';
2880 this.$(".oe_form_text_content").text(txt);
2883 is_syntax_valid: function() {
2884 if (!this.get("effective_readonly") && this.$textarea) {
2886 instance.web.parse_value(this.$textarea.val(), this, '');
2894 is_false: function() {
2895 return this.get('value') === '' || this._super();
2897 focus: function($el) {
2898 var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2899 return input ? input.focus() : false;
2901 set_dimensions: function (height, width) {
2902 this._super(height, width);
2903 if (!this.get("effective_readonly") && this.$textarea) {
2904 this.$textarea.css({
2913 * FieldTextHtml Widget
2914 * Intended for FieldText widgets meant to display HTML content. This
2915 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2916 * To find more information about CLEditor configutation: go to
2917 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2919 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2920 template: 'FieldTextHtml',
2922 this._super.apply(this, arguments);
2924 initialize_content: function() {
2926 if (! this.get("effective_readonly")) {
2927 self._updating_editor = false;
2928 this.$textarea = this.$el.find('textarea');
2929 var width = ((this.node.attrs || {}).editor_width || '100%');
2930 var height = ((this.node.attrs || {}).editor_height || 250);
2931 this.$textarea.cleditor({
2932 width: width, // width not including margins, borders or padding
2933 height: height, // height not including margins, borders or padding
2934 controls: // controls to add to the toolbar
2935 "bold italic underline strikethrough " +
2936 "| removeformat | bullets numbering | outdent " +
2937 "indent | link unlink | source",
2938 bodyStyle: // style to assign to document body contained within the editor
2939 "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2941 this.$cleditor = this.$textarea.cleditor()[0];
2942 this.$cleditor.change(function() {
2943 if (! self._updating_editor) {
2944 self.$cleditor.updateTextArea();
2945 self.internal_set_value(self.$textarea.val());
2948 if (this.field.translate) {
2949 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"/>')
2950 .click(this.on_translate);
2951 this.$cleditor.$toolbar.append($img);
2955 render_value: function() {
2956 if (! this.get("effective_readonly")) {
2957 this.$textarea.val(this.get('value') || '');
2958 this._updating_editor = true;
2959 this.$cleditor.updateFrame();
2960 this._updating_editor = false;
2962 this.$el.html(this.get('value'));
2967 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2968 template: 'FieldBoolean',
2971 this.$checkbox = $("input", this.$el);
2972 this.setupFocus(this.$checkbox);
2973 this.$el.click(_.bind(function() {
2974 this.internal_set_value(this.$checkbox.is(':checked'));
2976 var check_readonly = function() {
2977 self.$checkbox.prop('disabled', self.get("effective_readonly"));
2978 self.click_disabled_boolean();
2980 this.on("change:effective_readonly", this, check_readonly);
2981 check_readonly.call(this);
2982 this._super.apply(this, arguments);
2984 render_value: function() {
2985 this.$checkbox[0].checked = this.get('value');
2988 var input = this.$checkbox && this.$checkbox[0];
2989 return input ? input.focus() : false;
2991 click_disabled_boolean: function(){
2992 var $disabled = this.$el.find('input[type=checkbox]:disabled');
2993 $disabled.each(function (){
2994 $(this).next('div').remove();
2995 $(this).closest("span").append($('<div class="boolean"></div>'));
3001 The progressbar field expect a float from 0 to 100.
3003 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
3004 template: 'FieldProgressBar',
3005 render_value: function() {
3006 this.$el.progressbar({
3007 value: this.get('value') || 0,
3008 disabled: this.get("effective_readonly")
3010 var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
3011 this.$('span').html(formatted_value + '%');
3016 The PercentPie field expect a float from 0 to 100.
3018 instance.web.form.FieldPercentPie = instance.web.form.AbstractField.extend({
3019 template: 'FieldPercentPie',
3021 render_value: function() {
3022 var value = this.get('value'),
3023 formatted_value = Math.round(value || 0) + '%',
3024 svg = this.$('svg')[0];
3027 nv.addGraph(function() {
3028 var width = 42, height = 42;
3029 var chart = nv.models.pieChart()
3032 .margin({top: 0, right: 0, bottom: 0, left: 0})
3037 .color(['#7C7BAD','#DDD'])
3041 .datum([{'x': 'value', 'y': value}, {'x': 'complement', 'y': 100 - value}])
3044 .attr('style', 'width: ' + width + 'px; height:' + height + 'px;');
3048 .attr({x: width/2, y: height/2 + 3, 'text-anchor': 'middle'})
3049 .style({"font-size": "10px", "font-weight": "bold"})
3050 .text(formatted_value);
3059 The FieldBarChart expectsa list of values (indeed)
3061 instance.web.form.FieldBarChart = instance.web.form.AbstractField.extend({
3062 template: 'FieldBarChart',
3064 render_value: function() {
3065 var value = JSON.parse(this.get('value'));
3066 var svg = this.$('svg')[0];
3068 nv.addGraph(function() {
3069 var width = 34, height = 34;
3070 var chart = nv.models.discreteBarChart()
3071 .x(function (d) { return d.tooltip })
3072 .y(function (d) { return d.value })
3075 .margin({top: 0, right: 0, bottom: 0, left: 0})
3078 .transitionDuration(350)
3083 .datum([{key: 'values', values: value}])
3086 .attr('style', 'width: ' + (width + 4) + 'px; height: ' + (height + 8) + 'px;');
3088 nv.utils.windowResize(chart.update);
3097 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3098 template: 'FieldSelection',
3100 'change select': 'store_dom_value',
3102 init: function(field_manager, node) {
3104 this._super(field_manager, node);
3105 this.set("value", false);
3106 this.set("values", []);
3107 this.records_orderer = new instance.web.DropMisordered();
3108 this.field_manager.on("view_content_has_changed", this, function() {
3109 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
3110 if (! _.isEqual(domain, this.get("domain"))) {
3111 this.set("domain", domain);
3115 initialize_field: function() {
3116 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3117 this.on("change:domain", this, this.query_values);
3118 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
3119 this.on("change:values", this, this.render_value);
3121 query_values: function() {
3124 if (this.field.type === "many2one") {
3125 var model = new openerp.Model(openerp.session, this.field.relation);
3126 def = model.call("name_search", ['', this.get("domain")], {"context": this.build_context()});
3128 var values = _.reject(this.field.selection, function (v) { return v[0] === false && v[1] === ''; });
3129 def = $.when(values);
3131 this.records_orderer.add(def).then(function(values) {
3132 if (! _.isEqual(values, self.get("values"))) {
3133 self.set("values", values);
3137 initialize_content: function() {
3138 // Flag indicating whether we're in an event chain containing a change
3139 // event on the select, in order to know what to do on keyup[RETURN]:
3140 // * If the user presses [RETURN] as part of changing the value of a
3141 // selection, we should just let the value change and not let the
3142 // event broadcast further (e.g. to validating the current state of
3143 // the form in editable list view, which would lead to saving the
3144 // current row or switching to the next one)
3145 // * If the user presses [RETURN] with a select closed (side-effect:
3146 // also if the user opened the select and pressed [RETURN] without
3147 // changing the selected value), takes the action as validating the
3149 var ischanging = false;
3150 var $select = this.$el.find('select')
3151 .change(function () { ischanging = true; })
3152 .click(function () { ischanging = false; })
3153 .keyup(function (e) {
3154 if (e.which !== 13 || !ischanging) { return; }
3155 e.stopPropagation();
3158 this.setupFocus($select);
3160 commit_value: function () {
3161 this.store_dom_value();
3162 return this._super();
3164 store_dom_value: function () {
3165 if (!this.get('effective_readonly') && this.$('select').length) {
3166 var val = JSON.parse(this.$('select').val());
3167 this.internal_set_value(val);
3170 set_value: function(value_) {
3171 value_ = value_ === null ? false : value_;
3172 value_ = value_ instanceof Array ? value_[0] : value_;
3173 this._super(value_);
3175 render_value: function() {
3176 var values = this.get("values");
3177 values = [[false, this.node.attrs.placeholder || '']].concat(values);
3178 var found = _.find(values, function(el) { return el[0] === this.get("value"); }, this);
3180 found = [this.get("value"), _t('Unknown')];
3181 values = [found].concat(values);
3183 if (! this.get("effective_readonly")) {
3184 this.$().html(QWeb.render("FieldSelectionSelect", {widget: this, values: values}));
3185 this.$("select").val(JSON.stringify(found[0]));
3187 this.$el.text(found[1]);
3191 var input = this.$('select:first')[0];
3192 return input ? input.focus() : false;
3194 set_dimensions: function (height, width) {
3195 this._super(height, width);
3196 this.$('select').css({
3203 instance.web.form.FieldRadio = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3204 template: 'FieldRadio',
3206 'click input': 'click_change_value'
3208 init: function(field_manager, node) {
3209 /* Radio button widget: Attributes options:
3210 * - "horizontal" to display in column
3211 * - "no_radiolabel" don't display text values
3213 this._super(field_manager, node);
3214 this.selection = _.clone(this.field.selection) || [];
3215 this.domain = false;
3217 initialize_content: function () {
3218 this.uniqueId = _.uniqueId("radio");
3219 this.on("change:effective_readonly", this, this.render_value);
3220 this.field_manager.on("view_content_has_changed", this, this.get_selection);
3221 this.get_selection();
3223 click_change_value: function (event) {
3224 var val = $(event.target).val();
3225 val = this.field.type == "selection" ? val : +val;
3226 if (val == this.get_value()) {
3227 this.set_value(false);
3229 this.set_value(val);
3232 /** Get the selection and render it
3233 * selection: [[identifier, value_to_display], ...]
3234 * For selection fields: this is directly given by this.field.selection
3235 * For many2one fields: perform a search on the relation of the many2one field
3237 get_selection: function() {
3240 var def = $.Deferred();
3241 if (self.field.type == "many2one") {
3242 var domain = instance.web.pyeval.eval('domain', this.build_domain()) || [];
3243 if (! _.isEqual(self.domain, domain)) {
3244 self.domain = domain;
3245 var ds = new instance.web.DataSetStatic(self, self.field.relation, self.build_context());
3246 ds.call('search', [self.domain])
3247 .then(function (records) {
3248 ds.name_get(records).then(function (records) {
3249 selection = records;
3254 selection = self.selection;
3258 else if (self.field.type == "selection") {
3259 selection = self.field.selection || [];
3262 return def.then(function () {
3263 if (! _.isEqual(selection, self.selection)) {
3264 self.selection = _.clone(selection);
3265 self.renderElement();
3266 self.render_value();
3270 set_value: function (value_) {
3272 if (this.field.type == "selection") {
3273 value_ = _.find(this.field.selection, function (sel) { return sel[0] == value_;});
3275 else if (!this.selection.length) {
3276 this.selection = [value_];
3279 this._super(value_);
3281 get_value: function () {
3282 var value = this.get('value');
3283 return value instanceof Array ? value[0] : value;
3285 render_value: function () {
3287 this.$el.toggleClass("oe_readonly", this.get('effective_readonly'));
3288 this.$("input:checked").prop("checked", false);
3289 if (this.get_value()) {
3290 this.$("input").filter(function () {return this.value == self.get_value();}).prop("checked", true);
3291 this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : "");
3296 // jquery autocomplete tweak to allow html and classnames
3298 var proto = $.ui.autocomplete.prototype,
3299 initSource = proto._initSource;
3301 function filter( array, term ) {
3302 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
3303 return $.grep( array, function(value_) {
3304 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
3309 _initSource: function() {
3310 if ( this.options.html && $.isArray(this.options.source) ) {
3311 this.source = function( request, response ) {
3312 response( filter( this.options.source, request.term ) );
3315 initSource.call( this );
3319 _renderItem: function( ul, item) {
3320 return $( "<li></li>" )
3321 .data( "item.autocomplete", item )
3322 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
3324 .addClass(item.classname);
3330 A mixin containing some useful methods to handle completion inputs.
3332 The widget containing this option can have these arguments in its widget options:
3333 - no_quick_create: if true, it will disable the quick create
3335 instance.web.form.CompletionFieldMixin = {
3338 this.orderer = new instance.web.DropMisordered();
3341 * Call this method to search using a string.
3343 get_search_result: function(search_val) {
3346 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
3347 var blacklist = this.get_search_blacklist();
3348 this.last_query = search_val;
3350 return this.orderer.add(dataset.name_search(
3351 search_val, new instance.web.CompoundDomain(self.build_domain(), [["id", "not in", blacklist]]),
3352 'ilike', this.limit + 1, self.build_context())).then(function(data) {
3353 self.last_search = data;
3354 // possible selections for the m2o
3355 var values = _.map(data, function(x) {
3356 x[1] = x[1].split("\n")[0];
3358 label: _.str.escapeHTML(x[1]),
3365 // search more... if more results that max
3366 if (values.length > self.limit) {
3367 values = values.slice(0, self.limit);
3369 label: _t("Search More..."),
3370 action: function() {
3371 dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
3372 self._search_create_popup("search", data);
3375 classname: 'oe_m2o_dropdown_option'
3379 var raw_result = _(data.result).map(function(x) {return x[1];});
3380 if (search_val.length > 0 && !_.include(raw_result, search_val) &&
3381 ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3383 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
3384 $('<span />').text(search_val).html()),
3385 action: function() {
3386 self._quick_create(search_val);
3388 classname: 'oe_m2o_dropdown_option'
3392 if (!(self.options && self.options.no_create)){
3394 label: _t("Create and Edit..."),
3395 action: function() {
3396 self._search_create_popup("form", undefined, self._create_context(search_val));
3398 classname: 'oe_m2o_dropdown_option'
3401 else if (values.length == 0)
3403 label: _t("No results to show..."),
3404 action: function() {},
3405 classname: 'oe_m2o_dropdown_option'
3411 get_search_blacklist: function() {
3414 _quick_create: function(name) {
3416 var slow_create = function () {
3417 self._search_create_popup("form", undefined, self._create_context(name));
3419 if (self.options.quick_create === undefined || self.options.quick_create) {
3420 new instance.web.DataSet(this, this.field.relation, self.build_context())
3421 .name_create(name).done(function(data) {
3422 if (!self.get('effective_readonly'))
3423 self.add_id(data[0]);
3424 }).fail(function(error, event) {
3425 event.preventDefault();
3431 // all search/create popup handling
3432 _search_create_popup: function(view, ids, context) {
3434 var pop = new instance.web.form.SelectCreatePopup(this);
3436 self.field.relation,
3438 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3439 initial_ids: ids ? _.map(ids, function(x) {return x[0];}) : undefined,
3441 disable_multiple_selection: true
3443 self.build_domain(),
3444 new instance.web.CompoundContext(self.build_context(), context || {})
3446 pop.on("elements_selected", self, function(element_ids) {
3447 self.add_id(element_ids[0]);
3454 add_id: function(id) {},
3455 _create_context: function(name) {
3457 var field = (this.options || {}).create_name_field;
3458 if (field === undefined)
3460 if (field !== false && name && (this.options || {}).quick_create !== false)
3461 tmp["default_" + field] = name;
3466 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3467 template: "M2ODialog",
3468 init: function(parent) {
3469 this.name = parent.string;
3470 this._super(parent, {
3471 title: _.str.sprintf(_t("Create a %s"), parent.string),
3477 var text = _.str.sprintf(_t("You are creating a new %s, are you sure it does not exist yet?"), self.name);
3478 this.$("p").text( text );
3479 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3480 this.$("input").val(this.getParent().last_query);
3481 this.$buttons.find(".oe_form_m2o_qc_button").click(function(){
3482 self.getParent()._quick_create(self.$("input").val());
3485 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3486 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3489 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3495 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3496 template: "FieldMany2One",
3498 'keydown input': function (e) {
3500 case $.ui.keyCode.UP:
3501 case $.ui.keyCode.DOWN:
3502 e.stopPropagation();
3506 init: function(field_manager, node) {
3507 this._super(field_manager, node);
3508 instance.web.form.CompletionFieldMixin.init.call(this);
3509 this.set({'value': false});
3510 this.display_value = {};
3511 this.display_value_backup = {};
3512 this.last_search = [];
3513 this.floating = false;
3514 this.current_display = null;
3515 this.is_started = false;
3516 this.ignore_focusout = false;
3518 reinit_value: function(val) {
3519 this.internal_set_value(val);
3520 this.floating = false;
3521 if (this.is_started)
3522 this.render_value();
3524 initialize_field: function() {
3525 this.is_started = true;
3526 instance.web.bus.on('click', this, function() {
3527 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3528 this.$input.autocomplete("close");
3531 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3533 initialize_content: function() {
3534 if (!this.get("effective_readonly"))
3535 this.render_editable();
3537 destroy_content: function () {
3538 if (this.$drop_down) {
3539 this.$drop_down.off('click');
3540 delete this.$drop_down;
3543 this.$input.closest(".modal .modal-content").off('scroll');
3544 this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3545 'focus focusout change keydown');
3548 if (this.$follow_button) {
3549 this.$follow_button.off('blur focus click');
3550 delete this.$follow_button;
3553 destroy: function () {
3554 this.destroy_content();
3555 return this._super();
3557 init_error_displayer: function() {
3560 hide_error_displayer: function() {
3563 show_error_displayer: function() {
3564 new instance.web.form.M2ODialog(this).open();
3566 render_editable: function() {
3568 this.$input = this.$el.find("input");
3570 this.init_error_displayer();
3572 self.$input.on('focus', function() {
3573 self.hide_error_displayer();
3576 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3577 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3579 this.$follow_button.click(function(ev) {
3580 ev.preventDefault();
3581 if (!self.get('value')) {
3585 var pop = new instance.web.form.FormOpenPopup(self);
3586 var context = self.build_context().eval();
3587 var model_obj = new instance.web.Model(self.field.relation);
3588 model_obj.call('get_formview_id', [self.get("value"), context]).then(function(view_id){
3590 self.field.relation,
3592 self.build_context(),
3594 title: _t("Open: ") + self.string,
3598 pop.on('write_completed', self, function(){
3599 self.display_value = {};
3600 self.display_value_backup = {};
3601 self.render_value();
3603 self.trigger('changed_value');
3608 // some behavior for input
3609 var input_changed = function() {
3610 if (self.current_display !== self.$input.val()) {
3611 self.current_display = self.$input.val();
3612 if (self.$input.val() === "") {
3613 self.internal_set_value(false);
3614 self.floating = false;
3616 self.floating = true;
3620 this.$input.keydown(input_changed);
3621 this.$input.change(input_changed);
3622 this.$drop_down.click(function() {
3623 self.$input.focus();
3624 if (self.$input.autocomplete("widget").is(":visible")) {
3625 self.$input.autocomplete("close");
3627 if (self.get("value") && ! self.floating) {
3628 self.$input.autocomplete("search", "");
3630 self.$input.autocomplete("search");
3635 // Autocomplete close on dialog content scroll
3636 var close_autocomplete = _.debounce(function() {
3637 if (self.$input.autocomplete("widget").is(":visible")) {
3638 self.$input.autocomplete("close");
3641 this.$input.closest(".modal .modal-content").on('scroll', this, close_autocomplete);
3643 self.ed_def = $.Deferred();
3644 self.uned_def = $.Deferred();
3646 var ed_duration = 15000;
3647 var anyoneLoosesFocus = function (e) {
3648 if (self.ignore_focusout) { return; }
3650 if (self.floating) {
3651 if (self.last_search.length > 0) {
3652 if (self.last_search[0][0] != self.get("value")) {
3653 self.display_value = {};
3654 self.display_value_backup = {};
3655 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3656 self.reinit_value(self.last_search[0][0]);
3659 self.render_value();
3663 self.reinit_value(false);
3665 self.floating = false;
3667 if (used && self.get("value") === false && ! self.no_ed && (self.options.no_create === false || self.options.no_create === undefined)) {
3668 self.ed_def.reject();
3669 self.uned_def.reject();
3670 self.ed_def = $.Deferred();
3671 self.ed_def.done(function() {
3672 self.show_error_displayer();
3673 ignore_blur = false;
3674 self.trigger('focused');
3677 setTimeout(function() {
3678 self.ed_def.resolve();
3679 self.uned_def.reject();
3680 self.uned_def = $.Deferred();
3681 self.uned_def.done(function() {
3682 self.hide_error_displayer();
3684 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3688 self.ed_def.reject();
3691 var ignore_blur = false;
3693 focusout: anyoneLoosesFocus,
3694 focus: function () { self.trigger('focused'); },
3695 autocompleteopen: function () { ignore_blur = true; },
3696 autocompleteclose: function () { ignore_blur = false; },
3698 // autocomplete open
3699 if (ignore_blur) { return; }
3700 if (_(self.getChildren()).any(function (child) {
3701 return child instanceof instance.web.form.AbstractFormPopup;
3703 self.trigger('blurred');
3707 var isSelecting = false;
3709 this.$input.autocomplete({
3710 source: function(req, resp) {
3711 self.get_search_result(req.term).done(function(result) {
3715 select: function(event, ui) {
3719 self.display_value = {};
3720 self.display_value_backup = {};
3721 self.display_value["" + item.id] = item.name;
3722 self.reinit_value(item.id);
3723 } else if (item.action) {
3725 // Cancel widget blurring, to avoid form blur event
3726 self.trigger('focused');
3730 focus: function(e, ui) {
3734 // disabled to solve a bug, but may cause others
3735 //close: anyoneLoosesFocus,
3739 // set position for list of suggestions box
3740 this.$input.autocomplete( "option", "position", { my : "left top", at: "left bottom" } );
3741 this.$input.autocomplete("widget").openerpClass();
3742 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3743 this.$input.keyup(function(e) {
3744 if (e.which === 13) { // ENTER
3746 e.stopPropagation();
3748 isSelecting = false;
3750 this.setupFocus(this.$follow_button);
3752 render_value: function(no_recurse) {
3754 if (! this.get("value")) {
3755 this.display_string("");
3758 var display = this.display_value["" + this.get("value")];
3760 this.display_string(display);
3764 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3765 this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3767 self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
3770 self.display_value["" + self.get("value")] = data[0][1];
3771 self.render_value(true);
3772 }).fail( function (data, event) {
3773 // avoid displaying crash errors as many2One should be name_get compliant
3774 event.preventDefault();
3775 self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")];
3776 self.render_value(true);
3780 display_string: function(str) {
3782 if (!this.get("effective_readonly")) {
3783 this.$input.val(str.split("\n")[0]);
3784 this.current_display = this.$input.val();
3785 if (this.is_false()) {
3786 this.$('.oe_m2o_cm_button').css({'display':'none'});
3788 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3791 var lines = _.escape(str).split("\n");
3795 follow = _.rest(lines).join("<br />");
3798 var $link = this.$el.find('.oe_form_uri')
3801 if (! this.options.no_open)
3802 $link.click(function () {
3803 var context = self.build_context().eval();
3804 var model_obj = new instance.web.Model(self.field.relation);
3805 model_obj.call('get_formview_action', [self.get("value"), context]).then(function(action){
3806 self.do_action(action);
3810 $(".oe_form_m2o_follow", this.$el).html(follow);
3813 set_value: function(value_) {
3815 if (value_ instanceof Array) {
3816 this.display_value = {};
3817 this.display_value_backup = {};
3818 if (! this.options.always_reload) {
3819 this.display_value["" + value_[0]] = value_[1];
3822 this.display_value_backup["" + value_[0]] = value_[1];
3826 value_ = value_ || false;
3827 this.reinit_value(value_);
3829 get_displayed: function() {
3830 return this.display_value["" + this.get("value")];
3832 add_id: function(id) {
3833 this.display_value = {};
3834 this.display_value_backup = {};
3835 this.reinit_value(id);
3837 is_false: function() {
3838 return ! this.get("value");
3840 focus: function () {
3841 var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3842 return input ? input.focus() : false;
3844 _quick_create: function() {
3846 this.ed_def.reject();
3847 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3849 _search_create_popup: function() {
3851 this.ed_def.reject();
3852 this.ignore_focusout = true;
3853 this.reinit_value(false);
3854 var res = instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3855 this.ignore_focusout = false;
3859 set_dimensions: function (height, width) {
3860 this._super(height, width);
3861 if (!this.get("effective_readonly") && this.$input)
3862 this.$input.css('height', height);
3866 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3867 template: 'Many2OneButton',
3868 init: function(field_manager, node) {
3869 this._super.apply(this, arguments);
3872 this._super.apply(this, arguments);
3875 set_button: function() {
3878 this.$button.remove();
3881 this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3882 this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3883 this.$button.addClass('oe_link').css({'padding':'4px'});
3884 this.$el.append(this.$button);
3885 this.$button.on('click', self.on_click);
3887 on_click: function(ev) {
3889 this.popup = new instance.web.form.FormOpenPopup(this);
3890 this.popup.show_element(
3891 this.field.relation,
3893 this.build_context(),
3894 {title: this.string}
3896 this.popup.on('create_completed', self, function(r) {
3900 set_value: function(value_) {
3902 if (value_ instanceof Array) {
3905 value_ = value_ || false;
3906 this.set('value', value_);
3912 * Abstract-ish ListView.List subclass adding an "Add an item" row to replace
3913 * the big ugly button in the header.
3915 * Requires the implementation of a ``is_readonly`` method (usually a proxy to
3916 * the corresponding field's readonly or effective_readonly property) to
3917 * decide whether the special row should or should not be inserted.
3919 * Optionally an ``_add_row_class`` attribute can be set for the class(es) to
3920 * set on the insertion row.
3922 instance.web.form.AddAnItemList = instance.web.ListView.List.extend({
3923 pad_table_to: function (count) {
3924 if (!this.view.is_action_enabled('create') || this.is_readonly()) {
3929 this._super(count > 0 ? count - 1 : 0);
3932 var columns = _(this.columns).filter(function (column) {
3933 return column.invisible !== '1';
3935 if (this.options.selectable) { columns++; }
3936 if (this.options.deletable) { columns++; }
3938 var $cell = $('<td>', {
3940 'class': this._add_row_class || ''
3942 $('<a>', {href: '#'}).text(_t("Add an item"))
3943 .mousedown(function () {
3944 // FIXME: needs to be an official API somehow
3945 if (self.view.editor.is_editing()) {
3946 self.view.__ignore_blur = true;
3949 .click(function (e) {
3951 e.stopPropagation();
3952 // FIXME: there should also be an API for that one
3953 if (self.view.editor.form.__blur_timeout) {
3954 clearTimeout(self.view.editor.form.__blur_timeout);
3955 self.view.editor.form.__blur_timeout = false;
3957 self.view.ensure_saved().done(function () {
3958 self.view.do_add_record();
3962 var $padding = this.$current.find('tr:not([data-id]):first');
3963 var $newrow = $('<tr>').append($cell);
3964 if ($padding.length) {
3965 $padding.before($newrow);
3967 this.$current.append($newrow)
3973 # Values: (0, 0, { fields }) create
3974 # (1, ID, { fields }) update
3975 # (2, ID) remove (delete)
3976 # (3, ID) unlink one (target id or target of relation)
3978 # (5) unlink all (only valid for one2many)
3983 'create': function (values) {
3984 return [commands.CREATE, false, values];
3986 // (1, id, {values})
3988 'update': function (id, values) {
3989 return [commands.UPDATE, id, values];
3993 'delete': function (id) {
3994 return [commands.DELETE, id, false];
3996 // (3, id[, _]) removes relation, but not linked record itself
3998 'forget': function (id) {
3999 return [commands.FORGET, id, false];
4003 'link_to': function (id) {
4004 return [commands.LINK_TO, id, false];
4008 'delete_all': function () {
4009 return [5, false, false];
4011 // (6, _, ids) replaces all linked records with provided ids
4013 'replace_with': function (ids) {
4014 return [6, false, ids];
4017 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
4018 multi_selection: false,
4019 disable_utility_classes: true,
4020 init: function(field_manager, node) {
4021 this._super(field_manager, node);
4022 lazy_build_o2m_kanban_view();
4023 this.is_loaded = $.Deferred();
4024 this.initial_is_loaded = this.is_loaded;
4025 this.form_last_update = $.Deferred();
4026 this.init_form_last_update = this.form_last_update;
4027 this.is_started = false;
4028 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
4029 this.dataset.o2m = this;
4030 this.dataset.parent_view = this.view;
4031 this.dataset.child_name = this.name;
4033 this.dataset.on('dataset_changed', this, function() {
4034 self.trigger_on_change();
4039 this._super.apply(this, arguments);
4040 this.$el.addClass('oe_form_field oe_form_field_one2many');
4045 this.is_loaded.done(function() {
4046 self.on("change:effective_readonly", self, function() {
4047 self.is_loaded = self.is_loaded.then(function() {
4048 self.viewmanager.destroy();
4049 return $.when(self.load_views()).done(function() {
4050 self.reload_current_view();
4055 this.is_started = true;
4056 this.reload_current_view();
4058 trigger_on_change: function() {
4059 this.trigger('changed_value');
4061 load_views: function() {
4064 var modes = this.node.attrs.mode;
4065 modes = !!modes ? modes.split(",") : ["tree"];
4067 _.each(modes, function(mode) {
4068 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
4069 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
4073 view_type: mode == "tree" ? "list" : mode,
4076 if (self.field.views && self.field.views[mode]) {
4077 view.embedded_view = self.field.views[mode];
4079 if(view.view_type === "list") {
4080 _.extend(view.options, {
4082 selectable: self.multi_selection,
4084 import_enabled: false,
4087 if (self.get("effective_readonly")) {
4088 _.extend(view.options, {
4093 } else if (view.view_type === "form") {
4094 if (self.get("effective_readonly")) {
4095 view.view_type = 'form';
4097 _.extend(view.options, {
4098 not_interactible_on_create: true,
4100 } else if (view.view_type === "kanban") {
4101 _.extend(view.options, {
4102 confirm_on_delete: false,
4104 if (self.get("effective_readonly")) {
4105 _.extend(view.options, {
4106 action_buttons: false,
4107 quick_creatable: false,
4109 read_only_mode: true,
4117 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
4118 this.viewmanager.o2m = self;
4119 var once = $.Deferred().done(function() {
4120 self.init_form_last_update.resolve();
4122 var def = $.Deferred().done(function() {
4123 self.initial_is_loaded.resolve();
4125 this.viewmanager.on("controller_inited", self, function(view_type, controller) {
4126 controller.o2m = self;
4127 if (view_type == "list") {
4128 if (self.get("effective_readonly")) {
4129 controller.on('edit:before', self, function (e) {
4132 _(controller.columns).find(function (column) {
4133 if (!(column instanceof instance.web.list.Handle)) {
4136 column.modifiers.invisible = true;
4140 } else if (view_type === "form") {
4141 if (self.get("effective_readonly")) {
4142 $(".oe_form_buttons", controller.$el).children().remove();
4144 controller.on("load_record", self, function(){
4147 controller.on('pager_action_executed',self,self.save_any_view);
4148 } else if (view_type == "graph") {
4149 self.reload_current_view();
4153 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
4154 $.when(self.save_any_view()).done(function() {
4155 if (n_mode === "list") {
4156 $.async_when().done(function() {
4157 self.reload_current_view();
4162 $.async_when().done(function () {
4163 self.viewmanager.appendTo(self.$el);
4167 reload_current_view: function() {
4169 self.is_loaded = self.is_loaded.then(function() {
4170 var active_view = self.viewmanager.active_view;
4171 var view = self.viewmanager.views[active_view].controller;
4172 if(active_view === "list") {
4173 return view.reload_content();
4174 } else if (active_view === "form") {
4175 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
4176 self.dataset.index = 0;
4178 var act = function() {
4179 return view.do_show();
4181 self.form_last_update = self.form_last_update.then(act, act);
4182 return self.form_last_update;
4183 } else if (view.do_search) {
4184 return view.do_search(self.build_domain(), self.dataset.get_context(), []);
4187 return self.is_loaded;
4189 set_value: function(value_) {
4190 value_ = value_ || [];
4192 this.dataset.reset_ids([]);
4194 if(value_.length >= 1 && value_[0] instanceof Array) {
4196 _.each(value_, function(command) {
4197 var obj = {values: command[2]};
4198 switch (command[0]) {
4199 case commands.CREATE:
4200 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4202 self.dataset.to_create.push(obj);
4203 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4206 case commands.UPDATE:
4207 obj['id'] = command[1];
4208 self.dataset.to_write.push(obj);
4209 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4212 case commands.DELETE:
4213 self.dataset.to_delete.push({id: command[1]});
4215 case commands.LINK_TO:
4216 ids.push(command[1]);
4218 case commands.DELETE_ALL:
4219 self.dataset.delete_all = true;
4224 this.dataset.set_ids(ids);
4225 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
4227 this.dataset.delete_all = true;
4228 _.each(value_, function(command) {
4229 var obj = {values: command};
4230 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4232 self.dataset.to_create.push(obj);
4233 self.dataset.cache.push(_.clone(obj));
4237 this.dataset.set_ids(ids);
4239 this._super(value_);
4240 this.dataset.reset_ids(value_);
4242 if (this.dataset.index === null && this.dataset.ids.length > 0) {
4243 this.dataset.index = 0;
4245 this.trigger_on_change();
4246 if (this.is_started) {
4247 return self.reload_current_view();
4252 get_value: function() {
4256 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
4257 val = val.concat(_.map(this.dataset.ids, function(id) {
4258 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
4260 return commands.create(alter_order.values);
4262 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
4264 return commands.update(alter_order.id, alter_order.values);
4266 return commands.link_to(id);
4268 return val.concat(_.map(
4269 this.dataset.to_delete, function(x) {
4270 return commands['delete'](x.id);}));
4272 commit_value: function() {
4273 return this.save_any_view();
4275 save_any_view: function() {
4276 if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
4277 this.viewmanager.views[this.viewmanager.active_view] &&
4278 this.viewmanager.views[this.viewmanager.active_view].controller) {
4279 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
4280 if (this.viewmanager.active_view === "form") {
4281 if (view.is_initialized.state() !== 'resolved') {
4282 return $.when(false);
4284 return $.when(view.save());
4285 } else if (this.viewmanager.active_view === "list") {
4286 return $.when(view.ensure_saved());
4289 return $.when(false);
4291 is_syntax_valid: function() {
4292 if (! this.viewmanager || ! this.viewmanager.views[this.viewmanager.active_view])
4294 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
4295 switch (this.viewmanager.active_view) {
4297 return _(view.fields).chain()
4302 return view.is_valid();
4308 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
4309 template: 'One2Many.viewmanager',
4310 init: function(parent, dataset, views, flags) {
4311 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
4312 this.registry = this.registry.extend({
4313 list: 'instance.web.form.One2ManyListView',
4314 form: 'instance.web.form.One2ManyFormView',
4315 kanban: 'instance.web.form.One2ManyKanbanView',
4317 this.__ignore_blur = false;
4319 switch_mode: function(mode, unused) {
4320 if (mode !== 'form') {
4321 return this._super(mode, unused);
4324 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
4325 var pop = new instance.web.form.FormOpenPopup(this);
4326 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4327 title: _t("Open: ") + self.o2m.string,
4328 create_function: function(data, options) {
4329 return self.o2m.dataset.create(data, options).done(function(r) {
4330 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4331 self.o2m.dataset.trigger("dataset_changed", r);
4334 write_function: function(id, data, options) {
4335 return self.o2m.dataset.write(id, data, {}).done(function() {
4336 self.o2m.reload_current_view();
4339 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4340 parent_view: self.o2m.view,
4341 child_name: self.o2m.name,
4342 read_function: function() {
4343 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4345 form_view_options: {'not_interactible_on_create':true},
4346 readonly: self.o2m.get("effective_readonly")
4348 pop.on("elements_selected", self, function() {
4349 self.o2m.reload_current_view();
4354 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
4355 get_context: function() {
4356 this.context = this.o2m.build_context();
4357 return this.context;
4361 instance.web.form.One2ManyListView = instance.web.ListView.extend({
4362 _template: 'One2Many.listview',
4363 init: function (parent, dataset, view_id, options) {
4364 this._super(parent, dataset, view_id, _.extend(options || {}, {
4365 GroupsType: instance.web.form.One2ManyGroups,
4366 ListType: instance.web.form.One2ManyList
4368 this.on('edit:after', this, this.proxy('_after_edit'));
4369 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
4372 .bind('add', this.proxy("changed_records"))
4373 .bind('edit', this.proxy("changed_records"))
4374 .bind('remove', this.proxy("changed_records"));
4376 start: function () {
4377 var ret = this._super();
4379 .off('mousedown.handleButtons')
4380 .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
4383 changed_records: function () {
4384 this.o2m.trigger_on_change();
4386 is_valid: function () {
4387 var editor = this.editor;
4388 var form = editor.form;
4389 // If no edition is pending, the listview can not be invalid (?)
4390 if (!editor.record) {
4393 // If the form has not been modified, the view can only be valid
4394 // NB: is_dirty will also be set on defaults/onchanges/whatever?
4395 // oe_form_dirty seems to only be set on actual user actions
4396 if (!form.$el.is('.oe_form_dirty')) {
4399 this.o2m._dirty_flag = true;
4401 // Otherwise validate internal form
4402 return _(form.fields).chain()
4403 .invoke(function () {
4404 this._check_css_flags();
4405 return this.is_valid();
4410 do_add_record: function () {
4411 if (this.editable()) {
4412 this._super.apply(this, arguments);
4415 var pop = new instance.web.form.SelectCreatePopup(this);
4417 self.o2m.field.relation,
4419 title: _t("Create: ") + self.o2m.string,
4420 initial_view: "form",
4421 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4422 create_function: function(data, options) {
4423 return self.o2m.dataset.create(data, options).done(function(r) {
4424 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4425 self.o2m.dataset.trigger("dataset_changed", r);
4428 read_function: function() {
4429 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4431 parent_view: self.o2m.view,
4432 child_name: self.o2m.name,
4433 form_view_options: {'not_interactible_on_create':true}
4435 self.o2m.build_domain(),
4436 self.o2m.build_context()
4438 pop.on("elements_selected", self, function() {
4439 self.o2m.reload_current_view();
4443 do_activate_record: function(index, id) {
4445 var pop = new instance.web.form.FormOpenPopup(self);
4446 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4447 title: _t("Open: ") + self.o2m.string,
4448 write_function: function(id, data) {
4449 return self.o2m.dataset.write(id, data, {}).done(function() {
4450 self.o2m.reload_current_view();
4453 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4454 parent_view: self.o2m.view,
4455 child_name: self.o2m.name,
4456 read_function: function() {
4457 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4459 form_view_options: {'not_interactible_on_create':true},
4460 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
4463 do_button_action: function (name, id, callback) {
4464 if (!_.isNumber(id)) {
4465 instance.webclient.notification.warn(
4466 _t("Action Button"),
4467 _t("The o2m record must be saved before an action can be used"));
4470 var parent_form = this.o2m.view;
4472 this.ensure_saved().then(function () {
4474 return parent_form.save();
4477 }).done(function () {
4478 var ds = self.o2m.dataset;
4479 var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
4480 return value.length;
4482 if (!self.o2m.options.reload_on_button && !cached_records) {
4483 self.handle_button(name, id, callback);
4485 self.handle_button(name, id, function(){
4486 self.o2m.view.reload();
4492 _after_edit: function () {
4493 this.__ignore_blur = false;
4494 this.editor.form.on('blurred', this, this._on_form_blur);
4496 // The form's blur thing may be jiggered during the edition setup,
4497 // potentially leading to the o2m instasaving the row. Cancel any
4498 // blurring triggered the edition startup here
4499 this.editor.form.widgetFocused();
4501 _before_unedit: function () {
4502 this.editor.form.off('blurred', this, this._on_form_blur);
4504 _button_down: function () {
4505 // If a button is clicked (usually some sort of action button), it's
4506 // the button's responsibility to ensure the editable list is in the
4507 // correct state -> ignore form blurring
4508 this.__ignore_blur = true;
4511 * Handles blurring of the nested form (saves the currently edited row),
4512 * unless the flag to ignore the event is set to ``true``
4514 * Makes the internal form go away
4516 _on_form_blur: function () {
4517 if (this.__ignore_blur) {
4518 this.__ignore_blur = false;
4521 // FIXME: why isn't there an API for this?
4522 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4523 this.ensure_saved();
4526 this.cancel_edition();
4528 keypress_ENTER: function () {
4529 // blurring caused by hitting the [Return] key, should skip the
4530 // autosave-on-blur and let the handler for [Return] do its thing (save
4531 // the current row *anyway*, then create a new one/edit the next one)
4532 this.__ignore_blur = true;
4533 this._super.apply(this, arguments);
4535 do_delete: function (ids) {
4536 var confirm = window.confirm;
4537 window.confirm = function () { return true; };
4539 return this._super(ids);
4541 window.confirm = confirm;
4544 reload_record: function (record) {
4545 // Evict record.id from cache to ensure it will be reloaded correctly
4546 this.dataset.evict_record(record.get('id'));
4548 return this._super(record);
4551 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4552 setup_resequence_rows: function () {
4553 if (!this.view.o2m.get('effective_readonly')) {
4554 this._super.apply(this, arguments);
4558 instance.web.form.One2ManyList = instance.web.form.AddAnItemList.extend({
4559 _add_row_class: 'oe_form_field_one2many_list_row_add',
4560 is_readonly: function () {
4561 return this.view.o2m.get('effective_readonly');
4565 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4566 form_template: 'One2Many.formview',
4567 load_form: function(data) {
4570 this.$buttons.find('button.oe_form_button_create').click(function() {
4571 self.save().done(self.on_button_new);
4574 do_notify_change: function() {
4575 if (this.dataset.parent_view) {
4576 this.dataset.parent_view.do_notify_change();
4578 this._super.apply(this, arguments);
4583 var lazy_build_o2m_kanban_view = function() {
4584 if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
4586 instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4590 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4591 template: "FieldMany2ManyTags",
4592 tag_template: "FieldMany2ManyTag",
4594 this._super.apply(this, arguments);
4595 instance.web.form.CompletionFieldMixin.init.call(this);
4596 this.set({"value": []});
4597 this._display_orderer = new instance.web.DropMisordered();
4598 this._drop_shown = false;
4600 initialize_texttext: function(){
4603 plugins : 'tags arrow autocomplete',
4605 render: function(suggestion) {
4606 return $('<span class="text-label"/>').
4607 data('index', suggestion['index']).html(suggestion['label']);
4612 selectFromDropdown: function() {
4613 this.trigger('hideDropdown');
4614 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4615 var data = self.search_result[index];
4617 self.add_id(data.id);
4619 self.ignore_blur = true;
4622 this.trigger('setSuggestions', {result : []});
4626 isTagAllowed: function(tag) {
4630 removeTag: function(tag) {
4631 var id = tag.data("id");
4632 self.set({"value": _.without(self.get("value"), id)});
4634 renderTag: function(stuff) {
4635 return $.fn.textext.TextExtTags.prototype.renderTag.
4636 call(this, stuff).data("id", stuff.id);
4640 itemToString: function(item) {
4645 onSetInputData: function(e, data) {
4647 this._plugins.autocomplete._suggestions = null;
4649 this.input().val(data);
4655 initialize_content: function() {
4656 if (this.get("effective_readonly"))
4659 self.ignore_blur = false;
4660 self.$text = this.$("textarea");
4661 self.$text.textext(self.initialize_texttext()).bind('getSuggestions', function(e, data) {
4663 var str = !!data ? data.query || '' : '';
4664 self.get_search_result(str).done(function(result) {
4665 self.search_result = result;
4666 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4667 return _.extend(el, {index:i});
4670 }).bind('hideDropdown', function() {
4671 self._drop_shown = false;
4672 }).bind('showDropdown', function() {
4673 self._drop_shown = true;
4675 self.tags = self.$text.textext()[0].tags();
4677 .focusin(function () {
4678 self.trigger('focused');
4679 self.ignore_blur = false;
4681 .focusout(function() {
4682 self.$text.trigger("setInputData", "");
4683 if (!self.ignore_blur) {
4684 self.trigger('blurred');
4686 }).keydown(function(e) {
4687 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4688 self.$text.textext()[0].autocomplete().selectFromDropdown();
4692 set_value: function(value_) {
4693 value_ = value_ || [];
4694 if (value_.length >= 1 && value_[0] instanceof Array) {
4695 value_ = value_[0][2];
4697 this._super(value_);
4699 is_false: function() {
4700 return _(this.get("value")).isEmpty();
4702 get_value: function() {
4703 var tmp = [commands.replace_with(this.get("value"))];
4706 get_search_blacklist: function() {
4707 return this.get("value");
4709 map_tag: function(data){
4710 return _.map(data, function(el) {return {name: el[1], id:el[0]};})
4712 get_render_data: function(ids){
4714 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4715 return dataset.name_get(ids);
4717 render_tag: function(data) {
4719 if (! self.get("effective_readonly")) {
4720 self.tags.containerElement().children().remove();
4721 self.$('textarea').css("padding-left", "3px");
4722 self.tags.addTags(self.map_tag(data));
4724 self.$el.html(QWeb.render(self.tag_template, {elements: data}));
4727 render_value: function() {
4729 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4730 var values = self.get("value");
4731 var handle_names = function(data) {
4732 if (self.isDestroyed())
4735 _.each(data, function(el) {
4736 indexed[el[0]] = el;
4738 data = _.map(values, function(el) { return indexed[el]; });
4739 self.render_tag(data);
4741 if (! values || values.length > 0) {
4742 this._display_orderer.add(self.get_render_data(values)).done(handle_names);
4748 add_id: function(id) {
4749 this.set({'value': _.uniq(this.get('value').concat([id]))});
4751 focus: function () {
4752 var input = this.$text && this.$text[0];
4753 return input ? input.focus() : false;
4755 set_dimensions: function (height, width) {
4756 this._super(height, width);
4757 this.$("textarea").css({
4762 _search_create_popup: function() {
4763 self.ignore_blur = true;
4764 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
4770 - reload_on_button: Reload the whole form view if click on a button in a list view.
4771 If you see this options, do not use it, it's basically a dirty hack to make one
4772 precise o2m to behave the way we want.
4774 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4775 multi_selection: false,
4776 disable_utility_classes: true,
4777 init: function(field_manager, node) {
4778 this._super(field_manager, node);
4779 this.is_loaded = $.Deferred();
4780 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4781 this.dataset.m2m = this;
4783 this.dataset.on('unlink', self, function(ids) {
4784 self.dataset_changed();
4787 this.list_dm = new instance.web.DropMisordered();
4788 this.render_value_dm = new instance.web.DropMisordered();
4790 initialize_content: function() {
4793 this.$el.addClass('oe_form_field oe_form_field_many2many');
4795 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4797 'deletable': this.get("effective_readonly") ? false : true,
4798 'selectable': this.multi_selection,
4800 'reorderable': false,
4801 'import_enabled': false,
4803 var embedded = (this.field.views || {}).tree;
4805 this.list_view.set_embedded_view(embedded);
4807 this.list_view.m2m_field = this;
4808 var loaded = $.Deferred();
4809 this.list_view.on("list_view_loaded", this, function() {
4812 this.list_view.appendTo(this.$el);
4814 var old_def = self.is_loaded;
4815 self.is_loaded = $.Deferred().done(function() {
4818 this.list_dm.add(loaded).then(function() {
4819 self.is_loaded.resolve();
4822 destroy_content: function() {
4823 this.list_view.destroy();
4824 this.list_view = undefined;
4826 set_value: function(value_) {
4827 value_ = value_ || [];
4828 if (value_.length >= 1 && value_[0] instanceof Array) {
4829 value_ = value_[0][2];
4831 this._super(value_);
4833 get_value: function() {
4834 return [commands.replace_with(this.get('value'))];
4836 is_false: function () {
4837 return _(this.get("value")).isEmpty();
4839 render_value: function() {
4841 this.dataset.set_ids(this.get("value"));
4842 this.render_value_dm.add(this.is_loaded).then(function() {
4843 return self.list_view.reload_content();
4846 dataset_changed: function() {
4847 this.internal_set_value(this.dataset.ids);
4851 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4852 get_context: function() {
4853 this.context = this.m2m.build_context();
4854 return this.context;
4860 * @extends instance.web.ListView
4862 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4863 init: function (parent, dataset, view_id, options) {
4864 this._super(parent, dataset, view_id, _.extend(options || {}, {
4865 ListType: instance.web.form.Many2ManyList,
4868 do_add_record: function () {
4869 var pop = new instance.web.form.SelectCreatePopup(this);
4873 title: _t("Add: ") + this.m2m_field.string,
4874 no_create: this.m2m_field.options.no_create,
4876 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4877 this.m2m_field.build_context()
4880 pop.on("elements_selected", self, function(element_ids) {
4882 _(element_ids).each(function (id) {
4883 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4884 self.dataset.set_ids(self.dataset.ids.concat([id]));
4885 self.m2m_field.dataset_changed();
4890 self.reload_content();
4894 do_activate_record: function(index, id) {
4896 var pop = new instance.web.form.FormOpenPopup(this);
4897 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4898 title: _t("Open: ") + this.m2m_field.string,
4899 readonly: this.getParent().get("effective_readonly")
4901 pop.on('write_completed', self, self.reload_content);
4903 do_button_action: function(name, id, callback) {
4905 var _sup = _.bind(this._super, this);
4906 if (! this.m2m_field.options.reload_on_button) {
4907 return _sup(name, id, callback);
4909 return this.m2m_field.view.save().then(function() {
4910 return _sup(name, id, function() {
4911 self.m2m_field.view.reload();
4916 is_action_enabled: function () { return true; },
4918 instance.web.form.Many2ManyList = instance.web.form.AddAnItemList.extend({
4919 _add_row_class: 'oe_form_field_many2many_list_row_add',
4920 is_readonly: function () {
4921 return this.view.m2m_field.get('effective_readonly');
4925 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4926 disable_utility_classes: true,
4927 init: function(field_manager, node) {
4928 this._super(field_manager, node);
4929 instance.web.form.CompletionFieldMixin.init.call(this);
4930 m2m_kanban_lazy_init();
4931 this.is_loaded = $.Deferred();
4932 this.initial_is_loaded = this.is_loaded;
4935 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4936 this.dataset.m2m = this;
4937 this.dataset.on('unlink', self, function(ids) {
4938 self.dataset_changed();
4942 this._super.apply(this, arguments);
4947 self.on("change:effective_readonly", self, function() {
4948 self.is_loaded = self.is_loaded.then(function() {
4949 self.kanban_view.destroy();
4950 return $.when(self.load_view()).done(function() {
4951 self.render_value();
4956 set_value: function(value_) {
4957 value_ = value_ || [];
4958 if (value_.length >= 1 && value_[0] instanceof Array) {
4959 value_ = value_[0][2];
4961 this._super(value_);
4963 get_value: function() {
4964 return [commands.replace_with(this.get('value'))];
4966 load_view: function() {
4968 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4969 'create_text': _t("Add"),
4970 'creatable': self.get("effective_readonly") ? false : true,
4971 'quick_creatable': self.get("effective_readonly") ? false : true,
4972 'read_only_mode': self.get("effective_readonly") ? true : false,
4973 'confirm_on_delete': false,
4975 var embedded = (this.field.views || {}).kanban;
4977 this.kanban_view.set_embedded_view(embedded);
4979 this.kanban_view.m2m = this;
4980 var loaded = $.Deferred();
4981 this.kanban_view.on("kanban_view_loaded",self,function() {
4982 self.initial_is_loaded.resolve();
4985 this.kanban_view.on('switch_mode', this, this.open_popup);
4986 $.async_when().done(function () {
4987 self.kanban_view.appendTo(self.$el);
4991 render_value: function() {
4993 this.dataset.set_ids(this.get("value"));
4994 this.is_loaded = this.is_loaded.then(function() {
4995 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
4998 dataset_changed: function() {
4999 this.set({'value': this.dataset.ids});
5001 open_popup: function(type, unused) {
5002 if (type !== "form")
5006 if (this.dataset.index === null) {
5007 pop = new instance.web.form.SelectCreatePopup(this);
5009 this.field.relation,
5011 title: _t("Add: ") + this.string
5013 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
5014 this.build_context()
5016 pop.on("elements_selected", self, function(element_ids) {
5017 _.each(element_ids, function(one_id) {
5018 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
5019 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
5020 self.dataset_changed();
5021 self.render_value();
5026 var id = self.dataset.ids[self.dataset.index];
5027 pop = new instance.web.form.FormOpenPopup(this);
5028 pop.show_element(self.field.relation, id, self.build_context(), {
5029 title: _t("Open: ") + self.string,
5030 write_function: function(id, data, options) {
5031 return self.dataset.write(id, data, {}).done(function() {
5032 self.render_value();
5035 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
5036 parent_view: self.view,
5037 child_name: self.name,
5038 readonly: self.get("effective_readonly")
5042 add_id: function(id) {
5043 this.quick_create.add_id(id);
5047 function m2m_kanban_lazy_init() {
5048 if (instance.web.form.Many2ManyKanbanView)
5050 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
5051 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
5052 _is_quick_create_enabled: function() {
5053 return this._super() && ! this.group_by;
5056 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
5057 template: 'Many2ManyKanban.quick_create',
5060 * close_btn: If true, the widget will display a "Close" button able to trigger
5063 init: function(parent, dataset, context, buttons) {
5064 this._super(parent);
5065 this.m2m = this.getParent().view.m2m;
5066 this.m2m.quick_create = this;
5067 this._dataset = dataset;
5068 this._buttons = buttons || false;
5069 this._context = context || {};
5071 start: function () {
5073 self.$text = this.$el.find('input').css("width", "200px");
5074 self.$text.textext({
5075 plugins : 'arrow autocomplete',
5077 render: function(suggestion) {
5078 return $('<span class="text-label"/>').
5079 data('index', suggestion['index']).html(suggestion['label']);
5084 selectFromDropdown: function() {
5085 $(this).trigger('hideDropdown');
5086 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
5087 var data = self.search_result[index];
5089 self.add_id(data.id);
5096 itemToString: function(item) {
5101 }).bind('getSuggestions', function(e, data) {
5103 var str = !!data ? data.query || '' : '';
5104 self.m2m.get_search_result(str).done(function(result) {
5105 self.search_result = result;
5106 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
5107 return _.extend(el, {index:i});
5111 self.$text.focusout(function() {
5116 this.$text[0].focus();
5118 add_id: function(id) {
5121 self.trigger('added', id);
5122 this.m2m.dataset_changed();
5128 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
5130 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
5131 template: "AbstractFormPopup.render",
5134 * -readonly: only applicable when not in creation mode, default to false
5135 * - alternative_form_view
5142 * - form_view_options
5144 init_popup: function(model, row_id, domain, context, options) {
5145 this.row_id = row_id;
5147 this.domain = domain || [];
5148 this.context = context || {};
5149 this.options = options;
5150 _.defaults(this.options, {
5153 init_dataset: function() {
5155 this.created_elements = [];
5156 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
5157 this.dataset.read_function = this.options.read_function;
5158 this.dataset.create_function = function(data, options, sup) {
5159 var fct = self.options.create_function || sup;
5160 return fct.call(this, data, options).done(function(r) {
5161 self.trigger('create_completed saved', r);
5162 self.created_elements.push(r);
5165 this.dataset.write_function = function(id, data, options, sup) {
5166 var fct = self.options.write_function || sup;
5167 return fct.call(this, id, data, options).done(function(r) {
5168 self.trigger('write_completed saved', r);
5171 this.dataset.parent_view = this.options.parent_view;
5172 this.dataset.child_name = this.options.child_name;
5174 display_popup: function() {
5176 this.renderElement();
5177 var dialog = new instance.web.Dialog(this, {
5178 dialogClass: 'oe_act_window',
5179 title: this.options.title || "",
5180 }, this.$el).open();
5181 dialog.on('closing', this, function (e){
5182 self.check_exit(true);
5184 this.$buttonpane = dialog.$buttons;
5187 setup_form_view: function() {
5190 this.dataset.ids = [this.row_id];
5191 this.dataset.index = 0;
5193 this.dataset.index = null;
5195 var options = _.clone(self.options.form_view_options) || {};
5196 if (this.row_id !== null) {
5197 options.initial_mode = this.options.readonly ? "view" : "edit";
5200 $buttons: this.$buttonpane,
5202 this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
5203 if (this.options.alternative_form_view) {
5204 this.view_form.set_embedded_view(this.options.alternative_form_view);
5206 this.view_form.appendTo(this.$el.find(".oe_popup_form"));
5207 this.view_form.on("form_view_loaded", self, function() {
5208 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
5209 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
5210 multi_select: multi_select,
5211 readonly: self.row_id !== null && self.options.readonly,
5213 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
5214 $snbutton.click(function() {
5215 $.when(self.view_form.save()).done(function() {
5216 self.view_form.reload_mutex.exec(function() {
5217 self.view_form.on_button_new();
5221 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
5222 $sbutton.click(function() {
5223 $.when(self.view_form.save()).done(function() {
5224 self.view_form.reload_mutex.exec(function() {
5229 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
5230 $cbutton.click(function() {
5231 self.view_form.trigger('on_button_cancel');
5234 self.view_form.do_show();
5237 select_elements: function(element_ids) {
5238 this.trigger("elements_selected", element_ids);
5240 check_exit: function(no_destroy) {
5241 if (this.created_elements.length > 0) {
5242 this.select_elements(this.created_elements);
5243 this.created_elements = [];
5245 this.trigger('closed');
5248 destroy: function () {
5249 this.trigger('closed');
5250 if (this.$el.is(":data(bs.modal)")) {
5251 this.$el.parents('.modal').modal('hide');
5258 * Class to display a popup containing a form view.
5260 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
5261 show_element: function(model, row_id, context, options) {
5262 this.init_popup(model, row_id, [], context, options);
5263 _.defaults(this.options, {
5265 this.display_popup();
5269 this.init_dataset();
5270 this.setup_form_view();
5275 * Class to display a popup to display a list to search a row. It also allows
5276 * to switch to a form view to create a new row.
5278 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
5282 * - initial_view: form or search (default search)
5283 * - disable_multiple_selection
5284 * - list_view_options
5286 select_element: function(model, options, domain, context) {
5287 this.init_popup(model, null, domain, context, options);
5289 _.defaults(this.options, {
5290 initial_view: "search",
5292 this.initial_ids = this.options.initial_ids;
5293 this.display_popup();
5297 this.init_dataset();
5298 if (this.options.initial_view == "search") {
5299 instance.web.pyeval.eval_domains_and_contexts({
5301 contexts: [this.context]
5302 }).done(function (results) {
5303 var search_defaults = {};
5304 _.each(results.context, function (value_, key) {
5305 var match = /^search_default_(.*)$/.exec(key);
5307 search_defaults[match[1]] = value_;
5310 self.setup_search_view(search_defaults);
5316 setup_search_view: function(search_defaults) {
5318 if (this.searchview) {
5319 this.searchview.destroy();
5321 this.searchview = new instance.web.SearchView(this,
5322 this.dataset, false, search_defaults);
5323 this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
5324 if (self.initial_ids) {
5325 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
5326 contexts.concat(self.context), groupbys);
5327 self.initial_ids = undefined;
5329 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
5332 this.searchview.on("search_view_loaded", self, function() {
5333 self.view_list = new instance.web.form.SelectCreateListView(self,
5334 self.dataset, false,
5335 _.extend({'deletable': false,
5336 'selectable': !self.options.disable_multiple_selection,
5337 'import_enabled': false,
5338 '$buttons': self.$buttonpane,
5339 'disable_editable_mode': true,
5340 '$pager': self.$('.oe_popup_list_pager'),
5341 }, self.options.list_view_options || {}));
5342 self.view_list.on('edit:before', self, function (e) {
5345 self.view_list.popup = self;
5346 self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
5347 self.view_list.do_show();
5348 }).then(function() {
5349 self.searchview.do_search();
5351 self.view_list.on("list_view_loaded", self, function() {
5352 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
5353 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
5354 $cbutton.click(function() {
5357 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
5358 $sbutton.click(function() {
5359 self.select_elements(self.selected_ids);
5362 $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
5363 $cbutton.click(function() {
5368 this.searchview.appendTo($(".oe_popup_search", self.$el));
5370 do_search: function(domains, contexts, groupbys) {
5372 instance.web.pyeval.eval_domains_and_contexts({
5373 domains: domains || [],
5374 contexts: contexts || [],
5375 group_by_seq: groupbys || []
5376 }).done(function (results) {
5377 self.view_list.do_search(results.domain, results.context, results.group_by);
5380 on_click_element: function(ids) {
5382 this.selected_ids = ids || [];
5383 if(this.selected_ids.length > 0) {
5384 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
5386 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
5389 new_object: function() {
5390 if (this.searchview) {
5391 this.searchview.hide();
5393 if (this.view_list) {
5394 this.view_list.do_hide();
5396 this.setup_form_view();
5400 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
5401 do_add_record: function () {
5402 this.popup.new_object();
5404 select_record: function(index) {
5405 this.popup.select_elements([this.dataset.ids[index]]);
5406 this.popup.destroy();
5408 do_select: function(ids, records) {
5409 this._super(ids, records);
5410 this.popup.on_click_element(ids);
5414 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5415 template: 'FieldReference',
5416 init: function(field_manager, node) {
5417 this._super(field_manager, node);
5418 this.reference_ready = true;
5420 destroy_content: function() {
5423 this.fm = undefined;
5426 initialize_content: function() {
5428 var fm = new instance.web.form.DefaultFieldManager(this);
5430 fm.extend_field_desc({
5432 selection: this.field_manager.get_field_desc(this.name).selection,
5440 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
5442 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5444 this.selection.on("change:value", this, this.on_selection_changed);
5445 this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
5447 .on('focused', null, function () {self.trigger('focused');})
5448 .on('blurred', null, function () {self.trigger('blurred');});
5450 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
5451 name: 'Referenced Document',
5452 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5454 this.m2o.on("change:value", this, this.data_changed);
5455 this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
5457 .on('focused', null, function () {self.trigger('focused');})
5458 .on('blurred', null, function () {self.trigger('blurred');});
5460 on_selection_changed: function() {
5461 if (this.reference_ready) {
5462 this.internal_set_value([this.selection.get_value(), false]);
5463 this.render_value();
5466 data_changed: function() {
5467 if (this.reference_ready) {
5468 this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
5471 set_value: function(val) {
5473 val = val.split(',');
5474 val[0] = val[0] || false;
5475 val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
5477 this._super(val || [false, false]);
5479 get_value: function() {
5480 return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
5482 render_value: function() {
5483 this.reference_ready = false;
5484 if (!this.get("effective_readonly")) {
5485 this.selection.set_value(this.get('value')[0]);
5487 this.m2o.field.relation = this.get('value')[0];
5488 this.m2o.set_value(this.get('value')[1]);
5489 this.m2o.$el.toggle(!!this.get('value')[0]);
5490 this.reference_ready = true;
5494 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5495 init: function(field_manager, node) {
5497 this._super(field_manager, node);
5498 this.binary_value = false;
5499 this.useFileAPI = !!window.FileReader;
5500 this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5501 if (!this.useFileAPI) {
5502 this.fileupload_id = _.uniqueId('oe_fileupload');
5503 $(window).on(this.fileupload_id, function() {
5504 var args = [].slice.call(arguments).slice(1);
5505 self.on_file_uploaded.apply(self, args);
5510 if (!this.useFileAPI) {
5511 $(window).off(this.fileupload_id);
5513 this._super.apply(this, arguments);
5515 initialize_content: function() {
5516 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5517 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5518 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5520 on_file_change: function(e) {
5522 var file_node = e.target;
5523 if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5524 if (this.useFileAPI) {
5525 var file = file_node.files[0];
5526 if (file.size > this.max_upload_size) {
5527 var msg = _t("The selected file exceed the maximum file size of %s.");
5528 instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5531 var filereader = new FileReader();
5532 filereader.readAsDataURL(file);
5533 filereader.onloadend = function(upload) {
5534 var data = upload.target.result;
5535 data = data.split(',')[1];
5536 self.on_file_uploaded(file.size, file.name, file.type, data);
5539 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5540 this.$el.find('form.oe_form_binary_form').submit();
5542 this.$el.find('.oe_form_binary_progress').show();
5543 this.$el.find('.oe_form_binary').hide();
5546 on_file_uploaded: function(size, name, content_type, file_base64) {
5547 if (size === false) {
5548 this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5549 // TODO: use openerp web crashmanager
5550 console.warn("Error while uploading file : ", name);
5552 this.filename = name;
5553 this.on_file_uploaded_and_valid.apply(this, arguments);
5555 this.$el.find('.oe_form_binary_progress').hide();
5556 this.$el.find('.oe_form_binary').show();
5558 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5560 on_save_as: function(ev) {
5561 var value = this.get('value');
5563 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5564 ev.stopPropagation();
5566 instance.web.blockUI();
5567 var c = instance.webclient.crashmanager;
5568 this.session.get_file({
5569 url: '/web/binary/saveas_ajax',
5570 data: {data: JSON.stringify({
5571 model: this.view.dataset.model,
5572 id: (this.view.datarecord.id || ''),
5574 filename_field: (this.node.attrs.filename || ''),
5575 data: instance.web.form.is_bin_size(value) ? null : value,
5576 context: this.view.dataset.get_context()
5578 complete: instance.web.unblockUI,
5579 error: c.rpc_error.bind(c)
5581 ev.stopPropagation();
5585 set_filename: function(value) {
5586 var filename = this.node.attrs.filename;
5589 tmp[filename] = value;
5590 this.field_manager.set_values(tmp);
5593 on_clear: function() {
5594 if (this.get('value') !== false) {
5595 this.binary_value = false;
5596 this.internal_set_value(false);
5602 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5603 template: 'FieldBinaryFile',
5604 initialize_content: function() {
5606 if (this.get("effective_readonly")) {
5608 this.$el.find('a').click(function(ev) {
5609 if (self.get('value')) {
5610 self.on_save_as(ev);
5616 render_value: function() {
5618 if (!this.get("effective_readonly")) {
5619 if (this.node.attrs.filename) {
5620 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5622 show_value = (this.get('value') !== null && this.get('value') !== undefined && this.get('value') !== false) ? this.get('value') : '';
5624 this.$el.find('input').eq(0).val(show_value);
5626 this.$el.find('a').toggle(!!this.get('value'));
5627 if (this.get('value')) {
5628 show_value = _t("Download");
5630 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5631 this.$el.find('a').text(show_value);
5635 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5636 this.binary_value = true;
5637 this.internal_set_value(file_base64);
5638 var show_value = name + " (" + instance.web.human_size(size) + ")";
5639 this.$el.find('input').eq(0).val(show_value);
5640 this.set_filename(name);
5642 on_clear: function() {
5643 this._super.apply(this, arguments);
5644 this.$el.find('input').eq(0).val('');
5645 this.set_filename('');
5649 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5650 template: 'FieldBinaryImage',
5651 placeholder: "/web/static/src/img/placeholder.png",
5652 render_value: function() {
5655 if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5656 url = 'data:image/png;base64,' + this.get('value');
5657 } else if (this.get('value')) {
5658 var id = JSON.stringify(this.view.datarecord.id || null);
5659 var field = this.name;
5660 if (this.options.preview_image)
5661 field = this.options.preview_image;
5662 url = this.session.url('/web/binary/image', {
5663 model: this.view.dataset.model,
5666 t: (new Date().getTime()),
5669 url = this.placeholder;
5671 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5672 $($img).click(function(e) {
5673 if(self.view.get("actual_mode") == "view") {
5674 var $button = $(".oe_form_button_edit");
5675 $button.openerpBounce();
5676 e.stopPropagation();
5679 this.$el.find('> img').remove();
5680 this.$el.prepend($img);
5681 $img.load(function() {
5682 if (! self.options.size)
5684 $img.css("max-width", "" + self.options.size[0] + "px");
5685 $img.css("max-height", "" + self.options.size[1] + "px");
5686 $img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px");
5687 $img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px");
5689 $img.on('error', function() {
5690 $img.attr('src', self.placeholder);
5691 instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5694 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5695 this.internal_set_value(file_base64);
5696 this.binary_value = true;
5697 this.render_value();
5698 this.set_filename(name);
5700 on_clear: function() {
5701 this._super.apply(this, arguments);
5702 this.render_value();
5703 this.set_filename('');
5708 * Widget for (many2many field) to upload one or more file in same time and display in list.
5709 * The user can delete his files.
5710 * Options on attribute ; "blockui" {Boolean} block the UI or not
5711 * during the file is uploading
5713 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
5714 template: "FieldBinaryFileUploader",
5715 init: function(field_manager, node) {
5716 this._super(field_manager, node);
5717 this.field_manager = field_manager;
5719 if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5720 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);
5724 this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5725 this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5726 $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5730 this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5732 set_value: function(value_) {
5733 value_ = value_ || [];
5734 if (value_.length >= 1 && value_[0] instanceof Array) {
5735 value_ = value_[0][2];
5737 this._super(value_);
5739 get_value: function() {
5740 var tmp = [commands.replace_with(this.get("value"))];
5743 get_file_url: function (attachment) {
5744 return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5746 read_name_values : function () {
5748 // don't reset know values
5749 var ids = this.get('value');
5750 var _value = _.filter(ids, function (id) { return typeof self.data[id] == 'undefined'; } );
5751 // send request for get_name
5752 if (_value.length) {
5753 return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).then(function (datas) {
5754 _.each(datas, function (data) {
5755 data.no_unlink = true;
5756 data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5757 self.data[data.id] = data;
5765 render_value: function () {
5767 this.read_name_values().then(function (ids) {
5768 var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids}));
5769 render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5770 self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5772 // reinit input type file
5773 var $input = self.$('input.oe_form_binary_file');
5774 $input.after($input.clone(true)).remove();
5775 self.$(".oe_fileupload").show();
5779 on_file_change: function (event) {
5780 event.stopPropagation();
5782 var $target = $(event.target);
5783 if ($target.val() !== '') {
5784 var filename = $target.val().replace(/.*[\\\/]/,'');
5785 // don't uplode more of one file in same time
5786 if (self.data[0] && self.data[0].upload ) {
5789 for (var id in this.get('value')) {
5790 // if the files exits, delete the file before upload (if it's a new file)
5791 if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
5792 self.ds_file.unlink([id]);
5797 if(this.node.attrs.blockui>0) {
5798 instance.web.blockUI();
5801 // TODO : unactivate send on wizard and form
5804 this.$('form.oe_form_binary_form').submit();
5805 this.$(".oe_fileupload").hide();
5806 // add file on data result
5810 'filename': filename,
5816 on_file_loaded: function (event, result) {
5817 var files = this.get('value');
5820 if(this.node.attrs.blockui>0) {
5821 instance.web.unblockUI();
5824 if (result.error || !result.id ) {
5825 this.do_warn( _t('Uploading Error'), result.error);
5826 delete this.data[0];
5828 if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
5829 delete this.data[0];
5830 this.data[result.id] = {
5832 'name': result.name,
5833 'filename': result.filename,
5834 'url': this.get_file_url(result)
5837 this.data[result.id] = {
5839 'name': result.name,
5840 'filename': result.filename,
5841 'url': this.get_file_url(result)
5844 var values = _.clone(this.get('value'));
5845 values.push(result.id);
5846 this.set({'value': values});
5848 this.render_value();
5850 on_file_delete: function (event) {
5851 event.stopPropagation();
5852 var file_id=$(event.target).data("id");
5854 var files = _.filter(this.get('value'), function (id) {return id != file_id;});
5855 if(!this.data[file_id].no_unlink) {
5856 this.ds_file.unlink([file_id]);
5858 this.set({'value': files});
5863 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5864 template: "FieldStatus",
5865 init: function(field_manager, node) {
5866 this._super(field_manager, node);
5867 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5868 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5869 this.set({value: false});
5870 this.selection = {'unfolded': [], 'folded': []};
5871 this.set("selection", {'unfolded': [], 'folded': []});
5872 this.selection_dm = new instance.web.DropMisordered();
5873 this.dataset = new instance.web.DataSetStatic(this, this.field.relation, this.build_context());
5876 this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5878 this.on("change:value", this, this.get_selection);
5879 this.on("change:evaluated_selection_domain", this, this.get_selection);
5880 this.on("change:selection", this, function() {
5881 this.selection = this.get("selection");
5882 this.render_value();
5884 this.get_selection();
5885 if (this.options.clickable) {
5886 this.$el.on('click','li[data-id]',this.on_click_stage);
5888 if (this.$el.parent().is('header')) {
5889 this.$el.after('<div class="oe_clear"/>');
5893 set_value: function(value_) {
5894 if (value_ instanceof Array) {
5897 this._super(value_);
5899 render_value: function() {
5901 var content = QWeb.render("FieldStatus.content", {
5903 'value_folded': _.find(self.selection.folded, function(i){return i[0] === self.get('value');})
5905 self.$el.html(content);
5907 calc_domain: function() {
5908 var d = instance.web.pyeval.eval('domain', this.build_domain());
5909 var domain = []; //if there is no domain defined, fetch all the records
5912 domain = ['|',['id', '=', this.get('value')]].concat(d);
5915 if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5916 this.set("evaluated_selection_domain", domain);
5919 /** Get the selection and render it
5920 * selection: [[identifier, value_to_display], ...]
5921 * For selection fields: this is directly given by this.field.selection
5922 * For many2one fields: perform a search on the relation of the many2one field
5924 get_selection: function() {
5926 var selection_unfolded = [];
5927 var selection_folded = [];
5928 var fold_field = this.options.fold_field;
5930 var calculation = _.bind(function() {
5931 if (this.field.type == "many2one") {
5932 return self.get_distant_fields().then(function (fields) {
5933 return new instance.web.DataSetSearch(self, self.field.relation, self.build_context(), self.get("evaluated_selection_domain"))
5934 .read_slice(_.union(_.keys(self.distant_fields), ['id']), {}).then(function (records) {
5935 var ids = _.pluck(records, 'id');
5936 return self.dataset.name_get(ids).then(function (records_name) {
5937 _.each(records, function (record) {
5938 var name = _.find(records_name, function (val) {return val[0] == record.id;})[1];
5939 if (fold_field && record[fold_field] && record.id != self.get('value')) {
5940 selection_folded.push([record.id, name]);
5942 selection_unfolded.push([record.id, name]);
5949 // For field type selection filter values according to
5950 // statusbar_visible attribute of the field. For example:
5951 // statusbar_visible="draft,open".
5952 var select = this.field.selection;
5953 for(var i=0; i < select.length; i++) {
5954 var key = select[i][0];
5955 if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5956 selection_unfolded.push(select[i]);
5962 this.selection_dm.add(calculation()).then(function () {
5963 var selection = {'unfolded': selection_unfolded, 'folded': selection_folded};
5964 if (! _.isEqual(selection, self.get("selection"))) {
5965 self.set("selection", selection);
5970 * :deprecated: this feature will probably be removed with OpenERP v8
5972 get_distant_fields: function() {
5974 if (! this.options.fold_field) {
5975 this.distant_fields = {}
5977 if (this.distant_fields) {
5978 return $.when(this.distant_fields);
5980 return new instance.web.Model(self.field.relation).call("fields_get", [[this.options.fold_field]]).then(function(fields) {
5981 self.distant_fields = fields;
5985 on_click_stage: function (ev) {
5987 var $li = $(ev.currentTarget);
5989 if (this.field.type == "many2one") {
5990 val = parseInt($li.data("id"), 10);
5993 val = $li.data("id");
5995 if (val != self.get('value')) {
5996 this.view.recursive_save().done(function() {
5998 change[self.name] = val;
5999 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
6007 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
6008 template: "FieldMonetary",
6009 widget_class: 'oe_form_field_float oe_form_field_monetary',
6011 this._super.apply(this, arguments);
6012 this.set({"currency": false});
6013 if (this.options.currency_field) {
6014 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
6015 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
6018 this.on("change:currency", this, this.get_currency_info);
6019 this.get_currency_info();
6020 this.ci_dm = new instance.web.DropMisordered();
6023 var tmp = this._super();
6024 this.on("change:currency_info", this, this.reinitialize);
6027 get_currency_info: function() {
6029 if (this.get("currency") === false) {
6030 this.set({"currency_info": null});
6033 return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
6034 .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
6035 self.set({"currency_info": res});
6038 parse_value: function(val, def) {
6039 return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6041 format_value: function(val, def) {
6042 return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6047 This type of field display a list of checkboxes. It works only with m2ms. This field will display one checkbox for each
6048 record existing in the model targeted by the relation, according to the given domain if one is specified. Checked records
6049 will be added to the relation.
6051 instance.web.form.FieldMany2ManyCheckBoxes = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6052 className: "oe_form_many2many_checkboxes",
6054 this._super.apply(this, arguments);
6055 this.set("value", {});
6056 this.set("records", []);
6057 this.field_manager.on("view_content_has_changed", this, function() {
6058 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
6059 if (! _.isEqual(domain, this.get("domain"))) {
6060 this.set("domain", domain);
6063 this.records_orderer = new instance.web.DropMisordered();
6065 initialize_field: function() {
6066 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
6067 this.on("change:domain", this, this.query_records);
6068 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
6069 this.on("change:records", this, this.render_value);
6071 query_records: function() {
6073 var model = new openerp.Model(openerp.session, this.field.relation);
6074 this.records_orderer.add(model.call("search", [this.get("domain")], {"context": this.build_context()}).then(function(record_ids) {
6075 return model.call("name_get", [record_ids] , {"context": self.build_context()});
6076 })).then(function(res) {
6077 self.set("records", res);
6080 render_value: function() {
6081 this.$().html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this, selected: this.get("value")}));
6082 var inputs = this.$("input");
6083 inputs.change(_.bind(this.from_dom, this));
6084 if (this.get("effective_readonly"))
6085 inputs.attr("disabled", "true");
6087 from_dom: function() {
6089 this.$("input").each(function() {
6091 new_value[elem.data("record-id")] = elem.attr("checked") ? true : undefined;
6093 if (! _.isEqual(new_value, this.get("value")))
6094 this.internal_set_value(new_value);
6096 set_value: function(value) {
6097 value = value || [];
6098 if (value.length >= 1 && value[0] instanceof Array) {
6099 value = value[0][2];
6102 _.each(value, function(el) {
6103 formatted[JSON.stringify(el)] = true;
6105 this._super(formatted);
6107 get_value: function() {
6108 var value = _.filter(_.keys(this.get("value")), function(el) {
6109 return this.get("value")[el];
6111 value = _.map(value, function(el) {
6112 return JSON.parse(el);
6114 return [commands.replace_with(value)];
6119 This field can be applied on many2many and one2many. It is a read-only field that will display a single link whose name is
6120 "<number of linked records> <label of the field>". When the link is clicked, it will redirect to another act_window
6121 action on the model of the relation and show only the linked records.
6125 * views: The views to display in the act_window action. Must be a list of tuples whose first element is the id of the view
6126 to display (or False to take the default one) and the second element is the type of the view. Defaults to
6127 [[false, "tree"], [false, "form"]] .
6129 instance.web.form.X2ManyCounter = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6130 className: "oe_form_x2many_counter",
6132 this._super.apply(this, arguments);
6133 this.set("value", []);
6134 _.defaults(this.options, {
6135 "views": [[false, "tree"], [false, "form"]],
6138 render_value: function() {
6139 var text = _.str.sprintf("%d %s", this.val().length, this.string);
6140 this.$().html(QWeb.render("X2ManyCounter", {text: text}));
6141 this.$("a").click(_.bind(this.go_to, this));
6144 return this.view.recursive_save().then(_.bind(function() {
6145 var val = this.val();
6147 if (this.field.type === "one2many") {
6148 context["default_" + this.field.relation_field] = this.view.datarecord.id;
6150 var domain = [["id", "in", val]];
6151 return this.do_action({
6152 type: 'ir.actions.act_window',
6154 res_model: this.field.relation,
6155 views: this.options.views,
6163 var value = this.get("value") || [];
6164 if (value.length >= 1 && value[0] instanceof Array) {
6165 value = value[0][2];
6172 This widget is intended to be used on stat button numeric fields. It will display
6173 the value many2many and one2many. It is a read-only field that will
6174 display a simple string "<value of field> <label of the field>"
6176 instance.web.form.StatInfo = instance.web.form.AbstractField.extend({
6177 is_field_number: true,
6179 this._super.apply(this, arguments);
6180 this.internal_set_value(0);
6182 set_value: function(value_) {
6183 if (value_ === false || value_ === undefined) {
6186 this._super.apply(this, [value_]);
6188 render_value: function() {
6190 value: this.get("value") || 0,
6192 if (! this.node.attrs.nolabel) {
6193 options.text = this.string
6195 this.$el.html(QWeb.render("StatInfo", options));
6202 * Registry of form fields, called by :js:`instance.web.FormView`.
6204 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
6205 * will substitute to the <field> tags as defined in OpenERP's views.
6207 instance.web.form.widgets = new instance.web.Registry({
6208 'char' : 'instance.web.form.FieldChar',
6209 'id' : 'instance.web.form.FieldID',
6210 'email' : 'instance.web.form.FieldEmail',
6211 'url' : 'instance.web.form.FieldUrl',
6212 'text' : 'instance.web.form.FieldText',
6213 'html' : 'instance.web.form.FieldTextHtml',
6214 'char_domain': 'instance.web.form.FieldCharDomain',
6215 'date' : 'instance.web.form.FieldDate',
6216 'datetime' : 'instance.web.form.FieldDatetime',
6217 'selection' : 'instance.web.form.FieldSelection',
6218 'radio' : 'instance.web.form.FieldRadio',
6219 'many2one' : 'instance.web.form.FieldMany2One',
6220 'many2onebutton' : 'instance.web.form.Many2OneButton',
6221 'many2many' : 'instance.web.form.FieldMany2Many',
6222 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
6223 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
6224 'one2many' : 'instance.web.form.FieldOne2Many',
6225 'one2many_list' : 'instance.web.form.FieldOne2Many',
6226 'reference' : 'instance.web.form.FieldReference',
6227 'boolean' : 'instance.web.form.FieldBoolean',
6228 'float' : 'instance.web.form.FieldFloat',
6229 'percentpie': 'instance.web.form.FieldPercentPie',
6230 'barchart': 'instance.web.form.FieldBarChart',
6231 'integer': 'instance.web.form.FieldFloat',
6232 'float_time': 'instance.web.form.FieldFloat',
6233 'progressbar': 'instance.web.form.FieldProgressBar',
6234 'image': 'instance.web.form.FieldBinaryImage',
6235 'binary': 'instance.web.form.FieldBinaryFile',
6236 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
6237 'statusbar': 'instance.web.form.FieldStatus',
6238 'monetary': 'instance.web.form.FieldMonetary',
6239 'many2many_checkboxes': 'instance.web.form.FieldMany2ManyCheckBoxes',
6240 'x2many_counter': 'instance.web.form.X2ManyCounter',
6241 'priority':'instance.web.form.Priority',
6242 'kanban_state_selection':'instance.web.form.KanbanSelection',
6243 'statinfo': 'instance.web.form.StatInfo',
6247 * Registry of widgets usable in the form view that can substitute to any possible
6248 * tags defined in OpenERP's form views.
6250 * Every referenced class should extend FormWidget.
6252 instance.web.form.tags = new instance.web.Registry({
6253 'button' : 'instance.web.form.WidgetButton',
6256 instance.web.form.custom_widgets = new instance.web.Registry({
6261 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: