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 if (self.record_id) {
2403 return self.view.dataset._model.call('write', [[self.record_id], value, self.view.dataset.get_context()]).done(self.reload_record.bind(self));
2405 return self.view.on_button_save().done(function(result) {
2407 self.view.dataset._model.call('write', [[result], value, self.view.dataset.get_context()]).done(self.reload_record.bind(self));
2413 reload_record: function() {
2418 instance.web.form.Priority = instance.web.form.FieldChar.extend({
2419 init: function (field_manager, node) {
2420 this._super(field_manager, node);
2422 prepare_priority: function() {
2424 var selection = this.field.selection || [];
2425 var init_value = selection && selection[0][0] || 0;
2426 var data = _.map(selection.slice(1), function(element, index) {
2428 'value': element[0],
2430 'click_value': element[0],
2432 if (index == 0 && self.get('value') == element[0]) {
2433 value['click_value'] = init_value;
2439 render_value: function() {
2441 this.record_id = self.view.datarecord.id;
2442 this.priorities = self.prepare_priority();
2443 this.$el.html(QWeb.render("Priority", {'widget': this}));
2444 this.$el.find('.oe_legend').click(self.do_action.bind(self));
2446 do_action: function(e) {
2448 var li = $(e.target).closest( "li" );
2451 value[self.name] = String(li.data('value'));
2452 if (self.record_id) {
2453 return self.view.dataset._model.call('write', [[self.record_id], value, self.view.dataset.get_context()]).done(self.reload_record.bind(self));
2455 return self.view.on_button_save().done(function(result) {
2457 self.view.dataset._model.call('write', [[result], value, self.view.dataset.get_context()]).done(self.reload_record.bind(self));
2463 reload_record: function() {
2468 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2469 process_modifiers: function () {
2471 this.set({ readonly: true });
2475 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2476 template: 'FieldEmail',
2477 initialize_content: function() {
2479 var $button = this.$el.find('button');
2480 $button.click(this.on_button_clicked);
2481 this.setupFocus($button);
2483 render_value: function() {
2484 if (!this.get("effective_readonly")) {
2488 .attr('href', 'mailto:' + this.get('value'))
2489 .text(this.get('value') || '');
2492 on_button_clicked: function() {
2493 if (!this.get('value') || !this.is_syntax_valid()) {
2494 this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2496 location.href = 'mailto:' + this.get('value');
2501 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2502 template: 'FieldUrl',
2503 initialize_content: function() {
2505 var $button = this.$el.find('button');
2506 $button.click(this.on_button_clicked);
2507 this.setupFocus($button);
2509 render_value: function() {
2510 if (!this.get("effective_readonly")) {
2513 var tmp = this.get('value');
2514 var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2516 tmp = "http://" + this.get('value');
2518 var text = this.get('value') ? this.node.attrs.text || tmp : '';
2519 this.$el.find('a').attr('href', tmp).text(text);
2522 on_button_clicked: function() {
2523 if (!this.get('value')) {
2524 this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2526 var url = $.trim(this.get('value'));
2527 if(/^www\./i.test(url))
2528 url = 'http://'+url;
2534 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2535 is_field_number: true,
2536 widget_class: 'oe_form_field_float',
2537 init: function (field_manager, node) {
2538 this._super(field_manager, node);
2539 this.internal_set_value(0);
2540 if (this.node.attrs.digits) {
2541 this.digits = this.node.attrs.digits;
2543 this.digits = this.field.digits;
2546 set_value: function(value_) {
2547 if (value_ === false || value_ === undefined) {
2548 // As in GTK client, floats default to 0
2551 this._super.apply(this, [value_]);
2553 focus: function () {
2554 var $input = this.$('input:first');
2555 return $input.length ? $input.select() : false;
2559 instance.web.form.FieldCharDomain = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2560 init: function(field_manager, node) {
2561 this._super.apply(this, arguments);
2565 this._super.apply(this, arguments);
2566 this.on("change:effective_readonly", this, function () {
2567 this.display_field();
2568 this.render_value();
2570 this.display_field();
2571 return this._super();
2573 render_value: function() {
2574 this.$('button.select_records').css('visibility', this.get('effective_readonly') ? 'hidden': '');
2576 set_value: function(value_) {
2578 this.set('value', value_ || false);
2579 this.display_field();
2581 display_field: function() {
2583 this.$el.html(instance.web.qweb.render("FieldCharDomain", {widget: this}));
2584 if (this.get('value')) {
2585 var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2586 var domain = instance.web.pyeval.eval('domain', this.get('value'));
2587 var ds = new instance.web.DataSetStatic(self, model, self.build_context());
2588 ds.call('search_count', [domain]).then(function (results) {
2589 $('.oe_domain_count', self.$el).text(results + ' records selected');
2590 $('button span', self.$el).text(' Change selection');
2593 $('.oe_domain_count', this.$el).text('0 record selected');
2594 $('button span', this.$el).text(' Select records');
2596 this.$('.select_records').on('click', self.on_click);
2598 on_click: function(ev) {
2599 event.preventDefault();
2601 var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2602 this.pop = new instance.web.form.SelectCreatePopup(this);
2603 this.pop.select_element(
2604 model, {title: 'Select records...'},
2605 [], this.build_context());
2606 this.pop.on("elements_selected", self, function(element_ids) {
2607 if (this.pop.$('input.oe_list_record_selector').prop('checked')) {
2608 var search_data = this.pop.searchview.build_search_data();
2609 var domain_done = instance.web.pyeval.eval_domains_and_contexts({
2610 domains: search_data.domains,
2611 contexts: search_data.contexts,
2612 group_by_seq: search_data.groupbys || []
2613 }).then(function (results) {
2614 return results.domain;
2618 var domain = [["id", "in", element_ids]];
2619 var domain_done = $.Deferred().resolve(domain);
2621 $.when(domain_done).then(function (domain) {
2622 var domain = self.pop.dataset.domain.concat(domain || []);
2623 self.set_value(domain);
2629 instance.web.DateTimeWidget = instance.web.Widget.extend({
2630 template: "web.datepicker",
2631 jqueryui_object: 'datetimepicker',
2632 type_of_date: "datetime",
2634 'change .oe_datepicker_master': 'change_datetime',
2635 'keypress .oe_datepicker_master': 'change_datetime',
2637 init: function(parent) {
2638 this._super(parent);
2639 this.name = parent.name;
2643 this.$input = this.$el.find('input.oe_datepicker_master');
2644 this.$input_picker = this.$el.find('input.oe_datepicker_container');
2646 $.datepicker.setDefaults({
2647 clearText: _t('Clear'),
2648 clearStatus: _t('Erase the current date'),
2649 closeText: _t('Done'),
2650 closeStatus: _t('Close without change'),
2651 prevText: _t('<Prev'),
2652 prevStatus: _t('Show the previous month'),
2653 nextText: _t('Next>'),
2654 nextStatus: _t('Show the next month'),
2655 currentText: _t('Today'),
2656 currentStatus: _t('Show the current month'),
2657 monthNames: Date.CultureInfo.monthNames,
2658 monthNamesShort: Date.CultureInfo.abbreviatedMonthNames,
2659 monthStatus: _t('Show a different month'),
2660 yearStatus: _t('Show a different year'),
2661 weekHeader: _t('Wk'),
2662 weekStatus: _t('Week of the year'),
2663 dayNames: Date.CultureInfo.dayNames,
2664 dayNamesShort: Date.CultureInfo.abbreviatedDayNames,
2665 dayNamesMin: Date.CultureInfo.shortestDayNames,
2666 dayStatus: _t('Set DD as first week day'),
2667 dateStatus: _t('Select D, M d'),
2668 firstDay: Date.CultureInfo.firstDayOfWeek,
2669 initStatus: _t('Select a date'),
2672 $.timepicker.setDefaults({
2673 timeOnlyTitle: _t('Choose Time'),
2674 timeText: _t('Time'),
2675 hourText: _t('Hour'),
2676 minuteText: _t('Minute'),
2677 secondText: _t('Second'),
2678 currentText: _t('Now'),
2679 closeText: _t('Done')
2683 onClose: this.on_picker_select,
2684 onSelect: this.on_picker_select,
2688 showButtonPanel: true,
2689 firstDay: Date.CultureInfo.firstDayOfWeek
2691 // Some clicks in the datepicker dialog are not stopped by the
2692 // datepicker and "bubble through", unexpectedly triggering the bus's
2693 // click event. Prevent that.
2694 this.picker('widget').click(function (e) { e.stopPropagation(); });
2696 this.$el.find('img.oe_datepicker_trigger').click(function() {
2697 if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
2698 self.$input.focus();
2701 self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date());
2702 self.$input_picker.show();
2703 self.picker('show');
2704 self.$input_picker.hide();
2706 this.set_readonly(false);
2707 this.set({'value': false});
2709 picker: function() {
2710 return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
2712 on_picker_select: function(text, instance_) {
2713 var date = this.picker('getDate');
2715 .val(date ? this.format_client(date) : '')
2719 set_value: function(value_) {
2720 this.set({'value': value_});
2721 this.$input.val(value_ ? this.format_client(value_) : '');
2723 get_value: function() {
2724 return this.get('value');
2726 set_value_from_ui_: function() {
2727 var value_ = this.$input.val() || false;
2728 this.set({'value': this.parse_client(value_)});
2730 set_readonly: function(readonly) {
2731 this.readonly = readonly;
2732 this.$input.prop('readonly', this.readonly);
2733 this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
2735 is_valid_: function() {
2736 var value_ = this.$input.val();
2737 if (value_ === "") {
2741 this.parse_client(value_);
2748 parse_client: function(v) {
2749 return instance.web.parse_value(v, {"widget": this.type_of_date});
2751 format_client: function(v) {
2752 return instance.web.format_value(v, {"widget": this.type_of_date});
2754 change_datetime: function(e) {
2755 if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
2756 this.set_value_from_ui_();
2757 this.trigger("datetime_changed");
2760 commit_value: function () {
2761 this.change_datetime();
2765 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2766 jqueryui_object: 'datepicker',
2767 type_of_date: "date"
2770 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2771 template: "FieldDatetime",
2772 build_widget: function() {
2773 return new instance.web.DateTimeWidget(this);
2775 destroy_content: function() {
2776 if (this.datewidget) {
2777 this.datewidget.destroy();
2778 this.datewidget = undefined;
2781 initialize_content: function() {
2782 if (!this.get("effective_readonly")) {
2783 this.datewidget = this.build_widget();
2784 this.datewidget.on('datetime_changed', this, _.bind(function() {
2785 this.internal_set_value(this.datewidget.get_value());
2787 this.datewidget.appendTo(this.$el);
2788 this.setupFocus(this.datewidget.$input);
2791 render_value: function() {
2792 if (!this.get("effective_readonly")) {
2793 this.datewidget.set_value(this.get('value'));
2795 this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2798 is_syntax_valid: function() {
2799 if (!this.get("effective_readonly") && this.datewidget) {
2800 return this.datewidget.is_valid_();
2804 is_false: function() {
2805 return this.get('value') === '' || this._super();
2808 var input = this.datewidget && this.datewidget.$input[0];
2809 return input ? input.focus() : false;
2811 set_dimensions: function (height, width) {
2812 this._super(height, width);
2813 this.datewidget.$input.css('height', height);
2817 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2818 template: "FieldDate",
2819 build_widget: function() {
2820 return new instance.web.DateWidget(this);
2824 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2825 template: 'FieldText',
2827 'keyup': function (e) {
2828 if (e.which === $.ui.keyCode.ENTER) {
2829 e.stopPropagation();
2832 'keypress': function (e) {
2833 if (e.which === $.ui.keyCode.ENTER) {
2834 e.stopPropagation();
2837 'change textarea': 'store_dom_value',
2839 initialize_content: function() {
2841 if (! this.get("effective_readonly")) {
2842 this.$textarea = this.$el.find('textarea');
2843 this.auto_sized = false;
2844 this.default_height = this.$textarea.css('height');
2845 if (this.get("effective_readonly")) {
2846 this.$textarea.attr('disabled', 'disabled');
2848 this.setupFocus(this.$textarea);
2850 this.$textarea = undefined;
2853 commit_value: function () {
2854 if (! this.get("effective_readonly") && this.$textarea) {
2855 this.store_dom_value();
2857 return this._super();
2859 store_dom_value: function () {
2860 this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2862 render_value: function() {
2863 if (! this.get("effective_readonly")) {
2864 var show_value = instance.web.format_value(this.get('value'), this, '');
2865 if (show_value === '') {
2866 this.$textarea.css('height', parseInt(this.default_height, 10)+"px");
2868 this.$textarea.val(show_value);
2869 if (! this.auto_sized) {
2870 this.auto_sized = true;
2871 this.$textarea.autosize();
2873 this.$textarea.trigger("autosize");
2876 var txt = this.get("value") || '';
2877 this.$(".oe_form_text_content").text(txt);
2880 is_syntax_valid: function() {
2881 if (!this.get("effective_readonly") && this.$textarea) {
2883 instance.web.parse_value(this.$textarea.val(), this, '');
2891 is_false: function() {
2892 return this.get('value') === '' || this._super();
2894 focus: function($el) {
2895 var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2896 return input ? input.focus() : false;
2898 set_dimensions: function (height, width) {
2899 this._super(height, width);
2900 if (!this.get("effective_readonly") && this.$textarea) {
2901 this.$textarea.css({
2910 * FieldTextHtml Widget
2911 * Intended for FieldText widgets meant to display HTML content. This
2912 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2913 * To find more information about CLEditor configutation: go to
2914 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2916 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2917 template: 'FieldTextHtml',
2919 this._super.apply(this, arguments);
2921 initialize_content: function() {
2923 if (! this.get("effective_readonly")) {
2924 self._updating_editor = false;
2925 this.$textarea = this.$el.find('textarea');
2926 var width = ((this.node.attrs || {}).editor_width || '100%');
2927 var height = ((this.node.attrs || {}).editor_height || 250);
2928 this.$textarea.cleditor({
2929 width: width, // width not including margins, borders or padding
2930 height: height, // height not including margins, borders or padding
2931 controls: // controls to add to the toolbar
2932 "bold italic underline strikethrough " +
2933 "| removeformat | bullets numbering | outdent " +
2934 "indent | link unlink | source",
2935 bodyStyle: // style to assign to document body contained within the editor
2936 "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2938 this.$cleditor = this.$textarea.cleditor()[0];
2939 this.$cleditor.change(function() {
2940 if (! self._updating_editor) {
2941 self.$cleditor.updateTextArea();
2942 self.internal_set_value(self.$textarea.val());
2945 if (this.field.translate) {
2946 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"/>')
2947 .click(this.on_translate);
2948 this.$cleditor.$toolbar.append($img);
2952 render_value: function() {
2953 if (! this.get("effective_readonly")) {
2954 this.$textarea.val(this.get('value') || '');
2955 this._updating_editor = true;
2956 this.$cleditor.updateFrame();
2957 this._updating_editor = false;
2959 this.$el.html(this.get('value'));
2964 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2965 template: 'FieldBoolean',
2968 this.$checkbox = $("input", this.$el);
2969 this.setupFocus(this.$checkbox);
2970 this.$el.click(_.bind(function() {
2971 this.internal_set_value(this.$checkbox.is(':checked'));
2973 var check_readonly = function() {
2974 self.$checkbox.prop('disabled', self.get("effective_readonly"));
2975 self.click_disabled_boolean();
2977 this.on("change:effective_readonly", this, check_readonly);
2978 check_readonly.call(this);
2979 this._super.apply(this, arguments);
2981 render_value: function() {
2982 this.$checkbox[0].checked = this.get('value');
2985 var input = this.$checkbox && this.$checkbox[0];
2986 return input ? input.focus() : false;
2988 click_disabled_boolean: function(){
2989 var $disabled = this.$el.find('input[type=checkbox]:disabled');
2990 $disabled.each(function (){
2991 $(this).next('div').remove();
2992 $(this).closest("span").append($('<div class="boolean"></div>'));
2998 The progressbar field expect a float from 0 to 100.
3000 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
3001 template: 'FieldProgressBar',
3002 render_value: function() {
3003 this.$el.progressbar({
3004 value: this.get('value') || 0,
3005 disabled: this.get("effective_readonly")
3007 var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
3008 this.$('span').html(formatted_value + '%');
3013 The PercentPie field expect a float from 0 to 100.
3015 instance.web.form.FieldPercentPie = instance.web.form.AbstractField.extend({
3016 template: 'FieldPercentPie',
3018 render_value: function() {
3019 var value = this.get('value'),
3020 formatted_value = Math.round(value || 0) + '%',
3021 svg = this.$('svg')[0];
3024 nv.addGraph(function() {
3025 var width = 42, height = 42;
3026 var chart = nv.models.pieChart()
3029 .margin({top: 0, right: 0, bottom: 0, left: 0})
3034 .color(['#7C7BAD','#DDD'])
3038 .datum([{'x': 'value', 'y': value}, {'x': 'complement', 'y': 100 - value}])
3041 .attr('style', 'width: ' + width + 'px; height:' + height + 'px;');
3045 .attr({x: width/2, y: height/2 + 3, 'text-anchor': 'middle'})
3046 .style({"font-size": "10px", "font-weight": "bold"})
3047 .text(formatted_value);
3056 The FieldBarChart expectsa list of values (indeed)
3058 instance.web.form.FieldBarChart = instance.web.form.AbstractField.extend({
3059 template: 'FieldBarChart',
3061 render_value: function() {
3062 var value = JSON.parse(this.get('value'));
3063 var svg = this.$('svg')[0];
3065 nv.addGraph(function() {
3066 var width = 34, height = 34;
3067 var chart = nv.models.discreteBarChart()
3068 .x(function (d) { return d.tooltip })
3069 .y(function (d) { return d.value })
3072 .margin({top: 0, right: 0, bottom: 0, left: 0})
3075 .transitionDuration(350)
3080 .datum([{key: 'values', values: value}])
3083 .attr('style', 'width: ' + (width + 4) + 'px; height: ' + (height + 8) + 'px;');
3085 nv.utils.windowResize(chart.update);
3094 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3095 template: 'FieldSelection',
3097 'change select': 'store_dom_value',
3099 init: function(field_manager, node) {
3101 this._super(field_manager, node);
3102 this.set("value", false);
3103 this.set("values", []);
3104 this.records_orderer = new instance.web.DropMisordered();
3105 this.field_manager.on("view_content_has_changed", this, function() {
3106 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
3107 if (! _.isEqual(domain, this.get("domain"))) {
3108 this.set("domain", domain);
3112 initialize_field: function() {
3113 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3114 this.on("change:domain", this, this.query_values);
3115 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
3116 this.on("change:values", this, this.render_value);
3118 query_values: function() {
3121 if (this.field.type === "many2one") {
3122 var model = new openerp.Model(openerp.session, this.field.relation);
3123 def = model.call("name_search", ['', this.get("domain")], {"context": this.build_context()});
3125 var values = _.reject(this.field.selection, function (v) { return v[0] === false && v[1] === ''; });
3126 def = $.when(values);
3128 this.records_orderer.add(def).then(function(values) {
3129 if (! _.isEqual(values, self.get("values"))) {
3130 self.set("values", values);
3134 initialize_content: function() {
3135 // Flag indicating whether we're in an event chain containing a change
3136 // event on the select, in order to know what to do on keyup[RETURN]:
3137 // * If the user presses [RETURN] as part of changing the value of a
3138 // selection, we should just let the value change and not let the
3139 // event broadcast further (e.g. to validating the current state of
3140 // the form in editable list view, which would lead to saving the
3141 // current row or switching to the next one)
3142 // * If the user presses [RETURN] with a select closed (side-effect:
3143 // also if the user opened the select and pressed [RETURN] without
3144 // changing the selected value), takes the action as validating the
3146 var ischanging = false;
3147 var $select = this.$el.find('select')
3148 .change(function () { ischanging = true; })
3149 .click(function () { ischanging = false; })
3150 .keyup(function (e) {
3151 if (e.which !== 13 || !ischanging) { return; }
3152 e.stopPropagation();
3155 this.setupFocus($select);
3157 commit_value: function () {
3158 this.store_dom_value();
3159 return this._super();
3161 store_dom_value: function () {
3162 if (!this.get('effective_readonly') && this.$('select').length) {
3163 var val = JSON.parse(this.$('select').val());
3164 this.internal_set_value(val);
3167 set_value: function(value_) {
3168 value_ = value_ === null ? false : value_;
3169 value_ = value_ instanceof Array ? value_[0] : value_;
3170 this._super(value_);
3172 render_value: function() {
3173 var values = this.get("values");
3174 values = [[false, this.node.attrs.placeholder || '']].concat(values);
3175 var found = _.find(values, function(el) { return el[0] === this.get("value"); }, this);
3177 found = [this.get("value"), _t('Unknown')];
3178 values = [found].concat(values);
3180 if (! this.get("effective_readonly")) {
3181 this.$().html(QWeb.render("FieldSelectionSelect", {widget: this, values: values}));
3182 this.$("select").val(JSON.stringify(found[0]));
3184 this.$el.text(found[1]);
3188 var input = this.$('select:first')[0];
3189 return input ? input.focus() : false;
3191 set_dimensions: function (height, width) {
3192 this._super(height, width);
3193 this.$('select').css({
3200 instance.web.form.FieldRadio = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3201 template: 'FieldRadio',
3203 'click input': 'click_change_value'
3205 init: function(field_manager, node) {
3206 /* Radio button widget: Attributes options:
3207 * - "horizontal" to display in column
3208 * - "no_radiolabel" don't display text values
3210 this._super(field_manager, node);
3211 this.selection = _.clone(this.field.selection) || [];
3212 this.domain = false;
3214 initialize_content: function () {
3215 this.uniqueId = _.uniqueId("radio");
3216 this.on("change:effective_readonly", this, this.render_value);
3217 this.field_manager.on("view_content_has_changed", this, this.get_selection);
3218 this.get_selection();
3220 click_change_value: function (event) {
3221 var val = $(event.target).val();
3222 val = this.field.type == "selection" ? val : +val;
3223 if (val == this.get_value()) {
3224 this.set_value(false);
3226 this.set_value(val);
3229 /** Get the selection and render it
3230 * selection: [[identifier, value_to_display], ...]
3231 * For selection fields: this is directly given by this.field.selection
3232 * For many2one fields: perform a search on the relation of the many2one field
3234 get_selection: function() {
3237 var def = $.Deferred();
3238 if (self.field.type == "many2one") {
3239 var domain = instance.web.pyeval.eval('domain', this.build_domain()) || [];
3240 if (! _.isEqual(self.domain, domain)) {
3241 self.domain = domain;
3242 var ds = new instance.web.DataSetStatic(self, self.field.relation, self.build_context());
3243 ds.call('search', [self.domain])
3244 .then(function (records) {
3245 ds.name_get(records).then(function (records) {
3246 selection = records;
3251 selection = self.selection;
3255 else if (self.field.type == "selection") {
3256 selection = self.field.selection || [];
3259 return def.then(function () {
3260 if (! _.isEqual(selection, self.selection)) {
3261 self.selection = _.clone(selection);
3262 self.renderElement();
3263 self.render_value();
3267 set_value: function (value_) {
3269 if (this.field.type == "selection") {
3270 value_ = _.find(this.field.selection, function (sel) { return sel[0] == value_;});
3272 else if (!this.selection.length) {
3273 this.selection = [value_];
3276 this._super(value_);
3278 get_value: function () {
3279 var value = this.get('value');
3280 return value instanceof Array ? value[0] : value;
3282 render_value: function () {
3284 this.$el.toggleClass("oe_readonly", this.get('effective_readonly'));
3285 this.$("input:checked").prop("checked", false);
3286 if (this.get_value()) {
3287 this.$("input").filter(function () {return this.value == self.get_value();}).prop("checked", true);
3288 this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : "");
3293 // jquery autocomplete tweak to allow html and classnames
3295 var proto = $.ui.autocomplete.prototype,
3296 initSource = proto._initSource;
3298 function filter( array, term ) {
3299 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
3300 return $.grep( array, function(value_) {
3301 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
3306 _initSource: function() {
3307 if ( this.options.html && $.isArray(this.options.source) ) {
3308 this.source = function( request, response ) {
3309 response( filter( this.options.source, request.term ) );
3312 initSource.call( this );
3316 _renderItem: function( ul, item) {
3317 return $( "<li></li>" )
3318 .data( "item.autocomplete", item )
3319 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
3321 .addClass(item.classname);
3327 A mixin containing some useful methods to handle completion inputs.
3329 The widget containing this option can have these arguments in its widget options:
3330 - no_quick_create: if true, it will disable the quick create
3332 instance.web.form.CompletionFieldMixin = {
3335 this.orderer = new instance.web.DropMisordered();
3338 * Call this method to search using a string.
3340 get_search_result: function(search_val) {
3343 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
3344 var blacklist = this.get_search_blacklist();
3345 this.last_query = search_val;
3347 return this.orderer.add(dataset.name_search(
3348 search_val, new instance.web.CompoundDomain(self.build_domain(), [["id", "not in", blacklist]]),
3349 'ilike', this.limit + 1, self.build_context())).then(function(data) {
3350 self.last_search = data;
3351 // possible selections for the m2o
3352 var values = _.map(data, function(x) {
3353 x[1] = x[1].split("\n")[0];
3355 label: _.str.escapeHTML(x[1]),
3362 // search more... if more results that max
3363 if (values.length > self.limit) {
3364 values = values.slice(0, self.limit);
3366 label: _t("Search More..."),
3367 action: function() {
3368 dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
3369 self._search_create_popup("search", data);
3372 classname: 'oe_m2o_dropdown_option'
3376 var raw_result = _(data.result).map(function(x) {return x[1];});
3377 if (search_val.length > 0 && !_.include(raw_result, search_val) &&
3378 ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3380 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
3381 $('<span />').text(search_val).html()),
3382 action: function() {
3383 self._quick_create(search_val);
3385 classname: 'oe_m2o_dropdown_option'
3389 if (!(self.options && self.options.no_create)){
3391 label: _t("Create and Edit..."),
3392 action: function() {
3393 self._search_create_popup("form", undefined, self._create_context(search_val));
3395 classname: 'oe_m2o_dropdown_option'
3398 else if (values.length == 0)
3400 label: _t("No results to show..."),
3401 action: function() {},
3402 classname: 'oe_m2o_dropdown_option'
3408 get_search_blacklist: function() {
3411 _quick_create: function(name) {
3413 var slow_create = function () {
3414 self._search_create_popup("form", undefined, self._create_context(name));
3416 if (self.options.quick_create === undefined || self.options.quick_create) {
3417 new instance.web.DataSet(this, this.field.relation, self.build_context())
3418 .name_create(name).done(function(data) {
3419 if (!self.get('effective_readonly'))
3420 self.add_id(data[0]);
3421 }).fail(function(error, event) {
3422 event.preventDefault();
3428 // all search/create popup handling
3429 _search_create_popup: function(view, ids, context) {
3431 var pop = new instance.web.form.SelectCreatePopup(this);
3433 self.field.relation,
3435 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3436 initial_ids: ids ? _.map(ids, function(x) {return x[0];}) : undefined,
3438 disable_multiple_selection: true
3440 self.build_domain(),
3441 new instance.web.CompoundContext(self.build_context(), context || {})
3443 pop.on("elements_selected", self, function(element_ids) {
3444 self.add_id(element_ids[0]);
3451 add_id: function(id) {},
3452 _create_context: function(name) {
3454 var field = (this.options || {}).create_name_field;
3455 if (field === undefined)
3457 if (field !== false && name && (this.options || {}).quick_create !== false)
3458 tmp["default_" + field] = name;
3463 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3464 template: "M2ODialog",
3465 init: function(parent) {
3466 this.name = parent.string;
3467 this._super(parent, {
3468 title: _.str.sprintf(_t("Create a %s"), parent.string),
3474 var text = _.str.sprintf(_t("You are creating a new %s, are you sure it does not exist yet?"), self.name);
3475 this.$("p").text( text );
3476 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3477 this.$("input").val(this.getParent().last_query);
3478 this.$buttons.find(".oe_form_m2o_qc_button").click(function(){
3479 self.getParent()._quick_create(self.$("input").val());
3482 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3483 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3486 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3492 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3493 template: "FieldMany2One",
3495 'keydown input': function (e) {
3497 case $.ui.keyCode.UP:
3498 case $.ui.keyCode.DOWN:
3499 e.stopPropagation();
3503 init: function(field_manager, node) {
3504 this._super(field_manager, node);
3505 instance.web.form.CompletionFieldMixin.init.call(this);
3506 this.set({'value': false});
3507 this.display_value = {};
3508 this.display_value_backup = {};
3509 this.last_search = [];
3510 this.floating = false;
3511 this.current_display = null;
3512 this.is_started = false;
3514 reinit_value: function(val) {
3515 this.internal_set_value(val);
3516 this.floating = false;
3517 if (this.is_started)
3518 this.render_value();
3520 initialize_field: function() {
3521 this.is_started = true;
3522 instance.web.bus.on('click', this, function() {
3523 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3524 this.$input.autocomplete("close");
3527 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3529 initialize_content: function() {
3530 if (!this.get("effective_readonly"))
3531 this.render_editable();
3533 destroy_content: function () {
3534 if (this.$drop_down) {
3535 this.$drop_down.off('click');
3536 delete this.$drop_down;
3539 this.$input.closest(".modal .modal-content").off('scroll');
3540 this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3541 'focus focusout change keydown');
3544 if (this.$follow_button) {
3545 this.$follow_button.off('blur focus click');
3546 delete this.$follow_button;
3549 destroy: function () {
3550 this.destroy_content();
3551 return this._super();
3553 init_error_displayer: function() {
3556 hide_error_displayer: function() {
3559 show_error_displayer: function() {
3560 new instance.web.form.M2ODialog(this).open();
3562 render_editable: function() {
3564 this.$input = this.$el.find("input");
3566 this.init_error_displayer();
3568 self.$input.on('focus', function() {
3569 self.hide_error_displayer();
3572 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3573 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3575 this.$follow_button.click(function(ev) {
3576 ev.preventDefault();
3577 if (!self.get('value')) {
3581 var pop = new instance.web.form.FormOpenPopup(self);
3582 var context = self.build_context().eval();
3583 var model_obj = new instance.web.Model(self.field.relation);
3584 model_obj.call('get_formview_id', [self.get("value"), context]).then(function(view_id){
3586 self.field.relation,
3588 self.build_context(),
3590 title: _t("Open: ") + self.string,
3594 pop.on('write_completed', self, function(){
3595 self.display_value = {};
3596 self.display_value_backup = {};
3597 self.render_value();
3599 self.trigger('changed_value');
3604 // some behavior for input
3605 var input_changed = function() {
3606 if (self.current_display !== self.$input.val()) {
3607 self.current_display = self.$input.val();
3608 if (self.$input.val() === "") {
3609 self.internal_set_value(false);
3610 self.floating = false;
3612 self.floating = true;
3616 this.$input.keydown(input_changed);
3617 this.$input.change(input_changed);
3618 this.$drop_down.click(function() {
3619 self.$input.focus();
3620 if (self.$input.autocomplete("widget").is(":visible")) {
3621 self.$input.autocomplete("close");
3623 if (self.get("value") && ! self.floating) {
3624 self.$input.autocomplete("search", "");
3626 self.$input.autocomplete("search");
3631 // Autocomplete close on dialog content scroll
3632 var close_autocomplete = _.debounce(function() {
3633 if (self.$input.autocomplete("widget").is(":visible")) {
3634 self.$input.autocomplete("close");
3637 this.$input.closest(".modal .modal-content").on('scroll', this, close_autocomplete);
3639 self.ed_def = $.Deferred();
3640 self.uned_def = $.Deferred();
3642 var ed_duration = 15000;
3643 var anyoneLoosesFocus = function (e) {
3645 if (self.floating) {
3646 if (self.last_search.length > 0) {
3647 if (self.last_search[0][0] != self.get("value")) {
3648 self.display_value = {};
3649 self.display_value_backup = {};
3650 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3651 self.reinit_value(self.last_search[0][0]);
3654 self.render_value();
3658 self.reinit_value(false);
3660 self.floating = false;
3662 if (used && self.get("value") === false && ! self.no_ed && (self.options.no_create === false || self.options.no_create === undefined)) {
3663 self.ed_def.reject();
3664 self.uned_def.reject();
3665 self.ed_def = $.Deferred();
3666 self.ed_def.done(function() {
3667 self.show_error_displayer();
3668 ignore_blur = false;
3669 self.trigger('focused');
3672 setTimeout(function() {
3673 self.ed_def.resolve();
3674 self.uned_def.reject();
3675 self.uned_def = $.Deferred();
3676 self.uned_def.done(function() {
3677 self.hide_error_displayer();
3679 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3683 self.ed_def.reject();
3686 var ignore_blur = false;
3688 focusout: anyoneLoosesFocus,
3689 focus: function () { self.trigger('focused'); },
3690 autocompleteopen: function () { ignore_blur = true; },
3691 autocompleteclose: function () { ignore_blur = false; },
3693 // autocomplete open
3694 if (ignore_blur) { return; }
3695 if (_(self.getChildren()).any(function (child) {
3696 return child instanceof instance.web.form.AbstractFormPopup;
3698 self.trigger('blurred');
3702 var isSelecting = false;
3704 this.$input.autocomplete({
3705 source: function(req, resp) {
3706 self.get_search_result(req.term).done(function(result) {
3710 select: function(event, ui) {
3714 self.display_value = {};
3715 self.display_value_backup = {};
3716 self.display_value["" + item.id] = item.name;
3717 self.reinit_value(item.id);
3718 } else if (item.action) {
3720 // Cancel widget blurring, to avoid form blur event
3721 self.trigger('focused');
3725 focus: function(e, ui) {
3729 // disabled to solve a bug, but may cause others
3730 //close: anyoneLoosesFocus,
3734 // set position for list of suggestions box
3735 this.$input.autocomplete( "option", "position", { my : "left top", at: "left bottom" } );
3736 this.$input.autocomplete("widget").openerpClass();
3737 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3738 this.$input.keyup(function(e) {
3739 if (e.which === 13) { // ENTER
3741 e.stopPropagation();
3743 isSelecting = false;
3745 this.setupFocus(this.$follow_button);
3747 render_value: function(no_recurse) {
3749 if (! this.get("value")) {
3750 this.display_string("");
3753 var display = this.display_value["" + this.get("value")];
3755 this.display_string(display);
3759 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3760 this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3762 self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
3765 self.display_value["" + self.get("value")] = data[0][1];
3766 self.render_value(true);
3767 }).fail( function (data, event) {
3768 // avoid displaying crash errors as many2One should be name_get compliant
3769 event.preventDefault();
3770 self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")];
3771 self.render_value(true);
3775 display_string: function(str) {
3777 if (!this.get("effective_readonly")) {
3778 this.$input.val(str.split("\n")[0]);
3779 this.current_display = this.$input.val();
3780 if (this.is_false()) {
3781 this.$('.oe_m2o_cm_button').css({'display':'none'});
3783 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3786 var lines = _.escape(str).split("\n");
3790 follow = _.rest(lines).join("<br />");
3793 var $link = this.$el.find('.oe_form_uri')
3796 if (! this.options.no_open)
3797 $link.click(function () {
3798 var context = self.build_context().eval();
3799 var model_obj = new instance.web.Model(self.field.relation);
3800 model_obj.call('get_formview_action', [self.get("value"), context]).then(function(action){
3801 self.do_action(action);
3805 $(".oe_form_m2o_follow", this.$el).html(follow);
3808 set_value: function(value_) {
3810 if (value_ instanceof Array) {
3811 this.display_value = {};
3812 this.display_value_backup = {};
3813 if (! this.options.always_reload) {
3814 this.display_value["" + value_[0]] = value_[1];
3817 this.display_value_backup["" + value_[0]] = value_[1];
3821 value_ = value_ || false;
3822 this.reinit_value(value_);
3824 get_displayed: function() {
3825 return this.display_value["" + this.get("value")];
3827 add_id: function(id) {
3828 this.display_value = {};
3829 this.display_value_backup = {};
3830 this.reinit_value(id);
3832 is_false: function() {
3833 return ! this.get("value");
3835 focus: function () {
3836 var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3837 return input ? input.focus() : false;
3839 _quick_create: function() {
3841 this.ed_def.reject();
3842 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3844 _search_create_popup: function() {
3846 this.ed_def.reject();
3847 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3849 set_dimensions: function (height, width) {
3850 this._super(height, width);
3851 this.$input.css('height', height);
3855 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3856 template: 'Many2OneButton',
3857 init: function(field_manager, node) {
3858 this._super.apply(this, arguments);
3861 this._super.apply(this, arguments);
3864 set_button: function() {
3867 this.$button.remove();
3870 this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3871 this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3872 this.$button.addClass('oe_link').css({'padding':'4px'});
3873 this.$el.append(this.$button);
3874 this.$button.on('click', self.on_click);
3876 on_click: function(ev) {
3878 this.popup = new instance.web.form.FormOpenPopup(this);
3879 this.popup.show_element(
3880 this.field.relation,
3882 this.build_context(),
3883 {title: this.string}
3885 this.popup.on('create_completed', self, function(r) {
3889 set_value: function(value_) {
3891 if (value_ instanceof Array) {
3894 value_ = value_ || false;
3895 this.set('value', value_);
3901 * Abstract-ish ListView.List subclass adding an "Add an item" row to replace
3902 * the big ugly button in the header.
3904 * Requires the implementation of a ``is_readonly`` method (usually a proxy to
3905 * the corresponding field's readonly or effective_readonly property) to
3906 * decide whether the special row should or should not be inserted.
3908 * Optionally an ``_add_row_class`` attribute can be set for the class(es) to
3909 * set on the insertion row.
3911 instance.web.form.AddAnItemList = instance.web.ListView.List.extend({
3912 pad_table_to: function (count) {
3913 if (!this.view.is_action_enabled('create') || this.is_readonly()) {
3918 this._super(count > 0 ? count - 1 : 0);
3921 var columns = _(this.columns).filter(function (column) {
3922 return column.invisible !== '1';
3924 if (this.options.selectable) { columns++; }
3925 if (this.options.deletable) { columns++; }
3927 var $cell = $('<td>', {
3929 'class': this._add_row_class || ''
3931 $('<a>', {href: '#'}).text(_t("Add an item"))
3932 .mousedown(function () {
3933 // FIXME: needs to be an official API somehow
3934 if (self.view.editor.is_editing()) {
3935 self.view.__ignore_blur = true;
3938 .click(function (e) {
3940 e.stopPropagation();
3941 // FIXME: there should also be an API for that one
3942 if (self.view.editor.form.__blur_timeout) {
3943 clearTimeout(self.view.editor.form.__blur_timeout);
3944 self.view.editor.form.__blur_timeout = false;
3946 self.view.ensure_saved().done(function () {
3947 self.view.do_add_record();
3951 var $padding = this.$current.find('tr:not([data-id]):first');
3952 var $newrow = $('<tr>').append($cell);
3953 if ($padding.length) {
3954 $padding.before($newrow);
3956 this.$current.append($newrow)
3962 # Values: (0, 0, { fields }) create
3963 # (1, ID, { fields }) update
3964 # (2, ID) remove (delete)
3965 # (3, ID) unlink one (target id or target of relation)
3967 # (5) unlink all (only valid for one2many)
3972 'create': function (values) {
3973 return [commands.CREATE, false, values];
3975 // (1, id, {values})
3977 'update': function (id, values) {
3978 return [commands.UPDATE, id, values];
3982 'delete': function (id) {
3983 return [commands.DELETE, id, false];
3985 // (3, id[, _]) removes relation, but not linked record itself
3987 'forget': function (id) {
3988 return [commands.FORGET, id, false];
3992 'link_to': function (id) {
3993 return [commands.LINK_TO, id, false];
3997 'delete_all': function () {
3998 return [5, false, false];
4000 // (6, _, ids) replaces all linked records with provided ids
4002 'replace_with': function (ids) {
4003 return [6, false, ids];
4006 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
4007 multi_selection: false,
4008 disable_utility_classes: true,
4009 init: function(field_manager, node) {
4010 this._super(field_manager, node);
4011 lazy_build_o2m_kanban_view();
4012 this.is_loaded = $.Deferred();
4013 this.initial_is_loaded = this.is_loaded;
4014 this.form_last_update = $.Deferred();
4015 this.init_form_last_update = this.form_last_update;
4016 this.is_started = false;
4017 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
4018 this.dataset.o2m = this;
4019 this.dataset.parent_view = this.view;
4020 this.dataset.child_name = this.name;
4022 this.dataset.on('dataset_changed', this, function() {
4023 self.trigger_on_change();
4028 this._super.apply(this, arguments);
4029 this.$el.addClass('oe_form_field oe_form_field_one2many');
4034 this.is_loaded.done(function() {
4035 self.on("change:effective_readonly", self, function() {
4036 self.is_loaded = self.is_loaded.then(function() {
4037 self.viewmanager.destroy();
4038 return $.when(self.load_views()).done(function() {
4039 self.reload_current_view();
4044 this.is_started = true;
4045 this.reload_current_view();
4047 trigger_on_change: function() {
4048 this.trigger('changed_value');
4050 load_views: function() {
4053 var modes = this.node.attrs.mode;
4054 modes = !!modes ? modes.split(",") : ["tree"];
4056 _.each(modes, function(mode) {
4057 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
4058 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
4062 view_type: mode == "tree" ? "list" : mode,
4065 if (self.field.views && self.field.views[mode]) {
4066 view.embedded_view = self.field.views[mode];
4068 if(view.view_type === "list") {
4069 _.extend(view.options, {
4071 selectable: self.multi_selection,
4073 import_enabled: false,
4076 if (self.get("effective_readonly")) {
4077 _.extend(view.options, {
4082 } else if (view.view_type === "form") {
4083 if (self.get("effective_readonly")) {
4084 view.view_type = 'form';
4086 _.extend(view.options, {
4087 not_interactible_on_create: true,
4089 } else if (view.view_type === "kanban") {
4090 _.extend(view.options, {
4091 confirm_on_delete: false,
4093 if (self.get("effective_readonly")) {
4094 _.extend(view.options, {
4095 action_buttons: false,
4096 quick_creatable: false,
4098 read_only_mode: true,
4106 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
4107 this.viewmanager.o2m = self;
4108 var once = $.Deferred().done(function() {
4109 self.init_form_last_update.resolve();
4111 var def = $.Deferred().done(function() {
4112 self.initial_is_loaded.resolve();
4114 this.viewmanager.on("controller_inited", self, function(view_type, controller) {
4115 controller.o2m = self;
4116 if (view_type == "list") {
4117 if (self.get("effective_readonly")) {
4118 controller.on('edit:before', self, function (e) {
4121 _(controller.columns).find(function (column) {
4122 if (!(column instanceof instance.web.list.Handle)) {
4125 column.modifiers.invisible = true;
4129 } else if (view_type === "form") {
4130 if (self.get("effective_readonly")) {
4131 $(".oe_form_buttons", controller.$el).children().remove();
4133 controller.on("load_record", self, function(){
4136 controller.on('pager_action_executed',self,self.save_any_view);
4137 } else if (view_type == "graph") {
4138 self.reload_current_view();
4142 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
4143 $.when(self.save_any_view()).done(function() {
4144 if (n_mode === "list") {
4145 $.async_when().done(function() {
4146 self.reload_current_view();
4151 $.async_when().done(function () {
4152 self.viewmanager.appendTo(self.$el);
4156 reload_current_view: function() {
4158 self.is_loaded = self.is_loaded.then(function() {
4159 var active_view = self.viewmanager.active_view;
4160 var view = self.viewmanager.views[active_view].controller;
4161 if(active_view === "list") {
4162 return view.reload_content();
4163 } else if (active_view === "form") {
4164 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
4165 self.dataset.index = 0;
4167 var act = function() {
4168 return view.do_show();
4170 self.form_last_update = self.form_last_update.then(act, act);
4171 return self.form_last_update;
4172 } else if (view.do_search) {
4173 return view.do_search(self.build_domain(), self.dataset.get_context(), []);
4176 return self.is_loaded;
4178 set_value: function(value_) {
4179 value_ = value_ || [];
4181 this.dataset.reset_ids([]);
4183 if(value_.length >= 1 && value_[0] instanceof Array) {
4185 _.each(value_, function(command) {
4186 var obj = {values: command[2]};
4187 switch (command[0]) {
4188 case commands.CREATE:
4189 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4191 self.dataset.to_create.push(obj);
4192 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4195 case commands.UPDATE:
4196 obj['id'] = command[1];
4197 self.dataset.to_write.push(obj);
4198 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4201 case commands.DELETE:
4202 self.dataset.to_delete.push({id: command[1]});
4204 case commands.LINK_TO:
4205 ids.push(command[1]);
4207 case commands.DELETE_ALL:
4208 self.dataset.delete_all = true;
4213 this.dataset.set_ids(ids);
4214 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
4216 this.dataset.delete_all = true;
4217 _.each(value_, function(command) {
4218 var obj = {values: command};
4219 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4221 self.dataset.to_create.push(obj);
4222 self.dataset.cache.push(_.clone(obj));
4226 this.dataset.set_ids(ids);
4228 this._super(value_);
4229 this.dataset.reset_ids(value_);
4231 if (this.dataset.index === null && this.dataset.ids.length > 0) {
4232 this.dataset.index = 0;
4234 this.trigger_on_change();
4235 if (this.is_started) {
4236 return self.reload_current_view();
4241 get_value: function() {
4245 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
4246 val = val.concat(_.map(this.dataset.ids, function(id) {
4247 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
4249 return commands.create(alter_order.values);
4251 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
4253 return commands.update(alter_order.id, alter_order.values);
4255 return commands.link_to(id);
4257 return val.concat(_.map(
4258 this.dataset.to_delete, function(x) {
4259 return commands['delete'](x.id);}));
4261 commit_value: function() {
4262 return this.save_any_view();
4264 save_any_view: function() {
4265 if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
4266 this.viewmanager.views[this.viewmanager.active_view] &&
4267 this.viewmanager.views[this.viewmanager.active_view].controller) {
4268 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
4269 if (this.viewmanager.active_view === "form") {
4270 if (view.is_initialized.state() !== 'resolved') {
4271 return $.when(false);
4273 return $.when(view.save());
4274 } else if (this.viewmanager.active_view === "list") {
4275 return $.when(view.ensure_saved());
4278 return $.when(false);
4280 is_syntax_valid: function() {
4281 if (! this.viewmanager || ! this.viewmanager.views[this.viewmanager.active_view])
4283 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
4284 switch (this.viewmanager.active_view) {
4286 return _(view.fields).chain()
4291 return view.is_valid();
4297 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
4298 template: 'One2Many.viewmanager',
4299 init: function(parent, dataset, views, flags) {
4300 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
4301 this.registry = this.registry.extend({
4302 list: 'instance.web.form.One2ManyListView',
4303 form: 'instance.web.form.One2ManyFormView',
4304 kanban: 'instance.web.form.One2ManyKanbanView',
4306 this.__ignore_blur = false;
4308 switch_mode: function(mode, unused) {
4309 if (mode !== 'form') {
4310 return this._super(mode, unused);
4313 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
4314 var pop = new instance.web.form.FormOpenPopup(this);
4315 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4316 title: _t("Open: ") + self.o2m.string,
4317 create_function: function(data, options) {
4318 return self.o2m.dataset.create(data, options).done(function(r) {
4319 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4320 self.o2m.dataset.trigger("dataset_changed", r);
4323 write_function: function(id, data, options) {
4324 return self.o2m.dataset.write(id, data, {}).done(function() {
4325 self.o2m.reload_current_view();
4328 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4329 parent_view: self.o2m.view,
4330 child_name: self.o2m.name,
4331 read_function: function() {
4332 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4334 form_view_options: {'not_interactible_on_create':true},
4335 readonly: self.o2m.get("effective_readonly")
4337 pop.on("elements_selected", self, function() {
4338 self.o2m.reload_current_view();
4343 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
4344 get_context: function() {
4345 this.context = this.o2m.build_context();
4346 return this.context;
4350 instance.web.form.One2ManyListView = instance.web.ListView.extend({
4351 _template: 'One2Many.listview',
4352 init: function (parent, dataset, view_id, options) {
4353 this._super(parent, dataset, view_id, _.extend(options || {}, {
4354 GroupsType: instance.web.form.One2ManyGroups,
4355 ListType: instance.web.form.One2ManyList
4357 this.on('edit:after', this, this.proxy('_after_edit'));
4358 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
4361 .bind('add', this.proxy("changed_records"))
4362 .bind('edit', this.proxy("changed_records"))
4363 .bind('remove', this.proxy("changed_records"));
4365 start: function () {
4366 var ret = this._super();
4368 .off('mousedown.handleButtons')
4369 .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
4372 changed_records: function () {
4373 this.o2m.trigger_on_change();
4375 is_valid: function () {
4376 var editor = this.editor;
4377 var form = editor.form;
4378 // If no edition is pending, the listview can not be invalid (?)
4379 if (!editor.record) {
4382 // If the form has not been modified, the view can only be valid
4383 // NB: is_dirty will also be set on defaults/onchanges/whatever?
4384 // oe_form_dirty seems to only be set on actual user actions
4385 if (!form.$el.is('.oe_form_dirty')) {
4388 this.o2m._dirty_flag = true;
4390 // Otherwise validate internal form
4391 return _(form.fields).chain()
4392 .invoke(function () {
4393 this._check_css_flags();
4394 return this.is_valid();
4399 do_add_record: function () {
4400 if (this.editable()) {
4401 this._super.apply(this, arguments);
4404 var pop = new instance.web.form.SelectCreatePopup(this);
4406 self.o2m.field.relation,
4408 title: _t("Create: ") + self.o2m.string,
4409 initial_view: "form",
4410 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4411 create_function: function(data, options) {
4412 return self.o2m.dataset.create(data, options).done(function(r) {
4413 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4414 self.o2m.dataset.trigger("dataset_changed", r);
4417 read_function: function() {
4418 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4420 parent_view: self.o2m.view,
4421 child_name: self.o2m.name,
4422 form_view_options: {'not_interactible_on_create':true}
4424 self.o2m.build_domain(),
4425 self.o2m.build_context()
4427 pop.on("elements_selected", self, function() {
4428 self.o2m.reload_current_view();
4432 do_activate_record: function(index, id) {
4434 var pop = new instance.web.form.FormOpenPopup(self);
4435 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4436 title: _t("Open: ") + self.o2m.string,
4437 write_function: function(id, data) {
4438 return self.o2m.dataset.write(id, data, {}).done(function() {
4439 self.o2m.reload_current_view();
4442 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4443 parent_view: self.o2m.view,
4444 child_name: self.o2m.name,
4445 read_function: function() {
4446 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4448 form_view_options: {'not_interactible_on_create':true},
4449 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
4452 do_button_action: function (name, id, callback) {
4453 if (!_.isNumber(id)) {
4454 instance.webclient.notification.warn(
4455 _t("Action Button"),
4456 _t("The o2m record must be saved before an action can be used"));
4459 var parent_form = this.o2m.view;
4461 this.ensure_saved().then(function () {
4463 return parent_form.save();
4466 }).done(function () {
4467 var ds = self.o2m.dataset;
4468 var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
4469 return value.length;
4471 if (!self.o2m.options.reload_on_button && !cached_records) {
4472 self.handle_button(name, id, callback);
4474 self.handle_button(name, id, function(){
4475 self.o2m.view.reload();
4481 _after_edit: function () {
4482 this.__ignore_blur = false;
4483 this.editor.form.on('blurred', this, this._on_form_blur);
4485 // The form's blur thing may be jiggered during the edition setup,
4486 // potentially leading to the o2m instasaving the row. Cancel any
4487 // blurring triggered the edition startup here
4488 this.editor.form.widgetFocused();
4490 _before_unedit: function () {
4491 this.editor.form.off('blurred', this, this._on_form_blur);
4493 _button_down: function () {
4494 // If a button is clicked (usually some sort of action button), it's
4495 // the button's responsibility to ensure the editable list is in the
4496 // correct state -> ignore form blurring
4497 this.__ignore_blur = true;
4500 * Handles blurring of the nested form (saves the currently edited row),
4501 * unless the flag to ignore the event is set to ``true``
4503 * Makes the internal form go away
4505 _on_form_blur: function () {
4506 if (this.__ignore_blur) {
4507 this.__ignore_blur = false;
4510 // FIXME: why isn't there an API for this?
4511 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4512 this.ensure_saved();
4515 this.cancel_edition();
4517 keypress_ENTER: function () {
4518 // blurring caused by hitting the [Return] key, should skip the
4519 // autosave-on-blur and let the handler for [Return] do its thing (save
4520 // the current row *anyway*, then create a new one/edit the next one)
4521 this.__ignore_blur = true;
4522 this._super.apply(this, arguments);
4524 do_delete: function (ids) {
4525 var confirm = window.confirm;
4526 window.confirm = function () { return true; };
4528 return this._super(ids);
4530 window.confirm = confirm;
4533 reload_record: function (record) {
4534 // Evict record.id from cache to ensure it will be reloaded correctly
4535 this.dataset.evict_record(record.get('id'));
4537 return this._super(record);
4540 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4541 setup_resequence_rows: function () {
4542 if (!this.view.o2m.get('effective_readonly')) {
4543 this._super.apply(this, arguments);
4547 instance.web.form.One2ManyList = instance.web.form.AddAnItemList.extend({
4548 _add_row_class: 'oe_form_field_one2many_list_row_add',
4549 is_readonly: function () {
4550 return this.view.o2m.get('effective_readonly');
4554 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4555 form_template: 'One2Many.formview',
4556 load_form: function(data) {
4559 this.$buttons.find('button.oe_form_button_create').click(function() {
4560 self.save().done(self.on_button_new);
4563 do_notify_change: function() {
4564 if (this.dataset.parent_view) {
4565 this.dataset.parent_view.do_notify_change();
4567 this._super.apply(this, arguments);
4572 var lazy_build_o2m_kanban_view = function() {
4573 if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
4575 instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4579 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4580 template: "FieldMany2ManyTags",
4581 tag_template: "FieldMany2ManyTag",
4583 this._super.apply(this, arguments);
4584 instance.web.form.CompletionFieldMixin.init.call(this);
4585 this.set({"value": []});
4586 this._display_orderer = new instance.web.DropMisordered();
4587 this._drop_shown = false;
4589 initialize_texttext: function(){
4592 plugins : 'tags arrow autocomplete',
4594 render: function(suggestion) {
4595 return $('<span class="text-label"/>').
4596 data('index', suggestion['index']).html(suggestion['label']);
4601 selectFromDropdown: function() {
4602 this.trigger('hideDropdown');
4603 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4604 var data = self.search_result[index];
4606 self.add_id(data.id);
4608 self.ignore_blur = true;
4611 this.trigger('setSuggestions', {result : []});
4615 isTagAllowed: function(tag) {
4619 removeTag: function(tag) {
4620 var id = tag.data("id");
4621 self.set({"value": _.without(self.get("value"), id)});
4623 renderTag: function(stuff) {
4624 return $.fn.textext.TextExtTags.prototype.renderTag.
4625 call(this, stuff).data("id", stuff.id);
4629 itemToString: function(item) {
4634 onSetInputData: function(e, data) {
4636 this._plugins.autocomplete._suggestions = null;
4638 this.input().val(data);
4644 initialize_content: function() {
4645 if (this.get("effective_readonly"))
4648 self.ignore_blur = false;
4649 self.$text = this.$("textarea");
4650 self.$text.textext(self.initialize_texttext()).bind('getSuggestions', function(e, data) {
4652 var str = !!data ? data.query || '' : '';
4653 self.get_search_result(str).done(function(result) {
4654 self.search_result = result;
4655 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4656 return _.extend(el, {index:i});
4659 }).bind('hideDropdown', function() {
4660 self._drop_shown = false;
4661 }).bind('showDropdown', function() {
4662 self._drop_shown = true;
4664 self.tags = self.$text.textext()[0].tags();
4666 .focusin(function () {
4667 self.trigger('focused');
4668 self.ignore_blur = false;
4670 .focusout(function() {
4671 self.$text.trigger("setInputData", "");
4672 if (!self.ignore_blur) {
4673 self.trigger('blurred');
4675 }).keydown(function(e) {
4676 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4677 self.$text.textext()[0].autocomplete().selectFromDropdown();
4681 set_value: function(value_) {
4682 value_ = value_ || [];
4683 if (value_.length >= 1 && value_[0] instanceof Array) {
4684 value_ = value_[0][2];
4686 this._super(value_);
4688 is_false: function() {
4689 return _(this.get("value")).isEmpty();
4691 get_value: function() {
4692 var tmp = [commands.replace_with(this.get("value"))];
4695 get_search_blacklist: function() {
4696 return this.get("value");
4698 map_tag: function(data){
4699 return _.map(data, function(el) {return {name: el[1], id:el[0]};})
4701 get_render_data: function(ids){
4703 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4704 return dataset.name_get(ids);
4706 render_tag: function(data) {
4708 if (! self.get("effective_readonly")) {
4709 self.tags.containerElement().children().remove();
4710 self.$('textarea').css("padding-left", "3px");
4711 self.tags.addTags(self.map_tag(data));
4713 self.$el.html(QWeb.render(self.tag_template, {elements: data}));
4716 render_value: function() {
4718 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4719 var values = self.get("value");
4720 var handle_names = function(data) {
4721 if (self.isDestroyed())
4724 _.each(data, function(el) {
4725 indexed[el[0]] = el;
4727 data = _.map(values, function(el) { return indexed[el]; });
4728 self.render_tag(data);
4730 if (! values || values.length > 0) {
4731 this._display_orderer.add(self.get_render_data(values)).done(handle_names);
4737 add_id: function(id) {
4738 this.set({'value': _.uniq(this.get('value').concat([id]))});
4740 focus: function () {
4741 var input = this.$text && this.$text[0];
4742 return input ? input.focus() : false;
4744 set_dimensions: function (height, width) {
4745 this._super(height, width);
4746 this.$("textarea").css({
4751 _search_create_popup: function() {
4752 self.ignore_blur = true;
4753 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
4759 - reload_on_button: Reload the whole form view if click on a button in a list view.
4760 If you see this options, do not use it, it's basically a dirty hack to make one
4761 precise o2m to behave the way we want.
4763 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4764 multi_selection: false,
4765 disable_utility_classes: true,
4766 init: function(field_manager, node) {
4767 this._super(field_manager, node);
4768 this.is_loaded = $.Deferred();
4769 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4770 this.dataset.m2m = this;
4772 this.dataset.on('unlink', self, function(ids) {
4773 self.dataset_changed();
4776 this.list_dm = new instance.web.DropMisordered();
4777 this.render_value_dm = new instance.web.DropMisordered();
4779 initialize_content: function() {
4782 this.$el.addClass('oe_form_field oe_form_field_many2many');
4784 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4786 'deletable': this.get("effective_readonly") ? false : true,
4787 'selectable': this.multi_selection,
4789 'reorderable': false,
4790 'import_enabled': false,
4792 var embedded = (this.field.views || {}).tree;
4794 this.list_view.set_embedded_view(embedded);
4796 this.list_view.m2m_field = this;
4797 var loaded = $.Deferred();
4798 this.list_view.on("list_view_loaded", this, function() {
4801 this.list_view.appendTo(this.$el);
4803 var old_def = self.is_loaded;
4804 self.is_loaded = $.Deferred().done(function() {
4807 this.list_dm.add(loaded).then(function() {
4808 self.is_loaded.resolve();
4811 destroy_content: function() {
4812 this.list_view.destroy();
4813 this.list_view = undefined;
4815 set_value: function(value_) {
4816 value_ = value_ || [];
4817 if (value_.length >= 1 && value_[0] instanceof Array) {
4818 value_ = value_[0][2];
4820 this._super(value_);
4822 get_value: function() {
4823 return [commands.replace_with(this.get('value'))];
4825 is_false: function () {
4826 return _(this.get("value")).isEmpty();
4828 render_value: function() {
4830 this.dataset.set_ids(this.get("value"));
4831 this.render_value_dm.add(this.is_loaded).then(function() {
4832 return self.list_view.reload_content();
4835 dataset_changed: function() {
4836 this.internal_set_value(this.dataset.ids);
4840 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4841 get_context: function() {
4842 this.context = this.m2m.build_context();
4843 return this.context;
4849 * @extends instance.web.ListView
4851 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4852 init: function (parent, dataset, view_id, options) {
4853 this._super(parent, dataset, view_id, _.extend(options || {}, {
4854 ListType: instance.web.form.Many2ManyList,
4857 do_add_record: function () {
4858 var pop = new instance.web.form.SelectCreatePopup(this);
4862 title: _t("Add: ") + this.m2m_field.string,
4863 no_create: this.m2m_field.options.no_create,
4865 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4866 this.m2m_field.build_context()
4869 pop.on("elements_selected", self, function(element_ids) {
4871 _(element_ids).each(function (id) {
4872 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4873 self.dataset.set_ids(self.dataset.ids.concat([id]));
4874 self.m2m_field.dataset_changed();
4879 self.reload_content();
4883 do_activate_record: function(index, id) {
4885 var pop = new instance.web.form.FormOpenPopup(this);
4886 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4887 title: _t("Open: ") + this.m2m_field.string,
4888 readonly: this.getParent().get("effective_readonly")
4890 pop.on('write_completed', self, self.reload_content);
4892 do_button_action: function(name, id, callback) {
4894 var _sup = _.bind(this._super, this);
4895 if (! this.m2m_field.options.reload_on_button) {
4896 return _sup(name, id, callback);
4898 return this.m2m_field.view.save().then(function() {
4899 return _sup(name, id, function() {
4900 self.m2m_field.view.reload();
4905 is_action_enabled: function () { return true; },
4907 instance.web.form.Many2ManyList = instance.web.form.AddAnItemList.extend({
4908 _add_row_class: 'oe_form_field_many2many_list_row_add',
4909 is_readonly: function () {
4910 return this.view.m2m_field.get('effective_readonly');
4914 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4915 disable_utility_classes: true,
4916 init: function(field_manager, node) {
4917 this._super(field_manager, node);
4918 instance.web.form.CompletionFieldMixin.init.call(this);
4919 m2m_kanban_lazy_init();
4920 this.is_loaded = $.Deferred();
4921 this.initial_is_loaded = this.is_loaded;
4924 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4925 this.dataset.m2m = this;
4926 this.dataset.on('unlink', self, function(ids) {
4927 self.dataset_changed();
4931 this._super.apply(this, arguments);
4936 self.on("change:effective_readonly", self, function() {
4937 self.is_loaded = self.is_loaded.then(function() {
4938 self.kanban_view.destroy();
4939 return $.when(self.load_view()).done(function() {
4940 self.render_value();
4945 set_value: function(value_) {
4946 value_ = value_ || [];
4947 if (value_.length >= 1 && value_[0] instanceof Array) {
4948 value_ = value_[0][2];
4950 this._super(value_);
4952 get_value: function() {
4953 return [commands.replace_with(this.get('value'))];
4955 load_view: function() {
4957 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4958 'create_text': _t("Add"),
4959 'creatable': self.get("effective_readonly") ? false : true,
4960 'quick_creatable': self.get("effective_readonly") ? false : true,
4961 'read_only_mode': self.get("effective_readonly") ? true : false,
4962 'confirm_on_delete': false,
4964 var embedded = (this.field.views || {}).kanban;
4966 this.kanban_view.set_embedded_view(embedded);
4968 this.kanban_view.m2m = this;
4969 var loaded = $.Deferred();
4970 this.kanban_view.on("kanban_view_loaded",self,function() {
4971 self.initial_is_loaded.resolve();
4974 this.kanban_view.on('switch_mode', this, this.open_popup);
4975 $.async_when().done(function () {
4976 self.kanban_view.appendTo(self.$el);
4980 render_value: function() {
4982 this.dataset.set_ids(this.get("value"));
4983 this.is_loaded = this.is_loaded.then(function() {
4984 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
4987 dataset_changed: function() {
4988 this.set({'value': this.dataset.ids});
4990 open_popup: function(type, unused) {
4991 if (type !== "form")
4995 if (this.dataset.index === null) {
4996 pop = new instance.web.form.SelectCreatePopup(this);
4998 this.field.relation,
5000 title: _t("Add: ") + this.string
5002 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
5003 this.build_context()
5005 pop.on("elements_selected", self, function(element_ids) {
5006 _.each(element_ids, function(one_id) {
5007 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
5008 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
5009 self.dataset_changed();
5010 self.render_value();
5015 var id = self.dataset.ids[self.dataset.index];
5016 pop = new instance.web.form.FormOpenPopup(this);
5017 pop.show_element(self.field.relation, id, self.build_context(), {
5018 title: _t("Open: ") + self.string,
5019 write_function: function(id, data, options) {
5020 return self.dataset.write(id, data, {}).done(function() {
5021 self.render_value();
5024 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
5025 parent_view: self.view,
5026 child_name: self.name,
5027 readonly: self.get("effective_readonly")
5031 add_id: function(id) {
5032 this.quick_create.add_id(id);
5036 function m2m_kanban_lazy_init() {
5037 if (instance.web.form.Many2ManyKanbanView)
5039 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
5040 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
5041 _is_quick_create_enabled: function() {
5042 return this._super() && ! this.group_by;
5045 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
5046 template: 'Many2ManyKanban.quick_create',
5049 * close_btn: If true, the widget will display a "Close" button able to trigger
5052 init: function(parent, dataset, context, buttons) {
5053 this._super(parent);
5054 this.m2m = this.getParent().view.m2m;
5055 this.m2m.quick_create = this;
5056 this._dataset = dataset;
5057 this._buttons = buttons || false;
5058 this._context = context || {};
5060 start: function () {
5062 self.$text = this.$el.find('input').css("width", "200px");
5063 self.$text.textext({
5064 plugins : 'arrow autocomplete',
5066 render: function(suggestion) {
5067 return $('<span class="text-label"/>').
5068 data('index', suggestion['index']).html(suggestion['label']);
5073 selectFromDropdown: function() {
5074 $(this).trigger('hideDropdown');
5075 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
5076 var data = self.search_result[index];
5078 self.add_id(data.id);
5085 itemToString: function(item) {
5090 }).bind('getSuggestions', function(e, data) {
5092 var str = !!data ? data.query || '' : '';
5093 self.m2m.get_search_result(str).done(function(result) {
5094 self.search_result = result;
5095 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
5096 return _.extend(el, {index:i});
5100 self.$text.focusout(function() {
5105 this.$text[0].focus();
5107 add_id: function(id) {
5110 self.trigger('added', id);
5111 this.m2m.dataset_changed();
5117 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
5119 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
5120 template: "AbstractFormPopup.render",
5123 * -readonly: only applicable when not in creation mode, default to false
5124 * - alternative_form_view
5131 * - form_view_options
5133 init_popup: function(model, row_id, domain, context, options) {
5134 this.row_id = row_id;
5136 this.domain = domain || [];
5137 this.context = context || {};
5138 this.options = options;
5139 _.defaults(this.options, {
5142 init_dataset: function() {
5144 this.created_elements = [];
5145 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
5146 this.dataset.read_function = this.options.read_function;
5147 this.dataset.create_function = function(data, options, sup) {
5148 var fct = self.options.create_function || sup;
5149 return fct.call(this, data, options).done(function(r) {
5150 self.trigger('create_completed saved', r);
5151 self.created_elements.push(r);
5154 this.dataset.write_function = function(id, data, options, sup) {
5155 var fct = self.options.write_function || sup;
5156 return fct.call(this, id, data, options).done(function(r) {
5157 self.trigger('write_completed saved', r);
5160 this.dataset.parent_view = this.options.parent_view;
5161 this.dataset.child_name = this.options.child_name;
5163 display_popup: function() {
5165 this.renderElement();
5166 var dialog = new instance.web.Dialog(this, {
5167 dialogClass: 'oe_act_window',
5168 title: this.options.title || "",
5169 }, this.$el).open();
5170 dialog.on('closing', this, function (e){
5171 self.check_exit(true);
5173 this.$buttonpane = dialog.$buttons;
5176 setup_form_view: function() {
5179 this.dataset.ids = [this.row_id];
5180 this.dataset.index = 0;
5182 this.dataset.index = null;
5184 var options = _.clone(self.options.form_view_options) || {};
5185 if (this.row_id !== null) {
5186 options.initial_mode = this.options.readonly ? "view" : "edit";
5189 $buttons: this.$buttonpane,
5191 this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
5192 if (this.options.alternative_form_view) {
5193 this.view_form.set_embedded_view(this.options.alternative_form_view);
5195 this.view_form.appendTo(this.$el.find(".oe_popup_form"));
5196 this.view_form.on("form_view_loaded", self, function() {
5197 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
5198 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
5199 multi_select: multi_select,
5200 readonly: self.row_id !== null && self.options.readonly,
5202 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
5203 $snbutton.click(function() {
5204 $.when(self.view_form.save()).done(function() {
5205 self.view_form.reload_mutex.exec(function() {
5206 self.view_form.on_button_new();
5210 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
5211 $sbutton.click(function() {
5212 $.when(self.view_form.save()).done(function() {
5213 self.view_form.reload_mutex.exec(function() {
5218 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
5219 $cbutton.click(function() {
5220 self.view_form.trigger('on_button_cancel');
5223 self.view_form.do_show();
5226 select_elements: function(element_ids) {
5227 this.trigger("elements_selected", element_ids);
5229 check_exit: function(no_destroy) {
5230 if (this.created_elements.length > 0) {
5231 this.select_elements(this.created_elements);
5232 this.created_elements = [];
5234 this.trigger('closed');
5237 destroy: function () {
5238 this.trigger('closed');
5239 if (this.$el.is(":data(bs.modal)")) {
5240 this.$el.parents('.modal').modal('hide');
5247 * Class to display a popup containing a form view.
5249 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
5250 show_element: function(model, row_id, context, options) {
5251 this.init_popup(model, row_id, [], context, options);
5252 _.defaults(this.options, {
5254 this.display_popup();
5258 this.init_dataset();
5259 this.setup_form_view();
5264 * Class to display a popup to display a list to search a row. It also allows
5265 * to switch to a form view to create a new row.
5267 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
5271 * - initial_view: form or search (default search)
5272 * - disable_multiple_selection
5273 * - list_view_options
5275 select_element: function(model, options, domain, context) {
5276 this.init_popup(model, null, domain, context, options);
5278 _.defaults(this.options, {
5279 initial_view: "search",
5281 this.initial_ids = this.options.initial_ids;
5282 this.display_popup();
5286 this.init_dataset();
5287 if (this.options.initial_view == "search") {
5288 instance.web.pyeval.eval_domains_and_contexts({
5290 contexts: [this.context]
5291 }).done(function (results) {
5292 var search_defaults = {};
5293 _.each(results.context, function (value_, key) {
5294 var match = /^search_default_(.*)$/.exec(key);
5296 search_defaults[match[1]] = value_;
5299 self.setup_search_view(search_defaults);
5305 setup_search_view: function(search_defaults) {
5307 if (this.searchview) {
5308 this.searchview.destroy();
5310 this.searchview = new instance.web.SearchView(this,
5311 this.dataset, false, search_defaults);
5312 this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
5313 if (self.initial_ids) {
5314 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
5315 contexts.concat(self.context), groupbys);
5316 self.initial_ids = undefined;
5318 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
5321 this.searchview.on("search_view_loaded", self, function() {
5322 self.view_list = new instance.web.form.SelectCreateListView(self,
5323 self.dataset, false,
5324 _.extend({'deletable': false,
5325 'selectable': !self.options.disable_multiple_selection,
5326 'import_enabled': false,
5327 '$buttons': self.$buttonpane,
5328 'disable_editable_mode': true,
5329 '$pager': self.$('.oe_popup_list_pager'),
5330 }, self.options.list_view_options || {}));
5331 self.view_list.on('edit:before', self, function (e) {
5334 self.view_list.popup = self;
5335 self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
5336 self.view_list.do_show();
5337 }).then(function() {
5338 self.searchview.do_search();
5340 self.view_list.on("list_view_loaded", self, function() {
5341 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
5342 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
5343 $cbutton.click(function() {
5346 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
5347 $sbutton.click(function() {
5348 self.select_elements(self.selected_ids);
5351 $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
5352 $cbutton.click(function() {
5357 this.searchview.appendTo($(".oe_popup_search", self.$el));
5359 do_search: function(domains, contexts, groupbys) {
5361 instance.web.pyeval.eval_domains_and_contexts({
5362 domains: domains || [],
5363 contexts: contexts || [],
5364 group_by_seq: groupbys || []
5365 }).done(function (results) {
5366 self.view_list.do_search(results.domain, results.context, results.group_by);
5369 on_click_element: function(ids) {
5371 this.selected_ids = ids || [];
5372 if(this.selected_ids.length > 0) {
5373 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
5375 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
5378 new_object: function() {
5379 if (this.searchview) {
5380 this.searchview.hide();
5382 if (this.view_list) {
5383 this.view_list.do_hide();
5385 this.setup_form_view();
5389 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
5390 do_add_record: function () {
5391 this.popup.new_object();
5393 select_record: function(index) {
5394 this.popup.select_elements([this.dataset.ids[index]]);
5395 this.popup.destroy();
5397 do_select: function(ids, records) {
5398 this._super(ids, records);
5399 this.popup.on_click_element(ids);
5403 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5404 template: 'FieldReference',
5405 init: function(field_manager, node) {
5406 this._super(field_manager, node);
5407 this.reference_ready = true;
5409 destroy_content: function() {
5412 this.fm = undefined;
5415 initialize_content: function() {
5417 var fm = new instance.web.form.DefaultFieldManager(this);
5419 fm.extend_field_desc({
5421 selection: this.field_manager.get_field_desc(this.name).selection,
5429 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
5431 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5433 this.selection.on("change:value", this, this.on_selection_changed);
5434 this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
5436 .on('focused', null, function () {self.trigger('focused');})
5437 .on('blurred', null, function () {self.trigger('blurred');});
5439 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
5440 name: 'Referenced Document',
5441 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5443 this.m2o.on("change:value", this, this.data_changed);
5444 this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
5446 .on('focused', null, function () {self.trigger('focused');})
5447 .on('blurred', null, function () {self.trigger('blurred');});
5449 on_selection_changed: function() {
5450 if (this.reference_ready) {
5451 this.internal_set_value([this.selection.get_value(), false]);
5452 this.render_value();
5455 data_changed: function() {
5456 if (this.reference_ready) {
5457 this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
5460 set_value: function(val) {
5462 val = val.split(',');
5463 val[0] = val[0] || false;
5464 val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
5466 this._super(val || [false, false]);
5468 get_value: function() {
5469 return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
5471 render_value: function() {
5472 this.reference_ready = false;
5473 if (!this.get("effective_readonly")) {
5474 this.selection.set_value(this.get('value')[0]);
5476 this.m2o.field.relation = this.get('value')[0];
5477 this.m2o.set_value(this.get('value')[1]);
5478 this.m2o.$el.toggle(!!this.get('value')[0]);
5479 this.reference_ready = true;
5483 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5484 init: function(field_manager, node) {
5486 this._super(field_manager, node);
5487 this.binary_value = false;
5488 this.useFileAPI = !!window.FileReader;
5489 this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5490 if (!this.useFileAPI) {
5491 this.fileupload_id = _.uniqueId('oe_fileupload');
5492 $(window).on(this.fileupload_id, function() {
5493 var args = [].slice.call(arguments).slice(1);
5494 self.on_file_uploaded.apply(self, args);
5499 if (!this.useFileAPI) {
5500 $(window).off(this.fileupload_id);
5502 this._super.apply(this, arguments);
5504 initialize_content: function() {
5505 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5506 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5507 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5509 on_file_change: function(e) {
5511 var file_node = e.target;
5512 if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5513 if (this.useFileAPI) {
5514 var file = file_node.files[0];
5515 if (file.size > this.max_upload_size) {
5516 var msg = _t("The selected file exceed the maximum file size of %s.");
5517 instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5520 var filereader = new FileReader();
5521 filereader.readAsDataURL(file);
5522 filereader.onloadend = function(upload) {
5523 var data = upload.target.result;
5524 data = data.split(',')[1];
5525 self.on_file_uploaded(file.size, file.name, file.type, data);
5528 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5529 this.$el.find('form.oe_form_binary_form').submit();
5531 this.$el.find('.oe_form_binary_progress').show();
5532 this.$el.find('.oe_form_binary').hide();
5535 on_file_uploaded: function(size, name, content_type, file_base64) {
5536 if (size === false) {
5537 this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5538 // TODO: use openerp web crashmanager
5539 console.warn("Error while uploading file : ", name);
5541 this.filename = name;
5542 this.on_file_uploaded_and_valid.apply(this, arguments);
5544 this.$el.find('.oe_form_binary_progress').hide();
5545 this.$el.find('.oe_form_binary').show();
5547 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5549 on_save_as: function(ev) {
5550 var value = this.get('value');
5552 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5553 ev.stopPropagation();
5555 instance.web.blockUI();
5556 var c = instance.webclient.crashmanager;
5557 this.session.get_file({
5558 url: '/web/binary/saveas_ajax',
5559 data: {data: JSON.stringify({
5560 model: this.view.dataset.model,
5561 id: (this.view.datarecord.id || ''),
5563 filename_field: (this.node.attrs.filename || ''),
5564 data: instance.web.form.is_bin_size(value) ? null : value,
5565 context: this.view.dataset.get_context()
5567 complete: instance.web.unblockUI,
5568 error: c.rpc_error.bind(c)
5570 ev.stopPropagation();
5574 set_filename: function(value) {
5575 var filename = this.node.attrs.filename;
5578 tmp[filename] = value;
5579 this.field_manager.set_values(tmp);
5582 on_clear: function() {
5583 if (this.get('value') !== false) {
5584 this.binary_value = false;
5585 this.internal_set_value(false);
5591 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5592 template: 'FieldBinaryFile',
5593 initialize_content: function() {
5595 if (this.get("effective_readonly")) {
5597 this.$el.find('a').click(function(ev) {
5598 if (self.get('value')) {
5599 self.on_save_as(ev);
5605 render_value: function() {
5607 if (!this.get("effective_readonly")) {
5608 if (this.node.attrs.filename) {
5609 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5611 show_value = (this.get('value') !== null && this.get('value') !== undefined && this.get('value') !== false) ? this.get('value') : '';
5613 this.$el.find('input').eq(0).val(show_value);
5615 this.$el.find('a').toggle(!!this.get('value'));
5616 if (this.get('value')) {
5617 show_value = _t("Download");
5619 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5620 this.$el.find('a').text(show_value);
5624 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5625 this.binary_value = true;
5626 this.internal_set_value(file_base64);
5627 var show_value = name + " (" + instance.web.human_size(size) + ")";
5628 this.$el.find('input').eq(0).val(show_value);
5629 this.set_filename(name);
5631 on_clear: function() {
5632 this._super.apply(this, arguments);
5633 this.$el.find('input').eq(0).val('');
5634 this.set_filename('');
5638 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5639 template: 'FieldBinaryImage',
5640 placeholder: "/web/static/src/img/placeholder.png",
5641 render_value: function() {
5644 if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5645 url = 'data:image/png;base64,' + this.get('value');
5646 } else if (this.get('value')) {
5647 var id = JSON.stringify(this.view.datarecord.id || null);
5648 var field = this.name;
5649 if (this.options.preview_image)
5650 field = this.options.preview_image;
5651 url = this.session.url('/web/binary/image', {
5652 model: this.view.dataset.model,
5655 t: (new Date().getTime()),
5658 url = this.placeholder;
5660 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5661 $($img).click(function(e) {
5662 if(self.view.get("actual_mode") == "view") {
5663 var $button = $(".oe_form_button_edit");
5664 $button.openerpBounce();
5665 e.stopPropagation();
5668 this.$el.find('> img').remove();
5669 this.$el.prepend($img);
5670 $img.load(function() {
5671 if (! self.options.size)
5673 $img.css("max-width", "" + self.options.size[0] + "px");
5674 $img.css("max-height", "" + self.options.size[1] + "px");
5675 $img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px");
5676 $img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px");
5678 $img.on('error', function() {
5679 $img.attr('src', self.placeholder);
5680 instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5683 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5684 this.internal_set_value(file_base64);
5685 this.binary_value = true;
5686 this.render_value();
5687 this.set_filename(name);
5689 on_clear: function() {
5690 this._super.apply(this, arguments);
5691 this.render_value();
5692 this.set_filename('');
5697 * Widget for (many2many field) to upload one or more file in same time and display in list.
5698 * The user can delete his files.
5699 * Options on attribute ; "blockui" {Boolean} block the UI or not
5700 * during the file is uploading
5702 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
5703 template: "FieldBinaryFileUploader",
5704 init: function(field_manager, node) {
5705 this._super(field_manager, node);
5706 this.field_manager = field_manager;
5708 if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5709 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);
5713 this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5714 this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5715 $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5719 this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5721 set_value: function(value_) {
5722 value_ = value_ || [];
5723 if (value_.length >= 1 && value_[0] instanceof Array) {
5724 value_ = value_[0][2];
5726 this._super(value_);
5728 get_value: function() {
5729 var tmp = [commands.replace_with(this.get("value"))];
5732 get_file_url: function (attachment) {
5733 return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5735 read_name_values : function () {
5737 // don't reset know values
5738 var ids = this.get('value');
5739 var _value = _.filter(ids, function (id) { return typeof self.data[id] == 'undefined'; } );
5740 // send request for get_name
5741 if (_value.length) {
5742 return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).then(function (datas) {
5743 _.each(datas, function (data) {
5744 data.no_unlink = true;
5745 data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5746 self.data[data.id] = data;
5754 render_value: function () {
5756 this.read_name_values().then(function (ids) {
5757 var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids}));
5758 render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5759 self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5761 // reinit input type file
5762 var $input = self.$('input.oe_form_binary_file');
5763 $input.after($input.clone(true)).remove();
5764 self.$(".oe_fileupload").show();
5768 on_file_change: function (event) {
5769 event.stopPropagation();
5771 var $target = $(event.target);
5772 if ($target.val() !== '') {
5773 var filename = $target.val().replace(/.*[\\\/]/,'');
5774 // don't uplode more of one file in same time
5775 if (self.data[0] && self.data[0].upload ) {
5778 for (var id in this.get('value')) {
5779 // if the files exits, delete the file before upload (if it's a new file)
5780 if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
5781 self.ds_file.unlink([id]);
5786 if(this.node.attrs.blockui>0) {
5787 instance.web.blockUI();
5790 // TODO : unactivate send on wizard and form
5793 this.$('form.oe_form_binary_form').submit();
5794 this.$(".oe_fileupload").hide();
5795 // add file on data result
5799 'filename': filename,
5805 on_file_loaded: function (event, result) {
5806 var files = this.get('value');
5809 if(this.node.attrs.blockui>0) {
5810 instance.web.unblockUI();
5813 if (result.error || !result.id ) {
5814 this.do_warn( _t('Uploading Error'), result.error);
5815 delete this.data[0];
5817 if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
5818 delete this.data[0];
5819 this.data[result.id] = {
5821 'name': result.name,
5822 'filename': result.filename,
5823 'url': this.get_file_url(result)
5826 this.data[result.id] = {
5828 'name': result.name,
5829 'filename': result.filename,
5830 'url': this.get_file_url(result)
5833 var values = _.clone(this.get('value'));
5834 values.push(result.id);
5835 this.set({'value': values});
5837 this.render_value();
5839 on_file_delete: function (event) {
5840 event.stopPropagation();
5841 var file_id=$(event.target).data("id");
5843 var files = _.filter(this.get('value'), function (id) {return id != file_id;});
5844 if(!this.data[file_id].no_unlink) {
5845 this.ds_file.unlink([file_id]);
5847 this.set({'value': files});
5852 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5853 template: "FieldStatus",
5854 init: function(field_manager, node) {
5855 this._super(field_manager, node);
5856 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5857 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5858 this.set({value: false});
5859 this.selection = {'unfolded': [], 'folded': []};
5860 this.set("selection", {'unfolded': [], 'folded': []});
5861 this.selection_dm = new instance.web.DropMisordered();
5862 this.dataset = new instance.web.DataSetStatic(this, this.field.relation, this.build_context());
5865 this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5867 this.on("change:value", this, this.get_selection);
5868 this.on("change:evaluated_selection_domain", this, this.get_selection);
5869 this.on("change:selection", this, function() {
5870 this.selection = this.get("selection");
5871 this.render_value();
5873 this.get_selection();
5874 if (this.options.clickable) {
5875 this.$el.on('click','li[data-id]',this.on_click_stage);
5877 if (this.$el.parent().is('header')) {
5878 this.$el.after('<div class="oe_clear"/>');
5882 set_value: function(value_) {
5883 if (value_ instanceof Array) {
5886 this._super(value_);
5888 render_value: function() {
5890 var content = QWeb.render("FieldStatus.content", {
5892 'value_folded': _.find(self.selection.folded, function(i){return i[0] === self.get('value');})
5894 self.$el.html(content);
5896 calc_domain: function() {
5897 var d = instance.web.pyeval.eval('domain', this.build_domain());
5898 var domain = []; //if there is no domain defined, fetch all the records
5901 domain = ['|',['id', '=', this.get('value')]].concat(d);
5904 if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5905 this.set("evaluated_selection_domain", domain);
5908 /** Get the selection and render it
5909 * selection: [[identifier, value_to_display], ...]
5910 * For selection fields: this is directly given by this.field.selection
5911 * For many2one fields: perform a search on the relation of the many2one field
5913 get_selection: function() {
5915 var selection_unfolded = [];
5916 var selection_folded = [];
5917 var fold_field = this.options.fold_field;
5919 var calculation = _.bind(function() {
5920 if (this.field.type == "many2one") {
5921 return self.get_distant_fields().then(function (fields) {
5922 return new instance.web.DataSetSearch(self, self.field.relation, self.build_context(), self.get("evaluated_selection_domain"))
5923 .read_slice(_.union(_.keys(self.distant_fields), ['id']), {}).then(function (records) {
5924 var ids = _.pluck(records, 'id');
5925 return self.dataset.name_get(ids).then(function (records_name) {
5926 _.each(records, function (record) {
5927 var name = _.find(records_name, function (val) {return val[0] == record.id;})[1];
5928 if (fold_field && record[fold_field] && record.id != self.get('value')) {
5929 selection_folded.push([record.id, name]);
5931 selection_unfolded.push([record.id, name]);
5938 // For field type selection filter values according to
5939 // statusbar_visible attribute of the field. For example:
5940 // statusbar_visible="draft,open".
5941 var select = this.field.selection;
5942 for(var i=0; i < select.length; i++) {
5943 var key = select[i][0];
5944 if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5945 selection_unfolded.push(select[i]);
5951 this.selection_dm.add(calculation()).then(function () {
5952 var selection = {'unfolded': selection_unfolded, 'folded': selection_folded};
5953 if (! _.isEqual(selection, self.get("selection"))) {
5954 self.set("selection", selection);
5959 * :deprecated: this feature will probably be removed with OpenERP v8
5961 get_distant_fields: function() {
5963 if (! this.options.fold_field) {
5964 this.distant_fields = {}
5966 if (this.distant_fields) {
5967 return $.when(this.distant_fields);
5969 return new instance.web.Model(self.field.relation).call("fields_get", [[this.options.fold_field]]).then(function(fields) {
5970 self.distant_fields = fields;
5974 on_click_stage: function (ev) {
5976 var $li = $(ev.currentTarget);
5978 if (this.field.type == "many2one") {
5979 val = parseInt($li.data("id"), 10);
5982 val = $li.data("id");
5984 if (val != self.get('value')) {
5985 this.view.recursive_save().done(function() {
5987 change[self.name] = val;
5988 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
5996 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
5997 template: "FieldMonetary",
5998 widget_class: 'oe_form_field_float oe_form_field_monetary',
6000 this._super.apply(this, arguments);
6001 this.set({"currency": false});
6002 if (this.options.currency_field) {
6003 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
6004 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
6007 this.on("change:currency", this, this.get_currency_info);
6008 this.get_currency_info();
6009 this.ci_dm = new instance.web.DropMisordered();
6012 var tmp = this._super();
6013 this.on("change:currency_info", this, this.reinitialize);
6016 get_currency_info: function() {
6018 if (this.get("currency") === false) {
6019 this.set({"currency_info": null});
6022 return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
6023 .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
6024 self.set({"currency_info": res});
6027 parse_value: function(val, def) {
6028 return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6030 format_value: function(val, def) {
6031 return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6036 This type of field display a list of checkboxes. It works only with m2ms. This field will display one checkbox for each
6037 record existing in the model targeted by the relation, according to the given domain if one is specified. Checked records
6038 will be added to the relation.
6040 instance.web.form.FieldMany2ManyCheckBoxes = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6041 className: "oe_form_many2many_checkboxes",
6043 this._super.apply(this, arguments);
6044 this.set("value", {});
6045 this.set("records", []);
6046 this.field_manager.on("view_content_has_changed", this, function() {
6047 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
6048 if (! _.isEqual(domain, this.get("domain"))) {
6049 this.set("domain", domain);
6052 this.records_orderer = new instance.web.DropMisordered();
6054 initialize_field: function() {
6055 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
6056 this.on("change:domain", this, this.query_records);
6057 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
6058 this.on("change:records", this, this.render_value);
6060 query_records: function() {
6062 var model = new openerp.Model(openerp.session, this.field.relation);
6063 this.records_orderer.add(model.call("search", [this.get("domain")], {"context": this.build_context()}).then(function(record_ids) {
6064 return model.call("name_get", [record_ids] , {"context": self.build_context()});
6065 })).then(function(res) {
6066 self.set("records", res);
6069 render_value: function() {
6070 this.$().html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this, selected: this.get("value")}));
6071 var inputs = this.$("input");
6072 inputs.change(_.bind(this.from_dom, this));
6073 if (this.get("effective_readonly"))
6074 inputs.attr("disabled", "true");
6076 from_dom: function() {
6078 this.$("input").each(function() {
6080 new_value[elem.data("record-id")] = elem.attr("checked") ? true : undefined;
6082 if (! _.isEqual(new_value, this.get("value")))
6083 this.internal_set_value(new_value);
6085 set_value: function(value) {
6086 value = value || [];
6087 if (value.length >= 1 && value[0] instanceof Array) {
6088 value = value[0][2];
6091 _.each(value, function(el) {
6092 formatted[JSON.stringify(el)] = true;
6094 this._super(formatted);
6096 get_value: function() {
6097 var value = _.filter(_.keys(this.get("value")), function(el) {
6098 return this.get("value")[el];
6100 value = _.map(value, function(el) {
6101 return JSON.parse(el);
6103 return [commands.replace_with(value)];
6108 This field can be applied on many2many and one2many. It is a read-only field that will display a single link whose name is
6109 "<number of linked records> <label of the field>". When the link is clicked, it will redirect to another act_window
6110 action on the model of the relation and show only the linked records.
6114 * 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
6115 to display (or False to take the default one) and the second element is the type of the view. Defaults to
6116 [[false, "tree"], [false, "form"]] .
6118 instance.web.form.X2ManyCounter = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6119 className: "oe_form_x2many_counter",
6121 this._super.apply(this, arguments);
6122 this.set("value", []);
6123 _.defaults(this.options, {
6124 "views": [[false, "tree"], [false, "form"]],
6127 render_value: function() {
6128 var text = _.str.sprintf("%d %s", this.val().length, this.string);
6129 this.$().html(QWeb.render("X2ManyCounter", {text: text}));
6130 this.$("a").click(_.bind(this.go_to, this));
6133 return this.view.recursive_save().then(_.bind(function() {
6134 var val = this.val();
6136 if (this.field.type === "one2many") {
6137 context["default_" + this.field.relation_field] = this.view.datarecord.id;
6139 var domain = [["id", "in", val]];
6140 return this.do_action({
6141 type: 'ir.actions.act_window',
6143 res_model: this.field.relation,
6144 views: this.options.views,
6152 var value = this.get("value") || [];
6153 if (value.length >= 1 && value[0] instanceof Array) {
6154 value = value[0][2];
6161 This widget is intended to be used on stat button numeric fields. It will display
6162 the value many2many and one2many. It is a read-only field that will
6163 display a simple string "<value of field> <label of the field>"
6165 instance.web.form.StatInfo = instance.web.form.AbstractField.extend({
6166 is_field_number: true,
6168 this._super.apply(this, arguments);
6169 this.internal_set_value(0);
6171 set_value: function(value_) {
6172 if (value_ === false || value_ === undefined) {
6175 this._super.apply(this, [value_]);
6177 render_value: function() {
6179 value: this.get("value") || 0,
6181 if (! this.node.attrs.nolabel) {
6182 options.text = this.string
6184 this.$el.html(QWeb.render("StatInfo", options));
6191 * Registry of form fields, called by :js:`instance.web.FormView`.
6193 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
6194 * will substitute to the <field> tags as defined in OpenERP's views.
6196 instance.web.form.widgets = new instance.web.Registry({
6197 'char' : 'instance.web.form.FieldChar',
6198 'id' : 'instance.web.form.FieldID',
6199 'email' : 'instance.web.form.FieldEmail',
6200 'url' : 'instance.web.form.FieldUrl',
6201 'text' : 'instance.web.form.FieldText',
6202 'html' : 'instance.web.form.FieldTextHtml',
6203 'char_domain': 'instance.web.form.FieldCharDomain',
6204 'date' : 'instance.web.form.FieldDate',
6205 'datetime' : 'instance.web.form.FieldDatetime',
6206 'selection' : 'instance.web.form.FieldSelection',
6207 'radio' : 'instance.web.form.FieldRadio',
6208 'many2one' : 'instance.web.form.FieldMany2One',
6209 'many2onebutton' : 'instance.web.form.Many2OneButton',
6210 'many2many' : 'instance.web.form.FieldMany2Many',
6211 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
6212 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
6213 'one2many' : 'instance.web.form.FieldOne2Many',
6214 'one2many_list' : 'instance.web.form.FieldOne2Many',
6215 'reference' : 'instance.web.form.FieldReference',
6216 'boolean' : 'instance.web.form.FieldBoolean',
6217 'float' : 'instance.web.form.FieldFloat',
6218 'percentpie': 'instance.web.form.FieldPercentPie',
6219 'barchart': 'instance.web.form.FieldBarChart',
6220 'integer': 'instance.web.form.FieldFloat',
6221 'float_time': 'instance.web.form.FieldFloat',
6222 'progressbar': 'instance.web.form.FieldProgressBar',
6223 'image': 'instance.web.form.FieldBinaryImage',
6224 'binary': 'instance.web.form.FieldBinaryFile',
6225 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
6226 'statusbar': 'instance.web.form.FieldStatus',
6227 'monetary': 'instance.web.form.FieldMonetary',
6228 'many2many_checkboxes': 'instance.web.form.FieldMany2ManyCheckBoxes',
6229 'x2many_counter': 'instance.web.form.X2ManyCounter',
6230 'priority':'instance.web.form.Priority',
6231 'kanban_state_selection':'instance.web.form.KanbanSelection',
6232 'statinfo': 'instance.web.form.StatInfo',
6236 * Registry of widgets usable in the form view that can substitute to any possible
6237 * tags defined in OpenERP's form views.
6239 * Every referenced class should extend FormWidget.
6241 instance.web.form.tags = new instance.web.Registry({
6242 'button' : 'instance.web.form.WidgetButton',
6245 instance.web.form.custom_widgets = new instance.web.Registry({
6250 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: