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 {Object} values A dictonary 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 {Array} 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.onchanges_defs = [];
107 this.default_focus_field = null;
108 this.default_focus_button = null;
109 this.fields_registry = instance.web.form.widgets;
110 this.tags_registry = instance.web.form.tags;
111 this.widgets_registry = instance.web.form.custom_widgets;
112 this.has_been_loaded = $.Deferred();
113 this.translatable_fields = [];
114 _.defaults(this.options, {
115 "not_interactible_on_create": false,
116 "initial_mode": "view",
117 "disable_autofocus": false,
118 "footer_to_buttons": false,
120 this.is_initialized = $.Deferred();
121 this.mutating_mutex = new $.Mutex();
123 this.render_value_defs = [];
124 this.reload_mutex = new $.Mutex();
125 this.__clicked_inside = false;
126 this.__blur_timeout = null;
127 this.rendering_engine = new instance.web.form.FormRenderingEngine(this);
128 self.set({actual_mode: self.options.initial_mode});
129 this.has_been_loaded.done(function() {
130 self._build_onchange_specs();
131 self.on("change:actual_mode", self, self.check_actual_mode);
132 self.check_actual_mode();
133 self.on("change:actual_mode", self, self.init_pager);
136 self.on("load_record", self, self.load_record);
137 instance.web.bus.on('clear_uncommitted_changes', this, function(e) {
138 if (!this.can_be_discarded()) {
143 view_loading: function(r) {
144 return this.load_form(r);
146 destroy: function() {
147 _.each(this.get_widgets(), function(w) {
148 w.off('focused blurred');
152 this.$el.off('.formBlur');
156 load_form: function(data) {
159 throw new Error(_t("No data provided."));
162 throw "Form view does not support multiple calls to load_form";
164 this.fields_order = [];
165 this.fields_view = data;
167 this.rendering_engine.set_fields_registry(this.fields_registry);
168 this.rendering_engine.set_tags_registry(this.tags_registry);
169 this.rendering_engine.set_widgets_registry(this.widgets_registry);
170 this.rendering_engine.set_fields_view(data);
171 var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container');
172 this.rendering_engine.render_to($dest);
174 this.$el.on('mousedown.formBlur', function () {
175 self.__clicked_inside = true;
178 this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self}));
179 if (this.options.$buttons) {
180 this.$buttons.appendTo(this.options.$buttons);
182 this.$el.find('.oe_form_buttons').replaceWith(this.$buttons);
184 this.$buttons.on('click', '.oe_form_button_create',
185 this.guard_active(this.on_button_create));
186 this.$buttons.on('click', '.oe_form_button_edit',
187 this.guard_active(this.on_button_edit));
188 this.$buttons.on('click', '.oe_form_button_save',
189 this.guard_active(this.on_button_save));
190 this.$buttons.on('click', '.oe_form_button_cancel',
191 this.guard_active(this.on_button_cancel));
192 if (this.options.footer_to_buttons) {
193 this.$el.find('footer').appendTo(this.$buttons);
196 this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar');
197 if (!this.sidebar && this.options.$sidebar) {
198 this.sidebar = new instance.web.Sidebar(this);
199 this.sidebar.appendTo(this.$sidebar);
200 if (this.fields_view.toolbar) {
201 this.sidebar.add_toolbar(this.fields_view.toolbar);
203 this.sidebar.add_items('other', _.compact([
204 self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete },
205 self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate }
209 this.has_been_loaded.resolve();
211 // Add bounce effect on button 'Edit' when click on readonly page view.
212 this.$el.find(".oe_form_group_row,.oe_form_field,label,h1,.oe_title,.oe_notebook_page, .oe_list_content").on('click', function (e) {
213 if(self.get("actual_mode") == "view") {
214 var $button = self.options.$buttons.find(".oe_form_button_edit");
215 $button.openerpBounce();
217 instance.web.bus.trigger('click', e);
220 //bounce effect on red button when click on statusbar.
221 this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) {
222 if((self.get("actual_mode") == "view")) {
223 var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'});
224 $button.openerpBounce();
228 this.trigger('form_view_loaded', data);
231 widgetFocused: function() {
232 // Clear click flag if used to focus a widget
233 this.__clicked_inside = false;
234 if (this.__blur_timeout) {
235 clearTimeout(this.__blur_timeout);
236 this.__blur_timeout = null;
239 widgetBlurred: function() {
240 if (this.__clicked_inside) {
241 // clicked in an other section of the form (than the currently
242 // focused widget) => just ignore the blurring entirely?
243 this.__clicked_inside = false;
247 // clear timeout, if any
248 this.widgetFocused();
249 this.__blur_timeout = setTimeout(function () {
250 self.trigger('blurred');
254 do_load_state: function(state, warm) {
255 if (state.id && this.datarecord.id != state.id) {
256 if (this.dataset.get_id_index(state.id) === null) {
257 this.dataset.ids.push(state.id);
259 this.dataset.select_id(state.id);
265 * @param {Object} [options]
266 * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view".
267 * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record
268 * @return {$.Deferred}
270 do_show: function (options) {
272 options = options || {};
274 this.sidebar.$el.show();
277 this.$buttons.show();
279 this.$el.show().css({
281 filter: 'alpha(opacity = 0)'
283 this.$el.add(this.$buttons).removeClass('oe_form_dirty');
285 var shown = this.has_been_loaded;
286 if (options.reload !== false) {
287 shown = shown.then(function() {
288 if (self.dataset.index === null) {
289 // null index means we should start a new record
290 return self.on_button_new();
292 var fields = _.keys(self.fields_view.fields);
293 fields.push('display_name');
294 return self.dataset.read_index(fields, {
295 context: { 'bin_size': true, 'future_display_name' : true }
296 }).then(function(r) {
297 self.trigger('load_record', r);
301 return shown.then(function() {
302 self._actualize_mode(options.mode || self.options.initial_mode);
305 filter: 'alpha(opacity = 100)'
309 do_hide: function () {
311 this.sidebar.$el.hide();
314 this.$buttons.hide();
321 load_record: function(record) {
322 var self = this, set_values = [];
324 this.set({ 'title' : undefined });
325 this.do_warn(_t("Form"), _t("The record could not be found in the database."), true);
326 return $.Deferred().reject();
328 this.datarecord = record;
329 this._actualize_mode();
330 this.set({ 'title' : record.id ? record.display_name : _t("New") });
332 _(this.fields).each(function (field, f) {
333 field._dirty_flag = false;
334 field._inhibit_on_change_flag = true;
335 var result = field.set_value(self.datarecord[f] || false);
336 field._inhibit_on_change_flag = false;
337 set_values.push(result);
339 return $.when.apply(null, set_values).then(function() {
342 self.do_onchange(null);
344 self.on_form_changed();
345 self.rendering_engine.init_fields();
346 self.is_initialized.resolve();
347 self.do_update_pager(record.id === null || record.id === undefined);
349 self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
352 self.do_push_state({id:record.id});
354 self.do_push_state({});
356 self.$el.add(self.$buttons).removeClass('oe_form_dirty');
361 * Loads and sets up the default values for the model as the current
364 * @return {$.Deferred}
366 load_defaults: function () {
368 var keys = _.keys(this.fields_view.fields);
370 return this.dataset.default_get(keys).then(function(r) {
371 self.trigger('load_record', r);
374 return self.trigger('load_record', {});
376 on_form_changed: function() {
377 this.trigger("view_content_has_changed");
379 do_notify_change: function() {
380 this.$el.add(this.$buttons).addClass('oe_form_dirty');
382 execute_pager_action: function(action) {
383 if (this.can_be_discarded()) {
386 this.dataset.index = 0;
389 this.dataset.previous();
395 this.dataset.index = this.dataset.ids.length - 1;
398 var def = this.reload();
399 this.trigger('pager_action_executed');
404 init_pager: function() {
407 this.$pager.remove();
408 if (this.get("actual_mode") === "create")
410 this.$pager = $(QWeb.render("FormView.pager", {'widget':self})).hide();
411 if (this.options.$pager) {
412 this.$pager.appendTo(this.options.$pager);
414 this.$el.find('.oe_form_pager').replaceWith(this.$pager);
416 this.$pager.on('click','a[data-pager-action]',function() {
418 if ($el.attr("disabled"))
420 var action = $el.data('pager-action');
421 var def = $.when(self.execute_pager_action(action));
422 $el.attr("disabled");
423 def.always(function() {
424 $el.removeAttr("disabled");
427 this.do_update_pager();
429 do_update_pager: function(hide_index) {
430 this.$pager.toggle(this.dataset.ids.length > 1);
432 $(".oe_form_pager_state", this.$pager).html("");
434 $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
438 _build_onchange_specs: function() {
440 var find = function(field_name, root) {
442 while (fields.length) {
443 var node = fields.pop();
447 if (node.tag === 'field' && node.attrs.name === field_name) {
448 return node.attrs.on_change || "";
450 fields = _.union(fields, node.children);
455 self._onchange_specs = {};
456 _.each(this.fields, function(field, name) {
457 self._onchange_specs[name] = find(name, field.node);
458 _.each(field.field.views, function(view) {
459 _.each(view.fields, function(_, subname) {
460 self._onchange_specs[name + '.' + subname] = find(subname, view.arch);
465 _get_onchange_values: function() {
466 var field_values = this.get_fields_values();
467 if (field_values.id.toString().match(instance.web.BufferedDataSet.virtual_id_regex)) {
468 delete field_values.id;
470 if (this.dataset.parent_view) {
471 // this belongs to a parent view: add parent field if possible
472 var parent_view = this.dataset.parent_view;
473 var child_name = this.dataset.child_name;
474 var parent_name = parent_view.get_field_desc(child_name).relation_field;
476 // consider all fields except the inverse of the parent field
477 var parent_values = parent_view.get_fields_values();
478 delete parent_values[child_name];
479 field_values[parent_name] = parent_values;
485 do_onchange: function(widget) {
487 var onchange_specs = self._onchange_specs;
489 var def = $.when({});
490 var change_spec = widget ? onchange_specs[widget.name] : null;
491 if (!widget || (!_.isEmpty(change_spec) && change_spec !== "0")) {
493 trigger_field_name = widget ? widget.name : false,
494 values = self._get_onchange_values(),
495 context = new instance.web.CompoundContext(self.dataset.get_context());
497 if (widget && widget.build_context()) {
498 context.add(widget.build_context());
500 if (self.dataset.parent_view) {
501 var parent_name = self.dataset.parent_view.get_field_desc(self.dataset.child_name).relation_field;
502 context.add({field_parent: parent_name});
505 if (self.datarecord.id && !instance.web.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) {
506 // In case of a o2m virtual id, we should pass an empty ids list
507 ids.push(self.datarecord.id);
509 def = self.alive(new instance.web.Model(self.dataset.model).call(
510 "onchange", [ids, values, trigger_field_name, onchange_specs, context]));
512 var onchange_def = def.then(function(response) {
513 if (widget && widget.field['change_default']) {
514 var fieldname = widget.name;
516 if (response.value && (fieldname in response.value)) {
517 // Use value from onchange if onchange executed
518 value_ = response.value[fieldname];
520 // otherwise get form value for field
521 value_ = self.fields[fieldname].get_value();
523 var condition = fieldname + '=' + value_;
526 return self.alive(new instance.web.Model('ir.values').call(
527 'get_defaults', [self.model, condition]
528 )).then(function (results) {
529 if (!results.length) {
532 if (!response.value) {
535 for(var i=0; i<results.length; ++i) {
536 // [whatever, key, value]
537 var triplet = results[i];
538 response.value[triplet[1]] = triplet[2];
545 }).then(function(response) {
546 return self.on_processed_onchange(response);
548 this.onchanges_defs.push(onchange_def);
552 instance.webclient.crashmanager.show_message(e);
553 return $.Deferred().reject();
556 on_processed_onchange: function(result) {
558 var fields = this.fields;
559 _(result.domain).each(function (domain, fieldname) {
560 var field = fields[fieldname];
561 if (!field) { return; }
562 field.node.attrs.domain = domain;
565 if (!_.isEmpty(result.value)) {
566 this._internal_set_values(result.value);
568 // FIXME XXX a list of warnings?
569 if (!_.isEmpty(result.warning)) {
570 new instance.web.Dialog(this, {
572 title:result.warning.title,
574 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
576 }, QWeb.render("CrashManager.warning", result.warning)).open();
579 return $.Deferred().resolve();
582 instance.webclient.crashmanager.show_message(e);
583 return $.Deferred().reject();
586 _process_operations: function() {
588 return this.mutating_mutex.exec(function() {
590 var start = $.Deferred();
592 start = _.reduce(self.onchanges_defs, function(memo, d){
593 return memo.then(function(){
600 _.each(self.fields, function(field) {
601 defs.push(field.commit_value());
603 var args = _.toArray(arguments);
604 return $.when.apply($, defs).then(function() {
605 var save_obj = self.save_list.pop();
607 return self._process_save(save_obj).then(function() {
608 save_obj.ret = _.toArray(arguments);
611 save_obj.error = true;
616 self.save_list.pop();
623 _internal_set_values: function(values) {
624 for (var f in values) {
625 if (!values.hasOwnProperty(f)) { continue; }
626 var field = this.fields[f];
627 // If field is not defined in the view, just ignore it
629 var value_ = values[f];
630 if (field.get_value() != value_) {
631 field._inhibit_on_change_flag = true;
632 field.set_value(value_);
633 field._inhibit_on_change_flag = false;
634 field._dirty_flag = true;
638 this.on_form_changed();
640 set_values: function(values) {
642 return this.mutating_mutex.exec(function() {
643 self._internal_set_values(values);
647 * Ask the view to switch to view mode if possible. The view may not do it
648 * if the current record is not yet saved. It will then stay in create mode.
650 to_view_mode: function() {
651 this._actualize_mode("view");
654 * Ask the view to switch to edit mode if possible. The view may not do it
655 * if the current record is not yet saved. It will then stay in create mode.
657 to_edit_mode: function() {
658 this.onchanges_defs = [];
659 this._actualize_mode("edit");
662 * Ask the view to switch to a precise mode if possible. The view is free to
663 * not respect this command if the state of the dataset is not compatible with
664 * the new mode. For example, it is not possible to switch to edit mode if
665 * the current record is not yet saved in database.
667 * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
668 * undefined the view will test the actual mode to check if it is still consistent
669 * with the dataset state.
671 _actualize_mode: function(switch_to) {
672 var mode = switch_to || this.get("actual_mode");
673 if (! this.datarecord.id) {
675 } else if (mode === "create") {
678 this.render_value_defs = [];
679 this.set({actual_mode: mode});
681 check_actual_mode: function(source, options) {
683 if(this.get("actual_mode") === "view") {
684 self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
685 self.$buttons.find('.oe_form_buttons_edit').hide();
686 self.$buttons.find('.oe_form_buttons_view').show();
687 self.$sidebar.show();
689 self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
690 self.$buttons.find('.oe_form_buttons_edit').show();
691 self.$buttons.find('.oe_form_buttons_view').hide();
692 self.$sidebar.hide();
696 autofocus: function() {
697 if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
698 var fields_order = this.fields_order.slice(0);
699 if (this.default_focus_field) {
700 fields_order.unshift(this.default_focus_field.name);
702 for (var i = 0; i < fields_order.length; i += 1) {
703 var field = this.fields[fields_order[i]];
704 if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
705 if (field.focus() !== false) {
712 on_button_save: function(e) {
714 $(e.target).attr("disabled", true);
715 return this.save().done(function(result) {
716 self.trigger("save", result);
717 self.reload().then(function() {
719 var menu = instance.webclient.menu;
721 menu.do_reload_needaction();
724 }).always(function(){
725 $(e.target).attr("disabled", false);
728 on_button_cancel: function(event) {
730 if (this.can_be_discarded()) {
731 if (this.get('actual_mode') === 'create') {
732 this.trigger('history_back');
735 $.when.apply(null, this.render_value_defs).then(function(){
736 self.trigger('load_record', self.datarecord);
740 this.trigger('on_button_cancel');
743 on_button_new: function() {
746 return $.when(this.has_been_loaded).then(function() {
747 if (self.can_be_discarded()) {
748 return self.load_defaults();
752 on_button_edit: function() {
753 return this.to_edit_mode();
755 on_button_create: function() {
756 this.dataset.index = null;
759 on_button_duplicate: function() {
761 return this.has_been_loaded.then(function() {
762 return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
763 self.record_created(new_id);
768 on_button_delete: function() {
770 var def = $.Deferred();
771 this.has_been_loaded.done(function() {
772 if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
773 self.dataset.unlink([self.datarecord.id]).done(function() {
774 if (self.dataset.size()) {
775 self.execute_pager_action('next');
777 self.do_action('history_back');
782 $.async_when().done(function () {
787 return def.promise();
789 can_be_discarded: function() {
790 if (this.$el.is('.oe_form_dirty')) {
791 if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
794 this.$el.removeClass('oe_form_dirty');
799 * Triggers saving the form's record. Chooses between creating a new
800 * record or saving an existing one depending on whether the record
801 * already has an id property.
803 * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
804 * record, should that record be inserted at the start of the dataset (by
805 * default, records are added at the end)
807 save: function(prepend_on_create) {
809 var save_obj = {prepend_on_create: prepend_on_create, ret: null};
810 this.save_list.push(save_obj);
811 return self._process_operations().then(function() {
813 return $.Deferred().reject();
814 return $.when.apply($, save_obj.ret);
815 }).done(function(result) {
816 self.$el.removeClass('oe_form_dirty');
819 _process_save: function(save_obj) {
821 var prepend_on_create = save_obj.prepend_on_create;
823 var form_invalid = false,
825 first_invalid_field = null,
826 readonly_values = {};
827 for (var f in self.fields) {
828 if (!self.fields.hasOwnProperty(f)) { continue; }
832 if (!first_invalid_field) {
833 first_invalid_field = f;
835 } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
836 // Special case 'id' field, do not save this field
837 // on 'create' : save all non readonly fields
838 // on 'edit' : save non readonly modified fields
839 if (!f.get("readonly")) {
840 values[f.name] = f.get_value();
842 readonly_values[f.name] = f.get_value();
847 self.set({'display_invalid_fields': true});
848 first_invalid_field.focus();
850 return $.Deferred().reject();
852 self.set({'display_invalid_fields': false});
854 if (!self.datarecord.id) {
856 save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
857 return self.record_created(r, prepend_on_create);
859 } else if (_.isEmpty(values)) {
860 // Not dirty, noop save
861 save_deferral = $.Deferred().resolve({}).promise();
864 save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
865 return self.record_saved(r);
868 return save_deferral;
872 return $.Deferred().reject();
875 on_invalid: function() {
876 var warnings = _(this.fields).chain()
877 .filter(function (f) { return !f.is_valid(); })
879 return _.str.sprintf('<li>%s</li>',
882 warnings.unshift('<ul>');
883 warnings.push('</ul>');
884 this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
887 * Reload the form after saving
889 * @param {Object} r result of the write function.
891 record_saved: function(r) {
892 this.trigger('record_saved', r);
894 // should not happen in the server, but may happen for internal purpose
895 return $.Deferred().reject();
900 * Updates the form' dataset to contain the new record:
902 * * Adds the newly created record to the current dataset (at the end by
904 * * Selects that record (sets the dataset's index to point to the new
906 * * Updates the pager and sidebar displays
909 * @param {Boolean} [prepend_on_create=false] adds the newly created record
910 * at the beginning of the dataset instead of the end
912 record_created: function(r, prepend_on_create) {
915 // should not happen in the server, but may happen for internal purpose
916 this.trigger('record_created', r);
917 return $.Deferred().reject();
919 this.datarecord.id = r;
920 if (!prepend_on_create) {
921 this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
922 this.dataset.index = this.dataset.ids.length - 1;
924 this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
925 this.dataset.index = 0;
927 this.do_update_pager();
929 this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
931 //openerp.log("The record has been created with id #" + this.datarecord.id);
932 return $.when(this.reload()).then(function () {
933 self.trigger('record_created', r);
934 return _.extend(r, {created: true});
938 on_action: function (action) {
939 console.debug('Executing action', action);
943 return this.reload_mutex.exec(function() {
944 if (self.dataset.index === null || self.dataset.index === undefined) {
945 self.trigger("previous_view");
946 return $.Deferred().reject().promise();
948 if (self.dataset.index < 0) {
949 return $.when(self.on_button_new());
951 var fields = _.keys(self.fields_view.fields);
952 fields.push('display_name');
953 return self.dataset.read_index(fields,
957 'future_display_name': true
959 check_access_rule: true
960 }).then(function(r) {
961 self.trigger('load_record', r);
963 self.do_action('history_back');
968 get_widgets: function() {
969 return _.filter(this.getChildren(), function(obj) {
970 return obj instanceof instance.web.form.FormWidget;
973 get_fields_values: function() {
975 var ids = this.get_selected_ids();
976 values["id"] = ids.length > 0 ? ids[0] : false;
977 _.each(this.fields, function(value_, key) {
978 values[key] = value_.get_value();
982 get_selected_ids: function() {
983 var id = this.dataset.ids[this.dataset.index];
984 return id ? [id] : [];
986 recursive_save: function() {
988 return $.when(this.save()).then(function(res) {
989 if (self.dataset.parent_view)
990 return self.dataset.parent_view.recursive_save();
993 recursive_reload: function() {
996 if (self.dataset.parent_view)
997 pre = self.dataset.parent_view.recursive_reload();
998 return pre.then(function() {
999 return self.reload();
1002 is_dirty: function() {
1003 return _.any(this.fields, function (value_) {
1004 return value_._dirty_flag;
1007 is_interactible_record: function() {
1008 var id = this.datarecord.id;
1010 if (this.options.not_interactible_on_create)
1012 } else if (typeof(id) === "string") {
1013 if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
1018 sidebar_eval_context: function () {
1019 return $.when(this.build_eval_context());
1021 open_defaults_dialog: function () {
1023 var display = function (field, value) {
1024 if (!value) { return value; }
1025 if (field instanceof instance.web.form.FieldSelection) {
1026 return _(field.get('values')).find(function (option) {
1027 return option[0] === value;
1029 } else if (field instanceof instance.web.form.FieldMany2One) {
1030 return field.get_displayed();
1034 var fields = _.chain(this.fields)
1035 .map(function (field) {
1036 var value = field.get_value();
1037 // ignore fields which are empty, invisible, readonly, o2m
1040 || field.get('invisible')
1041 || field.get("readonly")
1042 || field.field.type === 'one2many'
1043 || field.field.type === 'many2many'
1044 || field.field.type === 'binary'
1045 || field.password) {
1051 string: field.string,
1053 displayed: display(field, value),
1057 .sortBy(function (field) { return field.string; })
1059 var conditions = _.chain(self.fields)
1060 .filter(function (field) { return field.field.change_default; })
1061 .map(function (field) {
1062 var value = field.get_value();
1065 string: field.string,
1067 displayed: display(field, value),
1071 var d = new instance.web.Dialog(this, {
1072 title: _t("Set Default"),
1075 conditions: conditions
1078 {text: _t("Close"), click: function () { d.close(); }},
1079 {text: _t("Save default"), click: function () {
1080 var $defaults = d.$el.find('#formview_default_fields');
1081 var field_to_set = $defaults.val();
1082 if (!field_to_set) {
1083 $defaults.parent().addClass('oe_form_invalid');
1086 var condition = d.$el.find('#formview_default_conditions').val(),
1087 all_users = d.$el.find('#formview_default_all').is(':checked');
1088 new instance.web.DataSet(self, 'ir.values').call(
1092 self.fields[field_to_set].get_value(),
1096 ]).done(function () { d.close(); });
1100 d.template = 'FormView.set_default';
1103 register_field: function(field, name) {
1104 this.fields[name] = field;
1105 this.fields_order.push(name);
1106 if (JSON.parse(field.node.attrs.default_focus || "0")) {
1107 this.default_focus_field = field;
1110 field.on('focused', null, this.proxy('widgetFocused'))
1111 .on('blurred', null, this.proxy('widgetBlurred'));
1112 if (this.get_field_desc(name).translate) {
1113 this.translatable_fields.push(field);
1115 field.on('changed_value', this, function() {
1116 if (field.is_syntax_valid()) {
1117 this.trigger('field_changed:' + name);
1119 if (field._inhibit_on_change_flag) {
1122 field._dirty_flag = true;
1123 if (field.is_syntax_valid()) {
1124 this.do_onchange(field);
1125 this.on_form_changed(true);
1126 this.do_notify_change();
1130 get_field_desc: function(field_name) {
1131 return this.fields_view.fields[field_name];
1133 get_field_value: function(field_name) {
1134 return this.fields[field_name].get_value();
1136 compute_domain: function(expression) {
1137 return instance.web.form.compute_domain(expression, this.fields);
1139 _build_view_fields_values: function() {
1140 var a_dataset = this.dataset;
1141 var fields_values = this.get_fields_values();
1142 var active_id = a_dataset.ids[a_dataset.index];
1143 _.extend(fields_values, {
1144 active_id: active_id || false,
1145 active_ids: active_id ? [active_id] : [],
1146 active_model: a_dataset.model,
1149 if (a_dataset.parent_view) {
1150 fields_values.parent = a_dataset.parent_view.get_fields_values();
1152 return fields_values;
1154 build_eval_context: function() {
1155 var a_dataset = this.dataset;
1156 return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
1161 * Interface to be implemented by rendering engines for the form view.
1163 instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
1164 set_fields_view: function(fields_view) {},
1165 set_fields_registry: function(fields_registry) {},
1166 render_to: function($el) {},
1170 * Default rendering engine for the form view.
1172 * It is necessary to set the view using set_view() before usage.
1174 instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
1175 init: function(view) {
1178 set_fields_view: function(fvg) {
1180 this.version = parseFloat(this.fvg.arch.attrs.version);
1181 if (isNaN(this.version)) {
1185 set_tags_registry: function(tags_registry) {
1186 this.tags_registry = tags_registry;
1188 set_fields_registry: function(fields_registry) {
1189 this.fields_registry = fields_registry;
1191 set_widgets_registry: function(widgets_registry) {
1192 this.widgets_registry = widgets_registry;
1194 // Backward compatibility tools, current default version: v7
1195 process_version: function() {
1196 if (this.version < 7.0) {
1197 this.$form.find('form:first').wrapInner('<group col="4"/>');
1198 this.$form.find('page').each(function() {
1199 if (!$(this).parents('field').length) {
1200 $(this).wrapInner('<group col="4"/>');
1205 get_arch_fragment: function() {
1206 var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
1207 // IE won't allow custom button@type and will revert it to spec default : 'submit'
1208 $('button', doc).each(function() {
1209 $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
1211 // IE's html parser is also a css parser. How convenient...
1212 $('board', doc).each(function() {
1213 $(this).attr('layout', $(this).attr('style'));
1215 return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
1217 render_to: function($target) {
1219 this.$target = $target;
1221 this.$form = this.get_arch_fragment();
1223 this.process_version();
1225 this.fields_to_init = [];
1226 this.tags_to_init = [];
1227 this.widgets_to_init = [];
1229 this.process(this.$form);
1231 this.$form.appendTo(this.$target);
1233 this.to_replace = [];
1235 _.each(this.fields_to_init, function($elem) {
1236 var name = $elem.attr("name");
1237 if (!self.fvg.fields[name]) {
1238 throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
1240 var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
1242 throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
1244 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1245 var $label = self.labels[$elem.attr("name")];
1247 w.set_input_id($label.attr("for"));
1249 self.alter_field(w);
1250 self.view.register_field(w, $elem.attr("name"));
1251 self.to_replace.push([w, $elem]);
1253 _.each(this.tags_to_init, function($elem) {
1254 var tag_name = $elem[0].tagName.toLowerCase();
1255 var obj = self.tags_registry.get_object(tag_name);
1256 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1257 self.to_replace.push([w, $elem]);
1259 _.each(this.widgets_to_init, function($elem) {
1260 var widget_type = $elem.attr("type");
1261 var obj = self.widgets_registry.get_object(widget_type);
1262 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1263 self.to_replace.push([w, $elem]);
1266 init_fields: function() {
1268 _.each(this.to_replace, function(el) {
1269 defs.push(el[0].replace(el[1]));
1270 if (el[1].children().length) {
1271 el[0].$el.append(el[1].children());
1274 this.to_replace = [];
1275 return $.when.apply($, defs);
1277 render_element: function(template /* dictionaries */) {
1278 var dicts = [].slice.call(arguments).slice(1);
1279 var dict = _.extend.apply(_, dicts);
1280 dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
1281 return $(QWeb.render(template, dict));
1283 alter_field: function(field) {
1285 toggle_layout_debugging: function() {
1286 if (!this.$target.has('.oe_layout_debug_cell:first').length) {
1287 this.$target.find('[title]').removeAttr('title');
1288 this.$target.find('.oe_form_group_cell').each(function() {
1289 var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
1290 $(this).attr('title', text);
1293 this.$target.toggleClass('oe_layout_debugging');
1295 process: function($tag) {
1297 var tagname = $tag[0].nodeName.toLowerCase();
1298 if (this.tags_registry.contains(tagname)) {
1299 this.tags_to_init.push($tag);
1300 return (tagname === 'button') ? this.process_button($tag) : $tag;
1302 var fn = self['process_' + tagname];
1304 var args = [].slice.call(arguments);
1306 return fn.apply(self, args);
1308 // generic tag handling, just process children
1309 $tag.children().each(function() {
1310 self.process($(this));
1312 self.handle_common_properties($tag, $tag);
1313 $tag.removeAttr("modifiers");
1317 process_button: function ($button) {
1319 $button.children().each(function() {
1320 self.process($(this));
1324 process_widget: function($widget) {
1325 this.widgets_to_init.push($widget);
1328 process_sheet: function($sheet) {
1329 var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
1330 this.handle_common_properties($new_sheet, $sheet);
1331 var $dst = $new_sheet.find('.oe_form_sheet');
1332 $sheet.contents().appendTo($dst);
1333 $sheet.before($new_sheet).remove();
1334 this.process($new_sheet);
1336 process_form: function($form) {
1337 if ($form.find('> sheet').length === 0) {
1338 $form.addClass('oe_form_nosheet');
1340 var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
1341 this.handle_common_properties($new_form, $form);
1342 $form.contents().appendTo($new_form);
1343 if ($form[0] === this.$form[0]) {
1344 // If root element, replace it
1345 this.$form = $new_form;
1347 $form.before($new_form).remove();
1349 this.process($new_form);
1352 * Used by direct <field> children of a <group> tag only
1353 * This method will add the implicit <label...> for every field
1356 preprocess_field: function($field) {
1358 var name = $field.attr('name'),
1359 field_colspan = parseInt($field.attr('colspan'), 10),
1360 field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
1362 if ($field.attr('nolabel') === '1')
1364 $field.attr('nolabel', '1');
1366 this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
1367 $(el).parents().each(function(unused, tag) {
1368 var name = tag.tagName.toLowerCase();
1369 if (name === "field" || name in self.tags_registry.map)
1376 var $label = $('<label/>').attr({
1378 "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
1379 "string": $field.attr('string'),
1380 "help": $field.attr('help'),
1381 "class": $field.attr('class'),
1383 $label.insertBefore($field);
1384 if (field_colspan > 1) {
1385 $field.attr('colspan', field_colspan - 1);
1389 process_field: function($field) {
1390 if ($field.parent().is('group')) {
1391 // No implicit labels for normal fields, only for <group> direct children
1392 var $label = this.preprocess_field($field);
1394 this.process($label);
1397 this.fields_to_init.push($field);
1400 process_group: function($group) {
1402 $group.children('field').each(function() {
1403 self.preprocess_field($(this));
1405 var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
1407 if ($new_group.first().is('table.oe_form_group')) {
1408 $table = $new_group;
1409 } else if ($new_group.filter('table.oe_form_group').length) {
1410 $table = $new_group.filter('table.oe_form_group').first();
1412 $table = $new_group.find('table.oe_form_group').first();
1416 cols = parseInt($group.attr('col') || 2, 10),
1420 $group.children().each(function(a,b,c) {
1421 var $child = $(this);
1422 var colspan = parseInt($child.attr('colspan') || 1, 10);
1423 var tagName = $child[0].tagName.toLowerCase();
1424 var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
1425 var newline = tagName === 'newline';
1427 // Note FME: those classes are used in layout debug mode
1428 if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
1429 $tr.addClass('oe_form_group_row_incomplete');
1431 $tr.addClass('oe_form_group_row_newline');
1438 if (!$tr || row_cols < colspan) {
1439 $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
1441 } else if (tagName==='group') {
1442 // When <group> <group/><group/> </group>, we need a spacing between the two groups
1443 $td.addClass('oe_group_right');
1445 row_cols -= colspan;
1447 // invisibility transfer
1448 var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
1449 var invisible = field_modifiers.invisible;
1450 self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
1452 $tr.append($td.append($child));
1453 children.push($child[0]);
1455 if (row_cols && $td) {
1456 $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
1458 $group.before($new_group).remove();
1460 $table.find('> tbody > tr').each(function() {
1461 var to_compute = [],
1464 $(this).children().each(function() {
1466 $child = $td.children(':first');
1467 if ($child.attr('cell-class')) {
1468 $td.addClass($child.attr('cell-class'));
1470 switch ($child[0].tagName.toLowerCase()) {
1474 if ($child.attr('for')) {
1475 $td.attr('width', '1%').addClass('oe_form_group_cell_label');
1476 row_cols-= $td.attr('colspan') || 1;
1481 var width = _.str.trim($child.attr('width') || ''),
1482 iwidth = parseInt(width, 10);
1484 if (width.substr(-1) === '%') {
1486 width = iwidth + '%';
1489 $td.css('min-width', width + 'px');
1491 $td.attr('width', width);
1492 $child.removeAttr('width');
1493 row_cols-= $td.attr('colspan') || 1;
1495 to_compute.push($td);
1501 var unit = Math.floor(total / row_cols);
1502 if (!$(this).is('.oe_form_group_row_incomplete')) {
1503 _.each(to_compute, function($td, i) {
1504 var width = parseInt($td.attr('colspan'), 10) * unit;
1505 $td.attr('width', width + '%');
1511 _.each(children, function(el) {
1512 self.process($(el));
1514 this.handle_common_properties($new_group, $group);
1517 process_notebook: function($notebook) {
1520 $notebook.find('> page').each(function() {
1521 var $page = $(this);
1522 var page_attrs = $page.getAttributes();
1523 page_attrs.id = _.uniqueId('notebook_page_');
1524 var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
1525 $page.contents().appendTo($new_page);
1526 $page.before($new_page).remove();
1527 var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
1528 page_attrs.__page = $new_page;
1529 page_attrs.__ic = ic;
1530 pages.push(page_attrs);
1532 $new_page.children().each(function() {
1533 self.process($(this));
1536 var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
1537 $notebook.contents().appendTo($new_notebook);
1538 $notebook.before($new_notebook).remove();
1539 self.process($($new_notebook.children()[0]));
1540 //tabs and invisibility handling
1541 $new_notebook.tabs();
1542 _.each(pages, function(page, i) {
1545 page.__ic.on("change:effective_invisible", null, function() {
1546 if (!page.__ic.get('effective_invisible') && page.autofocus) {
1547 $new_notebook.tabs('select', i);
1550 var current = $new_notebook.tabs("option", "selected");
1551 if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
1553 var first_visible = _.find(_.range(pages.length), function(i2) {
1554 return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
1556 if (first_visible !== undefined) {
1557 $new_notebook.tabs('select', first_visible);
1562 this.handle_common_properties($new_notebook, $notebook);
1563 return $new_notebook;
1565 process_separator: function($separator) {
1566 var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
1567 $separator.before($new_separator).remove();
1568 this.handle_common_properties($new_separator, $separator);
1569 return $new_separator;
1571 process_label: function($label) {
1572 var name = $label.attr("for"),
1573 field_orm = this.fvg.fields[name];
1575 string: $label.attr('string') || (field_orm || {}).string || '',
1576 help: $label.attr('help') || (field_orm || {}).help || '',
1577 _for: name ? _.uniqueId('oe-field-input-') : undefined,
1579 var align = parseFloat(dict.align);
1580 if (isNaN(align) || align === 1) {
1582 } else if (align === 0) {
1588 var $new_label = this.render_element('FormRenderingLabel', dict);
1589 $label.before($new_label).remove();
1590 this.handle_common_properties($new_label, $label);
1592 this.labels[name] = $new_label;
1596 handle_common_properties: function($new_element, $node) {
1597 var str_modifiers = $node.attr("modifiers") || "{}";
1598 var modifiers = JSON.parse(str_modifiers);
1600 if (modifiers.invisible !== undefined)
1601 ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
1602 $new_element.addClass($node.attr("class") || "");
1603 $new_element.attr('style', $node.attr('style'));
1604 return {invisibility_changer: ic,};
1611 If you read this documentation, it probably means that you were asked to use a form view widget outside of
1612 a form view. Before going further, you must understand that those fields were never really created for
1613 that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
1614 you to hack the system with more style.
1616 instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
1617 init: function(parent, eval_context) {
1618 this._super(parent);
1619 this.field_descs = {};
1620 this.eval_context = eval_context || {};
1622 display_invalid_fields: false,
1623 actual_mode: 'create',
1626 get_field_desc: function(field_name) {
1627 if (this.field_descs[field_name] === undefined) {
1628 this.field_descs[field_name] = {
1632 return this.field_descs[field_name];
1634 extend_field_desc: function(fields) {
1636 _.each(fields, function(v, k) {
1637 _.extend(self.get_field_desc(k), v);
1640 get_field_value: function(field_name) {
1643 set_values: function(values) {
1646 compute_domain: function(expression) {
1647 return instance.web.form.compute_domain(expression, {});
1649 build_eval_context: function() {
1650 return new instance.web.CompoundContext(this.eval_context);
1654 instance.web.form.compute_domain = function(expr, fields) {
1655 if (! (expr instanceof Array))
1658 for (var i = expr.length - 1; i >= 0; i--) {
1660 if (ex.length == 1) {
1661 var top = stack.pop();
1664 stack.push(stack.pop() || top);
1667 stack.push(stack.pop() && top);
1673 throw new Error(_.str.sprintf(
1674 _t("Unknown operator %s in domain %s"),
1675 ex, JSON.stringify(expr)));
1679 var field = fields[ex[0]];
1681 throw new Error(_.str.sprintf(
1682 _t("Unknown field %s in domain %s"),
1683 ex[0], JSON.stringify(expr)));
1685 var field_value = field.get_value ? field.get_value() : field.value;
1689 switch (op.toLowerCase()) {
1692 stack.push(_.isEqual(field_value, val));
1696 stack.push(!_.isEqual(field_value, val));
1699 stack.push(field_value < val);
1702 stack.push(field_value > val);
1705 stack.push(field_value <= val);
1708 stack.push(field_value >= val);
1711 if (!_.isArray(val)) val = [val];
1712 stack.push(_(val).contains(field_value));
1715 if (!_.isArray(val)) val = [val];
1716 stack.push(!_(val).contains(field_value));
1720 _t("Unsupported operator %s in domain %s"),
1721 op, JSON.stringify(expr));
1724 return _.all(stack, _.identity);
1727 instance.web.form.is_bin_size = function(v) {
1728 return (/^\d+(\.\d*)? \w+$/).test(v);
1732 * Must be applied over an class already possessing the PropertiesMixin.
1734 * Apply the result of the "invisible" domain to this.$el.
1736 instance.web.form.InvisibilityChangerMixin = {
1737 init: function(field_manager, invisible_domain) {
1739 this._ic_field_manager = field_manager;
1740 this._ic_invisible_modifier = invisible_domain;
1741 this._ic_field_manager.on("view_content_has_changed", this, function() {
1742 var result = self._ic_invisible_modifier === undefined ? false :
1743 self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
1744 self.set({"invisible": result});
1746 this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
1747 var check = function() {
1748 if (self.get("invisible") || self.get('force_invisible')) {
1749 self.set({"effective_invisible": true});
1751 self.set({"effective_invisible": false});
1754 this.on('change:invisible', this, check);
1755 this.on('change:force_invisible', this, check);
1759 this.on("change:effective_invisible", this, this._check_visibility);
1760 this._check_visibility();
1762 _check_visibility: function() {
1763 this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
1767 instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
1768 init: function(parent, field_manager, invisible_domain, $el) {
1769 this.setParent(parent);
1770 instance.web.PropertiesMixin.init.call(this);
1771 instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
1778 Base class for all fields, custom widgets and buttons to be displayed in the form view.
1781 - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
1782 the values of the "readonly" property and the "mode" property on the field manager.
1784 instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
1786 * @constructs instance.web.form.FormWidget
1787 * @extends instance.web.Widget
1789 * @param field_manager
1792 init: function(field_manager, node) {
1793 this._super(field_manager);
1794 this.field_manager = field_manager;
1795 if (this.field_manager instanceof instance.web.FormView)
1796 this.view = this.field_manager;
1798 this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
1799 instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
1801 this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
1807 // some events to make the property "effective_readonly" sync automatically with "readonly" and
1808 // "mode" on field_manager
1810 var test_effective_readonly = function() {
1811 self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
1813 this.on("change:readonly", this, test_effective_readonly);
1814 this.field_manager.on("change:actual_mode", this, test_effective_readonly);
1815 test_effective_readonly.call(this);
1817 renderElement: function() {
1818 this.process_modifiers();
1820 this.$el.addClass(this.node.attrs["class"] || "");
1822 destroy: function() {
1823 $.fn.tooltip('destroy');
1824 this._super.apply(this, arguments);
1827 * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
1829 * This method is an utility method that is meant to be called by child classes.
1831 * @param {jQuery} $e jQuery object of elements to bind focus/blur on
1833 setupFocus: function ($e) {
1836 focus: function () { self.trigger('focused'); },
1837 blur: function () { self.trigger('blurred'); }
1840 process_modifiers: function() {
1842 for (var a in this.modifiers) {
1843 if (!this.modifiers.hasOwnProperty(a)) { continue; }
1844 if (!_.include(["invisible"], a)) {
1845 var val = this.field_manager.compute_domain(this.modifiers[a]);
1851 do_attach_tooltip: function(widget, trigger, options) {
1852 widget = widget || this;
1853 trigger = trigger || this.$el;
1854 options = _.extend({
1855 delay: { show: 500, hide: 0 },
1857 var template = widget.template + '.tooltip';
1858 if (!QWeb.has_template(template)) {
1859 template = 'WidgetLabel.tooltip';
1861 return QWeb.render(template, {
1862 debug: instance.session.debug,
1867 //only show tooltip if we are in debug or if we have a help to show, otherwise it will display
1869 if (instance.session.debug || widget.node.attrs.help || (widget.field && widget.field.help)){
1870 $(trigger).tooltip(options);
1874 * Builds a new context usable for operations related to fields by merging
1875 * the fields'context with the action's context.
1877 build_context: function() {
1878 // only use the model's context if there is not context on the node
1879 var v_context = this.node.attrs.context;
1881 v_context = (this.field || {}).context || {};
1884 if (v_context.__ref || true) { //TODO: remove true
1885 var fields_values = this.field_manager.build_eval_context();
1886 v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
1890 build_domain: function() {
1891 var f_domain = this.field.domain || [];
1892 var n_domain = this.node.attrs.domain || null;
1893 // if there is a domain on the node, overrides the model's domain
1894 var final_domain = n_domain !== null ? n_domain : f_domain;
1895 if (!(final_domain instanceof Array) || true) { //TODO: remove true
1896 var fields_values = this.field_manager.build_eval_context();
1897 final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
1899 return final_domain;
1903 instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
1904 template: 'WidgetButton',
1905 init: function(field_manager, node) {
1906 node.attrs.type = node.attrs['data-button-type'];
1907 this.is_stat_button = /\boe_stat_button\b/.test(node.attrs['class']);
1908 this.icon_class = node.attrs.icon && "stat_button_icon fa " + node.attrs.icon + " fa-fw";
1909 this._super(field_manager, node);
1910 this.force_disabled = false;
1911 this.string = (this.node.attrs.string || '').replace(/_/g, '');
1912 if (JSON.parse(this.node.attrs.default_focus || "0")) {
1913 // TODO fme: provide enter key binding to widgets
1914 this.view.default_focus_button = this;
1916 if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
1917 this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
1921 this._super.apply(this, arguments);
1922 this.view.on('view_content_has_changed', this, this.check_disable);
1923 this.check_disable();
1924 this.$el.click(this.on_click);
1925 if (this.node.attrs.help || instance.session.debug) {
1926 this.do_attach_tooltip();
1928 this.setupFocus(this.$el);
1930 on_click: function() {
1932 this.force_disabled = true;
1933 this.check_disable();
1934 this.execute_action().always(function() {
1935 self.force_disabled = false;
1936 self.check_disable();
1939 execute_action: function() {
1941 var exec_action = function() {
1942 if (self.node.attrs.confirm) {
1943 var def = $.Deferred();
1944 var dialog = new instance.web.Dialog(this, {
1945 title: _t('Confirm'),
1947 {text: _t("Cancel"), click: function() {
1948 this.parents('.modal').modal('hide');
1951 {text: _t("Ok"), click: function() {
1953 self.on_confirmed().always(function() {
1954 self2.parents('.modal').modal('hide');
1959 }, $('<div/>').text(self.node.attrs.confirm)).open();
1960 dialog.on("closing", null, function() {def.resolve();});
1961 return def.promise();
1963 return self.on_confirmed();
1966 if (!this.node.attrs.special) {
1967 return this.view.recursive_save().then(exec_action);
1969 return exec_action();
1972 on_confirmed: function() {
1975 var context = this.build_context();
1976 return this.view.do_execute_action(
1977 _.extend({}, this.node.attrs, {context: context}),
1978 this.view.dataset, this.view.datarecord.id, function (reason) {
1979 if (!_.isObject(reason)) {
1980 self.view.recursive_reload();
1984 check_disable: function() {
1985 var disabled = (this.force_disabled || !this.view.is_interactible_record());
1986 this.$el.prop('disabled', disabled);
1987 this.$el.css('color', disabled ? 'grey' : '');
1992 * Interface to be implemented by fields.
1995 * - changed_value: triggered when the value of the field has changed. This can be due
1996 * to a user interaction or a call to set_value().
1999 instance.web.form.FieldInterface = {
2001 * Constructor takes 2 arguments:
2002 * - field_manager: Implements FieldManagerMixin
2003 * - node: the "<field>" node in json form
2005 init: function(field_manager, node) {},
2007 * Called by the form view to indicate the value of the field.
2009 * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
2010 * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
2011 * before the widget is inserted into the DOM.
2013 * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
2014 * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
2015 * also be able to handle any other format commonly used in the _defaults key on the models in the addons
2016 * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
2017 * no information is ever given to know which format is used.
2019 set_value: function(value_) {},
2021 * Get the current value of the widget.
2023 * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
2024 * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
2025 * For example if the field is marked as "required", a call to get_value() can return false.
2027 * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
2028 * return a default value according to the type of field.
2030 * This method is always assumed to perform synchronously, it can not return a promise.
2032 * If there was no user interaction to modify the value of the field, it is always assumed that
2033 * get_value() return the same semantic value than the one passed in the last call to set_value(),
2034 * although the syntax can be different. This can be the case for type of fields that have a different
2035 * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
2037 get_value: function() {},
2039 * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
2042 set_input_id: function(id) {},
2044 * Returns true if is_syntax_valid() returns true and the value is semantically
2045 * valid too according to the semantic restrictions applied to the field.
2047 is_valid: function() {},
2049 * Returns true if the field holds a value which is syntactically correct, ignoring
2050 * the potential semantic restrictions applied to the field.
2052 is_syntax_valid: function() {},
2054 * Must set the focus on the field. Return false if field is not focusable.
2056 focus: function() {},
2058 * Called when the translate button is clicked.
2060 on_translate: function() {},
2062 This method is called by the form view before reading on_change values and before saving. It tells
2063 the field to save its value before reading it using get_value(). Must return a promise.
2065 commit_value: function() {},
2069 * Abstract class for classes implementing FieldInterface.
2072 * - value: useful property to hold the value of the field. By default, set_value() and get_value()
2073 * set and retrieve the value property. Changing the value property also triggers automatically
2074 * a 'changed_value' event that inform the view to trigger on_changes.
2077 instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
2079 * @constructs instance.web.form.AbstractField
2080 * @extends instance.web.form.FormWidget
2082 * @param field_manager
2085 init: function(field_manager, node) {
2087 this._super(field_manager, node);
2088 this.name = this.node.attrs.name;
2089 this.field = this.field_manager.get_field_desc(this.name);
2090 this.widget = this.node.attrs.widget;
2091 this.string = this.node.attrs.string || this.field.string || this.name;
2092 this.options = instance.web.py_eval(this.node.attrs.options || '{}');
2093 this.set({'value': false});
2095 this.on("change:value", this, function() {
2096 this.trigger('changed_value');
2097 this._check_css_flags();
2100 renderElement: function() {
2103 if (this.field.translate && this.view) {
2104 this.$el.addClass('oe_form_field_translatable');
2105 this.$el.find('.oe_field_translate').click(this.on_translate);
2107 this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
2108 this.do_attach_tooltip(this, this.$label[0] || this.$el);
2109 if (instance.session.debug) {
2110 this.$label.off('dblclick').on('dblclick', function() {
2111 console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
2113 console.log("window.w =", window.w);
2116 if (!this.disable_utility_classes) {
2117 this.off("change:required", this, this._set_required);
2118 this.on("change:required", this, this._set_required);
2119 this._set_required();
2121 this._check_visibility();
2122 this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
2123 this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
2124 this._check_css_flags();
2127 var tmp = this._super();
2128 this.on("change:value", this, function() {
2129 if (! this.no_rerender)
2130 this.render_value();
2132 this.render_value();
2135 * Private. Do not use.
2137 _set_required: function() {
2138 this.$el.toggleClass('oe_form_required', this.get("required"));
2140 set_value: function(value_) {
2141 this.set({'value': value_});
2143 get_value: function() {
2144 return this.get('value');
2147 Utility method that all implementations should use to change the
2148 value without triggering a re-rendering.
2150 internal_set_value: function(value_) {
2151 var tmp = this.no_rerender;
2152 this.no_rerender = true;
2153 this.set({'value': value_});
2154 this.no_rerender = tmp;
2157 This method is called each time the value is modified.
2159 render_value: function() {},
2160 is_valid: function() {
2161 return this.is_syntax_valid() && !(this.get('required') && this.is_false());
2163 is_syntax_valid: function() {
2167 * Method useful to implement to ease validity testing. Must return true if the current
2168 * value is similar to false in OpenERP.
2170 is_false: function() {
2171 return this.get('value') === false;
2173 _check_css_flags: function() {
2174 if (this.field.translate) {
2175 this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
2177 if (!this.disable_utility_classes) {
2178 if (this.field_manager.get('display_invalid_fields')) {
2179 this.$el.toggleClass('oe_form_invalid', !this.is_valid());
2186 set_input_id: function(id) {
2187 this.id_for_label = id;
2189 on_translate: function() {
2191 var trans = new instance.web.DataSet(this, 'ir.translation');
2192 return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
2197 set_dimensions: function (height, width) {
2203 commit_value: function() {
2209 * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
2212 instance.web.form.ReinitializeWidgetMixin = {
2214 * Default implementation of, you should not override it, use initialize_field() instead.
2217 this.initialize_field();
2220 initialize_field: function() {
2221 this.on("change:effective_readonly", this, this.reinitialize);
2222 this.initialize_content();
2224 reinitialize: function() {
2225 this.destroy_content();
2226 this.renderElement();
2227 this.initialize_content();
2230 * Called to destroy anything that could have been created previously, called before a
2231 * re-initialization.
2233 destroy_content: function() {},
2235 * Called to initialize the content.
2237 initialize_content: function() {},
2241 * A mixin to apply on any field that has to completely re-render when its readonly state
2244 instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
2245 reinitialize: function() {
2246 instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
2247 var res = this.render_value();
2248 if (this.view && this.view.render_value_defs){
2249 this.view.render_value_defs.push(res);
2255 Some hack to make placeholders work in ie9.
2257 if (!('placeholder' in document.createElement('input'))) {
2258 document.addEventListener("DOMNodeInserted",function(event){
2259 var nodename = event.target.nodeName.toLowerCase();
2260 if ( nodename === "input" || nodename == "textarea" ) {
2261 $(event.target).placeholder();
2266 instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2267 template: 'FieldChar',
2268 widget_class: 'oe_form_field_char',
2270 'change input': 'store_dom_value',
2272 init: function (field_manager, node) {
2273 this._super(field_manager, node);
2274 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2276 initialize_content: function() {
2277 this.setupFocus(this.$('input'));
2279 store_dom_value: function () {
2280 if (!this.get('effective_readonly')
2281 && this.$('input').length
2282 && this.is_syntax_valid()) {
2283 this.internal_set_value(
2285 this.$('input').val()));
2288 commit_value: function () {
2289 this.store_dom_value();
2290 return this._super();
2292 render_value: function() {
2293 var show_value = this.format_value(this.get('value'), '');
2294 if (!this.get("effective_readonly")) {
2295 this.$el.find('input').val(show_value);
2297 if (this.password) {
2298 show_value = new Array(show_value.length + 1).join('*');
2300 this.$(".oe_form_char_content").text(show_value);
2303 is_syntax_valid: function() {
2304 if (!this.get("effective_readonly") && this.$("input").size() > 0) {
2306 this.parse_value(this.$('input').val(), '');
2314 parse_value: function(val, def) {
2315 return instance.web.parse_value(val, this, def);
2317 format_value: function(val, def) {
2318 return instance.web.format_value(val, this, def);
2320 is_false: function() {
2321 return this.get('value') === '' || this._super();
2324 var input = this.$('input:first')[0];
2325 return input ? input.focus() : false;
2327 set_dimensions: function (height, width) {
2328 this._super(height, width);
2329 this.$('input').css({
2336 instance.web.form.KanbanSelection = instance.web.form.FieldChar.extend({
2337 init: function (field_manager, node) {
2338 this._super(field_manager, node);
2340 prepare_dropdown_selection: function() {
2343 var selection = self.field.selection || [];
2344 _.map(selection, function(res) {
2348 'state_name': res[1],
2350 if (res[0] == 'normal') { value['state_class'] = 'oe_kanban_status'; }
2351 else if (res[0] == 'done') { value['state_class'] = 'oe_kanban_status oe_kanban_status_green'; }
2352 else { value['state_class'] = 'oe_kanban_status oe_kanban_status_red'; }
2357 render_value: function() {
2359 this.record_id = this.view.datarecord.id;
2360 this.states = this.prepare_dropdown_selection();;
2361 this.$el.html(QWeb.render("KanbanSelection", {'widget': self}));
2362 this.$el.find('li').on('click', this.set_kanban_selection.bind(this));
2364 /* setting the value: in view mode, perform an asynchronous call and reload
2365 the form view; in edit mode, use set_value to save the new value that will
2366 be written when saving the record. */
2367 set_kanban_selection: function (ev) {
2369 var li = $(ev.target).closest('li');
2371 var value = String(li.data('value'));
2372 if (this.view.get('actual_mode') == 'view') {
2373 var write_values = {}
2374 write_values[self.name] = value;
2375 return this.view.dataset._model.call(
2379 self.view.dataset.get_context()
2380 ]).done(self.reload_record.bind(self));
2383 return this.set_value(value);
2387 reload_record: function() {
2392 instance.web.form.Priority = instance.web.form.FieldChar.extend({
2393 init: function (field_manager, node) {
2394 this._super(field_manager, node);
2396 prepare_priority: function() {
2398 var selection = this.field.selection || [];
2399 var init_value = selection && selection[0][0] || 0;
2400 var data = _.map(selection.slice(1), function(element, index) {
2402 'value': element[0],
2404 'click_value': element[0],
2406 if (index == 0 && self.get('value') == element[0]) {
2407 value['click_value'] = init_value;
2413 render_value: function() {
2415 this.record_id = this.view.datarecord.id;
2416 this.priorities = this.prepare_priority();
2417 this.$el.html(QWeb.render("Priority", {'widget': this}));
2418 this.$el.find('li').on('click', this.set_priority.bind(this));
2420 /* setting the value: in view mode, perform an asynchronous call and reload
2421 the form view; in edit mode, use set_value to save the new value that will
2422 be written when saving the record. */
2423 set_priority: function (ev) {
2425 var li = $(ev.target).closest('li');
2427 var value = String(li.data('value'));
2428 if (this.view.get('actual_mode') == 'view') {
2429 var write_values = {}
2430 write_values[self.name] = value;
2431 return this.view.dataset._model.call(
2435 self.view.dataset.get_context()
2436 ]).done(self.reload_record.bind(self));
2439 return this.set_value(value);
2444 reload_record: function() {
2449 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2450 process_modifiers: function () {
2452 this.set({ readonly: true });
2456 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2457 template: 'FieldEmail',
2458 initialize_content: function() {
2460 var $button = this.$el.find('button');
2461 $button.click(this.on_button_clicked);
2462 this.setupFocus($button);
2464 render_value: function() {
2465 if (!this.get("effective_readonly")) {
2469 .attr('href', 'mailto:' + this.get('value'))
2470 .text(this.get('value') || '');
2473 on_button_clicked: function() {
2474 if (!this.get('value') || !this.is_syntax_valid()) {
2475 this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2477 location.href = 'mailto:' + this.get('value');
2482 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2483 template: 'FieldUrl',
2484 initialize_content: function() {
2486 var $button = this.$el.find('button');
2487 $button.click(this.on_button_clicked);
2488 this.setupFocus($button);
2490 render_value: function() {
2491 if (!this.get("effective_readonly")) {
2494 var tmp = this.get('value');
2495 var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2497 tmp = "http://" + this.get('value');
2499 var text = this.get('value') ? this.node.attrs.text || tmp : '';
2500 this.$el.find('a').attr('href', tmp).text(text);
2503 on_button_clicked: function() {
2504 if (!this.get('value')) {
2505 this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2507 var url = $.trim(this.get('value'));
2508 if(/^www\./i.test(url))
2509 url = 'http://'+url;
2515 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2516 is_field_number: true,
2517 widget_class: 'oe_form_field_float',
2518 init: function (field_manager, node) {
2519 this._super(field_manager, node);
2520 this.internal_set_value(0);
2521 if (this.node.attrs.digits) {
2522 this.digits = this.node.attrs.digits;
2524 this.digits = this.field.digits;
2527 set_value: function(value_) {
2528 if (value_ === false || value_ === undefined) {
2529 // As in GTK client, floats default to 0
2532 this._super.apply(this, [value_]);
2534 focus: function () {
2535 var $input = this.$('input:first');
2536 return $input.length ? $input.select() : false;
2540 instance.web.form.FieldCharDomain = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2541 init: function(field_manager, node) {
2542 this._super.apply(this, arguments);
2546 this._super.apply(this, arguments);
2547 this.on("change:effective_readonly", this, function () {
2548 this.display_field();
2550 this.display_field();
2551 return this._super();
2553 set_value: function(value_) {
2555 this.set('value', value_ || false);
2556 this.display_field();
2558 display_field: function() {
2560 this.$el.html(instance.web.qweb.render("FieldCharDomain", {widget: this}));
2561 if (this.get('value')) {
2562 var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2563 var domain = instance.web.pyeval.eval('domain', this.get('value'));
2564 var ds = new instance.web.DataSetStatic(self, model, self.build_context());
2565 ds.call('search_count', [domain]).then(function (results) {
2566 $('.oe_domain_count', self.$el).text(results + ' records selected');
2567 if (self.get('effective_readonly')) {
2568 $('button span', self.$el).text(' See selection');
2571 $('button span', self.$el).text(' Change selection');
2575 $('.oe_domain_count', this.$el).text('0 record selected');
2576 $('button span', this.$el).text(' Select records');
2578 this.$('.select_records').on('click', self.on_click);
2580 on_click: function(event) {
2581 event.preventDefault();
2583 var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2584 this.pop = new instance.web.form.SelectCreatePopup(this);
2585 this.pop.select_element(
2587 title: this.get('effective_readonly') ? 'Selected records' : 'Select records...',
2588 readonly: this.get('effective_readonly'),
2589 disable_multiple_selection: this.get('effective_readonly'),
2590 no_create: this.get('effective_readonly'),
2591 }, [], this.build_context());
2592 this.pop.on("elements_selected", self, function(element_ids) {
2593 if (this.pop.$('input.oe_list_record_selector').prop('checked')) {
2594 var search_data = this.pop.searchview.build_search_data();
2595 var domain_done = instance.web.pyeval.eval_domains_and_contexts({
2596 domains: search_data.domains,
2597 contexts: search_data.contexts,
2598 group_by_seq: search_data.groupbys || []
2599 }).then(function (results) {
2600 return results.domain;
2604 var domain = [["id", "in", element_ids]];
2605 var domain_done = $.Deferred().resolve(domain);
2607 $.when(domain_done).then(function (domain) {
2608 var domain = self.pop.dataset.domain.concat(domain || []);
2609 self.set_value(domain);
2615 instance.web.DateTimeWidget = instance.web.Widget.extend({
2616 template: "web.datepicker",
2617 jqueryui_object: 'datetimepicker',
2618 type_of_date: "datetime",
2620 'change .oe_datepicker_master': 'change_datetime',
2621 'keypress .oe_datepicker_master': 'change_datetime',
2623 init: function(parent) {
2624 this._super(parent);
2625 this.name = parent.name;
2629 this.$input = this.$el.find('input.oe_datepicker_master');
2630 this.$input_picker = this.$el.find('input.oe_datepicker_container');
2632 $.datepicker.setDefaults({
2633 clearText: _t('Clear'),
2634 clearStatus: _t('Erase the current date'),
2635 closeText: _t('Done'),
2636 closeStatus: _t('Close without change'),
2637 prevText: _t('<Prev'),
2638 prevStatus: _t('Show the previous month'),
2639 nextText: _t('Next>'),
2640 nextStatus: _t('Show the next month'),
2641 currentText: _t('Today'),
2642 currentStatus: _t('Show the current month'),
2643 monthNames: Date.CultureInfo.monthNames,
2644 monthNamesShort: Date.CultureInfo.abbreviatedMonthNames,
2645 monthStatus: _t('Show a different month'),
2646 yearStatus: _t('Show a different year'),
2647 weekHeader: _t('Wk'),
2648 weekStatus: _t('Week of the year'),
2649 dayNames: Date.CultureInfo.dayNames,
2650 dayNamesShort: Date.CultureInfo.abbreviatedDayNames,
2651 dayNamesMin: Date.CultureInfo.shortestDayNames,
2652 dayStatus: _t('Set DD as first week day'),
2653 dateStatus: _t('Select D, M d'),
2654 firstDay: Date.CultureInfo.firstDayOfWeek,
2655 initStatus: _t('Select a date'),
2658 $.timepicker.setDefaults({
2659 timeOnlyTitle: _t('Choose Time'),
2660 timeText: _t('Time'),
2661 hourText: _t('Hour'),
2662 minuteText: _t('Minute'),
2663 secondText: _t('Second'),
2664 currentText: _t('Now'),
2665 closeText: _t('Done')
2669 onClose: this.on_picker_select,
2670 onSelect: this.on_picker_select,
2674 showButtonPanel: true,
2675 firstDay: Date.CultureInfo.firstDayOfWeek
2677 // Some clicks in the datepicker dialog are not stopped by the
2678 // datepicker and "bubble through", unexpectedly triggering the bus's
2679 // click event. Prevent that.
2680 this.picker('widget').click(function (e) { e.stopPropagation(); });
2682 this.$el.find('img.oe_datepicker_trigger').click(function() {
2683 if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
2684 self.$input.focus();
2687 self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date());
2688 self.$input_picker.show();
2689 self.picker('show');
2690 self.$input_picker.hide();
2692 this.set_readonly(false);
2693 this.set({'value': false});
2695 picker: function() {
2696 return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
2698 on_picker_select: function(text, instance_) {
2699 var date = this.picker('getDate');
2701 .val(date ? this.format_client(date) : '')
2705 set_value: function(value_) {
2706 this.set({'value': value_});
2707 this.$input.val(value_ ? this.format_client(value_) : '');
2709 get_value: function() {
2710 return this.get('value');
2712 set_value_from_ui_: function() {
2713 var value_ = this.$input.val() || false;
2714 this.set({'value': this.parse_client(value_)});
2716 set_readonly: function(readonly) {
2717 this.readonly = readonly;
2718 this.$input.prop('readonly', this.readonly);
2719 this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
2721 is_valid_: function() {
2722 var value_ = this.$input.val();
2723 if (value_ === "") {
2727 this.parse_client(value_);
2734 parse_client: function(v) {
2735 return instance.web.parse_value(v, {"widget": this.type_of_date});
2737 format_client: function(v) {
2738 return instance.web.format_value(v, {"widget": this.type_of_date});
2740 change_datetime: function(e) {
2741 if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
2742 this.set_value_from_ui_();
2743 this.trigger("datetime_changed");
2746 commit_value: function () {
2747 this.change_datetime();
2751 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2752 jqueryui_object: 'datepicker',
2753 type_of_date: "date"
2756 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2757 template: "FieldDatetime",
2758 build_widget: function() {
2759 return new instance.web.DateTimeWidget(this);
2761 destroy_content: function() {
2762 if (this.datewidget) {
2763 this.datewidget.destroy();
2764 this.datewidget = undefined;
2767 initialize_content: function() {
2768 if (!this.get("effective_readonly")) {
2769 this.datewidget = this.build_widget();
2770 this.datewidget.on('datetime_changed', this, _.bind(function() {
2771 this.internal_set_value(this.datewidget.get_value());
2773 this.datewidget.appendTo(this.$el);
2774 this.setupFocus(this.datewidget.$input);
2777 render_value: function() {
2778 if (!this.get("effective_readonly")) {
2779 this.datewidget.set_value(this.get('value'));
2781 this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2784 is_syntax_valid: function() {
2785 if (!this.get("effective_readonly") && this.datewidget) {
2786 return this.datewidget.is_valid_();
2790 is_false: function() {
2791 return this.get('value') === '' || this._super();
2794 var input = this.datewidget && this.datewidget.$input[0];
2795 return input ? input.focus() : false;
2797 set_dimensions: function (height, width) {
2798 this._super(height, width);
2799 if (!this.get("effective_readonly")) {
2800 this.datewidget.$input.css('height', height);
2805 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2806 template: "FieldDate",
2807 build_widget: function() {
2808 return new instance.web.DateWidget(this);
2812 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2813 template: 'FieldText',
2815 'keyup': function (e) {
2816 if (e.which === $.ui.keyCode.ENTER) {
2817 e.stopPropagation();
2820 'keypress': function (e) {
2821 if (e.which === $.ui.keyCode.ENTER) {
2822 e.stopPropagation();
2825 'change textarea': 'store_dom_value',
2827 initialize_content: function() {
2829 if (! this.get("effective_readonly")) {
2830 this.$textarea = this.$el.find('textarea');
2831 this.auto_sized = false;
2832 this.default_height = this.$textarea.css('height');
2833 if (this.get("effective_readonly")) {
2834 this.$textarea.attr('disabled', 'disabled');
2836 this.setupFocus(this.$textarea);
2838 this.$textarea = undefined;
2841 commit_value: function () {
2842 if (! this.get("effective_readonly") && this.$textarea) {
2843 this.store_dom_value();
2845 return this._super();
2847 store_dom_value: function () {
2848 this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2850 render_value: function() {
2851 if (! this.get("effective_readonly")) {
2852 var show_value = instance.web.format_value(this.get('value'), this, '');
2853 if (show_value === '') {
2854 this.$textarea.css('height', parseInt(this.default_height, 10)+"px");
2856 this.$textarea.val(show_value);
2857 if (! this.auto_sized) {
2858 this.auto_sized = true;
2859 this.$textarea.autosize();
2861 this.$textarea.trigger("autosize");
2864 var txt = this.get("value") || '';
2865 this.$(".oe_form_text_content").text(txt);
2868 is_syntax_valid: function() {
2869 if (!this.get("effective_readonly") && this.$textarea) {
2871 instance.web.parse_value(this.$textarea.val(), this, '');
2879 is_false: function() {
2880 return this.get('value') === '' || this._super();
2882 focus: function($el) {
2883 var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2884 return input ? input.focus() : false;
2886 set_dimensions: function (height, width) {
2887 this._super(height, width);
2888 if (!this.get("effective_readonly") && this.$textarea) {
2889 this.$textarea.css({
2898 * FieldTextHtml Widget
2899 * Intended for FieldText widgets meant to display HTML content. This
2900 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2901 * To find more information about CLEditor configutation: go to
2902 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2904 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2905 template: 'FieldTextHtml',
2907 this._super.apply(this, arguments);
2909 initialize_content: function() {
2911 if (! this.get("effective_readonly")) {
2912 self._updating_editor = false;
2913 this.$textarea = this.$el.find('textarea');
2914 var width = ((this.node.attrs || {}).editor_width || 'calc(100% - 4px)');
2915 var height = ((this.node.attrs || {}).editor_height || 250);
2916 this.$textarea.cleditor({
2917 width: width, // width not including margins, borders or padding
2918 height: height, // height not including margins, borders or padding
2919 controls: // controls to add to the toolbar
2920 "bold italic underline strikethrough " +
2921 "| removeformat | bullets numbering | outdent " +
2922 "indent | link unlink | source",
2923 bodyStyle: // style to assign to document body contained within the editor
2924 "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2926 this.$cleditor = this.$textarea.cleditor()[0];
2927 this.$cleditor.change(function() {
2928 if (! self._updating_editor) {
2929 self.$cleditor.updateTextArea();
2930 self.internal_set_value(self.$textarea.val());
2933 if (this.field.translate) {
2934 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"/>')
2935 .click(this.on_translate);
2936 this.$cleditor.$toolbar.append($img);
2940 render_value: function() {
2941 if (! this.get("effective_readonly")) {
2942 this.$textarea.val(this.get('value') || '');
2943 this._updating_editor = true;
2944 this.$cleditor.updateFrame();
2945 this._updating_editor = false;
2947 this.$el.html(this.get('value'));
2952 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2953 template: 'FieldBoolean',
2956 this.$checkbox = $("input", this.$el);
2957 this.setupFocus(this.$checkbox);
2958 this.$el.click(_.bind(function() {
2959 this.internal_set_value(this.$checkbox.is(':checked'));
2961 var check_readonly = function() {
2962 self.$checkbox.prop('disabled', self.get("effective_readonly"));
2963 self.click_disabled_boolean();
2965 this.on("change:effective_readonly", this, check_readonly);
2966 check_readonly.call(this);
2967 this._super.apply(this, arguments);
2969 render_value: function() {
2970 this.$checkbox[0].checked = this.get('value');
2973 var input = this.$checkbox && this.$checkbox[0];
2974 return input ? input.focus() : false;
2976 click_disabled_boolean: function(){
2977 var $disabled = this.$el.find('input[type=checkbox]:disabled');
2978 $disabled.each(function (){
2979 $(this).next('div').remove();
2980 $(this).closest("span").append($('<div class="boolean"></div>'));
2986 The progressbar field expect a float from 0 to 100.
2988 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2989 template: 'FieldProgressBar',
2990 render_value: function() {
2991 this.$el.progressbar({
2992 value: this.get('value') || 0,
2993 disabled: this.get("effective_readonly")
2995 var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
2996 this.$('span').html(formatted_value + '%');
3001 The PercentPie field expect a float from 0 to 100.
3003 instance.web.form.FieldPercentPie = instance.web.form.AbstractField.extend({
3004 template: 'FieldPercentPie',
3006 render_value: function() {
3007 var value = this.get('value'),
3008 formatted_value = Math.round(value || 0) + '%',
3009 svg = this.$('svg')[0];
3012 nv.addGraph(function() {
3013 var width = 42, height = 42;
3014 var chart = nv.models.pieChart()
3017 .margin({top: 0, right: 0, bottom: 0, left: 0})
3022 .color(['#7C7BAD','#DDD'])
3026 .datum([{'x': 'value', 'y': value}, {'x': 'complement', 'y': 100 - value}])
3029 .attr('style', 'width: ' + width + 'px; height:' + height + 'px;');
3033 .attr({x: width/2, y: height/2 + 3, 'text-anchor': 'middle'})
3034 .style({"font-size": "10px", "font-weight": "bold"})
3035 .text(formatted_value);
3044 The FieldBarChart expectsa list of values (indeed)
3046 instance.web.form.FieldBarChart = instance.web.form.AbstractField.extend({
3047 template: 'FieldBarChart',
3049 render_value: function() {
3050 var value = JSON.parse(this.get('value'));
3051 var svg = this.$('svg')[0];
3053 nv.addGraph(function() {
3054 var width = 34, height = 34;
3055 var chart = nv.models.discreteBarChart()
3056 .x(function (d) { return d.tooltip })
3057 .y(function (d) { return d.value })
3060 .margin({top: 0, right: 0, bottom: 0, left: 0})
3063 .transitionDuration(350)
3068 .datum([{key: 'values', values: value}])
3071 .attr('style', 'width: ' + (width + 4) + 'px; height: ' + (height + 8) + 'px;');
3073 nv.utils.windowResize(chart.update);
3082 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3083 template: 'FieldSelection',
3085 'change select': 'store_dom_value',
3087 init: function(field_manager, node) {
3089 this._super(field_manager, node);
3090 this.set("value", false);
3091 this.set("values", []);
3092 this.records_orderer = new instance.web.DropMisordered();
3093 this.field_manager.on("view_content_has_changed", this, function() {
3094 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
3095 if (! _.isEqual(domain, this.get("domain"))) {
3096 this.set("domain", domain);
3100 initialize_field: function() {
3101 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3102 this.on("change:domain", this, this.query_values);
3103 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
3104 this.on("change:values", this, this.render_value);
3106 query_values: function() {
3109 if (this.field.type === "many2one") {
3110 var model = new openerp.Model(openerp.session, this.field.relation);
3111 def = model.call("name_search", ['', this.get("domain")], {"context": this.build_context()});
3113 var values = _.reject(this.field.selection, function (v) { return v[0] === false && v[1] === ''; });
3114 def = $.when(values);
3116 this.records_orderer.add(def).then(function(values) {
3117 if (! _.isEqual(values, self.get("values"))) {
3118 self.set("values", values);
3122 initialize_content: function() {
3123 // Flag indicating whether we're in an event chain containing a change
3124 // event on the select, in order to know what to do on keyup[RETURN]:
3125 // * If the user presses [RETURN] as part of changing the value of a
3126 // selection, we should just let the value change and not let the
3127 // event broadcast further (e.g. to validating the current state of
3128 // the form in editable list view, which would lead to saving the
3129 // current row or switching to the next one)
3130 // * If the user presses [RETURN] with a select closed (side-effect:
3131 // also if the user opened the select and pressed [RETURN] without
3132 // changing the selected value), takes the action as validating the
3134 var ischanging = false;
3135 var $select = this.$el.find('select')
3136 .change(function () { ischanging = true; })
3137 .click(function () { ischanging = false; })
3138 .keyup(function (e) {
3139 if (e.which !== 13 || !ischanging) { return; }
3140 e.stopPropagation();
3143 this.setupFocus($select);
3145 commit_value: function () {
3146 this.store_dom_value();
3147 return this._super();
3149 store_dom_value: function () {
3150 if (!this.get('effective_readonly') && this.$('select').length) {
3151 var val = JSON.parse(this.$('select').val());
3152 this.internal_set_value(val);
3155 set_value: function(value_) {
3156 value_ = value_ === null ? false : value_;
3157 value_ = value_ instanceof Array ? value_[0] : value_;
3158 this._super(value_);
3160 render_value: function() {
3161 var values = this.get("values");
3162 values = [[false, this.node.attrs.placeholder || '']].concat(values);
3163 var found = _.find(values, function(el) { return el[0] === this.get("value"); }, this);
3165 found = [this.get("value"), _t('Unknown')];
3166 values = [found].concat(values);
3168 if (! this.get("effective_readonly")) {
3169 this.$().html(QWeb.render("FieldSelectionSelect", {widget: this, values: values}));
3170 this.$("select").val(JSON.stringify(found[0]));
3172 this.$el.text(found[1]);
3176 var input = this.$('select:first')[0];
3177 return input ? input.focus() : false;
3179 set_dimensions: function (height, width) {
3180 this._super(height, width);
3181 this.$('select').css({
3188 instance.web.form.FieldRadio = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3189 template: 'FieldRadio',
3191 'click input': 'click_change_value'
3193 init: function(field_manager, node) {
3194 /* Radio button widget: Attributes options:
3195 * - "horizontal" to display in column
3196 * - "no_radiolabel" don't display text values
3198 this._super(field_manager, node);
3199 this.selection = _.clone(this.field.selection) || [];
3200 this.domain = false;
3202 initialize_content: function () {
3203 this.uniqueId = _.uniqueId("radio");
3204 this.on("change:effective_readonly", this, this.render_value);
3205 this.field_manager.on("view_content_has_changed", this, this.get_selection);
3206 this.get_selection();
3208 click_change_value: function (event) {
3209 var val = $(event.target).val();
3210 val = this.field.type == "selection" ? val : +val;
3211 if (val == this.get_value()) {
3212 this.set_value(false);
3214 this.set_value(val);
3217 /** Get the selection and render it
3218 * selection: [[identifier, value_to_display], ...]
3219 * For selection fields: this is directly given by this.field.selection
3220 * For many2one fields: perform a search on the relation of the many2one field
3222 get_selection: function() {
3225 var def = $.Deferred();
3226 if (self.field.type == "many2one") {
3227 var domain = instance.web.pyeval.eval('domain', this.build_domain()) || [];
3228 if (! _.isEqual(self.domain, domain)) {
3229 self.domain = domain;
3230 var ds = new instance.web.DataSetStatic(self, self.field.relation, self.build_context());
3231 ds.call('search', [self.domain])
3232 .then(function (records) {
3233 ds.name_get(records).then(function (records) {
3234 selection = records;
3239 selection = self.selection;
3243 else if (self.field.type == "selection") {
3244 selection = self.field.selection || [];
3247 return def.then(function () {
3248 if (! _.isEqual(selection, self.selection)) {
3249 self.selection = _.clone(selection);
3250 self.renderElement();
3251 self.render_value();
3255 set_value: function (value_) {
3257 if (this.field.type == "selection") {
3258 value_ = _.find(this.field.selection, function (sel) { return sel[0] == value_;});
3260 else if (!this.selection.length) {
3261 this.selection = [value_];
3264 this._super(value_);
3266 get_value: function () {
3267 var value = this.get('value');
3268 return value instanceof Array ? value[0] : value;
3270 render_value: function () {
3272 this.$el.toggleClass("oe_readonly", this.get('effective_readonly'));
3273 this.$("input:checked").prop("checked", false);
3274 if (this.get_value()) {
3275 this.$("input").filter(function () {return this.value == self.get_value();}).prop("checked", true);
3276 this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : "");
3281 // jquery autocomplete tweak to allow html and classnames
3283 var proto = $.ui.autocomplete.prototype,
3284 initSource = proto._initSource;
3286 function filter( array, term ) {
3287 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
3288 return $.grep( array, function(value_) {
3289 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
3294 _initSource: function() {
3295 if ( this.options.html && $.isArray(this.options.source) ) {
3296 this.source = function( request, response ) {
3297 response( filter( this.options.source, request.term ) );
3300 initSource.call( this );
3304 _renderItem: function( ul, item) {
3305 return $( "<li></li>" )
3306 .data( "item.autocomplete", item )
3307 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
3309 .addClass(item.classname);
3315 A mixin containing some useful methods to handle completion inputs.
3317 The widget containing this option can have these arguments in its widget options:
3318 - no_quick_create: if true, it will disable the quick create
3320 instance.web.form.CompletionFieldMixin = {
3323 this.orderer = new instance.web.DropMisordered();
3326 * Call this method to search using a string.
3328 get_search_result: function(search_val) {
3331 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
3332 this.last_query = search_val;
3333 var exclusion_domain = [], ids_blacklist = this.get_search_blacklist();
3334 if (!_(ids_blacklist).isEmpty()) {
3335 exclusion_domain.push(['id', 'not in', ids_blacklist]);
3338 return this.orderer.add(dataset.name_search(
3339 search_val, new instance.web.CompoundDomain(self.build_domain(), exclusion_domain),
3340 'ilike', this.limit + 1, self.build_context())).then(function(data) {
3341 self.last_search = data;
3342 // possible selections for the m2o
3343 var values = _.map(data, function(x) {
3344 x[1] = x[1].split("\n")[0];
3346 label: _.str.escapeHTML(x[1]),
3353 // search more... if more results that max
3354 if (values.length > self.limit) {
3355 values = values.slice(0, self.limit);
3357 label: _t("Search More..."),
3358 action: function() {
3359 dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
3360 self._search_create_popup("search", data);
3363 classname: 'oe_m2o_dropdown_option'
3367 var raw_result = _(data.result).map(function(x) {return x[1];});
3368 if (search_val.length > 0 && !_.include(raw_result, search_val) &&
3369 ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3371 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
3372 $('<span />').text(search_val).html()),
3373 action: function() {
3374 self._quick_create(search_val);
3376 classname: 'oe_m2o_dropdown_option'
3380 if (!(self.options && (self.options.no_create || self.options.no_create_edit))){
3382 label: _t("Create and Edit..."),
3383 action: function() {
3384 self._search_create_popup("form", undefined, self._create_context(search_val));
3386 classname: 'oe_m2o_dropdown_option'
3389 else if (values.length == 0)
3391 label: _t("No results to show..."),
3392 action: function() {},
3393 classname: 'oe_m2o_dropdown_option'
3399 get_search_blacklist: function() {
3402 _quick_create: function(name) {
3404 var slow_create = function () {
3405 self._search_create_popup("form", undefined, self._create_context(name));
3407 if (self.options.quick_create === undefined || self.options.quick_create) {
3408 new instance.web.DataSet(this, this.field.relation, self.build_context())
3409 .name_create(name).done(function(data) {
3410 if (!self.get('effective_readonly'))
3411 self.add_id(data[0]);
3412 }).fail(function(error, event) {
3413 event.preventDefault();
3419 // all search/create popup handling
3420 _search_create_popup: function(view, ids, context) {
3422 var pop = new instance.web.form.SelectCreatePopup(this);
3424 self.field.relation,
3426 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3427 initial_ids: ids ? _.map(ids, function(x) {return x[0];}) : undefined,
3429 disable_multiple_selection: true
3431 self.build_domain(),
3432 new instance.web.CompoundContext(self.build_context(), context || {})
3434 pop.on("elements_selected", self, function(element_ids) {
3435 self.add_id(element_ids[0]);
3442 add_id: function(id) {},
3443 _create_context: function(name) {
3445 var field = (this.options || {}).create_name_field;
3446 if (field === undefined)
3448 if (field !== false && name && (this.options || {}).quick_create !== false)
3449 tmp["default_" + field] = name;
3454 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3455 template: "M2ODialog",
3456 init: function(parent) {
3457 this.name = parent.string;
3458 this._super(parent, {
3459 title: _.str.sprintf(_t("Create a %s"), parent.string),
3465 var text = _.str.sprintf(_t("You are creating a new %s, are you sure it does not exist yet?"), self.name);
3466 this.$("p").text( text );
3467 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3468 this.$("input").val(this.getParent().last_query);
3469 this.$buttons.find(".oe_form_m2o_qc_button").click(function(e){
3470 if (self.$("input").val() != ''){
3471 self.getParent()._quick_create(self.$("input").val());
3475 self.$("input").focus();
3478 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3479 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3482 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3488 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3489 template: "FieldMany2One",
3491 'keydown input': function (e) {
3493 case $.ui.keyCode.UP:
3494 case $.ui.keyCode.DOWN:
3495 e.stopPropagation();
3499 init: function(field_manager, node) {
3500 this._super(field_manager, node);
3501 instance.web.form.CompletionFieldMixin.init.call(this);
3502 this.set({'value': false});
3503 this.display_value = {};
3504 this.display_value_backup = {};
3505 this.last_search = [];
3506 this.floating = false;
3507 this.current_display = null;
3508 this.is_started = false;
3509 this.ignore_focusout = false;
3511 reinit_value: function(val) {
3512 this.internal_set_value(val);
3513 this.floating = false;
3514 if (this.is_started)
3515 this.render_value();
3517 initialize_field: function() {
3518 this.is_started = true;
3519 instance.web.bus.on('click', this, function() {
3520 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3521 this.$input.autocomplete("close");
3524 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3526 initialize_content: function() {
3527 if (!this.get("effective_readonly"))
3528 this.render_editable();
3530 destroy_content: function () {
3531 if (this.$drop_down) {
3532 this.$drop_down.off('click');
3533 delete this.$drop_down;
3536 this.$input.closest(".modal .modal-content").off('scroll');
3537 this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3538 'focus focusout change keydown');
3541 if (this.$follow_button) {
3542 this.$follow_button.off('blur focus click');
3543 delete this.$follow_button;
3546 destroy: function () {
3547 this.destroy_content();
3548 return this._super();
3550 init_error_displayer: function() {
3553 hide_error_displayer: function() {
3556 show_error_displayer: function() {
3557 new instance.web.form.M2ODialog(this).open();
3559 render_editable: function() {
3561 this.$input = this.$el.find("input");
3563 this.init_error_displayer();
3565 self.$input.on('focus', function() {
3566 self.hide_error_displayer();
3569 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3570 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3572 this.$follow_button.click(function(ev) {
3573 ev.preventDefault();
3574 if (!self.get('value')) {
3578 var pop = new instance.web.form.FormOpenPopup(self);
3579 var context = self.build_context().eval();
3580 var model_obj = new instance.web.Model(self.field.relation);
3581 model_obj.call('get_formview_id', [self.get("value"), context]).then(function(view_id){
3583 self.field.relation,
3585 self.build_context(),
3587 title: _t("Open: ") + self.string,
3591 pop.on('write_completed', self, function(){
3592 self.display_value = {};
3593 self.display_value_backup = {};
3594 self.render_value();
3596 self.trigger('changed_value');
3601 // some behavior for input
3602 var input_changed = function() {
3603 if (self.current_display !== self.$input.val()) {
3604 self.current_display = self.$input.val();
3605 if (self.$input.val() === "") {
3606 self.internal_set_value(false);
3607 self.floating = false;
3609 self.floating = true;
3613 this.$input.keydown(input_changed);
3614 this.$input.change(input_changed);
3615 this.$drop_down.click(function() {
3616 self.$input.focus();
3617 if (self.$input.autocomplete("widget").is(":visible")) {
3618 self.$input.autocomplete("close");
3620 if (self.get("value") && ! self.floating) {
3621 self.$input.autocomplete("search", "");
3623 self.$input.autocomplete("search");
3628 // Autocomplete close on dialog content scroll
3629 var close_autocomplete = _.debounce(function() {
3630 if (self.$input.autocomplete("widget").is(":visible")) {
3631 self.$input.autocomplete("close");
3634 this.$input.closest(".modal .modal-content").on('scroll', this, close_autocomplete);
3636 self.ed_def = $.Deferred();
3637 self.uned_def = $.Deferred();
3639 var ed_duration = 15000;
3640 var anyoneLoosesFocus = function (e) {
3641 if (self.ignore_focusout) { return; }
3643 if (self.floating) {
3644 if (self.last_search.length > 0) {
3645 if (self.last_search[0][0] != self.get("value")) {
3646 self.display_value = {};
3647 self.display_value_backup = {};
3648 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3649 self.reinit_value(self.last_search[0][0]);
3652 self.render_value();
3656 self.reinit_value(false);
3658 self.floating = false;
3660 if (used && self.get("value") === false && ! self.no_ed && ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3661 self.ed_def.reject();
3662 self.uned_def.reject();
3663 self.ed_def = $.Deferred();
3664 self.ed_def.done(function() {
3665 self.show_error_displayer();
3666 ignore_blur = false;
3667 self.trigger('focused');
3670 setTimeout(function() {
3671 self.ed_def.resolve();
3672 self.uned_def.reject();
3673 self.uned_def = $.Deferred();
3674 self.uned_def.done(function() {
3675 self.hide_error_displayer();
3677 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3681 self.ed_def.reject();
3684 var ignore_blur = false;
3686 focusout: anyoneLoosesFocus,
3687 focus: function () { self.trigger('focused'); },
3688 autocompleteopen: function () { ignore_blur = true; },
3689 autocompleteclose: function () { setTimeout(function() {ignore_blur = false;},0); },
3691 // autocomplete open
3692 if (ignore_blur) { $(this).focus(); return; }
3693 if (_(self.getChildren()).any(function (child) {
3694 return child instanceof instance.web.form.AbstractFormPopup;
3696 self.trigger('blurred');
3700 var isSelecting = false;
3702 this.$input.autocomplete({
3703 source: function(req, resp) {
3704 self.get_search_result(req.term).done(function(result) {
3708 select: function(event, ui) {
3712 self.display_value = {};
3713 self.display_value_backup = {};
3714 self.display_value["" + item.id] = item.name;
3715 self.reinit_value(item.id);
3716 } else if (item.action) {
3718 // Cancel widget blurring, to avoid form blur event
3719 self.trigger('focused');
3723 focus: function(e, ui) {
3727 // disabled to solve a bug, but may cause others
3728 //close: anyoneLoosesFocus,
3732 // set position for list of suggestions box
3733 this.$input.autocomplete( "option", "position", { my : "left top", at: "left bottom" } );
3734 this.$input.autocomplete("widget").openerpClass();
3735 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3736 this.$input.keyup(function(e) {
3737 if (e.which === 13) { // ENTER
3739 e.stopPropagation();
3741 isSelecting = false;
3743 this.setupFocus(this.$follow_button);
3745 render_value: function(no_recurse) {
3747 if (! this.get("value")) {
3748 this.display_string("");
3751 var display = this.display_value["" + this.get("value")];
3753 this.display_string(display);
3757 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3758 this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3760 self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
3763 self.display_value["" + self.get("value")] = data[0][1];
3764 self.render_value(true);
3765 }).fail( function (data, event) {
3766 // avoid displaying crash errors as many2One should be name_get compliant
3767 event.preventDefault();
3768 self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")];
3769 self.render_value(true);
3773 display_string: function(str) {
3775 if (!this.get("effective_readonly")) {
3776 this.$input.val(str.split("\n")[0]);
3777 this.current_display = this.$input.val();
3778 if (this.is_false()) {
3779 this.$('.oe_m2o_cm_button').css({'display':'none'});
3781 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3784 var lines = _.escape(str).split("\n");
3788 follow = _.rest(lines).join("<br />");
3791 var $link = this.$el.find('.oe_form_uri')
3794 if (! this.options.no_open)
3795 $link.click(function () {
3796 var context = self.build_context().eval();
3797 var model_obj = new instance.web.Model(self.field.relation);
3798 model_obj.call('get_formview_action', [self.get("value"), context]).then(function(action){
3799 self.do_action(action);
3803 $(".oe_form_m2o_follow", this.$el).html(follow);
3806 set_value: function(value_) {
3808 if (value_ instanceof Array) {
3809 this.display_value = {};
3810 this.display_value_backup = {};
3811 if (! this.options.always_reload) {
3812 this.display_value["" + value_[0]] = value_[1];
3815 this.display_value_backup["" + value_[0]] = value_[1];
3819 value_ = value_ || false;
3820 this.reinit_value(value_);
3822 get_displayed: function() {
3823 return this.display_value["" + this.get("value")];
3825 add_id: function(id) {
3826 this.display_value = {};
3827 this.display_value_backup = {};
3828 this.reinit_value(id);
3830 is_false: function() {
3831 return ! this.get("value");
3833 focus: function () {
3834 var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3835 return input ? input.focus() : false;
3837 _quick_create: function() {
3839 this.ed_def.reject();
3840 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3842 _search_create_popup: function() {
3844 this.ed_def.reject();
3845 this.ignore_focusout = true;
3846 this.reinit_value(false);
3847 var res = instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3848 this.ignore_focusout = false;
3852 set_dimensions: function (height, width) {
3853 this._super(height, width);
3854 if (!this.get("effective_readonly") && this.$input)
3855 this.$input.css('height', height);
3859 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3860 template: 'Many2OneButton',
3861 init: function(field_manager, node) {
3862 this._super.apply(this, arguments);
3865 this._super.apply(this, arguments);
3868 set_button: function() {
3871 this.$button.remove();
3874 this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3875 this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3876 this.$button.addClass('oe_link').css({'padding':'4px'});
3877 this.$el.append(this.$button);
3878 this.$button.on('click', self.on_click);
3880 on_click: function(ev) {
3882 this.popup = new instance.web.form.FormOpenPopup(this);
3883 this.popup.show_element(
3884 this.field.relation,
3886 this.build_context(),
3887 {title: this.string}
3889 this.popup.on('create_completed', self, function(r) {
3893 set_value: function(value_) {
3895 if (value_ instanceof Array) {
3898 value_ = value_ || false;
3899 this.set('value', value_);
3905 * Abstract-ish ListView.List subclass adding an "Add an item" row to replace
3906 * the big ugly button in the header.
3908 * Requires the implementation of a ``is_readonly`` method (usually a proxy to
3909 * the corresponding field's readonly or effective_readonly property) to
3910 * decide whether the special row should or should not be inserted.
3912 * Optionally an ``_add_row_class`` attribute can be set for the class(es) to
3913 * set on the insertion row.
3915 instance.web.form.AddAnItemList = instance.web.ListView.List.extend({
3916 pad_table_to: function (count) {
3917 if (!this.view.is_action_enabled('create') || this.is_readonly()) {
3922 this._super(count > 0 ? count - 1 : 0);
3925 var columns = _(this.columns).filter(function (column) {
3926 return column.invisible !== '1';
3928 if (this.options.selectable) { columns++; }
3929 if (this.options.deletable) { columns++; }
3931 var $cell = $('<td>', {
3933 'class': this._add_row_class || ''
3935 $('<a>', {href: '#'}).text(_t("Add an item"))
3936 .mousedown(function () {
3937 // FIXME: needs to be an official API somehow
3938 if (self.view.editor.is_editing()) {
3939 self.view.__ignore_blur = true;
3942 .click(function (e) {
3944 e.stopPropagation();
3945 // FIXME: there should also be an API for that one
3946 if (self.view.editor.form.__blur_timeout) {
3947 clearTimeout(self.view.editor.form.__blur_timeout);
3948 self.view.editor.form.__blur_timeout = false;
3950 self.view.ensure_saved().done(function () {
3951 self.view.do_add_record();
3955 var $padding = this.$current.find('tr:not([data-id]):first');
3956 var $newrow = $('<tr>').append($cell);
3957 if ($padding.length) {
3958 $padding.before($newrow);
3960 this.$current.append($newrow)
3966 # Values: (0, 0, { fields }) create
3967 # (1, ID, { fields }) update
3968 # (2, ID) remove (delete)
3969 # (3, ID) unlink one (target id or target of relation)
3971 # (5) unlink all (only valid for one2many)
3976 'create': function (values) {
3977 return [commands.CREATE, false, values];
3979 // (1, id, {values})
3981 'update': function (id, values) {
3982 return [commands.UPDATE, id, values];
3986 'delete': function (id) {
3987 return [commands.DELETE, id, false];
3989 // (3, id[, _]) removes relation, but not linked record itself
3991 'forget': function (id) {
3992 return [commands.FORGET, id, false];
3996 'link_to': function (id) {
3997 return [commands.LINK_TO, id, false];
4001 'delete_all': function () {
4002 return [5, false, false];
4004 // (6, _, ids) replaces all linked records with provided ids
4006 'replace_with': function (ids) {
4007 return [6, false, ids];
4010 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
4011 multi_selection: false,
4012 disable_utility_classes: true,
4013 init: function(field_manager, node) {
4014 this._super(field_manager, node);
4015 lazy_build_o2m_kanban_view();
4016 this.is_loaded = $.Deferred();
4017 this.initial_is_loaded = this.is_loaded;
4018 this.form_last_update = $.Deferred();
4019 this.init_form_last_update = this.form_last_update;
4020 this.is_started = false;
4021 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
4022 this.dataset.o2m = this;
4023 this.dataset.parent_view = this.view;
4024 this.dataset.child_name = this.name;
4026 this.dataset.on('dataset_changed', this, function() {
4027 self.trigger_on_change();
4032 this._super.apply(this, arguments);
4033 this.$el.addClass('oe_form_field oe_form_field_one2many');
4038 this.is_loaded.done(function() {
4039 self.on("change:effective_readonly", self, function() {
4040 self.is_loaded = self.is_loaded.then(function() {
4041 self.viewmanager.destroy();
4042 return $.when(self.load_views()).done(function() {
4043 self.reload_current_view();
4048 this.is_started = true;
4049 this.reload_current_view();
4051 trigger_on_change: function() {
4052 this.trigger('changed_value');
4054 load_views: function() {
4057 var modes = this.node.attrs.mode;
4058 modes = !!modes ? modes.split(",") : ["tree"];
4060 _.each(modes, function(mode) {
4061 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
4062 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
4066 view_type: mode == "tree" ? "list" : mode,
4069 if (self.field.views && self.field.views[mode]) {
4070 view.embedded_view = self.field.views[mode];
4072 if(view.view_type === "list") {
4073 _.extend(view.options, {
4075 selectable: self.multi_selection,
4077 import_enabled: false,
4080 if (self.get("effective_readonly")) {
4081 _.extend(view.options, {
4086 } else if (view.view_type === "form") {
4087 if (self.get("effective_readonly")) {
4088 view.view_type = 'form';
4090 _.extend(view.options, {
4091 not_interactible_on_create: true,
4093 } else if (view.view_type === "kanban") {
4094 _.extend(view.options, {
4095 confirm_on_delete: false,
4097 if (self.get("effective_readonly")) {
4098 _.extend(view.options, {
4099 action_buttons: false,
4100 quick_creatable: false,
4102 read_only_mode: true,
4110 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
4111 this.viewmanager.o2m = self;
4112 var once = $.Deferred().done(function() {
4113 self.init_form_last_update.resolve();
4115 var def = $.Deferred().done(function() {
4116 self.initial_is_loaded.resolve();
4118 this.viewmanager.on("controller_inited", self, function(view_type, controller) {
4119 controller.o2m = self;
4120 if (view_type == "list") {
4121 if (self.get("effective_readonly")) {
4122 controller.on('edit:before', self, function (e) {
4125 _(controller.columns).find(function (column) {
4126 if (!(column instanceof instance.web.list.Handle)) {
4129 column.modifiers.invisible = true;
4133 } else if (view_type === "form") {
4134 if (self.get("effective_readonly")) {
4135 $(".oe_form_buttons", controller.$el).children().remove();
4137 controller.on("load_record", self, function(){
4140 controller.on('pager_action_executed',self,self.save_any_view);
4141 } else if (view_type == "graph") {
4142 self.reload_current_view();
4146 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
4147 $.when(self.save_any_view()).done(function() {
4148 if (n_mode === "list") {
4149 $.async_when().done(function() {
4150 self.reload_current_view();
4155 $.async_when().done(function () {
4156 self.viewmanager.appendTo(self.$el);
4160 reload_current_view: function() {
4162 self.is_loaded = self.is_loaded.then(function() {
4163 var view = self.get_active_view();
4164 if (view.type === "list") {
4165 return view.controller.reload_content();
4166 } else if (view.type === "form") {
4167 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
4168 self.dataset.index = 0;
4170 var act = function() {
4171 return view.controller.do_show();
4173 self.form_last_update = self.form_last_update.then(act, act);
4174 return self.form_last_update;
4175 } else if (view.controller.do_search) {
4176 return view.controller.do_search(self.build_domain(), self.dataset.get_context(), []);
4179 return self.is_loaded;
4181 get_active_view: function () {
4183 * Returns the current active view if any.
4185 if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
4186 this.viewmanager.views[this.viewmanager.active_view] &&
4187 this.viewmanager.views[this.viewmanager.active_view].controller) {
4189 type: this.viewmanager.active_view,
4190 controller: this.viewmanager.views[this.viewmanager.active_view].controller
4194 set_value: function(value_) {
4195 value_ = value_ || [];
4197 var view = this.get_active_view();
4198 this.dataset.reset_ids([]);
4200 if(value_.length >= 1 && value_[0] instanceof Array) {
4202 _.each(value_, function(command) {
4203 var obj = {values: command[2]};
4204 switch (command[0]) {
4205 case commands.CREATE:
4206 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4208 self.dataset.to_create.push(obj);
4209 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4212 case commands.UPDATE:
4213 obj['id'] = command[1];
4214 self.dataset.to_write.push(obj);
4215 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4218 case commands.DELETE:
4219 self.dataset.to_delete.push({id: command[1]});
4221 case commands.LINK_TO:
4222 ids.push(command[1]);
4224 case commands.DELETE_ALL:
4225 self.dataset.delete_all = true;
4230 this.dataset.set_ids(ids);
4231 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
4233 this.dataset.delete_all = true;
4234 _.each(value_, function(command) {
4235 var obj = {values: command};
4236 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4238 self.dataset.to_create.push(obj);
4239 self.dataset.cache.push(_.clone(obj));
4243 this.dataset.set_ids(ids);
4245 this._super(value_);
4246 this.dataset.reset_ids(value_);
4248 if (this.dataset.index === null && this.dataset.ids.length > 0) {
4249 this.dataset.index = 0;
4251 this.trigger_on_change();
4252 if (this.is_started) {
4253 return self.reload_current_view();
4258 get_value: function() {
4262 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
4263 val = val.concat(_.map(this.dataset.ids, function(id) {
4264 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
4266 return commands.create(alter_order.values);
4268 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
4270 return commands.update(alter_order.id, alter_order.values);
4272 return commands.link_to(id);
4274 return val.concat(_.map(
4275 this.dataset.to_delete, function(x) {
4276 return commands['delete'](x.id);}));
4278 commit_value: function() {
4279 return this.save_any_view();
4281 save_any_view: function() {
4282 var view = this.get_active_view();
4284 if (this.viewmanager.active_view === "form") {
4285 if (view.controller.is_initialized.state() !== 'resolved') {
4286 return $.when(false);
4288 return $.when(view.controller.save());
4289 } else if (this.viewmanager.active_view === "list") {
4290 return $.when(view.controller.ensure_saved());
4293 return $.when(false);
4295 is_syntax_valid: function() {
4296 var view = this.get_active_view();
4300 switch (this.viewmanager.active_view) {
4302 return _(view.controller.fields).chain()
4307 return view.controller.is_valid();
4313 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
4314 template: 'One2Many.viewmanager',
4315 init: function(parent, dataset, views, flags) {
4316 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
4317 this.registry = this.registry.extend({
4318 list: 'instance.web.form.One2ManyListView',
4319 form: 'instance.web.form.One2ManyFormView',
4320 kanban: 'instance.web.form.One2ManyKanbanView',
4322 this.__ignore_blur = false;
4324 switch_mode: function(mode, unused) {
4325 if (mode !== 'form') {
4326 return this._super(mode, unused);
4329 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
4330 var pop = new instance.web.form.FormOpenPopup(this);
4331 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4332 title: _t("Open: ") + self.o2m.string,
4333 create_function: function(data, options) {
4334 return self.o2m.dataset.create(data, options).done(function(r) {
4335 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4336 self.o2m.dataset.trigger("dataset_changed", r);
4339 write_function: function(id, data, options) {
4340 return self.o2m.dataset.write(id, data, {}).done(function() {
4341 self.o2m.reload_current_view();
4344 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4345 parent_view: self.o2m.view,
4346 child_name: self.o2m.name,
4347 read_function: function() {
4348 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4350 form_view_options: {'not_interactible_on_create':true},
4351 readonly: self.o2m.get("effective_readonly")
4353 pop.on("elements_selected", self, function() {
4354 self.o2m.reload_current_view();
4359 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
4360 get_context: function() {
4361 this.context = this.o2m.build_context();
4362 return this.context;
4366 instance.web.form.One2ManyListView = instance.web.ListView.extend({
4367 _template: 'One2Many.listview',
4368 init: function (parent, dataset, view_id, options) {
4369 this._super(parent, dataset, view_id, _.extend(options || {}, {
4370 GroupsType: instance.web.form.One2ManyGroups,
4371 ListType: instance.web.form.One2ManyList
4373 this.on('edit:after', this, this.proxy('_after_edit'));
4374 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
4377 .bind('add', this.proxy("changed_records"))
4378 .bind('edit', this.proxy("changed_records"))
4379 .bind('remove', this.proxy("changed_records"));
4381 start: function () {
4382 var ret = this._super();
4384 .off('mousedown.handleButtons')
4385 .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
4388 changed_records: function () {
4389 this.o2m.trigger_on_change();
4391 is_valid: function () {
4392 var editor = this.editor;
4393 var form = editor.form;
4394 // If no edition is pending, the listview can not be invalid (?)
4395 if (!editor.record) {
4398 // If the form has not been modified, the view can only be valid
4399 // NB: is_dirty will also be set on defaults/onchanges/whatever?
4400 // oe_form_dirty seems to only be set on actual user actions
4401 if (!form.$el.is('.oe_form_dirty')) {
4404 this.o2m._dirty_flag = true;
4406 // Otherwise validate internal form
4407 return _(form.fields).chain()
4408 .invoke(function () {
4409 this._check_css_flags();
4410 return this.is_valid();
4415 do_add_record: function () {
4416 if (this.editable()) {
4417 this._super.apply(this, arguments);
4420 var pop = new instance.web.form.SelectCreatePopup(this);
4422 self.o2m.field.relation,
4424 title: _t("Create: ") + self.o2m.string,
4425 initial_view: "form",
4426 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4427 create_function: function(data, options) {
4428 return self.o2m.dataset.create(data, options).done(function(r) {
4429 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4430 self.o2m.dataset.trigger("dataset_changed", r);
4433 read_function: function() {
4434 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4436 parent_view: self.o2m.view,
4437 child_name: self.o2m.name,
4438 form_view_options: {'not_interactible_on_create':true}
4440 self.o2m.build_domain(),
4441 self.o2m.build_context()
4443 pop.on("elements_selected", self, function() {
4444 self.o2m.reload_current_view();
4448 do_activate_record: function(index, id) {
4450 var pop = new instance.web.form.FormOpenPopup(self);
4451 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4452 title: _t("Open: ") + self.o2m.string,
4453 write_function: function(id, data) {
4454 return self.o2m.dataset.write(id, data, {}).done(function() {
4455 self.o2m.reload_current_view();
4458 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4459 parent_view: self.o2m.view,
4460 child_name: self.o2m.name,
4461 read_function: function() {
4462 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4464 form_view_options: {'not_interactible_on_create':true},
4465 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
4468 do_button_action: function (name, id, callback) {
4469 if (!_.isNumber(id)) {
4470 instance.webclient.notification.warn(
4471 _t("Action Button"),
4472 _t("The o2m record must be saved before an action can be used"));
4475 var parent_form = this.o2m.view;
4477 this.ensure_saved().then(function () {
4479 return parent_form.save();
4482 }).done(function () {
4483 var ds = self.o2m.dataset;
4484 var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
4485 return value.length;
4487 if (!self.o2m.options.reload_on_button && !cached_records) {
4488 self.handle_button(name, id, callback);
4490 self.handle_button(name, id, function(){
4491 self.o2m.view.reload();
4497 _after_edit: function () {
4498 this.__ignore_blur = false;
4499 this.editor.form.on('blurred', this, this._on_form_blur);
4501 // The form's blur thing may be jiggered during the edition setup,
4502 // potentially leading to the o2m instasaving the row. Cancel any
4503 // blurring triggered the edition startup here
4504 this.editor.form.widgetFocused();
4506 _before_unedit: function () {
4507 this.editor.form.off('blurred', this, this._on_form_blur);
4509 _button_down: function () {
4510 // If a button is clicked (usually some sort of action button), it's
4511 // the button's responsibility to ensure the editable list is in the
4512 // correct state -> ignore form blurring
4513 this.__ignore_blur = true;
4516 * Handles blurring of the nested form (saves the currently edited row),
4517 * unless the flag to ignore the event is set to ``true``
4519 * Makes the internal form go away
4521 _on_form_blur: function () {
4522 if (this.__ignore_blur) {
4523 this.__ignore_blur = false;
4526 // FIXME: why isn't there an API for this?
4527 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4528 this.ensure_saved();
4531 this.cancel_edition();
4533 keypress_ENTER: function () {
4534 // blurring caused by hitting the [Return] key, should skip the
4535 // autosave-on-blur and let the handler for [Return] do its thing (save
4536 // the current row *anyway*, then create a new one/edit the next one)
4537 this.__ignore_blur = true;
4538 this._super.apply(this, arguments);
4540 do_delete: function (ids) {
4541 var confirm = window.confirm;
4542 window.confirm = function () { return true; };
4544 return this._super(ids);
4546 window.confirm = confirm;
4549 reload_record: function (record) {
4550 // Evict record.id from cache to ensure it will be reloaded correctly
4551 this.dataset.evict_record(record.get('id'));
4553 return this._super(record);
4556 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4557 setup_resequence_rows: function () {
4558 if (!this.view.o2m.get('effective_readonly')) {
4559 this._super.apply(this, arguments);
4563 instance.web.form.One2ManyList = instance.web.form.AddAnItemList.extend({
4564 _add_row_class: 'oe_form_field_one2many_list_row_add',
4565 is_readonly: function () {
4566 return this.view.o2m.get('effective_readonly');
4570 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4571 form_template: 'One2Many.formview',
4572 load_form: function(data) {
4575 this.$buttons.find('button.oe_form_button_create').click(function() {
4576 self.save().done(self.on_button_new);
4579 do_notify_change: function() {
4580 if (this.dataset.parent_view) {
4581 this.dataset.parent_view.do_notify_change();
4583 this._super.apply(this, arguments);
4588 var lazy_build_o2m_kanban_view = function() {
4589 if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
4591 instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4595 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4596 template: "FieldMany2ManyTags",
4597 tag_template: "FieldMany2ManyTag",
4599 this._super.apply(this, arguments);
4600 instance.web.form.CompletionFieldMixin.init.call(this);
4601 this.set({"value": []});
4602 this._display_orderer = new instance.web.DropMisordered();
4603 this._drop_shown = false;
4605 initialize_texttext: function(){
4608 plugins : 'tags arrow autocomplete',
4610 render: function(suggestion) {
4611 return $('<span class="text-label"/>').
4612 data('index', suggestion['index']).html(suggestion['label']);
4617 selectFromDropdown: function() {
4618 this.trigger('hideDropdown');
4619 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4620 var data = self.search_result[index];
4622 self.add_id(data.id);
4624 self.ignore_blur = true;
4627 this.trigger('setSuggestions', {result : []});
4631 isTagAllowed: function(tag) {
4635 removeTag: function(tag) {
4636 var id = tag.data("id");
4637 self.set({"value": _.without(self.get("value"), id)});
4639 renderTag: function(stuff) {
4640 return $.fn.textext.TextExtTags.prototype.renderTag.
4641 call(this, stuff).data("id", stuff.id);
4645 itemToString: function(item) {
4650 onSetInputData: function(e, data) {
4652 this._plugins.autocomplete._suggestions = null;
4654 this.input().val(data);
4660 initialize_content: function() {
4661 if (this.get("effective_readonly"))
4664 self.ignore_blur = false;
4665 self.$text = this.$("textarea");
4666 self.$text.textext(self.initialize_texttext()).bind('getSuggestions', function(e, data) {
4668 var str = !!data ? data.query || '' : '';
4669 self.get_search_result(str).done(function(result) {
4670 self.search_result = result;
4671 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4672 return _.extend(el, {index:i});
4675 }).bind('hideDropdown', function() {
4676 self._drop_shown = false;
4677 }).bind('showDropdown', function() {
4678 self._drop_shown = true;
4680 self.tags = self.$text.textext()[0].tags();
4682 .focusin(function () {
4683 self.trigger('focused');
4684 self.ignore_blur = false;
4686 .focusout(function() {
4687 self.$text.trigger("setInputData", "");
4688 if (!self.ignore_blur) {
4689 self.trigger('blurred');
4691 }).keydown(function(e) {
4692 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4693 self.$text.textext()[0].autocomplete().selectFromDropdown();
4697 // WARNING: duplicated in 4 other M2M widgets
4698 set_value: function(value_) {
4699 value_ = value_ || [];
4700 if (value_.length >= 1 && value_[0] instanceof Array) {
4701 // value_ is a list of m2m commands. We only process
4702 // LINK_TO and REPLACE_WITH in this context
4704 _.each(value_, function (command) {
4705 if (command[0] === commands.LINK_TO) {
4706 val.push(command[1]); // (4, id[, _])
4707 } else if (command[0] === commands.REPLACE_WITH) {
4708 val = command[2]; // (6, _, ids)
4713 this._super(value_);
4715 is_false: function() {
4716 return _(this.get("value")).isEmpty();
4718 get_value: function() {
4719 var tmp = [commands.replace_with(this.get("value"))];
4722 get_search_blacklist: function() {
4723 return this.get("value");
4725 map_tag: function(data){
4726 return _.map(data, function(el) {return {name: el[1], id:el[0]};})
4728 get_render_data: function(ids){
4730 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4731 return dataset.name_get(ids);
4733 render_tag: function(data) {
4735 if (! self.get("effective_readonly")) {
4736 self.tags.containerElement().children().remove();
4737 self.$('textarea').css("padding-left", "3px");
4738 self.tags.addTags(self.map_tag(data));
4740 self.$el.html(QWeb.render(self.tag_template, {elements: data}));
4743 render_value: function() {
4745 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4746 var values = self.get("value");
4747 var handle_names = function(data) {
4748 if (self.isDestroyed())
4751 _.each(data, function(el) {
4752 indexed[el[0]] = el;
4754 data = _.map(values, function(el) { return indexed[el]; });
4755 self.render_tag(data);
4757 if (! values || values.length > 0) {
4758 return this._display_orderer.add(self.get_render_data(values)).done(handle_names);
4763 add_id: function(id) {
4764 this.set({'value': _.uniq(this.get('value').concat([id]))});
4766 focus: function () {
4767 var input = this.$text && this.$text[0];
4768 return input ? input.focus() : false;
4770 set_dimensions: function (height, width) {
4771 this._super(height, width);
4772 this.$("textarea").css({
4777 _search_create_popup: function() {
4778 self.ignore_blur = true;
4779 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
4785 - reload_on_button: Reload the whole form view if click on a button in a list view.
4786 If you see this options, do not use it, it's basically a dirty hack to make one
4787 precise o2m to behave the way we want.
4789 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4790 multi_selection: false,
4791 disable_utility_classes: true,
4792 init: function(field_manager, node) {
4793 this._super(field_manager, node);
4794 this.is_loaded = $.Deferred();
4795 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4796 this.dataset.m2m = this;
4798 this.dataset.on('unlink', self, function(ids) {
4799 self.dataset_changed();
4802 this.list_dm = new instance.web.DropMisordered();
4803 this.render_value_dm = new instance.web.DropMisordered();
4805 initialize_content: function() {
4808 this.$el.addClass('oe_form_field oe_form_field_many2many');
4810 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4812 'deletable': this.get("effective_readonly") ? false : true,
4813 'selectable': this.multi_selection,
4815 'reorderable': false,
4816 'import_enabled': false,
4818 var embedded = (this.field.views || {}).tree;
4820 this.list_view.set_embedded_view(embedded);
4822 this.list_view.m2m_field = this;
4823 var loaded = $.Deferred();
4824 this.list_view.on("list_view_loaded", this, function() {
4827 this.list_view.appendTo(this.$el);
4829 var old_def = self.is_loaded;
4830 self.is_loaded = $.Deferred().done(function() {
4833 this.list_dm.add(loaded).then(function() {
4834 self.is_loaded.resolve();
4837 destroy_content: function() {
4838 this.list_view.destroy();
4839 this.list_view = undefined;
4841 // WARNING: duplicated in 4 other M2M widgets
4842 set_value: function(value_) {
4843 value_ = value_ || [];
4844 if (value_.length >= 1 && value_[0] instanceof Array) {
4845 // value_ is a list of m2m commands. We only process
4846 // LINK_TO and REPLACE_WITH in this context
4848 _.each(value_, function (command) {
4849 if (command[0] === commands.LINK_TO) {
4850 val.push(command[1]); // (4, id[, _])
4851 } else if (command[0] === commands.REPLACE_WITH) {
4852 val = command[2]; // (6, _, ids)
4857 this._super(value_);
4859 get_value: function() {
4860 return [commands.replace_with(this.get('value'))];
4862 is_false: function () {
4863 return _(this.get("value")).isEmpty();
4865 render_value: function() {
4867 this.dataset.set_ids(this.get("value"));
4868 this.render_value_dm.add(this.is_loaded).then(function() {
4869 return self.list_view.reload_content();
4872 dataset_changed: function() {
4873 this.internal_set_value(this.dataset.ids);
4877 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4878 get_context: function() {
4879 this.context = this.m2m.build_context();
4880 return this.context;
4886 * @extends instance.web.ListView
4888 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4889 init: function (parent, dataset, view_id, options) {
4890 this._super(parent, dataset, view_id, _.extend(options || {}, {
4891 ListType: instance.web.form.Many2ManyList,
4894 do_add_record: function () {
4895 var pop = new instance.web.form.SelectCreatePopup(this);
4899 title: _t("Add: ") + this.m2m_field.string,
4900 no_create: this.m2m_field.options.no_create,
4902 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4903 this.m2m_field.build_context()
4906 pop.on("elements_selected", self, function(element_ids) {
4908 _(element_ids).each(function (id) {
4909 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4910 self.dataset.set_ids(self.dataset.ids.concat([id]));
4911 self.m2m_field.dataset_changed();
4916 self.reload_content();
4920 do_activate_record: function(index, id) {
4922 var pop = new instance.web.form.FormOpenPopup(this);
4923 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4924 title: _t("Open: ") + this.m2m_field.string,
4925 readonly: this.getParent().get("effective_readonly")
4927 pop.on('write_completed', self, self.reload_content);
4929 do_button_action: function(name, id, callback) {
4931 var _sup = _.bind(this._super, this);
4932 if (! this.m2m_field.options.reload_on_button) {
4933 return _sup(name, id, callback);
4935 return this.m2m_field.view.save().then(function() {
4936 return _sup(name, id, function() {
4937 self.m2m_field.view.reload();
4942 is_action_enabled: function () { return true; },
4944 instance.web.form.Many2ManyList = instance.web.form.AddAnItemList.extend({
4945 _add_row_class: 'oe_form_field_many2many_list_row_add',
4946 is_readonly: function () {
4947 return this.view.m2m_field.get('effective_readonly');
4951 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4952 disable_utility_classes: true,
4953 init: function(field_manager, node) {
4954 this._super(field_manager, node);
4955 instance.web.form.CompletionFieldMixin.init.call(this);
4956 m2m_kanban_lazy_init();
4957 this.is_loaded = $.Deferred();
4958 this.initial_is_loaded = this.is_loaded;
4961 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4962 this.dataset.m2m = this;
4963 this.dataset.on('unlink', self, function(ids) {
4964 self.dataset_changed();
4968 this._super.apply(this, arguments);
4973 self.on("change:effective_readonly", self, function() {
4974 self.is_loaded = self.is_loaded.then(function() {
4975 self.kanban_view.destroy();
4976 return $.when(self.load_view()).done(function() {
4977 self.render_value();
4982 // WARNING: duplicated in 4 other M2M widgets
4983 set_value: function(value_) {
4984 value_ = value_ || [];
4985 if (value_.length >= 1 && value_[0] instanceof Array) {
4986 // value_ is a list of m2m commands. We only process
4987 // LINK_TO and REPLACE_WITH in this context
4989 _.each(value_, function (command) {
4990 if (command[0] === commands.LINK_TO) {
4991 val.push(command[1]); // (4, id[, _])
4992 } else if (command[0] === commands.REPLACE_WITH) {
4993 val = command[2]; // (6, _, ids)
4998 this._super(value_);
5000 get_value: function() {
5001 return [commands.replace_with(this.get('value'))];
5003 load_view: function() {
5005 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
5006 'create_text': _t("Add"),
5007 'creatable': self.get("effective_readonly") ? false : true,
5008 'quick_creatable': self.get("effective_readonly") ? false : true,
5009 'read_only_mode': self.get("effective_readonly") ? true : false,
5010 'confirm_on_delete': false,
5012 var embedded = (this.field.views || {}).kanban;
5014 this.kanban_view.set_embedded_view(embedded);
5016 this.kanban_view.m2m = this;
5017 var loaded = $.Deferred();
5018 this.kanban_view.on("kanban_view_loaded",self,function() {
5019 self.initial_is_loaded.resolve();
5022 this.kanban_view.on('switch_mode', this, this.open_popup);
5023 $.async_when().done(function () {
5024 self.kanban_view.appendTo(self.$el);
5028 render_value: function() {
5030 this.dataset.set_ids(this.get("value"));
5031 this.is_loaded = this.is_loaded.then(function() {
5032 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
5035 dataset_changed: function() {
5036 this.set({'value': this.dataset.ids});
5038 open_popup: function(type, unused) {
5039 if (type !== "form")
5043 if (this.dataset.index === null) {
5044 pop = new instance.web.form.SelectCreatePopup(this);
5046 this.field.relation,
5048 title: _t("Add: ") + this.string
5050 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
5051 this.build_context()
5053 pop.on("elements_selected", self, function(element_ids) {
5054 _.each(element_ids, function(one_id) {
5055 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
5056 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
5057 self.dataset_changed();
5058 self.render_value();
5063 var id = self.dataset.ids[self.dataset.index];
5064 pop = new instance.web.form.FormOpenPopup(this);
5065 pop.show_element(self.field.relation, id, self.build_context(), {
5066 title: _t("Open: ") + self.string,
5067 write_function: function(id, data, options) {
5068 return self.dataset.write(id, data, {}).done(function() {
5069 self.render_value();
5072 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
5073 parent_view: self.view,
5074 child_name: self.name,
5075 readonly: self.get("effective_readonly")
5079 add_id: function(id) {
5080 this.quick_create.add_id(id);
5084 function m2m_kanban_lazy_init() {
5085 if (instance.web.form.Many2ManyKanbanView)
5087 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
5088 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
5089 _is_quick_create_enabled: function() {
5090 return this._super() && ! this.group_by;
5093 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
5094 template: 'Many2ManyKanban.quick_create',
5097 * close_btn: If true, the widget will display a "Close" button able to trigger
5100 init: function(parent, dataset, context, buttons) {
5101 this._super(parent);
5102 this.m2m = this.getParent().view.m2m;
5103 this.m2m.quick_create = this;
5104 this._dataset = dataset;
5105 this._buttons = buttons || false;
5106 this._context = context || {};
5108 start: function () {
5110 self.$text = this.$el.find('input').css("width", "200px");
5111 self.$text.textext({
5112 plugins : 'arrow autocomplete',
5114 render: function(suggestion) {
5115 return $('<span class="text-label"/>').
5116 data('index', suggestion['index']).html(suggestion['label']);
5121 selectFromDropdown: function() {
5122 $(this).trigger('hideDropdown');
5123 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
5124 var data = self.search_result[index];
5126 self.add_id(data.id);
5133 itemToString: function(item) {
5138 }).bind('getSuggestions', function(e, data) {
5140 var str = !!data ? data.query || '' : '';
5141 self.m2m.get_search_result(str).done(function(result) {
5142 self.search_result = result;
5143 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
5144 return _.extend(el, {index:i});
5148 self.$text.focusout(function() {
5153 this.$text[0].focus();
5155 add_id: function(id) {
5158 self.trigger('added', id);
5159 this.m2m.dataset_changed();
5165 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
5167 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
5168 template: "AbstractFormPopup.render",
5171 * -readonly: only applicable when not in creation mode, default to false
5172 * - alternative_form_view
5179 * - form_view_options
5181 init_popup: function(model, row_id, domain, context, options) {
5182 this.row_id = row_id;
5184 this.domain = domain || [];
5185 this.context = context || {};
5186 this.options = options;
5187 _.defaults(this.options, {
5190 init_dataset: function() {
5192 this.created_elements = [];
5193 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
5194 this.dataset.read_function = this.options.read_function;
5195 this.dataset.create_function = function(data, options, sup) {
5196 var fct = self.options.create_function || sup;
5197 return fct.call(this, data, options).done(function(r) {
5198 self.trigger('create_completed saved', r);
5199 self.created_elements.push(r);
5202 this.dataset.write_function = function(id, data, options, sup) {
5203 var fct = self.options.write_function || sup;
5204 return fct.call(this, id, data, options).done(function(r) {
5205 self.trigger('write_completed saved', r);
5208 this.dataset.parent_view = this.options.parent_view;
5209 this.dataset.child_name = this.options.child_name;
5211 display_popup: function() {
5213 this.renderElement();
5214 var dialog = new instance.web.Dialog(this, {
5215 dialogClass: 'oe_act_window',
5216 title: this.options.title || "",
5217 }, this.$el).open();
5218 dialog.on('closing', this, function (e){
5219 self.check_exit(true);
5221 this.$buttonpane = dialog.$buttons;
5224 setup_form_view: function() {
5227 this.dataset.ids = [this.row_id];
5228 this.dataset.index = 0;
5230 this.dataset.index = null;
5232 var options = _.clone(self.options.form_view_options) || {};
5233 if (this.row_id !== null) {
5234 options.initial_mode = this.options.readonly ? "view" : "edit";
5237 $buttons: this.$buttonpane,
5239 this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
5240 if (this.options.alternative_form_view) {
5241 this.view_form.set_embedded_view(this.options.alternative_form_view);
5243 this.view_form.appendTo(this.$el.find(".oe_popup_form"));
5244 this.view_form.on("form_view_loaded", self, function() {
5245 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
5246 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
5247 multi_select: multi_select,
5248 readonly: self.row_id !== null && self.options.readonly,
5250 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
5251 $snbutton.click(function() {
5252 $.when(self.view_form.save()).done(function() {
5253 self.view_form.reload_mutex.exec(function() {
5254 self.view_form.on_button_new();
5258 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
5259 $sbutton.click(function() {
5260 $.when(self.view_form.save()).done(function() {
5261 self.view_form.reload_mutex.exec(function() {
5266 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
5267 $cbutton.click(function() {
5268 self.view_form.trigger('on_button_cancel');
5271 self.view_form.do_show();
5274 select_elements: function(element_ids) {
5275 this.trigger("elements_selected", element_ids);
5277 check_exit: function(no_destroy) {
5278 if (this.created_elements.length > 0) {
5279 this.select_elements(this.created_elements);
5280 this.created_elements = [];
5282 this.trigger('closed');
5285 destroy: function () {
5286 this.trigger('closed');
5287 if (this.$el.is(":data(bs.modal)")) {
5288 this.$el.parents('.modal').modal('hide');
5295 * Class to display a popup containing a form view.
5297 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
5298 show_element: function(model, row_id, context, options) {
5299 this.init_popup(model, row_id, [], context, options);
5300 _.defaults(this.options, {
5302 this.display_popup();
5306 this.init_dataset();
5307 this.setup_form_view();
5312 * Class to display a popup to display a list to search a row. It also allows
5313 * to switch to a form view to create a new row.
5315 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
5319 * - initial_view: form or search (default search)
5320 * - disable_multiple_selection
5321 * - list_view_options
5323 select_element: function(model, options, domain, context) {
5324 this.init_popup(model, null, domain, context, options);
5326 _.defaults(this.options, {
5327 initial_view: "search",
5329 this.initial_ids = this.options.initial_ids;
5330 this.display_popup();
5334 this.init_dataset();
5335 if (this.options.initial_view == "search") {
5336 instance.web.pyeval.eval_domains_and_contexts({
5338 contexts: [this.context]
5339 }).done(function (results) {
5340 var search_defaults = {};
5341 _.each(results.context, function (value_, key) {
5342 var match = /^search_default_(.*)$/.exec(key);
5344 search_defaults[match[1]] = value_;
5347 self.setup_search_view(search_defaults);
5353 setup_search_view: function(search_defaults) {
5355 if (this.searchview) {
5356 this.searchview.destroy();
5358 if (this.searchview_drawer) {
5359 this.searchview_drawer.destroy();
5361 this.searchview = new instance.web.SearchView(this,
5362 this.dataset, false, search_defaults);
5363 this.searchview_drawer = new instance.web.SearchViewDrawer(this, this.searchview);
5364 this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
5365 if (self.initial_ids) {
5366 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
5367 contexts.concat(self.context), groupbys);
5368 self.initial_ids = undefined;
5370 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
5373 this.searchview.on("search_view_loaded", self, function() {
5374 self.view_list = new instance.web.form.SelectCreateListView(self,
5375 self.dataset, false,
5376 _.extend({'deletable': false,
5377 'selectable': !self.options.disable_multiple_selection,
5378 'import_enabled': false,
5379 '$buttons': self.$buttonpane,
5380 'disable_editable_mode': true,
5381 '$pager': self.$('.oe_popup_list_pager'),
5382 }, self.options.list_view_options || {}));
5383 self.view_list.on('edit:before', self, function (e) {
5386 self.view_list.popup = self;
5387 self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
5388 self.view_list.do_show();
5389 }).then(function() {
5390 self.searchview.do_search();
5392 self.view_list.on("list_view_loaded", self, function() {
5393 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
5394 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
5395 $cbutton.click(function() {
5398 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
5399 $sbutton.click(function() {
5400 self.select_elements(self.selected_ids);
5403 $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
5404 $cbutton.click(function() {
5409 this.searchview.appendTo(this.$(".oe_popup_search"));
5411 do_search: function(domains, contexts, groupbys) {
5413 instance.web.pyeval.eval_domains_and_contexts({
5414 domains: domains || [],
5415 contexts: contexts || [],
5416 group_by_seq: groupbys || []
5417 }).done(function (results) {
5418 self.view_list.do_search(results.domain, results.context, results.group_by);
5421 on_click_element: function(ids) {
5423 this.selected_ids = ids || [];
5424 if(this.selected_ids.length > 0) {
5425 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
5427 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
5430 new_object: function() {
5431 if (this.searchview) {
5432 this.searchview.hide();
5434 if (this.view_list) {
5435 this.view_list.do_hide();
5437 this.setup_form_view();
5441 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
5442 do_add_record: function () {
5443 this.popup.new_object();
5445 select_record: function(index) {
5446 this.popup.select_elements([this.dataset.ids[index]]);
5447 this.popup.destroy();
5449 do_select: function(ids, records) {
5450 this._super(ids, records);
5451 this.popup.on_click_element(ids);
5455 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5456 template: 'FieldReference',
5457 init: function(field_manager, node) {
5458 this._super(field_manager, node);
5459 this.reference_ready = true;
5461 destroy_content: function() {
5464 this.fm = undefined;
5467 initialize_content: function() {
5469 var fm = new instance.web.form.DefaultFieldManager(this);
5471 fm.extend_field_desc({
5473 selection: this.field_manager.get_field_desc(this.name).selection,
5481 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
5483 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5485 this.selection.on("change:value", this, this.on_selection_changed);
5486 this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
5488 .on('focused', null, function () {self.trigger('focused');})
5489 .on('blurred', null, function () {self.trigger('blurred');});
5491 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
5492 name: 'Referenced Document',
5493 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5495 this.m2o.on("change:value", this, this.data_changed);
5496 this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
5498 .on('focused', null, function () {self.trigger('focused');})
5499 .on('blurred', null, function () {self.trigger('blurred');});
5501 on_selection_changed: function() {
5502 if (this.reference_ready) {
5503 this.internal_set_value([this.selection.get_value(), false]);
5504 this.render_value();
5507 data_changed: function() {
5508 if (this.reference_ready) {
5509 this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
5512 set_value: function(val) {
5514 val = val.split(',');
5515 val[0] = val[0] || false;
5516 val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
5518 this._super(val || [false, false]);
5520 get_value: function() {
5521 return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
5523 render_value: function() {
5524 this.reference_ready = false;
5525 if (!this.get("effective_readonly")) {
5526 this.selection.set_value(this.get('value')[0]);
5528 this.m2o.field.relation = this.get('value')[0];
5529 this.m2o.set_value(this.get('value')[1]);
5530 this.m2o.$el.toggle(!!this.get('value')[0]);
5531 this.reference_ready = true;
5535 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5536 init: function(field_manager, node) {
5538 this._super(field_manager, node);
5539 this.binary_value = false;
5540 this.useFileAPI = !!window.FileReader;
5541 this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5542 if (!this.useFileAPI) {
5543 this.fileupload_id = _.uniqueId('oe_fileupload');
5544 $(window).on(this.fileupload_id, function() {
5545 var args = [].slice.call(arguments).slice(1);
5546 self.on_file_uploaded.apply(self, args);
5551 if (!this.useFileAPI) {
5552 $(window).off(this.fileupload_id);
5554 this._super.apply(this, arguments);
5556 initialize_content: function() {
5558 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5559 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5560 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5561 this.$el.find('.oe_form_binary_file_edit').click(function(event){
5562 self.$el.find('input.oe_form_binary_file').click();
5565 on_file_change: function(e) {
5567 var file_node = e.target;
5568 if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5569 if (this.useFileAPI) {
5570 var file = file_node.files[0];
5571 if (file.size > this.max_upload_size) {
5572 var msg = _t("The selected file exceed the maximum file size of %s.");
5573 instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5576 var filereader = new FileReader();
5577 filereader.readAsDataURL(file);
5578 filereader.onloadend = function(upload) {
5579 var data = upload.target.result;
5580 data = data.split(',')[1];
5581 self.on_file_uploaded(file.size, file.name, file.type, data);
5584 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5585 this.$el.find('form.oe_form_binary_form').submit();
5587 this.$el.find('.oe_form_binary_progress').show();
5588 this.$el.find('.oe_form_binary').hide();
5591 on_file_uploaded: function(size, name, content_type, file_base64) {
5592 if (size === false) {
5593 this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5594 // TODO: use openerp web crashmanager
5595 console.warn("Error while uploading file : ", name);
5597 this.filename = name;
5598 this.on_file_uploaded_and_valid.apply(this, arguments);
5600 this.$el.find('.oe_form_binary_progress').hide();
5601 this.$el.find('.oe_form_binary').show();
5603 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5605 on_save_as: function(ev) {
5606 var value = this.get('value');
5608 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5609 ev.stopPropagation();
5611 instance.web.blockUI();
5612 var c = instance.webclient.crashmanager;
5613 this.session.get_file({
5614 url: '/web/binary/saveas_ajax',
5615 data: {data: JSON.stringify({
5616 model: this.view.dataset.model,
5617 id: (this.view.datarecord.id || ''),
5619 filename_field: (this.node.attrs.filename || ''),
5620 data: instance.web.form.is_bin_size(value) ? null : value,
5621 context: this.view.dataset.get_context()
5623 complete: instance.web.unblockUI,
5624 error: c.rpc_error.bind(c)
5626 ev.stopPropagation();
5630 set_filename: function(value) {
5631 var filename = this.node.attrs.filename;
5634 tmp[filename] = value;
5635 this.field_manager.set_values(tmp);
5638 on_clear: function() {
5639 if (this.get('value') !== false) {
5640 this.binary_value = false;
5641 this.internal_set_value(false);
5647 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5648 template: 'FieldBinaryFile',
5649 initialize_content: function() {
5651 if (this.get("effective_readonly")) {
5653 this.$el.find('a').click(function(ev) {
5654 if (self.get('value')) {
5655 self.on_save_as(ev);
5661 render_value: function() {
5663 if (!this.get("effective_readonly")) {
5664 if (this.node.attrs.filename) {
5665 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5667 show_value = (this.get('value') !== null && this.get('value') !== undefined && this.get('value') !== false) ? this.get('value') : '';
5669 this.$el.find('input').eq(0).val(show_value);
5671 this.$el.find('a').toggle(!!this.get('value'));
5672 if (this.get('value')) {
5673 show_value = _t("Download");
5675 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5676 this.$el.find('a').text(show_value);
5680 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5681 this.binary_value = true;
5682 this.internal_set_value(file_base64);
5683 var show_value = name + " (" + instance.web.human_size(size) + ")";
5684 this.$el.find('input').eq(0).val(show_value);
5685 this.set_filename(name);
5687 on_clear: function() {
5688 this._super.apply(this, arguments);
5689 this.$el.find('input').eq(0).val('');
5690 this.set_filename('');
5694 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5695 template: 'FieldBinaryImage',
5696 placeholder: "/web/static/src/img/placeholder.png",
5697 render_value: function() {
5700 if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5701 url = 'data:image/png;base64,' + this.get('value');
5702 } else if (this.get('value')) {
5703 var id = JSON.stringify(this.view.datarecord.id || null);
5704 var field = this.name;
5705 if (this.options.preview_image)
5706 field = this.options.preview_image;
5707 url = this.session.url('/web/binary/image', {
5708 model: this.view.dataset.model,
5711 t: (new Date().getTime()),
5714 url = this.placeholder;
5716 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5717 $($img).click(function(e) {
5718 if(self.view.get("actual_mode") == "view") {
5719 var $button = $(".oe_form_button_edit");
5720 $button.openerpBounce();
5721 e.stopPropagation();
5724 this.$el.find('> img').remove();
5725 this.$el.prepend($img);
5726 $img.load(function() {
5727 if (! self.options.size)
5729 $img.css("max-width", "" + self.options.size[0] + "px");
5730 $img.css("max-height", "" + self.options.size[1] + "px");
5732 $img.on('error', function() {
5733 $img.attr('src', self.placeholder);
5734 instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5737 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5738 this.internal_set_value(file_base64);
5739 this.binary_value = true;
5740 this.render_value();
5741 this.set_filename(name);
5743 on_clear: function() {
5744 this._super.apply(this, arguments);
5745 this.render_value();
5746 this.set_filename('');
5748 set_value: function(value_){
5749 var changed = value_ !== this.get_value();
5750 this._super.apply(this, arguments);
5751 // By default, on binary images read, the server returns the binary size
5752 // This is possible that two images have the exact same size
5753 // Therefore we trigger the change in case the image value hasn't changed
5754 // So the image is re-rendered correctly
5756 this.trigger("change:value", this, {
5765 * Widget for (many2many field) to upload one or more file in same time and display in list.
5766 * The user can delete his files.
5767 * Options on attribute ; "blockui" {Boolean} block the UI or not
5768 * during the file is uploading
5770 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
5771 template: "FieldBinaryFileUploader",
5772 init: function(field_manager, node) {
5773 this._super(field_manager, node);
5774 this.field_manager = field_manager;
5776 if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5777 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);
5781 this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5782 this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5783 $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5787 this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5789 // WARNING: duplicated in 4 other M2M widgets
5790 set_value: function(value_) {
5791 value_ = value_ || [];
5792 if (value_.length >= 1 && value_[0] instanceof Array) {
5793 // value_ is a list of m2m commands. We only process
5794 // LINK_TO and REPLACE_WITH in this context
5796 _.each(value_, function (command) {
5797 if (command[0] === commands.LINK_TO) {
5798 val.push(command[1]); // (4, id[, _])
5799 } else if (command[0] === commands.REPLACE_WITH) {
5800 val = command[2]; // (6, _, ids)
5805 this._super(value_);
5807 get_value: function() {
5808 var tmp = [commands.replace_with(this.get("value"))];
5811 get_file_url: function (attachment) {
5812 return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5814 read_name_values : function () {
5816 // don't reset know values
5817 var ids = this.get('value');
5818 var _value = _.filter(ids, function (id) { return typeof self.data[id] == 'undefined'; } );
5819 // send request for get_name
5820 if (_value.length) {
5821 return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).then(function (datas) {
5822 _.each(datas, function (data) {
5823 data.no_unlink = true;
5824 data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5825 self.data[data.id] = data;
5833 render_value: function () {
5835 this.read_name_values().then(function (ids) {
5836 var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids}));
5837 render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5838 self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5840 // reinit input type file
5841 var $input = self.$('input.oe_form_binary_file');
5842 $input.after($input.clone(true)).remove();
5843 self.$(".oe_fileupload").show();
5847 on_file_change: function (event) {
5848 event.stopPropagation();
5850 var $target = $(event.target);
5851 if ($target.val() !== '') {
5852 var filename = $target.val().replace(/.*[\\\/]/,'');
5853 // don't uplode more of one file in same time
5854 if (self.data[0] && self.data[0].upload ) {
5857 for (var id in this.get('value')) {
5858 // if the files exits, delete the file before upload (if it's a new file)
5859 if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
5860 self.ds_file.unlink([id]);
5865 if(this.node.attrs.blockui>0) {
5866 instance.web.blockUI();
5869 // TODO : unactivate send on wizard and form
5872 this.$('form.oe_form_binary_form').submit();
5873 this.$(".oe_fileupload").hide();
5874 // add file on data result
5878 'filename': filename,
5884 on_file_loaded: function (event, result) {
5885 var files = this.get('value');
5888 if(this.node.attrs.blockui>0) {
5889 instance.web.unblockUI();
5892 if (result.error || !result.id ) {
5893 this.do_warn( _t('Uploading Error'), result.error);
5894 delete this.data[0];
5896 if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
5897 delete this.data[0];
5898 this.data[result.id] = {
5900 'name': result.name,
5901 'filename': result.filename,
5902 'url': this.get_file_url(result)
5905 this.data[result.id] = {
5907 'name': result.name,
5908 'filename': result.filename,
5909 'url': this.get_file_url(result)
5912 var values = _.clone(this.get('value'));
5913 values.push(result.id);
5914 this.set({'value': values});
5916 this.render_value();
5918 on_file_delete: function (event) {
5919 event.stopPropagation();
5920 var file_id=$(event.target).data("id");
5922 var files = _.filter(this.get('value'), function (id) {return id != file_id;});
5923 if(!this.data[file_id].no_unlink) {
5924 this.ds_file.unlink([file_id]);
5926 this.set({'value': files});
5931 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5932 template: "FieldStatus",
5933 init: function(field_manager, node) {
5934 this._super(field_manager, node);
5935 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5936 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5937 this.set({value: false});
5938 this.selection = {'unfolded': [], 'folded': []};
5939 this.set("selection", {'unfolded': [], 'folded': []});
5940 this.selection_dm = new instance.web.DropMisordered();
5941 this.dataset = new instance.web.DataSetStatic(this, this.field.relation, this.build_context());
5944 this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5946 this.on("change:value", this, this.get_selection);
5947 this.on("change:evaluated_selection_domain", this, this.get_selection);
5948 this.on("change:selection", this, function() {
5949 this.selection = this.get("selection");
5950 this.render_value();
5952 this.get_selection();
5953 if (this.options.clickable) {
5954 this.$el.on('click','li[data-id]',this.on_click_stage);
5956 if (this.$el.parent().is('header')) {
5957 this.$el.after('<div class="oe_clear"/>');
5961 set_value: function(value_) {
5962 if (value_ instanceof Array) {
5965 this._super(value_);
5967 render_value: function() {
5969 var content = QWeb.render("FieldStatus.content", {
5971 'value_folded': _.find(self.selection.folded, function(i){return i[0] === self.get('value');})
5973 self.$el.html(content);
5975 calc_domain: function() {
5976 var d = instance.web.pyeval.eval('domain', this.build_domain());
5977 var domain = []; //if there is no domain defined, fetch all the records
5980 domain = ['|',['id', '=', this.get('value')]].concat(d);
5983 if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5984 this.set("evaluated_selection_domain", domain);
5987 /** Get the selection and render it
5988 * selection: [[identifier, value_to_display], ...]
5989 * For selection fields: this is directly given by this.field.selection
5990 * For many2one fields: perform a search on the relation of the many2one field
5992 get_selection: function() {
5994 var selection_unfolded = [];
5995 var selection_folded = [];
5996 var fold_field = this.options.fold_field;
5998 var calculation = _.bind(function() {
5999 if (this.field.type == "many2one") {
6000 return self.get_distant_fields().then(function (fields) {
6001 return new instance.web.DataSetSearch(self, self.field.relation, self.build_context(), self.get("evaluated_selection_domain"))
6002 .read_slice(_.union(_.keys(self.distant_fields), ['id']), {}).then(function (records) {
6003 var ids = _.pluck(records, 'id');
6004 return self.dataset.name_get(ids).then(function (records_name) {
6005 _.each(records, function (record) {
6006 var name = _.find(records_name, function (val) {return val[0] == record.id;})[1];
6007 if (fold_field && record[fold_field] && record.id != self.get('value')) {
6008 selection_folded.push([record.id, name]);
6010 selection_unfolded.push([record.id, name]);
6017 // For field type selection filter values according to
6018 // statusbar_visible attribute of the field. For example:
6019 // statusbar_visible="draft,open".
6020 var select = this.field.selection;
6021 for(var i=0; i < select.length; i++) {
6022 var key = select[i][0];
6023 if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
6024 selection_unfolded.push(select[i]);
6030 this.selection_dm.add(calculation()).then(function () {
6031 var selection = {'unfolded': selection_unfolded, 'folded': selection_folded};
6032 if (! _.isEqual(selection, self.get("selection"))) {
6033 self.set("selection", selection);
6038 * :deprecated: this feature will probably be removed with OpenERP v8
6040 get_distant_fields: function() {
6042 if (! this.options.fold_field) {
6043 this.distant_fields = {}
6045 if (this.distant_fields) {
6046 return $.when(this.distant_fields);
6048 return new instance.web.Model(self.field.relation).call("fields_get", [[this.options.fold_field]]).then(function(fields) {
6049 self.distant_fields = fields;
6053 on_click_stage: function (ev) {
6055 var $li = $(ev.currentTarget);
6057 if (this.field.type == "many2one") {
6058 val = parseInt($li.data("id"), 10);
6061 val = $li.data("id");
6063 if (val != self.get('value')) {
6064 this.view.recursive_save().done(function() {
6066 change[self.name] = val;
6067 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
6075 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
6076 template: "FieldMonetary",
6077 widget_class: 'oe_form_field_float oe_form_field_monetary',
6079 this._super.apply(this, arguments);
6080 this.set({"currency": false});
6081 if (this.options.currency_field) {
6082 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
6083 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
6086 this.on("change:currency", this, this.get_currency_info);
6087 this.get_currency_info();
6088 this.ci_dm = new instance.web.DropMisordered();
6091 var tmp = this._super();
6092 this.on("change:currency_info", this, this.reinitialize);
6095 get_currency_info: function() {
6097 if (this.get("currency") === false) {
6098 this.set({"currency_info": null});
6101 return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
6102 .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
6103 self.set({"currency_info": res});
6106 parse_value: function(val, def) {
6107 return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6109 format_value: function(val, def) {
6110 return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6115 This type of field display a list of checkboxes. It works only with m2ms. This field will display one checkbox for each
6116 record existing in the model targeted by the relation, according to the given domain if one is specified. Checked records
6117 will be added to the relation.
6119 instance.web.form.FieldMany2ManyCheckBoxes = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6120 className: "oe_form_many2many_checkboxes",
6122 this._super.apply(this, arguments);
6123 this.set("value", {});
6124 this.set("records", []);
6125 this.field_manager.on("view_content_has_changed", this, function() {
6126 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
6127 if (! _.isEqual(domain, this.get("domain"))) {
6128 this.set("domain", domain);
6131 this.records_orderer = new instance.web.DropMisordered();
6133 initialize_field: function() {
6134 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
6135 this.on("change:domain", this, this.query_records);
6136 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
6137 this.on("change:records", this, this.render_value);
6139 query_records: function() {
6141 var model = new openerp.Model(openerp.session, this.field.relation);
6142 this.records_orderer.add(model.call("search", [this.get("domain")], {"context": this.build_context()}).then(function(record_ids) {
6143 return model.call("name_get", [record_ids] , {"context": self.build_context()});
6144 })).then(function(res) {
6145 self.set("records", res);
6148 render_value: function() {
6149 this.$().html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this, selected: this.get("value")}));
6150 var inputs = this.$("input");
6151 inputs.change(_.bind(this.from_dom, this));
6152 if (this.get("effective_readonly"))
6153 inputs.attr("disabled", "true");
6155 from_dom: function() {
6157 this.$("input").each(function() {
6159 new_value[elem.data("record-id")] = elem.attr("checked") ? true : undefined;
6161 if (! _.isEqual(new_value, this.get("value")))
6162 this.internal_set_value(new_value);
6164 // WARNING: (mostly) duplicated in 4 other M2M widgets
6165 set_value: function(value_) {
6166 value_ = value_ || [];
6167 if (value_.length >= 1 && value_[0] instanceof Array) {
6168 // value_ is a list of m2m commands. We only process
6169 // LINK_TO and REPLACE_WITH in this context
6171 _.each(value_, function (command) {
6172 if (command[0] === commands.LINK_TO) {
6173 val.push(command[1]); // (4, id[, _])
6174 } else if (command[0] === commands.REPLACE_WITH) {
6175 val = command[2]; // (6, _, ids)
6181 _.each(value_, function(el) {
6182 formatted[JSON.stringify(el)] = true;
6184 this._super(formatted);
6186 get_value: function() {
6187 var value = _.filter(_.keys(this.get("value")), function(el) {
6188 return this.get("value")[el];
6190 value = _.map(value, function(el) {
6191 return JSON.parse(el);
6193 return [commands.replace_with(value)];
6198 This field can be applied on many2many and one2many. It is a read-only field that will display a single link whose name is
6199 "<number of linked records> <label of the field>". When the link is clicked, it will redirect to another act_window
6200 action on the model of the relation and show only the linked records.
6204 * 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
6205 to display (or False to take the default one) and the second element is the type of the view. Defaults to
6206 [[false, "tree"], [false, "form"]] .
6208 instance.web.form.X2ManyCounter = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6209 className: "oe_form_x2many_counter",
6211 this._super.apply(this, arguments);
6212 this.set("value", []);
6213 _.defaults(this.options, {
6214 "views": [[false, "tree"], [false, "form"]],
6217 render_value: function() {
6218 var text = _.str.sprintf("%d %s", this.val().length, this.string);
6219 this.$().html(QWeb.render("X2ManyCounter", {text: text}));
6220 this.$("a").click(_.bind(this.go_to, this));
6223 return this.view.recursive_save().then(_.bind(function() {
6224 var val = this.val();
6226 if (this.field.type === "one2many") {
6227 context["default_" + this.field.relation_field] = this.view.datarecord.id;
6229 var domain = [["id", "in", val]];
6230 return this.do_action({
6231 type: 'ir.actions.act_window',
6233 res_model: this.field.relation,
6234 views: this.options.views,
6242 var value = this.get("value") || [];
6243 if (value.length >= 1 && value[0] instanceof Array) {
6244 value = value[0][2];
6251 This widget is intended to be used on stat button numeric fields. It will display
6252 the value many2many and one2many. It is a read-only field that will
6253 display a simple string "<value of field> <label of the field>"
6255 instance.web.form.StatInfo = instance.web.form.AbstractField.extend({
6256 is_field_number: true,
6258 this._super.apply(this, arguments);
6259 this.internal_set_value(0);
6261 set_value: function(value_) {
6262 if (value_ === false || value_ === undefined) {
6265 this._super.apply(this, [value_]);
6267 render_value: function() {
6269 value: this.get("value") || 0,
6271 if (! this.node.attrs.nolabel) {
6272 options.text = this.string
6274 this.$el.html(QWeb.render("StatInfo", options));
6281 * Registry of form fields, called by :js:`instance.web.FormView`.
6283 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
6284 * will substitute to the <field> tags as defined in OpenERP's views.
6286 instance.web.form.widgets = new instance.web.Registry({
6287 'char' : 'instance.web.form.FieldChar',
6288 'id' : 'instance.web.form.FieldID',
6289 'email' : 'instance.web.form.FieldEmail',
6290 'url' : 'instance.web.form.FieldUrl',
6291 'text' : 'instance.web.form.FieldText',
6292 'html' : 'instance.web.form.FieldTextHtml',
6293 'char_domain': 'instance.web.form.FieldCharDomain',
6294 'date' : 'instance.web.form.FieldDate',
6295 'datetime' : 'instance.web.form.FieldDatetime',
6296 'selection' : 'instance.web.form.FieldSelection',
6297 'radio' : 'instance.web.form.FieldRadio',
6298 'many2one' : 'instance.web.form.FieldMany2One',
6299 'many2onebutton' : 'instance.web.form.Many2OneButton',
6300 'many2many' : 'instance.web.form.FieldMany2Many',
6301 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
6302 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
6303 'one2many' : 'instance.web.form.FieldOne2Many',
6304 'one2many_list' : 'instance.web.form.FieldOne2Many',
6305 'reference' : 'instance.web.form.FieldReference',
6306 'boolean' : 'instance.web.form.FieldBoolean',
6307 'float' : 'instance.web.form.FieldFloat',
6308 'percentpie': 'instance.web.form.FieldPercentPie',
6309 'barchart': 'instance.web.form.FieldBarChart',
6310 'integer': 'instance.web.form.FieldFloat',
6311 'float_time': 'instance.web.form.FieldFloat',
6312 'progressbar': 'instance.web.form.FieldProgressBar',
6313 'image': 'instance.web.form.FieldBinaryImage',
6314 'binary': 'instance.web.form.FieldBinaryFile',
6315 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
6316 'statusbar': 'instance.web.form.FieldStatus',
6317 'monetary': 'instance.web.form.FieldMonetary',
6318 'many2many_checkboxes': 'instance.web.form.FieldMany2ManyCheckBoxes',
6319 'x2many_counter': 'instance.web.form.X2ManyCounter',
6320 'priority':'instance.web.form.Priority',
6321 'kanban_state_selection':'instance.web.form.KanbanSelection',
6322 'statinfo': 'instance.web.form.StatInfo',
6326 * Registry of widgets usable in the form view that can substitute to any possible
6327 * tags defined in OpenERP's form views.
6329 * Every referenced class should extend FormWidget.
6331 instance.web.form.tags = new instance.web.Registry({
6332 'button' : 'instance.web.form.WidgetButton',
6335 instance.web.form.custom_widgets = new instance.web.Registry({
6340 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: