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 () {
4393 if (!this.fields_view || !this.editable()){
4397 return _.every(this.records.records, function(record){
4399 _.each(self.editor.form.fields, function(field){
4400 field._inhibit_on_change_flag = true;
4401 field.set_value(r.attributes[field.name]);
4402 field._inhibit_on_change_flag = false;
4404 return _.every(self.editor.form.fields, function(field){
4405 field.process_modifiers();
4406 field._check_css_flags();
4407 return field.is_valid();
4411 do_add_record: function () {
4412 if (this.editable()) {
4413 this._super.apply(this, arguments);
4416 var pop = new instance.web.form.SelectCreatePopup(this);
4418 self.o2m.field.relation,
4420 title: _t("Create: ") + self.o2m.string,
4421 initial_view: "form",
4422 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4423 create_function: function(data, options) {
4424 return self.o2m.dataset.create(data, options).done(function(r) {
4425 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4426 self.o2m.dataset.trigger("dataset_changed", r);
4429 read_function: function() {
4430 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4432 parent_view: self.o2m.view,
4433 child_name: self.o2m.name,
4434 form_view_options: {'not_interactible_on_create':true}
4436 self.o2m.build_domain(),
4437 self.o2m.build_context()
4439 pop.on("elements_selected", self, function() {
4440 self.o2m.reload_current_view();
4444 do_activate_record: function(index, id) {
4446 var pop = new instance.web.form.FormOpenPopup(self);
4447 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4448 title: _t("Open: ") + self.o2m.string,
4449 write_function: function(id, data) {
4450 return self.o2m.dataset.write(id, data, {}).done(function() {
4451 self.o2m.reload_current_view();
4454 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4455 parent_view: self.o2m.view,
4456 child_name: self.o2m.name,
4457 read_function: function() {
4458 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4460 form_view_options: {'not_interactible_on_create':true},
4461 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
4464 do_button_action: function (name, id, callback) {
4465 if (!_.isNumber(id)) {
4466 instance.webclient.notification.warn(
4467 _t("Action Button"),
4468 _t("The o2m record must be saved before an action can be used"));
4471 var parent_form = this.o2m.view;
4473 this.ensure_saved().then(function () {
4475 return parent_form.save();
4478 }).done(function () {
4479 var ds = self.o2m.dataset;
4480 var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
4481 return value.length;
4483 if (!self.o2m.options.reload_on_button && !cached_records) {
4484 self.handle_button(name, id, callback);
4486 self.handle_button(name, id, function(){
4487 self.o2m.view.reload();
4493 _after_edit: function () {
4494 this.__ignore_blur = false;
4495 this.editor.form.on('blurred', this, this._on_form_blur);
4497 // The form's blur thing may be jiggered during the edition setup,
4498 // potentially leading to the o2m instasaving the row. Cancel any
4499 // blurring triggered the edition startup here
4500 this.editor.form.widgetFocused();
4502 _before_unedit: function () {
4503 this.editor.form.off('blurred', this, this._on_form_blur);
4505 _button_down: function () {
4506 // If a button is clicked (usually some sort of action button), it's
4507 // the button's responsibility to ensure the editable list is in the
4508 // correct state -> ignore form blurring
4509 this.__ignore_blur = true;
4512 * Handles blurring of the nested form (saves the currently edited row),
4513 * unless the flag to ignore the event is set to ``true``
4515 * Makes the internal form go away
4517 _on_form_blur: function () {
4518 if (this.__ignore_blur) {
4519 this.__ignore_blur = false;
4522 // FIXME: why isn't there an API for this?
4523 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4524 this.ensure_saved();
4527 this.cancel_edition();
4529 keypress_ENTER: function () {
4530 // blurring caused by hitting the [Return] key, should skip the
4531 // autosave-on-blur and let the handler for [Return] do its thing (save
4532 // the current row *anyway*, then create a new one/edit the next one)
4533 this.__ignore_blur = true;
4534 this._super.apply(this, arguments);
4536 do_delete: function (ids) {
4537 var confirm = window.confirm;
4538 window.confirm = function () { return true; };
4540 return this._super(ids);
4542 window.confirm = confirm;
4545 reload_record: function (record) {
4546 // Evict record.id from cache to ensure it will be reloaded correctly
4547 this.dataset.evict_record(record.get('id'));
4549 return this._super(record);
4552 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4553 setup_resequence_rows: function () {
4554 if (!this.view.o2m.get('effective_readonly')) {
4555 this._super.apply(this, arguments);
4559 instance.web.form.One2ManyList = instance.web.form.AddAnItemList.extend({
4560 _add_row_class: 'oe_form_field_one2many_list_row_add',
4561 is_readonly: function () {
4562 return this.view.o2m.get('effective_readonly');
4566 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4567 form_template: 'One2Many.formview',
4568 load_form: function(data) {
4571 this.$buttons.find('button.oe_form_button_create').click(function() {
4572 self.save().done(self.on_button_new);
4575 do_notify_change: function() {
4576 if (this.dataset.parent_view) {
4577 this.dataset.parent_view.do_notify_change();
4579 this._super.apply(this, arguments);
4584 var lazy_build_o2m_kanban_view = function() {
4585 if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
4587 instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4591 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4592 template: "FieldMany2ManyTags",
4593 tag_template: "FieldMany2ManyTag",
4595 this._super.apply(this, arguments);
4596 instance.web.form.CompletionFieldMixin.init.call(this);
4597 this.set({"value": []});
4598 this._display_orderer = new instance.web.DropMisordered();
4599 this._drop_shown = false;
4601 initialize_texttext: function(){
4604 plugins : 'tags arrow autocomplete',
4606 render: function(suggestion) {
4607 return $('<span class="text-label"/>').
4608 data('index', suggestion['index']).html(suggestion['label']);
4613 selectFromDropdown: function() {
4614 this.trigger('hideDropdown');
4615 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4616 var data = self.search_result[index];
4618 self.add_id(data.id);
4620 self.ignore_blur = true;
4623 this.trigger('setSuggestions', {result : []});
4627 isTagAllowed: function(tag) {
4631 removeTag: function(tag) {
4632 var id = tag.data("id");
4633 self.set({"value": _.without(self.get("value"), id)});
4635 renderTag: function(stuff) {
4636 return $.fn.textext.TextExtTags.prototype.renderTag.
4637 call(this, stuff).data("id", stuff.id);
4641 itemToString: function(item) {
4646 onSetInputData: function(e, data) {
4648 this._plugins.autocomplete._suggestions = null;
4650 this.input().val(data);
4656 initialize_content: function() {
4657 if (this.get("effective_readonly"))
4660 self.ignore_blur = false;
4661 self.$text = this.$("textarea");
4662 self.$text.textext(self.initialize_texttext()).bind('getSuggestions', function(e, data) {
4664 var str = !!data ? data.query || '' : '';
4665 self.get_search_result(str).done(function(result) {
4666 self.search_result = result;
4667 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4668 return _.extend(el, {index:i});
4671 }).bind('hideDropdown', function() {
4672 self._drop_shown = false;
4673 }).bind('showDropdown', function() {
4674 self._drop_shown = true;
4676 self.tags = self.$text.textext()[0].tags();
4678 .focusin(function () {
4679 self.trigger('focused');
4680 self.ignore_blur = false;
4682 .focusout(function() {
4683 self.$text.trigger("setInputData", "");
4684 if (!self.ignore_blur) {
4685 self.trigger('blurred');
4687 }).keydown(function(e) {
4688 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4689 self.$text.textext()[0].autocomplete().selectFromDropdown();
4693 // WARNING: duplicated in 4 other M2M widgets
4694 set_value: function(value_) {
4695 value_ = value_ || [];
4696 if (value_.length >= 1 && value_[0] instanceof Array) {
4697 // value_ is a list of m2m commands. We only process
4698 // LINK_TO and REPLACE_WITH in this context
4700 _.each(value_, function (command) {
4701 if (command[0] === commands.LINK_TO) {
4702 val.push(command[1]); // (4, id[, _])
4703 } else if (command[0] === commands.REPLACE_WITH) {
4704 val = command[2]; // (6, _, ids)
4709 this._super(value_);
4711 is_false: function() {
4712 return _(this.get("value")).isEmpty();
4714 get_value: function() {
4715 var tmp = [commands.replace_with(this.get("value"))];
4718 get_search_blacklist: function() {
4719 return this.get("value");
4721 map_tag: function(data){
4722 return _.map(data, function(el) {return {name: el[1], id:el[0]};})
4724 get_render_data: function(ids){
4726 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4727 return dataset.name_get(ids);
4729 render_tag: function(data) {
4731 if (! self.get("effective_readonly")) {
4732 self.tags.containerElement().children().remove();
4733 self.$('textarea').css("padding-left", "3px");
4734 self.tags.addTags(self.map_tag(data));
4736 self.$el.html(QWeb.render(self.tag_template, {elements: data}));
4739 render_value: function() {
4741 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4742 var values = self.get("value");
4743 var handle_names = function(data) {
4744 if (self.isDestroyed())
4747 _.each(data, function(el) {
4748 indexed[el[0]] = el;
4750 data = _.map(values, function(el) { return indexed[el]; });
4751 self.render_tag(data);
4753 if (! values || values.length > 0) {
4754 return this._display_orderer.add(self.get_render_data(values)).done(handle_names);
4759 add_id: function(id) {
4760 this.set({'value': _.uniq(this.get('value').concat([id]))});
4762 focus: function () {
4763 var input = this.$text && this.$text[0];
4764 return input ? input.focus() : false;
4766 set_dimensions: function (height, width) {
4767 this._super(height, width);
4768 this.$("textarea").css({
4773 _search_create_popup: function() {
4774 self.ignore_blur = true;
4775 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
4781 - reload_on_button: Reload the whole form view if click on a button in a list view.
4782 If you see this options, do not use it, it's basically a dirty hack to make one
4783 precise o2m to behave the way we want.
4785 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4786 multi_selection: false,
4787 disable_utility_classes: true,
4788 init: function(field_manager, node) {
4789 this._super(field_manager, node);
4790 this.is_loaded = $.Deferred();
4791 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4792 this.dataset.m2m = this;
4794 this.dataset.on('unlink', self, function(ids) {
4795 self.dataset_changed();
4798 this.list_dm = new instance.web.DropMisordered();
4799 this.render_value_dm = new instance.web.DropMisordered();
4801 initialize_content: function() {
4804 this.$el.addClass('oe_form_field oe_form_field_many2many');
4806 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4808 'deletable': this.get("effective_readonly") ? false : true,
4809 'selectable': this.multi_selection,
4811 'reorderable': false,
4812 'import_enabled': false,
4814 var embedded = (this.field.views || {}).tree;
4816 this.list_view.set_embedded_view(embedded);
4818 this.list_view.m2m_field = this;
4819 var loaded = $.Deferred();
4820 this.list_view.on("list_view_loaded", this, function() {
4823 this.list_view.appendTo(this.$el);
4825 var old_def = self.is_loaded;
4826 self.is_loaded = $.Deferred().done(function() {
4829 this.list_dm.add(loaded).then(function() {
4830 self.is_loaded.resolve();
4833 destroy_content: function() {
4834 this.list_view.destroy();
4835 this.list_view = undefined;
4837 // WARNING: duplicated in 4 other M2M widgets
4838 set_value: function(value_) {
4839 value_ = value_ || [];
4840 if (value_.length >= 1 && value_[0] instanceof Array) {
4841 // value_ is a list of m2m commands. We only process
4842 // LINK_TO and REPLACE_WITH in this context
4844 _.each(value_, function (command) {
4845 if (command[0] === commands.LINK_TO) {
4846 val.push(command[1]); // (4, id[, _])
4847 } else if (command[0] === commands.REPLACE_WITH) {
4848 val = command[2]; // (6, _, ids)
4853 this._super(value_);
4855 get_value: function() {
4856 return [commands.replace_with(this.get('value'))];
4858 is_false: function () {
4859 return _(this.get("value")).isEmpty();
4861 render_value: function() {
4863 this.dataset.set_ids(this.get("value"));
4864 this.render_value_dm.add(this.is_loaded).then(function() {
4865 return self.list_view.reload_content();
4868 dataset_changed: function() {
4869 this.internal_set_value(this.dataset.ids);
4873 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4874 get_context: function() {
4875 this.context = this.m2m.build_context();
4876 return this.context;
4882 * @extends instance.web.ListView
4884 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4885 init: function (parent, dataset, view_id, options) {
4886 this._super(parent, dataset, view_id, _.extend(options || {}, {
4887 ListType: instance.web.form.Many2ManyList,
4890 do_add_record: function () {
4891 var pop = new instance.web.form.SelectCreatePopup(this);
4895 title: _t("Add: ") + this.m2m_field.string,
4896 no_create: this.m2m_field.options.no_create,
4898 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4899 this.m2m_field.build_context()
4902 pop.on("elements_selected", self, function(element_ids) {
4904 _(element_ids).each(function (id) {
4905 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4906 self.dataset.set_ids(self.dataset.ids.concat([id]));
4907 self.m2m_field.dataset_changed();
4912 self.reload_content();
4916 do_activate_record: function(index, id) {
4918 var pop = new instance.web.form.FormOpenPopup(this);
4919 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4920 title: _t("Open: ") + this.m2m_field.string,
4921 readonly: this.getParent().get("effective_readonly")
4923 pop.on('write_completed', self, self.reload_content);
4925 do_button_action: function(name, id, callback) {
4927 var _sup = _.bind(this._super, this);
4928 if (! this.m2m_field.options.reload_on_button) {
4929 return _sup(name, id, callback);
4931 return this.m2m_field.view.save().then(function() {
4932 return _sup(name, id, function() {
4933 self.m2m_field.view.reload();
4938 is_action_enabled: function () { return true; },
4940 instance.web.form.Many2ManyList = instance.web.form.AddAnItemList.extend({
4941 _add_row_class: 'oe_form_field_many2many_list_row_add',
4942 is_readonly: function () {
4943 return this.view.m2m_field.get('effective_readonly');
4947 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4948 disable_utility_classes: true,
4949 init: function(field_manager, node) {
4950 this._super(field_manager, node);
4951 instance.web.form.CompletionFieldMixin.init.call(this);
4952 m2m_kanban_lazy_init();
4953 this.is_loaded = $.Deferred();
4954 this.initial_is_loaded = this.is_loaded;
4957 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4958 this.dataset.m2m = this;
4959 this.dataset.on('unlink', self, function(ids) {
4960 self.dataset_changed();
4964 this._super.apply(this, arguments);
4969 self.on("change:effective_readonly", self, function() {
4970 self.is_loaded = self.is_loaded.then(function() {
4971 self.kanban_view.destroy();
4972 return $.when(self.load_view()).done(function() {
4973 self.render_value();
4978 // WARNING: duplicated in 4 other M2M widgets
4979 set_value: function(value_) {
4980 value_ = value_ || [];
4981 if (value_.length >= 1 && value_[0] instanceof Array) {
4982 // value_ is a list of m2m commands. We only process
4983 // LINK_TO and REPLACE_WITH in this context
4985 _.each(value_, function (command) {
4986 if (command[0] === commands.LINK_TO) {
4987 val.push(command[1]); // (4, id[, _])
4988 } else if (command[0] === commands.REPLACE_WITH) {
4989 val = command[2]; // (6, _, ids)
4994 this._super(value_);
4996 get_value: function() {
4997 return [commands.replace_with(this.get('value'))];
4999 load_view: function() {
5001 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
5002 'create_text': _t("Add"),
5003 'creatable': self.get("effective_readonly") ? false : true,
5004 'quick_creatable': self.get("effective_readonly") ? false : true,
5005 'read_only_mode': self.get("effective_readonly") ? true : false,
5006 'confirm_on_delete': false,
5008 var embedded = (this.field.views || {}).kanban;
5010 this.kanban_view.set_embedded_view(embedded);
5012 this.kanban_view.m2m = this;
5013 var loaded = $.Deferred();
5014 this.kanban_view.on("kanban_view_loaded",self,function() {
5015 self.initial_is_loaded.resolve();
5018 this.kanban_view.on('switch_mode', this, this.open_popup);
5019 $.async_when().done(function () {
5020 self.kanban_view.appendTo(self.$el);
5024 render_value: function() {
5026 this.dataset.set_ids(this.get("value"));
5027 this.is_loaded = this.is_loaded.then(function() {
5028 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
5031 dataset_changed: function() {
5032 this.set({'value': this.dataset.ids});
5034 open_popup: function(type, unused) {
5035 if (type !== "form")
5039 if (this.dataset.index === null) {
5040 pop = new instance.web.form.SelectCreatePopup(this);
5042 this.field.relation,
5044 title: _t("Add: ") + this.string
5046 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
5047 this.build_context()
5049 pop.on("elements_selected", self, function(element_ids) {
5050 _.each(element_ids, function(one_id) {
5051 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
5052 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
5053 self.dataset_changed();
5054 self.render_value();
5059 var id = self.dataset.ids[self.dataset.index];
5060 pop = new instance.web.form.FormOpenPopup(this);
5061 pop.show_element(self.field.relation, id, self.build_context(), {
5062 title: _t("Open: ") + self.string,
5063 write_function: function(id, data, options) {
5064 return self.dataset.write(id, data, {}).done(function() {
5065 self.render_value();
5068 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
5069 parent_view: self.view,
5070 child_name: self.name,
5071 readonly: self.get("effective_readonly")
5075 add_id: function(id) {
5076 this.quick_create.add_id(id);
5080 function m2m_kanban_lazy_init() {
5081 if (instance.web.form.Many2ManyKanbanView)
5083 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
5084 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
5085 _is_quick_create_enabled: function() {
5086 return this._super() && ! this.group_by;
5089 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
5090 template: 'Many2ManyKanban.quick_create',
5093 * close_btn: If true, the widget will display a "Close" button able to trigger
5096 init: function(parent, dataset, context, buttons) {
5097 this._super(parent);
5098 this.m2m = this.getParent().view.m2m;
5099 this.m2m.quick_create = this;
5100 this._dataset = dataset;
5101 this._buttons = buttons || false;
5102 this._context = context || {};
5104 start: function () {
5106 self.$text = this.$el.find('input').css("width", "200px");
5107 self.$text.textext({
5108 plugins : 'arrow autocomplete',
5110 render: function(suggestion) {
5111 return $('<span class="text-label"/>').
5112 data('index', suggestion['index']).html(suggestion['label']);
5117 selectFromDropdown: function() {
5118 $(this).trigger('hideDropdown');
5119 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
5120 var data = self.search_result[index];
5122 self.add_id(data.id);
5129 itemToString: function(item) {
5134 }).bind('getSuggestions', function(e, data) {
5136 var str = !!data ? data.query || '' : '';
5137 self.m2m.get_search_result(str).done(function(result) {
5138 self.search_result = result;
5139 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
5140 return _.extend(el, {index:i});
5144 self.$text.focusout(function() {
5149 this.$text[0].focus();
5151 add_id: function(id) {
5154 self.trigger('added', id);
5155 this.m2m.dataset_changed();
5161 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
5163 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
5164 template: "AbstractFormPopup.render",
5167 * -readonly: only applicable when not in creation mode, default to false
5168 * - alternative_form_view
5175 * - form_view_options
5177 init_popup: function(model, row_id, domain, context, options) {
5178 this.row_id = row_id;
5180 this.domain = domain || [];
5181 this.context = context || {};
5182 this.options = options;
5183 _.defaults(this.options, {
5186 init_dataset: function() {
5188 this.created_elements = [];
5189 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
5190 this.dataset.read_function = this.options.read_function;
5191 this.dataset.create_function = function(data, options, sup) {
5192 var fct = self.options.create_function || sup;
5193 return fct.call(this, data, options).done(function(r) {
5194 self.trigger('create_completed saved', r);
5195 self.created_elements.push(r);
5198 this.dataset.write_function = function(id, data, options, sup) {
5199 var fct = self.options.write_function || sup;
5200 return fct.call(this, id, data, options).done(function(r) {
5201 self.trigger('write_completed saved', r);
5204 this.dataset.parent_view = this.options.parent_view;
5205 this.dataset.child_name = this.options.child_name;
5207 display_popup: function() {
5209 this.renderElement();
5210 var dialog = new instance.web.Dialog(this, {
5211 dialogClass: 'oe_act_window',
5212 title: this.options.title || "",
5213 }, this.$el).open();
5214 dialog.on('closing', this, function (e){
5215 self.check_exit(true);
5217 this.$buttonpane = dialog.$buttons;
5220 setup_form_view: function() {
5223 this.dataset.ids = [this.row_id];
5224 this.dataset.index = 0;
5226 this.dataset.index = null;
5228 var options = _.clone(self.options.form_view_options) || {};
5229 if (this.row_id !== null) {
5230 options.initial_mode = this.options.readonly ? "view" : "edit";
5233 $buttons: this.$buttonpane,
5235 this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
5236 if (this.options.alternative_form_view) {
5237 this.view_form.set_embedded_view(this.options.alternative_form_view);
5239 this.view_form.appendTo(this.$el.find(".oe_popup_form"));
5240 this.view_form.on("form_view_loaded", self, function() {
5241 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
5242 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
5243 multi_select: multi_select,
5244 readonly: self.row_id !== null && self.options.readonly,
5246 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
5247 $snbutton.click(function() {
5248 $.when(self.view_form.save()).done(function() {
5249 self.view_form.reload_mutex.exec(function() {
5250 self.view_form.on_button_new();
5254 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
5255 $sbutton.click(function() {
5256 $.when(self.view_form.save()).done(function() {
5257 self.view_form.reload_mutex.exec(function() {
5262 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
5263 $cbutton.click(function() {
5264 self.view_form.trigger('on_button_cancel');
5267 self.view_form.do_show();
5270 select_elements: function(element_ids) {
5271 this.trigger("elements_selected", element_ids);
5273 check_exit: function(no_destroy) {
5274 if (this.created_elements.length > 0) {
5275 this.select_elements(this.created_elements);
5276 this.created_elements = [];
5278 this.trigger('closed');
5281 destroy: function () {
5282 this.trigger('closed');
5283 if (this.$el.is(":data(bs.modal)")) {
5284 this.$el.parents('.modal').modal('hide');
5291 * Class to display a popup containing a form view.
5293 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
5294 show_element: function(model, row_id, context, options) {
5295 this.init_popup(model, row_id, [], context, options);
5296 _.defaults(this.options, {
5298 this.display_popup();
5302 this.init_dataset();
5303 this.setup_form_view();
5308 * Class to display a popup to display a list to search a row. It also allows
5309 * to switch to a form view to create a new row.
5311 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
5315 * - initial_view: form or search (default search)
5316 * - disable_multiple_selection
5317 * - list_view_options
5319 select_element: function(model, options, domain, context) {
5320 this.init_popup(model, null, domain, context, options);
5322 _.defaults(this.options, {
5323 initial_view: "search",
5325 this.initial_ids = this.options.initial_ids;
5326 this.display_popup();
5330 this.init_dataset();
5331 if (this.options.initial_view == "search") {
5332 instance.web.pyeval.eval_domains_and_contexts({
5334 contexts: [this.context]
5335 }).done(function (results) {
5336 var search_defaults = {};
5337 _.each(results.context, function (value_, key) {
5338 var match = /^search_default_(.*)$/.exec(key);
5340 search_defaults[match[1]] = value_;
5343 self.setup_search_view(search_defaults);
5349 setup_search_view: function(search_defaults) {
5351 if (this.searchview) {
5352 this.searchview.destroy();
5354 if (this.searchview_drawer) {
5355 this.searchview_drawer.destroy();
5357 this.searchview = new instance.web.SearchView(this,
5358 this.dataset, false, search_defaults);
5359 this.searchview_drawer = new instance.web.SearchViewDrawer(this, this.searchview);
5360 this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
5361 if (self.initial_ids) {
5362 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
5363 contexts.concat(self.context), groupbys);
5364 self.initial_ids = undefined;
5366 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
5369 this.searchview.on("search_view_loaded", self, function() {
5370 self.view_list = new instance.web.form.SelectCreateListView(self,
5371 self.dataset, false,
5372 _.extend({'deletable': false,
5373 'selectable': !self.options.disable_multiple_selection,
5374 'import_enabled': false,
5375 '$buttons': self.$buttonpane,
5376 'disable_editable_mode': true,
5377 '$pager': self.$('.oe_popup_list_pager'),
5378 }, self.options.list_view_options || {}));
5379 self.view_list.on('edit:before', self, function (e) {
5382 self.view_list.popup = self;
5383 self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
5384 self.view_list.do_show();
5385 }).then(function() {
5386 self.searchview.do_search();
5388 self.view_list.on("list_view_loaded", self, function() {
5389 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
5390 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
5391 $cbutton.click(function() {
5394 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
5395 $sbutton.click(function() {
5396 self.select_elements(self.selected_ids);
5399 $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
5400 $cbutton.click(function() {
5405 this.searchview.appendTo(this.$(".oe_popup_search"));
5407 do_search: function(domains, contexts, groupbys) {
5409 instance.web.pyeval.eval_domains_and_contexts({
5410 domains: domains || [],
5411 contexts: contexts || [],
5412 group_by_seq: groupbys || []
5413 }).done(function (results) {
5414 self.view_list.do_search(results.domain, results.context, results.group_by);
5417 on_click_element: function(ids) {
5419 this.selected_ids = ids || [];
5420 if(this.selected_ids.length > 0) {
5421 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
5423 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
5426 new_object: function() {
5427 if (this.searchview) {
5428 this.searchview.hide();
5430 if (this.view_list) {
5431 this.view_list.do_hide();
5433 this.setup_form_view();
5437 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
5438 do_add_record: function () {
5439 this.popup.new_object();
5441 select_record: function(index) {
5442 this.popup.select_elements([this.dataset.ids[index]]);
5443 this.popup.destroy();
5445 do_select: function(ids, records) {
5446 this._super(ids, records);
5447 this.popup.on_click_element(ids);
5451 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5452 template: 'FieldReference',
5453 init: function(field_manager, node) {
5454 this._super(field_manager, node);
5455 this.reference_ready = true;
5457 destroy_content: function() {
5460 this.fm = undefined;
5463 initialize_content: function() {
5465 var fm = new instance.web.form.DefaultFieldManager(this);
5467 fm.extend_field_desc({
5469 selection: this.field_manager.get_field_desc(this.name).selection,
5477 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
5479 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5481 this.selection.on("change:value", this, this.on_selection_changed);
5482 this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
5484 .on('focused', null, function () {self.trigger('focused');})
5485 .on('blurred', null, function () {self.trigger('blurred');});
5487 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
5488 name: 'Referenced Document',
5489 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5491 this.m2o.on("change:value", this, this.data_changed);
5492 this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
5494 .on('focused', null, function () {self.trigger('focused');})
5495 .on('blurred', null, function () {self.trigger('blurred');});
5497 on_selection_changed: function() {
5498 if (this.reference_ready) {
5499 this.internal_set_value([this.selection.get_value(), false]);
5500 this.render_value();
5503 data_changed: function() {
5504 if (this.reference_ready) {
5505 this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
5508 set_value: function(val) {
5510 val = val.split(',');
5511 val[0] = val[0] || false;
5512 val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
5514 this._super(val || [false, false]);
5516 get_value: function() {
5517 return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
5519 render_value: function() {
5520 this.reference_ready = false;
5521 if (!this.get("effective_readonly")) {
5522 this.selection.set_value(this.get('value')[0]);
5524 this.m2o.field.relation = this.get('value')[0];
5525 this.m2o.set_value(this.get('value')[1]);
5526 this.m2o.$el.toggle(!!this.get('value')[0]);
5527 this.reference_ready = true;
5531 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5532 init: function(field_manager, node) {
5534 this._super(field_manager, node);
5535 this.binary_value = false;
5536 this.useFileAPI = !!window.FileReader;
5537 this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5538 if (!this.useFileAPI) {
5539 this.fileupload_id = _.uniqueId('oe_fileupload');
5540 $(window).on(this.fileupload_id, function() {
5541 var args = [].slice.call(arguments).slice(1);
5542 self.on_file_uploaded.apply(self, args);
5547 if (!this.useFileAPI) {
5548 $(window).off(this.fileupload_id);
5550 this._super.apply(this, arguments);
5552 initialize_content: function() {
5554 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5555 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5556 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5557 this.$el.find('.oe_form_binary_file_edit').click(function(event){
5558 self.$el.find('input.oe_form_binary_file').click();
5561 on_file_change: function(e) {
5563 var file_node = e.target;
5564 if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5565 if (this.useFileAPI) {
5566 var file = file_node.files[0];
5567 if (file.size > this.max_upload_size) {
5568 var msg = _t("The selected file exceed the maximum file size of %s.");
5569 instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5572 var filereader = new FileReader();
5573 filereader.readAsDataURL(file);
5574 filereader.onloadend = function(upload) {
5575 var data = upload.target.result;
5576 data = data.split(',')[1];
5577 self.on_file_uploaded(file.size, file.name, file.type, data);
5580 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5581 this.$el.find('form.oe_form_binary_form').submit();
5583 this.$el.find('.oe_form_binary_progress').show();
5584 this.$el.find('.oe_form_binary').hide();
5587 on_file_uploaded: function(size, name, content_type, file_base64) {
5588 if (size === false) {
5589 this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5590 // TODO: use openerp web crashmanager
5591 console.warn("Error while uploading file : ", name);
5593 this.filename = name;
5594 this.on_file_uploaded_and_valid.apply(this, arguments);
5596 this.$el.find('.oe_form_binary_progress').hide();
5597 this.$el.find('.oe_form_binary').show();
5599 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5601 on_save_as: function(ev) {
5602 var value = this.get('value');
5604 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5605 ev.stopPropagation();
5607 instance.web.blockUI();
5608 var c = instance.webclient.crashmanager;
5609 this.session.get_file({
5610 url: '/web/binary/saveas_ajax',
5611 data: {data: JSON.stringify({
5612 model: this.view.dataset.model,
5613 id: (this.view.datarecord.id || ''),
5615 filename_field: (this.node.attrs.filename || ''),
5616 data: instance.web.form.is_bin_size(value) ? null : value,
5617 context: this.view.dataset.get_context()
5619 complete: instance.web.unblockUI,
5620 error: c.rpc_error.bind(c)
5622 ev.stopPropagation();
5626 set_filename: function(value) {
5627 var filename = this.node.attrs.filename;
5630 tmp[filename] = value;
5631 this.field_manager.set_values(tmp);
5634 on_clear: function() {
5635 if (this.get('value') !== false) {
5636 this.binary_value = false;
5637 this.internal_set_value(false);
5643 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5644 template: 'FieldBinaryFile',
5645 initialize_content: function() {
5647 if (this.get("effective_readonly")) {
5649 this.$el.find('a').click(function(ev) {
5650 if (self.get('value')) {
5651 self.on_save_as(ev);
5657 render_value: function() {
5659 if (!this.get("effective_readonly")) {
5660 if (this.node.attrs.filename) {
5661 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5663 show_value = (this.get('value') !== null && this.get('value') !== undefined && this.get('value') !== false) ? this.get('value') : '';
5665 this.$el.find('input').eq(0).val(show_value);
5667 this.$el.find('a').toggle(!!this.get('value'));
5668 if (this.get('value')) {
5669 show_value = _t("Download");
5671 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5672 this.$el.find('a').text(show_value);
5676 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5677 this.binary_value = true;
5678 this.internal_set_value(file_base64);
5679 var show_value = name + " (" + instance.web.human_size(size) + ")";
5680 this.$el.find('input').eq(0).val(show_value);
5681 this.set_filename(name);
5683 on_clear: function() {
5684 this._super.apply(this, arguments);
5685 this.$el.find('input').eq(0).val('');
5686 this.set_filename('');
5690 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5691 template: 'FieldBinaryImage',
5692 placeholder: "/web/static/src/img/placeholder.png",
5693 render_value: function() {
5696 if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5697 url = 'data:image/png;base64,' + this.get('value');
5698 } else if (this.get('value')) {
5699 var id = JSON.stringify(this.view.datarecord.id || null);
5700 var field = this.name;
5701 if (this.options.preview_image)
5702 field = this.options.preview_image;
5703 url = this.session.url('/web/binary/image', {
5704 model: this.view.dataset.model,
5707 t: (new Date().getTime()),
5710 url = this.placeholder;
5712 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5713 $($img).click(function(e) {
5714 if(self.view.get("actual_mode") == "view") {
5715 var $button = $(".oe_form_button_edit");
5716 $button.openerpBounce();
5717 e.stopPropagation();
5720 this.$el.find('> img').remove();
5721 this.$el.prepend($img);
5722 $img.load(function() {
5723 if (! self.options.size)
5725 $img.css("max-width", "" + self.options.size[0] + "px");
5726 $img.css("max-height", "" + self.options.size[1] + "px");
5728 $img.on('error', function() {
5729 $img.attr('src', self.placeholder);
5730 instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5733 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5734 this.internal_set_value(file_base64);
5735 this.binary_value = true;
5736 this.render_value();
5737 this.set_filename(name);
5739 on_clear: function() {
5740 this._super.apply(this, arguments);
5741 this.render_value();
5742 this.set_filename('');
5744 set_value: function(value_){
5745 var changed = value_ !== this.get_value();
5746 this._super.apply(this, arguments);
5747 // By default, on binary images read, the server returns the binary size
5748 // This is possible that two images have the exact same size
5749 // Therefore we trigger the change in case the image value hasn't changed
5750 // So the image is re-rendered correctly
5752 this.trigger("change:value", this, {
5761 * Widget for (many2many field) to upload one or more file in same time and display in list.
5762 * The user can delete his files.
5763 * Options on attribute ; "blockui" {Boolean} block the UI or not
5764 * during the file is uploading
5766 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5767 template: "FieldBinaryFileUploader",
5768 init: function(field_manager, node) {
5769 this._super(field_manager, node);
5770 this.field_manager = field_manager;
5772 if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5773 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);
5777 this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5778 this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5779 $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5781 initialize_content: function() {
5782 this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5784 // WARNING: duplicated in 4 other M2M widgets
5785 set_value: function(value_) {
5786 value_ = value_ || [];
5787 if (value_.length >= 1 && value_[0] instanceof Array) {
5788 // value_ is a list of m2m commands. We only process
5789 // LINK_TO and REPLACE_WITH in this context
5791 _.each(value_, function (command) {
5792 if (command[0] === commands.LINK_TO) {
5793 val.push(command[1]); // (4, id[, _])
5794 } else if (command[0] === commands.REPLACE_WITH) {
5795 val = command[2]; // (6, _, ids)
5800 this._super(value_);
5802 get_value: function() {
5803 var tmp = [commands.replace_with(this.get("value"))];
5806 get_file_url: function (attachment) {
5807 return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5809 read_name_values : function () {
5811 // don't reset know values
5812 var ids = this.get('value');
5813 var _value = _.filter(ids, function (id) { return typeof self.data[id] == 'undefined'; } );
5814 // send request for get_name
5815 if (_value.length) {
5816 return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).then(function (datas) {
5817 _.each(datas, function (data) {
5818 data.no_unlink = true;
5819 data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5820 self.data[data.id] = data;
5828 render_value: function () {
5830 this.read_name_values().then(function (ids) {
5831 var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids}));
5832 render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5833 self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5835 // reinit input type file
5836 var $input = self.$('input.oe_form_binary_file');
5837 $input.after($input.clone(true)).remove();
5838 self.$(".oe_fileupload").show();
5842 on_file_change: function (event) {
5843 event.stopPropagation();
5845 var $target = $(event.target);
5846 if ($target.val() !== '') {
5847 var filename = $target.val().replace(/.*[\\\/]/,'');
5848 // don't uplode more of one file in same time
5849 if (self.data[0] && self.data[0].upload ) {
5852 for (var id in this.get('value')) {
5853 // if the files exits, delete the file before upload (if it's a new file)
5854 if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
5855 self.ds_file.unlink([id]);
5860 if(this.node.attrs.blockui>0) {
5861 instance.web.blockUI();
5864 // TODO : unactivate send on wizard and form
5867 this.$('form.oe_form_binary_form').submit();
5868 this.$(".oe_fileupload").hide();
5869 // add file on data result
5873 'filename': filename,
5879 on_file_loaded: function (event, result) {
5880 var files = this.get('value');
5883 if(this.node.attrs.blockui>0) {
5884 instance.web.unblockUI();
5887 if (result.error || !result.id ) {
5888 this.do_warn( _t('Uploading Error'), result.error);
5889 delete this.data[0];
5891 if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
5892 delete this.data[0];
5893 this.data[result.id] = {
5895 'name': result.name,
5896 'filename': result.filename,
5897 'url': this.get_file_url(result)
5900 this.data[result.id] = {
5902 'name': result.name,
5903 'filename': result.filename,
5904 'url': this.get_file_url(result)
5907 var values = _.clone(this.get('value'));
5908 values.push(result.id);
5909 this.set({'value': values});
5911 this.render_value();
5913 on_file_delete: function (event) {
5914 event.stopPropagation();
5915 var file_id=$(event.target).data("id");
5917 var files = _.filter(this.get('value'), function (id) {return id != file_id;});
5918 if(!this.data[file_id].no_unlink) {
5919 this.ds_file.unlink([file_id]);
5921 this.set({'value': files});
5926 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5927 template: "FieldStatus",
5928 init: function(field_manager, node) {
5929 this._super(field_manager, node);
5930 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5931 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5932 this.set({value: false});
5933 this.selection = {'unfolded': [], 'folded': []};
5934 this.set("selection", {'unfolded': [], 'folded': []});
5935 this.selection_dm = new instance.web.DropMisordered();
5936 this.dataset = new instance.web.DataSetStatic(this, this.field.relation, this.build_context());
5939 this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5941 this.on("change:value", this, this.get_selection);
5942 this.on("change:evaluated_selection_domain", this, this.get_selection);
5943 this.on("change:selection", this, function() {
5944 this.selection = this.get("selection");
5945 this.render_value();
5947 this.get_selection();
5948 if (this.options.clickable) {
5949 this.$el.on('click','li[data-id]',this.on_click_stage);
5951 if (this.$el.parent().is('header')) {
5952 this.$el.after('<div class="oe_clear"/>');
5956 set_value: function(value_) {
5957 if (value_ instanceof Array) {
5960 this._super(value_);
5962 render_value: function() {
5964 var content = QWeb.render("FieldStatus.content", {
5966 'value_folded': _.find(self.selection.folded, function(i){return i[0] === self.get('value');})
5968 self.$el.html(content);
5970 calc_domain: function() {
5971 var d = instance.web.pyeval.eval('domain', this.build_domain());
5972 var domain = []; //if there is no domain defined, fetch all the records
5975 domain = ['|',['id', '=', this.get('value')]].concat(d);
5978 if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5979 this.set("evaluated_selection_domain", domain);
5982 /** Get the selection and render it
5983 * selection: [[identifier, value_to_display], ...]
5984 * For selection fields: this is directly given by this.field.selection
5985 * For many2one fields: perform a search on the relation of the many2one field
5987 get_selection: function() {
5989 var selection_unfolded = [];
5990 var selection_folded = [];
5991 var fold_field = this.options.fold_field;
5993 var calculation = _.bind(function() {
5994 if (this.field.type == "many2one") {
5995 return self.get_distant_fields().then(function (fields) {
5996 return new instance.web.DataSetSearch(self, self.field.relation, self.build_context(), self.get("evaluated_selection_domain"))
5997 .read_slice(_.union(_.keys(self.distant_fields), ['id']), {}).then(function (records) {
5998 var ids = _.pluck(records, 'id');
5999 return self.dataset.name_get(ids).then(function (records_name) {
6000 _.each(records, function (record) {
6001 var name = _.find(records_name, function (val) {return val[0] == record.id;})[1];
6002 if (fold_field && record[fold_field] && record.id != self.get('value')) {
6003 selection_folded.push([record.id, name]);
6005 selection_unfolded.push([record.id, name]);
6012 // For field type selection filter values according to
6013 // statusbar_visible attribute of the field. For example:
6014 // statusbar_visible="draft,open".
6015 var select = this.field.selection;
6016 for(var i=0; i < select.length; i++) {
6017 var key = select[i][0];
6018 if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
6019 selection_unfolded.push(select[i]);
6025 this.selection_dm.add(calculation()).then(function () {
6026 var selection = {'unfolded': selection_unfolded, 'folded': selection_folded};
6027 if (! _.isEqual(selection, self.get("selection"))) {
6028 self.set("selection", selection);
6033 * :deprecated: this feature will probably be removed with OpenERP v8
6035 get_distant_fields: function() {
6037 if (! this.options.fold_field) {
6038 this.distant_fields = {}
6040 if (this.distant_fields) {
6041 return $.when(this.distant_fields);
6043 return new instance.web.Model(self.field.relation).call("fields_get", [[this.options.fold_field]]).then(function(fields) {
6044 self.distant_fields = fields;
6048 on_click_stage: function (ev) {
6050 var $li = $(ev.currentTarget);
6052 if (this.field.type == "many2one") {
6053 val = parseInt($li.data("id"), 10);
6056 val = $li.data("id");
6058 if (val != self.get('value')) {
6059 this.view.recursive_save().done(function() {
6061 change[self.name] = val;
6062 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
6070 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
6071 template: "FieldMonetary",
6072 widget_class: 'oe_form_field_float oe_form_field_monetary',
6074 this._super.apply(this, arguments);
6075 this.set({"currency": false});
6076 if (this.options.currency_field) {
6077 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
6078 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
6081 this.on("change:currency", this, this.get_currency_info);
6082 this.get_currency_info();
6083 this.ci_dm = new instance.web.DropMisordered();
6086 var tmp = this._super();
6087 this.on("change:currency_info", this, this.reinitialize);
6090 get_currency_info: function() {
6092 if (this.get("currency") === false) {
6093 this.set({"currency_info": null});
6096 return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
6097 .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
6098 self.set({"currency_info": res});
6101 parse_value: function(val, def) {
6102 return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6104 format_value: function(val, def) {
6105 return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6110 This type of field display a list of checkboxes. It works only with m2ms. This field will display one checkbox for each
6111 record existing in the model targeted by the relation, according to the given domain if one is specified. Checked records
6112 will be added to the relation.
6114 instance.web.form.FieldMany2ManyCheckBoxes = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6115 className: "oe_form_many2many_checkboxes",
6117 this._super.apply(this, arguments);
6118 this.set("value", {});
6119 this.set("records", []);
6120 this.field_manager.on("view_content_has_changed", this, function() {
6121 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
6122 if (! _.isEqual(domain, this.get("domain"))) {
6123 this.set("domain", domain);
6126 this.records_orderer = new instance.web.DropMisordered();
6128 initialize_field: function() {
6129 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
6130 this.on("change:domain", this, this.query_records);
6131 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
6132 this.on("change:records", this, this.render_value);
6134 query_records: function() {
6136 var model = new openerp.Model(openerp.session, this.field.relation);
6137 this.records_orderer.add(model.call("search", [this.get("domain")], {"context": this.build_context()}).then(function(record_ids) {
6138 return model.call("name_get", [record_ids] , {"context": self.build_context()});
6139 })).then(function(res) {
6140 self.set("records", res);
6143 render_value: function() {
6144 this.$().html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this, selected: this.get("value")}));
6145 var inputs = this.$("input");
6146 inputs.change(_.bind(this.from_dom, this));
6147 if (this.get("effective_readonly"))
6148 inputs.attr("disabled", "true");
6150 from_dom: function() {
6152 this.$("input").each(function() {
6154 new_value[elem.data("record-id")] = elem.attr("checked") ? true : undefined;
6156 if (! _.isEqual(new_value, this.get("value")))
6157 this.internal_set_value(new_value);
6159 // WARNING: (mostly) duplicated in 4 other M2M widgets
6160 set_value: function(value_) {
6161 value_ = value_ || [];
6162 if (value_.length >= 1 && value_[0] instanceof Array) {
6163 // value_ is a list of m2m commands. We only process
6164 // LINK_TO and REPLACE_WITH in this context
6166 _.each(value_, function (command) {
6167 if (command[0] === commands.LINK_TO) {
6168 val.push(command[1]); // (4, id[, _])
6169 } else if (command[0] === commands.REPLACE_WITH) {
6170 val = command[2]; // (6, _, ids)
6176 _.each(value_, function(el) {
6177 formatted[JSON.stringify(el)] = true;
6179 this._super(formatted);
6181 get_value: function() {
6182 var value = _.filter(_.keys(this.get("value")), function(el) {
6183 return this.get("value")[el];
6185 value = _.map(value, function(el) {
6186 return JSON.parse(el);
6188 return [commands.replace_with(value)];
6193 This field can be applied on many2many and one2many. It is a read-only field that will display a single link whose name is
6194 "<number of linked records> <label of the field>". When the link is clicked, it will redirect to another act_window
6195 action on the model of the relation and show only the linked records.
6199 * 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
6200 to display (or False to take the default one) and the second element is the type of the view. Defaults to
6201 [[false, "tree"], [false, "form"]] .
6203 instance.web.form.X2ManyCounter = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6204 className: "oe_form_x2many_counter",
6206 this._super.apply(this, arguments);
6207 this.set("value", []);
6208 _.defaults(this.options, {
6209 "views": [[false, "tree"], [false, "form"]],
6212 render_value: function() {
6213 var text = _.str.sprintf("%d %s", this.val().length, this.string);
6214 this.$().html(QWeb.render("X2ManyCounter", {text: text}));
6215 this.$("a").click(_.bind(this.go_to, this));
6218 return this.view.recursive_save().then(_.bind(function() {
6219 var val = this.val();
6221 if (this.field.type === "one2many") {
6222 context["default_" + this.field.relation_field] = this.view.datarecord.id;
6224 var domain = [["id", "in", val]];
6225 return this.do_action({
6226 type: 'ir.actions.act_window',
6228 res_model: this.field.relation,
6229 views: this.options.views,
6237 var value = this.get("value") || [];
6238 if (value.length >= 1 && value[0] instanceof Array) {
6239 value = value[0][2];
6246 This widget is intended to be used on stat button numeric fields. It will display
6247 the value many2many and one2many. It is a read-only field that will
6248 display a simple string "<value of field> <label of the field>"
6250 instance.web.form.StatInfo = instance.web.form.AbstractField.extend({
6251 is_field_number: true,
6253 this._super.apply(this, arguments);
6254 this.internal_set_value(0);
6256 set_value: function(value_) {
6257 if (value_ === false || value_ === undefined) {
6260 this._super.apply(this, [value_]);
6262 render_value: function() {
6264 value: this.get("value") || 0,
6266 if (! this.node.attrs.nolabel) {
6267 options.text = this.string
6269 this.$el.html(QWeb.render("StatInfo", options));
6276 * Registry of form fields, called by :js:`instance.web.FormView`.
6278 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
6279 * will substitute to the <field> tags as defined in OpenERP's views.
6281 instance.web.form.widgets = new instance.web.Registry({
6282 'char' : 'instance.web.form.FieldChar',
6283 'id' : 'instance.web.form.FieldID',
6284 'email' : 'instance.web.form.FieldEmail',
6285 'url' : 'instance.web.form.FieldUrl',
6286 'text' : 'instance.web.form.FieldText',
6287 'html' : 'instance.web.form.FieldTextHtml',
6288 'char_domain': 'instance.web.form.FieldCharDomain',
6289 'date' : 'instance.web.form.FieldDate',
6290 'datetime' : 'instance.web.form.FieldDatetime',
6291 'selection' : 'instance.web.form.FieldSelection',
6292 'radio' : 'instance.web.form.FieldRadio',
6293 'many2one' : 'instance.web.form.FieldMany2One',
6294 'many2onebutton' : 'instance.web.form.Many2OneButton',
6295 'many2many' : 'instance.web.form.FieldMany2Many',
6296 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
6297 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
6298 'one2many' : 'instance.web.form.FieldOne2Many',
6299 'one2many_list' : 'instance.web.form.FieldOne2Many',
6300 'reference' : 'instance.web.form.FieldReference',
6301 'boolean' : 'instance.web.form.FieldBoolean',
6302 'float' : 'instance.web.form.FieldFloat',
6303 'percentpie': 'instance.web.form.FieldPercentPie',
6304 'barchart': 'instance.web.form.FieldBarChart',
6305 'integer': 'instance.web.form.FieldFloat',
6306 'float_time': 'instance.web.form.FieldFloat',
6307 'progressbar': 'instance.web.form.FieldProgressBar',
6308 'image': 'instance.web.form.FieldBinaryImage',
6309 'binary': 'instance.web.form.FieldBinaryFile',
6310 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
6311 'statusbar': 'instance.web.form.FieldStatus',
6312 'monetary': 'instance.web.form.FieldMonetary',
6313 'many2many_checkboxes': 'instance.web.form.FieldMany2ManyCheckBoxes',
6314 'x2many_counter': 'instance.web.form.X2ManyCounter',
6315 'priority':'instance.web.form.Priority',
6316 'kanban_state_selection':'instance.web.form.KanbanSelection',
6317 'statinfo': 'instance.web.form.StatInfo',
6321 * Registry of widgets usable in the form view that can substitute to any possible
6322 * tags defined in OpenERP's form views.
6324 * Every referenced class should extend FormWidget.
6326 instance.web.form.tags = new instance.web.Registry({
6327 'button' : 'instance.web.form.WidgetButton',
6330 instance.web.form.custom_widgets = new instance.web.Registry({
6335 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: