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._onchange_specs = {};
106 this.default_focus_field = null;
107 this.default_focus_button = null;
108 this.fields_registry = instance.web.form.widgets;
109 this.tags_registry = instance.web.form.tags;
110 this.widgets_registry = instance.web.form.custom_widgets;
111 this.has_been_loaded = $.Deferred();
112 this.translatable_fields = [];
113 _.defaults(this.options, {
114 "not_interactible_on_create": false,
115 "initial_mode": "view",
116 "disable_autofocus": false,
117 "footer_to_buttons": false,
119 this.is_initialized = $.Deferred();
120 this.mutating_mutex = new $.Mutex();
122 this.reload_mutex = new $.Mutex();
123 this.__clicked_inside = false;
124 this.__blur_timeout = null;
125 this.rendering_engine = new instance.web.form.FormRenderingEngine(this);
126 self.set({actual_mode: self.options.initial_mode});
127 this.has_been_loaded.done(function() {
128 self._build_onchange_specs();
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() {
340 self.do_onchange(null);
342 self.on_form_changed();
343 self.rendering_engine.init_fields();
344 self.is_initialized.resolve();
345 self.do_update_pager(record.id === null || record.id === undefined);
347 self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
350 self.do_push_state({id:record.id});
352 self.do_push_state({});
354 self.$el.add(self.$buttons).removeClass('oe_form_dirty');
359 * Loads and sets up the default values for the model as the current
362 * @return {$.Deferred}
364 load_defaults: function () {
366 var keys = _.keys(this.fields_view.fields);
368 return this.dataset.default_get(keys).then(function(r) {
369 self.trigger('load_record', r);
372 return self.trigger('load_record', {});
374 on_form_changed: function() {
375 this.trigger("view_content_has_changed");
377 do_notify_change: function() {
378 this.$el.add(this.$buttons).addClass('oe_form_dirty');
380 execute_pager_action: function(action) {
381 if (this.can_be_discarded()) {
384 this.dataset.index = 0;
387 this.dataset.previous();
393 this.dataset.index = this.dataset.ids.length - 1;
396 var def = this.reload();
397 this.trigger('pager_action_executed');
402 init_pager: function() {
405 this.$pager.remove();
406 if (this.get("actual_mode") === "create")
408 this.$pager = $(QWeb.render("FormView.pager", {'widget':self})).hide();
409 if (this.options.$pager) {
410 this.$pager.appendTo(this.options.$pager);
412 this.$el.find('.oe_form_pager').replaceWith(this.$pager);
414 this.$pager.on('click','a[data-pager-action]',function() {
416 if ($el.attr("disabled"))
418 var action = $el.data('pager-action');
419 var def = $.when(self.execute_pager_action(action));
420 $el.attr("disabled");
421 def.always(function() {
422 $el.removeAttr("disabled");
425 this.do_update_pager();
427 do_update_pager: function(hide_index) {
428 this.$pager.toggle(this.dataset.ids.length > 1);
430 $(".oe_form_pager_state", this.$pager).html("");
432 $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
436 _build_onchange_specs: function() {
438 var find = function(field_name, root) {
440 while (fields.length) {
441 var node = fields.pop();
445 if (node.tag === 'field' && node.attrs.name === field_name) {
446 return node.attrs.on_change || "";
448 fields = _.union(fields, node.children);
453 self._onchange_specs = {};
454 _.each(this.fields, function(field, name) {
455 self._onchange_specs[name] = find(name, field.node);
456 _.each(field.field.views, function(view) {
457 _.each(view.fields, function(_, subname) {
458 self._onchange_specs[name + '.' + subname] = find(subname, view.arch);
463 _get_onchange_values: function() {
464 var field_values = this.get_fields_values();
465 if (field_values.id.toString().match(instance.web.BufferedDataSet.virtual_id_regex)) {
466 delete field_values.id;
468 if (this.dataset.parent_view) {
469 // this belongs to a parent view: add parent field if possible
470 var parent_view = this.dataset.parent_view;
471 var child_name = this.dataset.child_name;
472 var parent_name = parent_view.get_field_desc(child_name).relation_field;
474 // consider all fields except the inverse of the parent field
475 var parent_values = parent_view.get_fields_values();
476 delete parent_values[child_name];
477 field_values[parent_name] = parent_values;
483 do_onchange: function(widget) {
485 var onchange_specs = self._onchange_specs;
487 var def = $.when({});
488 var change_spec = widget ? onchange_specs[widget.name] : null;
489 if (!widget || (!_.isEmpty(change_spec) && change_spec !== "0")) {
491 trigger_field_name = widget ? widget.name : false,
492 values = self._get_onchange_values(),
493 context = new instance.web.CompoundContext(self.dataset.get_context());
495 if (widget && widget.build_context()) {
496 context.add(widget.build_context());
498 if (self.dataset.parent_view) {
499 var parent_name = self.dataset.parent_view.get_field_desc(self.dataset.child_name).relation_field;
500 context.add({field_parent: parent_name});
503 if (self.datarecord.id && !instance.web.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) {
504 // In case of a o2m virtual id, we should pass an empty ids list
505 ids.push(self.datarecord.id);
507 def = self.alive(new instance.web.Model(self.dataset.model).call(
508 "onchange", [ids, values, trigger_field_name, onchange_specs, context]));
510 return def.then(function(response) {
511 if (widget && widget.field['change_default']) {
512 var fieldname = widget.name;
514 if (response.value && (fieldname in response.value)) {
515 // Use value from onchange if onchange executed
516 value_ = response.value[fieldname];
518 // otherwise get form value for field
519 value_ = self.fields[fieldname].get_value();
521 var condition = fieldname + '=' + value_;
524 return self.alive(new instance.web.Model('ir.values').call(
525 'get_defaults', [self.model, condition]
526 )).then(function (results) {
527 if (!results.length) {
530 if (!response.value) {
533 for(var i=0; i<results.length; ++i) {
534 // [whatever, key, value]
535 var triplet = results[i];
536 response.value[triplet[1]] = triplet[2];
543 }).then(function(response) {
544 return self.on_processed_onchange(response);
548 instance.webclient.crashmanager.show_message(e);
549 return $.Deferred().reject();
552 on_processed_onchange: function(result) {
554 var fields = this.fields;
555 _(result.domain).each(function (domain, fieldname) {
556 var field = fields[fieldname];
557 if (!field) { return; }
558 field.node.attrs.domain = domain;
561 if (!_.isEmpty(result.value)) {
562 this._internal_set_values(result.value);
564 // FIXME XXX a list of warnings?
565 if (!_.isEmpty(result.warning)) {
566 new instance.web.Dialog(this, {
568 title:result.warning.title,
570 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
572 }, QWeb.render("CrashManager.warning", result.warning)).open();
575 return $.Deferred().resolve();
578 instance.webclient.crashmanager.show_message(e);
579 return $.Deferred().reject();
582 _process_operations: function() {
584 return this.mutating_mutex.exec(function() {
587 _.each(self.fields, function(field) {
588 defs.push(field.commit_value());
590 var args = _.toArray(arguments);
591 return $.when.apply($, defs).then(function() {
592 var save_obj = self.save_list.pop();
594 return self._process_save(save_obj).then(function() {
595 save_obj.ret = _.toArray(arguments);
598 save_obj.error = true;
603 self.save_list.pop();
610 _internal_set_values: function(values) {
611 for (var f in values) {
612 if (!values.hasOwnProperty(f)) { continue; }
613 var field = this.fields[f];
614 // If field is not defined in the view, just ignore it
616 var value_ = values[f];
617 if (field.get_value() != value_) {
618 field._inhibit_on_change_flag = true;
619 field.set_value(value_);
620 field._inhibit_on_change_flag = false;
621 field._dirty_flag = true;
625 this.on_form_changed();
627 set_values: function(values) {
629 return this.mutating_mutex.exec(function() {
630 self._internal_set_values(values);
634 * Ask the view to switch to view mode if possible. The view may not do it
635 * if the current record is not yet saved. It will then stay in create mode.
637 to_view_mode: function() {
638 this._actualize_mode("view");
641 * Ask the view to switch to edit mode if possible. The view may not do it
642 * if the current record is not yet saved. It will then stay in create mode.
644 to_edit_mode: function() {
645 this._actualize_mode("edit");
648 * Ask the view to switch to a precise mode if possible. The view is free to
649 * not respect this command if the state of the dataset is not compatible with
650 * the new mode. For example, it is not possible to switch to edit mode if
651 * the current record is not yet saved in database.
653 * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
654 * undefined the view will test the actual mode to check if it is still consistent
655 * with the dataset state.
657 _actualize_mode: function(switch_to) {
658 var mode = switch_to || this.get("actual_mode");
659 if (! this.datarecord.id) {
661 } else if (mode === "create") {
664 this.set({actual_mode: mode});
666 check_actual_mode: function(source, options) {
668 if(this.get("actual_mode") === "view") {
669 self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
670 self.$buttons.find('.oe_form_buttons_edit').hide();
671 self.$buttons.find('.oe_form_buttons_view').show();
672 self.$sidebar.show();
674 self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
675 self.$buttons.find('.oe_form_buttons_edit').show();
676 self.$buttons.find('.oe_form_buttons_view').hide();
677 self.$sidebar.hide();
681 autofocus: function() {
682 if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
683 var fields_order = this.fields_order.slice(0);
684 if (this.default_focus_field) {
685 fields_order.unshift(this.default_focus_field.name);
687 for (var i = 0; i < fields_order.length; i += 1) {
688 var field = this.fields[fields_order[i]];
689 if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
690 if (field.focus() !== false) {
697 on_button_save: function(e) {
699 $(e.target).attr("disabled", true);
700 return this.save().done(function(result) {
701 self.trigger("save", result);
702 self.reload().then(function() {
704 var menu = instance.webclient.menu;
706 menu.do_reload_needaction();
709 }).always(function(){
710 $(e.target).attr("disabled", false);
713 on_button_cancel: function(event) {
714 if (this.can_be_discarded()) {
715 if (this.get('actual_mode') === 'create') {
716 this.trigger('history_back');
719 this.trigger('load_record', this.datarecord);
722 this.trigger('on_button_cancel');
725 on_button_new: function() {
728 return $.when(this.has_been_loaded).then(function() {
729 if (self.can_be_discarded()) {
730 return self.load_defaults();
734 on_button_edit: function() {
735 return this.to_edit_mode();
737 on_button_create: function() {
738 this.dataset.index = null;
741 on_button_duplicate: function() {
743 return this.has_been_loaded.then(function() {
744 return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
745 self.record_created(new_id);
750 on_button_delete: function() {
752 var def = $.Deferred();
753 this.has_been_loaded.done(function() {
754 if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
755 self.dataset.unlink([self.datarecord.id]).done(function() {
756 if (self.dataset.size()) {
757 self.execute_pager_action('next');
759 self.do_action('history_back');
764 $.async_when().done(function () {
769 return def.promise();
771 can_be_discarded: function() {
772 if (this.$el.is('.oe_form_dirty')) {
773 if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
776 this.$el.removeClass('oe_form_dirty');
781 * Triggers saving the form's record. Chooses between creating a new
782 * record or saving an existing one depending on whether the record
783 * already has an id property.
785 * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
786 * record, should that record be inserted at the start of the dataset (by
787 * default, records are added at the end)
789 save: function(prepend_on_create) {
791 var save_obj = {prepend_on_create: prepend_on_create, ret: null};
792 this.save_list.push(save_obj);
793 return this._process_operations().then(function() {
795 return $.Deferred().reject();
796 return $.when.apply($, save_obj.ret);
798 self.$el.removeClass('oe_form_dirty');
801 _process_save: function(save_obj) {
803 var prepend_on_create = save_obj.prepend_on_create;
805 var form_invalid = false,
807 first_invalid_field = null,
808 readonly_values = {};
809 for (var f in self.fields) {
810 if (!self.fields.hasOwnProperty(f)) { continue; }
814 if (!first_invalid_field) {
815 first_invalid_field = f;
817 } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
818 // Special case 'id' field, do not save this field
819 // on 'create' : save all non readonly fields
820 // on 'edit' : save non readonly modified fields
821 if (!f.get("readonly")) {
822 values[f.name] = f.get_value();
824 readonly_values[f.name] = f.get_value();
829 self.set({'display_invalid_fields': true});
830 first_invalid_field.focus();
832 return $.Deferred().reject();
834 self.set({'display_invalid_fields': false});
836 if (!self.datarecord.id) {
838 save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
839 return self.record_created(r, prepend_on_create);
841 } else if (_.isEmpty(values)) {
842 // Not dirty, noop save
843 save_deferral = $.Deferred().resolve({}).promise();
846 save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
847 return self.record_saved(r);
850 return save_deferral;
854 return $.Deferred().reject();
857 on_invalid: function() {
858 var warnings = _(this.fields).chain()
859 .filter(function (f) { return !f.is_valid(); })
861 return _.str.sprintf('<li>%s</li>',
864 warnings.unshift('<ul>');
865 warnings.push('</ul>');
866 this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
869 * Reload the form after saving
871 * @param {Object} r result of the write function.
873 record_saved: function(r) {
874 this.trigger('record_saved', r);
876 // should not happen in the server, but may happen for internal purpose
877 return $.Deferred().reject();
882 * Updates the form' dataset to contain the new record:
884 * * Adds the newly created record to the current dataset (at the end by
886 * * Selects that record (sets the dataset's index to point to the new
888 * * Updates the pager and sidebar displays
891 * @param {Boolean} [prepend_on_create=false] adds the newly created record
892 * at the beginning of the dataset instead of the end
894 record_created: function(r, prepend_on_create) {
897 // should not happen in the server, but may happen for internal purpose
898 this.trigger('record_created', r);
899 return $.Deferred().reject();
901 this.datarecord.id = r;
902 if (!prepend_on_create) {
903 this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
904 this.dataset.index = this.dataset.ids.length - 1;
906 this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
907 this.dataset.index = 0;
909 this.do_update_pager();
911 this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
913 //openerp.log("The record has been created with id #" + this.datarecord.id);
914 return $.when(this.reload()).then(function () {
915 self.trigger('record_created', r);
916 return _.extend(r, {created: true});
920 on_action: function (action) {
921 console.debug('Executing action', action);
925 return this.reload_mutex.exec(function() {
926 if (self.dataset.index === null || self.dataset.index === undefined) {
927 self.trigger("previous_view");
928 return $.Deferred().reject().promise();
930 if (self.dataset.index < 0) {
931 return $.when(self.on_button_new());
933 var fields = _.keys(self.fields_view.fields);
934 fields.push('display_name');
935 return self.dataset.read_index(fields,
939 'future_display_name': true
941 check_access_rule: true
942 }).then(function(r) {
943 self.trigger('load_record', r);
945 self.do_action('history_back');
950 get_widgets: function() {
951 return _.filter(this.getChildren(), function(obj) {
952 return obj instanceof instance.web.form.FormWidget;
955 get_fields_values: function() {
957 var ids = this.get_selected_ids();
958 values["id"] = ids.length > 0 ? ids[0] : false;
959 _.each(this.fields, function(value_, key) {
960 values[key] = value_.get_value();
964 get_selected_ids: function() {
965 var id = this.dataset.ids[this.dataset.index];
966 return id ? [id] : [];
968 recursive_save: function() {
970 return $.when(this.save()).then(function(res) {
971 if (self.dataset.parent_view)
972 return self.dataset.parent_view.recursive_save();
975 recursive_reload: function() {
978 if (self.dataset.parent_view)
979 pre = self.dataset.parent_view.recursive_reload();
980 return pre.then(function() {
981 return self.reload();
984 is_dirty: function() {
985 return _.any(this.fields, function (value_) {
986 return value_._dirty_flag;
989 is_interactible_record: function() {
990 var id = this.datarecord.id;
992 if (this.options.not_interactible_on_create)
994 } else if (typeof(id) === "string") {
995 if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
1000 sidebar_eval_context: function () {
1001 return $.when(this.build_eval_context());
1003 open_defaults_dialog: function () {
1005 var display = function (field, value) {
1006 if (!value) { return value; }
1007 if (field instanceof instance.web.form.FieldSelection) {
1008 return _(field.get('values')).find(function (option) {
1009 return option[0] === value;
1011 } else if (field instanceof instance.web.form.FieldMany2One) {
1012 return field.get_displayed();
1016 var fields = _.chain(this.fields)
1017 .map(function (field) {
1018 var value = field.get_value();
1019 // ignore fields which are empty, invisible, readonly, o2m
1022 || field.get('invisible')
1023 || field.get("readonly")
1024 || field.field.type === 'one2many'
1025 || field.field.type === 'many2many'
1026 || field.field.type === 'binary'
1027 || field.password) {
1033 string: field.string,
1035 displayed: display(field, value),
1039 .sortBy(function (field) { return field.string; })
1041 var conditions = _.chain(self.fields)
1042 .filter(function (field) { return field.field.change_default; })
1043 .map(function (field) {
1044 var value = field.get_value();
1047 string: field.string,
1049 displayed: display(field, value),
1053 var d = new instance.web.Dialog(this, {
1054 title: _t("Set Default"),
1057 conditions: conditions
1060 {text: _t("Close"), click: function () { d.close(); }},
1061 {text: _t("Save default"), click: function () {
1062 var $defaults = d.$el.find('#formview_default_fields');
1063 var field_to_set = $defaults.val();
1064 if (!field_to_set) {
1065 $defaults.parent().addClass('oe_form_invalid');
1068 var condition = d.$el.find('#formview_default_conditions').val(),
1069 all_users = d.$el.find('#formview_default_all').is(':checked');
1070 new instance.web.DataSet(self, 'ir.values').call(
1074 self.fields[field_to_set].get_value(),
1078 ]).done(function () { d.close(); });
1082 d.template = 'FormView.set_default';
1085 register_field: function(field, name) {
1086 this.fields[name] = field;
1087 this.fields_order.push(name);
1088 if (JSON.parse(field.node.attrs.default_focus || "0")) {
1089 this.default_focus_field = field;
1092 field.on('focused', null, this.proxy('widgetFocused'))
1093 .on('blurred', null, this.proxy('widgetBlurred'));
1094 if (this.get_field_desc(name).translate) {
1095 this.translatable_fields.push(field);
1097 field.on('changed_value', this, function() {
1098 if (field.is_syntax_valid()) {
1099 this.trigger('field_changed:' + name);
1101 if (field._inhibit_on_change_flag) {
1104 field._dirty_flag = true;
1105 if (field.is_syntax_valid()) {
1106 this.do_onchange(field);
1107 this.on_form_changed(true);
1108 this.do_notify_change();
1112 get_field_desc: function(field_name) {
1113 return this.fields_view.fields[field_name];
1115 get_field_value: function(field_name) {
1116 return this.fields[field_name].get_value();
1118 compute_domain: function(expression) {
1119 return instance.web.form.compute_domain(expression, this.fields);
1121 _build_view_fields_values: function() {
1122 var a_dataset = this.dataset;
1123 var fields_values = this.get_fields_values();
1124 var active_id = a_dataset.ids[a_dataset.index];
1125 _.extend(fields_values, {
1126 active_id: active_id || false,
1127 active_ids: active_id ? [active_id] : [],
1128 active_model: a_dataset.model,
1131 if (a_dataset.parent_view) {
1132 fields_values.parent = a_dataset.parent_view.get_fields_values();
1134 return fields_values;
1136 build_eval_context: function() {
1137 var a_dataset = this.dataset;
1138 return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
1143 * Interface to be implemented by rendering engines for the form view.
1145 instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
1146 set_fields_view: function(fields_view) {},
1147 set_fields_registry: function(fields_registry) {},
1148 render_to: function($el) {},
1152 * Default rendering engine for the form view.
1154 * It is necessary to set the view using set_view() before usage.
1156 instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
1157 init: function(view) {
1160 set_fields_view: function(fvg) {
1162 this.version = parseFloat(this.fvg.arch.attrs.version);
1163 if (isNaN(this.version)) {
1167 set_tags_registry: function(tags_registry) {
1168 this.tags_registry = tags_registry;
1170 set_fields_registry: function(fields_registry) {
1171 this.fields_registry = fields_registry;
1173 set_widgets_registry: function(widgets_registry) {
1174 this.widgets_registry = widgets_registry;
1176 // Backward compatibility tools, current default version: v7
1177 process_version: function() {
1178 if (this.version < 7.0) {
1179 this.$form.find('form:first').wrapInner('<group col="4"/>');
1180 this.$form.find('page').each(function() {
1181 if (!$(this).parents('field').length) {
1182 $(this).wrapInner('<group col="4"/>');
1187 get_arch_fragment: function() {
1188 var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
1189 // IE won't allow custom button@type and will revert it to spec default : 'submit'
1190 $('button', doc).each(function() {
1191 $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
1193 // IE's html parser is also a css parser. How convenient...
1194 $('board', doc).each(function() {
1195 $(this).attr('layout', $(this).attr('style'));
1197 return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
1199 render_to: function($target) {
1201 this.$target = $target;
1203 this.$form = this.get_arch_fragment();
1205 this.process_version();
1207 this.fields_to_init = [];
1208 this.tags_to_init = [];
1209 this.widgets_to_init = [];
1211 this.process(this.$form);
1213 this.$form.appendTo(this.$target);
1215 this.to_replace = [];
1217 _.each(this.fields_to_init, function($elem) {
1218 var name = $elem.attr("name");
1219 if (!self.fvg.fields[name]) {
1220 throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
1222 var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
1224 throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
1226 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1227 var $label = self.labels[$elem.attr("name")];
1229 w.set_input_id($label.attr("for"));
1231 self.alter_field(w);
1232 self.view.register_field(w, $elem.attr("name"));
1233 self.to_replace.push([w, $elem]);
1235 _.each(this.tags_to_init, function($elem) {
1236 var tag_name = $elem[0].tagName.toLowerCase();
1237 var obj = self.tags_registry.get_object(tag_name);
1238 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1239 self.to_replace.push([w, $elem]);
1241 _.each(this.widgets_to_init, function($elem) {
1242 var widget_type = $elem.attr("type");
1243 var obj = self.widgets_registry.get_object(widget_type);
1244 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1245 self.to_replace.push([w, $elem]);
1248 init_fields: function() {
1250 _.each(this.to_replace, function(el) {
1251 defs.push(el[0].replace(el[1]));
1252 if (el[1].children().length) {
1253 el[0].$el.append(el[1].children());
1256 this.to_replace = [];
1257 return $.when.apply($, defs);
1259 render_element: function(template /* dictionaries */) {
1260 var dicts = [].slice.call(arguments).slice(1);
1261 var dict = _.extend.apply(_, dicts);
1262 dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
1263 return $(QWeb.render(template, dict));
1265 alter_field: function(field) {
1267 toggle_layout_debugging: function() {
1268 if (!this.$target.has('.oe_layout_debug_cell:first').length) {
1269 this.$target.find('[title]').removeAttr('title');
1270 this.$target.find('.oe_form_group_cell').each(function() {
1271 var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
1272 $(this).attr('title', text);
1275 this.$target.toggleClass('oe_layout_debugging');
1277 process: function($tag) {
1279 var tagname = $tag[0].nodeName.toLowerCase();
1280 if (this.tags_registry.contains(tagname)) {
1281 this.tags_to_init.push($tag);
1282 return (tagname === 'button') ? this.process_button($tag) : $tag;
1284 var fn = self['process_' + tagname];
1286 var args = [].slice.call(arguments);
1288 return fn.apply(self, args);
1290 // generic tag handling, just process children
1291 $tag.children().each(function() {
1292 self.process($(this));
1294 self.handle_common_properties($tag, $tag);
1295 $tag.removeAttr("modifiers");
1299 process_button: function ($button) {
1301 $button.children().each(function() {
1302 self.process($(this));
1306 process_widget: function($widget) {
1307 this.widgets_to_init.push($widget);
1310 process_sheet: function($sheet) {
1311 var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
1312 this.handle_common_properties($new_sheet, $sheet);
1313 var $dst = $new_sheet.find('.oe_form_sheet');
1314 $sheet.contents().appendTo($dst);
1315 $sheet.before($new_sheet).remove();
1316 this.process($new_sheet);
1318 process_form: function($form) {
1319 if ($form.find('> sheet').length === 0) {
1320 $form.addClass('oe_form_nosheet');
1322 var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
1323 this.handle_common_properties($new_form, $form);
1324 $form.contents().appendTo($new_form);
1325 if ($form[0] === this.$form[0]) {
1326 // If root element, replace it
1327 this.$form = $new_form;
1329 $form.before($new_form).remove();
1331 this.process($new_form);
1334 * Used by direct <field> children of a <group> tag only
1335 * This method will add the implicit <label...> for every field
1338 preprocess_field: function($field) {
1340 var name = $field.attr('name'),
1341 field_colspan = parseInt($field.attr('colspan'), 10),
1342 field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
1344 if ($field.attr('nolabel') === '1')
1346 $field.attr('nolabel', '1');
1348 this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
1349 $(el).parents().each(function(unused, tag) {
1350 var name = tag.tagName.toLowerCase();
1351 if (name === "field" || name in self.tags_registry.map)
1358 var $label = $('<label/>').attr({
1360 "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
1361 "string": $field.attr('string'),
1362 "help": $field.attr('help'),
1363 "class": $field.attr('class'),
1365 $label.insertBefore($field);
1366 if (field_colspan > 1) {
1367 $field.attr('colspan', field_colspan - 1);
1371 process_field: function($field) {
1372 if ($field.parent().is('group')) {
1373 // No implicit labels for normal fields, only for <group> direct children
1374 var $label = this.preprocess_field($field);
1376 this.process($label);
1379 this.fields_to_init.push($field);
1382 process_group: function($group) {
1384 $group.children('field').each(function() {
1385 self.preprocess_field($(this));
1387 var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
1389 if ($new_group.first().is('table.oe_form_group')) {
1390 $table = $new_group;
1391 } else if ($new_group.filter('table.oe_form_group').length) {
1392 $table = $new_group.filter('table.oe_form_group').first();
1394 $table = $new_group.find('table.oe_form_group').first();
1398 cols = parseInt($group.attr('col') || 2, 10),
1402 $group.children().each(function(a,b,c) {
1403 var $child = $(this);
1404 var colspan = parseInt($child.attr('colspan') || 1, 10);
1405 var tagName = $child[0].tagName.toLowerCase();
1406 var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
1407 var newline = tagName === 'newline';
1409 // Note FME: those classes are used in layout debug mode
1410 if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
1411 $tr.addClass('oe_form_group_row_incomplete');
1413 $tr.addClass('oe_form_group_row_newline');
1420 if (!$tr || row_cols < colspan) {
1421 $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
1423 } else if (tagName==='group') {
1424 // When <group> <group/><group/> </group>, we need a spacing between the two groups
1425 $td.addClass('oe_group_right');
1427 row_cols -= colspan;
1429 // invisibility transfer
1430 var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
1431 var invisible = field_modifiers.invisible;
1432 self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
1434 $tr.append($td.append($child));
1435 children.push($child[0]);
1437 if (row_cols && $td) {
1438 $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
1440 $group.before($new_group).remove();
1442 $table.find('> tbody > tr').each(function() {
1443 var to_compute = [],
1446 $(this).children().each(function() {
1448 $child = $td.children(':first');
1449 if ($child.attr('cell-class')) {
1450 $td.addClass($child.attr('cell-class'));
1452 switch ($child[0].tagName.toLowerCase()) {
1456 if ($child.attr('for')) {
1457 $td.attr('width', '1%').addClass('oe_form_group_cell_label');
1458 row_cols-= $td.attr('colspan') || 1;
1463 var width = _.str.trim($child.attr('width') || ''),
1464 iwidth = parseInt(width, 10);
1466 if (width.substr(-1) === '%') {
1468 width = iwidth + '%';
1471 $td.css('min-width', width + 'px');
1473 $td.attr('width', width);
1474 $child.removeAttr('width');
1475 row_cols-= $td.attr('colspan') || 1;
1477 to_compute.push($td);
1483 var unit = Math.floor(total / row_cols);
1484 if (!$(this).is('.oe_form_group_row_incomplete')) {
1485 _.each(to_compute, function($td, i) {
1486 var width = parseInt($td.attr('colspan'), 10) * unit;
1487 $td.attr('width', width + '%');
1493 _.each(children, function(el) {
1494 self.process($(el));
1496 this.handle_common_properties($new_group, $group);
1499 process_notebook: function($notebook) {
1502 $notebook.find('> page').each(function() {
1503 var $page = $(this);
1504 var page_attrs = $page.getAttributes();
1505 page_attrs.id = _.uniqueId('notebook_page_');
1506 var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
1507 $page.contents().appendTo($new_page);
1508 $page.before($new_page).remove();
1509 var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
1510 page_attrs.__page = $new_page;
1511 page_attrs.__ic = ic;
1512 pages.push(page_attrs);
1514 $new_page.children().each(function() {
1515 self.process($(this));
1518 var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
1519 $notebook.contents().appendTo($new_notebook);
1520 $notebook.before($new_notebook).remove();
1521 self.process($($new_notebook.children()[0]));
1522 //tabs and invisibility handling
1523 $new_notebook.tabs();
1524 _.each(pages, function(page, i) {
1527 page.__ic.on("change:effective_invisible", null, function() {
1528 if (!page.__ic.get('effective_invisible') && page.autofocus) {
1529 $new_notebook.tabs('select', i);
1532 var current = $new_notebook.tabs("option", "selected");
1533 if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
1535 var first_visible = _.find(_.range(pages.length), function(i2) {
1536 return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
1538 if (first_visible !== undefined) {
1539 $new_notebook.tabs('select', first_visible);
1544 this.handle_common_properties($new_notebook, $notebook);
1545 return $new_notebook;
1547 process_separator: function($separator) {
1548 var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
1549 $separator.before($new_separator).remove();
1550 this.handle_common_properties($new_separator, $separator);
1551 return $new_separator;
1553 process_label: function($label) {
1554 var name = $label.attr("for"),
1555 field_orm = this.fvg.fields[name];
1557 string: $label.attr('string') || (field_orm || {}).string || '',
1558 help: $label.attr('help') || (field_orm || {}).help || '',
1559 _for: name ? _.uniqueId('oe-field-input-') : undefined,
1561 var align = parseFloat(dict.align);
1562 if (isNaN(align) || align === 1) {
1564 } else if (align === 0) {
1570 var $new_label = this.render_element('FormRenderingLabel', dict);
1571 $label.before($new_label).remove();
1572 this.handle_common_properties($new_label, $label);
1574 this.labels[name] = $new_label;
1578 handle_common_properties: function($new_element, $node) {
1579 var str_modifiers = $node.attr("modifiers") || "{}";
1580 var modifiers = JSON.parse(str_modifiers);
1582 if (modifiers.invisible !== undefined)
1583 ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
1584 $new_element.addClass($node.attr("class") || "");
1585 $new_element.attr('style', $node.attr('style'));
1586 return {invisibility_changer: ic,};
1593 If you read this documentation, it probably means that you were asked to use a form view widget outside of
1594 a form view. Before going further, you must understand that those fields were never really created for
1595 that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
1596 you to hack the system with more style.
1598 instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
1599 init: function(parent, eval_context) {
1600 this._super(parent);
1601 this.field_descs = {};
1602 this.eval_context = eval_context || {};
1604 display_invalid_fields: false,
1605 actual_mode: 'create',
1608 get_field_desc: function(field_name) {
1609 if (this.field_descs[field_name] === undefined) {
1610 this.field_descs[field_name] = {
1614 return this.field_descs[field_name];
1616 extend_field_desc: function(fields) {
1618 _.each(fields, function(v, k) {
1619 _.extend(self.get_field_desc(k), v);
1622 get_field_value: function(field_name) {
1625 set_values: function(values) {
1628 compute_domain: function(expression) {
1629 return instance.web.form.compute_domain(expression, {});
1631 build_eval_context: function() {
1632 return new instance.web.CompoundContext(this.eval_context);
1636 instance.web.form.compute_domain = function(expr, fields) {
1637 if (! (expr instanceof Array))
1640 for (var i = expr.length - 1; i >= 0; i--) {
1642 if (ex.length == 1) {
1643 var top = stack.pop();
1646 stack.push(stack.pop() || top);
1649 stack.push(stack.pop() && top);
1655 throw new Error(_.str.sprintf(
1656 _t("Unknown operator %s in domain %s"),
1657 ex, JSON.stringify(expr)));
1661 var field = fields[ex[0]];
1663 throw new Error(_.str.sprintf(
1664 _t("Unknown field %s in domain %s"),
1665 ex[0], JSON.stringify(expr)));
1667 var field_value = field.get_value ? field.get_value() : field.value;
1671 switch (op.toLowerCase()) {
1674 stack.push(_.isEqual(field_value, val));
1678 stack.push(!_.isEqual(field_value, val));
1681 stack.push(field_value < val);
1684 stack.push(field_value > val);
1687 stack.push(field_value <= val);
1690 stack.push(field_value >= val);
1693 if (!_.isArray(val)) val = [val];
1694 stack.push(_(val).contains(field_value));
1697 if (!_.isArray(val)) val = [val];
1698 stack.push(!_(val).contains(field_value));
1702 _t("Unsupported operator %s in domain %s"),
1703 op, JSON.stringify(expr));
1706 return _.all(stack, _.identity);
1709 instance.web.form.is_bin_size = function(v) {
1710 return (/^\d+(\.\d*)? \w+$/).test(v);
1714 * Must be applied over an class already possessing the PropertiesMixin.
1716 * Apply the result of the "invisible" domain to this.$el.
1718 instance.web.form.InvisibilityChangerMixin = {
1719 init: function(field_manager, invisible_domain) {
1721 this._ic_field_manager = field_manager;
1722 this._ic_invisible_modifier = invisible_domain;
1723 this._ic_field_manager.on("view_content_has_changed", this, function() {
1724 var result = self._ic_invisible_modifier === undefined ? false :
1725 self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
1726 self.set({"invisible": result});
1728 this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
1729 var check = function() {
1730 if (self.get("invisible") || self.get('force_invisible')) {
1731 self.set({"effective_invisible": true});
1733 self.set({"effective_invisible": false});
1736 this.on('change:invisible', this, check);
1737 this.on('change:force_invisible', this, check);
1741 this.on("change:effective_invisible", this, this._check_visibility);
1742 this._check_visibility();
1744 _check_visibility: function() {
1745 this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
1749 instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
1750 init: function(parent, field_manager, invisible_domain, $el) {
1751 this.setParent(parent);
1752 instance.web.PropertiesMixin.init.call(this);
1753 instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
1760 Base class for all fields, custom widgets and buttons to be displayed in the form view.
1763 - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
1764 the values of the "readonly" property and the "mode" property on the field manager.
1766 instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
1768 * @constructs instance.web.form.FormWidget
1769 * @extends instance.web.Widget
1771 * @param field_manager
1774 init: function(field_manager, node) {
1775 this._super(field_manager);
1776 this.field_manager = field_manager;
1777 if (this.field_manager instanceof instance.web.FormView)
1778 this.view = this.field_manager;
1780 this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
1781 instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
1783 this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
1789 // some events to make the property "effective_readonly" sync automatically with "readonly" and
1790 // "mode" on field_manager
1792 var test_effective_readonly = function() {
1793 self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
1795 this.on("change:readonly", this, test_effective_readonly);
1796 this.field_manager.on("change:actual_mode", this, test_effective_readonly);
1797 test_effective_readonly.call(this);
1799 renderElement: function() {
1800 this.process_modifiers();
1802 this.$el.addClass(this.node.attrs["class"] || "");
1804 destroy: function() {
1805 $.fn.tooltip('destroy');
1806 this._super.apply(this, arguments);
1809 * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
1811 * This method is an utility method that is meant to be called by child classes.
1813 * @param {jQuery} $e jQuery object of elements to bind focus/blur on
1815 setupFocus: function ($e) {
1818 focus: function () { self.trigger('focused'); },
1819 blur: function () { self.trigger('blurred'); }
1822 process_modifiers: function() {
1824 for (var a in this.modifiers) {
1825 if (!this.modifiers.hasOwnProperty(a)) { continue; }
1826 if (!_.include(["invisible"], a)) {
1827 var val = this.field_manager.compute_domain(this.modifiers[a]);
1833 do_attach_tooltip: function(widget, trigger, options) {
1834 widget = widget || this;
1835 trigger = trigger || this.$el;
1836 options = _.extend({
1837 delay: { show: 500, hide: 0 },
1839 var template = widget.template + '.tooltip';
1840 if (!QWeb.has_template(template)) {
1841 template = 'WidgetLabel.tooltip';
1843 return QWeb.render(template, {
1844 debug: instance.session.debug,
1849 //only show tooltip if we are in debug or if we have a help to show, otherwise it will display
1851 if (instance.session.debug || widget.node.attrs.help || (widget.field && widget.field.help)){
1852 $(trigger).tooltip(options);
1856 * Builds a new context usable for operations related to fields by merging
1857 * the fields'context with the action's context.
1859 build_context: function() {
1860 // only use the model's context if there is not context on the node
1861 var v_context = this.node.attrs.context;
1863 v_context = (this.field || {}).context || {};
1866 if (v_context.__ref || true) { //TODO: remove true
1867 var fields_values = this.field_manager.build_eval_context();
1868 v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
1872 build_domain: function() {
1873 var f_domain = this.field.domain || [];
1874 var n_domain = this.node.attrs.domain || null;
1875 // if there is a domain on the node, overrides the model's domain
1876 var final_domain = n_domain !== null ? n_domain : f_domain;
1877 if (!(final_domain instanceof Array) || true) { //TODO: remove true
1878 var fields_values = this.field_manager.build_eval_context();
1879 final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
1881 return final_domain;
1885 instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
1886 template: 'WidgetButton',
1887 init: function(field_manager, node) {
1888 node.attrs.type = node.attrs['data-button-type'];
1889 this.is_stat_button = /\boe_stat_button\b/.test(node.attrs['class']);
1890 this.icon_class = node.attrs.icon && "stat_button_icon fa " + node.attrs.icon + " fa-fw";
1891 this._super(field_manager, node);
1892 this.force_disabled = false;
1893 this.string = (this.node.attrs.string || '').replace(/_/g, '');
1894 if (JSON.parse(this.node.attrs.default_focus || "0")) {
1895 // TODO fme: provide enter key binding to widgets
1896 this.view.default_focus_button = this;
1898 if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
1899 this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
1903 this._super.apply(this, arguments);
1904 this.view.on('view_content_has_changed', this, this.check_disable);
1905 this.check_disable();
1906 this.$el.click(this.on_click);
1907 if (this.node.attrs.help || instance.session.debug) {
1908 this.do_attach_tooltip();
1910 this.setupFocus(this.$el);
1912 on_click: function() {
1914 this.force_disabled = true;
1915 this.check_disable();
1916 this.execute_action().always(function() {
1917 self.force_disabled = false;
1918 self.check_disable();
1921 execute_action: function() {
1923 var exec_action = function() {
1924 if (self.node.attrs.confirm) {
1925 var def = $.Deferred();
1926 var dialog = new instance.web.Dialog(this, {
1927 title: _t('Confirm'),
1929 {text: _t("Cancel"), click: function() {
1930 this.parents('.modal').modal('hide');
1933 {text: _t("Ok"), click: function() {
1935 self.on_confirmed().always(function() {
1936 self2.parents('.modal').modal('hide');
1941 }, $('<div/>').text(self.node.attrs.confirm)).open();
1942 dialog.on("closing", null, function() {def.resolve();});
1943 return def.promise();
1945 return self.on_confirmed();
1948 if (!this.node.attrs.special) {
1949 return this.view.recursive_save().then(exec_action);
1951 return exec_action();
1954 on_confirmed: function() {
1957 var context = this.build_context();
1958 return this.view.do_execute_action(
1959 _.extend({}, this.node.attrs, {context: context}),
1960 this.view.dataset, this.view.datarecord.id, function (reason) {
1961 if (!_.isObject(reason)) {
1962 self.view.recursive_reload();
1966 check_disable: function() {
1967 var disabled = (this.force_disabled || !this.view.is_interactible_record());
1968 this.$el.prop('disabled', disabled);
1969 this.$el.css('color', disabled ? 'grey' : '');
1974 * Interface to be implemented by fields.
1977 * - changed_value: triggered when the value of the field has changed. This can be due
1978 * to a user interaction or a call to set_value().
1981 instance.web.form.FieldInterface = {
1983 * Constructor takes 2 arguments:
1984 * - field_manager: Implements FieldManagerMixin
1985 * - node: the "<field>" node in json form
1987 init: function(field_manager, node) {},
1989 * Called by the form view to indicate the value of the field.
1991 * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
1992 * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
1993 * before the widget is inserted into the DOM.
1995 * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
1996 * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
1997 * also be able to handle any other format commonly used in the _defaults key on the models in the addons
1998 * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
1999 * no information is ever given to know which format is used.
2001 set_value: function(value_) {},
2003 * Get the current value of the widget.
2005 * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
2006 * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
2007 * For example if the field is marked as "required", a call to get_value() can return false.
2009 * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
2010 * return a default value according to the type of field.
2012 * This method is always assumed to perform synchronously, it can not return a promise.
2014 * If there was no user interaction to modify the value of the field, it is always assumed that
2015 * get_value() return the same semantic value than the one passed in the last call to set_value(),
2016 * although the syntax can be different. This can be the case for type of fields that have a different
2017 * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
2019 get_value: function() {},
2021 * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
2024 set_input_id: function(id) {},
2026 * Returns true if is_syntax_valid() returns true and the value is semantically
2027 * valid too according to the semantic restrictions applied to the field.
2029 is_valid: function() {},
2031 * Returns true if the field holds a value which is syntactically correct, ignoring
2032 * the potential semantic restrictions applied to the field.
2034 is_syntax_valid: function() {},
2036 * Must set the focus on the field. Return false if field is not focusable.
2038 focus: function() {},
2040 * Called when the translate button is clicked.
2042 on_translate: function() {},
2044 This method is called by the form view before reading on_change values and before saving. It tells
2045 the field to save its value before reading it using get_value(). Must return a promise.
2047 commit_value: function() {},
2051 * Abstract class for classes implementing FieldInterface.
2054 * - value: useful property to hold the value of the field. By default, set_value() and get_value()
2055 * set and retrieve the value property. Changing the value property also triggers automatically
2056 * a 'changed_value' event that inform the view to trigger on_changes.
2059 instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
2061 * @constructs instance.web.form.AbstractField
2062 * @extends instance.web.form.FormWidget
2064 * @param field_manager
2067 init: function(field_manager, node) {
2069 this._super(field_manager, node);
2070 this.name = this.node.attrs.name;
2071 this.field = this.field_manager.get_field_desc(this.name);
2072 this.widget = this.node.attrs.widget;
2073 this.string = this.node.attrs.string || this.field.string || this.name;
2074 this.options = instance.web.py_eval(this.node.attrs.options || '{}');
2075 this.set({'value': false});
2077 this.on("change:value", this, function() {
2078 this.trigger('changed_value');
2079 this._check_css_flags();
2082 renderElement: function() {
2085 if (this.field.translate && this.view) {
2086 this.$el.addClass('oe_form_field_translatable');
2087 this.$el.find('.oe_field_translate').click(this.on_translate);
2089 this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
2090 this.do_attach_tooltip(this, this.$label[0] || this.$el);
2091 if (instance.session.debug) {
2092 this.$label.off('dblclick').on('dblclick', function() {
2093 console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
2095 console.log("window.w =", window.w);
2098 if (!this.disable_utility_classes) {
2099 this.off("change:required", this, this._set_required);
2100 this.on("change:required", this, this._set_required);
2101 this._set_required();
2103 this._check_visibility();
2104 this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
2105 this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
2106 this._check_css_flags();
2109 var tmp = this._super();
2110 this.on("change:value", this, function() {
2111 if (! this.no_rerender)
2112 this.render_value();
2114 this.render_value();
2117 * Private. Do not use.
2119 _set_required: function() {
2120 this.$el.toggleClass('oe_form_required', this.get("required"));
2122 set_value: function(value_) {
2123 this.set({'value': value_});
2125 get_value: function() {
2126 return this.get('value');
2129 Utility method that all implementations should use to change the
2130 value without triggering a re-rendering.
2132 internal_set_value: function(value_) {
2133 var tmp = this.no_rerender;
2134 this.no_rerender = true;
2135 this.set({'value': value_});
2136 this.no_rerender = tmp;
2139 This method is called each time the value is modified.
2141 render_value: function() {},
2142 is_valid: function() {
2143 return this.is_syntax_valid() && !(this.get('required') && this.is_false());
2145 is_syntax_valid: function() {
2149 * Method useful to implement to ease validity testing. Must return true if the current
2150 * value is similar to false in OpenERP.
2152 is_false: function() {
2153 return this.get('value') === false;
2155 _check_css_flags: function() {
2156 if (this.field.translate) {
2157 this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
2159 if (!this.disable_utility_classes) {
2160 if (this.field_manager.get('display_invalid_fields')) {
2161 this.$el.toggleClass('oe_form_invalid', !this.is_valid());
2168 set_input_id: function(id) {
2169 this.id_for_label = id;
2171 on_translate: function() {
2173 var trans = new instance.web.DataSet(this, 'ir.translation');
2174 return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
2179 set_dimensions: function (height, width) {
2185 commit_value: function() {
2191 * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
2194 instance.web.form.ReinitializeWidgetMixin = {
2196 * Default implementation of, you should not override it, use initialize_field() instead.
2199 this.initialize_field();
2202 initialize_field: function() {
2203 this.on("change:effective_readonly", this, this.reinitialize);
2204 this.initialize_content();
2206 reinitialize: function() {
2207 this.destroy_content();
2208 this.renderElement();
2209 this.initialize_content();
2212 * Called to destroy anything that could have been created previously, called before a
2213 * re-initialization.
2215 destroy_content: function() {},
2217 * Called to initialize the content.
2219 initialize_content: function() {},
2223 * A mixin to apply on any field that has to completely re-render when its readonly state
2226 instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
2227 reinitialize: function() {
2228 instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
2229 this.render_value();
2234 Some hack to make placeholders work in ie9.
2236 if (!('placeholder' in document.createElement('input'))) {
2237 document.addEventListener("DOMNodeInserted",function(event){
2238 var nodename = event.target.nodeName.toLowerCase();
2239 if ( nodename === "input" || nodename == "textarea" ) {
2240 $(event.target).placeholder();
2245 instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2246 template: 'FieldChar',
2247 widget_class: 'oe_form_field_char',
2249 'change input': 'store_dom_value',
2251 init: function (field_manager, node) {
2252 this._super(field_manager, node);
2253 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2255 initialize_content: function() {
2256 this.setupFocus(this.$('input'));
2258 store_dom_value: function () {
2259 if (!this.get('effective_readonly')
2260 && this.$('input').length
2261 && this.is_syntax_valid()) {
2262 this.internal_set_value(
2264 this.$('input').val()));
2267 commit_value: function () {
2268 this.store_dom_value();
2269 return this._super();
2271 render_value: function() {
2272 var show_value = this.format_value(this.get('value'), '');
2273 if (!this.get("effective_readonly")) {
2274 this.$el.find('input').val(show_value);
2276 if (this.password) {
2277 show_value = new Array(show_value.length + 1).join('*');
2279 this.$(".oe_form_char_content").text(show_value);
2282 is_syntax_valid: function() {
2283 if (!this.get("effective_readonly") && this.$("input").size() > 0) {
2285 this.parse_value(this.$('input').val(), '');
2293 parse_value: function(val, def) {
2294 return instance.web.parse_value(val, this, def);
2296 format_value: function(val, def) {
2297 return instance.web.format_value(val, this, def);
2299 is_false: function() {
2300 return this.get('value') === '' || this._super();
2303 var input = this.$('input:first')[0];
2304 return input ? input.focus() : false;
2306 set_dimensions: function (height, width) {
2307 this._super(height, width);
2308 this.$('input').css({
2315 instance.web.form.KanbanSelection = instance.web.form.FieldChar.extend({
2316 init: function (field_manager, node) {
2317 this._super(field_manager, node);
2319 prepare_dropdown_selection: function() {
2322 var selection = self.field.selection || [];
2323 _.map(selection, function(res) {
2327 'state_name': res[1],
2329 if (res[0] == 'normal') { value['state_class'] = 'oe_kanban_status'; }
2330 else if (res[0] == 'done') { value['state_class'] = 'oe_kanban_status oe_kanban_status_green'; }
2331 else { value['state_class'] = 'oe_kanban_status oe_kanban_status_red'; }
2336 render_value: function() {
2338 this.record_id = this.view.datarecord.id;
2339 this.states = this.prepare_dropdown_selection();;
2340 this.$el.html(QWeb.render("KanbanSelection", {'widget': self}));
2341 this.$el.find('li').on('click', this.set_kanban_selection.bind(this));
2343 /* setting the value: in view mode, perform an asynchronous call and reload
2344 the form view; in edit mode, use set_value to save the new value that will
2345 be written when saving the record. */
2346 set_kanban_selection: function (ev) {
2348 var li = $(ev.target).closest('li');
2350 var value = String(li.data('value'));
2351 if (this.view.get('actual_mode') == 'view') {
2352 var write_values = {}
2353 write_values[self.name] = value;
2354 return this.view.dataset._model.call(
2358 self.view.dataset.get_context()
2359 ]).done(self.reload_record.bind(self));
2362 return this.set_value(value);
2366 reload_record: function() {
2371 instance.web.form.Priority = instance.web.form.FieldChar.extend({
2372 init: function (field_manager, node) {
2373 this._super(field_manager, node);
2375 prepare_priority: function() {
2377 var selection = this.field.selection || [];
2378 var init_value = selection && selection[0][0] || 0;
2379 var data = _.map(selection.slice(1), function(element, index) {
2381 'value': element[0],
2383 'click_value': element[0],
2385 if (index == 0 && self.get('value') == element[0]) {
2386 value['click_value'] = init_value;
2392 render_value: function() {
2394 this.record_id = this.view.datarecord.id;
2395 this.priorities = this.prepare_priority();
2396 this.$el.html(QWeb.render("Priority", {'widget': this}));
2397 this.$el.find('li').on('click', this.set_priority.bind(this));
2399 /* setting the value: in view mode, perform an asynchronous call and reload
2400 the form view; in edit mode, use set_value to save the new value that will
2401 be written when saving the record. */
2402 set_priority: function (ev) {
2404 var li = $(ev.target).closest('li');
2406 var value = String(li.data('value'));
2407 if (this.view.get('actual_mode') == 'view') {
2408 var write_values = {}
2409 write_values[self.name] = value;
2410 return this.view.dataset._model.call(
2414 self.view.dataset.get_context()
2415 ]).done(self.reload_record.bind(self));
2418 return this.set_value(value);
2423 reload_record: function() {
2428 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2429 process_modifiers: function () {
2431 this.set({ readonly: true });
2435 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2436 template: 'FieldEmail',
2437 initialize_content: function() {
2439 var $button = this.$el.find('button');
2440 $button.click(this.on_button_clicked);
2441 this.setupFocus($button);
2443 render_value: function() {
2444 if (!this.get("effective_readonly")) {
2448 .attr('href', 'mailto:' + this.get('value'))
2449 .text(this.get('value') || '');
2452 on_button_clicked: function() {
2453 if (!this.get('value') || !this.is_syntax_valid()) {
2454 this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2456 location.href = 'mailto:' + this.get('value');
2461 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2462 template: 'FieldUrl',
2463 initialize_content: function() {
2465 var $button = this.$el.find('button');
2466 $button.click(this.on_button_clicked);
2467 this.setupFocus($button);
2469 render_value: function() {
2470 if (!this.get("effective_readonly")) {
2473 var tmp = this.get('value');
2474 var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2476 tmp = "http://" + this.get('value');
2478 var text = this.get('value') ? this.node.attrs.text || tmp : '';
2479 this.$el.find('a').attr('href', tmp).text(text);
2482 on_button_clicked: function() {
2483 if (!this.get('value')) {
2484 this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2486 var url = $.trim(this.get('value'));
2487 if(/^www\./i.test(url))
2488 url = 'http://'+url;
2494 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2495 is_field_number: true,
2496 widget_class: 'oe_form_field_float',
2497 init: function (field_manager, node) {
2498 this._super(field_manager, node);
2499 this.internal_set_value(0);
2500 if (this.node.attrs.digits) {
2501 this.digits = this.node.attrs.digits;
2503 this.digits = this.field.digits;
2506 set_value: function(value_) {
2507 if (value_ === false || value_ === undefined) {
2508 // As in GTK client, floats default to 0
2511 this._super.apply(this, [value_]);
2513 focus: function () {
2514 var $input = this.$('input:first');
2515 return $input.length ? $input.select() : false;
2519 instance.web.form.FieldCharDomain = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2520 init: function(field_manager, node) {
2521 this._super.apply(this, arguments);
2525 this._super.apply(this, arguments);
2526 this.on("change:effective_readonly", this, function () {
2527 this.display_field();
2529 this.display_field();
2530 return this._super();
2532 set_value: function(value_) {
2534 this.set('value', value_ || false);
2535 this.display_field();
2537 display_field: function() {
2539 this.$el.html(instance.web.qweb.render("FieldCharDomain", {widget: this}));
2540 if (this.get('value')) {
2541 var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2542 var domain = instance.web.pyeval.eval('domain', this.get('value'));
2543 var ds = new instance.web.DataSetStatic(self, model, self.build_context());
2544 ds.call('search_count', [domain]).then(function (results) {
2545 $('.oe_domain_count', self.$el).text(results + ' records selected');
2546 if (self.get('effective_readonly')) {
2547 $('button span', self.$el).text(' See selection');
2550 $('button span', self.$el).text(' Change selection');
2554 $('.oe_domain_count', this.$el).text('0 record selected');
2555 $('button span', this.$el).text(' Select records');
2557 this.$('.select_records').on('click', self.on_click);
2559 on_click: function(event) {
2560 event.preventDefault();
2562 var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2563 this.pop = new instance.web.form.SelectCreatePopup(this);
2564 this.pop.select_element(
2566 title: this.get('effective_readonly') ? 'Selected records' : 'Select records...',
2567 readonly: this.get('effective_readonly'),
2568 disable_multiple_selection: this.get('effective_readonly'),
2569 no_create: this.get('effective_readonly'),
2570 }, [], this.build_context());
2571 this.pop.on("elements_selected", self, function(element_ids) {
2572 if (this.pop.$('input.oe_list_record_selector').prop('checked')) {
2573 var search_data = this.pop.searchview.build_search_data();
2574 var domain_done = instance.web.pyeval.eval_domains_and_contexts({
2575 domains: search_data.domains,
2576 contexts: search_data.contexts,
2577 group_by_seq: search_data.groupbys || []
2578 }).then(function (results) {
2579 return results.domain;
2583 var domain = [["id", "in", element_ids]];
2584 var domain_done = $.Deferred().resolve(domain);
2586 $.when(domain_done).then(function (domain) {
2587 var domain = self.pop.dataset.domain.concat(domain || []);
2588 self.set_value(domain);
2594 instance.web.DateTimeWidget = instance.web.Widget.extend({
2595 template: "web.datepicker",
2596 jqueryui_object: 'datetimepicker',
2597 type_of_date: "datetime",
2599 'change .oe_datepicker_master': 'change_datetime',
2600 'keypress .oe_datepicker_master': 'change_datetime',
2602 init: function(parent) {
2603 this._super(parent);
2604 this.name = parent.name;
2608 this.$input = this.$el.find('input.oe_datepicker_master');
2609 this.$input_picker = this.$el.find('input.oe_datepicker_container');
2611 $.datepicker.setDefaults({
2612 clearText: _t('Clear'),
2613 clearStatus: _t('Erase the current date'),
2614 closeText: _t('Done'),
2615 closeStatus: _t('Close without change'),
2616 prevText: _t('<Prev'),
2617 prevStatus: _t('Show the previous month'),
2618 nextText: _t('Next>'),
2619 nextStatus: _t('Show the next month'),
2620 currentText: _t('Today'),
2621 currentStatus: _t('Show the current month'),
2622 monthNames: Date.CultureInfo.monthNames,
2623 monthNamesShort: Date.CultureInfo.abbreviatedMonthNames,
2624 monthStatus: _t('Show a different month'),
2625 yearStatus: _t('Show a different year'),
2626 weekHeader: _t('Wk'),
2627 weekStatus: _t('Week of the year'),
2628 dayNames: Date.CultureInfo.dayNames,
2629 dayNamesShort: Date.CultureInfo.abbreviatedDayNames,
2630 dayNamesMin: Date.CultureInfo.shortestDayNames,
2631 dayStatus: _t('Set DD as first week day'),
2632 dateStatus: _t('Select D, M d'),
2633 firstDay: Date.CultureInfo.firstDayOfWeek,
2634 initStatus: _t('Select a date'),
2637 $.timepicker.setDefaults({
2638 timeOnlyTitle: _t('Choose Time'),
2639 timeText: _t('Time'),
2640 hourText: _t('Hour'),
2641 minuteText: _t('Minute'),
2642 secondText: _t('Second'),
2643 currentText: _t('Now'),
2644 closeText: _t('Done')
2648 onClose: this.on_picker_select,
2649 onSelect: this.on_picker_select,
2653 showButtonPanel: true,
2654 firstDay: Date.CultureInfo.firstDayOfWeek
2656 // Some clicks in the datepicker dialog are not stopped by the
2657 // datepicker and "bubble through", unexpectedly triggering the bus's
2658 // click event. Prevent that.
2659 this.picker('widget').click(function (e) { e.stopPropagation(); });
2661 this.$el.find('img.oe_datepicker_trigger').click(function() {
2662 if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
2663 self.$input.focus();
2666 self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date());
2667 self.$input_picker.show();
2668 self.picker('show');
2669 self.$input_picker.hide();
2671 this.set_readonly(false);
2672 this.set({'value': false});
2674 picker: function() {
2675 return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
2677 on_picker_select: function(text, instance_) {
2678 var date = this.picker('getDate');
2680 .val(date ? this.format_client(date) : '')
2684 set_value: function(value_) {
2685 this.set({'value': value_});
2686 this.$input.val(value_ ? this.format_client(value_) : '');
2688 get_value: function() {
2689 return this.get('value');
2691 set_value_from_ui_: function() {
2692 var value_ = this.$input.val() || false;
2693 this.set({'value': this.parse_client(value_)});
2695 set_readonly: function(readonly) {
2696 this.readonly = readonly;
2697 this.$input.prop('readonly', this.readonly);
2698 this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
2700 is_valid_: function() {
2701 var value_ = this.$input.val();
2702 if (value_ === "") {
2706 this.parse_client(value_);
2713 parse_client: function(v) {
2714 return instance.web.parse_value(v, {"widget": this.type_of_date});
2716 format_client: function(v) {
2717 return instance.web.format_value(v, {"widget": this.type_of_date});
2719 change_datetime: function(e) {
2720 if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
2721 this.set_value_from_ui_();
2722 this.trigger("datetime_changed");
2725 commit_value: function () {
2726 this.change_datetime();
2730 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2731 jqueryui_object: 'datepicker',
2732 type_of_date: "date"
2735 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2736 template: "FieldDatetime",
2737 build_widget: function() {
2738 return new instance.web.DateTimeWidget(this);
2740 destroy_content: function() {
2741 if (this.datewidget) {
2742 this.datewidget.destroy();
2743 this.datewidget = undefined;
2746 initialize_content: function() {
2747 if (!this.get("effective_readonly")) {
2748 this.datewidget = this.build_widget();
2749 this.datewidget.on('datetime_changed', this, _.bind(function() {
2750 this.internal_set_value(this.datewidget.get_value());
2752 this.datewidget.appendTo(this.$el);
2753 this.setupFocus(this.datewidget.$input);
2756 render_value: function() {
2757 if (!this.get("effective_readonly")) {
2758 this.datewidget.set_value(this.get('value'));
2760 this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2763 is_syntax_valid: function() {
2764 if (!this.get("effective_readonly") && this.datewidget) {
2765 return this.datewidget.is_valid_();
2769 is_false: function() {
2770 return this.get('value') === '' || this._super();
2773 var input = this.datewidget && this.datewidget.$input[0];
2774 return input ? input.focus() : false;
2776 set_dimensions: function (height, width) {
2777 this._super(height, width);
2778 if (!this.get("effective_readonly")) {
2779 this.datewidget.$input.css('height', height);
2784 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2785 template: "FieldDate",
2786 build_widget: function() {
2787 return new instance.web.DateWidget(this);
2791 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2792 template: 'FieldText',
2794 'keyup': function (e) {
2795 if (e.which === $.ui.keyCode.ENTER) {
2796 e.stopPropagation();
2799 'keypress': function (e) {
2800 if (e.which === $.ui.keyCode.ENTER) {
2801 e.stopPropagation();
2804 'change textarea': 'store_dom_value',
2806 initialize_content: function() {
2808 if (! this.get("effective_readonly")) {
2809 this.$textarea = this.$el.find('textarea');
2810 this.auto_sized = false;
2811 this.default_height = this.$textarea.css('height');
2812 if (this.get("effective_readonly")) {
2813 this.$textarea.attr('disabled', 'disabled');
2815 this.setupFocus(this.$textarea);
2817 this.$textarea = undefined;
2820 commit_value: function () {
2821 if (! this.get("effective_readonly") && this.$textarea) {
2822 this.store_dom_value();
2824 return this._super();
2826 store_dom_value: function () {
2827 this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2829 render_value: function() {
2830 if (! this.get("effective_readonly")) {
2831 var show_value = instance.web.format_value(this.get('value'), this, '');
2832 if (show_value === '') {
2833 this.$textarea.css('height', parseInt(this.default_height, 10)+"px");
2835 this.$textarea.val(show_value);
2836 if (! this.auto_sized) {
2837 this.auto_sized = true;
2838 this.$textarea.autosize();
2840 this.$textarea.trigger("autosize");
2843 var txt = this.get("value") || '';
2844 this.$(".oe_form_text_content").text(txt);
2847 is_syntax_valid: function() {
2848 if (!this.get("effective_readonly") && this.$textarea) {
2850 instance.web.parse_value(this.$textarea.val(), this, '');
2858 is_false: function() {
2859 return this.get('value') === '' || this._super();
2861 focus: function($el) {
2862 var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2863 return input ? input.focus() : false;
2865 set_dimensions: function (height, width) {
2866 this._super(height, width);
2867 if (!this.get("effective_readonly") && this.$textarea) {
2868 this.$textarea.css({
2877 * FieldTextHtml Widget
2878 * Intended for FieldText widgets meant to display HTML content. This
2879 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2880 * To find more information about CLEditor configutation: go to
2881 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2883 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2884 template: 'FieldTextHtml',
2886 this._super.apply(this, arguments);
2888 initialize_content: function() {
2890 if (! this.get("effective_readonly")) {
2891 self._updating_editor = false;
2892 this.$textarea = this.$el.find('textarea');
2893 var width = ((this.node.attrs || {}).editor_width || '100%');
2894 var height = ((this.node.attrs || {}).editor_height || 250);
2895 this.$textarea.cleditor({
2896 width: width, // width not including margins, borders or padding
2897 height: height, // height not including margins, borders or padding
2898 controls: // controls to add to the toolbar
2899 "bold italic underline strikethrough " +
2900 "| removeformat | bullets numbering | outdent " +
2901 "indent | link unlink | source",
2902 bodyStyle: // style to assign to document body contained within the editor
2903 "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2905 this.$cleditor = this.$textarea.cleditor()[0];
2906 this.$cleditor.change(function() {
2907 if (! self._updating_editor) {
2908 self.$cleditor.updateTextArea();
2909 self.internal_set_value(self.$textarea.val());
2912 if (this.field.translate) {
2913 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"/>')
2914 .click(this.on_translate);
2915 this.$cleditor.$toolbar.append($img);
2919 render_value: function() {
2920 if (! this.get("effective_readonly")) {
2921 this.$textarea.val(this.get('value') || '');
2922 this._updating_editor = true;
2923 this.$cleditor.updateFrame();
2924 this._updating_editor = false;
2926 this.$el.html(this.get('value'));
2931 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2932 template: 'FieldBoolean',
2935 this.$checkbox = $("input", this.$el);
2936 this.setupFocus(this.$checkbox);
2937 this.$el.click(_.bind(function() {
2938 this.internal_set_value(this.$checkbox.is(':checked'));
2940 var check_readonly = function() {
2941 self.$checkbox.prop('disabled', self.get("effective_readonly"));
2942 self.click_disabled_boolean();
2944 this.on("change:effective_readonly", this, check_readonly);
2945 check_readonly.call(this);
2946 this._super.apply(this, arguments);
2948 render_value: function() {
2949 this.$checkbox[0].checked = this.get('value');
2952 var input = this.$checkbox && this.$checkbox[0];
2953 return input ? input.focus() : false;
2955 click_disabled_boolean: function(){
2956 var $disabled = this.$el.find('input[type=checkbox]:disabled');
2957 $disabled.each(function (){
2958 $(this).next('div').remove();
2959 $(this).closest("span").append($('<div class="boolean"></div>'));
2965 The progressbar field expect a float from 0 to 100.
2967 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2968 template: 'FieldProgressBar',
2969 render_value: function() {
2970 this.$el.progressbar({
2971 value: this.get('value') || 0,
2972 disabled: this.get("effective_readonly")
2974 var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
2975 this.$('span').html(formatted_value + '%');
2980 The PercentPie field expect a float from 0 to 100.
2982 instance.web.form.FieldPercentPie = instance.web.form.AbstractField.extend({
2983 template: 'FieldPercentPie',
2985 render_value: function() {
2986 var value = this.get('value'),
2987 formatted_value = Math.round(value || 0) + '%',
2988 svg = this.$('svg')[0];
2991 nv.addGraph(function() {
2992 var width = 42, height = 42;
2993 var chart = nv.models.pieChart()
2996 .margin({top: 0, right: 0, bottom: 0, left: 0})
3001 .color(['#7C7BAD','#DDD'])
3005 .datum([{'x': 'value', 'y': value}, {'x': 'complement', 'y': 100 - value}])
3008 .attr('style', 'width: ' + width + 'px; height:' + height + 'px;');
3012 .attr({x: width/2, y: height/2 + 3, 'text-anchor': 'middle'})
3013 .style({"font-size": "10px", "font-weight": "bold"})
3014 .text(formatted_value);
3023 The FieldBarChart expectsa list of values (indeed)
3025 instance.web.form.FieldBarChart = instance.web.form.AbstractField.extend({
3026 template: 'FieldBarChart',
3028 render_value: function() {
3029 var value = JSON.parse(this.get('value'));
3030 var svg = this.$('svg')[0];
3032 nv.addGraph(function() {
3033 var width = 34, height = 34;
3034 var chart = nv.models.discreteBarChart()
3035 .x(function (d) { return d.tooltip })
3036 .y(function (d) { return d.value })
3039 .margin({top: 0, right: 0, bottom: 0, left: 0})
3042 .transitionDuration(350)
3047 .datum([{key: 'values', values: value}])
3050 .attr('style', 'width: ' + (width + 4) + 'px; height: ' + (height + 8) + 'px;');
3052 nv.utils.windowResize(chart.update);
3061 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3062 template: 'FieldSelection',
3064 'change select': 'store_dom_value',
3066 init: function(field_manager, node) {
3068 this._super(field_manager, node);
3069 this.set("value", false);
3070 this.set("values", []);
3071 this.records_orderer = new instance.web.DropMisordered();
3072 this.field_manager.on("view_content_has_changed", this, function() {
3073 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
3074 if (! _.isEqual(domain, this.get("domain"))) {
3075 this.set("domain", domain);
3079 initialize_field: function() {
3080 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3081 this.on("change:domain", this, this.query_values);
3082 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
3083 this.on("change:values", this, this.render_value);
3085 query_values: function() {
3088 if (this.field.type === "many2one") {
3089 var model = new openerp.Model(openerp.session, this.field.relation);
3090 def = model.call("name_search", ['', this.get("domain")], {"context": this.build_context()});
3092 var values = _.reject(this.field.selection, function (v) { return v[0] === false && v[1] === ''; });
3093 def = $.when(values);
3095 this.records_orderer.add(def).then(function(values) {
3096 if (! _.isEqual(values, self.get("values"))) {
3097 self.set("values", values);
3101 initialize_content: function() {
3102 // Flag indicating whether we're in an event chain containing a change
3103 // event on the select, in order to know what to do on keyup[RETURN]:
3104 // * If the user presses [RETURN] as part of changing the value of a
3105 // selection, we should just let the value change and not let the
3106 // event broadcast further (e.g. to validating the current state of
3107 // the form in editable list view, which would lead to saving the
3108 // current row or switching to the next one)
3109 // * If the user presses [RETURN] with a select closed (side-effect:
3110 // also if the user opened the select and pressed [RETURN] without
3111 // changing the selected value), takes the action as validating the
3113 var ischanging = false;
3114 var $select = this.$el.find('select')
3115 .change(function () { ischanging = true; })
3116 .click(function () { ischanging = false; })
3117 .keyup(function (e) {
3118 if (e.which !== 13 || !ischanging) { return; }
3119 e.stopPropagation();
3122 this.setupFocus($select);
3124 commit_value: function () {
3125 this.store_dom_value();
3126 return this._super();
3128 store_dom_value: function () {
3129 if (!this.get('effective_readonly') && this.$('select').length) {
3130 var val = JSON.parse(this.$('select').val());
3131 this.internal_set_value(val);
3134 set_value: function(value_) {
3135 value_ = value_ === null ? false : value_;
3136 value_ = value_ instanceof Array ? value_[0] : value_;
3137 this._super(value_);
3139 render_value: function() {
3140 var values = this.get("values");
3141 values = [[false, this.node.attrs.placeholder || '']].concat(values);
3142 var found = _.find(values, function(el) { return el[0] === this.get("value"); }, this);
3144 found = [this.get("value"), _t('Unknown')];
3145 values = [found].concat(values);
3147 if (! this.get("effective_readonly")) {
3148 this.$().html(QWeb.render("FieldSelectionSelect", {widget: this, values: values}));
3149 this.$("select").val(JSON.stringify(found[0]));
3151 this.$el.text(found[1]);
3155 var input = this.$('select:first')[0];
3156 return input ? input.focus() : false;
3158 set_dimensions: function (height, width) {
3159 this._super(height, width);
3160 this.$('select').css({
3167 instance.web.form.FieldRadio = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3168 template: 'FieldRadio',
3170 'click input': 'click_change_value'
3172 init: function(field_manager, node) {
3173 /* Radio button widget: Attributes options:
3174 * - "horizontal" to display in column
3175 * - "no_radiolabel" don't display text values
3177 this._super(field_manager, node);
3178 this.selection = _.clone(this.field.selection) || [];
3179 this.domain = false;
3181 initialize_content: function () {
3182 this.uniqueId = _.uniqueId("radio");
3183 this.on("change:effective_readonly", this, this.render_value);
3184 this.field_manager.on("view_content_has_changed", this, this.get_selection);
3185 this.get_selection();
3187 click_change_value: function (event) {
3188 var val = $(event.target).val();
3189 val = this.field.type == "selection" ? val : +val;
3190 if (val == this.get_value()) {
3191 this.set_value(false);
3193 this.set_value(val);
3196 /** Get the selection and render it
3197 * selection: [[identifier, value_to_display], ...]
3198 * For selection fields: this is directly given by this.field.selection
3199 * For many2one fields: perform a search on the relation of the many2one field
3201 get_selection: function() {
3204 var def = $.Deferred();
3205 if (self.field.type == "many2one") {
3206 var domain = instance.web.pyeval.eval('domain', this.build_domain()) || [];
3207 if (! _.isEqual(self.domain, domain)) {
3208 self.domain = domain;
3209 var ds = new instance.web.DataSetStatic(self, self.field.relation, self.build_context());
3210 ds.call('search', [self.domain])
3211 .then(function (records) {
3212 ds.name_get(records).then(function (records) {
3213 selection = records;
3218 selection = self.selection;
3222 else if (self.field.type == "selection") {
3223 selection = self.field.selection || [];
3226 return def.then(function () {
3227 if (! _.isEqual(selection, self.selection)) {
3228 self.selection = _.clone(selection);
3229 self.renderElement();
3230 self.render_value();
3234 set_value: function (value_) {
3236 if (this.field.type == "selection") {
3237 value_ = _.find(this.field.selection, function (sel) { return sel[0] == value_;});
3239 else if (!this.selection.length) {
3240 this.selection = [value_];
3243 this._super(value_);
3245 get_value: function () {
3246 var value = this.get('value');
3247 return value instanceof Array ? value[0] : value;
3249 render_value: function () {
3251 this.$el.toggleClass("oe_readonly", this.get('effective_readonly'));
3252 this.$("input:checked").prop("checked", false);
3253 if (this.get_value()) {
3254 this.$("input").filter(function () {return this.value == self.get_value();}).prop("checked", true);
3255 this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : "");
3260 // jquery autocomplete tweak to allow html and classnames
3262 var proto = $.ui.autocomplete.prototype,
3263 initSource = proto._initSource;
3265 function filter( array, term ) {
3266 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
3267 return $.grep( array, function(value_) {
3268 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
3273 _initSource: function() {
3274 if ( this.options.html && $.isArray(this.options.source) ) {
3275 this.source = function( request, response ) {
3276 response( filter( this.options.source, request.term ) );
3279 initSource.call( this );
3283 _renderItem: function( ul, item) {
3284 return $( "<li></li>" )
3285 .data( "item.autocomplete", item )
3286 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
3288 .addClass(item.classname);
3294 A mixin containing some useful methods to handle completion inputs.
3296 The widget containing this option can have these arguments in its widget options:
3297 - no_quick_create: if true, it will disable the quick create
3299 instance.web.form.CompletionFieldMixin = {
3302 this.orderer = new instance.web.DropMisordered();
3305 * Call this method to search using a string.
3307 get_search_result: function(search_val) {
3310 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
3311 this.last_query = search_val;
3312 var exclusion_domain = [], ids_blacklist = this.get_search_blacklist();
3313 if (!_(ids_blacklist).isEmpty()) {
3314 exclusion_domain.push(['id', 'not in', ids_blacklist]);
3317 return this.orderer.add(dataset.name_search(
3318 search_val, new instance.web.CompoundDomain(self.build_domain(), exclusion_domain),
3319 'ilike', this.limit + 1, self.build_context())).then(function(data) {
3320 self.last_search = data;
3321 // possible selections for the m2o
3322 var values = _.map(data, function(x) {
3323 x[1] = x[1].split("\n")[0];
3325 label: _.str.escapeHTML(x[1]),
3332 // search more... if more results that max
3333 if (values.length > self.limit) {
3334 values = values.slice(0, self.limit);
3336 label: _t("Search More..."),
3337 action: function() {
3338 dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
3339 self._search_create_popup("search", data);
3342 classname: 'oe_m2o_dropdown_option'
3346 var raw_result = _(data.result).map(function(x) {return x[1];});
3347 if (search_val.length > 0 && !_.include(raw_result, search_val) &&
3348 ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3350 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
3351 $('<span />').text(search_val).html()),
3352 action: function() {
3353 self._quick_create(search_val);
3355 classname: 'oe_m2o_dropdown_option'
3359 if (!(self.options && self.options.no_create)){
3361 label: _t("Create and Edit..."),
3362 action: function() {
3363 self._search_create_popup("form", undefined, self._create_context(search_val));
3365 classname: 'oe_m2o_dropdown_option'
3368 else if (values.length == 0)
3370 label: _t("No results to show..."),
3371 action: function() {},
3372 classname: 'oe_m2o_dropdown_option'
3378 get_search_blacklist: function() {
3381 _quick_create: function(name) {
3383 var slow_create = function () {
3384 self._search_create_popup("form", undefined, self._create_context(name));
3386 if (self.options.quick_create === undefined || self.options.quick_create) {
3387 new instance.web.DataSet(this, this.field.relation, self.build_context())
3388 .name_create(name).done(function(data) {
3389 if (!self.get('effective_readonly'))
3390 self.add_id(data[0]);
3391 }).fail(function(error, event) {
3392 event.preventDefault();
3398 // all search/create popup handling
3399 _search_create_popup: function(view, ids, context) {
3401 var pop = new instance.web.form.SelectCreatePopup(this);
3403 self.field.relation,
3405 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3406 initial_ids: ids ? _.map(ids, function(x) {return x[0];}) : undefined,
3408 disable_multiple_selection: true
3410 self.build_domain(),
3411 new instance.web.CompoundContext(self.build_context(), context || {})
3413 pop.on("elements_selected", self, function(element_ids) {
3414 self.add_id(element_ids[0]);
3421 add_id: function(id) {},
3422 _create_context: function(name) {
3424 var field = (this.options || {}).create_name_field;
3425 if (field === undefined)
3427 if (field !== false && name && (this.options || {}).quick_create !== false)
3428 tmp["default_" + field] = name;
3433 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3434 template: "M2ODialog",
3435 init: function(parent) {
3436 this.name = parent.string;
3437 this._super(parent, {
3438 title: _.str.sprintf(_t("Create a %s"), parent.string),
3444 var text = _.str.sprintf(_t("You are creating a new %s, are you sure it does not exist yet?"), self.name);
3445 this.$("p").text( text );
3446 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3447 this.$("input").val(this.getParent().last_query);
3448 this.$buttons.find(".oe_form_m2o_qc_button").click(function(){
3449 self.getParent()._quick_create(self.$("input").val());
3452 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3453 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3456 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3462 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3463 template: "FieldMany2One",
3465 'keydown input': function (e) {
3467 case $.ui.keyCode.UP:
3468 case $.ui.keyCode.DOWN:
3469 e.stopPropagation();
3473 init: function(field_manager, node) {
3474 this._super(field_manager, node);
3475 instance.web.form.CompletionFieldMixin.init.call(this);
3476 this.set({'value': false});
3477 this.display_value = {};
3478 this.display_value_backup = {};
3479 this.last_search = [];
3480 this.floating = false;
3481 this.current_display = null;
3482 this.is_started = false;
3483 this.ignore_focusout = false;
3485 reinit_value: function(val) {
3486 this.internal_set_value(val);
3487 this.floating = false;
3488 if (this.is_started)
3489 this.render_value();
3491 initialize_field: function() {
3492 this.is_started = true;
3493 instance.web.bus.on('click', this, function() {
3494 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3495 this.$input.autocomplete("close");
3498 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3500 initialize_content: function() {
3501 if (!this.get("effective_readonly"))
3502 this.render_editable();
3504 destroy_content: function () {
3505 if (this.$drop_down) {
3506 this.$drop_down.off('click');
3507 delete this.$drop_down;
3510 this.$input.closest(".modal .modal-content").off('scroll');
3511 this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3512 'focus focusout change keydown');
3515 if (this.$follow_button) {
3516 this.$follow_button.off('blur focus click');
3517 delete this.$follow_button;
3520 destroy: function () {
3521 this.destroy_content();
3522 return this._super();
3524 init_error_displayer: function() {
3527 hide_error_displayer: function() {
3530 show_error_displayer: function() {
3531 new instance.web.form.M2ODialog(this).open();
3533 render_editable: function() {
3535 this.$input = this.$el.find("input");
3537 this.init_error_displayer();
3539 self.$input.on('focus', function() {
3540 self.hide_error_displayer();
3543 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3544 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3546 this.$follow_button.click(function(ev) {
3547 ev.preventDefault();
3548 if (!self.get('value')) {
3552 var pop = new instance.web.form.FormOpenPopup(self);
3553 var context = self.build_context().eval();
3554 var model_obj = new instance.web.Model(self.field.relation);
3555 model_obj.call('get_formview_id', [self.get("value"), context]).then(function(view_id){
3557 self.field.relation,
3559 self.build_context(),
3561 title: _t("Open: ") + self.string,
3565 pop.on('write_completed', self, function(){
3566 self.display_value = {};
3567 self.display_value_backup = {};
3568 self.render_value();
3570 self.trigger('changed_value');
3575 // some behavior for input
3576 var input_changed = function() {
3577 if (self.current_display !== self.$input.val()) {
3578 self.current_display = self.$input.val();
3579 if (self.$input.val() === "") {
3580 self.internal_set_value(false);
3581 self.floating = false;
3583 self.floating = true;
3587 this.$input.keydown(input_changed);
3588 this.$input.change(input_changed);
3589 this.$drop_down.click(function() {
3590 self.$input.focus();
3591 if (self.$input.autocomplete("widget").is(":visible")) {
3592 self.$input.autocomplete("close");
3594 if (self.get("value") && ! self.floating) {
3595 self.$input.autocomplete("search", "");
3597 self.$input.autocomplete("search");
3602 // Autocomplete close on dialog content scroll
3603 var close_autocomplete = _.debounce(function() {
3604 if (self.$input.autocomplete("widget").is(":visible")) {
3605 self.$input.autocomplete("close");
3608 this.$input.closest(".modal .modal-content").on('scroll', this, close_autocomplete);
3610 self.ed_def = $.Deferred();
3611 self.uned_def = $.Deferred();
3613 var ed_duration = 15000;
3614 var anyoneLoosesFocus = function (e) {
3615 if (self.ignore_focusout) { return; }
3617 if (self.floating) {
3618 if (self.last_search.length > 0) {
3619 if (self.last_search[0][0] != self.get("value")) {
3620 self.display_value = {};
3621 self.display_value_backup = {};
3622 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3623 self.reinit_value(self.last_search[0][0]);
3626 self.render_value();
3630 self.reinit_value(false);
3632 self.floating = false;
3634 if (used && self.get("value") === false && ! self.no_ed && (self.options.no_create === false || self.options.no_create === undefined)) {
3635 self.ed_def.reject();
3636 self.uned_def.reject();
3637 self.ed_def = $.Deferred();
3638 self.ed_def.done(function() {
3639 self.show_error_displayer();
3640 ignore_blur = false;
3641 self.trigger('focused');
3644 setTimeout(function() {
3645 self.ed_def.resolve();
3646 self.uned_def.reject();
3647 self.uned_def = $.Deferred();
3648 self.uned_def.done(function() {
3649 self.hide_error_displayer();
3651 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3655 self.ed_def.reject();
3658 var ignore_blur = false;
3660 focusout: anyoneLoosesFocus,
3661 focus: function () { self.trigger('focused'); },
3662 autocompleteopen: function () { ignore_blur = true; },
3663 autocompleteclose: function () { ignore_blur = false; },
3665 // autocomplete open
3666 if (ignore_blur) { return; }
3667 if (_(self.getChildren()).any(function (child) {
3668 return child instanceof instance.web.form.AbstractFormPopup;
3670 self.trigger('blurred');
3674 var isSelecting = false;
3676 this.$input.autocomplete({
3677 source: function(req, resp) {
3678 self.get_search_result(req.term).done(function(result) {
3682 select: function(event, ui) {
3686 self.display_value = {};
3687 self.display_value_backup = {};
3688 self.display_value["" + item.id] = item.name;
3689 self.reinit_value(item.id);
3690 } else if (item.action) {
3692 // Cancel widget blurring, to avoid form blur event
3693 self.trigger('focused');
3697 focus: function(e, ui) {
3701 // disabled to solve a bug, but may cause others
3702 //close: anyoneLoosesFocus,
3706 // set position for list of suggestions box
3707 this.$input.autocomplete( "option", "position", { my : "left top", at: "left bottom" } );
3708 this.$input.autocomplete("widget").openerpClass();
3709 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3710 this.$input.keyup(function(e) {
3711 if (e.which === 13) { // ENTER
3713 e.stopPropagation();
3715 isSelecting = false;
3717 this.setupFocus(this.$follow_button);
3719 render_value: function(no_recurse) {
3721 if (! this.get("value")) {
3722 this.display_string("");
3725 var display = this.display_value["" + this.get("value")];
3727 this.display_string(display);
3731 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3732 this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3734 self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
3737 self.display_value["" + self.get("value")] = data[0][1];
3738 self.render_value(true);
3739 }).fail( function (data, event) {
3740 // avoid displaying crash errors as many2One should be name_get compliant
3741 event.preventDefault();
3742 self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")];
3743 self.render_value(true);
3747 display_string: function(str) {
3749 if (!this.get("effective_readonly")) {
3750 this.$input.val(str.split("\n")[0]);
3751 this.current_display = this.$input.val();
3752 if (this.is_false()) {
3753 this.$('.oe_m2o_cm_button').css({'display':'none'});
3755 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3758 var lines = _.escape(str).split("\n");
3762 follow = _.rest(lines).join("<br />");
3765 var $link = this.$el.find('.oe_form_uri')
3768 if (! this.options.no_open)
3769 $link.click(function () {
3770 var context = self.build_context().eval();
3771 var model_obj = new instance.web.Model(self.field.relation);
3772 model_obj.call('get_formview_action', [self.get("value"), context]).then(function(action){
3773 self.do_action(action);
3777 $(".oe_form_m2o_follow", this.$el).html(follow);
3780 set_value: function(value_) {
3782 if (value_ instanceof Array) {
3783 this.display_value = {};
3784 this.display_value_backup = {};
3785 if (! this.options.always_reload) {
3786 this.display_value["" + value_[0]] = value_[1];
3789 this.display_value_backup["" + value_[0]] = value_[1];
3793 value_ = value_ || false;
3794 this.reinit_value(value_);
3796 get_displayed: function() {
3797 return this.display_value["" + this.get("value")];
3799 add_id: function(id) {
3800 this.display_value = {};
3801 this.display_value_backup = {};
3802 this.reinit_value(id);
3804 is_false: function() {
3805 return ! this.get("value");
3807 focus: function () {
3808 var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3809 return input ? input.focus() : false;
3811 _quick_create: function() {
3813 this.ed_def.reject();
3814 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3816 _search_create_popup: function() {
3818 this.ed_def.reject();
3819 this.ignore_focusout = true;
3820 this.reinit_value(false);
3821 var res = instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3822 this.ignore_focusout = false;
3826 set_dimensions: function (height, width) {
3827 this._super(height, width);
3828 if (!this.get("effective_readonly") && this.$input)
3829 this.$input.css('height', height);
3833 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3834 template: 'Many2OneButton',
3835 init: function(field_manager, node) {
3836 this._super.apply(this, arguments);
3839 this._super.apply(this, arguments);
3842 set_button: function() {
3845 this.$button.remove();
3848 this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3849 this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3850 this.$button.addClass('oe_link').css({'padding':'4px'});
3851 this.$el.append(this.$button);
3852 this.$button.on('click', self.on_click);
3854 on_click: function(ev) {
3856 this.popup = new instance.web.form.FormOpenPopup(this);
3857 this.popup.show_element(
3858 this.field.relation,
3860 this.build_context(),
3861 {title: this.string}
3863 this.popup.on('create_completed', self, function(r) {
3867 set_value: function(value_) {
3869 if (value_ instanceof Array) {
3872 value_ = value_ || false;
3873 this.set('value', value_);
3879 * Abstract-ish ListView.List subclass adding an "Add an item" row to replace
3880 * the big ugly button in the header.
3882 * Requires the implementation of a ``is_readonly`` method (usually a proxy to
3883 * the corresponding field's readonly or effective_readonly property) to
3884 * decide whether the special row should or should not be inserted.
3886 * Optionally an ``_add_row_class`` attribute can be set for the class(es) to
3887 * set on the insertion row.
3889 instance.web.form.AddAnItemList = instance.web.ListView.List.extend({
3890 pad_table_to: function (count) {
3891 if (!this.view.is_action_enabled('create') || this.is_readonly()) {
3896 this._super(count > 0 ? count - 1 : 0);
3899 var columns = _(this.columns).filter(function (column) {
3900 return column.invisible !== '1';
3902 if (this.options.selectable) { columns++; }
3903 if (this.options.deletable) { columns++; }
3905 var $cell = $('<td>', {
3907 'class': this._add_row_class || ''
3909 $('<a>', {href: '#'}).text(_t("Add an item"))
3910 .mousedown(function () {
3911 // FIXME: needs to be an official API somehow
3912 if (self.view.editor.is_editing()) {
3913 self.view.__ignore_blur = true;
3916 .click(function (e) {
3918 e.stopPropagation();
3919 // FIXME: there should also be an API for that one
3920 if (self.view.editor.form.__blur_timeout) {
3921 clearTimeout(self.view.editor.form.__blur_timeout);
3922 self.view.editor.form.__blur_timeout = false;
3924 self.view.ensure_saved().done(function () {
3925 self.view.do_add_record();
3929 var $padding = this.$current.find('tr:not([data-id]):first');
3930 var $newrow = $('<tr>').append($cell);
3931 if ($padding.length) {
3932 $padding.before($newrow);
3934 this.$current.append($newrow)
3940 # Values: (0, 0, { fields }) create
3941 # (1, ID, { fields }) update
3942 # (2, ID) remove (delete)
3943 # (3, ID) unlink one (target id or target of relation)
3945 # (5) unlink all (only valid for one2many)
3950 'create': function (values) {
3951 return [commands.CREATE, false, values];
3953 // (1, id, {values})
3955 'update': function (id, values) {
3956 return [commands.UPDATE, id, values];
3960 'delete': function (id) {
3961 return [commands.DELETE, id, false];
3963 // (3, id[, _]) removes relation, but not linked record itself
3965 'forget': function (id) {
3966 return [commands.FORGET, id, false];
3970 'link_to': function (id) {
3971 return [commands.LINK_TO, id, false];
3975 'delete_all': function () {
3976 return [5, false, false];
3978 // (6, _, ids) replaces all linked records with provided ids
3980 'replace_with': function (ids) {
3981 return [6, false, ids];
3984 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3985 multi_selection: false,
3986 disable_utility_classes: true,
3987 init: function(field_manager, node) {
3988 this._super(field_manager, node);
3989 lazy_build_o2m_kanban_view();
3990 this.is_loaded = $.Deferred();
3991 this.initial_is_loaded = this.is_loaded;
3992 this.form_last_update = $.Deferred();
3993 this.init_form_last_update = this.form_last_update;
3994 this.is_started = false;
3995 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
3996 this.dataset.o2m = this;
3997 this.dataset.parent_view = this.view;
3998 this.dataset.child_name = this.name;
4000 this.dataset.on('dataset_changed', this, function() {
4001 self.trigger_on_change();
4006 this._super.apply(this, arguments);
4007 this.$el.addClass('oe_form_field oe_form_field_one2many');
4012 this.is_loaded.done(function() {
4013 self.on("change:effective_readonly", self, function() {
4014 self.is_loaded = self.is_loaded.then(function() {
4015 self.viewmanager.destroy();
4016 return $.when(self.load_views()).done(function() {
4017 self.reload_current_view();
4022 this.is_started = true;
4023 this.reload_current_view();
4025 trigger_on_change: function() {
4026 this.trigger('changed_value');
4028 load_views: function() {
4031 var modes = this.node.attrs.mode;
4032 modes = !!modes ? modes.split(",") : ["tree"];
4034 _.each(modes, function(mode) {
4035 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
4036 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
4040 view_type: mode == "tree" ? "list" : mode,
4043 if (self.field.views && self.field.views[mode]) {
4044 view.embedded_view = self.field.views[mode];
4046 if(view.view_type === "list") {
4047 _.extend(view.options, {
4049 selectable: self.multi_selection,
4051 import_enabled: false,
4054 if (self.get("effective_readonly")) {
4055 _.extend(view.options, {
4060 } else if (view.view_type === "form") {
4061 if (self.get("effective_readonly")) {
4062 view.view_type = 'form';
4064 _.extend(view.options, {
4065 not_interactible_on_create: true,
4067 } else if (view.view_type === "kanban") {
4068 _.extend(view.options, {
4069 confirm_on_delete: false,
4071 if (self.get("effective_readonly")) {
4072 _.extend(view.options, {
4073 action_buttons: false,
4074 quick_creatable: false,
4076 read_only_mode: true,
4084 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
4085 this.viewmanager.o2m = self;
4086 var once = $.Deferred().done(function() {
4087 self.init_form_last_update.resolve();
4089 var def = $.Deferred().done(function() {
4090 self.initial_is_loaded.resolve();
4092 this.viewmanager.on("controller_inited", self, function(view_type, controller) {
4093 controller.o2m = self;
4094 if (view_type == "list") {
4095 if (self.get("effective_readonly")) {
4096 controller.on('edit:before', self, function (e) {
4099 _(controller.columns).find(function (column) {
4100 if (!(column instanceof instance.web.list.Handle)) {
4103 column.modifiers.invisible = true;
4107 } else if (view_type === "form") {
4108 if (self.get("effective_readonly")) {
4109 $(".oe_form_buttons", controller.$el).children().remove();
4111 controller.on("load_record", self, function(){
4114 controller.on('pager_action_executed',self,self.save_any_view);
4115 } else if (view_type == "graph") {
4116 self.reload_current_view();
4120 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
4121 $.when(self.save_any_view()).done(function() {
4122 if (n_mode === "list") {
4123 $.async_when().done(function() {
4124 self.reload_current_view();
4129 $.async_when().done(function () {
4130 self.viewmanager.appendTo(self.$el);
4134 reload_current_view: function() {
4136 self.is_loaded = self.is_loaded.then(function() {
4137 var view = self.get_active_view();
4138 if (view.type === "list") {
4139 return view.controller.reload_content();
4140 } else if (view.type === "form") {
4141 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
4142 self.dataset.index = 0;
4144 var act = function() {
4145 return view.controller.do_show();
4147 self.form_last_update = self.form_last_update.then(act, act);
4148 return self.form_last_update;
4149 } else if (view.controller.do_search) {
4150 return view.controller.do_search(self.build_domain(), self.dataset.get_context(), []);
4153 return self.is_loaded;
4155 get_active_view: function () {
4157 * Returns the current active view if any.
4159 if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
4160 this.viewmanager.views[this.viewmanager.active_view] &&
4161 this.viewmanager.views[this.viewmanager.active_view].controller) {
4163 type: this.viewmanager.active_view,
4164 controller: this.viewmanager.views[this.viewmanager.active_view].controller
4168 set_value: function(value_) {
4169 value_ = value_ || [];
4171 var view = this.get_active_view();
4172 this.dataset.reset_ids([]);
4174 if(value_.length >= 1 && value_[0] instanceof Array) {
4176 _.each(value_, function(command) {
4177 var obj = {values: command[2]};
4178 switch (command[0]) {
4179 case commands.CREATE:
4180 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4182 self.dataset.to_create.push(obj);
4183 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4186 case commands.UPDATE:
4187 obj['id'] = command[1];
4188 self.dataset.to_write.push(obj);
4189 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4192 case commands.DELETE:
4193 self.dataset.to_delete.push({id: command[1]});
4195 case commands.LINK_TO:
4196 ids.push(command[1]);
4198 case commands.DELETE_ALL:
4199 self.dataset.delete_all = true;
4204 this.dataset.set_ids(ids);
4205 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
4207 this.dataset.delete_all = true;
4208 _.each(value_, function(command) {
4209 var obj = {values: command};
4210 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4212 self.dataset.to_create.push(obj);
4213 self.dataset.cache.push(_.clone(obj));
4217 this.dataset.set_ids(ids);
4219 this._super(value_);
4220 this.dataset.reset_ids(value_);
4222 if (this.dataset.index === null && this.dataset.ids.length > 0) {
4223 this.dataset.index = 0;
4225 this.trigger_on_change();
4226 if (this.is_started) {
4227 return self.reload_current_view();
4232 get_value: function() {
4236 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
4237 val = val.concat(_.map(this.dataset.ids, function(id) {
4238 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
4240 return commands.create(alter_order.values);
4242 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
4244 return commands.update(alter_order.id, alter_order.values);
4246 return commands.link_to(id);
4248 return val.concat(_.map(
4249 this.dataset.to_delete, function(x) {
4250 return commands['delete'](x.id);}));
4252 commit_value: function() {
4253 return this.save_any_view();
4255 save_any_view: function() {
4256 var view = this.get_active_view();
4258 if (this.viewmanager.active_view === "form") {
4259 if (view.controller.is_initialized.state() !== 'resolved') {
4260 return $.when(false);
4262 return $.when(view.controller.save());
4263 } else if (this.viewmanager.active_view === "list") {
4264 return $.when(view.controller.ensure_saved());
4267 return $.when(false);
4269 is_syntax_valid: function() {
4270 var view = this.get_active_view();
4274 switch (this.viewmanager.active_view) {
4276 return _(view.controller.fields).chain()
4281 return view.controller.is_valid();
4287 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
4288 template: 'One2Many.viewmanager',
4289 init: function(parent, dataset, views, flags) {
4290 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
4291 this.registry = this.registry.extend({
4292 list: 'instance.web.form.One2ManyListView',
4293 form: 'instance.web.form.One2ManyFormView',
4294 kanban: 'instance.web.form.One2ManyKanbanView',
4296 this.__ignore_blur = false;
4298 switch_mode: function(mode, unused) {
4299 if (mode !== 'form') {
4300 return this._super(mode, unused);
4303 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
4304 var pop = new instance.web.form.FormOpenPopup(this);
4305 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4306 title: _t("Open: ") + self.o2m.string,
4307 create_function: function(data, options) {
4308 return self.o2m.dataset.create(data, options).done(function(r) {
4309 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4310 self.o2m.dataset.trigger("dataset_changed", r);
4313 write_function: function(id, data, options) {
4314 return self.o2m.dataset.write(id, data, {}).done(function() {
4315 self.o2m.reload_current_view();
4318 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4319 parent_view: self.o2m.view,
4320 child_name: self.o2m.name,
4321 read_function: function() {
4322 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4324 form_view_options: {'not_interactible_on_create':true},
4325 readonly: self.o2m.get("effective_readonly")
4327 pop.on("elements_selected", self, function() {
4328 self.o2m.reload_current_view();
4333 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
4334 get_context: function() {
4335 this.context = this.o2m.build_context();
4336 return this.context;
4340 instance.web.form.One2ManyListView = instance.web.ListView.extend({
4341 _template: 'One2Many.listview',
4342 init: function (parent, dataset, view_id, options) {
4343 this._super(parent, dataset, view_id, _.extend(options || {}, {
4344 GroupsType: instance.web.form.One2ManyGroups,
4345 ListType: instance.web.form.One2ManyList
4347 this.on('edit:after', this, this.proxy('_after_edit'));
4348 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
4351 .bind('add', this.proxy("changed_records"))
4352 .bind('edit', this.proxy("changed_records"))
4353 .bind('remove', this.proxy("changed_records"));
4355 start: function () {
4356 var ret = this._super();
4358 .off('mousedown.handleButtons')
4359 .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
4362 changed_records: function () {
4363 this.o2m.trigger_on_change();
4365 is_valid: function () {
4366 var editor = this.editor;
4367 var form = editor.form;
4368 // If no edition is pending, the listview can not be invalid (?)
4369 if (!editor.record) {
4372 // If the form has not been modified, the view can only be valid
4373 // NB: is_dirty will also be set on defaults/onchanges/whatever?
4374 // oe_form_dirty seems to only be set on actual user actions
4375 if (!form.$el.is('.oe_form_dirty')) {
4378 this.o2m._dirty_flag = true;
4380 // Otherwise validate internal form
4381 return _(form.fields).chain()
4382 .invoke(function () {
4383 this._check_css_flags();
4384 return this.is_valid();
4389 do_add_record: function () {
4390 if (this.editable()) {
4391 this._super.apply(this, arguments);
4394 var pop = new instance.web.form.SelectCreatePopup(this);
4396 self.o2m.field.relation,
4398 title: _t("Create: ") + self.o2m.string,
4399 initial_view: "form",
4400 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4401 create_function: function(data, options) {
4402 return self.o2m.dataset.create(data, options).done(function(r) {
4403 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4404 self.o2m.dataset.trigger("dataset_changed", r);
4407 read_function: function() {
4408 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4410 parent_view: self.o2m.view,
4411 child_name: self.o2m.name,
4412 form_view_options: {'not_interactible_on_create':true}
4414 self.o2m.build_domain(),
4415 self.o2m.build_context()
4417 pop.on("elements_selected", self, function() {
4418 self.o2m.reload_current_view();
4422 do_activate_record: function(index, id) {
4424 var pop = new instance.web.form.FormOpenPopup(self);
4425 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4426 title: _t("Open: ") + self.o2m.string,
4427 write_function: function(id, data) {
4428 return self.o2m.dataset.write(id, data, {}).done(function() {
4429 self.o2m.reload_current_view();
4432 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4433 parent_view: self.o2m.view,
4434 child_name: self.o2m.name,
4435 read_function: function() {
4436 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4438 form_view_options: {'not_interactible_on_create':true},
4439 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
4442 do_button_action: function (name, id, callback) {
4443 if (!_.isNumber(id)) {
4444 instance.webclient.notification.warn(
4445 _t("Action Button"),
4446 _t("The o2m record must be saved before an action can be used"));
4449 var parent_form = this.o2m.view;
4451 this.ensure_saved().then(function () {
4453 return parent_form.save();
4456 }).done(function () {
4457 var ds = self.o2m.dataset;
4458 var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
4459 return value.length;
4461 if (!self.o2m.options.reload_on_button && !cached_records) {
4462 self.handle_button(name, id, callback);
4464 self.handle_button(name, id, function(){
4465 self.o2m.view.reload();
4471 _after_edit: function () {
4472 this.__ignore_blur = false;
4473 this.editor.form.on('blurred', this, this._on_form_blur);
4475 // The form's blur thing may be jiggered during the edition setup,
4476 // potentially leading to the o2m instasaving the row. Cancel any
4477 // blurring triggered the edition startup here
4478 this.editor.form.widgetFocused();
4480 _before_unedit: function () {
4481 this.editor.form.off('blurred', this, this._on_form_blur);
4483 _button_down: function () {
4484 // If a button is clicked (usually some sort of action button), it's
4485 // the button's responsibility to ensure the editable list is in the
4486 // correct state -> ignore form blurring
4487 this.__ignore_blur = true;
4490 * Handles blurring of the nested form (saves the currently edited row),
4491 * unless the flag to ignore the event is set to ``true``
4493 * Makes the internal form go away
4495 _on_form_blur: function () {
4496 if (this.__ignore_blur) {
4497 this.__ignore_blur = false;
4500 // FIXME: why isn't there an API for this?
4501 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4502 this.ensure_saved();
4505 this.cancel_edition();
4507 keypress_ENTER: function () {
4508 // blurring caused by hitting the [Return] key, should skip the
4509 // autosave-on-blur and let the handler for [Return] do its thing (save
4510 // the current row *anyway*, then create a new one/edit the next one)
4511 this.__ignore_blur = true;
4512 this._super.apply(this, arguments);
4514 do_delete: function (ids) {
4515 var confirm = window.confirm;
4516 window.confirm = function () { return true; };
4518 return this._super(ids);
4520 window.confirm = confirm;
4523 reload_record: function (record) {
4524 // Evict record.id from cache to ensure it will be reloaded correctly
4525 this.dataset.evict_record(record.get('id'));
4527 return this._super(record);
4530 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4531 setup_resequence_rows: function () {
4532 if (!this.view.o2m.get('effective_readonly')) {
4533 this._super.apply(this, arguments);
4537 instance.web.form.One2ManyList = instance.web.form.AddAnItemList.extend({
4538 _add_row_class: 'oe_form_field_one2many_list_row_add',
4539 is_readonly: function () {
4540 return this.view.o2m.get('effective_readonly');
4544 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4545 form_template: 'One2Many.formview',
4546 load_form: function(data) {
4549 this.$buttons.find('button.oe_form_button_create').click(function() {
4550 self.save().done(self.on_button_new);
4553 do_notify_change: function() {
4554 if (this.dataset.parent_view) {
4555 this.dataset.parent_view.do_notify_change();
4557 this._super.apply(this, arguments);
4562 var lazy_build_o2m_kanban_view = function() {
4563 if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
4565 instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4569 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4570 template: "FieldMany2ManyTags",
4571 tag_template: "FieldMany2ManyTag",
4573 this._super.apply(this, arguments);
4574 instance.web.form.CompletionFieldMixin.init.call(this);
4575 this.set({"value": []});
4576 this._display_orderer = new instance.web.DropMisordered();
4577 this._drop_shown = false;
4579 initialize_texttext: function(){
4582 plugins : 'tags arrow autocomplete',
4584 render: function(suggestion) {
4585 return $('<span class="text-label"/>').
4586 data('index', suggestion['index']).html(suggestion['label']);
4591 selectFromDropdown: function() {
4592 this.trigger('hideDropdown');
4593 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4594 var data = self.search_result[index];
4596 self.add_id(data.id);
4598 self.ignore_blur = true;
4601 this.trigger('setSuggestions', {result : []});
4605 isTagAllowed: function(tag) {
4609 removeTag: function(tag) {
4610 var id = tag.data("id");
4611 self.set({"value": _.without(self.get("value"), id)});
4613 renderTag: function(stuff) {
4614 return $.fn.textext.TextExtTags.prototype.renderTag.
4615 call(this, stuff).data("id", stuff.id);
4619 itemToString: function(item) {
4624 onSetInputData: function(e, data) {
4626 this._plugins.autocomplete._suggestions = null;
4628 this.input().val(data);
4634 initialize_content: function() {
4635 if (this.get("effective_readonly"))
4638 self.ignore_blur = false;
4639 self.$text = this.$("textarea");
4640 self.$text.textext(self.initialize_texttext()).bind('getSuggestions', function(e, data) {
4642 var str = !!data ? data.query || '' : '';
4643 self.get_search_result(str).done(function(result) {
4644 self.search_result = result;
4645 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4646 return _.extend(el, {index:i});
4649 }).bind('hideDropdown', function() {
4650 self._drop_shown = false;
4651 }).bind('showDropdown', function() {
4652 self._drop_shown = true;
4654 self.tags = self.$text.textext()[0].tags();
4656 .focusin(function () {
4657 self.trigger('focused');
4658 self.ignore_blur = false;
4660 .focusout(function() {
4661 self.$text.trigger("setInputData", "");
4662 if (!self.ignore_blur) {
4663 self.trigger('blurred');
4665 }).keydown(function(e) {
4666 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4667 self.$text.textext()[0].autocomplete().selectFromDropdown();
4671 // WARNING: duplicated in 4 other M2M widgets
4672 set_value: function(value_) {
4673 value_ = value_ || [];
4674 if (value_.length >= 1 && value_[0] instanceof Array) {
4675 // value_ is a list of m2m commands. We only process
4676 // LINK_TO and REPLACE_WITH in this context
4678 _.each(value_, function (command) {
4679 if (command[0] === commands.LINK_TO) {
4680 val.push(command[1]); // (4, id[, _])
4681 } else if (command[0] === commands.REPLACE_WITH) {
4682 val = command[2]; // (6, _, ids)
4687 this._super(value_);
4689 is_false: function() {
4690 return _(this.get("value")).isEmpty();
4692 get_value: function() {
4693 var tmp = [commands.replace_with(this.get("value"))];
4696 get_search_blacklist: function() {
4697 return this.get("value");
4699 map_tag: function(data){
4700 return _.map(data, function(el) {return {name: el[1], id:el[0]};})
4702 get_render_data: function(ids){
4704 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4705 return dataset.name_get(ids);
4707 render_tag: function(data) {
4709 if (! self.get("effective_readonly")) {
4710 self.tags.containerElement().children().remove();
4711 self.$('textarea').css("padding-left", "3px");
4712 self.tags.addTags(self.map_tag(data));
4714 self.$el.html(QWeb.render(self.tag_template, {elements: data}));
4717 render_value: function() {
4719 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4720 var values = self.get("value");
4721 var handle_names = function(data) {
4722 if (self.isDestroyed())
4725 _.each(data, function(el) {
4726 indexed[el[0]] = el;
4728 data = _.map(values, function(el) { return indexed[el]; });
4729 self.render_tag(data);
4731 if (! values || values.length > 0) {
4732 this._display_orderer.add(self.get_render_data(values)).done(handle_names);
4738 add_id: function(id) {
4739 this.set({'value': _.uniq(this.get('value').concat([id]))});
4741 focus: function () {
4742 var input = this.$text && this.$text[0];
4743 return input ? input.focus() : false;
4745 set_dimensions: function (height, width) {
4746 this._super(height, width);
4747 this.$("textarea").css({
4752 _search_create_popup: function() {
4753 self.ignore_blur = true;
4754 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
4760 - reload_on_button: Reload the whole form view if click on a button in a list view.
4761 If you see this options, do not use it, it's basically a dirty hack to make one
4762 precise o2m to behave the way we want.
4764 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4765 multi_selection: false,
4766 disable_utility_classes: true,
4767 init: function(field_manager, node) {
4768 this._super(field_manager, node);
4769 this.is_loaded = $.Deferred();
4770 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4771 this.dataset.m2m = this;
4773 this.dataset.on('unlink', self, function(ids) {
4774 self.dataset_changed();
4777 this.list_dm = new instance.web.DropMisordered();
4778 this.render_value_dm = new instance.web.DropMisordered();
4780 initialize_content: function() {
4783 this.$el.addClass('oe_form_field oe_form_field_many2many');
4785 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4787 'deletable': this.get("effective_readonly") ? false : true,
4788 'selectable': this.multi_selection,
4790 'reorderable': false,
4791 'import_enabled': false,
4793 var embedded = (this.field.views || {}).tree;
4795 this.list_view.set_embedded_view(embedded);
4797 this.list_view.m2m_field = this;
4798 var loaded = $.Deferred();
4799 this.list_view.on("list_view_loaded", this, function() {
4802 this.list_view.appendTo(this.$el);
4804 var old_def = self.is_loaded;
4805 self.is_loaded = $.Deferred().done(function() {
4808 this.list_dm.add(loaded).then(function() {
4809 self.is_loaded.resolve();
4812 destroy_content: function() {
4813 this.list_view.destroy();
4814 this.list_view = undefined;
4816 // WARNING: duplicated in 4 other M2M widgets
4817 set_value: function(value_) {
4818 value_ = value_ || [];
4819 if (value_.length >= 1 && value_[0] instanceof Array) {
4820 // value_ is a list of m2m commands. We only process
4821 // LINK_TO and REPLACE_WITH in this context
4823 _.each(value_, function (command) {
4824 if (command[0] === commands.LINK_TO) {
4825 val.push(command[1]); // (4, id[, _])
4826 } else if (command[0] === commands.REPLACE_WITH) {
4827 val = command[2]; // (6, _, ids)
4832 this._super(value_);
4834 get_value: function() {
4835 return [commands.replace_with(this.get('value'))];
4837 is_false: function () {
4838 return _(this.get("value")).isEmpty();
4840 render_value: function() {
4842 this.dataset.set_ids(this.get("value"));
4843 this.render_value_dm.add(this.is_loaded).then(function() {
4844 return self.list_view.reload_content();
4847 dataset_changed: function() {
4848 this.internal_set_value(this.dataset.ids);
4852 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4853 get_context: function() {
4854 this.context = this.m2m.build_context();
4855 return this.context;
4861 * @extends instance.web.ListView
4863 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4864 init: function (parent, dataset, view_id, options) {
4865 this._super(parent, dataset, view_id, _.extend(options || {}, {
4866 ListType: instance.web.form.Many2ManyList,
4869 do_add_record: function () {
4870 var pop = new instance.web.form.SelectCreatePopup(this);
4874 title: _t("Add: ") + this.m2m_field.string,
4875 no_create: this.m2m_field.options.no_create,
4877 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4878 this.m2m_field.build_context()
4881 pop.on("elements_selected", self, function(element_ids) {
4883 _(element_ids).each(function (id) {
4884 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4885 self.dataset.set_ids(self.dataset.ids.concat([id]));
4886 self.m2m_field.dataset_changed();
4891 self.reload_content();
4895 do_activate_record: function(index, id) {
4897 var pop = new instance.web.form.FormOpenPopup(this);
4898 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4899 title: _t("Open: ") + this.m2m_field.string,
4900 readonly: this.getParent().get("effective_readonly")
4902 pop.on('write_completed', self, self.reload_content);
4904 do_button_action: function(name, id, callback) {
4906 var _sup = _.bind(this._super, this);
4907 if (! this.m2m_field.options.reload_on_button) {
4908 return _sup(name, id, callback);
4910 return this.m2m_field.view.save().then(function() {
4911 return _sup(name, id, function() {
4912 self.m2m_field.view.reload();
4917 is_action_enabled: function () { return true; },
4919 instance.web.form.Many2ManyList = instance.web.form.AddAnItemList.extend({
4920 _add_row_class: 'oe_form_field_many2many_list_row_add',
4921 is_readonly: function () {
4922 return this.view.m2m_field.get('effective_readonly');
4926 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4927 disable_utility_classes: true,
4928 init: function(field_manager, node) {
4929 this._super(field_manager, node);
4930 instance.web.form.CompletionFieldMixin.init.call(this);
4931 m2m_kanban_lazy_init();
4932 this.is_loaded = $.Deferred();
4933 this.initial_is_loaded = this.is_loaded;
4936 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4937 this.dataset.m2m = this;
4938 this.dataset.on('unlink', self, function(ids) {
4939 self.dataset_changed();
4943 this._super.apply(this, arguments);
4948 self.on("change:effective_readonly", self, function() {
4949 self.is_loaded = self.is_loaded.then(function() {
4950 self.kanban_view.destroy();
4951 return $.when(self.load_view()).done(function() {
4952 self.render_value();
4957 // WARNING: duplicated in 4 other M2M widgets
4958 set_value: function(value_) {
4959 value_ = value_ || [];
4960 if (value_.length >= 1 && value_[0] instanceof Array) {
4961 // value_ is a list of m2m commands. We only process
4962 // LINK_TO and REPLACE_WITH in this context
4964 _.each(value_, function (command) {
4965 if (command[0] === commands.LINK_TO) {
4966 val.push(command[1]); // (4, id[, _])
4967 } else if (command[0] === commands.REPLACE_WITH) {
4968 val = command[2]; // (6, _, ids)
4973 this._super(value_);
4975 get_value: function() {
4976 return [commands.replace_with(this.get('value'))];
4978 load_view: function() {
4980 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4981 'create_text': _t("Add"),
4982 'creatable': self.get("effective_readonly") ? false : true,
4983 'quick_creatable': self.get("effective_readonly") ? false : true,
4984 'read_only_mode': self.get("effective_readonly") ? true : false,
4985 'confirm_on_delete': false,
4987 var embedded = (this.field.views || {}).kanban;
4989 this.kanban_view.set_embedded_view(embedded);
4991 this.kanban_view.m2m = this;
4992 var loaded = $.Deferred();
4993 this.kanban_view.on("kanban_view_loaded",self,function() {
4994 self.initial_is_loaded.resolve();
4997 this.kanban_view.on('switch_mode', this, this.open_popup);
4998 $.async_when().done(function () {
4999 self.kanban_view.appendTo(self.$el);
5003 render_value: function() {
5005 this.dataset.set_ids(this.get("value"));
5006 this.is_loaded = this.is_loaded.then(function() {
5007 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
5010 dataset_changed: function() {
5011 this.set({'value': this.dataset.ids});
5013 open_popup: function(type, unused) {
5014 if (type !== "form")
5018 if (this.dataset.index === null) {
5019 pop = new instance.web.form.SelectCreatePopup(this);
5021 this.field.relation,
5023 title: _t("Add: ") + this.string
5025 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
5026 this.build_context()
5028 pop.on("elements_selected", self, function(element_ids) {
5029 _.each(element_ids, function(one_id) {
5030 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
5031 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
5032 self.dataset_changed();
5033 self.render_value();
5038 var id = self.dataset.ids[self.dataset.index];
5039 pop = new instance.web.form.FormOpenPopup(this);
5040 pop.show_element(self.field.relation, id, self.build_context(), {
5041 title: _t("Open: ") + self.string,
5042 write_function: function(id, data, options) {
5043 return self.dataset.write(id, data, {}).done(function() {
5044 self.render_value();
5047 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
5048 parent_view: self.view,
5049 child_name: self.name,
5050 readonly: self.get("effective_readonly")
5054 add_id: function(id) {
5055 this.quick_create.add_id(id);
5059 function m2m_kanban_lazy_init() {
5060 if (instance.web.form.Many2ManyKanbanView)
5062 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
5063 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
5064 _is_quick_create_enabled: function() {
5065 return this._super() && ! this.group_by;
5068 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
5069 template: 'Many2ManyKanban.quick_create',
5072 * close_btn: If true, the widget will display a "Close" button able to trigger
5075 init: function(parent, dataset, context, buttons) {
5076 this._super(parent);
5077 this.m2m = this.getParent().view.m2m;
5078 this.m2m.quick_create = this;
5079 this._dataset = dataset;
5080 this._buttons = buttons || false;
5081 this._context = context || {};
5083 start: function () {
5085 self.$text = this.$el.find('input').css("width", "200px");
5086 self.$text.textext({
5087 plugins : 'arrow autocomplete',
5089 render: function(suggestion) {
5090 return $('<span class="text-label"/>').
5091 data('index', suggestion['index']).html(suggestion['label']);
5096 selectFromDropdown: function() {
5097 $(this).trigger('hideDropdown');
5098 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
5099 var data = self.search_result[index];
5101 self.add_id(data.id);
5108 itemToString: function(item) {
5113 }).bind('getSuggestions', function(e, data) {
5115 var str = !!data ? data.query || '' : '';
5116 self.m2m.get_search_result(str).done(function(result) {
5117 self.search_result = result;
5118 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
5119 return _.extend(el, {index:i});
5123 self.$text.focusout(function() {
5128 this.$text[0].focus();
5130 add_id: function(id) {
5133 self.trigger('added', id);
5134 this.m2m.dataset_changed();
5140 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
5142 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
5143 template: "AbstractFormPopup.render",
5146 * -readonly: only applicable when not in creation mode, default to false
5147 * - alternative_form_view
5154 * - form_view_options
5156 init_popup: function(model, row_id, domain, context, options) {
5157 this.row_id = row_id;
5159 this.domain = domain || [];
5160 this.context = context || {};
5161 this.options = options;
5162 _.defaults(this.options, {
5165 init_dataset: function() {
5167 this.created_elements = [];
5168 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
5169 this.dataset.read_function = this.options.read_function;
5170 this.dataset.create_function = function(data, options, sup) {
5171 var fct = self.options.create_function || sup;
5172 return fct.call(this, data, options).done(function(r) {
5173 self.trigger('create_completed saved', r);
5174 self.created_elements.push(r);
5177 this.dataset.write_function = function(id, data, options, sup) {
5178 var fct = self.options.write_function || sup;
5179 return fct.call(this, id, data, options).done(function(r) {
5180 self.trigger('write_completed saved', r);
5183 this.dataset.parent_view = this.options.parent_view;
5184 this.dataset.child_name = this.options.child_name;
5186 display_popup: function() {
5188 this.renderElement();
5189 var dialog = new instance.web.Dialog(this, {
5190 dialogClass: 'oe_act_window',
5191 title: this.options.title || "",
5192 }, this.$el).open();
5193 dialog.on('closing', this, function (e){
5194 self.check_exit(true);
5196 this.$buttonpane = dialog.$buttons;
5199 setup_form_view: function() {
5202 this.dataset.ids = [this.row_id];
5203 this.dataset.index = 0;
5205 this.dataset.index = null;
5207 var options = _.clone(self.options.form_view_options) || {};
5208 if (this.row_id !== null) {
5209 options.initial_mode = this.options.readonly ? "view" : "edit";
5212 $buttons: this.$buttonpane,
5214 this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
5215 if (this.options.alternative_form_view) {
5216 this.view_form.set_embedded_view(this.options.alternative_form_view);
5218 this.view_form.appendTo(this.$el.find(".oe_popup_form"));
5219 this.view_form.on("form_view_loaded", self, function() {
5220 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
5221 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
5222 multi_select: multi_select,
5223 readonly: self.row_id !== null && self.options.readonly,
5225 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
5226 $snbutton.click(function() {
5227 $.when(self.view_form.save()).done(function() {
5228 self.view_form.reload_mutex.exec(function() {
5229 self.view_form.on_button_new();
5233 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
5234 $sbutton.click(function() {
5235 $.when(self.view_form.save()).done(function() {
5236 self.view_form.reload_mutex.exec(function() {
5241 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
5242 $cbutton.click(function() {
5243 self.view_form.trigger('on_button_cancel');
5246 self.view_form.do_show();
5249 select_elements: function(element_ids) {
5250 this.trigger("elements_selected", element_ids);
5252 check_exit: function(no_destroy) {
5253 if (this.created_elements.length > 0) {
5254 this.select_elements(this.created_elements);
5255 this.created_elements = [];
5257 this.trigger('closed');
5260 destroy: function () {
5261 this.trigger('closed');
5262 if (this.$el.is(":data(bs.modal)")) {
5263 this.$el.parents('.modal').modal('hide');
5270 * Class to display a popup containing a form view.
5272 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
5273 show_element: function(model, row_id, context, options) {
5274 this.init_popup(model, row_id, [], context, options);
5275 _.defaults(this.options, {
5277 this.display_popup();
5281 this.init_dataset();
5282 this.setup_form_view();
5287 * Class to display a popup to display a list to search a row. It also allows
5288 * to switch to a form view to create a new row.
5290 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
5294 * - initial_view: form or search (default search)
5295 * - disable_multiple_selection
5296 * - list_view_options
5298 select_element: function(model, options, domain, context) {
5299 this.init_popup(model, null, domain, context, options);
5301 _.defaults(this.options, {
5302 initial_view: "search",
5304 this.initial_ids = this.options.initial_ids;
5305 this.display_popup();
5309 this.init_dataset();
5310 if (this.options.initial_view == "search") {
5311 instance.web.pyeval.eval_domains_and_contexts({
5313 contexts: [this.context]
5314 }).done(function (results) {
5315 var search_defaults = {};
5316 _.each(results.context, function (value_, key) {
5317 var match = /^search_default_(.*)$/.exec(key);
5319 search_defaults[match[1]] = value_;
5322 self.setup_search_view(search_defaults);
5328 setup_search_view: function(search_defaults) {
5330 if (this.searchview) {
5331 this.searchview.destroy();
5333 if (this.searchview_drawer) {
5334 this.searchview_drawer.destroy();
5336 this.searchview = new instance.web.SearchView(this,
5337 this.dataset, false, search_defaults);
5338 this.searchview_drawer = new instance.web.SearchViewDrawer(this, this.searchview);
5339 this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
5340 if (self.initial_ids) {
5341 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
5342 contexts.concat(self.context), groupbys);
5343 self.initial_ids = undefined;
5345 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
5348 this.searchview.on("search_view_loaded", self, function() {
5349 self.view_list = new instance.web.form.SelectCreateListView(self,
5350 self.dataset, false,
5351 _.extend({'deletable': false,
5352 'selectable': !self.options.disable_multiple_selection,
5353 'import_enabled': false,
5354 '$buttons': self.$buttonpane,
5355 'disable_editable_mode': true,
5356 '$pager': self.$('.oe_popup_list_pager'),
5357 }, self.options.list_view_options || {}));
5358 self.view_list.on('edit:before', self, function (e) {
5361 self.view_list.popup = self;
5362 self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
5363 self.view_list.do_show();
5364 }).then(function() {
5365 self.searchview.do_search();
5367 self.view_list.on("list_view_loaded", self, function() {
5368 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
5369 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
5370 $cbutton.click(function() {
5373 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
5374 $sbutton.click(function() {
5375 self.select_elements(self.selected_ids);
5378 $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
5379 $cbutton.click(function() {
5384 this.searchview.appendTo(this.$(".oe_popup_search"));
5386 do_search: function(domains, contexts, groupbys) {
5388 instance.web.pyeval.eval_domains_and_contexts({
5389 domains: domains || [],
5390 contexts: contexts || [],
5391 group_by_seq: groupbys || []
5392 }).done(function (results) {
5393 self.view_list.do_search(results.domain, results.context, results.group_by);
5396 on_click_element: function(ids) {
5398 this.selected_ids = ids || [];
5399 if(this.selected_ids.length > 0) {
5400 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
5402 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
5405 new_object: function() {
5406 if (this.searchview) {
5407 this.searchview.hide();
5409 if (this.view_list) {
5410 this.view_list.do_hide();
5412 this.setup_form_view();
5416 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
5417 do_add_record: function () {
5418 this.popup.new_object();
5420 select_record: function(index) {
5421 this.popup.select_elements([this.dataset.ids[index]]);
5422 this.popup.destroy();
5424 do_select: function(ids, records) {
5425 this._super(ids, records);
5426 this.popup.on_click_element(ids);
5430 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5431 template: 'FieldReference',
5432 init: function(field_manager, node) {
5433 this._super(field_manager, node);
5434 this.reference_ready = true;
5436 destroy_content: function() {
5439 this.fm = undefined;
5442 initialize_content: function() {
5444 var fm = new instance.web.form.DefaultFieldManager(this);
5446 fm.extend_field_desc({
5448 selection: this.field_manager.get_field_desc(this.name).selection,
5456 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
5458 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5460 this.selection.on("change:value", this, this.on_selection_changed);
5461 this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
5463 .on('focused', null, function () {self.trigger('focused');})
5464 .on('blurred', null, function () {self.trigger('blurred');});
5466 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
5467 name: 'Referenced Document',
5468 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5470 this.m2o.on("change:value", this, this.data_changed);
5471 this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
5473 .on('focused', null, function () {self.trigger('focused');})
5474 .on('blurred', null, function () {self.trigger('blurred');});
5476 on_selection_changed: function() {
5477 if (this.reference_ready) {
5478 this.internal_set_value([this.selection.get_value(), false]);
5479 this.render_value();
5482 data_changed: function() {
5483 if (this.reference_ready) {
5484 this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
5487 set_value: function(val) {
5489 val = val.split(',');
5490 val[0] = val[0] || false;
5491 val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
5493 this._super(val || [false, false]);
5495 get_value: function() {
5496 return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
5498 render_value: function() {
5499 this.reference_ready = false;
5500 if (!this.get("effective_readonly")) {
5501 this.selection.set_value(this.get('value')[0]);
5503 this.m2o.field.relation = this.get('value')[0];
5504 this.m2o.set_value(this.get('value')[1]);
5505 this.m2o.$el.toggle(!!this.get('value')[0]);
5506 this.reference_ready = true;
5510 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5511 init: function(field_manager, node) {
5513 this._super(field_manager, node);
5514 this.binary_value = false;
5515 this.useFileAPI = !!window.FileReader;
5516 this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5517 if (!this.useFileAPI) {
5518 this.fileupload_id = _.uniqueId('oe_fileupload');
5519 $(window).on(this.fileupload_id, function() {
5520 var args = [].slice.call(arguments).slice(1);
5521 self.on_file_uploaded.apply(self, args);
5526 if (!this.useFileAPI) {
5527 $(window).off(this.fileupload_id);
5529 this._super.apply(this, arguments);
5531 initialize_content: function() {
5533 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5534 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5535 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5536 this.$el.find('.oe_form_binary_file_edit').click(function(event){
5537 self.$el.find('input.oe_form_binary_file').click();
5540 on_file_change: function(e) {
5542 var file_node = e.target;
5543 if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5544 if (this.useFileAPI) {
5545 var file = file_node.files[0];
5546 if (file.size > this.max_upload_size) {
5547 var msg = _t("The selected file exceed the maximum file size of %s.");
5548 instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5551 var filereader = new FileReader();
5552 filereader.readAsDataURL(file);
5553 filereader.onloadend = function(upload) {
5554 var data = upload.target.result;
5555 data = data.split(',')[1];
5556 self.on_file_uploaded(file.size, file.name, file.type, data);
5559 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5560 this.$el.find('form.oe_form_binary_form').submit();
5562 this.$el.find('.oe_form_binary_progress').show();
5563 this.$el.find('.oe_form_binary').hide();
5566 on_file_uploaded: function(size, name, content_type, file_base64) {
5567 if (size === false) {
5568 this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5569 // TODO: use openerp web crashmanager
5570 console.warn("Error while uploading file : ", name);
5572 this.filename = name;
5573 this.on_file_uploaded_and_valid.apply(this, arguments);
5575 this.$el.find('.oe_form_binary_progress').hide();
5576 this.$el.find('.oe_form_binary').show();
5578 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5580 on_save_as: function(ev) {
5581 var value = this.get('value');
5583 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5584 ev.stopPropagation();
5586 instance.web.blockUI();
5587 var c = instance.webclient.crashmanager;
5588 this.session.get_file({
5589 url: '/web/binary/saveas_ajax',
5590 data: {data: JSON.stringify({
5591 model: this.view.dataset.model,
5592 id: (this.view.datarecord.id || ''),
5594 filename_field: (this.node.attrs.filename || ''),
5595 data: instance.web.form.is_bin_size(value) ? null : value,
5596 context: this.view.dataset.get_context()
5598 complete: instance.web.unblockUI,
5599 error: c.rpc_error.bind(c)
5601 ev.stopPropagation();
5605 set_filename: function(value) {
5606 var filename = this.node.attrs.filename;
5609 tmp[filename] = value;
5610 this.field_manager.set_values(tmp);
5613 on_clear: function() {
5614 if (this.get('value') !== false) {
5615 this.binary_value = false;
5616 this.internal_set_value(false);
5622 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5623 template: 'FieldBinaryFile',
5624 initialize_content: function() {
5626 if (this.get("effective_readonly")) {
5628 this.$el.find('a').click(function(ev) {
5629 if (self.get('value')) {
5630 self.on_save_as(ev);
5636 render_value: function() {
5638 if (!this.get("effective_readonly")) {
5639 if (this.node.attrs.filename) {
5640 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5642 show_value = (this.get('value') !== null && this.get('value') !== undefined && this.get('value') !== false) ? this.get('value') : '';
5644 this.$el.find('input').eq(0).val(show_value);
5646 this.$el.find('a').toggle(!!this.get('value'));
5647 if (this.get('value')) {
5648 show_value = _t("Download");
5650 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5651 this.$el.find('a').text(show_value);
5655 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5656 this.binary_value = true;
5657 this.internal_set_value(file_base64);
5658 var show_value = name + " (" + instance.web.human_size(size) + ")";
5659 this.$el.find('input').eq(0).val(show_value);
5660 this.set_filename(name);
5662 on_clear: function() {
5663 this._super.apply(this, arguments);
5664 this.$el.find('input').eq(0).val('');
5665 this.set_filename('');
5669 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5670 template: 'FieldBinaryImage',
5671 placeholder: "/web/static/src/img/placeholder.png",
5672 render_value: function() {
5675 if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5676 url = 'data:image/png;base64,' + this.get('value');
5677 } else if (this.get('value')) {
5678 var id = JSON.stringify(this.view.datarecord.id || null);
5679 var field = this.name;
5680 if (this.options.preview_image)
5681 field = this.options.preview_image;
5682 url = this.session.url('/web/binary/image', {
5683 model: this.view.dataset.model,
5686 t: (new Date().getTime()),
5689 url = this.placeholder;
5691 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5692 $($img).click(function(e) {
5693 if(self.view.get("actual_mode") == "view") {
5694 var $button = $(".oe_form_button_edit");
5695 $button.openerpBounce();
5696 e.stopPropagation();
5699 this.$el.find('> img').remove();
5700 this.$el.prepend($img);
5701 $img.load(function() {
5702 if (! self.options.size)
5704 $img.css("max-width", "" + self.options.size[0] + "px");
5705 $img.css("max-height", "" + self.options.size[1] + "px");
5707 $img.on('error', function() {
5708 $img.attr('src', self.placeholder);
5709 instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5712 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5713 this.internal_set_value(file_base64);
5714 this.binary_value = true;
5715 this.render_value();
5716 this.set_filename(name);
5718 on_clear: function() {
5719 this._super.apply(this, arguments);
5720 this.render_value();
5721 this.set_filename('');
5723 set_value: function(value_){
5724 var changed = value_ !== this.get_value();
5725 this._super.apply(this, arguments);
5726 // By default, on binary images read, the server returns the binary size
5727 // This is possible that two images have the exact same size
5728 // Therefore we trigger the change in case the image value hasn't changed
5729 // So the image is re-rendered correctly
5731 this.trigger("change:value", this, {
5740 * Widget for (many2many field) to upload one or more file in same time and display in list.
5741 * The user can delete his files.
5742 * Options on attribute ; "blockui" {Boolean} block the UI or not
5743 * during the file is uploading
5745 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
5746 template: "FieldBinaryFileUploader",
5747 init: function(field_manager, node) {
5748 this._super(field_manager, node);
5749 this.field_manager = field_manager;
5751 if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5752 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);
5756 this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5757 this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5758 $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5762 this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5764 // WARNING: duplicated in 4 other M2M widgets
5765 set_value: function(value_) {
5766 value_ = value_ || [];
5767 if (value_.length >= 1 && value_[0] instanceof Array) {
5768 // value_ is a list of m2m commands. We only process
5769 // LINK_TO and REPLACE_WITH in this context
5771 _.each(value_, function (command) {
5772 if (command[0] === commands.LINK_TO) {
5773 val.push(command[1]); // (4, id[, _])
5774 } else if (command[0] === commands.REPLACE_WITH) {
5775 val = command[2]; // (6, _, ids)
5780 this._super(value_);
5782 get_value: function() {
5783 var tmp = [commands.replace_with(this.get("value"))];
5786 get_file_url: function (attachment) {
5787 return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5789 read_name_values : function () {
5791 // don't reset know values
5792 var ids = this.get('value');
5793 var _value = _.filter(ids, function (id) { return typeof self.data[id] == 'undefined'; } );
5794 // send request for get_name
5795 if (_value.length) {
5796 return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).then(function (datas) {
5797 _.each(datas, function (data) {
5798 data.no_unlink = true;
5799 data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5800 self.data[data.id] = data;
5808 render_value: function () {
5810 this.read_name_values().then(function (ids) {
5811 var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids}));
5812 render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5813 self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5815 // reinit input type file
5816 var $input = self.$('input.oe_form_binary_file');
5817 $input.after($input.clone(true)).remove();
5818 self.$(".oe_fileupload").show();
5822 on_file_change: function (event) {
5823 event.stopPropagation();
5825 var $target = $(event.target);
5826 if ($target.val() !== '') {
5827 var filename = $target.val().replace(/.*[\\\/]/,'');
5828 // don't uplode more of one file in same time
5829 if (self.data[0] && self.data[0].upload ) {
5832 for (var id in this.get('value')) {
5833 // if the files exits, delete the file before upload (if it's a new file)
5834 if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
5835 self.ds_file.unlink([id]);
5840 if(this.node.attrs.blockui>0) {
5841 instance.web.blockUI();
5844 // TODO : unactivate send on wizard and form
5847 this.$('form.oe_form_binary_form').submit();
5848 this.$(".oe_fileupload").hide();
5849 // add file on data result
5853 'filename': filename,
5859 on_file_loaded: function (event, result) {
5860 var files = this.get('value');
5863 if(this.node.attrs.blockui>0) {
5864 instance.web.unblockUI();
5867 if (result.error || !result.id ) {
5868 this.do_warn( _t('Uploading Error'), result.error);
5869 delete this.data[0];
5871 if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
5872 delete this.data[0];
5873 this.data[result.id] = {
5875 'name': result.name,
5876 'filename': result.filename,
5877 'url': this.get_file_url(result)
5880 this.data[result.id] = {
5882 'name': result.name,
5883 'filename': result.filename,
5884 'url': this.get_file_url(result)
5887 var values = _.clone(this.get('value'));
5888 values.push(result.id);
5889 this.set({'value': values});
5891 this.render_value();
5893 on_file_delete: function (event) {
5894 event.stopPropagation();
5895 var file_id=$(event.target).data("id");
5897 var files = _.filter(this.get('value'), function (id) {return id != file_id;});
5898 if(!this.data[file_id].no_unlink) {
5899 this.ds_file.unlink([file_id]);
5901 this.set({'value': files});
5906 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5907 template: "FieldStatus",
5908 init: function(field_manager, node) {
5909 this._super(field_manager, node);
5910 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5911 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5912 this.set({value: false});
5913 this.selection = {'unfolded': [], 'folded': []};
5914 this.set("selection", {'unfolded': [], 'folded': []});
5915 this.selection_dm = new instance.web.DropMisordered();
5916 this.dataset = new instance.web.DataSetStatic(this, this.field.relation, this.build_context());
5919 this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5921 this.on("change:value", this, this.get_selection);
5922 this.on("change:evaluated_selection_domain", this, this.get_selection);
5923 this.on("change:selection", this, function() {
5924 this.selection = this.get("selection");
5925 this.render_value();
5927 this.get_selection();
5928 if (this.options.clickable) {
5929 this.$el.on('click','li[data-id]',this.on_click_stage);
5931 if (this.$el.parent().is('header')) {
5932 this.$el.after('<div class="oe_clear"/>');
5936 set_value: function(value_) {
5937 if (value_ instanceof Array) {
5940 this._super(value_);
5942 render_value: function() {
5944 var content = QWeb.render("FieldStatus.content", {
5946 'value_folded': _.find(self.selection.folded, function(i){return i[0] === self.get('value');})
5948 self.$el.html(content);
5950 calc_domain: function() {
5951 var d = instance.web.pyeval.eval('domain', this.build_domain());
5952 var domain = []; //if there is no domain defined, fetch all the records
5955 domain = ['|',['id', '=', this.get('value')]].concat(d);
5958 if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5959 this.set("evaluated_selection_domain", domain);
5962 /** Get the selection and render it
5963 * selection: [[identifier, value_to_display], ...]
5964 * For selection fields: this is directly given by this.field.selection
5965 * For many2one fields: perform a search on the relation of the many2one field
5967 get_selection: function() {
5969 var selection_unfolded = [];
5970 var selection_folded = [];
5971 var fold_field = this.options.fold_field;
5973 var calculation = _.bind(function() {
5974 if (this.field.type == "many2one") {
5975 return self.get_distant_fields().then(function (fields) {
5976 return new instance.web.DataSetSearch(self, self.field.relation, self.build_context(), self.get("evaluated_selection_domain"))
5977 .read_slice(_.union(_.keys(self.distant_fields), ['id']), {}).then(function (records) {
5978 var ids = _.pluck(records, 'id');
5979 return self.dataset.name_get(ids).then(function (records_name) {
5980 _.each(records, function (record) {
5981 var name = _.find(records_name, function (val) {return val[0] == record.id;})[1];
5982 if (fold_field && record[fold_field] && record.id != self.get('value')) {
5983 selection_folded.push([record.id, name]);
5985 selection_unfolded.push([record.id, name]);
5992 // For field type selection filter values according to
5993 // statusbar_visible attribute of the field. For example:
5994 // statusbar_visible="draft,open".
5995 var select = this.field.selection;
5996 for(var i=0; i < select.length; i++) {
5997 var key = select[i][0];
5998 if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5999 selection_unfolded.push(select[i]);
6005 this.selection_dm.add(calculation()).then(function () {
6006 var selection = {'unfolded': selection_unfolded, 'folded': selection_folded};
6007 if (! _.isEqual(selection, self.get("selection"))) {
6008 self.set("selection", selection);
6013 * :deprecated: this feature will probably be removed with OpenERP v8
6015 get_distant_fields: function() {
6017 if (! this.options.fold_field) {
6018 this.distant_fields = {}
6020 if (this.distant_fields) {
6021 return $.when(this.distant_fields);
6023 return new instance.web.Model(self.field.relation).call("fields_get", [[this.options.fold_field]]).then(function(fields) {
6024 self.distant_fields = fields;
6028 on_click_stage: function (ev) {
6030 var $li = $(ev.currentTarget);
6032 if (this.field.type == "many2one") {
6033 val = parseInt($li.data("id"), 10);
6036 val = $li.data("id");
6038 if (val != self.get('value')) {
6039 this.view.recursive_save().done(function() {
6041 change[self.name] = val;
6042 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
6050 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
6051 template: "FieldMonetary",
6052 widget_class: 'oe_form_field_float oe_form_field_monetary',
6054 this._super.apply(this, arguments);
6055 this.set({"currency": false});
6056 if (this.options.currency_field) {
6057 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
6058 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
6061 this.on("change:currency", this, this.get_currency_info);
6062 this.get_currency_info();
6063 this.ci_dm = new instance.web.DropMisordered();
6066 var tmp = this._super();
6067 this.on("change:currency_info", this, this.reinitialize);
6070 get_currency_info: function() {
6072 if (this.get("currency") === false) {
6073 this.set({"currency_info": null});
6076 return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
6077 .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
6078 self.set({"currency_info": res});
6081 parse_value: function(val, def) {
6082 return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6084 format_value: function(val, def) {
6085 return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6090 This type of field display a list of checkboxes. It works only with m2ms. This field will display one checkbox for each
6091 record existing in the model targeted by the relation, according to the given domain if one is specified. Checked records
6092 will be added to the relation.
6094 instance.web.form.FieldMany2ManyCheckBoxes = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6095 className: "oe_form_many2many_checkboxes",
6097 this._super.apply(this, arguments);
6098 this.set("value", {});
6099 this.set("records", []);
6100 this.field_manager.on("view_content_has_changed", this, function() {
6101 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
6102 if (! _.isEqual(domain, this.get("domain"))) {
6103 this.set("domain", domain);
6106 this.records_orderer = new instance.web.DropMisordered();
6108 initialize_field: function() {
6109 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
6110 this.on("change:domain", this, this.query_records);
6111 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
6112 this.on("change:records", this, this.render_value);
6114 query_records: function() {
6116 var model = new openerp.Model(openerp.session, this.field.relation);
6117 this.records_orderer.add(model.call("search", [this.get("domain")], {"context": this.build_context()}).then(function(record_ids) {
6118 return model.call("name_get", [record_ids] , {"context": self.build_context()});
6119 })).then(function(res) {
6120 self.set("records", res);
6123 render_value: function() {
6124 this.$().html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this, selected: this.get("value")}));
6125 var inputs = this.$("input");
6126 inputs.change(_.bind(this.from_dom, this));
6127 if (this.get("effective_readonly"))
6128 inputs.attr("disabled", "true");
6130 from_dom: function() {
6132 this.$("input").each(function() {
6134 new_value[elem.data("record-id")] = elem.attr("checked") ? true : undefined;
6136 if (! _.isEqual(new_value, this.get("value")))
6137 this.internal_set_value(new_value);
6139 // WARNING: (mostly) duplicated in 4 other M2M widgets
6140 set_value: function(value_) {
6141 value_ = value_ || [];
6142 if (value_.length >= 1 && value_[0] instanceof Array) {
6143 // value_ is a list of m2m commands. We only process
6144 // LINK_TO and REPLACE_WITH in this context
6146 _.each(value_, function (command) {
6147 if (command[0] === commands.LINK_TO) {
6148 val.push(command[1]); // (4, id[, _])
6149 } else if (command[0] === commands.REPLACE_WITH) {
6150 val = command[2]; // (6, _, ids)
6156 _.each(value_, function(el) {
6157 formatted[JSON.stringify(el)] = true;
6159 this._super(formatted);
6161 get_value: function() {
6162 var value = _.filter(_.keys(this.get("value")), function(el) {
6163 return this.get("value")[el];
6165 value = _.map(value, function(el) {
6166 return JSON.parse(el);
6168 return [commands.replace_with(value)];
6173 This field can be applied on many2many and one2many. It is a read-only field that will display a single link whose name is
6174 "<number of linked records> <label of the field>". When the link is clicked, it will redirect to another act_window
6175 action on the model of the relation and show only the linked records.
6179 * 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
6180 to display (or False to take the default one) and the second element is the type of the view. Defaults to
6181 [[false, "tree"], [false, "form"]] .
6183 instance.web.form.X2ManyCounter = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6184 className: "oe_form_x2many_counter",
6186 this._super.apply(this, arguments);
6187 this.set("value", []);
6188 _.defaults(this.options, {
6189 "views": [[false, "tree"], [false, "form"]],
6192 render_value: function() {
6193 var text = _.str.sprintf("%d %s", this.val().length, this.string);
6194 this.$().html(QWeb.render("X2ManyCounter", {text: text}));
6195 this.$("a").click(_.bind(this.go_to, this));
6198 return this.view.recursive_save().then(_.bind(function() {
6199 var val = this.val();
6201 if (this.field.type === "one2many") {
6202 context["default_" + this.field.relation_field] = this.view.datarecord.id;
6204 var domain = [["id", "in", val]];
6205 return this.do_action({
6206 type: 'ir.actions.act_window',
6208 res_model: this.field.relation,
6209 views: this.options.views,
6217 var value = this.get("value") || [];
6218 if (value.length >= 1 && value[0] instanceof Array) {
6219 value = value[0][2];
6226 This widget is intended to be used on stat button numeric fields. It will display
6227 the value many2many and one2many. It is a read-only field that will
6228 display a simple string "<value of field> <label of the field>"
6230 instance.web.form.StatInfo = instance.web.form.AbstractField.extend({
6231 is_field_number: true,
6233 this._super.apply(this, arguments);
6234 this.internal_set_value(0);
6236 set_value: function(value_) {
6237 if (value_ === false || value_ === undefined) {
6240 this._super.apply(this, [value_]);
6242 render_value: function() {
6244 value: this.get("value") || 0,
6246 if (! this.node.attrs.nolabel) {
6247 options.text = this.string
6249 this.$el.html(QWeb.render("StatInfo", options));
6256 * Registry of form fields, called by :js:`instance.web.FormView`.
6258 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
6259 * will substitute to the <field> tags as defined in OpenERP's views.
6261 instance.web.form.widgets = new instance.web.Registry({
6262 'char' : 'instance.web.form.FieldChar',
6263 'id' : 'instance.web.form.FieldID',
6264 'email' : 'instance.web.form.FieldEmail',
6265 'url' : 'instance.web.form.FieldUrl',
6266 'text' : 'instance.web.form.FieldText',
6267 'html' : 'instance.web.form.FieldTextHtml',
6268 'char_domain': 'instance.web.form.FieldCharDomain',
6269 'date' : 'instance.web.form.FieldDate',
6270 'datetime' : 'instance.web.form.FieldDatetime',
6271 'selection' : 'instance.web.form.FieldSelection',
6272 'radio' : 'instance.web.form.FieldRadio',
6273 'many2one' : 'instance.web.form.FieldMany2One',
6274 'many2onebutton' : 'instance.web.form.Many2OneButton',
6275 'many2many' : 'instance.web.form.FieldMany2Many',
6276 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
6277 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
6278 'one2many' : 'instance.web.form.FieldOne2Many',
6279 'one2many_list' : 'instance.web.form.FieldOne2Many',
6280 'reference' : 'instance.web.form.FieldReference',
6281 'boolean' : 'instance.web.form.FieldBoolean',
6282 'float' : 'instance.web.form.FieldFloat',
6283 'percentpie': 'instance.web.form.FieldPercentPie',
6284 'barchart': 'instance.web.form.FieldBarChart',
6285 'integer': 'instance.web.form.FieldFloat',
6286 'float_time': 'instance.web.form.FieldFloat',
6287 'progressbar': 'instance.web.form.FieldProgressBar',
6288 'image': 'instance.web.form.FieldBinaryImage',
6289 'binary': 'instance.web.form.FieldBinaryFile',
6290 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
6291 'statusbar': 'instance.web.form.FieldStatus',
6292 'monetary': 'instance.web.form.FieldMonetary',
6293 'many2many_checkboxes': 'instance.web.form.FieldMany2ManyCheckBoxes',
6294 'x2many_counter': 'instance.web.form.X2ManyCounter',
6295 'priority':'instance.web.form.Priority',
6296 'kanban_state_selection':'instance.web.form.KanbanSelection',
6297 'statinfo': 'instance.web.form.StatInfo',
6301 * Registry of widgets usable in the form view that can substitute to any possible
6302 * tags defined in OpenERP's form views.
6304 * Every referenced class should extend FormWidget.
6306 instance.web.form.tags = new instance.web.Registry({
6307 'button' : 'instance.web.form.WidgetButton',
6310 instance.web.form.custom_widgets = new instance.web.Registry({
6315 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: