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 start: function () {
2343 this._super.apply(this, arguments);
2344 // hook on form view content changed: recompute the states, because it may be related to the current stage
2345 this.getParent().on('view_content_has_changed', self, function () {
2346 self.render_value();
2349 prepare_dropdown_selection: function() {
2352 var selection = this.field.selection || [];
2353 var stage_id = _.isArray(this.view.datarecord.stage_id) ? this.view.datarecord.stage_id[0] : this.view.datarecord.stage_id;
2354 var legend_field = this.options && this.options.states_legend_field || false;
2355 var fields_to_read = _.map(
2356 this.options && this.options.states_legend || {},
2357 function (value, key, list) { return value; });
2358 if (legend_field && fields_to_read && stage_id) {
2359 var fetch_stage = new openerp.web.DataSet(
2361 self.view.fields[legend_field].field.relation).read_ids([stage_id],
2364 else { var fetch_stage = $.Deferred().resolve(false); }
2365 return $.when(fetch_stage).then(function (stage_data) {
2366 _.map(selection, function(res) {
2370 'state_name': res[1],
2372 if (stage_data && stage_data[0][self.options.states_legend[res[0]]]) {
2373 value['state_name'] = stage_data[0][self.options.states_legend[res[0]]];
2375 if (res[0] == 'normal') { value['state_class'] = 'oe_kanban_status'; }
2376 else if (res[0] == 'done') { value['state_class'] = 'oe_kanban_status oe_kanban_status_green'; }
2377 else { value['state_class'] = 'oe_kanban_status oe_kanban_status_red'; }
2383 render_value: function() {
2385 this.record_id = this.view.datarecord.id;
2386 var dd_fetched = this.prepare_dropdown_selection();;
2387 return $.when(dd_fetched).then(function (states) {
2388 self.states = states;
2389 self.$el.html(QWeb.render("KanbanSelection", {'widget': self}));
2390 self.$el.find('li').on('click', self.set_kanban_selection.bind(self));
2393 /* setting the value: in view mode, perform an asynchronous call and reload
2394 the form view; in edit mode, use set_value to save the new value that will
2395 be written when saving the record. */
2396 set_kanban_selection: function (ev) {
2398 var li = $(ev.target).closest('li');
2400 var value = String(li.data('value'));
2401 if (this.view.get('actual_mode') == 'view') {
2402 var write_values = {}
2403 write_values[self.name] = value;
2404 return this.view.dataset._model.call(
2408 self.view.dataset.get_context()
2409 ]).done(self.reload_record.bind(self));
2412 return this.set_value(value);
2416 reload_record: function() {
2421 instance.web.form.Priority = instance.web.form.FieldChar.extend({
2422 init: function (field_manager, node) {
2423 this._super(field_manager, node);
2425 prepare_priority: function() {
2427 var selection = this.field.selection || [];
2428 var init_value = selection && selection[0][0] || 0;
2429 var data = _.map(selection.slice(1), function(element, index) {
2431 'value': element[0],
2433 'click_value': element[0],
2435 if (index == 0 && self.get('value') == element[0]) {
2436 value['click_value'] = init_value;
2442 render_value: function() {
2444 this.record_id = this.view.datarecord.id;
2445 this.priorities = this.prepare_priority();
2446 this.$el.html(QWeb.render("Priority", {'widget': this}));
2447 this.$el.find('li').on('click', this.set_priority.bind(this));
2449 /* setting the value: in view mode, perform an asynchronous call and reload
2450 the form view; in edit mode, use set_value to save the new value that will
2451 be written when saving the record. */
2452 set_priority: function (ev) {
2454 var li = $(ev.target).closest('li');
2456 var value = String(li.data('value'));
2457 if (this.view.get('actual_mode') == 'view') {
2458 var write_values = {}
2459 write_values[self.name] = value;
2460 return this.view.dataset._model.call(
2464 self.view.dataset.get_context()
2465 ]).done(self.reload_record.bind(self));
2468 return this.set_value(value);
2473 reload_record: function() {
2478 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2479 process_modifiers: function () {
2481 this.set({ readonly: true });
2485 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2486 template: 'FieldEmail',
2487 initialize_content: function() {
2489 var $button = this.$el.find('button');
2490 $button.click(this.on_button_clicked);
2491 this.setupFocus($button);
2493 render_value: function() {
2494 if (!this.get("effective_readonly")) {
2498 .attr('href', 'mailto:' + this.get('value'))
2499 .text(this.get('value') || '');
2502 on_button_clicked: function() {
2503 if (!this.get('value') || !this.is_syntax_valid()) {
2504 this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2506 location.href = 'mailto:' + this.get('value');
2511 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2512 template: 'FieldUrl',
2513 initialize_content: function() {
2515 var $button = this.$el.find('button');
2516 $button.click(this.on_button_clicked);
2517 this.setupFocus($button);
2519 render_value: function() {
2520 if (!this.get("effective_readonly")) {
2523 var tmp = this.get('value');
2524 var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2526 tmp = "http://" + this.get('value');
2528 var text = this.get('value') ? this.node.attrs.text || tmp : '';
2529 this.$el.find('a').attr('href', tmp).text(text);
2532 on_button_clicked: function() {
2533 if (!this.get('value')) {
2534 this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2536 var url = $.trim(this.get('value'));
2537 if(/^www\./i.test(url))
2538 url = 'http://'+url;
2544 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2545 is_field_number: true,
2546 widget_class: 'oe_form_field_float',
2547 init: function (field_manager, node) {
2548 this._super(field_manager, node);
2549 this.internal_set_value(0);
2550 if (this.node.attrs.digits) {
2551 this.digits = this.node.attrs.digits;
2553 this.digits = this.field.digits;
2556 set_value: function(value_) {
2557 if (value_ === false || value_ === undefined) {
2558 // As in GTK client, floats default to 0
2561 this._super.apply(this, [value_]);
2563 focus: function () {
2564 var $input = this.$('input:first');
2565 return $input.length ? $input.select() : false;
2569 instance.web.form.FieldCharDomain = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2570 init: function(field_manager, node) {
2571 this._super.apply(this, arguments);
2575 this._super.apply(this, arguments);
2576 this.on("change:effective_readonly", this, function () {
2577 this.display_field();
2579 this.display_field();
2580 return this._super();
2582 set_value: function(value_) {
2584 this.set('value', value_ || false);
2585 this.display_field();
2587 display_field: function() {
2589 this.$el.html(instance.web.qweb.render("FieldCharDomain", {widget: this}));
2590 if (this.get('value')) {
2591 var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2592 var domain = instance.web.pyeval.eval('domain', this.get('value'));
2593 var ds = new instance.web.DataSetStatic(self, model, self.build_context());
2594 ds.call('search_count', [domain]).then(function (results) {
2595 $('.oe_domain_count', self.$el).text(results + ' records selected');
2596 if (self.get('effective_readonly')) {
2597 $('button span', self.$el).text(' See selection');
2600 $('button span', self.$el).text(' Change selection');
2604 $('.oe_domain_count', this.$el).text('0 record selected');
2605 $('button span', this.$el).text(' Select records');
2607 this.$('.select_records').on('click', self.on_click);
2609 on_click: function(event) {
2610 event.preventDefault();
2612 var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2613 this.pop = new instance.web.form.SelectCreatePopup(this);
2614 this.pop.select_element(
2616 title: this.get('effective_readonly') ? 'Selected records' : 'Select records...',
2617 readonly: this.get('effective_readonly'),
2618 disable_multiple_selection: this.get('effective_readonly'),
2619 no_create: this.get('effective_readonly'),
2620 }, [], this.build_context());
2621 this.pop.on("elements_selected", self, function(element_ids) {
2622 if (this.pop.$('input.oe_list_record_selector').prop('checked')) {
2623 var search_data = this.pop.searchview.build_search_data();
2624 var domain_done = instance.web.pyeval.eval_domains_and_contexts({
2625 domains: search_data.domains,
2626 contexts: search_data.contexts,
2627 group_by_seq: search_data.groupbys || []
2628 }).then(function (results) {
2629 return results.domain;
2633 var domain = [["id", "in", element_ids]];
2634 var domain_done = $.Deferred().resolve(domain);
2636 $.when(domain_done).then(function (domain) {
2637 var domain = self.pop.dataset.domain.concat(domain || []);
2638 self.set_value(domain);
2644 instance.web.DateTimeWidget = instance.web.Widget.extend({
2645 template: "web.datepicker",
2646 type_of_date: "datetime",
2648 'dp.change .oe_datepicker_main': 'change_datetime',
2649 'dp.show .oe_datepicker_main': 'set_datetime_default',
2650 'keypress .oe_datepicker_master': 'change_datetime',
2652 init: function(parent) {
2653 this._super(parent);
2654 this.name = parent.name;
2658 var l10n = _t.database.parameters;
2662 startDate: moment({ y: 1900 }),
2663 endDate: moment().add(200, "y"),
2664 calendarWeeks: true,
2666 time: 'fa fa-clock-o',
2667 date: 'fa fa-calendar',
2668 up: 'fa fa-chevron-up',
2669 down: 'fa fa-chevron-down'
2671 language : moment.locale(),
2672 format : instance.web.normalize_format(l10n.date_format +' '+ l10n.time_format),
2674 this.$input = this.$el.find('input.oe_datepicker_master');
2675 if (this.type_of_date === 'date') {
2676 options['pickTime'] = false;
2677 options['useSeconds'] = false;
2678 options['format'] = instance.web.normalize_format(l10n.date_format);
2680 this.picker = this.$('.oe_datepicker_main').datetimepicker(options);
2681 this.set_readonly(false);
2682 this.set({'value': false});
2684 set_value: function(value_) {
2685 this.set({'value': value_});
2686 this.$input.val(value_ ? this.format_client(value_) : '');
2688 get_value: function() {
2689 return this.get('value');
2691 set_value_from_ui_: function() {
2692 var value_ = this.$input.val() || false;
2693 this.set_value(this.parse_client(value_));
2695 set_readonly: function(readonly) {
2696 this.readonly = readonly;
2697 this.$input.prop('readonly', this.readonly);
2699 is_valid_: function() {
2700 var value_ = this.$input.val();
2701 if (value_ === "") {
2705 this.parse_client(value_);
2712 parse_client: function(v) {
2713 return instance.web.parse_value(v, {"widget": this.type_of_date});
2715 format_client: function(v) {
2716 return instance.web.format_value(v, {"widget": this.type_of_date});
2718 set_datetime_default: function(){
2719 //when opening datetimepicker the date and time by default should be the one from
2720 //the input field if any or the current day otherwise
2721 if (this.type_of_date === 'datetime') {
2722 value = moment().second(0);
2723 if (this.$input.val().length !== 0 && this.is_valid_()){
2724 var value = this.$input.val();
2726 this.$('.oe_datepicker_main').data('DateTimePicker').setValue(value);
2729 change_datetime: function(e) {
2730 if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
2731 this.set_value_from_ui_();
2732 this.trigger("datetime_changed");
2735 commit_value: function () {
2736 this.change_datetime();
2740 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2741 type_of_date: "date"
2744 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2745 template: "FieldDatetime",
2746 build_widget: function() {
2747 return new instance.web.DateTimeWidget(this);
2749 destroy_content: function() {
2750 if (this.datewidget) {
2751 this.datewidget.destroy();
2752 this.datewidget = undefined;
2755 initialize_content: function() {
2756 if (!this.get("effective_readonly")) {
2757 this.datewidget = this.build_widget();
2758 this.datewidget.on('datetime_changed', this, _.bind(function() {
2759 this.internal_set_value(this.datewidget.get_value());
2761 this.datewidget.appendTo(this.$el);
2762 this.setupFocus(this.datewidget.$input);
2765 render_value: function() {
2766 if (!this.get("effective_readonly")) {
2767 this.datewidget.set_value(this.get('value'));
2769 this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2772 is_syntax_valid: function() {
2773 if (!this.get("effective_readonly") && this.datewidget) {
2774 return this.datewidget.is_valid_();
2778 is_false: function() {
2779 return this.get('value') === '' || this._super();
2782 var input = this.datewidget && this.datewidget.$input[0];
2783 return input ? input.focus() : false;
2785 set_dimensions: function (height, width) {
2786 this._super(height, width);
2787 if (!this.get("effective_readonly")) {
2788 this.datewidget.$input.css('height', height);
2793 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2794 template: "FieldDate",
2795 build_widget: function() {
2796 return new instance.web.DateWidget(this);
2800 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2801 template: 'FieldText',
2803 'keyup': function (e) {
2804 if (e.which === $.ui.keyCode.ENTER) {
2805 e.stopPropagation();
2808 'keypress': function (e) {
2809 if (e.which === $.ui.keyCode.ENTER) {
2810 e.stopPropagation();
2813 'change textarea': 'store_dom_value',
2815 initialize_content: function() {
2817 if (! this.get("effective_readonly")) {
2818 this.$textarea = this.$el.find('textarea');
2819 this.auto_sized = false;
2820 this.default_height = this.$textarea.css('height');
2821 if (this.get("effective_readonly")) {
2822 this.$textarea.attr('disabled', 'disabled');
2824 this.setupFocus(this.$textarea);
2826 this.$textarea = undefined;
2829 commit_value: function () {
2830 if (! this.get("effective_readonly") && this.$textarea) {
2831 this.store_dom_value();
2833 return this._super();
2835 store_dom_value: function () {
2836 this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2838 render_value: function() {
2839 if (! this.get("effective_readonly")) {
2840 var show_value = instance.web.format_value(this.get('value'), this, '');
2841 if (show_value === '') {
2842 this.$textarea.css('height', parseInt(this.default_height, 10)+"px");
2844 this.$textarea.val(show_value);
2845 if (! this.auto_sized) {
2846 this.auto_sized = true;
2847 this.$textarea.autosize();
2849 this.$textarea.trigger("autosize");
2852 var txt = this.get("value") || '';
2853 this.$(".oe_form_text_content").text(txt);
2856 is_syntax_valid: function() {
2857 if (!this.get("effective_readonly") && this.$textarea) {
2859 instance.web.parse_value(this.$textarea.val(), this, '');
2867 is_false: function() {
2868 return this.get('value') === '' || this._super();
2870 focus: function($el) {
2871 var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2872 return input ? input.focus() : false;
2874 set_dimensions: function (height, width) {
2875 this._super(height, width);
2876 if (!this.get("effective_readonly") && this.$textarea) {
2877 this.$textarea.css({
2886 * FieldTextHtml Widget
2887 * Intended for FieldText widgets meant to display HTML content. This
2888 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2889 * To find more information about CLEditor configutation: go to
2890 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2892 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2893 template: 'FieldTextHtml',
2895 this._super.apply(this, arguments);
2897 initialize_content: function() {
2899 if (! this.get("effective_readonly")) {
2900 self._updating_editor = false;
2901 this.$textarea = this.$el.find('textarea');
2902 var width = ((this.node.attrs || {}).editor_width || 'calc(100% - 4px)');
2903 var height = ((this.node.attrs || {}).editor_height || 250);
2904 this.$textarea.cleditor({
2905 width: width, // width not including margins, borders or padding
2906 height: height, // height not including margins, borders or padding
2907 controls: // controls to add to the toolbar
2908 "bold italic underline strikethrough " +
2909 "| removeformat | bullets numbering | outdent " +
2910 "indent | link unlink | source",
2911 bodyStyle: // style to assign to document body contained within the editor
2912 "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2914 this.$cleditor = this.$textarea.cleditor()[0];
2915 this.$cleditor.change(function() {
2916 if (! self._updating_editor) {
2917 self.$cleditor.updateTextArea();
2918 self.internal_set_value(self.$textarea.val());
2921 if (this.field.translate) {
2922 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"/>')
2923 .click(this.on_translate);
2924 this.$cleditor.$toolbar.append($img);
2928 render_value: function() {
2929 if (! this.get("effective_readonly")) {
2930 this.$textarea.val(this.get('value') || '');
2931 this._updating_editor = true;
2932 this.$cleditor.updateFrame();
2933 this._updating_editor = false;
2935 this.$el.html(this.get('value'));
2940 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2941 template: 'FieldBoolean',
2944 this.$checkbox = $("input", this.$el);
2945 this.setupFocus(this.$checkbox);
2946 this.$el.click(_.bind(function() {
2947 this.internal_set_value(this.$checkbox.is(':checked'));
2949 var check_readonly = function() {
2950 self.$checkbox.prop('disabled', self.get("effective_readonly"));
2951 self.click_disabled_boolean();
2953 this.on("change:effective_readonly", this, check_readonly);
2954 check_readonly.call(this);
2955 this._super.apply(this, arguments);
2957 render_value: function() {
2958 this.$checkbox[0].checked = this.get('value');
2961 var input = this.$checkbox && this.$checkbox[0];
2962 return input ? input.focus() : false;
2964 click_disabled_boolean: function(){
2965 var $disabled = this.$el.find('input[type=checkbox]:disabled');
2966 $disabled.each(function (){
2967 $(this).next('div').remove();
2968 $(this).closest("span").append($('<div class="boolean"></div>'));
2974 The progressbar field expect a float from 0 to 100.
2976 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2977 template: 'FieldProgressBar',
2978 render_value: function() {
2979 this.$el.progressbar({
2980 value: this.get('value') || 0,
2981 disabled: this.get("effective_readonly")
2983 var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
2984 this.$('span').html(formatted_value + '%');
2989 The PercentPie field expect a float from 0 to 100.
2991 instance.web.form.FieldPercentPie = instance.web.form.AbstractField.extend({
2992 template: 'FieldPercentPie',
2994 render_value: function() {
2995 var value = this.get('value'),
2996 formatted_value = Math.round(value || 0) + '%',
2997 svg = this.$('svg')[0];
3000 nv.addGraph(function() {
3001 var width = 42, height = 42;
3002 var chart = nv.models.pieChart()
3005 .margin({top: 0, right: 0, bottom: 0, left: 0})
3010 .color(['#7C7BAD','#DDD'])
3014 .datum([{'x': 'value', 'y': value}, {'x': 'complement', 'y': 100 - value}])
3017 .attr('style', 'width: ' + width + 'px; height:' + height + 'px;');
3021 .attr({x: width/2, y: height/2 + 3, 'text-anchor': 'middle'})
3022 .style({"font-size": "10px", "font-weight": "bold"})
3023 .text(formatted_value);
3032 The FieldBarChart expectsa list of values (indeed)
3034 instance.web.form.FieldBarChart = instance.web.form.AbstractField.extend({
3035 template: 'FieldBarChart',
3037 render_value: function() {
3038 var value = JSON.parse(this.get('value'));
3039 var svg = this.$('svg')[0];
3041 nv.addGraph(function() {
3042 var width = 34, height = 34;
3043 var chart = nv.models.discreteBarChart()
3044 .x(function (d) { return d.tooltip })
3045 .y(function (d) { return d.value })
3048 .margin({top: 0, right: 0, bottom: 0, left: 0})
3051 .transitionDuration(350)
3056 .datum([{key: 'values', values: value}])
3059 .attr('style', 'width: ' + (width + 4) + 'px; height: ' + (height + 8) + 'px;');
3061 nv.utils.windowResize(chart.update);
3070 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3071 template: 'FieldSelection',
3073 'change select': 'store_dom_value',
3075 init: function(field_manager, node) {
3077 this._super(field_manager, node);
3078 this.set("value", false);
3079 this.set("values", []);
3080 this.records_orderer = new instance.web.DropMisordered();
3081 this.field_manager.on("view_content_has_changed", this, function() {
3082 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
3083 if (! _.isEqual(domain, this.get("domain"))) {
3084 this.set("domain", domain);
3088 initialize_field: function() {
3089 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3090 this.on("change:domain", this, this.query_values);
3091 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
3092 this.on("change:values", this, this.render_value);
3094 query_values: function() {
3097 if (this.field.type === "many2one") {
3098 var model = new openerp.Model(openerp.session, this.field.relation);
3099 def = model.call("name_search", ['', this.get("domain")], {"context": this.build_context()});
3101 var values = _.reject(this.field.selection, function (v) { return v[0] === false && v[1] === ''; });
3102 def = $.when(values);
3104 this.records_orderer.add(def).then(function(values) {
3105 if (! _.isEqual(values, self.get("values"))) {
3106 self.set("values", values);
3110 initialize_content: function() {
3111 // Flag indicating whether we're in an event chain containing a change
3112 // event on the select, in order to know what to do on keyup[RETURN]:
3113 // * If the user presses [RETURN] as part of changing the value of a
3114 // selection, we should just let the value change and not let the
3115 // event broadcast further (e.g. to validating the current state of
3116 // the form in editable list view, which would lead to saving the
3117 // current row or switching to the next one)
3118 // * If the user presses [RETURN] with a select closed (side-effect:
3119 // also if the user opened the select and pressed [RETURN] without
3120 // changing the selected value), takes the action as validating the
3122 var ischanging = false;
3123 var $select = this.$el.find('select')
3124 .change(function () { ischanging = true; })
3125 .click(function () { ischanging = false; })
3126 .keyup(function (e) {
3127 if (e.which !== 13 || !ischanging) { return; }
3128 e.stopPropagation();
3131 this.setupFocus($select);
3133 commit_value: function () {
3134 this.store_dom_value();
3135 return this._super();
3137 store_dom_value: function () {
3138 if (!this.get('effective_readonly') && this.$('select').length) {
3139 var val = JSON.parse(this.$('select').val());
3140 this.internal_set_value(val);
3143 set_value: function(value_) {
3144 value_ = value_ === null ? false : value_;
3145 value_ = value_ instanceof Array ? value_[0] : value_;
3146 this._super(value_);
3148 render_value: function() {
3149 var values = this.get("values");
3150 values = [[false, this.node.attrs.placeholder || '']].concat(values);
3151 var found = _.find(values, function(el) { return el[0] === this.get("value"); }, this);
3153 found = [this.get("value"), _t('Unknown')];
3154 values = [found].concat(values);
3156 if (! this.get("effective_readonly")) {
3157 this.$().html(QWeb.render("FieldSelectionSelect", {widget: this, values: values}));
3158 this.$("select").val(JSON.stringify(found[0]));
3160 this.$el.text(found[1]);
3164 var input = this.$('select:first')[0];
3165 return input ? input.focus() : false;
3167 set_dimensions: function (height, width) {
3168 this._super(height, width);
3169 this.$('select').css({
3176 instance.web.form.FieldRadio = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3177 template: 'FieldRadio',
3179 'click input': 'click_change_value'
3181 init: function(field_manager, node) {
3182 /* Radio button widget: Attributes options:
3183 * - "horizontal" to display in column
3184 * - "no_radiolabel" don't display text values
3186 this._super(field_manager, node);
3187 this.selection = _.clone(this.field.selection) || [];
3188 this.domain = false;
3189 this.uniqueId = _.uniqueId("radio");
3191 initialize_content: function () {
3192 this.on("change:effective_readonly", this, this.render_value);
3193 this.field_manager.on("view_content_has_changed", this, this.get_selection);
3194 this.get_selection();
3196 click_change_value: function (event) {
3197 var val = $(event.target).val();
3198 val = this.field.type == "selection" ? val : +val;
3199 if (val == this.get_value()) {
3200 this.set_value(false);
3202 this.set_value(val);
3205 /** Get the selection and render it
3206 * selection: [[identifier, value_to_display], ...]
3207 * For selection fields: this is directly given by this.field.selection
3208 * For many2one fields: perform a search on the relation of the many2one field
3210 get_selection: function() {
3213 var def = $.Deferred();
3214 if (self.field.type == "many2one") {
3215 var domain = instance.web.pyeval.eval('domain', this.build_domain()) || [];
3216 if (! _.isEqual(self.domain, domain)) {
3217 self.domain = domain;
3218 var ds = new instance.web.DataSetStatic(self, self.field.relation, self.build_context());
3219 ds.call('search', [self.domain])
3220 .then(function (records) {
3221 ds.name_get(records).then(function (records) {
3222 selection = records;
3227 selection = self.selection;
3231 else if (self.field.type == "selection") {
3232 selection = self.field.selection || [];
3235 return def.then(function () {
3236 if (! _.isEqual(selection, self.selection)) {
3237 self.selection = _.clone(selection);
3238 self.renderElement();
3239 self.render_value();
3243 set_value: function (value_) {
3245 if (this.field.type == "selection") {
3246 value_ = _.find(this.field.selection, function (sel) { return sel[0] == value_;});
3248 else if (!this.selection.length) {
3249 this.selection = [value_];
3252 this._super(value_);
3254 get_value: function () {
3255 var value = this.get('value');
3256 return value instanceof Array ? value[0] : value;
3258 render_value: function () {
3260 this.$el.toggleClass("oe_readonly", this.get('effective_readonly'));
3261 this.$("input:checked").prop("checked", false);
3262 if (this.get_value()) {
3263 this.$("input").filter(function () {return this.value == self.get_value();}).prop("checked", true);
3264 this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : "");
3269 // jquery autocomplete tweak to allow html and classnames
3271 var proto = $.ui.autocomplete.prototype,
3272 initSource = proto._initSource;
3274 function filter( array, term ) {
3275 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
3276 return $.grep( array, function(value_) {
3277 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
3282 _initSource: function() {
3283 if ( this.options.html && $.isArray(this.options.source) ) {
3284 this.source = function( request, response ) {
3285 response( filter( this.options.source, request.term ) );
3288 initSource.call( this );
3292 _renderItem: function( ul, item) {
3293 return $( "<li></li>" )
3294 .data( "item.autocomplete", item )
3295 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
3297 .addClass(item.classname);
3303 A mixin containing some useful methods to handle completion inputs.
3305 The widget containing this option can have these arguments in its widget options:
3306 - no_quick_create: if true, it will disable the quick create
3308 instance.web.form.CompletionFieldMixin = {
3311 this.orderer = new instance.web.DropMisordered();
3314 * Call this method to search using a string.
3316 get_search_result: function(search_val) {
3319 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
3320 this.last_query = search_val;
3321 var exclusion_domain = [], ids_blacklist = this.get_search_blacklist();
3322 if (!_(ids_blacklist).isEmpty()) {
3323 exclusion_domain.push(['id', 'not in', ids_blacklist]);
3326 return this.orderer.add(dataset.name_search(
3327 search_val, new instance.web.CompoundDomain(self.build_domain(), exclusion_domain),
3328 'ilike', this.limit + 1, self.build_context())).then(function(data) {
3329 self.last_search = data;
3330 // possible selections for the m2o
3331 var values = _.map(data, function(x) {
3332 x[1] = x[1].split("\n")[0];
3334 label: _.str.escapeHTML(x[1]),
3341 // search more... if more results that max
3342 if (values.length > self.limit) {
3343 values = values.slice(0, self.limit);
3345 label: _t("Search More..."),
3346 action: function() {
3347 dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
3348 self._search_create_popup("search", data);
3351 classname: 'oe_m2o_dropdown_option'
3355 var raw_result = _(data.result).map(function(x) {return x[1];});
3356 if (search_val.length > 0 && !_.include(raw_result, search_val) &&
3357 ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3359 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
3360 $('<span />').text(search_val).html()),
3361 action: function() {
3362 self._quick_create(search_val);
3364 classname: 'oe_m2o_dropdown_option'
3368 if (!(self.options && (self.options.no_create || self.options.no_create_edit))){
3370 label: _t("Create and Edit..."),
3371 action: function() {
3372 self._search_create_popup("form", undefined, self._create_context(search_val));
3374 classname: 'oe_m2o_dropdown_option'
3377 else if (values.length == 0)
3379 label: _t("No results to show..."),
3380 action: function() {},
3381 classname: 'oe_m2o_dropdown_option'
3387 get_search_blacklist: function() {
3390 _quick_create: function(name) {
3392 var slow_create = function () {
3393 self._search_create_popup("form", undefined, self._create_context(name));
3395 if (self.options.quick_create === undefined || self.options.quick_create) {
3396 new instance.web.DataSet(this, this.field.relation, self.build_context())
3397 .name_create(name).done(function(data) {
3398 if (!self.get('effective_readonly'))
3399 self.add_id(data[0]);
3400 }).fail(function(error, event) {
3401 event.preventDefault();
3407 // all search/create popup handling
3408 _search_create_popup: function(view, ids, context) {
3410 var pop = new instance.web.form.SelectCreatePopup(this);
3412 self.field.relation,
3414 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3415 initial_ids: ids ? _.map(ids, function(x) {return x[0];}) : undefined,
3417 disable_multiple_selection: true
3419 self.build_domain(),
3420 new instance.web.CompoundContext(self.build_context(), context || {})
3422 pop.on("elements_selected", self, function(element_ids) {
3423 self.add_id(element_ids[0]);
3430 add_id: function(id) {},
3431 _create_context: function(name) {
3433 var field = (this.options || {}).create_name_field;
3434 if (field === undefined)
3436 if (field !== false && name && (this.options || {}).quick_create !== false)
3437 tmp["default_" + field] = name;
3442 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3443 template: "M2ODialog",
3444 init: function(parent) {
3445 this.name = parent.string;
3446 this._super(parent, {
3447 title: _.str.sprintf(_t("Create a %s"), parent.string),
3453 var text = _.str.sprintf(_t("You are creating a new %s, are you sure it does not exist yet?"), self.name);
3454 this.$("p").text( text );
3455 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3456 this.$("input").val(this.getParent().last_query);
3457 this.$buttons.find(".oe_form_m2o_qc_button").click(function(e){
3458 if (self.$("input").val() != ''){
3459 self.getParent()._quick_create(self.$("input").val());
3463 self.$("input").focus();
3466 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3467 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3470 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3476 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3477 template: "FieldMany2One",
3479 'keydown input': function (e) {
3481 case $.ui.keyCode.UP:
3482 case $.ui.keyCode.DOWN:
3483 e.stopPropagation();
3487 init: function(field_manager, node) {
3488 this._super(field_manager, node);
3489 instance.web.form.CompletionFieldMixin.init.call(this);
3490 this.set({'value': false});
3491 this.display_value = {};
3492 this.display_value_backup = {};
3493 this.last_search = [];
3494 this.floating = false;
3495 this.current_display = null;
3496 this.is_started = false;
3497 this.ignore_focusout = false;
3499 reinit_value: function(val) {
3500 this.internal_set_value(val);
3501 this.floating = false;
3502 if (this.is_started)
3503 this.render_value();
3505 initialize_field: function() {
3506 this.is_started = true;
3507 instance.web.bus.on('click', this, function() {
3508 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3509 this.$input.autocomplete("close");
3512 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3514 initialize_content: function() {
3515 if (!this.get("effective_readonly"))
3516 this.render_editable();
3518 destroy_content: function () {
3519 if (this.$drop_down) {
3520 this.$drop_down.off('click');
3521 delete this.$drop_down;
3524 this.$input.closest(".modal .modal-content").off('scroll');
3525 this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3526 'focus focusout change keydown');
3529 if (this.$follow_button) {
3530 this.$follow_button.off('blur focus click');
3531 delete this.$follow_button;
3534 destroy: function () {
3535 this.destroy_content();
3536 return this._super();
3538 init_error_displayer: function() {
3541 hide_error_displayer: function() {
3544 show_error_displayer: function() {
3545 new instance.web.form.M2ODialog(this).open();
3547 render_editable: function() {
3549 this.$input = this.$el.find("input");
3551 this.init_error_displayer();
3553 self.$input.on('focus', function() {
3554 self.hide_error_displayer();
3557 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3558 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3560 this.$follow_button.click(function(ev) {
3561 ev.preventDefault();
3562 if (!self.get('value')) {
3566 var pop = new instance.web.form.FormOpenPopup(self);
3567 var context = self.build_context().eval();
3568 var model_obj = new instance.web.Model(self.field.relation);
3569 model_obj.call('get_formview_id', [self.get("value"), context]).then(function(view_id){
3571 self.field.relation,
3573 self.build_context(),
3575 title: _t("Open: ") + self.string,
3579 pop.on('write_completed', self, function(){
3580 self.display_value = {};
3581 self.display_value_backup = {};
3582 self.render_value();
3584 self.trigger('changed_value');
3589 // some behavior for input
3590 var input_changed = function() {
3591 if (self.current_display !== self.$input.val()) {
3592 self.current_display = self.$input.val();
3593 if (self.$input.val() === "") {
3594 self.internal_set_value(false);
3595 self.floating = false;
3597 self.floating = true;
3601 this.$input.keydown(input_changed);
3602 this.$input.change(input_changed);
3603 this.$drop_down.click(function() {
3604 self.$input.focus();
3605 if (self.$input.autocomplete("widget").is(":visible")) {
3606 self.$input.autocomplete("close");
3608 if (self.get("value") && ! self.floating) {
3609 self.$input.autocomplete("search", "");
3611 self.$input.autocomplete("search");
3616 // Autocomplete close on dialog content scroll
3617 var close_autocomplete = _.debounce(function() {
3618 if (self.$input.autocomplete("widget").is(":visible")) {
3619 self.$input.autocomplete("close");
3622 this.$input.closest(".modal .modal-content").on('scroll', this, close_autocomplete);
3624 self.ed_def = $.Deferred();
3625 self.uned_def = $.Deferred();
3627 var ed_duration = 15000;
3628 var anyoneLoosesFocus = function (e) {
3629 if (self.ignore_focusout) { return; }
3631 if (self.floating) {
3632 if (self.last_search.length > 0) {
3633 if (self.last_search[0][0] != self.get("value")) {
3634 self.display_value = {};
3635 self.display_value_backup = {};
3636 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3637 self.reinit_value(self.last_search[0][0]);
3640 self.render_value();
3644 self.reinit_value(false);
3646 self.floating = false;
3648 if (used && self.get("value") === false && ! self.no_ed && ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3649 self.ed_def.reject();
3650 self.uned_def.reject();
3651 self.ed_def = $.Deferred();
3652 self.ed_def.done(function() {
3653 self.show_error_displayer();
3654 ignore_blur = false;
3655 self.trigger('focused');
3658 setTimeout(function() {
3659 self.ed_def.resolve();
3660 self.uned_def.reject();
3661 self.uned_def = $.Deferred();
3662 self.uned_def.done(function() {
3663 self.hide_error_displayer();
3665 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3669 self.ed_def.reject();
3672 var ignore_blur = false;
3674 focusout: anyoneLoosesFocus,
3675 focus: function () { self.trigger('focused'); },
3676 autocompleteopen: function () { ignore_blur = true; },
3677 autocompleteclose: function () { setTimeout(function() {ignore_blur = false;},0); },
3679 // autocomplete open
3680 if (ignore_blur) { $(this).focus(); return; }
3681 if (_(self.getChildren()).any(function (child) {
3682 return child instanceof instance.web.form.AbstractFormPopup;
3684 self.trigger('blurred');
3688 var isSelecting = false;
3690 this.$input.autocomplete({
3691 source: function(req, resp) {
3692 self.get_search_result(req.term).done(function(result) {
3696 select: function(event, ui) {
3700 self.display_value = {};
3701 self.display_value_backup = {};
3702 self.display_value["" + item.id] = item.name;
3703 self.reinit_value(item.id);
3704 } else if (item.action) {
3706 // Cancel widget blurring, to avoid form blur event
3707 self.trigger('focused');
3711 focus: function(e, ui) {
3715 // disabled to solve a bug, but may cause others
3716 //close: anyoneLoosesFocus,
3720 // set position for list of suggestions box
3721 this.$input.autocomplete( "option", "position", { my : "left top", at: "left bottom" } );
3722 this.$input.autocomplete("widget").openerpClass();
3723 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3724 this.$input.keyup(function(e) {
3725 if (e.which === 13) { // ENTER
3727 e.stopPropagation();
3729 isSelecting = false;
3731 this.setupFocus(this.$follow_button);
3733 render_value: function(no_recurse) {
3735 if (! this.get("value")) {
3736 this.display_string("");
3739 var display = this.display_value["" + this.get("value")];
3741 this.display_string(display);
3745 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3746 this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3748 self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
3751 self.display_value["" + self.get("value")] = data[0][1];
3752 self.render_value(true);
3753 }).fail( function (data, event) {
3754 // avoid displaying crash errors as many2One should be name_get compliant
3755 event.preventDefault();
3756 self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")];
3757 self.render_value(true);
3761 display_string: function(str) {
3763 if (!this.get("effective_readonly")) {
3764 this.$input.val(str.split("\n")[0]);
3765 this.current_display = this.$input.val();
3766 if (this.is_false()) {
3767 this.$('.oe_m2o_cm_button').css({'display':'none'});
3769 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3772 var lines = _.escape(str).split("\n");
3776 follow = _.rest(lines).join("<br />");
3779 var $link = this.$el.find('.oe_form_uri')
3782 if (! this.options.no_open)
3783 $link.click(function () {
3784 var context = self.build_context().eval();
3785 var model_obj = new instance.web.Model(self.field.relation);
3786 model_obj.call('get_formview_action', [self.get("value"), context]).then(function(action){
3787 self.do_action(action);
3791 $(".oe_form_m2o_follow", this.$el).html(follow);
3794 set_value: function(value_) {
3796 if (value_ instanceof Array) {
3797 this.display_value = {};
3798 this.display_value_backup = {};
3799 if (! this.options.always_reload) {
3800 this.display_value["" + value_[0]] = value_[1];
3803 this.display_value_backup["" + value_[0]] = value_[1];
3807 value_ = value_ || false;
3808 this.reinit_value(value_);
3810 get_displayed: function() {
3811 return this.display_value["" + this.get("value")];
3813 add_id: function(id) {
3814 this.display_value = {};
3815 this.display_value_backup = {};
3816 this.reinit_value(id);
3818 is_false: function() {
3819 return ! this.get("value");
3821 focus: function () {
3822 var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3823 return input ? input.focus() : false;
3825 _quick_create: function() {
3827 this.ed_def.reject();
3828 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3830 _search_create_popup: function() {
3832 this.ed_def.reject();
3833 this.ignore_focusout = true;
3834 this.reinit_value(false);
3835 var res = instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3836 this.ignore_focusout = false;
3840 set_dimensions: function (height, width) {
3841 this._super(height, width);
3842 if (!this.get("effective_readonly") && this.$input)
3843 this.$input.css('height', height);
3847 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3848 template: 'Many2OneButton',
3849 init: function(field_manager, node) {
3850 this._super.apply(this, arguments);
3853 this._super.apply(this, arguments);
3856 set_button: function() {
3859 this.$button.remove();
3862 this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3863 this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3864 this.$button.addClass('oe_link').css({'padding':'4px'});
3865 this.$el.append(this.$button);
3866 this.$button.on('click', self.on_click);
3868 on_click: function(ev) {
3870 this.popup = new instance.web.form.FormOpenPopup(this);
3871 this.popup.show_element(
3872 this.field.relation,
3874 this.build_context(),
3875 {title: this.string}
3877 this.popup.on('create_completed', self, function(r) {
3881 set_value: function(value_) {
3883 if (value_ instanceof Array) {
3886 value_ = value_ || false;
3887 this.set('value', value_);
3893 * Abstract-ish ListView.List subclass adding an "Add an item" row to replace
3894 * the big ugly button in the header.
3896 * Requires the implementation of a ``is_readonly`` method (usually a proxy to
3897 * the corresponding field's readonly or effective_readonly property) to
3898 * decide whether the special row should or should not be inserted.
3900 * Optionally an ``_add_row_class`` attribute can be set for the class(es) to
3901 * set on the insertion row.
3903 instance.web.form.AddAnItemList = instance.web.ListView.List.extend({
3904 pad_table_to: function (count) {
3905 if (!this.view.is_action_enabled('create') || this.is_readonly()) {
3910 this._super(count > 0 ? count - 1 : 0);
3913 var columns = _(this.columns).filter(function (column) {
3914 return column.invisible !== '1';
3916 if (this.options.selectable) { columns++; }
3917 if (this.options.deletable) { columns++; }
3919 var $cell = $('<td>', {
3921 'class': this._add_row_class || ''
3923 $('<a>', {href: '#'}).text(_t("Add an item"))
3924 .mousedown(function () {
3925 // FIXME: needs to be an official API somehow
3926 if (self.view.editor.is_editing()) {
3927 self.view.__ignore_blur = true;
3930 .click(function (e) {
3932 e.stopPropagation();
3933 // FIXME: there should also be an API for that one
3934 if (self.view.editor.form.__blur_timeout) {
3935 clearTimeout(self.view.editor.form.__blur_timeout);
3936 self.view.editor.form.__blur_timeout = false;
3938 self.view.ensure_saved().done(function () {
3939 self.view.do_add_record();
3943 var $padding = this.$current.find('tr:not([data-id]):first');
3944 var $newrow = $('<tr>').append($cell);
3945 if ($padding.length) {
3946 $padding.before($newrow);
3948 this.$current.append($newrow)
3954 # Values: (0, 0, { fields }) create
3955 # (1, ID, { fields }) update
3956 # (2, ID) remove (delete)
3957 # (3, ID) unlink one (target id or target of relation)
3959 # (5) unlink all (only valid for one2many)
3964 'create': function (values) {
3965 return [commands.CREATE, false, values];
3967 // (1, id, {values})
3969 'update': function (id, values) {
3970 return [commands.UPDATE, id, values];
3974 'delete': function (id) {
3975 return [commands.DELETE, id, false];
3977 // (3, id[, _]) removes relation, but not linked record itself
3979 'forget': function (id) {
3980 return [commands.FORGET, id, false];
3984 'link_to': function (id) {
3985 return [commands.LINK_TO, id, false];
3989 'delete_all': function () {
3990 return [5, false, false];
3992 // (6, _, ids) replaces all linked records with provided ids
3994 'replace_with': function (ids) {
3995 return [6, false, ids];
3998 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3999 multi_selection: false,
4000 disable_utility_classes: true,
4001 init: function(field_manager, node) {
4002 this._super(field_manager, node);
4003 this.is_loaded = $.Deferred();
4004 this.initial_is_loaded = this.is_loaded;
4005 this.form_last_update = $.Deferred();
4006 this.init_form_last_update = this.form_last_update;
4007 this.is_started = false;
4008 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
4009 this.dataset.o2m = this;
4010 this.dataset.parent_view = this.view;
4011 this.dataset.child_name = this.name;
4013 this.dataset.on('dataset_changed', this, function() {
4014 self.trigger_on_change();
4019 this._super.apply(this, arguments);
4020 this.$el.addClass('oe_form_field oe_form_field_one2many');
4025 this.is_loaded.done(function() {
4026 self.on("change:effective_readonly", self, function() {
4027 self.is_loaded = self.is_loaded.then(function() {
4028 self.viewmanager.destroy();
4029 return $.when(self.load_views()).done(function() {
4030 self.reload_current_view();
4035 this.is_started = true;
4036 this.reload_current_view();
4038 trigger_on_change: function() {
4039 this.trigger('changed_value');
4041 load_views: function() {
4044 var modes = this.node.attrs.mode;
4045 modes = !!modes ? modes.split(",") : ["tree"];
4047 _.each(modes, function(mode) {
4048 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
4049 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
4053 view_type: mode == "tree" ? "list" : mode,
4056 if (self.field.views && self.field.views[mode]) {
4057 view.embedded_view = self.field.views[mode];
4059 if(view.view_type === "list") {
4060 _.extend(view.options, {
4062 selectable: self.multi_selection,
4064 import_enabled: false,
4067 if (self.get("effective_readonly")) {
4068 _.extend(view.options, {
4073 } else if (view.view_type === "form") {
4074 if (self.get("effective_readonly")) {
4075 view.view_type = 'form';
4077 _.extend(view.options, {
4078 not_interactible_on_create: true,
4080 } else if (view.view_type === "kanban") {
4081 _.extend(view.options, {
4082 confirm_on_delete: false,
4084 if (self.get("effective_readonly")) {
4085 _.extend(view.options, {
4086 action_buttons: false,
4087 quick_creatable: false,
4089 read_only_mode: true,
4097 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
4098 this.viewmanager.o2m = self;
4099 var once = $.Deferred().done(function() {
4100 self.init_form_last_update.resolve();
4102 var def = $.Deferred().done(function() {
4103 self.initial_is_loaded.resolve();
4105 this.viewmanager.on("controller_inited", self, function(view_type, controller) {
4106 controller.o2m = self;
4107 if (view_type == "list") {
4108 if (self.get("effective_readonly")) {
4109 controller.on('edit:before', self, function (e) {
4112 _(controller.columns).find(function (column) {
4113 if (!(column instanceof instance.web.list.Handle)) {
4116 column.modifiers.invisible = true;
4120 } else if (view_type === "form") {
4121 if (self.get("effective_readonly")) {
4122 $(".oe_form_buttons", controller.$el).children().remove();
4124 controller.on("load_record", self, function(){
4127 controller.on('pager_action_executed',self,self.save_any_view);
4128 } else if (view_type == "graph") {
4129 self.reload_current_view();
4133 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
4134 $.when(self.save_any_view()).done(function() {
4135 if (n_mode === "list") {
4136 $.async_when().done(function() {
4137 self.reload_current_view();
4142 $.async_when().done(function () {
4143 self.viewmanager.appendTo(self.$el);
4147 reload_current_view: function() {
4149 self.is_loaded = self.is_loaded.then(function() {
4150 var view = self.get_active_view();
4151 if (view.type === "list") {
4152 return view.controller.reload_content();
4153 } else if (view.type === "form") {
4154 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
4155 self.dataset.index = 0;
4157 var act = function() {
4158 return view.controller.do_show();
4160 self.form_last_update = self.form_last_update.then(act, act);
4161 return self.form_last_update;
4162 } else if (view.controller.do_search) {
4163 return view.controller.do_search(self.build_domain(), self.dataset.get_context(), []);
4166 return self.is_loaded;
4168 get_active_view: function () {
4170 * Returns the current active view if any.
4172 return (this.viewmanager && this.viewmanager.active_view);
4174 set_value: function(value_) {
4175 value_ = value_ || [];
4177 var view = this.get_active_view();
4178 this.dataset.reset_ids([]);
4180 if(value_.length >= 1 && value_[0] instanceof Array) {
4182 _.each(value_, function(command) {
4183 var obj = {values: command[2]};
4184 switch (command[0]) {
4185 case commands.CREATE:
4186 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4188 self.dataset.to_create.push(obj);
4189 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4192 case commands.UPDATE:
4193 obj['id'] = command[1];
4194 self.dataset.to_write.push(obj);
4195 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4198 case commands.DELETE:
4199 self.dataset.to_delete.push({id: command[1]});
4201 case commands.LINK_TO:
4202 ids.push(command[1]);
4204 case commands.DELETE_ALL:
4205 self.dataset.delete_all = true;
4210 this.dataset.set_ids(ids);
4211 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
4213 this.dataset.delete_all = true;
4214 _.each(value_, function(command) {
4215 var obj = {values: command};
4216 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4218 self.dataset.to_create.push(obj);
4219 self.dataset.cache.push(_.clone(obj));
4223 this.dataset.set_ids(ids);
4225 this._super(value_);
4226 this.dataset.reset_ids(value_);
4228 if (this.dataset.index === null && this.dataset.ids.length > 0) {
4229 this.dataset.index = 0;
4231 this.trigger_on_change();
4232 if (this.is_started) {
4233 return self.reload_current_view();
4238 get_value: function() {
4242 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
4243 val = val.concat(_.map(this.dataset.ids, function(id) {
4244 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
4246 return commands.create(alter_order.values);
4248 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
4250 return commands.update(alter_order.id, alter_order.values);
4252 return commands.link_to(id);
4254 return val.concat(_.map(
4255 this.dataset.to_delete, function(x) {
4256 return commands['delete'](x.id);}));
4258 commit_value: function() {
4259 return this.save_any_view();
4261 save_any_view: function() {
4262 var view = this.get_active_view();
4264 if (this.viewmanager.active_view.type === "form") {
4265 if (view.controller.is_initialized.state() !== 'resolved') {
4266 return $.when(false);
4268 return $.when(view.controller.save());
4269 } else if (this.viewmanager.active_view.type === "list") {
4270 return $.when(view.controller.ensure_saved());
4273 return $.when(false);
4275 is_syntax_valid: function() {
4276 var view = this.get_active_view();
4280 switch (this.viewmanager.active_view.type) {
4282 return _(view.controller.fields).chain()
4287 return view.controller.is_valid();
4293 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
4294 template: 'One2Many.viewmanager',
4295 init: function(parent, dataset, views, flags) {
4296 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
4297 this.registry = instance.web.views.extend({
4298 list: 'instance.web.form.One2ManyListView',
4299 form: 'instance.web.form.One2ManyFormView',
4301 this.__ignore_blur = false;
4303 switch_mode: function(mode, unused) {
4304 if (mode !== 'form') {
4305 return this._super(mode, unused);
4308 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
4309 var pop = new instance.web.form.FormOpenPopup(this);
4310 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4311 title: _t("Open: ") + self.o2m.string,
4312 create_function: function(data, options) {
4313 return self.o2m.dataset.create(data, options).done(function(r) {
4314 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4315 self.o2m.dataset.trigger("dataset_changed", r);
4318 write_function: function(id, data, options) {
4319 return self.o2m.dataset.write(id, data, {}).done(function() {
4320 self.o2m.reload_current_view();
4323 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4324 parent_view: self.o2m.view,
4325 child_name: self.o2m.name,
4326 read_function: function() {
4327 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4329 form_view_options: {'not_interactible_on_create':true},
4330 readonly: self.o2m.get("effective_readonly")
4332 pop.on("elements_selected", self, function() {
4333 self.o2m.reload_current_view();
4338 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
4339 get_context: function() {
4340 this.context = this.o2m.build_context();
4341 return this.context;
4345 instance.web.form.One2ManyListView = instance.web.ListView.extend({
4346 _template: 'One2Many.listview',
4347 init: function (parent, dataset, view_id, options) {
4348 this._super(parent, dataset, view_id, _.extend(options || {}, {
4349 GroupsType: instance.web.form.One2ManyGroups,
4350 ListType: instance.web.form.One2ManyList
4352 this.on('edit:after', this, this.proxy('_after_edit'));
4353 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
4356 .bind('add', this.proxy("changed_records"))
4357 .bind('edit', this.proxy("changed_records"))
4358 .bind('remove', this.proxy("changed_records"));
4360 start: function () {
4361 var ret = this._super();
4363 .off('mousedown.handleButtons')
4364 .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
4367 changed_records: function () {
4368 this.o2m.trigger_on_change();
4370 is_valid: function () {
4372 if (!this.fields_view || !this.editable()){
4376 return _.every(this.records.records, function(record){
4378 _.each(self.editor.form.fields, function(field){
4379 field._inhibit_on_change_flag = true;
4380 field.set_value(r.attributes[field.name]);
4381 field._inhibit_on_change_flag = false;
4383 return _.every(self.editor.form.fields, function(field){
4384 field.process_modifiers();
4385 field._check_css_flags();
4386 return field.is_valid();
4390 do_add_record: function () {
4391 if (this.editable()) {
4392 this._super.apply(this, arguments);
4395 var pop = new instance.web.form.SelectCreatePopup(this);
4397 self.o2m.field.relation,
4399 title: _t("Create: ") + self.o2m.string,
4400 initial_view: "form",
4401 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4402 create_function: function(data, options) {
4403 return self.o2m.dataset.create(data, options).done(function(r) {
4404 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4405 self.o2m.dataset.trigger("dataset_changed", r);
4408 read_function: function() {
4409 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4411 parent_view: self.o2m.view,
4412 child_name: self.o2m.name,
4413 form_view_options: {'not_interactible_on_create':true}
4415 self.o2m.build_domain(),
4416 self.o2m.build_context()
4418 pop.on("elements_selected", self, function() {
4419 self.o2m.reload_current_view();
4423 do_activate_record: function(index, id) {
4425 var pop = new instance.web.form.FormOpenPopup(self);
4426 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4427 title: _t("Open: ") + self.o2m.string,
4428 write_function: function(id, data) {
4429 return self.o2m.dataset.write(id, data, {}).done(function() {
4430 self.o2m.reload_current_view();
4433 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4434 parent_view: self.o2m.view,
4435 child_name: self.o2m.name,
4436 read_function: function() {
4437 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4439 form_view_options: {'not_interactible_on_create':true},
4440 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
4443 do_button_action: function (name, id, callback) {
4444 if (!_.isNumber(id)) {
4445 instance.webclient.notification.warn(
4446 _t("Action Button"),
4447 _t("The o2m record must be saved before an action can be used"));
4450 var parent_form = this.o2m.view;
4452 this.ensure_saved().then(function () {
4454 return parent_form.save();
4457 }).done(function () {
4458 var ds = self.o2m.dataset;
4459 var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
4460 return value.length;
4462 if (!self.o2m.options.reload_on_button && !cached_records) {
4463 self.handle_button(name, id, callback);
4465 self.handle_button(name, id, function(){
4466 self.o2m.view.reload();
4472 _after_edit: function () {
4473 this.__ignore_blur = false;
4474 this.editor.form.on('blurred', this, this._on_form_blur);
4476 // The form's blur thing may be jiggered during the edition setup,
4477 // potentially leading to the o2m instasaving the row. Cancel any
4478 // blurring triggered the edition startup here
4479 this.editor.form.widgetFocused();
4481 _before_unedit: function () {
4482 this.editor.form.off('blurred', this, this._on_form_blur);
4484 _button_down: function () {
4485 // If a button is clicked (usually some sort of action button), it's
4486 // the button's responsibility to ensure the editable list is in the
4487 // correct state -> ignore form blurring
4488 this.__ignore_blur = true;
4491 * Handles blurring of the nested form (saves the currently edited row),
4492 * unless the flag to ignore the event is set to ``true``
4494 * Makes the internal form go away
4496 _on_form_blur: function () {
4497 if (this.__ignore_blur) {
4498 this.__ignore_blur = false;
4501 // FIXME: why isn't there an API for this?
4502 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4503 this.ensure_saved();
4506 this.cancel_edition();
4508 keypress_ENTER: function () {
4509 // blurring caused by hitting the [Return] key, should skip the
4510 // autosave-on-blur and let the handler for [Return] do its thing (save
4511 // the current row *anyway*, then create a new one/edit the next one)
4512 this.__ignore_blur = true;
4513 this._super.apply(this, arguments);
4515 do_delete: function (ids) {
4516 var confirm = window.confirm;
4517 window.confirm = function () { return true; };
4519 return this._super(ids);
4521 window.confirm = confirm;
4524 reload_record: function (record, options) {
4525 if (!options || !options['do_not_evict']) {
4526 // Evict record.id from cache to ensure it will be reloaded correctly
4527 this.dataset.evict_record(record.get('id'));
4530 return this._super(record);
4533 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4534 setup_resequence_rows: function () {
4535 if (!this.view.o2m.get('effective_readonly')) {
4536 this._super.apply(this, arguments);
4540 instance.web.form.One2ManyList = instance.web.form.AddAnItemList.extend({
4541 _add_row_class: 'oe_form_field_one2many_list_row_add',
4542 is_readonly: function () {
4543 return this.view.o2m.get('effective_readonly');
4547 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4548 form_template: 'One2Many.formview',
4549 load_form: function(data) {
4552 this.$buttons.find('button.oe_form_button_create').click(function() {
4553 self.save().done(self.on_button_new);
4556 do_notify_change: function() {
4557 if (this.dataset.parent_view) {
4558 this.dataset.parent_view.do_notify_change();
4560 this._super.apply(this, arguments);
4565 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4566 template: "FieldMany2ManyTags",
4567 tag_template: "FieldMany2ManyTag",
4569 this._super.apply(this, arguments);
4570 instance.web.form.CompletionFieldMixin.init.call(this);
4571 this.set({"value": []});
4572 this._display_orderer = new instance.web.DropMisordered();
4573 this._drop_shown = false;
4575 initialize_texttext: function(){
4578 plugins : 'tags arrow autocomplete',
4580 render: function(suggestion) {
4581 return $('<span class="text-label"/>').
4582 data('index', suggestion['index']).html(suggestion['label']);
4587 selectFromDropdown: function() {
4588 this.trigger('hideDropdown');
4589 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4590 var data = self.search_result[index];
4592 self.add_id(data.id);
4594 self.ignore_blur = true;
4597 this.trigger('setSuggestions', {result : []});
4601 isTagAllowed: function(tag) {
4605 removeTag: function(tag) {
4606 var id = tag.data("id");
4607 self.set({"value": _.without(self.get("value"), id)});
4609 renderTag: function(stuff) {
4610 return $.fn.textext.TextExtTags.prototype.renderTag.
4611 call(this, stuff).data("id", stuff.id);
4615 itemToString: function(item) {
4620 onSetInputData: function(e, data) {
4622 this._plugins.autocomplete._suggestions = null;
4624 this.input().val(data);
4630 initialize_content: function() {
4631 if (this.get("effective_readonly"))
4634 self.ignore_blur = false;
4635 self.$text = this.$("textarea");
4636 self.$text.textext(self.initialize_texttext()).bind('getSuggestions', function(e, data) {
4638 var str = !!data ? data.query || '' : '';
4639 self.get_search_result(str).done(function(result) {
4640 self.search_result = result;
4641 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4642 return _.extend(el, {index:i});
4645 }).bind('hideDropdown', function() {
4646 self._drop_shown = false;
4647 }).bind('showDropdown', function() {
4648 self._drop_shown = true;
4650 self.tags = self.$text.textext()[0].tags();
4652 .focusin(function () {
4653 self.trigger('focused');
4654 self.ignore_blur = false;
4656 .focusout(function() {
4657 self.$text.trigger("setInputData", "");
4658 if (!self.ignore_blur) {
4659 self.trigger('blurred');
4661 }).keydown(function(e) {
4662 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4663 self.$text.textext()[0].autocomplete().selectFromDropdown();
4667 // WARNING: duplicated in 4 other M2M widgets
4668 set_value: function(value_) {
4669 value_ = value_ || [];
4670 if (value_.length >= 1 && value_[0] instanceof Array) {
4671 // value_ is a list of m2m commands. We only process
4672 // LINK_TO and REPLACE_WITH in this context
4674 _.each(value_, function (command) {
4675 if (command[0] === commands.LINK_TO) {
4676 val.push(command[1]); // (4, id[, _])
4677 } else if (command[0] === commands.REPLACE_WITH) {
4678 val = command[2]; // (6, _, ids)
4683 this._super(value_);
4685 is_false: function() {
4686 return _(this.get("value")).isEmpty();
4688 get_value: function() {
4689 var tmp = [commands.replace_with(this.get("value"))];
4692 get_search_blacklist: function() {
4693 return this.get("value");
4695 map_tag: function(data){
4696 return _.map(data, function(el) {return {name: el[1], id:el[0]};})
4698 get_render_data: function(ids){
4700 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4701 return dataset.name_get(ids);
4703 render_tag: function(data) {
4705 if (! self.get("effective_readonly")) {
4706 self.tags.containerElement().children().remove();
4707 self.$('textarea').css("padding-left", "3px");
4708 self.tags.addTags(self.map_tag(data));
4710 self.$el.html(QWeb.render(self.tag_template, {elements: data}));
4713 render_value: function() {
4715 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4716 var values = self.get("value");
4717 var handle_names = function(data) {
4718 if (self.isDestroyed())
4721 _.each(data, function(el) {
4722 indexed[el[0]] = el;
4724 data = _.map(values, function(el) { return indexed[el]; });
4725 self.render_tag(data);
4727 if (! values || values.length > 0) {
4728 return this._display_orderer.add(self.get_render_data(values)).done(handle_names);
4733 add_id: function(id) {
4734 this.set({'value': _.uniq(this.get('value').concat([id]))});
4736 focus: function () {
4737 var input = this.$text && this.$text[0];
4738 return input ? input.focus() : false;
4740 set_dimensions: function (height, width) {
4741 this._super(height, width);
4742 this.$("textarea").css({
4747 _search_create_popup: function() {
4748 self.ignore_blur = true;
4749 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
4755 - reload_on_button: Reload the whole form view if click on a button in a list view.
4756 If you see this options, do not use it, it's basically a dirty hack to make one
4757 precise o2m to behave the way we want.
4759 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4760 multi_selection: false,
4761 disable_utility_classes: true,
4762 init: function(field_manager, node) {
4763 this._super(field_manager, node);
4764 this.is_loaded = $.Deferred();
4765 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4766 this.dataset.m2m = this;
4768 this.dataset.on('unlink', self, function(ids) {
4769 self.dataset_changed();
4772 this.list_dm = new instance.web.DropMisordered();
4773 this.render_value_dm = new instance.web.DropMisordered();
4775 initialize_content: function() {
4778 this.$el.addClass('oe_form_field oe_form_field_many2many');
4780 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4782 'deletable': this.get("effective_readonly") ? false : true,
4783 'selectable': this.multi_selection,
4785 'reorderable': false,
4786 'import_enabled': false,
4788 var embedded = (this.field.views || {}).tree;
4790 this.list_view.set_embedded_view(embedded);
4792 this.list_view.m2m_field = this;
4793 var loaded = $.Deferred();
4794 this.list_view.on("list_view_loaded", this, function() {
4797 this.list_view.appendTo(this.$el);
4799 var old_def = self.is_loaded;
4800 self.is_loaded = $.Deferred().done(function() {
4803 this.list_dm.add(loaded).then(function() {
4804 self.is_loaded.resolve();
4807 destroy_content: function() {
4808 this.list_view.destroy();
4809 this.list_view = undefined;
4811 // WARNING: duplicated in 4 other M2M widgets
4812 set_value: function(value_) {
4813 value_ = value_ || [];
4814 if (value_.length >= 1 && value_[0] instanceof Array) {
4815 // value_ is a list of m2m commands. We only process
4816 // LINK_TO and REPLACE_WITH in this context
4818 _.each(value_, function (command) {
4819 if (command[0] === commands.LINK_TO) {
4820 val.push(command[1]); // (4, id[, _])
4821 } else if (command[0] === commands.REPLACE_WITH) {
4822 val = command[2]; // (6, _, ids)
4827 this._super(value_);
4829 get_value: function() {
4830 return [commands.replace_with(this.get('value'))];
4832 is_false: function () {
4833 return _(this.get("value")).isEmpty();
4835 render_value: function() {
4837 this.dataset.set_ids(this.get("value"));
4838 this.render_value_dm.add(this.is_loaded).then(function() {
4839 return self.list_view.reload_content();
4842 dataset_changed: function() {
4843 this.internal_set_value(this.dataset.ids);
4847 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4848 get_context: function() {
4849 this.context = this.m2m.build_context();
4850 return this.context;
4856 * @extends instance.web.ListView
4858 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4859 init: function (parent, dataset, view_id, options) {
4860 this._super(parent, dataset, view_id, _.extend(options || {}, {
4861 ListType: instance.web.form.Many2ManyList,
4864 do_add_record: function () {
4865 var pop = new instance.web.form.SelectCreatePopup(this);
4869 title: _t("Add: ") + this.m2m_field.string,
4870 no_create: this.m2m_field.options.no_create,
4872 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4873 this.m2m_field.build_context()
4876 pop.on("elements_selected", self, function(element_ids) {
4878 _(element_ids).each(function (id) {
4879 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4880 self.dataset.set_ids(self.dataset.ids.concat([id]));
4881 self.m2m_field.dataset_changed();
4886 self.reload_content();
4890 do_activate_record: function(index, id) {
4892 var pop = new instance.web.form.FormOpenPopup(this);
4893 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4894 title: _t("Open: ") + this.m2m_field.string,
4895 readonly: this.getParent().get("effective_readonly")
4897 pop.on('write_completed', self, self.reload_content);
4899 do_button_action: function(name, id, callback) {
4901 var _sup = _.bind(this._super, this);
4902 if (! this.m2m_field.options.reload_on_button) {
4903 return _sup(name, id, callback);
4905 return this.m2m_field.view.save().then(function() {
4906 return _sup(name, id, function() {
4907 self.m2m_field.view.reload();
4912 is_action_enabled: function () { return true; },
4914 instance.web.form.Many2ManyList = instance.web.form.AddAnItemList.extend({
4915 _add_row_class: 'oe_form_field_many2many_list_row_add',
4916 is_readonly: function () {
4917 return this.view.m2m_field.get('effective_readonly');
4921 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4922 disable_utility_classes: true,
4923 init: function(field_manager, node) {
4924 this._super(field_manager, node);
4925 instance.web.form.CompletionFieldMixin.init.call(this);
4926 m2m_kanban_lazy_init();
4927 this.is_loaded = $.Deferred();
4928 this.initial_is_loaded = this.is_loaded;
4931 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4932 this.dataset.m2m = this;
4933 this.dataset.on('unlink', self, function(ids) {
4934 self.dataset_changed();
4938 this._super.apply(this, arguments);
4943 self.on("change:effective_readonly", self, function() {
4944 self.is_loaded = self.is_loaded.then(function() {
4945 self.kanban_view.destroy();
4946 return $.when(self.load_view()).done(function() {
4947 self.render_value();
4952 // WARNING: duplicated in 4 other M2M widgets
4953 set_value: function(value_) {
4954 value_ = value_ || [];
4955 if (value_.length >= 1 && value_[0] instanceof Array) {
4956 // value_ is a list of m2m commands. We only process
4957 // LINK_TO and REPLACE_WITH in this context
4959 _.each(value_, function (command) {
4960 if (command[0] === commands.LINK_TO) {
4961 val.push(command[1]); // (4, id[, _])
4962 } else if (command[0] === commands.REPLACE_WITH) {
4963 val = command[2]; // (6, _, ids)
4968 this._super(value_);
4970 get_value: function() {
4971 return [commands.replace_with(this.get('value'))];
4973 load_view: function() {
4975 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4976 'create_text': _t("Add"),
4977 'creatable': self.get("effective_readonly") ? false : true,
4978 'quick_creatable': self.get("effective_readonly") ? false : true,
4979 'read_only_mode': self.get("effective_readonly") ? true : false,
4980 'confirm_on_delete': false,
4982 var embedded = (this.field.views || {}).kanban;
4984 this.kanban_view.set_embedded_view(embedded);
4986 this.kanban_view.m2m = this;
4987 var loaded = $.Deferred();
4988 this.kanban_view.on("kanban_view_loaded",self,function() {
4989 self.initial_is_loaded.resolve();
4992 this.kanban_view.on('switch_mode', this, this.open_popup);
4993 $.async_when().done(function () {
4994 self.kanban_view.appendTo(self.$el);
4998 render_value: function() {
5000 this.dataset.set_ids(this.get("value"));
5001 this.is_loaded = this.is_loaded.then(function() {
5002 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
5005 dataset_changed: function() {
5006 this.set({'value': this.dataset.ids});
5008 open_popup: function(type, unused) {
5009 if (type !== "form")
5013 if (this.dataset.index === null) {
5014 pop = new instance.web.form.SelectCreatePopup(this);
5016 this.field.relation,
5018 title: _t("Add: ") + this.string
5020 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
5021 this.build_context()
5023 pop.on("elements_selected", self, function(element_ids) {
5024 _.each(element_ids, function(one_id) {
5025 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
5026 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
5027 self.dataset_changed();
5028 self.render_value();
5033 var id = self.dataset.ids[self.dataset.index];
5034 pop = new instance.web.form.FormOpenPopup(this);
5035 pop.show_element(self.field.relation, id, self.build_context(), {
5036 title: _t("Open: ") + self.string,
5037 write_function: function(id, data, options) {
5038 return self.dataset.write(id, data, {}).done(function() {
5039 self.render_value();
5042 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
5043 parent_view: self.view,
5044 child_name: self.name,
5045 readonly: self.get("effective_readonly")
5049 add_id: function(id) {
5050 this.quick_create.add_id(id);
5054 function m2m_kanban_lazy_init() {
5055 if (instance.web.form.Many2ManyKanbanView)
5057 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
5058 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
5059 _is_quick_create_enabled: function() {
5060 return this._super() && ! this.group_by;
5063 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
5064 template: 'Many2ManyKanban.quick_create',
5067 * close_btn: If true, the widget will display a "Close" button able to trigger
5070 init: function(parent, dataset, context, buttons) {
5071 this._super(parent);
5072 this.m2m = this.getParent().view.m2m;
5073 this.m2m.quick_create = this;
5074 this._dataset = dataset;
5075 this._buttons = buttons || false;
5076 this._context = context || {};
5078 start: function () {
5080 self.$text = this.$el.find('input').css("width", "200px");
5081 self.$text.textext({
5082 plugins : 'arrow autocomplete',
5084 render: function(suggestion) {
5085 return $('<span class="text-label"/>').
5086 data('index', suggestion['index']).html(suggestion['label']);
5091 selectFromDropdown: function() {
5092 $(this).trigger('hideDropdown');
5093 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
5094 var data = self.search_result[index];
5096 self.add_id(data.id);
5103 itemToString: function(item) {
5108 }).bind('getSuggestions', function(e, data) {
5110 var str = !!data ? data.query || '' : '';
5111 self.m2m.get_search_result(str).done(function(result) {
5112 self.search_result = result;
5113 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
5114 return _.extend(el, {index:i});
5118 self.$text.focusout(function() {
5123 this.$text[0].focus();
5125 add_id: function(id) {
5128 self.trigger('added', id);
5129 this.m2m.dataset_changed();
5135 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
5137 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
5138 template: "AbstractFormPopup.render",
5141 * -readonly: only applicable when not in creation mode, default to false
5142 * - alternative_form_view
5149 * - form_view_options
5151 init_popup: function(model, row_id, domain, context, options) {
5152 this.row_id = row_id;
5154 this.domain = domain || [];
5155 this.context = context || {};
5156 this.options = options;
5157 _.defaults(this.options, {});
5159 init_dataset: function() {
5161 this.created_elements = [];
5162 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
5163 this.dataset.read_function = this.options.read_function;
5164 this.dataset.create_function = function(data, options, sup) {
5165 var fct = self.options.create_function || sup;
5166 return fct.call(this, data, options).done(function(r) {
5167 self.trigger('create_completed saved', r);
5168 self.created_elements.push(r);
5171 this.dataset.write_function = function(id, data, options, sup) {
5172 var fct = self.options.write_function || sup;
5173 return fct.call(this, id, data, options).done(function(r) {
5174 self.trigger('write_completed saved', r);
5177 this.dataset.parent_view = this.options.parent_view;
5178 this.dataset.child_name = this.options.child_name;
5180 display_popup: function() {
5182 this.renderElement();
5183 var dialog = new instance.web.Dialog(this, {
5184 dialogClass: 'oe_act_window',
5185 title: this.options.title || "",
5186 }, this.$el).open();
5187 dialog.on('closing', this, function (e){
5188 self.check_exit(true);
5190 this.$buttonpane = dialog.$buttons;
5193 setup_form_view: function() {
5196 this.dataset.ids = [this.row_id];
5197 this.dataset.index = 0;
5199 this.dataset.index = null;
5201 var options = _.clone(self.options.form_view_options) || {};
5202 if (this.row_id !== null) {
5203 options.initial_mode = this.options.readonly ? "view" : "edit";
5206 $buttons: this.$buttonpane,
5208 this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
5209 if (this.options.alternative_form_view) {
5210 this.view_form.set_embedded_view(this.options.alternative_form_view);
5212 this.view_form.appendTo(this.$(".oe_popup_form").show());
5213 this.view_form.on("form_view_loaded", self, function() {
5214 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
5215 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
5216 multi_select: multi_select,
5217 readonly: self.row_id !== null && self.options.readonly,
5219 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
5220 $snbutton.click(function() {
5221 $.when(self.view_form.save()).done(function() {
5222 self.view_form.reload_mutex.exec(function() {
5223 self.view_form.on_button_new();
5227 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
5228 $sbutton.click(function() {
5229 $.when(self.view_form.save()).done(function() {
5230 self.view_form.reload_mutex.exec(function() {
5235 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
5236 $cbutton.click(function() {
5237 self.view_form.trigger('on_button_cancel');
5240 self.view_form.do_show();
5243 select_elements: function(element_ids) {
5244 this.trigger("elements_selected", element_ids);
5246 check_exit: function(no_destroy) {
5247 if (this.created_elements.length > 0) {
5248 this.select_elements(this.created_elements);
5249 this.created_elements = [];
5251 this.trigger('closed');
5254 destroy: function () {
5255 this.trigger('closed');
5256 if (this.$el.is(":data(bs.modal)")) {
5257 this.$el.parents('.modal').modal('hide');
5264 * Class to display a popup containing a form view.
5266 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
5267 show_element: function(model, row_id, context, options) {
5268 this.init_popup(model, row_id, [], context, options);
5269 _.defaults(this.options, {
5271 this.display_popup();
5275 this.init_dataset();
5276 this.setup_form_view();
5281 * Class to display a popup to display a list to search a row. It also allows
5282 * to switch to a form view to create a new row.
5284 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
5288 * - initial_view: form or search (default search)
5289 * - disable_multiple_selection
5290 * - list_view_options
5292 select_element: function(model, options, domain, context) {
5293 this.init_popup(model, null, domain, context, options);
5295 _.defaults(this.options, {
5296 initial_view: "search",
5298 this.initial_ids = this.options.initial_ids;
5299 this.display_popup();
5302 this.init_dataset();
5303 if (this.options.initial_view == "search") {
5304 var context = instance.web.pyeval.sync_eval_domains_and_contexts({
5306 contexts: [this.context]
5308 var search_defaults = {};
5309 _.each(context, function (value_, key) {
5310 var match = /^search_default_(.*)$/.exec(key);
5312 search_defaults[match[1]] = value_;
5315 this.setup_search_view(search_defaults);
5320 setup_search_view: function(search_defaults) {
5322 if (this.searchview) {
5323 this.searchview.destroy();
5325 var $buttons = this.$('.o-search-options');
5326 this.searchview = new instance.web.SearchView(this,
5327 this.dataset, false, search_defaults, {$buttons: $buttons});
5328 this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
5329 if (self.initial_ids) {
5330 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
5331 contexts.concat(self.context), groupbys);
5332 self.initial_ids = undefined;
5334 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
5337 this.searchview.appendTo(this.$(".o-popup-search")).done(function() {
5338 self.searchview.toggle_visibility(true);
5339 self.view_list = new instance.web.form.SelectCreateListView(self,
5340 self.dataset, false,
5341 _.extend({'deletable': false,
5342 'selectable': !self.options.disable_multiple_selection,
5343 'import_enabled': false,
5344 '$buttons': self.$buttonpane,
5345 'disable_editable_mode': true,
5346 '$pager': self.$('.oe_popup_list_pager'),
5347 }, self.options.list_view_options || {}));
5348 self.view_list.on('edit:before', self, function (e) {
5351 self.view_list.popup = self;
5352 self.view_list.appendTo(self.$(".oe_popup_list").show()).then(function() {
5353 self.view_list.do_show();
5354 }).then(function() {
5355 self.searchview.do_search();
5357 self.view_list.on("list_view_loaded", self, function() {
5358 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
5359 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
5360 $cbutton.click(function() {
5363 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
5364 $sbutton.click(function() {
5365 self.select_elements(self.selected_ids);
5368 $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
5369 $cbutton.click(function() {
5375 do_search: function(domains, contexts, groupbys) {
5377 instance.web.pyeval.eval_domains_and_contexts({
5378 domains: domains || [],
5379 contexts: contexts || [],
5380 group_by_seq: groupbys || []
5381 }).done(function (results) {
5382 self.view_list.do_search(results.domain, results.context, results.group_by);
5385 on_click_element: function(ids) {
5387 this.selected_ids = ids || [];
5388 if(this.selected_ids.length > 0) {
5389 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
5391 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
5394 new_object: function() {
5395 if (this.searchview) {
5396 this.searchview.do_hide();
5398 if (this.view_list) {
5399 this.view_list.do_hide();
5401 this.setup_form_view();
5405 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
5406 do_add_record: function () {
5407 this.popup.new_object();
5409 select_record: function(index) {
5410 this.popup.select_elements([this.dataset.ids[index]]);
5411 this.popup.destroy();
5413 do_select: function(ids, records) {
5414 this._super(ids, records);
5415 this.popup.on_click_element(ids);
5419 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5420 template: 'FieldReference',
5421 init: function(field_manager, node) {
5422 this._super(field_manager, node);
5423 this.reference_ready = true;
5425 destroy_content: function() {
5428 this.fm = undefined;
5431 initialize_content: function() {
5433 var fm = new instance.web.form.DefaultFieldManager(this);
5435 fm.extend_field_desc({
5437 selection: this.field_manager.get_field_desc(this.name).selection,
5445 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
5447 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5449 this.selection.on("change:value", this, this.on_selection_changed);
5450 this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
5452 .on('focused', null, function () {self.trigger('focused');})
5453 .on('blurred', null, function () {self.trigger('blurred');});
5455 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
5456 name: 'Referenced Document',
5457 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5459 this.m2o.on("change:value", this, this.data_changed);
5460 this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
5462 .on('focused', null, function () {self.trigger('focused');})
5463 .on('blurred', null, function () {self.trigger('blurred');});
5465 on_selection_changed: function() {
5466 if (this.reference_ready) {
5467 this.internal_set_value([this.selection.get_value(), false]);
5468 this.render_value();
5471 data_changed: function() {
5472 if (this.reference_ready) {
5473 this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
5476 set_value: function(val) {
5478 val = val.split(',');
5479 val[0] = val[0] || false;
5480 val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
5482 this._super(val || [false, false]);
5484 get_value: function() {
5485 return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
5487 render_value: function() {
5488 this.reference_ready = false;
5489 if (!this.get("effective_readonly")) {
5490 this.selection.set_value(this.get('value')[0]);
5492 this.m2o.field.relation = this.get('value')[0];
5493 this.m2o.set_value(this.get('value')[1]);
5494 this.m2o.$el.toggle(!!this.get('value')[0]);
5495 this.reference_ready = true;
5499 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5500 init: function(field_manager, node) {
5502 this._super(field_manager, node);
5503 this.binary_value = false;
5504 this.useFileAPI = !!window.FileReader;
5505 this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5506 if (!this.useFileAPI) {
5507 this.fileupload_id = _.uniqueId('oe_fileupload');
5508 $(window).on(this.fileupload_id, function() {
5509 var args = [].slice.call(arguments).slice(1);
5510 self.on_file_uploaded.apply(self, args);
5515 if (!this.useFileAPI) {
5516 $(window).off(this.fileupload_id);
5518 this._super.apply(this, arguments);
5520 initialize_content: function() {
5522 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5523 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5524 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5525 this.$el.find('.oe_form_binary_file_edit').click(function(event){
5526 self.$el.find('input.oe_form_binary_file').click();
5529 on_file_change: function(e) {
5531 var file_node = e.target;
5532 if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5533 if (this.useFileAPI) {
5534 var file = file_node.files[0];
5535 if (file.size > this.max_upload_size) {
5536 var msg = _t("The selected file exceed the maximum file size of %s.");
5537 instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5540 var filereader = new FileReader();
5541 filereader.readAsDataURL(file);
5542 filereader.onloadend = function(upload) {
5543 var data = upload.target.result;
5544 data = data.split(',')[1];
5545 self.on_file_uploaded(file.size, file.name, file.type, data);
5548 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5549 this.$el.find('form.oe_form_binary_form').submit();
5551 this.$el.find('.oe_form_binary_progress').show();
5552 this.$el.find('.oe_form_binary').hide();
5555 on_file_uploaded: function(size, name, content_type, file_base64) {
5556 if (size === false) {
5557 this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5558 // TODO: use openerp web crashmanager
5559 console.warn("Error while uploading file : ", name);
5561 this.filename = name;
5562 this.on_file_uploaded_and_valid.apply(this, arguments);
5564 this.$el.find('.oe_form_binary_progress').hide();
5565 this.$el.find('.oe_form_binary').show();
5567 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5569 on_save_as: function(ev) {
5570 var value = this.get('value');
5572 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5573 ev.stopPropagation();
5575 instance.web.blockUI();
5576 var c = instance.webclient.crashmanager;
5577 this.session.get_file({
5578 url: '/web/binary/saveas_ajax',
5579 data: {data: JSON.stringify({
5580 model: this.view.dataset.model,
5581 id: (this.view.datarecord.id || ''),
5583 filename_field: (this.node.attrs.filename || ''),
5584 data: instance.web.form.is_bin_size(value) ? null : value,
5585 context: this.view.dataset.get_context()
5587 complete: instance.web.unblockUI,
5588 error: c.rpc_error.bind(c)
5590 ev.stopPropagation();
5594 set_filename: function(value) {
5595 var filename = this.node.attrs.filename;
5598 tmp[filename] = value;
5599 this.field_manager.set_values(tmp);
5602 on_clear: function() {
5603 if (this.get('value') !== false) {
5604 this.binary_value = false;
5605 this.internal_set_value(false);
5611 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5612 template: 'FieldBinaryFile',
5613 initialize_content: function() {
5615 if (this.get("effective_readonly")) {
5617 this.$el.find('a').click(function(ev) {
5618 if (self.get('value')) {
5619 self.on_save_as(ev);
5625 render_value: function() {
5627 if (!this.get("effective_readonly")) {
5628 if (this.node.attrs.filename) {
5629 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5631 show_value = (this.get('value') !== null && this.get('value') !== undefined && this.get('value') !== false) ? this.get('value') : '';
5633 this.$el.find('input').eq(0).val(show_value);
5635 this.$el.find('a').toggle(!!this.get('value'));
5636 if (this.get('value')) {
5637 show_value = _t("Download");
5639 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5640 this.$el.find('a').text(show_value);
5644 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5645 this.binary_value = true;
5646 this.internal_set_value(file_base64);
5647 var show_value = name + " (" + instance.web.human_size(size) + ")";
5648 this.$el.find('input').eq(0).val(show_value);
5649 this.set_filename(name);
5651 on_clear: function() {
5652 this._super.apply(this, arguments);
5653 this.$el.find('input').eq(0).val('');
5654 this.set_filename('');
5658 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5659 template: 'FieldBinaryImage',
5660 placeholder: "/web/static/src/img/placeholder.png",
5661 render_value: function() {
5664 if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5665 url = 'data:image/png;base64,' + this.get('value');
5666 } else if (this.get('value')) {
5667 var id = JSON.stringify(this.view.datarecord.id || null);
5668 var field = this.name;
5669 if (this.options.preview_image)
5670 field = this.options.preview_image;
5671 url = this.session.url('/web/binary/image', {
5672 model: this.view.dataset.model,
5675 t: (new Date().getTime()),
5678 url = this.placeholder;
5680 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5681 $($img).click(function(e) {
5682 if(self.view.get("actual_mode") == "view") {
5683 var $button = $(".oe_form_button_edit");
5684 $button.openerpBounce();
5685 e.stopPropagation();
5688 this.$el.find('> img').remove();
5689 this.$el.prepend($img);
5690 $img.load(function() {
5691 if (! self.options.size)
5693 $img.css("max-width", "" + self.options.size[0] + "px");
5694 $img.css("max-height", "" + self.options.size[1] + "px");
5696 $img.on('error', function() {
5697 $img.attr('src', self.placeholder);
5698 instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5701 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5702 this.internal_set_value(file_base64);
5703 this.binary_value = true;
5704 this.render_value();
5705 this.set_filename(name);
5707 on_clear: function() {
5708 this._super.apply(this, arguments);
5709 this.render_value();
5710 this.set_filename('');
5712 set_value: function(value_){
5713 var changed = value_ !== this.get_value();
5714 this._super.apply(this, arguments);
5715 // By default, on binary images read, the server returns the binary size
5716 // This is possible that two images have the exact same size
5717 // Therefore we trigger the change in case the image value hasn't changed
5718 // So the image is re-rendered correctly
5720 this.trigger("change:value", this, {
5729 * Widget for (many2many field) to upload one or more file in same time and display in list.
5730 * The user can delete his files.
5731 * Options on attribute ; "blockui" {Boolean} block the UI or not
5732 * during the file is uploading
5734 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5735 template: "FieldBinaryFileUploader",
5736 init: function(field_manager, node) {
5737 this._super(field_manager, node);
5738 this.field_manager = field_manager;
5740 if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5741 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);
5745 this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5746 this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5747 $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5749 initialize_content: function() {
5750 this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5752 // WARNING: duplicated in 4 other M2M widgets
5753 set_value: function(value_) {
5754 value_ = value_ || [];
5755 if (value_.length >= 1 && value_[0] instanceof Array) {
5756 // value_ is a list of m2m commands. We only process
5757 // LINK_TO and REPLACE_WITH in this context
5759 _.each(value_, function (command) {
5760 if (command[0] === commands.LINK_TO) {
5761 val.push(command[1]); // (4, id[, _])
5762 } else if (command[0] === commands.REPLACE_WITH) {
5763 val = command[2]; // (6, _, ids)
5768 this._super(value_);
5770 get_value: function() {
5771 var tmp = [commands.replace_with(this.get("value"))];
5774 get_file_url: function (attachment) {
5775 return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5777 read_name_values : function () {
5779 // don't reset know values
5780 var ids = this.get('value');
5781 var _value = _.filter(ids, function (id) { return typeof self.data[id] == 'undefined'; } );
5782 // send request for get_name
5783 if (_value.length) {
5784 return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).then(function (datas) {
5785 _.each(datas, function (data) {
5786 data.no_unlink = true;
5787 data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5788 self.data[data.id] = data;
5796 render_value: function () {
5798 this.read_name_values().then(function (ids) {
5799 var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids}));
5800 render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5801 self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5803 // reinit input type file
5804 var $input = self.$('input.oe_form_binary_file');
5805 $input.after($input.clone(true)).remove();
5806 self.$(".oe_fileupload").show();
5810 on_file_change: function (event) {
5811 event.stopPropagation();
5813 var $target = $(event.target);
5814 if ($target.val() !== '') {
5815 var filename = $target.val().replace(/.*[\\\/]/,'');
5816 // don't uplode more of one file in same time
5817 if (self.data[0] && self.data[0].upload ) {
5820 for (var id in this.get('value')) {
5821 // if the files exits, delete the file before upload (if it's a new file)
5822 if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
5823 self.ds_file.unlink([id]);
5828 if(this.node.attrs.blockui>0) {
5829 instance.web.blockUI();
5832 // TODO : unactivate send on wizard and form
5835 this.$('form.oe_form_binary_form').submit();
5836 this.$(".oe_fileupload").hide();
5837 // add file on data result
5841 'filename': filename,
5847 on_file_loaded: function (event, result) {
5848 var files = this.get('value');
5851 if(this.node.attrs.blockui>0) {
5852 instance.web.unblockUI();
5855 if (result.error || !result.id ) {
5856 this.do_warn( _t('Uploading Error'), result.error);
5857 delete this.data[0];
5859 if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
5860 delete this.data[0];
5861 this.data[result.id] = {
5863 'name': result.name,
5864 'filename': result.filename,
5865 'url': this.get_file_url(result)
5868 this.data[result.id] = {
5870 'name': result.name,
5871 'filename': result.filename,
5872 'url': this.get_file_url(result)
5875 var values = _.clone(this.get('value'));
5876 values.push(result.id);
5877 this.set({'value': values});
5879 this.render_value();
5881 on_file_delete: function (event) {
5882 event.stopPropagation();
5883 var file_id=$(event.target).data("id");
5885 var files = _.filter(this.get('value'), function (id) {return id != file_id;});
5886 if(!this.data[file_id].no_unlink) {
5887 this.ds_file.unlink([file_id]);
5889 this.set({'value': files});
5894 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5895 template: "FieldStatus",
5896 init: function(field_manager, node) {
5897 this._super(field_manager, node);
5898 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5899 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5900 this.set({value: false});
5901 this.selection = {'unfolded': [], 'folded': []};
5902 this.set("selection", {'unfolded': [], 'folded': []});
5903 this.selection_dm = new instance.web.DropMisordered();
5904 this.dataset = new instance.web.DataSetStatic(this, this.field.relation, this.build_context());
5907 this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5909 this.on("change:value", this, this.get_selection);
5910 this.on("change:evaluated_selection_domain", this, this.get_selection);
5911 this.on("change:selection", this, function() {
5912 this.selection = this.get("selection");
5913 this.render_value();
5915 this.get_selection();
5916 if (this.options.clickable) {
5917 this.$el.on('click','li[data-id]',this.on_click_stage);
5919 if (this.$el.parent().is('header')) {
5920 this.$el.after('<div class="oe_clear"/>');
5924 set_value: function(value_) {
5925 if (value_ instanceof Array) {
5928 this._super(value_);
5930 render_value: function() {
5932 var content = QWeb.render("FieldStatus.content", {
5934 'value_folded': _.find(self.selection.folded, function(i){return i[0] === self.get('value');})
5936 self.$el.html(content);
5938 calc_domain: function() {
5939 var d = instance.web.pyeval.eval('domain', this.build_domain());
5940 var domain = []; //if there is no domain defined, fetch all the records
5943 domain = ['|',['id', '=', this.get('value')]].concat(d);
5946 if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5947 this.set("evaluated_selection_domain", domain);
5950 /** Get the selection and render it
5951 * selection: [[identifier, value_to_display], ...]
5952 * For selection fields: this is directly given by this.field.selection
5953 * For many2one fields: perform a search on the relation of the many2one field
5955 get_selection: function() {
5957 var selection_unfolded = [];
5958 var selection_folded = [];
5959 var fold_field = this.options.fold_field;
5961 var calculation = _.bind(function() {
5962 if (this.field.type == "many2one") {
5963 return self.get_distant_fields().then(function (fields) {
5964 return new instance.web.DataSetSearch(self, self.field.relation, self.build_context(), self.get("evaluated_selection_domain"))
5965 .read_slice(_.union(_.keys(self.distant_fields), ['id']), {}).then(function (records) {
5966 var ids = _.pluck(records, 'id');
5967 return self.dataset.name_get(ids).then(function (records_name) {
5968 _.each(records, function (record) {
5969 var name = _.find(records_name, function (val) {return val[0] == record.id;})[1];
5970 if (fold_field && record[fold_field] && record.id != self.get('value')) {
5971 selection_folded.push([record.id, name]);
5973 selection_unfolded.push([record.id, name]);
5980 // For field type selection filter values according to
5981 // statusbar_visible attribute of the field. For example:
5982 // statusbar_visible="draft,open".
5983 var select = this.field.selection;
5984 for(var i=0; i < select.length; i++) {
5985 var key = select[i][0];
5986 if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5987 selection_unfolded.push(select[i]);
5993 this.selection_dm.add(calculation()).then(function () {
5994 var selection = {'unfolded': selection_unfolded, 'folded': selection_folded};
5995 if (! _.isEqual(selection, self.get("selection"))) {
5996 self.set("selection", selection);
6001 * :deprecated: this feature will probably be removed with OpenERP v8
6003 get_distant_fields: function() {
6005 if (! this.options.fold_field) {
6006 this.distant_fields = {}
6008 if (this.distant_fields) {
6009 return $.when(this.distant_fields);
6011 return new instance.web.Model(self.field.relation).call("fields_get", [[this.options.fold_field]]).then(function(fields) {
6012 self.distant_fields = fields;
6016 on_click_stage: function (ev) {
6018 var $li = $(ev.currentTarget);
6020 if (this.field.type == "many2one") {
6021 val = parseInt($li.data("id"), 10);
6024 val = $li.data("id");
6026 if (val != self.get('value')) {
6027 this.view.recursive_save().done(function() {
6029 change[self.name] = val;
6030 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
6038 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
6039 template: "FieldMonetary",
6040 widget_class: 'oe_form_field_float oe_form_field_monetary',
6042 this._super.apply(this, arguments);
6043 this.set({"currency": false});
6044 if (this.options.currency_field) {
6045 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
6046 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
6049 this.on("change:currency", this, this.get_currency_info);
6050 this.get_currency_info();
6051 this.ci_dm = new instance.web.DropMisordered();
6054 var tmp = this._super();
6055 this.on("change:currency_info", this, this.reinitialize);
6058 get_currency_info: function() {
6060 if (this.get("currency") === false) {
6061 this.set({"currency_info": null});
6064 return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
6065 .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
6066 self.set({"currency_info": res});
6069 parse_value: function(val, def) {
6070 return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6072 format_value: function(val, def) {
6073 return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6078 This type of field display a list of checkboxes. It works only with m2ms. This field will display one checkbox for each
6079 record existing in the model targeted by the relation, according to the given domain if one is specified. Checked records
6080 will be added to the relation.
6082 instance.web.form.FieldMany2ManyCheckBoxes = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6083 className: "oe_form_many2many_checkboxes",
6085 this._super.apply(this, arguments);
6086 this.set("value", {});
6087 this.set("records", []);
6088 this.field_manager.on("view_content_has_changed", this, function() {
6089 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
6090 if (! _.isEqual(domain, this.get("domain"))) {
6091 this.set("domain", domain);
6094 this.records_orderer = new instance.web.DropMisordered();
6096 initialize_field: function() {
6097 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
6098 this.on("change:domain", this, this.query_records);
6099 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
6100 this.on("change:records", this, this.render_value);
6102 query_records: function() {
6104 var model = new openerp.Model(openerp.session, this.field.relation);
6105 this.records_orderer.add(model.call("search", [this.get("domain")], {"context": this.build_context()}).then(function(record_ids) {
6106 return model.call("name_get", [record_ids] , {"context": self.build_context()});
6107 })).then(function(res) {
6108 self.set("records", res);
6111 render_value: function() {
6112 this.$().html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this, selected: this.get("value")}));
6113 var inputs = this.$("input");
6114 inputs.change(_.bind(this.from_dom, this));
6115 if (this.get("effective_readonly"))
6116 inputs.attr("disabled", "true");
6118 from_dom: function() {
6120 this.$("input").each(function() {
6122 new_value[elem.data("record-id")] = elem.attr("checked") ? true : undefined;
6124 if (! _.isEqual(new_value, this.get("value")))
6125 this.internal_set_value(new_value);
6127 // WARNING: (mostly) duplicated in 4 other M2M widgets
6128 set_value: function(value_) {
6129 value_ = value_ || [];
6130 if (value_.length >= 1 && value_[0] instanceof Array) {
6131 // value_ is a list of m2m commands. We only process
6132 // LINK_TO and REPLACE_WITH in this context
6134 _.each(value_, function (command) {
6135 if (command[0] === commands.LINK_TO) {
6136 val.push(command[1]); // (4, id[, _])
6137 } else if (command[0] === commands.REPLACE_WITH) {
6138 val = command[2]; // (6, _, ids)
6144 _.each(value_, function(el) {
6145 formatted[JSON.stringify(el)] = true;
6147 this._super(formatted);
6149 get_value: function() {
6150 var value = _.filter(_.keys(this.get("value")), function(el) {
6151 return this.get("value")[el];
6153 value = _.map(value, function(el) {
6154 return JSON.parse(el);
6156 return [commands.replace_with(value)];
6161 This field can be applied on many2many and one2many. It is a read-only field that will display a single link whose name is
6162 "<number of linked records> <label of the field>". When the link is clicked, it will redirect to another act_window
6163 action on the model of the relation and show only the linked records.
6167 * views: The views to display in the act_window action. Must be a list of tuples whose first element is the id of the view
6168 to display (or False to take the default one) and the second element is the type of the view. Defaults to
6169 [[false, "tree"], [false, "form"]] .
6171 instance.web.form.X2ManyCounter = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6172 className: "oe_form_x2many_counter",
6174 this._super.apply(this, arguments);
6175 this.set("value", []);
6176 _.defaults(this.options, {
6177 "views": [[false, "tree"], [false, "form"]],
6180 render_value: function() {
6181 var text = _.str.sprintf("%d %s", this.val().length, this.string);
6182 this.$().html(QWeb.render("X2ManyCounter", {text: text}));
6183 this.$("a").click(_.bind(this.go_to, this));
6186 return this.view.recursive_save().then(_.bind(function() {
6187 var val = this.val();
6189 if (this.field.type === "one2many") {
6190 context["default_" + this.field.relation_field] = this.view.datarecord.id;
6192 var domain = [["id", "in", val]];
6193 return this.do_action({
6194 type: 'ir.actions.act_window',
6196 res_model: this.field.relation,
6197 views: this.options.views,
6205 var value = this.get("value") || [];
6206 if (value.length >= 1 && value[0] instanceof Array) {
6207 value = value[0][2];
6214 This widget is intended to be used on stat button numeric fields. It will display
6215 the value many2many and one2many. It is a read-only field that will
6216 display a simple string "<value of field> <label of the field>"
6218 instance.web.form.StatInfo = instance.web.form.AbstractField.extend({
6219 is_field_number: true,
6221 this._super.apply(this, arguments);
6222 this.internal_set_value(0);
6224 set_value: function(value_) {
6225 if (value_ === false || value_ === undefined) {
6228 this._super.apply(this, [value_]);
6230 render_value: function() {
6232 value: this.get("value") || 0,
6234 if (! this.node.attrs.nolabel) {
6235 if(this.options.label_field && this.view.datarecord[this.options.label_field]) {
6236 options.text = this.view.datarecord[this.options.label_field];
6239 options.text = this.string;
6242 this.$el.html(QWeb.render("StatInfo", options));
6249 * Registry of form fields, called by :js:`instance.web.FormView`.
6251 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
6252 * will substitute to the <field> tags as defined in OpenERP's views.
6254 instance.web.form.widgets = new instance.web.Registry({
6255 'char' : 'instance.web.form.FieldChar',
6256 'id' : 'instance.web.form.FieldID',
6257 'email' : 'instance.web.form.FieldEmail',
6258 'url' : 'instance.web.form.FieldUrl',
6259 'text' : 'instance.web.form.FieldText',
6260 'html' : 'instance.web.form.FieldTextHtml',
6261 'char_domain': 'instance.web.form.FieldCharDomain',
6262 'date' : 'instance.web.form.FieldDate',
6263 'datetime' : 'instance.web.form.FieldDatetime',
6264 'selection' : 'instance.web.form.FieldSelection',
6265 'radio' : 'instance.web.form.FieldRadio',
6266 'many2one' : 'instance.web.form.FieldMany2One',
6267 'many2onebutton' : 'instance.web.form.Many2OneButton',
6268 'many2many' : 'instance.web.form.FieldMany2Many',
6269 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
6270 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
6271 'one2many' : 'instance.web.form.FieldOne2Many',
6272 'one2many_list' : 'instance.web.form.FieldOne2Many',
6273 'reference' : 'instance.web.form.FieldReference',
6274 'boolean' : 'instance.web.form.FieldBoolean',
6275 'float' : 'instance.web.form.FieldFloat',
6276 'percentpie': 'instance.web.form.FieldPercentPie',
6277 'barchart': 'instance.web.form.FieldBarChart',
6278 'integer': 'instance.web.form.FieldFloat',
6279 'float_time': 'instance.web.form.FieldFloat',
6280 'progressbar': 'instance.web.form.FieldProgressBar',
6281 'image': 'instance.web.form.FieldBinaryImage',
6282 'binary': 'instance.web.form.FieldBinaryFile',
6283 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
6284 'statusbar': 'instance.web.form.FieldStatus',
6285 'monetary': 'instance.web.form.FieldMonetary',
6286 'many2many_checkboxes': 'instance.web.form.FieldMany2ManyCheckBoxes',
6287 'x2many_counter': 'instance.web.form.X2ManyCounter',
6288 'priority':'instance.web.form.Priority',
6289 'kanban_state_selection':'instance.web.form.KanbanSelection',
6290 'statinfo': 'instance.web.form.StatInfo',
6294 * Registry of widgets usable in the form view that can substitute to any possible
6295 * tags defined in OpenERP's form views.
6297 * Every referenced class should extend FormWidget.
6299 instance.web.form.tags = new instance.web.Registry({
6300 'button' : 'instance.web.form.WidgetButton',
6303 instance.web.form.custom_widgets = new instance.web.Registry({
6308 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: