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 if (this.get_value()) {
3262 this.$("input").filter(function () {return this.value == self.get_value();}).prop("checked", true);
3263 this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : "");
3268 // jquery autocomplete tweak to allow html and classnames
3270 var proto = $.ui.autocomplete.prototype,
3271 initSource = proto._initSource;
3273 function filter( array, term ) {
3274 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
3275 return $.grep( array, function(value_) {
3276 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
3281 _initSource: function() {
3282 if ( this.options.html && $.isArray(this.options.source) ) {
3283 this.source = function( request, response ) {
3284 response( filter( this.options.source, request.term ) );
3287 initSource.call( this );
3291 _renderItem: function( ul, item) {
3292 return $( "<li></li>" )
3293 .data( "item.autocomplete", item )
3294 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
3296 .addClass(item.classname);
3302 A mixin containing some useful methods to handle completion inputs.
3304 The widget containing this option can have these arguments in its widget options:
3305 - no_quick_create: if true, it will disable the quick create
3307 instance.web.form.CompletionFieldMixin = {
3310 this.orderer = new instance.web.DropMisordered();
3313 * Call this method to search using a string.
3315 get_search_result: function(search_val) {
3318 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
3319 this.last_query = search_val;
3320 var exclusion_domain = [], ids_blacklist = this.get_search_blacklist();
3321 if (!_(ids_blacklist).isEmpty()) {
3322 exclusion_domain.push(['id', 'not in', ids_blacklist]);
3325 return this.orderer.add(dataset.name_search(
3326 search_val, new instance.web.CompoundDomain(self.build_domain(), exclusion_domain),
3327 'ilike', this.limit + 1, self.build_context())).then(function(data) {
3328 self.last_search = data;
3329 // possible selections for the m2o
3330 var values = _.map(data, function(x) {
3331 x[1] = x[1].split("\n")[0];
3333 label: _.str.escapeHTML(x[1]),
3340 // search more... if more results that max
3341 if (values.length > self.limit) {
3342 values = values.slice(0, self.limit);
3344 label: _t("Search More..."),
3345 action: function() {
3346 dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
3347 self._search_create_popup("search", data);
3350 classname: 'oe_m2o_dropdown_option'
3354 var raw_result = _(data.result).map(function(x) {return x[1];});
3355 if (search_val.length > 0 && !_.include(raw_result, search_val) &&
3356 ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3358 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
3359 $('<span />').text(search_val).html()),
3360 action: function() {
3361 self._quick_create(search_val);
3363 classname: 'oe_m2o_dropdown_option'
3367 if (!(self.options && (self.options.no_create || self.options.no_create_edit))){
3369 label: _t("Create and Edit..."),
3370 action: function() {
3371 self._search_create_popup("form", undefined, self._create_context(search_val));
3373 classname: 'oe_m2o_dropdown_option'
3376 else if (values.length == 0)
3378 label: _t("No results to show..."),
3379 action: function() {},
3380 classname: 'oe_m2o_dropdown_option'
3386 get_search_blacklist: function() {
3389 _quick_create: function(name) {
3391 var slow_create = function () {
3392 self._search_create_popup("form", undefined, self._create_context(name));
3394 if (self.options.quick_create === undefined || self.options.quick_create) {
3395 new instance.web.DataSet(this, this.field.relation, self.build_context())
3396 .name_create(name).done(function(data) {
3397 if (!self.get('effective_readonly'))
3398 self.add_id(data[0]);
3399 }).fail(function(error, event) {
3400 event.preventDefault();
3406 // all search/create popup handling
3407 _search_create_popup: function(view, ids, context) {
3409 var pop = new instance.web.form.SelectCreatePopup(this);
3411 self.field.relation,
3413 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3414 initial_ids: ids ? _.map(ids, function(x) {return x[0];}) : undefined,
3416 disable_multiple_selection: true
3418 self.build_domain(),
3419 new instance.web.CompoundContext(self.build_context(), context || {})
3421 pop.on("elements_selected", self, function(element_ids) {
3422 self.add_id(element_ids[0]);
3429 add_id: function(id) {},
3430 _create_context: function(name) {
3432 var field = (this.options || {}).create_name_field;
3433 if (field === undefined)
3435 if (field !== false && name && (this.options || {}).quick_create !== false)
3436 tmp["default_" + field] = name;
3441 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3442 template: "M2ODialog",
3443 init: function(parent) {
3444 this.name = parent.string;
3445 this._super(parent, {
3446 title: _.str.sprintf(_t("Create a %s"), parent.string),
3452 var text = _.str.sprintf(_t("You are creating a new %s, are you sure it does not exist yet?"), self.name);
3453 this.$("p").text( text );
3454 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3455 this.$("input").val(this.getParent().last_query);
3456 this.$buttons.find(".oe_form_m2o_qc_button").click(function(e){
3457 if (self.$("input").val() != ''){
3458 self.getParent()._quick_create(self.$("input").val());
3462 self.$("input").focus();
3465 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3466 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3469 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3475 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3476 template: "FieldMany2One",
3478 'keydown input': function (e) {
3480 case $.ui.keyCode.UP:
3481 case $.ui.keyCode.DOWN:
3482 e.stopPropagation();
3486 init: function(field_manager, node) {
3487 this._super(field_manager, node);
3488 instance.web.form.CompletionFieldMixin.init.call(this);
3489 this.set({'value': false});
3490 this.display_value = {};
3491 this.display_value_backup = {};
3492 this.last_search = [];
3493 this.floating = false;
3494 this.current_display = null;
3495 this.is_started = false;
3496 this.ignore_focusout = false;
3498 reinit_value: function(val) {
3499 this.internal_set_value(val);
3500 this.floating = false;
3501 if (this.is_started)
3502 this.render_value();
3504 initialize_field: function() {
3505 this.is_started = true;
3506 instance.web.bus.on('click', this, function() {
3507 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3508 this.$input.autocomplete("close");
3511 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3513 initialize_content: function() {
3514 if (!this.get("effective_readonly"))
3515 this.render_editable();
3517 destroy_content: function () {
3518 if (this.$drop_down) {
3519 this.$drop_down.off('click');
3520 delete this.$drop_down;
3523 this.$input.closest(".modal .modal-content").off('scroll');
3524 this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3525 'focus focusout change keydown');
3528 if (this.$follow_button) {
3529 this.$follow_button.off('blur focus click');
3530 delete this.$follow_button;
3533 destroy: function () {
3534 this.destroy_content();
3535 return this._super();
3537 init_error_displayer: function() {
3540 hide_error_displayer: function() {
3543 show_error_displayer: function() {
3544 new instance.web.form.M2ODialog(this).open();
3546 render_editable: function() {
3548 this.$input = this.$el.find("input");
3550 this.init_error_displayer();
3552 self.$input.on('focus', function() {
3553 self.hide_error_displayer();
3556 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3557 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3559 this.$follow_button.click(function(ev) {
3560 ev.preventDefault();
3561 if (!self.get('value')) {
3565 var pop = new instance.web.form.FormOpenPopup(self);
3566 var context = self.build_context().eval();
3567 var model_obj = new instance.web.Model(self.field.relation);
3568 model_obj.call('get_formview_id', [self.get("value"), context]).then(function(view_id){
3570 self.field.relation,
3572 self.build_context(),
3574 title: _t("Open: ") + self.string,
3578 pop.on('write_completed', self, function(){
3579 self.display_value = {};
3580 self.display_value_backup = {};
3581 self.render_value();
3583 self.trigger('changed_value');
3588 // some behavior for input
3589 var input_changed = function() {
3590 if (self.current_display !== self.$input.val()) {
3591 self.current_display = self.$input.val();
3592 if (self.$input.val() === "") {
3593 self.internal_set_value(false);
3594 self.floating = false;
3596 self.floating = true;
3600 this.$input.keydown(input_changed);
3601 this.$input.change(input_changed);
3602 this.$drop_down.click(function() {
3603 self.$input.focus();
3604 if (self.$input.autocomplete("widget").is(":visible")) {
3605 self.$input.autocomplete("close");
3607 if (self.get("value") && ! self.floating) {
3608 self.$input.autocomplete("search", "");
3610 self.$input.autocomplete("search");
3615 // Autocomplete close on dialog content scroll
3616 var close_autocomplete = _.debounce(function() {
3617 if (self.$input.autocomplete("widget").is(":visible")) {
3618 self.$input.autocomplete("close");
3621 this.$input.closest(".modal .modal-content").on('scroll', this, close_autocomplete);
3623 self.ed_def = $.Deferred();
3624 self.uned_def = $.Deferred();
3626 var ed_duration = 15000;
3627 var anyoneLoosesFocus = function (e) {
3628 if (self.ignore_focusout) { return; }
3630 if (self.floating) {
3631 if (self.last_search.length > 0) {
3632 if (self.last_search[0][0] != self.get("value")) {
3633 self.display_value = {};
3634 self.display_value_backup = {};
3635 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3636 self.reinit_value(self.last_search[0][0]);
3639 self.render_value();
3643 self.reinit_value(false);
3645 self.floating = false;
3647 if (used && self.get("value") === false && ! self.no_ed && ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3648 self.ed_def.reject();
3649 self.uned_def.reject();
3650 self.ed_def = $.Deferred();
3651 self.ed_def.done(function() {
3652 self.show_error_displayer();
3653 ignore_blur = false;
3654 self.trigger('focused');
3657 setTimeout(function() {
3658 self.ed_def.resolve();
3659 self.uned_def.reject();
3660 self.uned_def = $.Deferred();
3661 self.uned_def.done(function() {
3662 self.hide_error_displayer();
3664 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3668 self.ed_def.reject();
3671 var ignore_blur = false;
3673 focusout: anyoneLoosesFocus,
3674 focus: function () { self.trigger('focused'); },
3675 autocompleteopen: function () { ignore_blur = true; },
3676 autocompleteclose: function () { setTimeout(function() {ignore_blur = false;},0); },
3678 // autocomplete open
3679 if (ignore_blur) { $(this).focus(); return; }
3680 if (_(self.getChildren()).any(function (child) {
3681 return child instanceof instance.web.form.AbstractFormPopup;
3683 self.trigger('blurred');
3687 var isSelecting = false;
3689 this.$input.autocomplete({
3690 source: function(req, resp) {
3691 self.get_search_result(req.term).done(function(result) {
3695 select: function(event, ui) {
3699 self.display_value = {};
3700 self.display_value_backup = {};
3701 self.display_value["" + item.id] = item.name;
3702 self.reinit_value(item.id);
3703 } else if (item.action) {
3705 // Cancel widget blurring, to avoid form blur event
3706 self.trigger('focused');
3710 focus: function(e, ui) {
3714 // disabled to solve a bug, but may cause others
3715 //close: anyoneLoosesFocus,
3719 // set position for list of suggestions box
3720 this.$input.autocomplete( "option", "position", { my : "left top", at: "left bottom" } );
3721 this.$input.autocomplete("widget").openerpClass();
3722 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3723 this.$input.keyup(function(e) {
3724 if (e.which === 13) { // ENTER
3726 e.stopPropagation();
3728 isSelecting = false;
3730 this.setupFocus(this.$follow_button);
3732 render_value: function(no_recurse) {
3734 if (! this.get("value")) {
3735 this.display_string("");
3738 var display = this.display_value["" + this.get("value")];
3740 this.display_string(display);
3744 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3745 this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3747 self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
3750 self.display_value["" + self.get("value")] = data[0][1];
3751 self.render_value(true);
3752 }).fail( function (data, event) {
3753 // avoid displaying crash errors as many2One should be name_get compliant
3754 event.preventDefault();
3755 self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")];
3756 self.render_value(true);
3760 display_string: function(str) {
3762 if (!this.get("effective_readonly")) {
3763 this.$input.val(str.split("\n")[0]);
3764 this.current_display = this.$input.val();
3765 if (this.is_false()) {
3766 this.$('.oe_m2o_cm_button').css({'display':'none'});
3768 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3771 var lines = _.escape(str).split("\n");
3775 follow = _.rest(lines).join("<br />");
3778 var $link = this.$el.find('.oe_form_uri')
3781 if (! this.options.no_open)
3782 $link.click(function () {
3783 var context = self.build_context().eval();
3784 var model_obj = new instance.web.Model(self.field.relation);
3785 model_obj.call('get_formview_action', [self.get("value"), context]).then(function(action){
3786 self.do_action(action);
3790 $(".oe_form_m2o_follow", this.$el).html(follow);
3793 set_value: function(value_) {
3795 if (value_ instanceof Array) {
3796 this.display_value = {};
3797 this.display_value_backup = {};
3798 if (! this.options.always_reload) {
3799 this.display_value["" + value_[0]] = value_[1];
3802 this.display_value_backup["" + value_[0]] = value_[1];
3806 value_ = value_ || false;
3807 this.reinit_value(value_);
3809 get_displayed: function() {
3810 return this.display_value["" + this.get("value")];
3812 add_id: function(id) {
3813 this.display_value = {};
3814 this.display_value_backup = {};
3815 this.reinit_value(id);
3817 is_false: function() {
3818 return ! this.get("value");
3820 focus: function () {
3821 var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3822 return input ? input.focus() : false;
3824 _quick_create: function() {
3826 this.ed_def.reject();
3827 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3829 _search_create_popup: function() {
3831 this.ed_def.reject();
3832 this.ignore_focusout = true;
3833 this.reinit_value(false);
3834 var res = instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3835 this.ignore_focusout = false;
3839 set_dimensions: function (height, width) {
3840 this._super(height, width);
3841 if (!this.get("effective_readonly") && this.$input)
3842 this.$input.css('height', height);
3846 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3847 template: 'Many2OneButton',
3848 init: function(field_manager, node) {
3849 this._super.apply(this, arguments);
3852 this._super.apply(this, arguments);
3855 set_button: function() {
3858 this.$button.remove();
3861 this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3862 this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3863 this.$button.addClass('oe_link').css({'padding':'4px'});
3864 this.$el.append(this.$button);
3865 this.$button.on('click', self.on_click);
3867 on_click: function(ev) {
3869 this.popup = new instance.web.form.FormOpenPopup(this);
3870 this.popup.show_element(
3871 this.field.relation,
3873 this.build_context(),
3874 {title: this.string}
3876 this.popup.on('create_completed', self, function(r) {
3880 set_value: function(value_) {
3882 if (value_ instanceof Array) {
3885 value_ = value_ || false;
3886 this.set('value', value_);
3892 * Abstract-ish ListView.List subclass adding an "Add an item" row to replace
3893 * the big ugly button in the header.
3895 * Requires the implementation of a ``is_readonly`` method (usually a proxy to
3896 * the corresponding field's readonly or effective_readonly property) to
3897 * decide whether the special row should or should not be inserted.
3899 * Optionally an ``_add_row_class`` attribute can be set for the class(es) to
3900 * set on the insertion row.
3902 instance.web.form.AddAnItemList = instance.web.ListView.List.extend({
3903 pad_table_to: function (count) {
3904 if (!this.view.is_action_enabled('create') || this.is_readonly()) {
3909 this._super(count > 0 ? count - 1 : 0);
3912 var columns = _(this.columns).filter(function (column) {
3913 return column.invisible !== '1';
3915 if (this.options.selectable) { columns++; }
3916 if (this.options.deletable) { columns++; }
3918 var $cell = $('<td>', {
3920 'class': this._add_row_class || ''
3922 $('<a>', {href: '#'}).text(_t("Add an item"))
3923 .mousedown(function () {
3924 // FIXME: needs to be an official API somehow
3925 if (self.view.editor.is_editing()) {
3926 self.view.__ignore_blur = true;
3929 .click(function (e) {
3931 e.stopPropagation();
3932 // FIXME: there should also be an API for that one
3933 if (self.view.editor.form.__blur_timeout) {
3934 clearTimeout(self.view.editor.form.__blur_timeout);
3935 self.view.editor.form.__blur_timeout = false;
3937 self.view.ensure_saved().done(function () {
3938 self.view.do_add_record();
3942 var $padding = this.$current.find('tr:not([data-id]):first');
3943 var $newrow = $('<tr>').append($cell);
3944 if ($padding.length) {
3945 $padding.before($newrow);
3947 this.$current.append($newrow)
3953 # Values: (0, 0, { fields }) create
3954 # (1, ID, { fields }) update
3955 # (2, ID) remove (delete)
3956 # (3, ID) unlink one (target id or target of relation)
3958 # (5) unlink all (only valid for one2many)
3963 'create': function (values) {
3964 return [commands.CREATE, false, values];
3966 // (1, id, {values})
3968 'update': function (id, values) {
3969 return [commands.UPDATE, id, values];
3973 'delete': function (id) {
3974 return [commands.DELETE, id, false];
3976 // (3, id[, _]) removes relation, but not linked record itself
3978 'forget': function (id) {
3979 return [commands.FORGET, id, false];
3983 'link_to': function (id) {
3984 return [commands.LINK_TO, id, false];
3988 'delete_all': function () {
3989 return [5, false, false];
3991 // (6, _, ids) replaces all linked records with provided ids
3993 'replace_with': function (ids) {
3994 return [6, false, ids];
3997 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3998 multi_selection: false,
3999 disable_utility_classes: true,
4000 init: function(field_manager, node) {
4001 this._super(field_manager, node);
4002 this.is_loaded = $.Deferred();
4003 this.initial_is_loaded = this.is_loaded;
4004 this.form_last_update = $.Deferred();
4005 this.init_form_last_update = this.form_last_update;
4006 this.is_started = false;
4007 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
4008 this.dataset.o2m = this;
4009 this.dataset.parent_view = this.view;
4010 this.dataset.child_name = this.name;
4012 this.dataset.on('dataset_changed', this, function() {
4013 self.trigger_on_change();
4018 this._super.apply(this, arguments);
4019 this.$el.addClass('oe_form_field oe_form_field_one2many');
4024 this.is_loaded.done(function() {
4025 self.on("change:effective_readonly", self, function() {
4026 self.is_loaded = self.is_loaded.then(function() {
4027 self.viewmanager.destroy();
4028 return $.when(self.load_views()).done(function() {
4029 self.reload_current_view();
4034 this.is_started = true;
4035 this.reload_current_view();
4037 trigger_on_change: function() {
4038 this.trigger('changed_value');
4040 load_views: function() {
4043 var modes = this.node.attrs.mode;
4044 modes = !!modes ? modes.split(",") : ["tree"];
4046 _.each(modes, function(mode) {
4047 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
4048 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
4052 view_type: mode == "tree" ? "list" : mode,
4055 if (self.field.views && self.field.views[mode]) {
4056 view.embedded_view = self.field.views[mode];
4058 if(view.view_type === "list") {
4059 _.extend(view.options, {
4061 selectable: self.multi_selection,
4063 import_enabled: false,
4066 if (self.get("effective_readonly")) {
4067 _.extend(view.options, {
4072 } else if (view.view_type === "form") {
4073 if (self.get("effective_readonly")) {
4074 view.view_type = 'form';
4076 _.extend(view.options, {
4077 not_interactible_on_create: true,
4079 } else if (view.view_type === "kanban") {
4080 _.extend(view.options, {
4081 confirm_on_delete: false,
4083 if (self.get("effective_readonly")) {
4084 _.extend(view.options, {
4085 action_buttons: false,
4086 quick_creatable: false,
4088 read_only_mode: true,
4096 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
4097 this.viewmanager.o2m = self;
4098 var once = $.Deferred().done(function() {
4099 self.init_form_last_update.resolve();
4101 var def = $.Deferred().done(function() {
4102 self.initial_is_loaded.resolve();
4104 this.viewmanager.on("controller_inited", self, function(view_type, controller) {
4105 controller.o2m = self;
4106 if (view_type == "list") {
4107 if (self.get("effective_readonly")) {
4108 controller.on('edit:before', self, function (e) {
4111 _(controller.columns).find(function (column) {
4112 if (!(column instanceof instance.web.list.Handle)) {
4115 column.modifiers.invisible = true;
4119 } else if (view_type === "form") {
4120 if (self.get("effective_readonly")) {
4121 $(".oe_form_buttons", controller.$el).children().remove();
4123 controller.on("load_record", self, function(){
4126 controller.on('pager_action_executed',self,self.save_any_view);
4127 } else if (view_type == "graph") {
4128 self.reload_current_view();
4132 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
4133 $.when(self.save_any_view()).done(function() {
4134 if (n_mode === "list") {
4135 $.async_when().done(function() {
4136 self.reload_current_view();
4141 $.async_when().done(function () {
4142 self.viewmanager.appendTo(self.$el);
4146 reload_current_view: function() {
4148 self.is_loaded = self.is_loaded.then(function() {
4149 var view = self.get_active_view();
4150 if (view.type === "list") {
4151 return view.controller.reload_content();
4152 } else if (view.type === "form") {
4153 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
4154 self.dataset.index = 0;
4156 var act = function() {
4157 return view.controller.do_show();
4159 self.form_last_update = self.form_last_update.then(act, act);
4160 return self.form_last_update;
4161 } else if (view.controller.do_search) {
4162 return view.controller.do_search(self.build_domain(), self.dataset.get_context(), []);
4165 return self.is_loaded;
4167 get_active_view: function () {
4169 * Returns the current active view if any.
4171 return (this.viewmanager && this.viewmanager.active_view);
4173 set_value: function(value_) {
4174 value_ = value_ || [];
4176 var view = this.get_active_view();
4177 this.dataset.reset_ids([]);
4179 if(value_.length >= 1 && value_[0] instanceof Array) {
4181 _.each(value_, function(command) {
4182 var obj = {values: command[2]};
4183 switch (command[0]) {
4184 case commands.CREATE:
4185 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4187 self.dataset.to_create.push(obj);
4188 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4191 case commands.UPDATE:
4192 obj['id'] = command[1];
4193 self.dataset.to_write.push(obj);
4194 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4197 case commands.DELETE:
4198 self.dataset.to_delete.push({id: command[1]});
4200 case commands.LINK_TO:
4201 ids.push(command[1]);
4203 case commands.DELETE_ALL:
4204 self.dataset.delete_all = true;
4209 this.dataset.set_ids(ids);
4210 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
4212 this.dataset.delete_all = true;
4213 _.each(value_, function(command) {
4214 var obj = {values: command};
4215 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4217 self.dataset.to_create.push(obj);
4218 self.dataset.cache.push(_.clone(obj));
4222 this.dataset.set_ids(ids);
4224 this._super(value_);
4225 this.dataset.reset_ids(value_);
4227 if (this.dataset.index === null && this.dataset.ids.length > 0) {
4228 this.dataset.index = 0;
4230 this.trigger_on_change();
4231 if (this.is_started) {
4232 return self.reload_current_view();
4237 get_value: function() {
4241 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
4242 val = val.concat(_.map(this.dataset.ids, function(id) {
4243 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
4245 return commands.create(alter_order.values);
4247 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
4249 return commands.update(alter_order.id, alter_order.values);
4251 return commands.link_to(id);
4253 return val.concat(_.map(
4254 this.dataset.to_delete, function(x) {
4255 return commands['delete'](x.id);}));
4257 commit_value: function() {
4258 return this.save_any_view();
4260 save_any_view: function() {
4261 var view = this.get_active_view();
4263 if (this.viewmanager.active_view.type === "form") {
4264 if (view.controller.is_initialized.state() !== 'resolved') {
4265 return $.when(false);
4267 return $.when(view.controller.save());
4268 } else if (this.viewmanager.active_view.type === "list") {
4269 return $.when(view.controller.ensure_saved());
4272 return $.when(false);
4274 is_syntax_valid: function() {
4275 var view = this.get_active_view();
4279 switch (this.viewmanager.active_view.type) {
4281 return _(view.controller.fields).chain()
4286 return view.controller.is_valid();
4292 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
4293 template: 'One2Many.viewmanager',
4294 init: function(parent, dataset, views, flags) {
4295 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
4296 this.registry = instance.web.views.extend({
4297 list: 'instance.web.form.One2ManyListView',
4298 form: 'instance.web.form.One2ManyFormView',
4300 this.__ignore_blur = false;
4302 switch_mode: function(mode, unused) {
4303 if (mode !== 'form') {
4304 return this._super(mode, unused);
4307 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
4308 var pop = new instance.web.form.FormOpenPopup(this);
4309 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4310 title: _t("Open: ") + self.o2m.string,
4311 create_function: function(data, options) {
4312 return self.o2m.dataset.create(data, options).done(function(r) {
4313 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4314 self.o2m.dataset.trigger("dataset_changed", r);
4317 write_function: function(id, data, options) {
4318 return self.o2m.dataset.write(id, data, {}).done(function() {
4319 self.o2m.reload_current_view();
4322 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4323 parent_view: self.o2m.view,
4324 child_name: self.o2m.name,
4325 read_function: function() {
4326 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4328 form_view_options: {'not_interactible_on_create':true},
4329 readonly: self.o2m.get("effective_readonly")
4331 pop.on("elements_selected", self, function() {
4332 self.o2m.reload_current_view();
4337 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
4338 get_context: function() {
4339 this.context = this.o2m.build_context();
4340 return this.context;
4344 instance.web.form.One2ManyListView = instance.web.ListView.extend({
4345 _template: 'One2Many.listview',
4346 init: function (parent, dataset, view_id, options) {
4347 this._super(parent, dataset, view_id, _.extend(options || {}, {
4348 GroupsType: instance.web.form.One2ManyGroups,
4349 ListType: instance.web.form.One2ManyList
4351 this.on('edit:after', this, this.proxy('_after_edit'));
4352 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
4355 .bind('add', this.proxy("changed_records"))
4356 .bind('edit', this.proxy("changed_records"))
4357 .bind('remove', this.proxy("changed_records"));
4359 start: function () {
4360 var ret = this._super();
4362 .off('mousedown.handleButtons')
4363 .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
4366 changed_records: function () {
4367 this.o2m.trigger_on_change();
4369 is_valid: function () {
4371 if (!this.fields_view || !this.editable()){
4375 return _.every(this.records.records, function(record){
4377 _.each(self.editor.form.fields, function(field){
4378 field._inhibit_on_change_flag = true;
4379 field.set_value(r.attributes[field.name]);
4380 field._inhibit_on_change_flag = false;
4382 return _.every(self.editor.form.fields, function(field){
4383 field.process_modifiers();
4384 field._check_css_flags();
4385 return field.is_valid();
4389 do_add_record: function () {
4390 if (this.editable()) {
4391 this._super.apply(this, arguments);
4394 var pop = new instance.web.form.SelectCreatePopup(this);
4396 self.o2m.field.relation,
4398 title: _t("Create: ") + self.o2m.string,
4399 initial_view: "form",
4400 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4401 create_function: function(data, options) {
4402 return self.o2m.dataset.create(data, options).done(function(r) {
4403 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4404 self.o2m.dataset.trigger("dataset_changed", r);
4407 read_function: function() {
4408 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4410 parent_view: self.o2m.view,
4411 child_name: self.o2m.name,
4412 form_view_options: {'not_interactible_on_create':true}
4414 self.o2m.build_domain(),
4415 self.o2m.build_context()
4417 pop.on("elements_selected", self, function() {
4418 self.o2m.reload_current_view();
4422 do_activate_record: function(index, id) {
4424 var pop = new instance.web.form.FormOpenPopup(self);
4425 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4426 title: _t("Open: ") + self.o2m.string,
4427 write_function: function(id, data) {
4428 return self.o2m.dataset.write(id, data, {}).done(function() {
4429 self.o2m.reload_current_view();
4432 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4433 parent_view: self.o2m.view,
4434 child_name: self.o2m.name,
4435 read_function: function() {
4436 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4438 form_view_options: {'not_interactible_on_create':true},
4439 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
4442 do_button_action: function (name, id, callback) {
4443 if (!_.isNumber(id)) {
4444 instance.webclient.notification.warn(
4445 _t("Action Button"),
4446 _t("The o2m record must be saved before an action can be used"));
4449 var parent_form = this.o2m.view;
4451 this.ensure_saved().then(function () {
4453 return parent_form.save();
4456 }).done(function () {
4457 var ds = self.o2m.dataset;
4458 var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
4459 return value.length;
4461 if (!self.o2m.options.reload_on_button && !cached_records) {
4462 self.handle_button(name, id, callback);
4464 self.handle_button(name, id, function(){
4465 self.o2m.view.reload();
4471 _after_edit: function () {
4472 this.__ignore_blur = false;
4473 this.editor.form.on('blurred', this, this._on_form_blur);
4475 // The form's blur thing may be jiggered during the edition setup,
4476 // potentially leading to the o2m instasaving the row. Cancel any
4477 // blurring triggered the edition startup here
4478 this.editor.form.widgetFocused();
4480 _before_unedit: function () {
4481 this.editor.form.off('blurred', this, this._on_form_blur);
4483 _button_down: function () {
4484 // If a button is clicked (usually some sort of action button), it's
4485 // the button's responsibility to ensure the editable list is in the
4486 // correct state -> ignore form blurring
4487 this.__ignore_blur = true;
4490 * Handles blurring of the nested form (saves the currently edited row),
4491 * unless the flag to ignore the event is set to ``true``
4493 * Makes the internal form go away
4495 _on_form_blur: function () {
4496 if (this.__ignore_blur) {
4497 this.__ignore_blur = false;
4500 // FIXME: why isn't there an API for this?
4501 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4502 this.ensure_saved();
4505 this.cancel_edition();
4507 keypress_ENTER: function () {
4508 // blurring caused by hitting the [Return] key, should skip the
4509 // autosave-on-blur and let the handler for [Return] do its thing (save
4510 // the current row *anyway*, then create a new one/edit the next one)
4511 this.__ignore_blur = true;
4512 this._super.apply(this, arguments);
4514 do_delete: function (ids) {
4515 var confirm = window.confirm;
4516 window.confirm = function () { return true; };
4518 return this._super(ids);
4520 window.confirm = confirm;
4523 reload_record: function (record, options) {
4524 if (!options || !options['do_not_evict']) {
4525 // Evict record.id from cache to ensure it will be reloaded correctly
4526 this.dataset.evict_record(record.get('id'));
4529 return this._super(record);
4532 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4533 setup_resequence_rows: function () {
4534 if (!this.view.o2m.get('effective_readonly')) {
4535 this._super.apply(this, arguments);
4539 instance.web.form.One2ManyList = instance.web.form.AddAnItemList.extend({
4540 _add_row_class: 'oe_form_field_one2many_list_row_add',
4541 is_readonly: function () {
4542 return this.view.o2m.get('effective_readonly');
4546 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4547 form_template: 'One2Many.formview',
4548 load_form: function(data) {
4551 this.$buttons.find('button.oe_form_button_create').click(function() {
4552 self.save().done(self.on_button_new);
4555 do_notify_change: function() {
4556 if (this.dataset.parent_view) {
4557 this.dataset.parent_view.do_notify_change();
4559 this._super.apply(this, arguments);
4564 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4565 template: "FieldMany2ManyTags",
4566 tag_template: "FieldMany2ManyTag",
4568 this._super.apply(this, arguments);
4569 instance.web.form.CompletionFieldMixin.init.call(this);
4570 this.set({"value": []});
4571 this._display_orderer = new instance.web.DropMisordered();
4572 this._drop_shown = false;
4574 initialize_texttext: function(){
4577 plugins : 'tags arrow autocomplete',
4579 render: function(suggestion) {
4580 return $('<span class="text-label"/>').
4581 data('index', suggestion['index']).html(suggestion['label']);
4586 selectFromDropdown: function() {
4587 this.trigger('hideDropdown');
4588 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4589 var data = self.search_result[index];
4591 self.add_id(data.id);
4593 self.ignore_blur = true;
4596 this.trigger('setSuggestions', {result : []});
4600 isTagAllowed: function(tag) {
4604 removeTag: function(tag) {
4605 var id = tag.data("id");
4606 self.set({"value": _.without(self.get("value"), id)});
4608 renderTag: function(stuff) {
4609 return $.fn.textext.TextExtTags.prototype.renderTag.
4610 call(this, stuff).data("id", stuff.id);
4614 itemToString: function(item) {
4619 onSetInputData: function(e, data) {
4621 this._plugins.autocomplete._suggestions = null;
4623 this.input().val(data);
4629 initialize_content: function() {
4630 if (this.get("effective_readonly"))
4633 self.ignore_blur = false;
4634 self.$text = this.$("textarea");
4635 self.$text.textext(self.initialize_texttext()).bind('getSuggestions', function(e, data) {
4637 var str = !!data ? data.query || '' : '';
4638 self.get_search_result(str).done(function(result) {
4639 self.search_result = result;
4640 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4641 return _.extend(el, {index:i});
4644 }).bind('hideDropdown', function() {
4645 self._drop_shown = false;
4646 }).bind('showDropdown', function() {
4647 self._drop_shown = true;
4649 self.tags = self.$text.textext()[0].tags();
4651 .focusin(function () {
4652 self.trigger('focused');
4653 self.ignore_blur = false;
4655 .focusout(function() {
4656 self.$text.trigger("setInputData", "");
4657 if (!self.ignore_blur) {
4658 self.trigger('blurred');
4660 }).keydown(function(e) {
4661 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4662 self.$text.textext()[0].autocomplete().selectFromDropdown();
4666 // WARNING: duplicated in 4 other M2M widgets
4667 set_value: function(value_) {
4668 value_ = value_ || [];
4669 if (value_.length >= 1 && value_[0] instanceof Array) {
4670 // value_ is a list of m2m commands. We only process
4671 // LINK_TO and REPLACE_WITH in this context
4673 _.each(value_, function (command) {
4674 if (command[0] === commands.LINK_TO) {
4675 val.push(command[1]); // (4, id[, _])
4676 } else if (command[0] === commands.REPLACE_WITH) {
4677 val = command[2]; // (6, _, ids)
4682 this._super(value_);
4684 is_false: function() {
4685 return _(this.get("value")).isEmpty();
4687 get_value: function() {
4688 var tmp = [commands.replace_with(this.get("value"))];
4691 get_search_blacklist: function() {
4692 return this.get("value");
4694 map_tag: function(data){
4695 return _.map(data, function(el) {return {name: el[1], id:el[0]};})
4697 get_render_data: function(ids){
4699 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4700 return dataset.name_get(ids);
4702 render_tag: function(data) {
4704 if (! self.get("effective_readonly")) {
4705 self.tags.containerElement().children().remove();
4706 self.$('textarea').css("padding-left", "3px");
4707 self.tags.addTags(self.map_tag(data));
4709 self.$el.html(QWeb.render(self.tag_template, {elements: data}));
4712 render_value: function() {
4714 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4715 var values = self.get("value");
4716 var handle_names = function(data) {
4717 if (self.isDestroyed())
4720 _.each(data, function(el) {
4721 indexed[el[0]] = el;
4723 data = _.map(values, function(el) { return indexed[el]; });
4724 self.render_tag(data);
4726 if (! values || values.length > 0) {
4727 return this._display_orderer.add(self.get_render_data(values)).done(handle_names);
4732 add_id: function(id) {
4733 this.set({'value': _.uniq(this.get('value').concat([id]))});
4735 focus: function () {
4736 var input = this.$text && this.$text[0];
4737 return input ? input.focus() : false;
4739 set_dimensions: function (height, width) {
4740 this._super(height, width);
4741 this.$("textarea").css({
4746 _search_create_popup: function() {
4747 self.ignore_blur = true;
4748 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
4754 - reload_on_button: Reload the whole form view if click on a button in a list view.
4755 If you see this options, do not use it, it's basically a dirty hack to make one
4756 precise o2m to behave the way we want.
4758 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4759 multi_selection: false,
4760 disable_utility_classes: true,
4761 init: function(field_manager, node) {
4762 this._super(field_manager, node);
4763 this.is_loaded = $.Deferred();
4764 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4765 this.dataset.m2m = this;
4767 this.dataset.on('unlink', self, function(ids) {
4768 self.dataset_changed();
4771 this.list_dm = new instance.web.DropMisordered();
4772 this.render_value_dm = new instance.web.DropMisordered();
4774 initialize_content: function() {
4777 this.$el.addClass('oe_form_field oe_form_field_many2many');
4779 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4781 'deletable': this.get("effective_readonly") ? false : true,
4782 'selectable': this.multi_selection,
4784 'reorderable': false,
4785 'import_enabled': false,
4787 var embedded = (this.field.views || {}).tree;
4789 this.list_view.set_embedded_view(embedded);
4791 this.list_view.m2m_field = this;
4792 var loaded = $.Deferred();
4793 this.list_view.on("list_view_loaded", this, function() {
4796 this.list_view.appendTo(this.$el);
4798 var old_def = self.is_loaded;
4799 self.is_loaded = $.Deferred().done(function() {
4802 this.list_dm.add(loaded).then(function() {
4803 self.is_loaded.resolve();
4806 destroy_content: function() {
4807 this.list_view.destroy();
4808 this.list_view = undefined;
4810 // WARNING: duplicated in 4 other M2M widgets
4811 set_value: function(value_) {
4812 value_ = value_ || [];
4813 if (value_.length >= 1 && value_[0] instanceof Array) {
4814 // value_ is a list of m2m commands. We only process
4815 // LINK_TO and REPLACE_WITH in this context
4817 _.each(value_, function (command) {
4818 if (command[0] === commands.LINK_TO) {
4819 val.push(command[1]); // (4, id[, _])
4820 } else if (command[0] === commands.REPLACE_WITH) {
4821 val = command[2]; // (6, _, ids)
4826 this._super(value_);
4828 get_value: function() {
4829 return [commands.replace_with(this.get('value'))];
4831 is_false: function () {
4832 return _(this.get("value")).isEmpty();
4834 render_value: function() {
4836 this.dataset.set_ids(this.get("value"));
4837 this.render_value_dm.add(this.is_loaded).then(function() {
4838 return self.list_view.reload_content();
4841 dataset_changed: function() {
4842 this.internal_set_value(this.dataset.ids);
4846 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4847 get_context: function() {
4848 this.context = this.m2m.build_context();
4849 return this.context;
4855 * @extends instance.web.ListView
4857 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4858 init: function (parent, dataset, view_id, options) {
4859 this._super(parent, dataset, view_id, _.extend(options || {}, {
4860 ListType: instance.web.form.Many2ManyList,
4863 do_add_record: function () {
4864 var pop = new instance.web.form.SelectCreatePopup(this);
4868 title: _t("Add: ") + this.m2m_field.string,
4869 no_create: this.m2m_field.options.no_create,
4871 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4872 this.m2m_field.build_context()
4875 pop.on("elements_selected", self, function(element_ids) {
4877 _(element_ids).each(function (id) {
4878 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4879 self.dataset.set_ids(self.dataset.ids.concat([id]));
4880 self.m2m_field.dataset_changed();
4885 self.reload_content();
4889 do_activate_record: function(index, id) {
4891 var pop = new instance.web.form.FormOpenPopup(this);
4892 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4893 title: _t("Open: ") + this.m2m_field.string,
4894 readonly: this.getParent().get("effective_readonly")
4896 pop.on('write_completed', self, self.reload_content);
4898 do_button_action: function(name, id, callback) {
4900 var _sup = _.bind(this._super, this);
4901 if (! this.m2m_field.options.reload_on_button) {
4902 return _sup(name, id, callback);
4904 return this.m2m_field.view.save().then(function() {
4905 return _sup(name, id, function() {
4906 self.m2m_field.view.reload();
4911 is_action_enabled: function () { return true; },
4913 instance.web.form.Many2ManyList = instance.web.form.AddAnItemList.extend({
4914 _add_row_class: 'oe_form_field_many2many_list_row_add',
4915 is_readonly: function () {
4916 return this.view.m2m_field.get('effective_readonly');
4920 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4921 disable_utility_classes: true,
4922 init: function(field_manager, node) {
4923 this._super(field_manager, node);
4924 instance.web.form.CompletionFieldMixin.init.call(this);
4925 m2m_kanban_lazy_init();
4926 this.is_loaded = $.Deferred();
4927 this.initial_is_loaded = this.is_loaded;
4930 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4931 this.dataset.m2m = this;
4932 this.dataset.on('unlink', self, function(ids) {
4933 self.dataset_changed();
4937 this._super.apply(this, arguments);
4942 self.on("change:effective_readonly", self, function() {
4943 self.is_loaded = self.is_loaded.then(function() {
4944 self.kanban_view.destroy();
4945 return $.when(self.load_view()).done(function() {
4946 self.render_value();
4951 // WARNING: duplicated in 4 other M2M widgets
4952 set_value: function(value_) {
4953 value_ = value_ || [];
4954 if (value_.length >= 1 && value_[0] instanceof Array) {
4955 // value_ is a list of m2m commands. We only process
4956 // LINK_TO and REPLACE_WITH in this context
4958 _.each(value_, function (command) {
4959 if (command[0] === commands.LINK_TO) {
4960 val.push(command[1]); // (4, id[, _])
4961 } else if (command[0] === commands.REPLACE_WITH) {
4962 val = command[2]; // (6, _, ids)
4967 this._super(value_);
4969 get_value: function() {
4970 return [commands.replace_with(this.get('value'))];
4972 load_view: function() {
4974 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4975 'create_text': _t("Add"),
4976 'creatable': self.get("effective_readonly") ? false : true,
4977 'quick_creatable': self.get("effective_readonly") ? false : true,
4978 'read_only_mode': self.get("effective_readonly") ? true : false,
4979 'confirm_on_delete': false,
4981 var embedded = (this.field.views || {}).kanban;
4983 this.kanban_view.set_embedded_view(embedded);
4985 this.kanban_view.m2m = this;
4986 var loaded = $.Deferred();
4987 this.kanban_view.on("kanban_view_loaded",self,function() {
4988 self.initial_is_loaded.resolve();
4991 this.kanban_view.on('switch_mode', this, this.open_popup);
4992 $.async_when().done(function () {
4993 self.kanban_view.appendTo(self.$el);
4997 render_value: function() {
4999 this.dataset.set_ids(this.get("value"));
5000 this.is_loaded = this.is_loaded.then(function() {
5001 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
5004 dataset_changed: function() {
5005 this.set({'value': this.dataset.ids});
5007 open_popup: function(type, unused) {
5008 if (type !== "form")
5012 if (this.dataset.index === null) {
5013 pop = new instance.web.form.SelectCreatePopup(this);
5015 this.field.relation,
5017 title: _t("Add: ") + this.string
5019 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
5020 this.build_context()
5022 pop.on("elements_selected", self, function(element_ids) {
5023 _.each(element_ids, function(one_id) {
5024 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
5025 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
5026 self.dataset_changed();
5027 self.render_value();
5032 var id = self.dataset.ids[self.dataset.index];
5033 pop = new instance.web.form.FormOpenPopup(this);
5034 pop.show_element(self.field.relation, id, self.build_context(), {
5035 title: _t("Open: ") + self.string,
5036 write_function: function(id, data, options) {
5037 return self.dataset.write(id, data, {}).done(function() {
5038 self.render_value();
5041 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
5042 parent_view: self.view,
5043 child_name: self.name,
5044 readonly: self.get("effective_readonly")
5048 add_id: function(id) {
5049 this.quick_create.add_id(id);
5053 function m2m_kanban_lazy_init() {
5054 if (instance.web.form.Many2ManyKanbanView)
5056 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
5057 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
5058 _is_quick_create_enabled: function() {
5059 return this._super() && ! this.group_by;
5062 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
5063 template: 'Many2ManyKanban.quick_create',
5066 * close_btn: If true, the widget will display a "Close" button able to trigger
5069 init: function(parent, dataset, context, buttons) {
5070 this._super(parent);
5071 this.m2m = this.getParent().view.m2m;
5072 this.m2m.quick_create = this;
5073 this._dataset = dataset;
5074 this._buttons = buttons || false;
5075 this._context = context || {};
5077 start: function () {
5079 self.$text = this.$el.find('input').css("width", "200px");
5080 self.$text.textext({
5081 plugins : 'arrow autocomplete',
5083 render: function(suggestion) {
5084 return $('<span class="text-label"/>').
5085 data('index', suggestion['index']).html(suggestion['label']);
5090 selectFromDropdown: function() {
5091 $(this).trigger('hideDropdown');
5092 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
5093 var data = self.search_result[index];
5095 self.add_id(data.id);
5102 itemToString: function(item) {
5107 }).bind('getSuggestions', function(e, data) {
5109 var str = !!data ? data.query || '' : '';
5110 self.m2m.get_search_result(str).done(function(result) {
5111 self.search_result = result;
5112 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
5113 return _.extend(el, {index:i});
5117 self.$text.focusout(function() {
5122 this.$text[0].focus();
5124 add_id: function(id) {
5127 self.trigger('added', id);
5128 this.m2m.dataset_changed();
5134 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
5136 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
5137 template: "AbstractFormPopup.render",
5140 * -readonly: only applicable when not in creation mode, default to false
5141 * - alternative_form_view
5148 * - form_view_options
5150 init_popup: function(model, row_id, domain, context, options) {
5151 this.row_id = row_id;
5153 this.domain = domain || [];
5154 this.context = context || {};
5155 this.options = options;
5156 _.defaults(this.options, {});
5158 init_dataset: function() {
5160 this.created_elements = [];
5161 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
5162 this.dataset.read_function = this.options.read_function;
5163 this.dataset.create_function = function(data, options, sup) {
5164 var fct = self.options.create_function || sup;
5165 return fct.call(this, data, options).done(function(r) {
5166 self.trigger('create_completed saved', r);
5167 self.created_elements.push(r);
5170 this.dataset.write_function = function(id, data, options, sup) {
5171 var fct = self.options.write_function || sup;
5172 return fct.call(this, id, data, options).done(function(r) {
5173 self.trigger('write_completed saved', r);
5176 this.dataset.parent_view = this.options.parent_view;
5177 this.dataset.child_name = this.options.child_name;
5179 display_popup: function() {
5181 this.renderElement();
5182 var dialog = new instance.web.Dialog(this, {
5183 dialogClass: 'oe_act_window',
5184 title: this.options.title || "",
5185 }, this.$el).open();
5186 dialog.on('closing', this, function (e){
5187 self.check_exit(true);
5189 this.$buttonpane = dialog.$buttons;
5192 setup_form_view: function() {
5195 this.dataset.ids = [this.row_id];
5196 this.dataset.index = 0;
5198 this.dataset.index = null;
5200 var options = _.clone(self.options.form_view_options) || {};
5201 if (this.row_id !== null) {
5202 options.initial_mode = this.options.readonly ? "view" : "edit";
5205 $buttons: this.$buttonpane,
5207 this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
5208 if (this.options.alternative_form_view) {
5209 this.view_form.set_embedded_view(this.options.alternative_form_view);
5211 this.view_form.appendTo(this.$(".oe_popup_form").show());
5212 this.view_form.on("form_view_loaded", self, function() {
5213 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
5214 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
5215 multi_select: multi_select,
5216 readonly: self.row_id !== null && self.options.readonly,
5218 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
5219 $snbutton.click(function() {
5220 $.when(self.view_form.save()).done(function() {
5221 self.view_form.reload_mutex.exec(function() {
5222 self.view_form.on_button_new();
5226 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
5227 $sbutton.click(function() {
5228 $.when(self.view_form.save()).done(function() {
5229 self.view_form.reload_mutex.exec(function() {
5234 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
5235 $cbutton.click(function() {
5236 self.view_form.trigger('on_button_cancel');
5239 self.view_form.do_show();
5242 select_elements: function(element_ids) {
5243 this.trigger("elements_selected", element_ids);
5245 check_exit: function(no_destroy) {
5246 if (this.created_elements.length > 0) {
5247 this.select_elements(this.created_elements);
5248 this.created_elements = [];
5250 this.trigger('closed');
5253 destroy: function () {
5254 this.trigger('closed');
5255 if (this.$el.is(":data(bs.modal)")) {
5256 this.$el.parents('.modal').modal('hide');
5263 * Class to display a popup containing a form view.
5265 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
5266 show_element: function(model, row_id, context, options) {
5267 this.init_popup(model, row_id, [], context, options);
5268 _.defaults(this.options, {
5270 this.display_popup();
5274 this.init_dataset();
5275 this.setup_form_view();
5280 * Class to display a popup to display a list to search a row. It also allows
5281 * to switch to a form view to create a new row.
5283 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
5287 * - initial_view: form or search (default search)
5288 * - disable_multiple_selection
5289 * - list_view_options
5291 select_element: function(model, options, domain, context) {
5292 this.init_popup(model, null, domain, context, options);
5294 _.defaults(this.options, {
5295 initial_view: "search",
5297 this.initial_ids = this.options.initial_ids;
5298 this.display_popup();
5301 this.init_dataset();
5302 if (this.options.initial_view == "search") {
5303 var context = instance.web.pyeval.sync_eval_domains_and_contexts({
5305 contexts: [this.context]
5307 var search_defaults = {};
5308 _.each(context, function (value_, key) {
5309 var match = /^search_default_(.*)$/.exec(key);
5311 search_defaults[match[1]] = value_;
5314 this.setup_search_view(search_defaults);
5319 setup_search_view: function(search_defaults) {
5321 if (this.searchview) {
5322 this.searchview.destroy();
5324 var $buttons = this.$('.o-search-options');
5325 this.searchview = new instance.web.SearchView(this,
5326 this.dataset, false, search_defaults, {$buttons: $buttons});
5327 this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
5328 if (self.initial_ids) {
5329 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
5330 contexts.concat(self.context), groupbys);
5331 self.initial_ids = undefined;
5333 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
5336 this.searchview.appendTo(this.$(".o-popup-search")).done(function() {
5337 self.searchview.toggle_visibility(true);
5338 self.view_list = new instance.web.form.SelectCreateListView(self,
5339 self.dataset, false,
5340 _.extend({'deletable': false,
5341 'selectable': !self.options.disable_multiple_selection,
5342 'import_enabled': false,
5343 '$buttons': self.$buttonpane,
5344 'disable_editable_mode': true,
5345 '$pager': self.$('.oe_popup_list_pager'),
5346 }, self.options.list_view_options || {}));
5347 self.view_list.on('edit:before', self, function (e) {
5350 self.view_list.popup = self;
5351 self.view_list.appendTo(self.$(".oe_popup_list").show()).then(function() {
5352 self.view_list.do_show();
5353 }).then(function() {
5354 self.searchview.do_search();
5356 self.view_list.on("list_view_loaded", self, function() {
5357 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
5358 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
5359 $cbutton.click(function() {
5362 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
5363 $sbutton.click(function() {
5364 self.select_elements(self.selected_ids);
5367 $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
5368 $cbutton.click(function() {
5374 do_search: function(domains, contexts, groupbys) {
5376 instance.web.pyeval.eval_domains_and_contexts({
5377 domains: domains || [],
5378 contexts: contexts || [],
5379 group_by_seq: groupbys || []
5380 }).done(function (results) {
5381 self.view_list.do_search(results.domain, results.context, results.group_by);
5384 on_click_element: function(ids) {
5386 this.selected_ids = ids || [];
5387 if(this.selected_ids.length > 0) {
5388 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
5390 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
5393 new_object: function() {
5394 if (this.searchview) {
5395 this.searchview.do_hide();
5397 if (this.view_list) {
5398 this.view_list.do_hide();
5400 this.setup_form_view();
5404 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
5405 do_add_record: function () {
5406 this.popup.new_object();
5408 select_record: function(index) {
5409 this.popup.select_elements([this.dataset.ids[index]]);
5410 this.popup.destroy();
5412 do_select: function(ids, records) {
5413 this._super(ids, records);
5414 this.popup.on_click_element(ids);
5418 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5419 template: 'FieldReference',
5420 init: function(field_manager, node) {
5421 this._super(field_manager, node);
5422 this.reference_ready = true;
5424 destroy_content: function() {
5427 this.fm = undefined;
5430 initialize_content: function() {
5432 var fm = new instance.web.form.DefaultFieldManager(this);
5434 fm.extend_field_desc({
5436 selection: this.field_manager.get_field_desc(this.name).selection,
5444 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
5446 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5448 this.selection.on("change:value", this, this.on_selection_changed);
5449 this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
5451 .on('focused', null, function () {self.trigger('focused');})
5452 .on('blurred', null, function () {self.trigger('blurred');});
5454 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
5455 name: 'Referenced Document',
5456 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5458 this.m2o.on("change:value", this, this.data_changed);
5459 this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
5461 .on('focused', null, function () {self.trigger('focused');})
5462 .on('blurred', null, function () {self.trigger('blurred');});
5464 on_selection_changed: function() {
5465 if (this.reference_ready) {
5466 this.internal_set_value([this.selection.get_value(), false]);
5467 this.render_value();
5470 data_changed: function() {
5471 if (this.reference_ready) {
5472 this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
5475 set_value: function(val) {
5477 val = val.split(',');
5478 val[0] = val[0] || false;
5479 val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
5481 this._super(val || [false, false]);
5483 get_value: function() {
5484 return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
5486 render_value: function() {
5487 this.reference_ready = false;
5488 if (!this.get("effective_readonly")) {
5489 this.selection.set_value(this.get('value')[0]);
5491 this.m2o.field.relation = this.get('value')[0];
5492 this.m2o.set_value(this.get('value')[1]);
5493 this.m2o.$el.toggle(!!this.get('value')[0]);
5494 this.reference_ready = true;
5498 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5499 init: function(field_manager, node) {
5501 this._super(field_manager, node);
5502 this.binary_value = false;
5503 this.useFileAPI = !!window.FileReader;
5504 this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5505 if (!this.useFileAPI) {
5506 this.fileupload_id = _.uniqueId('oe_fileupload');
5507 $(window).on(this.fileupload_id, function() {
5508 var args = [].slice.call(arguments).slice(1);
5509 self.on_file_uploaded.apply(self, args);
5514 if (!this.useFileAPI) {
5515 $(window).off(this.fileupload_id);
5517 this._super.apply(this, arguments);
5519 initialize_content: function() {
5521 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5522 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5523 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5524 this.$el.find('.oe_form_binary_file_edit').click(function(event){
5525 self.$el.find('input.oe_form_binary_file').click();
5528 on_file_change: function(e) {
5530 var file_node = e.target;
5531 if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5532 if (this.useFileAPI) {
5533 var file = file_node.files[0];
5534 if (file.size > this.max_upload_size) {
5535 var msg = _t("The selected file exceed the maximum file size of %s.");
5536 instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5539 var filereader = new FileReader();
5540 filereader.readAsDataURL(file);
5541 filereader.onloadend = function(upload) {
5542 var data = upload.target.result;
5543 data = data.split(',')[1];
5544 self.on_file_uploaded(file.size, file.name, file.type, data);
5547 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5548 this.$el.find('form.oe_form_binary_form').submit();
5550 this.$el.find('.oe_form_binary_progress').show();
5551 this.$el.find('.oe_form_binary').hide();
5554 on_file_uploaded: function(size, name, content_type, file_base64) {
5555 if (size === false) {
5556 this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5557 // TODO: use openerp web crashmanager
5558 console.warn("Error while uploading file : ", name);
5560 this.filename = name;
5561 this.on_file_uploaded_and_valid.apply(this, arguments);
5563 this.$el.find('.oe_form_binary_progress').hide();
5564 this.$el.find('.oe_form_binary').show();
5566 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5568 on_save_as: function(ev) {
5569 var value = this.get('value');
5571 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5572 ev.stopPropagation();
5574 instance.web.blockUI();
5575 var c = instance.webclient.crashmanager;
5576 this.session.get_file({
5577 url: '/web/binary/saveas_ajax',
5578 data: {data: JSON.stringify({
5579 model: this.view.dataset.model,
5580 id: (this.view.datarecord.id || ''),
5582 filename_field: (this.node.attrs.filename || ''),
5583 data: instance.web.form.is_bin_size(value) ? null : value,
5584 context: this.view.dataset.get_context()
5586 complete: instance.web.unblockUI,
5587 error: c.rpc_error.bind(c)
5589 ev.stopPropagation();
5593 set_filename: function(value) {
5594 var filename = this.node.attrs.filename;
5597 tmp[filename] = value;
5598 this.field_manager.set_values(tmp);
5601 on_clear: function() {
5602 if (this.get('value') !== false) {
5603 this.binary_value = false;
5604 this.internal_set_value(false);
5610 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5611 template: 'FieldBinaryFile',
5612 initialize_content: function() {
5614 if (this.get("effective_readonly")) {
5616 this.$el.find('a').click(function(ev) {
5617 if (self.get('value')) {
5618 self.on_save_as(ev);
5624 render_value: function() {
5626 if (!this.get("effective_readonly")) {
5627 if (this.node.attrs.filename) {
5628 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5630 show_value = (this.get('value') !== null && this.get('value') !== undefined && this.get('value') !== false) ? this.get('value') : '';
5632 this.$el.find('input').eq(0).val(show_value);
5634 this.$el.find('a').toggle(!!this.get('value'));
5635 if (this.get('value')) {
5636 show_value = _t("Download");
5638 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5639 this.$el.find('a').text(show_value);
5643 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5644 this.binary_value = true;
5645 this.internal_set_value(file_base64);
5646 var show_value = name + " (" + instance.web.human_size(size) + ")";
5647 this.$el.find('input').eq(0).val(show_value);
5648 this.set_filename(name);
5650 on_clear: function() {
5651 this._super.apply(this, arguments);
5652 this.$el.find('input').eq(0).val('');
5653 this.set_filename('');
5657 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5658 template: 'FieldBinaryImage',
5659 placeholder: "/web/static/src/img/placeholder.png",
5660 render_value: function() {
5663 if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5664 url = 'data:image/png;base64,' + this.get('value');
5665 } else if (this.get('value')) {
5666 var id = JSON.stringify(this.view.datarecord.id || null);
5667 var field = this.name;
5668 if (this.options.preview_image)
5669 field = this.options.preview_image;
5670 url = this.session.url('/web/binary/image', {
5671 model: this.view.dataset.model,
5674 t: (new Date().getTime()),
5677 url = this.placeholder;
5679 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5680 $($img).click(function(e) {
5681 if(self.view.get("actual_mode") == "view") {
5682 var $button = $(".oe_form_button_edit");
5683 $button.openerpBounce();
5684 e.stopPropagation();
5687 this.$el.find('> img').remove();
5688 this.$el.prepend($img);
5689 $img.load(function() {
5690 if (! self.options.size)
5692 $img.css("max-width", "" + self.options.size[0] + "px");
5693 $img.css("max-height", "" + self.options.size[1] + "px");
5695 $img.on('error', function() {
5696 $img.attr('src', self.placeholder);
5697 instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5700 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5701 this.internal_set_value(file_base64);
5702 this.binary_value = true;
5703 this.render_value();
5704 this.set_filename(name);
5706 on_clear: function() {
5707 this._super.apply(this, arguments);
5708 this.render_value();
5709 this.set_filename('');
5711 set_value: function(value_){
5712 var changed = value_ !== this.get_value();
5713 this._super.apply(this, arguments);
5714 // By default, on binary images read, the server returns the binary size
5715 // This is possible that two images have the exact same size
5716 // Therefore we trigger the change in case the image value hasn't changed
5717 // So the image is re-rendered correctly
5719 this.trigger("change:value", this, {
5728 * Widget for (many2many field) to upload one or more file in same time and display in list.
5729 * The user can delete his files.
5730 * Options on attribute ; "blockui" {Boolean} block the UI or not
5731 * during the file is uploading
5733 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5734 template: "FieldBinaryFileUploader",
5735 init: function(field_manager, node) {
5736 this._super(field_manager, node);
5737 this.field_manager = field_manager;
5739 if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5740 throw _.str.sprintf(_t("The type of the field '%s' must be a many2many field with a relation to 'ir.attachment' model."), this.field.string);
5744 this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5745 this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5746 $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5748 initialize_content: function() {
5749 this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5751 // WARNING: duplicated in 4 other M2M widgets
5752 set_value: function(value_) {
5753 value_ = value_ || [];
5754 if (value_.length >= 1 && value_[0] instanceof Array) {
5755 // value_ is a list of m2m commands. We only process
5756 // LINK_TO and REPLACE_WITH in this context
5758 _.each(value_, function (command) {
5759 if (command[0] === commands.LINK_TO) {
5760 val.push(command[1]); // (4, id[, _])
5761 } else if (command[0] === commands.REPLACE_WITH) {
5762 val = command[2]; // (6, _, ids)
5767 this._super(value_);
5769 get_value: function() {
5770 var tmp = [commands.replace_with(this.get("value"))];
5773 get_file_url: function (attachment) {
5774 return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5776 read_name_values : function () {
5778 // don't reset know values
5779 var ids = this.get('value');
5780 var _value = _.filter(ids, function (id) { return typeof self.data[id] == 'undefined'; } );
5781 // send request for get_name
5782 if (_value.length) {
5783 return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).then(function (datas) {
5784 _.each(datas, function (data) {
5785 data.no_unlink = true;
5786 data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5787 self.data[data.id] = data;
5795 render_value: function () {
5797 this.read_name_values().then(function (ids) {
5798 var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids}));
5799 render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5800 self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5802 // reinit input type file
5803 var $input = self.$('input.oe_form_binary_file');
5804 $input.after($input.clone(true)).remove();
5805 self.$(".oe_fileupload").show();
5809 on_file_change: function (event) {
5810 event.stopPropagation();
5812 var $target = $(event.target);
5813 if ($target.val() !== '') {
5814 var filename = $target.val().replace(/.*[\\\/]/,'');
5815 // don't uplode more of one file in same time
5816 if (self.data[0] && self.data[0].upload ) {
5819 for (var id in this.get('value')) {
5820 // if the files exits, delete the file before upload (if it's a new file)
5821 if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
5822 self.ds_file.unlink([id]);
5827 if(this.node.attrs.blockui>0) {
5828 instance.web.blockUI();
5831 // TODO : unactivate send on wizard and form
5834 this.$('form.oe_form_binary_form').submit();
5835 this.$(".oe_fileupload").hide();
5836 // add file on data result
5840 'filename': filename,
5846 on_file_loaded: function (event, result) {
5847 var files = this.get('value');
5850 if(this.node.attrs.blockui>0) {
5851 instance.web.unblockUI();
5854 if (result.error || !result.id ) {
5855 this.do_warn( _t('Uploading Error'), result.error);
5856 delete this.data[0];
5858 if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
5859 delete this.data[0];
5860 this.data[result.id] = {
5862 'name': result.name,
5863 'filename': result.filename,
5864 'url': this.get_file_url(result)
5867 this.data[result.id] = {
5869 'name': result.name,
5870 'filename': result.filename,
5871 'url': this.get_file_url(result)
5874 var values = _.clone(this.get('value'));
5875 values.push(result.id);
5876 this.set({'value': values});
5878 this.render_value();
5880 on_file_delete: function (event) {
5881 event.stopPropagation();
5882 var file_id=$(event.target).data("id");
5884 var files = _.filter(this.get('value'), function (id) {return id != file_id;});
5885 if(!this.data[file_id].no_unlink) {
5886 this.ds_file.unlink([file_id]);
5888 this.set({'value': files});
5893 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5894 template: "FieldStatus",
5895 init: function(field_manager, node) {
5896 this._super(field_manager, node);
5897 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5898 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5899 this.set({value: false});
5900 this.selection = {'unfolded': [], 'folded': []};
5901 this.set("selection", {'unfolded': [], 'folded': []});
5902 this.selection_dm = new instance.web.DropMisordered();
5903 this.dataset = new instance.web.DataSetStatic(this, this.field.relation, this.build_context());
5906 this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5908 this.on("change:value", this, this.get_selection);
5909 this.on("change:evaluated_selection_domain", this, this.get_selection);
5910 this.on("change:selection", this, function() {
5911 this.selection = this.get("selection");
5912 this.render_value();
5914 this.get_selection();
5915 if (this.options.clickable) {
5916 this.$el.on('click','li[data-id]',this.on_click_stage);
5918 if (this.$el.parent().is('header')) {
5919 this.$el.after('<div class="oe_clear"/>');
5923 set_value: function(value_) {
5924 if (value_ instanceof Array) {
5927 this._super(value_);
5929 render_value: function() {
5931 var content = QWeb.render("FieldStatus.content", {
5933 'value_folded': _.find(self.selection.folded, function(i){return i[0] === self.get('value');})
5935 self.$el.html(content);
5937 calc_domain: function() {
5938 var d = instance.web.pyeval.eval('domain', this.build_domain());
5939 var domain = []; //if there is no domain defined, fetch all the records
5942 domain = ['|',['id', '=', this.get('value')]].concat(d);
5945 if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5946 this.set("evaluated_selection_domain", domain);
5949 /** Get the selection and render it
5950 * selection: [[identifier, value_to_display], ...]
5951 * For selection fields: this is directly given by this.field.selection
5952 * For many2one fields: perform a search on the relation of the many2one field
5954 get_selection: function() {
5956 var selection_unfolded = [];
5957 var selection_folded = [];
5958 var fold_field = this.options.fold_field;
5960 var calculation = _.bind(function() {
5961 if (this.field.type == "many2one") {
5962 return self.get_distant_fields().then(function (fields) {
5963 return new instance.web.DataSetSearch(self, self.field.relation, self.build_context(), self.get("evaluated_selection_domain"))
5964 .read_slice(_.union(_.keys(self.distant_fields), ['id']), {}).then(function (records) {
5965 var ids = _.pluck(records, 'id');
5966 return self.dataset.name_get(ids).then(function (records_name) {
5967 _.each(records, function (record) {
5968 var name = _.find(records_name, function (val) {return val[0] == record.id;})[1];
5969 if (fold_field && record[fold_field] && record.id != self.get('value')) {
5970 selection_folded.push([record.id, name]);
5972 selection_unfolded.push([record.id, name]);
5979 // For field type selection filter values according to
5980 // statusbar_visible attribute of the field. For example:
5981 // statusbar_visible="draft,open".
5982 var select = this.field.selection;
5983 for(var i=0; i < select.length; i++) {
5984 var key = select[i][0];
5985 if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5986 selection_unfolded.push(select[i]);
5992 this.selection_dm.add(calculation()).then(function () {
5993 var selection = {'unfolded': selection_unfolded, 'folded': selection_folded};
5994 if (! _.isEqual(selection, self.get("selection"))) {
5995 self.set("selection", selection);
6000 * :deprecated: this feature will probably be removed with OpenERP v8
6002 get_distant_fields: function() {
6004 if (! this.options.fold_field) {
6005 this.distant_fields = {}
6007 if (this.distant_fields) {
6008 return $.when(this.distant_fields);
6010 return new instance.web.Model(self.field.relation).call("fields_get", [[this.options.fold_field]]).then(function(fields) {
6011 self.distant_fields = fields;
6015 on_click_stage: function (ev) {
6017 var $li = $(ev.currentTarget);
6019 if (this.field.type == "many2one") {
6020 val = parseInt($li.data("id"), 10);
6023 val = $li.data("id");
6025 if (val != self.get('value')) {
6026 this.view.recursive_save().done(function() {
6028 change[self.name] = val;
6029 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
6037 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
6038 template: "FieldMonetary",
6039 widget_class: 'oe_form_field_float oe_form_field_monetary',
6041 this._super.apply(this, arguments);
6042 this.set({"currency": false});
6043 if (this.options.currency_field) {
6044 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
6045 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
6048 this.on("change:currency", this, this.get_currency_info);
6049 this.get_currency_info();
6050 this.ci_dm = new instance.web.DropMisordered();
6053 var tmp = this._super();
6054 this.on("change:currency_info", this, this.reinitialize);
6057 get_currency_info: function() {
6059 if (this.get("currency") === false) {
6060 this.set({"currency_info": null});
6063 return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
6064 .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
6065 self.set({"currency_info": res});
6068 parse_value: function(val, def) {
6069 return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6071 format_value: function(val, def) {
6072 return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6077 This type of field display a list of checkboxes. It works only with m2ms. This field will display one checkbox for each
6078 record existing in the model targeted by the relation, according to the given domain if one is specified. Checked records
6079 will be added to the relation.
6081 instance.web.form.FieldMany2ManyCheckBoxes = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6082 className: "oe_form_many2many_checkboxes",
6084 this._super.apply(this, arguments);
6085 this.set("value", {});
6086 this.set("records", []);
6087 this.field_manager.on("view_content_has_changed", this, function() {
6088 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
6089 if (! _.isEqual(domain, this.get("domain"))) {
6090 this.set("domain", domain);
6093 this.records_orderer = new instance.web.DropMisordered();
6095 initialize_field: function() {
6096 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
6097 this.on("change:domain", this, this.query_records);
6098 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
6099 this.on("change:records", this, this.render_value);
6101 query_records: function() {
6103 var model = new openerp.Model(openerp.session, this.field.relation);
6104 this.records_orderer.add(model.call("search", [this.get("domain")], {"context": this.build_context()}).then(function(record_ids) {
6105 return model.call("name_get", [record_ids] , {"context": self.build_context()});
6106 })).then(function(res) {
6107 self.set("records", res);
6110 render_value: function() {
6111 this.$().html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this, selected: this.get("value")}));
6112 var inputs = this.$("input");
6113 inputs.change(_.bind(this.from_dom, this));
6114 if (this.get("effective_readonly"))
6115 inputs.attr("disabled", "true");
6117 from_dom: function() {
6119 this.$("input").each(function() {
6121 new_value[elem.data("record-id")] = elem.attr("checked") ? true : undefined;
6123 if (! _.isEqual(new_value, this.get("value")))
6124 this.internal_set_value(new_value);
6126 // WARNING: (mostly) duplicated in 4 other M2M widgets
6127 set_value: function(value_) {
6128 value_ = value_ || [];
6129 if (value_.length >= 1 && value_[0] instanceof Array) {
6130 // value_ is a list of m2m commands. We only process
6131 // LINK_TO and REPLACE_WITH in this context
6133 _.each(value_, function (command) {
6134 if (command[0] === commands.LINK_TO) {
6135 val.push(command[1]); // (4, id[, _])
6136 } else if (command[0] === commands.REPLACE_WITH) {
6137 val = command[2]; // (6, _, ids)
6143 _.each(value_, function(el) {
6144 formatted[JSON.stringify(el)] = true;
6146 this._super(formatted);
6148 get_value: function() {
6149 var value = _.filter(_.keys(this.get("value")), function(el) {
6150 return this.get("value")[el];
6152 value = _.map(value, function(el) {
6153 return JSON.parse(el);
6155 return [commands.replace_with(value)];
6160 This field can be applied on many2many and one2many. It is a read-only field that will display a single link whose name is
6161 "<number of linked records> <label of the field>". When the link is clicked, it will redirect to another act_window
6162 action on the model of the relation and show only the linked records.
6166 * 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
6167 to display (or False to take the default one) and the second element is the type of the view. Defaults to
6168 [[false, "tree"], [false, "form"]] .
6170 instance.web.form.X2ManyCounter = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6171 className: "oe_form_x2many_counter",
6173 this._super.apply(this, arguments);
6174 this.set("value", []);
6175 _.defaults(this.options, {
6176 "views": [[false, "tree"], [false, "form"]],
6179 render_value: function() {
6180 var text = _.str.sprintf("%d %s", this.val().length, this.string);
6181 this.$().html(QWeb.render("X2ManyCounter", {text: text}));
6182 this.$("a").click(_.bind(this.go_to, this));
6185 return this.view.recursive_save().then(_.bind(function() {
6186 var val = this.val();
6188 if (this.field.type === "one2many") {
6189 context["default_" + this.field.relation_field] = this.view.datarecord.id;
6191 var domain = [["id", "in", val]];
6192 return this.do_action({
6193 type: 'ir.actions.act_window',
6195 res_model: this.field.relation,
6196 views: this.options.views,
6204 var value = this.get("value") || [];
6205 if (value.length >= 1 && value[0] instanceof Array) {
6206 value = value[0][2];
6213 This widget is intended to be used on stat button numeric fields. It will display
6214 the value many2many and one2many. It is a read-only field that will
6215 display a simple string "<value of field> <label of the field>"
6217 instance.web.form.StatInfo = instance.web.form.AbstractField.extend({
6218 is_field_number: true,
6220 this._super.apply(this, arguments);
6221 this.internal_set_value(0);
6223 set_value: function(value_) {
6224 if (value_ === false || value_ === undefined) {
6227 this._super.apply(this, [value_]);
6229 render_value: function() {
6231 value: this.get("value") || 0,
6233 if (! this.node.attrs.nolabel) {
6234 if(this.options.label_field && this.view.datarecord[this.options.label_field]) {
6235 options.text = this.view.datarecord[this.options.label_field];
6238 options.text = this.string;
6241 this.$el.html(QWeb.render("StatInfo", options));
6248 * Registry of form fields, called by :js:`instance.web.FormView`.
6250 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
6251 * will substitute to the <field> tags as defined in OpenERP's views.
6253 instance.web.form.widgets = new instance.web.Registry({
6254 'char' : 'instance.web.form.FieldChar',
6255 'id' : 'instance.web.form.FieldID',
6256 'email' : 'instance.web.form.FieldEmail',
6257 'url' : 'instance.web.form.FieldUrl',
6258 'text' : 'instance.web.form.FieldText',
6259 'html' : 'instance.web.form.FieldTextHtml',
6260 'char_domain': 'instance.web.form.FieldCharDomain',
6261 'date' : 'instance.web.form.FieldDate',
6262 'datetime' : 'instance.web.form.FieldDatetime',
6263 'selection' : 'instance.web.form.FieldSelection',
6264 'radio' : 'instance.web.form.FieldRadio',
6265 'many2one' : 'instance.web.form.FieldMany2One',
6266 'many2onebutton' : 'instance.web.form.Many2OneButton',
6267 'many2many' : 'instance.web.form.FieldMany2Many',
6268 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
6269 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
6270 'one2many' : 'instance.web.form.FieldOne2Many',
6271 'one2many_list' : 'instance.web.form.FieldOne2Many',
6272 'reference' : 'instance.web.form.FieldReference',
6273 'boolean' : 'instance.web.form.FieldBoolean',
6274 'float' : 'instance.web.form.FieldFloat',
6275 'percentpie': 'instance.web.form.FieldPercentPie',
6276 'barchart': 'instance.web.form.FieldBarChart',
6277 'integer': 'instance.web.form.FieldFloat',
6278 'float_time': 'instance.web.form.FieldFloat',
6279 'progressbar': 'instance.web.form.FieldProgressBar',
6280 'image': 'instance.web.form.FieldBinaryImage',
6281 'binary': 'instance.web.form.FieldBinaryFile',
6282 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
6283 'statusbar': 'instance.web.form.FieldStatus',
6284 'monetary': 'instance.web.form.FieldMonetary',
6285 'many2many_checkboxes': 'instance.web.form.FieldMany2ManyCheckBoxes',
6286 'x2many_counter': 'instance.web.form.X2ManyCounter',
6287 'priority':'instance.web.form.Priority',
6288 'kanban_state_selection':'instance.web.form.KanbanSelection',
6289 'statinfo': 'instance.web.form.StatInfo',
6293 * Registry of widgets usable in the form view that can substitute to any possible
6294 * tags defined in OpenERP's form views.
6296 * Every referenced class should extend FormWidget.
6298 instance.web.form.tags = new instance.web.Registry({
6299 'button' : 'instance.web.form.WidgetButton',
6302 instance.web.form.custom_widgets = new instance.web.Registry({
6307 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: