1 openerp.web.form = function (instance) {
2 var _t = instance.web._t,
3 _lt = instance.web._lt;
4 var QWeb = instance.web.qweb;
7 instance.web.form = {};
10 * Interface implemented by the form view or any other object
11 * able to provide the features necessary for the fields to work.
14 * - display_invalid_fields : if true, all fields where is_valid() return true should
15 * be displayed as invalid.
16 * - actual_mode : the current mode of the field manager. Can be "view", "edit" or "create".
18 * - view_content_has_changed : when the values of the fields have changed. When
19 * this event is triggered all fields should reprocess their modifiers.
20 * - field_changed:<field_name> : when the value of a field change, an event is triggered
21 * named "field_changed:<field_name>" with <field_name> replaced by the name of the field.
22 * This event is not related to the on_change mechanism of OpenERP and is always called
23 * when the value of a field is setted or changed. This event is only triggered when the
24 * value of the field is syntactically valid, but it can be triggered when the value
25 * is sematically invalid (ie, when a required field is false). It is possible that an event
26 * about a precise field is never triggered even if that field exists in the view, in that
27 * case the value of the field is assumed to be false.
29 instance.web.form.FieldManagerMixin = {
31 * Must return the asked field as in fields_get.
33 get_field_desc: function(field_name) {},
35 * Returns the current value of a field present in the view. See the get_value() method
36 * method in FieldInterface for further information.
38 get_field_value: function(field_name) {},
40 Gives new values for the fields contained in the view. The new values could not be setted
41 right after the call to this method. Setting new values can trigger on_changes.
43 @param (dict) values A dictonnary with key = field name and value = new value.
44 @return (Deferred) Is resolved after all the values are setted.
46 set_values: function(values) {},
48 Computes an OpenERP domain.
50 @param (list) expression An OpenERP domain.
51 @return (boolean) The computed value of the domain.
53 compute_domain: function(expression) {},
55 Builds an evaluation context for the resolution of the fields' contexts. Please note
56 the field are only supposed to use this context to evualuate their own, they should not
59 @return (CompoundContext) An OpenERP context.
61 build_eval_context: function() {},
64 instance.web.views.add('form', 'instance.web.FormView');
67 * - actual_mode: always "view", "edit" or "create". Read-only property. Determines
68 * the mode used by the view.
70 instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerMixin, {
72 * Indicates that this view is not searchable, and thus that no search
73 * view should be displayed (if there is one active).
77 display_name: _lt('Form'),
80 * @constructs instance.web.FormView
81 * @extends instance.web.View
83 * @param {instance.web.Session} session the current openerp session
84 * @param {instance.web.DataSet} dataset the dataset this view will work with
85 * @param {String} view_id the identifier of the OpenERP view object
86 * @param {Object} options
87 * - resize_textareas : [true|false|max_height]
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.default_focus_field = null;
104 this.default_focus_button = null;
105 this.fields_registry = instance.web.form.widgets;
106 this.tags_registry = instance.web.form.tags;
107 this.widgets_registry = instance.web.form.custom_widgets;
108 this.has_been_loaded = $.Deferred();
109 this.translatable_fields = [];
110 _.defaults(this.options, {
111 "not_interactible_on_create": false,
112 "initial_mode": "view",
113 "disable_autofocus": false,
114 "footer_to_buttons": false,
116 this.is_initialized = $.Deferred();
117 this.mutating_mutex = new $.Mutex();
118 this.on_change_list = [];
120 this.render_value_defs = [];
121 this.reload_mutex = new $.Mutex();
122 this.__clicked_inside = false;
123 this.__blur_timeout = null;
124 this.rendering_engine = new instance.web.form.FormRenderingEngine(this);
125 self.set({actual_mode: self.options.initial_mode});
126 this.has_been_loaded.done(function() {
127 self.on("change:actual_mode", self, self.check_actual_mode);
128 self.check_actual_mode();
129 self.on("change:actual_mode", self, self.init_pager);
132 self.on("load_record", self, self.load_record);
133 instance.web.bus.on('clear_uncommitted_changes', this, function(e) {
134 if (!this.can_be_discarded()) {
139 view_loading: function(r) {
140 return this.load_form(r);
142 destroy: function() {
143 _.each(this.get_widgets(), function(w) {
144 w.off('focused blurred');
148 this.$el.off('.formBlur');
152 this.$buttons.remove();
155 load_form: function(data) {
158 throw new Error(_t("No data provided."));
161 throw "Form view does not support multiple calls to load_form";
163 this.fields_order = [];
164 this.fields_view = data;
166 this.rendering_engine.set_fields_registry(this.fields_registry);
167 this.rendering_engine.set_tags_registry(this.tags_registry);
168 this.rendering_engine.set_widgets_registry(this.widgets_registry);
169 this.rendering_engine.set_fields_view(data);
170 var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container');
171 this.rendering_engine.render_to($dest);
173 this.$el.on('mousedown.formBlur', function () {
174 self.__clicked_inside = true;
177 this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self}));
178 if (this.options.$buttons) {
179 this.$buttons.appendTo(this.options.$buttons);
181 this.$el.find('.oe_form_buttons').replaceWith(this.$buttons);
183 this.$buttons.on('click', '.oe_form_button_create',
184 this.guard_active(this.on_button_create));
185 this.$buttons.on('click', '.oe_form_button_edit',
186 this.guard_active(this.on_button_edit));
187 this.$buttons.on('click', '.oe_form_button_save',
188 this.guard_active(this.on_button_save));
189 this.$buttons.on('click', '.oe_form_button_cancel',
190 this.guard_active(this.on_button_cancel));
191 if (this.options.footer_to_buttons) {
192 this.$el.find('footer').appendTo(this.$buttons);
195 this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar');
196 if (!this.sidebar && this.options.$sidebar) {
197 this.sidebar = new instance.web.Sidebar(this);
198 this.sidebar.appendTo(this.$sidebar);
199 if (this.fields_view.toolbar) {
200 this.sidebar.add_toolbar(this.fields_view.toolbar);
202 this.sidebar.add_items('other', _.compact([
203 self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete },
204 self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate }
208 this.has_been_loaded.resolve();
210 // Add bounce effect on button 'Edit' when click on readonly page view.
211 this.$el.find(".oe_form_group_row,.oe_form_field,label").on('click', function (e) {
212 if(self.get("actual_mode") == "view") {
213 var $button = self.options.$buttons.find(".oe_form_button_edit");
214 $button.openerpBounce();
216 instance.web.bus.trigger('click', e);
219 //bounce effect on red button when click on statusbar.
220 this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) {
221 if((self.get("actual_mode") == "view")) {
222 var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'});
223 $button.openerpBounce();
227 this.trigger('form_view_loaded', data);
230 widgetFocused: function() {
231 // Clear click flag if used to focus a widget
232 this.__clicked_inside = false;
233 if (this.__blur_timeout) {
234 clearTimeout(this.__blur_timeout);
235 this.__blur_timeout = null;
238 widgetBlurred: function() {
239 if (this.__clicked_inside) {
240 // clicked in an other section of the form (than the currently
241 // focused widget) => just ignore the blurring entirely?
242 this.__clicked_inside = false;
246 // clear timeout, if any
247 this.widgetFocused();
248 this.__blur_timeout = setTimeout(function () {
249 self.trigger('blurred');
253 do_load_state: function(state, warm) {
254 if (state.id && this.datarecord.id != state.id) {
255 if (this.dataset.get_id_index(state.id) === null) {
256 this.dataset.ids.push(state.id);
258 this.dataset.select_id(state.id);
259 this.do_show({ reload: warm });
264 * @param {Object} [options]
265 * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view".
266 * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record
267 * @return {$.Deferred}
269 do_show: function (options) {
271 options = options || {};
273 this.sidebar.$el.show();
276 this.$buttons.show();
278 this.$el.show().css({
280 filter: 'alpha(opacity = 0)'
282 this.$el.add(this.$buttons).removeClass('oe_form_dirty');
284 var shown = this.has_been_loaded;
285 if (options.reload !== false) {
286 shown = shown.then(function() {
287 if (self.dataset.index === null) {
288 // null index means we should start a new record
289 return self.on_button_new();
291 var fields = _.keys(self.fields_view.fields);
292 fields.push('display_name');
293 return self.dataset.read_index(fields, {
294 context: { 'bin_size': true, 'future_display_name' : true }
295 }).then(function(r) {
296 self.trigger('load_record', r);
300 return shown.then(function() {
301 self._actualize_mode(options.mode || self.options.initial_mode);
304 filter: 'alpha(opacity = 100)'
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() {
340 // New record: Second pass in order to trigger the onchanges
341 // respecting the fields order defined in the view
342 _.each(self.fields_order, function(field_name) {
343 if (record[field_name] !== undefined) {
344 var field = self.fields[field_name];
345 field._dirty_flag = true;
346 self.do_onchange(field);
350 self.on_form_changed();
351 self.rendering_engine.init_fields();
352 self.is_initialized.resolve();
353 self.do_update_pager(record.id == null);
355 self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
358 self.do_push_state({id:record.id});
360 self.do_push_state({});
362 self.$el.add(self.$buttons).removeClass('oe_form_dirty');
367 * Loads and sets up the default values for the model as the current
370 * @return {$.Deferred}
372 load_defaults: function () {
374 var keys = _.keys(this.fields_view.fields);
376 return this.dataset.default_get(keys).then(function(r) {
377 self.trigger('load_record', r);
380 return self.trigger('load_record', {});
382 on_form_changed: function() {
383 this.trigger("view_content_has_changed");
385 do_notify_change: function() {
386 this.$el.add(this.$buttons).addClass('oe_form_dirty');
388 execute_pager_action: function(action) {
389 if (this.can_be_discarded()) {
392 this.dataset.index = 0;
395 this.dataset.previous();
401 this.dataset.index = this.dataset.ids.length - 1;
405 this.trigger('pager_action_executed');
408 init_pager: function() {
411 this.$pager.remove();
412 if (this.get("actual_mode") === "create")
414 this.$pager = $(QWeb.render("FormView.pager", {'widget':self})).hide();
415 if (this.options.$pager) {
416 this.$pager.appendTo(this.options.$pager);
418 this.$el.find('.oe_form_pager').replaceWith(this.$pager);
420 this.$pager.on('click','a[data-pager-action]',function() {
421 var action = $(this).data('pager-action');
422 self.execute_pager_action(action);
424 this.do_update_pager();
426 do_update_pager: function(hide_index) {
427 this.$pager.toggle(this.dataset.ids.length > 1);
429 $(".oe_form_pager_state", this.$pager).html("");
431 $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
434 parse_on_change: function (on_change, widget) {
436 var onchange = _.str.trim(on_change);
437 var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
439 throw new Error(_.str.sprintf( _t("Wrong on change format: %s"), onchange ));
442 var method = call[1];
443 if (!_.str.trim(call[2])) {
444 return {method: method, args: []}
447 var argument_replacement = {
448 'False': function () {return false;},
449 'True': function () {return true;},
450 'None': function () {return null;},
451 'context': function () {
452 return new instance.web.CompoundContext(
453 self.dataset.get_context(),
454 widget.build_context() ? widget.build_context() : {});
457 var parent_fields = null;
458 var args = _.map(call[2].split(','), function (a, i) {
459 var field = _.str.trim(a);
461 // literal constant or context
462 if (field in argument_replacement) {
463 return argument_replacement[field]();
466 if (/^-?\d+(\.\d+)?$/.test(field)) {
467 return Number(field);
470 if (self.fields[field]) {
471 var value_ = self.fields[field].get_value();
472 return value_ == null ? false : value_;
475 var splitted = field.split('.');
476 if (splitted.length > 1 && _.str.trim(splitted[0]) === "parent" && self.dataset.parent_view) {
477 if (parent_fields === null) {
478 parent_fields = self.dataset.parent_view.get_fields_values();
480 var p_val = parent_fields[_.str.trim(splitted[1])];
481 if (p_val !== undefined) {
482 return p_val == null ? false : p_val;
486 var first_char = field[0], last_char = field[field.length-1];
487 if ((first_char === '"' && last_char === '"')
488 || (first_char === "'" && last_char === "'")) {
489 return field.slice(1, -1);
492 throw new Error("Could not get field with name '" + field +
493 "' for onchange '" + onchange + "'");
501 do_onchange: function(widget, processed) {
503 this.on_change_list = [{widget: widget, processed: processed}].concat(this.on_change_list);
504 return this._process_operations();
506 _process_onchange: function(on_change_obj) {
508 var widget = on_change_obj.widget;
509 var processed = on_change_obj.processed;
512 processed = processed || [];
513 processed.push(widget.name);
514 var on_change = widget.node.attrs.on_change;
516 var change_spec = self.parse_on_change(on_change, widget);
518 if (self.datarecord.id && !instance.web.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) {
519 // In case of a o2m virtual id, we should pass an empty ids list
520 ids.push(self.datarecord.id);
522 def = self.alive(new instance.web.Model(self.dataset.model).call(
523 change_spec.method, [ids].concat(change_spec.args)));
527 return def.then(function(response) {
528 if (widget.field['change_default']) {
529 var fieldname = widget.name;
531 if (response.value && (fieldname in response.value)) {
532 // Use value from onchange if onchange executed
533 value_ = response.value[fieldname];
535 // otherwise get form value for field
536 value_ = self.fields[fieldname].get_value();
538 var condition = fieldname + '=' + value_;
541 return self.alive(new instance.web.Model('ir.values').call(
542 'get_defaults', [self.model, condition]
543 )).then(function (results) {
544 if (!results.length) {
547 if (!response.value) {
550 for(var i=0; i<results.length; ++i) {
551 // [whatever, key, value]
552 var triplet = results[i];
553 response.value[triplet[1]] = triplet[2];
560 }).then(function(response) {
561 return self.on_processed_onchange(response, processed);
565 instance.webclient.crashmanager.show_message(e);
566 return $.Deferred().reject();
569 on_processed_onchange: function(result, processed) {
572 this._internal_set_values(result.value, processed);
574 if (!_.isEmpty(result.warning)) {
575 instance.web.dialog($(QWeb.render("CrashManager.warning", result.warning)), {
576 title:result.warning.title,
579 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
584 var fields = this.fields;
585 _(result.domain).each(function (domain, fieldname) {
586 var field = fields[fieldname];
587 if (!field) { return; }
588 field.node.attrs.domain = domain;
591 return $.Deferred().resolve();
594 instance.webclient.crashmanager.show_message(e);
595 return $.Deferred().reject();
598 _process_operations: function() {
600 return this.mutating_mutex.exec(function() {
602 var on_change_obj = self.on_change_list.shift();
604 return self._process_onchange(on_change_obj).then(function() {
609 _.each(self.fields, function(field) {
610 defs.push(field.commit_value());
612 var args = _.toArray(arguments);
613 return $.when.apply($, defs).then(function() {
614 if (self.on_change_list.length !== 0) {
617 var save_obj = self.save_list.pop();
619 return self._process_save(save_obj).then(function() {
620 save_obj.ret = _.toArray(arguments);
623 save_obj.error = true;
628 self.save_list.pop();
635 _internal_set_values: function(values, exclude) {
636 exclude = exclude || [];
637 for (var f in values) {
638 if (!values.hasOwnProperty(f)) { continue; }
639 var field = this.fields[f];
640 // If field is not defined in the view, just ignore it
642 var value_ = values[f];
643 if (field.get_value() != value_) {
644 field._inhibit_on_change_flag = true;
645 field.set_value(value_);
646 field._inhibit_on_change_flag = false;
647 field._dirty_flag = true;
648 if (!_.contains(exclude, field.name)) {
649 this.do_onchange(field, exclude);
654 this.on_form_changed();
656 set_values: function(values) {
658 return this.mutating_mutex.exec(function() {
659 self._internal_set_values(values);
663 * Ask the view to switch to view mode if possible. The view may not do it
664 * if the current record is not yet saved. It will then stay in create mode.
666 to_view_mode: function() {
667 this._actualize_mode("view");
670 * Ask the view to switch to edit mode if possible. The view may not do it
671 * if the current record is not yet saved. It will then stay in create mode.
673 to_edit_mode: function() {
674 this._actualize_mode("edit");
677 * Ask the view to switch to a precise mode if possible. The view is free to
678 * not respect this command if the state of the dataset is not compatible with
679 * the new mode. For example, it is not possible to switch to edit mode if
680 * the current record is not yet saved in database.
682 * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
683 * undefined the view will test the actual mode to check if it is still consistent
684 * with the dataset state.
686 _actualize_mode: function(switch_to) {
687 var mode = switch_to || this.get("actual_mode");
688 if (! this.datarecord.id) {
690 } else if (mode === "create") {
693 this.render_value_defs = [];
694 this.set({actual_mode: mode});
696 check_actual_mode: function(source, options) {
698 if(this.get("actual_mode") === "view") {
699 self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
700 self.$buttons.find('.oe_form_buttons_edit').hide();
701 self.$buttons.find('.oe_form_buttons_view').show();
702 self.$sidebar.show();
704 self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
705 self.$buttons.find('.oe_form_buttons_edit').show();
706 self.$buttons.find('.oe_form_buttons_view').hide();
707 self.$sidebar.hide();
711 autofocus: function() {
712 if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
713 var fields_order = this.fields_order.slice(0);
714 if (this.default_focus_field) {
715 fields_order.unshift(this.default_focus_field.name);
717 for (var i = 0; i < fields_order.length; i += 1) {
718 var field = this.fields[fields_order[i]];
719 if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
720 if (field.focus() !== false) {
727 on_button_save: function(e) {
729 $(e.target).attr("disabled", true);
730 return this.save().done(function(result) {
731 self.trigger("save", result);
732 self.reload().then(function() {
734 var parent = self.ViewManager.ActionManager.getParent();
736 parent.menu.do_reload_needaction();
739 }).always(function(){
740 $(e.target).attr("disabled", false);
743 on_button_cancel: function(event) {
745 if (this.can_be_discarded()) {
746 if (this.get('actual_mode') === 'create') {
747 this.trigger('history_back');
750 $.when.apply(null, this.render_value_defs).then(function(){
751 self.trigger('load_record', self.datarecord);
755 this.trigger('on_button_cancel');
758 on_button_new: function() {
761 return $.when(this.has_been_loaded).then(function() {
762 if (self.can_be_discarded()) {
763 return self.load_defaults();
767 on_button_edit: function() {
768 return this.to_edit_mode();
770 on_button_create: function() {
771 this.dataset.index = null;
774 on_button_duplicate: function() {
776 return this.has_been_loaded.then(function() {
777 return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
778 self.record_created(new_id);
783 on_button_delete: function() {
785 var def = $.Deferred();
786 this.has_been_loaded.done(function() {
787 if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
788 self.dataset.unlink([self.datarecord.id]).done(function() {
789 if (self.dataset.size()) {
790 self.execute_pager_action('next');
792 self.do_action('history_back');
797 $.async_when().done(function () {
802 return def.promise();
804 can_be_discarded: function() {
805 if (this.$el.is('.oe_form_dirty')) {
806 if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
809 this.$el.removeClass('oe_form_dirty');
814 * Triggers saving the form's record. Chooses between creating a new
815 * record or saving an existing one depending on whether the record
816 * already has an id property.
818 * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
819 * record, should that record be inserted at the start of the dataset (by
820 * default, records are added at the end)
822 save: function(prepend_on_create) {
824 var save_obj = {prepend_on_create: prepend_on_create, ret: null};
825 this.save_list.push(save_obj);
826 return this._process_operations().then(function() {
828 return $.Deferred().reject();
829 return $.when.apply($, save_obj.ret);
831 self.$el.removeClass('oe_form_dirty');
834 _process_save: function(save_obj) {
836 var prepend_on_create = save_obj.prepend_on_create;
838 var form_invalid = false,
840 first_invalid_field = null,
841 readonly_values = {};
842 for (var f in self.fields) {
843 if (!self.fields.hasOwnProperty(f)) { continue; }
847 if (!first_invalid_field) {
848 first_invalid_field = f;
850 } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
851 // Special case 'id' field, do not save this field
852 // on 'create' : save all non readonly fields
853 // on 'edit' : save non readonly modified fields
854 if (!f.get("readonly")) {
855 values[f.name] = f.get_value();
857 readonly_values[f.name] = f.get_value();
862 self.set({'display_invalid_fields': true});
863 first_invalid_field.focus();
865 return $.Deferred().reject();
867 self.set({'display_invalid_fields': false});
869 if (!self.datarecord.id) {
871 save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
872 return self.record_created(r, prepend_on_create);
874 } else if (_.isEmpty(values)) {
875 // Not dirty, noop save
876 save_deferral = $.Deferred().resolve({}).promise();
879 save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
880 return self.record_saved(r);
883 return save_deferral;
887 return $.Deferred().reject();
890 on_invalid: function() {
891 var warnings = _(this.fields).chain()
892 .filter(function (f) { return !f.is_valid(); })
894 return _.str.sprintf('<li>%s</li>',
897 warnings.unshift('<ul>');
898 warnings.push('</ul>');
899 this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
902 * Reload the form after saving
904 * @param {Object} r result of the write function.
906 record_saved: function(r) {
907 this.trigger('record_saved', r);
909 // should not happen in the server, but may happen for internal purpose
910 return $.Deferred().reject();
915 * Updates the form' dataset to contain the new record:
917 * * Adds the newly created record to the current dataset (at the end by
919 * * Selects that record (sets the dataset's index to point to the new
921 * * Updates the pager and sidebar displays
924 * @param {Boolean} [prepend_on_create=false] adds the newly created record
925 * at the beginning of the dataset instead of the end
927 record_created: function(r, prepend_on_create) {
930 // should not happen in the server, but may happen for internal purpose
931 this.trigger('record_created', r);
932 return $.Deferred().reject();
934 this.datarecord.id = r;
935 if (!prepend_on_create) {
936 this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
937 this.dataset.index = this.dataset.ids.length - 1;
939 this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
940 this.dataset.index = 0;
942 this.do_update_pager();
944 this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
946 //openerp.log("The record has been created with id #" + this.datarecord.id);
947 return $.when(this.reload()).then(function () {
948 self.trigger('record_created', r);
949 return _.extend(r, {created: true});
953 on_action: function (action) {
954 console.debug('Executing action', action);
958 return this.reload_mutex.exec(function() {
959 if (self.dataset.index == null) {
960 self.trigger("previous_view");
961 return $.Deferred().reject().promise();
963 if (self.dataset.index == null || self.dataset.index < 0) {
964 return $.when(self.on_button_new());
966 var fields = _.keys(self.fields_view.fields);
967 fields.push('display_name');
968 return self.dataset.read_index(fields,
972 'future_display_name': true
974 check_access_rule: true
975 }).then(function(r) {
976 self.trigger('load_record', r);
978 self.do_action('history_back');
983 get_widgets: function() {
984 return _.filter(this.getChildren(), function(obj) {
985 return obj instanceof instance.web.form.FormWidget;
988 get_fields_values: function() {
990 var ids = this.get_selected_ids();
991 values["id"] = ids.length > 0 ? ids[0] : false;
992 _.each(this.fields, function(value_, key) {
993 values[key] = value_.get_value();
997 get_selected_ids: function() {
998 var id = this.dataset.ids[this.dataset.index];
999 return id ? [id] : [];
1001 recursive_save: function() {
1003 return $.when(this.save()).then(function(res) {
1004 if (self.dataset.parent_view)
1005 return self.dataset.parent_view.recursive_save();
1008 recursive_reload: function() {
1011 if (self.dataset.parent_view)
1012 pre = self.dataset.parent_view.recursive_reload();
1013 return pre.then(function() {
1014 return self.reload();
1017 is_dirty: function() {
1018 return _.any(this.fields, function (value_) {
1019 return value_._dirty_flag;
1022 is_interactible_record: function() {
1023 var id = this.datarecord.id;
1025 if (this.options.not_interactible_on_create)
1027 } else if (typeof(id) === "string") {
1028 if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
1033 sidebar_eval_context: function () {
1034 return $.when(this.build_eval_context());
1036 open_defaults_dialog: function () {
1038 var display = function (field, value) {
1039 if (field instanceof instance.web.form.FieldSelection) {
1040 return _(field.values).find(function (option) {
1041 return option[0] === value;
1043 } else if (field instanceof instance.web.form.FieldMany2One) {
1044 return field.get_displayed();
1048 var fields = _.chain(this.fields)
1049 .map(function (field) {
1050 var value = field.get_value();
1051 // ignore fields which are empty, invisible, readonly, o2m
1054 || field.get('invisible')
1055 || field.get("readonly")
1056 || field.field.type === 'one2many'
1057 || field.field.type === 'many2many'
1058 || field.field.type === 'binary'
1059 || field.password) {
1065 string: field.string,
1067 displayed: display(field, value),
1071 .sortBy(function (field) { return field.string; })
1073 var conditions = _.chain(self.fields)
1074 .filter(function (field) { return field.field.change_default; })
1075 .map(function (field) {
1076 var value = field.get_value();
1079 string: field.string,
1081 displayed: display(field, value),
1086 var d = new instance.web.Dialog(this, {
1087 title: _t("Set Default"),
1090 conditions: conditions
1093 {text: _t("Close"), click: function () { d.close(); }},
1094 {text: _t("Save default"), click: function () {
1095 var $defaults = d.$el.find('#formview_default_fields');
1096 var field_to_set = $defaults.val();
1097 if (!field_to_set) {
1098 $defaults.parent().addClass('oe_form_invalid');
1101 var condition = d.$el.find('#formview_default_conditions').val(),
1102 all_users = d.$el.find('#formview_default_all').is(':checked');
1103 new instance.web.DataSet(self, 'ir.values').call(
1107 self.fields[field_to_set].get_value(),
1111 ]).done(function () { d.close(); });
1115 d.template = 'FormView.set_default';
1118 register_field: function(field, name) {
1119 this.fields[name] = field;
1120 this.fields_order.push(name);
1121 if (JSON.parse(field.node.attrs.default_focus || "0")) {
1122 this.default_focus_field = field;
1125 field.on('focused', null, this.proxy('widgetFocused'))
1126 .on('blurred', null, this.proxy('widgetBlurred'));
1127 if (this.get_field_desc(name).translate) {
1128 this.translatable_fields.push(field);
1130 field.on('changed_value', this, function() {
1131 if (field.is_syntax_valid()) {
1132 this.trigger('field_changed:' + name);
1134 if (field._inhibit_on_change_flag) {
1137 field._dirty_flag = true;
1138 if (field.is_syntax_valid()) {
1139 this.do_onchange(field);
1140 this.on_form_changed(true);
1141 this.do_notify_change();
1145 get_field_desc: function(field_name) {
1146 return this.fields_view.fields[field_name];
1148 get_field_value: function(field_name) {
1149 return this.fields[field_name].get_value();
1151 compute_domain: function(expression) {
1152 return instance.web.form.compute_domain(expression, this.fields);
1154 _build_view_fields_values: function() {
1155 var a_dataset = this.dataset;
1156 var fields_values = this.get_fields_values();
1157 var active_id = a_dataset.ids[a_dataset.index];
1158 _.extend(fields_values, {
1159 active_id: active_id || false,
1160 active_ids: active_id ? [active_id] : [],
1161 active_model: a_dataset.model,
1164 if (a_dataset.parent_view) {
1165 fields_values.parent = a_dataset.parent_view.get_fields_values();
1167 return fields_values;
1169 build_eval_context: function() {
1170 var a_dataset = this.dataset;
1171 return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
1176 * Interface to be implemented by rendering engines for the form view.
1178 instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
1179 set_fields_view: function(fields_view) {},
1180 set_fields_registry: function(fields_registry) {},
1181 render_to: function($el) {},
1185 * Default rendering engine for the form view.
1187 * It is necessary to set the view using set_view() before usage.
1189 instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
1190 init: function(view) {
1193 set_fields_view: function(fvg) {
1195 this.version = parseFloat(this.fvg.arch.attrs.version);
1196 if (isNaN(this.version)) {
1200 set_tags_registry: function(tags_registry) {
1201 this.tags_registry = tags_registry;
1203 set_fields_registry: function(fields_registry) {
1204 this.fields_registry = fields_registry;
1206 set_widgets_registry: function(widgets_registry) {
1207 this.widgets_registry = widgets_registry;
1209 // Backward compatibility tools, current default version: v6.1
1210 process_version: function() {
1211 if (this.version < 7.0) {
1212 this.$form.find('form:first').wrapInner('<group col="4"/>');
1213 this.$form.find('page').each(function() {
1214 if (!$(this).parents('field').length) {
1215 $(this).wrapInner('<group col="4"/>');
1220 get_arch_fragment: function() {
1221 var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
1222 // IE won't allow custom button@type and will revert it to spec default : 'submit'
1223 $('button', doc).each(function() {
1224 $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
1226 // IE's html parser is also a css parser. How convenient...
1227 $('board', doc).each(function() {
1228 $(this).attr('layout', $(this).attr('style'));
1230 return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
1232 render_to: function($target) {
1234 this.$target = $target;
1236 this.$form = this.get_arch_fragment();
1238 this.process_version();
1240 this.fields_to_init = [];
1241 this.tags_to_init = [];
1242 this.widgets_to_init = [];
1244 this.process(this.$form);
1246 this.$form.appendTo(this.$target);
1248 this.to_replace = [];
1250 _.each(this.fields_to_init, function($elem) {
1251 var name = $elem.attr("name");
1252 if (!self.fvg.fields[name]) {
1253 throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
1255 var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
1257 throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
1259 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1260 var $label = self.labels[$elem.attr("name")];
1262 w.set_input_id($label.attr("for"));
1264 self.alter_field(w);
1265 self.view.register_field(w, $elem.attr("name"));
1266 self.to_replace.push([w, $elem]);
1268 _.each(this.tags_to_init, function($elem) {
1269 var tag_name = $elem[0].tagName.toLowerCase();
1270 var obj = self.tags_registry.get_object(tag_name);
1271 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1272 self.to_replace.push([w, $elem]);
1274 _.each(this.widgets_to_init, function($elem) {
1275 var widget_type = $elem.attr("type");
1276 var obj = self.widgets_registry.get_object(widget_type);
1277 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1278 self.to_replace.push([w, $elem]);
1281 init_fields: function() {
1283 _.each(this.to_replace, function(el) {
1284 defs.push(el[0].replace(el[1]));
1286 this.to_replace = [];
1287 return $.when.apply($, defs);
1289 render_element: function(template /* dictionaries */) {
1290 var dicts = [].slice.call(arguments).slice(1);
1291 var dict = _.extend.apply(_, dicts);
1292 dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
1293 return $(QWeb.render(template, dict));
1295 alter_field: function(field) {
1297 toggle_layout_debugging: function() {
1298 if (!this.$target.has('.oe_layout_debug_cell:first').length) {
1299 this.$target.find('[title]').removeAttr('title');
1300 this.$target.find('.oe_form_group_cell').each(function() {
1301 var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
1302 $(this).attr('title', text);
1305 this.$target.toggleClass('oe_layout_debugging');
1307 process: function($tag) {
1309 var tagname = $tag[0].nodeName.toLowerCase();
1310 if (this.tags_registry.contains(tagname)) {
1311 this.tags_to_init.push($tag);
1314 var fn = self['process_' + tagname];
1316 var args = [].slice.call(arguments);
1318 return fn.apply(self, args);
1320 // generic tag handling, just process children
1321 $tag.children().each(function() {
1322 self.process($(this));
1324 self.handle_common_properties($tag, $tag);
1325 $tag.removeAttr("modifiers");
1329 process_widget: function($widget) {
1330 this.widgets_to_init.push($widget);
1333 process_sheet: function($sheet) {
1334 var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
1335 this.handle_common_properties($new_sheet, $sheet);
1336 var $dst = $new_sheet.find('.oe_form_sheet');
1337 $sheet.contents().appendTo($dst);
1338 $sheet.before($new_sheet).remove();
1339 this.process($new_sheet);
1341 process_form: function($form) {
1342 if ($form.find('> sheet').length === 0) {
1343 $form.addClass('oe_form_nosheet');
1345 var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
1346 this.handle_common_properties($new_form, $form);
1347 $form.contents().appendTo($new_form);
1348 if ($form[0] === this.$form[0]) {
1349 // If root element, replace it
1350 this.$form = $new_form;
1352 $form.before($new_form).remove();
1354 this.process($new_form);
1357 * Used by direct <field> children of a <group> tag only
1358 * This method will add the implicit <label...> for every field
1361 preprocess_field: function($field) {
1363 var name = $field.attr('name'),
1364 field_colspan = parseInt($field.attr('colspan'), 10),
1365 field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
1367 if ($field.attr('nolabel') === '1')
1369 $field.attr('nolabel', '1');
1371 this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
1372 $(el).parents().each(function(unused, tag) {
1373 var name = tag.tagName.toLowerCase();
1374 if (name === "field" || name in self.tags_registry.map)
1381 var $label = $('<label/>').attr({
1383 "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
1384 "string": $field.attr('string'),
1385 "help": $field.attr('help'),
1386 "class": $field.attr('class'),
1388 $label.insertBefore($field);
1389 if (field_colspan > 1) {
1390 $field.attr('colspan', field_colspan - 1);
1394 process_field: function($field) {
1395 if ($field.parent().is('group')) {
1396 // No implicit labels for normal fields, only for <group> direct children
1397 var $label = this.preprocess_field($field);
1399 this.process($label);
1402 this.fields_to_init.push($field);
1405 process_group: function($group) {
1407 $group.children('field').each(function() {
1408 self.preprocess_field($(this));
1410 var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
1412 if ($new_group.first().is('table.oe_form_group')) {
1413 $table = $new_group;
1414 } else if ($new_group.filter('table.oe_form_group').length) {
1415 $table = $new_group.filter('table.oe_form_group').first();
1417 $table = $new_group.find('table.oe_form_group').first();
1421 cols = parseInt($group.attr('col') || 2, 10),
1425 $group.children().each(function(a,b,c) {
1426 var $child = $(this);
1427 var colspan = parseInt($child.attr('colspan') || 1, 10);
1428 var tagName = $child[0].tagName.toLowerCase();
1429 var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
1430 var newline = tagName === 'newline';
1432 // Note FME: those classes are used in layout debug mode
1433 if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
1434 $tr.addClass('oe_form_group_row_incomplete');
1436 $tr.addClass('oe_form_group_row_newline');
1443 if (!$tr || row_cols < colspan) {
1444 $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
1446 } else if (tagName==='group') {
1447 // When <group> <group/><group/> </group>, we need a spacing between the two groups
1448 $td.addClass('oe_group_right')
1450 row_cols -= colspan;
1452 // invisibility transfer
1453 var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
1454 var invisible = field_modifiers.invisible;
1455 self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
1457 $tr.append($td.append($child));
1458 children.push($child[0]);
1460 if (row_cols && $td) {
1461 $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
1463 $group.before($new_group).remove();
1465 $table.find('> tbody > tr').each(function() {
1466 var to_compute = [],
1469 $(this).children().each(function() {
1471 $child = $td.children(':first');
1472 if ($child.attr('cell-class')) {
1473 $td.addClass($child.attr('cell-class'));
1475 switch ($child[0].tagName.toLowerCase()) {
1479 if ($child.attr('for')) {
1480 $td.attr('width', '1%').addClass('oe_form_group_cell_label');
1481 row_cols-= $td.attr('colspan') || 1;
1486 var width = _.str.trim($child.attr('width') || ''),
1487 iwidth = parseInt(width, 10);
1489 if (width.substr(-1) === '%') {
1491 width = iwidth + '%';
1494 $td.css('min-width', width + 'px');
1496 $td.attr('width', width);
1497 $child.removeAttr('width');
1498 row_cols-= $td.attr('colspan') || 1;
1500 to_compute.push($td);
1506 var unit = Math.floor(total / row_cols);
1507 if (!$(this).is('.oe_form_group_row_incomplete')) {
1508 _.each(to_compute, function($td, i) {
1509 var width = parseInt($td.attr('colspan'), 10) * unit;
1510 $td.attr('width', width + '%');
1516 _.each(children, function(el) {
1517 self.process($(el));
1519 this.handle_common_properties($new_group, $group);
1522 process_notebook: function($notebook) {
1525 $notebook.find('> page').each(function() {
1526 var $page = $(this);
1527 var page_attrs = $page.getAttributes();
1528 page_attrs.id = _.uniqueId('notebook_page_');
1529 var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
1530 $page.contents().appendTo($new_page);
1531 $page.before($new_page).remove();
1532 var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
1533 page_attrs.__page = $new_page;
1534 page_attrs.__ic = ic;
1535 pages.push(page_attrs);
1537 $new_page.children().each(function() {
1538 self.process($(this));
1541 var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
1542 $notebook.contents().appendTo($new_notebook);
1543 $notebook.before($new_notebook).remove();
1544 self.process($($new_notebook.children()[0]));
1545 //tabs and invisibility handling
1546 $new_notebook.tabs();
1547 _.each(pages, function(page, i) {
1550 page.__ic.on("change:effective_invisible", null, function() {
1551 if (!page.__ic.get('effective_invisible') && page.autofocus) {
1552 $new_notebook.tabs('select', i);
1555 var current = $new_notebook.tabs("option", "selected");
1556 if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
1558 var first_visible = _.find(_.range(pages.length), function(i2) {
1559 return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
1561 if (first_visible !== undefined) {
1562 $new_notebook.tabs('select', first_visible);
1567 this.handle_common_properties($new_notebook, $notebook);
1568 return $new_notebook;
1570 process_separator: function($separator) {
1571 var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
1572 $separator.before($new_separator).remove();
1573 this.handle_common_properties($new_separator, $separator);
1574 return $new_separator;
1576 process_label: function($label) {
1577 var name = $label.attr("for"),
1578 field_orm = this.fvg.fields[name];
1580 string: $label.attr('string') || (field_orm || {}).string || '',
1581 help: $label.attr('help') || (field_orm || {}).help || '',
1582 _for: name ? _.uniqueId('oe-field-input-') : undefined,
1584 var align = parseFloat(dict.align);
1585 if (isNaN(align) || align === 1) {
1587 } else if (align === 0) {
1593 var $new_label = this.render_element('FormRenderingLabel', dict);
1594 $label.before($new_label).remove();
1595 this.handle_common_properties($new_label, $label);
1597 this.labels[name] = $new_label;
1601 handle_common_properties: function($new_element, $node) {
1602 var str_modifiers = $node.attr("modifiers") || "{}";
1603 var modifiers = JSON.parse(str_modifiers);
1605 if (modifiers.invisible !== undefined)
1606 ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
1607 $new_element.addClass($node.attr("class") || "");
1608 $new_element.attr('style', $node.attr('style'));
1609 return {invisibility_changer: ic,};
1616 If you read this documentation, it probably means that you were asked to use a form view widget outside of
1617 a form view. Before going further, you must understand that those fields were never really created for
1618 that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
1619 you to hack the system with more style.
1621 instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
1622 init: function(parent, eval_context) {
1623 this._super(parent);
1624 this.field_descs = {};
1625 this.eval_context = eval_context || {};
1627 display_invalid_fields: false,
1628 actual_mode: 'create',
1631 get_field_desc: function(field_name) {
1632 if (this.field_descs[field_name] === undefined) {
1633 this.field_descs[field_name] = {
1637 return this.field_descs[field_name];
1639 extend_field_desc: function(fields) {
1641 _.each(fields, function(v, k) {
1642 _.extend(self.get_field_desc(k), v);
1645 get_field_value: function(field_name) {
1648 set_values: function(values) {
1651 compute_domain: function(expression) {
1652 return instance.web.form.compute_domain(expression, {});
1654 build_eval_context: function() {
1655 return new instance.web.CompoundContext(this.eval_context);
1659 instance.web.form.compute_domain = function(expr, fields) {
1660 if (! (expr instanceof Array))
1663 for (var i = expr.length - 1; i >= 0; i--) {
1665 if (ex.length == 1) {
1666 var top = stack.pop();
1669 stack.push(stack.pop() || top);
1672 stack.push(stack.pop() && top);
1678 throw new Error(_.str.sprintf(
1679 _t("Unknown operator %s in domain %s"),
1680 ex, JSON.stringify(expr)));
1684 var field = fields[ex[0]];
1686 throw new Error(_.str.sprintf(
1687 _t("Unknown field %s in domain %s"),
1688 ex[0], JSON.stringify(expr)));
1690 var field_value = field.get_value ? field.get_value() : field.value;
1694 switch (op.toLowerCase()) {
1697 stack.push(_.isEqual(field_value, val));
1701 stack.push(!_.isEqual(field_value, val));
1704 stack.push(field_value < val);
1707 stack.push(field_value > val);
1710 stack.push(field_value <= val);
1713 stack.push(field_value >= val);
1716 if (!_.isArray(val)) val = [val];
1717 stack.push(_(val).contains(field_value));
1720 if (!_.isArray(val)) val = [val];
1721 stack.push(!_(val).contains(field_value));
1725 _t("Unsupported operator %s in domain %s"),
1726 op, JSON.stringify(expr));
1729 return _.all(stack, _.identity);
1732 instance.web.form.is_bin_size = function(v) {
1733 return /^\d+(\.\d*)? \w+$/.test(v);
1737 * Must be applied over an class already possessing the PropertiesMixin.
1739 * Apply the result of the "invisible" domain to this.$el.
1741 instance.web.form.InvisibilityChangerMixin = {
1742 init: function(field_manager, invisible_domain) {
1744 this._ic_field_manager = field_manager;
1745 this._ic_invisible_modifier = invisible_domain;
1746 this._ic_field_manager.on("view_content_has_changed", this, function() {
1747 var result = self._ic_invisible_modifier === undefined ? false :
1748 self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
1749 self.set({"invisible": result});
1751 this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
1752 var check = function() {
1753 if (self.get("invisible") || self.get('force_invisible')) {
1754 self.set({"effective_invisible": true});
1756 self.set({"effective_invisible": false});
1759 this.on('change:invisible', this, check);
1760 this.on('change:force_invisible', this, check);
1764 this.on("change:effective_invisible", this, this._check_visibility);
1765 this._check_visibility();
1767 _check_visibility: function() {
1768 this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
1772 instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
1773 init: function(parent, field_manager, invisible_domain, $el) {
1774 this.setParent(parent);
1775 instance.web.PropertiesMixin.init.call(this);
1776 instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
1783 Base class for all fields, custom widgets and buttons to be displayed in the form view.
1786 - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
1787 the values of the "readonly" property and the "mode" property on the field manager.
1789 instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
1791 * @constructs instance.web.form.FormWidget
1792 * @extends instance.web.Widget
1794 * @param field_manager
1797 init: function(field_manager, node) {
1798 this._super(field_manager);
1799 this.field_manager = field_manager;
1800 if (this.field_manager instanceof instance.web.FormView)
1801 this.view = this.field_manager;
1803 this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
1804 instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
1806 this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
1812 // some events to make the property "effective_readonly" sync automatically with "readonly" and
1813 // "mode" on field_manager
1815 var test_effective_readonly = function() {
1816 self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
1818 this.on("change:readonly", this, test_effective_readonly);
1819 this.field_manager.on("change:actual_mode", this, test_effective_readonly);
1820 test_effective_readonly.call(this);
1822 renderElement: function() {
1823 this.process_modifiers();
1825 this.$el.addClass(this.node.attrs["class"] || "");
1827 destroy: function() {
1829 this._super.apply(this, arguments);
1832 * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
1834 * This method is an utility method that is meant to be called by child classes.
1836 * @param {jQuery} $e jQuery object of elements to bind focus/blur on
1838 setupFocus: function ($e) {
1841 focus: function () { self.trigger('focused'); },
1842 blur: function () { self.trigger('blurred'); }
1845 process_modifiers: function() {
1847 for (var a in this.modifiers) {
1848 if (!this.modifiers.hasOwnProperty(a)) { continue; }
1849 if (!_.include(["invisible"], a)) {
1850 var val = this.field_manager.compute_domain(this.modifiers[a]);
1856 do_attach_tooltip: function(widget, trigger, options) {
1857 widget = widget || this;
1858 trigger = trigger || this.$el;
1859 options = _.extend({
1864 var template = widget.template + '.tooltip';
1865 if (!QWeb.has_template(template)) {
1866 template = 'WidgetLabel.tooltip';
1868 return QWeb.render(template, {
1869 debug: instance.session.debug,
1872 gravity: $.fn.tipsy.autoBounds(50, 'nw'),
1877 $(trigger).tipsy(options);
1880 * Builds a new context usable for operations related to fields by merging
1881 * the fields'context with the action's context.
1883 build_context: function() {
1884 // only use the model's context if there is not context on the node
1885 var v_context = this.node.attrs.context;
1887 v_context = (this.field || {}).context || {};
1890 if (v_context.__ref || true) { //TODO: remove true
1891 var fields_values = this.field_manager.build_eval_context();
1892 v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
1896 build_domain: function() {
1897 var f_domain = this.field.domain || [];
1898 var n_domain = this.node.attrs.domain || null;
1899 // if there is a domain on the node, overrides the model's domain
1900 var final_domain = n_domain !== null ? n_domain : f_domain;
1901 if (!(final_domain instanceof Array) || true) { //TODO: remove true
1902 var fields_values = this.field_manager.build_eval_context();
1903 final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
1905 return final_domain;
1909 instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
1910 template: 'WidgetButton',
1911 init: function(field_manager, node) {
1912 node.attrs.type = node.attrs['data-button-type'];
1913 this._super(field_manager, node);
1914 this.force_disabled = false;
1915 this.string = (this.node.attrs.string || '').replace(/_/g, '');
1916 if (JSON.parse(this.node.attrs.default_focus || "0")) {
1917 // TODO fme: provide enter key binding to widgets
1918 this.view.default_focus_button = this;
1920 if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
1921 this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
1925 this._super.apply(this, arguments);
1926 this.view.on('view_content_has_changed', this, this.check_disable);
1927 this.check_disable();
1928 this.$el.click(this.on_click);
1929 if (this.node.attrs.help || instance.session.debug) {
1930 this.do_attach_tooltip();
1932 this.setupFocus(this.$el);
1934 on_click: function() {
1936 this.force_disabled = true;
1937 this.check_disable();
1938 this.execute_action().always(function() {
1939 self.force_disabled = false;
1940 self.check_disable();
1943 execute_action: function() {
1945 var exec_action = function() {
1946 if (self.node.attrs.confirm) {
1947 var def = $.Deferred();
1948 var dialog = instance.web.dialog($('<div/>').text(self.node.attrs.confirm), {
1949 title: _t('Confirm'),
1952 {text: _t("Cancel"), click: function() {
1953 $(this).dialog("close");
1956 {text: _t("Ok"), click: function() {
1958 self.on_confirmed().always(function() {
1959 $(self2).dialog("close");
1964 beforeClose: function() {
1968 return def.promise();
1970 return self.on_confirmed();
1973 if (!this.node.attrs.special) {
1974 return this.view.recursive_save().then(exec_action);
1976 return exec_action();
1979 on_confirmed: function() {
1982 var context = this.build_context();
1984 return this.view.do_execute_action(
1985 _.extend({}, this.node.attrs, {context: context}),
1986 this.view.dataset, this.view.datarecord.id, function (reason) {
1987 if (!_.isObject(reason)) {
1988 self.view.recursive_reload();
1992 check_disable: function() {
1993 var disabled = (this.force_disabled || !this.view.is_interactible_record());
1994 this.$el.prop('disabled', disabled);
1995 this.$el.css('color', disabled ? 'grey' : '');
2000 * Interface to be implemented by fields.
2003 * - changed_value: triggered when the value of the field has changed. This can be due
2004 * to a user interaction or a call to set_value().
2007 instance.web.form.FieldInterface = {
2009 * Constructor takes 2 arguments:
2010 * - field_manager: Implements FieldManagerMixin
2011 * - node: the "<field>" node in json form
2013 init: function(field_manager, node) {},
2015 * Called by the form view to indicate the value of the field.
2017 * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
2018 * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
2019 * before the widget is inserted into the DOM.
2021 * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
2022 * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
2023 * also be able to handle any other format commonly used in the _defaults key on the models in the addons
2024 * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
2025 * no information is ever given to know which format is used.
2027 set_value: function(value_) {},
2029 * Get the current value of the widget.
2031 * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
2032 * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
2033 * For example if the field is marked as "required", a call to get_value() can return false.
2035 * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
2036 * return a default value according to the type of field.
2038 * This method is always assumed to perform synchronously, it can not return a promise.
2040 * If there was no user interaction to modify the value of the field, it is always assumed that
2041 * get_value() return the same semantic value than the one passed in the last call to set_value(),
2042 * although the syntax can be different. This can be the case for type of fields that have a different
2043 * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
2045 get_value: function() {},
2047 * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
2050 set_input_id: function(id) {},
2052 * Returns true if is_syntax_valid() returns true and the value is semantically
2053 * valid too according to the semantic restrictions applied to the field.
2055 is_valid: function() {},
2057 * Returns true if the field holds a value which is syntactically correct, ignoring
2058 * the potential semantic restrictions applied to the field.
2060 is_syntax_valid: function() {},
2062 * Must set the focus on the field. Return false if field is not focusable.
2064 focus: function() {},
2066 * Called when the translate button is clicked.
2068 on_translate: function() {},
2070 This method is called by the form view before reading on_change values and before saving. It tells
2071 the field to save its value before reading it using get_value(). Must return a promise.
2073 commit_value: function() {},
2077 * Abstract class for classes implementing FieldInterface.
2080 * - value: useful property to hold the value of the field. By default, set_value() and get_value()
2081 * set and retrieve the value property. Changing the value property also triggers automatically
2082 * a 'changed_value' event that inform the view to trigger on_changes.
2085 instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
2087 * @constructs instance.web.form.AbstractField
2088 * @extends instance.web.form.FormWidget
2090 * @param field_manager
2093 init: function(field_manager, node) {
2095 this._super(field_manager, node);
2096 this.name = this.node.attrs.name;
2097 this.field = this.field_manager.get_field_desc(this.name);
2098 this.widget = this.node.attrs.widget;
2099 this.string = this.node.attrs.string || this.field.string || this.name;
2100 this.options = instance.web.py_eval(this.node.attrs.options || '{}');
2101 this.set({'value': false});
2103 this.on("change:value", this, function() {
2104 this.trigger('changed_value');
2105 this._check_css_flags();
2108 renderElement: function() {
2111 if (this.field.translate && this.view) {
2112 this.$el.addClass('oe_form_field_translatable');
2113 this.$el.find('.oe_field_translate').click(this.on_translate);
2115 this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
2116 if (instance.session.debug) {
2117 this.do_attach_tooltip(this, this.$label[0] || this.$el);
2118 this.$label.off('dblclick').on('dblclick', function() {
2119 console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
2121 console.log("window.w =", window.w);
2124 if (!this.disable_utility_classes) {
2125 this.off("change:required", this, this._set_required);
2126 this.on("change:required", this, this._set_required);
2127 this._set_required();
2129 this._check_visibility();
2130 this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
2131 this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
2132 this._check_css_flags();
2135 var tmp = this._super();
2136 this.on("change:value", this, function() {
2137 if (! this.no_rerender)
2138 this.render_value();
2140 this.render_value();
2143 * Private. Do not use.
2145 _set_required: function() {
2146 this.$el.toggleClass('oe_form_required', this.get("required"));
2148 set_value: function(value_) {
2149 this.set({'value': value_});
2151 get_value: function() {
2152 return this.get('value');
2155 Utility method that all implementations should use to change the
2156 value without triggering a re-rendering.
2158 internal_set_value: function(value_) {
2159 var tmp = this.no_rerender;
2160 this.no_rerender = true;
2161 this.set({'value': value_});
2162 this.no_rerender = tmp;
2165 This method is called each time the value is modified.
2167 render_value: function() {},
2168 is_valid: function() {
2169 return this.is_syntax_valid() && !(this.get('required') && this.is_false());
2171 is_syntax_valid: function() {
2175 * Method useful to implement to ease validity testing. Must return true if the current
2176 * value is similar to false in OpenERP.
2178 is_false: function() {
2179 return this.get('value') === false;
2181 _check_css_flags: function() {
2182 if (this.field.translate) {
2183 this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
2185 if (!this.disable_utility_classes) {
2186 if (this.field_manager.get('display_invalid_fields')) {
2187 this.$el.toggleClass('oe_form_invalid', !this.is_valid());
2194 set_input_id: function(id) {
2195 this.id_for_label = id;
2197 on_translate: function() {
2199 var trans = new instance.web.DataSet(this, 'ir.translation');
2200 return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
2205 set_dimensions: function (height, width) {
2211 commit_value: function() {
2217 * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
2220 instance.web.form.ReinitializeWidgetMixin = {
2222 * Default implementation of, you should not override it, use initialize_field() instead.
2225 this.initialize_field();
2228 initialize_field: function() {
2229 this.on("change:effective_readonly", this, this.reinitialize);
2230 this.initialize_content();
2232 reinitialize: function() {
2233 this.destroy_content();
2234 this.renderElement();
2235 this.initialize_content();
2238 * Called to destroy anything that could have been created previously, called before a
2239 * re-initialization.
2241 destroy_content: function() {},
2243 * Called to initialize the content.
2245 initialize_content: function() {},
2249 * A mixin to apply on any field that has to completely re-render when its readonly state
2252 instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
2253 reinitialize: function() {
2254 instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
2255 var res = this.render_value();
2256 if (this.view && this.view.render_value_defs){
2257 this.view.render_value_defs.push(res);
2263 Some hack to make placeholders work in ie9.
2265 if ($.browser.msie && $.browser.version === "9.0") {
2266 document.addEventListener("DOMNodeInserted",function(event){
2267 var nodename = event.target.nodeName.toLowerCase();
2268 if ( nodename === "input" || nodename == "textarea" ) {
2269 $(event.target).placeholder();
2274 instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2275 template: 'FieldChar',
2276 widget_class: 'oe_form_field_char',
2278 'change input': 'store_dom_value',
2280 init: function (field_manager, node) {
2281 this._super(field_manager, node);
2282 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2284 initialize_content: function() {
2285 this.setupFocus(this.$('input'));
2287 store_dom_value: function () {
2288 if (!this.get('effective_readonly')
2289 && this.$('input').length
2290 && this.is_syntax_valid()) {
2291 this.internal_set_value(
2293 this.$('input').val()));
2296 commit_value: function () {
2297 this.store_dom_value();
2298 return this._super();
2300 render_value: function() {
2301 var show_value = this.format_value(this.get('value'), '');
2302 if (!this.get("effective_readonly")) {
2303 this.$el.find('input').val(show_value);
2305 if (this.password) {
2306 show_value = new Array(show_value.length + 1).join('*');
2308 this.$(".oe_form_char_content").text(show_value);
2311 is_syntax_valid: function() {
2312 if (!this.get("effective_readonly") && this.$("input").size() > 0) {
2314 this.parse_value(this.$('input').val(), '');
2322 parse_value: function(val, def) {
2323 return instance.web.parse_value(val, this, def);
2325 format_value: function(val, def) {
2326 return instance.web.format_value(val, this, def);
2328 is_false: function() {
2329 return this.get('value') === '' || this._super();
2332 var input = this.$('input:first')[0];
2333 return input ? input.focus() : false;
2335 set_dimensions: function (height, width) {
2336 this._super(height, width);
2337 this.$('input').css({
2344 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2345 process_modifiers: function () {
2347 this.set({ readonly: true });
2351 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2352 template: 'FieldEmail',
2353 initialize_content: function() {
2355 var $button = this.$el.find('button');
2356 $button.click(this.on_button_clicked);
2357 this.setupFocus($button);
2359 render_value: function() {
2360 if (!this.get("effective_readonly")) {
2364 .attr('href', 'mailto:' + this.get('value'))
2365 .text(this.get('value') || '');
2368 on_button_clicked: function() {
2369 if (!this.get('value') || !this.is_syntax_valid()) {
2370 this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2372 location.href = 'mailto:' + this.get('value');
2377 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2378 template: 'FieldUrl',
2379 initialize_content: function() {
2381 var $button = this.$el.find('button');
2382 $button.click(this.on_button_clicked);
2383 this.setupFocus($button);
2385 render_value: function() {
2386 if (!this.get("effective_readonly")) {
2389 var tmp = this.get('value');
2390 var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2392 tmp = "http://" + this.get('value');
2394 var text = this.get('value') ? this.node.attrs.text || tmp : '';
2395 this.$el.find('a').attr('href', tmp).text(text);
2398 on_button_clicked: function() {
2399 if (!this.get('value')) {
2400 this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2402 var url = $.trim(this.get('value'));
2403 if(/^www\./i.test(url))
2404 url = 'http://'+url;
2410 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2411 is_field_number: true,
2412 widget_class: 'oe_form_field_float',
2413 init: function (field_manager, node) {
2414 this._super(field_manager, node);
2415 this.internal_set_value(0);
2416 if (this.node.attrs.digits) {
2417 this.digits = this.node.attrs.digits;
2419 this.digits = this.field.digits;
2422 set_value: function(value_) {
2423 if (value_ === false || value_ === undefined) {
2424 // As in GTK client, floats default to 0
2427 this._super.apply(this, [value_]);
2429 focus: function () {
2430 var $input = this.$('input:first');
2431 return $input.length ? $input.select() : false;
2435 instance.web.DateTimeWidget = instance.web.Widget.extend({
2436 template: "web.datepicker",
2437 jqueryui_object: 'datetimepicker',
2438 type_of_date: "datetime",
2440 'change .oe_datepicker_master': 'change_datetime',
2441 'keypress .oe_datepicker_master': 'change_datetime',
2443 init: function(parent) {
2444 this._super(parent);
2445 this.name = parent.name;
2449 this.$input = this.$el.find('input.oe_datepicker_master');
2450 this.$input_picker = this.$el.find('input.oe_datepicker_container');
2452 $.datepicker.setDefaults({
2453 clearText: _t('Clear'),
2454 clearStatus: _t('Erase the current date'),
2455 closeText: _t('Done'),
2456 closeStatus: _t('Close without change'),
2457 prevText: _t('<Prev'),
2458 prevStatus: _t('Show the previous month'),
2459 nextText: _t('Next>'),
2460 nextStatus: _t('Show the next month'),
2461 currentText: _t('Today'),
2462 currentStatus: _t('Show the current month'),
2463 monthNames: Date.CultureInfo.monthNames,
2464 monthNamesShort: Date.CultureInfo.abbreviatedMonthNames,
2465 monthStatus: _t('Show a different month'),
2466 yearStatus: _t('Show a different year'),
2467 weekHeader: _t('Wk'),
2468 weekStatus: _t('Week of the year'),
2469 dayNames: Date.CultureInfo.dayNames,
2470 dayNamesShort: Date.CultureInfo.abbreviatedDayNames,
2471 dayNamesMin: Date.CultureInfo.shortestDayNames,
2472 dayStatus: _t('Set DD as first week day'),
2473 dateStatus: _t('Select D, M d'),
2474 firstDay: Date.CultureInfo.firstDayOfWeek,
2475 initStatus: _t('Select a date'),
2478 $.timepicker.setDefaults({
2479 timeOnlyTitle: _t('Choose Time'),
2480 timeText: _t('Time'),
2481 hourText: _t('Hour'),
2482 minuteText: _t('Minute'),
2483 secondText: _t('Second'),
2484 currentText: _t('Now'),
2485 closeText: _t('Done')
2489 onClose: this.on_picker_select,
2490 onSelect: this.on_picker_select,
2494 showButtonPanel: true,
2495 firstDay: Date.CultureInfo.firstDayOfWeek
2497 // Some clicks in the datepicker dialog are not stopped by the
2498 // datepicker and "bubble through", unexpectedly triggering the bus's
2499 // click event. Prevent that.
2500 this.picker('widget').click(function (e) { e.stopPropagation(); });
2502 this.$el.find('img.oe_datepicker_trigger').click(function() {
2503 if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
2504 self.$input.focus();
2507 self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date());
2508 self.$input_picker.show();
2509 self.picker('show');
2510 self.$input_picker.hide();
2512 this.set_readonly(false);
2513 this.set({'value': false});
2515 picker: function() {
2516 return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
2518 on_picker_select: function(text, instance_) {
2519 var date = this.picker('getDate');
2521 .val(date ? this.format_client(date) : '')
2525 set_value: function(value_) {
2526 this.set({'value': value_});
2527 this.$input.val(value_ ? this.format_client(value_) : '');
2529 get_value: function() {
2530 return this.get('value');
2532 set_value_from_ui_: function() {
2533 var value_ = this.$input.val() || false;
2534 this.set({'value': this.parse_client(value_)});
2536 set_readonly: function(readonly) {
2537 this.readonly = readonly;
2538 this.$input.prop('readonly', this.readonly);
2539 this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
2541 is_valid_: function() {
2542 var value_ = this.$input.val();
2543 if (value_ === "") {
2547 this.parse_client(value_);
2554 parse_client: function(v) {
2555 return instance.web.parse_value(v, {"widget": this.type_of_date});
2557 format_client: function(v) {
2558 return instance.web.format_value(v, {"widget": this.type_of_date});
2560 change_datetime: function(e) {
2561 if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
2562 this.set_value_from_ui_();
2563 this.trigger("datetime_changed");
2566 commit_value: function () {
2567 this.change_datetime();
2571 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2572 jqueryui_object: 'datepicker',
2573 type_of_date: "date"
2576 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2577 template: "FieldDatetime",
2578 build_widget: function() {
2579 return new instance.web.DateTimeWidget(this);
2581 destroy_content: function() {
2582 if (this.datewidget) {
2583 this.datewidget.destroy();
2584 this.datewidget = undefined;
2587 initialize_content: function() {
2588 if (!this.get("effective_readonly")) {
2589 this.datewidget = this.build_widget();
2590 this.datewidget.on('datetime_changed', this, _.bind(function() {
2591 this.internal_set_value(this.datewidget.get_value());
2593 this.datewidget.appendTo(this.$el);
2594 this.setupFocus(this.datewidget.$input);
2597 render_value: function() {
2598 if (!this.get("effective_readonly")) {
2599 this.datewidget.set_value(this.get('value'));
2601 this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2604 is_syntax_valid: function() {
2605 if (!this.get("effective_readonly") && this.datewidget) {
2606 return this.datewidget.is_valid_();
2610 is_false: function() {
2611 return this.get('value') === '' || this._super();
2614 var input = this.datewidget && this.datewidget.$input[0];
2615 return input ? input.focus() : false;
2617 set_dimensions: function (height, width) {
2618 this._super(height, width);
2619 if (!this.get("effective_readonly")) {
2620 this.datewidget.$input.css('height', height);
2625 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2626 template: "FieldDate",
2627 build_widget: function() {
2628 return new instance.web.DateWidget(this);
2632 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2633 template: 'FieldText',
2635 'keyup': function (e) {
2636 if (e.which === $.ui.keyCode.ENTER) {
2637 e.stopPropagation();
2640 'keypress': function (e) {
2641 if (e.which === $.ui.keyCode.ENTER) {
2642 e.stopPropagation();
2645 'change textarea': 'store_dom_value',
2647 initialize_content: function() {
2649 if (! this.get("effective_readonly")) {
2650 this.$textarea = this.$el.find('textarea');
2651 this.auto_sized = false;
2652 this.default_height = this.$textarea.css('height');
2653 if (this.get("effective_readonly")) {
2654 this.$textarea.attr('disabled', 'disabled');
2656 this.setupFocus(this.$textarea);
2658 this.$textarea = undefined;
2661 commit_value: function () {
2662 if (! this.get("effective_readonly") && this.$textarea) {
2663 this.store_dom_value();
2665 return this._super();
2667 store_dom_value: function () {
2668 this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2670 render_value: function() {
2671 if (! this.get("effective_readonly")) {
2672 var show_value = instance.web.format_value(this.get('value'), this, '');
2673 if (show_value === '') {
2674 this.$textarea.css('height', parseInt(this.default_height)+"px");
2676 this.$textarea.val(show_value);
2677 if (! this.auto_sized) {
2678 this.auto_sized = true;
2679 this.$textarea.autosize();
2681 this.$textarea.trigger("autosize");
2684 var txt = this.get("value") || '';
2685 this.$(".oe_form_text_content").text(txt);
2688 is_syntax_valid: function() {
2689 if (!this.get("effective_readonly") && this.$textarea) {
2691 instance.web.parse_value(this.$textarea.val(), this, '');
2699 is_false: function() {
2700 return this.get('value') === '' || this._super();
2702 focus: function($el) {
2703 var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2704 return input ? input.focus() : false;
2706 set_dimensions: function (height, width) {
2707 this._super(height, width);
2708 if (!this.get("effective_readonly") && this.$textarea) {
2709 this.$textarea.css({
2718 * FieldTextHtml Widget
2719 * Intended for FieldText widgets meant to display HTML content. This
2720 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2721 * To find more information about CLEditor configutation: go to
2722 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2724 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2725 template: 'FieldTextHtml',
2727 this._super.apply(this, arguments);
2729 initialize_content: function() {
2731 if (! this.get("effective_readonly")) {
2732 self._updating_editor = false;
2733 this.$textarea = this.$el.find('textarea');
2734 var width = ((this.node.attrs || {}).editor_width || 'calc(100% - 4px)');
2735 var height = ((this.node.attrs || {}).editor_height || 250);
2736 this.$textarea.cleditor({
2737 width: width, // width not including margins, borders or padding
2738 height: height, // height not including margins, borders or padding
2739 controls: // controls to add to the toolbar
2740 "bold italic underline strikethrough " +
2741 "| removeformat | bullets numbering | outdent " +
2742 "indent | link unlink | source",
2743 bodyStyle: // style to assign to document body contained within the editor
2744 "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2746 this.$cleditor = this.$textarea.cleditor()[0];
2747 this.$cleditor.change(function() {
2748 if (! self._updating_editor) {
2749 self.$cleditor.updateTextArea();
2750 self.internal_set_value(self.$textarea.val());
2753 if (this.field.translate) {
2754 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"/>')
2755 .click(this.on_translate);
2756 this.$cleditor.$toolbar.append($img);
2760 render_value: function() {
2761 if (! this.get("effective_readonly")) {
2762 this.$textarea.val(this.get('value') || '');
2763 this._updating_editor = true;
2764 this.$cleditor.updateFrame();
2765 this._updating_editor = false;
2767 this.$el.html(this.get('value'));
2772 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2773 template: 'FieldBoolean',
2776 this.$checkbox = $("input", this.$el);
2777 this.setupFocus(this.$checkbox);
2778 this.$el.click(_.bind(function() {
2779 this.internal_set_value(this.$checkbox.is(':checked'));
2781 var check_readonly = function() {
2782 self.$checkbox.prop('disabled', self.get("effective_readonly"));
2784 this.on("change:effective_readonly", this, check_readonly);
2785 check_readonly.call(this);
2786 this._super.apply(this, arguments);
2788 render_value: function() {
2789 this.$checkbox[0].checked = this.get('value');
2792 var input = this.$checkbox && this.$checkbox[0];
2793 return input ? input.focus() : false;
2798 The progressbar field expect a float from 0 to 100.
2800 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2801 template: 'FieldProgressBar',
2802 render_value: function() {
2803 this.$el.progressbar({
2804 value: this.get('value') || 0,
2805 disabled: this.get("effective_readonly")
2807 var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
2808 this.$('span').html(formatted_value + '%');
2813 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2814 template: 'FieldSelection',
2816 'change select': 'store_dom_value',
2818 init: function(field_manager, node) {
2820 this._super(field_manager, node);
2821 this.values = _(this.field.selection).chain()
2822 .reject(function (v) { return v[0] === false && v[1] === ''; })
2823 .unshift([false, ''])
2826 initialize_content: function() {
2827 // Flag indicating whether we're in an event chain containing a change
2828 // event on the select, in order to know what to do on keyup[RETURN]:
2829 // * If the user presses [RETURN] as part of changing the value of a
2830 // selection, we should just let the value change and not let the
2831 // event broadcast further (e.g. to validating the current state of
2832 // the form in editable list view, which would lead to saving the
2833 // current row or switching to the next one)
2834 // * If the user presses [RETURN] with a select closed (side-effect:
2835 // also if the user opened the select and pressed [RETURN] without
2836 // changing the selected value), takes the action as validating the
2838 var ischanging = false;
2839 var $select = this.$el.find('select')
2840 .change(function () { ischanging = true; })
2841 .click(function () { ischanging = false; })
2842 .keyup(function (e) {
2843 if (e.which !== 13 || !ischanging) { return; }
2844 e.stopPropagation();
2847 this.setupFocus($select);
2849 commit_value: function () {
2850 this.store_dom_value();
2851 return this._super();
2853 store_dom_value: function () {
2854 if (!this.get('effective_readonly') && this.$('select').length) {
2855 this.internal_set_value(
2856 this.values[this.$('select')[0].selectedIndex][0]);
2859 set_value: function(value_) {
2860 value_ = value_ === null ? false : value_;
2861 value_ = value_ instanceof Array ? value_[0] : value_;
2862 this._super(value_);
2864 render_value: function() {
2865 if (!this.get("effective_readonly")) {
2867 for (var i = 0, ii = this.values.length; i < ii; i++) {
2868 if (this.values[i][0] === this.get('value')) index = i;
2870 this.$el.find('select')[0].selectedIndex = index;
2873 var option = _(this.values)
2874 .detect(function (record) { return record[0] === self.get('value'); });
2875 this.$el.text(option ? option[1] : this.values[0][1]);
2879 var input = this.$('select:first')[0];
2880 return input ? input.focus() : false;
2882 set_dimensions: function (height, width) {
2883 this._super(height, width);
2884 this.$('select').css({
2891 // jquery autocomplete tweak to allow html and classnames
2893 var proto = $.ui.autocomplete.prototype,
2894 initSource = proto._initSource;
2896 function filter( array, term ) {
2897 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
2898 return $.grep( array, function(value_) {
2899 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
2904 _initSource: function() {
2905 if ( this.options.html && $.isArray(this.options.source) ) {
2906 this.source = function( request, response ) {
2907 response( filter( this.options.source, request.term ) );
2910 initSource.call( this );
2914 _renderItem: function( ul, item) {
2915 return $( "<li></li>" )
2916 .data( "item.autocomplete", item )
2917 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
2919 .addClass(item.classname);
2925 * A mixin containing some useful methods to handle completion inputs.
2927 instance.web.form.CompletionFieldMixin = {
2930 this.orderer = new instance.web.DropMisordered();
2933 * Call this method to search using a string.
2935 get_search_result: function(search_val) {
2938 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
2939 var blacklist = this.get_search_blacklist();
2940 this.last_query = search_val;
2942 return this.orderer.add(dataset.name_search(
2943 search_val, new instance.web.CompoundDomain(self.build_domain(), [["id", "not in", blacklist]]),
2944 'ilike', this.limit + 1, self.build_context())).then(function(data) {
2945 self.last_search = data;
2946 // possible selections for the m2o
2947 var values = _.map(data, function(x) {
2948 x[1] = x[1].split("\n")[0];
2950 label: _.str.escapeHTML(x[1]),
2957 // search more... if more results that max
2958 if (values.length > self.limit) {
2959 values = values.slice(0, self.limit);
2961 label: _t("Search More..."),
2962 action: function() {
2963 dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
2964 self._search_create_popup("search", data);
2967 classname: 'oe_m2o_dropdown_option'
2971 var raw_result = _(data.result).map(function(x) {return x[1];});
2972 if (search_val.length > 0 && !_.include(raw_result, search_val)) {
2974 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
2975 $('<span />').text(search_val).html()),
2976 action: function() {
2977 self._quick_create(search_val);
2979 classname: 'oe_m2o_dropdown_option'
2984 label: _t("Create and Edit..."),
2985 action: function() {
2986 self._search_create_popup("form", undefined, self._create_context(search_val));
2988 classname: 'oe_m2o_dropdown_option'
2994 get_search_blacklist: function() {
2997 _quick_create: function(name) {
2999 var slow_create = function () {
3000 self._search_create_popup("form", undefined, self._create_context(name));
3002 if (self.options.quick_create === undefined || self.options.quick_create) {
3003 new instance.web.DataSet(this, this.field.relation, self.build_context())
3004 .name_create(name).done(function(data) {
3005 if (!self.get('effective_readonly'))
3006 self.add_id(data[0]);
3007 }).fail(function(error, event) {
3008 event.preventDefault();
3014 // all search/create popup handling
3015 _search_create_popup: function(view, ids, context) {
3017 var pop = new instance.web.form.SelectCreatePopup(this);
3019 self.field.relation,
3021 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3022 initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
3024 disable_multiple_selection: true
3026 self.build_domain(),
3027 new instance.web.CompoundContext(self.build_context(), context || {})
3029 pop.on("elements_selected", self, function(element_ids) {
3030 self.add_id(element_ids[0]);
3037 add_id: function(id) {},
3038 _create_context: function(name) {
3040 var field = (this.options || {}).create_name_field;
3041 if (field === undefined)
3043 if (field !== false && name && (this.options || {}).quick_create !== false)
3044 tmp["default_" + field] = name;
3049 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3050 template: "M2ODialog",
3051 init: function(parent) {
3052 this._super(parent, {
3053 title: _.str.sprintf(_t("Add %s"), parent.string),
3059 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3060 this.$("input").val(this.getParent().last_query);
3061 this.$buttons.find(".oe_form_m2o_qc_button").click(function(){
3062 self.getParent()._quick_create(self.$("input").val());
3065 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3066 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3069 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3075 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3076 template: "FieldMany2One",
3078 'keydown input': function (e) {
3080 case $.ui.keyCode.UP:
3081 case $.ui.keyCode.DOWN:
3082 e.stopPropagation();
3086 init: function(field_manager, node) {
3087 this._super(field_manager, node);
3088 instance.web.form.CompletionFieldMixin.init.call(this);
3089 this.set({'value': false});
3090 this.display_value = {};
3091 this.last_search = [];
3092 this.floating = false;
3093 this.current_display = null;
3094 this.is_started = false;
3095 this.ignore_focusout = false;
3097 reinit_value: function(val) {
3098 this.internal_set_value(val);
3099 this.floating = false;
3100 if (this.is_started)
3101 this.render_value();
3103 initialize_field: function() {
3104 this.is_started = true;
3105 instance.web.bus.on('click', this, function() {
3106 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3107 this.$input.autocomplete("close");
3110 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3112 initialize_content: function() {
3113 if (!this.get("effective_readonly"))
3114 this.render_editable();
3116 destroy_content: function () {
3117 if (this.$drop_down) {
3118 this.$drop_down.off('click');
3119 delete this.$drop_down;
3122 this.$input.closest(".ui-dialog .ui-dialog-content").off('scroll');
3123 this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3124 'focus focusout change keydown');
3127 if (this.$follow_button) {
3128 this.$follow_button.off('blur focus click');
3129 delete this.$follow_button;
3132 destroy: function () {
3133 this.destroy_content();
3134 return this._super();
3136 init_error_displayer: function() {
3139 hide_error_displayer: function() {
3142 show_error_displayer: function() {
3143 new instance.web.form.M2ODialog(this).open();
3145 render_editable: function() {
3147 this.$input = this.$el.find("input");
3149 this.init_error_displayer();
3151 self.$input.on('focus', function() {
3152 self.hide_error_displayer();
3155 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3156 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3158 this.$follow_button.click(function(ev) {
3159 ev.preventDefault();
3160 if (!self.get('value')) {
3164 var pop = new instance.web.form.FormOpenPopup(self);
3166 self.field.relation,
3168 self.build_context(),
3170 title: _t("Open: ") + self.string
3173 pop.on('write_completed', self, function(){
3174 self.display_value = {};
3175 self.render_value();
3177 self.trigger('changed_value');
3181 // some behavior for input
3182 var input_changed = function() {
3183 if (self.current_display !== self.$input.val()) {
3184 self.current_display = self.$input.val();
3185 if (self.$input.val() === "") {
3186 self.internal_set_value(false);
3187 self.floating = false;
3189 self.floating = true;
3193 this.$input.keydown(input_changed);
3194 this.$input.change(input_changed);
3195 this.$drop_down.click(function() {
3196 if (self.$input.autocomplete("widget").is(":visible")) {
3197 self.$input.autocomplete("close");
3198 self.$input.focus();
3200 if (self.get("value") && ! self.floating) {
3201 self.$input.autocomplete("search", "");
3203 self.$input.autocomplete("search");
3208 // Autocomplete close on dialog content scroll
3209 var close_autocomplete = _.debounce(function() {
3210 if (self.$input.autocomplete("widget").is(":visible")) {
3211 self.$input.autocomplete("close");
3214 this.$input.closest(".ui-dialog .ui-dialog-content").on('scroll', this, close_autocomplete);
3216 self.ed_def = $.Deferred();
3217 self.uned_def = $.Deferred();
3219 var ed_duration = 15000;
3220 var anyoneLoosesFocus = function (e) {
3221 if (self.ignore_focusout) { return; }
3223 if (self.floating) {
3224 if (self.last_search.length > 0) {
3225 if (self.last_search[0][0] != self.get("value")) {
3226 self.display_value = {};
3227 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3228 self.reinit_value(self.last_search[0][0]);
3231 self.render_value();
3235 self.reinit_value(false);
3237 self.floating = false;
3239 if (used && self.get("value") === false && ! self.no_ed) {
3240 self.ed_def.reject();
3241 self.uned_def.reject();
3242 self.ed_def = $.Deferred();
3243 self.ed_def.done(function() {
3244 self.show_error_displayer();
3245 ignore_blur = false;
3246 self.trigger('focused');
3249 setTimeout(function() {
3250 self.ed_def.resolve();
3251 self.uned_def.reject();
3252 self.uned_def = $.Deferred();
3253 self.uned_def.done(function() {
3254 self.hide_error_displayer();
3256 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3260 self.ed_def.reject();
3263 var ignore_blur = false;
3265 focusout: anyoneLoosesFocus,
3266 focus: function () { self.trigger('focused'); },
3267 autocompleteopen: function () { ignore_blur = true; },
3268 autocompleteclose: function () { ignore_blur = false; },
3270 // autocomplete open
3271 if (ignore_blur) { return; }
3272 if (_(self.getChildren()).any(function (child) {
3273 return child instanceof instance.web.form.AbstractFormPopup;
3275 self.trigger('blurred');
3279 var isSelecting = false;
3281 this.$input.autocomplete({
3282 source: function(req, resp) {
3283 self.get_search_result(req.term).done(function(result) {
3287 select: function(event, ui) {
3291 self.display_value = {};
3292 self.display_value["" + item.id] = item.name;
3293 self.reinit_value(item.id);
3294 } else if (item.action) {
3296 // Cancel widget blurring, to avoid form blur event
3297 self.trigger('focused');
3301 focus: function(e, ui) {
3305 // disabled to solve a bug, but may cause others
3306 //close: anyoneLoosesFocus,
3310 this.$input.autocomplete("widget").openerpClass();
3311 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3312 this.$input.keyup(function(e) {
3313 if (e.which === 13) { // ENTER
3315 e.stopPropagation();
3317 isSelecting = false;
3319 this.setupFocus(this.$follow_button);
3321 render_value: function(no_recurse) {
3323 if (! this.get("value")) {
3324 this.display_string("");
3327 var display = this.display_value["" + this.get("value")];
3329 this.display_string(display);
3333 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3334 this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3336 self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
3339 self.display_value["" + self.get("value")] = data[0][1];
3340 self.render_value(true);
3344 display_string: function(str) {
3346 if (!this.get("effective_readonly")) {
3347 this.$input.val(str.split("\n")[0]);
3348 this.current_display = this.$input.val();
3349 if (this.is_false()) {
3350 this.$('.oe_m2o_cm_button').css({'display':'none'});
3352 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3355 var lines = _.escape(str).split("\n");
3359 follow = _.rest(lines).join("<br />");
3362 var $link = this.$el.find('.oe_form_uri')
3365 if (! this.options.no_open)
3366 $link.click(function () {
3368 type: 'ir.actions.act_window',
3369 res_model: self.field.relation,
3370 res_id: self.get("value"),
3371 views: [[false, 'form']],
3373 context: self.build_context().eval(),
3377 $(".oe_form_m2o_follow", this.$el).html(follow);
3380 set_value: function(value_) {
3382 if (value_ instanceof Array) {
3383 this.display_value = {};
3384 if (! this.options.always_reload) {
3385 this.display_value["" + value_[0]] = value_[1];
3389 value_ = value_ || false;
3390 this.reinit_value(value_);
3392 get_displayed: function() {
3393 return this.display_value["" + this.get("value")];
3395 add_id: function(id) {
3396 this.display_value = {};
3397 this.reinit_value(id);
3399 is_false: function() {
3400 return ! this.get("value");
3402 focus: function () {
3403 var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3404 return input ? input.focus() : false;
3406 _quick_create: function() {
3408 this.ed_def.reject();
3409 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3411 _search_create_popup: function() {
3413 this.ed_def.reject();
3414 this.ignore_focusout = true;
3415 this.reinit_value(false);
3416 var res = instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3417 this.ignore_focusout = false;
3421 set_dimensions: function (height, width) {
3422 this._super(height, width);
3423 if (!this.get("effective_readonly") && this.$input)
3424 this.$input.css('height', height);
3428 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3429 template: 'Many2OneButton',
3430 init: function(field_manager, node) {
3431 this._super.apply(this, arguments);
3434 this._super.apply(this, arguments);
3437 set_button: function() {
3440 this.$button.remove();
3443 this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3444 this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3445 this.$button.addClass('oe_link').css({'padding':'4px'});
3446 this.$el.append(this.$button);
3447 this.$button.on('click', self.on_click);
3449 on_click: function(ev) {
3451 this.popup = new instance.web.form.FormOpenPopup(this);
3452 this.popup.show_element(
3453 this.field.relation,
3455 this.build_context(),
3456 {title: this.string}
3458 this.popup.on('create_completed', self, function(r) {
3462 set_value: function(value_) {
3464 if (value_ instanceof Array) {
3467 value_ = value_ || false;
3468 this.set('value', value_);
3474 # Values: (0, 0, { fields }) create
3475 # (1, ID, { fields }) update
3476 # (2, ID) remove (delete)
3477 # (3, ID) unlink one (target id or target of relation)
3479 # (5) unlink all (only valid for one2many)
3484 'create': function (values) {
3485 return [commands.CREATE, false, values];
3487 // (1, id, {values})
3489 'update': function (id, values) {
3490 return [commands.UPDATE, id, values];
3494 'delete': function (id) {
3495 return [commands.DELETE, id, false];
3497 // (3, id[, _]) removes relation, but not linked record itself
3499 'forget': function (id) {
3500 return [commands.FORGET, id, false];
3504 'link_to': function (id) {
3505 return [commands.LINK_TO, id, false];
3509 'delete_all': function () {
3510 return [5, false, false];
3512 // (6, _, ids) replaces all linked records with provided ids
3514 'replace_with': function (ids) {
3515 return [6, false, ids];
3518 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3519 multi_selection: false,
3520 init: function(field_manager, node) {
3521 this._super(field_manager, node);
3522 lazy_build_o2m_kanban_view();
3523 this.is_loaded = $.Deferred();
3524 this.initial_is_loaded = this.is_loaded;
3525 this.form_last_update = $.Deferred();
3526 this.init_form_last_update = this.form_last_update;
3527 this.is_started = false;
3528 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
3529 this.dataset.o2m = this;
3530 this.dataset.parent_view = this.view;
3531 this.dataset.child_name = this.name;
3533 this.dataset.on('dataset_changed', this, function() {
3534 self.trigger_on_change();
3539 this._super.apply(this, arguments);
3540 this.$el.addClass('oe_form_field oe_form_field_one2many');
3545 this.is_loaded.done(function() {
3546 self.on("change:effective_readonly", self, function() {
3547 self.is_loaded = self.is_loaded.then(function() {
3548 self.viewmanager.destroy();
3549 return $.when(self.load_views()).done(function() {
3550 self.reload_current_view();
3555 this.is_started = true;
3556 this.reload_current_view();
3558 trigger_on_change: function() {
3559 this.trigger('changed_value');
3561 load_views: function() {
3564 var modes = this.node.attrs.mode;
3565 modes = !!modes ? modes.split(",") : ["tree"];
3567 _.each(modes, function(mode) {
3568 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
3569 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
3573 view_type: mode == "tree" ? "list" : mode,
3576 if (self.field.views && self.field.views[mode]) {
3577 view.embedded_view = self.field.views[mode];
3579 if(view.view_type === "list") {
3580 _.extend(view.options, {
3582 selectable: self.multi_selection,
3584 import_enabled: false,
3587 if (self.get("effective_readonly")) {
3588 _.extend(view.options, {
3593 } else if (view.view_type === "form") {
3594 if (self.get("effective_readonly")) {
3595 view.view_type = 'form';
3597 _.extend(view.options, {
3598 not_interactible_on_create: true,
3600 } else if (view.view_type === "kanban") {
3601 _.extend(view.options, {
3602 confirm_on_delete: false,
3604 if (self.get("effective_readonly")) {
3605 _.extend(view.options, {
3606 action_buttons: false,
3607 quick_creatable: false,
3609 read_only_mode: true,
3617 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
3618 this.viewmanager.o2m = self;
3619 var once = $.Deferred().done(function() {
3620 self.init_form_last_update.resolve();
3622 var def = $.Deferred().done(function() {
3623 self.initial_is_loaded.resolve();
3625 this.viewmanager.on("controller_inited", self, function(view_type, controller) {
3626 controller.o2m = self;
3627 if (view_type == "list") {
3628 if (self.get("effective_readonly")) {
3629 controller.on('edit:before', self, function (e) {
3632 _(controller.columns).find(function (column) {
3633 if (!(column instanceof instance.web.list.Handle)) {
3636 column.modifiers.invisible = true;
3640 } else if (view_type === "form") {
3641 if (self.get("effective_readonly")) {
3642 $(".oe_form_buttons", controller.$el).children().remove();
3644 controller.on("load_record", self, function(){
3647 controller.on('pager_action_executed',self,self.save_any_view);
3648 } else if (view_type == "graph") {
3649 self.reload_current_view()
3653 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
3654 $.when(self.save_any_view()).done(function() {
3655 if (n_mode === "list") {
3656 $.async_when().done(function() {
3657 self.reload_current_view();
3662 $.async_when().done(function () {
3663 self.viewmanager.appendTo(self.$el);
3667 reload_current_view: function() {
3669 return self.is_loaded = self.is_loaded.then(function() {
3670 var active_view = self.viewmanager.active_view;
3671 var view = self.viewmanager.views[active_view].controller;
3672 if(active_view === "list") {
3673 return view.reload_content();
3674 } else if (active_view === "form") {
3675 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
3676 self.dataset.index = 0;
3678 var act = function() {
3679 return view.do_show();
3681 self.form_last_update = self.form_last_update.then(act, act);
3682 return self.form_last_update;
3683 } else if (view.do_search) {
3684 return view.do_search(self.build_domain(), self.dataset.get_context(), []);
3688 set_value: function(value_) {
3689 value_ = value_ || [];
3691 this.dataset.reset_ids([]);
3692 if(value_.length >= 1 && value_[0] instanceof Array) {
3694 _.each(value_, function(command) {
3695 var obj = {values: command[2]};
3696 switch (command[0]) {
3697 case commands.CREATE:
3698 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3700 self.dataset.to_create.push(obj);
3701 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3704 case commands.UPDATE:
3705 obj['id'] = command[1];
3706 self.dataset.to_write.push(obj);
3707 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3710 case commands.DELETE:
3711 self.dataset.to_delete.push({id: command[1]});
3713 case commands.LINK_TO:
3714 ids.push(command[1]);
3716 case commands.DELETE_ALL:
3717 self.dataset.delete_all = true;
3722 this.dataset.set_ids(ids);
3723 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
3725 this.dataset.delete_all = true;
3726 _.each(value_, function(command) {
3727 var obj = {values: command};
3728 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3730 self.dataset.to_create.push(obj);
3731 self.dataset.cache.push(_.clone(obj));
3735 this.dataset.set_ids(ids);
3737 this._super(value_);
3738 this.dataset.reset_ids(value_);
3740 if (this.dataset.index === null && this.dataset.ids.length > 0) {
3741 this.dataset.index = 0;
3743 this.trigger_on_change();
3744 if (this.is_started) {
3745 return self.reload_current_view();
3750 get_value: function() {
3754 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
3755 val = val.concat(_.map(this.dataset.ids, function(id) {
3756 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
3758 return commands.create(alter_order.values);
3760 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
3762 return commands.update(alter_order.id, alter_order.values);
3764 return commands.link_to(id);
3766 return val.concat(_.map(
3767 this.dataset.to_delete, function(x) {
3768 return commands['delete'](x.id);}));
3770 commit_value: function() {
3771 return this.save_any_view();
3773 save_any_view: function() {
3774 if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
3775 this.viewmanager.views[this.viewmanager.active_view] &&
3776 this.viewmanager.views[this.viewmanager.active_view].controller) {
3777 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3778 if (this.viewmanager.active_view === "form") {
3779 if (!view.is_initialized.state() === 'resolved') {
3780 return $.when(false);
3782 return $.when(view.save());
3783 } else if (this.viewmanager.active_view === "list") {
3784 return $.when(view.ensure_saved());
3787 return $.when(false);
3789 is_false: function() {
3790 return this.dataset.ids.length == 0;
3792 is_syntax_valid: function() {
3793 if (! this.viewmanager || ! this.viewmanager.views[this.viewmanager.active_view])
3795 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3796 switch (this.viewmanager.active_view) {
3798 return _(view.fields).chain()
3804 return view.is_valid();
3810 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
3811 template: 'One2Many.viewmanager',
3812 init: function(parent, dataset, views, flags) {
3813 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
3814 this.registry = this.registry.extend({
3815 list: 'instance.web.form.One2ManyListView',
3816 form: 'instance.web.form.One2ManyFormView',
3817 kanban: 'instance.web.form.One2ManyKanbanView',
3819 this.__ignore_blur = false;
3821 switch_mode: function(mode, unused) {
3822 if (mode !== 'form') {
3823 return this._super(mode, unused);
3826 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
3827 var pop = new instance.web.form.FormOpenPopup(this);
3828 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3829 title: _t("Open: ") + self.o2m.string,
3830 create_function: function(data, options) {
3831 return self.o2m.dataset.create(data, options).done(function(r) {
3832 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3833 self.o2m.dataset.trigger("dataset_changed", r);
3836 write_function: function(id, data, options) {
3837 return self.o2m.dataset.write(id, data, {}).done(function() {
3838 self.o2m.reload_current_view();
3841 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3842 parent_view: self.o2m.view,
3843 child_name: self.o2m.name,
3844 read_function: function() {
3845 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3847 form_view_options: {'not_interactible_on_create':true},
3848 readonly: self.o2m.get("effective_readonly")
3850 pop.on("elements_selected", self, function() {
3851 self.o2m.reload_current_view();
3856 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
3857 get_context: function() {
3858 this.context = this.o2m.build_context();
3860 _.each(arguments, function(context) {
3861 self.context.add(context);
3863 return this.context;
3867 instance.web.form.One2ManyListView = instance.web.ListView.extend({
3868 _template: 'One2Many.listview',
3869 init: function (parent, dataset, view_id, options) {
3870 this._super(parent, dataset, view_id, _.extend(options || {}, {
3871 GroupsType: instance.web.form.One2ManyGroups,
3872 ListType: instance.web.form.One2ManyList
3874 this.on('edit:after', this, this.proxy('_after_edit'));
3875 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
3878 .bind('add', this.proxy("changed_records"))
3879 .bind('edit', this.proxy("changed_records"))
3880 .bind('remove', this.proxy("changed_records"));
3882 start: function () {
3883 var ret = this._super();
3885 .off('mousedown.handleButtons')
3886 .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
3889 changed_records: function () {
3890 this.o2m.trigger_on_change();
3892 is_valid: function () {
3894 if (!this.fields_view || !this.editable()){
3898 return _.every(this.records.records, function(record){
3900 _.each(self.editor.form.fields, function(field){
3901 field._inhibit_on_change_flag = true;
3902 field.set_value(r.attributes[field.name]);
3903 field._inhibit_on_change_flag = false;
3905 return _.every(self.editor.form.fields, function(field){
3906 field.process_modifiers();
3907 field._check_css_flags();
3908 return field.is_valid();
3912 do_add_record: function () {
3913 if (this.editable()) {
3914 this._super.apply(this, arguments);
3917 var pop = new instance.web.form.SelectCreatePopup(this);
3919 self.o2m.field.relation,
3921 title: _t("Create: ") + self.o2m.string,
3922 initial_view: "form",
3923 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3924 create_function: function(data, options) {
3925 return self.o2m.dataset.create(data, options).done(function(r) {
3926 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3927 self.o2m.dataset.trigger("dataset_changed", r);
3930 read_function: function() {
3931 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3933 parent_view: self.o2m.view,
3934 child_name: self.o2m.name,
3935 form_view_options: {'not_interactible_on_create':true}
3937 self.o2m.build_domain(),
3938 self.o2m.build_context()
3940 pop.on("elements_selected", self, function() {
3941 self.o2m.reload_current_view();
3945 do_activate_record: function(index, id) {
3947 var pop = new instance.web.form.FormOpenPopup(self);
3948 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3949 title: _t("Open: ") + self.o2m.string,
3950 write_function: function(id, data) {
3951 return self.o2m.dataset.write(id, data, {}).done(function() {
3952 self.o2m.reload_current_view();
3955 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3956 parent_view: self.o2m.view,
3957 child_name: self.o2m.name,
3958 read_function: function() {
3959 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3961 form_view_options: {'not_interactible_on_create':true},
3962 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
3965 do_button_action: function (name, id, callback) {
3966 if (!_.isNumber(id)) {
3967 instance.webclient.notification.warn(
3968 _t("Action Button"),
3969 _t("The o2m record must be saved before an action can be used"));
3972 var parent_form = this.o2m.view;
3974 this.ensure_saved().then(function () {
3976 return parent_form.save();
3979 }).done(function () {
3980 var ds = self.o2m.dataset;
3981 var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
3982 return value.length;
3984 if (!self.o2m.options.reload_on_button && !cached_records) {
3985 self.handle_button(name, id, callback);
3987 self.handle_button(name, id, function(){
3988 self.o2m.view.reload();
3994 _after_edit: function () {
3995 this.__ignore_blur = false;
3996 this.editor.form.on('blurred', this, this._on_form_blur);
3998 // The form's blur thing may be jiggered during the edition setup,
3999 // potentially leading to the o2m instasaving the row. Cancel any
4000 // blurring triggered the edition startup here
4001 this.editor.form.widgetFocused();
4003 _before_unedit: function () {
4004 this.editor.form.off('blurred', this, this._on_form_blur);
4006 _button_down: function () {
4007 // If a button is clicked (usually some sort of action button), it's
4008 // the button's responsibility to ensure the editable list is in the
4009 // correct state -> ignore form blurring
4010 this.__ignore_blur = true;
4013 * Handles blurring of the nested form (saves the currently edited row),
4014 * unless the flag to ignore the event is set to ``true``
4016 * Makes the internal form go away
4018 _on_form_blur: function () {
4019 if (this.__ignore_blur) {
4020 this.__ignore_blur = false;
4023 // FIXME: why isn't there an API for this?
4024 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4025 this.ensure_saved();
4028 this.cancel_edition();
4030 keypress_ENTER: function () {
4031 // blurring caused by hitting the [Return] key, should skip the
4032 // autosave-on-blur and let the handler for [Return] do its thing (save
4033 // the current row *anyway*, then create a new one/edit the next one)
4034 this.__ignore_blur = true;
4035 this._super.apply(this, arguments);
4037 do_delete: function (ids) {
4038 var confirm = window.confirm;
4039 window.confirm = function () { return true; };
4041 return this._super(ids);
4043 window.confirm = confirm;
4046 reload_record: function (record) {
4047 // Evict record.id from cache to ensure it will be reloaded correctly
4048 this.dataset.evict_record(record.get('id'));
4050 return this._super(record);
4053 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4054 setup_resequence_rows: function () {
4055 if (!this.view.o2m.get('effective_readonly')) {
4056 this._super.apply(this, arguments);
4060 instance.web.form.One2ManyList = instance.web.ListView.List.extend({
4061 pad_table_to: function (count) {
4062 if (!this.view.is_action_enabled('create')) {
4065 this._super(count > 0 ? count - 1 : 0);
4068 // magical invocation of wtf does that do
4069 if (this.view.o2m.get('effective_readonly')) {
4074 var columns = _(this.columns).filter(function (column) {
4075 return column.invisible !== '1';
4077 if (this.options.selectable) { columns++; }
4078 if (this.options.deletable) { columns++; }
4080 if (!this.view.is_action_enabled('create')) {
4084 var $cell = $('<td>', {
4086 'class': 'oe_form_field_one2many_list_row_add'
4088 $('<a>', {href: '#'}).text(_t("Add an item"))
4089 .mousedown(function () {
4090 // FIXME: needs to be an official API somehow
4091 if (self.view.editor.is_editing()) {
4092 self.view.__ignore_blur = true;
4095 .click(function (e) {
4097 e.stopPropagation();
4098 // FIXME: there should also be an API for that one
4099 if (self.view.editor.form.__blur_timeout) {
4100 clearTimeout(self.view.editor.form.__blur_timeout);
4101 self.view.editor.form.__blur_timeout = false;
4103 self.view.ensure_saved().done(function () {
4104 self.view.do_add_record();
4108 var $padding = this.$current.find('tr:not([data-id]):first');
4109 var $newrow = $('<tr>').append($cell);
4110 if ($padding.length) {
4111 $padding.before($newrow);
4113 this.$current.append($newrow)
4118 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4119 form_template: 'One2Many.formview',
4120 load_form: function(data) {
4123 this.$buttons.find('button.oe_form_button_create').click(function() {
4124 self.save().done(self.on_button_new);
4127 do_notify_change: function() {
4128 if (this.dataset.parent_view) {
4129 this.dataset.parent_view.do_notify_change();
4131 this._super.apply(this, arguments);
4136 var lazy_build_o2m_kanban_view = function() {
4137 if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
4139 instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4143 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4144 template: "FieldMany2ManyTags",
4146 this._super.apply(this, arguments);
4147 instance.web.form.CompletionFieldMixin.init.call(this);
4148 this.set({"value": []});
4149 this._display_orderer = new instance.web.DropMisordered();
4150 this._drop_shown = false;
4152 initialize_content: function() {
4153 if (this.get("effective_readonly"))
4156 var ignore_blur = false;
4157 self.$text = this.$("textarea");
4158 self.$text.textext({
4159 plugins : 'tags arrow autocomplete',
4161 render: function(suggestion) {
4162 return $('<span class="text-label"/>').
4163 data('index', suggestion['index']).html(suggestion['label']);
4168 selectFromDropdown: function() {
4169 this.trigger('hideDropdown');
4170 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4171 var data = self.search_result[index];
4173 self.add_id(data.id);
4178 this.trigger('setSuggestions', {result : []});
4182 isTagAllowed: function(tag) {
4186 removeTag: function(tag) {
4187 var id = tag.data("id");
4188 self.set({"value": _.without(self.get("value"), id)});
4190 renderTag: function(stuff) {
4191 return $.fn.textext.TextExtTags.prototype.renderTag.
4192 call(this, stuff).data("id", stuff.id);
4196 itemToString: function(item) {
4201 onSetInputData: function(e, data) {
4203 this._plugins.autocomplete._suggestions = null;
4205 this.input().val(data);
4209 }).bind('getSuggestions', function(e, data) {
4211 var str = !!data ? data.query || '' : '';
4212 self.get_search_result(str).done(function(result) {
4213 self.search_result = result;
4214 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4215 return _.extend(el, {index:i});
4218 }).bind('hideDropdown', function() {
4219 self._drop_shown = false;
4220 }).bind('showDropdown', function() {
4221 self._drop_shown = true;
4223 self.tags = self.$text.textext()[0].tags();
4225 .focusin(function () {
4226 self.trigger('focused');
4227 ignore_blur = false;
4229 .focusout(function() {
4230 self.$text.trigger("setInputData", "");
4232 self.trigger('blurred');
4234 }).keydown(function(e) {
4235 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4236 self.$text.textext()[0].autocomplete().selectFromDropdown();
4240 set_value: function(value_) {
4241 value_ = value_ || [];
4242 if (value_.length >= 1 && value_[0] instanceof Array) {
4243 value_ = value_[0][2];
4245 this._super(value_);
4247 is_false: function() {
4248 return _(this.get("value")).isEmpty();
4250 get_value: function() {
4251 var tmp = [commands.replace_with(this.get("value"))];
4254 get_search_blacklist: function() {
4255 return this.get("value");
4257 render_value: function() {
4259 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4260 var values = self.get("value");
4261 var handle_names = function(data) {
4262 if (self.isDestroyed())
4265 _.each(data, function(el) {
4266 indexed[el[0]] = el;
4268 data = _.map(values, function(el) { return indexed[el]; });
4269 if (! self.get("effective_readonly")) {
4270 self.tags.containerElement().children().remove();
4271 self.$('textarea').css("padding-left", "3px");
4272 self.tags.addTags(_.map(data, function(el) {return {name: el[1], id:el[0]};}));
4274 self.$el.html(QWeb.render("FieldMany2ManyTag", {elements: data}));
4277 if (! values || values.length > 0) {
4278 return this._display_orderer.add(dataset.name_get(values)).done(handle_names);
4283 add_id: function(id) {
4284 this.set({'value': _.uniq(this.get('value').concat([id]))});
4286 focus: function () {
4287 var input = this.$text && this.$text[0];
4288 return input ? input.focus() : false;
4294 - reload_on_button: Reload the whole form view if click on a button in a list view.
4295 If you see this options, do not use it, it's basically a dirty hack to make one
4296 precise o2m to behave the way we want.
4298 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4299 multi_selection: false,
4300 init: function(field_manager, node) {
4301 this._super(field_manager, node);
4302 this.is_loaded = $.Deferred();
4303 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4304 this.dataset.m2m = this;
4306 this.dataset.on('unlink', self, function(ids) {
4307 self.dataset_changed();
4310 this.list_dm = new instance.web.DropMisordered();
4311 this.render_value_dm = new instance.web.DropMisordered();
4313 initialize_content: function() {
4316 this.$el.addClass('oe_form_field oe_form_field_many2many');
4318 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4319 'addable': this.get("effective_readonly") ? null : _t("Add"),
4320 'deletable': this.get("effective_readonly") ? false : true,
4321 'selectable': this.multi_selection,
4323 'reorderable': false,
4324 'import_enabled': false,
4326 var embedded = (this.field.views || {}).tree;
4328 this.list_view.set_embedded_view(embedded);
4330 this.list_view.m2m_field = this;
4331 var loaded = $.Deferred();
4332 this.list_view.on("list_view_loaded", this, function() {
4335 this.list_view.appendTo(this.$el);
4337 var old_def = self.is_loaded;
4338 self.is_loaded = $.Deferred().done(function() {
4341 this.list_dm.add(loaded).then(function() {
4342 self.is_loaded.resolve();
4345 destroy_content: function() {
4346 this.list_view.destroy();
4347 this.list_view = undefined;
4349 set_value: function(value_) {
4350 value_ = value_ || [];
4351 if (value_.length >= 1 && value_[0] instanceof Array) {
4352 value_ = value_[0][2];
4354 this._super(value_);
4356 get_value: function() {
4357 return [commands.replace_with(this.get('value'))];
4359 is_false: function () {
4360 return _(this.get("value")).isEmpty();
4362 render_value: function() {
4364 this.dataset.set_ids(this.get("value"));
4365 this.render_value_dm.add(this.is_loaded).then(function() {
4366 return self.list_view.reload_content();
4369 dataset_changed: function() {
4370 this.internal_set_value(this.dataset.ids);
4374 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4375 get_context: function() {
4376 this.context = this.m2m.build_context();
4377 return this.context;
4383 * @extends instance.web.ListView
4385 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4386 do_add_record: function () {
4387 var pop = new instance.web.form.SelectCreatePopup(this);
4391 title: _t("Add: ") + this.m2m_field.string,
4392 no_create: this.m2m_field.options.no_create,
4394 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4395 this.m2m_field.build_context()
4398 pop.on("elements_selected", self, function(element_ids) {
4400 _(element_ids).each(function (id) {
4401 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4402 self.dataset.set_ids(self.dataset.ids.concat([id]));
4403 self.m2m_field.dataset_changed();
4408 self.reload_content();
4412 do_activate_record: function(index, id) {
4414 var pop = new instance.web.form.FormOpenPopup(this);
4415 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4416 title: _t("Open: ") + this.m2m_field.string,
4417 readonly: this.getParent().get("effective_readonly")
4419 pop.on('write_completed', self, self.reload_content);
4421 do_button_action: function(name, id, callback) {
4423 var _sup = _.bind(this._super, this);
4424 if (! this.m2m_field.options.reload_on_button) {
4425 return _sup(name, id, callback);
4427 return this.m2m_field.view.save().then(function() {
4428 return _sup(name, id, function() {
4429 self.m2m_field.view.reload();
4434 is_action_enabled: function () { return true; },
4437 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4438 disable_utility_classes: true,
4439 init: function(field_manager, node) {
4440 this._super(field_manager, node);
4441 instance.web.form.CompletionFieldMixin.init.call(this);
4442 m2m_kanban_lazy_init();
4443 this.is_loaded = $.Deferred();
4444 this.initial_is_loaded = this.is_loaded;
4447 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4448 this.dataset.m2m = this;
4449 this.dataset.on('unlink', self, function(ids) {
4450 self.dataset_changed();
4454 this._super.apply(this, arguments);
4459 self.on("change:effective_readonly", self, function() {
4460 self.is_loaded = self.is_loaded.then(function() {
4461 self.kanban_view.destroy();
4462 return $.when(self.load_view()).done(function() {
4463 self.render_value();
4468 set_value: function(value_) {
4469 value_ = value_ || [];
4470 if (value_.length >= 1 && value_[0] instanceof Array) {
4471 value_ = value_[0][2];
4473 this._super(value_);
4475 get_value: function() {
4476 return [commands.replace_with(this.get('value'))];
4478 load_view: function() {
4480 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4481 'create_text': _t("Add"),
4482 'creatable': self.get("effective_readonly") ? false : true,
4483 'quick_creatable': self.get("effective_readonly") ? false : true,
4484 'read_only_mode': self.get("effective_readonly") ? true : false,
4485 'confirm_on_delete': false,
4487 var embedded = (this.field.views || {}).kanban;
4489 this.kanban_view.set_embedded_view(embedded);
4491 this.kanban_view.m2m = this;
4492 var loaded = $.Deferred();
4493 this.kanban_view.on("kanban_view_loaded",self,function() {
4494 self.initial_is_loaded.resolve();
4497 this.kanban_view.on('switch_mode', this, this.open_popup);
4498 $.async_when().done(function () {
4499 self.kanban_view.appendTo(self.$el);
4503 render_value: function() {
4505 this.dataset.set_ids(this.get("value"));
4506 this.is_loaded = this.is_loaded.then(function() {
4507 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
4510 dataset_changed: function() {
4511 this.set({'value': this.dataset.ids});
4513 open_popup: function(type, unused) {
4514 if (type !== "form")
4517 if (this.dataset.index === null) {
4518 var pop = new instance.web.form.SelectCreatePopup(this);
4520 this.field.relation,
4522 title: _t("Add: ") + this.string
4524 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
4525 this.build_context()
4527 pop.on("elements_selected", self, function(element_ids) {
4528 _.each(element_ids, function(one_id) {
4529 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
4530 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
4531 self.dataset_changed();
4532 self.render_value();
4537 var id = self.dataset.ids[self.dataset.index];
4538 var pop = new instance.web.form.FormOpenPopup(this);
4539 pop.show_element(self.field.relation, id, self.build_context(), {
4540 title: _t("Open: ") + self.string,
4541 write_function: function(id, data, options) {
4542 return self.dataset.write(id, data, {}).done(function() {
4543 self.render_value();
4546 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
4547 parent_view: self.view,
4548 child_name: self.name,
4549 readonly: self.get("effective_readonly")
4553 add_id: function(id) {
4554 this.quick_create.add_id(id);
4558 function m2m_kanban_lazy_init() {
4559 if (instance.web.form.Many2ManyKanbanView)
4561 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4562 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
4563 _is_quick_create_enabled: function() {
4564 return this._super() && ! this.group_by;
4567 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
4568 template: 'Many2ManyKanban.quick_create',
4571 * close_btn: If true, the widget will display a "Close" button able to trigger
4574 init: function(parent, dataset, context, buttons) {
4575 this._super(parent);
4576 this.m2m = this.getParent().view.m2m;
4577 this.m2m.quick_create = this;
4578 this._dataset = dataset;
4579 this._buttons = buttons || false;
4580 this._context = context || {};
4582 start: function () {
4584 self.$text = this.$el.find('input').css("width", "200px");
4585 self.$text.textext({
4586 plugins : 'arrow autocomplete',
4588 render: function(suggestion) {
4589 return $('<span class="text-label"/>').
4590 data('index', suggestion['index']).html(suggestion['label']);
4595 selectFromDropdown: function() {
4596 $(this).trigger('hideDropdown');
4597 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4598 var data = self.search_result[index];
4600 self.add_id(data.id);
4607 itemToString: function(item) {
4612 }).bind('getSuggestions', function(e, data) {
4614 var str = !!data ? data.query || '' : '';
4615 self.m2m.get_search_result(str).done(function(result) {
4616 self.search_result = result;
4617 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4618 return _.extend(el, {index:i});
4622 self.$text.focusout(function() {
4627 this.$text[0].focus();
4629 add_id: function(id) {
4632 self.trigger('added', id);
4633 this.m2m.dataset_changed();
4639 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
4641 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
4642 template: "AbstractFormPopup.render",
4645 * -readonly: only applicable when not in creation mode, default to false
4646 * - alternative_form_view
4653 * - form_view_options
4655 init_popup: function(model, row_id, domain, context, options) {
4656 this.row_id = row_id;
4658 this.domain = domain || [];
4659 this.context = context || {};
4660 this.options = options;
4661 _.defaults(this.options, {
4664 init_dataset: function() {
4666 this.created_elements = [];
4667 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
4668 this.dataset.read_function = this.options.read_function;
4669 this.dataset.create_function = function(data, options, sup) {
4670 var fct = self.options.create_function || sup;
4671 return fct.call(this, data, options).done(function(r) {
4672 self.trigger('create_completed saved', r);
4673 self.created_elements.push(r);
4676 this.dataset.write_function = function(id, data, options, sup) {
4677 var fct = self.options.write_function || sup;
4678 return fct.call(this, id, data, options).done(function(r) {
4679 self.trigger('write_completed saved', r);
4682 this.dataset.parent_view = this.options.parent_view;
4683 this.dataset.child_name = this.options.child_name;
4685 display_popup: function() {
4687 this.renderElement();
4688 var dialog = new instance.web.Dialog(this, {
4690 dialogClass: 'oe_act_window',
4692 self.check_exit(true);
4694 title: this.options.title || "",
4695 }, this.$el).open();
4696 this.$buttonpane = dialog.$buttons;
4699 setup_form_view: function() {
4702 this.dataset.ids = [this.row_id];
4703 this.dataset.index = 0;
4705 this.dataset.index = null;
4707 var options = _.clone(self.options.form_view_options) || {};
4708 if (this.row_id !== null) {
4709 options.initial_mode = this.options.readonly ? "view" : "edit";
4712 $buttons: this.$buttonpane,
4714 this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
4715 if (this.options.alternative_form_view) {
4716 this.view_form.set_embedded_view(this.options.alternative_form_view);
4718 this.view_form.appendTo(this.$el.find(".oe_popup_form"));
4719 this.view_form.on("form_view_loaded", self, function() {
4720 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
4721 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
4722 multi_select: multi_select,
4723 readonly: self.row_id !== null && self.options.readonly,
4725 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
4726 $snbutton.click(function() {
4727 $.when(self.view_form.save()).done(function() {
4728 self.view_form.reload_mutex.exec(function() {
4729 self.view_form.on_button_new();
4733 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
4734 $sbutton.click(function() {
4735 $.when(self.view_form.save()).done(function() {
4736 self.view_form.reload_mutex.exec(function() {
4741 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
4742 $cbutton.click(function() {
4743 self.view_form.trigger('on_button_cancel');
4746 self.view_form.do_show();
4749 select_elements: function(element_ids) {
4750 this.trigger("elements_selected", element_ids);
4752 check_exit: function(no_destroy) {
4753 if (this.created_elements.length > 0) {
4754 this.select_elements(this.created_elements);
4755 this.created_elements = [];
4757 this.trigger('closed');
4760 destroy: function () {
4761 this.trigger('closed');
4762 if (this.$el.is(":data(dialog)")) {
4763 this.$el.dialog('close');
4770 * Class to display a popup containing a form view.
4772 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
4773 show_element: function(model, row_id, context, options) {
4774 this.init_popup(model, row_id, [], context, options);
4775 _.defaults(this.options, {
4777 this.display_popup();
4781 this.init_dataset();
4782 this.setup_form_view();
4787 * Class to display a popup to display a list to search a row. It also allows
4788 * to switch to a form view to create a new row.
4790 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
4794 * - initial_view: form or search (default search)
4795 * - disable_multiple_selection
4796 * - list_view_options
4798 select_element: function(model, options, domain, context) {
4799 this.init_popup(model, null, domain, context, options);
4801 _.defaults(this.options, {
4802 initial_view: "search",
4804 this.initial_ids = this.options.initial_ids;
4805 this.display_popup();
4809 this.init_dataset();
4810 if (this.options.initial_view == "search") {
4811 instance.web.pyeval.eval_domains_and_contexts({
4813 contexts: [this.context]
4814 }).done(function (results) {
4815 var search_defaults = {};
4816 _.each(results.context, function (value_, key) {
4817 var match = /^search_default_(.*)$/.exec(key);
4819 search_defaults[match[1]] = value_;
4822 self.setup_search_view(search_defaults);
4828 setup_search_view: function(search_defaults) {
4830 if (this.searchview) {
4831 this.searchview.destroy();
4833 this.searchview = new instance.web.SearchView(this,
4834 this.dataset, false, search_defaults);
4835 this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
4836 if (self.initial_ids) {
4837 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
4838 contexts.concat(self.context), groupbys);
4839 self.initial_ids = undefined;
4841 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
4844 this.searchview.on("search_view_loaded", self, function() {
4845 self.view_list = new instance.web.form.SelectCreateListView(self,
4846 self.dataset, false,
4847 _.extend({'deletable': false,
4848 'selectable': !self.options.disable_multiple_selection,
4849 'import_enabled': false,
4850 '$buttons': self.$buttonpane,
4851 'disable_editable_mode': true,
4852 '$pager': self.$('.oe_popup_list_pager'),
4853 }, self.options.list_view_options || {}));
4854 self.view_list.on('edit:before', self, function (e) {
4857 self.view_list.popup = self;
4858 self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
4859 self.view_list.do_show();
4860 }).then(function() {
4861 self.searchview.do_search();
4863 self.view_list.on("list_view_loaded", self, function() {
4864 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
4865 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
4866 $cbutton.click(function() {
4869 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
4870 $sbutton.click(function() {
4871 self.select_elements(self.selected_ids);
4874 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
4875 $cbutton.click(function() {
4880 this.searchview.appendTo($(".oe_popup_search", self.$el));
4882 do_search: function(domains, contexts, groupbys) {
4884 instance.web.pyeval.eval_domains_and_contexts({
4885 domains: domains || [],
4886 contexts: contexts || [],
4887 group_by_seq: groupbys || []
4888 }).done(function (results) {
4889 self.view_list.do_search(results.domain, results.context, results.group_by);
4892 on_click_element: function(ids) {
4894 this.selected_ids = ids || [];
4895 if(this.selected_ids.length > 0) {
4896 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
4898 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
4901 new_object: function() {
4902 if (this.searchview) {
4903 this.searchview.hide();
4905 if (this.view_list) {
4906 this.view_list.do_hide();
4908 this.setup_form_view();
4912 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
4913 do_add_record: function () {
4914 this.popup.new_object();
4916 select_record: function(index) {
4917 this.popup.select_elements([this.dataset.ids[index]]);
4918 this.popup.destroy();
4920 do_select: function(ids, records) {
4921 this._super(ids, records);
4922 this.popup.on_click_element(ids);
4926 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4927 template: 'FieldReference',
4928 init: function(field_manager, node) {
4929 this._super(field_manager, node);
4930 this.reference_ready = true;
4932 destroy_content: function() {
4935 this.fm = undefined;
4938 initialize_content: function() {
4940 var fm = new instance.web.form.DefaultFieldManager(this);
4942 fm.extend_field_desc({
4944 selection: this.field_manager.get_field_desc(this.name).selection,
4952 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
4954 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
4956 this.selection.on("change:value", this, this.on_selection_changed);
4957 this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
4959 .on('focused', null, function () {self.trigger('focused')})
4960 .on('blurred', null, function () {self.trigger('blurred')});
4962 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
4963 name: 'Referenced Document',
4964 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
4966 this.m2o.on("change:value", this, this.data_changed);
4967 this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
4969 .on('focused', null, function () {self.trigger('focused')})
4970 .on('blurred', null, function () {self.trigger('blurred')});
4972 on_selection_changed: function() {
4973 if (this.reference_ready) {
4974 this.internal_set_value([this.selection.get_value(), false]);
4975 this.render_value();
4978 data_changed: function() {
4979 if (this.reference_ready) {
4980 this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
4983 set_value: function(val) {
4985 val = val.split(',');
4986 val[0] = val[0] || false;
4987 val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
4989 this._super(val || [false, false]);
4991 get_value: function() {
4992 return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
4994 render_value: function() {
4995 this.reference_ready = false;
4996 if (!this.get("effective_readonly")) {
4997 this.selection.set_value(this.get('value')[0]);
4999 this.m2o.field.relation = this.get('value')[0];
5000 this.m2o.set_value(this.get('value')[1]);
5001 this.m2o.$el.toggle(!!this.get('value')[0]);
5002 this.reference_ready = true;
5006 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5007 init: function(field_manager, node) {
5009 this._super(field_manager, node);
5010 this.binary_value = false;
5011 this.useFileAPI = !!window.FileReader;
5012 this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5013 if (!this.useFileAPI) {
5014 this.fileupload_id = _.uniqueId('oe_fileupload');
5015 $(window).on(this.fileupload_id, function() {
5016 var args = [].slice.call(arguments).slice(1);
5017 self.on_file_uploaded.apply(self, args);
5022 if (!this.useFileAPI) {
5023 $(window).off(this.fileupload_id);
5025 this._super.apply(this, arguments);
5027 initialize_content: function() {
5028 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5029 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5030 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5032 on_file_change: function(e) {
5034 var file_node = e.target;
5035 if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5036 if (this.useFileAPI) {
5037 var file = file_node.files[0];
5038 if (file.size > this.max_upload_size) {
5039 var msg = _t("The selected file exceed the maximum file size of %s.");
5040 instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5043 var filereader = new FileReader();
5044 filereader.readAsDataURL(file);
5045 filereader.onloadend = function(upload) {
5046 var data = upload.target.result;
5047 data = data.split(',')[1];
5048 self.on_file_uploaded(file.size, file.name, file.type, data);
5051 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5052 this.$el.find('form.oe_form_binary_form').submit();
5054 this.$el.find('.oe_form_binary_progress').show();
5055 this.$el.find('.oe_form_binary').hide();
5058 on_file_uploaded: function(size, name, content_type, file_base64) {
5059 if (size === false) {
5060 this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5061 // TODO: use openerp web crashmanager
5062 console.warn("Error while uploading file : ", name);
5064 this.filename = name;
5065 this.on_file_uploaded_and_valid.apply(this, arguments);
5067 this.$el.find('.oe_form_binary_progress').hide();
5068 this.$el.find('.oe_form_binary').show();
5070 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5072 on_save_as: function(ev) {
5073 var value = this.get('value');
5075 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5076 ev.stopPropagation();
5078 instance.web.blockUI();
5079 var c = instance.webclient.crashmanager;
5080 this.session.get_file({
5081 url: '/web/binary/saveas_ajax',
5082 data: {data: JSON.stringify({
5083 model: this.view.dataset.model,
5084 id: (this.view.datarecord.id || ''),
5086 filename_field: (this.node.attrs.filename || ''),
5087 data: instance.web.form.is_bin_size(value) ? null : value,
5088 context: this.view.dataset.get_context()
5090 complete: instance.web.unblockUI,
5091 error: c.rpc_error.bind(c)
5093 ev.stopPropagation();
5097 set_filename: function(value) {
5098 var filename = this.node.attrs.filename;
5101 tmp[filename] = value;
5102 this.field_manager.set_values(tmp);
5105 on_clear: function() {
5106 if (this.get('value') !== false) {
5107 this.binary_value = false;
5108 this.internal_set_value(false);
5114 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5115 template: 'FieldBinaryFile',
5116 initialize_content: function() {
5118 if (this.get("effective_readonly")) {
5120 this.$el.find('a').click(function(ev) {
5121 if (self.get('value')) {
5122 self.on_save_as(ev);
5128 render_value: function() {
5129 if (!this.get("effective_readonly")) {
5131 if (this.node.attrs.filename) {
5132 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5134 show_value = (this.get('value') != null && this.get('value') !== false) ? this.get('value') : '';
5136 this.$el.find('input').eq(0).val(show_value);
5138 this.$el.find('a').toggle(!!this.get('value'));
5139 if (this.get('value')) {
5140 var show_value = _t("Download")
5142 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5143 this.$el.find('a').text(show_value);
5147 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5148 this.binary_value = true;
5149 this.internal_set_value(file_base64);
5150 var show_value = name + " (" + instance.web.human_size(size) + ")";
5151 this.$el.find('input').eq(0).val(show_value);
5152 this.set_filename(name);
5154 on_clear: function() {
5155 this._super.apply(this, arguments);
5156 this.$el.find('input').eq(0).val('');
5157 this.set_filename('');
5161 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5162 template: 'FieldBinaryImage',
5163 placeholder: "/web/static/src/img/placeholder.png",
5164 render_value: function() {
5167 if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5168 url = 'data:image/png;base64,' + this.get('value');
5169 } else if (this.get('value')) {
5170 var id = JSON.stringify(this.view.datarecord.id || null);
5171 var field = this.name;
5172 if (this.options.preview_image)
5173 field = this.options.preview_image;
5174 url = this.session.url('/web/binary/image', {
5175 model: this.view.dataset.model,
5178 t: (new Date().getTime()),
5181 url = this.placeholder;
5183 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5184 this.$el.find('> img').remove();
5185 this.$el.prepend($img);
5186 $img.load(function() {
5187 if (! self.options.size)
5189 $img.css("max-width", "" + self.options.size[0] + "px");
5190 $img.css("max-height", "" + self.options.size[1] + "px");
5191 $img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px");
5192 $img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px");
5194 $img.on('error', function() {
5195 $img.attr('src', self.placeholder);
5196 instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5199 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5200 this.internal_set_value(file_base64);
5201 this.binary_value = true;
5202 this.render_value();
5203 this.set_filename(name);
5205 on_clear: function() {
5206 this._super.apply(this, arguments);
5207 this.render_value();
5208 this.set_filename('');
5210 set_value: function(value_){
5211 var changed = value_ !== this.get_value();
5212 this._super.apply(this, arguments);
5213 // By default, on binary images read, the server returns the binary size
5214 // This is possible that two images have the exact same size
5215 // Therefore we trigger the change in case the image value hasn't changed
5216 // So the image is re-rendered correctly
5218 this.trigger("change:value", this, {
5227 * Widget for (one2many field) to upload one or more file in same time and display in list.
5228 * The user can delete his files.
5229 * Options on attribute ; "blockui" {Boolean} block the UI or not
5230 * during the file is uploading
5232 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5233 template: "FieldBinaryFileUploader",
5234 init: function(field_manager, node) {
5235 this._super(field_manager, node);
5236 this.field_manager = field_manager;
5238 if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5239 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);
5241 this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5242 this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5243 $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5245 initialize_content: function() {
5246 this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5248 set_value: function(value_) {
5249 var value_ = value_ || [];
5252 _.each(value_, function(command) {
5253 if (isNaN(command) && command.id == undefined) {
5254 switch (command[0]) {
5255 case commands.CREATE:
5256 ids = ids.concat(command[2]);
5258 case commands.REPLACE_WITH:
5259 ids = ids.concat(command[2]);
5261 case commands.UPDATE:
5262 ids = ids.concat(command[2]);
5264 case commands.LINK_TO:
5265 ids = ids.concat(command[1]);
5267 case commands.DELETE:
5268 ids = _.filter(ids, function (id) { return id != command[1];});
5270 case commands.DELETE_ALL:
5280 get_value: function() {
5281 return _.map(this.get('value'), function (value) { return commands.link_to( isNaN(value) ? value.id : value ); });
5283 get_file_url: function (attachment) {
5284 return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5286 read_name_values : function () {
5288 // select the list of id for a get_name
5290 _.each(this.get('value'), function (val) {
5291 if (typeof val != 'object') {
5295 // send request for get_name
5296 if (values.length) {
5297 return this.ds_file.call('read', [values, ['id', 'name', 'datas_fname']]).done(function (datas) {
5298 _.each(datas, function (data) {
5299 data.no_unlink = true;
5300 data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5302 _.each(self.get('value'), function (val, key) {
5303 if(val == data.id) {
5304 self.get('value')[key] = data;
5310 return $.when(this.get('value'));
5313 render_value: function () {
5315 this.read_name_values().then(function (datas) {
5317 var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self}));
5318 render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5319 self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5321 // reinit input type file
5322 var $input = self.$('input.oe_form_binary_file');
5323 $input.after($input.clone(true)).remove();
5324 self.$(".oe_fileupload").show();
5328 on_file_change: function (event) {
5329 event.stopPropagation();
5331 var $target = $(event.target);
5332 if ($target.val() !== '') {
5334 var filename = $target.val().replace(/.*[\\\/]/,'');
5336 // if the files is currently uploded, don't send again
5337 if( !isNaN(_.find(this.get('value'), function (file) { return (file.filename || file.name) == filename && file.upload; } )) ) {
5342 if(this.node.attrs.blockui>0) {
5343 instance.web.blockUI();
5346 // if the files exits for this answer, delete the file before upload
5347 var files = _.filter(this.get('value'), function (file) {
5348 if((file.filename || file.name) == filename) {
5349 self.ds_file.unlink([file.id]);
5356 // TODO : unactivate send on wizard and form
5359 this.$('form.oe_form_binary_form').submit();
5360 this.$(".oe_fileupload").hide();
5362 // add file on result
5366 'filename': filename,
5371 this.set({'value': files});
5374 on_file_loaded: function (event, result) {
5375 var files = this.get('value');
5378 if(this.node.attrs.blockui>0) {
5379 instance.web.unblockUI();
5382 // TODO : activate send on wizard and form
5384 if (result.error || !result.id ) {
5385 this.do_warn( _t('Uploading Error'), result.error);
5386 files = _.filter(files, function (val) { return !val.upload; });
5388 for(var i in files){
5389 if(files[i].filename == result.filename && files[i].upload) {
5392 'name': result.name,
5393 'filename': result.filename,
5394 'url': this.get_file_url(result)
5400 this.set({'value': files});
5403 on_file_delete: function (event) {
5404 event.stopPropagation();
5405 var file_id=$(event.target).data("id");
5408 for(var i in this.get('value')){
5409 if(file_id != this.get('value')[i].id){
5410 files.push(this.get('value')[i]);
5412 else if(!this.get('value')[i].no_unlink) {
5413 this.ds_file.unlink([file_id]);
5416 this.set({'value': files});
5421 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5422 template: "FieldStatus",
5423 init: function(field_manager, node) {
5424 this._super(field_manager, node);
5425 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5426 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5427 this.set({value: false});
5428 this.selection = [];
5429 this.set("selection", []);
5430 this.selection_dm = new instance.web.DropMisordered();
5433 this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5435 this.on("change:value", this, this.get_selection);
5436 this.on("change:evaluated_selection_domain", this, this.get_selection);
5437 this.on("change:selection", this, function() {
5438 this.selection = this.get("selection");
5439 this.render_value();
5441 this.get_selection();
5442 if (this.options.clickable) {
5443 this.$el.on('click','li',this.on_click_stage);
5447 set_value: function(value_) {
5448 if (value_ instanceof Array) {
5451 this._super(value_);
5453 render_value: function() {
5455 var content = QWeb.render("FieldStatus.content", {widget: self});
5456 self.$el.html(content);
5457 var colors = JSON.parse((self.node.attrs || {}).statusbar_colors || "{}");
5458 var color = colors[self.get('value')];
5460 self.$("oe_active").css("color", color);
5463 calc_domain: function() {
5464 var d = instance.web.pyeval.eval('domain', this.build_domain());
5465 var domain = []; //if there is no domain defined, fetch all the records
5468 domain = ['|',['id', '=', this.get('value')]].concat(d);
5471 if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5472 this.set("evaluated_selection_domain", domain);
5475 /** Get the selection and render it
5476 * selection: [[identifier, value_to_display], ...]
5477 * For selection fields: this is directly given by this.field.selection
5478 * For many2one fields: perform a search on the relation of the many2one field
5480 get_selection: function() {
5484 var calculation = _.bind(function() {
5485 if (this.field.type == "many2one") {
5487 var ds = new instance.web.DataSetSearch(this, this.field.relation,
5488 self.build_context(), this.get("evaluated_selection_domain"));
5489 return ds.read_slice(['name'], {}).then(function (records) {
5490 for(var i = 0; i < records.length; i++) {
5491 selection.push([records[i].id, records[i].name]);
5495 // For field type selection filter values according to
5496 // statusbar_visible attribute of the field. For example:
5497 // statusbar_visible="draft,open".
5498 var select = this.field.selection;
5499 for(var i=0; i < select.length; i++) {
5500 var key = select[i][0];
5501 if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5502 selection.push(select[i]);
5508 this.selection_dm.add(calculation()).then(function () {
5509 if (! _.isEqual(selection, self.get("selection"))) {
5510 self.set("selection", selection);
5514 on_click_stage: function (ev) {
5516 var $li = $(ev.currentTarget);
5517 if (this.field.type == "many2one") {
5518 var val = parseInt($li.data("id"));
5521 var val = $li.data("id");
5523 if (val != self.get('value')) {
5524 this.view.recursive_save().done(function() {
5526 change[self.name] = val;
5527 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
5535 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
5536 template: "FieldMonetary",
5537 widget_class: 'oe_form_field_float oe_form_field_monetary',
5539 this._super.apply(this, arguments);
5540 this.set({"currency": false});
5541 if (this.options.currency_field) {
5542 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
5543 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
5546 this.on("change:currency", this, this.get_currency_info);
5547 this.get_currency_info();
5548 this.ci_dm = new instance.web.DropMisordered();
5551 var tmp = this._super();
5552 this.on("change:currency_info", this, this.reinitialize);
5555 get_currency_info: function() {
5557 if (this.get("currency") === false) {
5558 this.set({"currency_info": null});
5561 return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
5562 .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
5563 self.set({"currency_info": res});
5566 parse_value: function(val, def) {
5567 return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5569 format_value: function(val, def) {
5570 return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5575 * Registry of form fields, called by :js:`instance.web.FormView`.
5577 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
5578 * will substitute to the <field> tags as defined in OpenERP's views.
5580 instance.web.form.widgets = new instance.web.Registry({
5581 'char' : 'instance.web.form.FieldChar',
5582 'id' : 'instance.web.form.FieldID',
5583 'email' : 'instance.web.form.FieldEmail',
5584 'url' : 'instance.web.form.FieldUrl',
5585 'text' : 'instance.web.form.FieldText',
5586 'html' : 'instance.web.form.FieldTextHtml',
5587 'date' : 'instance.web.form.FieldDate',
5588 'datetime' : 'instance.web.form.FieldDatetime',
5589 'selection' : 'instance.web.form.FieldSelection',
5590 'many2one' : 'instance.web.form.FieldMany2One',
5591 'many2onebutton' : 'instance.web.form.Many2OneButton',
5592 'many2many' : 'instance.web.form.FieldMany2Many',
5593 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
5594 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
5595 'one2many' : 'instance.web.form.FieldOne2Many',
5596 'one2many_list' : 'instance.web.form.FieldOne2Many',
5597 'reference' : 'instance.web.form.FieldReference',
5598 'boolean' : 'instance.web.form.FieldBoolean',
5599 'float' : 'instance.web.form.FieldFloat',
5600 'integer': 'instance.web.form.FieldFloat',
5601 'float_time': 'instance.web.form.FieldFloat',
5602 'progressbar': 'instance.web.form.FieldProgressBar',
5603 'image': 'instance.web.form.FieldBinaryImage',
5604 'binary': 'instance.web.form.FieldBinaryFile',
5605 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
5606 'statusbar': 'instance.web.form.FieldStatus',
5607 'monetary': 'instance.web.form.FieldMonetary',
5611 * Registry of widgets usable in the form view that can substitute to any possible
5612 * tags defined in OpenERP's form views.
5614 * Every referenced class should extend FormWidget.
5616 instance.web.form.tags = new instance.web.Registry({
5617 'button' : 'instance.web.form.WidgetButton',
5620 instance.web.form.custom_widgets = new instance.web.Registry({
5625 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: