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.render_value_defs = [];
123 this.reload_mutex = new $.Mutex();
124 this.__clicked_inside = false;
125 this.__blur_timeout = null;
126 this.rendering_engine = new instance.web.form.FormRenderingEngine(this);
127 self.set({actual_mode: self.options.initial_mode});
128 this.has_been_loaded.done(function() {
129 self.on("change:actual_mode", self, self.check_actual_mode);
130 self.check_actual_mode();
131 self.on("change:actual_mode", self, self.init_pager);
134 self.on("load_record", self, self.load_record);
135 instance.web.bus.on('clear_uncommitted_changes', this, function(e) {
136 if (!this.can_be_discarded()) {
141 view_loading: function(r) {
142 return this.load_form(r);
144 destroy: function() {
145 _.each(this.get_widgets(), function(w) {
146 w.off('focused blurred');
150 this.$el.off('.formBlur');
154 load_form: function(data) {
157 throw new Error(_t("No data provided."));
160 throw "Form view does not support multiple calls to load_form";
162 this.fields_order = [];
163 this.fields_view = data;
165 this.rendering_engine.set_fields_registry(this.fields_registry);
166 this.rendering_engine.set_tags_registry(this.tags_registry);
167 this.rendering_engine.set_widgets_registry(this.widgets_registry);
168 this.rendering_engine.set_fields_view(data);
169 var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container');
170 this.rendering_engine.render_to($dest);
172 this.$el.on('mousedown.formBlur', function () {
173 self.__clicked_inside = true;
176 this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self}));
177 if (this.options.$buttons) {
178 this.$buttons.appendTo(this.options.$buttons);
180 this.$el.find('.oe_form_buttons').replaceWith(this.$buttons);
182 this.$buttons.on('click', '.oe_form_button_create',
183 this.guard_active(this.on_button_create));
184 this.$buttons.on('click', '.oe_form_button_edit',
185 this.guard_active(this.on_button_edit));
186 this.$buttons.on('click', '.oe_form_button_save',
187 this.guard_active(this.on_button_save));
188 this.$buttons.on('click', '.oe_form_button_cancel',
189 this.guard_active(this.on_button_cancel));
190 if (this.options.footer_to_buttons) {
191 this.$el.find('footer').appendTo(this.$buttons);
194 this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar');
195 if (!this.sidebar && this.options.$sidebar) {
196 this.sidebar = new instance.web.Sidebar(this);
197 this.sidebar.appendTo(this.$sidebar);
198 if (this.fields_view.toolbar) {
199 this.sidebar.add_toolbar(this.fields_view.toolbar);
201 this.sidebar.add_items('other', _.compact([
202 self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete },
203 self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate }
207 this.has_been_loaded.resolve();
209 // Add bounce effect on button 'Edit' when click on readonly page view.
210 this.$el.find(".oe_form_group_row,.oe_form_field,label,h1,.oe_title,.oe_notebook_page, .oe_list_content").on('click', function (e) {
211 if(self.get("actual_mode") == "view") {
212 var $button = self.options.$buttons.find(".oe_form_button_edit");
213 $button.openerpBounce();
215 instance.web.bus.trigger('click', e);
218 //bounce effect on red button when click on statusbar.
219 this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) {
220 if((self.get("actual_mode") == "view")) {
221 var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'});
222 $button.openerpBounce();
226 this.trigger('form_view_loaded', data);
229 widgetFocused: function() {
230 // Clear click flag if used to focus a widget
231 this.__clicked_inside = false;
232 if (this.__blur_timeout) {
233 clearTimeout(this.__blur_timeout);
234 this.__blur_timeout = null;
237 widgetBlurred: function() {
238 if (this.__clicked_inside) {
239 // clicked in an other section of the form (than the currently
240 // focused widget) => just ignore the blurring entirely?
241 this.__clicked_inside = false;
245 // clear timeout, if any
246 this.widgetFocused();
247 this.__blur_timeout = setTimeout(function () {
248 self.trigger('blurred');
252 do_load_state: function(state, warm) {
253 if (state.id && this.datarecord.id != state.id) {
254 if (this.dataset.get_id_index(state.id) === null) {
255 this.dataset.ids.push(state.id);
257 this.dataset.select_id(state.id);
258 this.do_show({ reload: warm });
263 * @param {Object} [options]
264 * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view".
265 * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record
266 * @return {$.Deferred}
268 do_show: function (options) {
270 options = options || {};
272 this.sidebar.$el.show();
275 this.$buttons.show();
277 this.$el.show().css({
279 filter: 'alpha(opacity = 0)'
281 this.$el.add(this.$buttons).removeClass('oe_form_dirty');
283 var shown = this.has_been_loaded;
284 if (options.reload !== false) {
285 shown = shown.then(function() {
286 if (self.dataset.index === null) {
287 // null index means we should start a new record
288 return self.on_button_new();
290 var fields = _.keys(self.fields_view.fields);
291 fields.push('display_name');
292 return self.dataset.read_index(fields, {
293 context: { 'bin_size': true, 'future_display_name' : true }
294 }).then(function(r) {
295 self.trigger('load_record', r);
299 return shown.then(function() {
300 self._actualize_mode(options.mode || self.options.initial_mode);
303 filter: 'alpha(opacity = 100)'
307 do_hide: function () {
309 this.sidebar.$el.hide();
312 this.$buttons.hide();
319 load_record: function(record) {
320 var self = this, set_values = [];
322 this.set({ 'title' : undefined });
323 this.do_warn(_t("Form"), _t("The record could not be found in the database."), true);
324 return $.Deferred().reject();
326 this.datarecord = record;
327 this._actualize_mode();
328 this.set({ 'title' : record.id ? record.display_name : _t("New") });
330 _(this.fields).each(function (field, f) {
331 field._dirty_flag = false;
332 field._inhibit_on_change_flag = true;
333 var result = field.set_value(self.datarecord[f] || false);
334 field._inhibit_on_change_flag = false;
335 set_values.push(result);
337 return $.when.apply(null, set_values).then(function() {
339 // New record: Second pass in order to trigger the onchanges
340 // respecting the fields order defined in the view
341 _.each(self.fields_order, function(field_name) {
342 if (record[field_name] !== undefined) {
343 var field = self.fields[field_name];
344 field._dirty_flag = true;
345 self.do_onchange(field);
349 self.on_form_changed();
350 self.rendering_engine.init_fields();
351 self.is_initialized.resolve();
352 self.do_update_pager(record.id === null || record.id === undefined);
354 self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
357 self.do_push_state({id:record.id});
359 self.do_push_state({});
361 self.$el.add(self.$buttons).removeClass('oe_form_dirty');
366 * Loads and sets up the default values for the model as the current
369 * @return {$.Deferred}
371 load_defaults: function () {
373 var keys = _.keys(this.fields_view.fields);
375 return this.dataset.default_get(keys).then(function(r) {
376 self.trigger('load_record', r);
379 return self.trigger('load_record', {});
381 on_form_changed: function() {
382 this.trigger("view_content_has_changed");
384 do_notify_change: function() {
385 this.$el.add(this.$buttons).addClass('oe_form_dirty');
387 execute_pager_action: function(action) {
388 if (this.can_be_discarded()) {
391 this.dataset.index = 0;
394 this.dataset.previous();
400 this.dataset.index = this.dataset.ids.length - 1;
403 var def = this.reload();
404 this.trigger('pager_action_executed');
409 init_pager: function() {
412 this.$pager.remove();
413 if (this.get("actual_mode") === "create")
415 this.$pager = $(QWeb.render("FormView.pager", {'widget':self})).hide();
416 if (this.options.$pager) {
417 this.$pager.appendTo(this.options.$pager);
419 this.$el.find('.oe_form_pager').replaceWith(this.$pager);
421 this.$pager.on('click','a[data-pager-action]',function() {
423 if ($el.attr("disabled"))
425 var action = $el.data('pager-action');
426 var def = $.when(self.execute_pager_action(action));
427 $el.attr("disabled");
428 def.always(function() {
429 $el.removeAttr("disabled");
432 this.do_update_pager();
434 do_update_pager: function(hide_index) {
435 this.$pager.toggle(this.dataset.ids.length > 1);
437 $(".oe_form_pager_state", this.$pager).html("");
439 $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
442 parse_on_change: function (on_change, widget) {
444 var onchange = _.str.trim(on_change);
445 var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
447 throw new Error(_.str.sprintf( _t("Wrong on change format: %s"), onchange ));
450 var method = call[1];
451 if (!_.str.trim(call[2])) {
452 return {method: method, args: []};
455 var argument_replacement = {
456 'False': function () {return false;},
457 'True': function () {return true;},
458 'None': function () {return null;},
459 'context': function () {
460 return new instance.web.CompoundContext(
461 self.dataset.get_context(),
462 widget.build_context() ? widget.build_context() : {});
465 var parent_fields = null;
466 var args = _.map(call[2].split(','), function (a, i) {
467 var field = _.str.trim(a);
469 // literal constant or context
470 if (field in argument_replacement) {
471 return argument_replacement[field]();
474 if (/^-?\d+(\.\d+)?$/.test(field)) {
475 return Number(field);
478 if (self.fields[field]) {
479 var value_ = self.fields[field].get_value();
480 return value_ === null || value_ === undefined ? false : value_;
483 var splitted = field.split('.');
484 if (splitted.length > 1 && _.str.trim(splitted[0]) === "parent" && self.dataset.parent_view) {
485 if (parent_fields === null) {
486 parent_fields = self.dataset.parent_view.get_fields_values();
488 var p_val = parent_fields[_.str.trim(splitted[1])];
489 if (p_val !== undefined) {
490 return p_val === null || p_val === undefined ? false : p_val;
494 var first_char = field[0], last_char = field[field.length-1];
495 if ((first_char === '"' && last_char === '"')
496 || (first_char === "'" && last_char === "'")) {
497 return field.slice(1, -1);
500 throw new Error("Could not get field with name '" + field +
501 "' for onchange '" + onchange + "'");
509 do_onchange: function(widget, processed) {
511 this.on_change_list = [{widget: widget, processed: processed}].concat(this.on_change_list);
512 return this._process_operations();
514 _process_onchange: function(on_change_obj) {
516 var widget = on_change_obj.widget;
517 var processed = on_change_obj.processed;
520 processed = processed || [];
521 processed.push(widget.name);
522 var on_change = widget.node.attrs.on_change;
524 var change_spec = self.parse_on_change(on_change, widget);
526 if (self.datarecord.id && !instance.web.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) {
527 // In case of a o2m virtual id, we should pass an empty ids list
528 ids.push(self.datarecord.id);
530 def = self.alive(new instance.web.Model(self.dataset.model).call(
531 change_spec.method, [ids].concat(change_spec.args)));
535 return def.then(function(response) {
536 if (widget.field['change_default']) {
537 var fieldname = widget.name;
539 if (response.value && (fieldname in response.value)) {
540 // Use value from onchange if onchange executed
541 value_ = response.value[fieldname];
543 // otherwise get form value for field
544 value_ = self.fields[fieldname].get_value();
546 var condition = fieldname + '=' + value_;
549 return self.alive(new instance.web.Model('ir.values').call(
550 'get_defaults', [self.model, condition]
551 )).then(function (results) {
552 if (!results.length) {
555 if (!response.value) {
558 for(var i=0; i<results.length; ++i) {
559 // [whatever, key, value]
560 var triplet = results[i];
561 response.value[triplet[1]] = triplet[2];
568 }).then(function(response) {
569 return self.on_processed_onchange(response, processed);
573 instance.webclient.crashmanager.show_message(e);
574 return $.Deferred().reject();
577 on_processed_onchange: function(result, processed) {
579 var fields = this.fields;
580 _(result.domain).each(function (domain, fieldname) {
581 var field = fields[fieldname];
582 if (!field) { return; }
583 field.node.attrs.domain = domain;
587 this._internal_set_values(result.value, processed);
589 if (!_.isEmpty(result.warning)) {
590 instance.web.dialog($(QWeb.render("CrashManager.warning", result.warning)), {
591 title:result.warning.title,
594 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
599 return $.Deferred().resolve();
602 instance.webclient.crashmanager.show_message(e);
603 return $.Deferred().reject();
606 _process_operations: function() {
608 return this.mutating_mutex.exec(function() {
610 var on_change_obj = self.on_change_list.shift();
612 return self._process_onchange(on_change_obj).then(function() {
617 _.each(self.fields, function(field) {
618 defs.push(field.commit_value());
620 var args = _.toArray(arguments);
621 return $.when.apply($, defs).then(function() {
622 if (self.on_change_list.length !== 0) {
625 var save_obj = self.save_list.pop();
627 return self._process_save(save_obj).then(function() {
628 save_obj.ret = _.toArray(arguments);
631 save_obj.error = true;
636 self.save_list.pop();
643 _internal_set_values: function(values, exclude) {
644 exclude = exclude || [];
645 for (var f in values) {
646 if (!values.hasOwnProperty(f)) { continue; }
647 var field = this.fields[f];
648 // If field is not defined in the view, just ignore it
650 var value_ = values[f];
651 if (field.get_value() != value_) {
652 field._inhibit_on_change_flag = true;
653 field.set_value(value_);
654 field._inhibit_on_change_flag = false;
655 field._dirty_flag = true;
656 if (!_.contains(exclude, field.name)) {
657 this.do_onchange(field, exclude);
662 this.on_form_changed();
664 set_values: function(values) {
666 return this.mutating_mutex.exec(function() {
667 self._internal_set_values(values);
671 * Ask the view to switch to view mode if possible. The view may not do it
672 * if the current record is not yet saved. It will then stay in create mode.
674 to_view_mode: function() {
675 this._actualize_mode("view");
678 * Ask the view to switch to edit mode if possible. The view may not do it
679 * if the current record is not yet saved. It will then stay in create mode.
681 to_edit_mode: function() {
682 this._actualize_mode("edit");
685 * Ask the view to switch to a precise mode if possible. The view is free to
686 * not respect this command if the state of the dataset is not compatible with
687 * the new mode. For example, it is not possible to switch to edit mode if
688 * the current record is not yet saved in database.
690 * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
691 * undefined the view will test the actual mode to check if it is still consistent
692 * with the dataset state.
694 _actualize_mode: function(switch_to) {
695 var mode = switch_to || this.get("actual_mode");
696 if (! this.datarecord.id) {
698 } else if (mode === "create") {
701 this.render_value_defs = [];
702 this.set({actual_mode: mode});
704 check_actual_mode: function(source, options) {
706 if(this.get("actual_mode") === "view") {
707 self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
708 self.$buttons.find('.oe_form_buttons_edit').hide();
709 self.$buttons.find('.oe_form_buttons_view').show();
710 self.$sidebar.show();
712 self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
713 self.$buttons.find('.oe_form_buttons_edit').show();
714 self.$buttons.find('.oe_form_buttons_view').hide();
715 self.$sidebar.hide();
719 autofocus: function() {
720 if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
721 var fields_order = this.fields_order.slice(0);
722 if (this.default_focus_field) {
723 fields_order.unshift(this.default_focus_field.name);
725 for (var i = 0; i < fields_order.length; i += 1) {
726 var field = this.fields[fields_order[i]];
727 if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
728 if (field.focus() !== false) {
735 on_button_save: function(e) {
737 $(e.target).attr("disabled", true);
738 return this.save().done(function(result) {
739 self.trigger("save", result);
740 self.reload().then(function() {
742 var parent = self.ViewManager.ActionManager.getParent();
744 parent.menu.do_reload_needaction();
747 }).always(function(){
748 $(e.target).attr("disabled", false);
751 on_button_cancel: function(event) {
753 if (this.can_be_discarded()) {
754 if (this.get('actual_mode') === 'create') {
755 this.trigger('history_back');
758 $.when.apply(null, this.render_value_defs).then(function(){
759 self.trigger('load_record', self.datarecord);
763 this.trigger('on_button_cancel');
766 on_button_new: function() {
769 return $.when(this.has_been_loaded).then(function() {
770 if (self.can_be_discarded()) {
771 return self.load_defaults();
775 on_button_edit: function() {
776 return this.to_edit_mode();
778 on_button_create: function() {
779 this.dataset.index = null;
782 on_button_duplicate: function() {
784 return this.has_been_loaded.then(function() {
785 return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
786 self.record_created(new_id);
791 on_button_delete: function() {
793 var def = $.Deferred();
794 this.has_been_loaded.done(function() {
795 if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
796 self.dataset.unlink([self.datarecord.id]).done(function() {
797 if (self.dataset.size()) {
798 self.execute_pager_action('next');
800 self.do_action('history_back');
805 $.async_when().done(function () {
810 return def.promise();
812 can_be_discarded: function() {
813 if (this.$el.is('.oe_form_dirty')) {
814 if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
817 this.$el.removeClass('oe_form_dirty');
822 * Triggers saving the form's record. Chooses between creating a new
823 * record or saving an existing one depending on whether the record
824 * already has an id property.
826 * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
827 * record, should that record be inserted at the start of the dataset (by
828 * default, records are added at the end)
830 save: function(prepend_on_create) {
832 var save_obj = {prepend_on_create: prepend_on_create, ret: null};
833 this.save_list.push(save_obj);
834 return this._process_operations().then(function() {
836 return $.Deferred().reject();
837 return $.when.apply($, save_obj.ret);
839 self.$el.removeClass('oe_form_dirty');
842 _process_save: function(save_obj) {
844 var prepend_on_create = save_obj.prepend_on_create;
846 var form_invalid = false,
848 first_invalid_field = null,
849 readonly_values = {};
850 for (var f in self.fields) {
851 if (!self.fields.hasOwnProperty(f)) { continue; }
855 if (!first_invalid_field) {
856 first_invalid_field = f;
858 } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
859 // Special case 'id' field, do not save this field
860 // on 'create' : save all non readonly fields
861 // on 'edit' : save non readonly modified fields
862 if (!f.get("readonly")) {
863 values[f.name] = f.get_value();
865 readonly_values[f.name] = f.get_value();
870 self.set({'display_invalid_fields': true});
871 first_invalid_field.focus();
873 return $.Deferred().reject();
875 self.set({'display_invalid_fields': false});
877 if (!self.datarecord.id) {
879 save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
880 return self.record_created(r, prepend_on_create);
882 } else if (_.isEmpty(values)) {
883 // Not dirty, noop save
884 save_deferral = $.Deferred().resolve({}).promise();
887 save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
888 return self.record_saved(r);
891 return save_deferral;
895 return $.Deferred().reject();
898 on_invalid: function() {
899 var warnings = _(this.fields).chain()
900 .filter(function (f) { return !f.is_valid(); })
902 return _.str.sprintf('<li>%s</li>',
905 warnings.unshift('<ul>');
906 warnings.push('</ul>');
907 this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
910 * Reload the form after saving
912 * @param {Object} r result of the write function.
914 record_saved: function(r) {
915 this.trigger('record_saved', r);
917 // should not happen in the server, but may happen for internal purpose
918 return $.Deferred().reject();
923 * Updates the form' dataset to contain the new record:
925 * * Adds the newly created record to the current dataset (at the end by
927 * * Selects that record (sets the dataset's index to point to the new
929 * * Updates the pager and sidebar displays
932 * @param {Boolean} [prepend_on_create=false] adds the newly created record
933 * at the beginning of the dataset instead of the end
935 record_created: function(r, prepend_on_create) {
938 // should not happen in the server, but may happen for internal purpose
939 this.trigger('record_created', r);
940 return $.Deferred().reject();
942 this.datarecord.id = r;
943 if (!prepend_on_create) {
944 this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
945 this.dataset.index = this.dataset.ids.length - 1;
947 this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
948 this.dataset.index = 0;
950 this.do_update_pager();
952 this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
954 //openerp.log("The record has been created with id #" + this.datarecord.id);
955 return $.when(this.reload()).then(function () {
956 self.trigger('record_created', r);
957 return _.extend(r, {created: true});
961 on_action: function (action) {
962 console.debug('Executing action', action);
966 return this.reload_mutex.exec(function() {
967 if (self.dataset.index === null || self.dataset.index === undefined) {
968 self.trigger("previous_view");
969 return $.Deferred().reject().promise();
971 if (self.dataset.index < 0) {
972 return $.when(self.on_button_new());
974 var fields = _.keys(self.fields_view.fields);
975 fields.push('display_name');
976 return self.dataset.read_index(fields,
980 'future_display_name': true
982 check_access_rule: true
983 }).then(function(r) {
984 self.trigger('load_record', r);
986 self.do_action('history_back');
991 get_widgets: function() {
992 return _.filter(this.getChildren(), function(obj) {
993 return obj instanceof instance.web.form.FormWidget;
996 get_fields_values: function() {
998 var ids = this.get_selected_ids();
999 values["id"] = ids.length > 0 ? ids[0] : false;
1000 _.each(this.fields, function(value_, key) {
1001 values[key] = value_.get_value();
1005 get_selected_ids: function() {
1006 var id = this.dataset.ids[this.dataset.index];
1007 return id ? [id] : [];
1009 recursive_save: function() {
1011 return $.when(this.save()).then(function(res) {
1012 if (self.dataset.parent_view)
1013 return self.dataset.parent_view.recursive_save();
1016 recursive_reload: function() {
1019 if (self.dataset.parent_view)
1020 pre = self.dataset.parent_view.recursive_reload();
1021 return pre.then(function() {
1022 return self.reload();
1025 is_dirty: function() {
1026 return _.any(this.fields, function (value_) {
1027 return value_._dirty_flag;
1030 is_interactible_record: function() {
1031 var id = this.datarecord.id;
1033 if (this.options.not_interactible_on_create)
1035 } else if (typeof(id) === "string") {
1036 if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
1041 sidebar_eval_context: function () {
1042 return $.when(this.build_eval_context());
1044 open_defaults_dialog: function () {
1046 var display = function (field, value) {
1047 if (!value) { return value; }
1048 if (field instanceof instance.web.form.FieldSelection) {
1049 return _(field.get('values')).find(function (option) {
1050 return option[0] === value;
1052 } else if (field instanceof instance.web.form.FieldMany2One) {
1053 return field.get_displayed();
1057 var fields = _.chain(this.fields)
1058 .map(function (field) {
1059 var value = field.get_value();
1060 // ignore fields which are empty, invisible, readonly, o2m
1063 || field.get('invisible')
1064 || field.get("readonly")
1065 || field.field.type === 'one2many'
1066 || field.field.type === 'many2many'
1067 || field.field.type === 'binary'
1068 || field.password) {
1074 string: field.string,
1076 displayed: display(field, value),
1080 .sortBy(function (field) { return field.string; })
1082 var conditions = _.chain(self.fields)
1083 .filter(function (field) { return field.field.change_default; })
1084 .map(function (field) {
1085 var value = field.get_value();
1088 string: field.string,
1090 displayed: display(field, value),
1095 var d = new instance.web.Dialog(this, {
1096 title: _t("Set Default"),
1099 conditions: conditions
1102 {text: _t("Close"), click: function () { d.close(); }},
1103 {text: _t("Save default"), click: function () {
1104 var $defaults = d.$el.find('#formview_default_fields');
1105 var field_to_set = $defaults.val();
1106 if (!field_to_set) {
1107 $defaults.parent().addClass('oe_form_invalid');
1110 var condition = d.$el.find('#formview_default_conditions').val(),
1111 all_users = d.$el.find('#formview_default_all').is(':checked');
1112 new instance.web.DataSet(self, 'ir.values').call(
1116 self.fields[field_to_set].get_value(),
1120 ]).done(function () { d.close(); });
1124 d.template = 'FormView.set_default';
1127 register_field: function(field, name) {
1128 this.fields[name] = field;
1129 this.fields_order.push(name);
1130 if (JSON.parse(field.node.attrs.default_focus || "0")) {
1131 this.default_focus_field = field;
1134 field.on('focused', null, this.proxy('widgetFocused'))
1135 .on('blurred', null, this.proxy('widgetBlurred'));
1136 if (this.get_field_desc(name).translate) {
1137 this.translatable_fields.push(field);
1139 field.on('changed_value', this, function() {
1140 if (field.is_syntax_valid()) {
1141 this.trigger('field_changed:' + name);
1143 if (field._inhibit_on_change_flag) {
1146 field._dirty_flag = true;
1147 if (field.is_syntax_valid()) {
1148 this.do_onchange(field);
1149 this.on_form_changed(true);
1150 this.do_notify_change();
1154 get_field_desc: function(field_name) {
1155 return this.fields_view.fields[field_name];
1157 get_field_value: function(field_name) {
1158 return this.fields[field_name].get_value();
1160 compute_domain: function(expression) {
1161 return instance.web.form.compute_domain(expression, this.fields);
1163 _build_view_fields_values: function() {
1164 var a_dataset = this.dataset;
1165 var fields_values = this.get_fields_values();
1166 var active_id = a_dataset.ids[a_dataset.index];
1167 _.extend(fields_values, {
1168 active_id: active_id || false,
1169 active_ids: active_id ? [active_id] : [],
1170 active_model: a_dataset.model,
1173 if (a_dataset.parent_view) {
1174 fields_values.parent = a_dataset.parent_view.get_fields_values();
1176 return fields_values;
1178 build_eval_context: function() {
1179 var a_dataset = this.dataset;
1180 return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
1185 * Interface to be implemented by rendering engines for the form view.
1187 instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
1188 set_fields_view: function(fields_view) {},
1189 set_fields_registry: function(fields_registry) {},
1190 render_to: function($el) {},
1194 * Default rendering engine for the form view.
1196 * It is necessary to set the view using set_view() before usage.
1198 instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
1199 init: function(view) {
1202 set_fields_view: function(fvg) {
1204 this.version = parseFloat(this.fvg.arch.attrs.version);
1205 if (isNaN(this.version)) {
1209 set_tags_registry: function(tags_registry) {
1210 this.tags_registry = tags_registry;
1212 set_fields_registry: function(fields_registry) {
1213 this.fields_registry = fields_registry;
1215 set_widgets_registry: function(widgets_registry) {
1216 this.widgets_registry = widgets_registry;
1218 // Backward compatibility tools, current default version: v6.1
1219 process_version: function() {
1220 if (this.version < 7.0) {
1221 this.$form.find('form:first').wrapInner('<group col="4"/>');
1222 this.$form.find('page').each(function() {
1223 if (!$(this).parents('field').length) {
1224 $(this).wrapInner('<group col="4"/>');
1229 get_arch_fragment: function() {
1230 var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
1231 // IE won't allow custom button@type and will revert it to spec default : 'submit'
1232 $('button', doc).each(function() {
1233 $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
1235 // IE's html parser is also a css parser. How convenient...
1236 $('board', doc).each(function() {
1237 $(this).attr('layout', $(this).attr('style'));
1239 return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
1241 render_to: function($target) {
1243 this.$target = $target;
1245 this.$form = this.get_arch_fragment();
1247 this.process_version();
1249 this.fields_to_init = [];
1250 this.tags_to_init = [];
1251 this.widgets_to_init = [];
1253 this.process(this.$form);
1255 this.$form.appendTo(this.$target);
1257 this.to_replace = [];
1259 _.each(this.fields_to_init, function($elem) {
1260 var name = $elem.attr("name");
1261 if (!self.fvg.fields[name]) {
1262 throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
1264 var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
1266 throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
1268 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1269 var $label = self.labels[$elem.attr("name")];
1271 w.set_input_id($label.attr("for"));
1273 self.alter_field(w);
1274 self.view.register_field(w, $elem.attr("name"));
1275 self.to_replace.push([w, $elem]);
1277 _.each(this.tags_to_init, function($elem) {
1278 var tag_name = $elem[0].tagName.toLowerCase();
1279 var obj = self.tags_registry.get_object(tag_name);
1280 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1281 self.to_replace.push([w, $elem]);
1283 _.each(this.widgets_to_init, function($elem) {
1284 var widget_type = $elem.attr("type");
1285 var obj = self.widgets_registry.get_object(widget_type);
1286 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1287 self.to_replace.push([w, $elem]);
1290 init_fields: function() {
1292 _.each(this.to_replace, function(el) {
1293 defs.push(el[0].replace(el[1]));
1295 this.to_replace = [];
1296 return $.when.apply($, defs);
1298 render_element: function(template /* dictionaries */) {
1299 var dicts = [].slice.call(arguments).slice(1);
1300 var dict = _.extend.apply(_, dicts);
1301 dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
1302 return $(QWeb.render(template, dict));
1304 alter_field: function(field) {
1306 toggle_layout_debugging: function() {
1307 if (!this.$target.has('.oe_layout_debug_cell:first').length) {
1308 this.$target.find('[title]').removeAttr('title');
1309 this.$target.find('.oe_form_group_cell').each(function() {
1310 var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
1311 $(this).attr('title', text);
1314 this.$target.toggleClass('oe_layout_debugging');
1316 process: function($tag) {
1318 var tagname = $tag[0].nodeName.toLowerCase();
1319 if (this.tags_registry.contains(tagname)) {
1320 this.tags_to_init.push($tag);
1323 var fn = self['process_' + tagname];
1325 var args = [].slice.call(arguments);
1327 return fn.apply(self, args);
1329 // generic tag handling, just process children
1330 $tag.children().each(function() {
1331 self.process($(this));
1333 self.handle_common_properties($tag, $tag);
1334 $tag.removeAttr("modifiers");
1338 process_widget: function($widget) {
1339 this.widgets_to_init.push($widget);
1342 process_sheet: function($sheet) {
1343 var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
1344 this.handle_common_properties($new_sheet, $sheet);
1345 var $dst = $new_sheet.find('.oe_form_sheet');
1346 $sheet.contents().appendTo($dst);
1347 $sheet.before($new_sheet).remove();
1348 this.process($new_sheet);
1350 process_form: function($form) {
1351 if ($form.find('> sheet').length === 0) {
1352 $form.addClass('oe_form_nosheet');
1354 var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
1355 this.handle_common_properties($new_form, $form);
1356 $form.contents().appendTo($new_form);
1357 if ($form[0] === this.$form[0]) {
1358 // If root element, replace it
1359 this.$form = $new_form;
1361 $form.before($new_form).remove();
1363 this.process($new_form);
1366 * Used by direct <field> children of a <group> tag only
1367 * This method will add the implicit <label...> for every field
1370 preprocess_field: function($field) {
1372 var name = $field.attr('name'),
1373 field_colspan = parseInt($field.attr('colspan'), 10),
1374 field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
1376 if ($field.attr('nolabel') === '1')
1378 $field.attr('nolabel', '1');
1380 this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
1381 $(el).parents().each(function(unused, tag) {
1382 var name = tag.tagName.toLowerCase();
1383 if (name === "field" || name in self.tags_registry.map)
1390 var $label = $('<label/>').attr({
1392 "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
1393 "string": $field.attr('string'),
1394 "help": $field.attr('help'),
1395 "class": $field.attr('class'),
1397 $label.insertBefore($field);
1398 if (field_colspan > 1) {
1399 $field.attr('colspan', field_colspan - 1);
1403 process_field: function($field) {
1404 if ($field.parent().is('group')) {
1405 // No implicit labels for normal fields, only for <group> direct children
1406 var $label = this.preprocess_field($field);
1408 this.process($label);
1411 this.fields_to_init.push($field);
1414 process_group: function($group) {
1416 $group.children('field').each(function() {
1417 self.preprocess_field($(this));
1419 var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
1421 if ($new_group.first().is('table.oe_form_group')) {
1422 $table = $new_group;
1423 } else if ($new_group.filter('table.oe_form_group').length) {
1424 $table = $new_group.filter('table.oe_form_group').first();
1426 $table = $new_group.find('table.oe_form_group').first();
1430 cols = parseInt($group.attr('col') || 2, 10),
1434 $group.children().each(function(a,b,c) {
1435 var $child = $(this);
1436 var colspan = parseInt($child.attr('colspan') || 1, 10);
1437 var tagName = $child[0].tagName.toLowerCase();
1438 var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
1439 var newline = tagName === 'newline';
1441 // Note FME: those classes are used in layout debug mode
1442 if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
1443 $tr.addClass('oe_form_group_row_incomplete');
1445 $tr.addClass('oe_form_group_row_newline');
1452 if (!$tr || row_cols < colspan) {
1453 $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
1455 } else if (tagName==='group') {
1456 // When <group> <group/><group/> </group>, we need a spacing between the two groups
1457 $td.addClass('oe_group_right');
1459 row_cols -= colspan;
1461 // invisibility transfer
1462 var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
1463 var invisible = field_modifiers.invisible;
1464 self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
1466 $tr.append($td.append($child));
1467 children.push($child[0]);
1469 if (row_cols && $td) {
1470 $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
1472 $group.before($new_group).remove();
1474 $table.find('> tbody > tr').each(function() {
1475 var to_compute = [],
1478 $(this).children().each(function() {
1480 $child = $td.children(':first');
1481 if ($child.attr('cell-class')) {
1482 $td.addClass($child.attr('cell-class'));
1484 switch ($child[0].tagName.toLowerCase()) {
1488 if ($child.attr('for')) {
1489 $td.attr('width', '1%').addClass('oe_form_group_cell_label');
1490 row_cols-= $td.attr('colspan') || 1;
1495 var width = _.str.trim($child.attr('width') || ''),
1496 iwidth = parseInt(width, 10);
1498 if (width.substr(-1) === '%') {
1500 width = iwidth + '%';
1503 $td.css('min-width', width + 'px');
1505 $td.attr('width', width);
1506 $child.removeAttr('width');
1507 row_cols-= $td.attr('colspan') || 1;
1509 to_compute.push($td);
1515 var unit = Math.floor(total / row_cols);
1516 if (!$(this).is('.oe_form_group_row_incomplete')) {
1517 _.each(to_compute, function($td, i) {
1518 var width = parseInt($td.attr('colspan'), 10) * unit;
1519 $td.attr('width', width + '%');
1525 _.each(children, function(el) {
1526 self.process($(el));
1528 this.handle_common_properties($new_group, $group);
1531 process_notebook: function($notebook) {
1534 $notebook.find('> page').each(function() {
1535 var $page = $(this);
1536 var page_attrs = $page.getAttributes();
1537 page_attrs.id = _.uniqueId('notebook_page_');
1538 var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
1539 $page.contents().appendTo($new_page);
1540 $page.before($new_page).remove();
1541 var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
1542 page_attrs.__page = $new_page;
1543 page_attrs.__ic = ic;
1544 pages.push(page_attrs);
1546 $new_page.children().each(function() {
1547 self.process($(this));
1550 var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
1551 $notebook.contents().appendTo($new_notebook);
1552 $notebook.before($new_notebook).remove();
1553 self.process($($new_notebook.children()[0]));
1554 //tabs and invisibility handling
1555 $new_notebook.tabs();
1556 _.each(pages, function(page, i) {
1559 page.__ic.on("change:effective_invisible", null, function() {
1560 if (!page.__ic.get('effective_invisible') && page.autofocus) {
1561 $new_notebook.tabs('select', i);
1564 var current = $new_notebook.tabs("option", "selected");
1565 if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
1567 var first_visible = _.find(_.range(pages.length), function(i2) {
1568 return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
1570 if (first_visible !== undefined) {
1571 $new_notebook.tabs('select', first_visible);
1576 this.handle_common_properties($new_notebook, $notebook);
1577 return $new_notebook;
1579 process_separator: function($separator) {
1580 var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
1581 $separator.before($new_separator).remove();
1582 this.handle_common_properties($new_separator, $separator);
1583 return $new_separator;
1585 process_label: function($label) {
1586 var name = $label.attr("for"),
1587 field_orm = this.fvg.fields[name];
1589 string: $label.attr('string') || (field_orm || {}).string || '',
1590 help: $label.attr('help') || (field_orm || {}).help || '',
1591 _for: name ? _.uniqueId('oe-field-input-') : undefined,
1593 var align = parseFloat(dict.align);
1594 if (isNaN(align) || align === 1) {
1596 } else if (align === 0) {
1602 var $new_label = this.render_element('FormRenderingLabel', dict);
1603 $label.before($new_label).remove();
1604 this.handle_common_properties($new_label, $label);
1606 this.labels[name] = $new_label;
1610 handle_common_properties: function($new_element, $node) {
1611 var str_modifiers = $node.attr("modifiers") || "{}";
1612 var modifiers = JSON.parse(str_modifiers);
1614 if (modifiers.invisible !== undefined)
1615 ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
1616 $new_element.addClass($node.attr("class") || "");
1617 $new_element.attr('style', $node.attr('style'));
1618 return {invisibility_changer: ic,};
1625 If you read this documentation, it probably means that you were asked to use a form view widget outside of
1626 a form view. Before going further, you must understand that those fields were never really created for
1627 that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
1628 you to hack the system with more style.
1630 instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
1631 init: function(parent, eval_context) {
1632 this._super(parent);
1633 this.field_descs = {};
1634 this.eval_context = eval_context || {};
1636 display_invalid_fields: false,
1637 actual_mode: 'create',
1640 get_field_desc: function(field_name) {
1641 if (this.field_descs[field_name] === undefined) {
1642 this.field_descs[field_name] = {
1646 return this.field_descs[field_name];
1648 extend_field_desc: function(fields) {
1650 _.each(fields, function(v, k) {
1651 _.extend(self.get_field_desc(k), v);
1654 get_field_value: function(field_name) {
1657 set_values: function(values) {
1660 compute_domain: function(expression) {
1661 return instance.web.form.compute_domain(expression, {});
1663 build_eval_context: function() {
1664 return new instance.web.CompoundContext(this.eval_context);
1668 instance.web.form.compute_domain = function(expr, fields) {
1669 if (! (expr instanceof Array))
1672 for (var i = expr.length - 1; i >= 0; i--) {
1674 if (ex.length == 1) {
1675 var top = stack.pop();
1678 stack.push(stack.pop() || top);
1681 stack.push(stack.pop() && top);
1687 throw new Error(_.str.sprintf(
1688 _t("Unknown operator %s in domain %s"),
1689 ex, JSON.stringify(expr)));
1693 var field = fields[ex[0]];
1695 throw new Error(_.str.sprintf(
1696 _t("Unknown field %s in domain %s"),
1697 ex[0], JSON.stringify(expr)));
1699 var field_value = field.get_value ? field.get_value() : field.value;
1703 switch (op.toLowerCase()) {
1706 stack.push(_.isEqual(field_value, val));
1710 stack.push(!_.isEqual(field_value, val));
1713 stack.push(field_value < val);
1716 stack.push(field_value > val);
1719 stack.push(field_value <= val);
1722 stack.push(field_value >= val);
1725 if (!_.isArray(val)) val = [val];
1726 stack.push(_(val).contains(field_value));
1729 if (!_.isArray(val)) val = [val];
1730 stack.push(!_(val).contains(field_value));
1734 _t("Unsupported operator %s in domain %s"),
1735 op, JSON.stringify(expr));
1738 return _.all(stack, _.identity);
1741 instance.web.form.is_bin_size = function(v) {
1742 return (/^\d+(\.\d*)? \w+$/).test(v);
1746 * Must be applied over an class already possessing the PropertiesMixin.
1748 * Apply the result of the "invisible" domain to this.$el.
1750 instance.web.form.InvisibilityChangerMixin = {
1751 init: function(field_manager, invisible_domain) {
1753 this._ic_field_manager = field_manager;
1754 this._ic_invisible_modifier = invisible_domain;
1755 this._ic_field_manager.on("view_content_has_changed", this, function() {
1756 var result = self._ic_invisible_modifier === undefined ? false :
1757 self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
1758 self.set({"invisible": result});
1760 this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
1761 var check = function() {
1762 if (self.get("invisible") || self.get('force_invisible')) {
1763 self.set({"effective_invisible": true});
1765 self.set({"effective_invisible": false});
1768 this.on('change:invisible', this, check);
1769 this.on('change:force_invisible', this, check);
1773 this.on("change:effective_invisible", this, this._check_visibility);
1774 this._check_visibility();
1776 _check_visibility: function() {
1777 this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
1781 instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
1782 init: function(parent, field_manager, invisible_domain, $el) {
1783 this.setParent(parent);
1784 instance.web.PropertiesMixin.init.call(this);
1785 instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
1792 Base class for all fields, custom widgets and buttons to be displayed in the form view.
1795 - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
1796 the values of the "readonly" property and the "mode" property on the field manager.
1798 instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
1800 * @constructs instance.web.form.FormWidget
1801 * @extends instance.web.Widget
1803 * @param field_manager
1806 init: function(field_manager, node) {
1807 this._super(field_manager);
1808 this.field_manager = field_manager;
1809 if (this.field_manager instanceof instance.web.FormView)
1810 this.view = this.field_manager;
1812 this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
1813 instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
1815 this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
1821 // some events to make the property "effective_readonly" sync automatically with "readonly" and
1822 // "mode" on field_manager
1824 var test_effective_readonly = function() {
1825 self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
1827 this.on("change:readonly", this, test_effective_readonly);
1828 this.field_manager.on("change:actual_mode", this, test_effective_readonly);
1829 test_effective_readonly.call(this);
1831 renderElement: function() {
1832 this.process_modifiers();
1834 this.$el.addClass(this.node.attrs["class"] || "");
1836 destroy: function() {
1838 this._super.apply(this, arguments);
1841 * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
1843 * This method is an utility method that is meant to be called by child classes.
1845 * @param {jQuery} $e jQuery object of elements to bind focus/blur on
1847 setupFocus: function ($e) {
1850 focus: function () { self.trigger('focused'); },
1851 blur: function () { self.trigger('blurred'); }
1854 process_modifiers: function() {
1856 for (var a in this.modifiers) {
1857 if (!this.modifiers.hasOwnProperty(a)) { continue; }
1858 if (!_.include(["invisible"], a)) {
1859 var val = this.field_manager.compute_domain(this.modifiers[a]);
1865 do_attach_tooltip: function(widget, trigger, options) {
1866 widget = widget || this;
1867 trigger = trigger || this.$el;
1868 options = _.extend({
1873 var template = widget.template + '.tooltip';
1874 if (!QWeb.has_template(template)) {
1875 template = 'WidgetLabel.tooltip';
1877 return QWeb.render(template, {
1878 debug: instance.session.debug,
1882 gravity: $.fn.tipsy.autoBounds(50, 'nw'),
1887 $(trigger).tipsy(options);
1890 * Builds a new context usable for operations related to fields by merging
1891 * the fields'context with the action's context.
1893 build_context: function() {
1894 // only use the model's context if there is not context on the node
1895 var v_context = this.node.attrs.context;
1897 v_context = (this.field || {}).context || {};
1900 if (v_context.__ref || true) { //TODO: remove true
1901 var fields_values = this.field_manager.build_eval_context();
1902 v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
1906 build_domain: function() {
1907 var f_domain = this.field.domain || [];
1908 var n_domain = this.node.attrs.domain || null;
1909 // if there is a domain on the node, overrides the model's domain
1910 var final_domain = n_domain !== null ? n_domain : f_domain;
1911 if (!(final_domain instanceof Array) || true) { //TODO: remove true
1912 var fields_values = this.field_manager.build_eval_context();
1913 final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
1915 return final_domain;
1919 instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
1920 template: 'WidgetButton',
1921 init: function(field_manager, node) {
1922 node.attrs.type = node.attrs['data-button-type'];
1923 this._super(field_manager, node);
1924 this.force_disabled = false;
1925 this.string = (this.node.attrs.string || '').replace(/_/g, '');
1926 if (JSON.parse(this.node.attrs.default_focus || "0")) {
1927 // TODO fme: provide enter key binding to widgets
1928 this.view.default_focus_button = this;
1930 if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
1931 this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
1935 this._super.apply(this, arguments);
1936 this.view.on('view_content_has_changed', this, this.check_disable);
1937 this.check_disable();
1938 this.$el.click(this.on_click);
1939 if (this.node.attrs.help || instance.session.debug) {
1940 this.do_attach_tooltip();
1942 this.setupFocus(this.$el);
1944 on_click: function() {
1946 this.force_disabled = true;
1947 this.check_disable();
1948 this.execute_action().always(function() {
1949 self.force_disabled = false;
1950 self.check_disable();
1953 execute_action: function() {
1955 var exec_action = function() {
1956 if (self.node.attrs.confirm) {
1957 var def = $.Deferred();
1958 var dialog = instance.web.dialog($('<div/>').text(self.node.attrs.confirm), {
1959 title: _t('Confirm'),
1962 {text: _t("Cancel"), click: function() {
1963 $(this).dialog("close");
1966 {text: _t("Ok"), click: function() {
1968 self.on_confirmed().always(function() {
1969 $(self2).dialog("close");
1974 beforeClose: function() {
1978 return def.promise();
1980 return self.on_confirmed();
1983 if (!this.node.attrs.special) {
1984 return this.view.recursive_save().then(exec_action);
1986 return exec_action();
1989 on_confirmed: function() {
1992 var context = this.build_context();
1994 return this.view.do_execute_action(
1995 _.extend({}, this.node.attrs, {context: context}),
1996 this.view.dataset, this.view.datarecord.id, function (reason) {
1997 if (!_.isObject(reason)) {
1998 self.view.recursive_reload();
2002 check_disable: function() {
2003 var disabled = (this.force_disabled || !this.view.is_interactible_record());
2004 this.$el.prop('disabled', disabled);
2005 this.$el.css('color', disabled ? 'grey' : '');
2010 * Interface to be implemented by fields.
2013 * - changed_value: triggered when the value of the field has changed. This can be due
2014 * to a user interaction or a call to set_value().
2017 instance.web.form.FieldInterface = {
2019 * Constructor takes 2 arguments:
2020 * - field_manager: Implements FieldManagerMixin
2021 * - node: the "<field>" node in json form
2023 init: function(field_manager, node) {},
2025 * Called by the form view to indicate the value of the field.
2027 * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
2028 * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
2029 * before the widget is inserted into the DOM.
2031 * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
2032 * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
2033 * also be able to handle any other format commonly used in the _defaults key on the models in the addons
2034 * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
2035 * no information is ever given to know which format is used.
2037 set_value: function(value_) {},
2039 * Get the current value of the widget.
2041 * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
2042 * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
2043 * For example if the field is marked as "required", a call to get_value() can return false.
2045 * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
2046 * return a default value according to the type of field.
2048 * This method is always assumed to perform synchronously, it can not return a promise.
2050 * If there was no user interaction to modify the value of the field, it is always assumed that
2051 * get_value() return the same semantic value than the one passed in the last call to set_value(),
2052 * although the syntax can be different. This can be the case for type of fields that have a different
2053 * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
2055 get_value: function() {},
2057 * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
2060 set_input_id: function(id) {},
2062 * Returns true if is_syntax_valid() returns true and the value is semantically
2063 * valid too according to the semantic restrictions applied to the field.
2065 is_valid: function() {},
2067 * Returns true if the field holds a value which is syntactically correct, ignoring
2068 * the potential semantic restrictions applied to the field.
2070 is_syntax_valid: function() {},
2072 * Must set the focus on the field. Return false if field is not focusable.
2074 focus: function() {},
2076 * Called when the translate button is clicked.
2078 on_translate: function() {},
2080 This method is called by the form view before reading on_change values and before saving. It tells
2081 the field to save its value before reading it using get_value(). Must return a promise.
2083 commit_value: function() {},
2087 * Abstract class for classes implementing FieldInterface.
2090 * - value: useful property to hold the value of the field. By default, set_value() and get_value()
2091 * set and retrieve the value property. Changing the value property also triggers automatically
2092 * a 'changed_value' event that inform the view to trigger on_changes.
2095 instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
2097 * @constructs instance.web.form.AbstractField
2098 * @extends instance.web.form.FormWidget
2100 * @param field_manager
2103 init: function(field_manager, node) {
2105 this._super(field_manager, node);
2106 this.name = this.node.attrs.name;
2107 this.field = this.field_manager.get_field_desc(this.name);
2108 this.widget = this.node.attrs.widget;
2109 this.string = this.node.attrs.string || this.field.string || this.name;
2110 this.options = instance.web.py_eval(this.node.attrs.options || '{}');
2111 this.set({'value': false});
2113 this.on("change:value", this, function() {
2114 this.trigger('changed_value');
2115 this._check_css_flags();
2118 renderElement: function() {
2121 if (this.field.translate && this.view) {
2122 this.$el.addClass('oe_form_field_translatable');
2123 this.$el.find('.oe_field_translate').click(this.on_translate);
2125 this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
2126 if (instance.session.debug) {
2127 this.do_attach_tooltip(this, this.$label[0] || this.$el);
2128 this.$label.off('dblclick').on('dblclick', function() {
2129 console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
2131 console.log("window.w =", window.w);
2134 if (!this.disable_utility_classes) {
2135 this.off("change:required", this, this._set_required);
2136 this.on("change:required", this, this._set_required);
2137 this._set_required();
2139 this._check_visibility();
2140 this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
2141 this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
2142 this._check_css_flags();
2145 var tmp = this._super();
2146 this.on("change:value", this, function() {
2147 if (! this.no_rerender)
2148 this.render_value();
2150 this.render_value();
2153 * Private. Do not use.
2155 _set_required: function() {
2156 this.$el.toggleClass('oe_form_required', this.get("required"));
2158 set_value: function(value_) {
2159 this.set({'value': value_});
2161 get_value: function() {
2162 return this.get('value');
2165 Utility method that all implementations should use to change the
2166 value without triggering a re-rendering.
2168 internal_set_value: function(value_) {
2169 var tmp = this.no_rerender;
2170 this.no_rerender = true;
2171 this.set({'value': value_});
2172 this.no_rerender = tmp;
2175 This method is called each time the value is modified.
2177 render_value: function() {},
2178 is_valid: function() {
2179 return this.is_syntax_valid() && !(this.get('required') && this.is_false());
2181 is_syntax_valid: function() {
2185 * Method useful to implement to ease validity testing. Must return true if the current
2186 * value is similar to false in OpenERP.
2188 is_false: function() {
2189 return this.get('value') === false;
2191 _check_css_flags: function() {
2192 if (this.field.translate) {
2193 this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
2195 if (!this.disable_utility_classes) {
2196 if (this.field_manager.get('display_invalid_fields')) {
2197 this.$el.toggleClass('oe_form_invalid', !this.is_valid());
2204 set_input_id: function(id) {
2205 this.id_for_label = id;
2207 on_translate: function() {
2209 var trans = new instance.web.DataSet(this, 'ir.translation');
2210 return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
2215 set_dimensions: function (height, width) {
2221 commit_value: function() {
2227 * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
2230 instance.web.form.ReinitializeWidgetMixin = {
2232 * Default implementation of, you should not override it, use initialize_field() instead.
2235 this.initialize_field();
2238 initialize_field: function() {
2239 this.on("change:effective_readonly", this, this.reinitialize);
2240 this.initialize_content();
2242 reinitialize: function() {
2243 this.destroy_content();
2244 this.renderElement();
2245 this.initialize_content();
2248 * Called to destroy anything that could have been created previously, called before a
2249 * re-initialization.
2251 destroy_content: function() {},
2253 * Called to initialize the content.
2255 initialize_content: function() {},
2259 * A mixin to apply on any field that has to completely re-render when its readonly state
2262 instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
2263 reinitialize: function() {
2264 instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
2265 var res = this.render_value();
2266 if (this.view && this.view.render_value_defs){
2267 this.view.render_value_defs.push(res);
2273 Some hack to make placeholders work in ie9.
2275 if (!('placeholder' in document.createElement('input'))) {
2276 document.addEventListener("DOMNodeInserted",function(event){
2277 var nodename = event.target.nodeName.toLowerCase();
2278 if ( nodename === "input" || nodename == "textarea" ) {
2279 $(event.target).placeholder();
2284 instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2285 template: 'FieldChar',
2286 widget_class: 'oe_form_field_char',
2288 'change input': 'store_dom_value',
2290 init: function (field_manager, node) {
2291 this._super(field_manager, node);
2292 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2294 initialize_content: function() {
2295 this.setupFocus(this.$('input'));
2297 store_dom_value: function () {
2298 if (!this.get('effective_readonly')
2299 && this.$('input').length
2300 && this.is_syntax_valid()) {
2301 this.internal_set_value(
2303 this.$('input').val()));
2306 commit_value: function () {
2307 this.store_dom_value();
2308 return this._super();
2310 render_value: function() {
2311 var show_value = this.format_value(this.get('value'), '');
2312 if (!this.get("effective_readonly")) {
2313 this.$el.find('input').val(show_value);
2315 if (this.password) {
2316 show_value = new Array(show_value.length + 1).join('*');
2318 this.$(".oe_form_char_content").text(show_value);
2321 is_syntax_valid: function() {
2322 if (!this.get("effective_readonly") && this.$("input").size() > 0) {
2324 this.parse_value(this.$('input').val(), '');
2332 parse_value: function(val, def) {
2333 return instance.web.parse_value(val, this, def);
2335 format_value: function(val, def) {
2336 return instance.web.format_value(val, this, def);
2338 is_false: function() {
2339 return this.get('value') === '' || this._super();
2342 var input = this.$('input:first')[0];
2343 return input ? input.focus() : false;
2345 set_dimensions: function (height, width) {
2346 this._super(height, width);
2347 this.$('input').css({
2354 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2355 process_modifiers: function () {
2357 this.set({ readonly: true });
2361 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2362 template: 'FieldEmail',
2363 initialize_content: function() {
2365 var $button = this.$el.find('button');
2366 $button.click(this.on_button_clicked);
2367 this.setupFocus($button);
2369 render_value: function() {
2370 if (!this.get("effective_readonly")) {
2374 .attr('href', 'mailto:' + this.get('value'))
2375 .text(this.get('value') || '');
2378 on_button_clicked: function() {
2379 if (!this.get('value') || !this.is_syntax_valid()) {
2380 this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2382 location.href = 'mailto:' + this.get('value');
2387 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2388 template: 'FieldUrl',
2389 initialize_content: function() {
2391 var $button = this.$el.find('button');
2392 $button.click(this.on_button_clicked);
2393 this.setupFocus($button);
2395 render_value: function() {
2396 if (!this.get("effective_readonly")) {
2399 var tmp = this.get('value');
2400 var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2402 tmp = "http://" + this.get('value');
2404 var text = this.get('value') ? this.node.attrs.text || tmp : '';
2405 this.$el.find('a').attr('href', tmp).text(text);
2408 on_button_clicked: function() {
2409 if (!this.get('value')) {
2410 this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2412 var url = $.trim(this.get('value'));
2413 if(/^www\./i.test(url))
2414 url = 'http://'+url;
2420 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2421 is_field_number: true,
2422 widget_class: 'oe_form_field_float',
2423 init: function (field_manager, node) {
2424 this._super(field_manager, node);
2425 this.internal_set_value(0);
2426 if (this.node.attrs.digits) {
2427 this.digits = this.node.attrs.digits;
2429 this.digits = this.field.digits;
2432 set_value: function(value_) {
2433 if (value_ === false || value_ === undefined) {
2434 // As in GTK client, floats default to 0
2437 this._super.apply(this, [value_]);
2439 focus: function () {
2440 var $input = this.$('input:first');
2441 return $input.length ? $input.select() : false;
2445 instance.web.DateTimeWidget = instance.web.Widget.extend({
2446 template: "web.datepicker",
2447 jqueryui_object: 'datetimepicker',
2448 type_of_date: "datetime",
2450 'change .oe_datepicker_master': 'change_datetime',
2451 'keypress .oe_datepicker_master': 'change_datetime',
2453 init: function(parent) {
2454 this._super(parent);
2455 this.name = parent.name;
2459 this.$input = this.$el.find('input.oe_datepicker_master');
2460 this.$input_picker = this.$el.find('input.oe_datepicker_container');
2462 $.datepicker.setDefaults({
2463 clearText: _t('Clear'),
2464 clearStatus: _t('Erase the current date'),
2465 closeText: _t('Done'),
2466 closeStatus: _t('Close without change'),
2467 prevText: _t('<Prev'),
2468 prevStatus: _t('Show the previous month'),
2469 nextText: _t('Next>'),
2470 nextStatus: _t('Show the next month'),
2471 currentText: _t('Today'),
2472 currentStatus: _t('Show the current month'),
2473 monthNames: Date.CultureInfo.monthNames,
2474 monthNamesShort: Date.CultureInfo.abbreviatedMonthNames,
2475 monthStatus: _t('Show a different month'),
2476 yearStatus: _t('Show a different year'),
2477 weekHeader: _t('Wk'),
2478 weekStatus: _t('Week of the year'),
2479 dayNames: Date.CultureInfo.dayNames,
2480 dayNamesShort: Date.CultureInfo.abbreviatedDayNames,
2481 dayNamesMin: Date.CultureInfo.shortestDayNames,
2482 dayStatus: _t('Set DD as first week day'),
2483 dateStatus: _t('Select D, M d'),
2484 firstDay: Date.CultureInfo.firstDayOfWeek,
2485 initStatus: _t('Select a date'),
2488 $.timepicker.setDefaults({
2489 timeOnlyTitle: _t('Choose Time'),
2490 timeText: _t('Time'),
2491 hourText: _t('Hour'),
2492 minuteText: _t('Minute'),
2493 secondText: _t('Second'),
2494 currentText: _t('Now'),
2495 closeText: _t('Done')
2499 onClose: this.on_picker_select,
2500 onSelect: this.on_picker_select,
2504 showButtonPanel: true,
2505 firstDay: Date.CultureInfo.firstDayOfWeek
2507 // Some clicks in the datepicker dialog are not stopped by the
2508 // datepicker and "bubble through", unexpectedly triggering the bus's
2509 // click event. Prevent that.
2510 this.picker('widget').click(function (e) { e.stopPropagation(); });
2512 this.$el.find('img.oe_datepicker_trigger').click(function() {
2513 if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
2514 self.$input.focus();
2517 self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date());
2518 self.$input_picker.show();
2519 self.picker('show');
2520 self.$input_picker.hide();
2522 this.set_readonly(false);
2523 this.set({'value': false});
2525 picker: function() {
2526 return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
2528 on_picker_select: function(text, instance_) {
2529 var date = this.picker('getDate');
2531 .val(date ? this.format_client(date) : '')
2535 set_value: function(value_) {
2536 this.set({'value': value_});
2537 this.$input.val(value_ ? this.format_client(value_) : '');
2539 get_value: function() {
2540 return this.get('value');
2542 set_value_from_ui_: function() {
2543 var value_ = this.$input.val() || false;
2544 this.set({'value': this.parse_client(value_)});
2546 set_readonly: function(readonly) {
2547 this.readonly = readonly;
2548 this.$input.prop('readonly', this.readonly);
2549 this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
2551 is_valid_: function() {
2552 var value_ = this.$input.val();
2553 if (value_ === "") {
2557 this.parse_client(value_);
2564 parse_client: function(v) {
2565 return instance.web.parse_value(v, {"widget": this.type_of_date});
2567 format_client: function(v) {
2568 return instance.web.format_value(v, {"widget": this.type_of_date});
2570 change_datetime: function(e) {
2571 if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
2572 this.set_value_from_ui_();
2573 this.trigger("datetime_changed");
2576 commit_value: function () {
2577 this.change_datetime();
2581 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2582 jqueryui_object: 'datepicker',
2583 type_of_date: "date"
2586 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2587 template: "FieldDatetime",
2588 build_widget: function() {
2589 return new instance.web.DateTimeWidget(this);
2591 destroy_content: function() {
2592 if (this.datewidget) {
2593 this.datewidget.destroy();
2594 this.datewidget = undefined;
2597 initialize_content: function() {
2598 if (!this.get("effective_readonly")) {
2599 this.datewidget = this.build_widget();
2600 this.datewidget.on('datetime_changed', this, _.bind(function() {
2601 this.internal_set_value(this.datewidget.get_value());
2603 this.datewidget.appendTo(this.$el);
2604 this.setupFocus(this.datewidget.$input);
2607 render_value: function() {
2608 if (!this.get("effective_readonly")) {
2609 this.datewidget.set_value(this.get('value'));
2611 this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2614 is_syntax_valid: function() {
2615 if (!this.get("effective_readonly") && this.datewidget) {
2616 return this.datewidget.is_valid_();
2620 is_false: function() {
2621 return this.get('value') === '' || this._super();
2624 var input = this.datewidget && this.datewidget.$input[0];
2625 return input ? input.focus() : false;
2627 set_dimensions: function (height, width) {
2628 this._super(height, width);
2629 if (!this.get("effective_readonly")) {
2630 this.datewidget.$input.css('height', height);
2635 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2636 template: "FieldDate",
2637 build_widget: function() {
2638 return new instance.web.DateWidget(this);
2642 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2643 template: 'FieldText',
2645 'keyup': function (e) {
2646 if (e.which === $.ui.keyCode.ENTER) {
2647 e.stopPropagation();
2650 'keypress': function (e) {
2651 if (e.which === $.ui.keyCode.ENTER) {
2652 e.stopPropagation();
2655 'change textarea': 'store_dom_value',
2657 initialize_content: function() {
2659 if (! this.get("effective_readonly")) {
2660 this.$textarea = this.$el.find('textarea');
2661 this.auto_sized = false;
2662 this.default_height = this.$textarea.css('height');
2663 if (this.get("effective_readonly")) {
2664 this.$textarea.attr('disabled', 'disabled');
2666 this.setupFocus(this.$textarea);
2668 this.$textarea = undefined;
2671 commit_value: function () {
2672 if (! this.get("effective_readonly") && this.$textarea) {
2673 this.store_dom_value();
2675 return this._super();
2677 store_dom_value: function () {
2678 this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2680 render_value: function() {
2681 if (! this.get("effective_readonly")) {
2682 var show_value = instance.web.format_value(this.get('value'), this, '');
2683 if (show_value === '') {
2684 this.$textarea.css('height', parseInt(this.default_height, 10)+"px");
2686 this.$textarea.val(show_value);
2687 if (! this.auto_sized) {
2688 this.auto_sized = true;
2689 this.$textarea.autosize();
2691 this.$textarea.trigger("autosize");
2694 var txt = this.get("value") || '';
2695 this.$(".oe_form_text_content").text(txt);
2698 is_syntax_valid: function() {
2699 if (!this.get("effective_readonly") && this.$textarea) {
2701 instance.web.parse_value(this.$textarea.val(), this, '');
2709 is_false: function() {
2710 return this.get('value') === '' || this._super();
2712 focus: function($el) {
2713 var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2714 return input ? input.focus() : false;
2716 set_dimensions: function (height, width) {
2717 this._super(height, width);
2718 if (!this.get("effective_readonly") && this.$textarea) {
2719 this.$textarea.css({
2728 * FieldTextHtml Widget
2729 * Intended for FieldText widgets meant to display HTML content. This
2730 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2731 * To find more information about CLEditor configutation: go to
2732 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2734 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2735 template: 'FieldTextHtml',
2737 this._super.apply(this, arguments);
2739 initialize_content: function() {
2741 if (! this.get("effective_readonly")) {
2742 self._updating_editor = false;
2743 this.$textarea = this.$el.find('textarea');
2744 var width = ((this.node.attrs || {}).editor_width || 'calc(100% - 4px)');
2745 var height = ((this.node.attrs || {}).editor_height || 250);
2746 this.$textarea.cleditor({
2747 width: width, // width not including margins, borders or padding
2748 height: height, // height not including margins, borders or padding
2749 controls: // controls to add to the toolbar
2750 "bold italic underline strikethrough " +
2751 "| removeformat | bullets numbering | outdent " +
2752 "indent | link unlink | source",
2753 bodyStyle: // style to assign to document body contained within the editor
2754 "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2756 this.$cleditor = this.$textarea.cleditor()[0];
2757 this.$cleditor.change(function() {
2758 if (! self._updating_editor) {
2759 self.$cleditor.updateTextArea();
2760 self.internal_set_value(self.$textarea.val());
2763 if (this.field.translate) {
2764 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"/>')
2765 .click(this.on_translate);
2766 this.$cleditor.$toolbar.append($img);
2770 render_value: function() {
2771 if (! this.get("effective_readonly")) {
2772 this.$textarea.val(this.get('value') || '');
2773 this._updating_editor = true;
2774 this.$cleditor.updateFrame();
2775 this._updating_editor = false;
2777 this.$el.html(this.get('value'));
2782 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2783 template: 'FieldBoolean',
2786 this.$checkbox = $("input", this.$el);
2787 this.setupFocus(this.$checkbox);
2788 this.$el.click(_.bind(function() {
2789 this.internal_set_value(this.$checkbox.is(':checked'));
2791 var check_readonly = function() {
2792 self.$checkbox.prop('disabled', self.get("effective_readonly"));
2793 self.click_disabled_boolean();
2795 this.on("change:effective_readonly", this, check_readonly);
2796 check_readonly.call(this);
2797 this._super.apply(this, arguments);
2799 render_value: function() {
2800 this.$checkbox[0].checked = this.get('value');
2803 var input = this.$checkbox && this.$checkbox[0];
2804 return input ? input.focus() : false;
2806 click_disabled_boolean: function(){
2807 var $disabled = this.$el.find('input[type=checkbox]:disabled');
2808 $disabled.each(function (){
2809 $(this).next('div').remove();
2810 $(this).closest("span").append($('<div class="boolean"></div>'));
2816 The progressbar field expect a float from 0 to 100.
2818 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2819 template: 'FieldProgressBar',
2820 render_value: function() {
2821 this.$el.progressbar({
2822 value: this.get('value') || 0,
2823 disabled: this.get("effective_readonly")
2825 var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
2826 this.$('span').html(formatted_value + '%');
2831 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2832 template: 'FieldSelection',
2834 'change select': 'store_dom_value',
2836 init: function(field_manager, node) {
2838 this._super(field_manager, node);
2839 this.set("value", false);
2840 this.set("values", []);
2841 this.records_orderer = new instance.web.DropMisordered();
2842 this.field_manager.on("view_content_has_changed", this, function() {
2843 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
2844 if (! _.isEqual(domain, this.get("domain"))) {
2845 this.set("domain", domain);
2849 initialize_field: function() {
2850 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
2851 this.on("change:domain", this, this.query_values);
2852 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
2853 this.on("change:values", this, this.render_value);
2855 query_values: function() {
2858 if (this.field.type === "many2one") {
2859 var model = new openerp.Model(openerp.session, this.field.relation);
2860 def = model.call("name_search", ['', this.get("domain")], {"context": this.build_context()});
2862 var values = _.reject(this.field.selection, function (v) { return v[0] === false && v[1] === ''; });
2863 def = $.when(values);
2865 this.records_orderer.add(def).then(function(values) {
2866 if (! _.isEqual(values, self.get("values"))) {
2867 self.set("values", values);
2871 initialize_content: function() {
2872 // Flag indicating whether we're in an event chain containing a change
2873 // event on the select, in order to know what to do on keyup[RETURN]:
2874 // * If the user presses [RETURN] as part of changing the value of a
2875 // selection, we should just let the value change and not let the
2876 // event broadcast further (e.g. to validating the current state of
2877 // the form in editable list view, which would lead to saving the
2878 // current row or switching to the next one)
2879 // * If the user presses [RETURN] with a select closed (side-effect:
2880 // also if the user opened the select and pressed [RETURN] without
2881 // changing the selected value), takes the action as validating the
2883 var ischanging = false;
2884 var $select = this.$el.find('select')
2885 .change(function () { ischanging = true; })
2886 .click(function () { ischanging = false; })
2887 .keyup(function (e) {
2888 if (e.which !== 13 || !ischanging) { return; }
2889 e.stopPropagation();
2892 this.setupFocus($select);
2894 commit_value: function () {
2895 this.store_dom_value();
2896 return this._super();
2898 store_dom_value: function () {
2899 if (!this.get('effective_readonly') && this.$('select').length) {
2900 var val = JSON.parse(this.$('select').val());
2901 this.internal_set_value(val);
2904 set_value: function(value_) {
2905 value_ = value_ === null ? false : value_;
2906 value_ = value_ instanceof Array ? value_[0] : value_;
2907 this._super(value_);
2909 render_value: function() {
2910 var values = this.get("values");
2911 values = [[false, this.node.attrs.placeholder || '']].concat(values);
2912 var found = _.find(values, function(el) { return el[0] === this.get("value"); }, this);
2914 found = [this.get("value"), _t('Unknown')];
2915 values = [found].concat(values);
2917 if (! this.get("effective_readonly")) {
2918 this.$().html(QWeb.render("FieldSelectionSelect", {widget: this, values: values}));
2919 this.$("select").val(JSON.stringify(found[0]));
2921 this.$el.text(found[1]);
2925 var input = this.$('select:first')[0];
2926 return input ? input.focus() : false;
2928 set_dimensions: function (height, width) {
2929 this._super(height, width);
2930 this.$('select').css({
2937 instance.web.form.FieldRadio = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2938 template: 'FieldRadio',
2940 'click input': 'click_change_value'
2942 init: function(field_manager, node) {
2943 /* Radio button widget: Attributes options:
2944 * - "horizontal" to display in column
2945 * - "no_radiolabel" don't display text values
2947 this._super(field_manager, node);
2948 this.selection = _.clone(this.field.selection) || [];
2949 this.domain = false;
2951 initialize_content: function () {
2952 this.uniqueId = _.uniqueId("radio");
2953 this.on("change:effective_readonly", this, this.render_value);
2954 this.field_manager.on("view_content_has_changed", this, this.get_selection);
2955 this.get_selection();
2957 click_change_value: function (event) {
2958 var val = $(event.target).val();
2959 val = this.field.type == "selection" ? val : +val;
2960 if (val == this.get_value()) {
2961 this.set_value(false);
2963 this.set_value(val);
2966 /** Get the selection and render it
2967 * selection: [[identifier, value_to_display], ...]
2968 * For selection fields: this is directly given by this.field.selection
2969 * For many2one fields: perform a search on the relation of the many2one field
2971 get_selection: function() {
2974 var def = $.Deferred();
2975 if (self.field.type == "many2one") {
2976 var domain = instance.web.pyeval.eval('domain', this.build_domain()) || [];
2977 if (! _.isEqual(self.domain, domain)) {
2978 self.domain = domain;
2979 var ds = new instance.web.DataSetStatic(self, self.field.relation, self.build_context());
2980 ds.call('search', [self.domain])
2981 .then(function (records) {
2982 ds.name_get(records).then(function (records) {
2983 selection = records;
2988 selection = self.selection;
2992 else if (self.field.type == "selection") {
2993 selection = self.field.selection || [];
2996 return def.then(function () {
2997 if (! _.isEqual(selection, self.selection)) {
2998 self.selection = _.clone(selection);
2999 self.renderElement();
3000 self.render_value();
3004 set_value: function (value_) {
3006 if (this.field.type == "selection") {
3007 value_ = _.find(this.field.selection, function (sel) { return sel[0] == value_;});
3009 else if (!this.selection.length) {
3010 this.selection = [value_];
3013 this._super(value_);
3015 get_value: function () {
3016 var value = this.get('value');
3017 return value instanceof Array ? value[0] : value;
3019 render_value: function () {
3021 this.$el.toggleClass("oe_readonly", this.get('effective_readonly'));
3022 this.$("input:checked").prop("checked", false);
3023 if (this.get_value()) {
3024 this.$("input").filter(function () {return this.value == self.get_value();}).prop("checked", true);
3025 this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : "");
3030 // jquery autocomplete tweak to allow html and classnames
3032 var proto = $.ui.autocomplete.prototype,
3033 initSource = proto._initSource;
3035 function filter( array, term ) {
3036 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
3037 return $.grep( array, function(value_) {
3038 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
3043 _initSource: function() {
3044 if ( this.options.html && $.isArray(this.options.source) ) {
3045 this.source = function( request, response ) {
3046 response( filter( this.options.source, request.term ) );
3049 initSource.call( this );
3053 _renderItem: function( ul, item) {
3054 return $( "<li></li>" )
3055 .data( "item.autocomplete", item )
3056 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
3058 .addClass(item.classname);
3064 A mixin containing some useful methods to handle completion inputs.
3066 The widget containing this option can have these arguments in its widget options:
3067 - no_quick_create: if true, it will disable the quick create
3069 instance.web.form.CompletionFieldMixin = {
3072 this.orderer = new instance.web.DropMisordered();
3075 * Call this method to search using a string.
3077 get_search_result: function(search_val) {
3080 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
3081 var blacklist = this.get_search_blacklist();
3082 this.last_query = search_val;
3084 return this.orderer.add(dataset.name_search(
3085 search_val, new instance.web.CompoundDomain(self.build_domain(), [["id", "not in", blacklist]]),
3086 'ilike', this.limit + 1, self.build_context())).then(function(data) {
3087 self.last_search = data;
3088 // possible selections for the m2o
3089 var values = _.map(data, function(x) {
3090 x[1] = x[1].split("\n")[0];
3092 label: _.str.escapeHTML(x[1]),
3099 // search more... if more results that max
3100 if (values.length > self.limit) {
3101 values = values.slice(0, self.limit);
3103 label: _t("Search More..."),
3104 action: function() {
3105 dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
3106 self._search_create_popup("search", data);
3109 classname: 'oe_m2o_dropdown_option'
3113 var raw_result = _(data.result).map(function(x) {return x[1];});
3114 if (search_val.length > 0 && !_.include(raw_result, search_val) &&
3115 ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3117 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
3118 $('<span />').text(search_val).html()),
3119 action: function() {
3120 self._quick_create(search_val);
3122 classname: 'oe_m2o_dropdown_option'
3126 if (!(self.options && self.options.no_create)){
3128 label: _t("Create and Edit..."),
3129 action: function() {
3130 self._search_create_popup("form", undefined, self._create_context(search_val));
3132 classname: 'oe_m2o_dropdown_option'
3139 get_search_blacklist: function() {
3142 _quick_create: function(name) {
3144 var slow_create = function () {
3145 self._search_create_popup("form", undefined, self._create_context(name));
3147 if (self.options.quick_create === undefined || self.options.quick_create) {
3148 new instance.web.DataSet(this, this.field.relation, self.build_context())
3149 .name_create(name).done(function(data) {
3150 if (!self.get('effective_readonly'))
3151 self.add_id(data[0]);
3152 }).fail(function(error, event) {
3153 event.preventDefault();
3159 // all search/create popup handling
3160 _search_create_popup: function(view, ids, context) {
3162 var pop = new instance.web.form.SelectCreatePopup(this);
3164 self.field.relation,
3166 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3167 initial_ids: ids ? _.map(ids, function(x) {return x[0];}) : undefined,
3169 disable_multiple_selection: true
3171 self.build_domain(),
3172 new instance.web.CompoundContext(self.build_context(), context || {})
3174 pop.on("elements_selected", self, function(element_ids) {
3175 self.add_id(element_ids[0]);
3182 add_id: function(id) {},
3183 _create_context: function(name) {
3185 var field = (this.options || {}).create_name_field;
3186 if (field === undefined)
3188 if (field !== false && name && (this.options || {}).quick_create !== false)
3189 tmp["default_" + field] = name;
3194 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3195 template: "M2ODialog",
3196 init: function(parent) {
3197 this._super(parent, {
3198 title: _.str.sprintf(_t("Add %s"), parent.string),
3204 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3205 this.$("input").val(this.getParent().last_query);
3206 this.$buttons.find(".oe_form_m2o_qc_button").click(function(){
3207 self.getParent()._quick_create(self.$("input").val());
3210 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3211 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3214 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3220 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3221 template: "FieldMany2One",
3223 'keydown input': function (e) {
3225 case $.ui.keyCode.UP:
3226 case $.ui.keyCode.DOWN:
3227 e.stopPropagation();
3231 init: function(field_manager, node) {
3232 this._super(field_manager, node);
3233 instance.web.form.CompletionFieldMixin.init.call(this);
3234 this.set({'value': false});
3235 this.display_value = {};
3236 this.display_value_backup = {};
3237 this.last_search = [];
3238 this.floating = false;
3239 this.current_display = null;
3240 this.is_started = false;
3241 this.ignore_focusout = false;
3243 reinit_value: function(val) {
3244 this.internal_set_value(val);
3245 this.floating = false;
3246 if (this.is_started)
3247 this.render_value();
3249 initialize_field: function() {
3250 this.is_started = true;
3251 instance.web.bus.on('click', this, function() {
3252 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3253 this.$input.autocomplete("close");
3256 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3258 initialize_content: function() {
3259 if (!this.get("effective_readonly"))
3260 this.render_editable();
3262 destroy_content: function () {
3263 if (this.$drop_down) {
3264 this.$drop_down.off('click');
3265 delete this.$drop_down;
3268 this.$input.closest(".ui-dialog .ui-dialog-content").off('scroll');
3269 this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3270 'focus focusout change keydown');
3273 if (this.$follow_button) {
3274 this.$follow_button.off('blur focus click');
3275 delete this.$follow_button;
3278 destroy: function () {
3279 this.destroy_content();
3280 return this._super();
3282 init_error_displayer: function() {
3285 hide_error_displayer: function() {
3288 show_error_displayer: function() {
3289 new instance.web.form.M2ODialog(this).open();
3291 render_editable: function() {
3293 this.$input = this.$el.find("input");
3295 this.init_error_displayer();
3297 self.$input.on('focus', function() {
3298 self.hide_error_displayer();
3301 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3302 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3304 this.$follow_button.click(function(ev) {
3305 ev.preventDefault();
3306 if (!self.get('value')) {
3310 var pop = new instance.web.form.FormOpenPopup(self);
3312 self.field.relation,
3314 self.build_context(),
3316 title: _t("Open: ") + self.string
3319 pop.on('write_completed', self, function(){
3320 self.display_value = {};
3321 self.display_value_backup = {};
3322 self.render_value();
3324 self.trigger('changed_value');
3328 // some behavior for input
3329 var input_changed = function() {
3330 if (self.current_display !== self.$input.val()) {
3331 self.current_display = self.$input.val();
3332 if (self.$input.val() === "") {
3333 self.internal_set_value(false);
3334 self.floating = false;
3336 self.floating = true;
3340 this.$input.keydown(input_changed);
3341 this.$input.change(input_changed);
3342 this.$drop_down.click(function() {
3343 self.$input.focus();
3344 if (self.$input.autocomplete("widget").is(":visible")) {
3345 self.$input.autocomplete("close");
3347 if (self.get("value") && ! self.floating) {
3348 self.$input.autocomplete("search", "");
3350 self.$input.autocomplete("search");
3355 // Autocomplete close on dialog content scroll
3356 var close_autocomplete = _.debounce(function() {
3357 if (self.$input.autocomplete("widget").is(":visible")) {
3358 self.$input.autocomplete("close");
3361 this.$input.closest(".ui-dialog .ui-dialog-content").on('scroll', this, close_autocomplete);
3363 self.ed_def = $.Deferred();
3364 self.uned_def = $.Deferred();
3366 var ed_duration = 15000;
3367 var anyoneLoosesFocus = function (e) {
3368 if (self.ignore_focusout) { return; }
3370 if (self.floating) {
3371 if (self.last_search.length > 0) {
3372 if (self.last_search[0][0] != self.get("value")) {
3373 self.display_value = {};
3374 self.display_value_backup = {};
3375 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3376 self.reinit_value(self.last_search[0][0]);
3379 self.render_value();
3383 self.reinit_value(false);
3385 self.floating = false;
3387 if (used && self.get("value") === false && ! self.no_ed && ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3388 self.ed_def.reject();
3389 self.uned_def.reject();
3390 self.ed_def = $.Deferred();
3391 self.ed_def.done(function() {
3392 self.show_error_displayer();
3393 ignore_blur = false;
3394 self.trigger('focused');
3397 setTimeout(function() {
3398 self.ed_def.resolve();
3399 self.uned_def.reject();
3400 self.uned_def = $.Deferred();
3401 self.uned_def.done(function() {
3402 self.hide_error_displayer();
3404 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3408 self.ed_def.reject();
3411 var ignore_blur = false;
3413 focusout: anyoneLoosesFocus,
3414 focus: function () { self.trigger('focused'); },
3415 autocompleteopen: function () { ignore_blur = true; },
3416 autocompleteclose: function () { ignore_blur = false; },
3418 // autocomplete open
3419 if (ignore_blur) { return; }
3420 if (_(self.getChildren()).any(function (child) {
3421 return child instanceof instance.web.form.AbstractFormPopup;
3423 self.trigger('blurred');
3427 var isSelecting = false;
3429 this.$input.autocomplete({
3430 source: function(req, resp) {
3431 self.get_search_result(req.term).done(function(result) {
3435 select: function(event, ui) {
3439 self.display_value = {};
3440 self.display_value_backup = {};
3441 self.display_value["" + item.id] = item.name;
3442 self.reinit_value(item.id);
3443 } else if (item.action) {
3445 // Cancel widget blurring, to avoid form blur event
3446 self.trigger('focused');
3450 focus: function(e, ui) {
3454 // disabled to solve a bug, but may cause others
3455 //close: anyoneLoosesFocus,
3459 this.$input.autocomplete("widget").openerpClass();
3460 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3461 this.$input.keyup(function(e) {
3462 if (e.which === 13) { // ENTER
3464 e.stopPropagation();
3466 isSelecting = false;
3468 this.setupFocus(this.$follow_button);
3470 render_value: function(no_recurse) {
3472 if (! this.get("value")) {
3473 this.display_string("");
3476 var display = this.display_value["" + this.get("value")];
3478 this.display_string(display);
3482 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3483 this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3485 self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
3488 self.display_value["" + self.get("value")] = data[0][1];
3489 self.render_value(true);
3490 }).fail( function (data, event) {
3491 // avoid displaying crash errors as many2One should be name_get compliant
3492 event.preventDefault();
3493 self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")];
3494 self.render_value(true);
3498 display_string: function(str) {
3500 if (!this.get("effective_readonly")) {
3501 this.$input.val(str.split("\n")[0]);
3502 this.current_display = this.$input.val();
3503 if (this.is_false()) {
3504 this.$('.oe_m2o_cm_button').css({'display':'none'});
3506 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3509 var lines = _.escape(str).split("\n");
3513 follow = _.rest(lines).join("<br />");
3516 var $link = this.$el.find('.oe_form_uri')
3519 if (! this.options.no_open)
3520 $link.click(function () {
3522 type: 'ir.actions.act_window',
3523 res_model: self.field.relation,
3524 res_id: self.get("value"),
3525 views: [[false, 'form']],
3527 context: self.build_context().eval(),
3531 $(".oe_form_m2o_follow", this.$el).html(follow);
3534 set_value: function(value_) {
3536 if (value_ instanceof Array) {
3537 this.display_value = {};
3538 this.display_value_backup = {};
3539 if (! this.options.always_reload) {
3540 this.display_value["" + value_[0]] = value_[1];
3543 this.display_value_backup["" + value_[0]] = value_[1];
3547 value_ = value_ || false;
3548 this.reinit_value(value_);
3550 get_displayed: function() {
3551 return this.display_value["" + this.get("value")];
3553 add_id: function(id) {
3554 this.display_value = {};
3555 this.display_value_backup = {};
3556 this.reinit_value(id);
3558 is_false: function() {
3559 return ! this.get("value");
3561 focus: function () {
3562 var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3563 return input ? input.focus() : false;
3565 _quick_create: function() {
3567 this.ed_def.reject();
3568 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3570 _search_create_popup: function() {
3572 this.ed_def.reject();
3573 this.ignore_focusout = true;
3574 this.reinit_value(false);
3575 var res = instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3576 this.ignore_focusout = false;
3580 set_dimensions: function (height, width) {
3581 this._super(height, width);
3582 if (!this.get("effective_readonly") && this.$input)
3583 this.$input.css('height', height);
3587 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3588 template: 'Many2OneButton',
3589 init: function(field_manager, node) {
3590 this._super.apply(this, arguments);
3593 this._super.apply(this, arguments);
3596 set_button: function() {
3599 this.$button.remove();
3602 this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3603 this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3604 this.$button.addClass('oe_link').css({'padding':'4px'});
3605 this.$el.append(this.$button);
3606 this.$button.on('click', self.on_click);
3608 on_click: function(ev) {
3610 this.popup = new instance.web.form.FormOpenPopup(this);
3611 this.popup.show_element(
3612 this.field.relation,
3614 this.build_context(),
3615 {title: this.string}
3617 this.popup.on('create_completed', self, function(r) {
3621 set_value: function(value_) {
3623 if (value_ instanceof Array) {
3626 value_ = value_ || false;
3627 this.set('value', value_);
3633 * Abstract-ish ListView.List subclass adding an "Add an item" row to replace
3634 * the big ugly button in the header.
3636 * Requires the implementation of a ``is_readonly`` method (usually a proxy to
3637 * the corresponding field's readonly or effective_readonly property) to
3638 * decide whether the special row should or should not be inserted.
3640 * Optionally an ``_add_row_class`` attribute can be set for the class(es) to
3641 * set on the insertion row.
3643 instance.web.form.AddAnItemList = instance.web.ListView.List.extend({
3644 pad_table_to: function (count) {
3645 if (!this.view.is_action_enabled('create') || this.is_readonly()) {
3650 this._super(count > 0 ? count - 1 : 0);
3653 var columns = _(this.columns).filter(function (column) {
3654 return column.invisible !== '1';
3656 if (this.options.selectable) { columns++; }
3657 if (this.options.deletable) { columns++; }
3659 var $cell = $('<td>', {
3661 'class': this._add_row_class || ''
3663 $('<a>', {href: '#'}).text(_t("Add an item"))
3664 .mousedown(function () {
3665 // FIXME: needs to be an official API somehow
3666 if (self.view.editor.is_editing()) {
3667 self.view.__ignore_blur = true;
3670 .click(function (e) {
3672 e.stopPropagation();
3673 // FIXME: there should also be an API for that one
3674 if (self.view.editor.form.__blur_timeout) {
3675 clearTimeout(self.view.editor.form.__blur_timeout);
3676 self.view.editor.form.__blur_timeout = false;
3678 self.view.ensure_saved().done(function () {
3679 self.view.do_add_record();
3683 var $padding = this.$current.find('tr:not([data-id]):first');
3684 var $newrow = $('<tr>').append($cell);
3685 if ($padding.length) {
3686 $padding.before($newrow);
3688 this.$current.append($newrow)
3694 # Values: (0, 0, { fields }) create
3695 # (1, ID, { fields }) update
3696 # (2, ID) remove (delete)
3697 # (3, ID) unlink one (target id or target of relation)
3699 # (5) unlink all (only valid for one2many)
3704 'create': function (values) {
3705 return [commands.CREATE, false, values];
3707 // (1, id, {values})
3709 'update': function (id, values) {
3710 return [commands.UPDATE, id, values];
3714 'delete': function (id) {
3715 return [commands.DELETE, id, false];
3717 // (3, id[, _]) removes relation, but not linked record itself
3719 'forget': function (id) {
3720 return [commands.FORGET, id, false];
3724 'link_to': function (id) {
3725 return [commands.LINK_TO, id, false];
3729 'delete_all': function () {
3730 return [5, false, false];
3732 // (6, _, ids) replaces all linked records with provided ids
3734 'replace_with': function (ids) {
3735 return [6, false, ids];
3738 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3739 multi_selection: false,
3740 disable_utility_classes: true,
3741 init: function(field_manager, node) {
3742 this._super(field_manager, node);
3743 lazy_build_o2m_kanban_view();
3744 this.is_loaded = $.Deferred();
3745 this.initial_is_loaded = this.is_loaded;
3746 this.form_last_update = $.Deferred();
3747 this.init_form_last_update = this.form_last_update;
3748 this.is_started = false;
3749 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
3750 this.dataset.o2m = this;
3751 this.dataset.parent_view = this.view;
3752 this.dataset.child_name = this.name;
3754 this.dataset.on('dataset_changed', this, function() {
3755 self.trigger_on_change();
3760 this._super.apply(this, arguments);
3761 this.$el.addClass('oe_form_field oe_form_field_one2many');
3766 this.is_loaded.done(function() {
3767 self.on("change:effective_readonly", self, function() {
3768 self.is_loaded = self.is_loaded.then(function() {
3769 self.viewmanager.destroy();
3770 return $.when(self.load_views()).done(function() {
3771 self.reload_current_view();
3776 this.is_started = true;
3777 this.reload_current_view();
3779 trigger_on_change: function() {
3780 this.trigger('changed_value');
3782 load_views: function() {
3785 var modes = this.node.attrs.mode;
3786 modes = !!modes ? modes.split(",") : ["tree"];
3788 _.each(modes, function(mode) {
3789 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
3790 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
3794 view_type: mode == "tree" ? "list" : mode,
3797 if (self.field.views && self.field.views[mode]) {
3798 view.embedded_view = self.field.views[mode];
3800 if(view.view_type === "list") {
3801 _.extend(view.options, {
3803 selectable: self.multi_selection,
3805 import_enabled: false,
3808 if (self.get("effective_readonly")) {
3809 _.extend(view.options, {
3814 } else if (view.view_type === "form") {
3815 if (self.get("effective_readonly")) {
3816 view.view_type = 'form';
3818 _.extend(view.options, {
3819 not_interactible_on_create: true,
3821 } else if (view.view_type === "kanban") {
3822 _.extend(view.options, {
3823 confirm_on_delete: false,
3825 if (self.get("effective_readonly")) {
3826 _.extend(view.options, {
3827 action_buttons: false,
3828 quick_creatable: false,
3830 read_only_mode: true,
3838 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
3839 this.viewmanager.o2m = self;
3840 var once = $.Deferred().done(function() {
3841 self.init_form_last_update.resolve();
3843 var def = $.Deferred().done(function() {
3844 self.initial_is_loaded.resolve();
3846 this.viewmanager.on("controller_inited", self, function(view_type, controller) {
3847 controller.o2m = self;
3848 if (view_type == "list") {
3849 if (self.get("effective_readonly")) {
3850 controller.on('edit:before', self, function (e) {
3853 _(controller.columns).find(function (column) {
3854 if (!(column instanceof instance.web.list.Handle)) {
3857 column.modifiers.invisible = true;
3861 } else if (view_type === "form") {
3862 if (self.get("effective_readonly")) {
3863 $(".oe_form_buttons", controller.$el).children().remove();
3865 controller.on("load_record", self, function(){
3868 controller.on('pager_action_executed',self,self.save_any_view);
3869 } else if (view_type == "graph") {
3870 self.reload_current_view();
3874 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
3875 $.when(self.save_any_view()).done(function() {
3876 if (n_mode === "list") {
3877 $.async_when().done(function() {
3878 self.reload_current_view();
3883 $.async_when().done(function () {
3884 self.viewmanager.appendTo(self.$el);
3888 reload_current_view: function() {
3890 self.is_loaded = self.is_loaded.then(function() {
3891 var active_view = self.viewmanager.active_view;
3892 var view = self.viewmanager.views[active_view].controller;
3893 if(active_view === "list") {
3894 return view.reload_content();
3895 } else if (active_view === "form") {
3896 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
3897 self.dataset.index = 0;
3899 var act = function() {
3900 return view.do_show();
3902 self.form_last_update = self.form_last_update.then(act, act);
3903 return self.form_last_update;
3904 } else if (view.do_search) {
3905 return view.do_search(self.build_domain(), self.dataset.get_context(), []);
3908 return self.is_loaded;
3910 set_value: function(value_) {
3911 value_ = value_ || [];
3913 this.dataset.reset_ids([]);
3915 if(value_.length >= 1 && value_[0] instanceof Array) {
3917 _.each(value_, function(command) {
3918 var obj = {values: command[2]};
3919 switch (command[0]) {
3920 case commands.CREATE:
3921 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3923 self.dataset.to_create.push(obj);
3924 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3927 case commands.UPDATE:
3928 obj['id'] = command[1];
3929 self.dataset.to_write.push(obj);
3930 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3933 case commands.DELETE:
3934 self.dataset.to_delete.push({id: command[1]});
3936 case commands.LINK_TO:
3937 ids.push(command[1]);
3939 case commands.DELETE_ALL:
3940 self.dataset.delete_all = true;
3945 this.dataset.set_ids(ids);
3946 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
3948 this.dataset.delete_all = true;
3949 _.each(value_, function(command) {
3950 var obj = {values: command};
3951 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3953 self.dataset.to_create.push(obj);
3954 self.dataset.cache.push(_.clone(obj));
3958 this.dataset.set_ids(ids);
3960 this._super(value_);
3961 this.dataset.reset_ids(value_);
3963 if (this.dataset.index === null && this.dataset.ids.length > 0) {
3964 this.dataset.index = 0;
3966 this.trigger_on_change();
3967 if (this.is_started) {
3968 return self.reload_current_view();
3973 get_value: function() {
3977 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
3978 val = val.concat(_.map(this.dataset.ids, function(id) {
3979 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
3981 return commands.create(alter_order.values);
3983 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
3985 return commands.update(alter_order.id, alter_order.values);
3987 return commands.link_to(id);
3989 return val.concat(_.map(
3990 this.dataset.to_delete, function(x) {
3991 return commands['delete'](x.id);}));
3993 commit_value: function() {
3994 return this.save_any_view();
3996 save_any_view: function() {
3997 if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
3998 this.viewmanager.views[this.viewmanager.active_view] &&
3999 this.viewmanager.views[this.viewmanager.active_view].controller) {
4000 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
4001 if (this.viewmanager.active_view === "form") {
4002 if (view.is_initialized.state() !== 'resolved') {
4003 return $.when(false);
4005 return $.when(view.save());
4006 } else if (this.viewmanager.active_view === "list") {
4007 return $.when(view.ensure_saved());
4010 return $.when(false);
4012 is_syntax_valid: function() {
4013 if (! this.viewmanager || ! this.viewmanager.views[this.viewmanager.active_view])
4015 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
4016 switch (this.viewmanager.active_view) {
4018 return _(view.fields).chain()
4023 return view.is_valid();
4029 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
4030 template: 'One2Many.viewmanager',
4031 init: function(parent, dataset, views, flags) {
4032 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
4033 this.registry = this.registry.extend({
4034 list: 'instance.web.form.One2ManyListView',
4035 form: 'instance.web.form.One2ManyFormView',
4036 kanban: 'instance.web.form.One2ManyKanbanView',
4038 this.__ignore_blur = false;
4040 switch_mode: function(mode, unused) {
4041 if (mode !== 'form') {
4042 return this._super(mode, unused);
4045 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
4046 var pop = new instance.web.form.FormOpenPopup(this);
4047 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4048 title: _t("Open: ") + self.o2m.string,
4049 create_function: function(data, options) {
4050 return self.o2m.dataset.create(data, options).done(function(r) {
4051 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4052 self.o2m.dataset.trigger("dataset_changed", r);
4055 write_function: function(id, data, options) {
4056 return self.o2m.dataset.write(id, data, {}).done(function() {
4057 self.o2m.reload_current_view();
4060 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4061 parent_view: self.o2m.view,
4062 child_name: self.o2m.name,
4063 read_function: function() {
4064 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4066 form_view_options: {'not_interactible_on_create':true},
4067 readonly: self.o2m.get("effective_readonly")
4069 pop.on("elements_selected", self, function() {
4070 self.o2m.reload_current_view();
4075 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
4076 get_context: function() {
4077 this.context = this.o2m.build_context();
4078 return this.context;
4082 instance.web.form.One2ManyListView = instance.web.ListView.extend({
4083 _template: 'One2Many.listview',
4084 init: function (parent, dataset, view_id, options) {
4085 this._super(parent, dataset, view_id, _.extend(options || {}, {
4086 GroupsType: instance.web.form.One2ManyGroups,
4087 ListType: instance.web.form.One2ManyList
4089 this.on('edit:after', this, this.proxy('_after_edit'));
4090 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
4093 .bind('add', this.proxy("changed_records"))
4094 .bind('edit', this.proxy("changed_records"))
4095 .bind('remove', this.proxy("changed_records"));
4097 start: function () {
4098 var ret = this._super();
4100 .off('mousedown.handleButtons')
4101 .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
4104 changed_records: function () {
4105 this.o2m.trigger_on_change();
4107 is_valid: function () {
4109 if (!this.editable()){
4112 this.o2m._dirty_flag = true;
4114 return _.every(this.records.records, function(record){
4116 _.each(self.editor.form.fields, function(field){
4117 field.set_value(r.attributes[field.name]);
4119 return _.every(self.editor.form.fields, function(field){
4120 field.process_modifiers();
4121 field._check_css_flags();
4122 return field.is_valid();
4126 do_add_record: function () {
4127 if (this.editable()) {
4128 this._super.apply(this, arguments);
4131 var pop = new instance.web.form.SelectCreatePopup(this);
4133 self.o2m.field.relation,
4135 title: _t("Create: ") + self.o2m.string,
4136 initial_view: "form",
4137 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4138 create_function: function(data, options) {
4139 return self.o2m.dataset.create(data, options).done(function(r) {
4140 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4141 self.o2m.dataset.trigger("dataset_changed", r);
4144 read_function: function() {
4145 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4147 parent_view: self.o2m.view,
4148 child_name: self.o2m.name,
4149 form_view_options: {'not_interactible_on_create':true}
4151 self.o2m.build_domain(),
4152 self.o2m.build_context()
4154 pop.on("elements_selected", self, function() {
4155 self.o2m.reload_current_view();
4159 do_activate_record: function(index, id) {
4161 var pop = new instance.web.form.FormOpenPopup(self);
4162 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4163 title: _t("Open: ") + self.o2m.string,
4164 write_function: function(id, data) {
4165 return self.o2m.dataset.write(id, data, {}).done(function() {
4166 self.o2m.reload_current_view();
4169 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4170 parent_view: self.o2m.view,
4171 child_name: self.o2m.name,
4172 read_function: function() {
4173 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4175 form_view_options: {'not_interactible_on_create':true},
4176 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
4179 do_button_action: function (name, id, callback) {
4180 if (!_.isNumber(id)) {
4181 instance.webclient.notification.warn(
4182 _t("Action Button"),
4183 _t("The o2m record must be saved before an action can be used"));
4186 var parent_form = this.o2m.view;
4188 this.ensure_saved().then(function () {
4190 return parent_form.save();
4193 }).done(function () {
4194 var ds = self.o2m.dataset;
4195 var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
4196 return value.length;
4198 if (!self.o2m.options.reload_on_button && !cached_records) {
4199 self.handle_button(name, id, callback);
4201 self.handle_button(name, id, function(){
4202 self.o2m.view.reload();
4208 _after_edit: function () {
4209 this.__ignore_blur = false;
4210 this.editor.form.on('blurred', this, this._on_form_blur);
4212 // The form's blur thing may be jiggered during the edition setup,
4213 // potentially leading to the o2m instasaving the row. Cancel any
4214 // blurring triggered the edition startup here
4215 this.editor.form.widgetFocused();
4217 _before_unedit: function () {
4218 this.editor.form.off('blurred', this, this._on_form_blur);
4220 _button_down: function () {
4221 // If a button is clicked (usually some sort of action button), it's
4222 // the button's responsibility to ensure the editable list is in the
4223 // correct state -> ignore form blurring
4224 this.__ignore_blur = true;
4227 * Handles blurring of the nested form (saves the currently edited row),
4228 * unless the flag to ignore the event is set to ``true``
4230 * Makes the internal form go away
4232 _on_form_blur: function () {
4233 if (this.__ignore_blur) {
4234 this.__ignore_blur = false;
4237 // FIXME: why isn't there an API for this?
4238 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4239 this.ensure_saved();
4242 this.cancel_edition();
4244 keypress_ENTER: function () {
4245 // blurring caused by hitting the [Return] key, should skip the
4246 // autosave-on-blur and let the handler for [Return] do its thing (save
4247 // the current row *anyway*, then create a new one/edit the next one)
4248 this.__ignore_blur = true;
4249 this._super.apply(this, arguments);
4251 do_delete: function (ids) {
4252 var confirm = window.confirm;
4253 window.confirm = function () { return true; };
4255 return this._super(ids);
4257 window.confirm = confirm;
4260 reload_record: function (record) {
4261 // Evict record.id from cache to ensure it will be reloaded correctly
4262 this.dataset.evict_record(record.get('id'));
4264 return this._super(record);
4267 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4268 setup_resequence_rows: function () {
4269 if (!this.view.o2m.get('effective_readonly')) {
4270 this._super.apply(this, arguments);
4274 instance.web.form.One2ManyList = instance.web.form.AddAnItemList.extend({
4275 _add_row_class: 'oe_form_field_one2many_list_row_add',
4276 is_readonly: function () {
4277 return this.view.o2m.get('effective_readonly');
4281 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4282 form_template: 'One2Many.formview',
4283 load_form: function(data) {
4286 this.$buttons.find('button.oe_form_button_create').click(function() {
4287 self.save().done(self.on_button_new);
4290 do_notify_change: function() {
4291 if (this.dataset.parent_view) {
4292 this.dataset.parent_view.do_notify_change();
4294 this._super.apply(this, arguments);
4299 var lazy_build_o2m_kanban_view = function() {
4300 if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
4302 instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4306 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4307 template: "FieldMany2ManyTags",
4308 tag_template: "FieldMany2ManyTag",
4310 this._super.apply(this, arguments);
4311 instance.web.form.CompletionFieldMixin.init.call(this);
4312 this.set({"value": []});
4313 this._display_orderer = new instance.web.DropMisordered();
4314 this._drop_shown = false;
4316 initialize_texttext: function(){
4319 plugins : 'tags arrow autocomplete',
4321 render: function(suggestion) {
4322 return $('<span class="text-label"/>').
4323 data('index', suggestion['index']).html(suggestion['label']);
4328 selectFromDropdown: function() {
4329 this.trigger('hideDropdown');
4330 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4331 var data = self.search_result[index];
4333 self.add_id(data.id);
4335 self.ignore_blur = true;
4338 this.trigger('setSuggestions', {result : []});
4342 isTagAllowed: function(tag) {
4346 removeTag: function(tag) {
4347 var id = tag.data("id");
4348 self.set({"value": _.without(self.get("value"), id)});
4350 renderTag: function(stuff) {
4351 return $.fn.textext.TextExtTags.prototype.renderTag.
4352 call(this, stuff).data("id", stuff.id);
4356 itemToString: function(item) {
4361 onSetInputData: function(e, data) {
4363 this._plugins.autocomplete._suggestions = null;
4365 this.input().val(data);
4371 initialize_content: function() {
4372 if (this.get("effective_readonly"))
4375 self.ignore_blur = false;
4376 self.$text = this.$("textarea");
4377 self.$text.textext(self.initialize_texttext()).bind('getSuggestions', function(e, data) {
4379 var str = !!data ? data.query || '' : '';
4380 self.get_search_result(str).done(function(result) {
4381 self.search_result = result;
4382 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4383 return _.extend(el, {index:i});
4386 }).bind('hideDropdown', function() {
4387 self._drop_shown = false;
4388 }).bind('showDropdown', function() {
4389 self._drop_shown = true;
4391 self.tags = self.$text.textext()[0].tags();
4393 .focusin(function () {
4394 self.trigger('focused');
4395 self.ignore_blur = false;
4397 .focusout(function() {
4398 self.$text.trigger("setInputData", "");
4399 if (!self.ignore_blur) {
4400 self.trigger('blurred');
4402 }).keydown(function(e) {
4403 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4404 self.$text.textext()[0].autocomplete().selectFromDropdown();
4408 set_value: function(value_) {
4409 value_ = value_ || [];
4410 if (value_.length >= 1 && value_[0] instanceof Array) {
4411 value_ = value_[0][2];
4413 this._super(value_);
4415 is_false: function() {
4416 return _(this.get("value")).isEmpty();
4418 get_value: function() {
4419 var tmp = [commands.replace_with(this.get("value"))];
4422 get_search_blacklist: function() {
4423 return this.get("value");
4425 map_tag: function(data){
4426 return _.map(data, function(el) {return {name: el[1], id:el[0]};})
4428 get_render_data: function(ids){
4430 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4431 return dataset.name_get(ids);
4433 render_tag: function(data) {
4435 if (! self.get("effective_readonly")) {
4436 self.tags.containerElement().children().remove();
4437 self.$('textarea').css("padding-left", "3px");
4438 self.tags.addTags(self.map_tag(data));
4440 self.$el.html(QWeb.render(self.tag_template, {elements: data}));
4443 render_value: function() {
4445 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4446 var values = self.get("value");
4447 var handle_names = function(data) {
4448 if (self.isDestroyed())
4451 _.each(data, function(el) {
4452 indexed[el[0]] = el;
4454 data = _.map(values, function(el) { return indexed[el]; });
4455 self.render_tag(data);
4457 if (! values || values.length > 0) {
4458 return this._display_orderer.add(self.get_render_data(values)).done(handle_names);
4463 add_id: function(id) {
4464 this.set({'value': _.uniq(this.get('value').concat([id]))});
4466 focus: function () {
4467 var input = this.$text && this.$text[0];
4468 return input ? input.focus() : false;
4470 set_dimensions: function (height, width) {
4471 this._super(height, width);
4472 this.$("textarea").css({
4477 _search_create_popup: function() {
4478 self.ignore_blur = true;
4479 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
4485 - reload_on_button: Reload the whole form view if click on a button in a list view.
4486 If you see this options, do not use it, it's basically a dirty hack to make one
4487 precise o2m to behave the way we want.
4489 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4490 multi_selection: false,
4491 disable_utility_classes: true,
4492 init: function(field_manager, node) {
4493 this._super(field_manager, node);
4494 this.is_loaded = $.Deferred();
4495 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4496 this.dataset.m2m = this;
4498 this.dataset.on('unlink', self, function(ids) {
4499 self.dataset_changed();
4502 this.list_dm = new instance.web.DropMisordered();
4503 this.render_value_dm = new instance.web.DropMisordered();
4505 initialize_content: function() {
4508 this.$el.addClass('oe_form_field oe_form_field_many2many');
4510 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4512 'deletable': this.get("effective_readonly") ? false : true,
4513 'selectable': this.multi_selection,
4515 'reorderable': false,
4516 'import_enabled': false,
4518 var embedded = (this.field.views || {}).tree;
4520 this.list_view.set_embedded_view(embedded);
4522 this.list_view.m2m_field = this;
4523 var loaded = $.Deferred();
4524 this.list_view.on("list_view_loaded", this, function() {
4527 this.list_view.appendTo(this.$el);
4529 var old_def = self.is_loaded;
4530 self.is_loaded = $.Deferred().done(function() {
4533 this.list_dm.add(loaded).then(function() {
4534 self.is_loaded.resolve();
4537 destroy_content: function() {
4538 this.list_view.destroy();
4539 this.list_view = undefined;
4541 set_value: function(value_) {
4542 value_ = value_ || [];
4543 if (value_.length >= 1 && value_[0] instanceof Array) {
4544 value_ = value_[0][2];
4546 this._super(value_);
4548 get_value: function() {
4549 return [commands.replace_with(this.get('value'))];
4551 is_false: function () {
4552 return _(this.get("value")).isEmpty();
4554 render_value: function() {
4556 this.dataset.set_ids(this.get("value"));
4557 this.render_value_dm.add(this.is_loaded).then(function() {
4558 return self.list_view.reload_content();
4561 dataset_changed: function() {
4562 this.internal_set_value(this.dataset.ids);
4566 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4567 get_context: function() {
4568 this.context = this.m2m.build_context();
4569 return this.context;
4575 * @extends instance.web.ListView
4577 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4578 init: function (parent, dataset, view_id, options) {
4579 this._super(parent, dataset, view_id, _.extend(options || {}, {
4580 ListType: instance.web.form.Many2ManyList,
4583 do_add_record: function () {
4584 var pop = new instance.web.form.SelectCreatePopup(this);
4588 title: _t("Add: ") + this.m2m_field.string,
4589 no_create: this.m2m_field.options.no_create,
4591 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4592 this.m2m_field.build_context()
4595 pop.on("elements_selected", self, function(element_ids) {
4597 _(element_ids).each(function (id) {
4598 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4599 self.dataset.set_ids(self.dataset.ids.concat([id]));
4600 self.m2m_field.dataset_changed();
4605 self.reload_content();
4609 do_activate_record: function(index, id) {
4611 var pop = new instance.web.form.FormOpenPopup(this);
4612 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4613 title: _t("Open: ") + this.m2m_field.string,
4614 readonly: this.getParent().get("effective_readonly")
4616 pop.on('write_completed', self, self.reload_content);
4618 do_button_action: function(name, id, callback) {
4620 var _sup = _.bind(this._super, this);
4621 if (! this.m2m_field.options.reload_on_button) {
4622 return _sup(name, id, callback);
4624 return this.m2m_field.view.save().then(function() {
4625 return _sup(name, id, function() {
4626 self.m2m_field.view.reload();
4631 is_action_enabled: function () { return true; },
4633 instance.web.form.Many2ManyList = instance.web.form.AddAnItemList.extend({
4634 _add_row_class: 'oe_form_field_many2many_list_row_add',
4635 is_readonly: function () {
4636 return this.view.m2m_field.get('effective_readonly');
4640 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4641 disable_utility_classes: true,
4642 init: function(field_manager, node) {
4643 this._super(field_manager, node);
4644 instance.web.form.CompletionFieldMixin.init.call(this);
4645 m2m_kanban_lazy_init();
4646 this.is_loaded = $.Deferred();
4647 this.initial_is_loaded = this.is_loaded;
4650 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4651 this.dataset.m2m = this;
4652 this.dataset.on('unlink', self, function(ids) {
4653 self.dataset_changed();
4657 this._super.apply(this, arguments);
4662 self.on("change:effective_readonly", self, function() {
4663 self.is_loaded = self.is_loaded.then(function() {
4664 self.kanban_view.destroy();
4665 return $.when(self.load_view()).done(function() {
4666 self.render_value();
4671 set_value: function(value_) {
4672 value_ = value_ || [];
4673 if (value_.length >= 1 && value_[0] instanceof Array) {
4674 value_ = value_[0][2];
4676 this._super(value_);
4678 get_value: function() {
4679 return [commands.replace_with(this.get('value'))];
4681 load_view: function() {
4683 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4684 'create_text': _t("Add"),
4685 'creatable': self.get("effective_readonly") ? false : true,
4686 'quick_creatable': self.get("effective_readonly") ? false : true,
4687 'read_only_mode': self.get("effective_readonly") ? true : false,
4688 'confirm_on_delete': false,
4690 var embedded = (this.field.views || {}).kanban;
4692 this.kanban_view.set_embedded_view(embedded);
4694 this.kanban_view.m2m = this;
4695 var loaded = $.Deferred();
4696 this.kanban_view.on("kanban_view_loaded",self,function() {
4697 self.initial_is_loaded.resolve();
4700 this.kanban_view.on('switch_mode', this, this.open_popup);
4701 $.async_when().done(function () {
4702 self.kanban_view.appendTo(self.$el);
4706 render_value: function() {
4708 this.dataset.set_ids(this.get("value"));
4709 this.is_loaded = this.is_loaded.then(function() {
4710 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
4713 dataset_changed: function() {
4714 this.set({'value': this.dataset.ids});
4716 open_popup: function(type, unused) {
4717 if (type !== "form")
4721 if (this.dataset.index === null) {
4722 pop = new instance.web.form.SelectCreatePopup(this);
4724 this.field.relation,
4726 title: _t("Add: ") + this.string
4728 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
4729 this.build_context()
4731 pop.on("elements_selected", self, function(element_ids) {
4732 _.each(element_ids, function(one_id) {
4733 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
4734 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
4735 self.dataset_changed();
4736 self.render_value();
4741 var id = self.dataset.ids[self.dataset.index];
4742 pop = new instance.web.form.FormOpenPopup(this);
4743 pop.show_element(self.field.relation, id, self.build_context(), {
4744 title: _t("Open: ") + self.string,
4745 write_function: function(id, data, options) {
4746 return self.dataset.write(id, data, {}).done(function() {
4747 self.render_value();
4750 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
4751 parent_view: self.view,
4752 child_name: self.name,
4753 readonly: self.get("effective_readonly")
4757 add_id: function(id) {
4758 this.quick_create.add_id(id);
4762 function m2m_kanban_lazy_init() {
4763 if (instance.web.form.Many2ManyKanbanView)
4765 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4766 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
4767 _is_quick_create_enabled: function() {
4768 return this._super() && ! this.group_by;
4771 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
4772 template: 'Many2ManyKanban.quick_create',
4775 * close_btn: If true, the widget will display a "Close" button able to trigger
4778 init: function(parent, dataset, context, buttons) {
4779 this._super(parent);
4780 this.m2m = this.getParent().view.m2m;
4781 this.m2m.quick_create = this;
4782 this._dataset = dataset;
4783 this._buttons = buttons || false;
4784 this._context = context || {};
4786 start: function () {
4788 self.$text = this.$el.find('input').css("width", "200px");
4789 self.$text.textext({
4790 plugins : 'arrow autocomplete',
4792 render: function(suggestion) {
4793 return $('<span class="text-label"/>').
4794 data('index', suggestion['index']).html(suggestion['label']);
4799 selectFromDropdown: function() {
4800 $(this).trigger('hideDropdown');
4801 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4802 var data = self.search_result[index];
4804 self.add_id(data.id);
4811 itemToString: function(item) {
4816 }).bind('getSuggestions', function(e, data) {
4818 var str = !!data ? data.query || '' : '';
4819 self.m2m.get_search_result(str).done(function(result) {
4820 self.search_result = result;
4821 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4822 return _.extend(el, {index:i});
4826 self.$text.focusout(function() {
4831 this.$text[0].focus();
4833 add_id: function(id) {
4836 self.trigger('added', id);
4837 this.m2m.dataset_changed();
4843 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
4845 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
4846 template: "AbstractFormPopup.render",
4849 * -readonly: only applicable when not in creation mode, default to false
4850 * - alternative_form_view
4857 * - form_view_options
4859 init_popup: function(model, row_id, domain, context, options) {
4860 this.row_id = row_id;
4862 this.domain = domain || [];
4863 this.context = context || {};
4864 this.options = options;
4865 _.defaults(this.options, {
4868 init_dataset: function() {
4870 this.created_elements = [];
4871 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
4872 this.dataset.read_function = this.options.read_function;
4873 this.dataset.create_function = function(data, options, sup) {
4874 var fct = self.options.create_function || sup;
4875 return fct.call(this, data, options).done(function(r) {
4876 self.trigger('create_completed saved', r);
4877 self.created_elements.push(r);
4880 this.dataset.write_function = function(id, data, options, sup) {
4881 var fct = self.options.write_function || sup;
4882 return fct.call(this, id, data, options).done(function(r) {
4883 self.trigger('write_completed saved', r);
4886 this.dataset.parent_view = this.options.parent_view;
4887 this.dataset.child_name = this.options.child_name;
4889 display_popup: function() {
4891 this.renderElement();
4892 var dialog = new instance.web.Dialog(this, {
4894 dialogClass: 'oe_act_window',
4896 self.check_exit(true);
4898 title: this.options.title || "",
4899 }, this.$el).open();
4900 this.$buttonpane = dialog.$buttons;
4903 setup_form_view: function() {
4906 this.dataset.ids = [this.row_id];
4907 this.dataset.index = 0;
4909 this.dataset.index = null;
4911 var options = _.clone(self.options.form_view_options) || {};
4912 if (this.row_id !== null) {
4913 options.initial_mode = this.options.readonly ? "view" : "edit";
4916 $buttons: this.$buttonpane,
4918 this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
4919 if (this.options.alternative_form_view) {
4920 this.view_form.set_embedded_view(this.options.alternative_form_view);
4922 this.view_form.appendTo(this.$el.find(".oe_popup_form"));
4923 this.view_form.on("form_view_loaded", self, function() {
4924 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
4925 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
4926 multi_select: multi_select,
4927 readonly: self.row_id !== null && self.options.readonly,
4929 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
4930 $snbutton.click(function() {
4931 $.when(self.view_form.save()).done(function() {
4932 self.view_form.reload_mutex.exec(function() {
4933 self.view_form.on_button_new();
4937 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
4938 $sbutton.click(function() {
4939 $.when(self.view_form.save()).done(function() {
4940 self.view_form.reload_mutex.exec(function() {
4945 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
4946 $cbutton.click(function() {
4947 self.view_form.trigger('on_button_cancel');
4950 self.view_form.do_show();
4953 select_elements: function(element_ids) {
4954 this.trigger("elements_selected", element_ids);
4956 check_exit: function(no_destroy) {
4957 if (this.created_elements.length > 0) {
4958 this.select_elements(this.created_elements);
4959 this.created_elements = [];
4961 this.trigger('closed');
4964 destroy: function () {
4965 this.trigger('closed');
4966 if (this.$el.is(":data(dialog)")) {
4967 this.$el.dialog('close');
4974 * Class to display a popup containing a form view.
4976 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
4977 show_element: function(model, row_id, context, options) {
4978 this.init_popup(model, row_id, [], context, options);
4979 _.defaults(this.options, {
4981 this.display_popup();
4985 this.init_dataset();
4986 this.setup_form_view();
4991 * Class to display a popup to display a list to search a row. It also allows
4992 * to switch to a form view to create a new row.
4994 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
4998 * - initial_view: form or search (default search)
4999 * - disable_multiple_selection
5000 * - list_view_options
5002 select_element: function(model, options, domain, context) {
5003 this.init_popup(model, null, domain, context, options);
5005 _.defaults(this.options, {
5006 initial_view: "search",
5008 this.initial_ids = this.options.initial_ids;
5009 this.display_popup();
5013 this.init_dataset();
5014 if (this.options.initial_view == "search") {
5015 instance.web.pyeval.eval_domains_and_contexts({
5017 contexts: [this.context]
5018 }).done(function (results) {
5019 var search_defaults = {};
5020 _.each(results.context, function (value_, key) {
5021 var match = /^search_default_(.*)$/.exec(key);
5023 search_defaults[match[1]] = value_;
5026 self.setup_search_view(search_defaults);
5032 setup_search_view: function(search_defaults) {
5034 if (this.searchview) {
5035 this.searchview.destroy();
5037 this.searchview = new instance.web.SearchView(this,
5038 this.dataset, false, search_defaults);
5039 this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
5040 if (self.initial_ids) {
5041 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
5042 contexts.concat(self.context), groupbys);
5043 self.initial_ids = undefined;
5045 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
5048 this.searchview.on("search_view_loaded", self, function() {
5049 self.view_list = new instance.web.form.SelectCreateListView(self,
5050 self.dataset, false,
5051 _.extend({'deletable': false,
5052 'selectable': !self.options.disable_multiple_selection,
5053 'import_enabled': false,
5054 '$buttons': self.$buttonpane,
5055 'disable_editable_mode': true,
5056 '$pager': self.$('.oe_popup_list_pager'),
5057 }, self.options.list_view_options || {}));
5058 self.view_list.on('edit:before', self, function (e) {
5061 self.view_list.popup = self;
5062 self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
5063 self.view_list.do_show();
5064 }).then(function() {
5065 self.searchview.do_search();
5067 self.view_list.on("list_view_loaded", self, function() {
5068 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
5069 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
5070 $cbutton.click(function() {
5073 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
5074 $sbutton.click(function() {
5075 self.select_elements(self.selected_ids);
5078 $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
5079 $cbutton.click(function() {
5084 this.searchview.appendTo($(".oe_popup_search", self.$el));
5086 do_search: function(domains, contexts, groupbys) {
5088 instance.web.pyeval.eval_domains_and_contexts({
5089 domains: domains || [],
5090 contexts: contexts || [],
5091 group_by_seq: groupbys || []
5092 }).done(function (results) {
5093 self.view_list.do_search(results.domain, results.context, results.group_by);
5096 on_click_element: function(ids) {
5098 this.selected_ids = ids || [];
5099 if(this.selected_ids.length > 0) {
5100 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
5102 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
5105 new_object: function() {
5106 if (this.searchview) {
5107 this.searchview.hide();
5109 if (this.view_list) {
5110 this.view_list.do_hide();
5112 this.setup_form_view();
5116 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
5117 do_add_record: function () {
5118 this.popup.new_object();
5120 select_record: function(index) {
5121 this.popup.select_elements([this.dataset.ids[index]]);
5122 this.popup.destroy();
5124 do_select: function(ids, records) {
5125 this._super(ids, records);
5126 this.popup.on_click_element(ids);
5130 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5131 template: 'FieldReference',
5132 init: function(field_manager, node) {
5133 this._super(field_manager, node);
5134 this.reference_ready = true;
5136 destroy_content: function() {
5139 this.fm = undefined;
5142 initialize_content: function() {
5144 var fm = new instance.web.form.DefaultFieldManager(this);
5146 fm.extend_field_desc({
5148 selection: this.field_manager.get_field_desc(this.name).selection,
5156 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
5158 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5160 this.selection.on("change:value", this, this.on_selection_changed);
5161 this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
5163 .on('focused', null, function () {self.trigger('focused');})
5164 .on('blurred', null, function () {self.trigger('blurred');});
5166 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
5167 name: 'Referenced Document',
5168 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5170 this.m2o.on("change:value", this, this.data_changed);
5171 this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
5173 .on('focused', null, function () {self.trigger('focused');})
5174 .on('blurred', null, function () {self.trigger('blurred');});
5176 on_selection_changed: function() {
5177 if (this.reference_ready) {
5178 this.internal_set_value([this.selection.get_value(), false]);
5179 this.render_value();
5182 data_changed: function() {
5183 if (this.reference_ready) {
5184 this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
5187 set_value: function(val) {
5189 val = val.split(',');
5190 val[0] = val[0] || false;
5191 val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
5193 this._super(val || [false, false]);
5195 get_value: function() {
5196 return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
5198 render_value: function() {
5199 this.reference_ready = false;
5200 if (!this.get("effective_readonly")) {
5201 this.selection.set_value(this.get('value')[0]);
5203 this.m2o.field.relation = this.get('value')[0];
5204 this.m2o.set_value(this.get('value')[1]);
5205 this.m2o.$el.toggle(!!this.get('value')[0]);
5206 this.reference_ready = true;
5210 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5211 init: function(field_manager, node) {
5213 this._super(field_manager, node);
5214 this.binary_value = false;
5215 this.useFileAPI = !!window.FileReader;
5216 this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5217 if (!this.useFileAPI) {
5218 this.fileupload_id = _.uniqueId('oe_fileupload');
5219 $(window).on(this.fileupload_id, function() {
5220 var args = [].slice.call(arguments).slice(1);
5221 self.on_file_uploaded.apply(self, args);
5226 if (!this.useFileAPI) {
5227 $(window).off(this.fileupload_id);
5229 this._super.apply(this, arguments);
5231 initialize_content: function() {
5232 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5233 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5234 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5236 on_file_change: function(e) {
5238 var file_node = e.target;
5239 if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5240 if (this.useFileAPI) {
5241 var file = file_node.files[0];
5242 if (file.size > this.max_upload_size) {
5243 var msg = _t("The selected file exceed the maximum file size of %s.");
5244 instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5247 var filereader = new FileReader();
5248 filereader.readAsDataURL(file);
5249 filereader.onloadend = function(upload) {
5250 var data = upload.target.result;
5251 data = data.split(',')[1];
5252 self.on_file_uploaded(file.size, file.name, file.type, data);
5255 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5256 this.$el.find('form.oe_form_binary_form').submit();
5258 this.$el.find('.oe_form_binary_progress').show();
5259 this.$el.find('.oe_form_binary').hide();
5262 on_file_uploaded: function(size, name, content_type, file_base64) {
5263 if (size === false) {
5264 this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5265 // TODO: use openerp web crashmanager
5266 console.warn("Error while uploading file : ", name);
5268 this.filename = name;
5269 this.on_file_uploaded_and_valid.apply(this, arguments);
5271 this.$el.find('.oe_form_binary_progress').hide();
5272 this.$el.find('.oe_form_binary').show();
5274 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5276 on_save_as: function(ev) {
5277 var value = this.get('value');
5279 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5280 ev.stopPropagation();
5282 instance.web.blockUI();
5283 var c = instance.webclient.crashmanager;
5284 this.session.get_file({
5285 url: '/web/binary/saveas_ajax',
5286 data: {data: JSON.stringify({
5287 model: this.view.dataset.model,
5288 id: (this.view.datarecord.id || ''),
5290 filename_field: (this.node.attrs.filename || ''),
5291 data: instance.web.form.is_bin_size(value) ? null : value,
5292 context: this.view.dataset.get_context()
5294 complete: instance.web.unblockUI,
5295 error: c.rpc_error.bind(c)
5297 ev.stopPropagation();
5301 set_filename: function(value) {
5302 var filename = this.node.attrs.filename;
5305 tmp[filename] = value;
5306 this.field_manager.set_values(tmp);
5309 on_clear: function() {
5310 if (this.get('value') !== false) {
5311 this.binary_value = false;
5312 this.internal_set_value(false);
5318 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5319 template: 'FieldBinaryFile',
5320 initialize_content: function() {
5322 if (this.get("effective_readonly")) {
5324 this.$el.find('a').click(function(ev) {
5325 if (self.get('value')) {
5326 self.on_save_as(ev);
5332 render_value: function() {
5334 if (!this.get("effective_readonly")) {
5335 if (this.node.attrs.filename) {
5336 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5338 show_value = (this.get('value') !== null && this.get('value') !== undefined && this.get('value') !== false) ? this.get('value') : '';
5340 this.$el.find('input').eq(0).val(show_value);
5342 this.$el.find('a').toggle(!!this.get('value'));
5343 if (this.get('value')) {
5344 show_value = _t("Download");
5346 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5347 this.$el.find('a').text(show_value);
5351 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5352 this.binary_value = true;
5353 this.internal_set_value(file_base64);
5354 var show_value = name + " (" + instance.web.human_size(size) + ")";
5355 this.$el.find('input').eq(0).val(show_value);
5356 this.set_filename(name);
5358 on_clear: function() {
5359 this._super.apply(this, arguments);
5360 this.$el.find('input').eq(0).val('');
5361 this.set_filename('');
5365 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5366 template: 'FieldBinaryImage',
5367 placeholder: "/web/static/src/img/placeholder.png",
5368 render_value: function() {
5371 if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5372 url = 'data:image/png;base64,' + this.get('value');
5373 } else if (this.get('value')) {
5374 var id = JSON.stringify(this.view.datarecord.id || null);
5375 var field = this.name;
5376 if (this.options.preview_image)
5377 field = this.options.preview_image;
5378 url = this.session.url('/web/binary/image', {
5379 model: this.view.dataset.model,
5382 t: (new Date().getTime()),
5385 url = this.placeholder;
5387 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5388 $($img).click(function(e) {
5389 if(self.view.get("actual_mode") == "view") {
5390 var $button = $(".oe_form_button_edit");
5391 $button.openerpBounce();
5392 e.stopPropagation();
5395 this.$el.find('> img').remove();
5396 this.$el.prepend($img);
5397 $img.load(function() {
5398 if (! self.options.size)
5400 $img.css("max-width", "" + self.options.size[0] + "px");
5401 $img.css("max-height", "" + self.options.size[1] + "px");
5402 $img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px");
5403 $img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px");
5405 $img.on('error', function() {
5406 $img.attr('src', self.placeholder);
5407 instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5410 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5411 this.internal_set_value(file_base64);
5412 this.binary_value = true;
5413 this.render_value();
5414 this.set_filename(name);
5416 on_clear: function() {
5417 this._super.apply(this, arguments);
5418 this.render_value();
5419 this.set_filename('');
5421 set_value: function(value_){
5422 var changed = value_ !== this.get_value();
5423 this._super.apply(this, arguments);
5424 // By default, on binary images read, the server returns the binary size
5425 // This is possible that two images have the exact same size
5426 // Therefore we trigger the change in case the image value hasn't changed
5427 // So the image is re-rendered correctly
5429 this.trigger("change:value", this, {
5438 * Widget for (many2many field) to upload one or more file in same time and display in list.
5439 * The user can delete his files.
5440 * Options on attribute ; "blockui" {Boolean} block the UI or not
5441 * during the file is uploading
5443 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5444 template: "FieldBinaryFileUploader",
5445 init: function(field_manager, node) {
5446 this._super(field_manager, node);
5447 this.field_manager = field_manager;
5449 if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5450 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);
5454 this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5455 this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5456 $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5460 this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5461 this.on("change:effective_readonly", this, function () {
5462 this.render_value();
5465 set_value: function(value_) {
5466 value_ = value_ || [];
5467 if (value_.length >= 1 && value_[0] instanceof Array) {
5468 value_ = value_[0][2];
5470 this._super(value_);
5472 get_value: function() {
5473 var tmp = [commands.replace_with(this.get("value"))];
5476 get_file_url: function (attachment) {
5477 return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5479 read_name_values : function () {
5481 // don't reset know values
5482 var ids = this.get('value');
5483 var _value = _.filter(ids, function (id) { return typeof self.data[id] == 'undefined'; } );
5484 // send request for get_name
5485 if (_value.length) {
5486 return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).then(function (datas) {
5487 _.each(datas, function (data) {
5488 data.no_unlink = true;
5489 data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5490 self.data[data.id] = data;
5498 render_value: function () {
5500 this.$('.oe_add').css('visibility', this.get('effective_readonly') ? 'hidden': '');
5501 this.read_name_values().then(function (ids) {
5502 var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids}));
5503 render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5504 self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5506 // reinit input type file
5507 var $input = self.$('input.oe_form_binary_file');
5508 $input.after($input.clone(true)).remove();
5509 self.$(".oe_fileupload").show();
5513 on_file_change: function (event) {
5514 event.stopPropagation();
5516 var $target = $(event.target);
5517 if ($target.val() !== '') {
5518 var filename = $target.val().replace(/.*[\\\/]/,'');
5519 // don't uplode more of one file in same time
5520 if (self.data[0] && self.data[0].upload ) {
5523 for (var id in this.get('value')) {
5524 // if the files exits, delete the file before upload (if it's a new file)
5525 if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
5526 self.ds_file.unlink([id]);
5531 if(this.node.attrs.blockui>0) {
5532 instance.web.blockUI();
5535 // TODO : unactivate send on wizard and form
5538 this.$('form.oe_form_binary_form').submit();
5539 this.$(".oe_fileupload").hide();
5540 // add file on data result
5544 'filename': filename,
5550 on_file_loaded: function (event, result) {
5551 var files = this.get('value');
5554 if(this.node.attrs.blockui>0) {
5555 instance.web.unblockUI();
5558 if (result.error || !result.id ) {
5559 this.do_warn( _t('Uploading Error'), result.error);
5560 delete this.data[0];
5562 if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
5563 delete this.data[0];
5564 this.data[result.id] = {
5566 'name': result.name,
5567 'filename': result.filename,
5568 'url': this.get_file_url(result)
5571 this.data[result.id] = {
5573 'name': result.name,
5574 'filename': result.filename,
5575 'url': this.get_file_url(result)
5578 var values = _.clone(this.get('value'));
5579 values.push(result.id);
5580 this.set({'value': values});
5582 this.render_value();
5584 on_file_delete: function (event) {
5585 event.stopPropagation();
5586 var file_id=$(event.target).data("id");
5588 var files = _.filter(this.get('value'), function (id) {return id != file_id;});
5589 if(!this.data[file_id].no_unlink) {
5590 this.ds_file.unlink([file_id]);
5592 this.set({'value': files});
5597 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5598 template: "FieldStatus",
5599 init: function(field_manager, node) {
5600 this._super(field_manager, node);
5601 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5602 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5603 this.set({value: false});
5604 this.selection = {'unfolded': [], 'folded': []};
5605 this.set("selection", {'unfolded': [], 'folded': []});
5606 this.selection_dm = new instance.web.DropMisordered();
5607 this.dataset = new instance.web.DataSetStatic(this, this.field.relation, this.build_context());
5610 this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5612 this.on("change:value", this, this.get_selection);
5613 this.on("change:evaluated_selection_domain", this, this.get_selection);
5614 this.on("change:selection", this, function() {
5615 this.selection = this.get("selection");
5616 this.render_value();
5618 this.get_selection();
5619 if (this.options.clickable) {
5620 this.$el.on('click','li[data-id]',this.on_click_stage);
5622 if (this.$el.parent().is('header')) {
5623 this.$el.after('<div class="oe_clear"/>');
5627 set_value: function(value_) {
5628 if (value_ instanceof Array) {
5631 this._super(value_);
5633 render_value: function() {
5635 var content = QWeb.render("FieldStatus.content", {
5637 'value_folded': _.find(self.selection.folded, function(i){return i[0] === self.get('value');})
5639 self.$el.html(content);
5641 calc_domain: function() {
5642 var d = instance.web.pyeval.eval('domain', this.build_domain());
5643 var domain = []; //if there is no domain defined, fetch all the records
5646 domain = ['|',['id', '=', this.get('value')]].concat(d);
5649 if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5650 this.set("evaluated_selection_domain", domain);
5653 /** Get the selection and render it
5654 * selection: [[identifier, value_to_display], ...]
5655 * For selection fields: this is directly given by this.field.selection
5656 * For many2one fields: perform a search on the relation of the many2one field
5658 get_selection: function() {
5660 var selection_unfolded = [];
5661 var selection_folded = [];
5662 var fold_field = this.options.fold_field;
5664 var calculation = _.bind(function() {
5665 if (this.field.type == "many2one") {
5666 return self.get_distant_fields().then(function (fields) {
5667 return new instance.web.DataSetSearch(self, self.field.relation, self.build_context(), self.get("evaluated_selection_domain"))
5668 .read_slice(_.union(_.keys(self.distant_fields), ['id']), {}).then(function (records) {
5669 var ids = _.pluck(records, 'id');
5670 return self.dataset.name_get(ids).then(function (records_name) {
5671 _.each(records, function (record) {
5672 var name = _.find(records_name, function (val) {return val[0] == record.id;})[1];
5673 if (fold_field && record[fold_field] && record.id != self.get('value')) {
5674 selection_folded.push([record.id, name]);
5676 selection_unfolded.push([record.id, name]);
5683 // For field type selection filter values according to
5684 // statusbar_visible attribute of the field. For example:
5685 // statusbar_visible="draft,open".
5686 var select = this.field.selection;
5687 for(var i=0; i < select.length; i++) {
5688 var key = select[i][0];
5689 if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5690 selection_unfolded.push(select[i]);
5696 this.selection_dm.add(calculation()).then(function () {
5697 var selection = {'unfolded': selection_unfolded, 'folded': selection_folded};
5698 if (! _.isEqual(selection, self.get("selection"))) {
5699 self.set("selection", selection);
5704 * :deprecated: this feature will probably be removed with OpenERP v8
5706 get_distant_fields: function() {
5708 if (! this.options.fold_field) {
5709 this.distant_fields = {}
5711 if (this.distant_fields) {
5712 return $.when(this.distant_fields);
5714 return new instance.web.Model(self.field.relation).call("fields_get", [[this.options.fold_field]]).then(function(fields) {
5715 self.distant_fields = fields;
5719 on_click_stage: function (ev) {
5721 var $li = $(ev.currentTarget);
5723 if (this.field.type == "many2one") {
5724 val = parseInt($li.data("id"), 10);
5727 val = $li.data("id");
5729 if (val != self.get('value')) {
5730 this.view.recursive_save().done(function() {
5732 change[self.name] = val;
5733 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
5741 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
5742 template: "FieldMonetary",
5743 widget_class: 'oe_form_field_float oe_form_field_monetary',
5745 this._super.apply(this, arguments);
5746 this.set({"currency": false});
5747 if (this.options.currency_field) {
5748 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
5749 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
5752 this.on("change:currency", this, this.get_currency_info);
5753 this.get_currency_info();
5754 this.ci_dm = new instance.web.DropMisordered();
5757 var tmp = this._super();
5758 this.on("change:currency_info", this, this.reinitialize);
5761 get_currency_info: function() {
5763 if (this.get("currency") === false) {
5764 this.set({"currency_info": null});
5767 return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
5768 .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
5769 self.set({"currency_info": res});
5772 parse_value: function(val, def) {
5773 return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5775 format_value: function(val, def) {
5776 return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5781 This type of field display a list of checkboxes. It works only with m2ms. This field will display one checkbox for each
5782 record existing in the model targeted by the relation, according to the given domain if one is specified. Checked records
5783 will be added to the relation.
5785 instance.web.form.FieldMany2ManyCheckBoxes = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5786 className: "oe_form_many2many_checkboxes",
5788 this._super.apply(this, arguments);
5789 this.set("value", {});
5790 this.set("records", []);
5791 this.field_manager.on("view_content_has_changed", this, function() {
5792 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
5793 if (! _.isEqual(domain, this.get("domain"))) {
5794 this.set("domain", domain);
5797 this.records_orderer = new instance.web.DropMisordered();
5799 initialize_field: function() {
5800 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
5801 this.on("change:domain", this, this.query_records);
5802 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
5803 this.on("change:records", this, this.render_value);
5805 query_records: function() {
5807 var model = new openerp.Model(openerp.session, this.field.relation);
5808 this.records_orderer.add(model.call("search", [this.get("domain")], {"context": this.build_context()}).then(function(record_ids) {
5809 return model.call("name_get", [record_ids] , {"context": self.build_context()});
5810 })).then(function(res) {
5811 self.set("records", res);
5814 render_value: function() {
5815 this.$().html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this, selected: this.get("value")}));
5816 var inputs = this.$("input");
5817 inputs.change(_.bind(this.from_dom, this));
5818 if (this.get("effective_readonly"))
5819 inputs.attr("disabled", "true");
5821 from_dom: function() {
5823 this.$("input").each(function() {
5825 new_value[elem.data("record-id")] = elem.attr("checked") ? true : undefined;
5827 if (! _.isEqual(new_value, this.get("value")))
5828 this.internal_set_value(new_value);
5830 set_value: function(value) {
5831 value = value || [];
5832 if (value.length >= 1 && value[0] instanceof Array) {
5833 value = value[0][2];
5836 _.each(value, function(el) {
5837 formatted[JSON.stringify(el)] = true;
5839 this._super(formatted);
5841 get_value: function() {
5842 var value = _.filter(_.keys(this.get("value")), function(el) {
5843 return this.get("value")[el];
5845 value = _.map(value, function(el) {
5846 return JSON.parse(el);
5848 return [commands.replace_with(value)];
5853 This field can be applied on many2many and one2many. It is a read-only field that will display a single link whose name is
5854 "<number of linked records> <label of the field>". When the link is clicked, it will redirect to another act_window
5855 action on the model of the relation and show only the linked records.
5859 * 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
5860 to display (or False to take the default one) and the second element is the type of the view. Defaults to
5861 [[false, "tree"], [false, "form"]] .
5863 instance.web.form.X2ManyCounter = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5864 className: "oe_form_x2many_counter",
5866 this._super.apply(this, arguments);
5867 this.set("value", []);
5868 _.defaults(this.options, {
5869 "views": [[false, "tree"], [false, "form"]],
5872 render_value: function() {
5873 var text = _.str.sprintf("%d %s", this.val().length, this.string);
5874 this.$().html(QWeb.render("X2ManyCounter", {text: text}));
5875 this.$("a").click(_.bind(this.go_to, this));
5878 return this.view.recursive_save().then(_.bind(function() {
5879 var val = this.val();
5881 if (this.field.type === "one2many") {
5882 context["default_" + this.field.relation_field] = this.view.datarecord.id;
5884 var domain = [["id", "in", val]];
5885 return this.do_action({
5886 type: 'ir.actions.act_window',
5888 res_model: this.field.relation,
5889 views: this.options.views,
5897 var value = this.get("value") || [];
5898 if (value.length >= 1 && value[0] instanceof Array) {
5899 value = value[0][2];
5906 * Registry of form fields, called by :js:`instance.web.FormView`.
5908 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
5909 * will substitute to the <field> tags as defined in OpenERP's views.
5911 instance.web.form.widgets = new instance.web.Registry({
5912 'char' : 'instance.web.form.FieldChar',
5913 'id' : 'instance.web.form.FieldID',
5914 'email' : 'instance.web.form.FieldEmail',
5915 'url' : 'instance.web.form.FieldUrl',
5916 'text' : 'instance.web.form.FieldText',
5917 'html' : 'instance.web.form.FieldTextHtml',
5918 'date' : 'instance.web.form.FieldDate',
5919 'datetime' : 'instance.web.form.FieldDatetime',
5920 'selection' : 'instance.web.form.FieldSelection',
5921 'radio' : 'instance.web.form.FieldRadio',
5922 'many2one' : 'instance.web.form.FieldMany2One',
5923 'many2onebutton' : 'instance.web.form.Many2OneButton',
5924 'many2many' : 'instance.web.form.FieldMany2Many',
5925 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
5926 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
5927 'one2many' : 'instance.web.form.FieldOne2Many',
5928 'one2many_list' : 'instance.web.form.FieldOne2Many',
5929 'reference' : 'instance.web.form.FieldReference',
5930 'boolean' : 'instance.web.form.FieldBoolean',
5931 'float' : 'instance.web.form.FieldFloat',
5932 'integer': 'instance.web.form.FieldFloat',
5933 'float_time': 'instance.web.form.FieldFloat',
5934 'progressbar': 'instance.web.form.FieldProgressBar',
5935 'image': 'instance.web.form.FieldBinaryImage',
5936 'binary': 'instance.web.form.FieldBinaryFile',
5937 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
5938 'statusbar': 'instance.web.form.FieldStatus',
5939 'monetary': 'instance.web.form.FieldMonetary',
5940 'many2many_checkboxes': 'instance.web.form.FieldMany2ManyCheckBoxes',
5941 'x2many_counter': 'instance.web.form.X2ManyCounter',
5945 * Registry of widgets usable in the form view that can substitute to any possible
5946 * tags defined in OpenERP's form views.
5948 * Every referenced class should extend FormWidget.
5950 instance.web.form.tags = new instance.web.Registry({
5951 'button' : 'instance.web.form.WidgetButton',
5954 instance.web.form.custom_widgets = new instance.web.Registry({
5959 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: