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
89 * @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance
91 init: function(parent, dataset, view_id, options) {
94 this.ViewManager = parent;
95 this.set_default_options(options);
96 this.dataset = dataset;
97 this.model = dataset.model;
98 this.view_id = view_id || false;
99 this.fields_view = {};
101 this.fields_order = [];
102 this.datarecord = {};
103 this._onchange_specs = {};
104 this.onchanges_defs = [];
105 this.default_focus_field = null;
106 this.default_focus_button = null;
107 this.fields_registry = instance.web.form.widgets;
108 this.tags_registry = instance.web.form.tags;
109 this.widgets_registry = instance.web.form.custom_widgets;
110 this.has_been_loaded = $.Deferred();
111 this.translatable_fields = [];
112 _.defaults(this.options, {
113 "not_interactible_on_create": false,
114 "initial_mode": "view",
115 "disable_autofocus": false,
116 "footer_to_buttons": false,
118 this.is_initialized = $.Deferred();
119 this.mutating_mutex = new $.Mutex();
121 this.render_value_defs = [];
122 this.reload_mutex = new $.Mutex();
123 this.__clicked_inside = false;
124 this.__blur_timeout = null;
125 this.rendering_engine = new instance.web.form.FormRenderingEngine(this);
126 self.set({actual_mode: self.options.initial_mode});
127 this.has_been_loaded.done(function() {
128 self._build_onchange_specs();
129 self.on("change:actual_mode", self, self.check_actual_mode);
130 self.check_actual_mode();
131 self.on("change:actual_mode", self, self.init_pager);
134 self.on("load_record", self, self.load_record);
135 instance.web.bus.on('clear_uncommitted_changes', this, function(e) {
136 if (!this.can_be_discarded()) {
141 view_loading: function(r) {
142 return this.load_form(r);
144 destroy: function() {
145 _.each(this.get_widgets(), function(w) {
146 w.off('focused blurred');
150 this.$el.off('.formBlur');
154 load_form: function(data) {
157 throw new Error(_t("No data provided."));
160 throw "Form view does not support multiple calls to load_form";
162 this.fields_order = [];
163 this.fields_view = data;
165 this.rendering_engine.set_fields_registry(this.fields_registry);
166 this.rendering_engine.set_tags_registry(this.tags_registry);
167 this.rendering_engine.set_widgets_registry(this.widgets_registry);
168 this.rendering_engine.set_fields_view(data);
169 var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container');
170 this.rendering_engine.render_to($dest);
172 this.$el.on('mousedown.formBlur', function () {
173 self.__clicked_inside = true;
176 this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self}));
177 if (this.options.$buttons) {
178 this.$buttons.appendTo(this.options.$buttons);
180 this.$el.find('.oe_form_buttons').replaceWith(this.$buttons);
182 this.$buttons.on('click', '.oe_form_button_create',
183 this.guard_active(this.on_button_create));
184 this.$buttons.on('click', '.oe_form_button_edit',
185 this.guard_active(this.on_button_edit));
186 this.$buttons.on('click', '.oe_form_button_save',
187 this.guard_active(this.on_button_save));
188 this.$buttons.on('click', '.oe_form_button_cancel',
189 this.guard_active(this.on_button_cancel));
190 if (this.options.footer_to_buttons) {
191 this.$el.find('footer').appendTo(this.$buttons);
194 this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar');
195 if (!this.sidebar && this.options.$sidebar) {
196 this.sidebar = new instance.web.Sidebar(this);
197 this.sidebar.appendTo(this.$sidebar);
198 if (this.fields_view.toolbar) {
199 this.sidebar.add_toolbar(this.fields_view.toolbar);
201 this.sidebar.add_items('other', _.compact([
202 self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete },
203 self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate }
207 this.has_been_loaded.resolve();
209 // Add bounce effect on button 'Edit' when click on readonly page view.
210 this.$el.find(".oe_form_group_row,.oe_form_field,label,h1,.oe_title,.oe_notebook_page, .oe_list_content").on('click', function (e) {
211 if(self.get("actual_mode") == "view") {
212 var $button = self.options.$buttons.find(".oe_form_button_edit");
213 $button.openerpBounce();
215 instance.web.bus.trigger('click', e);
218 //bounce effect on red button when click on statusbar.
219 this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) {
220 if((self.get("actual_mode") == "view")) {
221 var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'});
222 $button.openerpBounce();
226 this.trigger('form_view_loaded', data);
229 widgetFocused: function() {
230 // Clear click flag if used to focus a widget
231 this.__clicked_inside = false;
232 if (this.__blur_timeout) {
233 clearTimeout(this.__blur_timeout);
234 this.__blur_timeout = null;
237 widgetBlurred: function() {
238 if (this.__clicked_inside) {
239 // clicked in an other section of the form (than the currently
240 // focused widget) => just ignore the blurring entirely?
241 this.__clicked_inside = false;
245 // clear timeout, if any
246 this.widgetFocused();
247 this.__blur_timeout = setTimeout(function () {
248 self.trigger('blurred');
252 do_load_state: function(state, warm) {
253 if (state.id && this.datarecord.id != state.id) {
254 if (this.dataset.get_id_index(state.id) === null) {
255 this.dataset.ids.push(state.id);
257 this.dataset.select_id(state.id);
263 * @param {Object} [options]
264 * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view".
265 * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record
266 * @return {$.Deferred}
268 do_show: function (options) {
270 options = options || {};
272 this.sidebar.$el.show();
275 this.$buttons.show();
277 this.$el.show().css({
279 filter: 'alpha(opacity = 0)'
281 this.$el.add(this.$buttons).removeClass('oe_form_dirty');
283 var shown = this.has_been_loaded;
284 if (options.reload !== false) {
285 shown = shown.then(function() {
286 if (self.dataset.index === null) {
287 // null index means we should start a new record
288 return self.on_button_new();
290 var fields = _.keys(self.fields_view.fields);
291 fields.push('display_name');
292 return self.dataset.read_index(fields, {
293 context: { 'bin_size': true, 'future_display_name' : true }
294 }).then(function(r) {
295 self.trigger('load_record', r);
299 return shown.then(function() {
300 self._actualize_mode(options.mode || self.options.initial_mode);
303 filter: 'alpha(opacity = 100)'
305 instance.web.bus.trigger('form_view_shown', self);
308 do_hide: function () {
310 this.sidebar.$el.hide();
313 this.$buttons.hide();
320 load_record: function(record) {
321 var self = this, set_values = [];
323 this.set({ 'title' : undefined });
324 this.do_warn(_t("Form"), _t("The record could not be found in the database."), true);
325 return $.Deferred().reject();
327 this.datarecord = record;
328 this._actualize_mode();
329 this.set({ 'title' : record.id ? record.display_name : _t("New") });
331 _(this.fields).each(function (field, f) {
332 field._dirty_flag = false;
333 field._inhibit_on_change_flag = true;
334 var result = field.set_value(self.datarecord[f] || false);
335 field._inhibit_on_change_flag = false;
336 set_values.push(result);
338 return $.when.apply(null, set_values).then(function() {
341 self.do_onchange(null);
343 self.on_form_changed();
344 self.rendering_engine.init_fields();
345 self.is_initialized.resolve();
346 self.do_update_pager(record.id === null || record.id === undefined);
348 self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
351 self.do_push_state({id:record.id});
353 self.do_push_state({});
355 self.$el.add(self.$buttons).removeClass('oe_form_dirty');
360 * Loads and sets up the default values for the model as the current
363 * @return {$.Deferred}
365 load_defaults: function () {
367 var keys = _.keys(this.fields_view.fields);
369 return this.dataset.default_get(keys).then(function(r) {
370 self.trigger('load_record', r);
373 return self.trigger('load_record', {});
375 on_form_changed: function() {
376 this.trigger("view_content_has_changed");
378 do_notify_change: function() {
379 this.$el.add(this.$buttons).addClass('oe_form_dirty');
381 execute_pager_action: function(action) {
382 if (this.can_be_discarded()) {
385 this.dataset.index = 0;
388 this.dataset.previous();
394 this.dataset.index = this.dataset.ids.length - 1;
397 var def = this.reload();
398 this.trigger('pager_action_executed');
403 init_pager: function() {
406 this.$pager.remove();
407 if (this.get("actual_mode") === "create")
409 this.$pager = $(QWeb.render("FormView.pager", {'widget':self}));
410 if (this.options.$pager) {
411 this.$pager.appendTo(this.options.$pager);
413 this.$el.find('.oe_form_pager').replaceWith(this.$pager);
415 this.$pager.on('click','a[data-pager-action]',function() {
417 if ($el.attr("disabled"))
419 var action = $el.data('pager-action');
420 var def = $.when(self.execute_pager_action(action));
421 $el.attr("disabled");
422 def.always(function() {
423 $el.removeAttr("disabled");
426 this.do_update_pager();
428 do_update_pager: function(hide_index) {
429 this.$pager.toggle(this.dataset.ids.length > 1);
431 $(".oe_form_pager_state", this.$pager).html("");
433 $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
437 _build_onchange_specs: function() {
439 var find = function(field_name, root) {
441 while (fields.length) {
442 var node = fields.pop();
446 if (node.tag === 'field' && node.attrs.name === field_name) {
447 return node.attrs.on_change || "";
449 fields = _.union(fields, node.children);
454 self._onchange_specs = {};
455 _.each(this.fields, function(field, name) {
456 self._onchange_specs[name] = find(name, field.node);
457 _.each(field.field.views, function(view) {
458 _.each(view.fields, function(_, subname) {
459 self._onchange_specs[name + '.' + subname] = find(subname, view.arch);
464 _get_onchange_values: function() {
465 var field_values = this.get_fields_values();
466 if (field_values.id.toString().match(instance.web.BufferedDataSet.virtual_id_regex)) {
467 delete field_values.id;
469 if (this.dataset.parent_view) {
470 // this belongs to a parent view: add parent field if possible
471 var parent_view = this.dataset.parent_view;
472 var child_name = this.dataset.child_name;
473 var parent_name = parent_view.get_field_desc(child_name).relation_field;
475 // consider all fields except the inverse of the parent field
476 var parent_values = parent_view.get_fields_values();
477 delete parent_values[child_name];
478 field_values[parent_name] = parent_values;
484 do_onchange: function(widget) {
486 var onchange_specs = self._onchange_specs;
488 var def = $.when({});
489 var change_spec = widget ? onchange_specs[widget.name] : null;
490 if (!widget || (!_.isEmpty(change_spec) && change_spec !== "0")) {
492 trigger_field_name = widget ? widget.name : false,
493 values = self._get_onchange_values(),
494 context = new instance.web.CompoundContext(self.dataset.get_context());
496 if (widget && widget.build_context()) {
497 context.add(widget.build_context());
499 if (self.dataset.parent_view) {
500 var parent_name = self.dataset.parent_view.get_field_desc(self.dataset.child_name).relation_field;
501 context.add({field_parent: parent_name});
504 if (self.datarecord.id && !instance.web.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) {
505 // In case of a o2m virtual id, we should pass an empty ids list
506 ids.push(self.datarecord.id);
508 def = self.alive(new instance.web.Model(self.dataset.model).call(
509 "onchange", [ids, values, trigger_field_name, onchange_specs, context]));
511 var onchange_def = def.then(function(response) {
512 if (widget && widget.field['change_default']) {
513 var fieldname = widget.name;
515 if (response.value && (fieldname in response.value)) {
516 // Use value from onchange if onchange executed
517 value_ = response.value[fieldname];
519 // otherwise get form value for field
520 value_ = self.fields[fieldname].get_value();
522 var condition = fieldname + '=' + value_;
525 return self.alive(new instance.web.Model('ir.values').call(
526 'get_defaults', [self.model, condition]
527 )).then(function (results) {
528 if (!results.length) {
531 if (!response.value) {
534 for(var i=0; i<results.length; ++i) {
535 // [whatever, key, value]
536 var triplet = results[i];
537 response.value[triplet[1]] = triplet[2];
544 }).then(function(response) {
545 return self.on_processed_onchange(response);
547 this.onchanges_defs.push(onchange_def);
551 instance.webclient.crashmanager.show_message(e);
552 return $.Deferred().reject();
555 on_processed_onchange: function(result) {
557 var fields = this.fields;
558 _(result.domain).each(function (domain, fieldname) {
559 var field = fields[fieldname];
560 if (!field) { return; }
561 field.node.attrs.domain = domain;
564 if (!_.isEmpty(result.value)) {
565 this._internal_set_values(result.value);
567 // FIXME XXX a list of warnings?
568 if (!_.isEmpty(result.warning)) {
569 new instance.web.Dialog(this, {
571 title:result.warning.title,
573 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
575 }, QWeb.render("CrashManager.warning", result.warning)).open();
578 return $.Deferred().resolve();
581 instance.webclient.crashmanager.show_message(e);
582 return $.Deferred().reject();
585 _process_operations: function() {
587 return this.mutating_mutex.exec(function() {
589 var start = $.Deferred();
591 start = _.reduce(self.onchanges_defs, function(memo, d){
592 return memo.then(function(){
599 _.each(self.fields, function(field) {
600 defs.push(field.commit_value());
602 var args = _.toArray(arguments);
603 return $.when.apply($, defs).then(function() {
604 var save_obj = self.save_list.pop();
606 return self._process_save(save_obj).then(function() {
607 save_obj.ret = _.toArray(arguments);
610 save_obj.error = true;
615 self.save_list.pop();
622 _internal_set_values: function(values) {
623 for (var f in values) {
624 if (!values.hasOwnProperty(f)) { continue; }
625 var field = this.fields[f];
626 // If field is not defined in the view, just ignore it
628 var value_ = values[f];
629 if (field.get_value() != value_) {
630 field._inhibit_on_change_flag = true;
631 field.set_value(value_);
632 field._inhibit_on_change_flag = false;
633 field._dirty_flag = true;
637 this.on_form_changed();
639 set_values: function(values) {
641 return this.mutating_mutex.exec(function() {
642 self._internal_set_values(values);
646 * Ask the view to switch to view mode if possible. The view may not do it
647 * if the current record is not yet saved. It will then stay in create mode.
649 to_view_mode: function() {
650 this._actualize_mode("view");
653 * Ask the view to switch to edit mode if possible. The view may not do it
654 * if the current record is not yet saved. It will then stay in create mode.
656 to_edit_mode: function() {
657 this.onchanges_defs = [];
658 this._actualize_mode("edit");
661 * Ask the view to switch to a precise mode if possible. The view is free to
662 * not respect this command if the state of the dataset is not compatible with
663 * the new mode. For example, it is not possible to switch to edit mode if
664 * the current record is not yet saved in database.
666 * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
667 * undefined the view will test the actual mode to check if it is still consistent
668 * with the dataset state.
670 _actualize_mode: function(switch_to) {
671 var mode = switch_to || this.get("actual_mode");
672 if (! this.datarecord.id) {
674 } else if (mode === "create") {
677 this.render_value_defs = [];
678 this.set({actual_mode: mode});
680 check_actual_mode: function(source, options) {
682 if(this.get("actual_mode") === "view") {
683 self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
684 self.$buttons.find('.oe_form_buttons_edit').hide();
685 self.$buttons.find('.oe_form_buttons_view').show();
686 self.$sidebar.show();
688 self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
689 self.$buttons.find('.oe_form_buttons_edit').show();
690 self.$buttons.find('.oe_form_buttons_view').hide();
691 self.$sidebar.hide();
695 autofocus: function() {
696 if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
697 var fields_order = this.fields_order.slice(0);
698 if (this.default_focus_field) {
699 fields_order.unshift(this.default_focus_field.name);
701 for (var i = 0; i < fields_order.length; i += 1) {
702 var field = this.fields[fields_order[i]];
703 if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
704 if (field.focus() !== false) {
711 on_button_save: function(e) {
713 $(e.target).attr("disabled", true);
714 return this.save().done(function(result) {
715 self.trigger("save", result);
716 self.reload().then(function() {
718 var menu = instance.webclient.menu;
720 menu.do_reload_needaction();
722 instance.web.bus.trigger('form_view_saved', self);
724 }).always(function(){
725 $(e.target).attr("disabled", false);
728 on_button_cancel: function(event) {
730 if (this.can_be_discarded()) {
731 if (this.get('actual_mode') === 'create') {
732 this.trigger('history_back');
735 $.when.apply(null, this.render_value_defs).then(function(){
736 self.trigger('load_record', self.datarecord);
740 this.trigger('on_button_cancel');
743 on_button_new: function() {
746 return $.when(this.has_been_loaded).then(function() {
747 if (self.can_be_discarded()) {
748 return self.load_defaults();
752 on_button_edit: function() {
753 return this.to_edit_mode();
755 on_button_create: function() {
756 this.dataset.index = null;
759 on_button_duplicate: function() {
761 return this.has_been_loaded.then(function() {
762 return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
763 self.record_created(new_id);
768 on_button_delete: function() {
770 var def = $.Deferred();
771 this.has_been_loaded.done(function() {
772 if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
773 self.dataset.unlink([self.datarecord.id]).done(function() {
774 if (self.dataset.size()) {
775 self.execute_pager_action('next');
777 self.do_action('history_back');
782 $.async_when().done(function () {
787 return def.promise();
789 can_be_discarded: function() {
790 if (this.$el.is('.oe_form_dirty')) {
791 if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
794 this.$el.removeClass('oe_form_dirty');
799 * Triggers saving the form's record. Chooses between creating a new
800 * record or saving an existing one depending on whether the record
801 * already has an id property.
803 * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
804 * record, should that record be inserted at the start of the dataset (by
805 * default, records are added at the end)
807 save: function(prepend_on_create) {
809 var save_obj = {prepend_on_create: prepend_on_create, ret: null};
810 this.save_list.push(save_obj);
811 return self._process_operations().then(function() {
813 return $.Deferred().reject();
814 return $.when.apply($, save_obj.ret);
815 }).done(function(result) {
816 self.$el.removeClass('oe_form_dirty');
819 _process_save: function(save_obj) {
821 var prepend_on_create = save_obj.prepend_on_create;
823 var form_invalid = false,
825 first_invalid_field = null,
826 readonly_values = {};
827 for (var f in self.fields) {
828 if (!self.fields.hasOwnProperty(f)) { continue; }
832 if (!first_invalid_field) {
833 first_invalid_field = f;
835 } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
836 // Special case 'id' field, do not save this field
837 // on 'create' : save all non readonly fields
838 // on 'edit' : save non readonly modified fields
839 if (!f.get("readonly")) {
840 values[f.name] = f.get_value();
842 readonly_values[f.name] = f.get_value();
847 self.set({'display_invalid_fields': true});
848 first_invalid_field.focus();
850 return $.Deferred().reject();
852 self.set({'display_invalid_fields': false});
854 if (!self.datarecord.id) {
856 save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
857 return self.record_created(r, prepend_on_create);
859 } else if (_.isEmpty(values)) {
860 // Not dirty, noop save
861 save_deferral = $.Deferred().resolve({}).promise();
864 save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
865 return self.record_saved(r);
868 return save_deferral;
872 return $.Deferred().reject();
875 on_invalid: function() {
876 var warnings = _(this.fields).chain()
877 .filter(function (f) { return !f.is_valid(); })
879 return _.str.sprintf('<li>%s</li>',
882 warnings.unshift('<ul>');
883 warnings.push('</ul>');
884 this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
887 * Reload the form after saving
889 * @param {Object} r result of the write function.
891 record_saved: function(r) {
892 this.trigger('record_saved', r);
894 // should not happen in the server, but may happen for internal purpose
895 return $.Deferred().reject();
900 * Updates the form' dataset to contain the new record:
902 * * Adds the newly created record to the current dataset (at the end by
904 * * Selects that record (sets the dataset's index to point to the new
906 * * Updates the pager and sidebar displays
909 * @param {Boolean} [prepend_on_create=false] adds the newly created record
910 * at the beginning of the dataset instead of the end
912 record_created: function(r, prepend_on_create) {
915 // should not happen in the server, but may happen for internal purpose
916 this.trigger('record_created', r);
917 return $.Deferred().reject();
919 this.datarecord.id = r;
920 if (!prepend_on_create) {
921 this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
922 this.dataset.index = this.dataset.ids.length - 1;
924 this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
925 this.dataset.index = 0;
927 this.do_update_pager();
929 this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
931 //openerp.log("The record has been created with id #" + this.datarecord.id);
932 return $.when(this.reload()).then(function () {
933 self.trigger('record_created', r);
934 return _.extend(r, {created: true});
938 on_action: function (action) {
939 console.debug('Executing action', action);
943 return this.reload_mutex.exec(function() {
944 if (self.dataset.index === null || self.dataset.index === undefined) {
945 self.trigger("previous_view");
946 return $.Deferred().reject().promise();
948 if (self.dataset.index < 0) {
949 return $.when(self.on_button_new());
951 var fields = _.keys(self.fields_view.fields);
952 fields.push('display_name');
953 return self.dataset.read_index(fields,
957 'future_display_name': true
959 check_access_rule: true
960 }).then(function(r) {
961 self.trigger('load_record', r);
963 self.do_action('history_back');
968 get_widgets: function() {
969 return _.filter(this.getChildren(), function(obj) {
970 return obj instanceof instance.web.form.FormWidget;
973 get_fields_values: function() {
975 var ids = this.get_selected_ids();
976 values["id"] = ids.length > 0 ? ids[0] : false;
977 _.each(this.fields, function(value_, key) {
978 values[key] = value_.get_value();
982 get_selected_ids: function() {
983 var id = this.dataset.ids[this.dataset.index];
984 return id ? [id] : [];
986 recursive_save: function() {
988 return $.when(this.save()).then(function(res) {
989 if (self.dataset.parent_view)
990 return self.dataset.parent_view.recursive_save();
993 recursive_reload: function() {
996 if (self.dataset.parent_view)
997 pre = self.dataset.parent_view.recursive_reload();
998 return pre.then(function() {
999 return self.reload();
1002 is_dirty: function() {
1003 return _.any(this.fields, function (value_) {
1004 return value_._dirty_flag;
1007 is_interactible_record: function() {
1008 var id = this.datarecord.id;
1010 if (this.options.not_interactible_on_create)
1012 } else if (typeof(id) === "string") {
1013 if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
1018 sidebar_eval_context: function () {
1019 return $.when(this.build_eval_context());
1021 open_defaults_dialog: function () {
1023 var display = function (field, value) {
1024 if (!value) { return value; }
1025 if (field instanceof instance.web.form.FieldSelection) {
1026 return _(field.get('values')).find(function (option) {
1027 return option[0] === value;
1029 } else if (field instanceof instance.web.form.FieldMany2One) {
1030 return field.get_displayed();
1034 var fields = _.chain(this.fields)
1035 .map(function (field) {
1036 var value = field.get_value();
1037 // ignore fields which are empty, invisible, readonly, o2m
1040 || field.get('invisible')
1041 || field.get("readonly")
1042 || field.field.type === 'one2many'
1043 || field.field.type === 'many2many'
1044 || field.field.type === 'binary'
1045 || field.password) {
1051 string: field.string,
1053 displayed: display(field, value),
1057 .sortBy(function (field) { return field.string; })
1059 var conditions = _.chain(self.fields)
1060 .filter(function (field) { return field.field.change_default; })
1061 .map(function (field) {
1062 var value = field.get_value();
1065 string: field.string,
1067 displayed: display(field, value),
1071 var d = new instance.web.Dialog(this, {
1072 title: _t("Set Default"),
1075 conditions: conditions
1078 {text: _t("Close"), click: function () { d.close(); }},
1079 {text: _t("Save default"), click: function () {
1080 var $defaults = d.$el.find('#formview_default_fields');
1081 var field_to_set = $defaults.val();
1082 if (!field_to_set) {
1083 $defaults.parent().addClass('oe_form_invalid');
1086 var condition = d.$el.find('#formview_default_conditions').val(),
1087 all_users = d.$el.find('#formview_default_all').is(':checked');
1088 new instance.web.DataSet(self, 'ir.values').call(
1092 self.fields[field_to_set].get_value(),
1096 ]).done(function () { d.close(); });
1100 d.template = 'FormView.set_default';
1103 register_field: function(field, name) {
1104 this.fields[name] = field;
1105 this.fields_order.push(name);
1106 if (JSON.parse(field.node.attrs.default_focus || "0")) {
1107 this.default_focus_field = field;
1110 field.on('focused', null, this.proxy('widgetFocused'))
1111 .on('blurred', null, this.proxy('widgetBlurred'));
1112 if (this.get_field_desc(name).translate) {
1113 this.translatable_fields.push(field);
1115 field.on('changed_value', this, function() {
1116 if (field.is_syntax_valid()) {
1117 this.trigger('field_changed:' + name);
1119 if (field._inhibit_on_change_flag) {
1122 field._dirty_flag = true;
1123 if (field.is_syntax_valid()) {
1124 this.do_onchange(field);
1125 this.on_form_changed(true);
1126 this.do_notify_change();
1130 get_field_desc: function(field_name) {
1131 return this.fields_view.fields[field_name];
1133 get_field_value: function(field_name) {
1134 return this.fields[field_name].get_value();
1136 compute_domain: function(expression) {
1137 return instance.web.form.compute_domain(expression, this.fields);
1139 _build_view_fields_values: function() {
1140 var a_dataset = this.dataset;
1141 var fields_values = this.get_fields_values();
1142 var active_id = a_dataset.ids[a_dataset.index];
1143 _.extend(fields_values, {
1144 active_id: active_id || false,
1145 active_ids: active_id ? [active_id] : [],
1146 active_model: a_dataset.model,
1149 if (a_dataset.parent_view) {
1150 fields_values.parent = a_dataset.parent_view.get_fields_values();
1152 return fields_values;
1154 build_eval_context: function() {
1155 var a_dataset = this.dataset;
1156 return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
1161 * Interface to be implemented by rendering engines for the form view.
1163 instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
1164 set_fields_view: function(fields_view) {},
1165 set_fields_registry: function(fields_registry) {},
1166 render_to: function($el) {},
1170 * Default rendering engine for the form view.
1172 * It is necessary to set the view using set_view() before usage.
1174 instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
1175 init: function(view) {
1178 set_fields_view: function(fvg) {
1180 this.version = parseFloat(this.fvg.arch.attrs.version);
1181 if (isNaN(this.version)) {
1185 set_tags_registry: function(tags_registry) {
1186 this.tags_registry = tags_registry;
1188 set_fields_registry: function(fields_registry) {
1189 this.fields_registry = fields_registry;
1191 set_widgets_registry: function(widgets_registry) {
1192 this.widgets_registry = widgets_registry;
1194 // Backward compatibility tools, current default version: v7
1195 process_version: function() {
1196 if (this.version < 7.0) {
1197 this.$form.find('form:first').wrapInner('<group col="4"/>');
1198 this.$form.find('page').each(function() {
1199 if (!$(this).parents('field').length) {
1200 $(this).wrapInner('<group col="4"/>');
1205 get_arch_fragment: function() {
1206 var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
1207 // IE won't allow custom button@type and will revert it to spec default : 'submit'
1208 $('button', doc).each(function() {
1209 $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
1211 // IE's html parser is also a css parser. How convenient...
1212 $('board', doc).each(function() {
1213 $(this).attr('layout', $(this).attr('style'));
1215 return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
1217 render_to: function($target) {
1219 this.$target = $target;
1221 this.$form = this.get_arch_fragment();
1223 this.process_version();
1225 this.fields_to_init = [];
1226 this.tags_to_init = [];
1227 this.widgets_to_init = [];
1229 this.process(this.$form);
1231 this.$form.appendTo(this.$target);
1233 this.to_replace = [];
1235 _.each(this.fields_to_init, function($elem) {
1236 var name = $elem.attr("name");
1237 if (!self.fvg.fields[name]) {
1238 throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
1240 var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
1242 throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
1244 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1245 var $label = self.labels[$elem.attr("name")];
1247 w.set_input_id($label.attr("for"));
1249 self.alter_field(w);
1250 self.view.register_field(w, $elem.attr("name"));
1251 self.to_replace.push([w, $elem]);
1253 _.each(this.tags_to_init, function($elem) {
1254 var tag_name = $elem[0].tagName.toLowerCase();
1255 var obj = self.tags_registry.get_object(tag_name);
1256 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1257 self.to_replace.push([w, $elem]);
1259 _.each(this.widgets_to_init, function($elem) {
1260 var widget_type = $elem.attr("type");
1261 var obj = self.widgets_registry.get_object(widget_type);
1262 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1263 self.to_replace.push([w, $elem]);
1266 init_fields: function() {
1268 _.each(this.to_replace, function(el) {
1269 defs.push(el[0].replace(el[1]));
1270 if (el[1].children().length) {
1271 el[0].$el.append(el[1].children());
1274 this.to_replace = [];
1275 return $.when.apply($, defs);
1277 render_element: function(template /* dictionaries */) {
1278 var dicts = [].slice.call(arguments).slice(1);
1279 var dict = _.extend.apply(_, dicts);
1280 dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
1281 return $(QWeb.render(template, dict));
1283 alter_field: function(field) {
1285 toggle_layout_debugging: function() {
1286 if (!this.$target.has('.oe_layout_debug_cell:first').length) {
1287 this.$target.find('[title]').removeAttr('title');
1288 this.$target.find('.oe_form_group_cell').each(function() {
1289 var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
1290 $(this).attr('title', text);
1293 this.$target.toggleClass('oe_layout_debugging');
1295 process: function($tag) {
1297 var tagname = $tag[0].nodeName.toLowerCase();
1298 if (this.tags_registry.contains(tagname)) {
1299 this.tags_to_init.push($tag);
1300 return (tagname === 'button') ? this.process_button($tag) : $tag;
1302 var fn = self['process_' + tagname];
1304 var args = [].slice.call(arguments);
1306 return fn.apply(self, args);
1308 // generic tag handling, just process children
1309 $tag.children().each(function() {
1310 self.process($(this));
1312 self.handle_common_properties($tag, $tag);
1313 $tag.removeAttr("modifiers");
1317 process_button: function ($button) {
1319 $button.children().each(function() {
1320 self.process($(this));
1324 process_widget: function($widget) {
1325 this.widgets_to_init.push($widget);
1328 process_sheet: function($sheet) {
1329 var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
1330 this.handle_common_properties($new_sheet, $sheet);
1331 var $dst = $new_sheet.find('.oe_form_sheet');
1332 $sheet.contents().appendTo($dst);
1333 $sheet.before($new_sheet).remove();
1334 this.process($new_sheet);
1336 process_form: function($form) {
1337 if ($form.find('> sheet').length === 0) {
1338 $form.addClass('oe_form_nosheet');
1340 var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
1341 this.handle_common_properties($new_form, $form);
1342 $form.contents().appendTo($new_form);
1343 if ($form[0] === this.$form[0]) {
1344 // If root element, replace it
1345 this.$form = $new_form;
1347 $form.before($new_form).remove();
1349 this.process($new_form);
1352 * Used by direct <field> children of a <group> tag only
1353 * This method will add the implicit <label...> for every field
1356 preprocess_field: function($field) {
1358 var name = $field.attr('name'),
1359 field_colspan = parseInt($field.attr('colspan'), 10),
1360 field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
1362 if ($field.attr('nolabel') === '1')
1364 $field.attr('nolabel', '1');
1366 this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
1367 $(el).parents().each(function(unused, tag) {
1368 var name = tag.tagName.toLowerCase();
1369 if (name === "field" || name in self.tags_registry.map)
1376 var $label = $('<label/>').attr({
1378 "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
1379 "string": $field.attr('string'),
1380 "help": $field.attr('help'),
1381 "class": $field.attr('class'),
1383 $label.insertBefore($field);
1384 if (field_colspan > 1) {
1385 $field.attr('colspan', field_colspan - 1);
1389 process_field: function($field) {
1390 if ($field.parent().is('group')) {
1391 // No implicit labels for normal fields, only for <group> direct children
1392 var $label = this.preprocess_field($field);
1394 this.process($label);
1397 this.fields_to_init.push($field);
1400 process_group: function($group) {
1402 $group.children('field').each(function() {
1403 self.preprocess_field($(this));
1405 var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
1407 if ($new_group.first().is('table.oe_form_group')) {
1408 $table = $new_group;
1409 } else if ($new_group.filter('table.oe_form_group').length) {
1410 $table = $new_group.filter('table.oe_form_group').first();
1412 $table = $new_group.find('table.oe_form_group').first();
1416 cols = parseInt($group.attr('col') || 2, 10),
1420 $group.children().each(function(a,b,c) {
1421 var $child = $(this);
1422 var colspan = parseInt($child.attr('colspan') || 1, 10);
1423 var tagName = $child[0].tagName.toLowerCase();
1424 var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
1425 var newline = tagName === 'newline';
1427 // Note FME: those classes are used in layout debug mode
1428 if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
1429 $tr.addClass('oe_form_group_row_incomplete');
1431 $tr.addClass('oe_form_group_row_newline');
1438 if (!$tr || row_cols < colspan) {
1439 $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
1441 } else if (tagName==='group') {
1442 // When <group> <group/><group/> </group>, we need a spacing between the two groups
1443 $td.addClass('oe_group_right');
1445 row_cols -= colspan;
1447 // invisibility transfer
1448 var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
1449 var invisible = field_modifiers.invisible;
1450 self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
1452 $tr.append($td.append($child));
1453 children.push($child[0]);
1455 if (row_cols && $td) {
1456 $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
1458 $group.before($new_group).remove();
1460 $table.find('> tbody > tr').each(function() {
1461 var to_compute = [],
1464 $(this).children().each(function() {
1466 $child = $td.children(':first');
1467 if ($child.attr('cell-class')) {
1468 $td.addClass($child.attr('cell-class'));
1470 switch ($child[0].tagName.toLowerCase()) {
1474 if ($child.attr('for')) {
1475 $td.attr('width', '1%').addClass('oe_form_group_cell_label');
1476 row_cols-= $td.attr('colspan') || 1;
1481 var width = _.str.trim($child.attr('width') || ''),
1482 iwidth = parseInt(width, 10);
1484 if (width.substr(-1) === '%') {
1486 width = iwidth + '%';
1489 $td.css('min-width', width + 'px');
1491 $td.attr('width', width);
1492 $child.removeAttr('width');
1493 row_cols-= $td.attr('colspan') || 1;
1495 to_compute.push($td);
1501 var unit = Math.floor(total / row_cols);
1502 if (!$(this).is('.oe_form_group_row_incomplete')) {
1503 _.each(to_compute, function($td, i) {
1504 var width = parseInt($td.attr('colspan'), 10) * unit;
1505 $td.attr('width', width + '%');
1511 _.each(children, function(el) {
1512 self.process($(el));
1514 this.handle_common_properties($new_group, $group);
1517 process_notebook: function($notebook) {
1520 $notebook.find('> page').each(function() {
1521 var $page = $(this);
1522 var page_attrs = $page.getAttributes();
1523 page_attrs.id = _.uniqueId('notebook_page_');
1524 var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
1525 $page.contents().appendTo($new_page);
1526 $page.before($new_page).remove();
1527 var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
1528 page_attrs.__page = $new_page;
1529 page_attrs.__ic = ic;
1530 pages.push(page_attrs);
1532 $new_page.children().each(function() {
1533 self.process($(this));
1536 var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
1537 $notebook.contents().appendTo($new_notebook);
1538 $notebook.before($new_notebook).remove();
1539 self.process($($new_notebook.children()[0]));
1540 //tabs and invisibility handling
1541 $new_notebook.tabs();
1542 _.each(pages, function(page, i) {
1545 page.__ic.on("change:effective_invisible", null, function() {
1546 if (!page.__ic.get('effective_invisible') && page.autofocus) {
1547 $new_notebook.tabs('select', i);
1550 var current = $new_notebook.tabs("option", "selected");
1551 if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
1553 var first_visible = _.find(_.range(pages.length), function(i2) {
1554 return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
1556 if (first_visible !== undefined) {
1557 $new_notebook.tabs('select', first_visible);
1562 this.handle_common_properties($new_notebook, $notebook);
1563 return $new_notebook;
1565 process_separator: function($separator) {
1566 var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
1567 $separator.before($new_separator).remove();
1568 this.handle_common_properties($new_separator, $separator);
1569 return $new_separator;
1571 process_label: function($label) {
1572 var name = $label.attr("for"),
1573 field_orm = this.fvg.fields[name];
1575 string: $label.attr('string') || (field_orm || {}).string || '',
1576 help: $label.attr('help') || (field_orm || {}).help || '',
1577 _for: name ? _.uniqueId('oe-field-input-') : undefined,
1579 var align = parseFloat(dict.align);
1580 if (isNaN(align) || align === 1) {
1582 } else if (align === 0) {
1588 var $new_label = this.render_element('FormRenderingLabel', dict);
1589 $label.before($new_label).remove();
1590 this.handle_common_properties($new_label, $label);
1592 this.labels[name] = $new_label;
1596 handle_common_properties: function($new_element, $node) {
1597 var str_modifiers = $node.attr("modifiers") || "{}";
1598 var modifiers = JSON.parse(str_modifiers);
1600 if (modifiers.invisible !== undefined)
1601 ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
1602 $new_element.addClass($node.attr("class") || "");
1603 $new_element.attr('style', $node.attr('style'));
1604 return {invisibility_changer: ic,};
1611 If you read this documentation, it probably means that you were asked to use a form view widget outside of
1612 a form view. Before going further, you must understand that those fields were never really created for
1613 that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
1614 you to hack the system with more style.
1616 instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
1617 init: function(parent, eval_context) {
1618 this._super(parent);
1619 this.field_descs = {};
1620 this.eval_context = eval_context || {};
1622 display_invalid_fields: false,
1623 actual_mode: 'create',
1626 get_field_desc: function(field_name) {
1627 if (this.field_descs[field_name] === undefined) {
1628 this.field_descs[field_name] = {
1632 return this.field_descs[field_name];
1634 extend_field_desc: function(fields) {
1636 _.each(fields, function(v, k) {
1637 _.extend(self.get_field_desc(k), v);
1640 get_field_value: function(field_name) {
1643 set_values: function(values) {
1646 compute_domain: function(expression) {
1647 return instance.web.form.compute_domain(expression, {});
1649 build_eval_context: function() {
1650 return new instance.web.CompoundContext(this.eval_context);
1654 instance.web.form.compute_domain = function(expr, fields) {
1655 if (! (expr instanceof Array))
1658 for (var i = expr.length - 1; i >= 0; i--) {
1660 if (ex.length == 1) {
1661 var top = stack.pop();
1664 stack.push(stack.pop() || top);
1667 stack.push(stack.pop() && top);
1673 throw new Error(_.str.sprintf(
1674 _t("Unknown operator %s in domain %s"),
1675 ex, JSON.stringify(expr)));
1679 var field = fields[ex[0]];
1681 throw new Error(_.str.sprintf(
1682 _t("Unknown field %s in domain %s"),
1683 ex[0], JSON.stringify(expr)));
1685 var field_value = field.get_value ? field.get_value() : field.value;
1689 switch (op.toLowerCase()) {
1692 stack.push(_.isEqual(field_value, val));
1696 stack.push(!_.isEqual(field_value, val));
1699 stack.push(field_value < val);
1702 stack.push(field_value > val);
1705 stack.push(field_value <= val);
1708 stack.push(field_value >= val);
1711 if (!_.isArray(val)) val = [val];
1712 stack.push(_(val).contains(field_value));
1715 if (!_.isArray(val)) val = [val];
1716 stack.push(!_(val).contains(field_value));
1720 _t("Unsupported operator %s in domain %s"),
1721 op, JSON.stringify(expr));
1724 return _.all(stack, _.identity);
1727 instance.web.form.is_bin_size = function(v) {
1728 return (/^\d+(\.\d*)? \w+$/).test(v);
1732 * Must be applied over an class already possessing the PropertiesMixin.
1734 * Apply the result of the "invisible" domain to this.$el.
1736 instance.web.form.InvisibilityChangerMixin = {
1737 init: function(field_manager, invisible_domain) {
1739 this._ic_field_manager = field_manager;
1740 this._ic_invisible_modifier = invisible_domain;
1741 this._ic_field_manager.on("view_content_has_changed", this, function() {
1742 var result = self._ic_invisible_modifier === undefined ? false :
1743 self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
1744 self.set({"invisible": result});
1746 this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
1747 var check = function() {
1748 if (self.get("invisible") || self.get('force_invisible')) {
1749 self.set({"effective_invisible": true});
1751 self.set({"effective_invisible": false});
1754 this.on('change:invisible', this, check);
1755 this.on('change:force_invisible', this, check);
1759 this.on("change:effective_invisible", this, this._check_visibility);
1760 this._check_visibility();
1762 _check_visibility: function() {
1763 this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
1767 instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
1768 init: function(parent, field_manager, invisible_domain, $el) {
1769 this.setParent(parent);
1770 instance.web.PropertiesMixin.init.call(this);
1771 instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
1778 Base class for all fields, custom widgets and buttons to be displayed in the form view.
1781 - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
1782 the values of the "readonly" property and the "mode" property on the field manager.
1784 instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
1786 * @constructs instance.web.form.FormWidget
1787 * @extends instance.web.Widget
1789 * @param field_manager
1792 init: function(field_manager, node) {
1793 this._super(field_manager);
1794 this.field_manager = field_manager;
1795 if (this.field_manager instanceof instance.web.FormView)
1796 this.view = this.field_manager;
1798 this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
1799 instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
1801 this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
1807 // some events to make the property "effective_readonly" sync automatically with "readonly" and
1808 // "mode" on field_manager
1810 var test_effective_readonly = function() {
1811 self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
1813 this.on("change:readonly", this, test_effective_readonly);
1814 this.field_manager.on("change:actual_mode", this, test_effective_readonly);
1815 test_effective_readonly.call(this);
1817 renderElement: function() {
1818 this.process_modifiers();
1820 this.$el.addClass(this.node.attrs["class"] || "");
1822 destroy: function() {
1823 $.fn.tooltip('destroy');
1824 this._super.apply(this, arguments);
1827 * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
1829 * This method is an utility method that is meant to be called by child classes.
1831 * @param {jQuery} $e jQuery object of elements to bind focus/blur on
1833 setupFocus: function ($e) {
1836 focus: function () { self.trigger('focused'); },
1837 blur: function () { self.trigger('blurred'); }
1840 process_modifiers: function() {
1842 for (var a in this.modifiers) {
1843 if (!this.modifiers.hasOwnProperty(a)) { continue; }
1844 if (!_.include(["invisible"], a)) {
1845 var val = this.field_manager.compute_domain(this.modifiers[a]);
1851 do_attach_tooltip: function(widget, trigger, options) {
1852 widget = widget || this;
1853 trigger = trigger || this.$el;
1854 options = _.extend({
1855 delay: { show: 500, hide: 0 },
1857 var template = widget.template + '.tooltip';
1858 if (!QWeb.has_template(template)) {
1859 template = 'WidgetLabel.tooltip';
1861 return QWeb.render(template, {
1862 debug: instance.session.debug,
1867 //only show tooltip if we are in debug or if we have a help to show, otherwise it will display
1869 if (instance.session.debug || widget.node.attrs.help || (widget.field && widget.field.help)){
1870 $(trigger).tooltip(options);
1874 * Builds a new context usable for operations related to fields by merging
1875 * the fields'context with the action's context.
1877 build_context: function() {
1878 // only use the model's context if there is not context on the node
1879 var v_context = this.node.attrs.context;
1881 v_context = (this.field || {}).context || {};
1884 if (v_context.__ref || true) { //TODO: remove true
1885 var fields_values = this.field_manager.build_eval_context();
1886 v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
1890 build_domain: function() {
1891 var f_domain = this.field.domain || [];
1892 var n_domain = this.node.attrs.domain || null;
1893 // if there is a domain on the node, overrides the model's domain
1894 var final_domain = n_domain !== null ? n_domain : f_domain;
1895 if (!(final_domain instanceof Array) || true) { //TODO: remove true
1896 var fields_values = this.field_manager.build_eval_context();
1897 final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
1899 return final_domain;
1903 instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
1904 template: 'WidgetButton',
1905 init: function(field_manager, node) {
1906 node.attrs.type = node.attrs['data-button-type'];
1907 this.is_stat_button = /\boe_stat_button\b/.test(node.attrs['class']);
1908 this.icon_class = node.attrs.icon && "stat_button_icon fa " + node.attrs.icon + " fa-fw";
1909 this._super(field_manager, node);
1910 this.force_disabled = false;
1911 this.string = (this.node.attrs.string || '').replace(/_/g, '');
1912 if (JSON.parse(this.node.attrs.default_focus || "0")) {
1913 // TODO fme: provide enter key binding to widgets
1914 this.view.default_focus_button = this;
1916 if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
1917 this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
1921 this._super.apply(this, arguments);
1922 this.view.on('view_content_has_changed', this, this.check_disable);
1923 this.check_disable();
1924 this.$el.click(this.on_click);
1925 if (this.node.attrs.help || instance.session.debug) {
1926 this.do_attach_tooltip();
1928 this.setupFocus(this.$el);
1930 on_click: function() {
1932 this.force_disabled = true;
1933 this.check_disable();
1934 this.execute_action().always(function() {
1935 self.force_disabled = false;
1936 self.check_disable();
1939 execute_action: function() {
1941 var exec_action = function() {
1942 if (self.node.attrs.confirm) {
1943 var def = $.Deferred();
1944 var dialog = new instance.web.Dialog(this, {
1945 title: _t('Confirm'),
1947 {text: _t("Cancel"), click: function() {
1948 this.parents('.modal').modal('hide');
1951 {text: _t("Ok"), click: function() {
1953 self.on_confirmed().always(function() {
1954 self2.parents('.modal').modal('hide');
1959 }, $('<div/>').text(self.node.attrs.confirm)).open();
1960 dialog.on("closing", null, function() {def.resolve();});
1961 return def.promise();
1963 return self.on_confirmed();
1966 if (!this.node.attrs.special) {
1967 return this.view.recursive_save().then(exec_action);
1969 return exec_action();
1972 on_confirmed: function() {
1975 var context = this.build_context();
1976 return this.view.do_execute_action(
1977 _.extend({}, this.node.attrs, {context: context}),
1978 this.view.dataset, this.view.datarecord.id, function (reason) {
1979 if (!_.isObject(reason)) {
1980 self.view.recursive_reload();
1984 check_disable: function() {
1985 var disabled = (this.force_disabled || !this.view.is_interactible_record());
1986 this.$el.prop('disabled', disabled);
1987 this.$el.css('color', disabled ? 'grey' : '');
1992 * Interface to be implemented by fields.
1995 * - changed_value: triggered when the value of the field has changed. This can be due
1996 * to a user interaction or a call to set_value().
1999 instance.web.form.FieldInterface = {
2001 * Constructor takes 2 arguments:
2002 * - field_manager: Implements FieldManagerMixin
2003 * - node: the "<field>" node in json form
2005 init: function(field_manager, node) {},
2007 * Called by the form view to indicate the value of the field.
2009 * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
2010 * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
2011 * before the widget is inserted into the DOM.
2013 * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
2014 * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
2015 * also be able to handle any other format commonly used in the _defaults key on the models in the addons
2016 * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
2017 * no information is ever given to know which format is used.
2019 set_value: function(value_) {},
2021 * Get the current value of the widget.
2023 * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
2024 * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
2025 * For example if the field is marked as "required", a call to get_value() can return false.
2027 * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
2028 * return a default value according to the type of field.
2030 * This method is always assumed to perform synchronously, it can not return a promise.
2032 * If there was no user interaction to modify the value of the field, it is always assumed that
2033 * get_value() return the same semantic value than the one passed in the last call to set_value(),
2034 * although the syntax can be different. This can be the case for type of fields that have a different
2035 * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
2037 get_value: function() {},
2039 * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
2042 set_input_id: function(id) {},
2044 * Returns true if is_syntax_valid() returns true and the value is semantically
2045 * valid too according to the semantic restrictions applied to the field.
2047 is_valid: function() {},
2049 * Returns true if the field holds a value which is syntactically correct, ignoring
2050 * the potential semantic restrictions applied to the field.
2052 is_syntax_valid: function() {},
2054 * Must set the focus on the field. Return false if field is not focusable.
2056 focus: function() {},
2058 * Called when the translate button is clicked.
2060 on_translate: function() {},
2062 This method is called by the form view before reading on_change values and before saving. It tells
2063 the field to save its value before reading it using get_value(). Must return a promise.
2065 commit_value: function() {},
2069 * Abstract class for classes implementing FieldInterface.
2072 * - value: useful property to hold the value of the field. By default, set_value() and get_value()
2073 * set and retrieve the value property. Changing the value property also triggers automatically
2074 * a 'changed_value' event that inform the view to trigger on_changes.
2077 instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
2079 * @constructs instance.web.form.AbstractField
2080 * @extends instance.web.form.FormWidget
2082 * @param field_manager
2085 init: function(field_manager, node) {
2087 this._super(field_manager, node);
2088 this.name = this.node.attrs.name;
2089 this.field = this.field_manager.get_field_desc(this.name);
2090 this.widget = this.node.attrs.widget;
2091 this.string = this.node.attrs.string || this.field.string || this.name;
2092 this.options = instance.web.py_eval(this.node.attrs.options || '{}');
2093 this.set({'value': false});
2095 this.on("change:value", this, function() {
2096 this.trigger('changed_value');
2097 this._check_css_flags();
2100 renderElement: function() {
2103 if (this.field.translate && this.view) {
2104 this.$el.addClass('oe_form_field_translatable');
2105 this.$el.find('.oe_field_translate').click(this.on_translate);
2107 this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
2108 this.do_attach_tooltip(this, this.$label[0] || this.$el);
2109 if (instance.session.debug) {
2110 this.$label.off('dblclick').on('dblclick', function() {
2111 console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
2113 console.log("window.w =", window.w);
2116 if (!this.disable_utility_classes) {
2117 this.off("change:required", this, this._set_required);
2118 this.on("change:required", this, this._set_required);
2119 this._set_required();
2121 this._check_visibility();
2122 this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
2123 this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
2124 this._check_css_flags();
2127 var tmp = this._super();
2128 this.on("change:value", this, function() {
2129 if (! this.no_rerender)
2130 this.render_value();
2132 this.render_value();
2135 * Private. Do not use.
2137 _set_required: function() {
2138 this.$el.toggleClass('oe_form_required', this.get("required"));
2140 set_value: function(value_) {
2141 this.set({'value': value_});
2143 get_value: function() {
2144 return this.get('value');
2147 Utility method that all implementations should use to change the
2148 value without triggering a re-rendering.
2150 internal_set_value: function(value_) {
2151 var tmp = this.no_rerender;
2152 this.no_rerender = true;
2153 this.set({'value': value_});
2154 this.no_rerender = tmp;
2157 This method is called each time the value is modified.
2159 render_value: function() {},
2160 is_valid: function() {
2161 return this.is_syntax_valid() && !(this.get('required') && this.is_false());
2163 is_syntax_valid: function() {
2167 * Method useful to implement to ease validity testing. Must return true if the current
2168 * value is similar to false in OpenERP.
2170 is_false: function() {
2171 return this.get('value') === false;
2173 _check_css_flags: function() {
2174 if (this.field.translate) {
2175 this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
2177 if (!this.disable_utility_classes) {
2178 if (this.field_manager.get('display_invalid_fields')) {
2179 this.$el.toggleClass('oe_form_invalid', !this.is_valid());
2186 set_input_id: function(id) {
2187 this.id_for_label = id;
2189 on_translate: function() {
2191 var trans = new instance.web.DataSet(this, 'ir.translation');
2192 return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
2197 set_dimensions: function (height, width) {
2203 commit_value: function() {
2209 * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
2212 instance.web.form.ReinitializeWidgetMixin = {
2214 * Default implementation of, you should not override it, use initialize_field() instead.
2217 this.initialize_field();
2220 initialize_field: function() {
2221 this.on("change:effective_readonly", this, this.reinitialize);
2222 this.initialize_content();
2224 reinitialize: function() {
2225 this.destroy_content();
2226 this.renderElement();
2227 this.initialize_content();
2230 * Called to destroy anything that could have been created previously, called before a
2231 * re-initialization.
2233 destroy_content: function() {},
2235 * Called to initialize the content.
2237 initialize_content: function() {},
2241 * A mixin to apply on any field that has to completely re-render when its readonly state
2244 instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
2245 reinitialize: function() {
2246 instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
2247 var res = this.render_value();
2248 if (this.view && this.view.render_value_defs){
2249 this.view.render_value_defs.push(res);
2255 Some hack to make placeholders work in ie9.
2257 if (!('placeholder' in document.createElement('input'))) {
2258 document.addEventListener("DOMNodeInserted",function(event){
2259 var nodename = event.target.nodeName.toLowerCase();
2260 if ( nodename === "input" || nodename == "textarea" ) {
2261 $(event.target).placeholder();
2266 instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2267 template: 'FieldChar',
2268 widget_class: 'oe_form_field_char',
2270 'change input': 'store_dom_value',
2272 init: function (field_manager, node) {
2273 this._super(field_manager, node);
2274 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2276 initialize_content: function() {
2277 this.setupFocus(this.$('input'));
2279 store_dom_value: function () {
2280 if (!this.get('effective_readonly')
2281 && this.$('input').length
2282 && this.is_syntax_valid()) {
2283 this.internal_set_value(
2285 this.$('input').val()));
2288 commit_value: function () {
2289 this.store_dom_value();
2290 return this._super();
2292 render_value: function() {
2293 var show_value = this.format_value(this.get('value'), '');
2294 if (!this.get("effective_readonly")) {
2295 this.$el.find('input').val(show_value);
2297 if (this.password) {
2298 show_value = new Array(show_value.length + 1).join('*');
2300 this.$(".oe_form_char_content").text(show_value);
2303 is_syntax_valid: function() {
2304 if (!this.get("effective_readonly") && this.$("input").size() > 0) {
2306 this.parse_value(this.$('input').val(), '');
2314 parse_value: function(val, def) {
2315 return instance.web.parse_value(val, this, def);
2317 format_value: function(val, def) {
2318 return instance.web.format_value(val, this, def);
2320 is_false: function() {
2321 return this.get('value') === '' || this._super();
2324 var input = this.$('input:first')[0];
2325 return input ? input.focus() : false;
2327 set_dimensions: function (height, width) {
2328 this._super(height, width);
2329 this.$('input').css({
2336 instance.web.form.KanbanSelection = instance.web.form.FieldChar.extend({
2337 init: function (field_manager, node) {
2338 this._super(field_manager, node);
2340 prepare_dropdown_selection: function() {
2343 var selection = self.field.selection || [];
2344 _.map(selection, function(res) {
2348 'state_name': res[1],
2350 if (res[0] == 'normal') { value['state_class'] = 'oe_kanban_status'; }
2351 else if (res[0] == 'done') { value['state_class'] = 'oe_kanban_status oe_kanban_status_green'; }
2352 else { value['state_class'] = 'oe_kanban_status oe_kanban_status_red'; }
2357 render_value: function() {
2359 this.record_id = this.view.datarecord.id;
2360 this.states = this.prepare_dropdown_selection();;
2361 this.$el.html(QWeb.render("KanbanSelection", {'widget': self}));
2362 this.$el.find('li').on('click', this.set_kanban_selection.bind(this));
2364 /* setting the value: in view mode, perform an asynchronous call and reload
2365 the form view; in edit mode, use set_value to save the new value that will
2366 be written when saving the record. */
2367 set_kanban_selection: function (ev) {
2369 var li = $(ev.target).closest('li');
2371 var value = String(li.data('value'));
2372 if (this.view.get('actual_mode') == 'view') {
2373 var write_values = {}
2374 write_values[self.name] = value;
2375 return this.view.dataset._model.call(
2379 self.view.dataset.get_context()
2380 ]).done(self.reload_record.bind(self));
2383 return this.set_value(value);
2387 reload_record: function() {
2392 instance.web.form.Priority = instance.web.form.FieldChar.extend({
2393 init: function (field_manager, node) {
2394 this._super(field_manager, node);
2396 prepare_priority: function() {
2398 var selection = this.field.selection || [];
2399 var init_value = selection && selection[0][0] || 0;
2400 var data = _.map(selection.slice(1), function(element, index) {
2402 'value': element[0],
2404 'click_value': element[0],
2406 if (index == 0 && self.get('value') == element[0]) {
2407 value['click_value'] = init_value;
2413 render_value: function() {
2415 this.record_id = this.view.datarecord.id;
2416 this.priorities = this.prepare_priority();
2417 this.$el.html(QWeb.render("Priority", {'widget': this}));
2418 this.$el.find('li').on('click', this.set_priority.bind(this));
2420 /* setting the value: in view mode, perform an asynchronous call and reload
2421 the form view; in edit mode, use set_value to save the new value that will
2422 be written when saving the record. */
2423 set_priority: function (ev) {
2425 var li = $(ev.target).closest('li');
2427 var value = String(li.data('value'));
2428 if (this.view.get('actual_mode') == 'view') {
2429 var write_values = {}
2430 write_values[self.name] = value;
2431 return this.view.dataset._model.call(
2435 self.view.dataset.get_context()
2436 ]).done(self.reload_record.bind(self));
2439 return this.set_value(value);
2444 reload_record: function() {
2449 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2450 process_modifiers: function () {
2452 this.set({ readonly: true });
2456 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2457 template: 'FieldEmail',
2458 initialize_content: function() {
2460 var $button = this.$el.find('button');
2461 $button.click(this.on_button_clicked);
2462 this.setupFocus($button);
2464 render_value: function() {
2465 if (!this.get("effective_readonly")) {
2469 .attr('href', 'mailto:' + this.get('value'))
2470 .text(this.get('value') || '');
2473 on_button_clicked: function() {
2474 if (!this.get('value') || !this.is_syntax_valid()) {
2475 this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2477 location.href = 'mailto:' + this.get('value');
2482 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2483 template: 'FieldUrl',
2484 initialize_content: function() {
2486 var $button = this.$el.find('button');
2487 $button.click(this.on_button_clicked);
2488 this.setupFocus($button);
2490 render_value: function() {
2491 if (!this.get("effective_readonly")) {
2494 var tmp = this.get('value');
2495 var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2497 tmp = "http://" + this.get('value');
2499 var text = this.get('value') ? this.node.attrs.text || tmp : '';
2500 this.$el.find('a').attr('href', tmp).text(text);
2503 on_button_clicked: function() {
2504 if (!this.get('value')) {
2505 this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2507 var url = $.trim(this.get('value'));
2508 if(/^www\./i.test(url))
2509 url = 'http://'+url;
2515 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2516 is_field_number: true,
2517 widget_class: 'oe_form_field_float',
2518 init: function (field_manager, node) {
2519 this._super(field_manager, node);
2520 this.internal_set_value(0);
2521 if (this.node.attrs.digits) {
2522 this.digits = this.node.attrs.digits;
2524 this.digits = this.field.digits;
2527 set_value: function(value_) {
2528 if (value_ === false || value_ === undefined) {
2529 // As in GTK client, floats default to 0
2532 this._super.apply(this, [value_]);
2534 focus: function () {
2535 var $input = this.$('input:first');
2536 return $input.length ? $input.select() : false;
2540 instance.web.form.FieldCharDomain = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2541 init: function(field_manager, node) {
2542 this._super.apply(this, arguments);
2546 this._super.apply(this, arguments);
2547 this.on("change:effective_readonly", this, function () {
2548 this.display_field();
2550 this.display_field();
2551 return this._super();
2553 set_value: function(value_) {
2555 this.set('value', value_ || false);
2556 this.display_field();
2558 display_field: function() {
2560 this.$el.html(instance.web.qweb.render("FieldCharDomain", {widget: this}));
2561 if (this.get('value')) {
2562 var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2563 var domain = instance.web.pyeval.eval('domain', this.get('value'));
2564 var ds = new instance.web.DataSetStatic(self, model, self.build_context());
2565 ds.call('search_count', [domain]).then(function (results) {
2566 $('.oe_domain_count', self.$el).text(results + ' records selected');
2567 if (self.get('effective_readonly')) {
2568 $('button span', self.$el).text(' See selection');
2571 $('button span', self.$el).text(' Change selection');
2575 $('.oe_domain_count', this.$el).text('0 record selected');
2576 $('button span', this.$el).text(' Select records');
2578 this.$('.select_records').on('click', self.on_click);
2580 on_click: function(event) {
2581 event.preventDefault();
2583 var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2584 this.pop = new instance.web.form.SelectCreatePopup(this);
2585 this.pop.select_element(
2587 title: this.get('effective_readonly') ? 'Selected records' : 'Select records...',
2588 readonly: this.get('effective_readonly'),
2589 disable_multiple_selection: this.get('effective_readonly'),
2590 no_create: this.get('effective_readonly'),
2591 }, [], this.build_context());
2592 this.pop.on("elements_selected", self, function(element_ids) {
2593 if (this.pop.$('input.oe_list_record_selector').prop('checked')) {
2594 var search_data = this.pop.searchview.build_search_data();
2595 var domain_done = instance.web.pyeval.eval_domains_and_contexts({
2596 domains: search_data.domains,
2597 contexts: search_data.contexts,
2598 group_by_seq: search_data.groupbys || []
2599 }).then(function (results) {
2600 return results.domain;
2604 var domain = [["id", "in", element_ids]];
2605 var domain_done = $.Deferred().resolve(domain);
2607 $.when(domain_done).then(function (domain) {
2608 var domain = self.pop.dataset.domain.concat(domain || []);
2609 self.set_value(domain);
2615 instance.web.DateTimeWidget = instance.web.Widget.extend({
2616 template: "web.datepicker",
2617 type_of_date: "datetime",
2619 'dp.change .oe_datepicker_main': 'change_datetime',
2620 'dp.show .oe_datepicker_main': 'set_datetime_default',
2621 'keypress .oe_datepicker_master': 'change_datetime',
2623 init: function(parent) {
2624 this._super(parent);
2625 this.name = parent.name;
2629 var l10n = _t.database.parameters;
2633 startDate: moment({ y: 1900 }),
2634 endDate: moment().add(200, "y"),
2635 calendarWeeks: true,
2637 time: 'fa fa-clock-o',
2638 date: 'fa fa-calendar',
2639 up: 'fa fa-chevron-up',
2640 down: 'fa fa-chevron-down'
2642 language : moment.locale(),
2643 format : instance.web.normalize_format(l10n.date_format +' '+ l10n.time_format),
2645 this.$input = this.$el.find('input.oe_datepicker_master');
2646 if (this.type_of_date === 'date') {
2647 options['pickTime'] = false;
2648 options['useSeconds'] = false;
2649 options['format'] = instance.web.normalize_format(l10n.date_format);
2651 this.picker = this.$('.oe_datepicker_main').datetimepicker(options);
2652 this.set_readonly(false);
2653 this.set({'value': false});
2655 set_value: function(value_) {
2656 this.set({'value': value_});
2657 this.$input.val(value_ ? this.format_client(value_) : '');
2659 get_value: function() {
2660 return this.get('value');
2662 set_value_from_ui_: function() {
2663 var value_ = this.$input.val() || false;
2664 this.set_value(this.parse_client(value_));
2666 set_readonly: function(readonly) {
2667 this.readonly = readonly;
2668 this.$input.prop('readonly', this.readonly);
2670 is_valid_: function() {
2671 var value_ = this.$input.val();
2672 if (value_ === "") {
2676 this.parse_client(value_);
2683 parse_client: function(v) {
2684 return instance.web.parse_value(v, {"widget": this.type_of_date});
2686 format_client: function(v) {
2687 return instance.web.format_value(v, {"widget": this.type_of_date});
2689 set_datetime_default: function(){
2690 //when opening datetimepicker the date and time by default should be the one from
2691 //the input field if any or the current day otherwise
2692 if (this.type_of_date === 'datetime') {
2693 value = moment().second(0);
2694 if (this.$input.val().length !== 0 && this.is_valid_()){
2695 var value = this.$input.val();
2697 this.$('.oe_datepicker_main').data('DateTimePicker').setValue(value);
2700 change_datetime: function(e) {
2701 if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
2702 this.set_value_from_ui_();
2703 this.trigger("datetime_changed");
2706 commit_value: function () {
2707 this.change_datetime();
2711 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2712 type_of_date: "date"
2715 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2716 template: "FieldDatetime",
2717 build_widget: function() {
2718 return new instance.web.DateTimeWidget(this);
2720 destroy_content: function() {
2721 if (this.datewidget) {
2722 this.datewidget.destroy();
2723 this.datewidget = undefined;
2726 initialize_content: function() {
2727 if (!this.get("effective_readonly")) {
2728 this.datewidget = this.build_widget();
2729 this.datewidget.on('datetime_changed', this, _.bind(function() {
2730 this.internal_set_value(this.datewidget.get_value());
2732 this.datewidget.appendTo(this.$el);
2733 this.setupFocus(this.datewidget.$input);
2736 render_value: function() {
2737 if (!this.get("effective_readonly")) {
2738 this.datewidget.set_value(this.get('value'));
2740 this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2743 is_syntax_valid: function() {
2744 if (!this.get("effective_readonly") && this.datewidget) {
2745 return this.datewidget.is_valid_();
2749 is_false: function() {
2750 return this.get('value') === '' || this._super();
2753 var input = this.datewidget && this.datewidget.$input[0];
2754 return input ? input.focus() : false;
2756 set_dimensions: function (height, width) {
2757 this._super(height, width);
2758 if (!this.get("effective_readonly")) {
2759 this.datewidget.$input.css('height', height);
2764 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2765 template: "FieldDate",
2766 build_widget: function() {
2767 return new instance.web.DateWidget(this);
2771 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2772 template: 'FieldText',
2774 'keyup': function (e) {
2775 if (e.which === $.ui.keyCode.ENTER) {
2776 e.stopPropagation();
2779 'keypress': function (e) {
2780 if (e.which === $.ui.keyCode.ENTER) {
2781 e.stopPropagation();
2784 'change textarea': 'store_dom_value',
2786 initialize_content: function() {
2788 if (! this.get("effective_readonly")) {
2789 this.$textarea = this.$el.find('textarea');
2790 this.auto_sized = false;
2791 this.default_height = this.$textarea.css('height');
2792 if (this.get("effective_readonly")) {
2793 this.$textarea.attr('disabled', 'disabled');
2795 this.setupFocus(this.$textarea);
2797 this.$textarea = undefined;
2800 commit_value: function () {
2801 if (! this.get("effective_readonly") && this.$textarea) {
2802 this.store_dom_value();
2804 return this._super();
2806 store_dom_value: function () {
2807 this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2809 render_value: function() {
2810 if (! this.get("effective_readonly")) {
2811 var show_value = instance.web.format_value(this.get('value'), this, '');
2812 if (show_value === '') {
2813 this.$textarea.css('height', parseInt(this.default_height, 10)+"px");
2815 this.$textarea.val(show_value);
2816 if (! this.auto_sized) {
2817 this.auto_sized = true;
2818 this.$textarea.autosize();
2820 this.$textarea.trigger("autosize");
2823 var txt = this.get("value") || '';
2824 this.$(".oe_form_text_content").text(txt);
2827 is_syntax_valid: function() {
2828 if (!this.get("effective_readonly") && this.$textarea) {
2830 instance.web.parse_value(this.$textarea.val(), this, '');
2838 is_false: function() {
2839 return this.get('value') === '' || this._super();
2841 focus: function($el) {
2842 var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2843 return input ? input.focus() : false;
2845 set_dimensions: function (height, width) {
2846 this._super(height, width);
2847 if (!this.get("effective_readonly") && this.$textarea) {
2848 this.$textarea.css({
2857 * FieldTextHtml Widget
2858 * Intended for FieldText widgets meant to display HTML content. This
2859 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2860 * To find more information about CLEditor configutation: go to
2861 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2863 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2864 template: 'FieldTextHtml',
2866 this._super.apply(this, arguments);
2868 initialize_content: function() {
2870 if (! this.get("effective_readonly")) {
2871 self._updating_editor = false;
2872 this.$textarea = this.$el.find('textarea');
2873 var width = ((this.node.attrs || {}).editor_width || 'calc(100% - 4px)');
2874 var height = ((this.node.attrs || {}).editor_height || 250);
2875 this.$textarea.cleditor({
2876 width: width, // width not including margins, borders or padding
2877 height: height, // height not including margins, borders or padding
2878 controls: // controls to add to the toolbar
2879 "bold italic underline strikethrough " +
2880 "| removeformat | bullets numbering | outdent " +
2881 "indent | link unlink | source",
2882 bodyStyle: // style to assign to document body contained within the editor
2883 "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2885 this.$cleditor = this.$textarea.cleditor()[0];
2886 this.$cleditor.change(function() {
2887 if (! self._updating_editor) {
2888 self.$cleditor.updateTextArea();
2889 self.internal_set_value(self.$textarea.val());
2892 if (this.field.translate) {
2893 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"/>')
2894 .click(this.on_translate);
2895 this.$cleditor.$toolbar.append($img);
2899 render_value: function() {
2900 if (! this.get("effective_readonly")) {
2901 this.$textarea.val(this.get('value') || '');
2902 this._updating_editor = true;
2903 this.$cleditor.updateFrame();
2904 this._updating_editor = false;
2906 this.$el.html(this.get('value'));
2911 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2912 template: 'FieldBoolean',
2915 this.$checkbox = $("input", this.$el);
2916 this.setupFocus(this.$checkbox);
2917 this.$el.click(_.bind(function() {
2918 this.internal_set_value(this.$checkbox.is(':checked'));
2920 var check_readonly = function() {
2921 self.$checkbox.prop('disabled', self.get("effective_readonly"));
2922 self.click_disabled_boolean();
2924 this.on("change:effective_readonly", this, check_readonly);
2925 check_readonly.call(this);
2926 this._super.apply(this, arguments);
2928 render_value: function() {
2929 this.$checkbox[0].checked = this.get('value');
2932 var input = this.$checkbox && this.$checkbox[0];
2933 return input ? input.focus() : false;
2935 click_disabled_boolean: function(){
2936 var $disabled = this.$el.find('input[type=checkbox]:disabled');
2937 $disabled.each(function (){
2938 $(this).next('div').remove();
2939 $(this).closest("span").append($('<div class="boolean"></div>'));
2945 The progressbar field expect a float from 0 to 100.
2947 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2948 template: 'FieldProgressBar',
2949 render_value: function() {
2950 this.$el.progressbar({
2951 value: this.get('value') || 0,
2952 disabled: this.get("effective_readonly")
2954 var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
2955 this.$('span').html(formatted_value + '%');
2960 The PercentPie field expect a float from 0 to 100.
2962 instance.web.form.FieldPercentPie = instance.web.form.AbstractField.extend({
2963 template: 'FieldPercentPie',
2965 render_value: function() {
2966 var value = this.get('value'),
2967 formatted_value = Math.round(value || 0) + '%',
2968 svg = this.$('svg')[0];
2971 nv.addGraph(function() {
2972 var width = 42, height = 42;
2973 var chart = nv.models.pieChart()
2976 .margin({top: 0, right: 0, bottom: 0, left: 0})
2981 .color(['#7C7BAD','#DDD'])
2985 .datum([{'x': 'value', 'y': value}, {'x': 'complement', 'y': 100 - value}])
2988 .attr('style', 'width: ' + width + 'px; height:' + height + 'px;');
2992 .attr({x: width/2, y: height/2 + 3, 'text-anchor': 'middle'})
2993 .style({"font-size": "10px", "font-weight": "bold"})
2994 .text(formatted_value);
3003 The FieldBarChart expectsa list of values (indeed)
3005 instance.web.form.FieldBarChart = instance.web.form.AbstractField.extend({
3006 template: 'FieldBarChart',
3008 render_value: function() {
3009 var value = JSON.parse(this.get('value'));
3010 var svg = this.$('svg')[0];
3012 nv.addGraph(function() {
3013 var width = 34, height = 34;
3014 var chart = nv.models.discreteBarChart()
3015 .x(function (d) { return d.tooltip })
3016 .y(function (d) { return d.value })
3019 .margin({top: 0, right: 0, bottom: 0, left: 0})
3022 .transitionDuration(350)
3027 .datum([{key: 'values', values: value}])
3030 .attr('style', 'width: ' + (width + 4) + 'px; height: ' + (height + 8) + 'px;');
3032 nv.utils.windowResize(chart.update);
3041 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3042 template: 'FieldSelection',
3044 'change select': 'store_dom_value',
3046 init: function(field_manager, node) {
3048 this._super(field_manager, node);
3049 this.set("value", false);
3050 this.set("values", []);
3051 this.records_orderer = new instance.web.DropMisordered();
3052 this.field_manager.on("view_content_has_changed", this, function() {
3053 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
3054 if (! _.isEqual(domain, this.get("domain"))) {
3055 this.set("domain", domain);
3059 initialize_field: function() {
3060 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3061 this.on("change:domain", this, this.query_values);
3062 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
3063 this.on("change:values", this, this.render_value);
3065 query_values: function() {
3068 if (this.field.type === "many2one") {
3069 var model = new openerp.Model(openerp.session, this.field.relation);
3070 def = model.call("name_search", ['', this.get("domain")], {"context": this.build_context()});
3072 var values = _.reject(this.field.selection, function (v) { return v[0] === false && v[1] === ''; });
3073 def = $.when(values);
3075 this.records_orderer.add(def).then(function(values) {
3076 if (! _.isEqual(values, self.get("values"))) {
3077 self.set("values", values);
3081 initialize_content: function() {
3082 // Flag indicating whether we're in an event chain containing a change
3083 // event on the select, in order to know what to do on keyup[RETURN]:
3084 // * If the user presses [RETURN] as part of changing the value of a
3085 // selection, we should just let the value change and not let the
3086 // event broadcast further (e.g. to validating the current state of
3087 // the form in editable list view, which would lead to saving the
3088 // current row or switching to the next one)
3089 // * If the user presses [RETURN] with a select closed (side-effect:
3090 // also if the user opened the select and pressed [RETURN] without
3091 // changing the selected value), takes the action as validating the
3093 var ischanging = false;
3094 var $select = this.$el.find('select')
3095 .change(function () { ischanging = true; })
3096 .click(function () { ischanging = false; })
3097 .keyup(function (e) {
3098 if (e.which !== 13 || !ischanging) { return; }
3099 e.stopPropagation();
3102 this.setupFocus($select);
3104 commit_value: function () {
3105 this.store_dom_value();
3106 return this._super();
3108 store_dom_value: function () {
3109 if (!this.get('effective_readonly') && this.$('select').length) {
3110 var val = JSON.parse(this.$('select').val());
3111 this.internal_set_value(val);
3114 set_value: function(value_) {
3115 value_ = value_ === null ? false : value_;
3116 value_ = value_ instanceof Array ? value_[0] : value_;
3117 this._super(value_);
3119 render_value: function() {
3120 var values = this.get("values");
3121 values = [[false, this.node.attrs.placeholder || '']].concat(values);
3122 var found = _.find(values, function(el) { return el[0] === this.get("value"); }, this);
3124 found = [this.get("value"), _t('Unknown')];
3125 values = [found].concat(values);
3127 if (! this.get("effective_readonly")) {
3128 this.$().html(QWeb.render("FieldSelectionSelect", {widget: this, values: values}));
3129 this.$("select").val(JSON.stringify(found[0]));
3131 this.$el.text(found[1]);
3135 var input = this.$('select:first')[0];
3136 return input ? input.focus() : false;
3138 set_dimensions: function (height, width) {
3139 this._super(height, width);
3140 this.$('select').css({
3147 instance.web.form.FieldRadio = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3148 template: 'FieldRadio',
3150 'click input': 'click_change_value'
3152 init: function(field_manager, node) {
3153 /* Radio button widget: Attributes options:
3154 * - "horizontal" to display in column
3155 * - "no_radiolabel" don't display text values
3157 this._super(field_manager, node);
3158 this.selection = _.clone(this.field.selection) || [];
3159 this.domain = false;
3160 this.uniqueId = _.uniqueId("radio");
3162 initialize_content: function () {
3163 this.on("change:effective_readonly", this, this.render_value);
3164 this.field_manager.on("view_content_has_changed", this, this.get_selection);
3165 this.get_selection();
3167 click_change_value: function (event) {
3168 var val = $(event.target).val();
3169 val = this.field.type == "selection" ? val : +val;
3170 if (val == this.get_value()) {
3171 this.set_value(false);
3173 this.set_value(val);
3176 /** Get the selection and render it
3177 * selection: [[identifier, value_to_display], ...]
3178 * For selection fields: this is directly given by this.field.selection
3179 * For many2one fields: perform a search on the relation of the many2one field
3181 get_selection: function() {
3184 var def = $.Deferred();
3185 if (self.field.type == "many2one") {
3186 var domain = instance.web.pyeval.eval('domain', this.build_domain()) || [];
3187 if (! _.isEqual(self.domain, domain)) {
3188 self.domain = domain;
3189 var ds = new instance.web.DataSetStatic(self, self.field.relation, self.build_context());
3190 ds.call('search', [self.domain])
3191 .then(function (records) {
3192 ds.name_get(records).then(function (records) {
3193 selection = records;
3198 selection = self.selection;
3202 else if (self.field.type == "selection") {
3203 selection = self.field.selection || [];
3206 return def.then(function () {
3207 if (! _.isEqual(selection, self.selection)) {
3208 self.selection = _.clone(selection);
3209 self.renderElement();
3210 self.render_value();
3214 set_value: function (value_) {
3216 if (this.field.type == "selection") {
3217 value_ = _.find(this.field.selection, function (sel) { return sel[0] == value_;});
3219 else if (!this.selection.length) {
3220 this.selection = [value_];
3223 this._super(value_);
3225 get_value: function () {
3226 var value = this.get('value');
3227 return value instanceof Array ? value[0] : value;
3229 render_value: function () {
3231 this.$el.toggleClass("oe_readonly", this.get('effective_readonly'));
3232 this.$("input:checked").prop("checked", false);
3233 if (this.get_value()) {
3234 this.$("input").filter(function () {return this.value == self.get_value();}).prop("checked", true);
3235 this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : "");
3240 // jquery autocomplete tweak to allow html and classnames
3242 var proto = $.ui.autocomplete.prototype,
3243 initSource = proto._initSource;
3245 function filter( array, term ) {
3246 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
3247 return $.grep( array, function(value_) {
3248 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
3253 _initSource: function() {
3254 if ( this.options.html && $.isArray(this.options.source) ) {
3255 this.source = function( request, response ) {
3256 response( filter( this.options.source, request.term ) );
3259 initSource.call( this );
3263 _renderItem: function( ul, item) {
3264 return $( "<li></li>" )
3265 .data( "item.autocomplete", item )
3266 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
3268 .addClass(item.classname);
3274 A mixin containing some useful methods to handle completion inputs.
3276 The widget containing this option can have these arguments in its widget options:
3277 - no_quick_create: if true, it will disable the quick create
3279 instance.web.form.CompletionFieldMixin = {
3282 this.orderer = new instance.web.DropMisordered();
3285 * Call this method to search using a string.
3287 get_search_result: function(search_val) {
3290 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
3291 this.last_query = search_val;
3292 var exclusion_domain = [], ids_blacklist = this.get_search_blacklist();
3293 if (!_(ids_blacklist).isEmpty()) {
3294 exclusion_domain.push(['id', 'not in', ids_blacklist]);
3297 return this.orderer.add(dataset.name_search(
3298 search_val, new instance.web.CompoundDomain(self.build_domain(), exclusion_domain),
3299 'ilike', this.limit + 1, self.build_context())).then(function(data) {
3300 self.last_search = data;
3301 // possible selections for the m2o
3302 var values = _.map(data, function(x) {
3303 x[1] = x[1].split("\n")[0];
3305 label: _.str.escapeHTML(x[1]),
3312 // search more... if more results that max
3313 if (values.length > self.limit) {
3314 values = values.slice(0, self.limit);
3316 label: _t("Search More..."),
3317 action: function() {
3318 dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
3319 self._search_create_popup("search", data);
3322 classname: 'oe_m2o_dropdown_option'
3326 var raw_result = _(data.result).map(function(x) {return x[1];});
3327 if (search_val.length > 0 && !_.include(raw_result, search_val) &&
3328 ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3330 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
3331 $('<span />').text(search_val).html()),
3332 action: function() {
3333 self._quick_create(search_val);
3335 classname: 'oe_m2o_dropdown_option'
3339 if (!(self.options && (self.options.no_create || self.options.no_create_edit))){
3341 label: _t("Create and Edit..."),
3342 action: function() {
3343 self._search_create_popup("form", undefined, self._create_context(search_val));
3345 classname: 'oe_m2o_dropdown_option'
3348 else if (values.length == 0)
3350 label: _t("No results to show..."),
3351 action: function() {},
3352 classname: 'oe_m2o_dropdown_option'
3358 get_search_blacklist: function() {
3361 _quick_create: function(name) {
3363 var slow_create = function () {
3364 self._search_create_popup("form", undefined, self._create_context(name));
3366 if (self.options.quick_create === undefined || self.options.quick_create) {
3367 new instance.web.DataSet(this, this.field.relation, self.build_context())
3368 .name_create(name).done(function(data) {
3369 if (!self.get('effective_readonly'))
3370 self.add_id(data[0]);
3371 }).fail(function(error, event) {
3372 event.preventDefault();
3378 // all search/create popup handling
3379 _search_create_popup: function(view, ids, context) {
3381 var pop = new instance.web.form.SelectCreatePopup(this);
3383 self.field.relation,
3385 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3386 initial_ids: ids ? _.map(ids, function(x) {return x[0];}) : undefined,
3388 disable_multiple_selection: true
3390 self.build_domain(),
3391 new instance.web.CompoundContext(self.build_context(), context || {})
3393 pop.on("elements_selected", self, function(element_ids) {
3394 self.add_id(element_ids[0]);
3401 add_id: function(id) {},
3402 _create_context: function(name) {
3404 var field = (this.options || {}).create_name_field;
3405 if (field === undefined)
3407 if (field !== false && name && (this.options || {}).quick_create !== false)
3408 tmp["default_" + field] = name;
3413 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3414 template: "M2ODialog",
3415 init: function(parent) {
3416 this.name = parent.string;
3417 this._super(parent, {
3418 title: _.str.sprintf(_t("Create a %s"), parent.string),
3424 var text = _.str.sprintf(_t("You are creating a new %s, are you sure it does not exist yet?"), self.name);
3425 this.$("p").text( text );
3426 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3427 this.$("input").val(this.getParent().last_query);
3428 this.$buttons.find(".oe_form_m2o_qc_button").click(function(e){
3429 if (self.$("input").val() != ''){
3430 self.getParent()._quick_create(self.$("input").val());
3434 self.$("input").focus();
3437 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3438 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3441 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3447 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3448 template: "FieldMany2One",
3450 'keydown input': function (e) {
3452 case $.ui.keyCode.UP:
3453 case $.ui.keyCode.DOWN:
3454 e.stopPropagation();
3458 init: function(field_manager, node) {
3459 this._super(field_manager, node);
3460 instance.web.form.CompletionFieldMixin.init.call(this);
3461 this.set({'value': false});
3462 this.display_value = {};
3463 this.display_value_backup = {};
3464 this.last_search = [];
3465 this.floating = false;
3466 this.current_display = null;
3467 this.is_started = false;
3468 this.ignore_focusout = false;
3470 reinit_value: function(val) {
3471 this.internal_set_value(val);
3472 this.floating = false;
3473 if (this.is_started)
3474 this.render_value();
3476 initialize_field: function() {
3477 this.is_started = true;
3478 instance.web.bus.on('click', this, function() {
3479 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3480 this.$input.autocomplete("close");
3483 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3485 initialize_content: function() {
3486 if (!this.get("effective_readonly"))
3487 this.render_editable();
3489 destroy_content: function () {
3490 if (this.$drop_down) {
3491 this.$drop_down.off('click');
3492 delete this.$drop_down;
3495 this.$input.closest(".modal .modal-content").off('scroll');
3496 this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3497 'focus focusout change keydown');
3500 if (this.$follow_button) {
3501 this.$follow_button.off('blur focus click');
3502 delete this.$follow_button;
3505 destroy: function () {
3506 this.destroy_content();
3507 return this._super();
3509 init_error_displayer: function() {
3512 hide_error_displayer: function() {
3515 show_error_displayer: function() {
3516 new instance.web.form.M2ODialog(this).open();
3518 render_editable: function() {
3520 this.$input = this.$el.find("input");
3522 this.init_error_displayer();
3524 self.$input.on('focus', function() {
3525 self.hide_error_displayer();
3528 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3529 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3531 this.$follow_button.click(function(ev) {
3532 ev.preventDefault();
3533 if (!self.get('value')) {
3537 var pop = new instance.web.form.FormOpenPopup(self);
3538 var context = self.build_context().eval();
3539 var model_obj = new instance.web.Model(self.field.relation);
3540 model_obj.call('get_formview_id', [self.get("value"), context]).then(function(view_id){
3542 self.field.relation,
3544 self.build_context(),
3546 title: _t("Open: ") + self.string,
3550 pop.on('write_completed', self, function(){
3551 self.display_value = {};
3552 self.display_value_backup = {};
3553 self.render_value();
3555 self.trigger('changed_value');
3560 // some behavior for input
3561 var input_changed = function() {
3562 if (self.current_display !== self.$input.val()) {
3563 self.current_display = self.$input.val();
3564 if (self.$input.val() === "") {
3565 self.internal_set_value(false);
3566 self.floating = false;
3568 self.floating = true;
3572 this.$input.keydown(input_changed);
3573 this.$input.change(input_changed);
3574 this.$drop_down.click(function() {
3575 self.$input.focus();
3576 if (self.$input.autocomplete("widget").is(":visible")) {
3577 self.$input.autocomplete("close");
3579 if (self.get("value") && ! self.floating) {
3580 self.$input.autocomplete("search", "");
3582 self.$input.autocomplete("search");
3587 // Autocomplete close on dialog content scroll
3588 var close_autocomplete = _.debounce(function() {
3589 if (self.$input.autocomplete("widget").is(":visible")) {
3590 self.$input.autocomplete("close");
3593 this.$input.closest(".modal .modal-content").on('scroll', this, close_autocomplete);
3595 self.ed_def = $.Deferred();
3596 self.uned_def = $.Deferred();
3598 var ed_duration = 15000;
3599 var anyoneLoosesFocus = function (e) {
3600 if (self.ignore_focusout) { return; }
3602 if (self.floating) {
3603 if (self.last_search.length > 0) {
3604 if (self.last_search[0][0] != self.get("value")) {
3605 self.display_value = {};
3606 self.display_value_backup = {};
3607 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3608 self.reinit_value(self.last_search[0][0]);
3611 self.render_value();
3615 self.reinit_value(false);
3617 self.floating = false;
3619 if (used && self.get("value") === false && ! self.no_ed && ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3620 self.ed_def.reject();
3621 self.uned_def.reject();
3622 self.ed_def = $.Deferred();
3623 self.ed_def.done(function() {
3624 self.show_error_displayer();
3625 ignore_blur = false;
3626 self.trigger('focused');
3629 setTimeout(function() {
3630 self.ed_def.resolve();
3631 self.uned_def.reject();
3632 self.uned_def = $.Deferred();
3633 self.uned_def.done(function() {
3634 self.hide_error_displayer();
3636 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3640 self.ed_def.reject();
3643 var ignore_blur = false;
3645 focusout: anyoneLoosesFocus,
3646 focus: function () { self.trigger('focused'); },
3647 autocompleteopen: function () { ignore_blur = true; },
3648 autocompleteclose: function () { setTimeout(function() {ignore_blur = false;},0); },
3650 // autocomplete open
3651 if (ignore_blur) { $(this).focus(); return; }
3652 if (_(self.getChildren()).any(function (child) {
3653 return child instanceof instance.web.form.AbstractFormPopup;
3655 self.trigger('blurred');
3659 var isSelecting = false;
3661 this.$input.autocomplete({
3662 source: function(req, resp) {
3663 self.get_search_result(req.term).done(function(result) {
3667 select: function(event, ui) {
3671 self.display_value = {};
3672 self.display_value_backup = {};
3673 self.display_value["" + item.id] = item.name;
3674 self.reinit_value(item.id);
3675 } else if (item.action) {
3677 // Cancel widget blurring, to avoid form blur event
3678 self.trigger('focused');
3682 focus: function(e, ui) {
3686 // disabled to solve a bug, but may cause others
3687 //close: anyoneLoosesFocus,
3691 // set position for list of suggestions box
3692 this.$input.autocomplete( "option", "position", { my : "left top", at: "left bottom" } );
3693 this.$input.autocomplete("widget").openerpClass();
3694 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3695 this.$input.keyup(function(e) {
3696 if (e.which === 13) { // ENTER
3698 e.stopPropagation();
3700 isSelecting = false;
3702 this.setupFocus(this.$follow_button);
3704 render_value: function(no_recurse) {
3706 if (! this.get("value")) {
3707 this.display_string("");
3710 var display = this.display_value["" + this.get("value")];
3712 this.display_string(display);
3716 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3717 this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3719 self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
3722 self.display_value["" + self.get("value")] = data[0][1];
3723 self.render_value(true);
3724 }).fail( function (data, event) {
3725 // avoid displaying crash errors as many2One should be name_get compliant
3726 event.preventDefault();
3727 self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")];
3728 self.render_value(true);
3732 display_string: function(str) {
3734 if (!this.get("effective_readonly")) {
3735 this.$input.val(str.split("\n")[0]);
3736 this.current_display = this.$input.val();
3737 if (this.is_false()) {
3738 this.$('.oe_m2o_cm_button').css({'display':'none'});
3740 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3743 var lines = _.escape(str).split("\n");
3747 follow = _.rest(lines).join("<br />");
3750 var $link = this.$el.find('.oe_form_uri')
3753 if (! this.options.no_open)
3754 $link.click(function () {
3755 var context = self.build_context().eval();
3756 var model_obj = new instance.web.Model(self.field.relation);
3757 model_obj.call('get_formview_action', [self.get("value"), context]).then(function(action){
3758 self.do_action(action);
3762 $(".oe_form_m2o_follow", this.$el).html(follow);
3765 set_value: function(value_) {
3767 if (value_ instanceof Array) {
3768 this.display_value = {};
3769 this.display_value_backup = {};
3770 if (! this.options.always_reload) {
3771 this.display_value["" + value_[0]] = value_[1];
3774 this.display_value_backup["" + value_[0]] = value_[1];
3778 value_ = value_ || false;
3779 this.reinit_value(value_);
3781 get_displayed: function() {
3782 return this.display_value["" + this.get("value")];
3784 add_id: function(id) {
3785 this.display_value = {};
3786 this.display_value_backup = {};
3787 this.reinit_value(id);
3789 is_false: function() {
3790 return ! this.get("value");
3792 focus: function () {
3793 var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3794 return input ? input.focus() : false;
3796 _quick_create: function() {
3798 this.ed_def.reject();
3799 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3801 _search_create_popup: function() {
3803 this.ed_def.reject();
3804 this.ignore_focusout = true;
3805 this.reinit_value(false);
3806 var res = instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3807 this.ignore_focusout = false;
3811 set_dimensions: function (height, width) {
3812 this._super(height, width);
3813 if (!this.get("effective_readonly") && this.$input)
3814 this.$input.css('height', height);
3818 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3819 template: 'Many2OneButton',
3820 init: function(field_manager, node) {
3821 this._super.apply(this, arguments);
3824 this._super.apply(this, arguments);
3827 set_button: function() {
3830 this.$button.remove();
3833 this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3834 this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3835 this.$button.addClass('oe_link').css({'padding':'4px'});
3836 this.$el.append(this.$button);
3837 this.$button.on('click', self.on_click);
3839 on_click: function(ev) {
3841 this.popup = new instance.web.form.FormOpenPopup(this);
3842 this.popup.show_element(
3843 this.field.relation,
3845 this.build_context(),
3846 {title: this.string}
3848 this.popup.on('create_completed', self, function(r) {
3852 set_value: function(value_) {
3854 if (value_ instanceof Array) {
3857 value_ = value_ || false;
3858 this.set('value', value_);
3864 * Abstract-ish ListView.List subclass adding an "Add an item" row to replace
3865 * the big ugly button in the header.
3867 * Requires the implementation of a ``is_readonly`` method (usually a proxy to
3868 * the corresponding field's readonly or effective_readonly property) to
3869 * decide whether the special row should or should not be inserted.
3871 * Optionally an ``_add_row_class`` attribute can be set for the class(es) to
3872 * set on the insertion row.
3874 instance.web.form.AddAnItemList = instance.web.ListView.List.extend({
3875 pad_table_to: function (count) {
3876 if (!this.view.is_action_enabled('create') || this.is_readonly()) {
3881 this._super(count > 0 ? count - 1 : 0);
3884 var columns = _(this.columns).filter(function (column) {
3885 return column.invisible !== '1';
3887 if (this.options.selectable) { columns++; }
3888 if (this.options.deletable) { columns++; }
3890 var $cell = $('<td>', {
3892 'class': this._add_row_class || ''
3894 $('<a>', {href: '#'}).text(_t("Add an item"))
3895 .mousedown(function () {
3896 // FIXME: needs to be an official API somehow
3897 if (self.view.editor.is_editing()) {
3898 self.view.__ignore_blur = true;
3901 .click(function (e) {
3903 e.stopPropagation();
3904 // FIXME: there should also be an API for that one
3905 if (self.view.editor.form.__blur_timeout) {
3906 clearTimeout(self.view.editor.form.__blur_timeout);
3907 self.view.editor.form.__blur_timeout = false;
3909 self.view.ensure_saved().done(function () {
3910 self.view.do_add_record();
3914 var $padding = this.$current.find('tr:not([data-id]):first');
3915 var $newrow = $('<tr>').append($cell);
3916 if ($padding.length) {
3917 $padding.before($newrow);
3919 this.$current.append($newrow)
3925 # Values: (0, 0, { fields }) create
3926 # (1, ID, { fields }) update
3927 # (2, ID) remove (delete)
3928 # (3, ID) unlink one (target id or target of relation)
3930 # (5) unlink all (only valid for one2many)
3935 'create': function (values) {
3936 return [commands.CREATE, false, values];
3938 // (1, id, {values})
3940 'update': function (id, values) {
3941 return [commands.UPDATE, id, values];
3945 'delete': function (id) {
3946 return [commands.DELETE, id, false];
3948 // (3, id[, _]) removes relation, but not linked record itself
3950 'forget': function (id) {
3951 return [commands.FORGET, id, false];
3955 'link_to': function (id) {
3956 return [commands.LINK_TO, id, false];
3960 'delete_all': function () {
3961 return [5, false, false];
3963 // (6, _, ids) replaces all linked records with provided ids
3965 'replace_with': function (ids) {
3966 return [6, false, ids];
3969 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3970 multi_selection: false,
3971 disable_utility_classes: true,
3972 init: function(field_manager, node) {
3973 this._super(field_manager, node);
3974 this.is_loaded = $.Deferred();
3975 this.initial_is_loaded = this.is_loaded;
3976 this.form_last_update = $.Deferred();
3977 this.init_form_last_update = this.form_last_update;
3978 this.is_started = false;
3979 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
3980 this.dataset.o2m = this;
3981 this.dataset.parent_view = this.view;
3982 this.dataset.child_name = this.name;
3984 this.dataset.on('dataset_changed', this, function() {
3985 self.trigger_on_change();
3990 this._super.apply(this, arguments);
3991 this.$el.addClass('oe_form_field oe_form_field_one2many');
3996 this.is_loaded.done(function() {
3997 self.on("change:effective_readonly", self, function() {
3998 self.is_loaded = self.is_loaded.then(function() {
3999 self.viewmanager.destroy();
4000 return $.when(self.load_views()).done(function() {
4001 self.reload_current_view();
4006 this.is_started = true;
4007 this.reload_current_view();
4009 trigger_on_change: function() {
4010 this.trigger('changed_value');
4012 load_views: function() {
4015 var modes = this.node.attrs.mode;
4016 modes = !!modes ? modes.split(",") : ["tree"];
4018 _.each(modes, function(mode) {
4019 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
4020 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
4024 view_type: mode == "tree" ? "list" : mode,
4027 if (self.field.views && self.field.views[mode]) {
4028 view.embedded_view = self.field.views[mode];
4030 if(view.view_type === "list") {
4031 _.extend(view.options, {
4033 selectable: self.multi_selection,
4035 import_enabled: false,
4038 if (self.get("effective_readonly")) {
4039 _.extend(view.options, {
4044 } else if (view.view_type === "form") {
4045 if (self.get("effective_readonly")) {
4046 view.view_type = 'form';
4048 _.extend(view.options, {
4049 not_interactible_on_create: true,
4051 } else if (view.view_type === "kanban") {
4052 _.extend(view.options, {
4053 confirm_on_delete: false,
4055 if (self.get("effective_readonly")) {
4056 _.extend(view.options, {
4057 action_buttons: false,
4058 quick_creatable: false,
4060 read_only_mode: true,
4068 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
4069 this.viewmanager.o2m = self;
4070 var once = $.Deferred().done(function() {
4071 self.init_form_last_update.resolve();
4073 var def = $.Deferred().done(function() {
4074 self.initial_is_loaded.resolve();
4076 this.viewmanager.on("controller_inited", self, function(view_type, controller) {
4077 controller.o2m = self;
4078 if (view_type == "list") {
4079 if (self.get("effective_readonly")) {
4080 controller.on('edit:before', self, function (e) {
4083 _(controller.columns).find(function (column) {
4084 if (!(column instanceof instance.web.list.Handle)) {
4087 column.modifiers.invisible = true;
4091 } else if (view_type === "form") {
4092 if (self.get("effective_readonly")) {
4093 $(".oe_form_buttons", controller.$el).children().remove();
4095 controller.on("load_record", self, function(){
4098 controller.on('pager_action_executed',self,self.save_any_view);
4099 } else if (view_type == "graph") {
4100 self.reload_current_view();
4104 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
4105 $.when(self.save_any_view()).done(function() {
4106 if (n_mode === "list") {
4107 $.async_when().done(function() {
4108 self.reload_current_view();
4113 $.async_when().done(function () {
4114 self.viewmanager.appendTo(self.$el);
4118 reload_current_view: function() {
4120 self.is_loaded = self.is_loaded.then(function() {
4121 var view = self.get_active_view();
4122 if (view.type === "list") {
4123 return view.controller.reload_content();
4124 } else if (view.type === "form") {
4125 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
4126 self.dataset.index = 0;
4128 var act = function() {
4129 return view.controller.do_show();
4131 self.form_last_update = self.form_last_update.then(act, act);
4132 return self.form_last_update;
4133 } else if (view.controller.do_search) {
4134 return view.controller.do_search(self.build_domain(), self.dataset.get_context(), []);
4137 return self.is_loaded;
4139 get_active_view: function () {
4141 * Returns the current active view if any.
4143 return (this.viewmanager && this.viewmanager.active_view);
4145 set_value: function(value_) {
4146 value_ = value_ || [];
4148 var view = this.get_active_view();
4149 this.dataset.reset_ids([]);
4151 if(value_.length >= 1 && value_[0] instanceof Array) {
4153 _.each(value_, function(command) {
4154 var obj = {values: command[2]};
4155 switch (command[0]) {
4156 case commands.CREATE:
4157 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4159 self.dataset.to_create.push(obj);
4160 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4163 case commands.UPDATE:
4164 obj['id'] = command[1];
4165 self.dataset.to_write.push(obj);
4166 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4169 case commands.DELETE:
4170 self.dataset.to_delete.push({id: command[1]});
4172 case commands.LINK_TO:
4173 ids.push(command[1]);
4175 case commands.DELETE_ALL:
4176 self.dataset.delete_all = true;
4181 this.dataset.set_ids(ids);
4182 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
4184 this.dataset.delete_all = true;
4185 _.each(value_, function(command) {
4186 var obj = {values: command};
4187 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4189 self.dataset.to_create.push(obj);
4190 self.dataset.cache.push(_.clone(obj));
4194 this.dataset.set_ids(ids);
4196 this._super(value_);
4197 this.dataset.reset_ids(value_);
4199 if (this.dataset.index === null && this.dataset.ids.length > 0) {
4200 this.dataset.index = 0;
4202 this.trigger_on_change();
4203 if (this.is_started) {
4204 return self.reload_current_view();
4209 get_value: function() {
4213 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
4214 val = val.concat(_.map(this.dataset.ids, function(id) {
4215 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
4217 return commands.create(alter_order.values);
4219 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
4221 return commands.update(alter_order.id, alter_order.values);
4223 return commands.link_to(id);
4225 return val.concat(_.map(
4226 this.dataset.to_delete, function(x) {
4227 return commands['delete'](x.id);}));
4229 commit_value: function() {
4230 return this.save_any_view();
4232 save_any_view: function() {
4233 var view = this.get_active_view();
4235 if (this.viewmanager.active_view.type === "form") {
4236 if (view.controller.is_initialized.state() !== 'resolved') {
4237 return $.when(false);
4239 return $.when(view.controller.save());
4240 } else if (this.viewmanager.active_view.type === "list") {
4241 return $.when(view.controller.ensure_saved());
4244 return $.when(false);
4246 is_syntax_valid: function() {
4247 var view = this.get_active_view();
4251 switch (this.viewmanager.active_view.type) {
4253 return _(view.controller.fields).chain()
4258 return view.controller.is_valid();
4264 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
4265 template: 'One2Many.viewmanager',
4266 init: function(parent, dataset, views, flags) {
4267 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
4268 this.registry = instance.web.views.extend({
4269 list: 'instance.web.form.One2ManyListView',
4270 form: 'instance.web.form.One2ManyFormView',
4272 this.__ignore_blur = false;
4274 switch_mode: function(mode, unused) {
4275 if (mode !== 'form') {
4276 return this._super(mode, unused);
4279 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
4280 var pop = new instance.web.form.FormOpenPopup(this);
4281 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4282 title: _t("Open: ") + self.o2m.string,
4283 create_function: function(data, options) {
4284 return self.o2m.dataset.create(data, options).done(function(r) {
4285 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4286 self.o2m.dataset.trigger("dataset_changed", r);
4289 write_function: function(id, data, options) {
4290 return self.o2m.dataset.write(id, data, {}).done(function() {
4291 self.o2m.reload_current_view();
4294 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4295 parent_view: self.o2m.view,
4296 child_name: self.o2m.name,
4297 read_function: function() {
4298 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4300 form_view_options: {'not_interactible_on_create':true},
4301 readonly: self.o2m.get("effective_readonly")
4303 pop.on("elements_selected", self, function() {
4304 self.o2m.reload_current_view();
4309 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
4310 get_context: function() {
4311 this.context = this.o2m.build_context();
4312 return this.context;
4316 instance.web.form.One2ManyListView = instance.web.ListView.extend({
4317 _template: 'One2Many.listview',
4318 init: function (parent, dataset, view_id, options) {
4319 this._super(parent, dataset, view_id, _.extend(options || {}, {
4320 GroupsType: instance.web.form.One2ManyGroups,
4321 ListType: instance.web.form.One2ManyList
4323 this.on('edit:after', this, this.proxy('_after_edit'));
4324 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
4327 .bind('add', this.proxy("changed_records"))
4328 .bind('edit', this.proxy("changed_records"))
4329 .bind('remove', this.proxy("changed_records"));
4331 start: function () {
4332 var ret = this._super();
4334 .off('mousedown.handleButtons')
4335 .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
4338 changed_records: function () {
4339 this.o2m.trigger_on_change();
4341 is_valid: function () {
4343 if (!this.fields_view || !this.editable()){
4346 this.o2m._dirty_flag = true;
4348 return _.every(this.records.records, function(record){
4350 _.each(self.editor.form.fields, function(field){
4351 field._inhibit_on_change_flag = true;
4352 field.set_value(r.attributes[field.name]);
4353 field._inhibit_on_change_flag = false;
4355 return _.every(self.editor.form.fields, function(field){
4356 field.process_modifiers();
4357 field._check_css_flags();
4358 return field.is_valid();
4362 do_add_record: function () {
4363 if (this.editable()) {
4364 this._super.apply(this, arguments);
4367 var pop = new instance.web.form.SelectCreatePopup(this);
4369 self.o2m.field.relation,
4371 title: _t("Create: ") + self.o2m.string,
4372 initial_view: "form",
4373 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4374 create_function: function(data, options) {
4375 return self.o2m.dataset.create(data, options).done(function(r) {
4376 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4377 self.o2m.dataset.trigger("dataset_changed", r);
4380 read_function: function() {
4381 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4383 parent_view: self.o2m.view,
4384 child_name: self.o2m.name,
4385 form_view_options: {'not_interactible_on_create':true}
4387 self.o2m.build_domain(),
4388 self.o2m.build_context()
4390 pop.on("elements_selected", self, function() {
4391 self.o2m.reload_current_view();
4395 do_activate_record: function(index, id) {
4397 var pop = new instance.web.form.FormOpenPopup(self);
4398 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4399 title: _t("Open: ") + self.o2m.string,
4400 write_function: function(id, data) {
4401 return self.o2m.dataset.write(id, data, {}).done(function() {
4402 self.o2m.reload_current_view();
4405 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4406 parent_view: self.o2m.view,
4407 child_name: self.o2m.name,
4408 read_function: function() {
4409 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4411 form_view_options: {'not_interactible_on_create':true},
4412 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
4415 do_button_action: function (name, id, callback) {
4416 if (!_.isNumber(id)) {
4417 instance.webclient.notification.warn(
4418 _t("Action Button"),
4419 _t("The o2m record must be saved before an action can be used"));
4422 var parent_form = this.o2m.view;
4424 this.ensure_saved().then(function () {
4426 return parent_form.save();
4429 }).done(function () {
4430 var ds = self.o2m.dataset;
4431 var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
4432 return value.length;
4434 if (!self.o2m.options.reload_on_button && !cached_records) {
4435 self.handle_button(name, id, callback);
4437 self.handle_button(name, id, function(){
4438 self.o2m.view.reload();
4444 _after_edit: function () {
4445 this.__ignore_blur = false;
4446 this.editor.form.on('blurred', this, this._on_form_blur);
4448 // The form's blur thing may be jiggered during the edition setup,
4449 // potentially leading to the o2m instasaving the row. Cancel any
4450 // blurring triggered the edition startup here
4451 this.editor.form.widgetFocused();
4453 _before_unedit: function () {
4454 this.editor.form.off('blurred', this, this._on_form_blur);
4456 _button_down: function () {
4457 // If a button is clicked (usually some sort of action button), it's
4458 // the button's responsibility to ensure the editable list is in the
4459 // correct state -> ignore form blurring
4460 this.__ignore_blur = true;
4463 * Handles blurring of the nested form (saves the currently edited row),
4464 * unless the flag to ignore the event is set to ``true``
4466 * Makes the internal form go away
4468 _on_form_blur: function () {
4469 if (this.__ignore_blur) {
4470 this.__ignore_blur = false;
4473 // FIXME: why isn't there an API for this?
4474 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4475 this.ensure_saved();
4478 this.cancel_edition();
4480 keypress_ENTER: function () {
4481 // blurring caused by hitting the [Return] key, should skip the
4482 // autosave-on-blur and let the handler for [Return] do its thing (save
4483 // the current row *anyway*, then create a new one/edit the next one)
4484 this.__ignore_blur = true;
4485 this._super.apply(this, arguments);
4487 do_delete: function (ids) {
4488 var confirm = window.confirm;
4489 window.confirm = function () { return true; };
4491 return this._super(ids);
4493 window.confirm = confirm;
4496 reload_record: function (record, options) {
4497 if (!options || !options['do_not_evict']) {
4498 // Evict record.id from cache to ensure it will be reloaded correctly
4499 this.dataset.evict_record(record.get('id'));
4502 return this._super(record);
4505 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4506 setup_resequence_rows: function () {
4507 if (!this.view.o2m.get('effective_readonly')) {
4508 this._super.apply(this, arguments);
4512 instance.web.form.One2ManyList = instance.web.form.AddAnItemList.extend({
4513 _add_row_class: 'oe_form_field_one2many_list_row_add',
4514 is_readonly: function () {
4515 return this.view.o2m.get('effective_readonly');
4519 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4520 form_template: 'One2Many.formview',
4521 load_form: function(data) {
4524 this.$buttons.find('button.oe_form_button_create').click(function() {
4525 self.save().done(self.on_button_new);
4528 do_notify_change: function() {
4529 if (this.dataset.parent_view) {
4530 this.dataset.parent_view.do_notify_change();
4532 this._super.apply(this, arguments);
4537 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4538 template: "FieldMany2ManyTags",
4539 tag_template: "FieldMany2ManyTag",
4541 this._super.apply(this, arguments);
4542 instance.web.form.CompletionFieldMixin.init.call(this);
4543 this.set({"value": []});
4544 this._display_orderer = new instance.web.DropMisordered();
4545 this._drop_shown = false;
4547 initialize_texttext: function(){
4550 plugins : 'tags arrow autocomplete',
4552 render: function(suggestion) {
4553 return $('<span class="text-label"/>').
4554 data('index', suggestion['index']).html(suggestion['label']);
4559 selectFromDropdown: function() {
4560 this.trigger('hideDropdown');
4561 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4562 var data = self.search_result[index];
4564 self.add_id(data.id);
4566 self.ignore_blur = true;
4569 this.trigger('setSuggestions', {result : []});
4573 isTagAllowed: function(tag) {
4577 removeTag: function(tag) {
4578 var id = tag.data("id");
4579 self.set({"value": _.without(self.get("value"), id)});
4581 renderTag: function(stuff) {
4582 return $.fn.textext.TextExtTags.prototype.renderTag.
4583 call(this, stuff).data("id", stuff.id);
4587 itemToString: function(item) {
4592 onSetInputData: function(e, data) {
4594 this._plugins.autocomplete._suggestions = null;
4596 this.input().val(data);
4602 initialize_content: function() {
4603 if (this.get("effective_readonly"))
4606 self.ignore_blur = false;
4607 self.$text = this.$("textarea");
4608 self.$text.textext(self.initialize_texttext()).bind('getSuggestions', function(e, data) {
4610 var str = !!data ? data.query || '' : '';
4611 self.get_search_result(str).done(function(result) {
4612 self.search_result = result;
4613 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4614 return _.extend(el, {index:i});
4617 }).bind('hideDropdown', function() {
4618 self._drop_shown = false;
4619 }).bind('showDropdown', function() {
4620 self._drop_shown = true;
4622 self.tags = self.$text.textext()[0].tags();
4624 .focusin(function () {
4625 self.trigger('focused');
4626 self.ignore_blur = false;
4628 .focusout(function() {
4629 self.$text.trigger("setInputData", "");
4630 if (!self.ignore_blur) {
4631 self.trigger('blurred');
4633 }).keydown(function(e) {
4634 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4635 self.$text.textext()[0].autocomplete().selectFromDropdown();
4639 // WARNING: duplicated in 4 other M2M widgets
4640 set_value: function(value_) {
4641 value_ = value_ || [];
4642 if (value_.length >= 1 && value_[0] instanceof Array) {
4643 // value_ is a list of m2m commands. We only process
4644 // LINK_TO and REPLACE_WITH in this context
4646 _.each(value_, function (command) {
4647 if (command[0] === commands.LINK_TO) {
4648 val.push(command[1]); // (4, id[, _])
4649 } else if (command[0] === commands.REPLACE_WITH) {
4650 val = command[2]; // (6, _, ids)
4655 this._super(value_);
4657 is_false: function() {
4658 return _(this.get("value")).isEmpty();
4660 get_value: function() {
4661 var tmp = [commands.replace_with(this.get("value"))];
4664 get_search_blacklist: function() {
4665 return this.get("value");
4667 map_tag: function(data){
4668 return _.map(data, function(el) {return {name: el[1], id:el[0]};})
4670 get_render_data: function(ids){
4672 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4673 return dataset.name_get(ids);
4675 render_tag: function(data) {
4677 if (! self.get("effective_readonly")) {
4678 self.tags.containerElement().children().remove();
4679 self.$('textarea').css("padding-left", "3px");
4680 self.tags.addTags(self.map_tag(data));
4682 self.$el.html(QWeb.render(self.tag_template, {elements: data}));
4685 render_value: function() {
4687 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4688 var values = self.get("value");
4689 var handle_names = function(data) {
4690 if (self.isDestroyed())
4693 _.each(data, function(el) {
4694 indexed[el[0]] = el;
4696 data = _.map(values, function(el) { return indexed[el]; });
4697 self.render_tag(data);
4699 if (! values || values.length > 0) {
4700 return this._display_orderer.add(self.get_render_data(values)).done(handle_names);
4705 add_id: function(id) {
4706 this.set({'value': _.uniq(this.get('value').concat([id]))});
4708 focus: function () {
4709 var input = this.$text && this.$text[0];
4710 return input ? input.focus() : false;
4712 set_dimensions: function (height, width) {
4713 this._super(height, width);
4714 this.$("textarea").css({
4719 _search_create_popup: function() {
4720 self.ignore_blur = true;
4721 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
4727 - reload_on_button: Reload the whole form view if click on a button in a list view.
4728 If you see this options, do not use it, it's basically a dirty hack to make one
4729 precise o2m to behave the way we want.
4731 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4732 multi_selection: false,
4733 disable_utility_classes: true,
4734 init: function(field_manager, node) {
4735 this._super(field_manager, node);
4736 this.is_loaded = $.Deferred();
4737 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4738 this.dataset.m2m = this;
4740 this.dataset.on('unlink', self, function(ids) {
4741 self.dataset_changed();
4744 this.list_dm = new instance.web.DropMisordered();
4745 this.render_value_dm = new instance.web.DropMisordered();
4747 initialize_content: function() {
4750 this.$el.addClass('oe_form_field oe_form_field_many2many');
4752 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4754 'deletable': this.get("effective_readonly") ? false : true,
4755 'selectable': this.multi_selection,
4757 'reorderable': false,
4758 'import_enabled': false,
4760 var embedded = (this.field.views || {}).tree;
4762 this.list_view.set_embedded_view(embedded);
4764 this.list_view.m2m_field = this;
4765 var loaded = $.Deferred();
4766 this.list_view.on("list_view_loaded", this, function() {
4769 this.list_view.appendTo(this.$el);
4771 var old_def = self.is_loaded;
4772 self.is_loaded = $.Deferred().done(function() {
4775 this.list_dm.add(loaded).then(function() {
4776 self.is_loaded.resolve();
4779 destroy_content: function() {
4780 this.list_view.destroy();
4781 this.list_view = undefined;
4783 // WARNING: duplicated in 4 other M2M widgets
4784 set_value: function(value_) {
4785 value_ = value_ || [];
4786 if (value_.length >= 1 && value_[0] instanceof Array) {
4787 // value_ is a list of m2m commands. We only process
4788 // LINK_TO and REPLACE_WITH in this context
4790 _.each(value_, function (command) {
4791 if (command[0] === commands.LINK_TO) {
4792 val.push(command[1]); // (4, id[, _])
4793 } else if (command[0] === commands.REPLACE_WITH) {
4794 val = command[2]; // (6, _, ids)
4799 this._super(value_);
4801 get_value: function() {
4802 return [commands.replace_with(this.get('value'))];
4804 is_false: function () {
4805 return _(this.get("value")).isEmpty();
4807 render_value: function() {
4809 this.dataset.set_ids(this.get("value"));
4810 this.render_value_dm.add(this.is_loaded).then(function() {
4811 return self.list_view.reload_content();
4814 dataset_changed: function() {
4815 this.internal_set_value(this.dataset.ids);
4819 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4820 get_context: function() {
4821 this.context = this.m2m.build_context();
4822 return this.context;
4828 * @extends instance.web.ListView
4830 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4831 init: function (parent, dataset, view_id, options) {
4832 this._super(parent, dataset, view_id, _.extend(options || {}, {
4833 ListType: instance.web.form.Many2ManyList,
4836 do_add_record: function () {
4837 var pop = new instance.web.form.SelectCreatePopup(this);
4841 title: _t("Add: ") + this.m2m_field.string,
4842 no_create: this.m2m_field.options.no_create,
4844 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4845 this.m2m_field.build_context()
4848 pop.on("elements_selected", self, function(element_ids) {
4850 _(element_ids).each(function (id) {
4851 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4852 self.dataset.set_ids(self.dataset.ids.concat([id]));
4853 self.m2m_field.dataset_changed();
4858 self.reload_content();
4862 do_activate_record: function(index, id) {
4864 var pop = new instance.web.form.FormOpenPopup(this);
4865 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4866 title: _t("Open: ") + this.m2m_field.string,
4867 readonly: this.getParent().get("effective_readonly")
4869 pop.on('write_completed', self, self.reload_content);
4871 do_button_action: function(name, id, callback) {
4873 var _sup = _.bind(this._super, this);
4874 if (! this.m2m_field.options.reload_on_button) {
4875 return _sup(name, id, callback);
4877 return this.m2m_field.view.save().then(function() {
4878 return _sup(name, id, function() {
4879 self.m2m_field.view.reload();
4884 is_action_enabled: function () { return true; },
4886 instance.web.form.Many2ManyList = instance.web.form.AddAnItemList.extend({
4887 _add_row_class: 'oe_form_field_many2many_list_row_add',
4888 is_readonly: function () {
4889 return this.view.m2m_field.get('effective_readonly');
4893 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4894 disable_utility_classes: true,
4895 init: function(field_manager, node) {
4896 this._super(field_manager, node);
4897 instance.web.form.CompletionFieldMixin.init.call(this);
4898 m2m_kanban_lazy_init();
4899 this.is_loaded = $.Deferred();
4900 this.initial_is_loaded = this.is_loaded;
4903 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4904 this.dataset.m2m = this;
4905 this.dataset.on('unlink', self, function(ids) {
4906 self.dataset_changed();
4910 this._super.apply(this, arguments);
4915 self.on("change:effective_readonly", self, function() {
4916 self.is_loaded = self.is_loaded.then(function() {
4917 self.kanban_view.destroy();
4918 return $.when(self.load_view()).done(function() {
4919 self.render_value();
4924 // WARNING: duplicated in 4 other M2M widgets
4925 set_value: function(value_) {
4926 value_ = value_ || [];
4927 if (value_.length >= 1 && value_[0] instanceof Array) {
4928 // value_ is a list of m2m commands. We only process
4929 // LINK_TO and REPLACE_WITH in this context
4931 _.each(value_, function (command) {
4932 if (command[0] === commands.LINK_TO) {
4933 val.push(command[1]); // (4, id[, _])
4934 } else if (command[0] === commands.REPLACE_WITH) {
4935 val = command[2]; // (6, _, ids)
4940 this._super(value_);
4942 get_value: function() {
4943 return [commands.replace_with(this.get('value'))];
4945 load_view: function() {
4947 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4948 'create_text': _t("Add"),
4949 'creatable': self.get("effective_readonly") ? false : true,
4950 'quick_creatable': self.get("effective_readonly") ? false : true,
4951 'read_only_mode': self.get("effective_readonly") ? true : false,
4952 'confirm_on_delete': false,
4954 var embedded = (this.field.views || {}).kanban;
4956 this.kanban_view.set_embedded_view(embedded);
4958 this.kanban_view.m2m = this;
4959 var loaded = $.Deferred();
4960 this.kanban_view.on("kanban_view_loaded",self,function() {
4961 self.initial_is_loaded.resolve();
4964 this.kanban_view.on('switch_mode', this, this.open_popup);
4965 $.async_when().done(function () {
4966 self.kanban_view.appendTo(self.$el);
4970 render_value: function() {
4972 this.dataset.set_ids(this.get("value"));
4973 this.is_loaded = this.is_loaded.then(function() {
4974 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
4977 dataset_changed: function() {
4978 this.set({'value': this.dataset.ids});
4980 open_popup: function(type, unused) {
4981 if (type !== "form")
4985 if (this.dataset.index === null) {
4986 pop = new instance.web.form.SelectCreatePopup(this);
4988 this.field.relation,
4990 title: _t("Add: ") + this.string
4992 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
4993 this.build_context()
4995 pop.on("elements_selected", self, function(element_ids) {
4996 _.each(element_ids, function(one_id) {
4997 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
4998 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
4999 self.dataset_changed();
5000 self.render_value();
5005 var id = self.dataset.ids[self.dataset.index];
5006 pop = new instance.web.form.FormOpenPopup(this);
5007 pop.show_element(self.field.relation, id, self.build_context(), {
5008 title: _t("Open: ") + self.string,
5009 write_function: function(id, data, options) {
5010 return self.dataset.write(id, data, {}).done(function() {
5011 self.render_value();
5014 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
5015 parent_view: self.view,
5016 child_name: self.name,
5017 readonly: self.get("effective_readonly")
5021 add_id: function(id) {
5022 this.quick_create.add_id(id);
5026 function m2m_kanban_lazy_init() {
5027 if (instance.web.form.Many2ManyKanbanView)
5029 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
5030 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
5031 _is_quick_create_enabled: function() {
5032 return this._super() && ! this.group_by;
5035 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
5036 template: 'Many2ManyKanban.quick_create',
5039 * close_btn: If true, the widget will display a "Close" button able to trigger
5042 init: function(parent, dataset, context, buttons) {
5043 this._super(parent);
5044 this.m2m = this.getParent().view.m2m;
5045 this.m2m.quick_create = this;
5046 this._dataset = dataset;
5047 this._buttons = buttons || false;
5048 this._context = context || {};
5050 start: function () {
5052 self.$text = this.$el.find('input').css("width", "200px");
5053 self.$text.textext({
5054 plugins : 'arrow autocomplete',
5056 render: function(suggestion) {
5057 return $('<span class="text-label"/>').
5058 data('index', suggestion['index']).html(suggestion['label']);
5063 selectFromDropdown: function() {
5064 $(this).trigger('hideDropdown');
5065 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
5066 var data = self.search_result[index];
5068 self.add_id(data.id);
5075 itemToString: function(item) {
5080 }).bind('getSuggestions', function(e, data) {
5082 var str = !!data ? data.query || '' : '';
5083 self.m2m.get_search_result(str).done(function(result) {
5084 self.search_result = result;
5085 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
5086 return _.extend(el, {index:i});
5090 self.$text.focusout(function() {
5095 this.$text[0].focus();
5097 add_id: function(id) {
5100 self.trigger('added', id);
5101 this.m2m.dataset_changed();
5107 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
5109 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
5110 template: "AbstractFormPopup.render",
5113 * -readonly: only applicable when not in creation mode, default to false
5114 * - alternative_form_view
5121 * - form_view_options
5123 init_popup: function(model, row_id, domain, context, options) {
5124 this.row_id = row_id;
5126 this.domain = domain || [];
5127 this.context = context || {};
5128 this.options = options;
5129 _.defaults(this.options, {});
5131 init_dataset: function() {
5133 this.created_elements = [];
5134 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
5135 this.dataset.read_function = this.options.read_function;
5136 this.dataset.create_function = function(data, options, sup) {
5137 var fct = self.options.create_function || sup;
5138 return fct.call(this, data, options).done(function(r) {
5139 self.trigger('create_completed saved', r);
5140 self.created_elements.push(r);
5143 this.dataset.write_function = function(id, data, options, sup) {
5144 var fct = self.options.write_function || sup;
5145 return fct.call(this, id, data, options).done(function(r) {
5146 self.trigger('write_completed saved', r);
5149 this.dataset.parent_view = this.options.parent_view;
5150 this.dataset.child_name = this.options.child_name;
5152 display_popup: function() {
5154 this.renderElement();
5155 var dialog = new instance.web.Dialog(this, {
5156 dialogClass: 'oe_act_window',
5157 title: this.options.title || "",
5158 }, this.$el).open();
5159 dialog.on('closing', this, function (e){
5160 self.check_exit(true);
5162 this.$buttonpane = dialog.$buttons;
5165 setup_form_view: function() {
5168 this.dataset.ids = [this.row_id];
5169 this.dataset.index = 0;
5171 this.dataset.index = null;
5173 var options = _.clone(self.options.form_view_options) || {};
5174 if (this.row_id !== null) {
5175 options.initial_mode = this.options.readonly ? "view" : "edit";
5178 $buttons: this.$buttonpane,
5180 this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
5181 if (this.options.alternative_form_view) {
5182 this.view_form.set_embedded_view(this.options.alternative_form_view);
5184 this.view_form.appendTo(this.$(".oe_popup_form").show());
5185 this.view_form.on("form_view_loaded", self, function() {
5186 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
5187 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
5188 multi_select: multi_select,
5189 readonly: self.row_id !== null && self.options.readonly,
5191 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
5192 $snbutton.click(function() {
5193 $.when(self.view_form.save()).done(function() {
5194 self.view_form.reload_mutex.exec(function() {
5195 self.view_form.on_button_new();
5199 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
5200 $sbutton.click(function() {
5201 $.when(self.view_form.save()).done(function() {
5202 self.view_form.reload_mutex.exec(function() {
5207 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
5208 $cbutton.click(function() {
5209 self.view_form.trigger('on_button_cancel');
5212 self.view_form.do_show();
5215 select_elements: function(element_ids) {
5216 this.trigger("elements_selected", element_ids);
5218 check_exit: function(no_destroy) {
5219 if (this.created_elements.length > 0) {
5220 this.select_elements(this.created_elements);
5221 this.created_elements = [];
5223 this.trigger('closed');
5226 destroy: function () {
5227 this.trigger('closed');
5228 if (this.$el.is(":data(bs.modal)")) {
5229 this.$el.parents('.modal').modal('hide');
5236 * Class to display a popup containing a form view.
5238 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
5239 show_element: function(model, row_id, context, options) {
5240 this.init_popup(model, row_id, [], context, options);
5241 _.defaults(this.options, {
5243 this.display_popup();
5247 this.init_dataset();
5248 this.setup_form_view();
5253 * Class to display a popup to display a list to search a row. It also allows
5254 * to switch to a form view to create a new row.
5256 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
5260 * - initial_view: form or search (default search)
5261 * - disable_multiple_selection
5262 * - list_view_options
5264 select_element: function(model, options, domain, context) {
5265 this.init_popup(model, null, domain, context, options);
5267 _.defaults(this.options, {
5268 initial_view: "search",
5270 this.initial_ids = this.options.initial_ids;
5271 this.display_popup();
5274 this.init_dataset();
5275 if (this.options.initial_view == "search") {
5276 var context = instance.web.pyeval.sync_eval_domains_and_contexts({
5278 contexts: [this.context]
5280 var search_defaults = {};
5281 _.each(context, function (value_, key) {
5282 var match = /^search_default_(.*)$/.exec(key);
5284 search_defaults[match[1]] = value_;
5287 this.setup_search_view(search_defaults);
5292 setup_search_view: function(search_defaults) {
5294 if (this.searchview) {
5295 this.searchview.destroy();
5297 var $buttons = this.$('.o-search-options');
5298 this.searchview = new instance.web.SearchView(this,
5299 this.dataset, false, search_defaults, {$buttons: $buttons});
5300 this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
5301 if (self.initial_ids) {
5302 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
5303 contexts.concat(self.context), groupbys);
5304 self.initial_ids = undefined;
5306 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
5309 this.searchview.appendTo(this.$(".o-popup-search")).done(function() {
5310 self.searchview.toggle_visibility(true);
5311 self.view_list = new instance.web.form.SelectCreateListView(self,
5312 self.dataset, false,
5313 _.extend({'deletable': false,
5314 'selectable': !self.options.disable_multiple_selection,
5315 'import_enabled': false,
5316 '$buttons': self.$buttonpane,
5317 'disable_editable_mode': true,
5318 '$pager': self.$('.oe_popup_list_pager'),
5319 }, self.options.list_view_options || {}));
5320 self.view_list.on('edit:before', self, function (e) {
5323 self.view_list.popup = self;
5324 self.view_list.appendTo(self.$(".oe_popup_list").show()).then(function() {
5325 self.view_list.do_show();
5326 }).then(function() {
5327 self.searchview.do_search();
5329 self.view_list.on("list_view_loaded", self, function() {
5330 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
5331 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
5332 $cbutton.click(function() {
5335 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
5336 $sbutton.click(function() {
5337 self.select_elements(self.selected_ids);
5340 $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
5341 $cbutton.click(function() {
5347 do_search: function(domains, contexts, groupbys) {
5349 instance.web.pyeval.eval_domains_and_contexts({
5350 domains: domains || [],
5351 contexts: contexts || [],
5352 group_by_seq: groupbys || []
5353 }).done(function (results) {
5354 self.view_list.do_search(results.domain, results.context, results.group_by);
5357 on_click_element: function(ids) {
5359 this.selected_ids = ids || [];
5360 if(this.selected_ids.length > 0) {
5361 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
5363 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
5366 new_object: function() {
5367 if (this.searchview) {
5368 this.searchview.do_hide();
5370 if (this.view_list) {
5371 this.view_list.do_hide();
5373 this.setup_form_view();
5377 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
5378 do_add_record: function () {
5379 this.popup.new_object();
5381 select_record: function(index) {
5382 this.popup.select_elements([this.dataset.ids[index]]);
5383 this.popup.destroy();
5385 do_select: function(ids, records) {
5386 this._super(ids, records);
5387 this.popup.on_click_element(ids);
5391 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5392 template: 'FieldReference',
5393 init: function(field_manager, node) {
5394 this._super(field_manager, node);
5395 this.reference_ready = true;
5397 destroy_content: function() {
5400 this.fm = undefined;
5403 initialize_content: function() {
5405 var fm = new instance.web.form.DefaultFieldManager(this);
5407 fm.extend_field_desc({
5409 selection: this.field_manager.get_field_desc(this.name).selection,
5417 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
5419 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5421 this.selection.on("change:value", this, this.on_selection_changed);
5422 this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
5424 .on('focused', null, function () {self.trigger('focused');})
5425 .on('blurred', null, function () {self.trigger('blurred');});
5427 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
5428 name: 'Referenced Document',
5429 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5431 this.m2o.on("change:value", this, this.data_changed);
5432 this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
5434 .on('focused', null, function () {self.trigger('focused');})
5435 .on('blurred', null, function () {self.trigger('blurred');});
5437 on_selection_changed: function() {
5438 if (this.reference_ready) {
5439 this.internal_set_value([this.selection.get_value(), false]);
5440 this.render_value();
5443 data_changed: function() {
5444 if (this.reference_ready) {
5445 this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
5448 set_value: function(val) {
5450 val = val.split(',');
5451 val[0] = val[0] || false;
5452 val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
5454 this._super(val || [false, false]);
5456 get_value: function() {
5457 return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
5459 render_value: function() {
5460 this.reference_ready = false;
5461 if (!this.get("effective_readonly")) {
5462 this.selection.set_value(this.get('value')[0]);
5464 this.m2o.field.relation = this.get('value')[0];
5465 this.m2o.set_value(this.get('value')[1]);
5466 this.m2o.$el.toggle(!!this.get('value')[0]);
5467 this.reference_ready = true;
5471 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5472 init: function(field_manager, node) {
5474 this._super(field_manager, node);
5475 this.binary_value = false;
5476 this.useFileAPI = !!window.FileReader;
5477 this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5478 if (!this.useFileAPI) {
5479 this.fileupload_id = _.uniqueId('oe_fileupload');
5480 $(window).on(this.fileupload_id, function() {
5481 var args = [].slice.call(arguments).slice(1);
5482 self.on_file_uploaded.apply(self, args);
5487 if (!this.useFileAPI) {
5488 $(window).off(this.fileupload_id);
5490 this._super.apply(this, arguments);
5492 initialize_content: function() {
5494 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5495 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5496 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5497 this.$el.find('.oe_form_binary_file_edit').click(function(event){
5498 self.$el.find('input.oe_form_binary_file').click();
5501 on_file_change: function(e) {
5503 var file_node = e.target;
5504 if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5505 if (this.useFileAPI) {
5506 var file = file_node.files[0];
5507 if (file.size > this.max_upload_size) {
5508 var msg = _t("The selected file exceed the maximum file size of %s.");
5509 instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5512 var filereader = new FileReader();
5513 filereader.readAsDataURL(file);
5514 filereader.onloadend = function(upload) {
5515 var data = upload.target.result;
5516 data = data.split(',')[1];
5517 self.on_file_uploaded(file.size, file.name, file.type, data);
5520 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5521 this.$el.find('form.oe_form_binary_form').submit();
5523 this.$el.find('.oe_form_binary_progress').show();
5524 this.$el.find('.oe_form_binary').hide();
5527 on_file_uploaded: function(size, name, content_type, file_base64) {
5528 if (size === false) {
5529 this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5530 // TODO: use openerp web crashmanager
5531 console.warn("Error while uploading file : ", name);
5533 this.filename = name;
5534 this.on_file_uploaded_and_valid.apply(this, arguments);
5536 this.$el.find('.oe_form_binary_progress').hide();
5537 this.$el.find('.oe_form_binary').show();
5539 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5541 on_save_as: function(ev) {
5542 var value = this.get('value');
5544 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5545 ev.stopPropagation();
5547 instance.web.blockUI();
5548 var c = instance.webclient.crashmanager;
5549 this.session.get_file({
5550 url: '/web/binary/saveas_ajax',
5551 data: {data: JSON.stringify({
5552 model: this.view.dataset.model,
5553 id: (this.view.datarecord.id || ''),
5555 filename_field: (this.node.attrs.filename || ''),
5556 data: instance.web.form.is_bin_size(value) ? null : value,
5557 context: this.view.dataset.get_context()
5559 complete: instance.web.unblockUI,
5560 error: c.rpc_error.bind(c)
5562 ev.stopPropagation();
5566 set_filename: function(value) {
5567 var filename = this.node.attrs.filename;
5570 tmp[filename] = value;
5571 this.field_manager.set_values(tmp);
5574 on_clear: function() {
5575 if (this.get('value') !== false) {
5576 this.binary_value = false;
5577 this.internal_set_value(false);
5583 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5584 template: 'FieldBinaryFile',
5585 initialize_content: function() {
5587 if (this.get("effective_readonly")) {
5589 this.$el.find('a').click(function(ev) {
5590 if (self.get('value')) {
5591 self.on_save_as(ev);
5597 render_value: function() {
5599 if (!this.get("effective_readonly")) {
5600 if (this.node.attrs.filename) {
5601 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5603 show_value = (this.get('value') !== null && this.get('value') !== undefined && this.get('value') !== false) ? this.get('value') : '';
5605 this.$el.find('input').eq(0).val(show_value);
5607 this.$el.find('a').toggle(!!this.get('value'));
5608 if (this.get('value')) {
5609 show_value = _t("Download");
5611 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5612 this.$el.find('a').text(show_value);
5616 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5617 this.binary_value = true;
5618 this.internal_set_value(file_base64);
5619 var show_value = name + " (" + instance.web.human_size(size) + ")";
5620 this.$el.find('input').eq(0).val(show_value);
5621 this.set_filename(name);
5623 on_clear: function() {
5624 this._super.apply(this, arguments);
5625 this.$el.find('input').eq(0).val('');
5626 this.set_filename('');
5630 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5631 template: 'FieldBinaryImage',
5632 placeholder: "/web/static/src/img/placeholder.png",
5633 render_value: function() {
5636 if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5637 url = 'data:image/png;base64,' + this.get('value');
5638 } else if (this.get('value')) {
5639 var id = JSON.stringify(this.view.datarecord.id || null);
5640 var field = this.name;
5641 if (this.options.preview_image)
5642 field = this.options.preview_image;
5643 url = this.session.url('/web/binary/image', {
5644 model: this.view.dataset.model,
5647 t: (new Date().getTime()),
5650 url = this.placeholder;
5652 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5653 $($img).click(function(e) {
5654 if(self.view.get("actual_mode") == "view") {
5655 var $button = $(".oe_form_button_edit");
5656 $button.openerpBounce();
5657 e.stopPropagation();
5660 this.$el.find('> img').remove();
5661 this.$el.prepend($img);
5662 $img.load(function() {
5663 if (! self.options.size)
5665 $img.css("max-width", "" + self.options.size[0] + "px");
5666 $img.css("max-height", "" + self.options.size[1] + "px");
5668 $img.on('error', function() {
5669 $img.attr('src', self.placeholder);
5670 instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5673 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5674 this.internal_set_value(file_base64);
5675 this.binary_value = true;
5676 this.render_value();
5677 this.set_filename(name);
5679 on_clear: function() {
5680 this._super.apply(this, arguments);
5681 this.render_value();
5682 this.set_filename('');
5684 set_value: function(value_){
5685 var changed = value_ !== this.get_value();
5686 this._super.apply(this, arguments);
5687 // By default, on binary images read, the server returns the binary size
5688 // This is possible that two images have the exact same size
5689 // Therefore we trigger the change in case the image value hasn't changed
5690 // So the image is re-rendered correctly
5692 this.trigger("change:value", this, {
5701 * Widget for (many2many field) to upload one or more file in same time and display in list.
5702 * The user can delete his files.
5703 * Options on attribute ; "blockui" {Boolean} block the UI or not
5704 * during the file is uploading
5706 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
5707 template: "FieldBinaryFileUploader",
5708 init: function(field_manager, node) {
5709 this._super(field_manager, node);
5710 this.field_manager = field_manager;
5712 if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5713 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);
5717 this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5718 this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5719 $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5723 this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5725 // WARNING: duplicated in 4 other M2M widgets
5726 set_value: function(value_) {
5727 value_ = value_ || [];
5728 if (value_.length >= 1 && value_[0] instanceof Array) {
5729 // value_ is a list of m2m commands. We only process
5730 // LINK_TO and REPLACE_WITH in this context
5732 _.each(value_, function (command) {
5733 if (command[0] === commands.LINK_TO) {
5734 val.push(command[1]); // (4, id[, _])
5735 } else if (command[0] === commands.REPLACE_WITH) {
5736 val = command[2]; // (6, _, ids)
5741 this._super(value_);
5743 get_value: function() {
5744 var tmp = [commands.replace_with(this.get("value"))];
5747 get_file_url: function (attachment) {
5748 return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5750 read_name_values : function () {
5752 // don't reset know values
5753 var ids = this.get('value');
5754 var _value = _.filter(ids, function (id) { return typeof self.data[id] == 'undefined'; } );
5755 // send request for get_name
5756 if (_value.length) {
5757 return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).then(function (datas) {
5758 _.each(datas, function (data) {
5759 data.no_unlink = true;
5760 data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5761 self.data[data.id] = data;
5769 render_value: function () {
5771 this.read_name_values().then(function (ids) {
5772 var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids}));
5773 render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5774 self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5776 // reinit input type file
5777 var $input = self.$('input.oe_form_binary_file');
5778 $input.after($input.clone(true)).remove();
5779 self.$(".oe_fileupload").show();
5783 on_file_change: function (event) {
5784 event.stopPropagation();
5786 var $target = $(event.target);
5787 if ($target.val() !== '') {
5788 var filename = $target.val().replace(/.*[\\\/]/,'');
5789 // don't uplode more of one file in same time
5790 if (self.data[0] && self.data[0].upload ) {
5793 for (var id in this.get('value')) {
5794 // if the files exits, delete the file before upload (if it's a new file)
5795 if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
5796 self.ds_file.unlink([id]);
5801 if(this.node.attrs.blockui>0) {
5802 instance.web.blockUI();
5805 // TODO : unactivate send on wizard and form
5808 this.$('form.oe_form_binary_form').submit();
5809 this.$(".oe_fileupload").hide();
5810 // add file on data result
5814 'filename': filename,
5820 on_file_loaded: function (event, result) {
5821 var files = this.get('value');
5824 if(this.node.attrs.blockui>0) {
5825 instance.web.unblockUI();
5828 if (result.error || !result.id ) {
5829 this.do_warn( _t('Uploading Error'), result.error);
5830 delete this.data[0];
5832 if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
5833 delete this.data[0];
5834 this.data[result.id] = {
5836 'name': result.name,
5837 'filename': result.filename,
5838 'url': this.get_file_url(result)
5841 this.data[result.id] = {
5843 'name': result.name,
5844 'filename': result.filename,
5845 'url': this.get_file_url(result)
5848 var values = _.clone(this.get('value'));
5849 values.push(result.id);
5850 this.set({'value': values});
5852 this.render_value();
5854 on_file_delete: function (event) {
5855 event.stopPropagation();
5856 var file_id=$(event.target).data("id");
5858 var files = _.filter(this.get('value'), function (id) {return id != file_id;});
5859 if(!this.data[file_id].no_unlink) {
5860 this.ds_file.unlink([file_id]);
5862 this.set({'value': files});
5867 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5868 template: "FieldStatus",
5869 init: function(field_manager, node) {
5870 this._super(field_manager, node);
5871 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5872 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5873 this.set({value: false});
5874 this.selection = {'unfolded': [], 'folded': []};
5875 this.set("selection", {'unfolded': [], 'folded': []});
5876 this.selection_dm = new instance.web.DropMisordered();
5877 this.dataset = new instance.web.DataSetStatic(this, this.field.relation, this.build_context());
5880 this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5882 this.on("change:value", this, this.get_selection);
5883 this.on("change:evaluated_selection_domain", this, this.get_selection);
5884 this.on("change:selection", this, function() {
5885 this.selection = this.get("selection");
5886 this.render_value();
5888 this.get_selection();
5889 if (this.options.clickable) {
5890 this.$el.on('click','li[data-id]',this.on_click_stage);
5892 if (this.$el.parent().is('header')) {
5893 this.$el.after('<div class="oe_clear"/>');
5897 set_value: function(value_) {
5898 if (value_ instanceof Array) {
5901 this._super(value_);
5903 render_value: function() {
5905 var content = QWeb.render("FieldStatus.content", {
5907 'value_folded': _.find(self.selection.folded, function(i){return i[0] === self.get('value');})
5909 self.$el.html(content);
5911 calc_domain: function() {
5912 var d = instance.web.pyeval.eval('domain', this.build_domain());
5913 var domain = []; //if there is no domain defined, fetch all the records
5916 domain = ['|',['id', '=', this.get('value')]].concat(d);
5919 if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5920 this.set("evaluated_selection_domain", domain);
5923 /** Get the selection and render it
5924 * selection: [[identifier, value_to_display], ...]
5925 * For selection fields: this is directly given by this.field.selection
5926 * For many2one fields: perform a search on the relation of the many2one field
5928 get_selection: function() {
5930 var selection_unfolded = [];
5931 var selection_folded = [];
5932 var fold_field = this.options.fold_field;
5934 var calculation = _.bind(function() {
5935 if (this.field.type == "many2one") {
5936 return self.get_distant_fields().then(function (fields) {
5937 return new instance.web.DataSetSearch(self, self.field.relation, self.build_context(), self.get("evaluated_selection_domain"))
5938 .read_slice(_.union(_.keys(self.distant_fields), ['id']), {}).then(function (records) {
5939 var ids = _.pluck(records, 'id');
5940 return self.dataset.name_get(ids).then(function (records_name) {
5941 _.each(records, function (record) {
5942 var name = _.find(records_name, function (val) {return val[0] == record.id;})[1];
5943 if (fold_field && record[fold_field] && record.id != self.get('value')) {
5944 selection_folded.push([record.id, name]);
5946 selection_unfolded.push([record.id, name]);
5953 // For field type selection filter values according to
5954 // statusbar_visible attribute of the field. For example:
5955 // statusbar_visible="draft,open".
5956 var select = this.field.selection;
5957 for(var i=0; i < select.length; i++) {
5958 var key = select[i][0];
5959 if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5960 selection_unfolded.push(select[i]);
5966 this.selection_dm.add(calculation()).then(function () {
5967 var selection = {'unfolded': selection_unfolded, 'folded': selection_folded};
5968 if (! _.isEqual(selection, self.get("selection"))) {
5969 self.set("selection", selection);
5974 * :deprecated: this feature will probably be removed with OpenERP v8
5976 get_distant_fields: function() {
5978 if (! this.options.fold_field) {
5979 this.distant_fields = {}
5981 if (this.distant_fields) {
5982 return $.when(this.distant_fields);
5984 return new instance.web.Model(self.field.relation).call("fields_get", [[this.options.fold_field]]).then(function(fields) {
5985 self.distant_fields = fields;
5989 on_click_stage: function (ev) {
5991 var $li = $(ev.currentTarget);
5993 if (this.field.type == "many2one") {
5994 val = parseInt($li.data("id"), 10);
5997 val = $li.data("id");
5999 if (val != self.get('value')) {
6000 this.view.recursive_save().done(function() {
6002 change[self.name] = val;
6003 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
6011 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
6012 template: "FieldMonetary",
6013 widget_class: 'oe_form_field_float oe_form_field_monetary',
6015 this._super.apply(this, arguments);
6016 this.set({"currency": false});
6017 if (this.options.currency_field) {
6018 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
6019 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
6022 this.on("change:currency", this, this.get_currency_info);
6023 this.get_currency_info();
6024 this.ci_dm = new instance.web.DropMisordered();
6027 var tmp = this._super();
6028 this.on("change:currency_info", this, this.reinitialize);
6031 get_currency_info: function() {
6033 if (this.get("currency") === false) {
6034 this.set({"currency_info": null});
6037 return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
6038 .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
6039 self.set({"currency_info": res});
6042 parse_value: function(val, def) {
6043 return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6045 format_value: function(val, def) {
6046 return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6051 This type of field display a list of checkboxes. It works only with m2ms. This field will display one checkbox for each
6052 record existing in the model targeted by the relation, according to the given domain if one is specified. Checked records
6053 will be added to the relation.
6055 instance.web.form.FieldMany2ManyCheckBoxes = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6056 className: "oe_form_many2many_checkboxes",
6058 this._super.apply(this, arguments);
6059 this.set("value", {});
6060 this.set("records", []);
6061 this.field_manager.on("view_content_has_changed", this, function() {
6062 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
6063 if (! _.isEqual(domain, this.get("domain"))) {
6064 this.set("domain", domain);
6067 this.records_orderer = new instance.web.DropMisordered();
6069 initialize_field: function() {
6070 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
6071 this.on("change:domain", this, this.query_records);
6072 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
6073 this.on("change:records", this, this.render_value);
6075 query_records: function() {
6077 var model = new openerp.Model(openerp.session, this.field.relation);
6078 this.records_orderer.add(model.call("search", [this.get("domain")], {"context": this.build_context()}).then(function(record_ids) {
6079 return model.call("name_get", [record_ids] , {"context": self.build_context()});
6080 })).then(function(res) {
6081 self.set("records", res);
6084 render_value: function() {
6085 this.$().html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this, selected: this.get("value")}));
6086 var inputs = this.$("input");
6087 inputs.change(_.bind(this.from_dom, this));
6088 if (this.get("effective_readonly"))
6089 inputs.attr("disabled", "true");
6091 from_dom: function() {
6093 this.$("input").each(function() {
6095 new_value[elem.data("record-id")] = elem.attr("checked") ? true : undefined;
6097 if (! _.isEqual(new_value, this.get("value")))
6098 this.internal_set_value(new_value);
6100 // WARNING: (mostly) duplicated in 4 other M2M widgets
6101 set_value: function(value_) {
6102 value_ = value_ || [];
6103 if (value_.length >= 1 && value_[0] instanceof Array) {
6104 // value_ is a list of m2m commands. We only process
6105 // LINK_TO and REPLACE_WITH in this context
6107 _.each(value_, function (command) {
6108 if (command[0] === commands.LINK_TO) {
6109 val.push(command[1]); // (4, id[, _])
6110 } else if (command[0] === commands.REPLACE_WITH) {
6111 val = command[2]; // (6, _, ids)
6117 _.each(value_, function(el) {
6118 formatted[JSON.stringify(el)] = true;
6120 this._super(formatted);
6122 get_value: function() {
6123 var value = _.filter(_.keys(this.get("value")), function(el) {
6124 return this.get("value")[el];
6126 value = _.map(value, function(el) {
6127 return JSON.parse(el);
6129 return [commands.replace_with(value)];
6134 This field can be applied on many2many and one2many. It is a read-only field that will display a single link whose name is
6135 "<number of linked records> <label of the field>". When the link is clicked, it will redirect to another act_window
6136 action on the model of the relation and show only the linked records.
6140 * 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
6141 to display (or False to take the default one) and the second element is the type of the view. Defaults to
6142 [[false, "tree"], [false, "form"]] .
6144 instance.web.form.X2ManyCounter = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6145 className: "oe_form_x2many_counter",
6147 this._super.apply(this, arguments);
6148 this.set("value", []);
6149 _.defaults(this.options, {
6150 "views": [[false, "tree"], [false, "form"]],
6153 render_value: function() {
6154 var text = _.str.sprintf("%d %s", this.val().length, this.string);
6155 this.$().html(QWeb.render("X2ManyCounter", {text: text}));
6156 this.$("a").click(_.bind(this.go_to, this));
6159 return this.view.recursive_save().then(_.bind(function() {
6160 var val = this.val();
6162 if (this.field.type === "one2many") {
6163 context["default_" + this.field.relation_field] = this.view.datarecord.id;
6165 var domain = [["id", "in", val]];
6166 return this.do_action({
6167 type: 'ir.actions.act_window',
6169 res_model: this.field.relation,
6170 views: this.options.views,
6178 var value = this.get("value") || [];
6179 if (value.length >= 1 && value[0] instanceof Array) {
6180 value = value[0][2];
6187 This widget is intended to be used on stat button numeric fields. It will display
6188 the value many2many and one2many. It is a read-only field that will
6189 display a simple string "<value of field> <label of the field>"
6191 instance.web.form.StatInfo = instance.web.form.AbstractField.extend({
6192 is_field_number: true,
6194 this._super.apply(this, arguments);
6195 this.internal_set_value(0);
6197 set_value: function(value_) {
6198 if (value_ === false || value_ === undefined) {
6201 this._super.apply(this, [value_]);
6203 render_value: function() {
6205 value: this.get("value") || 0,
6207 if (! this.node.attrs.nolabel) {
6208 options.text = this.string
6210 this.$el.html(QWeb.render("StatInfo", options));
6217 * Registry of form fields, called by :js:`instance.web.FormView`.
6219 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
6220 * will substitute to the <field> tags as defined in OpenERP's views.
6222 instance.web.form.widgets = new instance.web.Registry({
6223 'char' : 'instance.web.form.FieldChar',
6224 'id' : 'instance.web.form.FieldID',
6225 'email' : 'instance.web.form.FieldEmail',
6226 'url' : 'instance.web.form.FieldUrl',
6227 'text' : 'instance.web.form.FieldText',
6228 'html' : 'instance.web.form.FieldTextHtml',
6229 'char_domain': 'instance.web.form.FieldCharDomain',
6230 'date' : 'instance.web.form.FieldDate',
6231 'datetime' : 'instance.web.form.FieldDatetime',
6232 'selection' : 'instance.web.form.FieldSelection',
6233 'radio' : 'instance.web.form.FieldRadio',
6234 'many2one' : 'instance.web.form.FieldMany2One',
6235 'many2onebutton' : 'instance.web.form.Many2OneButton',
6236 'many2many' : 'instance.web.form.FieldMany2Many',
6237 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
6238 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
6239 'one2many' : 'instance.web.form.FieldOne2Many',
6240 'one2many_list' : 'instance.web.form.FieldOne2Many',
6241 'reference' : 'instance.web.form.FieldReference',
6242 'boolean' : 'instance.web.form.FieldBoolean',
6243 'float' : 'instance.web.form.FieldFloat',
6244 'percentpie': 'instance.web.form.FieldPercentPie',
6245 'barchart': 'instance.web.form.FieldBarChart',
6246 'integer': 'instance.web.form.FieldFloat',
6247 'float_time': 'instance.web.form.FieldFloat',
6248 'progressbar': 'instance.web.form.FieldProgressBar',
6249 'image': 'instance.web.form.FieldBinaryImage',
6250 'binary': 'instance.web.form.FieldBinaryFile',
6251 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
6252 'statusbar': 'instance.web.form.FieldStatus',
6253 'monetary': 'instance.web.form.FieldMonetary',
6254 'many2many_checkboxes': 'instance.web.form.FieldMany2ManyCheckBoxes',
6255 'x2many_counter': 'instance.web.form.X2ManyCounter',
6256 'priority':'instance.web.form.Priority',
6257 'kanban_state_selection':'instance.web.form.KanbanSelection',
6258 'statinfo': 'instance.web.form.StatInfo',
6262 * Registry of widgets usable in the form view that can substitute to any possible
6263 * tags defined in OpenERP's form views.
6265 * Every referenced class should extend FormWidget.
6267 instance.web.form.tags = new instance.web.Registry({
6268 'button' : 'instance.web.form.WidgetButton',
6271 instance.web.form.custom_widgets = new instance.web.Registry({
6276 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: