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");
428 this.do_update_pager();
430 do_update_pager: function(hide_index) {
431 this.$pager.toggle(this.dataset.ids.length > 1);
433 $(".oe_form_pager_state", this.$pager).html("");
435 $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
439 _build_onchange_specs: function() {
441 var find = function(field_name, root) {
443 while (fields.length) {
444 var node = fields.pop();
448 if (node.tag === 'field' && node.attrs.name === field_name) {
449 return node.attrs.on_change || "";
451 fields = _.union(fields, node.children);
456 self._onchange_specs = {};
457 _.each(this.fields, function(field, name) {
458 self._onchange_specs[name] = find(name, field.node);
459 _.each(field.field.views, function(view) {
460 _.each(view.fields, function(_, subname) {
461 self._onchange_specs[name + '.' + subname] = find(subname, view.arch);
466 _get_onchange_values: function() {
467 var field_values = this.get_fields_values();
468 if (field_values.id.toString().match(instance.web.BufferedDataSet.virtual_id_regex)) {
469 delete field_values.id;
471 if (this.dataset.parent_view) {
472 // this belongs to a parent view: add parent field if possible
473 var parent_view = this.dataset.parent_view;
474 var child_name = this.dataset.child_name;
475 var parent_name = parent_view.get_field_desc(child_name).relation_field;
477 // consider all fields except the inverse of the parent field
478 var parent_values = parent_view.get_fields_values();
479 delete parent_values[child_name];
480 field_values[parent_name] = parent_values;
486 do_onchange: function(widget) {
488 var onchange_specs = self._onchange_specs;
490 var def = $.when({});
491 var change_spec = widget ? onchange_specs[widget.name] : null;
492 if (!widget || (!_.isEmpty(change_spec) && change_spec !== "0")) {
494 trigger_field_name = widget ? widget.name : false,
495 values = self._get_onchange_values(),
496 context = new instance.web.CompoundContext(self.dataset.get_context());
498 if (widget && widget.build_context()) {
499 context.add(widget.build_context());
501 if (self.dataset.parent_view) {
502 var parent_name = self.dataset.parent_view.get_field_desc(self.dataset.child_name).relation_field;
503 context.add({field_parent: parent_name});
506 if (self.datarecord.id && !instance.web.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) {
507 // In case of a o2m virtual id, we should pass an empty ids list
508 ids.push(self.datarecord.id);
510 def = self.alive(new instance.web.Model(self.dataset.model).call(
511 "onchange", [ids, values, trigger_field_name, onchange_specs, context]));
513 var onchange_def = def.then(function(response) {
514 if (widget && widget.field['change_default']) {
515 var fieldname = widget.name;
517 if (response.value && (fieldname in response.value)) {
518 // Use value from onchange if onchange executed
519 value_ = response.value[fieldname];
521 // otherwise get form value for field
522 value_ = self.fields[fieldname].get_value();
524 var condition = fieldname + '=' + value_;
527 return self.alive(new instance.web.Model('ir.values').call(
528 'get_defaults', [self.model, condition]
529 )).then(function (results) {
530 if (!results.length) {
533 if (!response.value) {
536 for(var i=0; i<results.length; ++i) {
537 // [whatever, key, value]
538 var triplet = results[i];
539 response.value[triplet[1]] = triplet[2];
546 }).then(function(response) {
547 return self.on_processed_onchange(response);
549 this.onchanges_defs.push(onchange_def);
553 instance.webclient.crashmanager.show_message(e);
554 return $.Deferred().reject();
557 on_processed_onchange: function(result) {
559 var fields = this.fields;
560 _(result.domain).each(function (domain, fieldname) {
561 var field = fields[fieldname];
562 if (!field) { return; }
563 field.node.attrs.domain = domain;
566 if (!_.isEmpty(result.value)) {
567 this._internal_set_values(result.value);
569 // FIXME XXX a list of warnings?
570 if (!_.isEmpty(result.warning)) {
571 new instance.web.Dialog(this, {
573 title:result.warning.title,
575 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
577 }, QWeb.render("CrashManager.warning", result.warning)).open();
580 return $.Deferred().resolve();
583 instance.webclient.crashmanager.show_message(e);
584 return $.Deferred().reject();
587 _process_operations: function() {
589 return this.mutating_mutex.exec(function() {
591 var start = $.Deferred();
593 start = _.reduce(self.onchanges_defs, function(memo, d){
594 return memo.then(function(){
601 _.each(self.fields, function(field) {
602 defs.push(field.commit_value());
604 var args = _.toArray(arguments);
605 return $.when.apply($, defs).then(function() {
606 var save_obj = self.save_list.pop();
608 return self._process_save(save_obj).then(function() {
609 save_obj.ret = _.toArray(arguments);
612 save_obj.error = true;
617 self.save_list.pop();
624 _internal_set_values: function(values) {
625 for (var f in values) {
626 if (!values.hasOwnProperty(f)) { continue; }
627 var field = this.fields[f];
628 // If field is not defined in the view, just ignore it
630 var value_ = values[f];
631 if (field.get_value() != value_) {
632 field._inhibit_on_change_flag = true;
633 field.set_value(value_);
634 field._inhibit_on_change_flag = false;
635 field._dirty_flag = true;
639 this.on_form_changed();
641 set_values: function(values) {
643 return this.mutating_mutex.exec(function() {
644 self._internal_set_values(values);
648 * Ask the view to switch to view mode if possible. The view may not do it
649 * if the current record is not yet saved. It will then stay in create mode.
651 to_view_mode: function() {
652 this._actualize_mode("view");
655 * Ask the view to switch to edit mode if possible. The view may not do it
656 * if the current record is not yet saved. It will then stay in create mode.
658 to_edit_mode: function() {
659 this.onchanges_defs = [];
660 this._actualize_mode("edit");
663 * Ask the view to switch to a precise mode if possible. The view is free to
664 * not respect this command if the state of the dataset is not compatible with
665 * the new mode. For example, it is not possible to switch to edit mode if
666 * the current record is not yet saved in database.
668 * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
669 * undefined the view will test the actual mode to check if it is still consistent
670 * with the dataset state.
672 _actualize_mode: function(switch_to) {
673 var mode = switch_to || this.get("actual_mode");
674 if (! this.datarecord.id) {
676 } else if (mode === "create") {
679 this.render_value_defs = [];
680 this.set({actual_mode: mode});
682 check_actual_mode: function(source, options) {
684 if(this.get("actual_mode") === "view") {
685 self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
686 self.$buttons.find('.oe_form_buttons_edit').hide();
687 self.$buttons.find('.oe_form_buttons_view').show();
688 self.$sidebar.show();
690 self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
691 self.$buttons.find('.oe_form_buttons_edit').show();
692 self.$buttons.find('.oe_form_buttons_view').hide();
693 self.$sidebar.hide();
697 autofocus: function() {
698 if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
699 var fields_order = this.fields_order.slice(0);
700 if (this.default_focus_field) {
701 fields_order.unshift(this.default_focus_field.name);
703 for (var i = 0; i < fields_order.length; i += 1) {
704 var field = this.fields[fields_order[i]];
705 if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
706 if (field.focus() !== false) {
713 on_button_save: function(e) {
715 $(e.target).attr("disabled", true);
716 return this.save().done(function(result) {
717 self.trigger("save", result);
718 self.reload().then(function() {
720 var menu = instance.webclient.menu;
722 menu.do_reload_needaction();
724 instance.web.bus.trigger('form_view_saved', self);
726 }).always(function(){
727 $(e.target).attr("disabled", false);
730 on_button_cancel: function(event) {
732 if (this.can_be_discarded()) {
733 if (this.get('actual_mode') === 'create') {
734 this.trigger('history_back');
737 $.when.apply(null, this.render_value_defs).then(function(){
738 self.trigger('load_record', self.datarecord);
742 this.trigger('on_button_cancel');
745 on_button_new: function() {
748 return $.when(this.has_been_loaded).then(function() {
749 if (self.can_be_discarded()) {
750 return self.load_defaults();
754 on_button_edit: function() {
755 return this.to_edit_mode();
757 on_button_create: function() {
758 this.dataset.index = null;
761 on_button_duplicate: function() {
763 return this.has_been_loaded.then(function() {
764 return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
765 self.record_created(new_id);
770 on_button_delete: function() {
772 var def = $.Deferred();
773 this.has_been_loaded.done(function() {
774 if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
775 self.dataset.unlink([self.datarecord.id]).done(function() {
776 if (self.dataset.size()) {
777 self.execute_pager_action('next');
779 self.do_action('history_back');
784 $.async_when().done(function () {
789 return def.promise();
791 can_be_discarded: function() {
792 if (this.$el.is('.oe_form_dirty')) {
793 if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
796 this.$el.removeClass('oe_form_dirty');
801 * Triggers saving the form's record. Chooses between creating a new
802 * record or saving an existing one depending on whether the record
803 * already has an id property.
805 * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
806 * record, should that record be inserted at the start of the dataset (by
807 * default, records are added at the end)
809 save: function(prepend_on_create) {
811 var save_obj = {prepend_on_create: prepend_on_create, ret: null};
812 this.save_list.push(save_obj);
813 return self._process_operations().then(function() {
815 return $.Deferred().reject();
816 return $.when.apply($, save_obj.ret);
817 }).done(function(result) {
818 self.$el.removeClass('oe_form_dirty');
821 _process_save: function(save_obj) {
823 var prepend_on_create = save_obj.prepend_on_create;
825 var form_invalid = false,
827 first_invalid_field = null,
828 readonly_values = {};
829 for (var f in self.fields) {
830 if (!self.fields.hasOwnProperty(f)) { continue; }
834 if (!first_invalid_field) {
835 first_invalid_field = f;
837 } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
838 // Special case 'id' field, do not save this field
839 // on 'create' : save all non readonly fields
840 // on 'edit' : save non readonly modified fields
841 if (!f.get("readonly")) {
842 values[f.name] = f.get_value();
844 readonly_values[f.name] = f.get_value();
849 self.set({'display_invalid_fields': true});
850 first_invalid_field.focus();
852 return $.Deferred().reject();
854 self.set({'display_invalid_fields': false});
856 if (!self.datarecord.id) {
858 save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
859 return self.record_created(r, prepend_on_create);
861 } else if (_.isEmpty(values)) {
862 // Not dirty, noop save
863 save_deferral = $.Deferred().resolve({}).promise();
866 save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
867 return self.record_saved(r);
870 return save_deferral;
874 return $.Deferred().reject();
877 on_invalid: function() {
878 var warnings = _(this.fields).chain()
879 .filter(function (f) { return !f.is_valid(); })
881 return _.str.sprintf('<li>%s</li>',
884 warnings.unshift('<ul>');
885 warnings.push('</ul>');
886 this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
889 * Reload the form after saving
891 * @param {Object} r result of the write function.
893 record_saved: function(r) {
894 this.trigger('record_saved', r);
896 // should not happen in the server, but may happen for internal purpose
897 return $.Deferred().reject();
902 * Updates the form' dataset to contain the new record:
904 * * Adds the newly created record to the current dataset (at the end by
906 * * Selects that record (sets the dataset's index to point to the new
908 * * Updates the pager and sidebar displays
911 * @param {Boolean} [prepend_on_create=false] adds the newly created record
912 * at the beginning of the dataset instead of the end
914 record_created: function(r, prepend_on_create) {
917 // should not happen in the server, but may happen for internal purpose
918 this.trigger('record_created', r);
919 return $.Deferred().reject();
921 this.datarecord.id = r;
922 if (!prepend_on_create) {
923 this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
924 this.dataset.index = this.dataset.ids.length - 1;
926 this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
927 this.dataset.index = 0;
929 this.do_update_pager();
931 this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
933 //openerp.log("The record has been created with id #" + this.datarecord.id);
934 return $.when(this.reload()).then(function () {
935 self.trigger('record_created', r);
936 return _.extend(r, {created: true});
940 on_action: function (action) {
941 console.debug('Executing action', action);
945 return this.reload_mutex.exec(function() {
946 if (self.dataset.index === null || self.dataset.index === undefined) {
947 self.trigger("previous_view");
948 return $.Deferred().reject().promise();
950 if (self.dataset.index < 0) {
951 return $.when(self.on_button_new());
953 var fields = _.keys(self.fields_view.fields);
954 fields.push('display_name');
955 return self.dataset.read_index(fields,
959 'future_display_name': true
961 check_access_rule: true
962 }).then(function(r) {
963 self.trigger('load_record', r);
965 self.do_action('history_back');
970 get_widgets: function() {
971 return _.filter(this.getChildren(), function(obj) {
972 return obj instanceof instance.web.form.FormWidget;
975 get_fields_values: function() {
977 var ids = this.get_selected_ids();
978 values["id"] = ids.length > 0 ? ids[0] : false;
979 _.each(this.fields, function(value_, key) {
980 values[key] = value_.get_value();
984 get_selected_ids: function() {
985 var id = this.dataset.ids[this.dataset.index];
986 return id ? [id] : [];
988 recursive_save: function() {
990 return $.when(this.save()).then(function(res) {
991 if (self.dataset.parent_view)
992 return self.dataset.parent_view.recursive_save();
995 recursive_reload: function() {
998 if (self.dataset.parent_view)
999 pre = self.dataset.parent_view.recursive_reload();
1000 return pre.then(function() {
1001 return self.reload();
1004 is_dirty: function() {
1005 return _.any(this.fields, function (value_) {
1006 return value_._dirty_flag;
1009 is_interactible_record: function() {
1010 var id = this.datarecord.id;
1012 if (this.options.not_interactible_on_create)
1014 } else if (typeof(id) === "string") {
1015 if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
1020 sidebar_eval_context: function () {
1021 return $.when(this.build_eval_context());
1023 open_defaults_dialog: function () {
1025 var display = function (field, value) {
1026 if (!value) { return value; }
1027 if (field instanceof instance.web.form.FieldSelection) {
1028 return _(field.get('values')).find(function (option) {
1029 return option[0] === value;
1031 } else if (field instanceof instance.web.form.FieldMany2One) {
1032 return field.get_displayed();
1036 var fields = _.chain(this.fields)
1037 .map(function (field) {
1038 var value = field.get_value();
1039 // ignore fields which are empty, invisible, readonly, o2m
1042 || field.get('invisible')
1043 || field.get("readonly")
1044 || field.field.type === 'one2many'
1045 || field.field.type === 'many2many'
1046 || field.field.type === 'binary'
1047 || field.password) {
1053 string: field.string,
1055 displayed: display(field, value),
1059 .sortBy(function (field) { return field.string; })
1061 var conditions = _.chain(self.fields)
1062 .filter(function (field) { return field.field.change_default; })
1063 .map(function (field) {
1064 var value = field.get_value();
1067 string: field.string,
1069 displayed: display(field, value),
1073 var d = new instance.web.Dialog(this, {
1074 title: _t("Set Default"),
1077 conditions: conditions
1080 {text: _t("Close"), click: function () { d.close(); }},
1081 {text: _t("Save default"), click: function () {
1082 var $defaults = d.$el.find('#formview_default_fields');
1083 var field_to_set = $defaults.val();
1084 if (!field_to_set) {
1085 $defaults.parent().addClass('oe_form_invalid');
1088 var condition = d.$el.find('#formview_default_conditions').val(),
1089 all_users = d.$el.find('#formview_default_all').is(':checked');
1090 new instance.web.DataSet(self, 'ir.values').call(
1094 self.fields[field_to_set].get_value(),
1098 ]).done(function () { d.close(); });
1102 d.template = 'FormView.set_default';
1105 register_field: function(field, name) {
1106 this.fields[name] = field;
1107 this.fields_order.push(name);
1108 if (JSON.parse(field.node.attrs.default_focus || "0")) {
1109 this.default_focus_field = field;
1112 field.on('focused', null, this.proxy('widgetFocused'))
1113 .on('blurred', null, this.proxy('widgetBlurred'));
1114 if (this.get_field_desc(name).translate) {
1115 this.translatable_fields.push(field);
1117 field.on('changed_value', this, function() {
1118 if (field.is_syntax_valid()) {
1119 this.trigger('field_changed:' + name);
1121 if (field._inhibit_on_change_flag) {
1124 field._dirty_flag = true;
1125 if (field.is_syntax_valid()) {
1126 this.do_onchange(field);
1127 this.on_form_changed(true);
1128 this.do_notify_change();
1132 get_field_desc: function(field_name) {
1133 return this.fields_view.fields[field_name];
1135 get_field_value: function(field_name) {
1136 return this.fields[field_name].get_value();
1138 compute_domain: function(expression) {
1139 return instance.web.form.compute_domain(expression, this.fields);
1141 _build_view_fields_values: function() {
1142 var a_dataset = this.dataset;
1143 var fields_values = this.get_fields_values();
1144 var active_id = a_dataset.ids[a_dataset.index];
1145 _.extend(fields_values, {
1146 active_id: active_id || false,
1147 active_ids: active_id ? [active_id] : [],
1148 active_model: a_dataset.model,
1151 if (a_dataset.parent_view) {
1152 fields_values.parent = a_dataset.parent_view.get_fields_values();
1154 return fields_values;
1156 build_eval_context: function() {
1157 var a_dataset = this.dataset;
1158 return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
1163 * Interface to be implemented by rendering engines for the form view.
1165 instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
1166 set_fields_view: function(fields_view) {},
1167 set_fields_registry: function(fields_registry) {},
1168 render_to: function($el) {},
1172 * Default rendering engine for the form view.
1174 * It is necessary to set the view using set_view() before usage.
1176 instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
1177 init: function(view) {
1180 set_fields_view: function(fvg) {
1182 this.version = parseFloat(this.fvg.arch.attrs.version);
1183 if (isNaN(this.version)) {
1187 set_tags_registry: function(tags_registry) {
1188 this.tags_registry = tags_registry;
1190 set_fields_registry: function(fields_registry) {
1191 this.fields_registry = fields_registry;
1193 set_widgets_registry: function(widgets_registry) {
1194 this.widgets_registry = widgets_registry;
1196 // Backward compatibility tools, current default version: v7
1197 process_version: function() {
1198 if (this.version < 7.0) {
1199 this.$form.find('form:first').wrapInner('<group col="4"/>');
1200 this.$form.find('page').each(function() {
1201 if (!$(this).parents('field').length) {
1202 $(this).wrapInner('<group col="4"/>');
1207 get_arch_fragment: function() {
1208 var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
1209 // IE won't allow custom button@type and will revert it to spec default : 'submit'
1210 $('button', doc).each(function() {
1211 $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
1213 // IE's html parser is also a css parser. How convenient...
1214 $('board', doc).each(function() {
1215 $(this).attr('layout', $(this).attr('style'));
1217 return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
1219 render_to: function($target) {
1221 this.$target = $target;
1223 this.$form = this.get_arch_fragment();
1225 this.process_version();
1227 this.fields_to_init = [];
1228 this.tags_to_init = [];
1229 this.widgets_to_init = [];
1231 this.process(this.$form);
1233 this.$form.appendTo(this.$target);
1235 this.to_replace = [];
1237 _.each(this.fields_to_init, function($elem) {
1238 var name = $elem.attr("name");
1239 if (!self.fvg.fields[name]) {
1240 throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
1242 var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
1244 throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
1246 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1247 var $label = self.labels[$elem.attr("name")];
1249 w.set_input_id($label.attr("for"));
1251 self.alter_field(w);
1252 self.view.register_field(w, $elem.attr("name"));
1253 self.to_replace.push([w, $elem]);
1255 _.each(this.tags_to_init, function($elem) {
1256 var tag_name = $elem[0].tagName.toLowerCase();
1257 var obj = self.tags_registry.get_object(tag_name);
1258 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1259 self.to_replace.push([w, $elem]);
1261 _.each(this.widgets_to_init, function($elem) {
1262 var widget_type = $elem.attr("type");
1263 var obj = self.widgets_registry.get_object(widget_type);
1264 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1265 self.to_replace.push([w, $elem]);
1268 init_fields: function() {
1270 _.each(this.to_replace, function(el) {
1271 defs.push(el[0].replace(el[1]));
1272 if (el[1].children().length) {
1273 el[0].$el.append(el[1].children());
1276 this.to_replace = [];
1277 return $.when.apply($, defs);
1279 render_element: function(template /* dictionaries */) {
1280 var dicts = [].slice.call(arguments).slice(1);
1281 var dict = _.extend.apply(_, dicts);
1282 dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
1283 return $(QWeb.render(template, dict));
1285 alter_field: function(field) {
1287 toggle_layout_debugging: function() {
1288 if (!this.$target.has('.oe_layout_debug_cell:first').length) {
1289 this.$target.find('[title]').removeAttr('title');
1290 this.$target.find('.oe_form_group_cell').each(function() {
1291 var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
1292 $(this).attr('title', text);
1295 this.$target.toggleClass('oe_layout_debugging');
1297 process: function($tag) {
1299 var tagname = $tag[0].nodeName.toLowerCase();
1300 if (this.tags_registry.contains(tagname)) {
1301 this.tags_to_init.push($tag);
1302 return (tagname === 'button') ? this.process_button($tag) : $tag;
1304 var fn = self['process_' + tagname];
1306 var args = [].slice.call(arguments);
1308 return fn.apply(self, args);
1310 // generic tag handling, just process children
1311 $tag.children().each(function() {
1312 self.process($(this));
1314 self.handle_common_properties($tag, $tag);
1315 $tag.removeAttr("modifiers");
1319 process_button: function ($button) {
1321 $button.children().each(function() {
1322 self.process($(this));
1326 process_widget: function($widget) {
1327 this.widgets_to_init.push($widget);
1330 process_sheet: function($sheet) {
1331 var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
1332 this.handle_common_properties($new_sheet, $sheet);
1333 var $dst = $new_sheet.find('.oe_form_sheet');
1334 $sheet.contents().appendTo($dst);
1335 $sheet.before($new_sheet).remove();
1336 this.process($new_sheet);
1338 process_form: function($form) {
1339 if ($form.find('> sheet').length === 0) {
1340 $form.addClass('oe_form_nosheet');
1342 var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
1343 this.handle_common_properties($new_form, $form);
1344 $form.contents().appendTo($new_form);
1345 if ($form[0] === this.$form[0]) {
1346 // If root element, replace it
1347 this.$form = $new_form;
1349 $form.before($new_form).remove();
1351 this.process($new_form);
1354 * Used by direct <field> children of a <group> tag only
1355 * This method will add the implicit <label...> for every field
1358 preprocess_field: function($field) {
1360 var name = $field.attr('name'),
1361 field_colspan = parseInt($field.attr('colspan'), 10),
1362 field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
1364 if ($field.attr('nolabel') === '1')
1366 $field.attr('nolabel', '1');
1368 this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
1369 $(el).parents().each(function(unused, tag) {
1370 var name = tag.tagName.toLowerCase();
1371 if (name === "field" || name in self.tags_registry.map)
1378 var $label = $('<label/>').attr({
1380 "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
1381 "string": $field.attr('string'),
1382 "help": $field.attr('help'),
1383 "class": $field.attr('class'),
1385 $label.insertBefore($field);
1386 if (field_colspan > 1) {
1387 $field.attr('colspan', field_colspan - 1);
1391 process_field: function($field) {
1392 if ($field.parent().is('group')) {
1393 // No implicit labels for normal fields, only for <group> direct children
1394 var $label = this.preprocess_field($field);
1396 this.process($label);
1399 this.fields_to_init.push($field);
1402 process_group: function($group) {
1404 $group.children('field').each(function() {
1405 self.preprocess_field($(this));
1407 var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
1409 if ($new_group.first().is('table.oe_form_group')) {
1410 $table = $new_group;
1411 } else if ($new_group.filter('table.oe_form_group').length) {
1412 $table = $new_group.filter('table.oe_form_group').first();
1414 $table = $new_group.find('table.oe_form_group').first();
1418 cols = parseInt($group.attr('col') || 2, 10),
1422 $group.children().each(function(a,b,c) {
1423 var $child = $(this);
1424 var colspan = parseInt($child.attr('colspan') || 1, 10);
1425 var tagName = $child[0].tagName.toLowerCase();
1426 var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
1427 var newline = tagName === 'newline';
1429 // Note FME: those classes are used in layout debug mode
1430 if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
1431 $tr.addClass('oe_form_group_row_incomplete');
1433 $tr.addClass('oe_form_group_row_newline');
1440 if (!$tr || row_cols < colspan) {
1441 $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
1443 } else if (tagName==='group') {
1444 // When <group> <group/><group/> </group>, we need a spacing between the two groups
1445 $td.addClass('oe_group_right');
1447 row_cols -= colspan;
1449 // invisibility transfer
1450 var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
1451 var invisible = field_modifiers.invisible;
1452 self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
1454 $tr.append($td.append($child));
1455 children.push($child[0]);
1457 if (row_cols && $td) {
1458 $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
1460 $group.before($new_group).remove();
1462 $table.find('> tbody > tr').each(function() {
1463 var to_compute = [],
1466 $(this).children().each(function() {
1468 $child = $td.children(':first');
1469 if ($child.attr('cell-class')) {
1470 $td.addClass($child.attr('cell-class'));
1472 switch ($child[0].tagName.toLowerCase()) {
1476 if ($child.attr('for')) {
1477 $td.attr('width', '1%').addClass('oe_form_group_cell_label');
1478 row_cols-= $td.attr('colspan') || 1;
1483 var width = _.str.trim($child.attr('width') || ''),
1484 iwidth = parseInt(width, 10);
1486 if (width.substr(-1) === '%') {
1488 width = iwidth + '%';
1491 $td.css('min-width', width + 'px');
1493 $td.attr('width', width);
1494 $child.removeAttr('width');
1495 row_cols-= $td.attr('colspan') || 1;
1497 to_compute.push($td);
1503 var unit = Math.floor(total / row_cols);
1504 if (!$(this).is('.oe_form_group_row_incomplete')) {
1505 _.each(to_compute, function($td, i) {
1506 var width = parseInt($td.attr('colspan'), 10) * unit;
1507 $td.attr('width', width + '%');
1513 _.each(children, function(el) {
1514 self.process($(el));
1516 this.handle_common_properties($new_group, $group);
1519 process_notebook: function($notebook) {
1522 $notebook.find('> page').each(function() {
1523 var $page = $(this);
1524 var page_attrs = $page.getAttributes();
1525 page_attrs.id = _.uniqueId('notebook_page_');
1526 var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
1527 $page.contents().appendTo($new_page);
1528 $page.before($new_page).remove();
1529 var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
1530 page_attrs.__page = $new_page;
1531 page_attrs.__ic = ic;
1532 pages.push(page_attrs);
1534 $new_page.children().each(function() {
1535 self.process($(this));
1538 var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
1539 $notebook.contents().appendTo($new_notebook);
1540 $notebook.before($new_notebook).remove();
1541 self.process($($new_notebook.children()[0]));
1542 //tabs and invisibility handling
1543 $new_notebook.tabs();
1544 _.each(pages, function(page, i) {
1547 page.__ic.on("change:effective_invisible", null, function() {
1548 if (!page.__ic.get('effective_invisible') && page.autofocus) {
1549 $new_notebook.tabs('select', i);
1552 var current = $new_notebook.tabs("option", "selected");
1553 if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
1555 var first_visible = _.find(_.range(pages.length), function(i2) {
1556 return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
1558 if (first_visible !== undefined) {
1559 $new_notebook.tabs('select', first_visible);
1564 this.handle_common_properties($new_notebook, $notebook);
1565 return $new_notebook;
1567 process_separator: function($separator) {
1568 var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
1569 $separator.before($new_separator).remove();
1570 this.handle_common_properties($new_separator, $separator);
1571 return $new_separator;
1573 process_label: function($label) {
1574 var name = $label.attr("for"),
1575 field_orm = this.fvg.fields[name];
1577 string: $label.attr('string') || (field_orm || {}).string || '',
1578 help: $label.attr('help') || (field_orm || {}).help || '',
1579 _for: name ? _.uniqueId('oe-field-input-') : undefined,
1581 var align = parseFloat(dict.align);
1582 if (isNaN(align) || align === 1) {
1584 } else if (align === 0) {
1590 var $new_label = this.render_element('FormRenderingLabel', dict);
1591 $label.before($new_label).remove();
1592 this.handle_common_properties($new_label, $label);
1594 this.labels[name] = $new_label;
1598 handle_common_properties: function($new_element, $node) {
1599 var str_modifiers = $node.attr("modifiers") || "{}";
1600 var modifiers = JSON.parse(str_modifiers);
1602 if (modifiers.invisible !== undefined)
1603 ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
1604 $new_element.addClass($node.attr("class") || "");
1605 $new_element.attr('style', $node.attr('style'));
1606 return {invisibility_changer: ic,};
1613 If you read this documentation, it probably means that you were asked to use a form view widget outside of
1614 a form view. Before going further, you must understand that those fields were never really created for
1615 that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
1616 you to hack the system with more style.
1618 instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
1619 init: function(parent, eval_context) {
1620 this._super(parent);
1621 this.field_descs = {};
1622 this.eval_context = eval_context || {};
1624 display_invalid_fields: false,
1625 actual_mode: 'create',
1628 get_field_desc: function(field_name) {
1629 if (this.field_descs[field_name] === undefined) {
1630 this.field_descs[field_name] = {
1634 return this.field_descs[field_name];
1636 extend_field_desc: function(fields) {
1638 _.each(fields, function(v, k) {
1639 _.extend(self.get_field_desc(k), v);
1642 get_field_value: function(field_name) {
1645 set_values: function(values) {
1648 compute_domain: function(expression) {
1649 return instance.web.form.compute_domain(expression, {});
1651 build_eval_context: function() {
1652 return new instance.web.CompoundContext(this.eval_context);
1656 instance.web.form.compute_domain = function(expr, fields) {
1657 if (! (expr instanceof Array))
1660 for (var i = expr.length - 1; i >= 0; i--) {
1662 if (ex.length == 1) {
1663 var top = stack.pop();
1666 stack.push(stack.pop() || top);
1669 stack.push(stack.pop() && top);
1675 throw new Error(_.str.sprintf(
1676 _t("Unknown operator %s in domain %s"),
1677 ex, JSON.stringify(expr)));
1681 var field = fields[ex[0]];
1683 throw new Error(_.str.sprintf(
1684 _t("Unknown field %s in domain %s"),
1685 ex[0], JSON.stringify(expr)));
1687 var field_value = field.get_value ? field.get_value() : field.value;
1691 switch (op.toLowerCase()) {
1694 stack.push(_.isEqual(field_value, val));
1698 stack.push(!_.isEqual(field_value, val));
1701 stack.push(field_value < val);
1704 stack.push(field_value > val);
1707 stack.push(field_value <= val);
1710 stack.push(field_value >= val);
1713 if (!_.isArray(val)) val = [val];
1714 stack.push(_(val).contains(field_value));
1717 if (!_.isArray(val)) val = [val];
1718 stack.push(!_(val).contains(field_value));
1722 _t("Unsupported operator %s in domain %s"),
1723 op, JSON.stringify(expr));
1726 return _.all(stack, _.identity);
1729 instance.web.form.is_bin_size = function(v) {
1730 return (/^\d+(\.\d*)? \w+$/).test(v);
1734 * Must be applied over an class already possessing the PropertiesMixin.
1736 * Apply the result of the "invisible" domain to this.$el.
1738 instance.web.form.InvisibilityChangerMixin = {
1739 init: function(field_manager, invisible_domain) {
1741 this._ic_field_manager = field_manager;
1742 this._ic_invisible_modifier = invisible_domain;
1743 this._ic_field_manager.on("view_content_has_changed", this, function() {
1744 var result = self._ic_invisible_modifier === undefined ? false :
1745 self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
1746 self.set({"invisible": result});
1748 this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
1749 var check = function() {
1750 if (self.get("invisible") || self.get('force_invisible')) {
1751 self.set({"effective_invisible": true});
1753 self.set({"effective_invisible": false});
1756 this.on('change:invisible', this, check);
1757 this.on('change:force_invisible', this, check);
1761 this.on("change:effective_invisible", this, this._check_visibility);
1762 this._check_visibility();
1764 _check_visibility: function() {
1765 this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
1769 instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
1770 init: function(parent, field_manager, invisible_domain, $el) {
1771 this.setParent(parent);
1772 instance.web.PropertiesMixin.init.call(this);
1773 instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
1780 Base class for all fields, custom widgets and buttons to be displayed in the form view.
1783 - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
1784 the values of the "readonly" property and the "mode" property on the field manager.
1786 instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
1788 * @constructs instance.web.form.FormWidget
1789 * @extends instance.web.Widget
1791 * @param field_manager
1794 init: function(field_manager, node) {
1795 this._super(field_manager);
1796 this.field_manager = field_manager;
1797 if (this.field_manager instanceof instance.web.FormView)
1798 this.view = this.field_manager;
1800 this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
1801 instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
1803 this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
1809 // some events to make the property "effective_readonly" sync automatically with "readonly" and
1810 // "mode" on field_manager
1812 var test_effective_readonly = function() {
1813 self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
1815 this.on("change:readonly", this, test_effective_readonly);
1816 this.field_manager.on("change:actual_mode", this, test_effective_readonly);
1817 test_effective_readonly.call(this);
1819 renderElement: function() {
1820 this.process_modifiers();
1822 this.$el.addClass(this.node.attrs["class"] || "");
1824 destroy: function() {
1825 $.fn.tooltip('destroy');
1826 this._super.apply(this, arguments);
1829 * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
1831 * This method is an utility method that is meant to be called by child classes.
1833 * @param {jQuery} $e jQuery object of elements to bind focus/blur on
1835 setupFocus: function ($e) {
1838 focus: function () { self.trigger('focused'); },
1839 blur: function () { self.trigger('blurred'); }
1842 process_modifiers: function() {
1844 for (var a in this.modifiers) {
1845 if (!this.modifiers.hasOwnProperty(a)) { continue; }
1846 if (!_.include(["invisible"], a)) {
1847 var val = this.field_manager.compute_domain(this.modifiers[a]);
1853 do_attach_tooltip: function(widget, trigger, options) {
1854 widget = widget || this;
1855 trigger = trigger || this.$el;
1856 options = _.extend({
1857 delay: { show: 500, hide: 0 },
1859 var template = widget.template + '.tooltip';
1860 if (!QWeb.has_template(template)) {
1861 template = 'WidgetLabel.tooltip';
1863 return QWeb.render(template, {
1864 debug: instance.session.debug,
1869 //only show tooltip if we are in debug or if we have a help to show, otherwise it will display
1871 if (instance.session.debug || widget.node.attrs.help || (widget.field && widget.field.help)){
1872 $(trigger).tooltip(options);
1876 * Builds a new context usable for operations related to fields by merging
1877 * the fields'context with the action's context.
1879 build_context: function() {
1880 // only use the model's context if there is not context on the node
1881 var v_context = this.node.attrs.context;
1883 v_context = (this.field || {}).context || {};
1886 if (v_context.__ref || true) { //TODO: remove true
1887 var fields_values = this.field_manager.build_eval_context();
1888 v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
1892 build_domain: function() {
1893 var f_domain = this.field.domain || [];
1894 var n_domain = this.node.attrs.domain || null;
1895 // if there is a domain on the node, overrides the model's domain
1896 var final_domain = n_domain !== null ? n_domain : f_domain;
1897 if (!(final_domain instanceof Array) || true) { //TODO: remove true
1898 var fields_values = this.field_manager.build_eval_context();
1899 final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
1901 return final_domain;
1905 instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
1906 template: 'WidgetButton',
1907 init: function(field_manager, node) {
1908 node.attrs.type = node.attrs['data-button-type'];
1909 this.is_stat_button = /\boe_stat_button\b/.test(node.attrs['class']);
1910 this.icon_class = node.attrs.icon && "stat_button_icon fa " + node.attrs.icon + " fa-fw";
1911 this._super(field_manager, node);
1912 this.force_disabled = false;
1913 this.string = (this.node.attrs.string || '').replace(/_/g, '');
1914 if (JSON.parse(this.node.attrs.default_focus || "0")) {
1915 // TODO fme: provide enter key binding to widgets
1916 this.view.default_focus_button = this;
1918 if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
1919 this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
1923 this._super.apply(this, arguments);
1924 this.view.on('view_content_has_changed', this, this.check_disable);
1925 this.check_disable();
1926 this.$el.click(this.on_click);
1927 if (this.node.attrs.help || instance.session.debug) {
1928 this.do_attach_tooltip();
1930 this.setupFocus(this.$el);
1932 on_click: function() {
1934 this.force_disabled = true;
1935 this.check_disable();
1936 this.execute_action().always(function() {
1937 self.force_disabled = false;
1938 self.check_disable();
1941 execute_action: function() {
1943 var exec_action = function() {
1944 if (self.node.attrs.confirm) {
1945 var def = $.Deferred();
1946 var dialog = new instance.web.Dialog(this, {
1947 title: _t('Confirm'),
1949 {text: _t("Cancel"), click: function() {
1950 this.parents('.modal').modal('hide');
1953 {text: _t("Ok"), click: function() {
1955 self.on_confirmed().always(function() {
1956 self2.parents('.modal').modal('hide');
1961 }, $('<div/>').text(self.node.attrs.confirm)).open();
1962 dialog.on("closing", null, function() {def.resolve();});
1963 return def.promise();
1965 return self.on_confirmed();
1968 if (!this.node.attrs.special) {
1969 return this.view.recursive_save().then(exec_action);
1971 return exec_action();
1974 on_confirmed: function() {
1977 var context = this.build_context();
1978 return this.view.do_execute_action(
1979 _.extend({}, this.node.attrs, {context: context}),
1980 this.view.dataset, this.view.datarecord.id, function (reason) {
1981 if (!_.isObject(reason)) {
1982 self.view.recursive_reload();
1986 check_disable: function() {
1987 var disabled = (this.force_disabled || !this.view.is_interactible_record());
1988 this.$el.prop('disabled', disabled);
1989 this.$el.css('color', disabled ? 'grey' : '');
1994 * Interface to be implemented by fields.
1997 * - changed_value: triggered when the value of the field has changed. This can be due
1998 * to a user interaction or a call to set_value().
2001 instance.web.form.FieldInterface = {
2003 * Constructor takes 2 arguments:
2004 * - field_manager: Implements FieldManagerMixin
2005 * - node: the "<field>" node in json form
2007 init: function(field_manager, node) {},
2009 * Called by the form view to indicate the value of the field.
2011 * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
2012 * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
2013 * before the widget is inserted into the DOM.
2015 * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
2016 * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
2017 * also be able to handle any other format commonly used in the _defaults key on the models in the addons
2018 * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
2019 * no information is ever given to know which format is used.
2021 set_value: function(value_) {},
2023 * Get the current value of the widget.
2025 * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
2026 * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
2027 * For example if the field is marked as "required", a call to get_value() can return false.
2029 * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
2030 * return a default value according to the type of field.
2032 * This method is always assumed to perform synchronously, it can not return a promise.
2034 * If there was no user interaction to modify the value of the field, it is always assumed that
2035 * get_value() return the same semantic value than the one passed in the last call to set_value(),
2036 * although the syntax can be different. This can be the case for type of fields that have a different
2037 * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
2039 get_value: function() {},
2041 * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
2044 set_input_id: function(id) {},
2046 * Returns true if is_syntax_valid() returns true and the value is semantically
2047 * valid too according to the semantic restrictions applied to the field.
2049 is_valid: function() {},
2051 * Returns true if the field holds a value which is syntactically correct, ignoring
2052 * the potential semantic restrictions applied to the field.
2054 is_syntax_valid: function() {},
2056 * Must set the focus on the field. Return false if field is not focusable.
2058 focus: function() {},
2060 * Called when the translate button is clicked.
2062 on_translate: function() {},
2064 This method is called by the form view before reading on_change values and before saving. It tells
2065 the field to save its value before reading it using get_value(). Must return a promise.
2067 commit_value: function() {},
2071 * Abstract class for classes implementing FieldInterface.
2074 * - value: useful property to hold the value of the field. By default, set_value() and get_value()
2075 * set and retrieve the value property. Changing the value property also triggers automatically
2076 * a 'changed_value' event that inform the view to trigger on_changes.
2079 instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
2081 * @constructs instance.web.form.AbstractField
2082 * @extends instance.web.form.FormWidget
2084 * @param field_manager
2087 init: function(field_manager, node) {
2089 this._super(field_manager, node);
2090 this.name = this.node.attrs.name;
2091 this.field = this.field_manager.get_field_desc(this.name);
2092 this.widget = this.node.attrs.widget;
2093 this.string = this.node.attrs.string || this.field.string || this.name;
2094 this.options = instance.web.py_eval(this.node.attrs.options || '{}');
2095 this.set({'value': false});
2097 this.on("change:value", this, function() {
2098 this.trigger('changed_value');
2099 this._check_css_flags();
2102 renderElement: function() {
2105 if (this.field.translate && this.view) {
2106 this.$el.addClass('oe_form_field_translatable');
2107 this.$el.find('.oe_field_translate').click(this.on_translate);
2109 this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
2110 this.do_attach_tooltip(this, this.$label[0] || this.$el);
2111 if (instance.session.debug) {
2112 this.$label.off('dblclick').on('dblclick', function() {
2113 console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
2115 console.log("window.w =", window.w);
2118 if (!this.disable_utility_classes) {
2119 this.off("change:required", this, this._set_required);
2120 this.on("change:required", this, this._set_required);
2121 this._set_required();
2123 this._check_visibility();
2124 this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
2125 this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
2126 this._check_css_flags();
2129 var tmp = this._super();
2130 this.on("change:value", this, function() {
2131 if (! this.no_rerender)
2132 this.render_value();
2134 this.render_value();
2137 * Private. Do not use.
2139 _set_required: function() {
2140 this.$el.toggleClass('oe_form_required', this.get("required"));
2142 set_value: function(value_) {
2143 this.set({'value': value_});
2145 get_value: function() {
2146 return this.get('value');
2149 Utility method that all implementations should use to change the
2150 value without triggering a re-rendering.
2152 internal_set_value: function(value_) {
2153 var tmp = this.no_rerender;
2154 this.no_rerender = true;
2155 this.set({'value': value_});
2156 this.no_rerender = tmp;
2159 This method is called each time the value is modified.
2161 render_value: function() {},
2162 is_valid: function() {
2163 return this.is_syntax_valid() && !(this.get('required') && this.is_false());
2165 is_syntax_valid: function() {
2169 * Method useful to implement to ease validity testing. Must return true if the current
2170 * value is similar to false in OpenERP.
2172 is_false: function() {
2173 return this.get('value') === false;
2175 _check_css_flags: function() {
2176 if (this.field.translate) {
2177 this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
2179 if (!this.disable_utility_classes) {
2180 if (this.field_manager.get('display_invalid_fields')) {
2181 this.$el.toggleClass('oe_form_invalid', !this.is_valid());
2188 set_input_id: function(id) {
2189 this.id_for_label = id;
2191 on_translate: function() {
2193 var trans = new instance.web.DataSet(this, 'ir.translation');
2194 return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
2199 set_dimensions: function (height, width) {
2205 commit_value: function() {
2211 * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
2214 instance.web.form.ReinitializeWidgetMixin = {
2216 * Default implementation of, you should not override it, use initialize_field() instead.
2219 this.initialize_field();
2222 initialize_field: function() {
2223 this.on("change:effective_readonly", this, this.reinitialize);
2224 this.initialize_content();
2226 reinitialize: function() {
2227 this.destroy_content();
2228 this.renderElement();
2229 this.initialize_content();
2232 * Called to destroy anything that could have been created previously, called before a
2233 * re-initialization.
2235 destroy_content: function() {},
2237 * Called to initialize the content.
2239 initialize_content: function() {},
2243 * A mixin to apply on any field that has to completely re-render when its readonly state
2246 instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
2247 reinitialize: function() {
2248 instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
2249 var res = this.render_value();
2250 if (this.view && this.view.render_value_defs){
2251 this.view.render_value_defs.push(res);
2257 Some hack to make placeholders work in ie9.
2259 if (!('placeholder' in document.createElement('input'))) {
2260 document.addEventListener("DOMNodeInserted",function(event){
2261 var nodename = event.target.nodeName.toLowerCase();
2262 if ( nodename === "input" || nodename == "textarea" ) {
2263 $(event.target).placeholder();
2268 instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2269 template: 'FieldChar',
2270 widget_class: 'oe_form_field_char',
2272 'change input': 'store_dom_value',
2274 init: function (field_manager, node) {
2275 this._super(field_manager, node);
2276 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2278 initialize_content: function() {
2279 this.setupFocus(this.$('input'));
2281 store_dom_value: function () {
2282 if (!this.get('effective_readonly')
2283 && this.$('input').length
2284 && this.is_syntax_valid()) {
2285 this.internal_set_value(
2287 this.$('input').val()));
2290 commit_value: function () {
2291 this.store_dom_value();
2292 return this._super();
2294 render_value: function() {
2295 var show_value = this.format_value(this.get('value'), '');
2296 if (!this.get("effective_readonly")) {
2297 this.$el.find('input').val(show_value);
2299 if (this.password) {
2300 show_value = new Array(show_value.length + 1).join('*');
2302 this.$(".oe_form_char_content").text(show_value);
2305 is_syntax_valid: function() {
2306 if (!this.get("effective_readonly") && this.$("input").size() > 0) {
2308 this.parse_value(this.$('input').val(), '');
2316 parse_value: function(val, def) {
2317 return instance.web.parse_value(val, this, def);
2319 format_value: function(val, def) {
2320 return instance.web.format_value(val, this, def);
2322 is_false: function() {
2323 return this.get('value') === '' || this._super();
2326 var input = this.$('input:first')[0];
2327 return input ? input.focus() : false;
2329 set_dimensions: function (height, width) {
2330 this._super(height, width);
2331 this.$('input').css({
2338 instance.web.form.KanbanSelection = instance.web.form.FieldChar.extend({
2339 init: function (field_manager, node) {
2340 this._super(field_manager, node);
2342 prepare_dropdown_selection: function() {
2345 var selection = self.field.selection || [];
2346 _.map(selection, function(res) {
2350 'state_name': res[1],
2352 if (res[0] == 'normal') { value['state_class'] = 'oe_kanban_status'; }
2353 else if (res[0] == 'done') { value['state_class'] = 'oe_kanban_status oe_kanban_status_green'; }
2354 else { value['state_class'] = 'oe_kanban_status oe_kanban_status_red'; }
2359 render_value: function() {
2361 this.record_id = this.view.datarecord.id;
2362 this.states = this.prepare_dropdown_selection();;
2363 this.$el.html(QWeb.render("KanbanSelection", {'widget': self}));
2364 this.$el.find('li').on('click', this.set_kanban_selection.bind(this));
2366 /* setting the value: in view mode, perform an asynchronous call and reload
2367 the form view; in edit mode, use set_value to save the new value that will
2368 be written when saving the record. */
2369 set_kanban_selection: function (ev) {
2371 var li = $(ev.target).closest('li');
2373 var value = String(li.data('value'));
2374 if (this.view.get('actual_mode') == 'view') {
2375 var write_values = {}
2376 write_values[self.name] = value;
2377 return this.view.dataset._model.call(
2381 self.view.dataset.get_context()
2382 ]).done(self.reload_record.bind(self));
2385 return this.set_value(value);
2389 reload_record: function() {
2394 instance.web.form.Priority = instance.web.form.FieldChar.extend({
2395 init: function (field_manager, node) {
2396 this._super(field_manager, node);
2398 prepare_priority: function() {
2400 var selection = this.field.selection || [];
2401 var init_value = selection && selection[0][0] || 0;
2402 var data = _.map(selection.slice(1), function(element, index) {
2404 'value': element[0],
2406 'click_value': element[0],
2408 if (index == 0 && self.get('value') == element[0]) {
2409 value['click_value'] = init_value;
2415 render_value: function() {
2417 this.record_id = this.view.datarecord.id;
2418 this.priorities = this.prepare_priority();
2419 this.$el.html(QWeb.render("Priority", {'widget': this}));
2420 this.$el.find('li').on('click', this.set_priority.bind(this));
2422 /* setting the value: in view mode, perform an asynchronous call and reload
2423 the form view; in edit mode, use set_value to save the new value that will
2424 be written when saving the record. */
2425 set_priority: function (ev) {
2427 var li = $(ev.target).closest('li');
2429 var value = String(li.data('value'));
2430 if (this.view.get('actual_mode') == 'view') {
2431 var write_values = {}
2432 write_values[self.name] = value;
2433 return this.view.dataset._model.call(
2437 self.view.dataset.get_context()
2438 ]).done(self.reload_record.bind(self));
2441 return this.set_value(value);
2446 reload_record: function() {
2451 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2452 process_modifiers: function () {
2454 this.set({ readonly: true });
2458 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2459 template: 'FieldEmail',
2460 initialize_content: function() {
2462 var $button = this.$el.find('button');
2463 $button.click(this.on_button_clicked);
2464 this.setupFocus($button);
2466 render_value: function() {
2467 if (!this.get("effective_readonly")) {
2471 .attr('href', 'mailto:' + this.get('value'))
2472 .text(this.get('value') || '');
2475 on_button_clicked: function() {
2476 if (!this.get('value') || !this.is_syntax_valid()) {
2477 this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2479 location.href = 'mailto:' + this.get('value');
2484 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2485 template: 'FieldUrl',
2486 initialize_content: function() {
2488 var $button = this.$el.find('button');
2489 $button.click(this.on_button_clicked);
2490 this.setupFocus($button);
2492 render_value: function() {
2493 if (!this.get("effective_readonly")) {
2496 var tmp = this.get('value');
2497 var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2499 tmp = "http://" + this.get('value');
2501 var text = this.get('value') ? this.node.attrs.text || tmp : '';
2502 this.$el.find('a').attr('href', tmp).text(text);
2505 on_button_clicked: function() {
2506 if (!this.get('value')) {
2507 this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2509 var url = $.trim(this.get('value'));
2510 if(/^www\./i.test(url))
2511 url = 'http://'+url;
2517 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2518 is_field_number: true,
2519 widget_class: 'oe_form_field_float',
2520 init: function (field_manager, node) {
2521 this._super(field_manager, node);
2522 this.internal_set_value(0);
2523 if (this.node.attrs.digits) {
2524 this.digits = this.node.attrs.digits;
2526 this.digits = this.field.digits;
2529 set_value: function(value_) {
2530 if (value_ === false || value_ === undefined) {
2531 // As in GTK client, floats default to 0
2534 this._super.apply(this, [value_]);
2536 focus: function () {
2537 var $input = this.$('input:first');
2538 return $input.length ? $input.select() : false;
2542 instance.web.form.FieldCharDomain = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2543 init: function(field_manager, node) {
2544 this._super.apply(this, arguments);
2548 this._super.apply(this, arguments);
2549 this.on("change:effective_readonly", this, function () {
2550 this.display_field();
2552 this.display_field();
2553 return this._super();
2555 set_value: function(value_) {
2557 this.set('value', value_ || false);
2558 this.display_field();
2560 display_field: function() {
2562 this.$el.html(instance.web.qweb.render("FieldCharDomain", {widget: this}));
2563 if (this.get('value')) {
2564 var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2565 var domain = instance.web.pyeval.eval('domain', this.get('value'));
2566 var ds = new instance.web.DataSetStatic(self, model, self.build_context());
2567 ds.call('search_count', [domain]).then(function (results) {
2568 $('.oe_domain_count', self.$el).text(results + ' records selected');
2569 if (self.get('effective_readonly')) {
2570 $('button span', self.$el).text(' See selection');
2573 $('button span', self.$el).text(' Change selection');
2577 $('.oe_domain_count', this.$el).text('0 record selected');
2578 $('button span', this.$el).text(' Select records');
2580 this.$('.select_records').on('click', self.on_click);
2582 on_click: function(event) {
2583 event.preventDefault();
2585 var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2586 this.pop = new instance.web.form.SelectCreatePopup(this);
2587 this.pop.select_element(
2589 title: this.get('effective_readonly') ? 'Selected records' : 'Select records...',
2590 readonly: this.get('effective_readonly'),
2591 disable_multiple_selection: this.get('effective_readonly'),
2592 no_create: this.get('effective_readonly'),
2593 }, [], this.build_context());
2594 this.pop.on("elements_selected", self, function(element_ids) {
2595 if (this.pop.$('input.oe_list_record_selector').prop('checked')) {
2596 var search_data = this.pop.searchview.build_search_data();
2597 var domain_done = instance.web.pyeval.eval_domains_and_contexts({
2598 domains: search_data.domains,
2599 contexts: search_data.contexts,
2600 group_by_seq: search_data.groupbys || []
2601 }).then(function (results) {
2602 return results.domain;
2606 var domain = [["id", "in", element_ids]];
2607 var domain_done = $.Deferred().resolve(domain);
2609 $.when(domain_done).then(function (domain) {
2610 var domain = self.pop.dataset.domain.concat(domain || []);
2611 self.set_value(domain);
2617 instance.web.DateTimeWidget = instance.web.Widget.extend({
2618 template: "web.datepicker",
2619 type_of_date: "datetime",
2621 'dp.change .oe_datepicker_main': 'change_datetime',
2622 'dp.show .oe_datepicker_main': 'set_datetime_default',
2623 'keypress .oe_datepicker_master': 'change_datetime',
2625 init: function(parent) {
2626 this._super(parent);
2627 this.name = parent.name;
2631 var l10n = _t.database.parameters;
2635 startDate: new moment({ y: 1900 }),
2636 endDate: new moment().add(200, "y"),
2637 calendarWeeks: true,
2639 time: 'fa fa-clock-o',
2640 date: 'fa fa-calendar',
2641 up: 'fa fa-chevron-up',
2642 down: 'fa fa-chevron-down'
2644 language : moment.locale(),
2645 format : instance.web.convert_to_moment_format(l10n.date_format +' '+ l10n.time_format),
2647 this.$input = this.$el.find('input.oe_datepicker_master');
2648 if (this.type_of_date === 'date') {
2649 options['pickTime'] = false;
2650 options['useSeconds'] = false;
2651 options['format'] = instance.web.convert_to_moment_format(l10n.date_format);
2653 this.picker = this.$('.oe_datepicker_main').datetimepicker(options);
2654 this.set_readonly(false);
2655 this.set({'value': false});
2657 set_value: function(value_) {
2658 this.set({'value': value_});
2659 this.$input.val(value_ ? this.format_client(value_) : '');
2661 get_value: function() {
2662 return this.get('value');
2664 set_value_from_ui_: function() {
2665 var value_ = this.$input.val() || false;
2666 this.set_value(this.parse_client(value_));
2668 set_readonly: function(readonly) {
2669 this.readonly = readonly;
2670 this.$input.prop('readonly', this.readonly);
2672 is_valid_: function() {
2673 var value_ = this.$input.val();
2674 if (value_ === "") {
2678 this.parse_client(value_);
2685 parse_client: function(v) {
2686 return instance.web.parse_value(v, {"widget": this.type_of_date});
2688 format_client: function(v) {
2689 return instance.web.format_value(v, {"widget": this.type_of_date});
2691 set_datetime_default: function(){
2692 //when opening datetimepicker the date and time by default should be the one from
2693 //the input field if any or the current day otherwise
2694 if (this.type_of_date === 'datetime') {
2695 value = new moment().second(0);
2696 if (this.$input.val().length !== 0 && this.is_valid_()){
2697 var value = this.$input.val();
2699 this.$('.oe_datepicker_main').data('DateTimePicker').setValue(value);
2702 change_datetime: function(e) {
2703 if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
2704 this.set_value_from_ui_();
2705 this.trigger("datetime_changed");
2708 commit_value: function () {
2709 this.change_datetime();
2713 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2714 type_of_date: "date"
2717 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2718 template: "FieldDatetime",
2719 build_widget: function() {
2720 return new instance.web.DateTimeWidget(this);
2722 destroy_content: function() {
2723 if (this.datewidget) {
2724 this.datewidget.destroy();
2725 this.datewidget = undefined;
2728 initialize_content: function() {
2729 if (!this.get("effective_readonly")) {
2730 this.datewidget = this.build_widget();
2731 this.datewidget.on('datetime_changed', this, _.bind(function() {
2732 this.internal_set_value(this.datewidget.get_value());
2734 this.datewidget.appendTo(this.$el);
2735 this.setupFocus(this.datewidget.$input);
2738 render_value: function() {
2739 if (!this.get("effective_readonly")) {
2740 this.datewidget.set_value(this.get('value'));
2742 this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2745 is_syntax_valid: function() {
2746 if (!this.get("effective_readonly") && this.datewidget) {
2747 return this.datewidget.is_valid_();
2751 is_false: function() {
2752 return this.get('value') === '' || this._super();
2755 var input = this.datewidget && this.datewidget.$input[0];
2756 return input ? input.focus() : false;
2758 set_dimensions: function (height, width) {
2759 this._super(height, width);
2760 if (!this.get("effective_readonly")) {
2761 this.datewidget.$input.css('height', height);
2766 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2767 template: "FieldDate",
2768 build_widget: function() {
2769 return new instance.web.DateWidget(this);
2773 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2774 template: 'FieldText',
2776 'keyup': function (e) {
2777 if (e.which === $.ui.keyCode.ENTER) {
2778 e.stopPropagation();
2781 'keypress': function (e) {
2782 if (e.which === $.ui.keyCode.ENTER) {
2783 e.stopPropagation();
2786 'change textarea': 'store_dom_value',
2788 initialize_content: function() {
2790 if (! this.get("effective_readonly")) {
2791 this.$textarea = this.$el.find('textarea');
2792 this.auto_sized = false;
2793 this.default_height = this.$textarea.css('height');
2794 if (this.get("effective_readonly")) {
2795 this.$textarea.attr('disabled', 'disabled');
2797 this.setupFocus(this.$textarea);
2799 this.$textarea = undefined;
2802 commit_value: function () {
2803 if (! this.get("effective_readonly") && this.$textarea) {
2804 this.store_dom_value();
2806 return this._super();
2808 store_dom_value: function () {
2809 this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2811 render_value: function() {
2812 if (! this.get("effective_readonly")) {
2813 var show_value = instance.web.format_value(this.get('value'), this, '');
2814 if (show_value === '') {
2815 this.$textarea.css('height', parseInt(this.default_height, 10)+"px");
2817 this.$textarea.val(show_value);
2818 if (! this.auto_sized) {
2819 this.auto_sized = true;
2820 this.$textarea.autosize();
2822 this.$textarea.trigger("autosize");
2825 var txt = this.get("value") || '';
2826 this.$(".oe_form_text_content").text(txt);
2829 is_syntax_valid: function() {
2830 if (!this.get("effective_readonly") && this.$textarea) {
2832 instance.web.parse_value(this.$textarea.val(), this, '');
2840 is_false: function() {
2841 return this.get('value') === '' || this._super();
2843 focus: function($el) {
2844 var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2845 return input ? input.focus() : false;
2847 set_dimensions: function (height, width) {
2848 this._super(height, width);
2849 if (!this.get("effective_readonly") && this.$textarea) {
2850 this.$textarea.css({
2859 * FieldTextHtml Widget
2860 * Intended for FieldText widgets meant to display HTML content. This
2861 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2862 * To find more information about CLEditor configutation: go to
2863 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2865 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2866 template: 'FieldTextHtml',
2868 this._super.apply(this, arguments);
2870 initialize_content: function() {
2872 if (! this.get("effective_readonly")) {
2873 self._updating_editor = false;
2874 this.$textarea = this.$el.find('textarea');
2875 var width = ((this.node.attrs || {}).editor_width || 'calc(100% - 4px)');
2876 var height = ((this.node.attrs || {}).editor_height || 250);
2877 this.$textarea.cleditor({
2878 width: width, // width not including margins, borders or padding
2879 height: height, // height not including margins, borders or padding
2880 controls: // controls to add to the toolbar
2881 "bold italic underline strikethrough " +
2882 "| removeformat | bullets numbering | outdent " +
2883 "indent | link unlink | source",
2884 bodyStyle: // style to assign to document body contained within the editor
2885 "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2887 this.$cleditor = this.$textarea.cleditor()[0];
2888 this.$cleditor.change(function() {
2889 if (! self._updating_editor) {
2890 self.$cleditor.updateTextArea();
2891 self.internal_set_value(self.$textarea.val());
2894 if (this.field.translate) {
2895 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"/>')
2896 .click(this.on_translate);
2897 this.$cleditor.$toolbar.append($img);
2901 render_value: function() {
2902 if (! this.get("effective_readonly")) {
2903 this.$textarea.val(this.get('value') || '');
2904 this._updating_editor = true;
2905 this.$cleditor.updateFrame();
2906 this._updating_editor = false;
2908 this.$el.html(this.get('value'));
2913 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2914 template: 'FieldBoolean',
2917 this.$checkbox = $("input", this.$el);
2918 this.setupFocus(this.$checkbox);
2919 this.$el.click(_.bind(function() {
2920 this.internal_set_value(this.$checkbox.is(':checked'));
2922 var check_readonly = function() {
2923 self.$checkbox.prop('disabled', self.get("effective_readonly"));
2924 self.click_disabled_boolean();
2926 this.on("change:effective_readonly", this, check_readonly);
2927 check_readonly.call(this);
2928 this._super.apply(this, arguments);
2930 render_value: function() {
2931 this.$checkbox[0].checked = this.get('value');
2934 var input = this.$checkbox && this.$checkbox[0];
2935 return input ? input.focus() : false;
2937 click_disabled_boolean: function(){
2938 var $disabled = this.$el.find('input[type=checkbox]:disabled');
2939 $disabled.each(function (){
2940 $(this).next('div').remove();
2941 $(this).closest("span").append($('<div class="boolean"></div>'));
2947 The progressbar field expect a float from 0 to 100.
2949 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2950 template: 'FieldProgressBar',
2951 render_value: function() {
2952 this.$el.progressbar({
2953 value: this.get('value') || 0,
2954 disabled: this.get("effective_readonly")
2956 var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
2957 this.$('span').html(formatted_value + '%');
2962 The PercentPie field expect a float from 0 to 100.
2964 instance.web.form.FieldPercentPie = instance.web.form.AbstractField.extend({
2965 template: 'FieldPercentPie',
2967 render_value: function() {
2968 var value = this.get('value'),
2969 formatted_value = Math.round(value || 0) + '%',
2970 svg = this.$('svg')[0];
2973 nv.addGraph(function() {
2974 var width = 42, height = 42;
2975 var chart = nv.models.pieChart()
2978 .margin({top: 0, right: 0, bottom: 0, left: 0})
2983 .color(['#7C7BAD','#DDD'])
2987 .datum([{'x': 'value', 'y': value}, {'x': 'complement', 'y': 100 - value}])
2990 .attr('style', 'width: ' + width + 'px; height:' + height + 'px;');
2994 .attr({x: width/2, y: height/2 + 3, 'text-anchor': 'middle'})
2995 .style({"font-size": "10px", "font-weight": "bold"})
2996 .text(formatted_value);
3005 The FieldBarChart expectsa list of values (indeed)
3007 instance.web.form.FieldBarChart = instance.web.form.AbstractField.extend({
3008 template: 'FieldBarChart',
3010 render_value: function() {
3011 var value = JSON.parse(this.get('value'));
3012 var svg = this.$('svg')[0];
3014 nv.addGraph(function() {
3015 var width = 34, height = 34;
3016 var chart = nv.models.discreteBarChart()
3017 .x(function (d) { return d.tooltip })
3018 .y(function (d) { return d.value })
3021 .margin({top: 0, right: 0, bottom: 0, left: 0})
3024 .transitionDuration(350)
3029 .datum([{key: 'values', values: value}])
3032 .attr('style', 'width: ' + (width + 4) + 'px; height: ' + (height + 8) + 'px;');
3034 nv.utils.windowResize(chart.update);
3043 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3044 template: 'FieldSelection',
3046 'change select': 'store_dom_value',
3048 init: function(field_manager, node) {
3050 this._super(field_manager, node);
3051 this.set("value", false);
3052 this.set("values", []);
3053 this.records_orderer = new instance.web.DropMisordered();
3054 this.field_manager.on("view_content_has_changed", this, function() {
3055 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
3056 if (! _.isEqual(domain, this.get("domain"))) {
3057 this.set("domain", domain);
3061 initialize_field: function() {
3062 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3063 this.on("change:domain", this, this.query_values);
3064 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
3065 this.on("change:values", this, this.render_value);
3067 query_values: function() {
3070 if (this.field.type === "many2one") {
3071 var model = new openerp.Model(openerp.session, this.field.relation);
3072 def = model.call("name_search", ['', this.get("domain")], {"context": this.build_context()});
3074 var values = _.reject(this.field.selection, function (v) { return v[0] === false && v[1] === ''; });
3075 def = $.when(values);
3077 this.records_orderer.add(def).then(function(values) {
3078 if (! _.isEqual(values, self.get("values"))) {
3079 self.set("values", values);
3083 initialize_content: function() {
3084 // Flag indicating whether we're in an event chain containing a change
3085 // event on the select, in order to know what to do on keyup[RETURN]:
3086 // * If the user presses [RETURN] as part of changing the value of a
3087 // selection, we should just let the value change and not let the
3088 // event broadcast further (e.g. to validating the current state of
3089 // the form in editable list view, which would lead to saving the
3090 // current row or switching to the next one)
3091 // * If the user presses [RETURN] with a select closed (side-effect:
3092 // also if the user opened the select and pressed [RETURN] without
3093 // changing the selected value), takes the action as validating the
3095 var ischanging = false;
3096 var $select = this.$el.find('select')
3097 .change(function () { ischanging = true; })
3098 .click(function () { ischanging = false; })
3099 .keyup(function (e) {
3100 if (e.which !== 13 || !ischanging) { return; }
3101 e.stopPropagation();
3104 this.setupFocus($select);
3106 commit_value: function () {
3107 this.store_dom_value();
3108 return this._super();
3110 store_dom_value: function () {
3111 if (!this.get('effective_readonly') && this.$('select').length) {
3112 var val = JSON.parse(this.$('select').val());
3113 this.internal_set_value(val);
3116 set_value: function(value_) {
3117 value_ = value_ === null ? false : value_;
3118 value_ = value_ instanceof Array ? value_[0] : value_;
3119 this._super(value_);
3121 render_value: function() {
3122 var values = this.get("values");
3123 values = [[false, this.node.attrs.placeholder || '']].concat(values);
3124 var found = _.find(values, function(el) { return el[0] === this.get("value"); }, this);
3126 found = [this.get("value"), _t('Unknown')];
3127 values = [found].concat(values);
3129 if (! this.get("effective_readonly")) {
3130 this.$().html(QWeb.render("FieldSelectionSelect", {widget: this, values: values}));
3131 this.$("select").val(JSON.stringify(found[0]));
3133 this.$el.text(found[1]);
3137 var input = this.$('select:first')[0];
3138 return input ? input.focus() : false;
3140 set_dimensions: function (height, width) {
3141 this._super(height, width);
3142 this.$('select').css({
3149 instance.web.form.FieldRadio = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3150 template: 'FieldRadio',
3152 'click input': 'click_change_value'
3154 init: function(field_manager, node) {
3155 /* Radio button widget: Attributes options:
3156 * - "horizontal" to display in column
3157 * - "no_radiolabel" don't display text values
3159 this._super(field_manager, node);
3160 this.selection = _.clone(this.field.selection) || [];
3161 this.domain = false;
3162 this.uniqueId = _.uniqueId("radio");
3164 initialize_content: function () {
3165 this.on("change:effective_readonly", this, this.render_value);
3166 this.field_manager.on("view_content_has_changed", this, this.get_selection);
3167 this.get_selection();
3169 click_change_value: function (event) {
3170 var val = $(event.target).val();
3171 val = this.field.type == "selection" ? val : +val;
3172 if (val == this.get_value()) {
3173 this.set_value(false);
3175 this.set_value(val);
3178 /** Get the selection and render it
3179 * selection: [[identifier, value_to_display], ...]
3180 * For selection fields: this is directly given by this.field.selection
3181 * For many2one fields: perform a search on the relation of the many2one field
3183 get_selection: function() {
3186 var def = $.Deferred();
3187 if (self.field.type == "many2one") {
3188 var domain = instance.web.pyeval.eval('domain', this.build_domain()) || [];
3189 if (! _.isEqual(self.domain, domain)) {
3190 self.domain = domain;
3191 var ds = new instance.web.DataSetStatic(self, self.field.relation, self.build_context());
3192 ds.call('search', [self.domain])
3193 .then(function (records) {
3194 ds.name_get(records).then(function (records) {
3195 selection = records;
3200 selection = self.selection;
3204 else if (self.field.type == "selection") {
3205 selection = self.field.selection || [];
3208 return def.then(function () {
3209 if (! _.isEqual(selection, self.selection)) {
3210 self.selection = _.clone(selection);
3211 self.renderElement();
3212 self.render_value();
3216 set_value: function (value_) {
3218 if (this.field.type == "selection") {
3219 value_ = _.find(this.field.selection, function (sel) { return sel[0] == value_;});
3221 else if (!this.selection.length) {
3222 this.selection = [value_];
3225 this._super(value_);
3227 get_value: function () {
3228 var value = this.get('value');
3229 return value instanceof Array ? value[0] : value;
3231 render_value: function () {
3233 this.$el.toggleClass("oe_readonly", this.get('effective_readonly'));
3234 this.$("input:checked").prop("checked", false);
3235 if (this.get_value()) {
3236 this.$("input").filter(function () {return this.value == self.get_value();}).prop("checked", true);
3237 this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : "");
3242 // jquery autocomplete tweak to allow html and classnames
3244 var proto = $.ui.autocomplete.prototype,
3245 initSource = proto._initSource;
3247 function filter( array, term ) {
3248 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
3249 return $.grep( array, function(value_) {
3250 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
3255 _initSource: function() {
3256 if ( this.options.html && $.isArray(this.options.source) ) {
3257 this.source = function( request, response ) {
3258 response( filter( this.options.source, request.term ) );
3261 initSource.call( this );
3265 _renderItem: function( ul, item) {
3266 return $( "<li></li>" )
3267 .data( "item.autocomplete", item )
3268 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
3270 .addClass(item.classname);
3276 A mixin containing some useful methods to handle completion inputs.
3278 The widget containing this option can have these arguments in its widget options:
3279 - no_quick_create: if true, it will disable the quick create
3281 instance.web.form.CompletionFieldMixin = {
3284 this.orderer = new instance.web.DropMisordered();
3287 * Call this method to search using a string.
3289 get_search_result: function(search_val) {
3292 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
3293 this.last_query = search_val;
3294 var exclusion_domain = [], ids_blacklist = this.get_search_blacklist();
3295 if (!_(ids_blacklist).isEmpty()) {
3296 exclusion_domain.push(['id', 'not in', ids_blacklist]);
3299 return this.orderer.add(dataset.name_search(
3300 search_val, new instance.web.CompoundDomain(self.build_domain(), exclusion_domain),
3301 'ilike', this.limit + 1, self.build_context())).then(function(data) {
3302 self.last_search = data;
3303 // possible selections for the m2o
3304 var values = _.map(data, function(x) {
3305 x[1] = x[1].split("\n")[0];
3307 label: _.str.escapeHTML(x[1]),
3314 // search more... if more results that max
3315 if (values.length > self.limit) {
3316 values = values.slice(0, self.limit);
3318 label: _t("Search More..."),
3319 action: function() {
3320 dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
3321 self._search_create_popup("search", data);
3324 classname: 'oe_m2o_dropdown_option'
3328 var raw_result = _(data.result).map(function(x) {return x[1];});
3329 if (search_val.length > 0 && !_.include(raw_result, search_val) &&
3330 ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3332 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
3333 $('<span />').text(search_val).html()),
3334 action: function() {
3335 self._quick_create(search_val);
3337 classname: 'oe_m2o_dropdown_option'
3341 if (!(self.options && (self.options.no_create || self.options.no_create_edit))){
3343 label: _t("Create and Edit..."),
3344 action: function() {
3345 self._search_create_popup("form", undefined, self._create_context(search_val));
3347 classname: 'oe_m2o_dropdown_option'
3350 else if (values.length == 0)
3352 label: _t("No results to show..."),
3353 action: function() {},
3354 classname: 'oe_m2o_dropdown_option'
3360 get_search_blacklist: function() {
3363 _quick_create: function(name) {
3365 var slow_create = function () {
3366 self._search_create_popup("form", undefined, self._create_context(name));
3368 if (self.options.quick_create === undefined || self.options.quick_create) {
3369 new instance.web.DataSet(this, this.field.relation, self.build_context())
3370 .name_create(name).done(function(data) {
3371 if (!self.get('effective_readonly'))
3372 self.add_id(data[0]);
3373 }).fail(function(error, event) {
3374 event.preventDefault();
3380 // all search/create popup handling
3381 _search_create_popup: function(view, ids, context) {
3383 var pop = new instance.web.form.SelectCreatePopup(this);
3385 self.field.relation,
3387 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3388 initial_ids: ids ? _.map(ids, function(x) {return x[0];}) : undefined,
3390 disable_multiple_selection: true
3392 self.build_domain(),
3393 new instance.web.CompoundContext(self.build_context(), context || {})
3395 pop.on("elements_selected", self, function(element_ids) {
3396 self.add_id(element_ids[0]);
3403 add_id: function(id) {},
3404 _create_context: function(name) {
3406 var field = (this.options || {}).create_name_field;
3407 if (field === undefined)
3409 if (field !== false && name && (this.options || {}).quick_create !== false)
3410 tmp["default_" + field] = name;
3415 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3416 template: "M2ODialog",
3417 init: function(parent) {
3418 this.name = parent.string;
3419 this._super(parent, {
3420 title: _.str.sprintf(_t("Create a %s"), parent.string),
3426 var text = _.str.sprintf(_t("You are creating a new %s, are you sure it does not exist yet?"), self.name);
3427 this.$("p").text( text );
3428 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3429 this.$("input").val(this.getParent().last_query);
3430 this.$buttons.find(".oe_form_m2o_qc_button").click(function(e){
3431 if (self.$("input").val() != ''){
3432 self.getParent()._quick_create(self.$("input").val());
3436 self.$("input").focus();
3439 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3440 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3443 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3449 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3450 template: "FieldMany2One",
3452 'keydown input': function (e) {
3454 case $.ui.keyCode.UP:
3455 case $.ui.keyCode.DOWN:
3456 e.stopPropagation();
3460 init: function(field_manager, node) {
3461 this._super(field_manager, node);
3462 instance.web.form.CompletionFieldMixin.init.call(this);
3463 this.set({'value': false});
3464 this.display_value = {};
3465 this.display_value_backup = {};
3466 this.last_search = [];
3467 this.floating = false;
3468 this.current_display = null;
3469 this.is_started = false;
3470 this.ignore_focusout = false;
3472 reinit_value: function(val) {
3473 this.internal_set_value(val);
3474 this.floating = false;
3475 if (this.is_started)
3476 this.render_value();
3478 initialize_field: function() {
3479 this.is_started = true;
3480 instance.web.bus.on('click', this, function() {
3481 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3482 this.$input.autocomplete("close");
3485 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3487 initialize_content: function() {
3488 if (!this.get("effective_readonly"))
3489 this.render_editable();
3491 destroy_content: function () {
3492 if (this.$drop_down) {
3493 this.$drop_down.off('click');
3494 delete this.$drop_down;
3497 this.$input.closest(".modal .modal-content").off('scroll');
3498 this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3499 'focus focusout change keydown');
3502 if (this.$follow_button) {
3503 this.$follow_button.off('blur focus click');
3504 delete this.$follow_button;
3507 destroy: function () {
3508 this.destroy_content();
3509 return this._super();
3511 init_error_displayer: function() {
3514 hide_error_displayer: function() {
3517 show_error_displayer: function() {
3518 new instance.web.form.M2ODialog(this).open();
3520 render_editable: function() {
3522 this.$input = this.$el.find("input");
3524 this.init_error_displayer();
3526 self.$input.on('focus', function() {
3527 self.hide_error_displayer();
3530 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3531 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3533 this.$follow_button.click(function(ev) {
3534 ev.preventDefault();
3535 if (!self.get('value')) {
3539 var pop = new instance.web.form.FormOpenPopup(self);
3540 var context = self.build_context().eval();
3541 var model_obj = new instance.web.Model(self.field.relation);
3542 model_obj.call('get_formview_id', [self.get("value"), context]).then(function(view_id){
3544 self.field.relation,
3546 self.build_context(),
3548 title: _t("Open: ") + self.string,
3552 pop.on('write_completed', self, function(){
3553 self.display_value = {};
3554 self.display_value_backup = {};
3555 self.render_value();
3557 self.trigger('changed_value');
3562 // some behavior for input
3563 var input_changed = function() {
3564 if (self.current_display !== self.$input.val()) {
3565 self.current_display = self.$input.val();
3566 if (self.$input.val() === "") {
3567 self.internal_set_value(false);
3568 self.floating = false;
3570 self.floating = true;
3574 this.$input.keydown(input_changed);
3575 this.$input.change(input_changed);
3576 this.$drop_down.click(function() {
3577 self.$input.focus();
3578 if (self.$input.autocomplete("widget").is(":visible")) {
3579 self.$input.autocomplete("close");
3581 if (self.get("value") && ! self.floating) {
3582 self.$input.autocomplete("search", "");
3584 self.$input.autocomplete("search");
3589 // Autocomplete close on dialog content scroll
3590 var close_autocomplete = _.debounce(function() {
3591 if (self.$input.autocomplete("widget").is(":visible")) {
3592 self.$input.autocomplete("close");
3595 this.$input.closest(".modal .modal-content").on('scroll', this, close_autocomplete);
3597 self.ed_def = $.Deferred();
3598 self.uned_def = $.Deferred();
3600 var ed_duration = 15000;
3601 var anyoneLoosesFocus = function (e) {
3602 if (self.ignore_focusout) { return; }
3604 if (self.floating) {
3605 if (self.last_search.length > 0) {
3606 if (self.last_search[0][0] != self.get("value")) {
3607 self.display_value = {};
3608 self.display_value_backup = {};
3609 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3610 self.reinit_value(self.last_search[0][0]);
3613 self.render_value();
3617 self.reinit_value(false);
3619 self.floating = false;
3621 if (used && self.get("value") === false && ! self.no_ed && ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3622 self.ed_def.reject();
3623 self.uned_def.reject();
3624 self.ed_def = $.Deferred();
3625 self.ed_def.done(function() {
3626 self.show_error_displayer();
3627 ignore_blur = false;
3628 self.trigger('focused');
3631 setTimeout(function() {
3632 self.ed_def.resolve();
3633 self.uned_def.reject();
3634 self.uned_def = $.Deferred();
3635 self.uned_def.done(function() {
3636 self.hide_error_displayer();
3638 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3642 self.ed_def.reject();
3645 var ignore_blur = false;
3647 focusout: anyoneLoosesFocus,
3648 focus: function () { self.trigger('focused'); },
3649 autocompleteopen: function () { ignore_blur = true; },
3650 autocompleteclose: function () { setTimeout(function() {ignore_blur = false;},0); },
3652 // autocomplete open
3653 if (ignore_blur) { $(this).focus(); return; }
3654 if (_(self.getChildren()).any(function (child) {
3655 return child instanceof instance.web.form.AbstractFormPopup;
3657 self.trigger('blurred');
3661 var isSelecting = false;
3663 this.$input.autocomplete({
3664 source: function(req, resp) {
3665 self.get_search_result(req.term).done(function(result) {
3669 select: function(event, ui) {
3673 self.display_value = {};
3674 self.display_value_backup = {};
3675 self.display_value["" + item.id] = item.name;
3676 self.reinit_value(item.id);
3677 } else if (item.action) {
3679 // Cancel widget blurring, to avoid form blur event
3680 self.trigger('focused');
3684 focus: function(e, ui) {
3688 // disabled to solve a bug, but may cause others
3689 //close: anyoneLoosesFocus,
3693 // set position for list of suggestions box
3694 this.$input.autocomplete( "option", "position", { my : "left top", at: "left bottom" } );
3695 this.$input.autocomplete("widget").openerpClass();
3696 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3697 this.$input.keyup(function(e) {
3698 if (e.which === 13) { // ENTER
3700 e.stopPropagation();
3702 isSelecting = false;
3704 this.setupFocus(this.$follow_button);
3706 render_value: function(no_recurse) {
3708 if (! this.get("value")) {
3709 this.display_string("");
3712 var display = this.display_value["" + this.get("value")];
3714 this.display_string(display);
3718 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3719 this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3721 self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
3724 self.display_value["" + self.get("value")] = data[0][1];
3725 self.render_value(true);
3726 }).fail( function (data, event) {
3727 // avoid displaying crash errors as many2One should be name_get compliant
3728 event.preventDefault();
3729 self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")];
3730 self.render_value(true);
3734 display_string: function(str) {
3736 if (!this.get("effective_readonly")) {
3737 this.$input.val(str.split("\n")[0]);
3738 this.current_display = this.$input.val();
3739 if (this.is_false()) {
3740 this.$('.oe_m2o_cm_button').css({'display':'none'});
3742 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3745 var lines = _.escape(str).split("\n");
3749 follow = _.rest(lines).join("<br />");
3752 var $link = this.$el.find('.oe_form_uri')
3755 if (! this.options.no_open)
3756 $link.click(function () {
3757 var context = self.build_context().eval();
3758 var model_obj = new instance.web.Model(self.field.relation);
3759 model_obj.call('get_formview_action', [self.get("value"), context]).then(function(action){
3760 self.do_action(action);
3764 $(".oe_form_m2o_follow", this.$el).html(follow);
3767 set_value: function(value_) {
3769 if (value_ instanceof Array) {
3770 this.display_value = {};
3771 this.display_value_backup = {};
3772 if (! this.options.always_reload) {
3773 this.display_value["" + value_[0]] = value_[1];
3776 this.display_value_backup["" + value_[0]] = value_[1];
3780 value_ = value_ || false;
3781 this.reinit_value(value_);
3783 get_displayed: function() {
3784 return this.display_value["" + this.get("value")];
3786 add_id: function(id) {
3787 this.display_value = {};
3788 this.display_value_backup = {};
3789 this.reinit_value(id);
3791 is_false: function() {
3792 return ! this.get("value");
3794 focus: function () {
3795 var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3796 return input ? input.focus() : false;
3798 _quick_create: function() {
3800 this.ed_def.reject();
3801 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3803 _search_create_popup: function() {
3805 this.ed_def.reject();
3806 this.ignore_focusout = true;
3807 this.reinit_value(false);
3808 var res = instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3809 this.ignore_focusout = false;
3813 set_dimensions: function (height, width) {
3814 this._super(height, width);
3815 if (!this.get("effective_readonly") && this.$input)
3816 this.$input.css('height', height);
3820 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3821 template: 'Many2OneButton',
3822 init: function(field_manager, node) {
3823 this._super.apply(this, arguments);
3826 this._super.apply(this, arguments);
3829 set_button: function() {
3832 this.$button.remove();
3835 this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3836 this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3837 this.$button.addClass('oe_link').css({'padding':'4px'});
3838 this.$el.append(this.$button);
3839 this.$button.on('click', self.on_click);
3841 on_click: function(ev) {
3843 this.popup = new instance.web.form.FormOpenPopup(this);
3844 this.popup.show_element(
3845 this.field.relation,
3847 this.build_context(),
3848 {title: this.string}
3850 this.popup.on('create_completed', self, function(r) {
3854 set_value: function(value_) {
3856 if (value_ instanceof Array) {
3859 value_ = value_ || false;
3860 this.set('value', value_);
3866 * Abstract-ish ListView.List subclass adding an "Add an item" row to replace
3867 * the big ugly button in the header.
3869 * Requires the implementation of a ``is_readonly`` method (usually a proxy to
3870 * the corresponding field's readonly or effective_readonly property) to
3871 * decide whether the special row should or should not be inserted.
3873 * Optionally an ``_add_row_class`` attribute can be set for the class(es) to
3874 * set on the insertion row.
3876 instance.web.form.AddAnItemList = instance.web.ListView.List.extend({
3877 pad_table_to: function (count) {
3878 if (!this.view.is_action_enabled('create') || this.is_readonly()) {
3883 this._super(count > 0 ? count - 1 : 0);
3886 var columns = _(this.columns).filter(function (column) {
3887 return column.invisible !== '1';
3889 if (this.options.selectable) { columns++; }
3890 if (this.options.deletable) { columns++; }
3892 var $cell = $('<td>', {
3894 'class': this._add_row_class || ''
3896 $('<a>', {href: '#'}).text(_t("Add an item"))
3897 .mousedown(function () {
3898 // FIXME: needs to be an official API somehow
3899 if (self.view.editor.is_editing()) {
3900 self.view.__ignore_blur = true;
3903 .click(function (e) {
3905 e.stopPropagation();
3906 // FIXME: there should also be an API for that one
3907 if (self.view.editor.form.__blur_timeout) {
3908 clearTimeout(self.view.editor.form.__blur_timeout);
3909 self.view.editor.form.__blur_timeout = false;
3911 self.view.ensure_saved().done(function () {
3912 self.view.do_add_record();
3916 var $padding = this.$current.find('tr:not([data-id]):first');
3917 var $newrow = $('<tr>').append($cell);
3918 if ($padding.length) {
3919 $padding.before($newrow);
3921 this.$current.append($newrow)
3927 # Values: (0, 0, { fields }) create
3928 # (1, ID, { fields }) update
3929 # (2, ID) remove (delete)
3930 # (3, ID) unlink one (target id or target of relation)
3932 # (5) unlink all (only valid for one2many)
3937 'create': function (values) {
3938 return [commands.CREATE, false, values];
3940 // (1, id, {values})
3942 'update': function (id, values) {
3943 return [commands.UPDATE, id, values];
3947 'delete': function (id) {
3948 return [commands.DELETE, id, false];
3950 // (3, id[, _]) removes relation, but not linked record itself
3952 'forget': function (id) {
3953 return [commands.FORGET, id, false];
3957 'link_to': function (id) {
3958 return [commands.LINK_TO, id, false];
3962 'delete_all': function () {
3963 return [5, false, false];
3965 // (6, _, ids) replaces all linked records with provided ids
3967 'replace_with': function (ids) {
3968 return [6, false, ids];
3971 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3972 multi_selection: false,
3973 disable_utility_classes: true,
3974 init: function(field_manager, node) {
3975 this._super(field_manager, node);
3976 lazy_build_o2m_kanban_view();
3977 this.is_loaded = $.Deferred();
3978 this.initial_is_loaded = this.is_loaded;
3979 this.form_last_update = $.Deferred();
3980 this.init_form_last_update = this.form_last_update;
3981 this.is_started = false;
3982 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
3983 this.dataset.o2m = this;
3984 this.dataset.parent_view = this.view;
3985 this.dataset.child_name = this.name;
3987 this.dataset.on('dataset_changed', this, function() {
3988 self.trigger_on_change();
3993 this._super.apply(this, arguments);
3994 this.$el.addClass('oe_form_field oe_form_field_one2many');
3999 this.is_loaded.done(function() {
4000 self.on("change:effective_readonly", self, function() {
4001 self.is_loaded = self.is_loaded.then(function() {
4002 self.viewmanager.destroy();
4003 return $.when(self.load_views()).done(function() {
4004 self.reload_current_view();
4009 this.is_started = true;
4010 this.reload_current_view();
4012 trigger_on_change: function() {
4013 this.trigger('changed_value');
4015 load_views: function() {
4018 var modes = this.node.attrs.mode;
4019 modes = !!modes ? modes.split(",") : ["tree"];
4021 _.each(modes, function(mode) {
4022 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
4023 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
4027 view_type: mode == "tree" ? "list" : mode,
4030 if (self.field.views && self.field.views[mode]) {
4031 view.embedded_view = self.field.views[mode];
4033 if(view.view_type === "list") {
4034 _.extend(view.options, {
4036 selectable: self.multi_selection,
4038 import_enabled: false,
4041 if (self.get("effective_readonly")) {
4042 _.extend(view.options, {
4047 } else if (view.view_type === "form") {
4048 if (self.get("effective_readonly")) {
4049 view.view_type = 'form';
4051 _.extend(view.options, {
4052 not_interactible_on_create: true,
4054 } else if (view.view_type === "kanban") {
4055 _.extend(view.options, {
4056 confirm_on_delete: false,
4058 if (self.get("effective_readonly")) {
4059 _.extend(view.options, {
4060 action_buttons: false,
4061 quick_creatable: false,
4063 read_only_mode: true,
4071 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
4072 this.viewmanager.o2m = self;
4073 var once = $.Deferred().done(function() {
4074 self.init_form_last_update.resolve();
4076 var def = $.Deferred().done(function() {
4077 self.initial_is_loaded.resolve();
4079 this.viewmanager.on("controller_inited", self, function(view_type, controller) {
4080 controller.o2m = self;
4081 if (view_type == "list") {
4082 if (self.get("effective_readonly")) {
4083 controller.on('edit:before', self, function (e) {
4086 _(controller.columns).find(function (column) {
4087 if (!(column instanceof instance.web.list.Handle)) {
4090 column.modifiers.invisible = true;
4094 } else if (view_type === "form") {
4095 if (self.get("effective_readonly")) {
4096 $(".oe_form_buttons", controller.$el).children().remove();
4098 controller.on("load_record", self, function(){
4101 controller.on('pager_action_executed',self,self.save_any_view);
4102 } else if (view_type == "graph") {
4103 self.reload_current_view();
4107 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
4108 $.when(self.save_any_view()).done(function() {
4109 if (n_mode === "list") {
4110 $.async_when().done(function() {
4111 self.reload_current_view();
4116 $.async_when().done(function () {
4117 self.viewmanager.appendTo(self.$el);
4121 reload_current_view: function() {
4123 self.is_loaded = self.is_loaded.then(function() {
4124 var view = self.get_active_view();
4125 if (view.type === "list") {
4126 return view.controller.reload_content();
4127 } else if (view.type === "form") {
4128 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
4129 self.dataset.index = 0;
4131 var act = function() {
4132 return view.controller.do_show();
4134 self.form_last_update = self.form_last_update.then(act, act);
4135 return self.form_last_update;
4136 } else if (view.controller.do_search) {
4137 return view.controller.do_search(self.build_domain(), self.dataset.get_context(), []);
4140 return self.is_loaded;
4142 get_active_view: function () {
4144 * Returns the current active view if any.
4146 if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
4147 this.viewmanager.views[this.viewmanager.active_view] &&
4148 this.viewmanager.views[this.viewmanager.active_view].controller) {
4150 type: this.viewmanager.active_view,
4151 controller: this.viewmanager.views[this.viewmanager.active_view].controller
4155 set_value: function(value_) {
4156 value_ = value_ || [];
4158 var view = this.get_active_view();
4159 this.dataset.reset_ids([]);
4161 if(value_.length >= 1 && value_[0] instanceof Array) {
4163 _.each(value_, function(command) {
4164 var obj = {values: command[2]};
4165 switch (command[0]) {
4166 case commands.CREATE:
4167 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4169 self.dataset.to_create.push(obj);
4170 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4173 case commands.UPDATE:
4174 obj['id'] = command[1];
4175 self.dataset.to_write.push(obj);
4176 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4179 case commands.DELETE:
4180 self.dataset.to_delete.push({id: command[1]});
4182 case commands.LINK_TO:
4183 ids.push(command[1]);
4185 case commands.DELETE_ALL:
4186 self.dataset.delete_all = true;
4191 this.dataset.set_ids(ids);
4192 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
4194 this.dataset.delete_all = true;
4195 _.each(value_, function(command) {
4196 var obj = {values: command};
4197 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4199 self.dataset.to_create.push(obj);
4200 self.dataset.cache.push(_.clone(obj));
4204 this.dataset.set_ids(ids);
4206 this._super(value_);
4207 this.dataset.reset_ids(value_);
4209 if (this.dataset.index === null && this.dataset.ids.length > 0) {
4210 this.dataset.index = 0;
4212 this.trigger_on_change();
4213 if (this.is_started) {
4214 return self.reload_current_view();
4219 get_value: function() {
4223 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
4224 val = val.concat(_.map(this.dataset.ids, function(id) {
4225 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
4227 return commands.create(alter_order.values);
4229 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
4231 return commands.update(alter_order.id, alter_order.values);
4233 return commands.link_to(id);
4235 return val.concat(_.map(
4236 this.dataset.to_delete, function(x) {
4237 return commands['delete'](x.id);}));
4239 commit_value: function() {
4240 return this.save_any_view();
4242 save_any_view: function() {
4243 var view = this.get_active_view();
4245 if (this.viewmanager.active_view === "form") {
4246 if (view.controller.is_initialized.state() !== 'resolved') {
4247 return $.when(false);
4249 return $.when(view.controller.save());
4250 } else if (this.viewmanager.active_view === "list") {
4251 return $.when(view.controller.ensure_saved());
4254 return $.when(false);
4256 is_syntax_valid: function() {
4257 var view = this.get_active_view();
4261 switch (this.viewmanager.active_view) {
4263 return _(view.controller.fields).chain()
4268 return view.controller.is_valid();
4274 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
4275 template: 'One2Many.viewmanager',
4276 init: function(parent, dataset, views, flags) {
4277 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
4278 this.registry = this.registry.extend({
4279 list: 'instance.web.form.One2ManyListView',
4280 form: 'instance.web.form.One2ManyFormView',
4281 kanban: 'instance.web.form.One2ManyKanbanView',
4283 this.__ignore_blur = false;
4285 switch_mode: function(mode, unused) {
4286 if (mode !== 'form') {
4287 return this._super(mode, unused);
4290 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
4291 var pop = new instance.web.form.FormOpenPopup(this);
4292 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4293 title: _t("Open: ") + self.o2m.string,
4294 create_function: function(data, options) {
4295 return self.o2m.dataset.create(data, options).done(function(r) {
4296 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4297 self.o2m.dataset.trigger("dataset_changed", r);
4300 write_function: function(id, data, options) {
4301 return self.o2m.dataset.write(id, data, {}).done(function() {
4302 self.o2m.reload_current_view();
4305 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4306 parent_view: self.o2m.view,
4307 child_name: self.o2m.name,
4308 read_function: function() {
4309 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4311 form_view_options: {'not_interactible_on_create':true},
4312 readonly: self.o2m.get("effective_readonly")
4314 pop.on("elements_selected", self, function() {
4315 self.o2m.reload_current_view();
4320 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
4321 get_context: function() {
4322 this.context = this.o2m.build_context();
4323 return this.context;
4327 instance.web.form.One2ManyListView = instance.web.ListView.extend({
4328 _template: 'One2Many.listview',
4329 init: function (parent, dataset, view_id, options) {
4330 this._super(parent, dataset, view_id, _.extend(options || {}, {
4331 GroupsType: instance.web.form.One2ManyGroups,
4332 ListType: instance.web.form.One2ManyList
4334 this.on('edit:after', this, this.proxy('_after_edit'));
4335 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
4338 .bind('add', this.proxy("changed_records"))
4339 .bind('edit', this.proxy("changed_records"))
4340 .bind('remove', this.proxy("changed_records"));
4342 start: function () {
4343 var ret = this._super();
4345 .off('mousedown.handleButtons')
4346 .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
4349 changed_records: function () {
4350 this.o2m.trigger_on_change();
4352 is_valid: function () {
4353 var editor = this.editor;
4354 var form = editor.form;
4355 // If no edition is pending, the listview can not be invalid (?)
4356 if (!editor.record) {
4359 // If the form has not been modified, the view can only be valid
4360 // NB: is_dirty will also be set on defaults/onchanges/whatever?
4361 // oe_form_dirty seems to only be set on actual user actions
4362 if (!form.$el.is('.oe_form_dirty')) {
4365 this.o2m._dirty_flag = true;
4367 // Otherwise validate internal form
4368 return _(form.fields).chain()
4369 .invoke(function () {
4370 this._check_css_flags();
4371 return this.is_valid();
4376 do_add_record: function () {
4377 if (this.editable()) {
4378 this._super.apply(this, arguments);
4381 var pop = new instance.web.form.SelectCreatePopup(this);
4383 self.o2m.field.relation,
4385 title: _t("Create: ") + self.o2m.string,
4386 initial_view: "form",
4387 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4388 create_function: function(data, options) {
4389 return self.o2m.dataset.create(data, options).done(function(r) {
4390 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4391 self.o2m.dataset.trigger("dataset_changed", r);
4394 read_function: function() {
4395 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4397 parent_view: self.o2m.view,
4398 child_name: self.o2m.name,
4399 form_view_options: {'not_interactible_on_create':true}
4401 self.o2m.build_domain(),
4402 self.o2m.build_context()
4404 pop.on("elements_selected", self, function() {
4405 self.o2m.reload_current_view();
4409 do_activate_record: function(index, id) {
4411 var pop = new instance.web.form.FormOpenPopup(self);
4412 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4413 title: _t("Open: ") + self.o2m.string,
4414 write_function: function(id, data) {
4415 return self.o2m.dataset.write(id, data, {}).done(function() {
4416 self.o2m.reload_current_view();
4419 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4420 parent_view: self.o2m.view,
4421 child_name: self.o2m.name,
4422 read_function: function() {
4423 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4425 form_view_options: {'not_interactible_on_create':true},
4426 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
4429 do_button_action: function (name, id, callback) {
4430 if (!_.isNumber(id)) {
4431 instance.webclient.notification.warn(
4432 _t("Action Button"),
4433 _t("The o2m record must be saved before an action can be used"));
4436 var parent_form = this.o2m.view;
4438 this.ensure_saved().then(function () {
4440 return parent_form.save();
4443 }).done(function () {
4444 var ds = self.o2m.dataset;
4445 var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
4446 return value.length;
4448 if (!self.o2m.options.reload_on_button && !cached_records) {
4449 self.handle_button(name, id, callback);
4451 self.handle_button(name, id, function(){
4452 self.o2m.view.reload();
4458 _after_edit: function () {
4459 this.__ignore_blur = false;
4460 this.editor.form.on('blurred', this, this._on_form_blur);
4462 // The form's blur thing may be jiggered during the edition setup,
4463 // potentially leading to the o2m instasaving the row. Cancel any
4464 // blurring triggered the edition startup here
4465 this.editor.form.widgetFocused();
4467 _before_unedit: function () {
4468 this.editor.form.off('blurred', this, this._on_form_blur);
4470 _button_down: function () {
4471 // If a button is clicked (usually some sort of action button), it's
4472 // the button's responsibility to ensure the editable list is in the
4473 // correct state -> ignore form blurring
4474 this.__ignore_blur = true;
4477 * Handles blurring of the nested form (saves the currently edited row),
4478 * unless the flag to ignore the event is set to ``true``
4480 * Makes the internal form go away
4482 _on_form_blur: function () {
4483 if (this.__ignore_blur) {
4484 this.__ignore_blur = false;
4487 // FIXME: why isn't there an API for this?
4488 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4489 this.ensure_saved();
4492 this.cancel_edition();
4494 keypress_ENTER: function () {
4495 // blurring caused by hitting the [Return] key, should skip the
4496 // autosave-on-blur and let the handler for [Return] do its thing (save
4497 // the current row *anyway*, then create a new one/edit the next one)
4498 this.__ignore_blur = true;
4499 this._super.apply(this, arguments);
4501 do_delete: function (ids) {
4502 var confirm = window.confirm;
4503 window.confirm = function () { return true; };
4505 return this._super(ids);
4507 window.confirm = confirm;
4510 reload_record: function (record, options) {
4511 if (!options || !options['do_not_evict']) {
4512 // Evict record.id from cache to ensure it will be reloaded correctly
4513 this.dataset.evict_record(record.get('id'));
4516 return this._super(record);
4519 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4520 setup_resequence_rows: function () {
4521 if (!this.view.o2m.get('effective_readonly')) {
4522 this._super.apply(this, arguments);
4526 instance.web.form.One2ManyList = instance.web.form.AddAnItemList.extend({
4527 _add_row_class: 'oe_form_field_one2many_list_row_add',
4528 is_readonly: function () {
4529 return this.view.o2m.get('effective_readonly');
4533 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4534 form_template: 'One2Many.formview',
4535 load_form: function(data) {
4538 this.$buttons.find('button.oe_form_button_create').click(function() {
4539 self.save().done(self.on_button_new);
4542 do_notify_change: function() {
4543 if (this.dataset.parent_view) {
4544 this.dataset.parent_view.do_notify_change();
4546 this._super.apply(this, arguments);
4551 var lazy_build_o2m_kanban_view = function() {
4552 if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
4554 instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4558 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4559 template: "FieldMany2ManyTags",
4560 tag_template: "FieldMany2ManyTag",
4562 this._super.apply(this, arguments);
4563 instance.web.form.CompletionFieldMixin.init.call(this);
4564 this.set({"value": []});
4565 this._display_orderer = new instance.web.DropMisordered();
4566 this._drop_shown = false;
4568 initialize_texttext: function(){
4571 plugins : 'tags arrow autocomplete',
4573 render: function(suggestion) {
4574 return $('<span class="text-label"/>').
4575 data('index', suggestion['index']).html(suggestion['label']);
4580 selectFromDropdown: function() {
4581 this.trigger('hideDropdown');
4582 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4583 var data = self.search_result[index];
4585 self.add_id(data.id);
4587 self.ignore_blur = true;
4590 this.trigger('setSuggestions', {result : []});
4594 isTagAllowed: function(tag) {
4598 removeTag: function(tag) {
4599 var id = tag.data("id");
4600 self.set({"value": _.without(self.get("value"), id)});
4602 renderTag: function(stuff) {
4603 return $.fn.textext.TextExtTags.prototype.renderTag.
4604 call(this, stuff).data("id", stuff.id);
4608 itemToString: function(item) {
4613 onSetInputData: function(e, data) {
4615 this._plugins.autocomplete._suggestions = null;
4617 this.input().val(data);
4623 initialize_content: function() {
4624 if (this.get("effective_readonly"))
4627 self.ignore_blur = false;
4628 self.$text = this.$("textarea");
4629 self.$text.textext(self.initialize_texttext()).bind('getSuggestions', function(e, data) {
4631 var str = !!data ? data.query || '' : '';
4632 self.get_search_result(str).done(function(result) {
4633 self.search_result = result;
4634 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4635 return _.extend(el, {index:i});
4638 }).bind('hideDropdown', function() {
4639 self._drop_shown = false;
4640 }).bind('showDropdown', function() {
4641 self._drop_shown = true;
4643 self.tags = self.$text.textext()[0].tags();
4645 .focusin(function () {
4646 self.trigger('focused');
4647 self.ignore_blur = false;
4649 .focusout(function() {
4650 self.$text.trigger("setInputData", "");
4651 if (!self.ignore_blur) {
4652 self.trigger('blurred');
4654 }).keydown(function(e) {
4655 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4656 self.$text.textext()[0].autocomplete().selectFromDropdown();
4660 // WARNING: duplicated in 4 other M2M widgets
4661 set_value: function(value_) {
4662 value_ = value_ || [];
4663 if (value_.length >= 1 && value_[0] instanceof Array) {
4664 // value_ is a list of m2m commands. We only process
4665 // LINK_TO and REPLACE_WITH in this context
4667 _.each(value_, function (command) {
4668 if (command[0] === commands.LINK_TO) {
4669 val.push(command[1]); // (4, id[, _])
4670 } else if (command[0] === commands.REPLACE_WITH) {
4671 val = command[2]; // (6, _, ids)
4676 this._super(value_);
4678 is_false: function() {
4679 return _(this.get("value")).isEmpty();
4681 get_value: function() {
4682 var tmp = [commands.replace_with(this.get("value"))];
4685 get_search_blacklist: function() {
4686 return this.get("value");
4688 map_tag: function(data){
4689 return _.map(data, function(el) {return {name: el[1], id:el[0]};})
4691 get_render_data: function(ids){
4693 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4694 return dataset.name_get(ids);
4696 render_tag: function(data) {
4698 if (! self.get("effective_readonly")) {
4699 self.tags.containerElement().children().remove();
4700 self.$('textarea').css("padding-left", "3px");
4701 self.tags.addTags(self.map_tag(data));
4703 self.$el.html(QWeb.render(self.tag_template, {elements: data}));
4706 render_value: function() {
4708 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4709 var values = self.get("value");
4710 var handle_names = function(data) {
4711 if (self.isDestroyed())
4714 _.each(data, function(el) {
4715 indexed[el[0]] = el;
4717 data = _.map(values, function(el) { return indexed[el]; });
4718 self.render_tag(data);
4720 if (! values || values.length > 0) {
4721 return this._display_orderer.add(self.get_render_data(values)).done(handle_names);
4726 add_id: function(id) {
4727 this.set({'value': _.uniq(this.get('value').concat([id]))});
4729 focus: function () {
4730 var input = this.$text && this.$text[0];
4731 return input ? input.focus() : false;
4733 set_dimensions: function (height, width) {
4734 this._super(height, width);
4735 this.$("textarea").css({
4740 _search_create_popup: function() {
4741 self.ignore_blur = true;
4742 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
4748 - reload_on_button: Reload the whole form view if click on a button in a list view.
4749 If you see this options, do not use it, it's basically a dirty hack to make one
4750 precise o2m to behave the way we want.
4752 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4753 multi_selection: false,
4754 disable_utility_classes: true,
4755 init: function(field_manager, node) {
4756 this._super(field_manager, node);
4757 this.is_loaded = $.Deferred();
4758 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4759 this.dataset.m2m = this;
4761 this.dataset.on('unlink', self, function(ids) {
4762 self.dataset_changed();
4765 this.list_dm = new instance.web.DropMisordered();
4766 this.render_value_dm = new instance.web.DropMisordered();
4768 initialize_content: function() {
4771 this.$el.addClass('oe_form_field oe_form_field_many2many');
4773 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4775 'deletable': this.get("effective_readonly") ? false : true,
4776 'selectable': this.multi_selection,
4778 'reorderable': false,
4779 'import_enabled': false,
4781 var embedded = (this.field.views || {}).tree;
4783 this.list_view.set_embedded_view(embedded);
4785 this.list_view.m2m_field = this;
4786 var loaded = $.Deferred();
4787 this.list_view.on("list_view_loaded", this, function() {
4790 this.list_view.appendTo(this.$el);
4792 var old_def = self.is_loaded;
4793 self.is_loaded = $.Deferred().done(function() {
4796 this.list_dm.add(loaded).then(function() {
4797 self.is_loaded.resolve();
4800 destroy_content: function() {
4801 this.list_view.destroy();
4802 this.list_view = undefined;
4804 // WARNING: duplicated in 4 other M2M widgets
4805 set_value: function(value_) {
4806 value_ = value_ || [];
4807 if (value_.length >= 1 && value_[0] instanceof Array) {
4808 // value_ is a list of m2m commands. We only process
4809 // LINK_TO and REPLACE_WITH in this context
4811 _.each(value_, function (command) {
4812 if (command[0] === commands.LINK_TO) {
4813 val.push(command[1]); // (4, id[, _])
4814 } else if (command[0] === commands.REPLACE_WITH) {
4815 val = command[2]; // (6, _, ids)
4820 this._super(value_);
4822 get_value: function() {
4823 return [commands.replace_with(this.get('value'))];
4825 is_false: function () {
4826 return _(this.get("value")).isEmpty();
4828 render_value: function() {
4830 this.dataset.set_ids(this.get("value"));
4831 this.render_value_dm.add(this.is_loaded).then(function() {
4832 return self.list_view.reload_content();
4835 dataset_changed: function() {
4836 this.internal_set_value(this.dataset.ids);
4840 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4841 get_context: function() {
4842 this.context = this.m2m.build_context();
4843 return this.context;
4849 * @extends instance.web.ListView
4851 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4852 init: function (parent, dataset, view_id, options) {
4853 this._super(parent, dataset, view_id, _.extend(options || {}, {
4854 ListType: instance.web.form.Many2ManyList,
4857 do_add_record: function () {
4858 var pop = new instance.web.form.SelectCreatePopup(this);
4862 title: _t("Add: ") + this.m2m_field.string,
4863 no_create: this.m2m_field.options.no_create,
4865 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4866 this.m2m_field.build_context()
4869 pop.on("elements_selected", self, function(element_ids) {
4871 _(element_ids).each(function (id) {
4872 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4873 self.dataset.set_ids(self.dataset.ids.concat([id]));
4874 self.m2m_field.dataset_changed();
4879 self.reload_content();
4883 do_activate_record: function(index, id) {
4885 var pop = new instance.web.form.FormOpenPopup(this);
4886 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4887 title: _t("Open: ") + this.m2m_field.string,
4888 readonly: this.getParent().get("effective_readonly")
4890 pop.on('write_completed', self, self.reload_content);
4892 do_button_action: function(name, id, callback) {
4894 var _sup = _.bind(this._super, this);
4895 if (! this.m2m_field.options.reload_on_button) {
4896 return _sup(name, id, callback);
4898 return this.m2m_field.view.save().then(function() {
4899 return _sup(name, id, function() {
4900 self.m2m_field.view.reload();
4905 is_action_enabled: function () { return true; },
4907 instance.web.form.Many2ManyList = instance.web.form.AddAnItemList.extend({
4908 _add_row_class: 'oe_form_field_many2many_list_row_add',
4909 is_readonly: function () {
4910 return this.view.m2m_field.get('effective_readonly');
4914 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4915 disable_utility_classes: true,
4916 init: function(field_manager, node) {
4917 this._super(field_manager, node);
4918 instance.web.form.CompletionFieldMixin.init.call(this);
4919 m2m_kanban_lazy_init();
4920 this.is_loaded = $.Deferred();
4921 this.initial_is_loaded = this.is_loaded;
4924 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4925 this.dataset.m2m = this;
4926 this.dataset.on('unlink', self, function(ids) {
4927 self.dataset_changed();
4931 this._super.apply(this, arguments);
4936 self.on("change:effective_readonly", self, function() {
4937 self.is_loaded = self.is_loaded.then(function() {
4938 self.kanban_view.destroy();
4939 return $.when(self.load_view()).done(function() {
4940 self.render_value();
4945 // WARNING: duplicated in 4 other M2M widgets
4946 set_value: function(value_) {
4947 value_ = value_ || [];
4948 if (value_.length >= 1 && value_[0] instanceof Array) {
4949 // value_ is a list of m2m commands. We only process
4950 // LINK_TO and REPLACE_WITH in this context
4952 _.each(value_, function (command) {
4953 if (command[0] === commands.LINK_TO) {
4954 val.push(command[1]); // (4, id[, _])
4955 } else if (command[0] === commands.REPLACE_WITH) {
4956 val = command[2]; // (6, _, ids)
4961 this._super(value_);
4963 get_value: function() {
4964 return [commands.replace_with(this.get('value'))];
4966 load_view: function() {
4968 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4969 'create_text': _t("Add"),
4970 'creatable': self.get("effective_readonly") ? false : true,
4971 'quick_creatable': self.get("effective_readonly") ? false : true,
4972 'read_only_mode': self.get("effective_readonly") ? true : false,
4973 'confirm_on_delete': false,
4975 var embedded = (this.field.views || {}).kanban;
4977 this.kanban_view.set_embedded_view(embedded);
4979 this.kanban_view.m2m = this;
4980 var loaded = $.Deferred();
4981 this.kanban_view.on("kanban_view_loaded",self,function() {
4982 self.initial_is_loaded.resolve();
4985 this.kanban_view.on('switch_mode', this, this.open_popup);
4986 $.async_when().done(function () {
4987 self.kanban_view.appendTo(self.$el);
4991 render_value: function() {
4993 this.dataset.set_ids(this.get("value"));
4994 this.is_loaded = this.is_loaded.then(function() {
4995 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
4998 dataset_changed: function() {
4999 this.set({'value': this.dataset.ids});
5001 open_popup: function(type, unused) {
5002 if (type !== "form")
5006 if (this.dataset.index === null) {
5007 pop = new instance.web.form.SelectCreatePopup(this);
5009 this.field.relation,
5011 title: _t("Add: ") + this.string
5013 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
5014 this.build_context()
5016 pop.on("elements_selected", self, function(element_ids) {
5017 _.each(element_ids, function(one_id) {
5018 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
5019 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
5020 self.dataset_changed();
5021 self.render_value();
5026 var id = self.dataset.ids[self.dataset.index];
5027 pop = new instance.web.form.FormOpenPopup(this);
5028 pop.show_element(self.field.relation, id, self.build_context(), {
5029 title: _t("Open: ") + self.string,
5030 write_function: function(id, data, options) {
5031 return self.dataset.write(id, data, {}).done(function() {
5032 self.render_value();
5035 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
5036 parent_view: self.view,
5037 child_name: self.name,
5038 readonly: self.get("effective_readonly")
5042 add_id: function(id) {
5043 this.quick_create.add_id(id);
5047 function m2m_kanban_lazy_init() {
5048 if (instance.web.form.Many2ManyKanbanView)
5050 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
5051 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
5052 _is_quick_create_enabled: function() {
5053 return this._super() && ! this.group_by;
5056 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
5057 template: 'Many2ManyKanban.quick_create',
5060 * close_btn: If true, the widget will display a "Close" button able to trigger
5063 init: function(parent, dataset, context, buttons) {
5064 this._super(parent);
5065 this.m2m = this.getParent().view.m2m;
5066 this.m2m.quick_create = this;
5067 this._dataset = dataset;
5068 this._buttons = buttons || false;
5069 this._context = context || {};
5071 start: function () {
5073 self.$text = this.$el.find('input').css("width", "200px");
5074 self.$text.textext({
5075 plugins : 'arrow autocomplete',
5077 render: function(suggestion) {
5078 return $('<span class="text-label"/>').
5079 data('index', suggestion['index']).html(suggestion['label']);
5084 selectFromDropdown: function() {
5085 $(this).trigger('hideDropdown');
5086 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
5087 var data = self.search_result[index];
5089 self.add_id(data.id);
5096 itemToString: function(item) {
5101 }).bind('getSuggestions', function(e, data) {
5103 var str = !!data ? data.query || '' : '';
5104 self.m2m.get_search_result(str).done(function(result) {
5105 self.search_result = result;
5106 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
5107 return _.extend(el, {index:i});
5111 self.$text.focusout(function() {
5116 this.$text[0].focus();
5118 add_id: function(id) {
5121 self.trigger('added', id);
5122 this.m2m.dataset_changed();
5128 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
5130 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
5131 template: "AbstractFormPopup.render",
5134 * -readonly: only applicable when not in creation mode, default to false
5135 * - alternative_form_view
5142 * - form_view_options
5144 init_popup: function(model, row_id, domain, context, options) {
5145 this.row_id = row_id;
5147 this.domain = domain || [];
5148 this.context = context || {};
5149 this.options = options;
5150 _.defaults(this.options, {
5153 init_dataset: function() {
5155 this.created_elements = [];
5156 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
5157 this.dataset.read_function = this.options.read_function;
5158 this.dataset.create_function = function(data, options, sup) {
5159 var fct = self.options.create_function || sup;
5160 return fct.call(this, data, options).done(function(r) {
5161 self.trigger('create_completed saved', r);
5162 self.created_elements.push(r);
5165 this.dataset.write_function = function(id, data, options, sup) {
5166 var fct = self.options.write_function || sup;
5167 return fct.call(this, id, data, options).done(function(r) {
5168 self.trigger('write_completed saved', r);
5171 this.dataset.parent_view = this.options.parent_view;
5172 this.dataset.child_name = this.options.child_name;
5174 display_popup: function() {
5176 this.renderElement();
5177 var dialog = new instance.web.Dialog(this, {
5178 dialogClass: 'oe_act_window',
5179 title: this.options.title || "",
5180 }, this.$el).open();
5181 dialog.on('closing', this, function (e){
5182 self.check_exit(true);
5184 this.$buttonpane = dialog.$buttons;
5187 setup_form_view: function() {
5190 this.dataset.ids = [this.row_id];
5191 this.dataset.index = 0;
5193 this.dataset.index = null;
5195 var options = _.clone(self.options.form_view_options) || {};
5196 if (this.row_id !== null) {
5197 options.initial_mode = this.options.readonly ? "view" : "edit";
5200 $buttons: this.$buttonpane,
5202 this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
5203 if (this.options.alternative_form_view) {
5204 this.view_form.set_embedded_view(this.options.alternative_form_view);
5206 this.view_form.appendTo(this.$el.find(".oe_popup_form"));
5207 this.view_form.on("form_view_loaded", self, function() {
5208 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
5209 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
5210 multi_select: multi_select,
5211 readonly: self.row_id !== null && self.options.readonly,
5213 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
5214 $snbutton.click(function() {
5215 $.when(self.view_form.save()).done(function() {
5216 self.view_form.reload_mutex.exec(function() {
5217 self.view_form.on_button_new();
5221 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
5222 $sbutton.click(function() {
5223 $.when(self.view_form.save()).done(function() {
5224 self.view_form.reload_mutex.exec(function() {
5229 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
5230 $cbutton.click(function() {
5231 self.view_form.trigger('on_button_cancel');
5234 self.view_form.do_show();
5237 select_elements: function(element_ids) {
5238 this.trigger("elements_selected", element_ids);
5240 check_exit: function(no_destroy) {
5241 if (this.created_elements.length > 0) {
5242 this.select_elements(this.created_elements);
5243 this.created_elements = [];
5245 this.trigger('closed');
5248 destroy: function () {
5249 this.trigger('closed');
5250 if (this.$el.is(":data(bs.modal)")) {
5251 this.$el.parents('.modal').modal('hide');
5258 * Class to display a popup containing a form view.
5260 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
5261 show_element: function(model, row_id, context, options) {
5262 this.init_popup(model, row_id, [], context, options);
5263 _.defaults(this.options, {
5265 this.display_popup();
5269 this.init_dataset();
5270 this.setup_form_view();
5275 * Class to display a popup to display a list to search a row. It also allows
5276 * to switch to a form view to create a new row.
5278 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
5282 * - initial_view: form or search (default search)
5283 * - disable_multiple_selection
5284 * - list_view_options
5286 select_element: function(model, options, domain, context) {
5287 this.init_popup(model, null, domain, context, options);
5289 _.defaults(this.options, {
5290 initial_view: "search",
5292 this.initial_ids = this.options.initial_ids;
5293 this.display_popup();
5297 this.init_dataset();
5298 if (this.options.initial_view == "search") {
5299 instance.web.pyeval.eval_domains_and_contexts({
5301 contexts: [this.context]
5302 }).done(function (results) {
5303 var search_defaults = {};
5304 _.each(results.context, function (value_, key) {
5305 var match = /^search_default_(.*)$/.exec(key);
5307 search_defaults[match[1]] = value_;
5310 self.setup_search_view(search_defaults);
5316 setup_search_view: function(search_defaults) {
5318 if (this.searchview) {
5319 this.searchview.destroy();
5321 if (this.searchview_drawer) {
5322 this.searchview_drawer.destroy();
5324 this.searchview = new instance.web.SearchView(this,
5325 this.dataset, false, search_defaults);
5326 this.searchview_drawer = new instance.web.SearchViewDrawer(this, this.searchview);
5327 this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
5328 if (self.initial_ids) {
5329 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
5330 contexts.concat(self.context), groupbys);
5331 self.initial_ids = undefined;
5333 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
5336 this.searchview.on("search_view_loaded", self, function() {
5337 self.view_list = new instance.web.form.SelectCreateListView(self,
5338 self.dataset, false,
5339 _.extend({'deletable': false,
5340 'selectable': !self.options.disable_multiple_selection,
5341 'import_enabled': false,
5342 '$buttons': self.$buttonpane,
5343 'disable_editable_mode': true,
5344 '$pager': self.$('.oe_popup_list_pager'),
5345 }, self.options.list_view_options || {}));
5346 self.view_list.on('edit:before', self, function (e) {
5349 self.view_list.popup = self;
5350 self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
5351 self.view_list.do_show();
5352 }).then(function() {
5353 self.searchview.do_search();
5355 self.view_list.on("list_view_loaded", self, function() {
5356 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
5357 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
5358 $cbutton.click(function() {
5361 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
5362 $sbutton.click(function() {
5363 self.select_elements(self.selected_ids);
5366 $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
5367 $cbutton.click(function() {
5372 this.searchview.appendTo(this.$(".oe_popup_search"));
5374 do_search: function(domains, contexts, groupbys) {
5376 instance.web.pyeval.eval_domains_and_contexts({
5377 domains: domains || [],
5378 contexts: contexts || [],
5379 group_by_seq: groupbys || []
5380 }).done(function (results) {
5381 self.view_list.do_search(results.domain, results.context, results.group_by);
5384 on_click_element: function(ids) {
5386 this.selected_ids = ids || [];
5387 if(this.selected_ids.length > 0) {
5388 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
5390 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
5393 new_object: function() {
5394 if (this.searchview) {
5395 this.searchview.hide();
5397 if (this.view_list) {
5398 this.view_list.do_hide();
5400 this.setup_form_view();
5404 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
5405 do_add_record: function () {
5406 this.popup.new_object();
5408 select_record: function(index) {
5409 this.popup.select_elements([this.dataset.ids[index]]);
5410 this.popup.destroy();
5412 do_select: function(ids, records) {
5413 this._super(ids, records);
5414 this.popup.on_click_element(ids);
5418 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5419 template: 'FieldReference',
5420 init: function(field_manager, node) {
5421 this._super(field_manager, node);
5422 this.reference_ready = true;
5424 destroy_content: function() {
5427 this.fm = undefined;
5430 initialize_content: function() {
5432 var fm = new instance.web.form.DefaultFieldManager(this);
5434 fm.extend_field_desc({
5436 selection: this.field_manager.get_field_desc(this.name).selection,
5444 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
5446 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5448 this.selection.on("change:value", this, this.on_selection_changed);
5449 this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
5451 .on('focused', null, function () {self.trigger('focused');})
5452 .on('blurred', null, function () {self.trigger('blurred');});
5454 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
5455 name: 'Referenced Document',
5456 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5458 this.m2o.on("change:value", this, this.data_changed);
5459 this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
5461 .on('focused', null, function () {self.trigger('focused');})
5462 .on('blurred', null, function () {self.trigger('blurred');});
5464 on_selection_changed: function() {
5465 if (this.reference_ready) {
5466 this.internal_set_value([this.selection.get_value(), false]);
5467 this.render_value();
5470 data_changed: function() {
5471 if (this.reference_ready) {
5472 this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
5475 set_value: function(val) {
5477 val = val.split(',');
5478 val[0] = val[0] || false;
5479 val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
5481 this._super(val || [false, false]);
5483 get_value: function() {
5484 return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
5486 render_value: function() {
5487 this.reference_ready = false;
5488 if (!this.get("effective_readonly")) {
5489 this.selection.set_value(this.get('value')[0]);
5491 this.m2o.field.relation = this.get('value')[0];
5492 this.m2o.set_value(this.get('value')[1]);
5493 this.m2o.$el.toggle(!!this.get('value')[0]);
5494 this.reference_ready = true;
5498 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5499 init: function(field_manager, node) {
5501 this._super(field_manager, node);
5502 this.binary_value = false;
5503 this.useFileAPI = !!window.FileReader;
5504 this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5505 if (!this.useFileAPI) {
5506 this.fileupload_id = _.uniqueId('oe_fileupload');
5507 $(window).on(this.fileupload_id, function() {
5508 var args = [].slice.call(arguments).slice(1);
5509 self.on_file_uploaded.apply(self, args);
5514 if (!this.useFileAPI) {
5515 $(window).off(this.fileupload_id);
5517 this._super.apply(this, arguments);
5519 initialize_content: function() {
5521 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5522 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5523 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5524 this.$el.find('.oe_form_binary_file_edit').click(function(event){
5525 self.$el.find('input.oe_form_binary_file').click();
5528 on_file_change: function(e) {
5530 var file_node = e.target;
5531 if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5532 if (this.useFileAPI) {
5533 var file = file_node.files[0];
5534 if (file.size > this.max_upload_size) {
5535 var msg = _t("The selected file exceed the maximum file size of %s.");
5536 instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5539 var filereader = new FileReader();
5540 filereader.readAsDataURL(file);
5541 filereader.onloadend = function(upload) {
5542 var data = upload.target.result;
5543 data = data.split(',')[1];
5544 self.on_file_uploaded(file.size, file.name, file.type, data);
5547 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5548 this.$el.find('form.oe_form_binary_form').submit();
5550 this.$el.find('.oe_form_binary_progress').show();
5551 this.$el.find('.oe_form_binary').hide();
5554 on_file_uploaded: function(size, name, content_type, file_base64) {
5555 if (size === false) {
5556 this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5557 // TODO: use openerp web crashmanager
5558 console.warn("Error while uploading file : ", name);
5560 this.filename = name;
5561 this.on_file_uploaded_and_valid.apply(this, arguments);
5563 this.$el.find('.oe_form_binary_progress').hide();
5564 this.$el.find('.oe_form_binary').show();
5566 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5568 on_save_as: function(ev) {
5569 var value = this.get('value');
5571 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5572 ev.stopPropagation();
5574 instance.web.blockUI();
5575 var c = instance.webclient.crashmanager;
5576 this.session.get_file({
5577 url: '/web/binary/saveas_ajax',
5578 data: {data: JSON.stringify({
5579 model: this.view.dataset.model,
5580 id: (this.view.datarecord.id || ''),
5582 filename_field: (this.node.attrs.filename || ''),
5583 data: instance.web.form.is_bin_size(value) ? null : value,
5584 context: this.view.dataset.get_context()
5586 complete: instance.web.unblockUI,
5587 error: c.rpc_error.bind(c)
5589 ev.stopPropagation();
5593 set_filename: function(value) {
5594 var filename = this.node.attrs.filename;
5597 tmp[filename] = value;
5598 this.field_manager.set_values(tmp);
5601 on_clear: function() {
5602 if (this.get('value') !== false) {
5603 this.binary_value = false;
5604 this.internal_set_value(false);
5610 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5611 template: 'FieldBinaryFile',
5612 initialize_content: function() {
5614 if (this.get("effective_readonly")) {
5616 this.$el.find('a').click(function(ev) {
5617 if (self.get('value')) {
5618 self.on_save_as(ev);
5624 render_value: function() {
5626 if (!this.get("effective_readonly")) {
5627 if (this.node.attrs.filename) {
5628 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5630 show_value = (this.get('value') !== null && this.get('value') !== undefined && this.get('value') !== false) ? this.get('value') : '';
5632 this.$el.find('input').eq(0).val(show_value);
5634 this.$el.find('a').toggle(!!this.get('value'));
5635 if (this.get('value')) {
5636 show_value = _t("Download");
5638 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5639 this.$el.find('a').text(show_value);
5643 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5644 this.binary_value = true;
5645 this.internal_set_value(file_base64);
5646 var show_value = name + " (" + instance.web.human_size(size) + ")";
5647 this.$el.find('input').eq(0).val(show_value);
5648 this.set_filename(name);
5650 on_clear: function() {
5651 this._super.apply(this, arguments);
5652 this.$el.find('input').eq(0).val('');
5653 this.set_filename('');
5657 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5658 template: 'FieldBinaryImage',
5659 placeholder: "/web/static/src/img/placeholder.png",
5660 render_value: function() {
5663 if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5664 url = 'data:image/png;base64,' + this.get('value');
5665 } else if (this.get('value')) {
5666 var id = JSON.stringify(this.view.datarecord.id || null);
5667 var field = this.name;
5668 if (this.options.preview_image)
5669 field = this.options.preview_image;
5670 url = this.session.url('/web/binary/image', {
5671 model: this.view.dataset.model,
5674 t: (new Date().getTime()),
5677 url = this.placeholder;
5679 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5680 $($img).click(function(e) {
5681 if(self.view.get("actual_mode") == "view") {
5682 var $button = $(".oe_form_button_edit");
5683 $button.openerpBounce();
5684 e.stopPropagation();
5687 this.$el.find('> img').remove();
5688 this.$el.prepend($img);
5689 $img.load(function() {
5690 if (! self.options.size)
5692 $img.css("max-width", "" + self.options.size[0] + "px");
5693 $img.css("max-height", "" + self.options.size[1] + "px");
5695 $img.on('error', function() {
5696 $img.attr('src', self.placeholder);
5697 instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5700 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5701 this.internal_set_value(file_base64);
5702 this.binary_value = true;
5703 this.render_value();
5704 this.set_filename(name);
5706 on_clear: function() {
5707 this._super.apply(this, arguments);
5708 this.render_value();
5709 this.set_filename('');
5711 set_value: function(value_){
5712 var changed = value_ !== this.get_value();
5713 this._super.apply(this, arguments);
5714 // By default, on binary images read, the server returns the binary size
5715 // This is possible that two images have the exact same size
5716 // Therefore we trigger the change in case the image value hasn't changed
5717 // So the image is re-rendered correctly
5719 this.trigger("change:value", this, {
5728 * Widget for (many2many field) to upload one or more file in same time and display in list.
5729 * The user can delete his files.
5730 * Options on attribute ; "blockui" {Boolean} block the UI or not
5731 * during the file is uploading
5733 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
5734 template: "FieldBinaryFileUploader",
5735 init: function(field_manager, node) {
5736 this._super(field_manager, node);
5737 this.field_manager = field_manager;
5739 if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5740 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);
5744 this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5745 this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5746 $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5750 this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5752 // WARNING: duplicated in 4 other M2M widgets
5753 set_value: function(value_) {
5754 value_ = value_ || [];
5755 if (value_.length >= 1 && value_[0] instanceof Array) {
5756 // value_ is a list of m2m commands. We only process
5757 // LINK_TO and REPLACE_WITH in this context
5759 _.each(value_, function (command) {
5760 if (command[0] === commands.LINK_TO) {
5761 val.push(command[1]); // (4, id[, _])
5762 } else if (command[0] === commands.REPLACE_WITH) {
5763 val = command[2]; // (6, _, ids)
5768 this._super(value_);
5770 get_value: function() {
5771 var tmp = [commands.replace_with(this.get("value"))];
5774 get_file_url: function (attachment) {
5775 return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5777 read_name_values : function () {
5779 // don't reset know values
5780 var ids = this.get('value');
5781 var _value = _.filter(ids, function (id) { return typeof self.data[id] == 'undefined'; } );
5782 // send request for get_name
5783 if (_value.length) {
5784 return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).then(function (datas) {
5785 _.each(datas, function (data) {
5786 data.no_unlink = true;
5787 data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5788 self.data[data.id] = data;
5796 render_value: function () {
5798 this.read_name_values().then(function (ids) {
5799 var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids}));
5800 render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5801 self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5803 // reinit input type file
5804 var $input = self.$('input.oe_form_binary_file');
5805 $input.after($input.clone(true)).remove();
5806 self.$(".oe_fileupload").show();
5810 on_file_change: function (event) {
5811 event.stopPropagation();
5813 var $target = $(event.target);
5814 if ($target.val() !== '') {
5815 var filename = $target.val().replace(/.*[\\\/]/,'');
5816 // don't uplode more of one file in same time
5817 if (self.data[0] && self.data[0].upload ) {
5820 for (var id in this.get('value')) {
5821 // if the files exits, delete the file before upload (if it's a new file)
5822 if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
5823 self.ds_file.unlink([id]);
5828 if(this.node.attrs.blockui>0) {
5829 instance.web.blockUI();
5832 // TODO : unactivate send on wizard and form
5835 this.$('form.oe_form_binary_form').submit();
5836 this.$(".oe_fileupload").hide();
5837 // add file on data result
5841 'filename': filename,
5847 on_file_loaded: function (event, result) {
5848 var files = this.get('value');
5851 if(this.node.attrs.blockui>0) {
5852 instance.web.unblockUI();
5855 if (result.error || !result.id ) {
5856 this.do_warn( _t('Uploading Error'), result.error);
5857 delete this.data[0];
5859 if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
5860 delete this.data[0];
5861 this.data[result.id] = {
5863 'name': result.name,
5864 'filename': result.filename,
5865 'url': this.get_file_url(result)
5868 this.data[result.id] = {
5870 'name': result.name,
5871 'filename': result.filename,
5872 'url': this.get_file_url(result)
5875 var values = _.clone(this.get('value'));
5876 values.push(result.id);
5877 this.set({'value': values});
5879 this.render_value();
5881 on_file_delete: function (event) {
5882 event.stopPropagation();
5883 var file_id=$(event.target).data("id");
5885 var files = _.filter(this.get('value'), function (id) {return id != file_id;});
5886 if(!this.data[file_id].no_unlink) {
5887 this.ds_file.unlink([file_id]);
5889 this.set({'value': files});
5894 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5895 template: "FieldStatus",
5896 init: function(field_manager, node) {
5897 this._super(field_manager, node);
5898 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5899 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5900 this.set({value: false});
5901 this.selection = {'unfolded': [], 'folded': []};
5902 this.set("selection", {'unfolded': [], 'folded': []});
5903 this.selection_dm = new instance.web.DropMisordered();
5904 this.dataset = new instance.web.DataSetStatic(this, this.field.relation, this.build_context());
5907 this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5909 this.on("change:value", this, this.get_selection);
5910 this.on("change:evaluated_selection_domain", this, this.get_selection);
5911 this.on("change:selection", this, function() {
5912 this.selection = this.get("selection");
5913 this.render_value();
5915 this.get_selection();
5916 if (this.options.clickable) {
5917 this.$el.on('click','li[data-id]',this.on_click_stage);
5919 if (this.$el.parent().is('header')) {
5920 this.$el.after('<div class="oe_clear"/>');
5924 set_value: function(value_) {
5925 if (value_ instanceof Array) {
5928 this._super(value_);
5930 render_value: function() {
5932 var content = QWeb.render("FieldStatus.content", {
5934 'value_folded': _.find(self.selection.folded, function(i){return i[0] === self.get('value');})
5936 self.$el.html(content);
5938 calc_domain: function() {
5939 var d = instance.web.pyeval.eval('domain', this.build_domain());
5940 var domain = []; //if there is no domain defined, fetch all the records
5943 domain = ['|',['id', '=', this.get('value')]].concat(d);
5946 if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5947 this.set("evaluated_selection_domain", domain);
5950 /** Get the selection and render it
5951 * selection: [[identifier, value_to_display], ...]
5952 * For selection fields: this is directly given by this.field.selection
5953 * For many2one fields: perform a search on the relation of the many2one field
5955 get_selection: function() {
5957 var selection_unfolded = [];
5958 var selection_folded = [];
5959 var fold_field = this.options.fold_field;
5961 var calculation = _.bind(function() {
5962 if (this.field.type == "many2one") {
5963 return self.get_distant_fields().then(function (fields) {
5964 return new instance.web.DataSetSearch(self, self.field.relation, self.build_context(), self.get("evaluated_selection_domain"))
5965 .read_slice(_.union(_.keys(self.distant_fields), ['id']), {}).then(function (records) {
5966 var ids = _.pluck(records, 'id');
5967 return self.dataset.name_get(ids).then(function (records_name) {
5968 _.each(records, function (record) {
5969 var name = _.find(records_name, function (val) {return val[0] == record.id;})[1];
5970 if (fold_field && record[fold_field] && record.id != self.get('value')) {
5971 selection_folded.push([record.id, name]);
5973 selection_unfolded.push([record.id, name]);
5980 // For field type selection filter values according to
5981 // statusbar_visible attribute of the field. For example:
5982 // statusbar_visible="draft,open".
5983 var select = this.field.selection;
5984 for(var i=0; i < select.length; i++) {
5985 var key = select[i][0];
5986 if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5987 selection_unfolded.push(select[i]);
5993 this.selection_dm.add(calculation()).then(function () {
5994 var selection = {'unfolded': selection_unfolded, 'folded': selection_folded};
5995 if (! _.isEqual(selection, self.get("selection"))) {
5996 self.set("selection", selection);
6001 * :deprecated: this feature will probably be removed with OpenERP v8
6003 get_distant_fields: function() {
6005 if (! this.options.fold_field) {
6006 this.distant_fields = {}
6008 if (this.distant_fields) {
6009 return $.when(this.distant_fields);
6011 return new instance.web.Model(self.field.relation).call("fields_get", [[this.options.fold_field]]).then(function(fields) {
6012 self.distant_fields = fields;
6016 on_click_stage: function (ev) {
6018 var $li = $(ev.currentTarget);
6020 if (this.field.type == "many2one") {
6021 val = parseInt($li.data("id"), 10);
6024 val = $li.data("id");
6026 if (val != self.get('value')) {
6027 this.view.recursive_save().done(function() {
6029 change[self.name] = val;
6030 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
6038 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
6039 template: "FieldMonetary",
6040 widget_class: 'oe_form_field_float oe_form_field_monetary',
6042 this._super.apply(this, arguments);
6043 this.set({"currency": false});
6044 if (this.options.currency_field) {
6045 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
6046 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
6049 this.on("change:currency", this, this.get_currency_info);
6050 this.get_currency_info();
6051 this.ci_dm = new instance.web.DropMisordered();
6054 var tmp = this._super();
6055 this.on("change:currency_info", this, this.reinitialize);
6058 get_currency_info: function() {
6060 if (this.get("currency") === false) {
6061 this.set({"currency_info": null});
6064 return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
6065 .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
6066 self.set({"currency_info": res});
6069 parse_value: function(val, def) {
6070 return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6072 format_value: function(val, def) {
6073 return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6078 This type of field display a list of checkboxes. It works only with m2ms. This field will display one checkbox for each
6079 record existing in the model targeted by the relation, according to the given domain if one is specified. Checked records
6080 will be added to the relation.
6082 instance.web.form.FieldMany2ManyCheckBoxes = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6083 className: "oe_form_many2many_checkboxes",
6085 this._super.apply(this, arguments);
6086 this.set("value", {});
6087 this.set("records", []);
6088 this.field_manager.on("view_content_has_changed", this, function() {
6089 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
6090 if (! _.isEqual(domain, this.get("domain"))) {
6091 this.set("domain", domain);
6094 this.records_orderer = new instance.web.DropMisordered();
6096 initialize_field: function() {
6097 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
6098 this.on("change:domain", this, this.query_records);
6099 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
6100 this.on("change:records", this, this.render_value);
6102 query_records: function() {
6104 var model = new openerp.Model(openerp.session, this.field.relation);
6105 this.records_orderer.add(model.call("search", [this.get("domain")], {"context": this.build_context()}).then(function(record_ids) {
6106 return model.call("name_get", [record_ids] , {"context": self.build_context()});
6107 })).then(function(res) {
6108 self.set("records", res);
6111 render_value: function() {
6112 this.$().html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this, selected: this.get("value")}));
6113 var inputs = this.$("input");
6114 inputs.change(_.bind(this.from_dom, this));
6115 if (this.get("effective_readonly"))
6116 inputs.attr("disabled", "true");
6118 from_dom: function() {
6120 this.$("input").each(function() {
6122 new_value[elem.data("record-id")] = elem.attr("checked") ? true : undefined;
6124 if (! _.isEqual(new_value, this.get("value")))
6125 this.internal_set_value(new_value);
6127 // WARNING: (mostly) duplicated in 4 other M2M widgets
6128 set_value: function(value_) {
6129 value_ = value_ || [];
6130 if (value_.length >= 1 && value_[0] instanceof Array) {
6131 // value_ is a list of m2m commands. We only process
6132 // LINK_TO and REPLACE_WITH in this context
6134 _.each(value_, function (command) {
6135 if (command[0] === commands.LINK_TO) {
6136 val.push(command[1]); // (4, id[, _])
6137 } else if (command[0] === commands.REPLACE_WITH) {
6138 val = command[2]; // (6, _, ids)
6144 _.each(value_, function(el) {
6145 formatted[JSON.stringify(el)] = true;
6147 this._super(formatted);
6149 get_value: function() {
6150 var value = _.filter(_.keys(this.get("value")), function(el) {
6151 return this.get("value")[el];
6153 value = _.map(value, function(el) {
6154 return JSON.parse(el);
6156 return [commands.replace_with(value)];
6161 This field can be applied on many2many and one2many. It is a read-only field that will display a single link whose name is
6162 "<number of linked records> <label of the field>". When the link is clicked, it will redirect to another act_window
6163 action on the model of the relation and show only the linked records.
6167 * 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
6168 to display (or False to take the default one) and the second element is the type of the view. Defaults to
6169 [[false, "tree"], [false, "form"]] .
6171 instance.web.form.X2ManyCounter = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6172 className: "oe_form_x2many_counter",
6174 this._super.apply(this, arguments);
6175 this.set("value", []);
6176 _.defaults(this.options, {
6177 "views": [[false, "tree"], [false, "form"]],
6180 render_value: function() {
6181 var text = _.str.sprintf("%d %s", this.val().length, this.string);
6182 this.$().html(QWeb.render("X2ManyCounter", {text: text}));
6183 this.$("a").click(_.bind(this.go_to, this));
6186 return this.view.recursive_save().then(_.bind(function() {
6187 var val = this.val();
6189 if (this.field.type === "one2many") {
6190 context["default_" + this.field.relation_field] = this.view.datarecord.id;
6192 var domain = [["id", "in", val]];
6193 return this.do_action({
6194 type: 'ir.actions.act_window',
6196 res_model: this.field.relation,
6197 views: this.options.views,
6205 var value = this.get("value") || [];
6206 if (value.length >= 1 && value[0] instanceof Array) {
6207 value = value[0][2];
6214 This widget is intended to be used on stat button numeric fields. It will display
6215 the value many2many and one2many. It is a read-only field that will
6216 display a simple string "<value of field> <label of the field>"
6218 instance.web.form.StatInfo = instance.web.form.AbstractField.extend({
6219 is_field_number: true,
6221 this._super.apply(this, arguments);
6222 this.internal_set_value(0);
6224 set_value: function(value_) {
6225 if (value_ === false || value_ === undefined) {
6228 this._super.apply(this, [value_]);
6230 render_value: function() {
6232 value: this.get("value") || 0,
6234 if (! this.node.attrs.nolabel) {
6235 options.text = this.string
6237 this.$el.html(QWeb.render("StatInfo", options));
6244 * Registry of form fields, called by :js:`instance.web.FormView`.
6246 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
6247 * will substitute to the <field> tags as defined in OpenERP's views.
6249 instance.web.form.widgets = new instance.web.Registry({
6250 'char' : 'instance.web.form.FieldChar',
6251 'id' : 'instance.web.form.FieldID',
6252 'email' : 'instance.web.form.FieldEmail',
6253 'url' : 'instance.web.form.FieldUrl',
6254 'text' : 'instance.web.form.FieldText',
6255 'html' : 'instance.web.form.FieldTextHtml',
6256 'char_domain': 'instance.web.form.FieldCharDomain',
6257 'date' : 'instance.web.form.FieldDate',
6258 'datetime' : 'instance.web.form.FieldDatetime',
6259 'selection' : 'instance.web.form.FieldSelection',
6260 'radio' : 'instance.web.form.FieldRadio',
6261 'many2one' : 'instance.web.form.FieldMany2One',
6262 'many2onebutton' : 'instance.web.form.Many2OneButton',
6263 'many2many' : 'instance.web.form.FieldMany2Many',
6264 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
6265 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
6266 'one2many' : 'instance.web.form.FieldOne2Many',
6267 'one2many_list' : 'instance.web.form.FieldOne2Many',
6268 'reference' : 'instance.web.form.FieldReference',
6269 'boolean' : 'instance.web.form.FieldBoolean',
6270 'float' : 'instance.web.form.FieldFloat',
6271 'percentpie': 'instance.web.form.FieldPercentPie',
6272 'barchart': 'instance.web.form.FieldBarChart',
6273 'integer': 'instance.web.form.FieldFloat',
6274 'float_time': 'instance.web.form.FieldFloat',
6275 'progressbar': 'instance.web.form.FieldProgressBar',
6276 'image': 'instance.web.form.FieldBinaryImage',
6277 'binary': 'instance.web.form.FieldBinaryFile',
6278 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
6279 'statusbar': 'instance.web.form.FieldStatus',
6280 'monetary': 'instance.web.form.FieldMonetary',
6281 'many2many_checkboxes': 'instance.web.form.FieldMany2ManyCheckBoxes',
6282 'x2many_counter': 'instance.web.form.X2ManyCounter',
6283 'priority':'instance.web.form.Priority',
6284 'kanban_state_selection':'instance.web.form.KanbanSelection',
6285 'statinfo': 'instance.web.form.StatInfo',
6289 * Registry of widgets usable in the form view that can substitute to any possible
6290 * tags defined in OpenERP's form views.
6292 * Every referenced class should extend FormWidget.
6294 instance.web.form.tags = new instance.web.Registry({
6295 'button' : 'instance.web.form.WidgetButton',
6298 instance.web.form.custom_widgets = new instance.web.Registry({
6303 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: