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_mutex = new $.Mutex();
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 this.onchanges_mutex.exec(function(){
512 return def.then(function(response) {
513 if (widget && widget.field['change_default']) {
514 var fieldname = widget.name;
516 if (response.value && (fieldname in response.value)) {
517 // Use value from onchange if onchange executed
518 value_ = response.value[fieldname];
520 // otherwise get form value for field
521 value_ = self.fields[fieldname].get_value();
523 var condition = fieldname + '=' + value_;
526 return self.alive(new instance.web.Model('ir.values').call(
527 'get_defaults', [self.model, condition]
528 )).then(function (results) {
529 if (!results.length) {
532 if (!response.value) {
535 for(var i=0; i<results.length; ++i) {
536 // [whatever, key, value]
537 var triplet = results[i];
538 response.value[triplet[1]] = triplet[2];
545 }).then(function(response) {
546 return self.on_processed_onchange(response);
549 return this.onchanges_mutex.def;
552 instance.webclient.crashmanager.show_message(e);
553 return $.Deferred().reject();
556 on_processed_onchange: function(result) {
558 var fields = this.fields;
559 _(result.domain).each(function (domain, fieldname) {
560 var field = fields[fieldname];
561 if (!field) { return; }
562 field.node.attrs.domain = domain;
565 if (!_.isEmpty(result.value)) {
566 this._internal_set_values(result.value);
568 // FIXME XXX a list of warnings?
569 if (!_.isEmpty(result.warning)) {
570 new instance.web.Dialog(this, {
572 title:result.warning.title,
574 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
576 }, QWeb.render("CrashManager.warning", result.warning)).open();
579 return $.Deferred().resolve();
582 instance.webclient.crashmanager.show_message(e);
583 return $.Deferred().reject();
586 _process_operations: function() {
588 return this.mutating_mutex.exec(function() {
591 var mutex = new $.Mutex();
592 _.each(self.fields, function(field) {
593 self.onchanges_mutex.def.then(function(){
594 mutex.exec(function(){
595 return field.commit_value();
600 var args = _.toArray(arguments);
601 return $.when.apply(null, [mutex.def, self.onchanges_mutex.def]).then(function() {
602 var save_obj = self.save_list.pop();
604 return self._process_save(save_obj).then(function() {
605 save_obj.ret = _.toArray(arguments);
608 save_obj.error = true;
613 self.save_list.pop();
620 _internal_set_values: function(values) {
621 for (var f in values) {
622 if (!values.hasOwnProperty(f)) { continue; }
623 var field = this.fields[f];
624 // If field is not defined in the view, just ignore it
626 var value_ = values[f];
627 if (field.get_value() != value_) {
628 field._inhibit_on_change_flag = true;
629 field.set_value(value_);
630 field._inhibit_on_change_flag = false;
631 field._dirty_flag = true;
635 this.on_form_changed();
637 set_values: function(values) {
639 return this.mutating_mutex.exec(function() {
640 self._internal_set_values(values);
644 * Ask the view to switch to view mode if possible. The view may not do it
645 * if the current record is not yet saved. It will then stay in create mode.
647 to_view_mode: function() {
648 this._actualize_mode("view");
651 * Ask the view to switch to edit mode if possible. The view may not do it
652 * if the current record is not yet saved. It will then stay in create mode.
654 to_edit_mode: function() {
655 this.onchanges_mutex = new $.Mutex();
656 this._actualize_mode("edit");
659 * Ask the view to switch to a precise mode if possible. The view is free to
660 * not respect this command if the state of the dataset is not compatible with
661 * the new mode. For example, it is not possible to switch to edit mode if
662 * the current record is not yet saved in database.
664 * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
665 * undefined the view will test the actual mode to check if it is still consistent
666 * with the dataset state.
668 _actualize_mode: function(switch_to) {
669 var mode = switch_to || this.get("actual_mode");
670 if (! this.datarecord.id) {
672 } else if (mode === "create") {
675 this.render_value_defs = [];
676 this.set({actual_mode: mode});
678 check_actual_mode: function(source, options) {
680 if(this.get("actual_mode") === "view") {
681 self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
682 self.$buttons.find('.oe_form_buttons_edit').hide();
683 self.$buttons.find('.oe_form_buttons_view').show();
684 self.$sidebar.show();
686 self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
687 self.$buttons.find('.oe_form_buttons_edit').show();
688 self.$buttons.find('.oe_form_buttons_view').hide();
689 self.$sidebar.hide();
693 autofocus: function() {
694 if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
695 var fields_order = this.fields_order.slice(0);
696 if (this.default_focus_field) {
697 fields_order.unshift(this.default_focus_field.name);
699 for (var i = 0; i < fields_order.length; i += 1) {
700 var field = this.fields[fields_order[i]];
701 if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
702 if (field.focus() !== false) {
709 on_button_save: function(e) {
711 $(e.target).attr("disabled", true);
712 return this.save().done(function(result) {
713 self.trigger("save", result);
714 self.reload().then(function() {
716 var menu = instance.webclient.menu;
718 menu.do_reload_needaction();
720 instance.web.bus.trigger('form_view_saved', self);
722 }).always(function(){
723 $(e.target).attr("disabled", false);
726 on_button_cancel: function(event) {
728 if (this.can_be_discarded()) {
729 if (this.get('actual_mode') === 'create') {
730 this.trigger('history_back');
733 $.when.apply(null, this.render_value_defs).then(function(){
734 self.trigger('load_record', self.datarecord);
738 this.trigger('on_button_cancel');
741 on_button_new: function() {
744 return $.when(this.has_been_loaded).then(function() {
745 if (self.can_be_discarded()) {
746 return self.load_defaults();
750 on_button_edit: function() {
751 return this.to_edit_mode();
753 on_button_create: function() {
754 this.dataset.index = null;
757 on_button_duplicate: function() {
759 return this.has_been_loaded.then(function() {
760 return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
761 self.record_created(new_id);
766 on_button_delete: function() {
768 var def = $.Deferred();
769 this.has_been_loaded.done(function() {
770 if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
771 self.dataset.unlink([self.datarecord.id]).done(function() {
772 if (self.dataset.size()) {
773 self.execute_pager_action('next');
775 self.do_action('history_back');
780 $.async_when().done(function () {
785 return def.promise();
787 can_be_discarded: function() {
788 if (this.$el.is('.oe_form_dirty')) {
789 if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
792 this.$el.removeClass('oe_form_dirty');
797 * Triggers saving the form's record. Chooses between creating a new
798 * record or saving an existing one depending on whether the record
799 * already has an id property.
801 * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
802 * record, should that record be inserted at the start of the dataset (by
803 * default, records are added at the end)
805 save: function(prepend_on_create) {
807 var save_obj = {prepend_on_create: prepend_on_create, ret: null};
808 this.save_list.push(save_obj);
809 return self._process_operations().then(function() {
811 return $.Deferred().reject();
812 return $.when.apply($, save_obj.ret);
813 }).done(function(result) {
814 self.$el.removeClass('oe_form_dirty');
817 _process_save: function(save_obj) {
819 var prepend_on_create = save_obj.prepend_on_create;
821 var form_invalid = false,
823 first_invalid_field = null,
824 readonly_values = {};
825 for (var f in self.fields) {
826 if (!self.fields.hasOwnProperty(f)) { continue; }
830 if (!first_invalid_field) {
831 first_invalid_field = f;
833 } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
834 // Special case 'id' field, do not save this field
835 // on 'create' : save all non readonly fields
836 // on 'edit' : save non readonly modified fields
837 if (!f.get("readonly")) {
838 values[f.name] = f.get_value();
840 readonly_values[f.name] = f.get_value();
845 self.set({'display_invalid_fields': true});
846 first_invalid_field.focus();
848 return $.Deferred().reject();
850 self.set({'display_invalid_fields': false});
852 if (!self.datarecord.id) {
854 save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
855 return self.record_created(r, prepend_on_create);
857 } else if (_.isEmpty(values)) {
858 // Not dirty, noop save
859 save_deferral = $.Deferred().resolve({}).promise();
862 save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
863 return self.record_saved(r);
866 return save_deferral;
870 return $.Deferred().reject();
873 on_invalid: function() {
874 var warnings = _(this.fields).chain()
875 .filter(function (f) { return !f.is_valid(); })
877 return _.str.sprintf('<li>%s</li>',
880 warnings.unshift('<ul>');
881 warnings.push('</ul>');
882 this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
885 * Reload the form after saving
887 * @param {Object} r result of the write function.
889 record_saved: function(r) {
890 this.trigger('record_saved', r);
892 // should not happen in the server, but may happen for internal purpose
893 return $.Deferred().reject();
898 * Updates the form' dataset to contain the new record:
900 * * Adds the newly created record to the current dataset (at the end by
902 * * Selects that record (sets the dataset's index to point to the new
904 * * Updates the pager and sidebar displays
907 * @param {Boolean} [prepend_on_create=false] adds the newly created record
908 * at the beginning of the dataset instead of the end
910 record_created: function(r, prepend_on_create) {
913 // should not happen in the server, but may happen for internal purpose
914 this.trigger('record_created', r);
915 return $.Deferred().reject();
917 this.datarecord.id = r;
918 if (!prepend_on_create) {
919 this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
920 this.dataset.index = this.dataset.ids.length - 1;
922 this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
923 this.dataset.index = 0;
925 this.do_update_pager();
927 this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
929 //openerp.log("The record has been created with id #" + this.datarecord.id);
930 return $.when(this.reload()).then(function () {
931 self.trigger('record_created', r);
932 return _.extend(r, {created: true});
936 on_action: function (action) {
937 console.debug('Executing action', action);
941 return this.reload_mutex.exec(function() {
942 if (self.dataset.index === null || self.dataset.index === undefined) {
943 self.trigger("previous_view");
944 return $.Deferred().reject().promise();
946 if (self.dataset.index < 0) {
947 return $.when(self.on_button_new());
949 var fields = _.keys(self.fields_view.fields);
950 fields.push('display_name');
951 return self.dataset.read_index(fields,
955 'future_display_name': true
957 check_access_rule: true
958 }).then(function(r) {
959 self.trigger('load_record', r);
961 self.do_action('history_back');
966 get_widgets: function() {
967 return _.filter(this.getChildren(), function(obj) {
968 return obj instanceof instance.web.form.FormWidget;
971 get_fields_values: function() {
973 var ids = this.get_selected_ids();
974 values["id"] = ids.length > 0 ? ids[0] : false;
975 _.each(this.fields, function(value_, key) {
976 values[key] = value_.get_value();
980 get_selected_ids: function() {
981 var id = this.dataset.ids[this.dataset.index];
982 return id ? [id] : [];
984 recursive_save: function() {
986 return $.when(this.save()).then(function(res) {
987 if (self.dataset.parent_view)
988 return self.dataset.parent_view.recursive_save();
991 recursive_reload: function() {
994 if (self.dataset.parent_view)
995 pre = self.dataset.parent_view.recursive_reload();
996 return pre.then(function() {
997 return self.reload();
1000 is_dirty: function() {
1001 return _.any(this.fields, function (value_) {
1002 return value_._dirty_flag;
1005 is_interactible_record: function() {
1006 var id = this.datarecord.id;
1008 if (this.options.not_interactible_on_create)
1010 } else if (typeof(id) === "string") {
1011 if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
1016 sidebar_eval_context: function () {
1017 return $.when(this.build_eval_context());
1019 open_defaults_dialog: function () {
1021 var display = function (field, value) {
1022 if (!value) { return value; }
1023 if (field instanceof instance.web.form.FieldSelection) {
1024 return _(field.get('values')).find(function (option) {
1025 return option[0] === value;
1027 } else if (field instanceof instance.web.form.FieldMany2One) {
1028 return field.get_displayed();
1032 var fields = _.chain(this.fields)
1033 .map(function (field) {
1034 var value = field.get_value();
1035 // ignore fields which are empty, invisible, readonly, o2m
1038 || field.get('invisible')
1039 || field.get("readonly")
1040 || field.field.type === 'one2many'
1041 || field.field.type === 'many2many'
1042 || field.field.type === 'binary'
1043 || field.password) {
1049 string: field.string,
1051 displayed: display(field, value),
1055 .sortBy(function (field) { return field.string; })
1057 var conditions = _.chain(self.fields)
1058 .filter(function (field) { return field.field.change_default; })
1059 .map(function (field) {
1060 var value = field.get_value();
1063 string: field.string,
1065 displayed: display(field, value),
1069 var d = new instance.web.Dialog(this, {
1070 title: _t("Set Default"),
1073 conditions: conditions
1076 {text: _t("Close"), click: function () { d.close(); }},
1077 {text: _t("Save default"), click: function () {
1078 var $defaults = d.$el.find('#formview_default_fields');
1079 var field_to_set = $defaults.val();
1080 if (!field_to_set) {
1081 $defaults.parent().addClass('oe_form_invalid');
1084 var condition = d.$el.find('#formview_default_conditions').val(),
1085 all_users = d.$el.find('#formview_default_all').is(':checked');
1086 new instance.web.DataSet(self, 'ir.values').call(
1090 self.fields[field_to_set].get_value(),
1094 ]).done(function () { d.close(); });
1098 d.template = 'FormView.set_default';
1101 register_field: function(field, name) {
1102 this.fields[name] = field;
1103 this.fields_order.push(name);
1104 if (JSON.parse(field.node.attrs.default_focus || "0")) {
1105 this.default_focus_field = field;
1108 field.on('focused', null, this.proxy('widgetFocused'))
1109 .on('blurred', null, this.proxy('widgetBlurred'));
1110 if (this.get_field_desc(name).translate) {
1111 this.translatable_fields.push(field);
1113 field.on('changed_value', this, function() {
1114 if (field.is_syntax_valid()) {
1115 this.trigger('field_changed:' + name);
1117 if (field._inhibit_on_change_flag) {
1120 field._dirty_flag = true;
1121 if (field.is_syntax_valid()) {
1122 this.do_onchange(field);
1123 this.on_form_changed(true);
1124 this.do_notify_change();
1128 get_field_desc: function(field_name) {
1129 return this.fields_view.fields[field_name];
1131 get_field_value: function(field_name) {
1132 return this.fields[field_name].get_value();
1134 compute_domain: function(expression) {
1135 return instance.web.form.compute_domain(expression, this.fields);
1137 _build_view_fields_values: function() {
1138 var a_dataset = this.dataset;
1139 var fields_values = this.get_fields_values();
1140 var active_id = a_dataset.ids[a_dataset.index];
1141 _.extend(fields_values, {
1142 active_id: active_id || false,
1143 active_ids: active_id ? [active_id] : [],
1144 active_model: a_dataset.model,
1147 if (a_dataset.parent_view) {
1148 fields_values.parent = a_dataset.parent_view.get_fields_values();
1150 return fields_values;
1152 build_eval_context: function() {
1153 var a_dataset = this.dataset;
1154 return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
1159 * Interface to be implemented by rendering engines for the form view.
1161 instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
1162 set_fields_view: function(fields_view) {},
1163 set_fields_registry: function(fields_registry) {},
1164 render_to: function($el) {},
1168 * Default rendering engine for the form view.
1170 * It is necessary to set the view using set_view() before usage.
1172 instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
1173 init: function(view) {
1176 set_fields_view: function(fvg) {
1178 this.version = parseFloat(this.fvg.arch.attrs.version);
1179 if (isNaN(this.version)) {
1183 set_tags_registry: function(tags_registry) {
1184 this.tags_registry = tags_registry;
1186 set_fields_registry: function(fields_registry) {
1187 this.fields_registry = fields_registry;
1189 set_widgets_registry: function(widgets_registry) {
1190 this.widgets_registry = widgets_registry;
1192 // Backward compatibility tools, current default version: v7
1193 process_version: function() {
1194 if (this.version < 7.0) {
1195 this.$form.find('form:first').wrapInner('<group col="4"/>');
1196 this.$form.find('page').each(function() {
1197 if (!$(this).parents('field').length) {
1198 $(this).wrapInner('<group col="4"/>');
1203 get_arch_fragment: function() {
1204 var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
1205 // IE won't allow custom button@type and will revert it to spec default : 'submit'
1206 $('button', doc).each(function() {
1207 $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
1209 // IE's html parser is also a css parser. How convenient...
1210 $('board', doc).each(function() {
1211 $(this).attr('layout', $(this).attr('style'));
1213 return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
1215 render_to: function($target) {
1217 this.$target = $target;
1219 this.$form = this.get_arch_fragment();
1221 this.process_version();
1223 this.fields_to_init = [];
1224 this.tags_to_init = [];
1225 this.widgets_to_init = [];
1227 this.process(this.$form);
1229 this.$form.appendTo(this.$target);
1231 this.to_replace = [];
1233 _.each(this.fields_to_init, function($elem) {
1234 var name = $elem.attr("name");
1235 if (!self.fvg.fields[name]) {
1236 throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
1238 var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
1240 throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
1242 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1243 var $label = self.labels[$elem.attr("name")];
1245 w.set_input_id($label.attr("for"));
1247 self.alter_field(w);
1248 self.view.register_field(w, $elem.attr("name"));
1249 self.to_replace.push([w, $elem]);
1251 _.each(this.tags_to_init, function($elem) {
1252 var tag_name = $elem[0].tagName.toLowerCase();
1253 var obj = self.tags_registry.get_object(tag_name);
1254 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1255 self.to_replace.push([w, $elem]);
1257 _.each(this.widgets_to_init, function($elem) {
1258 var widget_type = $elem.attr("type");
1259 var obj = self.widgets_registry.get_object(widget_type);
1260 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1261 self.to_replace.push([w, $elem]);
1264 init_fields: function() {
1266 _.each(this.to_replace, function(el) {
1267 defs.push(el[0].replace(el[1]));
1268 if (el[1].children().length) {
1269 el[0].$el.append(el[1].children());
1272 this.to_replace = [];
1273 return $.when.apply($, defs);
1275 render_element: function(template /* dictionaries */) {
1276 var dicts = [].slice.call(arguments).slice(1);
1277 var dict = _.extend.apply(_, dicts);
1278 dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
1279 return $(QWeb.render(template, dict));
1281 alter_field: function(field) {
1283 toggle_layout_debugging: function() {
1284 if (!this.$target.has('.oe_layout_debug_cell:first').length) {
1285 this.$target.find('[title]').removeAttr('title');
1286 this.$target.find('.oe_form_group_cell').each(function() {
1287 var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
1288 $(this).attr('title', text);
1291 this.$target.toggleClass('oe_layout_debugging');
1293 process: function($tag) {
1295 var tagname = $tag[0].nodeName.toLowerCase();
1296 if (this.tags_registry.contains(tagname)) {
1297 this.tags_to_init.push($tag);
1298 return (tagname === 'button') ? this.process_button($tag) : $tag;
1300 var fn = self['process_' + tagname];
1302 var args = [].slice.call(arguments);
1304 return fn.apply(self, args);
1306 // generic tag handling, just process children
1307 $tag.children().each(function() {
1308 self.process($(this));
1310 self.handle_common_properties($tag, $tag);
1311 $tag.removeAttr("modifiers");
1315 process_button: function ($button) {
1317 $button.children().each(function() {
1318 self.process($(this));
1322 process_widget: function($widget) {
1323 this.widgets_to_init.push($widget);
1326 process_sheet: function($sheet) {
1327 var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
1328 this.handle_common_properties($new_sheet, $sheet);
1329 var $dst = $new_sheet.find('.oe_form_sheet');
1330 $sheet.contents().appendTo($dst);
1331 $sheet.before($new_sheet).remove();
1332 this.process($new_sheet);
1334 process_form: function($form) {
1335 if ($form.find('> sheet').length === 0) {
1336 $form.addClass('oe_form_nosheet');
1338 var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
1339 this.handle_common_properties($new_form, $form);
1340 $form.contents().appendTo($new_form);
1341 if ($form[0] === this.$form[0]) {
1342 // If root element, replace it
1343 this.$form = $new_form;
1345 $form.before($new_form).remove();
1347 this.process($new_form);
1350 * Used by direct <field> children of a <group> tag only
1351 * This method will add the implicit <label...> for every field
1354 preprocess_field: function($field) {
1356 var name = $field.attr('name'),
1357 field_colspan = parseInt($field.attr('colspan'), 10),
1358 field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
1360 if ($field.attr('nolabel') === '1')
1362 $field.attr('nolabel', '1');
1364 this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
1365 $(el).parents().each(function(unused, tag) {
1366 var name = tag.tagName.toLowerCase();
1367 if (name === "field" || name in self.tags_registry.map)
1374 var $label = $('<label/>').attr({
1376 "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
1377 "string": $field.attr('string'),
1378 "help": $field.attr('help'),
1379 "class": $field.attr('class'),
1381 $label.insertBefore($field);
1382 if (field_colspan > 1) {
1383 $field.attr('colspan', field_colspan - 1);
1387 process_field: function($field) {
1388 if ($field.parent().is('group')) {
1389 // No implicit labels for normal fields, only for <group> direct children
1390 var $label = this.preprocess_field($field);
1392 this.process($label);
1395 this.fields_to_init.push($field);
1398 process_group: function($group) {
1400 $group.children('field').each(function() {
1401 self.preprocess_field($(this));
1403 var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
1405 if ($new_group.first().is('table.oe_form_group')) {
1406 $table = $new_group;
1407 } else if ($new_group.filter('table.oe_form_group').length) {
1408 $table = $new_group.filter('table.oe_form_group').first();
1410 $table = $new_group.find('table.oe_form_group').first();
1414 cols = parseInt($group.attr('col') || 2, 10),
1418 $group.children().each(function(a,b,c) {
1419 var $child = $(this);
1420 var colspan = parseInt($child.attr('colspan') || 1, 10);
1421 var tagName = $child[0].tagName.toLowerCase();
1422 var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
1423 var newline = tagName === 'newline';
1425 // Note FME: those classes are used in layout debug mode
1426 if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
1427 $tr.addClass('oe_form_group_row_incomplete');
1429 $tr.addClass('oe_form_group_row_newline');
1436 if (!$tr || row_cols < colspan) {
1437 $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
1439 } else if (tagName==='group') {
1440 // When <group> <group/><group/> </group>, we need a spacing between the two groups
1441 $td.addClass('oe_group_right');
1443 row_cols -= colspan;
1445 // invisibility transfer
1446 var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
1447 var invisible = field_modifiers.invisible;
1448 self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
1450 $tr.append($td.append($child));
1451 children.push($child[0]);
1453 if (row_cols && $td) {
1454 $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
1456 $group.before($new_group).remove();
1458 $table.find('> tbody > tr').each(function() {
1459 var to_compute = [],
1462 $(this).children().each(function() {
1464 $child = $td.children(':first');
1465 if ($child.attr('cell-class')) {
1466 $td.addClass($child.attr('cell-class'));
1468 switch ($child[0].tagName.toLowerCase()) {
1472 if ($child.attr('for')) {
1473 $td.attr('width', '1%').addClass('oe_form_group_cell_label');
1474 row_cols-= $td.attr('colspan') || 1;
1479 var width = _.str.trim($child.attr('width') || ''),
1480 iwidth = parseInt(width, 10);
1482 if (width.substr(-1) === '%') {
1484 width = iwidth + '%';
1487 $td.css('min-width', width + 'px');
1489 $td.attr('width', width);
1490 $child.removeAttr('width');
1491 row_cols-= $td.attr('colspan') || 1;
1493 to_compute.push($td);
1499 var unit = Math.floor(total / row_cols);
1500 if (!$(this).is('.oe_form_group_row_incomplete')) {
1501 _.each(to_compute, function($td, i) {
1502 var width = parseInt($td.attr('colspan'), 10) * unit;
1503 $td.attr('width', width + '%');
1509 _.each(children, function(el) {
1510 self.process($(el));
1512 this.handle_common_properties($new_group, $group);
1515 process_notebook: function($notebook) {
1518 $notebook.find('> page').each(function() {
1519 var $page = $(this);
1520 var page_attrs = $page.getAttributes();
1521 page_attrs.id = _.uniqueId('notebook_page_');
1522 var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
1523 $page.contents().appendTo($new_page);
1524 $page.before($new_page).remove();
1525 var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
1526 page_attrs.__page = $new_page;
1527 page_attrs.__ic = ic;
1528 pages.push(page_attrs);
1530 $new_page.children().each(function() {
1531 self.process($(this));
1534 var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
1535 $notebook.contents().appendTo($new_notebook);
1536 $notebook.before($new_notebook).remove();
1537 self.process($($new_notebook.children()[0]));
1538 //tabs and invisibility handling
1539 $new_notebook.tabs();
1540 _.each(pages, function(page, i) {
1543 page.__ic.on("change:effective_invisible", null, function() {
1544 if (!page.__ic.get('effective_invisible') && page.autofocus) {
1545 $new_notebook.tabs('select', i);
1548 var current = $new_notebook.tabs("option", "selected");
1549 if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
1551 var first_visible = _.find(_.range(pages.length), function(i2) {
1552 return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
1554 if (first_visible !== undefined) {
1555 $new_notebook.tabs('select', first_visible);
1560 this.handle_common_properties($new_notebook, $notebook);
1561 return $new_notebook;
1563 process_separator: function($separator) {
1564 var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
1565 $separator.before($new_separator).remove();
1566 this.handle_common_properties($new_separator, $separator);
1567 return $new_separator;
1569 process_label: function($label) {
1570 var name = $label.attr("for"),
1571 field_orm = this.fvg.fields[name];
1573 string: $label.attr('string') || (field_orm || {}).string || '',
1574 help: $label.attr('help') || (field_orm || {}).help || '',
1575 _for: name ? _.uniqueId('oe-field-input-') : undefined,
1577 var align = parseFloat(dict.align);
1578 if (isNaN(align) || align === 1) {
1580 } else if (align === 0) {
1586 var $new_label = this.render_element('FormRenderingLabel', dict);
1587 $label.before($new_label).remove();
1588 this.handle_common_properties($new_label, $label);
1590 this.labels[name] = $new_label;
1594 handle_common_properties: function($new_element, $node) {
1595 var str_modifiers = $node.attr("modifiers") || "{}";
1596 var modifiers = JSON.parse(str_modifiers);
1598 if (modifiers.invisible !== undefined)
1599 ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
1600 $new_element.addClass($node.attr("class") || "");
1601 $new_element.attr('style', $node.attr('style'));
1602 return {invisibility_changer: ic,};
1609 If you read this documentation, it probably means that you were asked to use a form view widget outside of
1610 a form view. Before going further, you must understand that those fields were never really created for
1611 that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
1612 you to hack the system with more style.
1614 instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
1615 init: function(parent, eval_context) {
1616 this._super(parent);
1617 this.field_descs = {};
1618 this.eval_context = eval_context || {};
1620 display_invalid_fields: false,
1621 actual_mode: 'create',
1624 get_field_desc: function(field_name) {
1625 if (this.field_descs[field_name] === undefined) {
1626 this.field_descs[field_name] = {
1630 return this.field_descs[field_name];
1632 extend_field_desc: function(fields) {
1634 _.each(fields, function(v, k) {
1635 _.extend(self.get_field_desc(k), v);
1638 get_field_value: function(field_name) {
1641 set_values: function(values) {
1644 compute_domain: function(expression) {
1645 return instance.web.form.compute_domain(expression, {});
1647 build_eval_context: function() {
1648 return new instance.web.CompoundContext(this.eval_context);
1652 instance.web.form.compute_domain = function(expr, fields) {
1653 if (! (expr instanceof Array))
1656 for (var i = expr.length - 1; i >= 0; i--) {
1658 if (ex.length == 1) {
1659 var top = stack.pop();
1662 stack.push(stack.pop() || top);
1665 stack.push(stack.pop() && top);
1671 throw new Error(_.str.sprintf(
1672 _t("Unknown operator %s in domain %s"),
1673 ex, JSON.stringify(expr)));
1677 var field = fields[ex[0]];
1679 throw new Error(_.str.sprintf(
1680 _t("Unknown field %s in domain %s"),
1681 ex[0], JSON.stringify(expr)));
1683 var field_value = field.get_value ? field.get_value() : field.value;
1687 switch (op.toLowerCase()) {
1690 stack.push(_.isEqual(field_value, val));
1694 stack.push(!_.isEqual(field_value, val));
1697 stack.push(field_value < val);
1700 stack.push(field_value > val);
1703 stack.push(field_value <= val);
1706 stack.push(field_value >= val);
1709 if (!_.isArray(val)) val = [val];
1710 stack.push(_(val).contains(field_value));
1713 if (!_.isArray(val)) val = [val];
1714 stack.push(!_(val).contains(field_value));
1718 _t("Unsupported operator %s in domain %s"),
1719 op, JSON.stringify(expr));
1722 return _.all(stack, _.identity);
1725 instance.web.form.is_bin_size = function(v) {
1726 return (/^\d+(\.\d*)? \w+$/).test(v);
1730 * Must be applied over an class already possessing the PropertiesMixin.
1732 * Apply the result of the "invisible" domain to this.$el.
1734 instance.web.form.InvisibilityChangerMixin = {
1735 init: function(field_manager, invisible_domain) {
1737 this._ic_field_manager = field_manager;
1738 this._ic_invisible_modifier = invisible_domain;
1739 this._ic_field_manager.on("view_content_has_changed", this, function() {
1740 var result = self._ic_invisible_modifier === undefined ? false :
1741 self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
1742 self.set({"invisible": result});
1744 this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
1745 var check = function() {
1746 if (self.get("invisible") || self.get('force_invisible')) {
1747 self.set({"effective_invisible": true});
1749 self.set({"effective_invisible": false});
1752 this.on('change:invisible', this, check);
1753 this.on('change:force_invisible', this, check);
1757 this.on("change:effective_invisible", this, this._check_visibility);
1758 this._check_visibility();
1760 _check_visibility: function() {
1761 this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
1765 instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
1766 init: function(parent, field_manager, invisible_domain, $el) {
1767 this.setParent(parent);
1768 instance.web.PropertiesMixin.init.call(this);
1769 instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
1776 Base class for all fields, custom widgets and buttons to be displayed in the form view.
1779 - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
1780 the values of the "readonly" property and the "mode" property on the field manager.
1782 instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
1784 * @constructs instance.web.form.FormWidget
1785 * @extends instance.web.Widget
1787 * @param field_manager
1790 init: function(field_manager, node) {
1791 this._super(field_manager);
1792 this.field_manager = field_manager;
1793 if (this.field_manager instanceof instance.web.FormView)
1794 this.view = this.field_manager;
1796 this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
1797 instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
1799 this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
1805 // some events to make the property "effective_readonly" sync automatically with "readonly" and
1806 // "mode" on field_manager
1808 var test_effective_readonly = function() {
1809 self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
1811 this.on("change:readonly", this, test_effective_readonly);
1812 this.field_manager.on("change:actual_mode", this, test_effective_readonly);
1813 test_effective_readonly.call(this);
1815 renderElement: function() {
1816 this.process_modifiers();
1818 this.$el.addClass(this.node.attrs["class"] || "");
1820 destroy: function() {
1821 $.fn.tooltip('destroy');
1822 this._super.apply(this, arguments);
1825 * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
1827 * This method is an utility method that is meant to be called by child classes.
1829 * @param {jQuery} $e jQuery object of elements to bind focus/blur on
1831 setupFocus: function ($e) {
1834 focus: function () { self.trigger('focused'); },
1835 blur: function () { self.trigger('blurred'); }
1838 process_modifiers: function() {
1840 for (var a in this.modifiers) {
1841 if (!this.modifiers.hasOwnProperty(a)) { continue; }
1842 if (!_.include(["invisible"], a)) {
1843 var val = this.field_manager.compute_domain(this.modifiers[a]);
1849 do_attach_tooltip: function(widget, trigger, options) {
1850 widget = widget || this;
1851 trigger = trigger || this.$el;
1852 options = _.extend({
1853 delay: { show: 500, hide: 0 },
1855 var template = widget.template + '.tooltip';
1856 if (!QWeb.has_template(template)) {
1857 template = 'WidgetLabel.tooltip';
1859 return QWeb.render(template, {
1860 debug: instance.session.debug,
1865 //only show tooltip if we are in debug or if we have a help to show, otherwise it will display
1867 if (instance.session.debug || widget.node.attrs.help || (widget.field && widget.field.help)){
1868 $(trigger).tooltip(options);
1872 * Builds a new context usable for operations related to fields by merging
1873 * the fields'context with the action's context.
1875 build_context: function() {
1876 // only use the model's context if there is not context on the node
1877 var v_context = this.node.attrs.context;
1879 v_context = (this.field || {}).context || {};
1882 if (v_context.__ref || true) { //TODO: remove true
1883 var fields_values = this.field_manager.build_eval_context();
1884 v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
1888 build_domain: function() {
1889 var f_domain = this.field.domain || [];
1890 var n_domain = this.node.attrs.domain || null;
1891 // if there is a domain on the node, overrides the model's domain
1892 var final_domain = n_domain !== null ? n_domain : f_domain;
1893 if (!(final_domain instanceof Array) || true) { //TODO: remove true
1894 var fields_values = this.field_manager.build_eval_context();
1895 final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
1897 return final_domain;
1901 instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
1902 template: 'WidgetButton',
1903 init: function(field_manager, node) {
1904 node.attrs.type = node.attrs['data-button-type'];
1905 this.is_stat_button = /\boe_stat_button\b/.test(node.attrs['class']);
1906 this.icon_class = node.attrs.icon && "stat_button_icon fa " + node.attrs.icon + " fa-fw";
1907 this._super(field_manager, node);
1908 this.force_disabled = false;
1909 this.string = (this.node.attrs.string || '').replace(/_/g, '');
1910 if (JSON.parse(this.node.attrs.default_focus || "0")) {
1911 // TODO fme: provide enter key binding to widgets
1912 this.view.default_focus_button = this;
1914 if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
1915 this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
1919 this._super.apply(this, arguments);
1920 this.view.on('view_content_has_changed', this, this.check_disable);
1921 this.check_disable();
1922 this.$el.click(this.on_click);
1923 if (this.node.attrs.help || instance.session.debug) {
1924 this.do_attach_tooltip();
1926 this.setupFocus(this.$el);
1928 on_click: function() {
1930 this.force_disabled = true;
1931 this.check_disable();
1932 this.execute_action().always(function() {
1933 self.force_disabled = false;
1934 self.check_disable();
1937 execute_action: function() {
1939 var exec_action = function() {
1940 if (self.node.attrs.confirm) {
1941 var def = $.Deferred();
1942 var dialog = new instance.web.Dialog(this, {
1943 title: _t('Confirm'),
1945 {text: _t("Cancel"), click: function() {
1946 this.parents('.modal').modal('hide');
1949 {text: _t("Ok"), click: function() {
1951 self.on_confirmed().always(function() {
1952 self2.parents('.modal').modal('hide');
1957 }, $('<div/>').text(self.node.attrs.confirm)).open();
1958 dialog.on("closing", null, function() {def.resolve();});
1959 return def.promise();
1961 return self.on_confirmed();
1964 if (!this.node.attrs.special) {
1965 return this.view.recursive_save().then(exec_action);
1967 return exec_action();
1970 on_confirmed: function() {
1973 var context = this.build_context();
1974 return this.view.do_execute_action(
1975 _.extend({}, this.node.attrs, {context: context}),
1976 this.view.dataset, this.view.datarecord.id, function (reason) {
1977 if (!_.isObject(reason)) {
1978 self.view.recursive_reload();
1982 check_disable: function() {
1983 var disabled = (this.force_disabled || !this.view.is_interactible_record());
1984 this.$el.prop('disabled', disabled);
1985 this.$el.css('color', disabled ? 'grey' : '');
1990 * Interface to be implemented by fields.
1993 * - changed_value: triggered when the value of the field has changed. This can be due
1994 * to a user interaction or a call to set_value().
1997 instance.web.form.FieldInterface = {
1999 * Constructor takes 2 arguments:
2000 * - field_manager: Implements FieldManagerMixin
2001 * - node: the "<field>" node in json form
2003 init: function(field_manager, node) {},
2005 * Called by the form view to indicate the value of the field.
2007 * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
2008 * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
2009 * before the widget is inserted into the DOM.
2011 * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
2012 * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
2013 * also be able to handle any other format commonly used in the _defaults key on the models in the addons
2014 * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
2015 * no information is ever given to know which format is used.
2017 set_value: function(value_) {},
2019 * Get the current value of the widget.
2021 * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
2022 * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
2023 * For example if the field is marked as "required", a call to get_value() can return false.
2025 * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
2026 * return a default value according to the type of field.
2028 * This method is always assumed to perform synchronously, it can not return a promise.
2030 * If there was no user interaction to modify the value of the field, it is always assumed that
2031 * get_value() return the same semantic value than the one passed in the last call to set_value(),
2032 * although the syntax can be different. This can be the case for type of fields that have a different
2033 * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
2035 get_value: function() {},
2037 * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
2040 set_input_id: function(id) {},
2042 * Returns true if is_syntax_valid() returns true and the value is semantically
2043 * valid too according to the semantic restrictions applied to the field.
2045 is_valid: function() {},
2047 * Returns true if the field holds a value which is syntactically correct, ignoring
2048 * the potential semantic restrictions applied to the field.
2050 is_syntax_valid: function() {},
2052 * Must set the focus on the field. Return false if field is not focusable.
2054 focus: function() {},
2056 * Called when the translate button is clicked.
2058 on_translate: function() {},
2060 This method is called by the form view before reading on_change values and before saving. It tells
2061 the field to save its value before reading it using get_value(). Must return a promise.
2063 commit_value: function() {},
2067 * Abstract class for classes implementing FieldInterface.
2070 * - value: useful property to hold the value of the field. By default, set_value() and get_value()
2071 * set and retrieve the value property. Changing the value property also triggers automatically
2072 * a 'changed_value' event that inform the view to trigger on_changes.
2075 instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
2077 * @constructs instance.web.form.AbstractField
2078 * @extends instance.web.form.FormWidget
2080 * @param field_manager
2083 init: function(field_manager, node) {
2085 this._super(field_manager, node);
2086 this.name = this.node.attrs.name;
2087 this.field = this.field_manager.get_field_desc(this.name);
2088 this.widget = this.node.attrs.widget;
2089 this.string = this.node.attrs.string || this.field.string || this.name;
2090 this.options = instance.web.py_eval(this.node.attrs.options || '{}');
2091 this.set({'value': false});
2093 this.on("change:value", this, function() {
2094 this.trigger('changed_value');
2095 this._check_css_flags();
2098 renderElement: function() {
2101 if (this.field.translate && this.view) {
2102 this.$el.addClass('oe_form_field_translatable');
2103 this.$el.find('.oe_field_translate').click(this.on_translate);
2105 this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
2106 this.do_attach_tooltip(this, this.$label[0] || this.$el);
2107 if (instance.session.debug) {
2108 this.$label.off('dblclick').on('dblclick', function() {
2109 console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
2111 console.log("window.w =", window.w);
2114 if (!this.disable_utility_classes) {
2115 this.off("change:required", this, this._set_required);
2116 this.on("change:required", this, this._set_required);
2117 this._set_required();
2119 this._check_visibility();
2120 this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
2121 this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
2122 this._check_css_flags();
2125 var tmp = this._super();
2126 this.on("change:value", this, function() {
2127 if (! this.no_rerender)
2128 this.render_value();
2130 this.render_value();
2133 * Private. Do not use.
2135 _set_required: function() {
2136 this.$el.toggleClass('oe_form_required', this.get("required"));
2138 set_value: function(value_) {
2139 this.set({'value': value_});
2141 get_value: function() {
2142 return this.get('value');
2145 Utility method that all implementations should use to change the
2146 value without triggering a re-rendering.
2148 internal_set_value: function(value_) {
2149 var tmp = this.no_rerender;
2150 this.no_rerender = true;
2151 this.set({'value': value_});
2152 this.no_rerender = tmp;
2155 This method is called each time the value is modified.
2157 render_value: function() {},
2158 is_valid: function() {
2159 return this.is_syntax_valid() && !(this.get('required') && this.is_false());
2161 is_syntax_valid: function() {
2165 * Method useful to implement to ease validity testing. Must return true if the current
2166 * value is similar to false in OpenERP.
2168 is_false: function() {
2169 return this.get('value') === false;
2171 _check_css_flags: function() {
2172 if (this.field.translate) {
2173 this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
2175 if (!this.disable_utility_classes) {
2176 if (this.field_manager.get('display_invalid_fields')) {
2177 this.$el.toggleClass('oe_form_invalid', !this.is_valid());
2184 set_input_id: function(id) {
2185 this.id_for_label = id;
2187 on_translate: function() {
2189 var trans = new instance.web.DataSet(this, 'ir.translation');
2190 return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
2195 set_dimensions: function (height, width) {
2201 commit_value: function() {
2207 * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
2210 instance.web.form.ReinitializeWidgetMixin = {
2212 * Default implementation of, you should not override it, use initialize_field() instead.
2215 this.initialize_field();
2218 initialize_field: function() {
2219 this.on("change:effective_readonly", this, this.reinitialize);
2220 this.initialize_content();
2222 reinitialize: function() {
2223 this.destroy_content();
2224 this.renderElement();
2225 this.initialize_content();
2228 * Called to destroy anything that could have been created previously, called before a
2229 * re-initialization.
2231 destroy_content: function() {},
2233 * Called to initialize the content.
2235 initialize_content: function() {},
2239 * A mixin to apply on any field that has to completely re-render when its readonly state
2242 instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
2243 reinitialize: function() {
2244 instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
2245 var res = this.render_value();
2246 if (this.view && this.view.render_value_defs){
2247 this.view.render_value_defs.push(res);
2253 Some hack to make placeholders work in ie9.
2255 if (!('placeholder' in document.createElement('input'))) {
2256 document.addEventListener("DOMNodeInserted",function(event){
2257 var nodename = event.target.nodeName.toLowerCase();
2258 if ( nodename === "input" || nodename == "textarea" ) {
2259 $(event.target).placeholder();
2264 instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2265 template: 'FieldChar',
2266 widget_class: 'oe_form_field_char',
2268 'change input': 'store_dom_value',
2270 init: function (field_manager, node) {
2271 this._super(field_manager, node);
2272 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2274 initialize_content: function() {
2275 this.setupFocus(this.$('input'));
2277 store_dom_value: function () {
2278 if (!this.get('effective_readonly')
2279 && this.$('input').length
2280 && this.is_syntax_valid()) {
2281 this.internal_set_value(
2283 this.$('input').val()));
2286 commit_value: function () {
2287 this.store_dom_value();
2288 return this._super();
2290 render_value: function() {
2291 var show_value = this.format_value(this.get('value'), '');
2292 if (!this.get("effective_readonly")) {
2293 this.$el.find('input').val(show_value);
2295 if (this.password) {
2296 show_value = new Array(show_value.length + 1).join('*');
2298 this.$(".oe_form_char_content").text(show_value);
2301 is_syntax_valid: function() {
2302 if (!this.get("effective_readonly") && this.$("input").size() > 0) {
2304 this.parse_value(this.$('input').val(), '');
2312 parse_value: function(val, def) {
2313 return instance.web.parse_value(val, this, def);
2315 format_value: function(val, def) {
2316 return instance.web.format_value(val, this, def);
2318 is_false: function() {
2319 return this.get('value') === '' || this._super();
2322 var input = this.$('input:first')[0];
2323 return input ? input.focus() : false;
2325 set_dimensions: function (height, width) {
2326 this._super(height, width);
2327 this.$('input').css({
2334 instance.web.form.KanbanSelection = instance.web.form.FieldChar.extend({
2335 init: function (field_manager, node) {
2336 this._super(field_manager, node);
2338 start: function () {
2341 this._super.apply(this, arguments);
2342 // hook on form view content changed: recompute the states, because it may be related to the current stage
2343 this.getParent().on('view_content_has_changed', self, function () {
2344 self.render_value();
2347 prepare_dropdown_selection: function() {
2350 var selection = this.field.selection || [];
2351 var stage_id = _.isArray(this.view.datarecord.stage_id) ? this.view.datarecord.stage_id[0] : this.view.datarecord.stage_id;
2352 var legend_field = this.options && this.options.states_legend_field || false;
2353 var fields_to_read = _.map(
2354 this.options && this.options.states_legend || {},
2355 function (value, key, list) { return value; });
2356 if (legend_field && fields_to_read && stage_id) {
2357 var fetch_stage = new openerp.web.DataSet(
2359 self.view.fields[legend_field].field.relation).read_ids([stage_id],
2362 else { var fetch_stage = $.Deferred().resolve(false); }
2363 return $.when(fetch_stage).then(function (stage_data) {
2364 _.map(selection, function(res) {
2368 'state_name': res[1],
2370 if (stage_data && stage_data[0][self.options.states_legend[res[0]]]) {
2371 value['state_name'] = stage_data[0][self.options.states_legend[res[0]]];
2373 if (res[0] == 'normal') { value['state_class'] = 'oe_kanban_status'; }
2374 else if (res[0] == 'done') { value['state_class'] = 'oe_kanban_status oe_kanban_status_green'; }
2375 else { value['state_class'] = 'oe_kanban_status oe_kanban_status_red'; }
2381 render_value: function() {
2383 this.record_id = this.view.datarecord.id;
2384 var dd_fetched = this.prepare_dropdown_selection();;
2385 return $.when(dd_fetched).then(function (states) {
2386 self.states = states;
2387 self.$el.html(QWeb.render("KanbanSelection", {'widget': self}));
2388 self.$el.find('li').on('click', self.set_kanban_selection.bind(self));
2391 /* setting the value: in view mode, perform an asynchronous call and reload
2392 the form view; in edit mode, use set_value to save the new value that will
2393 be written when saving the record. */
2394 set_kanban_selection: function (ev) {
2396 var li = $(ev.target).closest('li');
2398 var value = String(li.data('value'));
2399 if (this.view.get('actual_mode') == 'view') {
2400 var write_values = {}
2401 write_values[self.name] = value;
2402 return this.view.dataset._model.call(
2406 self.view.dataset.get_context()
2407 ]).done(self.reload_record.bind(self));
2410 return this.set_value(value);
2414 reload_record: function() {
2419 instance.web.form.Priority = instance.web.form.FieldChar.extend({
2420 init: function (field_manager, node) {
2421 this._super(field_manager, node);
2423 prepare_priority: function() {
2425 var selection = this.field.selection || [];
2426 var init_value = selection && selection[0][0] || 0;
2427 var data = _.map(selection.slice(1), function(element, index) {
2429 'value': element[0],
2431 'click_value': element[0],
2433 if (index == 0 && self.get('value') == element[0]) {
2434 value['click_value'] = init_value;
2440 render_value: function() {
2442 this.record_id = this.view.datarecord.id;
2443 this.priorities = this.prepare_priority();
2444 this.$el.html(QWeb.render("Priority", {'widget': this}));
2445 this.$el.find('li').on('click', this.set_priority.bind(this));
2447 /* setting the value: in view mode, perform an asynchronous call and reload
2448 the form view; in edit mode, use set_value to save the new value that will
2449 be written when saving the record. */
2450 set_priority: function (ev) {
2452 var li = $(ev.target).closest('li');
2454 var value = String(li.data('value'));
2455 if (this.view.get('actual_mode') == 'view') {
2456 var write_values = {}
2457 write_values[self.name] = value;
2458 return this.view.dataset._model.call(
2462 self.view.dataset.get_context()
2463 ]).done(self.reload_record.bind(self));
2466 return this.set_value(value);
2471 reload_record: function() {
2476 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2477 process_modifiers: function () {
2479 this.set({ readonly: true });
2483 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2484 template: 'FieldEmail',
2485 initialize_content: function() {
2487 var $button = this.$el.find('button');
2488 $button.click(this.on_button_clicked);
2489 this.setupFocus($button);
2491 render_value: function() {
2492 if (!this.get("effective_readonly")) {
2496 .attr('href', 'mailto:' + this.get('value'))
2497 .text(this.get('value') || '');
2500 on_button_clicked: function() {
2501 if (!this.get('value') || !this.is_syntax_valid()) {
2502 this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2504 location.href = 'mailto:' + this.get('value');
2509 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2510 template: 'FieldUrl',
2511 initialize_content: function() {
2513 var $button = this.$el.find('button');
2514 $button.click(this.on_button_clicked);
2515 this.setupFocus($button);
2517 render_value: function() {
2518 if (!this.get("effective_readonly")) {
2521 var tmp = this.get('value');
2522 var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2524 tmp = "http://" + this.get('value');
2526 var text = this.get('value') ? this.node.attrs.text || tmp : '';
2527 this.$el.find('a').attr('href', tmp).text(text);
2530 on_button_clicked: function() {
2531 if (!this.get('value')) {
2532 this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2534 var url = $.trim(this.get('value'));
2535 if(/^www\./i.test(url))
2536 url = 'http://'+url;
2542 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2543 is_field_number: true,
2544 widget_class: 'oe_form_field_float',
2545 init: function (field_manager, node) {
2546 this._super(field_manager, node);
2547 this.internal_set_value(0);
2548 if (this.node.attrs.digits) {
2549 this.digits = this.node.attrs.digits;
2551 this.digits = this.field.digits;
2554 set_value: function(value_) {
2555 if (value_ === false || value_ === undefined) {
2556 // As in GTK client, floats default to 0
2559 this._super.apply(this, [value_]);
2561 focus: function () {
2562 var $input = this.$('input:first');
2563 return $input.length ? $input.select() : false;
2567 instance.web.form.FieldCharDomain = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2568 init: function(field_manager, node) {
2569 this._super.apply(this, arguments);
2573 this._super.apply(this, arguments);
2574 this.on("change:effective_readonly", this, function () {
2575 this.display_field();
2577 this.display_field();
2578 return this._super();
2580 set_value: function(value_) {
2582 this.set('value', value_ || false);
2583 this.display_field();
2585 display_field: function() {
2587 this.$el.html(instance.web.qweb.render("FieldCharDomain", {widget: this}));
2588 if (this.get('value')) {
2589 var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2590 var domain = instance.web.pyeval.eval('domain', this.get('value'));
2591 var ds = new instance.web.DataSetStatic(self, model, self.build_context());
2592 ds.call('search_count', [domain]).then(function (results) {
2593 $('.oe_domain_count', self.$el).text(results + ' records selected');
2594 if (self.get('effective_readonly')) {
2595 $('button span', self.$el).text(' See selection');
2598 $('button span', self.$el).text(' Change selection');
2602 $('.oe_domain_count', this.$el).text('0 record selected');
2603 $('button span', this.$el).text(' Select records');
2605 this.$('.select_records').on('click', self.on_click);
2607 on_click: function(event) {
2608 event.preventDefault();
2610 var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2611 this.pop = new instance.web.form.SelectCreatePopup(this);
2612 this.pop.select_element(
2614 title: this.get('effective_readonly') ? 'Selected records' : 'Select records...',
2615 readonly: this.get('effective_readonly'),
2616 disable_multiple_selection: this.get('effective_readonly'),
2617 no_create: this.get('effective_readonly'),
2618 }, [], this.build_context());
2619 this.pop.on("elements_selected", self, function(element_ids) {
2620 if (this.pop.$('input.oe_list_record_selector').prop('checked')) {
2621 var search_data = this.pop.searchview.build_search_data();
2622 var domain_done = instance.web.pyeval.eval_domains_and_contexts({
2623 domains: search_data.domains,
2624 contexts: search_data.contexts,
2625 group_by_seq: search_data.groupbys || []
2626 }).then(function (results) {
2627 return results.domain;
2631 var domain = [["id", "in", element_ids]];
2632 var domain_done = $.Deferred().resolve(domain);
2634 $.when(domain_done).then(function (domain) {
2635 var domain = self.pop.dataset.domain.concat(domain || []);
2636 self.set_value(domain);
2642 instance.web.DateTimeWidget = instance.web.Widget.extend({
2643 template: "web.datepicker",
2644 type_of_date: "datetime",
2646 'dp.change .oe_datepicker_main': 'change_datetime',
2647 'dp.show .oe_datepicker_main': 'set_datetime_default',
2648 'keypress .oe_datepicker_master': 'change_datetime',
2650 init: function(parent) {
2651 this._super(parent);
2652 this.name = parent.name;
2656 var l10n = _t.database.parameters;
2660 startDate: moment({ y: 1900 }),
2661 endDate: moment().add(200, "y"),
2662 calendarWeeks: true,
2664 time: 'fa fa-clock-o',
2665 date: 'fa fa-calendar',
2666 up: 'fa fa-chevron-up',
2667 down: 'fa fa-chevron-down'
2669 language : moment.locale(),
2670 format : instance.web.normalize_format(l10n.date_format +' '+ l10n.time_format),
2672 this.$input = this.$el.find('input.oe_datepicker_master');
2673 if (this.type_of_date === 'date') {
2674 options['pickTime'] = false;
2675 options['useSeconds'] = false;
2676 options['format'] = instance.web.normalize_format(l10n.date_format);
2678 this.picker = this.$('.oe_datepicker_main').datetimepicker(options);
2679 this.set_readonly(false);
2680 this.set({'value': false});
2682 set_value: function(value_) {
2683 this.set({'value': value_});
2684 this.$input.val(value_ ? this.format_client(value_) : '');
2686 get_value: function() {
2687 return this.get('value');
2689 set_value_from_ui_: function() {
2690 var value_ = this.$input.val() || false;
2691 this.set_value(this.parse_client(value_));
2693 set_readonly: function(readonly) {
2694 this.readonly = readonly;
2695 this.$input.prop('readonly', this.readonly);
2697 is_valid_: function() {
2698 var value_ = this.$input.val();
2699 if (value_ === "") {
2703 this.parse_client(value_);
2710 parse_client: function(v) {
2711 return instance.web.parse_value(v, {"widget": this.type_of_date});
2713 format_client: function(v) {
2714 return instance.web.format_value(v, {"widget": this.type_of_date});
2716 set_datetime_default: function(){
2717 //when opening datetimepicker the date and time by default should be the one from
2718 //the input field if any or the current day otherwise
2719 if (this.type_of_date === 'datetime') {
2720 value = moment().second(0);
2721 if (this.$input.val().length !== 0 && this.is_valid_()){
2722 var value = this.$input.val();
2724 this.$('.oe_datepicker_main').data('DateTimePicker').setValue(value);
2727 change_datetime: function(e) {
2728 if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
2729 this.set_value_from_ui_();
2730 this.trigger("datetime_changed");
2733 commit_value: function () {
2734 this.change_datetime();
2738 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2739 type_of_date: "date"
2742 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2743 template: "FieldDatetime",
2744 build_widget: function() {
2745 return new instance.web.DateTimeWidget(this);
2747 destroy_content: function() {
2748 if (this.datewidget) {
2749 this.datewidget.destroy();
2750 this.datewidget = undefined;
2753 initialize_content: function() {
2754 if (!this.get("effective_readonly")) {
2755 this.datewidget = this.build_widget();
2756 this.datewidget.on('datetime_changed', this, _.bind(function() {
2757 this.internal_set_value(this.datewidget.get_value());
2759 this.datewidget.appendTo(this.$el);
2760 this.setupFocus(this.datewidget.$input);
2763 render_value: function() {
2764 if (!this.get("effective_readonly")) {
2765 this.datewidget.set_value(this.get('value'));
2767 this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2770 is_syntax_valid: function() {
2771 if (!this.get("effective_readonly") && this.datewidget) {
2772 return this.datewidget.is_valid_();
2776 is_false: function() {
2777 return this.get('value') === '' || this._super();
2780 var input = this.datewidget && this.datewidget.$input[0];
2781 return input ? input.focus() : false;
2783 set_dimensions: function (height, width) {
2784 this._super(height, width);
2785 if (!this.get("effective_readonly")) {
2786 this.datewidget.$input.css('height', height);
2791 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2792 template: "FieldDate",
2793 build_widget: function() {
2794 return new instance.web.DateWidget(this);
2798 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2799 template: 'FieldText',
2801 'keyup': function (e) {
2802 if (e.which === $.ui.keyCode.ENTER) {
2803 e.stopPropagation();
2806 'keypress': function (e) {
2807 if (e.which === $.ui.keyCode.ENTER) {
2808 e.stopPropagation();
2811 'change textarea': 'store_dom_value',
2813 initialize_content: function() {
2815 if (! this.get("effective_readonly")) {
2816 this.$textarea = this.$el.find('textarea');
2817 this.auto_sized = false;
2818 this.default_height = this.$textarea.css('height');
2819 if (this.get("effective_readonly")) {
2820 this.$textarea.attr('disabled', 'disabled');
2822 this.setupFocus(this.$textarea);
2824 this.$textarea = undefined;
2827 commit_value: function () {
2828 if (! this.get("effective_readonly") && this.$textarea) {
2829 this.store_dom_value();
2831 return this._super();
2833 store_dom_value: function () {
2834 this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2836 render_value: function() {
2837 if (! this.get("effective_readonly")) {
2838 var show_value = instance.web.format_value(this.get('value'), this, '');
2839 if (show_value === '') {
2840 this.$textarea.css('height', parseInt(this.default_height, 10)+"px");
2842 this.$textarea.val(show_value);
2843 if (! this.auto_sized) {
2844 this.auto_sized = true;
2845 this.$textarea.autosize();
2847 this.$textarea.trigger("autosize");
2850 var txt = this.get("value") || '';
2851 this.$(".oe_form_text_content").text(txt);
2854 is_syntax_valid: function() {
2855 if (!this.get("effective_readonly") && this.$textarea) {
2857 instance.web.parse_value(this.$textarea.val(), this, '');
2865 is_false: function() {
2866 return this.get('value') === '' || this._super();
2868 focus: function($el) {
2869 var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2870 return input ? input.focus() : false;
2872 set_dimensions: function (height, width) {
2873 this._super(height, width);
2874 if (!this.get("effective_readonly") && this.$textarea) {
2875 this.$textarea.css({
2884 * FieldTextHtml Widget
2885 * Intended for FieldText widgets meant to display HTML content. This
2886 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2887 * To find more information about CLEditor configutation: go to
2888 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2890 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2891 template: 'FieldTextHtml',
2893 this._super.apply(this, arguments);
2895 initialize_content: function() {
2897 if (! this.get("effective_readonly")) {
2898 self._updating_editor = false;
2899 this.$textarea = this.$el.find('textarea');
2900 var width = ((this.node.attrs || {}).editor_width || 'calc(100% - 4px)');
2901 var height = ((this.node.attrs || {}).editor_height || 250);
2902 this.$textarea.cleditor({
2903 width: width, // width not including margins, borders or padding
2904 height: height, // height not including margins, borders or padding
2905 controls: // controls to add to the toolbar
2906 "bold italic underline strikethrough " +
2907 "| removeformat | bullets numbering | outdent " +
2908 "indent | link unlink | source",
2909 bodyStyle: // style to assign to document body contained within the editor
2910 "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2912 this.$cleditor = this.$textarea.cleditor()[0];
2913 this.$cleditor.change(function() {
2914 if (! self._updating_editor) {
2915 self.$cleditor.updateTextArea();
2916 self.internal_set_value(self.$textarea.val());
2919 if (this.field.translate) {
2920 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"/>')
2921 .click(this.on_translate);
2922 this.$cleditor.$toolbar.append($img);
2926 render_value: function() {
2927 if (! this.get("effective_readonly")) {
2928 this.$textarea.val(this.get('value') || '');
2929 this._updating_editor = true;
2930 this.$cleditor.updateFrame();
2931 this._updating_editor = false;
2933 this.$el.html(this.get('value'));
2938 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2939 template: 'FieldBoolean',
2942 this.$checkbox = $("input", this.$el);
2943 this.setupFocus(this.$checkbox);
2944 this.$el.click(_.bind(function() {
2945 this.internal_set_value(this.$checkbox.is(':checked'));
2947 var check_readonly = function() {
2948 self.$checkbox.prop('disabled', self.get("effective_readonly"));
2949 self.click_disabled_boolean();
2951 this.on("change:effective_readonly", this, check_readonly);
2952 check_readonly.call(this);
2953 this._super.apply(this, arguments);
2955 render_value: function() {
2956 this.$checkbox[0].checked = this.get('value');
2959 var input = this.$checkbox && this.$checkbox[0];
2960 return input ? input.focus() : false;
2962 click_disabled_boolean: function(){
2963 var $disabled = this.$el.find('input[type=checkbox]:disabled');
2964 $disabled.each(function (){
2965 $(this).next('div').remove();
2966 $(this).closest("span").append($('<div class="boolean"></div>'));
2972 The progressbar field expect a float from 0 to 100.
2974 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2975 template: 'FieldProgressBar',
2976 render_value: function() {
2977 this.$el.progressbar({
2978 value: this.get('value') || 0,
2979 disabled: this.get("effective_readonly")
2981 var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
2982 this.$('span').html(formatted_value + '%');
2987 The PercentPie field expect a float from 0 to 100.
2989 instance.web.form.FieldPercentPie = instance.web.form.AbstractField.extend({
2990 template: 'FieldPercentPie',
2992 render_value: function() {
2993 var value = this.get('value'),
2994 formatted_value = Math.round(value || 0) + '%',
2995 svg = this.$('svg')[0];
2998 nv.addGraph(function() {
2999 var width = 42, height = 42;
3000 var chart = nv.models.pieChart()
3003 .margin({top: 0, right: 0, bottom: 0, left: 0})
3008 .color(['#7C7BAD','#DDD'])
3012 .datum([{'x': 'value', 'y': value}, {'x': 'complement', 'y': 100 - value}])
3015 .attr('style', 'width: ' + width + 'px; height:' + height + 'px;');
3019 .attr({x: width/2, y: height/2 + 3, 'text-anchor': 'middle'})
3020 .style({"font-size": "10px", "font-weight": "bold"})
3021 .text(formatted_value);
3030 The FieldBarChart expectsa list of values (indeed)
3032 instance.web.form.FieldBarChart = instance.web.form.AbstractField.extend({
3033 template: 'FieldBarChart',
3035 render_value: function() {
3036 var value = JSON.parse(this.get('value'));
3037 var svg = this.$('svg')[0];
3039 nv.addGraph(function() {
3040 var width = 34, height = 34;
3041 var chart = nv.models.discreteBarChart()
3042 .x(function (d) { return d.tooltip })
3043 .y(function (d) { return d.value })
3046 .margin({top: 0, right: 0, bottom: 0, left: 0})
3049 .transitionDuration(350)
3054 .datum([{key: 'values', values: value}])
3057 .attr('style', 'width: ' + (width + 4) + 'px; height: ' + (height + 8) + 'px;');
3059 nv.utils.windowResize(chart.update);
3068 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3069 template: 'FieldSelection',
3071 'change select': 'store_dom_value',
3073 init: function(field_manager, node) {
3075 this._super(field_manager, node);
3076 this.set("value", false);
3077 this.set("values", []);
3078 this.records_orderer = new instance.web.DropMisordered();
3079 this.field_manager.on("view_content_has_changed", this, function() {
3080 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
3081 if (! _.isEqual(domain, this.get("domain"))) {
3082 this.set("domain", domain);
3086 initialize_field: function() {
3087 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3088 this.on("change:domain", this, this.query_values);
3089 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
3090 this.on("change:values", this, this.render_value);
3092 query_values: function() {
3095 if (this.field.type === "many2one") {
3096 var model = new openerp.Model(openerp.session, this.field.relation);
3097 def = model.call("name_search", ['', this.get("domain")], {"context": this.build_context()});
3099 var values = _.reject(this.field.selection, function (v) { return v[0] === false && v[1] === ''; });
3100 def = $.when(values);
3102 this.records_orderer.add(def).then(function(values) {
3103 if (! _.isEqual(values, self.get("values"))) {
3104 self.set("values", values);
3108 initialize_content: function() {
3109 // Flag indicating whether we're in an event chain containing a change
3110 // event on the select, in order to know what to do on keyup[RETURN]:
3111 // * If the user presses [RETURN] as part of changing the value of a
3112 // selection, we should just let the value change and not let the
3113 // event broadcast further (e.g. to validating the current state of
3114 // the form in editable list view, which would lead to saving the
3115 // current row or switching to the next one)
3116 // * If the user presses [RETURN] with a select closed (side-effect:
3117 // also if the user opened the select and pressed [RETURN] without
3118 // changing the selected value), takes the action as validating the
3120 var ischanging = false;
3121 var $select = this.$el.find('select')
3122 .change(function () { ischanging = true; })
3123 .click(function () { ischanging = false; })
3124 .keyup(function (e) {
3125 if (e.which !== 13 || !ischanging) { return; }
3126 e.stopPropagation();
3129 this.setupFocus($select);
3131 commit_value: function () {
3132 this.store_dom_value();
3133 return this._super();
3135 store_dom_value: function () {
3136 if (!this.get('effective_readonly') && this.$('select').length) {
3137 var val = JSON.parse(this.$('select').val());
3138 this.internal_set_value(val);
3141 set_value: function(value_) {
3142 value_ = value_ === null ? false : value_;
3143 value_ = value_ instanceof Array ? value_[0] : value_;
3144 this._super(value_);
3146 render_value: function() {
3147 var values = this.get("values");
3148 values = [[false, this.node.attrs.placeholder || '']].concat(values);
3149 var found = _.find(values, function(el) { return el[0] === this.get("value"); }, this);
3151 found = [this.get("value"), _t('Unknown')];
3152 values = [found].concat(values);
3154 if (! this.get("effective_readonly")) {
3155 this.$().html(QWeb.render("FieldSelectionSelect", {widget: this, values: values}));
3156 this.$("select").val(JSON.stringify(found[0]));
3158 this.$el.text(found[1]);
3162 var input = this.$('select:first')[0];
3163 return input ? input.focus() : false;
3165 set_dimensions: function (height, width) {
3166 this._super(height, width);
3167 this.$('select').css({
3174 instance.web.form.FieldRadio = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3175 template: 'FieldRadio',
3177 'click input': 'click_change_value'
3179 init: function(field_manager, node) {
3180 /* Radio button widget: Attributes options:
3181 * - "horizontal" to display in column
3182 * - "no_radiolabel" don't display text values
3184 this._super(field_manager, node);
3185 this.selection = _.clone(this.field.selection) || [];
3186 this.domain = false;
3187 this.uniqueId = _.uniqueId("radio");
3189 initialize_content: function () {
3190 this.on("change:effective_readonly", this, this.render_value);
3191 this.field_manager.on("view_content_has_changed", this, this.get_selection);
3192 this.get_selection();
3194 click_change_value: function (event) {
3195 var val = $(event.target).val();
3196 val = this.field.type == "selection" ? val : +val;
3197 if (val == this.get_value()) {
3198 this.set_value(false);
3200 this.set_value(val);
3203 /** Get the selection and render it
3204 * selection: [[identifier, value_to_display], ...]
3205 * For selection fields: this is directly given by this.field.selection
3206 * For many2one fields: perform a search on the relation of the many2one field
3208 get_selection: function() {
3211 var def = $.Deferred();
3212 if (self.field.type == "many2one") {
3213 var domain = instance.web.pyeval.eval('domain', this.build_domain()) || [];
3214 if (! _.isEqual(self.domain, domain)) {
3215 self.domain = domain;
3216 var ds = new instance.web.DataSetStatic(self, self.field.relation, self.build_context());
3217 ds.call('search', [self.domain])
3218 .then(function (records) {
3219 ds.name_get(records).then(function (records) {
3220 selection = records;
3225 selection = self.selection;
3229 else if (self.field.type == "selection") {
3230 selection = self.field.selection || [];
3233 return def.then(function () {
3234 if (! _.isEqual(selection, self.selection)) {
3235 self.selection = _.clone(selection);
3236 self.renderElement();
3237 self.render_value();
3241 set_value: function (value_) {
3243 if (this.field.type == "selection") {
3244 value_ = _.find(this.field.selection, function (sel) { return sel[0] == value_;});
3246 else if (!this.selection.length) {
3247 this.selection = [value_];
3250 this._super(value_);
3252 get_value: function () {
3253 var value = this.get('value');
3254 return value instanceof Array ? value[0] : value;
3256 render_value: function () {
3258 this.$el.toggleClass("oe_readonly", this.get('effective_readonly'));
3259 if (this.get_value()) {
3260 this.$("input").filter(function () {return this.value == self.get_value();}).prop("checked", true);
3261 this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : "");
3266 // jquery autocomplete tweak to allow html and classnames
3268 var proto = $.ui.autocomplete.prototype,
3269 initSource = proto._initSource;
3271 function filter( array, term ) {
3272 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
3273 return $.grep( array, function(value_) {
3274 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
3279 _initSource: function() {
3280 if ( this.options.html && $.isArray(this.options.source) ) {
3281 this.source = function( request, response ) {
3282 response( filter( this.options.source, request.term ) );
3285 initSource.call( this );
3289 _renderItem: function( ul, item) {
3290 return $( "<li></li>" )
3291 .data( "item.autocomplete", item )
3292 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
3294 .addClass(item.classname);
3300 A mixin containing some useful methods to handle completion inputs.
3302 The widget containing this option can have these arguments in its widget options:
3303 - no_quick_create: if true, it will disable the quick create
3305 instance.web.form.CompletionFieldMixin = {
3308 this.orderer = new instance.web.DropMisordered();
3311 * Call this method to search using a string.
3313 get_search_result: function(search_val) {
3316 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
3317 this.last_query = search_val;
3318 var exclusion_domain = [], ids_blacklist = this.get_search_blacklist();
3319 if (!_(ids_blacklist).isEmpty()) {
3320 exclusion_domain.push(['id', 'not in', ids_blacklist]);
3323 return this.orderer.add(dataset.name_search(
3324 search_val, new instance.web.CompoundDomain(self.build_domain(), exclusion_domain),
3325 'ilike', this.limit + 1, self.build_context())).then(function(data) {
3326 self.last_search = data;
3327 // possible selections for the m2o
3328 var values = _.map(data, function(x) {
3329 x[1] = x[1].split("\n")[0];
3331 label: _.str.escapeHTML(x[1]),
3338 // search more... if more results that max
3339 if (values.length > self.limit) {
3340 values = values.slice(0, self.limit);
3342 label: _t("Search More..."),
3343 action: function() {
3344 dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
3345 self._search_create_popup("search", data);
3348 classname: 'oe_m2o_dropdown_option'
3352 var raw_result = _(data.result).map(function(x) {return x[1];});
3353 if (search_val.length > 0 && !_.include(raw_result, search_val) &&
3354 ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3356 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
3357 $('<span />').text(search_val).html()),
3358 action: function() {
3359 self._quick_create(search_val);
3361 classname: 'oe_m2o_dropdown_option'
3365 if (!(self.options && (self.options.no_create || self.options.no_create_edit))){
3367 label: _t("Create and Edit..."),
3368 action: function() {
3369 self._search_create_popup("form", undefined, self._create_context(search_val));
3371 classname: 'oe_m2o_dropdown_option'
3374 else if (values.length == 0)
3376 label: _t("No results to show..."),
3377 action: function() {},
3378 classname: 'oe_m2o_dropdown_option'
3384 get_search_blacklist: function() {
3387 _quick_create: function(name) {
3389 var slow_create = function () {
3390 self._search_create_popup("form", undefined, self._create_context(name));
3392 if (self.options.quick_create === undefined || self.options.quick_create) {
3393 new instance.web.DataSet(this, this.field.relation, self.build_context())
3394 .name_create(name).done(function(data) {
3395 if (!self.get('effective_readonly'))
3396 self.add_id(data[0]);
3397 }).fail(function(error, event) {
3398 event.preventDefault();
3404 // all search/create popup handling
3405 _search_create_popup: function(view, ids, context) {
3407 var pop = new instance.web.form.SelectCreatePopup(this);
3409 self.field.relation,
3411 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3412 initial_ids: ids ? _.map(ids, function(x) {return x[0];}) : undefined,
3414 disable_multiple_selection: true
3416 self.build_domain(),
3417 new instance.web.CompoundContext(self.build_context(), context || {})
3419 pop.on("elements_selected", self, function(element_ids) {
3420 self.add_id(element_ids[0]);
3427 add_id: function(id) {},
3428 _create_context: function(name) {
3430 var field = (this.options || {}).create_name_field;
3431 if (field === undefined)
3433 if (field !== false && name && (this.options || {}).quick_create !== false)
3434 tmp["default_" + field] = name;
3439 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3440 template: "M2ODialog",
3441 init: function(parent) {
3442 this.name = parent.string;
3443 this._super(parent, {
3444 title: _.str.sprintf(_t("Create a %s"), parent.string),
3450 var text = _.str.sprintf(_t("You are creating a new %s, are you sure it does not exist yet?"), self.name);
3451 this.$("p").text( text );
3452 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3453 this.$("input").val(this.getParent().last_query);
3454 this.$buttons.find(".oe_form_m2o_qc_button").click(function(e){
3455 if (self.$("input").val() != ''){
3456 self.getParent()._quick_create(self.$("input").val());
3460 self.$("input").focus();
3463 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3464 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3467 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3473 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3474 template: "FieldMany2One",
3476 'keydown input': function (e) {
3478 case $.ui.keyCode.UP:
3479 case $.ui.keyCode.DOWN:
3480 e.stopPropagation();
3484 init: function(field_manager, node) {
3485 this._super(field_manager, node);
3486 instance.web.form.CompletionFieldMixin.init.call(this);
3487 this.set({'value': false});
3488 this.display_value = {};
3489 this.display_value_backup = {};
3490 this.last_search = [];
3491 this.floating = false;
3492 this.current_display = null;
3493 this.is_started = false;
3494 this.ignore_focusout = false;
3496 reinit_value: function(val) {
3497 this.internal_set_value(val);
3498 this.floating = false;
3499 if (this.is_started)
3500 this.render_value();
3502 initialize_field: function() {
3503 this.is_started = true;
3504 instance.web.bus.on('click', this, function() {
3505 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3506 this.$input.autocomplete("close");
3509 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3511 initialize_content: function() {
3512 if (!this.get("effective_readonly"))
3513 this.render_editable();
3515 destroy_content: function () {
3516 if (this.$drop_down) {
3517 this.$drop_down.off('click');
3518 delete this.$drop_down;
3521 this.$input.closest(".modal .modal-content").off('scroll');
3522 this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3523 'focus focusout change keydown');
3526 if (this.$follow_button) {
3527 this.$follow_button.off('blur focus click');
3528 delete this.$follow_button;
3531 destroy: function () {
3532 this.destroy_content();
3533 return this._super();
3535 init_error_displayer: function() {
3538 hide_error_displayer: function() {
3541 show_error_displayer: function() {
3542 new instance.web.form.M2ODialog(this).open();
3544 render_editable: function() {
3546 this.$input = this.$el.find("input");
3548 this.init_error_displayer();
3550 self.$input.on('focus', function() {
3551 self.hide_error_displayer();
3554 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3555 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3557 this.$follow_button.click(function(ev) {
3558 ev.preventDefault();
3559 if (!self.get('value')) {
3563 var pop = new instance.web.form.FormOpenPopup(self);
3564 var context = self.build_context().eval();
3565 var model_obj = new instance.web.Model(self.field.relation);
3566 model_obj.call('get_formview_id', [self.get("value"), context]).then(function(view_id){
3568 self.field.relation,
3570 self.build_context(),
3572 title: _t("Open: ") + self.string,
3576 pop.on('write_completed', self, function(){
3577 self.display_value = {};
3578 self.display_value_backup = {};
3579 self.render_value();
3581 self.trigger('changed_value');
3586 // some behavior for input
3587 var input_changed = function() {
3588 if (self.current_display !== self.$input.val()) {
3589 self.current_display = self.$input.val();
3590 if (self.$input.val() === "") {
3591 self.internal_set_value(false);
3592 self.floating = false;
3594 self.floating = true;
3598 this.$input.keydown(input_changed);
3599 this.$input.change(input_changed);
3600 this.$drop_down.click(function() {
3601 self.$input.focus();
3602 if (self.$input.autocomplete("widget").is(":visible")) {
3603 self.$input.autocomplete("close");
3605 if (self.get("value") && ! self.floating) {
3606 self.$input.autocomplete("search", "");
3608 self.$input.autocomplete("search");
3613 // Autocomplete close on dialog content scroll
3614 var close_autocomplete = _.debounce(function() {
3615 if (self.$input.autocomplete("widget").is(":visible")) {
3616 self.$input.autocomplete("close");
3619 this.$input.closest(".modal .modal-content").on('scroll', this, close_autocomplete);
3621 self.ed_def = $.Deferred();
3622 self.uned_def = $.Deferred();
3624 var ed_duration = 15000;
3625 var anyoneLoosesFocus = function (e) {
3626 if (self.ignore_focusout) { return; }
3628 if (self.floating) {
3629 if (self.last_search.length > 0) {
3630 if (self.last_search[0][0] != self.get("value")) {
3631 self.display_value = {};
3632 self.display_value_backup = {};
3633 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3634 self.reinit_value(self.last_search[0][0]);
3637 self.render_value();
3641 self.reinit_value(false);
3643 self.floating = false;
3645 if (used && self.get("value") === false && ! self.no_ed && ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3646 self.ed_def.reject();
3647 self.uned_def.reject();
3648 self.ed_def = $.Deferred();
3649 self.ed_def.done(function() {
3650 self.show_error_displayer();
3651 ignore_blur = false;
3652 self.trigger('focused');
3655 setTimeout(function() {
3656 self.ed_def.resolve();
3657 self.uned_def.reject();
3658 self.uned_def = $.Deferred();
3659 self.uned_def.done(function() {
3660 self.hide_error_displayer();
3662 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3666 self.ed_def.reject();
3669 var ignore_blur = false;
3671 focusout: anyoneLoosesFocus,
3672 focus: function () { self.trigger('focused'); },
3673 autocompleteopen: function () { ignore_blur = true; },
3674 autocompleteclose: function () { setTimeout(function() {ignore_blur = false;},0); },
3676 // autocomplete open
3677 if (ignore_blur) { $(this).focus(); return; }
3678 if (_(self.getChildren()).any(function (child) {
3679 return child instanceof instance.web.form.AbstractFormPopup;
3681 self.trigger('blurred');
3685 var isSelecting = false;
3687 this.$input.autocomplete({
3688 source: function(req, resp) {
3689 self.get_search_result(req.term).done(function(result) {
3693 select: function(event, ui) {
3697 self.display_value = {};
3698 self.display_value_backup = {};
3699 self.display_value["" + item.id] = item.name;
3700 self.reinit_value(item.id);
3701 } else if (item.action) {
3703 // Cancel widget blurring, to avoid form blur event
3704 self.trigger('focused');
3708 focus: function(e, ui) {
3712 // disabled to solve a bug, but may cause others
3713 //close: anyoneLoosesFocus,
3717 // set position for list of suggestions box
3718 this.$input.autocomplete( "option", "position", { my : "left top", at: "left bottom" } );
3719 this.$input.autocomplete("widget").openerpClass();
3720 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3721 this.$input.keyup(function(e) {
3722 if (e.which === 13) { // ENTER
3724 e.stopPropagation();
3726 isSelecting = false;
3728 this.setupFocus(this.$follow_button);
3730 render_value: function(no_recurse) {
3732 if (! this.get("value")) {
3733 this.display_string("");
3736 var display = this.display_value["" + this.get("value")];
3738 this.display_string(display);
3742 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3743 this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3745 self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
3748 self.display_value["" + self.get("value")] = data[0][1];
3749 self.render_value(true);
3750 }).fail( function (data, event) {
3751 // avoid displaying crash errors as many2One should be name_get compliant
3752 event.preventDefault();
3753 self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")];
3754 self.render_value(true);
3758 display_string: function(str) {
3760 if (!this.get("effective_readonly")) {
3761 this.$input.val(str.split("\n")[0]);
3762 this.current_display = this.$input.val();
3763 if (this.is_false()) {
3764 this.$('.oe_m2o_cm_button').css({'display':'none'});
3766 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3769 var lines = _.escape(str).split("\n");
3773 follow = _.rest(lines).join("<br />");
3776 var $link = this.$el.find('.oe_form_uri')
3779 if (! this.options.no_open)
3780 $link.click(function () {
3781 var context = self.build_context().eval();
3782 var model_obj = new instance.web.Model(self.field.relation);
3783 model_obj.call('get_formview_action', [self.get("value"), context]).then(function(action){
3784 self.do_action(action);
3788 $(".oe_form_m2o_follow", this.$el).html(follow);
3791 set_value: function(value_) {
3793 if (value_ instanceof Array) {
3794 this.display_value = {};
3795 this.display_value_backup = {};
3796 if (! this.options.always_reload) {
3797 this.display_value["" + value_[0]] = value_[1];
3800 this.display_value_backup["" + value_[0]] = value_[1];
3804 value_ = value_ || false;
3805 this.reinit_value(value_);
3807 get_displayed: function() {
3808 return this.display_value["" + this.get("value")];
3810 add_id: function(id) {
3811 this.display_value = {};
3812 this.display_value_backup = {};
3813 this.reinit_value(id);
3815 is_false: function() {
3816 return ! this.get("value");
3818 focus: function () {
3819 var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3820 return input ? input.focus() : false;
3822 _quick_create: function() {
3824 this.ed_def.reject();
3825 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3827 _search_create_popup: function() {
3829 this.ed_def.reject();
3830 this.ignore_focusout = true;
3831 this.reinit_value(false);
3832 var res = instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3833 this.ignore_focusout = false;
3837 set_dimensions: function (height, width) {
3838 this._super(height, width);
3839 if (!this.get("effective_readonly") && this.$input)
3840 this.$input.css('height', height);
3844 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3845 template: 'Many2OneButton',
3846 init: function(field_manager, node) {
3847 this._super.apply(this, arguments);
3850 this._super.apply(this, arguments);
3853 set_button: function() {
3856 this.$button.remove();
3859 this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3860 this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3861 this.$button.addClass('oe_link').css({'padding':'4px'});
3862 this.$el.append(this.$button);
3863 this.$button.on('click', self.on_click);
3865 on_click: function(ev) {
3867 this.popup = new instance.web.form.FormOpenPopup(this);
3868 this.popup.show_element(
3869 this.field.relation,
3871 this.build_context(),
3872 {title: this.string}
3874 this.popup.on('create_completed', self, function(r) {
3878 set_value: function(value_) {
3880 if (value_ instanceof Array) {
3883 value_ = value_ || false;
3884 this.set('value', value_);
3890 * Abstract-ish ListView.List subclass adding an "Add an item" row to replace
3891 * the big ugly button in the header.
3893 * Requires the implementation of a ``is_readonly`` method (usually a proxy to
3894 * the corresponding field's readonly or effective_readonly property) to
3895 * decide whether the special row should or should not be inserted.
3897 * Optionally an ``_add_row_class`` attribute can be set for the class(es) to
3898 * set on the insertion row.
3900 instance.web.form.AddAnItemList = instance.web.ListView.List.extend({
3901 pad_table_to: function (count) {
3902 if (!this.view.is_action_enabled('create') || this.is_readonly()) {
3907 this._super(count > 0 ? count - 1 : 0);
3910 var columns = _(this.columns).filter(function (column) {
3911 return column.invisible !== '1';
3913 if (this.options.selectable) { columns++; }
3914 if (this.options.deletable) { columns++; }
3916 var $cell = $('<td>', {
3918 'class': this._add_row_class || ''
3920 $('<a>', {href: '#'}).text(_t("Add an item"))
3921 .mousedown(function () {
3922 // FIXME: needs to be an official API somehow
3923 if (self.view.editor.is_editing()) {
3924 self.view.__ignore_blur = true;
3927 .click(function (e) {
3929 e.stopPropagation();
3930 // FIXME: there should also be an API for that one
3931 if (self.view.editor.form.__blur_timeout) {
3932 clearTimeout(self.view.editor.form.__blur_timeout);
3933 self.view.editor.form.__blur_timeout = false;
3935 self.view.ensure_saved().done(function () {
3936 self.view.do_add_record();
3940 var $padding = this.$current.find('tr:not([data-id]):first');
3941 var $newrow = $('<tr>').append($cell);
3942 if ($padding.length) {
3943 $padding.before($newrow);
3945 this.$current.append($newrow)
3951 # Values: (0, 0, { fields }) create
3952 # (1, ID, { fields }) update
3953 # (2, ID) remove (delete)
3954 # (3, ID) unlink one (target id or target of relation)
3956 # (5) unlink all (only valid for one2many)
3961 'create': function (values) {
3962 return [commands.CREATE, false, values];
3964 // (1, id, {values})
3966 'update': function (id, values) {
3967 return [commands.UPDATE, id, values];
3971 'delete': function (id) {
3972 return [commands.DELETE, id, false];
3974 // (3, id[, _]) removes relation, but not linked record itself
3976 'forget': function (id) {
3977 return [commands.FORGET, id, false];
3981 'link_to': function (id) {
3982 return [commands.LINK_TO, id, false];
3986 'delete_all': function () {
3987 return [5, false, false];
3989 // (6, _, ids) replaces all linked records with provided ids
3991 'replace_with': function (ids) {
3992 return [6, false, ids];
3995 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3996 multi_selection: false,
3997 disable_utility_classes: true,
3998 init: function(field_manager, node) {
3999 this._super(field_manager, node);
4000 this.is_loaded = $.Deferred();
4001 this.initial_is_loaded = this.is_loaded;
4002 this.form_last_update = $.Deferred();
4003 this.init_form_last_update = this.form_last_update;
4004 this.is_started = false;
4005 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
4006 this.dataset.o2m = this;
4007 this.dataset.parent_view = this.view;
4008 this.dataset.child_name = this.name;
4010 this.dataset.on('dataset_changed', this, function() {
4011 self.trigger_on_change();
4016 this._super.apply(this, arguments);
4017 this.$el.addClass('oe_form_field oe_form_field_one2many');
4022 this.is_loaded.done(function() {
4023 self.on("change:effective_readonly", self, function() {
4024 self.is_loaded = self.is_loaded.then(function() {
4025 self.viewmanager.destroy();
4026 return $.when(self.load_views()).done(function() {
4027 self.reload_current_view();
4032 this.is_started = true;
4033 this.reload_current_view();
4035 trigger_on_change: function() {
4036 this.trigger('changed_value');
4038 load_views: function() {
4041 var modes = this.node.attrs.mode;
4042 modes = !!modes ? modes.split(",") : ["tree"];
4044 _.each(modes, function(mode) {
4045 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
4046 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
4050 view_type: mode == "tree" ? "list" : mode,
4053 if (self.field.views && self.field.views[mode]) {
4054 view.embedded_view = self.field.views[mode];
4056 if(view.view_type === "list") {
4057 _.extend(view.options, {
4059 selectable: self.multi_selection,
4061 import_enabled: false,
4064 if (self.get("effective_readonly")) {
4065 _.extend(view.options, {
4070 } else if (view.view_type === "form") {
4071 if (self.get("effective_readonly")) {
4072 view.view_type = 'form';
4074 _.extend(view.options, {
4075 not_interactible_on_create: true,
4077 } else if (view.view_type === "kanban") {
4078 _.extend(view.options, {
4079 confirm_on_delete: false,
4081 if (self.get("effective_readonly")) {
4082 _.extend(view.options, {
4083 action_buttons: false,
4084 quick_creatable: false,
4086 read_only_mode: true,
4094 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
4095 this.viewmanager.o2m = self;
4096 var once = $.Deferred().done(function() {
4097 self.init_form_last_update.resolve();
4099 var def = $.Deferred().done(function() {
4100 self.initial_is_loaded.resolve();
4102 this.viewmanager.on("controller_inited", self, function(view_type, controller) {
4103 controller.o2m = self;
4104 if (view_type == "list") {
4105 if (self.get("effective_readonly")) {
4106 controller.on('edit:before', self, function (e) {
4109 _(controller.columns).find(function (column) {
4110 if (!(column instanceof instance.web.list.Handle)) {
4113 column.modifiers.invisible = true;
4117 } else if (view_type === "form") {
4118 if (self.get("effective_readonly")) {
4119 $(".oe_form_buttons", controller.$el).children().remove();
4121 controller.on("load_record", self, function(){
4124 controller.on('pager_action_executed',self,self.save_any_view);
4125 } else if (view_type == "graph") {
4126 self.reload_current_view();
4130 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
4131 $.when(self.save_any_view()).done(function() {
4132 if (n_mode === "list") {
4133 $.async_when().done(function() {
4134 self.reload_current_view();
4139 $.async_when().done(function () {
4140 self.viewmanager.appendTo(self.$el);
4144 reload_current_view: function() {
4146 self.is_loaded = self.is_loaded.then(function() {
4147 var view = self.get_active_view();
4148 if (view.type === "list") {
4149 return view.controller.reload_content();
4150 } else if (view.type === "form") {
4151 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
4152 self.dataset.index = 0;
4154 var act = function() {
4155 return view.controller.do_show();
4157 self.form_last_update = self.form_last_update.then(act, act);
4158 return self.form_last_update;
4159 } else if (view.controller.do_search) {
4160 return view.controller.do_search(self.build_domain(), self.dataset.get_context(), []);
4163 return self.is_loaded;
4165 get_active_view: function () {
4167 * Returns the current active view if any.
4169 return (this.viewmanager && this.viewmanager.active_view);
4171 set_value: function(value_) {
4172 value_ = value_ || [];
4174 var view = this.get_active_view();
4175 this.dataset.reset_ids([]);
4177 if(value_.length >= 1 && value_[0] instanceof Array) {
4179 _.each(value_, function(command) {
4180 var obj = {values: command[2]};
4181 switch (command[0]) {
4182 case commands.CREATE:
4183 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4185 self.dataset.to_create.push(obj);
4186 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4189 case commands.UPDATE:
4190 obj['id'] = command[1];
4191 self.dataset.to_write.push(obj);
4192 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4195 case commands.DELETE:
4196 self.dataset.to_delete.push({id: command[1]});
4198 case commands.LINK_TO:
4199 ids.push(command[1]);
4201 case commands.DELETE_ALL:
4202 self.dataset.delete_all = true;
4207 this.dataset.set_ids(ids);
4208 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
4210 this.dataset.delete_all = true;
4211 _.each(value_, function(command) {
4212 var obj = {values: command};
4213 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4215 self.dataset.to_create.push(obj);
4216 self.dataset.cache.push(_.clone(obj));
4220 this.dataset.set_ids(ids);
4222 this._super(value_);
4223 this.dataset.reset_ids(value_);
4225 if (this.dataset.index === null && this.dataset.ids.length > 0) {
4226 this.dataset.index = 0;
4228 this.trigger_on_change();
4229 if (this.is_started) {
4230 return self.reload_current_view();
4235 get_value: function() {
4239 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
4240 val = val.concat(_.map(this.dataset.ids, function(id) {
4241 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
4243 return commands.create(alter_order.values);
4245 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
4247 return commands.update(alter_order.id, alter_order.values);
4249 return commands.link_to(id);
4251 return val.concat(_.map(
4252 this.dataset.to_delete, function(x) {
4253 return commands['delete'](x.id);}));
4255 commit_value: function() {
4256 return this.save_any_view();
4258 save_any_view: function() {
4259 var view = this.get_active_view();
4261 if (this.viewmanager.active_view.type === "form") {
4262 if (view.controller.is_initialized.state() !== 'resolved') {
4263 return $.when(false);
4265 return $.when(view.controller.save());
4266 } else if (this.viewmanager.active_view.type === "list") {
4267 return $.when(view.controller.ensure_saved());
4270 return $.when(false);
4272 is_syntax_valid: function() {
4273 var view = this.get_active_view();
4277 switch (this.viewmanager.active_view.type) {
4279 return _(view.controller.fields).chain()
4284 return view.controller.is_valid();
4290 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
4291 template: 'One2Many.viewmanager',
4292 init: function(parent, dataset, views, flags) {
4293 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
4294 this.registry = instance.web.views.extend({
4295 list: 'instance.web.form.One2ManyListView',
4296 form: 'instance.web.form.One2ManyFormView',
4298 this.__ignore_blur = false;
4300 switch_mode: function(mode, unused) {
4301 if (mode !== 'form') {
4302 return this._super(mode, unused);
4305 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
4306 var pop = new instance.web.form.FormOpenPopup(this);
4307 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4308 title: _t("Open: ") + self.o2m.string,
4309 create_function: function(data, options) {
4310 return self.o2m.dataset.create(data, options).done(function(r) {
4311 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4312 self.o2m.dataset.trigger("dataset_changed", r);
4315 write_function: function(id, data, options) {
4316 return self.o2m.dataset.write(id, data, {}).done(function() {
4317 self.o2m.reload_current_view();
4320 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4321 parent_view: self.o2m.view,
4322 child_name: self.o2m.name,
4323 read_function: function() {
4324 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4326 form_view_options: {'not_interactible_on_create':true},
4327 readonly: self.o2m.get("effective_readonly")
4329 pop.on("elements_selected", self, function() {
4330 self.o2m.reload_current_view();
4335 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
4336 get_context: function() {
4337 this.context = this.o2m.build_context();
4338 return this.context;
4342 instance.web.form.One2ManyListView = instance.web.ListView.extend({
4343 _template: 'One2Many.listview',
4344 init: function (parent, dataset, view_id, options) {
4345 this._super(parent, dataset, view_id, _.extend(options || {}, {
4346 GroupsType: instance.web.form.One2ManyGroups,
4347 ListType: instance.web.form.One2ManyList
4349 this.on('edit:after', this, this.proxy('_after_edit'));
4350 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
4353 .bind('add', this.proxy("changed_records"))
4354 .bind('edit', this.proxy("changed_records"))
4355 .bind('remove', this.proxy("changed_records"));
4357 start: function () {
4358 var ret = this._super();
4360 .off('mousedown.handleButtons')
4361 .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
4364 changed_records: function () {
4365 this.o2m.trigger_on_change();
4367 is_valid: function () {
4369 if (!this.fields_view || !this.editable()){
4373 return _.every(this.records.records, function(record){
4375 _.each(self.editor.form.fields, function(field){
4376 field._inhibit_on_change_flag = true;
4377 field.set_value(r.attributes[field.name]);
4378 field._inhibit_on_change_flag = false;
4380 return _.every(self.editor.form.fields, function(field){
4381 field.process_modifiers();
4382 field._check_css_flags();
4383 return field.is_valid();
4387 do_add_record: function () {
4388 if (this.editable()) {
4389 this._super.apply(this, arguments);
4392 var pop = new instance.web.form.SelectCreatePopup(this);
4394 self.o2m.field.relation,
4396 title: _t("Create: ") + self.o2m.string,
4397 initial_view: "form",
4398 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4399 create_function: function(data, options) {
4400 return self.o2m.dataset.create(data, options).done(function(r) {
4401 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4402 self.o2m.dataset.trigger("dataset_changed", r);
4405 read_function: function() {
4406 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4408 parent_view: self.o2m.view,
4409 child_name: self.o2m.name,
4410 form_view_options: {'not_interactible_on_create':true}
4412 self.o2m.build_domain(),
4413 self.o2m.build_context()
4415 pop.on("elements_selected", self, function() {
4416 self.o2m.reload_current_view();
4420 do_activate_record: function(index, id) {
4422 var pop = new instance.web.form.FormOpenPopup(self);
4423 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4424 title: _t("Open: ") + self.o2m.string,
4425 write_function: function(id, data) {
4426 return self.o2m.dataset.write(id, data, {}).done(function() {
4427 self.o2m.reload_current_view();
4430 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4431 parent_view: self.o2m.view,
4432 child_name: self.o2m.name,
4433 read_function: function() {
4434 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4436 form_view_options: {'not_interactible_on_create':true},
4437 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
4440 do_button_action: function (name, id, callback) {
4441 if (!_.isNumber(id)) {
4442 instance.webclient.notification.warn(
4443 _t("Action Button"),
4444 _t("The o2m record must be saved before an action can be used"));
4447 var parent_form = this.o2m.view;
4449 this.ensure_saved().then(function () {
4451 return parent_form.save();
4454 }).done(function () {
4455 var ds = self.o2m.dataset;
4456 var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
4457 return value.length;
4459 if (!self.o2m.options.reload_on_button && !cached_records) {
4460 self.handle_button(name, id, callback);
4462 self.handle_button(name, id, function(){
4463 self.o2m.view.reload();
4469 _after_edit: function () {
4470 this.__ignore_blur = false;
4471 this.editor.form.on('blurred', this, this._on_form_blur);
4473 // The form's blur thing may be jiggered during the edition setup,
4474 // potentially leading to the o2m instasaving the row. Cancel any
4475 // blurring triggered the edition startup here
4476 this.editor.form.widgetFocused();
4478 _before_unedit: function () {
4479 this.editor.form.off('blurred', this, this._on_form_blur);
4481 _button_down: function () {
4482 // If a button is clicked (usually some sort of action button), it's
4483 // the button's responsibility to ensure the editable list is in the
4484 // correct state -> ignore form blurring
4485 this.__ignore_blur = true;
4488 * Handles blurring of the nested form (saves the currently edited row),
4489 * unless the flag to ignore the event is set to ``true``
4491 * Makes the internal form go away
4493 _on_form_blur: function () {
4494 if (this.__ignore_blur) {
4495 this.__ignore_blur = false;
4498 // FIXME: why isn't there an API for this?
4499 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4500 this.ensure_saved();
4503 this.cancel_edition();
4505 keypress_ENTER: function () {
4506 // blurring caused by hitting the [Return] key, should skip the
4507 // autosave-on-blur and let the handler for [Return] do its thing (save
4508 // the current row *anyway*, then create a new one/edit the next one)
4509 this.__ignore_blur = true;
4510 this._super.apply(this, arguments);
4512 do_delete: function (ids) {
4513 var confirm = window.confirm;
4514 window.confirm = function () { return true; };
4516 return this._super(ids);
4518 window.confirm = confirm;
4521 reload_record: function (record, options) {
4522 if (!options || !options['do_not_evict']) {
4523 // Evict record.id from cache to ensure it will be reloaded correctly
4524 this.dataset.evict_record(record.get('id'));
4527 return this._super(record);
4530 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4531 setup_resequence_rows: function () {
4532 if (!this.view.o2m.get('effective_readonly')) {
4533 this._super.apply(this, arguments);
4537 instance.web.form.One2ManyList = instance.web.form.AddAnItemList.extend({
4538 _add_row_class: 'oe_form_field_one2many_list_row_add',
4539 is_readonly: function () {
4540 return this.view.o2m.get('effective_readonly');
4544 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4545 form_template: 'One2Many.formview',
4546 load_form: function(data) {
4549 this.$buttons.find('button.oe_form_button_create').click(function() {
4550 self.save().done(self.on_button_new);
4553 do_notify_change: function() {
4554 if (this.dataset.parent_view) {
4555 this.dataset.parent_view.do_notify_change();
4557 this._super.apply(this, arguments);
4562 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4563 template: "FieldMany2ManyTags",
4564 tag_template: "FieldMany2ManyTag",
4566 this._super.apply(this, arguments);
4567 instance.web.form.CompletionFieldMixin.init.call(this);
4568 this.set({"value": []});
4569 this._display_orderer = new instance.web.DropMisordered();
4570 this._drop_shown = false;
4572 initialize_texttext: function(){
4575 plugins : 'tags arrow autocomplete',
4577 render: function(suggestion) {
4578 return $('<span class="text-label"/>').
4579 data('index', suggestion['index']).html(suggestion['label']);
4584 selectFromDropdown: function() {
4585 this.trigger('hideDropdown');
4586 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4587 var data = self.search_result[index];
4589 self.add_id(data.id);
4591 self.ignore_blur = true;
4594 this.trigger('setSuggestions', {result : []});
4598 isTagAllowed: function(tag) {
4602 removeTag: function(tag) {
4603 var id = tag.data("id");
4604 self.set({"value": _.without(self.get("value"), id)});
4606 renderTag: function(stuff) {
4607 return $.fn.textext.TextExtTags.prototype.renderTag.
4608 call(this, stuff).data("id", stuff.id);
4612 itemToString: function(item) {
4617 onSetInputData: function(e, data) {
4619 this._plugins.autocomplete._suggestions = null;
4621 this.input().val(data);
4627 initialize_content: function() {
4628 if (this.get("effective_readonly"))
4631 self.ignore_blur = false;
4632 self.$text = this.$("textarea");
4633 self.$text.textext(self.initialize_texttext()).bind('getSuggestions', function(e, data) {
4635 var str = !!data ? data.query || '' : '';
4636 self.get_search_result(str).done(function(result) {
4637 self.search_result = result;
4638 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4639 return _.extend(el, {index:i});
4642 }).bind('hideDropdown', function() {
4643 self._drop_shown = false;
4644 }).bind('showDropdown', function() {
4645 self._drop_shown = true;
4647 self.tags = self.$text.textext()[0].tags();
4649 .focusin(function () {
4650 self.trigger('focused');
4651 self.ignore_blur = false;
4653 .focusout(function() {
4654 self.$text.trigger("setInputData", "");
4655 if (!self.ignore_blur) {
4656 self.trigger('blurred');
4658 }).keydown(function(e) {
4659 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4660 self.$text.textext()[0].autocomplete().selectFromDropdown();
4664 // WARNING: duplicated in 4 other M2M widgets
4665 set_value: function(value_) {
4666 value_ = value_ || [];
4667 if (value_.length >= 1 && value_[0] instanceof Array) {
4668 // value_ is a list of m2m commands. We only process
4669 // LINK_TO and REPLACE_WITH in this context
4671 _.each(value_, function (command) {
4672 if (command[0] === commands.LINK_TO) {
4673 val.push(command[1]); // (4, id[, _])
4674 } else if (command[0] === commands.REPLACE_WITH) {
4675 val = command[2]; // (6, _, ids)
4680 this._super(value_);
4682 is_false: function() {
4683 return _(this.get("value")).isEmpty();
4685 get_value: function() {
4686 var tmp = [commands.replace_with(this.get("value"))];
4689 get_search_blacklist: function() {
4690 return this.get("value");
4692 map_tag: function(data){
4693 return _.map(data, function(el) {return {name: el[1], id:el[0]};})
4695 get_render_data: function(ids){
4697 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4698 return dataset.name_get(ids);
4700 render_tag: function(data) {
4702 if (! self.get("effective_readonly")) {
4703 self.tags.containerElement().children().remove();
4704 self.$('textarea').css("padding-left", "3px");
4705 self.tags.addTags(self.map_tag(data));
4707 self.$el.html(QWeb.render(self.tag_template, {elements: data}));
4710 render_value: function() {
4712 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4713 var values = self.get("value");
4714 var handle_names = function(data) {
4715 if (self.isDestroyed())
4718 _.each(data, function(el) {
4719 indexed[el[0]] = el;
4721 data = _.map(values, function(el) { return indexed[el]; });
4722 self.render_tag(data);
4724 if (! values || values.length > 0) {
4725 return this._display_orderer.add(self.get_render_data(values)).done(handle_names);
4730 add_id: function(id) {
4731 this.set({'value': _.uniq(this.get('value').concat([id]))});
4733 focus: function () {
4734 var input = this.$text && this.$text[0];
4735 return input ? input.focus() : false;
4737 set_dimensions: function (height, width) {
4738 this._super(height, width);
4739 this.$("textarea").css({
4744 _search_create_popup: function() {
4745 self.ignore_blur = true;
4746 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
4752 - reload_on_button: Reload the whole form view if click on a button in a list view.
4753 If you see this options, do not use it, it's basically a dirty hack to make one
4754 precise o2m to behave the way we want.
4756 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4757 multi_selection: false,
4758 disable_utility_classes: true,
4759 init: function(field_manager, node) {
4760 this._super(field_manager, node);
4761 this.is_loaded = $.Deferred();
4762 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4763 this.dataset.m2m = this;
4765 this.dataset.on('unlink', self, function(ids) {
4766 self.dataset_changed();
4769 this.list_dm = new instance.web.DropMisordered();
4770 this.render_value_dm = new instance.web.DropMisordered();
4772 initialize_content: function() {
4775 this.$el.addClass('oe_form_field oe_form_field_many2many');
4777 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4779 'deletable': this.get("effective_readonly") ? false : true,
4780 'selectable': this.multi_selection,
4782 'reorderable': false,
4783 'import_enabled': false,
4785 var embedded = (this.field.views || {}).tree;
4787 this.list_view.set_embedded_view(embedded);
4789 this.list_view.m2m_field = this;
4790 var loaded = $.Deferred();
4791 this.list_view.on("list_view_loaded", this, function() {
4794 this.list_view.appendTo(this.$el);
4796 var old_def = self.is_loaded;
4797 self.is_loaded = $.Deferred().done(function() {
4800 this.list_dm.add(loaded).then(function() {
4801 self.is_loaded.resolve();
4804 destroy_content: function() {
4805 this.list_view.destroy();
4806 this.list_view = undefined;
4808 // WARNING: duplicated in 4 other M2M widgets
4809 set_value: function(value_) {
4810 value_ = value_ || [];
4811 if (value_.length >= 1 && value_[0] instanceof Array) {
4812 // value_ is a list of m2m commands. We only process
4813 // LINK_TO and REPLACE_WITH in this context
4815 _.each(value_, function (command) {
4816 if (command[0] === commands.LINK_TO) {
4817 val.push(command[1]); // (4, id[, _])
4818 } else if (command[0] === commands.REPLACE_WITH) {
4819 val = command[2]; // (6, _, ids)
4824 this._super(value_);
4826 get_value: function() {
4827 return [commands.replace_with(this.get('value'))];
4829 is_false: function () {
4830 return _(this.get("value")).isEmpty();
4832 render_value: function() {
4834 this.dataset.set_ids(this.get("value"));
4835 this.render_value_dm.add(this.is_loaded).then(function() {
4836 return self.list_view.reload_content();
4839 dataset_changed: function() {
4840 this.internal_set_value(this.dataset.ids);
4844 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4845 get_context: function() {
4846 this.context = this.m2m.build_context();
4847 return this.context;
4853 * @extends instance.web.ListView
4855 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4856 init: function (parent, dataset, view_id, options) {
4857 this._super(parent, dataset, view_id, _.extend(options || {}, {
4858 ListType: instance.web.form.Many2ManyList,
4861 do_add_record: function () {
4862 var pop = new instance.web.form.SelectCreatePopup(this);
4866 title: _t("Add: ") + this.m2m_field.string,
4867 alternative_form_view: this.m2m_field.field.views ? this.m2m_field.field.views["form"] : undefined,
4868 no_create: this.m2m_field.options.no_create,
4870 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4871 this.m2m_field.build_context()
4874 pop.on("elements_selected", self, function(element_ids) {
4876 _(element_ids).each(function (id) {
4877 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4878 self.dataset.set_ids(self.dataset.ids.concat([id]));
4879 self.m2m_field.dataset_changed();
4884 self.reload_content();
4888 do_activate_record: function(index, id) {
4890 var pop = new instance.web.form.FormOpenPopup(this);
4891 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4892 title: _t("Open: ") + this.m2m_field.string,
4893 alternative_form_view: this.m2m_field.field.views ? this.m2m_field.field.views["form"] : undefined,
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: