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);
260 this.do_show({ reload: warm });
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)'
307 instance.web.bus.trigger('form_view_shown', self);
310 do_hide: function () {
312 this.sidebar.$el.hide();
315 this.$buttons.hide();
322 load_record: function(record) {
323 var self = this, set_values = [];
325 this.set({ 'title' : undefined });
326 this.do_warn(_t("Form"), _t("The record could not be found in the database."), true);
327 return $.Deferred().reject();
329 this.datarecord = record;
330 this._actualize_mode();
331 this.set({ 'title' : record.id ? record.display_name : _t("New") });
333 _(this.fields).each(function (field, f) {
334 field._dirty_flag = false;
335 field._inhibit_on_change_flag = true;
336 var result = field.set_value(self.datarecord[f] || false);
337 field._inhibit_on_change_flag = false;
338 set_values.push(result);
340 return $.when.apply(null, set_values).then(function() {
343 self.do_onchange(null);
345 self.on_form_changed();
346 self.rendering_engine.init_fields();
347 self.is_initialized.resolve();
348 self.do_update_pager(record.id === null || record.id === undefined);
350 self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
353 self.do_push_state({id:record.id});
355 self.do_push_state({});
357 self.$el.add(self.$buttons).removeClass('oe_form_dirty');
362 * Loads and sets up the default values for the model as the current
365 * @return {$.Deferred}
367 load_defaults: function () {
369 var keys = _.keys(this.fields_view.fields);
371 return this.dataset.default_get(keys).then(function(r) {
372 self.trigger('load_record', r);
375 return self.trigger('load_record', {});
377 on_form_changed: function() {
378 this.trigger("view_content_has_changed");
380 do_notify_change: function() {
381 this.$el.add(this.$buttons).addClass('oe_form_dirty');
383 execute_pager_action: function(action) {
384 if (this.can_be_discarded()) {
387 this.dataset.index = 0;
390 this.dataset.previous();
396 this.dataset.index = this.dataset.ids.length - 1;
399 var def = this.reload();
400 this.trigger('pager_action_executed');
405 init_pager: function() {
408 this.$pager.remove();
409 if (this.get("actual_mode") === "create")
411 this.$pager = $(QWeb.render("FormView.pager", {'widget':self})).hide();
412 if (this.options.$pager) {
413 this.$pager.appendTo(this.options.$pager);
415 this.$el.find('.oe_form_pager').replaceWith(this.$pager);
417 this.$pager.on('click','a[data-pager-action]',function() {
419 if ($el.attr("disabled"))
421 var action = $el.data('pager-action');
422 var def = $.when(self.execute_pager_action(action));
423 $el.attr("disabled");
424 def.always(function() {
425 $el.removeAttr("disabled");
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();
723 instance.web.bus.trigger('form_view_saved', self);
725 }).always(function(){
726 $(e.target).attr("disabled", false);
729 on_button_cancel: function(event) {
731 if (this.can_be_discarded()) {
732 if (this.get('actual_mode') === 'create') {
733 this.trigger('history_back');
736 $.when.apply(null, this.render_value_defs).then(function(){
737 self.trigger('load_record', self.datarecord);
741 this.trigger('on_button_cancel');
744 on_button_new: function() {
747 return $.when(this.has_been_loaded).then(function() {
748 if (self.can_be_discarded()) {
749 return self.load_defaults();
753 on_button_edit: function() {
754 return this.to_edit_mode();
756 on_button_create: function() {
757 this.dataset.index = null;
760 on_button_duplicate: function() {
762 return this.has_been_loaded.then(function() {
763 return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
764 self.record_created(new_id);
769 on_button_delete: function() {
771 var def = $.Deferred();
772 this.has_been_loaded.done(function() {
773 if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
774 self.dataset.unlink([self.datarecord.id]).done(function() {
775 if (self.dataset.size()) {
776 self.execute_pager_action('next');
778 self.do_action('history_back');
783 $.async_when().done(function () {
788 return def.promise();
790 can_be_discarded: function() {
791 if (this.$el.is('.oe_form_dirty')) {
792 if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
795 this.$el.removeClass('oe_form_dirty');
800 * Triggers saving the form's record. Chooses between creating a new
801 * record or saving an existing one depending on whether the record
802 * already has an id property.
804 * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
805 * record, should that record be inserted at the start of the dataset (by
806 * default, records are added at the end)
808 save: function(prepend_on_create) {
810 var save_obj = {prepend_on_create: prepend_on_create, ret: null};
811 this.save_list.push(save_obj);
812 return self._process_operations().then(function() {
814 return $.Deferred().reject();
815 return $.when.apply($, save_obj.ret);
816 }).done(function(result) {
817 self.$el.removeClass('oe_form_dirty');
820 _process_save: function(save_obj) {
822 var prepend_on_create = save_obj.prepend_on_create;
824 var form_invalid = false,
826 first_invalid_field = null,
827 readonly_values = {};
828 for (var f in self.fields) {
829 if (!self.fields.hasOwnProperty(f)) { continue; }
833 if (!first_invalid_field) {
834 first_invalid_field = f;
836 } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
837 // Special case 'id' field, do not save this field
838 // on 'create' : save all non readonly fields
839 // on 'edit' : save non readonly modified fields
840 if (!f.get("readonly")) {
841 values[f.name] = f.get_value();
843 readonly_values[f.name] = f.get_value();
848 self.set({'display_invalid_fields': true});
849 first_invalid_field.focus();
851 return $.Deferred().reject();
853 self.set({'display_invalid_fields': false});
855 if (!self.datarecord.id) {
857 save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
858 return self.record_created(r, prepend_on_create);
860 } else if (_.isEmpty(values)) {
861 // Not dirty, noop save
862 save_deferral = $.Deferred().resolve({}).promise();
865 save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
866 return self.record_saved(r);
869 return save_deferral;
873 return $.Deferred().reject();
876 on_invalid: function() {
877 var warnings = _(this.fields).chain()
878 .filter(function (f) { return !f.is_valid(); })
880 return _.str.sprintf('<li>%s</li>',
883 warnings.unshift('<ul>');
884 warnings.push('</ul>');
885 this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
888 * Reload the form after saving
890 * @param {Object} r result of the write function.
892 record_saved: function(r) {
893 this.trigger('record_saved', r);
895 // should not happen in the server, but may happen for internal purpose
896 return $.Deferred().reject();
901 * Updates the form' dataset to contain the new record:
903 * * Adds the newly created record to the current dataset (at the end by
905 * * Selects that record (sets the dataset's index to point to the new
907 * * Updates the pager and sidebar displays
910 * @param {Boolean} [prepend_on_create=false] adds the newly created record
911 * at the beginning of the dataset instead of the end
913 record_created: function(r, prepend_on_create) {
916 // should not happen in the server, but may happen for internal purpose
917 this.trigger('record_created', r);
918 return $.Deferred().reject();
920 this.datarecord.id = r;
921 if (!prepend_on_create) {
922 this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
923 this.dataset.index = this.dataset.ids.length - 1;
925 this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
926 this.dataset.index = 0;
928 this.do_update_pager();
930 this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
932 //openerp.log("The record has been created with id #" + this.datarecord.id);
933 return $.when(this.reload()).then(function () {
934 self.trigger('record_created', r);
935 return _.extend(r, {created: true});
939 on_action: function (action) {
940 console.debug('Executing action', action);
944 return this.reload_mutex.exec(function() {
945 if (self.dataset.index === null || self.dataset.index === undefined) {
946 self.trigger("previous_view");
947 return $.Deferred().reject().promise();
949 if (self.dataset.index < 0) {
950 return $.when(self.on_button_new());
952 var fields = _.keys(self.fields_view.fields);
953 fields.push('display_name');
954 return self.dataset.read_index(fields,
958 'future_display_name': true
960 check_access_rule: true
961 }).then(function(r) {
962 self.trigger('load_record', r);
964 self.do_action('history_back');
969 get_widgets: function() {
970 return _.filter(this.getChildren(), function(obj) {
971 return obj instanceof instance.web.form.FormWidget;
974 get_fields_values: function() {
976 var ids = this.get_selected_ids();
977 values["id"] = ids.length > 0 ? ids[0] : false;
978 _.each(this.fields, function(value_, key) {
979 values[key] = value_.get_value();
983 get_selected_ids: function() {
984 var id = this.dataset.ids[this.dataset.index];
985 return id ? [id] : [];
987 recursive_save: function() {
989 return $.when(this.save()).then(function(res) {
990 if (self.dataset.parent_view)
991 return self.dataset.parent_view.recursive_save();
994 recursive_reload: function() {
997 if (self.dataset.parent_view)
998 pre = self.dataset.parent_view.recursive_reload();
999 return pre.then(function() {
1000 return self.reload();
1003 is_dirty: function() {
1004 return _.any(this.fields, function (value_) {
1005 return value_._dirty_flag;
1008 is_interactible_record: function() {
1009 var id = this.datarecord.id;
1011 if (this.options.not_interactible_on_create)
1013 } else if (typeof(id) === "string") {
1014 if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
1019 sidebar_eval_context: function () {
1020 return $.when(this.build_eval_context());
1022 open_defaults_dialog: function () {
1024 var display = function (field, value) {
1025 if (!value) { return value; }
1026 if (field instanceof instance.web.form.FieldSelection) {
1027 return _(field.get('values')).find(function (option) {
1028 return option[0] === value;
1030 } else if (field instanceof instance.web.form.FieldMany2One) {
1031 return field.get_displayed();
1035 var fields = _.chain(this.fields)
1036 .map(function (field) {
1037 var value = field.get_value();
1038 // ignore fields which are empty, invisible, readonly, o2m
1041 || field.get('invisible')
1042 || field.get("readonly")
1043 || field.field.type === 'one2many'
1044 || field.field.type === 'many2many'
1045 || field.field.type === 'binary'
1046 || field.password) {
1052 string: field.string,
1054 displayed: display(field, value),
1058 .sortBy(function (field) { return field.string; })
1060 var conditions = _.chain(self.fields)
1061 .filter(function (field) { return field.field.change_default; })
1062 .map(function (field) {
1063 var value = field.get_value();
1066 string: field.string,
1068 displayed: display(field, value),
1072 var d = new instance.web.Dialog(this, {
1073 title: _t("Set Default"),
1076 conditions: conditions
1079 {text: _t("Close"), click: function () { d.close(); }},
1080 {text: _t("Save default"), click: function () {
1081 var $defaults = d.$el.find('#formview_default_fields');
1082 var field_to_set = $defaults.val();
1083 if (!field_to_set) {
1084 $defaults.parent().addClass('oe_form_invalid');
1087 var condition = d.$el.find('#formview_default_conditions').val(),
1088 all_users = d.$el.find('#formview_default_all').is(':checked');
1089 new instance.web.DataSet(self, 'ir.values').call(
1093 self.fields[field_to_set].get_value(),
1097 ]).done(function () { d.close(); });
1101 d.template = 'FormView.set_default';
1104 register_field: function(field, name) {
1105 this.fields[name] = field;
1106 this.fields_order.push(name);
1107 if (JSON.parse(field.node.attrs.default_focus || "0")) {
1108 this.default_focus_field = field;
1111 field.on('focused', null, this.proxy('widgetFocused'))
1112 .on('blurred', null, this.proxy('widgetBlurred'));
1113 if (this.get_field_desc(name).translate) {
1114 this.translatable_fields.push(field);
1116 field.on('changed_value', this, function() {
1117 if (field.is_syntax_valid()) {
1118 this.trigger('field_changed:' + name);
1120 if (field._inhibit_on_change_flag) {
1123 field._dirty_flag = true;
1124 if (field.is_syntax_valid()) {
1125 this.do_onchange(field);
1126 this.on_form_changed(true);
1127 this.do_notify_change();
1131 get_field_desc: function(field_name) {
1132 return this.fields_view.fields[field_name];
1134 get_field_value: function(field_name) {
1135 return this.fields[field_name].get_value();
1137 compute_domain: function(expression) {
1138 return instance.web.form.compute_domain(expression, this.fields);
1140 _build_view_fields_values: function() {
1141 var a_dataset = this.dataset;
1142 var fields_values = this.get_fields_values();
1143 var active_id = a_dataset.ids[a_dataset.index];
1144 _.extend(fields_values, {
1145 active_id: active_id || false,
1146 active_ids: active_id ? [active_id] : [],
1147 active_model: a_dataset.model,
1150 if (a_dataset.parent_view) {
1151 fields_values.parent = a_dataset.parent_view.get_fields_values();
1153 return fields_values;
1155 build_eval_context: function() {
1156 var a_dataset = this.dataset;
1157 return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
1162 * Interface to be implemented by rendering engines for the form view.
1164 instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
1165 set_fields_view: function(fields_view) {},
1166 set_fields_registry: function(fields_registry) {},
1167 render_to: function($el) {},
1171 * Default rendering engine for the form view.
1173 * It is necessary to set the view using set_view() before usage.
1175 instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
1176 init: function(view) {
1179 set_fields_view: function(fvg) {
1181 this.version = parseFloat(this.fvg.arch.attrs.version);
1182 if (isNaN(this.version)) {
1186 set_tags_registry: function(tags_registry) {
1187 this.tags_registry = tags_registry;
1189 set_fields_registry: function(fields_registry) {
1190 this.fields_registry = fields_registry;
1192 set_widgets_registry: function(widgets_registry) {
1193 this.widgets_registry = widgets_registry;
1195 // Backward compatibility tools, current default version: v7
1196 process_version: function() {
1197 if (this.version < 7.0) {
1198 this.$form.find('form:first').wrapInner('<group col="4"/>');
1199 this.$form.find('page').each(function() {
1200 if (!$(this).parents('field').length) {
1201 $(this).wrapInner('<group col="4"/>');
1206 get_arch_fragment: function() {
1207 var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
1208 // IE won't allow custom button@type and will revert it to spec default : 'submit'
1209 $('button', doc).each(function() {
1210 $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
1212 // IE's html parser is also a css parser. How convenient...
1213 $('board', doc).each(function() {
1214 $(this).attr('layout', $(this).attr('style'));
1216 return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
1218 render_to: function($target) {
1220 this.$target = $target;
1222 this.$form = this.get_arch_fragment();
1224 this.process_version();
1226 this.fields_to_init = [];
1227 this.tags_to_init = [];
1228 this.widgets_to_init = [];
1230 this.process(this.$form);
1232 this.$form.appendTo(this.$target);
1234 this.to_replace = [];
1236 _.each(this.fields_to_init, function($elem) {
1237 var name = $elem.attr("name");
1238 if (!self.fvg.fields[name]) {
1239 throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
1241 var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
1243 throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
1245 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1246 var $label = self.labels[$elem.attr("name")];
1248 w.set_input_id($label.attr("for"));
1250 self.alter_field(w);
1251 self.view.register_field(w, $elem.attr("name"));
1252 self.to_replace.push([w, $elem]);
1254 _.each(this.tags_to_init, function($elem) {
1255 var tag_name = $elem[0].tagName.toLowerCase();
1256 var obj = self.tags_registry.get_object(tag_name);
1257 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1258 self.to_replace.push([w, $elem]);
1260 _.each(this.widgets_to_init, function($elem) {
1261 var widget_type = $elem.attr("type");
1262 var obj = self.widgets_registry.get_object(widget_type);
1263 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1264 self.to_replace.push([w, $elem]);
1267 init_fields: function() {
1269 _.each(this.to_replace, function(el) {
1270 defs.push(el[0].replace(el[1]));
1271 if (el[1].children().length) {
1272 el[0].$el.append(el[1].children());
1275 this.to_replace = [];
1276 return $.when.apply($, defs);
1278 render_element: function(template /* dictionaries */) {
1279 var dicts = [].slice.call(arguments).slice(1);
1280 var dict = _.extend.apply(_, dicts);
1281 dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
1282 return $(QWeb.render(template, dict));
1284 alter_field: function(field) {
1286 toggle_layout_debugging: function() {
1287 if (!this.$target.has('.oe_layout_debug_cell:first').length) {
1288 this.$target.find('[title]').removeAttr('title');
1289 this.$target.find('.oe_form_group_cell').each(function() {
1290 var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
1291 $(this).attr('title', text);
1294 this.$target.toggleClass('oe_layout_debugging');
1296 process: function($tag) {
1298 var tagname = $tag[0].nodeName.toLowerCase();
1299 if (this.tags_registry.contains(tagname)) {
1300 this.tags_to_init.push($tag);
1301 return (tagname === 'button') ? this.process_button($tag) : $tag;
1303 var fn = self['process_' + tagname];
1305 var args = [].slice.call(arguments);
1307 return fn.apply(self, args);
1309 // generic tag handling, just process children
1310 $tag.children().each(function() {
1311 self.process($(this));
1313 self.handle_common_properties($tag, $tag);
1314 $tag.removeAttr("modifiers");
1318 process_button: function ($button) {
1320 $button.children().each(function() {
1321 self.process($(this));
1325 process_widget: function($widget) {
1326 this.widgets_to_init.push($widget);
1329 process_sheet: function($sheet) {
1330 var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
1331 this.handle_common_properties($new_sheet, $sheet);
1332 var $dst = $new_sheet.find('.oe_form_sheet');
1333 $sheet.contents().appendTo($dst);
1334 $sheet.before($new_sheet).remove();
1335 this.process($new_sheet);
1337 process_form: function($form) {
1338 if ($form.find('> sheet').length === 0) {
1339 $form.addClass('oe_form_nosheet');
1341 var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
1342 this.handle_common_properties($new_form, $form);
1343 $form.contents().appendTo($new_form);
1344 if ($form[0] === this.$form[0]) {
1345 // If root element, replace it
1346 this.$form = $new_form;
1348 $form.before($new_form).remove();
1350 this.process($new_form);
1353 * Used by direct <field> children of a <group> tag only
1354 * This method will add the implicit <label...> for every field
1357 preprocess_field: function($field) {
1359 var name = $field.attr('name'),
1360 field_colspan = parseInt($field.attr('colspan'), 10),
1361 field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
1363 if ($field.attr('nolabel') === '1')
1365 $field.attr('nolabel', '1');
1367 this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
1368 $(el).parents().each(function(unused, tag) {
1369 var name = tag.tagName.toLowerCase();
1370 if (name === "field" || name in self.tags_registry.map)
1377 var $label = $('<label/>').attr({
1379 "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
1380 "string": $field.attr('string'),
1381 "help": $field.attr('help'),
1382 "class": $field.attr('class'),
1384 $label.insertBefore($field);
1385 if (field_colspan > 1) {
1386 $field.attr('colspan', field_colspan - 1);
1390 process_field: function($field) {
1391 if ($field.parent().is('group')) {
1392 // No implicit labels for normal fields, only for <group> direct children
1393 var $label = this.preprocess_field($field);
1395 this.process($label);
1398 this.fields_to_init.push($field);
1401 process_group: function($group) {
1403 $group.children('field').each(function() {
1404 self.preprocess_field($(this));
1406 var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
1408 if ($new_group.first().is('table.oe_form_group')) {
1409 $table = $new_group;
1410 } else if ($new_group.filter('table.oe_form_group').length) {
1411 $table = $new_group.filter('table.oe_form_group').first();
1413 $table = $new_group.find('table.oe_form_group').first();
1417 cols = parseInt($group.attr('col') || 2, 10),
1421 $group.children().each(function(a,b,c) {
1422 var $child = $(this);
1423 var colspan = parseInt($child.attr('colspan') || 1, 10);
1424 var tagName = $child[0].tagName.toLowerCase();
1425 var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
1426 var newline = tagName === 'newline';
1428 // Note FME: those classes are used in layout debug mode
1429 if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
1430 $tr.addClass('oe_form_group_row_incomplete');
1432 $tr.addClass('oe_form_group_row_newline');
1439 if (!$tr || row_cols < colspan) {
1440 $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
1442 } else if (tagName==='group') {
1443 // When <group> <group/><group/> </group>, we need a spacing between the two groups
1444 $td.addClass('oe_group_right');
1446 row_cols -= colspan;
1448 // invisibility transfer
1449 var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
1450 var invisible = field_modifiers.invisible;
1451 self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
1453 $tr.append($td.append($child));
1454 children.push($child[0]);
1456 if (row_cols && $td) {
1457 $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
1459 $group.before($new_group).remove();
1461 $table.find('> tbody > tr').each(function() {
1462 var to_compute = [],
1465 $(this).children().each(function() {
1467 $child = $td.children(':first');
1468 if ($child.attr('cell-class')) {
1469 $td.addClass($child.attr('cell-class'));
1471 switch ($child[0].tagName.toLowerCase()) {
1475 if ($child.attr('for')) {
1476 $td.attr('width', '1%').addClass('oe_form_group_cell_label');
1477 row_cols-= $td.attr('colspan') || 1;
1482 var width = _.str.trim($child.attr('width') || ''),
1483 iwidth = parseInt(width, 10);
1485 if (width.substr(-1) === '%') {
1487 width = iwidth + '%';
1490 $td.css('min-width', width + 'px');
1492 $td.attr('width', width);
1493 $child.removeAttr('width');
1494 row_cols-= $td.attr('colspan') || 1;
1496 to_compute.push($td);
1502 var unit = Math.floor(total / row_cols);
1503 if (!$(this).is('.oe_form_group_row_incomplete')) {
1504 _.each(to_compute, function($td, i) {
1505 var width = parseInt($td.attr('colspan'), 10) * unit;
1506 $td.attr('width', width + '%');
1512 _.each(children, function(el) {
1513 self.process($(el));
1515 this.handle_common_properties($new_group, $group);
1518 process_notebook: function($notebook) {
1521 $notebook.find('> page').each(function() {
1522 var $page = $(this);
1523 var page_attrs = $page.getAttributes();
1524 page_attrs.id = _.uniqueId('notebook_page_');
1525 var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
1526 $page.contents().appendTo($new_page);
1527 $page.before($new_page).remove();
1528 var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
1529 page_attrs.__page = $new_page;
1530 page_attrs.__ic = ic;
1531 pages.push(page_attrs);
1533 $new_page.children().each(function() {
1534 self.process($(this));
1537 var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
1538 $notebook.contents().appendTo($new_notebook);
1539 $notebook.before($new_notebook).remove();
1540 self.process($($new_notebook.children()[0]));
1541 //tabs and invisibility handling
1542 $new_notebook.tabs();
1543 _.each(pages, function(page, i) {
1546 page.__ic.on("change:effective_invisible", null, function() {
1547 if (!page.__ic.get('effective_invisible') && page.autofocus) {
1548 $new_notebook.tabs('select', i);
1551 var current = $new_notebook.tabs("option", "selected");
1552 if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
1554 var first_visible = _.find(_.range(pages.length), function(i2) {
1555 return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
1557 if (first_visible !== undefined) {
1558 $new_notebook.tabs('select', first_visible);
1563 this.handle_common_properties($new_notebook, $notebook);
1564 return $new_notebook;
1566 process_separator: function($separator) {
1567 var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
1568 $separator.before($new_separator).remove();
1569 this.handle_common_properties($new_separator, $separator);
1570 return $new_separator;
1572 process_label: function($label) {
1573 var name = $label.attr("for"),
1574 field_orm = this.fvg.fields[name];
1576 string: $label.attr('string') || (field_orm || {}).string || '',
1577 help: $label.attr('help') || (field_orm || {}).help || '',
1578 _for: name ? _.uniqueId('oe-field-input-') : undefined,
1580 var align = parseFloat(dict.align);
1581 if (isNaN(align) || align === 1) {
1583 } else if (align === 0) {
1589 var $new_label = this.render_element('FormRenderingLabel', dict);
1590 $label.before($new_label).remove();
1591 this.handle_common_properties($new_label, $label);
1593 this.labels[name] = $new_label;
1597 handle_common_properties: function($new_element, $node) {
1598 var str_modifiers = $node.attr("modifiers") || "{}";
1599 var modifiers = JSON.parse(str_modifiers);
1601 if (modifiers.invisible !== undefined)
1602 ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
1603 $new_element.addClass($node.attr("class") || "");
1604 $new_element.attr('style', $node.attr('style'));
1605 return {invisibility_changer: ic,};
1612 If you read this documentation, it probably means that you were asked to use a form view widget outside of
1613 a form view. Before going further, you must understand that those fields were never really created for
1614 that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
1615 you to hack the system with more style.
1617 instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
1618 init: function(parent, eval_context) {
1619 this._super(parent);
1620 this.field_descs = {};
1621 this.eval_context = eval_context || {};
1623 display_invalid_fields: false,
1624 actual_mode: 'create',
1627 get_field_desc: function(field_name) {
1628 if (this.field_descs[field_name] === undefined) {
1629 this.field_descs[field_name] = {
1633 return this.field_descs[field_name];
1635 extend_field_desc: function(fields) {
1637 _.each(fields, function(v, k) {
1638 _.extend(self.get_field_desc(k), v);
1641 get_field_value: function(field_name) {
1644 set_values: function(values) {
1647 compute_domain: function(expression) {
1648 return instance.web.form.compute_domain(expression, {});
1650 build_eval_context: function() {
1651 return new instance.web.CompoundContext(this.eval_context);
1655 instance.web.form.compute_domain = function(expr, fields) {
1656 if (! (expr instanceof Array))
1659 for (var i = expr.length - 1; i >= 0; i--) {
1661 if (ex.length == 1) {
1662 var top = stack.pop();
1665 stack.push(stack.pop() || top);
1668 stack.push(stack.pop() && top);
1674 throw new Error(_.str.sprintf(
1675 _t("Unknown operator %s in domain %s"),
1676 ex, JSON.stringify(expr)));
1680 var field = fields[ex[0]];
1682 throw new Error(_.str.sprintf(
1683 _t("Unknown field %s in domain %s"),
1684 ex[0], JSON.stringify(expr)));
1686 var field_value = field.get_value ? field.get_value() : field.value;
1690 switch (op.toLowerCase()) {
1693 stack.push(_.isEqual(field_value, val));
1697 stack.push(!_.isEqual(field_value, val));
1700 stack.push(field_value < val);
1703 stack.push(field_value > val);
1706 stack.push(field_value <= val);
1709 stack.push(field_value >= val);
1712 if (!_.isArray(val)) val = [val];
1713 stack.push(_(val).contains(field_value));
1716 if (!_.isArray(val)) val = [val];
1717 stack.push(!_(val).contains(field_value));
1721 _t("Unsupported operator %s in domain %s"),
1722 op, JSON.stringify(expr));
1725 return _.all(stack, _.identity);
1728 instance.web.form.is_bin_size = function(v) {
1729 return (/^\d+(\.\d*)? \w+$/).test(v);
1733 * Must be applied over an class already possessing the PropertiesMixin.
1735 * Apply the result of the "invisible" domain to this.$el.
1737 instance.web.form.InvisibilityChangerMixin = {
1738 init: function(field_manager, invisible_domain) {
1740 this._ic_field_manager = field_manager;
1741 this._ic_invisible_modifier = invisible_domain;
1742 this._ic_field_manager.on("view_content_has_changed", this, function() {
1743 var result = self._ic_invisible_modifier === undefined ? false :
1744 self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
1745 self.set({"invisible": result});
1747 this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
1748 var check = function() {
1749 if (self.get("invisible") || self.get('force_invisible')) {
1750 self.set({"effective_invisible": true});
1752 self.set({"effective_invisible": false});
1755 this.on('change:invisible', this, check);
1756 this.on('change:force_invisible', this, check);
1760 this.on("change:effective_invisible", this, this._check_visibility);
1761 this._check_visibility();
1763 _check_visibility: function() {
1764 this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
1768 instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
1769 init: function(parent, field_manager, invisible_domain, $el) {
1770 this.setParent(parent);
1771 instance.web.PropertiesMixin.init.call(this);
1772 instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
1779 Base class for all fields, custom widgets and buttons to be displayed in the form view.
1782 - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
1783 the values of the "readonly" property and the "mode" property on the field manager.
1785 instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
1787 * @constructs instance.web.form.FormWidget
1788 * @extends instance.web.Widget
1790 * @param field_manager
1793 init: function(field_manager, node) {
1794 this._super(field_manager);
1795 this.field_manager = field_manager;
1796 if (this.field_manager instanceof instance.web.FormView)
1797 this.view = this.field_manager;
1799 this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
1800 instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
1802 this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
1808 // some events to make the property "effective_readonly" sync automatically with "readonly" and
1809 // "mode" on field_manager
1811 var test_effective_readonly = function() {
1812 self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
1814 this.on("change:readonly", this, test_effective_readonly);
1815 this.field_manager.on("change:actual_mode", this, test_effective_readonly);
1816 test_effective_readonly.call(this);
1818 renderElement: function() {
1819 this.process_modifiers();
1821 this.$el.addClass(this.node.attrs["class"] || "");
1823 destroy: function() {
1824 $.fn.tooltip('destroy');
1825 this._super.apply(this, arguments);
1828 * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
1830 * This method is an utility method that is meant to be called by child classes.
1832 * @param {jQuery} $e jQuery object of elements to bind focus/blur on
1834 setupFocus: function ($e) {
1837 focus: function () { self.trigger('focused'); },
1838 blur: function () { self.trigger('blurred'); }
1841 process_modifiers: function() {
1843 for (var a in this.modifiers) {
1844 if (!this.modifiers.hasOwnProperty(a)) { continue; }
1845 if (!_.include(["invisible"], a)) {
1846 var val = this.field_manager.compute_domain(this.modifiers[a]);
1852 do_attach_tooltip: function(widget, trigger, options) {
1853 widget = widget || this;
1854 trigger = trigger || this.$el;
1855 options = _.extend({
1856 delay: { show: 500, hide: 0 },
1858 var template = widget.template + '.tooltip';
1859 if (!QWeb.has_template(template)) {
1860 template = 'WidgetLabel.tooltip';
1862 return QWeb.render(template, {
1863 debug: instance.session.debug,
1868 //only show tooltip if we are in debug or if we have a help to show, otherwise it will display
1870 if (instance.session.debug || widget.node.attrs.help || (widget.field && widget.field.help)){
1871 $(trigger).tooltip(options);
1875 * Builds a new context usable for operations related to fields by merging
1876 * the fields'context with the action's context.
1878 build_context: function() {
1879 // only use the model's context if there is not context on the node
1880 var v_context = this.node.attrs.context;
1882 v_context = (this.field || {}).context || {};
1885 if (v_context.__ref || true) { //TODO: remove true
1886 var fields_values = this.field_manager.build_eval_context();
1887 v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
1891 build_domain: function() {
1892 var f_domain = this.field.domain || [];
1893 var n_domain = this.node.attrs.domain || null;
1894 // if there is a domain on the node, overrides the model's domain
1895 var final_domain = n_domain !== null ? n_domain : f_domain;
1896 if (!(final_domain instanceof Array) || true) { //TODO: remove true
1897 var fields_values = this.field_manager.build_eval_context();
1898 final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
1900 return final_domain;
1904 instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
1905 template: 'WidgetButton',
1906 init: function(field_manager, node) {
1907 node.attrs.type = node.attrs['data-button-type'];
1908 this.is_stat_button = /\boe_stat_button\b/.test(node.attrs['class']);
1909 this.icon_class = node.attrs.icon && "stat_button_icon fa " + node.attrs.icon + " fa-fw";
1910 this._super(field_manager, node);
1911 this.force_disabled = false;
1912 this.string = (this.node.attrs.string || '').replace(/_/g, '');
1913 if (JSON.parse(this.node.attrs.default_focus || "0")) {
1914 // TODO fme: provide enter key binding to widgets
1915 this.view.default_focus_button = this;
1917 if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
1918 this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
1922 this._super.apply(this, arguments);
1923 this.view.on('view_content_has_changed', this, this.check_disable);
1924 this.check_disable();
1925 this.$el.click(this.on_click);
1926 if (this.node.attrs.help || instance.session.debug) {
1927 this.do_attach_tooltip();
1929 this.setupFocus(this.$el);
1931 on_click: function() {
1933 this.force_disabled = true;
1934 this.check_disable();
1935 this.execute_action().always(function() {
1936 self.force_disabled = false;
1937 self.check_disable();
1940 execute_action: function() {
1942 var exec_action = function() {
1943 if (self.node.attrs.confirm) {
1944 var def = $.Deferred();
1945 var dialog = new instance.web.Dialog(this, {
1946 title: _t('Confirm'),
1948 {text: _t("Cancel"), click: function() {
1949 this.parents('.modal').modal('hide');
1952 {text: _t("Ok"), click: function() {
1954 self.on_confirmed().always(function() {
1955 self2.parents('.modal').modal('hide');
1960 }, $('<div/>').text(self.node.attrs.confirm)).open();
1961 dialog.on("closing", null, function() {def.resolve();});
1962 return def.promise();
1964 return self.on_confirmed();
1967 if (!this.node.attrs.special) {
1968 return this.view.recursive_save().then(exec_action);
1970 return exec_action();
1973 on_confirmed: function() {
1976 var context = this.build_context();
1977 return this.view.do_execute_action(
1978 _.extend({}, this.node.attrs, {context: context}),
1979 this.view.dataset, this.view.datarecord.id, function (reason) {
1980 if (!_.isObject(reason)) {
1981 self.view.recursive_reload();
1985 check_disable: function() {
1986 var disabled = (this.force_disabled || !this.view.is_interactible_record());
1987 this.$el.prop('disabled', disabled);
1988 this.$el.css('color', disabled ? 'grey' : '');
1993 * Interface to be implemented by fields.
1996 * - changed_value: triggered when the value of the field has changed. This can be due
1997 * to a user interaction or a call to set_value().
2000 instance.web.form.FieldInterface = {
2002 * Constructor takes 2 arguments:
2003 * - field_manager: Implements FieldManagerMixin
2004 * - node: the "<field>" node in json form
2006 init: function(field_manager, node) {},
2008 * Called by the form view to indicate the value of the field.
2010 * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
2011 * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
2012 * before the widget is inserted into the DOM.
2014 * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
2015 * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
2016 * also be able to handle any other format commonly used in the _defaults key on the models in the addons
2017 * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
2018 * no information is ever given to know which format is used.
2020 set_value: function(value_) {},
2022 * Get the current value of the widget.
2024 * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
2025 * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
2026 * For example if the field is marked as "required", a call to get_value() can return false.
2028 * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
2029 * return a default value according to the type of field.
2031 * This method is always assumed to perform synchronously, it can not return a promise.
2033 * If there was no user interaction to modify the value of the field, it is always assumed that
2034 * get_value() return the same semantic value than the one passed in the last call to set_value(),
2035 * although the syntax can be different. This can be the case for type of fields that have a different
2036 * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
2038 get_value: function() {},
2040 * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
2043 set_input_id: function(id) {},
2045 * Returns true if is_syntax_valid() returns true and the value is semantically
2046 * valid too according to the semantic restrictions applied to the field.
2048 is_valid: function() {},
2050 * Returns true if the field holds a value which is syntactically correct, ignoring
2051 * the potential semantic restrictions applied to the field.
2053 is_syntax_valid: function() {},
2055 * Must set the focus on the field. Return false if field is not focusable.
2057 focus: function() {},
2059 * Called when the translate button is clicked.
2061 on_translate: function() {},
2063 This method is called by the form view before reading on_change values and before saving. It tells
2064 the field to save its value before reading it using get_value(). Must return a promise.
2066 commit_value: function() {},
2070 * Abstract class for classes implementing FieldInterface.
2073 * - value: useful property to hold the value of the field. By default, set_value() and get_value()
2074 * set and retrieve the value property. Changing the value property also triggers automatically
2075 * a 'changed_value' event that inform the view to trigger on_changes.
2078 instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
2080 * @constructs instance.web.form.AbstractField
2081 * @extends instance.web.form.FormWidget
2083 * @param field_manager
2086 init: function(field_manager, node) {
2088 this._super(field_manager, node);
2089 this.name = this.node.attrs.name;
2090 this.field = this.field_manager.get_field_desc(this.name);
2091 this.widget = this.node.attrs.widget;
2092 this.string = this.node.attrs.string || this.field.string || this.name;
2093 this.options = instance.web.py_eval(this.node.attrs.options || '{}');
2094 this.set({'value': false});
2096 this.on("change:value", this, function() {
2097 this.trigger('changed_value');
2098 this._check_css_flags();
2101 renderElement: function() {
2104 if (this.field.translate && this.view) {
2105 this.$el.addClass('oe_form_field_translatable');
2106 this.$el.find('.oe_field_translate').click(this.on_translate);
2108 this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
2109 this.do_attach_tooltip(this, this.$label[0] || this.$el);
2110 if (instance.session.debug) {
2111 this.$label.off('dblclick').on('dblclick', function() {
2112 console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
2114 console.log("window.w =", window.w);
2117 if (!this.disable_utility_classes) {
2118 this.off("change:required", this, this._set_required);
2119 this.on("change:required", this, this._set_required);
2120 this._set_required();
2122 this._check_visibility();
2123 this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
2124 this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
2125 this._check_css_flags();
2128 var tmp = this._super();
2129 this.on("change:value", this, function() {
2130 if (! this.no_rerender)
2131 this.render_value();
2133 this.render_value();
2136 * Private. Do not use.
2138 _set_required: function() {
2139 this.$el.toggleClass('oe_form_required', this.get("required"));
2141 set_value: function(value_) {
2142 this.set({'value': value_});
2144 get_value: function() {
2145 return this.get('value');
2148 Utility method that all implementations should use to change the
2149 value without triggering a re-rendering.
2151 internal_set_value: function(value_) {
2152 var tmp = this.no_rerender;
2153 this.no_rerender = true;
2154 this.set({'value': value_});
2155 this.no_rerender = tmp;
2158 This method is called each time the value is modified.
2160 render_value: function() {},
2161 is_valid: function() {
2162 return this.is_syntax_valid() && !(this.get('required') && this.is_false());
2164 is_syntax_valid: function() {
2168 * Method useful to implement to ease validity testing. Must return true if the current
2169 * value is similar to false in OpenERP.
2171 is_false: function() {
2172 return this.get('value') === false;
2174 _check_css_flags: function() {
2175 if (this.field.translate) {
2176 this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
2178 if (!this.disable_utility_classes) {
2179 if (this.field_manager.get('display_invalid_fields')) {
2180 this.$el.toggleClass('oe_form_invalid', !this.is_valid());
2187 set_input_id: function(id) {
2188 this.id_for_label = id;
2190 on_translate: function() {
2192 var trans = new instance.web.DataSet(this, 'ir.translation');
2193 return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
2198 set_dimensions: function (height, width) {
2204 commit_value: function() {
2210 * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
2213 instance.web.form.ReinitializeWidgetMixin = {
2215 * Default implementation of, you should not override it, use initialize_field() instead.
2218 this.initialize_field();
2221 initialize_field: function() {
2222 this.on("change:effective_readonly", this, this.reinitialize);
2223 this.initialize_content();
2225 reinitialize: function() {
2226 this.destroy_content();
2227 this.renderElement();
2228 this.initialize_content();
2231 * Called to destroy anything that could have been created previously, called before a
2232 * re-initialization.
2234 destroy_content: function() {},
2236 * Called to initialize the content.
2238 initialize_content: function() {},
2242 * A mixin to apply on any field that has to completely re-render when its readonly state
2245 instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
2246 reinitialize: function() {
2247 instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
2248 var res = this.render_value();
2249 if (this.view && this.view.render_value_defs){
2250 this.view.render_value_defs.push(res);
2256 Some hack to make placeholders work in ie9.
2258 if (!('placeholder' in document.createElement('input'))) {
2259 document.addEventListener("DOMNodeInserted",function(event){
2260 var nodename = event.target.nodeName.toLowerCase();
2261 if ( nodename === "input" || nodename == "textarea" ) {
2262 $(event.target).placeholder();
2267 instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2268 template: 'FieldChar',
2269 widget_class: 'oe_form_field_char',
2271 'change input': 'store_dom_value',
2273 init: function (field_manager, node) {
2274 this._super(field_manager, node);
2275 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2277 initialize_content: function() {
2278 this.setupFocus(this.$('input'));
2280 store_dom_value: function () {
2281 if (!this.get('effective_readonly')
2282 && this.$('input').length
2283 && this.is_syntax_valid()) {
2284 this.internal_set_value(
2286 this.$('input').val()));
2289 commit_value: function () {
2290 this.store_dom_value();
2291 return this._super();
2293 render_value: function() {
2294 var show_value = this.format_value(this.get('value'), '');
2295 if (!this.get("effective_readonly")) {
2296 this.$el.find('input').val(show_value);
2298 if (this.password) {
2299 show_value = new Array(show_value.length + 1).join('*');
2301 this.$(".oe_form_char_content").text(show_value);
2304 is_syntax_valid: function() {
2305 if (!this.get("effective_readonly") && this.$("input").size() > 0) {
2307 this.parse_value(this.$('input').val(), '');
2315 parse_value: function(val, def) {
2316 return instance.web.parse_value(val, this, def);
2318 format_value: function(val, def) {
2319 return instance.web.format_value(val, this, def);
2321 is_false: function() {
2322 return this.get('value') === '' || this._super();
2325 var input = this.$('input:first')[0];
2326 return input ? input.focus() : false;
2328 set_dimensions: function (height, width) {
2329 this._super(height, width);
2330 this.$('input').css({
2337 instance.web.form.KanbanSelection = instance.web.form.FieldChar.extend({
2338 init: function (field_manager, node) {
2339 this._super(field_manager, node);
2341 prepare_dropdown_selection: function() {
2344 var selection = self.field.selection || [];
2345 _.map(selection, function(res) {
2349 'state_name': res[1],
2351 if (res[0] == 'normal') { value['state_class'] = 'oe_kanban_status'; }
2352 else if (res[0] == 'done') { value['state_class'] = 'oe_kanban_status oe_kanban_status_green'; }
2353 else { value['state_class'] = 'oe_kanban_status oe_kanban_status_red'; }
2358 render_value: function() {
2360 this.record_id = this.view.datarecord.id;
2361 this.states = this.prepare_dropdown_selection();;
2362 this.$el.html(QWeb.render("KanbanSelection", {'widget': self}));
2363 this.$el.find('li').on('click', this.set_kanban_selection.bind(this));
2365 /* setting the value: in view mode, perform an asynchronous call and reload
2366 the form view; in edit mode, use set_value to save the new value that will
2367 be written when saving the record. */
2368 set_kanban_selection: function (ev) {
2370 var li = $(ev.target).closest('li');
2372 var value = String(li.data('value'));
2373 if (this.view.get('actual_mode') == 'view') {
2374 var write_values = {}
2375 write_values[self.name] = value;
2376 return this.view.dataset._model.call(
2380 self.view.dataset.get_context()
2381 ]).done(self.reload_record.bind(self));
2384 return this.set_value(value);
2388 reload_record: function() {
2393 instance.web.form.Priority = instance.web.form.FieldChar.extend({
2394 init: function (field_manager, node) {
2395 this._super(field_manager, node);
2397 prepare_priority: function() {
2399 var selection = this.field.selection || [];
2400 var init_value = selection && selection[0][0] || 0;
2401 var data = _.map(selection.slice(1), function(element, index) {
2403 'value': element[0],
2405 'click_value': element[0],
2407 if (index == 0 && self.get('value') == element[0]) {
2408 value['click_value'] = init_value;
2414 render_value: function() {
2416 this.record_id = this.view.datarecord.id;
2417 this.priorities = this.prepare_priority();
2418 this.$el.html(QWeb.render("Priority", {'widget': this}));
2419 this.$el.find('li').on('click', this.set_priority.bind(this));
2421 /* setting the value: in view mode, perform an asynchronous call and reload
2422 the form view; in edit mode, use set_value to save the new value that will
2423 be written when saving the record. */
2424 set_priority: function (ev) {
2426 var li = $(ev.target).closest('li');
2428 var value = String(li.data('value'));
2429 if (this.view.get('actual_mode') == 'view') {
2430 var write_values = {}
2431 write_values[self.name] = value;
2432 return this.view.dataset._model.call(
2436 self.view.dataset.get_context()
2437 ]).done(self.reload_record.bind(self));
2440 return this.set_value(value);
2445 reload_record: function() {
2450 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2451 process_modifiers: function () {
2453 this.set({ readonly: true });
2457 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2458 template: 'FieldEmail',
2459 initialize_content: function() {
2461 var $button = this.$el.find('button');
2462 $button.click(this.on_button_clicked);
2463 this.setupFocus($button);
2465 render_value: function() {
2466 if (!this.get("effective_readonly")) {
2470 .attr('href', 'mailto:' + this.get('value'))
2471 .text(this.get('value') || '');
2474 on_button_clicked: function() {
2475 if (!this.get('value') || !this.is_syntax_valid()) {
2476 this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2478 location.href = 'mailto:' + this.get('value');
2483 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2484 template: 'FieldUrl',
2485 initialize_content: function() {
2487 var $button = this.$el.find('button');
2488 $button.click(this.on_button_clicked);
2489 this.setupFocus($button);
2491 render_value: function() {
2492 if (!this.get("effective_readonly")) {
2495 var tmp = this.get('value');
2496 var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2498 tmp = "http://" + this.get('value');
2500 var text = this.get('value') ? this.node.attrs.text || tmp : '';
2501 this.$el.find('a').attr('href', tmp).text(text);
2504 on_button_clicked: function() {
2505 if (!this.get('value')) {
2506 this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2508 var url = $.trim(this.get('value'));
2509 if(/^www\./i.test(url))
2510 url = 'http://'+url;
2516 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2517 is_field_number: true,
2518 widget_class: 'oe_form_field_float',
2519 init: function (field_manager, node) {
2520 this._super(field_manager, node);
2521 this.internal_set_value(0);
2522 if (this.node.attrs.digits) {
2523 this.digits = this.node.attrs.digits;
2525 this.digits = this.field.digits;
2528 set_value: function(value_) {
2529 if (value_ === false || value_ === undefined) {
2530 // As in GTK client, floats default to 0
2533 this._super.apply(this, [value_]);
2535 focus: function () {
2536 var $input = this.$('input:first');
2537 return $input.length ? $input.select() : false;
2541 instance.web.form.FieldCharDomain = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2542 init: function(field_manager, node) {
2543 this._super.apply(this, arguments);
2547 this._super.apply(this, arguments);
2548 this.on("change:effective_readonly", this, function () {
2549 this.display_field();
2551 this.display_field();
2552 return this._super();
2554 set_value: function(value_) {
2556 this.set('value', value_ || false);
2557 this.display_field();
2559 display_field: function() {
2561 this.$el.html(instance.web.qweb.render("FieldCharDomain", {widget: this}));
2562 if (this.get('value')) {
2563 var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2564 var domain = instance.web.pyeval.eval('domain', this.get('value'));
2565 var ds = new instance.web.DataSetStatic(self, model, self.build_context());
2566 ds.call('search_count', [domain]).then(function (results) {
2567 $('.oe_domain_count', self.$el).text(results + ' records selected');
2568 if (self.get('effective_readonly')) {
2569 $('button span', self.$el).text(' See selection');
2572 $('button span', self.$el).text(' Change selection');
2576 $('.oe_domain_count', this.$el).text('0 record selected');
2577 $('button span', this.$el).text(' Select records');
2579 this.$('.select_records').on('click', self.on_click);
2581 on_click: function(event) {
2582 event.preventDefault();
2584 var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2585 this.pop = new instance.web.form.SelectCreatePopup(this);
2586 this.pop.select_element(
2588 title: this.get('effective_readonly') ? 'Selected records' : 'Select records...',
2589 readonly: this.get('effective_readonly'),
2590 disable_multiple_selection: this.get('effective_readonly'),
2591 no_create: this.get('effective_readonly'),
2592 }, [], this.build_context());
2593 this.pop.on("elements_selected", self, function(element_ids) {
2594 if (this.pop.$('input.oe_list_record_selector').prop('checked')) {
2595 var search_data = this.pop.searchview.build_search_data();
2596 var domain_done = instance.web.pyeval.eval_domains_and_contexts({
2597 domains: search_data.domains,
2598 contexts: search_data.contexts,
2599 group_by_seq: search_data.groupbys || []
2600 }).then(function (results) {
2601 return results.domain;
2605 var domain = [["id", "in", element_ids]];
2606 var domain_done = $.Deferred().resolve(domain);
2608 $.when(domain_done).then(function (domain) {
2609 var domain = self.pop.dataset.domain.concat(domain || []);
2610 self.set_value(domain);
2616 instance.web.DateTimeWidget = instance.web.Widget.extend({
2617 template: "web.datepicker",
2618 type_of_date: "datetime",
2620 'dp.change .oe_datepicker_main': 'change_datetime',
2621 'dp.show .oe_datepicker_main': 'set_datetime_default',
2622 'keypress .oe_datepicker_master': 'change_datetime',
2624 init: function(parent) {
2625 this._super(parent);
2626 this.name = parent.name;
2630 var l10n = _t.database.parameters;
2634 startDate: new moment({ y: 1900 }),
2635 endDate: new moment().add(200, "y"),
2636 calendarWeeks: true,
2638 time: 'fa fa-clock-o',
2639 date: 'fa fa-calendar',
2640 up: 'fa fa-chevron-up',
2641 down: 'fa fa-chevron-down'
2643 language : moment.locale(),
2644 format : instance.web.convert_to_moment_format(l10n.date_format +' '+ l10n.time_format),
2646 this.$input = this.$el.find('input.oe_datepicker_master');
2647 if (this.type_of_date === 'date') {
2648 options['pickTime'] = false;
2649 options['useSeconds'] = false;
2650 options['format'] = instance.web.convert_to_moment_format(l10n.date_format);
2652 this.picker = this.$('.oe_datepicker_main').datetimepicker(options);
2653 this.set_readonly(false);
2654 this.set({'value': false});
2656 set_value: function(value_) {
2657 this.set({'value': value_});
2658 this.$input.val(value_ ? this.format_client(value_) : '');
2660 get_value: function() {
2661 return this.get('value');
2663 set_value_from_ui_: function() {
2664 var value_ = this.$input.val() || false;
2665 this.set_value(this.parse_client(value_));
2667 set_readonly: function(readonly) {
2668 this.readonly = readonly;
2669 this.$input.prop('readonly', this.readonly);
2671 is_valid_: function() {
2672 var value_ = this.$input.val();
2673 if (value_ === "") {
2677 this.parse_client(value_);
2684 parse_client: function(v) {
2685 return instance.web.parse_value(v, {"widget": this.type_of_date});
2687 format_client: function(v) {
2688 return instance.web.format_value(v, {"widget": this.type_of_date});
2690 set_datetime_default: function(){
2691 //when opening datetimepicker the date and time by default should be the one from
2692 //the input field if any or the current day otherwise
2693 if (this.type_of_date === 'datetime') {
2694 value = new moment().second(0);
2695 if (this.$input.val().length !== 0 && this.is_valid_()){
2696 var value = this.$input.val();
2698 this.$('.oe_datepicker_main').data('DateTimePicker').setValue(value);
2701 change_datetime: function(e) {
2702 if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
2703 this.set_value_from_ui_();
2704 this.trigger("datetime_changed");
2707 commit_value: function () {
2708 this.change_datetime();
2712 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2713 type_of_date: "date"
2716 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2717 template: "FieldDatetime",
2718 build_widget: function() {
2719 return new instance.web.DateTimeWidget(this);
2721 destroy_content: function() {
2722 if (this.datewidget) {
2723 this.datewidget.destroy();
2724 this.datewidget = undefined;
2727 initialize_content: function() {
2728 if (!this.get("effective_readonly")) {
2729 this.datewidget = this.build_widget();
2730 this.datewidget.on('datetime_changed', this, _.bind(function() {
2731 this.internal_set_value(this.datewidget.get_value());
2733 this.datewidget.appendTo(this.$el);
2734 this.setupFocus(this.datewidget.$input);
2737 render_value: function() {
2738 if (!this.get("effective_readonly")) {
2739 this.datewidget.set_value(this.get('value'));
2741 this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2744 is_syntax_valid: function() {
2745 if (!this.get("effective_readonly") && this.datewidget) {
2746 return this.datewidget.is_valid_();
2750 is_false: function() {
2751 return this.get('value') === '' || this._super();
2754 var input = this.datewidget && this.datewidget.$input[0];
2755 return input ? input.focus() : false;
2757 set_dimensions: function (height, width) {
2758 this._super(height, width);
2759 if (!this.get("effective_readonly")) {
2760 this.datewidget.$input.css('height', height);
2765 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2766 template: "FieldDate",
2767 build_widget: function() {
2768 return new instance.web.DateWidget(this);
2772 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2773 template: 'FieldText',
2775 'keyup': function (e) {
2776 if (e.which === $.ui.keyCode.ENTER) {
2777 e.stopPropagation();
2780 'keypress': function (e) {
2781 if (e.which === $.ui.keyCode.ENTER) {
2782 e.stopPropagation();
2785 'change textarea': 'store_dom_value',
2787 initialize_content: function() {
2789 if (! this.get("effective_readonly")) {
2790 this.$textarea = this.$el.find('textarea');
2791 this.auto_sized = false;
2792 this.default_height = this.$textarea.css('height');
2793 if (this.get("effective_readonly")) {
2794 this.$textarea.attr('disabled', 'disabled');
2796 this.setupFocus(this.$textarea);
2798 this.$textarea = undefined;
2801 commit_value: function () {
2802 if (! this.get("effective_readonly") && this.$textarea) {
2803 this.store_dom_value();
2805 return this._super();
2807 store_dom_value: function () {
2808 this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2810 render_value: function() {
2811 if (! this.get("effective_readonly")) {
2812 var show_value = instance.web.format_value(this.get('value'), this, '');
2813 if (show_value === '') {
2814 this.$textarea.css('height', parseInt(this.default_height, 10)+"px");
2816 this.$textarea.val(show_value);
2817 if (! this.auto_sized) {
2818 this.auto_sized = true;
2819 this.$textarea.autosize();
2821 this.$textarea.trigger("autosize");
2824 var txt = this.get("value") || '';
2825 this.$(".oe_form_text_content").text(txt);
2828 is_syntax_valid: function() {
2829 if (!this.get("effective_readonly") && this.$textarea) {
2831 instance.web.parse_value(this.$textarea.val(), this, '');
2839 is_false: function() {
2840 return this.get('value') === '' || this._super();
2842 focus: function($el) {
2843 var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2844 return input ? input.focus() : false;
2846 set_dimensions: function (height, width) {
2847 this._super(height, width);
2848 if (!this.get("effective_readonly") && this.$textarea) {
2849 this.$textarea.css({
2858 * FieldTextHtml Widget
2859 * Intended for FieldText widgets meant to display HTML content. This
2860 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2861 * To find more information about CLEditor configutation: go to
2862 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2864 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2865 template: 'FieldTextHtml',
2867 this._super.apply(this, arguments);
2869 initialize_content: function() {
2871 if (! this.get("effective_readonly")) {
2872 self._updating_editor = false;
2873 this.$textarea = this.$el.find('textarea');
2874 var width = ((this.node.attrs || {}).editor_width || 'calc(100% - 4px)');
2875 var height = ((this.node.attrs || {}).editor_height || 250);
2876 this.$textarea.cleditor({
2877 width: width, // width not including margins, borders or padding
2878 height: height, // height not including margins, borders or padding
2879 controls: // controls to add to the toolbar
2880 "bold italic underline strikethrough " +
2881 "| removeformat | bullets numbering | outdent " +
2882 "indent | link unlink | source",
2883 bodyStyle: // style to assign to document body contained within the editor
2884 "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2886 this.$cleditor = this.$textarea.cleditor()[0];
2887 this.$cleditor.change(function() {
2888 if (! self._updating_editor) {
2889 self.$cleditor.updateTextArea();
2890 self.internal_set_value(self.$textarea.val());
2893 if (this.field.translate) {
2894 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"/>')
2895 .click(this.on_translate);
2896 this.$cleditor.$toolbar.append($img);
2900 render_value: function() {
2901 if (! this.get("effective_readonly")) {
2902 this.$textarea.val(this.get('value') || '');
2903 this._updating_editor = true;
2904 this.$cleditor.updateFrame();
2905 this._updating_editor = false;
2907 this.$el.html(this.get('value'));
2912 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2913 template: 'FieldBoolean',
2916 this.$checkbox = $("input", this.$el);
2917 this.setupFocus(this.$checkbox);
2918 this.$el.click(_.bind(function() {
2919 this.internal_set_value(this.$checkbox.is(':checked'));
2921 var check_readonly = function() {
2922 self.$checkbox.prop('disabled', self.get("effective_readonly"));
2923 self.click_disabled_boolean();
2925 this.on("change:effective_readonly", this, check_readonly);
2926 check_readonly.call(this);
2927 this._super.apply(this, arguments);
2929 render_value: function() {
2930 this.$checkbox[0].checked = this.get('value');
2933 var input = this.$checkbox && this.$checkbox[0];
2934 return input ? input.focus() : false;
2936 click_disabled_boolean: function(){
2937 var $disabled = this.$el.find('input[type=checkbox]:disabled');
2938 $disabled.each(function (){
2939 $(this).next('div').remove();
2940 $(this).closest("span").append($('<div class="boolean"></div>'));
2946 The progressbar field expect a float from 0 to 100.
2948 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2949 template: 'FieldProgressBar',
2950 render_value: function() {
2951 this.$el.progressbar({
2952 value: this.get('value') || 0,
2953 disabled: this.get("effective_readonly")
2955 var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
2956 this.$('span').html(formatted_value + '%');
2961 The PercentPie field expect a float from 0 to 100.
2963 instance.web.form.FieldPercentPie = instance.web.form.AbstractField.extend({
2964 template: 'FieldPercentPie',
2966 render_value: function() {
2967 var value = this.get('value'),
2968 formatted_value = Math.round(value || 0) + '%',
2969 svg = this.$('svg')[0];
2972 nv.addGraph(function() {
2973 var width = 42, height = 42;
2974 var chart = nv.models.pieChart()
2977 .margin({top: 0, right: 0, bottom: 0, left: 0})
2982 .color(['#7C7BAD','#DDD'])
2986 .datum([{'x': 'value', 'y': value}, {'x': 'complement', 'y': 100 - value}])
2989 .attr('style', 'width: ' + width + 'px; height:' + height + 'px;');
2993 .attr({x: width/2, y: height/2 + 3, 'text-anchor': 'middle'})
2994 .style({"font-size": "10px", "font-weight": "bold"})
2995 .text(formatted_value);
3004 The FieldBarChart expectsa list of values (indeed)
3006 instance.web.form.FieldBarChart = instance.web.form.AbstractField.extend({
3007 template: 'FieldBarChart',
3009 render_value: function() {
3010 var value = JSON.parse(this.get('value'));
3011 var svg = this.$('svg')[0];
3013 nv.addGraph(function() {
3014 var width = 34, height = 34;
3015 var chart = nv.models.discreteBarChart()
3016 .x(function (d) { return d.tooltip })
3017 .y(function (d) { return d.value })
3020 .margin({top: 0, right: 0, bottom: 0, left: 0})
3023 .transitionDuration(350)
3028 .datum([{key: 'values', values: value}])
3031 .attr('style', 'width: ' + (width + 4) + 'px; height: ' + (height + 8) + 'px;');
3033 nv.utils.windowResize(chart.update);
3042 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3043 template: 'FieldSelection',
3045 'change select': 'store_dom_value',
3047 init: function(field_manager, node) {
3049 this._super(field_manager, node);
3050 this.set("value", false);
3051 this.set("values", []);
3052 this.records_orderer = new instance.web.DropMisordered();
3053 this.field_manager.on("view_content_has_changed", this, function() {
3054 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
3055 if (! _.isEqual(domain, this.get("domain"))) {
3056 this.set("domain", domain);
3060 initialize_field: function() {
3061 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3062 this.on("change:domain", this, this.query_values);
3063 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
3064 this.on("change:values", this, this.render_value);
3066 query_values: function() {
3069 if (this.field.type === "many2one") {
3070 var model = new openerp.Model(openerp.session, this.field.relation);
3071 def = model.call("name_search", ['', this.get("domain")], {"context": this.build_context()});
3073 var values = _.reject(this.field.selection, function (v) { return v[0] === false && v[1] === ''; });
3074 def = $.when(values);
3076 this.records_orderer.add(def).then(function(values) {
3077 if (! _.isEqual(values, self.get("values"))) {
3078 self.set("values", values);
3082 initialize_content: function() {
3083 // Flag indicating whether we're in an event chain containing a change
3084 // event on the select, in order to know what to do on keyup[RETURN]:
3085 // * If the user presses [RETURN] as part of changing the value of a
3086 // selection, we should just let the value change and not let the
3087 // event broadcast further (e.g. to validating the current state of
3088 // the form in editable list view, which would lead to saving the
3089 // current row or switching to the next one)
3090 // * If the user presses [RETURN] with a select closed (side-effect:
3091 // also if the user opened the select and pressed [RETURN] without
3092 // changing the selected value), takes the action as validating the
3094 var ischanging = false;
3095 var $select = this.$el.find('select')
3096 .change(function () { ischanging = true; })
3097 .click(function () { ischanging = false; })
3098 .keyup(function (e) {
3099 if (e.which !== 13 || !ischanging) { return; }
3100 e.stopPropagation();
3103 this.setupFocus($select);
3105 commit_value: function () {
3106 this.store_dom_value();
3107 return this._super();
3109 store_dom_value: function () {
3110 if (!this.get('effective_readonly') && this.$('select').length) {
3111 var val = JSON.parse(this.$('select').val());
3112 this.internal_set_value(val);
3115 set_value: function(value_) {
3116 value_ = value_ === null ? false : value_;
3117 value_ = value_ instanceof Array ? value_[0] : value_;
3118 this._super(value_);
3120 render_value: function() {
3121 var values = this.get("values");
3122 values = [[false, this.node.attrs.placeholder || '']].concat(values);
3123 var found = _.find(values, function(el) { return el[0] === this.get("value"); }, this);
3125 found = [this.get("value"), _t('Unknown')];
3126 values = [found].concat(values);
3128 if (! this.get("effective_readonly")) {
3129 this.$().html(QWeb.render("FieldSelectionSelect", {widget: this, values: values}));
3130 this.$("select").val(JSON.stringify(found[0]));
3132 this.$el.text(found[1]);
3136 var input = this.$('select:first')[0];
3137 return input ? input.focus() : false;
3139 set_dimensions: function (height, width) {
3140 this._super(height, width);
3141 this.$('select').css({
3148 instance.web.form.FieldRadio = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3149 template: 'FieldRadio',
3151 'click input': 'click_change_value'
3153 init: function(field_manager, node) {
3154 /* Radio button widget: Attributes options:
3155 * - "horizontal" to display in column
3156 * - "no_radiolabel" don't display text values
3158 this._super(field_manager, node);
3159 this.selection = _.clone(this.field.selection) || [];
3160 this.domain = false;
3161 this.uniqueId = _.uniqueId("radio");
3163 initialize_content: function () {
3164 this.on("change:effective_readonly", this, this.render_value);
3165 this.field_manager.on("view_content_has_changed", this, this.get_selection);
3166 this.get_selection();
3168 click_change_value: function (event) {
3169 var val = $(event.target).val();
3170 val = this.field.type == "selection" ? val : +val;
3171 if (val == this.get_value()) {
3172 this.set_value(false);
3174 this.set_value(val);
3177 /** Get the selection and render it
3178 * selection: [[identifier, value_to_display], ...]
3179 * For selection fields: this is directly given by this.field.selection
3180 * For many2one fields: perform a search on the relation of the many2one field
3182 get_selection: function() {
3185 var def = $.Deferred();
3186 if (self.field.type == "many2one") {
3187 var domain = instance.web.pyeval.eval('domain', this.build_domain()) || [];
3188 if (! _.isEqual(self.domain, domain)) {
3189 self.domain = domain;
3190 var ds = new instance.web.DataSetStatic(self, self.field.relation, self.build_context());
3191 ds.call('search', [self.domain])
3192 .then(function (records) {
3193 ds.name_get(records).then(function (records) {
3194 selection = records;
3199 selection = self.selection;
3203 else if (self.field.type == "selection") {
3204 selection = self.field.selection || [];
3207 return def.then(function () {
3208 if (! _.isEqual(selection, self.selection)) {
3209 self.selection = _.clone(selection);
3210 self.renderElement();
3211 self.render_value();
3215 set_value: function (value_) {
3217 if (this.field.type == "selection") {
3218 value_ = _.find(this.field.selection, function (sel) { return sel[0] == value_;});
3220 else if (!this.selection.length) {
3221 this.selection = [value_];
3224 this._super(value_);
3226 get_value: function () {
3227 var value = this.get('value');
3228 return value instanceof Array ? value[0] : value;
3230 render_value: function () {
3232 this.$el.toggleClass("oe_readonly", this.get('effective_readonly'));
3233 this.$("input:checked").prop("checked", false);
3234 if (this.get_value()) {
3235 this.$("input").filter(function () {return this.value == self.get_value();}).prop("checked", true);
3236 this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : "");
3241 // jquery autocomplete tweak to allow html and classnames
3243 var proto = $.ui.autocomplete.prototype,
3244 initSource = proto._initSource;
3246 function filter( array, term ) {
3247 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
3248 return $.grep( array, function(value_) {
3249 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
3254 _initSource: function() {
3255 if ( this.options.html && $.isArray(this.options.source) ) {
3256 this.source = function( request, response ) {
3257 response( filter( this.options.source, request.term ) );
3260 initSource.call( this );
3264 _renderItem: function( ul, item) {
3265 return $( "<li></li>" )
3266 .data( "item.autocomplete", item )
3267 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
3269 .addClass(item.classname);
3275 A mixin containing some useful methods to handle completion inputs.
3277 The widget containing this option can have these arguments in its widget options:
3278 - no_quick_create: if true, it will disable the quick create
3280 instance.web.form.CompletionFieldMixin = {
3283 this.orderer = new instance.web.DropMisordered();
3286 * Call this method to search using a string.
3288 get_search_result: function(search_val) {
3291 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
3292 this.last_query = search_val;
3293 var exclusion_domain = [], ids_blacklist = this.get_search_blacklist();
3294 if (!_(ids_blacklist).isEmpty()) {
3295 exclusion_domain.push(['id', 'not in', ids_blacklist]);
3298 return this.orderer.add(dataset.name_search(
3299 search_val, new instance.web.CompoundDomain(self.build_domain(), exclusion_domain),
3300 'ilike', this.limit + 1, self.build_context())).then(function(data) {
3301 self.last_search = data;
3302 // possible selections for the m2o
3303 var values = _.map(data, function(x) {
3304 x[1] = x[1].split("\n")[0];
3306 label: _.str.escapeHTML(x[1]),
3313 // search more... if more results that max
3314 if (values.length > self.limit) {
3315 values = values.slice(0, self.limit);
3317 label: _t("Search More..."),
3318 action: function() {
3319 dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
3320 self._search_create_popup("search", data);
3323 classname: 'oe_m2o_dropdown_option'
3327 var raw_result = _(data.result).map(function(x) {return x[1];});
3328 if (search_val.length > 0 && !_.include(raw_result, search_val) &&
3329 ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3331 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
3332 $('<span />').text(search_val).html()),
3333 action: function() {
3334 self._quick_create(search_val);
3336 classname: 'oe_m2o_dropdown_option'
3340 if (!(self.options && (self.options.no_create || self.options.no_create_edit))){
3342 label: _t("Create and Edit..."),
3343 action: function() {
3344 self._search_create_popup("form", undefined, self._create_context(search_val));
3346 classname: 'oe_m2o_dropdown_option'
3349 else if (values.length == 0)
3351 label: _t("No results to show..."),
3352 action: function() {},
3353 classname: 'oe_m2o_dropdown_option'
3359 get_search_blacklist: function() {
3362 _quick_create: function(name) {
3364 var slow_create = function () {
3365 self._search_create_popup("form", undefined, self._create_context(name));
3367 if (self.options.quick_create === undefined || self.options.quick_create) {
3368 new instance.web.DataSet(this, this.field.relation, self.build_context())
3369 .name_create(name).done(function(data) {
3370 if (!self.get('effective_readonly'))
3371 self.add_id(data[0]);
3372 }).fail(function(error, event) {
3373 event.preventDefault();
3379 // all search/create popup handling
3380 _search_create_popup: function(view, ids, context) {
3382 var pop = new instance.web.form.SelectCreatePopup(this);
3384 self.field.relation,
3386 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3387 initial_ids: ids ? _.map(ids, function(x) {return x[0];}) : undefined,
3389 disable_multiple_selection: true
3391 self.build_domain(),
3392 new instance.web.CompoundContext(self.build_context(), context || {})
3394 pop.on("elements_selected", self, function(element_ids) {
3395 self.add_id(element_ids[0]);
3402 add_id: function(id) {},
3403 _create_context: function(name) {
3405 var field = (this.options || {}).create_name_field;
3406 if (field === undefined)
3408 if (field !== false && name && (this.options || {}).quick_create !== false)
3409 tmp["default_" + field] = name;
3414 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3415 template: "M2ODialog",
3416 init: function(parent) {
3417 this.name = parent.string;
3418 this._super(parent, {
3419 title: _.str.sprintf(_t("Create a %s"), parent.string),
3425 var text = _.str.sprintf(_t("You are creating a new %s, are you sure it does not exist yet?"), self.name);
3426 this.$("p").text( text );
3427 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3428 this.$("input").val(this.getParent().last_query);
3429 this.$buttons.find(".oe_form_m2o_qc_button").click(function(e){
3430 if (self.$("input").val() != ''){
3431 self.getParent()._quick_create(self.$("input").val());
3435 self.$("input").focus();
3438 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3439 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3442 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3448 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3449 template: "FieldMany2One",
3451 'keydown input': function (e) {
3453 case $.ui.keyCode.UP:
3454 case $.ui.keyCode.DOWN:
3455 e.stopPropagation();
3459 init: function(field_manager, node) {
3460 this._super(field_manager, node);
3461 instance.web.form.CompletionFieldMixin.init.call(this);
3462 this.set({'value': false});
3463 this.display_value = {};
3464 this.display_value_backup = {};
3465 this.last_search = [];
3466 this.floating = false;
3467 this.current_display = null;
3468 this.is_started = false;
3469 this.ignore_focusout = false;
3471 reinit_value: function(val) {
3472 this.internal_set_value(val);
3473 this.floating = false;
3474 if (this.is_started)
3475 this.render_value();
3477 initialize_field: function() {
3478 this.is_started = true;
3479 instance.web.bus.on('click', this, function() {
3480 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3481 this.$input.autocomplete("close");
3484 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3486 initialize_content: function() {
3487 if (!this.get("effective_readonly"))
3488 this.render_editable();
3490 destroy_content: function () {
3491 if (this.$drop_down) {
3492 this.$drop_down.off('click');
3493 delete this.$drop_down;
3496 this.$input.closest(".modal .modal-content").off('scroll');
3497 this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3498 'focus focusout change keydown');
3501 if (this.$follow_button) {
3502 this.$follow_button.off('blur focus click');
3503 delete this.$follow_button;
3506 destroy: function () {
3507 this.destroy_content();
3508 return this._super();
3510 init_error_displayer: function() {
3513 hide_error_displayer: function() {
3516 show_error_displayer: function() {
3517 new instance.web.form.M2ODialog(this).open();
3519 render_editable: function() {
3521 this.$input = this.$el.find("input");
3523 this.init_error_displayer();
3525 self.$input.on('focus', function() {
3526 self.hide_error_displayer();
3529 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3530 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3532 this.$follow_button.click(function(ev) {
3533 ev.preventDefault();
3534 if (!self.get('value')) {
3538 var pop = new instance.web.form.FormOpenPopup(self);
3539 var context = self.build_context().eval();
3540 var model_obj = new instance.web.Model(self.field.relation);
3541 model_obj.call('get_formview_id', [self.get("value"), context]).then(function(view_id){
3543 self.field.relation,
3545 self.build_context(),
3547 title: _t("Open: ") + self.string,
3551 pop.on('write_completed', self, function(){
3552 self.display_value = {};
3553 self.display_value_backup = {};
3554 self.render_value();
3556 self.trigger('changed_value');
3561 // some behavior for input
3562 var input_changed = function() {
3563 if (self.current_display !== self.$input.val()) {
3564 self.current_display = self.$input.val();
3565 if (self.$input.val() === "") {
3566 self.internal_set_value(false);
3567 self.floating = false;
3569 self.floating = true;
3573 this.$input.keydown(input_changed);
3574 this.$input.change(input_changed);
3575 this.$drop_down.click(function() {
3576 self.$input.focus();
3577 if (self.$input.autocomplete("widget").is(":visible")) {
3578 self.$input.autocomplete("close");
3580 if (self.get("value") && ! self.floating) {
3581 self.$input.autocomplete("search", "");
3583 self.$input.autocomplete("search");
3588 // Autocomplete close on dialog content scroll
3589 var close_autocomplete = _.debounce(function() {
3590 if (self.$input.autocomplete("widget").is(":visible")) {
3591 self.$input.autocomplete("close");
3594 this.$input.closest(".modal .modal-content").on('scroll', this, close_autocomplete);
3596 self.ed_def = $.Deferred();
3597 self.uned_def = $.Deferred();
3599 var ed_duration = 15000;
3600 var anyoneLoosesFocus = function (e) {
3601 if (self.ignore_focusout) { return; }
3603 if (self.floating) {
3604 if (self.last_search.length > 0) {
3605 if (self.last_search[0][0] != self.get("value")) {
3606 self.display_value = {};
3607 self.display_value_backup = {};
3608 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3609 self.reinit_value(self.last_search[0][0]);
3612 self.render_value();
3616 self.reinit_value(false);
3618 self.floating = false;
3620 if (used && self.get("value") === false && ! self.no_ed && ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3621 self.ed_def.reject();
3622 self.uned_def.reject();
3623 self.ed_def = $.Deferred();
3624 self.ed_def.done(function() {
3625 self.show_error_displayer();
3626 ignore_blur = false;
3627 self.trigger('focused');
3630 setTimeout(function() {
3631 self.ed_def.resolve();
3632 self.uned_def.reject();
3633 self.uned_def = $.Deferred();
3634 self.uned_def.done(function() {
3635 self.hide_error_displayer();
3637 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3641 self.ed_def.reject();
3644 var ignore_blur = false;
3646 focusout: anyoneLoosesFocus,
3647 focus: function () { self.trigger('focused'); },
3648 autocompleteopen: function () { ignore_blur = true; },
3649 autocompleteclose: function () { setTimeout(function() {ignore_blur = false;},0); },
3651 // autocomplete open
3652 if (ignore_blur) { $(this).focus(); return; }
3653 if (_(self.getChildren()).any(function (child) {
3654 return child instanceof instance.web.form.AbstractFormPopup;
3656 self.trigger('blurred');
3660 var isSelecting = false;
3662 this.$input.autocomplete({
3663 source: function(req, resp) {
3664 self.get_search_result(req.term).done(function(result) {
3668 select: function(event, ui) {
3672 self.display_value = {};
3673 self.display_value_backup = {};
3674 self.display_value["" + item.id] = item.name;
3675 self.reinit_value(item.id);
3676 } else if (item.action) {
3678 // Cancel widget blurring, to avoid form blur event
3679 self.trigger('focused');
3683 focus: function(e, ui) {
3687 // disabled to solve a bug, but may cause others
3688 //close: anyoneLoosesFocus,
3692 // set position for list of suggestions box
3693 this.$input.autocomplete( "option", "position", { my : "left top", at: "left bottom" } );
3694 this.$input.autocomplete("widget").openerpClass();
3695 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3696 this.$input.keyup(function(e) {
3697 if (e.which === 13) { // ENTER
3699 e.stopPropagation();
3701 isSelecting = false;
3703 this.setupFocus(this.$follow_button);
3705 render_value: function(no_recurse) {
3707 if (! this.get("value")) {
3708 this.display_string("");
3711 var display = this.display_value["" + this.get("value")];
3713 this.display_string(display);
3717 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3718 this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3720 self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
3723 self.display_value["" + self.get("value")] = data[0][1];
3724 self.render_value(true);
3725 }).fail( function (data, event) {
3726 // avoid displaying crash errors as many2One should be name_get compliant
3727 event.preventDefault();
3728 self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")];
3729 self.render_value(true);
3733 display_string: function(str) {
3735 if (!this.get("effective_readonly")) {
3736 this.$input.val(str.split("\n")[0]);
3737 this.current_display = this.$input.val();
3738 if (this.is_false()) {
3739 this.$('.oe_m2o_cm_button').css({'display':'none'});
3741 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3744 var lines = _.escape(str).split("\n");
3748 follow = _.rest(lines).join("<br />");
3751 var $link = this.$el.find('.oe_form_uri')
3754 if (! this.options.no_open)
3755 $link.click(function () {
3756 var context = self.build_context().eval();
3757 var model_obj = new instance.web.Model(self.field.relation);
3758 model_obj.call('get_formview_action', [self.get("value"), context]).then(function(action){
3759 self.do_action(action);
3763 $(".oe_form_m2o_follow", this.$el).html(follow);
3766 set_value: function(value_) {
3768 if (value_ instanceof Array) {
3769 this.display_value = {};
3770 this.display_value_backup = {};
3771 if (! this.options.always_reload) {
3772 this.display_value["" + value_[0]] = value_[1];
3775 this.display_value_backup["" + value_[0]] = value_[1];
3779 value_ = value_ || false;
3780 this.reinit_value(value_);
3782 get_displayed: function() {
3783 return this.display_value["" + this.get("value")];
3785 add_id: function(id) {
3786 this.display_value = {};
3787 this.display_value_backup = {};
3788 this.reinit_value(id);
3790 is_false: function() {
3791 return ! this.get("value");
3793 focus: function () {
3794 var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3795 return input ? input.focus() : false;
3797 _quick_create: function() {
3799 this.ed_def.reject();
3800 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3802 _search_create_popup: function() {
3804 this.ed_def.reject();
3805 this.ignore_focusout = true;
3806 this.reinit_value(false);
3807 var res = instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3808 this.ignore_focusout = false;
3812 set_dimensions: function (height, width) {
3813 this._super(height, width);
3814 if (!this.get("effective_readonly") && this.$input)
3815 this.$input.css('height', height);
3819 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3820 template: 'Many2OneButton',
3821 init: function(field_manager, node) {
3822 this._super.apply(this, arguments);
3825 this._super.apply(this, arguments);
3828 set_button: function() {
3831 this.$button.remove();
3834 this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3835 this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3836 this.$button.addClass('oe_link').css({'padding':'4px'});
3837 this.$el.append(this.$button);
3838 this.$button.on('click', self.on_click);
3840 on_click: function(ev) {
3842 this.popup = new instance.web.form.FormOpenPopup(this);
3843 this.popup.show_element(
3844 this.field.relation,
3846 this.build_context(),
3847 {title: this.string}
3849 this.popup.on('create_completed', self, function(r) {
3853 set_value: function(value_) {
3855 if (value_ instanceof Array) {
3858 value_ = value_ || false;
3859 this.set('value', value_);
3865 * Abstract-ish ListView.List subclass adding an "Add an item" row to replace
3866 * the big ugly button in the header.
3868 * Requires the implementation of a ``is_readonly`` method (usually a proxy to
3869 * the corresponding field's readonly or effective_readonly property) to
3870 * decide whether the special row should or should not be inserted.
3872 * Optionally an ``_add_row_class`` attribute can be set for the class(es) to
3873 * set on the insertion row.
3875 instance.web.form.AddAnItemList = instance.web.ListView.List.extend({
3876 pad_table_to: function (count) {
3877 if (!this.view.is_action_enabled('create') || this.is_readonly()) {
3882 this._super(count > 0 ? count - 1 : 0);
3885 var columns = _(this.columns).filter(function (column) {
3886 return column.invisible !== '1';
3888 if (this.options.selectable) { columns++; }
3889 if (this.options.deletable) { columns++; }
3891 var $cell = $('<td>', {
3893 'class': this._add_row_class || ''
3895 $('<a>', {href: '#'}).text(_t("Add an item"))
3896 .mousedown(function () {
3897 // FIXME: needs to be an official API somehow
3898 if (self.view.editor.is_editing()) {
3899 self.view.__ignore_blur = true;
3902 .click(function (e) {
3904 e.stopPropagation();
3905 // FIXME: there should also be an API for that one
3906 if (self.view.editor.form.__blur_timeout) {
3907 clearTimeout(self.view.editor.form.__blur_timeout);
3908 self.view.editor.form.__blur_timeout = false;
3910 self.view.ensure_saved().done(function () {
3911 self.view.do_add_record();
3915 var $padding = this.$current.find('tr:not([data-id]):first');
3916 var $newrow = $('<tr>').append($cell);
3917 if ($padding.length) {
3918 $padding.before($newrow);
3920 this.$current.append($newrow)
3926 # Values: (0, 0, { fields }) create
3927 # (1, ID, { fields }) update
3928 # (2, ID) remove (delete)
3929 # (3, ID) unlink one (target id or target of relation)
3931 # (5) unlink all (only valid for one2many)
3936 'create': function (values) {
3937 return [commands.CREATE, false, values];
3939 // (1, id, {values})
3941 'update': function (id, values) {
3942 return [commands.UPDATE, id, values];
3946 'delete': function (id) {
3947 return [commands.DELETE, id, false];
3949 // (3, id[, _]) removes relation, but not linked record itself
3951 'forget': function (id) {
3952 return [commands.FORGET, id, false];
3956 'link_to': function (id) {
3957 return [commands.LINK_TO, id, false];
3961 'delete_all': function () {
3962 return [5, false, false];
3964 // (6, _, ids) replaces all linked records with provided ids
3966 'replace_with': function (ids) {
3967 return [6, false, ids];
3970 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3971 multi_selection: false,
3972 disable_utility_classes: true,
3973 init: function(field_manager, node) {
3974 this._super(field_manager, node);
3975 lazy_build_o2m_kanban_view();
3976 this.is_loaded = $.Deferred();
3977 this.initial_is_loaded = this.is_loaded;
3978 this.form_last_update = $.Deferred();
3979 this.init_form_last_update = this.form_last_update;
3980 this.is_started = false;
3981 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
3982 this.dataset.o2m = this;
3983 this.dataset.parent_view = this.view;
3984 this.dataset.child_name = this.name;
3986 this.dataset.on('dataset_changed', this, function() {
3987 self.trigger_on_change();
3992 this._super.apply(this, arguments);
3993 this.$el.addClass('oe_form_field oe_form_field_one2many');
3998 this.is_loaded.done(function() {
3999 self.on("change:effective_readonly", self, function() {
4000 self.is_loaded = self.is_loaded.then(function() {
4001 self.viewmanager.destroy();
4002 return $.when(self.load_views()).done(function() {
4003 self.reload_current_view();
4008 this.is_started = true;
4009 this.reload_current_view();
4011 trigger_on_change: function() {
4012 this.trigger('changed_value');
4014 load_views: function() {
4017 var modes = this.node.attrs.mode;
4018 modes = !!modes ? modes.split(",") : ["tree"];
4020 _.each(modes, function(mode) {
4021 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
4022 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
4026 view_type: mode == "tree" ? "list" : mode,
4029 if (self.field.views && self.field.views[mode]) {
4030 view.embedded_view = self.field.views[mode];
4032 if(view.view_type === "list") {
4033 _.extend(view.options, {
4035 selectable: self.multi_selection,
4037 import_enabled: false,
4040 if (self.get("effective_readonly")) {
4041 _.extend(view.options, {
4046 } else if (view.view_type === "form") {
4047 if (self.get("effective_readonly")) {
4048 view.view_type = 'form';
4050 _.extend(view.options, {
4051 not_interactible_on_create: true,
4053 } else if (view.view_type === "kanban") {
4054 _.extend(view.options, {
4055 confirm_on_delete: false,
4057 if (self.get("effective_readonly")) {
4058 _.extend(view.options, {
4059 action_buttons: false,
4060 quick_creatable: false,
4062 read_only_mode: true,
4070 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
4071 this.viewmanager.o2m = self;
4072 var once = $.Deferred().done(function() {
4073 self.init_form_last_update.resolve();
4075 var def = $.Deferred().done(function() {
4076 self.initial_is_loaded.resolve();
4078 this.viewmanager.on("controller_inited", self, function(view_type, controller) {
4079 controller.o2m = self;
4080 if (view_type == "list") {
4081 if (self.get("effective_readonly")) {
4082 controller.on('edit:before', self, function (e) {
4085 _(controller.columns).find(function (column) {
4086 if (!(column instanceof instance.web.list.Handle)) {
4089 column.modifiers.invisible = true;
4093 } else if (view_type === "form") {
4094 if (self.get("effective_readonly")) {
4095 $(".oe_form_buttons", controller.$el).children().remove();
4097 controller.on("load_record", self, function(){
4100 controller.on('pager_action_executed',self,self.save_any_view);
4101 } else if (view_type == "graph") {
4102 self.reload_current_view();
4106 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
4107 $.when(self.save_any_view()).done(function() {
4108 if (n_mode === "list") {
4109 $.async_when().done(function() {
4110 self.reload_current_view();
4115 $.async_when().done(function () {
4116 self.viewmanager.appendTo(self.$el);
4120 reload_current_view: function() {
4122 self.is_loaded = self.is_loaded.then(function() {
4123 var view = self.get_active_view();
4124 if (view.type === "list") {
4125 return view.controller.reload_content();
4126 } else if (view.type === "form") {
4127 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
4128 self.dataset.index = 0;
4130 var act = function() {
4131 return view.controller.do_show();
4133 self.form_last_update = self.form_last_update.then(act, act);
4134 return self.form_last_update;
4135 } else if (view.controller.do_search) {
4136 return view.controller.do_search(self.build_domain(), self.dataset.get_context(), []);
4139 return self.is_loaded;
4141 get_active_view: function () {
4143 * Returns the current active view if any.
4145 return (this.viewmanager && this.viewmanager.active_view);
4147 set_value: function(value_) {
4148 value_ = value_ || [];
4150 var view = this.get_active_view();
4151 this.dataset.reset_ids([]);
4153 if(value_.length >= 1 && value_[0] instanceof Array) {
4155 _.each(value_, function(command) {
4156 var obj = {values: command[2]};
4157 switch (command[0]) {
4158 case commands.CREATE:
4159 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4161 self.dataset.to_create.push(obj);
4162 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4165 case commands.UPDATE:
4166 obj['id'] = command[1];
4167 self.dataset.to_write.push(obj);
4168 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4171 case commands.DELETE:
4172 self.dataset.to_delete.push({id: command[1]});
4174 case commands.LINK_TO:
4175 ids.push(command[1]);
4177 case commands.DELETE_ALL:
4178 self.dataset.delete_all = true;
4183 this.dataset.set_ids(ids);
4184 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
4186 this.dataset.delete_all = true;
4187 _.each(value_, function(command) {
4188 var obj = {values: command};
4189 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4191 self.dataset.to_create.push(obj);
4192 self.dataset.cache.push(_.clone(obj));
4196 this.dataset.set_ids(ids);
4198 this._super(value_);
4199 this.dataset.reset_ids(value_);
4201 if (this.dataset.index === null && this.dataset.ids.length > 0) {
4202 this.dataset.index = 0;
4204 this.trigger_on_change();
4205 if (this.is_started) {
4206 return self.reload_current_view();
4211 get_value: function() {
4215 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
4216 val = val.concat(_.map(this.dataset.ids, function(id) {
4217 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
4219 return commands.create(alter_order.values);
4221 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
4223 return commands.update(alter_order.id, alter_order.values);
4225 return commands.link_to(id);
4227 return val.concat(_.map(
4228 this.dataset.to_delete, function(x) {
4229 return commands['delete'](x.id);}));
4231 commit_value: function() {
4232 return this.save_any_view();
4234 save_any_view: function() {
4235 var view = this.get_active_view();
4237 if (this.viewmanager.active_view.type === "form") {
4238 if (view.controller.is_initialized.state() !== 'resolved') {
4239 return $.when(false);
4241 return $.when(view.controller.save());
4242 } else if (this.viewmanager.active_view.type === "list") {
4243 return $.when(view.controller.ensure_saved());
4246 return $.when(false);
4248 is_syntax_valid: function() {
4249 var view = this.get_active_view();
4253 switch (this.viewmanager.active_view.type) {
4255 return _(view.controller.fields).chain()
4260 return view.controller.is_valid();
4266 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
4267 template: 'One2Many.viewmanager',
4268 init: function(parent, dataset, views, flags) {
4269 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
4270 this.registry = instance.web.views.extend({
4271 list: 'instance.web.form.One2ManyListView',
4272 form: 'instance.web.form.One2ManyFormView',
4273 kanban: 'instance.web.form.One2ManyKanbanView',
4275 this.__ignore_blur = false;
4277 switch_mode: function(mode, unused) {
4278 if (mode !== 'form') {
4279 return this._super(mode, unused);
4282 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
4283 var pop = new instance.web.form.FormOpenPopup(this);
4284 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4285 title: _t("Open: ") + self.o2m.string,
4286 create_function: function(data, options) {
4287 return self.o2m.dataset.create(data, options).done(function(r) {
4288 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4289 self.o2m.dataset.trigger("dataset_changed", r);
4292 write_function: function(id, data, options) {
4293 return self.o2m.dataset.write(id, data, {}).done(function() {
4294 self.o2m.reload_current_view();
4297 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4298 parent_view: self.o2m.view,
4299 child_name: self.o2m.name,
4300 read_function: function() {
4301 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4303 form_view_options: {'not_interactible_on_create':true},
4304 readonly: self.o2m.get("effective_readonly")
4306 pop.on("elements_selected", self, function() {
4307 self.o2m.reload_current_view();
4312 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
4313 get_context: function() {
4314 this.context = this.o2m.build_context();
4315 return this.context;
4319 instance.web.form.One2ManyListView = instance.web.ListView.extend({
4320 _template: 'One2Many.listview',
4321 init: function (parent, dataset, view_id, options) {
4322 this._super(parent, dataset, view_id, _.extend(options || {}, {
4323 GroupsType: instance.web.form.One2ManyGroups,
4324 ListType: instance.web.form.One2ManyList
4326 this.on('edit:after', this, this.proxy('_after_edit'));
4327 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
4330 .bind('add', this.proxy("changed_records"))
4331 .bind('edit', this.proxy("changed_records"))
4332 .bind('remove', this.proxy("changed_records"));
4334 start: function () {
4335 var ret = this._super();
4337 .off('mousedown.handleButtons')
4338 .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
4341 changed_records: function () {
4342 this.o2m.trigger_on_change();
4344 is_valid: function () {
4345 var editor = this.editor;
4346 var form = editor.form;
4347 // If no edition is pending, the listview can not be invalid (?)
4348 if (!editor.record) {
4351 // If the form has not been modified, the view can only be valid
4352 // NB: is_dirty will also be set on defaults/onchanges/whatever?
4353 // oe_form_dirty seems to only be set on actual user actions
4354 if (!form.$el.is('.oe_form_dirty')) {
4357 this.o2m._dirty_flag = true;
4359 // Otherwise validate internal form
4360 return _(form.fields).chain()
4361 .invoke(function () {
4362 this._check_css_flags();
4363 return this.is_valid();
4368 do_add_record: function () {
4369 if (this.editable()) {
4370 this._super.apply(this, arguments);
4373 var pop = new instance.web.form.SelectCreatePopup(this);
4375 self.o2m.field.relation,
4377 title: _t("Create: ") + self.o2m.string,
4378 initial_view: "form",
4379 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4380 create_function: function(data, options) {
4381 return self.o2m.dataset.create(data, options).done(function(r) {
4382 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4383 self.o2m.dataset.trigger("dataset_changed", r);
4386 read_function: function() {
4387 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4389 parent_view: self.o2m.view,
4390 child_name: self.o2m.name,
4391 form_view_options: {'not_interactible_on_create':true}
4393 self.o2m.build_domain(),
4394 self.o2m.build_context()
4396 pop.on("elements_selected", self, function() {
4397 self.o2m.reload_current_view();
4401 do_activate_record: function(index, id) {
4403 var pop = new instance.web.form.FormOpenPopup(self);
4404 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4405 title: _t("Open: ") + self.o2m.string,
4406 write_function: function(id, data) {
4407 return self.o2m.dataset.write(id, data, {}).done(function() {
4408 self.o2m.reload_current_view();
4411 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4412 parent_view: self.o2m.view,
4413 child_name: self.o2m.name,
4414 read_function: function() {
4415 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4417 form_view_options: {'not_interactible_on_create':true},
4418 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
4421 do_button_action: function (name, id, callback) {
4422 if (!_.isNumber(id)) {
4423 instance.webclient.notification.warn(
4424 _t("Action Button"),
4425 _t("The o2m record must be saved before an action can be used"));
4428 var parent_form = this.o2m.view;
4430 this.ensure_saved().then(function () {
4432 return parent_form.save();
4435 }).done(function () {
4436 var ds = self.o2m.dataset;
4437 var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
4438 return value.length;
4440 if (!self.o2m.options.reload_on_button && !cached_records) {
4441 self.handle_button(name, id, callback);
4443 self.handle_button(name, id, function(){
4444 self.o2m.view.reload();
4450 _after_edit: function () {
4451 this.__ignore_blur = false;
4452 this.editor.form.on('blurred', this, this._on_form_blur);
4454 // The form's blur thing may be jiggered during the edition setup,
4455 // potentially leading to the o2m instasaving the row. Cancel any
4456 // blurring triggered the edition startup here
4457 this.editor.form.widgetFocused();
4459 _before_unedit: function () {
4460 this.editor.form.off('blurred', this, this._on_form_blur);
4462 _button_down: function () {
4463 // If a button is clicked (usually some sort of action button), it's
4464 // the button's responsibility to ensure the editable list is in the
4465 // correct state -> ignore form blurring
4466 this.__ignore_blur = true;
4469 * Handles blurring of the nested form (saves the currently edited row),
4470 * unless the flag to ignore the event is set to ``true``
4472 * Makes the internal form go away
4474 _on_form_blur: function () {
4475 if (this.__ignore_blur) {
4476 this.__ignore_blur = false;
4479 // FIXME: why isn't there an API for this?
4480 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4481 this.ensure_saved();
4484 this.cancel_edition();
4486 keypress_ENTER: function () {
4487 // blurring caused by hitting the [Return] key, should skip the
4488 // autosave-on-blur and let the handler for [Return] do its thing (save
4489 // the current row *anyway*, then create a new one/edit the next one)
4490 this.__ignore_blur = true;
4491 this._super.apply(this, arguments);
4493 do_delete: function (ids) {
4494 var confirm = window.confirm;
4495 window.confirm = function () { return true; };
4497 return this._super(ids);
4499 window.confirm = confirm;
4502 reload_record: function (record, options) {
4503 if (!options || !options['do_not_evict']) {
4504 // Evict record.id from cache to ensure it will be reloaded correctly
4505 this.dataset.evict_record(record.get('id'));
4508 return this._super(record);
4511 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4512 setup_resequence_rows: function () {
4513 if (!this.view.o2m.get('effective_readonly')) {
4514 this._super.apply(this, arguments);
4518 instance.web.form.One2ManyList = instance.web.form.AddAnItemList.extend({
4519 _add_row_class: 'oe_form_field_one2many_list_row_add',
4520 is_readonly: function () {
4521 return this.view.o2m.get('effective_readonly');
4525 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4526 form_template: 'One2Many.formview',
4527 load_form: function(data) {
4530 this.$buttons.find('button.oe_form_button_create').click(function() {
4531 self.save().done(self.on_button_new);
4534 do_notify_change: function() {
4535 if (this.dataset.parent_view) {
4536 this.dataset.parent_view.do_notify_change();
4538 this._super.apply(this, arguments);
4543 var lazy_build_o2m_kanban_view = function() {
4544 if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
4546 instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4550 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4551 template: "FieldMany2ManyTags",
4552 tag_template: "FieldMany2ManyTag",
4554 this._super.apply(this, arguments);
4555 instance.web.form.CompletionFieldMixin.init.call(this);
4556 this.set({"value": []});
4557 this._display_orderer = new instance.web.DropMisordered();
4558 this._drop_shown = false;
4560 initialize_texttext: function(){
4563 plugins : 'tags arrow autocomplete',
4565 render: function(suggestion) {
4566 return $('<span class="text-label"/>').
4567 data('index', suggestion['index']).html(suggestion['label']);
4572 selectFromDropdown: function() {
4573 this.trigger('hideDropdown');
4574 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4575 var data = self.search_result[index];
4577 self.add_id(data.id);
4579 self.ignore_blur = true;
4582 this.trigger('setSuggestions', {result : []});
4586 isTagAllowed: function(tag) {
4590 removeTag: function(tag) {
4591 var id = tag.data("id");
4592 self.set({"value": _.without(self.get("value"), id)});
4594 renderTag: function(stuff) {
4595 return $.fn.textext.TextExtTags.prototype.renderTag.
4596 call(this, stuff).data("id", stuff.id);
4600 itemToString: function(item) {
4605 onSetInputData: function(e, data) {
4607 this._plugins.autocomplete._suggestions = null;
4609 this.input().val(data);
4615 initialize_content: function() {
4616 if (this.get("effective_readonly"))
4619 self.ignore_blur = false;
4620 self.$text = this.$("textarea");
4621 self.$text.textext(self.initialize_texttext()).bind('getSuggestions', function(e, data) {
4623 var str = !!data ? data.query || '' : '';
4624 self.get_search_result(str).done(function(result) {
4625 self.search_result = result;
4626 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4627 return _.extend(el, {index:i});
4630 }).bind('hideDropdown', function() {
4631 self._drop_shown = false;
4632 }).bind('showDropdown', function() {
4633 self._drop_shown = true;
4635 self.tags = self.$text.textext()[0].tags();
4637 .focusin(function () {
4638 self.trigger('focused');
4639 self.ignore_blur = false;
4641 .focusout(function() {
4642 self.$text.trigger("setInputData", "");
4643 if (!self.ignore_blur) {
4644 self.trigger('blurred');
4646 }).keydown(function(e) {
4647 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4648 self.$text.textext()[0].autocomplete().selectFromDropdown();
4652 // WARNING: duplicated in 4 other M2M widgets
4653 set_value: function(value_) {
4654 value_ = value_ || [];
4655 if (value_.length >= 1 && value_[0] instanceof Array) {
4656 // value_ is a list of m2m commands. We only process
4657 // LINK_TO and REPLACE_WITH in this context
4659 _.each(value_, function (command) {
4660 if (command[0] === commands.LINK_TO) {
4661 val.push(command[1]); // (4, id[, _])
4662 } else if (command[0] === commands.REPLACE_WITH) {
4663 val = command[2]; // (6, _, ids)
4668 this._super(value_);
4670 is_false: function() {
4671 return _(this.get("value")).isEmpty();
4673 get_value: function() {
4674 var tmp = [commands.replace_with(this.get("value"))];
4677 get_search_blacklist: function() {
4678 return this.get("value");
4680 map_tag: function(data){
4681 return _.map(data, function(el) {return {name: el[1], id:el[0]};})
4683 get_render_data: function(ids){
4685 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4686 return dataset.name_get(ids);
4688 render_tag: function(data) {
4690 if (! self.get("effective_readonly")) {
4691 self.tags.containerElement().children().remove();
4692 self.$('textarea').css("padding-left", "3px");
4693 self.tags.addTags(self.map_tag(data));
4695 self.$el.html(QWeb.render(self.tag_template, {elements: data}));
4698 render_value: function() {
4700 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4701 var values = self.get("value");
4702 var handle_names = function(data) {
4703 if (self.isDestroyed())
4706 _.each(data, function(el) {
4707 indexed[el[0]] = el;
4709 data = _.map(values, function(el) { return indexed[el]; });
4710 self.render_tag(data);
4712 if (! values || values.length > 0) {
4713 return this._display_orderer.add(self.get_render_data(values)).done(handle_names);
4718 add_id: function(id) {
4719 this.set({'value': _.uniq(this.get('value').concat([id]))});
4721 focus: function () {
4722 var input = this.$text && this.$text[0];
4723 return input ? input.focus() : false;
4725 set_dimensions: function (height, width) {
4726 this._super(height, width);
4727 this.$("textarea").css({
4732 _search_create_popup: function() {
4733 self.ignore_blur = true;
4734 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
4740 - reload_on_button: Reload the whole form view if click on a button in a list view.
4741 If you see this options, do not use it, it's basically a dirty hack to make one
4742 precise o2m to behave the way we want.
4744 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4745 multi_selection: false,
4746 disable_utility_classes: true,
4747 init: function(field_manager, node) {
4748 this._super(field_manager, node);
4749 this.is_loaded = $.Deferred();
4750 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4751 this.dataset.m2m = this;
4753 this.dataset.on('unlink', self, function(ids) {
4754 self.dataset_changed();
4757 this.list_dm = new instance.web.DropMisordered();
4758 this.render_value_dm = new instance.web.DropMisordered();
4760 initialize_content: function() {
4763 this.$el.addClass('oe_form_field oe_form_field_many2many');
4765 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4767 'deletable': this.get("effective_readonly") ? false : true,
4768 'selectable': this.multi_selection,
4770 'reorderable': false,
4771 'import_enabled': false,
4773 var embedded = (this.field.views || {}).tree;
4775 this.list_view.set_embedded_view(embedded);
4777 this.list_view.m2m_field = this;
4778 var loaded = $.Deferred();
4779 this.list_view.on("list_view_loaded", this, function() {
4782 this.list_view.appendTo(this.$el);
4784 var old_def = self.is_loaded;
4785 self.is_loaded = $.Deferred().done(function() {
4788 this.list_dm.add(loaded).then(function() {
4789 self.is_loaded.resolve();
4792 destroy_content: function() {
4793 this.list_view.destroy();
4794 this.list_view = undefined;
4796 // WARNING: duplicated in 4 other M2M widgets
4797 set_value: function(value_) {
4798 value_ = value_ || [];
4799 if (value_.length >= 1 && value_[0] instanceof Array) {
4800 // value_ is a list of m2m commands. We only process
4801 // LINK_TO and REPLACE_WITH in this context
4803 _.each(value_, function (command) {
4804 if (command[0] === commands.LINK_TO) {
4805 val.push(command[1]); // (4, id[, _])
4806 } else if (command[0] === commands.REPLACE_WITH) {
4807 val = command[2]; // (6, _, ids)
4812 this._super(value_);
4814 get_value: function() {
4815 return [commands.replace_with(this.get('value'))];
4817 is_false: function () {
4818 return _(this.get("value")).isEmpty();
4820 render_value: function() {
4822 this.dataset.set_ids(this.get("value"));
4823 this.render_value_dm.add(this.is_loaded).then(function() {
4824 return self.list_view.reload_content();
4827 dataset_changed: function() {
4828 this.internal_set_value(this.dataset.ids);
4832 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4833 get_context: function() {
4834 this.context = this.m2m.build_context();
4835 return this.context;
4841 * @extends instance.web.ListView
4843 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4844 init: function (parent, dataset, view_id, options) {
4845 this._super(parent, dataset, view_id, _.extend(options || {}, {
4846 ListType: instance.web.form.Many2ManyList,
4849 do_add_record: function () {
4850 var pop = new instance.web.form.SelectCreatePopup(this);
4854 title: _t("Add: ") + this.m2m_field.string,
4855 no_create: this.m2m_field.options.no_create,
4857 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4858 this.m2m_field.build_context()
4861 pop.on("elements_selected", self, function(element_ids) {
4863 _(element_ids).each(function (id) {
4864 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4865 self.dataset.set_ids(self.dataset.ids.concat([id]));
4866 self.m2m_field.dataset_changed();
4871 self.reload_content();
4875 do_activate_record: function(index, id) {
4877 var pop = new instance.web.form.FormOpenPopup(this);
4878 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4879 title: _t("Open: ") + this.m2m_field.string,
4880 readonly: this.getParent().get("effective_readonly")
4882 pop.on('write_completed', self, self.reload_content);
4884 do_button_action: function(name, id, callback) {
4886 var _sup = _.bind(this._super, this);
4887 if (! this.m2m_field.options.reload_on_button) {
4888 return _sup(name, id, callback);
4890 return this.m2m_field.view.save().then(function() {
4891 return _sup(name, id, function() {
4892 self.m2m_field.view.reload();
4897 is_action_enabled: function () { return true; },
4899 instance.web.form.Many2ManyList = instance.web.form.AddAnItemList.extend({
4900 _add_row_class: 'oe_form_field_many2many_list_row_add',
4901 is_readonly: function () {
4902 return this.view.m2m_field.get('effective_readonly');
4906 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4907 disable_utility_classes: true,
4908 init: function(field_manager, node) {
4909 this._super(field_manager, node);
4910 instance.web.form.CompletionFieldMixin.init.call(this);
4911 m2m_kanban_lazy_init();
4912 this.is_loaded = $.Deferred();
4913 this.initial_is_loaded = this.is_loaded;
4916 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4917 this.dataset.m2m = this;
4918 this.dataset.on('unlink', self, function(ids) {
4919 self.dataset_changed();
4923 this._super.apply(this, arguments);
4928 self.on("change:effective_readonly", self, function() {
4929 self.is_loaded = self.is_loaded.then(function() {
4930 self.kanban_view.destroy();
4931 return $.when(self.load_view()).done(function() {
4932 self.render_value();
4937 // WARNING: duplicated in 4 other M2M widgets
4938 set_value: function(value_) {
4939 value_ = value_ || [];
4940 if (value_.length >= 1 && value_[0] instanceof Array) {
4941 // value_ is a list of m2m commands. We only process
4942 // LINK_TO and REPLACE_WITH in this context
4944 _.each(value_, function (command) {
4945 if (command[0] === commands.LINK_TO) {
4946 val.push(command[1]); // (4, id[, _])
4947 } else if (command[0] === commands.REPLACE_WITH) {
4948 val = command[2]; // (6, _, ids)
4953 this._super(value_);
4955 get_value: function() {
4956 return [commands.replace_with(this.get('value'))];
4958 load_view: function() {
4960 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4961 'create_text': _t("Add"),
4962 'creatable': self.get("effective_readonly") ? false : true,
4963 'quick_creatable': self.get("effective_readonly") ? false : true,
4964 'read_only_mode': self.get("effective_readonly") ? true : false,
4965 'confirm_on_delete': false,
4967 var embedded = (this.field.views || {}).kanban;
4969 this.kanban_view.set_embedded_view(embedded);
4971 this.kanban_view.m2m = this;
4972 var loaded = $.Deferred();
4973 this.kanban_view.on("kanban_view_loaded",self,function() {
4974 self.initial_is_loaded.resolve();
4977 this.kanban_view.on('switch_mode', this, this.open_popup);
4978 $.async_when().done(function () {
4979 self.kanban_view.appendTo(self.$el);
4983 render_value: function() {
4985 this.dataset.set_ids(this.get("value"));
4986 this.is_loaded = this.is_loaded.then(function() {
4987 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
4990 dataset_changed: function() {
4991 this.set({'value': this.dataset.ids});
4993 open_popup: function(type, unused) {
4994 if (type !== "form")
4998 if (this.dataset.index === null) {
4999 pop = new instance.web.form.SelectCreatePopup(this);
5001 this.field.relation,
5003 title: _t("Add: ") + this.string
5005 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
5006 this.build_context()
5008 pop.on("elements_selected", self, function(element_ids) {
5009 _.each(element_ids, function(one_id) {
5010 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
5011 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
5012 self.dataset_changed();
5013 self.render_value();
5018 var id = self.dataset.ids[self.dataset.index];
5019 pop = new instance.web.form.FormOpenPopup(this);
5020 pop.show_element(self.field.relation, id, self.build_context(), {
5021 title: _t("Open: ") + self.string,
5022 write_function: function(id, data, options) {
5023 return self.dataset.write(id, data, {}).done(function() {
5024 self.render_value();
5027 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
5028 parent_view: self.view,
5029 child_name: self.name,
5030 readonly: self.get("effective_readonly")
5034 add_id: function(id) {
5035 this.quick_create.add_id(id);
5039 function m2m_kanban_lazy_init() {
5040 if (instance.web.form.Many2ManyKanbanView)
5042 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
5043 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
5044 _is_quick_create_enabled: function() {
5045 return this._super() && ! this.group_by;
5048 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
5049 template: 'Many2ManyKanban.quick_create',
5052 * close_btn: If true, the widget will display a "Close" button able to trigger
5055 init: function(parent, dataset, context, buttons) {
5056 this._super(parent);
5057 this.m2m = this.getParent().view.m2m;
5058 this.m2m.quick_create = this;
5059 this._dataset = dataset;
5060 this._buttons = buttons || false;
5061 this._context = context || {};
5063 start: function () {
5065 self.$text = this.$el.find('input').css("width", "200px");
5066 self.$text.textext({
5067 plugins : 'arrow autocomplete',
5069 render: function(suggestion) {
5070 return $('<span class="text-label"/>').
5071 data('index', suggestion['index']).html(suggestion['label']);
5076 selectFromDropdown: function() {
5077 $(this).trigger('hideDropdown');
5078 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
5079 var data = self.search_result[index];
5081 self.add_id(data.id);
5088 itemToString: function(item) {
5093 }).bind('getSuggestions', function(e, data) {
5095 var str = !!data ? data.query || '' : '';
5096 self.m2m.get_search_result(str).done(function(result) {
5097 self.search_result = result;
5098 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
5099 return _.extend(el, {index:i});
5103 self.$text.focusout(function() {
5108 this.$text[0].focus();
5110 add_id: function(id) {
5113 self.trigger('added', id);
5114 this.m2m.dataset_changed();
5120 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
5122 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
5123 template: "AbstractFormPopup.render",
5126 * -readonly: only applicable when not in creation mode, default to false
5127 * - alternative_form_view
5134 * - form_view_options
5136 init_popup: function(model, row_id, domain, context, options) {
5137 this.row_id = row_id;
5139 this.domain = domain || [];
5140 this.context = context || {};
5141 this.options = options;
5142 _.defaults(this.options, {
5145 init_dataset: function() {
5147 this.created_elements = [];
5148 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
5149 this.dataset.read_function = this.options.read_function;
5150 this.dataset.create_function = function(data, options, sup) {
5151 var fct = self.options.create_function || sup;
5152 return fct.call(this, data, options).done(function(r) {
5153 self.trigger('create_completed saved', r);
5154 self.created_elements.push(r);
5157 this.dataset.write_function = function(id, data, options, sup) {
5158 var fct = self.options.write_function || sup;
5159 return fct.call(this, id, data, options).done(function(r) {
5160 self.trigger('write_completed saved', r);
5163 this.dataset.parent_view = this.options.parent_view;
5164 this.dataset.child_name = this.options.child_name;
5166 display_popup: function() {
5168 this.renderElement();
5169 var dialog = new instance.web.Dialog(this, {
5170 dialogClass: 'oe_act_window',
5171 title: this.options.title || "",
5172 }, this.$el).open();
5173 dialog.on('closing', this, function (e){
5174 self.check_exit(true);
5176 this.$buttonpane = dialog.$buttons;
5179 setup_form_view: function() {
5182 this.dataset.ids = [this.row_id];
5183 this.dataset.index = 0;
5185 this.dataset.index = null;
5187 var options = _.clone(self.options.form_view_options) || {};
5188 if (this.row_id !== null) {
5189 options.initial_mode = this.options.readonly ? "view" : "edit";
5192 $buttons: this.$buttonpane,
5194 this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
5195 if (this.options.alternative_form_view) {
5196 this.view_form.set_embedded_view(this.options.alternative_form_view);
5198 this.view_form.appendTo(this.$el.find(".oe_popup_form"));
5199 this.view_form.on("form_view_loaded", self, function() {
5200 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
5201 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
5202 multi_select: multi_select,
5203 readonly: self.row_id !== null && self.options.readonly,
5205 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
5206 $snbutton.click(function() {
5207 $.when(self.view_form.save()).done(function() {
5208 self.view_form.reload_mutex.exec(function() {
5209 self.view_form.on_button_new();
5213 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
5214 $sbutton.click(function() {
5215 $.when(self.view_form.save()).done(function() {
5216 self.view_form.reload_mutex.exec(function() {
5221 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
5222 $cbutton.click(function() {
5223 self.view_form.trigger('on_button_cancel');
5226 self.view_form.do_show();
5229 select_elements: function(element_ids) {
5230 this.trigger("elements_selected", element_ids);
5232 check_exit: function(no_destroy) {
5233 if (this.created_elements.length > 0) {
5234 this.select_elements(this.created_elements);
5235 this.created_elements = [];
5237 this.trigger('closed');
5240 destroy: function () {
5241 this.trigger('closed');
5242 if (this.$el.is(":data(bs.modal)")) {
5243 this.$el.parents('.modal').modal('hide');
5250 * Class to display a popup containing a form view.
5252 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
5253 show_element: function(model, row_id, context, options) {
5254 this.init_popup(model, row_id, [], context, options);
5255 _.defaults(this.options, {
5257 this.display_popup();
5261 this.init_dataset();
5262 this.setup_form_view();
5267 * Class to display a popup to display a list to search a row. It also allows
5268 * to switch to a form view to create a new row.
5270 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
5274 * - initial_view: form or search (default search)
5275 * - disable_multiple_selection
5276 * - list_view_options
5278 select_element: function(model, options, domain, context) {
5279 this.init_popup(model, null, domain, context, options);
5281 _.defaults(this.options, {
5282 initial_view: "search",
5284 this.initial_ids = this.options.initial_ids;
5285 this.display_popup();
5289 this.init_dataset();
5290 if (this.options.initial_view == "search") {
5291 instance.web.pyeval.eval_domains_and_contexts({
5293 contexts: [this.context]
5294 }).done(function (results) {
5295 var search_defaults = {};
5296 _.each(results.context, function (value_, key) {
5297 var match = /^search_default_(.*)$/.exec(key);
5299 search_defaults[match[1]] = value_;
5302 self.setup_search_view(search_defaults);
5308 setup_search_view: function(search_defaults) {
5310 if (this.searchview) {
5311 this.searchview.destroy();
5313 if (this.searchview_drawer) {
5314 this.searchview_drawer.destroy();
5316 this.searchview = new instance.web.SearchView(this,
5317 this.dataset, false, search_defaults);
5318 this.searchview_drawer = new instance.web.SearchViewDrawer(this, this.searchview);
5319 this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
5320 if (self.initial_ids) {
5321 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
5322 contexts.concat(self.context), groupbys);
5323 self.initial_ids = undefined;
5325 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
5328 this.searchview.on("search_view_loaded", self, function() {
5329 self.view_list = new instance.web.form.SelectCreateListView(self,
5330 self.dataset, false,
5331 _.extend({'deletable': false,
5332 'selectable': !self.options.disable_multiple_selection,
5333 'import_enabled': false,
5334 '$buttons': self.$buttonpane,
5335 'disable_editable_mode': true,
5336 '$pager': self.$('.oe_popup_list_pager'),
5337 }, self.options.list_view_options || {}));
5338 self.view_list.on('edit:before', self, function (e) {
5341 self.view_list.popup = self;
5342 self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
5343 self.view_list.do_show();
5344 }).then(function() {
5345 self.searchview.do_search();
5347 self.view_list.on("list_view_loaded", self, function() {
5348 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
5349 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
5350 $cbutton.click(function() {
5353 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
5354 $sbutton.click(function() {
5355 self.select_elements(self.selected_ids);
5358 $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
5359 $cbutton.click(function() {
5364 this.searchview.appendTo(this.$(".oe_popup_search"));
5366 do_search: function(domains, contexts, groupbys) {
5368 instance.web.pyeval.eval_domains_and_contexts({
5369 domains: domains || [],
5370 contexts: contexts || [],
5371 group_by_seq: groupbys || []
5372 }).done(function (results) {
5373 self.view_list.do_search(results.domain, results.context, results.group_by);
5376 on_click_element: function(ids) {
5378 this.selected_ids = ids || [];
5379 if(this.selected_ids.length > 0) {
5380 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
5382 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
5385 new_object: function() {
5386 if (this.searchview) {
5387 this.searchview.hide();
5389 if (this.view_list) {
5390 this.view_list.do_hide();
5392 this.setup_form_view();
5396 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
5397 do_add_record: function () {
5398 this.popup.new_object();
5400 select_record: function(index) {
5401 this.popup.select_elements([this.dataset.ids[index]]);
5402 this.popup.destroy();
5404 do_select: function(ids, records) {
5405 this._super(ids, records);
5406 this.popup.on_click_element(ids);
5410 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5411 template: 'FieldReference',
5412 init: function(field_manager, node) {
5413 this._super(field_manager, node);
5414 this.reference_ready = true;
5416 destroy_content: function() {
5419 this.fm = undefined;
5422 initialize_content: function() {
5424 var fm = new instance.web.form.DefaultFieldManager(this);
5426 fm.extend_field_desc({
5428 selection: this.field_manager.get_field_desc(this.name).selection,
5436 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
5438 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5440 this.selection.on("change:value", this, this.on_selection_changed);
5441 this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
5443 .on('focused', null, function () {self.trigger('focused');})
5444 .on('blurred', null, function () {self.trigger('blurred');});
5446 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
5447 name: 'Referenced Document',
5448 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5450 this.m2o.on("change:value", this, this.data_changed);
5451 this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
5453 .on('focused', null, function () {self.trigger('focused');})
5454 .on('blurred', null, function () {self.trigger('blurred');});
5456 on_selection_changed: function() {
5457 if (this.reference_ready) {
5458 this.internal_set_value([this.selection.get_value(), false]);
5459 this.render_value();
5462 data_changed: function() {
5463 if (this.reference_ready) {
5464 this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
5467 set_value: function(val) {
5469 val = val.split(',');
5470 val[0] = val[0] || false;
5471 val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
5473 this._super(val || [false, false]);
5475 get_value: function() {
5476 return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
5478 render_value: function() {
5479 this.reference_ready = false;
5480 if (!this.get("effective_readonly")) {
5481 this.selection.set_value(this.get('value')[0]);
5483 this.m2o.field.relation = this.get('value')[0];
5484 this.m2o.set_value(this.get('value')[1]);
5485 this.m2o.$el.toggle(!!this.get('value')[0]);
5486 this.reference_ready = true;
5490 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5491 init: function(field_manager, node) {
5493 this._super(field_manager, node);
5494 this.binary_value = false;
5495 this.useFileAPI = !!window.FileReader;
5496 this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5497 if (!this.useFileAPI) {
5498 this.fileupload_id = _.uniqueId('oe_fileupload');
5499 $(window).on(this.fileupload_id, function() {
5500 var args = [].slice.call(arguments).slice(1);
5501 self.on_file_uploaded.apply(self, args);
5506 if (!this.useFileAPI) {
5507 $(window).off(this.fileupload_id);
5509 this._super.apply(this, arguments);
5511 initialize_content: function() {
5513 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5514 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5515 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5516 this.$el.find('.oe_form_binary_file_edit').click(function(event){
5517 self.$el.find('input.oe_form_binary_file').click();
5520 on_file_change: function(e) {
5522 var file_node = e.target;
5523 if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5524 if (this.useFileAPI) {
5525 var file = file_node.files[0];
5526 if (file.size > this.max_upload_size) {
5527 var msg = _t("The selected file exceed the maximum file size of %s.");
5528 instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5531 var filereader = new FileReader();
5532 filereader.readAsDataURL(file);
5533 filereader.onloadend = function(upload) {
5534 var data = upload.target.result;
5535 data = data.split(',')[1];
5536 self.on_file_uploaded(file.size, file.name, file.type, data);
5539 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5540 this.$el.find('form.oe_form_binary_form').submit();
5542 this.$el.find('.oe_form_binary_progress').show();
5543 this.$el.find('.oe_form_binary').hide();
5546 on_file_uploaded: function(size, name, content_type, file_base64) {
5547 if (size === false) {
5548 this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5549 // TODO: use openerp web crashmanager
5550 console.warn("Error while uploading file : ", name);
5552 this.filename = name;
5553 this.on_file_uploaded_and_valid.apply(this, arguments);
5555 this.$el.find('.oe_form_binary_progress').hide();
5556 this.$el.find('.oe_form_binary').show();
5558 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5560 on_save_as: function(ev) {
5561 var value = this.get('value');
5563 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5564 ev.stopPropagation();
5566 instance.web.blockUI();
5567 var c = instance.webclient.crashmanager;
5568 this.session.get_file({
5569 url: '/web/binary/saveas_ajax',
5570 data: {data: JSON.stringify({
5571 model: this.view.dataset.model,
5572 id: (this.view.datarecord.id || ''),
5574 filename_field: (this.node.attrs.filename || ''),
5575 data: instance.web.form.is_bin_size(value) ? null : value,
5576 context: this.view.dataset.get_context()
5578 complete: instance.web.unblockUI,
5579 error: c.rpc_error.bind(c)
5581 ev.stopPropagation();
5585 set_filename: function(value) {
5586 var filename = this.node.attrs.filename;
5589 tmp[filename] = value;
5590 this.field_manager.set_values(tmp);
5593 on_clear: function() {
5594 if (this.get('value') !== false) {
5595 this.binary_value = false;
5596 this.internal_set_value(false);
5602 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5603 template: 'FieldBinaryFile',
5604 initialize_content: function() {
5606 if (this.get("effective_readonly")) {
5608 this.$el.find('a').click(function(ev) {
5609 if (self.get('value')) {
5610 self.on_save_as(ev);
5616 render_value: function() {
5618 if (!this.get("effective_readonly")) {
5619 if (this.node.attrs.filename) {
5620 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5622 show_value = (this.get('value') !== null && this.get('value') !== undefined && this.get('value') !== false) ? this.get('value') : '';
5624 this.$el.find('input').eq(0).val(show_value);
5626 this.$el.find('a').toggle(!!this.get('value'));
5627 if (this.get('value')) {
5628 show_value = _t("Download");
5630 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5631 this.$el.find('a').text(show_value);
5635 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5636 this.binary_value = true;
5637 this.internal_set_value(file_base64);
5638 var show_value = name + " (" + instance.web.human_size(size) + ")";
5639 this.$el.find('input').eq(0).val(show_value);
5640 this.set_filename(name);
5642 on_clear: function() {
5643 this._super.apply(this, arguments);
5644 this.$el.find('input').eq(0).val('');
5645 this.set_filename('');
5649 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5650 template: 'FieldBinaryImage',
5651 placeholder: "/web/static/src/img/placeholder.png",
5652 render_value: function() {
5655 if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5656 url = 'data:image/png;base64,' + this.get('value');
5657 } else if (this.get('value')) {
5658 var id = JSON.stringify(this.view.datarecord.id || null);
5659 var field = this.name;
5660 if (this.options.preview_image)
5661 field = this.options.preview_image;
5662 url = this.session.url('/web/binary/image', {
5663 model: this.view.dataset.model,
5666 t: (new Date().getTime()),
5669 url = this.placeholder;
5671 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5672 $($img).click(function(e) {
5673 if(self.view.get("actual_mode") == "view") {
5674 var $button = $(".oe_form_button_edit");
5675 $button.openerpBounce();
5676 e.stopPropagation();
5679 this.$el.find('> img').remove();
5680 this.$el.prepend($img);
5681 $img.load(function() {
5682 if (! self.options.size)
5684 $img.css("max-width", "" + self.options.size[0] + "px");
5685 $img.css("max-height", "" + self.options.size[1] + "px");
5687 $img.on('error', function() {
5688 $img.attr('src', self.placeholder);
5689 instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5692 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5693 this.internal_set_value(file_base64);
5694 this.binary_value = true;
5695 this.render_value();
5696 this.set_filename(name);
5698 on_clear: function() {
5699 this._super.apply(this, arguments);
5700 this.render_value();
5701 this.set_filename('');
5703 set_value: function(value_){
5704 var changed = value_ !== this.get_value();
5705 this._super.apply(this, arguments);
5706 // By default, on binary images read, the server returns the binary size
5707 // This is possible that two images have the exact same size
5708 // Therefore we trigger the change in case the image value hasn't changed
5709 // So the image is re-rendered correctly
5711 this.trigger("change:value", this, {
5720 * Widget for (many2many field) to upload one or more file in same time and display in list.
5721 * The user can delete his files.
5722 * Options on attribute ; "blockui" {Boolean} block the UI or not
5723 * during the file is uploading
5725 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
5726 template: "FieldBinaryFileUploader",
5727 init: function(field_manager, node) {
5728 this._super(field_manager, node);
5729 this.field_manager = field_manager;
5731 if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5732 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);
5736 this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5737 this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5738 $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5742 this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5744 // WARNING: duplicated in 4 other M2M widgets
5745 set_value: function(value_) {
5746 value_ = value_ || [];
5747 if (value_.length >= 1 && value_[0] instanceof Array) {
5748 // value_ is a list of m2m commands. We only process
5749 // LINK_TO and REPLACE_WITH in this context
5751 _.each(value_, function (command) {
5752 if (command[0] === commands.LINK_TO) {
5753 val.push(command[1]); // (4, id[, _])
5754 } else if (command[0] === commands.REPLACE_WITH) {
5755 val = command[2]; // (6, _, ids)
5760 this._super(value_);
5762 get_value: function() {
5763 var tmp = [commands.replace_with(this.get("value"))];
5766 get_file_url: function (attachment) {
5767 return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5769 read_name_values : function () {
5771 // don't reset know values
5772 var ids = this.get('value');
5773 var _value = _.filter(ids, function (id) { return typeof self.data[id] == 'undefined'; } );
5774 // send request for get_name
5775 if (_value.length) {
5776 return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).then(function (datas) {
5777 _.each(datas, function (data) {
5778 data.no_unlink = true;
5779 data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5780 self.data[data.id] = data;
5788 render_value: function () {
5790 this.read_name_values().then(function (ids) {
5791 var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids}));
5792 render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5793 self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5795 // reinit input type file
5796 var $input = self.$('input.oe_form_binary_file');
5797 $input.after($input.clone(true)).remove();
5798 self.$(".oe_fileupload").show();
5802 on_file_change: function (event) {
5803 event.stopPropagation();
5805 var $target = $(event.target);
5806 if ($target.val() !== '') {
5807 var filename = $target.val().replace(/.*[\\\/]/,'');
5808 // don't uplode more of one file in same time
5809 if (self.data[0] && self.data[0].upload ) {
5812 for (var id in this.get('value')) {
5813 // if the files exits, delete the file before upload (if it's a new file)
5814 if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
5815 self.ds_file.unlink([id]);
5820 if(this.node.attrs.blockui>0) {
5821 instance.web.blockUI();
5824 // TODO : unactivate send on wizard and form
5827 this.$('form.oe_form_binary_form').submit();
5828 this.$(".oe_fileupload").hide();
5829 // add file on data result
5833 'filename': filename,
5839 on_file_loaded: function (event, result) {
5840 var files = this.get('value');
5843 if(this.node.attrs.blockui>0) {
5844 instance.web.unblockUI();
5847 if (result.error || !result.id ) {
5848 this.do_warn( _t('Uploading Error'), result.error);
5849 delete this.data[0];
5851 if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
5852 delete this.data[0];
5853 this.data[result.id] = {
5855 'name': result.name,
5856 'filename': result.filename,
5857 'url': this.get_file_url(result)
5860 this.data[result.id] = {
5862 'name': result.name,
5863 'filename': result.filename,
5864 'url': this.get_file_url(result)
5867 var values = _.clone(this.get('value'));
5868 values.push(result.id);
5869 this.set({'value': values});
5871 this.render_value();
5873 on_file_delete: function (event) {
5874 event.stopPropagation();
5875 var file_id=$(event.target).data("id");
5877 var files = _.filter(this.get('value'), function (id) {return id != file_id;});
5878 if(!this.data[file_id].no_unlink) {
5879 this.ds_file.unlink([file_id]);
5881 this.set({'value': files});
5886 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5887 template: "FieldStatus",
5888 init: function(field_manager, node) {
5889 this._super(field_manager, node);
5890 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5891 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5892 this.set({value: false});
5893 this.selection = {'unfolded': [], 'folded': []};
5894 this.set("selection", {'unfolded': [], 'folded': []});
5895 this.selection_dm = new instance.web.DropMisordered();
5896 this.dataset = new instance.web.DataSetStatic(this, this.field.relation, this.build_context());
5899 this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5901 this.on("change:value", this, this.get_selection);
5902 this.on("change:evaluated_selection_domain", this, this.get_selection);
5903 this.on("change:selection", this, function() {
5904 this.selection = this.get("selection");
5905 this.render_value();
5907 this.get_selection();
5908 if (this.options.clickable) {
5909 this.$el.on('click','li[data-id]',this.on_click_stage);
5911 if (this.$el.parent().is('header')) {
5912 this.$el.after('<div class="oe_clear"/>');
5916 set_value: function(value_) {
5917 if (value_ instanceof Array) {
5920 this._super(value_);
5922 render_value: function() {
5924 var content = QWeb.render("FieldStatus.content", {
5926 'value_folded': _.find(self.selection.folded, function(i){return i[0] === self.get('value');})
5928 self.$el.html(content);
5930 calc_domain: function() {
5931 var d = instance.web.pyeval.eval('domain', this.build_domain());
5932 var domain = []; //if there is no domain defined, fetch all the records
5935 domain = ['|',['id', '=', this.get('value')]].concat(d);
5938 if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5939 this.set("evaluated_selection_domain", domain);
5942 /** Get the selection and render it
5943 * selection: [[identifier, value_to_display], ...]
5944 * For selection fields: this is directly given by this.field.selection
5945 * For many2one fields: perform a search on the relation of the many2one field
5947 get_selection: function() {
5949 var selection_unfolded = [];
5950 var selection_folded = [];
5951 var fold_field = this.options.fold_field;
5953 var calculation = _.bind(function() {
5954 if (this.field.type == "many2one") {
5955 return self.get_distant_fields().then(function (fields) {
5956 return new instance.web.DataSetSearch(self, self.field.relation, self.build_context(), self.get("evaluated_selection_domain"))
5957 .read_slice(_.union(_.keys(self.distant_fields), ['id']), {}).then(function (records) {
5958 var ids = _.pluck(records, 'id');
5959 return self.dataset.name_get(ids).then(function (records_name) {
5960 _.each(records, function (record) {
5961 var name = _.find(records_name, function (val) {return val[0] == record.id;})[1];
5962 if (fold_field && record[fold_field] && record.id != self.get('value')) {
5963 selection_folded.push([record.id, name]);
5965 selection_unfolded.push([record.id, name]);
5972 // For field type selection filter values according to
5973 // statusbar_visible attribute of the field. For example:
5974 // statusbar_visible="draft,open".
5975 var select = this.field.selection;
5976 for(var i=0; i < select.length; i++) {
5977 var key = select[i][0];
5978 if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5979 selection_unfolded.push(select[i]);
5985 this.selection_dm.add(calculation()).then(function () {
5986 var selection = {'unfolded': selection_unfolded, 'folded': selection_folded};
5987 if (! _.isEqual(selection, self.get("selection"))) {
5988 self.set("selection", selection);
5993 * :deprecated: this feature will probably be removed with OpenERP v8
5995 get_distant_fields: function() {
5997 if (! this.options.fold_field) {
5998 this.distant_fields = {}
6000 if (this.distant_fields) {
6001 return $.when(this.distant_fields);
6003 return new instance.web.Model(self.field.relation).call("fields_get", [[this.options.fold_field]]).then(function(fields) {
6004 self.distant_fields = fields;
6008 on_click_stage: function (ev) {
6010 var $li = $(ev.currentTarget);
6012 if (this.field.type == "many2one") {
6013 val = parseInt($li.data("id"), 10);
6016 val = $li.data("id");
6018 if (val != self.get('value')) {
6019 this.view.recursive_save().done(function() {
6021 change[self.name] = val;
6022 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
6030 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
6031 template: "FieldMonetary",
6032 widget_class: 'oe_form_field_float oe_form_field_monetary',
6034 this._super.apply(this, arguments);
6035 this.set({"currency": false});
6036 if (this.options.currency_field) {
6037 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
6038 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
6041 this.on("change:currency", this, this.get_currency_info);
6042 this.get_currency_info();
6043 this.ci_dm = new instance.web.DropMisordered();
6046 var tmp = this._super();
6047 this.on("change:currency_info", this, this.reinitialize);
6050 get_currency_info: function() {
6052 if (this.get("currency") === false) {
6053 this.set({"currency_info": null});
6056 return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
6057 .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
6058 self.set({"currency_info": res});
6061 parse_value: function(val, def) {
6062 return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6064 format_value: function(val, def) {
6065 return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6070 This type of field display a list of checkboxes. It works only with m2ms. This field will display one checkbox for each
6071 record existing in the model targeted by the relation, according to the given domain if one is specified. Checked records
6072 will be added to the relation.
6074 instance.web.form.FieldMany2ManyCheckBoxes = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6075 className: "oe_form_many2many_checkboxes",
6077 this._super.apply(this, arguments);
6078 this.set("value", {});
6079 this.set("records", []);
6080 this.field_manager.on("view_content_has_changed", this, function() {
6081 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
6082 if (! _.isEqual(domain, this.get("domain"))) {
6083 this.set("domain", domain);
6086 this.records_orderer = new instance.web.DropMisordered();
6088 initialize_field: function() {
6089 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
6090 this.on("change:domain", this, this.query_records);
6091 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
6092 this.on("change:records", this, this.render_value);
6094 query_records: function() {
6096 var model = new openerp.Model(openerp.session, this.field.relation);
6097 this.records_orderer.add(model.call("search", [this.get("domain")], {"context": this.build_context()}).then(function(record_ids) {
6098 return model.call("name_get", [record_ids] , {"context": self.build_context()});
6099 })).then(function(res) {
6100 self.set("records", res);
6103 render_value: function() {
6104 this.$().html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this, selected: this.get("value")}));
6105 var inputs = this.$("input");
6106 inputs.change(_.bind(this.from_dom, this));
6107 if (this.get("effective_readonly"))
6108 inputs.attr("disabled", "true");
6110 from_dom: function() {
6112 this.$("input").each(function() {
6114 new_value[elem.data("record-id")] = elem.attr("checked") ? true : undefined;
6116 if (! _.isEqual(new_value, this.get("value")))
6117 this.internal_set_value(new_value);
6119 // WARNING: (mostly) duplicated in 4 other M2M widgets
6120 set_value: function(value_) {
6121 value_ = value_ || [];
6122 if (value_.length >= 1 && value_[0] instanceof Array) {
6123 // value_ is a list of m2m commands. We only process
6124 // LINK_TO and REPLACE_WITH in this context
6126 _.each(value_, function (command) {
6127 if (command[0] === commands.LINK_TO) {
6128 val.push(command[1]); // (4, id[, _])
6129 } else if (command[0] === commands.REPLACE_WITH) {
6130 val = command[2]; // (6, _, ids)
6136 _.each(value_, function(el) {
6137 formatted[JSON.stringify(el)] = true;
6139 this._super(formatted);
6141 get_value: function() {
6142 var value = _.filter(_.keys(this.get("value")), function(el) {
6143 return this.get("value")[el];
6145 value = _.map(value, function(el) {
6146 return JSON.parse(el);
6148 return [commands.replace_with(value)];
6153 This field can be applied on many2many and one2many. It is a read-only field that will display a single link whose name is
6154 "<number of linked records> <label of the field>". When the link is clicked, it will redirect to another act_window
6155 action on the model of the relation and show only the linked records.
6159 * 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
6160 to display (or False to take the default one) and the second element is the type of the view. Defaults to
6161 [[false, "tree"], [false, "form"]] .
6163 instance.web.form.X2ManyCounter = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6164 className: "oe_form_x2many_counter",
6166 this._super.apply(this, arguments);
6167 this.set("value", []);
6168 _.defaults(this.options, {
6169 "views": [[false, "tree"], [false, "form"]],
6172 render_value: function() {
6173 var text = _.str.sprintf("%d %s", this.val().length, this.string);
6174 this.$().html(QWeb.render("X2ManyCounter", {text: text}));
6175 this.$("a").click(_.bind(this.go_to, this));
6178 return this.view.recursive_save().then(_.bind(function() {
6179 var val = this.val();
6181 if (this.field.type === "one2many") {
6182 context["default_" + this.field.relation_field] = this.view.datarecord.id;
6184 var domain = [["id", "in", val]];
6185 return this.do_action({
6186 type: 'ir.actions.act_window',
6188 res_model: this.field.relation,
6189 views: this.options.views,
6197 var value = this.get("value") || [];
6198 if (value.length >= 1 && value[0] instanceof Array) {
6199 value = value[0][2];
6206 This widget is intended to be used on stat button numeric fields. It will display
6207 the value many2many and one2many. It is a read-only field that will
6208 display a simple string "<value of field> <label of the field>"
6210 instance.web.form.StatInfo = instance.web.form.AbstractField.extend({
6211 is_field_number: true,
6213 this._super.apply(this, arguments);
6214 this.internal_set_value(0);
6216 set_value: function(value_) {
6217 if (value_ === false || value_ === undefined) {
6220 this._super.apply(this, [value_]);
6222 render_value: function() {
6224 value: this.get("value") || 0,
6226 if (! this.node.attrs.nolabel) {
6227 options.text = this.string
6229 this.$el.html(QWeb.render("StatInfo", options));
6236 * Registry of form fields, called by :js:`instance.web.FormView`.
6238 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
6239 * will substitute to the <field> tags as defined in OpenERP's views.
6241 instance.web.form.widgets = new instance.web.Registry({
6242 'char' : 'instance.web.form.FieldChar',
6243 'id' : 'instance.web.form.FieldID',
6244 'email' : 'instance.web.form.FieldEmail',
6245 'url' : 'instance.web.form.FieldUrl',
6246 'text' : 'instance.web.form.FieldText',
6247 'html' : 'instance.web.form.FieldTextHtml',
6248 'char_domain': 'instance.web.form.FieldCharDomain',
6249 'date' : 'instance.web.form.FieldDate',
6250 'datetime' : 'instance.web.form.FieldDatetime',
6251 'selection' : 'instance.web.form.FieldSelection',
6252 'radio' : 'instance.web.form.FieldRadio',
6253 'many2one' : 'instance.web.form.FieldMany2One',
6254 'many2onebutton' : 'instance.web.form.Many2OneButton',
6255 'many2many' : 'instance.web.form.FieldMany2Many',
6256 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
6257 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
6258 'one2many' : 'instance.web.form.FieldOne2Many',
6259 'one2many_list' : 'instance.web.form.FieldOne2Many',
6260 'reference' : 'instance.web.form.FieldReference',
6261 'boolean' : 'instance.web.form.FieldBoolean',
6262 'float' : 'instance.web.form.FieldFloat',
6263 'percentpie': 'instance.web.form.FieldPercentPie',
6264 'barchart': 'instance.web.form.FieldBarChart',
6265 'integer': 'instance.web.form.FieldFloat',
6266 'float_time': 'instance.web.form.FieldFloat',
6267 'progressbar': 'instance.web.form.FieldProgressBar',
6268 'image': 'instance.web.form.FieldBinaryImage',
6269 'binary': 'instance.web.form.FieldBinaryFile',
6270 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
6271 'statusbar': 'instance.web.form.FieldStatus',
6272 'monetary': 'instance.web.form.FieldMonetary',
6273 'many2many_checkboxes': 'instance.web.form.FieldMany2ManyCheckBoxes',
6274 'x2many_counter': 'instance.web.form.X2ManyCounter',
6275 'priority':'instance.web.form.Priority',
6276 'kanban_state_selection':'instance.web.form.KanbanSelection',
6277 'statinfo': 'instance.web.form.StatInfo',
6281 * Registry of widgets usable in the form view that can substitute to any possible
6282 * tags defined in OpenERP's form views.
6284 * Every referenced class should extend FormWidget.
6286 instance.web.form.tags = new instance.web.Registry({
6287 'button' : 'instance.web.form.WidgetButton',
6290 instance.web.form.custom_widgets = new instance.web.Registry({
6295 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: