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)'
309 do_hide: function () {
311 this.sidebar.$el.hide();
314 this.$buttons.hide();
321 load_record: function(record) {
322 var self = this, set_values = [];
324 this.set({ 'title' : undefined });
325 this.do_warn(_t("Form"), _t("The record could not be found in the database."), true);
326 return $.Deferred().reject();
328 this.datarecord = record;
329 this._actualize_mode();
330 this.set({ 'title' : record.id ? record.display_name : _t("New") });
332 _(this.fields).each(function (field, f) {
333 field._dirty_flag = false;
334 field._inhibit_on_change_flag = true;
335 var result = field.set_value(self.datarecord[f] || false);
336 field._inhibit_on_change_flag = false;
337 set_values.push(result);
339 return $.when.apply(null, set_values).then(function() {
342 self.do_onchange(null);
344 self.on_form_changed();
345 self.rendering_engine.init_fields();
346 self.is_initialized.resolve();
347 self.do_update_pager(record.id === null || record.id === undefined);
349 self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
352 self.do_push_state({id:record.id});
354 self.do_push_state({});
356 self.$el.add(self.$buttons).removeClass('oe_form_dirty');
361 * Loads and sets up the default values for the model as the current
364 * @return {$.Deferred}
366 load_defaults: function () {
368 var keys = _.keys(this.fields_view.fields);
370 return this.dataset.default_get(keys).then(function(r) {
371 self.trigger('load_record', r);
374 return self.trigger('load_record', {});
376 on_form_changed: function() {
377 this.trigger("view_content_has_changed");
379 do_notify_change: function() {
380 this.$el.add(this.$buttons).addClass('oe_form_dirty');
382 execute_pager_action: function(action) {
383 if (this.can_be_discarded()) {
386 this.dataset.index = 0;
389 this.dataset.previous();
395 this.dataset.index = this.dataset.ids.length - 1;
398 var def = this.reload();
399 this.trigger('pager_action_executed');
404 init_pager: function() {
407 this.$pager.remove();
408 if (this.get("actual_mode") === "create")
410 this.$pager = $(QWeb.render("FormView.pager", {'widget':self})).hide();
411 if (this.options.$pager) {
412 this.$pager.appendTo(this.options.$pager);
414 this.$el.find('.oe_form_pager').replaceWith(this.$pager);
416 this.$pager.on('click','a[data-pager-action]',function() {
418 if ($el.attr("disabled"))
420 var action = $el.data('pager-action');
421 var def = $.when(self.execute_pager_action(action));
422 $el.attr("disabled");
423 def.always(function() {
424 $el.removeAttr("disabled");
427 this.do_update_pager();
429 do_update_pager: function(hide_index) {
430 this.$pager.toggle(this.dataset.ids.length > 1);
432 $(".oe_form_pager_state", this.$pager).html("");
434 $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
438 _build_onchange_specs: function() {
440 var find = function(field_name, root) {
442 while (fields.length) {
443 var node = fields.pop();
447 if (node.tag === 'field' && node.attrs.name === field_name) {
448 return node.attrs.on_change || "";
450 fields = _.union(fields, node.children);
455 self._onchange_specs = {};
456 _.each(this.fields, function(field, name) {
457 self._onchange_specs[name] = find(name, field.node);
458 _.each(field.field.views, function(view) {
459 _.each(view.fields, function(_, subname) {
460 self._onchange_specs[name + '.' + subname] = find(subname, view.arch);
465 _get_onchange_values: function() {
466 var field_values = this.get_fields_values();
467 if (field_values.id.toString().match(instance.web.BufferedDataSet.virtual_id_regex)) {
468 delete field_values.id;
470 if (this.dataset.parent_view) {
471 // this belongs to a parent view: add parent field if possible
472 var parent_view = this.dataset.parent_view;
473 var child_name = this.dataset.child_name;
474 var parent_name = parent_view.get_field_desc(child_name).relation_field;
476 // consider all fields except the inverse of the parent field
477 var parent_values = parent_view.get_fields_values();
478 delete parent_values[child_name];
479 field_values[parent_name] = parent_values;
485 do_onchange: function(widget) {
487 var onchange_specs = self._onchange_specs;
489 var def = $.when({});
490 var change_spec = widget ? onchange_specs[widget.name] : null;
491 if (!widget || (!_.isEmpty(change_spec) && change_spec !== "0")) {
493 trigger_field_name = widget ? widget.name : false,
494 values = self._get_onchange_values(),
495 context = new instance.web.CompoundContext(self.dataset.get_context());
497 if (widget && widget.build_context()) {
498 context.add(widget.build_context());
500 if (self.dataset.parent_view) {
501 var parent_name = self.dataset.parent_view.get_field_desc(self.dataset.child_name).relation_field;
502 context.add({field_parent: parent_name});
505 if (self.datarecord.id && !instance.web.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) {
506 // In case of a o2m virtual id, we should pass an empty ids list
507 ids.push(self.datarecord.id);
509 def = self.alive(new instance.web.Model(self.dataset.model).call(
510 "onchange", [ids, values, trigger_field_name, onchange_specs, context]));
512 var onchange_def = def.then(function(response) {
513 if (widget && widget.field['change_default']) {
514 var fieldname = widget.name;
516 if (response.value && (fieldname in response.value)) {
517 // Use value from onchange if onchange executed
518 value_ = response.value[fieldname];
520 // otherwise get form value for field
521 value_ = self.fields[fieldname].get_value();
523 var condition = fieldname + '=' + value_;
526 return self.alive(new instance.web.Model('ir.values').call(
527 'get_defaults', [self.model, condition]
528 )).then(function (results) {
529 if (!results.length) {
532 if (!response.value) {
535 for(var i=0; i<results.length; ++i) {
536 // [whatever, key, value]
537 var triplet = results[i];
538 response.value[triplet[1]] = triplet[2];
545 }).then(function(response) {
546 return self.on_processed_onchange(response);
548 this.onchanges_defs.push(onchange_def);
552 instance.webclient.crashmanager.show_message(e);
553 return $.Deferred().reject();
556 on_processed_onchange: function(result) {
558 var fields = this.fields;
559 _(result.domain).each(function (domain, fieldname) {
560 var field = fields[fieldname];
561 if (!field) { return; }
562 field.node.attrs.domain = domain;
565 if (!_.isEmpty(result.value)) {
566 this._internal_set_values(result.value);
568 // FIXME XXX a list of warnings?
569 if (!_.isEmpty(result.warning)) {
570 new instance.web.Dialog(this, {
572 title:result.warning.title,
574 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
576 }, QWeb.render("CrashManager.warning", result.warning)).open();
579 return $.Deferred().resolve();
582 instance.webclient.crashmanager.show_message(e);
583 return $.Deferred().reject();
586 _process_operations: function() {
588 return this.mutating_mutex.exec(function() {
590 var start = $.Deferred();
592 start = _.reduce(self.onchanges_defs, function(memo, d){
593 return memo.then(function(){
600 _.each(self.fields, function(field) {
601 defs.push(field.commit_value());
603 var args = _.toArray(arguments);
604 return $.when.apply($, defs).then(function() {
605 var save_obj = self.save_list.pop();
607 return self._process_save(save_obj).then(function() {
608 save_obj.ret = _.toArray(arguments);
611 save_obj.error = true;
616 self.save_list.pop();
623 _internal_set_values: function(values) {
624 for (var f in values) {
625 if (!values.hasOwnProperty(f)) { continue; }
626 var field = this.fields[f];
627 // If field is not defined in the view, just ignore it
629 var value_ = values[f];
630 if (field.get_value() != value_) {
631 field._inhibit_on_change_flag = true;
632 field.set_value(value_);
633 field._inhibit_on_change_flag = false;
634 field._dirty_flag = true;
638 this.on_form_changed();
640 set_values: function(values) {
642 return this.mutating_mutex.exec(function() {
643 self._internal_set_values(values);
647 * Ask the view to switch to view mode if possible. The view may not do it
648 * if the current record is not yet saved. It will then stay in create mode.
650 to_view_mode: function() {
651 this._actualize_mode("view");
654 * Ask the view to switch to edit mode if possible. The view may not do it
655 * if the current record is not yet saved. It will then stay in create mode.
657 to_edit_mode: function() {
658 this.onchanges_defs = [];
659 this._actualize_mode("edit");
662 * Ask the view to switch to a precise mode if possible. The view is free to
663 * not respect this command if the state of the dataset is not compatible with
664 * the new mode. For example, it is not possible to switch to edit mode if
665 * the current record is not yet saved in database.
667 * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
668 * undefined the view will test the actual mode to check if it is still consistent
669 * with the dataset state.
671 _actualize_mode: function(switch_to) {
672 var mode = switch_to || this.get("actual_mode");
673 if (! this.datarecord.id) {
675 } else if (mode === "create") {
678 this.render_value_defs = [];
679 this.set({actual_mode: mode});
681 check_actual_mode: function(source, options) {
683 if(this.get("actual_mode") === "view") {
684 self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
685 self.$buttons.find('.oe_form_buttons_edit').hide();
686 self.$buttons.find('.oe_form_buttons_view').show();
687 self.$sidebar.show();
689 self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
690 self.$buttons.find('.oe_form_buttons_edit').show();
691 self.$buttons.find('.oe_form_buttons_view').hide();
692 self.$sidebar.hide();
696 autofocus: function() {
697 if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
698 var fields_order = this.fields_order.slice(0);
699 if (this.default_focus_field) {
700 fields_order.unshift(this.default_focus_field.name);
702 for (var i = 0; i < fields_order.length; i += 1) {
703 var field = this.fields[fields_order[i]];
704 if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
705 if (field.focus() !== false) {
712 on_button_save: function(e) {
714 $(e.target).attr("disabled", true);
715 return this.save().done(function(result) {
716 self.trigger("save", result);
717 self.reload().then(function() {
719 var menu = instance.webclient.menu;
721 menu.do_reload_needaction();
724 }).always(function(){
725 $(e.target).attr("disabled", false);
728 on_button_cancel: function(event) {
730 if (this.can_be_discarded()) {
731 if (this.get('actual_mode') === 'create') {
732 this.trigger('history_back');
735 $.when.apply(null, this.render_value_defs).then(function(){
736 self.trigger('load_record', self.datarecord);
740 this.trigger('on_button_cancel');
743 on_button_new: function() {
746 return $.when(this.has_been_loaded).then(function() {
747 if (self.can_be_discarded()) {
748 return self.load_defaults();
752 on_button_edit: function() {
753 return this.to_edit_mode();
755 on_button_create: function() {
756 this.dataset.index = null;
759 on_button_duplicate: function() {
761 return this.has_been_loaded.then(function() {
762 return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
763 self.record_created(new_id);
768 on_button_delete: function() {
770 var def = $.Deferred();
771 this.has_been_loaded.done(function() {
772 if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
773 self.dataset.unlink([self.datarecord.id]).done(function() {
774 if (self.dataset.size()) {
775 self.execute_pager_action('next');
777 self.do_action('history_back');
782 $.async_when().done(function () {
787 return def.promise();
789 can_be_discarded: function() {
790 if (this.$el.is('.oe_form_dirty')) {
791 if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
794 this.$el.removeClass('oe_form_dirty');
799 * Triggers saving the form's record. Chooses between creating a new
800 * record or saving an existing one depending on whether the record
801 * already has an id property.
803 * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
804 * record, should that record be inserted at the start of the dataset (by
805 * default, records are added at the end)
807 save: function(prepend_on_create) {
809 var save_obj = {prepend_on_create: prepend_on_create, ret: null};
810 this.save_list.push(save_obj);
811 return self._process_operations().then(function() {
813 return $.Deferred().reject();
814 return $.when.apply($, save_obj.ret);
815 }).done(function(result) {
816 self.$el.removeClass('oe_form_dirty');
819 _process_save: function(save_obj) {
821 var prepend_on_create = save_obj.prepend_on_create;
823 var form_invalid = false,
825 first_invalid_field = null,
826 readonly_values = {};
827 for (var f in self.fields) {
828 if (!self.fields.hasOwnProperty(f)) { continue; }
832 if (!first_invalid_field) {
833 first_invalid_field = f;
835 } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
836 // Special case 'id' field, do not save this field
837 // on 'create' : save all non readonly fields
838 // on 'edit' : save non readonly modified fields
839 if (!f.get("readonly")) {
840 values[f.name] = f.get_value();
842 readonly_values[f.name] = f.get_value();
847 self.set({'display_invalid_fields': true});
848 first_invalid_field.focus();
850 return $.Deferred().reject();
852 self.set({'display_invalid_fields': false});
854 if (!self.datarecord.id) {
856 save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
857 return self.record_created(r, prepend_on_create);
859 } else if (_.isEmpty(values)) {
860 // Not dirty, noop save
861 save_deferral = $.Deferred().resolve({}).promise();
864 save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
865 return self.record_saved(r);
868 return save_deferral;
872 return $.Deferred().reject();
875 on_invalid: function() {
876 var warnings = _(this.fields).chain()
877 .filter(function (f) { return !f.is_valid(); })
879 return _.str.sprintf('<li>%s</li>',
882 warnings.unshift('<ul>');
883 warnings.push('</ul>');
884 this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
887 * Reload the form after saving
889 * @param {Object} r result of the write function.
891 record_saved: function(r) {
892 this.trigger('record_saved', r);
894 // should not happen in the server, but may happen for internal purpose
895 return $.Deferred().reject();
900 * Updates the form' dataset to contain the new record:
902 * * Adds the newly created record to the current dataset (at the end by
904 * * Selects that record (sets the dataset's index to point to the new
906 * * Updates the pager and sidebar displays
909 * @param {Boolean} [prepend_on_create=false] adds the newly created record
910 * at the beginning of the dataset instead of the end
912 record_created: function(r, prepend_on_create) {
915 // should not happen in the server, but may happen for internal purpose
916 this.trigger('record_created', r);
917 return $.Deferred().reject();
919 this.datarecord.id = r;
920 if (!prepend_on_create) {
921 this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
922 this.dataset.index = this.dataset.ids.length - 1;
924 this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
925 this.dataset.index = 0;
927 this.do_update_pager();
929 this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
931 //openerp.log("The record has been created with id #" + this.datarecord.id);
932 return $.when(this.reload()).then(function () {
933 self.trigger('record_created', r);
934 return _.extend(r, {created: true});
938 on_action: function (action) {
939 console.debug('Executing action', action);
943 return this.reload_mutex.exec(function() {
944 if (self.dataset.index === null || self.dataset.index === undefined) {
945 self.trigger("previous_view");
946 return $.Deferred().reject().promise();
948 if (self.dataset.index < 0) {
949 return $.when(self.on_button_new());
951 var fields = _.keys(self.fields_view.fields);
952 fields.push('display_name');
953 return self.dataset.read_index(fields,
957 'future_display_name': true
959 check_access_rule: true
960 }).then(function(r) {
961 self.trigger('load_record', r);
963 self.do_action('history_back');
968 get_widgets: function() {
969 return _.filter(this.getChildren(), function(obj) {
970 return obj instanceof instance.web.form.FormWidget;
973 get_fields_values: function() {
975 var ids = this.get_selected_ids();
976 values["id"] = ids.length > 0 ? ids[0] : false;
977 _.each(this.fields, function(value_, key) {
978 values[key] = value_.get_value();
982 get_selected_ids: function() {
983 var id = this.dataset.ids[this.dataset.index];
984 return id ? [id] : [];
986 recursive_save: function() {
988 return $.when(this.save()).then(function(res) {
989 if (self.dataset.parent_view)
990 return self.dataset.parent_view.recursive_save();
993 recursive_reload: function() {
996 if (self.dataset.parent_view)
997 pre = self.dataset.parent_view.recursive_reload();
998 return pre.then(function() {
999 return self.reload();
1002 is_dirty: function() {
1003 return _.any(this.fields, function (value_) {
1004 return value_._dirty_flag;
1007 is_interactible_record: function() {
1008 var id = this.datarecord.id;
1010 if (this.options.not_interactible_on_create)
1012 } else if (typeof(id) === "string") {
1013 if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
1018 sidebar_eval_context: function () {
1019 return $.when(this.build_eval_context());
1021 open_defaults_dialog: function () {
1023 var display = function (field, value) {
1024 if (!value) { return value; }
1025 if (field instanceof instance.web.form.FieldSelection) {
1026 return _(field.get('values')).find(function (option) {
1027 return option[0] === value;
1029 } else if (field instanceof instance.web.form.FieldMany2One) {
1030 return field.get_displayed();
1034 var fields = _.chain(this.fields)
1035 .map(function (field) {
1036 var value = field.get_value();
1037 // ignore fields which are empty, invisible, readonly, o2m
1040 || field.get('invisible')
1041 || field.get("readonly")
1042 || field.field.type === 'one2many'
1043 || field.field.type === 'many2many'
1044 || field.field.type === 'binary'
1045 || field.password) {
1051 string: field.string,
1053 displayed: display(field, value),
1057 .sortBy(function (field) { return field.string; })
1059 var conditions = _.chain(self.fields)
1060 .filter(function (field) { return field.field.change_default; })
1061 .map(function (field) {
1062 var value = field.get_value();
1065 string: field.string,
1067 displayed: display(field, value),
1071 var d = new instance.web.Dialog(this, {
1072 title: _t("Set Default"),
1075 conditions: conditions
1078 {text: _t("Close"), click: function () { d.close(); }},
1079 {text: _t("Save default"), click: function () {
1080 var $defaults = d.$el.find('#formview_default_fields');
1081 var field_to_set = $defaults.val();
1082 if (!field_to_set) {
1083 $defaults.parent().addClass('oe_form_invalid');
1086 var condition = d.$el.find('#formview_default_conditions').val(),
1087 all_users = d.$el.find('#formview_default_all').is(':checked');
1088 new instance.web.DataSet(self, 'ir.values').call(
1092 self.fields[field_to_set].get_value(),
1096 ]).done(function () { d.close(); });
1100 d.template = 'FormView.set_default';
1103 register_field: function(field, name) {
1104 this.fields[name] = field;
1105 this.fields_order.push(name);
1106 if (JSON.parse(field.node.attrs.default_focus || "0")) {
1107 this.default_focus_field = field;
1110 field.on('focused', null, this.proxy('widgetFocused'))
1111 .on('blurred', null, this.proxy('widgetBlurred'));
1112 if (this.get_field_desc(name).translate) {
1113 this.translatable_fields.push(field);
1115 field.on('changed_value', this, function() {
1116 if (field.is_syntax_valid()) {
1117 this.trigger('field_changed:' + name);
1119 if (field._inhibit_on_change_flag) {
1122 field._dirty_flag = true;
1123 if (field.is_syntax_valid()) {
1124 this.do_onchange(field);
1125 this.on_form_changed(true);
1126 this.do_notify_change();
1130 get_field_desc: function(field_name) {
1131 return this.fields_view.fields[field_name];
1133 get_field_value: function(field_name) {
1134 return this.fields[field_name].get_value();
1136 compute_domain: function(expression) {
1137 return instance.web.form.compute_domain(expression, this.fields);
1139 _build_view_fields_values: function() {
1140 var a_dataset = this.dataset;
1141 var fields_values = this.get_fields_values();
1142 var active_id = a_dataset.ids[a_dataset.index];
1143 _.extend(fields_values, {
1144 active_id: active_id || false,
1145 active_ids: active_id ? [active_id] : [],
1146 active_model: a_dataset.model,
1149 if (a_dataset.parent_view) {
1150 fields_values.parent = a_dataset.parent_view.get_fields_values();
1152 return fields_values;
1154 build_eval_context: function() {
1155 var a_dataset = this.dataset;
1156 return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
1161 * Interface to be implemented by rendering engines for the form view.
1163 instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
1164 set_fields_view: function(fields_view) {},
1165 set_fields_registry: function(fields_registry) {},
1166 render_to: function($el) {},
1170 * Default rendering engine for the form view.
1172 * It is necessary to set the view using set_view() before usage.
1174 instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
1175 init: function(view) {
1178 set_fields_view: function(fvg) {
1180 this.version = parseFloat(this.fvg.arch.attrs.version);
1181 if (isNaN(this.version)) {
1185 set_tags_registry: function(tags_registry) {
1186 this.tags_registry = tags_registry;
1188 set_fields_registry: function(fields_registry) {
1189 this.fields_registry = fields_registry;
1191 set_widgets_registry: function(widgets_registry) {
1192 this.widgets_registry = widgets_registry;
1194 // Backward compatibility tools, current default version: v7
1195 process_version: function() {
1196 if (this.version < 7.0) {
1197 this.$form.find('form:first').wrapInner('<group col="4"/>');
1198 this.$form.find('page').each(function() {
1199 if (!$(this).parents('field').length) {
1200 $(this).wrapInner('<group col="4"/>');
1205 get_arch_fragment: function() {
1206 var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
1207 // IE won't allow custom button@type and will revert it to spec default : 'submit'
1208 $('button', doc).each(function() {
1209 $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
1211 // IE's html parser is also a css parser. How convenient...
1212 $('board', doc).each(function() {
1213 $(this).attr('layout', $(this).attr('style'));
1215 return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
1217 render_to: function($target) {
1219 this.$target = $target;
1221 this.$form = this.get_arch_fragment();
1223 this.process_version();
1225 this.fields_to_init = [];
1226 this.tags_to_init = [];
1227 this.widgets_to_init = [];
1229 this.process(this.$form);
1231 this.$form.appendTo(this.$target);
1233 this.to_replace = [];
1235 _.each(this.fields_to_init, function($elem) {
1236 var name = $elem.attr("name");
1237 if (!self.fvg.fields[name]) {
1238 throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
1240 var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
1242 throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
1244 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1245 var $label = self.labels[$elem.attr("name")];
1247 w.set_input_id($label.attr("for"));
1249 self.alter_field(w);
1250 self.view.register_field(w, $elem.attr("name"));
1251 self.to_replace.push([w, $elem]);
1253 _.each(this.tags_to_init, function($elem) {
1254 var tag_name = $elem[0].tagName.toLowerCase();
1255 var obj = self.tags_registry.get_object(tag_name);
1256 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1257 self.to_replace.push([w, $elem]);
1259 _.each(this.widgets_to_init, function($elem) {
1260 var widget_type = $elem.attr("type");
1261 var obj = self.widgets_registry.get_object(widget_type);
1262 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1263 self.to_replace.push([w, $elem]);
1266 init_fields: function() {
1268 _.each(this.to_replace, function(el) {
1269 defs.push(el[0].replace(el[1]));
1270 if (el[1].children().length) {
1271 el[0].$el.append(el[1].children());
1274 this.to_replace = [];
1275 return $.when.apply($, defs);
1277 render_element: function(template /* dictionaries */) {
1278 var dicts = [].slice.call(arguments).slice(1);
1279 var dict = _.extend.apply(_, dicts);
1280 dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
1281 return $(QWeb.render(template, dict));
1283 alter_field: function(field) {
1285 toggle_layout_debugging: function() {
1286 if (!this.$target.has('.oe_layout_debug_cell:first').length) {
1287 this.$target.find('[title]').removeAttr('title');
1288 this.$target.find('.oe_form_group_cell').each(function() {
1289 var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
1290 $(this).attr('title', text);
1293 this.$target.toggleClass('oe_layout_debugging');
1295 process: function($tag) {
1297 var tagname = $tag[0].nodeName.toLowerCase();
1298 if (this.tags_registry.contains(tagname)) {
1299 this.tags_to_init.push($tag);
1300 return (tagname === 'button') ? this.process_button($tag) : $tag;
1302 var fn = self['process_' + tagname];
1304 var args = [].slice.call(arguments);
1306 return fn.apply(self, args);
1308 // generic tag handling, just process children
1309 $tag.children().each(function() {
1310 self.process($(this));
1312 self.handle_common_properties($tag, $tag);
1313 $tag.removeAttr("modifiers");
1317 process_button: function ($button) {
1319 $button.children().each(function() {
1320 self.process($(this));
1324 process_widget: function($widget) {
1325 this.widgets_to_init.push($widget);
1328 process_sheet: function($sheet) {
1329 var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
1330 this.handle_common_properties($new_sheet, $sheet);
1331 var $dst = $new_sheet.find('.oe_form_sheet');
1332 $sheet.contents().appendTo($dst);
1333 $sheet.before($new_sheet).remove();
1334 this.process($new_sheet);
1336 process_form: function($form) {
1337 if ($form.find('> sheet').length === 0) {
1338 $form.addClass('oe_form_nosheet');
1340 var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
1341 this.handle_common_properties($new_form, $form);
1342 $form.contents().appendTo($new_form);
1343 if ($form[0] === this.$form[0]) {
1344 // If root element, replace it
1345 this.$form = $new_form;
1347 $form.before($new_form).remove();
1349 this.process($new_form);
1352 * Used by direct <field> children of a <group> tag only
1353 * This method will add the implicit <label...> for every field
1356 preprocess_field: function($field) {
1358 var name = $field.attr('name'),
1359 field_colspan = parseInt($field.attr('colspan'), 10),
1360 field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
1362 if ($field.attr('nolabel') === '1')
1364 $field.attr('nolabel', '1');
1366 this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
1367 $(el).parents().each(function(unused, tag) {
1368 var name = tag.tagName.toLowerCase();
1369 if (name === "field" || name in self.tags_registry.map)
1376 var $label = $('<label/>').attr({
1378 "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
1379 "string": $field.attr('string'),
1380 "help": $field.attr('help'),
1381 "class": $field.attr('class'),
1383 $label.insertBefore($field);
1384 if (field_colspan > 1) {
1385 $field.attr('colspan', field_colspan - 1);
1389 process_field: function($field) {
1390 if ($field.parent().is('group')) {
1391 // No implicit labels for normal fields, only for <group> direct children
1392 var $label = this.preprocess_field($field);
1394 this.process($label);
1397 this.fields_to_init.push($field);
1400 process_group: function($group) {
1402 $group.children('field').each(function() {
1403 self.preprocess_field($(this));
1405 var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
1407 if ($new_group.first().is('table.oe_form_group')) {
1408 $table = $new_group;
1409 } else if ($new_group.filter('table.oe_form_group').length) {
1410 $table = $new_group.filter('table.oe_form_group').first();
1412 $table = $new_group.find('table.oe_form_group').first();
1416 cols = parseInt($group.attr('col') || 2, 10),
1420 $group.children().each(function(a,b,c) {
1421 var $child = $(this);
1422 var colspan = parseInt($child.attr('colspan') || 1, 10);
1423 var tagName = $child[0].tagName.toLowerCase();
1424 var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
1425 var newline = tagName === 'newline';
1427 // Note FME: those classes are used in layout debug mode
1428 if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
1429 $tr.addClass('oe_form_group_row_incomplete');
1431 $tr.addClass('oe_form_group_row_newline');
1438 if (!$tr || row_cols < colspan) {
1439 $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
1441 } else if (tagName==='group') {
1442 // When <group> <group/><group/> </group>, we need a spacing between the two groups
1443 $td.addClass('oe_group_right');
1445 row_cols -= colspan;
1447 // invisibility transfer
1448 var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
1449 var invisible = field_modifiers.invisible;
1450 self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
1452 $tr.append($td.append($child));
1453 children.push($child[0]);
1455 if (row_cols && $td) {
1456 $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
1458 $group.before($new_group).remove();
1460 $table.find('> tbody > tr').each(function() {
1461 var to_compute = [],
1464 $(this).children().each(function() {
1466 $child = $td.children(':first');
1467 if ($child.attr('cell-class')) {
1468 $td.addClass($child.attr('cell-class'));
1470 switch ($child[0].tagName.toLowerCase()) {
1474 if ($child.attr('for')) {
1475 $td.attr('width', '1%').addClass('oe_form_group_cell_label');
1476 row_cols-= $td.attr('colspan') || 1;
1481 var width = _.str.trim($child.attr('width') || ''),
1482 iwidth = parseInt(width, 10);
1484 if (width.substr(-1) === '%') {
1486 width = iwidth + '%';
1489 $td.css('min-width', width + 'px');
1491 $td.attr('width', width);
1492 $child.removeAttr('width');
1493 row_cols-= $td.attr('colspan') || 1;
1495 to_compute.push($td);
1501 var unit = Math.floor(total / row_cols);
1502 if (!$(this).is('.oe_form_group_row_incomplete')) {
1503 _.each(to_compute, function($td, i) {
1504 var width = parseInt($td.attr('colspan'), 10) * unit;
1505 $td.attr('width', width + '%');
1511 _.each(children, function(el) {
1512 self.process($(el));
1514 this.handle_common_properties($new_group, $group);
1517 process_notebook: function($notebook) {
1520 $notebook.find('> page').each(function() {
1521 var $page = $(this);
1522 var page_attrs = $page.getAttributes();
1523 page_attrs.id = _.uniqueId('notebook_page_');
1524 var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
1525 $page.contents().appendTo($new_page);
1526 $page.before($new_page).remove();
1527 var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
1528 page_attrs.__page = $new_page;
1529 page_attrs.__ic = ic;
1530 pages.push(page_attrs);
1532 $new_page.children().each(function() {
1533 self.process($(this));
1536 var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
1537 $notebook.contents().appendTo($new_notebook);
1538 $notebook.before($new_notebook).remove();
1539 self.process($($new_notebook.children()[0]));
1540 //tabs and invisibility handling
1541 $new_notebook.tabs();
1542 _.each(pages, function(page, i) {
1545 page.__ic.on("change:effective_invisible", null, function() {
1546 if (!page.__ic.get('effective_invisible') && page.autofocus) {
1547 $new_notebook.tabs('select', i);
1550 var current = $new_notebook.tabs("option", "selected");
1551 if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
1553 var first_visible = _.find(_.range(pages.length), function(i2) {
1554 return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
1556 if (first_visible !== undefined) {
1557 $new_notebook.tabs('select', first_visible);
1562 this.handle_common_properties($new_notebook, $notebook);
1563 return $new_notebook;
1565 process_separator: function($separator) {
1566 var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
1567 $separator.before($new_separator).remove();
1568 this.handle_common_properties($new_separator, $separator);
1569 return $new_separator;
1571 process_label: function($label) {
1572 var name = $label.attr("for"),
1573 field_orm = this.fvg.fields[name];
1575 string: $label.attr('string') || (field_orm || {}).string || '',
1576 help: $label.attr('help') || (field_orm || {}).help || '',
1577 _for: name ? _.uniqueId('oe-field-input-') : undefined,
1579 var align = parseFloat(dict.align);
1580 if (isNaN(align) || align === 1) {
1582 } else if (align === 0) {
1588 var $new_label = this.render_element('FormRenderingLabel', dict);
1589 $label.before($new_label).remove();
1590 this.handle_common_properties($new_label, $label);
1592 this.labels[name] = $new_label;
1596 handle_common_properties: function($new_element, $node) {
1597 var str_modifiers = $node.attr("modifiers") || "{}";
1598 var modifiers = JSON.parse(str_modifiers);
1600 if (modifiers.invisible !== undefined)
1601 ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
1602 $new_element.addClass($node.attr("class") || "");
1603 $new_element.attr('style', $node.attr('style'));
1604 return {invisibility_changer: ic,};
1611 If you read this documentation, it probably means that you were asked to use a form view widget outside of
1612 a form view. Before going further, you must understand that those fields were never really created for
1613 that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
1614 you to hack the system with more style.
1616 instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
1617 init: function(parent, eval_context) {
1618 this._super(parent);
1619 this.field_descs = {};
1620 this.eval_context = eval_context || {};
1622 display_invalid_fields: false,
1623 actual_mode: 'create',
1626 get_field_desc: function(field_name) {
1627 if (this.field_descs[field_name] === undefined) {
1628 this.field_descs[field_name] = {
1632 return this.field_descs[field_name];
1634 extend_field_desc: function(fields) {
1636 _.each(fields, function(v, k) {
1637 _.extend(self.get_field_desc(k), v);
1640 get_field_value: function(field_name) {
1643 set_values: function(values) {
1646 compute_domain: function(expression) {
1647 return instance.web.form.compute_domain(expression, {});
1649 build_eval_context: function() {
1650 return new instance.web.CompoundContext(this.eval_context);
1654 instance.web.form.compute_domain = function(expr, fields) {
1655 if (! (expr instanceof Array))
1658 for (var i = expr.length - 1; i >= 0; i--) {
1660 if (ex.length == 1) {
1661 var top = stack.pop();
1664 stack.push(stack.pop() || top);
1667 stack.push(stack.pop() && top);
1673 throw new Error(_.str.sprintf(
1674 _t("Unknown operator %s in domain %s"),
1675 ex, JSON.stringify(expr)));
1679 var field = fields[ex[0]];
1681 throw new Error(_.str.sprintf(
1682 _t("Unknown field %s in domain %s"),
1683 ex[0], JSON.stringify(expr)));
1685 var field_value = field.get_value ? field.get_value() : field.value;
1689 switch (op.toLowerCase()) {
1692 stack.push(_.isEqual(field_value, val));
1696 stack.push(!_.isEqual(field_value, val));
1699 stack.push(field_value < val);
1702 stack.push(field_value > val);
1705 stack.push(field_value <= val);
1708 stack.push(field_value >= val);
1711 if (!_.isArray(val)) val = [val];
1712 stack.push(_(val).contains(field_value));
1715 if (!_.isArray(val)) val = [val];
1716 stack.push(!_(val).contains(field_value));
1720 _t("Unsupported operator %s in domain %s"),
1721 op, JSON.stringify(expr));
1724 return _.all(stack, _.identity);
1727 instance.web.form.is_bin_size = function(v) {
1728 return (/^\d+(\.\d*)? \w+$/).test(v);
1732 * Must be applied over an class already possessing the PropertiesMixin.
1734 * Apply the result of the "invisible" domain to this.$el.
1736 instance.web.form.InvisibilityChangerMixin = {
1737 init: function(field_manager, invisible_domain) {
1739 this._ic_field_manager = field_manager;
1740 this._ic_invisible_modifier = invisible_domain;
1741 this._ic_field_manager.on("view_content_has_changed", this, function() {
1742 var result = self._ic_invisible_modifier === undefined ? false :
1743 self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
1744 self.set({"invisible": result});
1746 this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
1747 var check = function() {
1748 if (self.get("invisible") || self.get('force_invisible')) {
1749 self.set({"effective_invisible": true});
1751 self.set({"effective_invisible": false});
1754 this.on('change:invisible', this, check);
1755 this.on('change:force_invisible', this, check);
1759 this.on("change:effective_invisible", this, this._check_visibility);
1760 this._check_visibility();
1762 _check_visibility: function() {
1763 this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
1767 instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
1768 init: function(parent, field_manager, invisible_domain, $el) {
1769 this.setParent(parent);
1770 instance.web.PropertiesMixin.init.call(this);
1771 instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
1778 Base class for all fields, custom widgets and buttons to be displayed in the form view.
1781 - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
1782 the values of the "readonly" property and the "mode" property on the field manager.
1784 instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
1786 * @constructs instance.web.form.FormWidget
1787 * @extends instance.web.Widget
1789 * @param field_manager
1792 init: function(field_manager, node) {
1793 this._super(field_manager);
1794 this.field_manager = field_manager;
1795 if (this.field_manager instanceof instance.web.FormView)
1796 this.view = this.field_manager;
1798 this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
1799 instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
1801 this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
1807 // some events to make the property "effective_readonly" sync automatically with "readonly" and
1808 // "mode" on field_manager
1810 var test_effective_readonly = function() {
1811 self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
1813 this.on("change:readonly", this, test_effective_readonly);
1814 this.field_manager.on("change:actual_mode", this, test_effective_readonly);
1815 test_effective_readonly.call(this);
1817 renderElement: function() {
1818 this.process_modifiers();
1820 this.$el.addClass(this.node.attrs["class"] || "");
1822 destroy: function() {
1823 $.fn.tooltip('destroy');
1824 this._super.apply(this, arguments);
1827 * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
1829 * This method is an utility method that is meant to be called by child classes.
1831 * @param {jQuery} $e jQuery object of elements to bind focus/blur on
1833 setupFocus: function ($e) {
1836 focus: function () { self.trigger('focused'); },
1837 blur: function () { self.trigger('blurred'); }
1840 process_modifiers: function() {
1842 for (var a in this.modifiers) {
1843 if (!this.modifiers.hasOwnProperty(a)) { continue; }
1844 if (!_.include(["invisible"], a)) {
1845 var val = this.field_manager.compute_domain(this.modifiers[a]);
1851 do_attach_tooltip: function(widget, trigger, options) {
1852 widget = widget || this;
1853 trigger = trigger || this.$el;
1854 options = _.extend({
1855 delay: { show: 500, hide: 0 },
1857 var template = widget.template + '.tooltip';
1858 if (!QWeb.has_template(template)) {
1859 template = 'WidgetLabel.tooltip';
1861 return QWeb.render(template, {
1862 debug: instance.session.debug,
1867 //only show tooltip if we are in debug or if we have a help to show, otherwise it will display
1869 if (instance.session.debug || widget.node.attrs.help || (widget.field && widget.field.help)){
1870 $(trigger).tooltip(options);
1874 * Builds a new context usable for operations related to fields by merging
1875 * the fields'context with the action's context.
1877 build_context: function() {
1878 // only use the model's context if there is not context on the node
1879 var v_context = this.node.attrs.context;
1881 v_context = (this.field || {}).context || {};
1884 if (v_context.__ref || true) { //TODO: remove true
1885 var fields_values = this.field_manager.build_eval_context();
1886 v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
1890 build_domain: function() {
1891 var f_domain = this.field.domain || [];
1892 var n_domain = this.node.attrs.domain || null;
1893 // if there is a domain on the node, overrides the model's domain
1894 var final_domain = n_domain !== null ? n_domain : f_domain;
1895 if (!(final_domain instanceof Array) || true) { //TODO: remove true
1896 var fields_values = this.field_manager.build_eval_context();
1897 final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
1899 return final_domain;
1903 instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
1904 template: 'WidgetButton',
1905 init: function(field_manager, node) {
1906 node.attrs.type = node.attrs['data-button-type'];
1907 this.is_stat_button = /\boe_stat_button\b/.test(node.attrs['class']);
1908 this.icon_class = node.attrs.icon && "stat_button_icon fa " + node.attrs.icon + " fa-fw";
1909 this._super(field_manager, node);
1910 this.force_disabled = false;
1911 this.string = (this.node.attrs.string || '').replace(/_/g, '');
1912 if (JSON.parse(this.node.attrs.default_focus || "0")) {
1913 // TODO fme: provide enter key binding to widgets
1914 this.view.default_focus_button = this;
1916 if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
1917 this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
1921 this._super.apply(this, arguments);
1922 this.view.on('view_content_has_changed', this, this.check_disable);
1923 this.check_disable();
1924 this.$el.click(this.on_click);
1925 if (this.node.attrs.help || instance.session.debug) {
1926 this.do_attach_tooltip();
1928 this.setupFocus(this.$el);
1930 on_click: function() {
1932 this.force_disabled = true;
1933 this.check_disable();
1934 this.execute_action().always(function() {
1935 self.force_disabled = false;
1936 self.check_disable();
1939 execute_action: function() {
1941 var exec_action = function() {
1942 if (self.node.attrs.confirm) {
1943 var def = $.Deferred();
1944 var dialog = new instance.web.Dialog(this, {
1945 title: _t('Confirm'),
1947 {text: _t("Cancel"), click: function() {
1948 this.parents('.modal').modal('hide');
1951 {text: _t("Ok"), click: function() {
1953 self.on_confirmed().always(function() {
1954 self2.parents('.modal').modal('hide');
1959 }, $('<div/>').text(self.node.attrs.confirm)).open();
1960 dialog.on("closing", null, function() {def.resolve();});
1961 return def.promise();
1963 return self.on_confirmed();
1966 if (!this.node.attrs.special) {
1967 return this.view.recursive_save().then(exec_action);
1969 return exec_action();
1972 on_confirmed: function() {
1975 var context = this.build_context();
1976 return this.view.do_execute_action(
1977 _.extend({}, this.node.attrs, {context: context}),
1978 this.view.dataset, this.view.datarecord.id, function (reason) {
1979 if (!_.isObject(reason)) {
1980 self.view.recursive_reload();
1984 check_disable: function() {
1985 var disabled = (this.force_disabled || !this.view.is_interactible_record());
1986 this.$el.prop('disabled', disabled);
1987 this.$el.css('color', disabled ? 'grey' : '');
1992 * Interface to be implemented by fields.
1995 * - changed_value: triggered when the value of the field has changed. This can be due
1996 * to a user interaction or a call to set_value().
1999 instance.web.form.FieldInterface = {
2001 * Constructor takes 2 arguments:
2002 * - field_manager: Implements FieldManagerMixin
2003 * - node: the "<field>" node in json form
2005 init: function(field_manager, node) {},
2007 * Called by the form view to indicate the value of the field.
2009 * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
2010 * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
2011 * before the widget is inserted into the DOM.
2013 * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
2014 * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
2015 * also be able to handle any other format commonly used in the _defaults key on the models in the addons
2016 * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
2017 * no information is ever given to know which format is used.
2019 set_value: function(value_) {},
2021 * Get the current value of the widget.
2023 * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
2024 * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
2025 * For example if the field is marked as "required", a call to get_value() can return false.
2027 * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
2028 * return a default value according to the type of field.
2030 * This method is always assumed to perform synchronously, it can not return a promise.
2032 * If there was no user interaction to modify the value of the field, it is always assumed that
2033 * get_value() return the same semantic value than the one passed in the last call to set_value(),
2034 * although the syntax can be different. This can be the case for type of fields that have a different
2035 * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
2037 get_value: function() {},
2039 * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
2042 set_input_id: function(id) {},
2044 * Returns true if is_syntax_valid() returns true and the value is semantically
2045 * valid too according to the semantic restrictions applied to the field.
2047 is_valid: function() {},
2049 * Returns true if the field holds a value which is syntactically correct, ignoring
2050 * the potential semantic restrictions applied to the field.
2052 is_syntax_valid: function() {},
2054 * Must set the focus on the field. Return false if field is not focusable.
2056 focus: function() {},
2058 * Called when the translate button is clicked.
2060 on_translate: function() {},
2062 This method is called by the form view before reading on_change values and before saving. It tells
2063 the field to save its value before reading it using get_value(). Must return a promise.
2065 commit_value: function() {},
2069 * Abstract class for classes implementing FieldInterface.
2072 * - value: useful property to hold the value of the field. By default, set_value() and get_value()
2073 * set and retrieve the value property. Changing the value property also triggers automatically
2074 * a 'changed_value' event that inform the view to trigger on_changes.
2077 instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
2079 * @constructs instance.web.form.AbstractField
2080 * @extends instance.web.form.FormWidget
2082 * @param field_manager
2085 init: function(field_manager, node) {
2087 this._super(field_manager, node);
2088 this.name = this.node.attrs.name;
2089 this.field = this.field_manager.get_field_desc(this.name);
2090 this.widget = this.node.attrs.widget;
2091 this.string = this.node.attrs.string || this.field.string || this.name;
2092 this.options = instance.web.py_eval(this.node.attrs.options || '{}');
2093 this.set({'value': false});
2095 this.on("change:value", this, function() {
2096 this.trigger('changed_value');
2097 this._check_css_flags();
2100 renderElement: function() {
2103 if (this.field.translate && this.view) {
2104 this.$el.addClass('oe_form_field_translatable');
2105 this.$el.find('.oe_field_translate').click(this.on_translate);
2107 this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
2108 this.do_attach_tooltip(this, this.$label[0] || this.$el);
2109 if (instance.session.debug) {
2110 this.$label.off('dblclick').on('dblclick', function() {
2111 console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
2113 console.log("window.w =", window.w);
2116 if (!this.disable_utility_classes) {
2117 this.off("change:required", this, this._set_required);
2118 this.on("change:required", this, this._set_required);
2119 this._set_required();
2121 this._check_visibility();
2122 this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
2123 this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
2124 this._check_css_flags();
2127 var tmp = this._super();
2128 this.on("change:value", this, function() {
2129 if (! this.no_rerender)
2130 this.render_value();
2132 this.render_value();
2135 * Private. Do not use.
2137 _set_required: function() {
2138 this.$el.toggleClass('oe_form_required', this.get("required"));
2140 set_value: function(value_) {
2141 this.set({'value': value_});
2143 get_value: function() {
2144 return this.get('value');
2147 Utility method that all implementations should use to change the
2148 value without triggering a re-rendering.
2150 internal_set_value: function(value_) {
2151 var tmp = this.no_rerender;
2152 this.no_rerender = true;
2153 this.set({'value': value_});
2154 this.no_rerender = tmp;
2157 This method is called each time the value is modified.
2159 render_value: function() {},
2160 is_valid: function() {
2161 return this.is_syntax_valid() && !(this.get('required') && this.is_false());
2163 is_syntax_valid: function() {
2167 * Method useful to implement to ease validity testing. Must return true if the current
2168 * value is similar to false in OpenERP.
2170 is_false: function() {
2171 return this.get('value') === false;
2173 _check_css_flags: function() {
2174 if (this.field.translate) {
2175 this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
2177 if (!this.disable_utility_classes) {
2178 if (this.field_manager.get('display_invalid_fields')) {
2179 this.$el.toggleClass('oe_form_invalid', !this.is_valid());
2186 set_input_id: function(id) {
2187 this.id_for_label = id;
2189 on_translate: function() {
2191 var trans = new instance.web.DataSet(this, 'ir.translation');
2192 return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
2197 set_dimensions: function (height, width) {
2203 commit_value: function() {
2209 * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
2212 instance.web.form.ReinitializeWidgetMixin = {
2214 * Default implementation of, you should not override it, use initialize_field() instead.
2217 this.initialize_field();
2220 initialize_field: function() {
2221 this.on("change:effective_readonly", this, this.reinitialize);
2222 this.initialize_content();
2224 reinitialize: function() {
2225 this.destroy_content();
2226 this.renderElement();
2227 this.initialize_content();
2230 * Called to destroy anything that could have been created previously, called before a
2231 * re-initialization.
2233 destroy_content: function() {},
2235 * Called to initialize the content.
2237 initialize_content: function() {},
2241 * A mixin to apply on any field that has to completely re-render when its readonly state
2244 instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
2245 reinitialize: function() {
2246 instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
2247 this.view.render_value_defs.push(this.render_value());
2252 Some hack to make placeholders work in ie9.
2254 if (!('placeholder' in document.createElement('input'))) {
2255 document.addEventListener("DOMNodeInserted",function(event){
2256 var nodename = event.target.nodeName.toLowerCase();
2257 if ( nodename === "input" || nodename == "textarea" ) {
2258 $(event.target).placeholder();
2263 instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2264 template: 'FieldChar',
2265 widget_class: 'oe_form_field_char',
2267 'change input': 'store_dom_value',
2269 init: function (field_manager, node) {
2270 this._super(field_manager, node);
2271 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2273 initialize_content: function() {
2274 this.setupFocus(this.$('input'));
2276 store_dom_value: function () {
2277 if (!this.get('effective_readonly')
2278 && this.$('input').length
2279 && this.is_syntax_valid()) {
2280 this.internal_set_value(
2282 this.$('input').val()));
2285 commit_value: function () {
2286 this.store_dom_value();
2287 return this._super();
2289 render_value: function() {
2290 var show_value = this.format_value(this.get('value'), '');
2291 if (!this.get("effective_readonly")) {
2292 this.$el.find('input').val(show_value);
2294 if (this.password) {
2295 show_value = new Array(show_value.length + 1).join('*');
2297 this.$(".oe_form_char_content").text(show_value);
2300 is_syntax_valid: function() {
2301 if (!this.get("effective_readonly") && this.$("input").size() > 0) {
2303 this.parse_value(this.$('input').val(), '');
2311 parse_value: function(val, def) {
2312 return instance.web.parse_value(val, this, def);
2314 format_value: function(val, def) {
2315 return instance.web.format_value(val, this, def);
2317 is_false: function() {
2318 return this.get('value') === '' || this._super();
2321 var input = this.$('input:first')[0];
2322 return input ? input.focus() : false;
2324 set_dimensions: function (height, width) {
2325 this._super(height, width);
2326 this.$('input').css({
2333 instance.web.form.KanbanSelection = instance.web.form.FieldChar.extend({
2334 init: function (field_manager, node) {
2335 this._super(field_manager, node);
2337 prepare_dropdown_selection: function() {
2340 var selection = self.field.selection || [];
2341 _.map(selection, function(res) {
2345 'state_name': res[1],
2347 if (res[0] == 'normal') { value['state_class'] = 'oe_kanban_status'; }
2348 else if (res[0] == 'done') { value['state_class'] = 'oe_kanban_status oe_kanban_status_green'; }
2349 else { value['state_class'] = 'oe_kanban_status oe_kanban_status_red'; }
2354 render_value: function() {
2356 this.record_id = this.view.datarecord.id;
2357 this.states = this.prepare_dropdown_selection();;
2358 this.$el.html(QWeb.render("KanbanSelection", {'widget': self}));
2359 this.$el.find('li').on('click', this.set_kanban_selection.bind(this));
2361 /* setting the value: in view mode, perform an asynchronous call and reload
2362 the form view; in edit mode, use set_value to save the new value that will
2363 be written when saving the record. */
2364 set_kanban_selection: function (ev) {
2366 var li = $(ev.target).closest('li');
2368 var value = String(li.data('value'));
2369 if (this.view.get('actual_mode') == 'view') {
2370 var write_values = {}
2371 write_values[self.name] = value;
2372 return this.view.dataset._model.call(
2376 self.view.dataset.get_context()
2377 ]).done(self.reload_record.bind(self));
2380 return this.set_value(value);
2384 reload_record: function() {
2389 instance.web.form.Priority = instance.web.form.FieldChar.extend({
2390 init: function (field_manager, node) {
2391 this._super(field_manager, node);
2393 prepare_priority: function() {
2395 var selection = this.field.selection || [];
2396 var init_value = selection && selection[0][0] || 0;
2397 var data = _.map(selection.slice(1), function(element, index) {
2399 'value': element[0],
2401 'click_value': element[0],
2403 if (index == 0 && self.get('value') == element[0]) {
2404 value['click_value'] = init_value;
2410 render_value: function() {
2412 this.record_id = this.view.datarecord.id;
2413 this.priorities = this.prepare_priority();
2414 this.$el.html(QWeb.render("Priority", {'widget': this}));
2415 this.$el.find('li').on('click', this.set_priority.bind(this));
2417 /* setting the value: in view mode, perform an asynchronous call and reload
2418 the form view; in edit mode, use set_value to save the new value that will
2419 be written when saving the record. */
2420 set_priority: function (ev) {
2422 var li = $(ev.target).closest('li');
2424 var value = String(li.data('value'));
2425 if (this.view.get('actual_mode') == 'view') {
2426 var write_values = {}
2427 write_values[self.name] = value;
2428 return this.view.dataset._model.call(
2432 self.view.dataset.get_context()
2433 ]).done(self.reload_record.bind(self));
2436 return this.set_value(value);
2441 reload_record: function() {
2446 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2447 process_modifiers: function () {
2449 this.set({ readonly: true });
2453 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2454 template: 'FieldEmail',
2455 initialize_content: function() {
2457 var $button = this.$el.find('button');
2458 $button.click(this.on_button_clicked);
2459 this.setupFocus($button);
2461 render_value: function() {
2462 if (!this.get("effective_readonly")) {
2466 .attr('href', 'mailto:' + this.get('value'))
2467 .text(this.get('value') || '');
2470 on_button_clicked: function() {
2471 if (!this.get('value') || !this.is_syntax_valid()) {
2472 this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2474 location.href = 'mailto:' + this.get('value');
2479 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2480 template: 'FieldUrl',
2481 initialize_content: function() {
2483 var $button = this.$el.find('button');
2484 $button.click(this.on_button_clicked);
2485 this.setupFocus($button);
2487 render_value: function() {
2488 if (!this.get("effective_readonly")) {
2491 var tmp = this.get('value');
2492 var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2494 tmp = "http://" + this.get('value');
2496 var text = this.get('value') ? this.node.attrs.text || tmp : '';
2497 this.$el.find('a').attr('href', tmp).text(text);
2500 on_button_clicked: function() {
2501 if (!this.get('value')) {
2502 this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2504 var url = $.trim(this.get('value'));
2505 if(/^www\./i.test(url))
2506 url = 'http://'+url;
2512 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2513 is_field_number: true,
2514 widget_class: 'oe_form_field_float',
2515 init: function (field_manager, node) {
2516 this._super(field_manager, node);
2517 this.internal_set_value(0);
2518 if (this.node.attrs.digits) {
2519 this.digits = this.node.attrs.digits;
2521 this.digits = this.field.digits;
2524 set_value: function(value_) {
2525 if (value_ === false || value_ === undefined) {
2526 // As in GTK client, floats default to 0
2529 this._super.apply(this, [value_]);
2531 focus: function () {
2532 var $input = this.$('input:first');
2533 return $input.length ? $input.select() : false;
2537 instance.web.form.FieldCharDomain = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2538 init: function(field_manager, node) {
2539 this._super.apply(this, arguments);
2543 this._super.apply(this, arguments);
2544 this.on("change:effective_readonly", this, function () {
2545 this.display_field();
2547 this.display_field();
2548 return this._super();
2550 set_value: function(value_) {
2552 this.set('value', value_ || false);
2553 this.display_field();
2555 display_field: function() {
2557 this.$el.html(instance.web.qweb.render("FieldCharDomain", {widget: this}));
2558 if (this.get('value')) {
2559 var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2560 var domain = instance.web.pyeval.eval('domain', this.get('value'));
2561 var ds = new instance.web.DataSetStatic(self, model, self.build_context());
2562 ds.call('search_count', [domain]).then(function (results) {
2563 $('.oe_domain_count', self.$el).text(results + ' records selected');
2564 if (self.get('effective_readonly')) {
2565 $('button span', self.$el).text(' See selection');
2568 $('button span', self.$el).text(' Change selection');
2572 $('.oe_domain_count', this.$el).text('0 record selected');
2573 $('button span', this.$el).text(' Select records');
2575 this.$('.select_records').on('click', self.on_click);
2577 on_click: function(event) {
2578 event.preventDefault();
2580 var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2581 this.pop = new instance.web.form.SelectCreatePopup(this);
2582 this.pop.select_element(
2584 title: this.get('effective_readonly') ? 'Selected records' : 'Select records...',
2585 readonly: this.get('effective_readonly'),
2586 disable_multiple_selection: this.get('effective_readonly'),
2587 no_create: this.get('effective_readonly'),
2588 }, [], this.build_context());
2589 this.pop.on("elements_selected", self, function(element_ids) {
2590 if (this.pop.$('input.oe_list_record_selector').prop('checked')) {
2591 var search_data = this.pop.searchview.build_search_data();
2592 var domain_done = instance.web.pyeval.eval_domains_and_contexts({
2593 domains: search_data.domains,
2594 contexts: search_data.contexts,
2595 group_by_seq: search_data.groupbys || []
2596 }).then(function (results) {
2597 return results.domain;
2601 var domain = [["id", "in", element_ids]];
2602 var domain_done = $.Deferred().resolve(domain);
2604 $.when(domain_done).then(function (domain) {
2605 var domain = self.pop.dataset.domain.concat(domain || []);
2606 self.set_value(domain);
2612 instance.web.DateTimeWidget = instance.web.Widget.extend({
2613 template: "web.datepicker",
2614 jqueryui_object: 'datetimepicker',
2615 type_of_date: "datetime",
2617 'change .oe_datepicker_master': 'change_datetime',
2618 'keypress .oe_datepicker_master': 'change_datetime',
2620 init: function(parent) {
2621 this._super(parent);
2622 this.name = parent.name;
2626 this.$input = this.$el.find('input.oe_datepicker_master');
2627 this.$input_picker = this.$el.find('input.oe_datepicker_container');
2629 $.datepicker.setDefaults({
2630 clearText: _t('Clear'),
2631 clearStatus: _t('Erase the current date'),
2632 closeText: _t('Done'),
2633 closeStatus: _t('Close without change'),
2634 prevText: _t('<Prev'),
2635 prevStatus: _t('Show the previous month'),
2636 nextText: _t('Next>'),
2637 nextStatus: _t('Show the next month'),
2638 currentText: _t('Today'),
2639 currentStatus: _t('Show the current month'),
2640 monthNames: Date.CultureInfo.monthNames,
2641 monthNamesShort: Date.CultureInfo.abbreviatedMonthNames,
2642 monthStatus: _t('Show a different month'),
2643 yearStatus: _t('Show a different year'),
2644 weekHeader: _t('Wk'),
2645 weekStatus: _t('Week of the year'),
2646 dayNames: Date.CultureInfo.dayNames,
2647 dayNamesShort: Date.CultureInfo.abbreviatedDayNames,
2648 dayNamesMin: Date.CultureInfo.shortestDayNames,
2649 dayStatus: _t('Set DD as first week day'),
2650 dateStatus: _t('Select D, M d'),
2651 firstDay: Date.CultureInfo.firstDayOfWeek,
2652 initStatus: _t('Select a date'),
2655 $.timepicker.setDefaults({
2656 timeOnlyTitle: _t('Choose Time'),
2657 timeText: _t('Time'),
2658 hourText: _t('Hour'),
2659 minuteText: _t('Minute'),
2660 secondText: _t('Second'),
2661 currentText: _t('Now'),
2662 closeText: _t('Done')
2666 onClose: this.on_picker_select,
2667 onSelect: this.on_picker_select,
2671 showButtonPanel: true,
2672 firstDay: Date.CultureInfo.firstDayOfWeek
2674 // Some clicks in the datepicker dialog are not stopped by the
2675 // datepicker and "bubble through", unexpectedly triggering the bus's
2676 // click event. Prevent that.
2677 this.picker('widget').click(function (e) { e.stopPropagation(); });
2679 this.$el.find('img.oe_datepicker_trigger').click(function() {
2680 if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
2681 self.$input.focus();
2684 self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date());
2685 self.$input_picker.show();
2686 self.picker('show');
2687 self.$input_picker.hide();
2689 this.set_readonly(false);
2690 this.set({'value': false});
2692 picker: function() {
2693 return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
2695 on_picker_select: function(text, instance_) {
2696 var date = this.picker('getDate');
2698 .val(date ? this.format_client(date) : '')
2702 set_value: function(value_) {
2703 this.set({'value': value_});
2704 this.$input.val(value_ ? this.format_client(value_) : '');
2706 get_value: function() {
2707 return this.get('value');
2709 set_value_from_ui_: function() {
2710 var value_ = this.$input.val() || false;
2711 this.set({'value': this.parse_client(value_)});
2713 set_readonly: function(readonly) {
2714 this.readonly = readonly;
2715 this.$input.prop('readonly', this.readonly);
2716 this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
2718 is_valid_: function() {
2719 var value_ = this.$input.val();
2720 if (value_ === "") {
2724 this.parse_client(value_);
2731 parse_client: function(v) {
2732 return instance.web.parse_value(v, {"widget": this.type_of_date});
2734 format_client: function(v) {
2735 return instance.web.format_value(v, {"widget": this.type_of_date});
2737 change_datetime: function(e) {
2738 if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
2739 this.set_value_from_ui_();
2740 this.trigger("datetime_changed");
2743 commit_value: function () {
2744 this.change_datetime();
2748 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2749 jqueryui_object: 'datepicker',
2750 type_of_date: "date"
2753 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2754 template: "FieldDatetime",
2755 build_widget: function() {
2756 return new instance.web.DateTimeWidget(this);
2758 destroy_content: function() {
2759 if (this.datewidget) {
2760 this.datewidget.destroy();
2761 this.datewidget = undefined;
2764 initialize_content: function() {
2765 if (!this.get("effective_readonly")) {
2766 this.datewidget = this.build_widget();
2767 this.datewidget.on('datetime_changed', this, _.bind(function() {
2768 this.internal_set_value(this.datewidget.get_value());
2770 this.datewidget.appendTo(this.$el);
2771 this.setupFocus(this.datewidget.$input);
2774 render_value: function() {
2775 if (!this.get("effective_readonly")) {
2776 this.datewidget.set_value(this.get('value'));
2778 this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2781 is_syntax_valid: function() {
2782 if (!this.get("effective_readonly") && this.datewidget) {
2783 return this.datewidget.is_valid_();
2787 is_false: function() {
2788 return this.get('value') === '' || this._super();
2791 var input = this.datewidget && this.datewidget.$input[0];
2792 return input ? input.focus() : false;
2794 set_dimensions: function (height, width) {
2795 this._super(height, width);
2796 if (!this.get("effective_readonly")) {
2797 this.datewidget.$input.css('height', height);
2802 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2803 template: "FieldDate",
2804 build_widget: function() {
2805 return new instance.web.DateWidget(this);
2809 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2810 template: 'FieldText',
2812 'keyup': function (e) {
2813 if (e.which === $.ui.keyCode.ENTER) {
2814 e.stopPropagation();
2817 'keypress': function (e) {
2818 if (e.which === $.ui.keyCode.ENTER) {
2819 e.stopPropagation();
2822 'change textarea': 'store_dom_value',
2824 initialize_content: function() {
2826 if (! this.get("effective_readonly")) {
2827 this.$textarea = this.$el.find('textarea');
2828 this.auto_sized = false;
2829 this.default_height = this.$textarea.css('height');
2830 if (this.get("effective_readonly")) {
2831 this.$textarea.attr('disabled', 'disabled');
2833 this.setupFocus(this.$textarea);
2835 this.$textarea = undefined;
2838 commit_value: function () {
2839 if (! this.get("effective_readonly") && this.$textarea) {
2840 this.store_dom_value();
2842 return this._super();
2844 store_dom_value: function () {
2845 this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2847 render_value: function() {
2848 if (! this.get("effective_readonly")) {
2849 var show_value = instance.web.format_value(this.get('value'), this, '');
2850 if (show_value === '') {
2851 this.$textarea.css('height', parseInt(this.default_height, 10)+"px");
2853 this.$textarea.val(show_value);
2854 if (! this.auto_sized) {
2855 this.auto_sized = true;
2856 this.$textarea.autosize();
2858 this.$textarea.trigger("autosize");
2861 var txt = this.get("value") || '';
2862 this.$(".oe_form_text_content").text(txt);
2865 is_syntax_valid: function() {
2866 if (!this.get("effective_readonly") && this.$textarea) {
2868 instance.web.parse_value(this.$textarea.val(), this, '');
2876 is_false: function() {
2877 return this.get('value') === '' || this._super();
2879 focus: function($el) {
2880 var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2881 return input ? input.focus() : false;
2883 set_dimensions: function (height, width) {
2884 this._super(height, width);
2885 if (!this.get("effective_readonly") && this.$textarea) {
2886 this.$textarea.css({
2895 * FieldTextHtml Widget
2896 * Intended for FieldText widgets meant to display HTML content. This
2897 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2898 * To find more information about CLEditor configutation: go to
2899 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2901 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2902 template: 'FieldTextHtml',
2904 this._super.apply(this, arguments);
2906 initialize_content: function() {
2908 if (! this.get("effective_readonly")) {
2909 self._updating_editor = false;
2910 this.$textarea = this.$el.find('textarea');
2911 var width = ((this.node.attrs || {}).editor_width || 'calc(100% - 4px)');
2912 var height = ((this.node.attrs || {}).editor_height || 250);
2913 this.$textarea.cleditor({
2914 width: width, // width not including margins, borders or padding
2915 height: height, // height not including margins, borders or padding
2916 controls: // controls to add to the toolbar
2917 "bold italic underline strikethrough " +
2918 "| removeformat | bullets numbering | outdent " +
2919 "indent | link unlink | source",
2920 bodyStyle: // style to assign to document body contained within the editor
2921 "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2923 this.$cleditor = this.$textarea.cleditor()[0];
2924 this.$cleditor.change(function() {
2925 if (! self._updating_editor) {
2926 self.$cleditor.updateTextArea();
2927 self.internal_set_value(self.$textarea.val());
2930 if (this.field.translate) {
2931 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"/>')
2932 .click(this.on_translate);
2933 this.$cleditor.$toolbar.append($img);
2937 render_value: function() {
2938 if (! this.get("effective_readonly")) {
2939 this.$textarea.val(this.get('value') || '');
2940 this._updating_editor = true;
2941 this.$cleditor.updateFrame();
2942 this._updating_editor = false;
2944 this.$el.html(this.get('value'));
2949 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2950 template: 'FieldBoolean',
2953 this.$checkbox = $("input", this.$el);
2954 this.setupFocus(this.$checkbox);
2955 this.$el.click(_.bind(function() {
2956 this.internal_set_value(this.$checkbox.is(':checked'));
2958 var check_readonly = function() {
2959 self.$checkbox.prop('disabled', self.get("effective_readonly"));
2960 self.click_disabled_boolean();
2962 this.on("change:effective_readonly", this, check_readonly);
2963 check_readonly.call(this);
2964 this._super.apply(this, arguments);
2966 render_value: function() {
2967 this.$checkbox[0].checked = this.get('value');
2970 var input = this.$checkbox && this.$checkbox[0];
2971 return input ? input.focus() : false;
2973 click_disabled_boolean: function(){
2974 var $disabled = this.$el.find('input[type=checkbox]:disabled');
2975 $disabled.each(function (){
2976 $(this).next('div').remove();
2977 $(this).closest("span").append($('<div class="boolean"></div>'));
2983 The progressbar field expect a float from 0 to 100.
2985 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2986 template: 'FieldProgressBar',
2987 render_value: function() {
2988 this.$el.progressbar({
2989 value: this.get('value') || 0,
2990 disabled: this.get("effective_readonly")
2992 var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
2993 this.$('span').html(formatted_value + '%');
2998 The PercentPie field expect a float from 0 to 100.
3000 instance.web.form.FieldPercentPie = instance.web.form.AbstractField.extend({
3001 template: 'FieldPercentPie',
3003 render_value: function() {
3004 var value = this.get('value'),
3005 formatted_value = Math.round(value || 0) + '%',
3006 svg = this.$('svg')[0];
3009 nv.addGraph(function() {
3010 var width = 42, height = 42;
3011 var chart = nv.models.pieChart()
3014 .margin({top: 0, right: 0, bottom: 0, left: 0})
3019 .color(['#7C7BAD','#DDD'])
3023 .datum([{'x': 'value', 'y': value}, {'x': 'complement', 'y': 100 - value}])
3026 .attr('style', 'width: ' + width + 'px; height:' + height + 'px;');
3030 .attr({x: width/2, y: height/2 + 3, 'text-anchor': 'middle'})
3031 .style({"font-size": "10px", "font-weight": "bold"})
3032 .text(formatted_value);
3041 The FieldBarChart expectsa list of values (indeed)
3043 instance.web.form.FieldBarChart = instance.web.form.AbstractField.extend({
3044 template: 'FieldBarChart',
3046 render_value: function() {
3047 var value = JSON.parse(this.get('value'));
3048 var svg = this.$('svg')[0];
3050 nv.addGraph(function() {
3051 var width = 34, height = 34;
3052 var chart = nv.models.discreteBarChart()
3053 .x(function (d) { return d.tooltip })
3054 .y(function (d) { return d.value })
3057 .margin({top: 0, right: 0, bottom: 0, left: 0})
3060 .transitionDuration(350)
3065 .datum([{key: 'values', values: value}])
3068 .attr('style', 'width: ' + (width + 4) + 'px; height: ' + (height + 8) + 'px;');
3070 nv.utils.windowResize(chart.update);
3079 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3080 template: 'FieldSelection',
3082 'change select': 'store_dom_value',
3084 init: function(field_manager, node) {
3086 this._super(field_manager, node);
3087 this.set("value", false);
3088 this.set("values", []);
3089 this.records_orderer = new instance.web.DropMisordered();
3090 this.field_manager.on("view_content_has_changed", this, function() {
3091 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
3092 if (! _.isEqual(domain, this.get("domain"))) {
3093 this.set("domain", domain);
3097 initialize_field: function() {
3098 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3099 this.on("change:domain", this, this.query_values);
3100 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
3101 this.on("change:values", this, this.render_value);
3103 query_values: function() {
3106 if (this.field.type === "many2one") {
3107 var model = new openerp.Model(openerp.session, this.field.relation);
3108 def = model.call("name_search", ['', this.get("domain")], {"context": this.build_context()});
3110 var values = _.reject(this.field.selection, function (v) { return v[0] === false && v[1] === ''; });
3111 def = $.when(values);
3113 this.records_orderer.add(def).then(function(values) {
3114 if (! _.isEqual(values, self.get("values"))) {
3115 self.set("values", values);
3119 initialize_content: function() {
3120 // Flag indicating whether we're in an event chain containing a change
3121 // event on the select, in order to know what to do on keyup[RETURN]:
3122 // * If the user presses [RETURN] as part of changing the value of a
3123 // selection, we should just let the value change and not let the
3124 // event broadcast further (e.g. to validating the current state of
3125 // the form in editable list view, which would lead to saving the
3126 // current row or switching to the next one)
3127 // * If the user presses [RETURN] with a select closed (side-effect:
3128 // also if the user opened the select and pressed [RETURN] without
3129 // changing the selected value), takes the action as validating the
3131 var ischanging = false;
3132 var $select = this.$el.find('select')
3133 .change(function () { ischanging = true; })
3134 .click(function () { ischanging = false; })
3135 .keyup(function (e) {
3136 if (e.which !== 13 || !ischanging) { return; }
3137 e.stopPropagation();
3140 this.setupFocus($select);
3142 commit_value: function () {
3143 this.store_dom_value();
3144 return this._super();
3146 store_dom_value: function () {
3147 if (!this.get('effective_readonly') && this.$('select').length) {
3148 var val = JSON.parse(this.$('select').val());
3149 this.internal_set_value(val);
3152 set_value: function(value_) {
3153 value_ = value_ === null ? false : value_;
3154 value_ = value_ instanceof Array ? value_[0] : value_;
3155 this._super(value_);
3157 render_value: function() {
3158 var values = this.get("values");
3159 values = [[false, this.node.attrs.placeholder || '']].concat(values);
3160 var found = _.find(values, function(el) { return el[0] === this.get("value"); }, this);
3162 found = [this.get("value"), _t('Unknown')];
3163 values = [found].concat(values);
3165 if (! this.get("effective_readonly")) {
3166 this.$().html(QWeb.render("FieldSelectionSelect", {widget: this, values: values}));
3167 this.$("select").val(JSON.stringify(found[0]));
3169 this.$el.text(found[1]);
3173 var input = this.$('select:first')[0];
3174 return input ? input.focus() : false;
3176 set_dimensions: function (height, width) {
3177 this._super(height, width);
3178 this.$('select').css({
3185 instance.web.form.FieldRadio = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3186 template: 'FieldRadio',
3188 'click input': 'click_change_value'
3190 init: function(field_manager, node) {
3191 /* Radio button widget: Attributes options:
3192 * - "horizontal" to display in column
3193 * - "no_radiolabel" don't display text values
3195 this._super(field_manager, node);
3196 this.selection = _.clone(this.field.selection) || [];
3197 this.domain = false;
3199 initialize_content: function () {
3200 this.uniqueId = _.uniqueId("radio");
3201 this.on("change:effective_readonly", this, this.render_value);
3202 this.field_manager.on("view_content_has_changed", this, this.get_selection);
3203 this.get_selection();
3205 click_change_value: function (event) {
3206 var val = $(event.target).val();
3207 val = this.field.type == "selection" ? val : +val;
3208 if (val == this.get_value()) {
3209 this.set_value(false);
3211 this.set_value(val);
3214 /** Get the selection and render it
3215 * selection: [[identifier, value_to_display], ...]
3216 * For selection fields: this is directly given by this.field.selection
3217 * For many2one fields: perform a search on the relation of the many2one field
3219 get_selection: function() {
3222 var def = $.Deferred();
3223 if (self.field.type == "many2one") {
3224 var domain = instance.web.pyeval.eval('domain', this.build_domain()) || [];
3225 if (! _.isEqual(self.domain, domain)) {
3226 self.domain = domain;
3227 var ds = new instance.web.DataSetStatic(self, self.field.relation, self.build_context());
3228 ds.call('search', [self.domain])
3229 .then(function (records) {
3230 ds.name_get(records).then(function (records) {
3231 selection = records;
3236 selection = self.selection;
3240 else if (self.field.type == "selection") {
3241 selection = self.field.selection || [];
3244 return def.then(function () {
3245 if (! _.isEqual(selection, self.selection)) {
3246 self.selection = _.clone(selection);
3247 self.renderElement();
3248 self.render_value();
3252 set_value: function (value_) {
3254 if (this.field.type == "selection") {
3255 value_ = _.find(this.field.selection, function (sel) { return sel[0] == value_;});
3257 else if (!this.selection.length) {
3258 this.selection = [value_];
3261 this._super(value_);
3263 get_value: function () {
3264 var value = this.get('value');
3265 return value instanceof Array ? value[0] : value;
3267 render_value: function () {
3269 this.$el.toggleClass("oe_readonly", this.get('effective_readonly'));
3270 this.$("input:checked").prop("checked", false);
3271 if (this.get_value()) {
3272 this.$("input").filter(function () {return this.value == self.get_value();}).prop("checked", true);
3273 this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : "");
3278 // jquery autocomplete tweak to allow html and classnames
3280 var proto = $.ui.autocomplete.prototype,
3281 initSource = proto._initSource;
3283 function filter( array, term ) {
3284 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
3285 return $.grep( array, function(value_) {
3286 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
3291 _initSource: function() {
3292 if ( this.options.html && $.isArray(this.options.source) ) {
3293 this.source = function( request, response ) {
3294 response( filter( this.options.source, request.term ) );
3297 initSource.call( this );
3301 _renderItem: function( ul, item) {
3302 return $( "<li></li>" )
3303 .data( "item.autocomplete", item )
3304 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
3306 .addClass(item.classname);
3312 A mixin containing some useful methods to handle completion inputs.
3314 The widget containing this option can have these arguments in its widget options:
3315 - no_quick_create: if true, it will disable the quick create
3317 instance.web.form.CompletionFieldMixin = {
3320 this.orderer = new instance.web.DropMisordered();
3323 * Call this method to search using a string.
3325 get_search_result: function(search_val) {
3328 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
3329 this.last_query = search_val;
3330 var exclusion_domain = [], ids_blacklist = this.get_search_blacklist();
3331 if (!_(ids_blacklist).isEmpty()) {
3332 exclusion_domain.push(['id', 'not in', ids_blacklist]);
3335 return this.orderer.add(dataset.name_search(
3336 search_val, new instance.web.CompoundDomain(self.build_domain(), exclusion_domain),
3337 'ilike', this.limit + 1, self.build_context())).then(function(data) {
3338 self.last_search = data;
3339 // possible selections for the m2o
3340 var values = _.map(data, function(x) {
3341 x[1] = x[1].split("\n")[0];
3343 label: _.str.escapeHTML(x[1]),
3350 // search more... if more results that max
3351 if (values.length > self.limit) {
3352 values = values.slice(0, self.limit);
3354 label: _t("Search More..."),
3355 action: function() {
3356 dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
3357 self._search_create_popup("search", data);
3360 classname: 'oe_m2o_dropdown_option'
3364 var raw_result = _(data.result).map(function(x) {return x[1];});
3365 if (search_val.length > 0 && !_.include(raw_result, search_val) &&
3366 ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3368 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
3369 $('<span />').text(search_val).html()),
3370 action: function() {
3371 self._quick_create(search_val);
3373 classname: 'oe_m2o_dropdown_option'
3377 if (!(self.options && (self.options.no_create || self.options.no_create_edit))){
3379 label: _t("Create and Edit..."),
3380 action: function() {
3381 self._search_create_popup("form", undefined, self._create_context(search_val));
3383 classname: 'oe_m2o_dropdown_option'
3386 else if (values.length == 0)
3388 label: _t("No results to show..."),
3389 action: function() {},
3390 classname: 'oe_m2o_dropdown_option'
3396 get_search_blacklist: function() {
3399 _quick_create: function(name) {
3401 var slow_create = function () {
3402 self._search_create_popup("form", undefined, self._create_context(name));
3404 if (self.options.quick_create === undefined || self.options.quick_create) {
3405 new instance.web.DataSet(this, this.field.relation, self.build_context())
3406 .name_create(name).done(function(data) {
3407 if (!self.get('effective_readonly'))
3408 self.add_id(data[0]);
3409 }).fail(function(error, event) {
3410 event.preventDefault();
3416 // all search/create popup handling
3417 _search_create_popup: function(view, ids, context) {
3419 var pop = new instance.web.form.SelectCreatePopup(this);
3421 self.field.relation,
3423 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3424 initial_ids: ids ? _.map(ids, function(x) {return x[0];}) : undefined,
3426 disable_multiple_selection: true
3428 self.build_domain(),
3429 new instance.web.CompoundContext(self.build_context(), context || {})
3431 pop.on("elements_selected", self, function(element_ids) {
3432 self.add_id(element_ids[0]);
3439 add_id: function(id) {},
3440 _create_context: function(name) {
3442 var field = (this.options || {}).create_name_field;
3443 if (field === undefined)
3445 if (field !== false && name && (this.options || {}).quick_create !== false)
3446 tmp["default_" + field] = name;
3451 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3452 template: "M2ODialog",
3453 init: function(parent) {
3454 this.name = parent.string;
3455 this._super(parent, {
3456 title: _.str.sprintf(_t("Create a %s"), parent.string),
3462 var text = _.str.sprintf(_t("You are creating a new %s, are you sure it does not exist yet?"), self.name);
3463 this.$("p").text( text );
3464 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3465 this.$("input").val(this.getParent().last_query);
3466 this.$buttons.find(".oe_form_m2o_qc_button").click(function(e){
3467 if (self.$("input").val() != ''){
3468 self.getParent()._quick_create(self.$("input").val());
3472 self.$("input").focus();
3475 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3476 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3479 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3485 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3486 template: "FieldMany2One",
3488 'keydown input': function (e) {
3490 case $.ui.keyCode.UP:
3491 case $.ui.keyCode.DOWN:
3492 e.stopPropagation();
3496 init: function(field_manager, node) {
3497 this._super(field_manager, node);
3498 instance.web.form.CompletionFieldMixin.init.call(this);
3499 this.set({'value': false});
3500 this.display_value = {};
3501 this.display_value_backup = {};
3502 this.last_search = [];
3503 this.floating = false;
3504 this.current_display = null;
3505 this.is_started = false;
3506 this.ignore_focusout = false;
3508 reinit_value: function(val) {
3509 this.internal_set_value(val);
3510 this.floating = false;
3511 if (this.is_started)
3512 this.render_value();
3514 initialize_field: function() {
3515 this.is_started = true;
3516 instance.web.bus.on('click', this, function() {
3517 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3518 this.$input.autocomplete("close");
3521 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3523 initialize_content: function() {
3524 if (!this.get("effective_readonly"))
3525 this.render_editable();
3527 destroy_content: function () {
3528 if (this.$drop_down) {
3529 this.$drop_down.off('click');
3530 delete this.$drop_down;
3533 this.$input.closest(".modal .modal-content").off('scroll');
3534 this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3535 'focus focusout change keydown');
3538 if (this.$follow_button) {
3539 this.$follow_button.off('blur focus click');
3540 delete this.$follow_button;
3543 destroy: function () {
3544 this.destroy_content();
3545 return this._super();
3547 init_error_displayer: function() {
3550 hide_error_displayer: function() {
3553 show_error_displayer: function() {
3554 new instance.web.form.M2ODialog(this).open();
3556 render_editable: function() {
3558 this.$input = this.$el.find("input");
3560 this.init_error_displayer();
3562 self.$input.on('focus', function() {
3563 self.hide_error_displayer();
3566 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3567 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3569 this.$follow_button.click(function(ev) {
3570 ev.preventDefault();
3571 if (!self.get('value')) {
3575 var pop = new instance.web.form.FormOpenPopup(self);
3576 var context = self.build_context().eval();
3577 var model_obj = new instance.web.Model(self.field.relation);
3578 model_obj.call('get_formview_id', [self.get("value"), context]).then(function(view_id){
3580 self.field.relation,
3582 self.build_context(),
3584 title: _t("Open: ") + self.string,
3588 pop.on('write_completed', self, function(){
3589 self.display_value = {};
3590 self.display_value_backup = {};
3591 self.render_value();
3593 self.trigger('changed_value');
3598 // some behavior for input
3599 var input_changed = function() {
3600 if (self.current_display !== self.$input.val()) {
3601 self.current_display = self.$input.val();
3602 if (self.$input.val() === "") {
3603 self.internal_set_value(false);
3604 self.floating = false;
3606 self.floating = true;
3610 this.$input.keydown(input_changed);
3611 this.$input.change(input_changed);
3612 this.$drop_down.click(function() {
3613 self.$input.focus();
3614 if (self.$input.autocomplete("widget").is(":visible")) {
3615 self.$input.autocomplete("close");
3617 if (self.get("value") && ! self.floating) {
3618 self.$input.autocomplete("search", "");
3620 self.$input.autocomplete("search");
3625 // Autocomplete close on dialog content scroll
3626 var close_autocomplete = _.debounce(function() {
3627 if (self.$input.autocomplete("widget").is(":visible")) {
3628 self.$input.autocomplete("close");
3631 this.$input.closest(".modal .modal-content").on('scroll', this, close_autocomplete);
3633 self.ed_def = $.Deferred();
3634 self.uned_def = $.Deferred();
3636 var ed_duration = 15000;
3637 var anyoneLoosesFocus = function (e) {
3638 if (self.ignore_focusout) { return; }
3640 if (self.floating) {
3641 if (self.last_search.length > 0) {
3642 if (self.last_search[0][0] != self.get("value")) {
3643 self.display_value = {};
3644 self.display_value_backup = {};
3645 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3646 self.reinit_value(self.last_search[0][0]);
3649 self.render_value();
3653 self.reinit_value(false);
3655 self.floating = false;
3657 if (used && self.get("value") === false && ! self.no_ed && ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3658 self.ed_def.reject();
3659 self.uned_def.reject();
3660 self.ed_def = $.Deferred();
3661 self.ed_def.done(function() {
3662 self.show_error_displayer();
3663 ignore_blur = false;
3664 self.trigger('focused');
3667 setTimeout(function() {
3668 self.ed_def.resolve();
3669 self.uned_def.reject();
3670 self.uned_def = $.Deferred();
3671 self.uned_def.done(function() {
3672 self.hide_error_displayer();
3674 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3678 self.ed_def.reject();
3681 var ignore_blur = false;
3683 focusout: anyoneLoosesFocus,
3684 focus: function () { self.trigger('focused'); },
3685 autocompleteopen: function () { ignore_blur = true; },
3686 autocompleteclose: function () { setTimeout(function() {ignore_blur = false;},0); },
3688 // autocomplete open
3689 if (ignore_blur) { $(this).focus(); return; }
3690 if (_(self.getChildren()).any(function (child) {
3691 return child instanceof instance.web.form.AbstractFormPopup;
3693 self.trigger('blurred');
3697 var isSelecting = false;
3699 this.$input.autocomplete({
3700 source: function(req, resp) {
3701 self.get_search_result(req.term).done(function(result) {
3705 select: function(event, ui) {
3709 self.display_value = {};
3710 self.display_value_backup = {};
3711 self.display_value["" + item.id] = item.name;
3712 self.reinit_value(item.id);
3713 } else if (item.action) {
3715 // Cancel widget blurring, to avoid form blur event
3716 self.trigger('focused');
3720 focus: function(e, ui) {
3724 // disabled to solve a bug, but may cause others
3725 //close: anyoneLoosesFocus,
3729 // set position for list of suggestions box
3730 this.$input.autocomplete( "option", "position", { my : "left top", at: "left bottom" } );
3731 this.$input.autocomplete("widget").openerpClass();
3732 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3733 this.$input.keyup(function(e) {
3734 if (e.which === 13) { // ENTER
3736 e.stopPropagation();
3738 isSelecting = false;
3740 this.setupFocus(this.$follow_button);
3742 render_value: function(no_recurse) {
3744 if (! this.get("value")) {
3745 this.display_string("");
3748 var display = this.display_value["" + this.get("value")];
3750 this.display_string(display);
3754 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3755 this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3757 self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
3760 self.display_value["" + self.get("value")] = data[0][1];
3761 self.render_value(true);
3762 }).fail( function (data, event) {
3763 // avoid displaying crash errors as many2One should be name_get compliant
3764 event.preventDefault();
3765 self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")];
3766 self.render_value(true);
3770 display_string: function(str) {
3772 if (!this.get("effective_readonly")) {
3773 this.$input.val(str.split("\n")[0]);
3774 this.current_display = this.$input.val();
3775 if (this.is_false()) {
3776 this.$('.oe_m2o_cm_button').css({'display':'none'});
3778 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3781 var lines = _.escape(str).split("\n");
3785 follow = _.rest(lines).join("<br />");
3788 var $link = this.$el.find('.oe_form_uri')
3791 if (! this.options.no_open)
3792 $link.click(function () {
3793 var context = self.build_context().eval();
3794 var model_obj = new instance.web.Model(self.field.relation);
3795 model_obj.call('get_formview_action', [self.get("value"), context]).then(function(action){
3796 self.do_action(action);
3800 $(".oe_form_m2o_follow", this.$el).html(follow);
3803 set_value: function(value_) {
3805 if (value_ instanceof Array) {
3806 this.display_value = {};
3807 this.display_value_backup = {};
3808 if (! this.options.always_reload) {
3809 this.display_value["" + value_[0]] = value_[1];
3812 this.display_value_backup["" + value_[0]] = value_[1];
3816 value_ = value_ || false;
3817 this.reinit_value(value_);
3819 get_displayed: function() {
3820 return this.display_value["" + this.get("value")];
3822 add_id: function(id) {
3823 this.display_value = {};
3824 this.display_value_backup = {};
3825 this.reinit_value(id);
3827 is_false: function() {
3828 return ! this.get("value");
3830 focus: function () {
3831 var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3832 return input ? input.focus() : false;
3834 _quick_create: function() {
3836 this.ed_def.reject();
3837 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3839 _search_create_popup: function() {
3841 this.ed_def.reject();
3842 this.ignore_focusout = true;
3843 this.reinit_value(false);
3844 var res = instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3845 this.ignore_focusout = false;
3849 set_dimensions: function (height, width) {
3850 this._super(height, width);
3851 if (!this.get("effective_readonly") && this.$input)
3852 this.$input.css('height', height);
3856 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3857 template: 'Many2OneButton',
3858 init: function(field_manager, node) {
3859 this._super.apply(this, arguments);
3862 this._super.apply(this, arguments);
3865 set_button: function() {
3868 this.$button.remove();
3871 this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3872 this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3873 this.$button.addClass('oe_link').css({'padding':'4px'});
3874 this.$el.append(this.$button);
3875 this.$button.on('click', self.on_click);
3877 on_click: function(ev) {
3879 this.popup = new instance.web.form.FormOpenPopup(this);
3880 this.popup.show_element(
3881 this.field.relation,
3883 this.build_context(),
3884 {title: this.string}
3886 this.popup.on('create_completed', self, function(r) {
3890 set_value: function(value_) {
3892 if (value_ instanceof Array) {
3895 value_ = value_ || false;
3896 this.set('value', value_);
3902 * Abstract-ish ListView.List subclass adding an "Add an item" row to replace
3903 * the big ugly button in the header.
3905 * Requires the implementation of a ``is_readonly`` method (usually a proxy to
3906 * the corresponding field's readonly or effective_readonly property) to
3907 * decide whether the special row should or should not be inserted.
3909 * Optionally an ``_add_row_class`` attribute can be set for the class(es) to
3910 * set on the insertion row.
3912 instance.web.form.AddAnItemList = instance.web.ListView.List.extend({
3913 pad_table_to: function (count) {
3914 if (!this.view.is_action_enabled('create') || this.is_readonly()) {
3919 this._super(count > 0 ? count - 1 : 0);
3922 var columns = _(this.columns).filter(function (column) {
3923 return column.invisible !== '1';
3925 if (this.options.selectable) { columns++; }
3926 if (this.options.deletable) { columns++; }
3928 var $cell = $('<td>', {
3930 'class': this._add_row_class || ''
3932 $('<a>', {href: '#'}).text(_t("Add an item"))
3933 .mousedown(function () {
3934 // FIXME: needs to be an official API somehow
3935 if (self.view.editor.is_editing()) {
3936 self.view.__ignore_blur = true;
3939 .click(function (e) {
3941 e.stopPropagation();
3942 // FIXME: there should also be an API for that one
3943 if (self.view.editor.form.__blur_timeout) {
3944 clearTimeout(self.view.editor.form.__blur_timeout);
3945 self.view.editor.form.__blur_timeout = false;
3947 self.view.ensure_saved().done(function () {
3948 self.view.do_add_record();
3952 var $padding = this.$current.find('tr:not([data-id]):first');
3953 var $newrow = $('<tr>').append($cell);
3954 if ($padding.length) {
3955 $padding.before($newrow);
3957 this.$current.append($newrow)
3963 # Values: (0, 0, { fields }) create
3964 # (1, ID, { fields }) update
3965 # (2, ID) remove (delete)
3966 # (3, ID) unlink one (target id or target of relation)
3968 # (5) unlink all (only valid for one2many)
3973 'create': function (values) {
3974 return [commands.CREATE, false, values];
3976 // (1, id, {values})
3978 'update': function (id, values) {
3979 return [commands.UPDATE, id, values];
3983 'delete': function (id) {
3984 return [commands.DELETE, id, false];
3986 // (3, id[, _]) removes relation, but not linked record itself
3988 'forget': function (id) {
3989 return [commands.FORGET, id, false];
3993 'link_to': function (id) {
3994 return [commands.LINK_TO, id, false];
3998 'delete_all': function () {
3999 return [5, false, false];
4001 // (6, _, ids) replaces all linked records with provided ids
4003 'replace_with': function (ids) {
4004 return [6, false, ids];
4007 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
4008 multi_selection: false,
4009 disable_utility_classes: true,
4010 init: function(field_manager, node) {
4011 this._super(field_manager, node);
4012 lazy_build_o2m_kanban_view();
4013 this.is_loaded = $.Deferred();
4014 this.initial_is_loaded = this.is_loaded;
4015 this.form_last_update = $.Deferred();
4016 this.init_form_last_update = this.form_last_update;
4017 this.is_started = false;
4018 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
4019 this.dataset.o2m = this;
4020 this.dataset.parent_view = this.view;
4021 this.dataset.child_name = this.name;
4023 this.dataset.on('dataset_changed', this, function() {
4024 self.trigger_on_change();
4029 this._super.apply(this, arguments);
4030 this.$el.addClass('oe_form_field oe_form_field_one2many');
4035 this.is_loaded.done(function() {
4036 self.on("change:effective_readonly", self, function() {
4037 self.is_loaded = self.is_loaded.then(function() {
4038 self.viewmanager.destroy();
4039 return $.when(self.load_views()).done(function() {
4040 self.reload_current_view();
4045 this.is_started = true;
4046 this.reload_current_view();
4048 trigger_on_change: function() {
4049 this.trigger('changed_value');
4051 load_views: function() {
4054 var modes = this.node.attrs.mode;
4055 modes = !!modes ? modes.split(",") : ["tree"];
4057 _.each(modes, function(mode) {
4058 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
4059 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
4063 view_type: mode == "tree" ? "list" : mode,
4066 if (self.field.views && self.field.views[mode]) {
4067 view.embedded_view = self.field.views[mode];
4069 if(view.view_type === "list") {
4070 _.extend(view.options, {
4072 selectable: self.multi_selection,
4074 import_enabled: false,
4077 if (self.get("effective_readonly")) {
4078 _.extend(view.options, {
4083 } else if (view.view_type === "form") {
4084 if (self.get("effective_readonly")) {
4085 view.view_type = 'form';
4087 _.extend(view.options, {
4088 not_interactible_on_create: true,
4090 } else if (view.view_type === "kanban") {
4091 _.extend(view.options, {
4092 confirm_on_delete: false,
4094 if (self.get("effective_readonly")) {
4095 _.extend(view.options, {
4096 action_buttons: false,
4097 quick_creatable: false,
4099 read_only_mode: true,
4107 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
4108 this.viewmanager.o2m = self;
4109 var once = $.Deferred().done(function() {
4110 self.init_form_last_update.resolve();
4112 var def = $.Deferred().done(function() {
4113 self.initial_is_loaded.resolve();
4115 this.viewmanager.on("controller_inited", self, function(view_type, controller) {
4116 controller.o2m = self;
4117 if (view_type == "list") {
4118 if (self.get("effective_readonly")) {
4119 controller.on('edit:before', self, function (e) {
4122 _(controller.columns).find(function (column) {
4123 if (!(column instanceof instance.web.list.Handle)) {
4126 column.modifiers.invisible = true;
4130 } else if (view_type === "form") {
4131 if (self.get("effective_readonly")) {
4132 $(".oe_form_buttons", controller.$el).children().remove();
4134 controller.on("load_record", self, function(){
4137 controller.on('pager_action_executed',self,self.save_any_view);
4138 } else if (view_type == "graph") {
4139 self.reload_current_view();
4143 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
4144 $.when(self.save_any_view()).done(function() {
4145 if (n_mode === "list") {
4146 $.async_when().done(function() {
4147 self.reload_current_view();
4152 $.async_when().done(function () {
4153 self.viewmanager.appendTo(self.$el);
4157 reload_current_view: function() {
4159 self.is_loaded = self.is_loaded.then(function() {
4160 var view = self.get_active_view();
4161 if (view.type === "list") {
4162 return view.controller.reload_content();
4163 } else if (view.type === "form") {
4164 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
4165 self.dataset.index = 0;
4167 var act = function() {
4168 return view.controller.do_show();
4170 self.form_last_update = self.form_last_update.then(act, act);
4171 return self.form_last_update;
4172 } else if (view.controller.do_search) {
4173 return view.controller.do_search(self.build_domain(), self.dataset.get_context(), []);
4176 return self.is_loaded;
4178 get_active_view: function () {
4180 * Returns the current active view if any.
4182 if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
4183 this.viewmanager.views[this.viewmanager.active_view] &&
4184 this.viewmanager.views[this.viewmanager.active_view].controller) {
4186 type: this.viewmanager.active_view,
4187 controller: this.viewmanager.views[this.viewmanager.active_view].controller
4191 set_value: function(value_) {
4192 value_ = value_ || [];
4194 var view = this.get_active_view();
4195 this.dataset.reset_ids([]);
4197 if(value_.length >= 1 && value_[0] instanceof Array) {
4199 _.each(value_, function(command) {
4200 var obj = {values: command[2]};
4201 switch (command[0]) {
4202 case commands.CREATE:
4203 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4205 self.dataset.to_create.push(obj);
4206 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4209 case commands.UPDATE:
4210 obj['id'] = command[1];
4211 self.dataset.to_write.push(obj);
4212 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4215 case commands.DELETE:
4216 self.dataset.to_delete.push({id: command[1]});
4218 case commands.LINK_TO:
4219 ids.push(command[1]);
4221 case commands.DELETE_ALL:
4222 self.dataset.delete_all = true;
4227 this.dataset.set_ids(ids);
4228 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
4230 this.dataset.delete_all = true;
4231 _.each(value_, function(command) {
4232 var obj = {values: command};
4233 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4235 self.dataset.to_create.push(obj);
4236 self.dataset.cache.push(_.clone(obj));
4240 this.dataset.set_ids(ids);
4242 this._super(value_);
4243 this.dataset.reset_ids(value_);
4245 if (this.dataset.index === null && this.dataset.ids.length > 0) {
4246 this.dataset.index = 0;
4248 this.trigger_on_change();
4249 if (this.is_started) {
4250 return self.reload_current_view();
4255 get_value: function() {
4259 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
4260 val = val.concat(_.map(this.dataset.ids, function(id) {
4261 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
4263 return commands.create(alter_order.values);
4265 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
4267 return commands.update(alter_order.id, alter_order.values);
4269 return commands.link_to(id);
4271 return val.concat(_.map(
4272 this.dataset.to_delete, function(x) {
4273 return commands['delete'](x.id);}));
4275 commit_value: function() {
4276 return this.save_any_view();
4278 save_any_view: function() {
4279 var view = this.get_active_view();
4281 if (this.viewmanager.active_view === "form") {
4282 if (view.controller.is_initialized.state() !== 'resolved') {
4283 return $.when(false);
4285 return $.when(view.controller.save());
4286 } else if (this.viewmanager.active_view === "list") {
4287 return $.when(view.controller.ensure_saved());
4290 return $.when(false);
4292 is_syntax_valid: function() {
4293 var view = this.get_active_view();
4297 switch (this.viewmanager.active_view) {
4299 return _(view.controller.fields).chain()
4304 return view.controller.is_valid();
4310 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
4311 template: 'One2Many.viewmanager',
4312 init: function(parent, dataset, views, flags) {
4313 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
4314 this.registry = this.registry.extend({
4315 list: 'instance.web.form.One2ManyListView',
4316 form: 'instance.web.form.One2ManyFormView',
4317 kanban: 'instance.web.form.One2ManyKanbanView',
4319 this.__ignore_blur = false;
4321 switch_mode: function(mode, unused) {
4322 if (mode !== 'form') {
4323 return this._super(mode, unused);
4326 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
4327 var pop = new instance.web.form.FormOpenPopup(this);
4328 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4329 title: _t("Open: ") + self.o2m.string,
4330 create_function: function(data, options) {
4331 return self.o2m.dataset.create(data, options).done(function(r) {
4332 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4333 self.o2m.dataset.trigger("dataset_changed", r);
4336 write_function: function(id, data, options) {
4337 return self.o2m.dataset.write(id, data, {}).done(function() {
4338 self.o2m.reload_current_view();
4341 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4342 parent_view: self.o2m.view,
4343 child_name: self.o2m.name,
4344 read_function: function() {
4345 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4347 form_view_options: {'not_interactible_on_create':true},
4348 readonly: self.o2m.get("effective_readonly")
4350 pop.on("elements_selected", self, function() {
4351 self.o2m.reload_current_view();
4356 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
4357 get_context: function() {
4358 this.context = this.o2m.build_context();
4359 return this.context;
4363 instance.web.form.One2ManyListView = instance.web.ListView.extend({
4364 _template: 'One2Many.listview',
4365 init: function (parent, dataset, view_id, options) {
4366 this._super(parent, dataset, view_id, _.extend(options || {}, {
4367 GroupsType: instance.web.form.One2ManyGroups,
4368 ListType: instance.web.form.One2ManyList
4370 this.on('edit:after', this, this.proxy('_after_edit'));
4371 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
4374 .bind('add', this.proxy("changed_records"))
4375 .bind('edit', this.proxy("changed_records"))
4376 .bind('remove', this.proxy("changed_records"));
4378 start: function () {
4379 var ret = this._super();
4381 .off('mousedown.handleButtons')
4382 .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
4385 changed_records: function () {
4386 this.o2m.trigger_on_change();
4388 is_valid: function () {
4389 var editor = this.editor;
4390 var form = editor.form;
4391 // If no edition is pending, the listview can not be invalid (?)
4392 if (!editor.record) {
4395 // If the form has not been modified, the view can only be valid
4396 // NB: is_dirty will also be set on defaults/onchanges/whatever?
4397 // oe_form_dirty seems to only be set on actual user actions
4398 if (!form.$el.is('.oe_form_dirty')) {
4401 this.o2m._dirty_flag = true;
4403 // Otherwise validate internal form
4404 return _(form.fields).chain()
4405 .invoke(function () {
4406 this._check_css_flags();
4407 return this.is_valid();
4412 do_add_record: function () {
4413 if (this.editable()) {
4414 this._super.apply(this, arguments);
4417 var pop = new instance.web.form.SelectCreatePopup(this);
4419 self.o2m.field.relation,
4421 title: _t("Create: ") + self.o2m.string,
4422 initial_view: "form",
4423 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4424 create_function: function(data, options) {
4425 return self.o2m.dataset.create(data, options).done(function(r) {
4426 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4427 self.o2m.dataset.trigger("dataset_changed", r);
4430 read_function: function() {
4431 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4433 parent_view: self.o2m.view,
4434 child_name: self.o2m.name,
4435 form_view_options: {'not_interactible_on_create':true}
4437 self.o2m.build_domain(),
4438 self.o2m.build_context()
4440 pop.on("elements_selected", self, function() {
4441 self.o2m.reload_current_view();
4445 do_activate_record: function(index, id) {
4447 var pop = new instance.web.form.FormOpenPopup(self);
4448 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4449 title: _t("Open: ") + self.o2m.string,
4450 write_function: function(id, data) {
4451 return self.o2m.dataset.write(id, data, {}).done(function() {
4452 self.o2m.reload_current_view();
4455 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4456 parent_view: self.o2m.view,
4457 child_name: self.o2m.name,
4458 read_function: function() {
4459 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4461 form_view_options: {'not_interactible_on_create':true},
4462 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
4465 do_button_action: function (name, id, callback) {
4466 if (!_.isNumber(id)) {
4467 instance.webclient.notification.warn(
4468 _t("Action Button"),
4469 _t("The o2m record must be saved before an action can be used"));
4472 var parent_form = this.o2m.view;
4474 this.ensure_saved().then(function () {
4476 return parent_form.save();
4479 }).done(function () {
4480 var ds = self.o2m.dataset;
4481 var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
4482 return value.length;
4484 if (!self.o2m.options.reload_on_button && !cached_records) {
4485 self.handle_button(name, id, callback);
4487 self.handle_button(name, id, function(){
4488 self.o2m.view.reload();
4494 _after_edit: function () {
4495 this.__ignore_blur = false;
4496 this.editor.form.on('blurred', this, this._on_form_blur);
4498 // The form's blur thing may be jiggered during the edition setup,
4499 // potentially leading to the o2m instasaving the row. Cancel any
4500 // blurring triggered the edition startup here
4501 this.editor.form.widgetFocused();
4503 _before_unedit: function () {
4504 this.editor.form.off('blurred', this, this._on_form_blur);
4506 _button_down: function () {
4507 // If a button is clicked (usually some sort of action button), it's
4508 // the button's responsibility to ensure the editable list is in the
4509 // correct state -> ignore form blurring
4510 this.__ignore_blur = true;
4513 * Handles blurring of the nested form (saves the currently edited row),
4514 * unless the flag to ignore the event is set to ``true``
4516 * Makes the internal form go away
4518 _on_form_blur: function () {
4519 if (this.__ignore_blur) {
4520 this.__ignore_blur = false;
4523 // FIXME: why isn't there an API for this?
4524 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4525 this.ensure_saved();
4528 this.cancel_edition();
4530 keypress_ENTER: function () {
4531 // blurring caused by hitting the [Return] key, should skip the
4532 // autosave-on-blur and let the handler for [Return] do its thing (save
4533 // the current row *anyway*, then create a new one/edit the next one)
4534 this.__ignore_blur = true;
4535 this._super.apply(this, arguments);
4537 do_delete: function (ids) {
4538 var confirm = window.confirm;
4539 window.confirm = function () { return true; };
4541 return this._super(ids);
4543 window.confirm = confirm;
4546 reload_record: function (record) {
4547 // Evict record.id from cache to ensure it will be reloaded correctly
4548 this.dataset.evict_record(record.get('id'));
4550 return this._super(record);
4553 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4554 setup_resequence_rows: function () {
4555 if (!this.view.o2m.get('effective_readonly')) {
4556 this._super.apply(this, arguments);
4560 instance.web.form.One2ManyList = instance.web.form.AddAnItemList.extend({
4561 _add_row_class: 'oe_form_field_one2many_list_row_add',
4562 is_readonly: function () {
4563 return this.view.o2m.get('effective_readonly');
4567 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4568 form_template: 'One2Many.formview',
4569 load_form: function(data) {
4572 this.$buttons.find('button.oe_form_button_create').click(function() {
4573 self.save().done(self.on_button_new);
4576 do_notify_change: function() {
4577 if (this.dataset.parent_view) {
4578 this.dataset.parent_view.do_notify_change();
4580 this._super.apply(this, arguments);
4585 var lazy_build_o2m_kanban_view = function() {
4586 if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
4588 instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4592 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4593 template: "FieldMany2ManyTags",
4594 tag_template: "FieldMany2ManyTag",
4596 this._super.apply(this, arguments);
4597 instance.web.form.CompletionFieldMixin.init.call(this);
4598 this.set({"value": []});
4599 this._display_orderer = new instance.web.DropMisordered();
4600 this._drop_shown = false;
4602 initialize_texttext: function(){
4605 plugins : 'tags arrow autocomplete',
4607 render: function(suggestion) {
4608 return $('<span class="text-label"/>').
4609 data('index', suggestion['index']).html(suggestion['label']);
4614 selectFromDropdown: function() {
4615 this.trigger('hideDropdown');
4616 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4617 var data = self.search_result[index];
4619 self.add_id(data.id);
4621 self.ignore_blur = true;
4624 this.trigger('setSuggestions', {result : []});
4628 isTagAllowed: function(tag) {
4632 removeTag: function(tag) {
4633 var id = tag.data("id");
4634 self.set({"value": _.without(self.get("value"), id)});
4636 renderTag: function(stuff) {
4637 return $.fn.textext.TextExtTags.prototype.renderTag.
4638 call(this, stuff).data("id", stuff.id);
4642 itemToString: function(item) {
4647 onSetInputData: function(e, data) {
4649 this._plugins.autocomplete._suggestions = null;
4651 this.input().val(data);
4657 initialize_content: function() {
4658 if (this.get("effective_readonly"))
4661 self.ignore_blur = false;
4662 self.$text = this.$("textarea");
4663 self.$text.textext(self.initialize_texttext()).bind('getSuggestions', function(e, data) {
4665 var str = !!data ? data.query || '' : '';
4666 self.get_search_result(str).done(function(result) {
4667 self.search_result = result;
4668 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4669 return _.extend(el, {index:i});
4672 }).bind('hideDropdown', function() {
4673 self._drop_shown = false;
4674 }).bind('showDropdown', function() {
4675 self._drop_shown = true;
4677 self.tags = self.$text.textext()[0].tags();
4679 .focusin(function () {
4680 self.trigger('focused');
4681 self.ignore_blur = false;
4683 .focusout(function() {
4684 self.$text.trigger("setInputData", "");
4685 if (!self.ignore_blur) {
4686 self.trigger('blurred');
4688 }).keydown(function(e) {
4689 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4690 self.$text.textext()[0].autocomplete().selectFromDropdown();
4694 // WARNING: duplicated in 4 other M2M widgets
4695 set_value: function(value_) {
4696 value_ = value_ || [];
4697 if (value_.length >= 1 && value_[0] instanceof Array) {
4698 // value_ is a list of m2m commands. We only process
4699 // LINK_TO and REPLACE_WITH in this context
4701 _.each(value_, function (command) {
4702 if (command[0] === commands.LINK_TO) {
4703 val.push(command[1]); // (4, id[, _])
4704 } else if (command[0] === commands.REPLACE_WITH) {
4705 val = command[2]; // (6, _, ids)
4710 this._super(value_);
4712 is_false: function() {
4713 return _(this.get("value")).isEmpty();
4715 get_value: function() {
4716 var tmp = [commands.replace_with(this.get("value"))];
4719 get_search_blacklist: function() {
4720 return this.get("value");
4722 map_tag: function(data){
4723 return _.map(data, function(el) {return {name: el[1], id:el[0]};})
4725 get_render_data: function(ids){
4727 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4728 return dataset.name_get(ids);
4730 render_tag: function(data) {
4732 if (! self.get("effective_readonly")) {
4733 self.tags.containerElement().children().remove();
4734 self.$('textarea').css("padding-left", "3px");
4735 self.tags.addTags(self.map_tag(data));
4737 self.$el.html(QWeb.render(self.tag_template, {elements: data}));
4740 render_value: function() {
4742 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4743 var values = self.get("value");
4744 var handle_names = function(data) {
4745 if (self.isDestroyed())
4748 _.each(data, function(el) {
4749 indexed[el[0]] = el;
4751 data = _.map(values, function(el) { return indexed[el]; });
4752 self.render_tag(data);
4754 if (! values || values.length > 0) {
4755 return this._display_orderer.add(self.get_render_data(values)).done(handle_names);
4760 add_id: function(id) {
4761 this.set({'value': _.uniq(this.get('value').concat([id]))});
4763 focus: function () {
4764 var input = this.$text && this.$text[0];
4765 return input ? input.focus() : false;
4767 set_dimensions: function (height, width) {
4768 this._super(height, width);
4769 this.$("textarea").css({
4774 _search_create_popup: function() {
4775 self.ignore_blur = true;
4776 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
4782 - reload_on_button: Reload the whole form view if click on a button in a list view.
4783 If you see this options, do not use it, it's basically a dirty hack to make one
4784 precise o2m to behave the way we want.
4786 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4787 multi_selection: false,
4788 disable_utility_classes: true,
4789 init: function(field_manager, node) {
4790 this._super(field_manager, node);
4791 this.is_loaded = $.Deferred();
4792 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4793 this.dataset.m2m = this;
4795 this.dataset.on('unlink', self, function(ids) {
4796 self.dataset_changed();
4799 this.list_dm = new instance.web.DropMisordered();
4800 this.render_value_dm = new instance.web.DropMisordered();
4802 initialize_content: function() {
4805 this.$el.addClass('oe_form_field oe_form_field_many2many');
4807 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4809 'deletable': this.get("effective_readonly") ? false : true,
4810 'selectable': this.multi_selection,
4812 'reorderable': false,
4813 'import_enabled': false,
4815 var embedded = (this.field.views || {}).tree;
4817 this.list_view.set_embedded_view(embedded);
4819 this.list_view.m2m_field = this;
4820 var loaded = $.Deferred();
4821 this.list_view.on("list_view_loaded", this, function() {
4824 this.list_view.appendTo(this.$el);
4826 var old_def = self.is_loaded;
4827 self.is_loaded = $.Deferred().done(function() {
4830 this.list_dm.add(loaded).then(function() {
4831 self.is_loaded.resolve();
4834 destroy_content: function() {
4835 this.list_view.destroy();
4836 this.list_view = undefined;
4838 // WARNING: duplicated in 4 other M2M widgets
4839 set_value: function(value_) {
4840 value_ = value_ || [];
4841 if (value_.length >= 1 && value_[0] instanceof Array) {
4842 // value_ is a list of m2m commands. We only process
4843 // LINK_TO and REPLACE_WITH in this context
4845 _.each(value_, function (command) {
4846 if (command[0] === commands.LINK_TO) {
4847 val.push(command[1]); // (4, id[, _])
4848 } else if (command[0] === commands.REPLACE_WITH) {
4849 val = command[2]; // (6, _, ids)
4854 this._super(value_);
4856 get_value: function() {
4857 return [commands.replace_with(this.get('value'))];
4859 is_false: function () {
4860 return _(this.get("value")).isEmpty();
4862 render_value: function() {
4864 this.dataset.set_ids(this.get("value"));
4865 this.render_value_dm.add(this.is_loaded).then(function() {
4866 return self.list_view.reload_content();
4869 dataset_changed: function() {
4870 this.internal_set_value(this.dataset.ids);
4874 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4875 get_context: function() {
4876 this.context = this.m2m.build_context();
4877 return this.context;
4883 * @extends instance.web.ListView
4885 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4886 init: function (parent, dataset, view_id, options) {
4887 this._super(parent, dataset, view_id, _.extend(options || {}, {
4888 ListType: instance.web.form.Many2ManyList,
4891 do_add_record: function () {
4892 var pop = new instance.web.form.SelectCreatePopup(this);
4896 title: _t("Add: ") + this.m2m_field.string,
4897 no_create: this.m2m_field.options.no_create,
4899 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4900 this.m2m_field.build_context()
4903 pop.on("elements_selected", self, function(element_ids) {
4905 _(element_ids).each(function (id) {
4906 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4907 self.dataset.set_ids(self.dataset.ids.concat([id]));
4908 self.m2m_field.dataset_changed();
4913 self.reload_content();
4917 do_activate_record: function(index, id) {
4919 var pop = new instance.web.form.FormOpenPopup(this);
4920 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4921 title: _t("Open: ") + this.m2m_field.string,
4922 readonly: this.getParent().get("effective_readonly")
4924 pop.on('write_completed', self, self.reload_content);
4926 do_button_action: function(name, id, callback) {
4928 var _sup = _.bind(this._super, this);
4929 if (! this.m2m_field.options.reload_on_button) {
4930 return _sup(name, id, callback);
4932 return this.m2m_field.view.save().then(function() {
4933 return _sup(name, id, function() {
4934 self.m2m_field.view.reload();
4939 is_action_enabled: function () { return true; },
4941 instance.web.form.Many2ManyList = instance.web.form.AddAnItemList.extend({
4942 _add_row_class: 'oe_form_field_many2many_list_row_add',
4943 is_readonly: function () {
4944 return this.view.m2m_field.get('effective_readonly');
4948 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4949 disable_utility_classes: true,
4950 init: function(field_manager, node) {
4951 this._super(field_manager, node);
4952 instance.web.form.CompletionFieldMixin.init.call(this);
4953 m2m_kanban_lazy_init();
4954 this.is_loaded = $.Deferred();
4955 this.initial_is_loaded = this.is_loaded;
4958 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4959 this.dataset.m2m = this;
4960 this.dataset.on('unlink', self, function(ids) {
4961 self.dataset_changed();
4965 this._super.apply(this, arguments);
4970 self.on("change:effective_readonly", self, function() {
4971 self.is_loaded = self.is_loaded.then(function() {
4972 self.kanban_view.destroy();
4973 return $.when(self.load_view()).done(function() {
4974 self.render_value();
4979 // WARNING: duplicated in 4 other M2M widgets
4980 set_value: function(value_) {
4981 value_ = value_ || [];
4982 if (value_.length >= 1 && value_[0] instanceof Array) {
4983 // value_ is a list of m2m commands. We only process
4984 // LINK_TO and REPLACE_WITH in this context
4986 _.each(value_, function (command) {
4987 if (command[0] === commands.LINK_TO) {
4988 val.push(command[1]); // (4, id[, _])
4989 } else if (command[0] === commands.REPLACE_WITH) {
4990 val = command[2]; // (6, _, ids)
4995 this._super(value_);
4997 get_value: function() {
4998 return [commands.replace_with(this.get('value'))];
5000 load_view: function() {
5002 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
5003 'create_text': _t("Add"),
5004 'creatable': self.get("effective_readonly") ? false : true,
5005 'quick_creatable': self.get("effective_readonly") ? false : true,
5006 'read_only_mode': self.get("effective_readonly") ? true : false,
5007 'confirm_on_delete': false,
5009 var embedded = (this.field.views || {}).kanban;
5011 this.kanban_view.set_embedded_view(embedded);
5013 this.kanban_view.m2m = this;
5014 var loaded = $.Deferred();
5015 this.kanban_view.on("kanban_view_loaded",self,function() {
5016 self.initial_is_loaded.resolve();
5019 this.kanban_view.on('switch_mode', this, this.open_popup);
5020 $.async_when().done(function () {
5021 self.kanban_view.appendTo(self.$el);
5025 render_value: function() {
5027 this.dataset.set_ids(this.get("value"));
5028 this.is_loaded = this.is_loaded.then(function() {
5029 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
5032 dataset_changed: function() {
5033 this.set({'value': this.dataset.ids});
5035 open_popup: function(type, unused) {
5036 if (type !== "form")
5040 if (this.dataset.index === null) {
5041 pop = new instance.web.form.SelectCreatePopup(this);
5043 this.field.relation,
5045 title: _t("Add: ") + this.string
5047 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
5048 this.build_context()
5050 pop.on("elements_selected", self, function(element_ids) {
5051 _.each(element_ids, function(one_id) {
5052 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
5053 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
5054 self.dataset_changed();
5055 self.render_value();
5060 var id = self.dataset.ids[self.dataset.index];
5061 pop = new instance.web.form.FormOpenPopup(this);
5062 pop.show_element(self.field.relation, id, self.build_context(), {
5063 title: _t("Open: ") + self.string,
5064 write_function: function(id, data, options) {
5065 return self.dataset.write(id, data, {}).done(function() {
5066 self.render_value();
5069 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
5070 parent_view: self.view,
5071 child_name: self.name,
5072 readonly: self.get("effective_readonly")
5076 add_id: function(id) {
5077 this.quick_create.add_id(id);
5081 function m2m_kanban_lazy_init() {
5082 if (instance.web.form.Many2ManyKanbanView)
5084 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
5085 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
5086 _is_quick_create_enabled: function() {
5087 return this._super() && ! this.group_by;
5090 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
5091 template: 'Many2ManyKanban.quick_create',
5094 * close_btn: If true, the widget will display a "Close" button able to trigger
5097 init: function(parent, dataset, context, buttons) {
5098 this._super(parent);
5099 this.m2m = this.getParent().view.m2m;
5100 this.m2m.quick_create = this;
5101 this._dataset = dataset;
5102 this._buttons = buttons || false;
5103 this._context = context || {};
5105 start: function () {
5107 self.$text = this.$el.find('input').css("width", "200px");
5108 self.$text.textext({
5109 plugins : 'arrow autocomplete',
5111 render: function(suggestion) {
5112 return $('<span class="text-label"/>').
5113 data('index', suggestion['index']).html(suggestion['label']);
5118 selectFromDropdown: function() {
5119 $(this).trigger('hideDropdown');
5120 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
5121 var data = self.search_result[index];
5123 self.add_id(data.id);
5130 itemToString: function(item) {
5135 }).bind('getSuggestions', function(e, data) {
5137 var str = !!data ? data.query || '' : '';
5138 self.m2m.get_search_result(str).done(function(result) {
5139 self.search_result = result;
5140 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
5141 return _.extend(el, {index:i});
5145 self.$text.focusout(function() {
5150 this.$text[0].focus();
5152 add_id: function(id) {
5155 self.trigger('added', id);
5156 this.m2m.dataset_changed();
5162 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
5164 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
5165 template: "AbstractFormPopup.render",
5168 * -readonly: only applicable when not in creation mode, default to false
5169 * - alternative_form_view
5176 * - form_view_options
5178 init_popup: function(model, row_id, domain, context, options) {
5179 this.row_id = row_id;
5181 this.domain = domain || [];
5182 this.context = context || {};
5183 this.options = options;
5184 _.defaults(this.options, {
5187 init_dataset: function() {
5189 this.created_elements = [];
5190 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
5191 this.dataset.read_function = this.options.read_function;
5192 this.dataset.create_function = function(data, options, sup) {
5193 var fct = self.options.create_function || sup;
5194 return fct.call(this, data, options).done(function(r) {
5195 self.trigger('create_completed saved', r);
5196 self.created_elements.push(r);
5199 this.dataset.write_function = function(id, data, options, sup) {
5200 var fct = self.options.write_function || sup;
5201 return fct.call(this, id, data, options).done(function(r) {
5202 self.trigger('write_completed saved', r);
5205 this.dataset.parent_view = this.options.parent_view;
5206 this.dataset.child_name = this.options.child_name;
5208 display_popup: function() {
5210 this.renderElement();
5211 var dialog = new instance.web.Dialog(this, {
5212 dialogClass: 'oe_act_window',
5213 title: this.options.title || "",
5214 }, this.$el).open();
5215 dialog.on('closing', this, function (e){
5216 self.check_exit(true);
5218 this.$buttonpane = dialog.$buttons;
5221 setup_form_view: function() {
5224 this.dataset.ids = [this.row_id];
5225 this.dataset.index = 0;
5227 this.dataset.index = null;
5229 var options = _.clone(self.options.form_view_options) || {};
5230 if (this.row_id !== null) {
5231 options.initial_mode = this.options.readonly ? "view" : "edit";
5234 $buttons: this.$buttonpane,
5236 this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
5237 if (this.options.alternative_form_view) {
5238 this.view_form.set_embedded_view(this.options.alternative_form_view);
5240 this.view_form.appendTo(this.$el.find(".oe_popup_form"));
5241 this.view_form.on("form_view_loaded", self, function() {
5242 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
5243 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
5244 multi_select: multi_select,
5245 readonly: self.row_id !== null && self.options.readonly,
5247 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
5248 $snbutton.click(function() {
5249 $.when(self.view_form.save()).done(function() {
5250 self.view_form.reload_mutex.exec(function() {
5251 self.view_form.on_button_new();
5255 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
5256 $sbutton.click(function() {
5257 $.when(self.view_form.save()).done(function() {
5258 self.view_form.reload_mutex.exec(function() {
5263 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
5264 $cbutton.click(function() {
5265 self.view_form.trigger('on_button_cancel');
5268 self.view_form.do_show();
5271 select_elements: function(element_ids) {
5272 this.trigger("elements_selected", element_ids);
5274 check_exit: function(no_destroy) {
5275 if (this.created_elements.length > 0) {
5276 this.select_elements(this.created_elements);
5277 this.created_elements = [];
5279 this.trigger('closed');
5282 destroy: function () {
5283 this.trigger('closed');
5284 if (this.$el.is(":data(bs.modal)")) {
5285 this.$el.parents('.modal').modal('hide');
5292 * Class to display a popup containing a form view.
5294 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
5295 show_element: function(model, row_id, context, options) {
5296 this.init_popup(model, row_id, [], context, options);
5297 _.defaults(this.options, {
5299 this.display_popup();
5303 this.init_dataset();
5304 this.setup_form_view();
5309 * Class to display a popup to display a list to search a row. It also allows
5310 * to switch to a form view to create a new row.
5312 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
5316 * - initial_view: form or search (default search)
5317 * - disable_multiple_selection
5318 * - list_view_options
5320 select_element: function(model, options, domain, context) {
5321 this.init_popup(model, null, domain, context, options);
5323 _.defaults(this.options, {
5324 initial_view: "search",
5326 this.initial_ids = this.options.initial_ids;
5327 this.display_popup();
5331 this.init_dataset();
5332 if (this.options.initial_view == "search") {
5333 instance.web.pyeval.eval_domains_and_contexts({
5335 contexts: [this.context]
5336 }).done(function (results) {
5337 var search_defaults = {};
5338 _.each(results.context, function (value_, key) {
5339 var match = /^search_default_(.*)$/.exec(key);
5341 search_defaults[match[1]] = value_;
5344 self.setup_search_view(search_defaults);
5350 setup_search_view: function(search_defaults) {
5352 if (this.searchview) {
5353 this.searchview.destroy();
5355 if (this.searchview_drawer) {
5356 this.searchview_drawer.destroy();
5358 this.searchview = new instance.web.SearchView(this,
5359 this.dataset, false, search_defaults);
5360 this.searchview_drawer = new instance.web.SearchViewDrawer(this, this.searchview);
5361 this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
5362 if (self.initial_ids) {
5363 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
5364 contexts.concat(self.context), groupbys);
5365 self.initial_ids = undefined;
5367 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
5370 this.searchview.on("search_view_loaded", self, function() {
5371 self.view_list = new instance.web.form.SelectCreateListView(self,
5372 self.dataset, false,
5373 _.extend({'deletable': false,
5374 'selectable': !self.options.disable_multiple_selection,
5375 'import_enabled': false,
5376 '$buttons': self.$buttonpane,
5377 'disable_editable_mode': true,
5378 '$pager': self.$('.oe_popup_list_pager'),
5379 }, self.options.list_view_options || {}));
5380 self.view_list.on('edit:before', self, function (e) {
5383 self.view_list.popup = self;
5384 self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
5385 self.view_list.do_show();
5386 }).then(function() {
5387 self.searchview.do_search();
5389 self.view_list.on("list_view_loaded", self, function() {
5390 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
5391 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
5392 $cbutton.click(function() {
5395 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
5396 $sbutton.click(function() {
5397 self.select_elements(self.selected_ids);
5400 $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
5401 $cbutton.click(function() {
5406 this.searchview.appendTo(this.$(".oe_popup_search"));
5408 do_search: function(domains, contexts, groupbys) {
5410 instance.web.pyeval.eval_domains_and_contexts({
5411 domains: domains || [],
5412 contexts: contexts || [],
5413 group_by_seq: groupbys || []
5414 }).done(function (results) {
5415 self.view_list.do_search(results.domain, results.context, results.group_by);
5418 on_click_element: function(ids) {
5420 this.selected_ids = ids || [];
5421 if(this.selected_ids.length > 0) {
5422 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
5424 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
5427 new_object: function() {
5428 if (this.searchview) {
5429 this.searchview.hide();
5431 if (this.view_list) {
5432 this.view_list.do_hide();
5434 this.setup_form_view();
5438 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
5439 do_add_record: function () {
5440 this.popup.new_object();
5442 select_record: function(index) {
5443 this.popup.select_elements([this.dataset.ids[index]]);
5444 this.popup.destroy();
5446 do_select: function(ids, records) {
5447 this._super(ids, records);
5448 this.popup.on_click_element(ids);
5452 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5453 template: 'FieldReference',
5454 init: function(field_manager, node) {
5455 this._super(field_manager, node);
5456 this.reference_ready = true;
5458 destroy_content: function() {
5461 this.fm = undefined;
5464 initialize_content: function() {
5466 var fm = new instance.web.form.DefaultFieldManager(this);
5468 fm.extend_field_desc({
5470 selection: this.field_manager.get_field_desc(this.name).selection,
5478 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
5480 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5482 this.selection.on("change:value", this, this.on_selection_changed);
5483 this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
5485 .on('focused', null, function () {self.trigger('focused');})
5486 .on('blurred', null, function () {self.trigger('blurred');});
5488 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
5489 name: 'Referenced Document',
5490 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5492 this.m2o.on("change:value", this, this.data_changed);
5493 this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
5495 .on('focused', null, function () {self.trigger('focused');})
5496 .on('blurred', null, function () {self.trigger('blurred');});
5498 on_selection_changed: function() {
5499 if (this.reference_ready) {
5500 this.internal_set_value([this.selection.get_value(), false]);
5501 this.render_value();
5504 data_changed: function() {
5505 if (this.reference_ready) {
5506 this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
5509 set_value: function(val) {
5511 val = val.split(',');
5512 val[0] = val[0] || false;
5513 val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
5515 this._super(val || [false, false]);
5517 get_value: function() {
5518 return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
5520 render_value: function() {
5521 this.reference_ready = false;
5522 if (!this.get("effective_readonly")) {
5523 this.selection.set_value(this.get('value')[0]);
5525 this.m2o.field.relation = this.get('value')[0];
5526 this.m2o.set_value(this.get('value')[1]);
5527 this.m2o.$el.toggle(!!this.get('value')[0]);
5528 this.reference_ready = true;
5532 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5533 init: function(field_manager, node) {
5535 this._super(field_manager, node);
5536 this.binary_value = false;
5537 this.useFileAPI = !!window.FileReader;
5538 this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5539 if (!this.useFileAPI) {
5540 this.fileupload_id = _.uniqueId('oe_fileupload');
5541 $(window).on(this.fileupload_id, function() {
5542 var args = [].slice.call(arguments).slice(1);
5543 self.on_file_uploaded.apply(self, args);
5548 if (!this.useFileAPI) {
5549 $(window).off(this.fileupload_id);
5551 this._super.apply(this, arguments);
5553 initialize_content: function() {
5555 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5556 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5557 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5558 this.$el.find('.oe_form_binary_file_edit').click(function(event){
5559 self.$el.find('input.oe_form_binary_file').click();
5562 on_file_change: function(e) {
5564 var file_node = e.target;
5565 if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5566 if (this.useFileAPI) {
5567 var file = file_node.files[0];
5568 if (file.size > this.max_upload_size) {
5569 var msg = _t("The selected file exceed the maximum file size of %s.");
5570 instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5573 var filereader = new FileReader();
5574 filereader.readAsDataURL(file);
5575 filereader.onloadend = function(upload) {
5576 var data = upload.target.result;
5577 data = data.split(',')[1];
5578 self.on_file_uploaded(file.size, file.name, file.type, data);
5581 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5582 this.$el.find('form.oe_form_binary_form').submit();
5584 this.$el.find('.oe_form_binary_progress').show();
5585 this.$el.find('.oe_form_binary').hide();
5588 on_file_uploaded: function(size, name, content_type, file_base64) {
5589 if (size === false) {
5590 this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5591 // TODO: use openerp web crashmanager
5592 console.warn("Error while uploading file : ", name);
5594 this.filename = name;
5595 this.on_file_uploaded_and_valid.apply(this, arguments);
5597 this.$el.find('.oe_form_binary_progress').hide();
5598 this.$el.find('.oe_form_binary').show();
5600 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5602 on_save_as: function(ev) {
5603 var value = this.get('value');
5605 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5606 ev.stopPropagation();
5608 instance.web.blockUI();
5609 var c = instance.webclient.crashmanager;
5610 this.session.get_file({
5611 url: '/web/binary/saveas_ajax',
5612 data: {data: JSON.stringify({
5613 model: this.view.dataset.model,
5614 id: (this.view.datarecord.id || ''),
5616 filename_field: (this.node.attrs.filename || ''),
5617 data: instance.web.form.is_bin_size(value) ? null : value,
5618 context: this.view.dataset.get_context()
5620 complete: instance.web.unblockUI,
5621 error: c.rpc_error.bind(c)
5623 ev.stopPropagation();
5627 set_filename: function(value) {
5628 var filename = this.node.attrs.filename;
5631 tmp[filename] = value;
5632 this.field_manager.set_values(tmp);
5635 on_clear: function() {
5636 if (this.get('value') !== false) {
5637 this.binary_value = false;
5638 this.internal_set_value(false);
5644 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5645 template: 'FieldBinaryFile',
5646 initialize_content: function() {
5648 if (this.get("effective_readonly")) {
5650 this.$el.find('a').click(function(ev) {
5651 if (self.get('value')) {
5652 self.on_save_as(ev);
5658 render_value: function() {
5660 if (!this.get("effective_readonly")) {
5661 if (this.node.attrs.filename) {
5662 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5664 show_value = (this.get('value') !== null && this.get('value') !== undefined && this.get('value') !== false) ? this.get('value') : '';
5666 this.$el.find('input').eq(0).val(show_value);
5668 this.$el.find('a').toggle(!!this.get('value'));
5669 if (this.get('value')) {
5670 show_value = _t("Download");
5672 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5673 this.$el.find('a').text(show_value);
5677 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5678 this.binary_value = true;
5679 this.internal_set_value(file_base64);
5680 var show_value = name + " (" + instance.web.human_size(size) + ")";
5681 this.$el.find('input').eq(0).val(show_value);
5682 this.set_filename(name);
5684 on_clear: function() {
5685 this._super.apply(this, arguments);
5686 this.$el.find('input').eq(0).val('');
5687 this.set_filename('');
5691 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5692 template: 'FieldBinaryImage',
5693 placeholder: "/web/static/src/img/placeholder.png",
5694 render_value: function() {
5697 if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5698 url = 'data:image/png;base64,' + this.get('value');
5699 } else if (this.get('value')) {
5700 var id = JSON.stringify(this.view.datarecord.id || null);
5701 var field = this.name;
5702 if (this.options.preview_image)
5703 field = this.options.preview_image;
5704 url = this.session.url('/web/binary/image', {
5705 model: this.view.dataset.model,
5708 t: (new Date().getTime()),
5711 url = this.placeholder;
5713 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5714 $($img).click(function(e) {
5715 if(self.view.get("actual_mode") == "view") {
5716 var $button = $(".oe_form_button_edit");
5717 $button.openerpBounce();
5718 e.stopPropagation();
5721 this.$el.find('> img').remove();
5722 this.$el.prepend($img);
5723 $img.load(function() {
5724 if (! self.options.size)
5726 $img.css("max-width", "" + self.options.size[0] + "px");
5727 $img.css("max-height", "" + self.options.size[1] + "px");
5729 $img.on('error', function() {
5730 $img.attr('src', self.placeholder);
5731 instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5734 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5735 this.internal_set_value(file_base64);
5736 this.binary_value = true;
5737 this.render_value();
5738 this.set_filename(name);
5740 on_clear: function() {
5741 this._super.apply(this, arguments);
5742 this.render_value();
5743 this.set_filename('');
5745 set_value: function(value_){
5746 var changed = value_ !== this.get_value();
5747 this._super.apply(this, arguments);
5748 // By default, on binary images read, the server returns the binary size
5749 // This is possible that two images have the exact same size
5750 // Therefore we trigger the change in case the image value hasn't changed
5751 // So the image is re-rendered correctly
5753 this.trigger("change:value", this, {
5762 * Widget for (many2many field) to upload one or more file in same time and display in list.
5763 * The user can delete his files.
5764 * Options on attribute ; "blockui" {Boolean} block the UI or not
5765 * during the file is uploading
5767 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
5768 template: "FieldBinaryFileUploader",
5769 init: function(field_manager, node) {
5770 this._super(field_manager, node);
5771 this.field_manager = field_manager;
5773 if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5774 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);
5778 this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5779 this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5780 $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5784 this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5786 // WARNING: duplicated in 4 other M2M widgets
5787 set_value: function(value_) {
5788 value_ = value_ || [];
5789 if (value_.length >= 1 && value_[0] instanceof Array) {
5790 // value_ is a list of m2m commands. We only process
5791 // LINK_TO and REPLACE_WITH in this context
5793 _.each(value_, function (command) {
5794 if (command[0] === commands.LINK_TO) {
5795 val.push(command[1]); // (4, id[, _])
5796 } else if (command[0] === commands.REPLACE_WITH) {
5797 val = command[2]; // (6, _, ids)
5802 this._super(value_);
5804 get_value: function() {
5805 var tmp = [commands.replace_with(this.get("value"))];
5808 get_file_url: function (attachment) {
5809 return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5811 read_name_values : function () {
5813 // don't reset know values
5814 var ids = this.get('value');
5815 var _value = _.filter(ids, function (id) { return typeof self.data[id] == 'undefined'; } );
5816 // send request for get_name
5817 if (_value.length) {
5818 return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).then(function (datas) {
5819 _.each(datas, function (data) {
5820 data.no_unlink = true;
5821 data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5822 self.data[data.id] = data;
5830 render_value: function () {
5832 this.read_name_values().then(function (ids) {
5833 var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids}));
5834 render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5835 self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5837 // reinit input type file
5838 var $input = self.$('input.oe_form_binary_file');
5839 $input.after($input.clone(true)).remove();
5840 self.$(".oe_fileupload").show();
5844 on_file_change: function (event) {
5845 event.stopPropagation();
5847 var $target = $(event.target);
5848 if ($target.val() !== '') {
5849 var filename = $target.val().replace(/.*[\\\/]/,'');
5850 // don't uplode more of one file in same time
5851 if (self.data[0] && self.data[0].upload ) {
5854 for (var id in this.get('value')) {
5855 // if the files exits, delete the file before upload (if it's a new file)
5856 if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
5857 self.ds_file.unlink([id]);
5862 if(this.node.attrs.blockui>0) {
5863 instance.web.blockUI();
5866 // TODO : unactivate send on wizard and form
5869 this.$('form.oe_form_binary_form').submit();
5870 this.$(".oe_fileupload").hide();
5871 // add file on data result
5875 'filename': filename,
5881 on_file_loaded: function (event, result) {
5882 var files = this.get('value');
5885 if(this.node.attrs.blockui>0) {
5886 instance.web.unblockUI();
5889 if (result.error || !result.id ) {
5890 this.do_warn( _t('Uploading Error'), result.error);
5891 delete this.data[0];
5893 if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
5894 delete this.data[0];
5895 this.data[result.id] = {
5897 'name': result.name,
5898 'filename': result.filename,
5899 'url': this.get_file_url(result)
5902 this.data[result.id] = {
5904 'name': result.name,
5905 'filename': result.filename,
5906 'url': this.get_file_url(result)
5909 var values = _.clone(this.get('value'));
5910 values.push(result.id);
5911 this.set({'value': values});
5913 this.render_value();
5915 on_file_delete: function (event) {
5916 event.stopPropagation();
5917 var file_id=$(event.target).data("id");
5919 var files = _.filter(this.get('value'), function (id) {return id != file_id;});
5920 if(!this.data[file_id].no_unlink) {
5921 this.ds_file.unlink([file_id]);
5923 this.set({'value': files});
5928 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5929 template: "FieldStatus",
5930 init: function(field_manager, node) {
5931 this._super(field_manager, node);
5932 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5933 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5934 this.set({value: false});
5935 this.selection = {'unfolded': [], 'folded': []};
5936 this.set("selection", {'unfolded': [], 'folded': []});
5937 this.selection_dm = new instance.web.DropMisordered();
5938 this.dataset = new instance.web.DataSetStatic(this, this.field.relation, this.build_context());
5941 this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5943 this.on("change:value", this, this.get_selection);
5944 this.on("change:evaluated_selection_domain", this, this.get_selection);
5945 this.on("change:selection", this, function() {
5946 this.selection = this.get("selection");
5947 this.render_value();
5949 this.get_selection();
5950 if (this.options.clickable) {
5951 this.$el.on('click','li[data-id]',this.on_click_stage);
5953 if (this.$el.parent().is('header')) {
5954 this.$el.after('<div class="oe_clear"/>');
5958 set_value: function(value_) {
5959 if (value_ instanceof Array) {
5962 this._super(value_);
5964 render_value: function() {
5966 var content = QWeb.render("FieldStatus.content", {
5968 'value_folded': _.find(self.selection.folded, function(i){return i[0] === self.get('value');})
5970 self.$el.html(content);
5972 calc_domain: function() {
5973 var d = instance.web.pyeval.eval('domain', this.build_domain());
5974 var domain = []; //if there is no domain defined, fetch all the records
5977 domain = ['|',['id', '=', this.get('value')]].concat(d);
5980 if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5981 this.set("evaluated_selection_domain", domain);
5984 /** Get the selection and render it
5985 * selection: [[identifier, value_to_display], ...]
5986 * For selection fields: this is directly given by this.field.selection
5987 * For many2one fields: perform a search on the relation of the many2one field
5989 get_selection: function() {
5991 var selection_unfolded = [];
5992 var selection_folded = [];
5993 var fold_field = this.options.fold_field;
5995 var calculation = _.bind(function() {
5996 if (this.field.type == "many2one") {
5997 return self.get_distant_fields().then(function (fields) {
5998 return new instance.web.DataSetSearch(self, self.field.relation, self.build_context(), self.get("evaluated_selection_domain"))
5999 .read_slice(_.union(_.keys(self.distant_fields), ['id']), {}).then(function (records) {
6000 var ids = _.pluck(records, 'id');
6001 return self.dataset.name_get(ids).then(function (records_name) {
6002 _.each(records, function (record) {
6003 var name = _.find(records_name, function (val) {return val[0] == record.id;})[1];
6004 if (fold_field && record[fold_field] && record.id != self.get('value')) {
6005 selection_folded.push([record.id, name]);
6007 selection_unfolded.push([record.id, name]);
6014 // For field type selection filter values according to
6015 // statusbar_visible attribute of the field. For example:
6016 // statusbar_visible="draft,open".
6017 var select = this.field.selection;
6018 for(var i=0; i < select.length; i++) {
6019 var key = select[i][0];
6020 if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
6021 selection_unfolded.push(select[i]);
6027 this.selection_dm.add(calculation()).then(function () {
6028 var selection = {'unfolded': selection_unfolded, 'folded': selection_folded};
6029 if (! _.isEqual(selection, self.get("selection"))) {
6030 self.set("selection", selection);
6035 * :deprecated: this feature will probably be removed with OpenERP v8
6037 get_distant_fields: function() {
6039 if (! this.options.fold_field) {
6040 this.distant_fields = {}
6042 if (this.distant_fields) {
6043 return $.when(this.distant_fields);
6045 return new instance.web.Model(self.field.relation).call("fields_get", [[this.options.fold_field]]).then(function(fields) {
6046 self.distant_fields = fields;
6050 on_click_stage: function (ev) {
6052 var $li = $(ev.currentTarget);
6054 if (this.field.type == "many2one") {
6055 val = parseInt($li.data("id"), 10);
6058 val = $li.data("id");
6060 if (val != self.get('value')) {
6061 this.view.recursive_save().done(function() {
6063 change[self.name] = val;
6064 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
6072 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
6073 template: "FieldMonetary",
6074 widget_class: 'oe_form_field_float oe_form_field_monetary',
6076 this._super.apply(this, arguments);
6077 this.set({"currency": false});
6078 if (this.options.currency_field) {
6079 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
6080 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
6083 this.on("change:currency", this, this.get_currency_info);
6084 this.get_currency_info();
6085 this.ci_dm = new instance.web.DropMisordered();
6088 var tmp = this._super();
6089 this.on("change:currency_info", this, this.reinitialize);
6092 get_currency_info: function() {
6094 if (this.get("currency") === false) {
6095 this.set({"currency_info": null});
6098 return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
6099 .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
6100 self.set({"currency_info": res});
6103 parse_value: function(val, def) {
6104 return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6106 format_value: function(val, def) {
6107 return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6112 This type of field display a list of checkboxes. It works only with m2ms. This field will display one checkbox for each
6113 record existing in the model targeted by the relation, according to the given domain if one is specified. Checked records
6114 will be added to the relation.
6116 instance.web.form.FieldMany2ManyCheckBoxes = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6117 className: "oe_form_many2many_checkboxes",
6119 this._super.apply(this, arguments);
6120 this.set("value", {});
6121 this.set("records", []);
6122 this.field_manager.on("view_content_has_changed", this, function() {
6123 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
6124 if (! _.isEqual(domain, this.get("domain"))) {
6125 this.set("domain", domain);
6128 this.records_orderer = new instance.web.DropMisordered();
6130 initialize_field: function() {
6131 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
6132 this.on("change:domain", this, this.query_records);
6133 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
6134 this.on("change:records", this, this.render_value);
6136 query_records: function() {
6138 var model = new openerp.Model(openerp.session, this.field.relation);
6139 this.records_orderer.add(model.call("search", [this.get("domain")], {"context": this.build_context()}).then(function(record_ids) {
6140 return model.call("name_get", [record_ids] , {"context": self.build_context()});
6141 })).then(function(res) {
6142 self.set("records", res);
6145 render_value: function() {
6146 this.$().html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this, selected: this.get("value")}));
6147 var inputs = this.$("input");
6148 inputs.change(_.bind(this.from_dom, this));
6149 if (this.get("effective_readonly"))
6150 inputs.attr("disabled", "true");
6152 from_dom: function() {
6154 this.$("input").each(function() {
6156 new_value[elem.data("record-id")] = elem.attr("checked") ? true : undefined;
6158 if (! _.isEqual(new_value, this.get("value")))
6159 this.internal_set_value(new_value);
6161 // WARNING: (mostly) duplicated in 4 other M2M widgets
6162 set_value: function(value_) {
6163 value_ = value_ || [];
6164 if (value_.length >= 1 && value_[0] instanceof Array) {
6165 // value_ is a list of m2m commands. We only process
6166 // LINK_TO and REPLACE_WITH in this context
6168 _.each(value_, function (command) {
6169 if (command[0] === commands.LINK_TO) {
6170 val.push(command[1]); // (4, id[, _])
6171 } else if (command[0] === commands.REPLACE_WITH) {
6172 val = command[2]; // (6, _, ids)
6178 _.each(value_, function(el) {
6179 formatted[JSON.stringify(el)] = true;
6181 this._super(formatted);
6183 get_value: function() {
6184 var value = _.filter(_.keys(this.get("value")), function(el) {
6185 return this.get("value")[el];
6187 value = _.map(value, function(el) {
6188 return JSON.parse(el);
6190 return [commands.replace_with(value)];
6195 This field can be applied on many2many and one2many. It is a read-only field that will display a single link whose name is
6196 "<number of linked records> <label of the field>". When the link is clicked, it will redirect to another act_window
6197 action on the model of the relation and show only the linked records.
6201 * 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
6202 to display (or False to take the default one) and the second element is the type of the view. Defaults to
6203 [[false, "tree"], [false, "form"]] .
6205 instance.web.form.X2ManyCounter = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6206 className: "oe_form_x2many_counter",
6208 this._super.apply(this, arguments);
6209 this.set("value", []);
6210 _.defaults(this.options, {
6211 "views": [[false, "tree"], [false, "form"]],
6214 render_value: function() {
6215 var text = _.str.sprintf("%d %s", this.val().length, this.string);
6216 this.$().html(QWeb.render("X2ManyCounter", {text: text}));
6217 this.$("a").click(_.bind(this.go_to, this));
6220 return this.view.recursive_save().then(_.bind(function() {
6221 var val = this.val();
6223 if (this.field.type === "one2many") {
6224 context["default_" + this.field.relation_field] = this.view.datarecord.id;
6226 var domain = [["id", "in", val]];
6227 return this.do_action({
6228 type: 'ir.actions.act_window',
6230 res_model: this.field.relation,
6231 views: this.options.views,
6239 var value = this.get("value") || [];
6240 if (value.length >= 1 && value[0] instanceof Array) {
6241 value = value[0][2];
6248 This widget is intended to be used on stat button numeric fields. It will display
6249 the value many2many and one2many. It is a read-only field that will
6250 display a simple string "<value of field> <label of the field>"
6252 instance.web.form.StatInfo = instance.web.form.AbstractField.extend({
6253 is_field_number: true,
6255 this._super.apply(this, arguments);
6256 this.internal_set_value(0);
6258 set_value: function(value_) {
6259 if (value_ === false || value_ === undefined) {
6262 this._super.apply(this, [value_]);
6264 render_value: function() {
6266 value: this.get("value") || 0,
6268 if (! this.node.attrs.nolabel) {
6269 options.text = this.string
6271 this.$el.html(QWeb.render("StatInfo", options));
6278 * Registry of form fields, called by :js:`instance.web.FormView`.
6280 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
6281 * will substitute to the <field> tags as defined in OpenERP's views.
6283 instance.web.form.widgets = new instance.web.Registry({
6284 'char' : 'instance.web.form.FieldChar',
6285 'id' : 'instance.web.form.FieldID',
6286 'email' : 'instance.web.form.FieldEmail',
6287 'url' : 'instance.web.form.FieldUrl',
6288 'text' : 'instance.web.form.FieldText',
6289 'html' : 'instance.web.form.FieldTextHtml',
6290 'char_domain': 'instance.web.form.FieldCharDomain',
6291 'date' : 'instance.web.form.FieldDate',
6292 'datetime' : 'instance.web.form.FieldDatetime',
6293 'selection' : 'instance.web.form.FieldSelection',
6294 'radio' : 'instance.web.form.FieldRadio',
6295 'many2one' : 'instance.web.form.FieldMany2One',
6296 'many2onebutton' : 'instance.web.form.Many2OneButton',
6297 'many2many' : 'instance.web.form.FieldMany2Many',
6298 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
6299 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
6300 'one2many' : 'instance.web.form.FieldOne2Many',
6301 'one2many_list' : 'instance.web.form.FieldOne2Many',
6302 'reference' : 'instance.web.form.FieldReference',
6303 'boolean' : 'instance.web.form.FieldBoolean',
6304 'float' : 'instance.web.form.FieldFloat',
6305 'percentpie': 'instance.web.form.FieldPercentPie',
6306 'barchart': 'instance.web.form.FieldBarChart',
6307 'integer': 'instance.web.form.FieldFloat',
6308 'float_time': 'instance.web.form.FieldFloat',
6309 'progressbar': 'instance.web.form.FieldProgressBar',
6310 'image': 'instance.web.form.FieldBinaryImage',
6311 'binary': 'instance.web.form.FieldBinaryFile',
6312 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
6313 'statusbar': 'instance.web.form.FieldStatus',
6314 'monetary': 'instance.web.form.FieldMonetary',
6315 'many2many_checkboxes': 'instance.web.form.FieldMany2ManyCheckBoxes',
6316 'x2many_counter': 'instance.web.form.X2ManyCounter',
6317 'priority':'instance.web.form.Priority',
6318 'kanban_state_selection':'instance.web.form.KanbanSelection',
6319 'statinfo': 'instance.web.form.StatInfo',
6323 * Registry of widgets usable in the form view that can substitute to any possible
6324 * tags defined in OpenERP's form views.
6326 * Every referenced class should extend FormWidget.
6328 instance.web.form.tags = new instance.web.Registry({
6329 'button' : 'instance.web.form.WidgetButton',
6332 instance.web.form.custom_widgets = new instance.web.Registry({
6337 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: