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.reload_mutex = new $.Mutex();
121 this.__clicked_inside = false;
122 this.__blur_timeout = null;
123 this.rendering_engine = new instance.web.form.FormRenderingEngine(this);
124 self.set({actual_mode: self.options.initial_mode});
125 this.has_been_loaded.done(function() {
126 self.on("change:actual_mode", self, self.check_actual_mode);
127 self.check_actual_mode();
128 self.on("change:actual_mode", self, self.init_pager);
131 self.on("load_record", self, self.load_record);
132 instance.web.bus.on('clear_uncommitted_changes', this, function(e) {
133 if (!this.can_be_discarded()) {
138 view_loading: function(r) {
139 return this.load_form(r);
141 destroy: function() {
142 _.each(this.get_widgets(), function(w) {
143 w.off('focused blurred');
147 this.$el.off('.formBlur');
151 load_form: function(data) {
154 throw new Error(_t("No data provided."));
157 throw "Form view does not support multiple calls to load_form";
159 this.fields_order = [];
160 this.fields_view = data;
162 this.rendering_engine.set_fields_registry(this.fields_registry);
163 this.rendering_engine.set_tags_registry(this.tags_registry);
164 this.rendering_engine.set_widgets_registry(this.widgets_registry);
165 this.rendering_engine.set_fields_view(data);
166 var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container');
167 this.rendering_engine.render_to($dest);
169 this.$el.on('mousedown.formBlur', function () {
170 self.__clicked_inside = true;
173 this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self}));
174 if (this.options.$buttons) {
175 this.$buttons.appendTo(this.options.$buttons);
177 this.$el.find('.oe_form_buttons').replaceWith(this.$buttons);
179 this.$buttons.on('click', '.oe_form_button_create',
180 this.guard_active(this.on_button_create));
181 this.$buttons.on('click', '.oe_form_button_edit',
182 this.guard_active(this.on_button_edit));
183 this.$buttons.on('click', '.oe_form_button_save',
184 this.guard_active(this.on_button_save));
185 this.$buttons.on('click', '.oe_form_button_cancel',
186 this.guard_active(this.on_button_cancel));
187 if (this.options.footer_to_buttons) {
188 this.$el.find('footer').appendTo(this.$buttons);
191 this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar');
192 if (!this.sidebar && this.options.$sidebar) {
193 this.sidebar = new instance.web.Sidebar(this);
194 this.sidebar.appendTo(this.$sidebar);
195 if (this.fields_view.toolbar) {
196 this.sidebar.add_toolbar(this.fields_view.toolbar);
198 this.sidebar.add_items('other', _.compact([
199 self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete },
200 self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate }
204 this.has_been_loaded.resolve();
206 // Add bounce effect on button 'Edit' when click on readonly page view.
207 this.$el.find(".oe_form_group_row,.oe_form_field,label").on('click', function (e) {
208 if(self.get("actual_mode") == "view") {
209 var $button = self.options.$buttons.find(".oe_form_button_edit");
210 $button.openerpBounce();
212 instance.web.bus.trigger('click', e);
215 //bounce effect on red button when click on statusbar.
216 this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) {
217 if((self.get("actual_mode") == "view")) {
218 var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'});
219 $button.openerpBounce();
223 this.trigger('form_view_loaded', data);
226 widgetFocused: function() {
227 // Clear click flag if used to focus a widget
228 this.__clicked_inside = false;
229 if (this.__blur_timeout) {
230 clearTimeout(this.__blur_timeout);
231 this.__blur_timeout = null;
234 widgetBlurred: function() {
235 if (this.__clicked_inside) {
236 // clicked in an other section of the form (than the currently
237 // focused widget) => just ignore the blurring entirely?
238 this.__clicked_inside = false;
242 // clear timeout, if any
243 this.widgetFocused();
244 this.__blur_timeout = setTimeout(function () {
245 self.trigger('blurred');
249 do_load_state: function(state, warm) {
250 if (state.id && this.datarecord.id != state.id) {
251 if (this.dataset.get_id_index(state.id) === null) {
252 this.dataset.ids.push(state.id);
254 this.dataset.select_id(state.id);
255 this.do_show({ reload: warm });
260 * @param {Object} [options]
261 * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view".
262 * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record
263 * @return {$.Deferred}
265 do_show: function (options) {
267 options = options || {};
269 this.sidebar.$el.show();
272 this.$buttons.show();
274 this.$el.show().css({
276 filter: 'alpha(opacity = 0)'
278 this.$el.add(this.$buttons).removeClass('oe_form_dirty');
280 var shown = this.has_been_loaded;
281 if (options.reload !== false) {
282 shown = shown.then(function() {
283 if (self.dataset.index === null) {
284 // null index means we should start a new record
285 return self.on_button_new();
287 var fields = _.keys(self.fields_view.fields);
288 fields.push('display_name');
289 return self.dataset.read_index(fields, {
290 context: { 'bin_size': true, 'future_display_name' : true }
291 }).then(function(r) {
292 self.trigger('load_record', r);
296 return shown.then(function() {
297 self._actualize_mode(options.mode || self.options.initial_mode);
300 filter: 'alpha(opacity = 100)'
304 do_hide: function () {
306 this.sidebar.$el.hide();
309 this.$buttons.hide();
316 load_record: function(record) {
317 var self = this, set_values = [];
319 this.set({ 'title' : undefined });
320 this.do_warn(_t("Form"), _t("The record could not be found in the database."), true);
321 return $.Deferred().reject();
323 this.datarecord = record;
324 this._actualize_mode();
325 this.set({ 'title' : record.id ? record.display_name : _t("New") });
327 _(this.fields).each(function (field, f) {
328 field._dirty_flag = false;
329 field._inhibit_on_change_flag = true;
330 var result = field.set_value(self.datarecord[f] || false);
331 field._inhibit_on_change_flag = false;
332 set_values.push(result);
334 return $.when.apply(null, set_values).then(function() {
336 // New record: Second pass in order to trigger the onchanges
337 // respecting the fields order defined in the view
338 _.each(self.fields_order, function(field_name) {
339 if (record[field_name] !== undefined) {
340 var field = self.fields[field_name];
341 field._dirty_flag = true;
342 self.do_onchange(field);
346 self.on_form_changed();
347 self.rendering_engine.init_fields();
348 self.is_initialized.resolve();
349 self.do_update_pager(record.id == null);
351 self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
354 self.do_push_state({id:record.id});
356 self.do_push_state({});
358 self.$el.add(self.$buttons).removeClass('oe_form_dirty');
363 * Loads and sets up the default values for the model as the current
366 * @return {$.Deferred}
368 load_defaults: function () {
370 var keys = _.keys(this.fields_view.fields);
372 return this.dataset.default_get(keys).then(function(r) {
373 self.trigger('load_record', r);
376 return self.trigger('load_record', {});
378 on_form_changed: function() {
379 this.trigger("view_content_has_changed");
381 do_notify_change: function() {
382 this.$el.add(this.$buttons).addClass('oe_form_dirty');
384 execute_pager_action: function(action) {
385 if (this.can_be_discarded()) {
388 this.dataset.index = 0;
391 this.dataset.previous();
397 this.dataset.index = this.dataset.ids.length - 1;
401 this.trigger('pager_action_executed');
404 init_pager: function() {
407 this.$pager.remove();
408 if (this.get("actual_mode") === "create")
410 this.$pager = $(QWeb.render("FormView.pager", {'widget':self})).hide();
411 if (this.options.$pager) {
412 this.$pager.appendTo(this.options.$pager);
414 this.$el.find('.oe_form_pager').replaceWith(this.$pager);
416 this.$pager.on('click','a[data-pager-action]',function() {
417 var action = $(this).data('pager-action');
418 self.execute_pager_action(action);
420 this.do_update_pager();
422 do_update_pager: function(hide_index) {
423 this.$pager.toggle(this.dataset.ids.length > 1);
425 $(".oe_form_pager_state", this.$pager).html("");
427 $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
430 parse_on_change: function (on_change, widget) {
432 var onchange = _.str.trim(on_change);
433 var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
435 throw new Error(_.str.sprintf( _t("Wrong on change format: %s"), onchange ));
438 var method = call[1];
439 if (!_.str.trim(call[2])) {
440 return {method: method, args: []}
443 var argument_replacement = {
444 'False': function () {return false;},
445 'True': function () {return true;},
446 'None': function () {return null;},
447 'context': function () {
448 return new instance.web.CompoundContext(
449 self.dataset.get_context(),
450 widget.build_context() ? widget.build_context() : {});
453 var parent_fields = null;
454 var args = _.map(call[2].split(','), function (a, i) {
455 var field = _.str.trim(a);
457 // literal constant or context
458 if (field in argument_replacement) {
459 return argument_replacement[field]();
462 if (/^-?\d+(\.\d+)?$/.test(field)) {
463 return Number(field);
466 if (self.fields[field]) {
467 var value_ = self.fields[field].get_value();
468 return value_ == null ? false : value_;
471 var splitted = field.split('.');
472 if (splitted.length > 1 && _.str.trim(splitted[0]) === "parent" && self.dataset.parent_view) {
473 if (parent_fields === null) {
474 parent_fields = self.dataset.parent_view.get_fields_values();
476 var p_val = parent_fields[_.str.trim(splitted[1])];
477 if (p_val !== undefined) {
478 return p_val == null ? false : p_val;
482 var first_char = field[0], last_char = field[field.length-1];
483 if ((first_char === '"' && last_char === '"')
484 || (first_char === "'" && last_char === "'")) {
485 return field.slice(1, -1);
488 throw new Error("Could not get field with name '" + field +
489 "' for onchange '" + onchange + "'");
497 do_onchange: function(widget, processed) {
499 this.on_change_list = [{widget: widget, processed: processed}].concat(this.on_change_list);
500 return this._process_operations();
502 _process_onchange: function(on_change_obj) {
504 var widget = on_change_obj.widget;
505 var processed = on_change_obj.processed;
508 processed = processed || [];
509 processed.push(widget.name);
510 var on_change = widget.node.attrs.on_change;
512 var change_spec = self.parse_on_change(on_change, widget);
514 if (self.datarecord.id && !instance.web.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) {
515 // In case of a o2m virtual id, we should pass an empty ids list
516 ids.push(self.datarecord.id);
518 def = self.alive(new instance.web.Model(self.dataset.model).call(
519 change_spec.method, [ids].concat(change_spec.args)));
523 return def.then(function(response) {
524 if (widget.field['change_default']) {
525 var fieldname = widget.name;
527 if (response.value && (fieldname in response.value)) {
528 // Use value from onchange if onchange executed
529 value_ = response.value[fieldname];
531 // otherwise get form value for field
532 value_ = self.fields[fieldname].get_value();
534 var condition = fieldname + '=' + value_;
537 return self.alive(new instance.web.Model('ir.values').call(
538 'get_defaults', [self.model, condition]
539 )).then(function (results) {
540 if (!results.length) {
543 if (!response.value) {
546 for(var i=0; i<results.length; ++i) {
547 // [whatever, key, value]
548 var triplet = results[i];
549 response.value[triplet[1]] = triplet[2];
556 }).then(function(response) {
557 return self.on_processed_onchange(response, processed);
561 instance.webclient.crashmanager.show_message(e);
562 return $.Deferred().reject();
565 on_processed_onchange: function(result, processed) {
568 this._internal_set_values(result.value, processed);
570 if (!_.isEmpty(result.warning)) {
571 instance.web.dialog($(QWeb.render("CrashManager.warning", result.warning)), {
572 title:result.warning.title,
575 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
580 var fields = this.fields;
581 _(result.domain).each(function (domain, fieldname) {
582 var field = fields[fieldname];
583 if (!field) { return; }
584 field.node.attrs.domain = domain;
587 return $.Deferred().resolve();
590 instance.webclient.crashmanager.show_message(e);
591 return $.Deferred().reject();
594 _process_operations: function() {
596 return this.mutating_mutex.exec(function() {
598 var on_change_obj = self.on_change_list.shift();
600 return self._process_onchange(on_change_obj).then(function() {
605 _.each(self.fields, function(field) {
606 defs.push(field.commit_value());
608 var args = _.toArray(arguments);
609 return $.when.apply($, defs).then(function() {
610 if (self.on_change_list.length !== 0) {
613 var save_obj = self.save_list.pop();
615 return self._process_save(save_obj).then(function() {
616 save_obj.ret = _.toArray(arguments);
619 save_obj.error = true;
628 _internal_set_values: function(values, exclude) {
629 exclude = exclude || [];
630 for (var f in values) {
631 if (!values.hasOwnProperty(f)) { continue; }
632 var field = this.fields[f];
633 // If field is not defined in the view, just ignore it
635 var value_ = values[f];
636 if (field.get_value() != value_) {
637 field._inhibit_on_change_flag = true;
638 field.set_value(value_);
639 field._inhibit_on_change_flag = false;
640 field._dirty_flag = true;
641 if (!_.contains(exclude, field.name)) {
642 this.do_onchange(field, exclude);
647 this.on_form_changed();
649 set_values: function(values) {
651 return this.mutating_mutex.exec(function() {
652 self._internal_set_values(values);
656 * Ask the view to switch to view mode if possible. The view may not do it
657 * if the current record is not yet saved. It will then stay in create mode.
659 to_view_mode: function() {
660 this._actualize_mode("view");
663 * Ask the view to switch to edit 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_edit_mode: function() {
667 this._actualize_mode("edit");
670 * Ask the view to switch to a precise mode if possible. The view is free to
671 * not respect this command if the state of the dataset is not compatible with
672 * the new mode. For example, it is not possible to switch to edit mode if
673 * the current record is not yet saved in database.
675 * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
676 * undefined the view will test the actual mode to check if it is still consistent
677 * with the dataset state.
679 _actualize_mode: function(switch_to) {
680 var mode = switch_to || this.get("actual_mode");
681 if (! this.datarecord.id) {
683 } else if (mode === "create") {
686 this.set({actual_mode: mode});
688 check_actual_mode: function(source, options) {
690 if(this.get("actual_mode") === "view") {
691 self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
692 self.$buttons.find('.oe_form_buttons_edit').hide();
693 self.$buttons.find('.oe_form_buttons_view').show();
694 self.$sidebar.show();
696 self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
697 self.$buttons.find('.oe_form_buttons_edit').show();
698 self.$buttons.find('.oe_form_buttons_view').hide();
699 self.$sidebar.hide();
703 autofocus: function() {
704 if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
705 var fields_order = this.fields_order.slice(0);
706 if (this.default_focus_field) {
707 fields_order.unshift(this.default_focus_field.name);
709 for (var i = 0; i < fields_order.length; i += 1) {
710 var field = this.fields[fields_order[i]];
711 if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
712 if (field.focus() !== false) {
719 on_button_save: function() {
721 return this.save().done(function(result) {
722 self.trigger("save", result);
724 }).then(function(result) {
725 var parent = self.ViewManager.ActionManager.getParent();
727 parent.menu.do_reload_needaction();
731 on_button_cancel: function(event) {
732 if (this.can_be_discarded()) {
733 if (this.get('actual_mode') === 'create') {
734 this.trigger('history_back');
737 this.trigger('load_record', this.datarecord);
740 this.trigger('on_button_cancel');
743 on_button_new: function() {
746 return $.when(this.has_been_loaded).then(function() {
747 if (self.can_be_discarded()) {
748 return self.load_defaults();
752 on_button_edit: function() {
753 return this.to_edit_mode();
755 on_button_create: function() {
756 this.dataset.index = null;
759 on_button_duplicate: function() {
761 return this.has_been_loaded.then(function() {
762 return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
763 self.record_created(new_id);
768 on_button_delete: function() {
770 var def = $.Deferred();
771 this.has_been_loaded.done(function() {
772 if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
773 self.dataset.unlink([self.datarecord.id]).done(function() {
774 if (self.dataset.size()) {
775 self.execute_pager_action('next');
777 self.do_action('history_back');
782 $.async_when().done(function () {
787 return def.promise();
789 can_be_discarded: function() {
790 if (this.$el.is('.oe_form_dirty')) {
791 if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
794 this.$el.removeClass('oe_form_dirty');
799 * Triggers saving the form's record. Chooses between creating a new
800 * record or saving an existing one depending on whether the record
801 * already has an id property.
803 * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
804 * record, should that record be inserted at the start of the dataset (by
805 * default, records are added at the end)
807 save: function(prepend_on_create) {
809 var save_obj = {prepend_on_create: prepend_on_create, ret: null};
810 this.save_list.push(save_obj);
811 return this._process_operations().then(function() {
813 return $.Deferred().reject();
814 return $.when.apply($, save_obj.ret);
816 self.$el.removeClass('oe_form_dirty');
819 _process_save: function(save_obj) {
821 var prepend_on_create = save_obj.prepend_on_create;
823 var form_invalid = false,
825 first_invalid_field = null,
826 readonly_values = {};
827 for (var f in self.fields) {
828 if (!self.fields.hasOwnProperty(f)) { continue; }
832 if (!first_invalid_field) {
833 first_invalid_field = f;
835 } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
836 // Special case 'id' field, do not save this field
837 // on 'create' : save all non readonly fields
838 // on 'edit' : save non readonly modified fields
839 if (!f.get("readonly")) {
840 values[f.name] = f.get_value();
842 readonly_values[f.name] = f.get_value();
847 self.set({'display_invalid_fields': true});
848 first_invalid_field.focus();
850 return $.Deferred().reject();
852 self.set({'display_invalid_fields': false});
854 if (!self.datarecord.id) {
856 save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
857 return self.record_created(r, prepend_on_create);
859 } else if (_.isEmpty(values)) {
860 // Not dirty, noop save
861 save_deferral = $.Deferred().resolve({}).promise();
864 save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
865 return self.record_saved(r);
868 return save_deferral;
872 return $.Deferred().reject();
875 on_invalid: function() {
876 var warnings = _(this.fields).chain()
877 .filter(function (f) { return !f.is_valid(); })
879 return _.str.sprintf('<li>%s</li>',
882 warnings.unshift('<ul>');
883 warnings.push('</ul>');
884 this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
887 * Reload the form after saving
889 * @param {Object} r result of the write function.
891 record_saved: function(r) {
894 // should not happen in the server, but may happen for internal purpose
895 this.trigger('record_saved', r);
896 return $.Deferred().reject();
898 return $.when(this.reload()).then(function () {
899 self.trigger('record_saved', r);
905 * Updates the form' dataset to contain the new record:
907 * * Adds the newly created record to the current dataset (at the end by
909 * * Selects that record (sets the dataset's index to point to the new
911 * * Updates the pager and sidebar displays
914 * @param {Boolean} [prepend_on_create=false] adds the newly created record
915 * at the beginning of the dataset instead of the end
917 record_created: function(r, prepend_on_create) {
920 // should not happen in the server, but may happen for internal purpose
921 this.trigger('record_created', r);
922 return $.Deferred().reject();
924 this.datarecord.id = r;
925 if (!prepend_on_create) {
926 this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
927 this.dataset.index = this.dataset.ids.length - 1;
929 this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
930 this.dataset.index = 0;
932 this.do_update_pager();
934 this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
936 //openerp.log("The record has been created with id #" + this.datarecord.id);
937 return $.when(this.reload()).then(function () {
938 self.trigger('record_created', r);
939 return _.extend(r, {created: true});
943 on_action: function (action) {
944 console.debug('Executing action', action);
948 return this.reload_mutex.exec(function() {
949 if (self.dataset.index == null) {
950 self.trigger("previous_view");
951 return $.Deferred().reject().promise();
953 if (self.dataset.index == null || self.dataset.index < 0) {
954 return $.when(self.on_button_new());
956 var fields = _.keys(self.fields_view.fields);
957 fields.push('display_name');
958 return self.dataset.read_index(fields,
962 'future_display_name': true
964 }).then(function(r) {
965 self.trigger('load_record', r);
970 get_widgets: function() {
971 return _.filter(this.getChildren(), function(obj) {
972 return obj instanceof instance.web.form.FormWidget;
975 get_fields_values: function() {
977 var ids = this.get_selected_ids();
978 values["id"] = ids.length > 0 ? ids[0] : false;
979 _.each(this.fields, function(value_, key) {
980 values[key] = value_.get_value();
984 get_selected_ids: function() {
985 var id = this.dataset.ids[this.dataset.index];
986 return id ? [id] : [];
988 recursive_save: function() {
990 return $.when(this.save()).then(function(res) {
991 if (self.dataset.parent_view)
992 return self.dataset.parent_view.recursive_save();
995 recursive_reload: function() {
998 if (self.dataset.parent_view)
999 pre = self.dataset.parent_view.recursive_reload();
1000 return pre.then(function() {
1001 return self.reload();
1004 is_dirty: function() {
1005 return _.any(this.fields, function (value_) {
1006 return value_._dirty_flag;
1009 is_interactible_record: function() {
1010 var id = this.datarecord.id;
1012 if (this.options.not_interactible_on_create)
1014 } else if (typeof(id) === "string") {
1015 if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
1020 sidebar_eval_context: function () {
1021 return $.when(this.build_eval_context());
1023 open_defaults_dialog: function () {
1025 var display = function (field, value) {
1026 if (field instanceof instance.web.form.FieldSelection) {
1027 return _(field.values).find(function (option) {
1028 return option[0] === value;
1030 } else if (field instanceof instance.web.form.FieldMany2One) {
1031 return field.get_displayed();
1035 var fields = _.chain(this.fields)
1036 .map(function (field) {
1037 var value = field.get_value();
1038 // ignore fields which are empty, invisible, readonly, o2m
1041 || field.get('invisible')
1042 || field.get("readonly")
1043 || field.field.type === 'one2many'
1044 || field.field.type === 'many2many'
1045 || field.field.type === 'binary'
1046 || field.password) {
1052 string: field.string,
1054 displayed: display(field, value),
1058 .sortBy(function (field) { return field.string; })
1060 var conditions = _.chain(self.fields)
1061 .filter(function (field) { return field.field.change_default; })
1062 .map(function (field) {
1063 var value = field.get_value();
1066 string: field.string,
1068 displayed: display(field, value),
1073 var d = new instance.web.Dialog(this, {
1074 title: _t("Set Default"),
1077 conditions: conditions
1080 {text: _t("Close"), click: function () { d.close(); }},
1081 {text: _t("Save default"), click: function () {
1082 var $defaults = d.$el.find('#formview_default_fields');
1083 var field_to_set = $defaults.val();
1084 if (!field_to_set) {
1085 $defaults.parent().addClass('oe_form_invalid');
1088 var condition = d.$el.find('#formview_default_conditions').val(),
1089 all_users = d.$el.find('#formview_default_all').is(':checked');
1090 new instance.web.DataSet(self, 'ir.values').call(
1094 self.fields[field_to_set].get_value(),
1098 ]).done(function () { d.close(); });
1102 d.template = 'FormView.set_default';
1105 register_field: function(field, name) {
1106 this.fields[name] = field;
1107 this.fields_order.push(name);
1108 if (JSON.parse(field.node.attrs.default_focus || "0")) {
1109 this.default_focus_field = field;
1112 field.on('focused', null, this.proxy('widgetFocused'))
1113 .on('blurred', null, this.proxy('widgetBlurred'));
1114 if (this.get_field_desc(name).translate) {
1115 this.translatable_fields.push(field);
1117 field.on('changed_value', this, function() {
1118 if (field.is_syntax_valid()) {
1119 this.trigger('field_changed:' + name);
1121 if (field._inhibit_on_change_flag) {
1124 field._dirty_flag = true;
1125 if (field.is_syntax_valid()) {
1126 this.do_onchange(field);
1127 this.on_form_changed(true);
1128 this.do_notify_change();
1132 get_field_desc: function(field_name) {
1133 return this.fields_view.fields[field_name];
1135 get_field_value: function(field_name) {
1136 return this.fields[field_name].get_value();
1138 compute_domain: function(expression) {
1139 return instance.web.form.compute_domain(expression, this.fields);
1141 _build_view_fields_values: function() {
1142 var a_dataset = this.dataset;
1143 var fields_values = this.get_fields_values();
1144 var active_id = a_dataset.ids[a_dataset.index];
1145 _.extend(fields_values, {
1146 active_id: active_id || false,
1147 active_ids: active_id ? [active_id] : [],
1148 active_model: a_dataset.model,
1151 if (a_dataset.parent_view) {
1152 fields_values.parent = a_dataset.parent_view.get_fields_values();
1154 return fields_values;
1156 build_eval_context: function() {
1157 var a_dataset = this.dataset;
1158 return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
1163 * Interface to be implemented by rendering engines for the form view.
1165 instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
1166 set_fields_view: function(fields_view) {},
1167 set_fields_registry: function(fields_registry) {},
1168 render_to: function($el) {},
1172 * Default rendering engine for the form view.
1174 * It is necessary to set the view using set_view() before usage.
1176 instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
1177 init: function(view) {
1180 set_fields_view: function(fvg) {
1182 this.version = parseFloat(this.fvg.arch.attrs.version);
1183 if (isNaN(this.version)) {
1187 set_tags_registry: function(tags_registry) {
1188 this.tags_registry = tags_registry;
1190 set_fields_registry: function(fields_registry) {
1191 this.fields_registry = fields_registry;
1193 set_widgets_registry: function(widgets_registry) {
1194 this.widgets_registry = widgets_registry;
1196 // Backward compatibility tools, current default version: v6.1
1197 process_version: function() {
1198 if (this.version < 7.0) {
1199 this.$form.find('form:first').wrapInner('<group col="4"/>');
1200 this.$form.find('page').each(function() {
1201 if (!$(this).parents('field').length) {
1202 $(this).wrapInner('<group col="4"/>');
1207 get_arch_fragment: function() {
1208 var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
1209 // IE won't allow custom button@type and will revert it to spec default : 'submit'
1210 $('button', doc).each(function() {
1211 $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
1213 // IE's html parser is also a css parser. How convenient...
1214 $('board', doc).each(function() {
1215 $(this).attr('layout', $(this).attr('style'));
1217 return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
1219 render_to: function($target) {
1221 this.$target = $target;
1223 this.$form = this.get_arch_fragment();
1225 this.process_version();
1227 this.fields_to_init = [];
1228 this.tags_to_init = [];
1229 this.widgets_to_init = [];
1231 this.process(this.$form);
1233 this.$form.appendTo(this.$target);
1235 this.to_replace = [];
1237 _.each(this.fields_to_init, function($elem) {
1238 var name = $elem.attr("name");
1239 if (!self.fvg.fields[name]) {
1240 throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
1242 var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
1244 throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
1246 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1247 var $label = self.labels[$elem.attr("name")];
1249 w.set_input_id($label.attr("for"));
1251 self.alter_field(w);
1252 self.view.register_field(w, $elem.attr("name"));
1253 self.to_replace.push([w, $elem]);
1255 _.each(this.tags_to_init, function($elem) {
1256 var tag_name = $elem[0].tagName.toLowerCase();
1257 var obj = self.tags_registry.get_object(tag_name);
1258 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1259 self.to_replace.push([w, $elem]);
1261 _.each(this.widgets_to_init, function($elem) {
1262 var widget_type = $elem.attr("type");
1263 var obj = self.widgets_registry.get_object(widget_type);
1264 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1265 self.to_replace.push([w, $elem]);
1268 init_fields: function() {
1270 _.each(this.to_replace, function(el) {
1271 defs.push(el[0].replace(el[1]));
1273 this.to_replace = [];
1274 return $.when.apply($, defs);
1276 render_element: function(template /* dictionaries */) {
1277 var dicts = [].slice.call(arguments).slice(1);
1278 var dict = _.extend.apply(_, dicts);
1279 dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
1280 return $(QWeb.render(template, dict));
1282 alter_field: function(field) {
1284 toggle_layout_debugging: function() {
1285 if (!this.$target.has('.oe_layout_debug_cell:first').length) {
1286 this.$target.find('[title]').removeAttr('title');
1287 this.$target.find('.oe_form_group_cell').each(function() {
1288 var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
1289 $(this).attr('title', text);
1292 this.$target.toggleClass('oe_layout_debugging');
1294 process: function($tag) {
1296 var tagname = $tag[0].nodeName.toLowerCase();
1297 if (this.tags_registry.contains(tagname)) {
1298 this.tags_to_init.push($tag);
1301 var fn = self['process_' + tagname];
1303 var args = [].slice.call(arguments);
1305 return fn.apply(self, args);
1307 // generic tag handling, just process children
1308 $tag.children().each(function() {
1309 self.process($(this));
1311 self.handle_common_properties($tag, $tag);
1312 $tag.removeAttr("modifiers");
1316 process_widget: function($widget) {
1317 this.widgets_to_init.push($widget);
1320 process_sheet: function($sheet) {
1321 var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
1322 this.handle_common_properties($new_sheet, $sheet);
1323 var $dst = $new_sheet.find('.oe_form_sheet');
1324 $sheet.contents().appendTo($dst);
1325 $sheet.before($new_sheet).remove();
1326 this.process($new_sheet);
1328 process_form: function($form) {
1329 if ($form.find('> sheet').length === 0) {
1330 $form.addClass('oe_form_nosheet');
1332 var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
1333 this.handle_common_properties($new_form, $form);
1334 $form.contents().appendTo($new_form);
1335 if ($form[0] === this.$form[0]) {
1336 // If root element, replace it
1337 this.$form = $new_form;
1339 $form.before($new_form).remove();
1341 this.process($new_form);
1344 * Used by direct <field> children of a <group> tag only
1345 * This method will add the implicit <label...> for every field
1348 preprocess_field: function($field) {
1350 var name = $field.attr('name'),
1351 field_colspan = parseInt($field.attr('colspan'), 10),
1352 field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
1354 if ($field.attr('nolabel') === '1')
1356 $field.attr('nolabel', '1');
1358 this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
1359 $(el).parents().each(function(unused, tag) {
1360 var name = tag.tagName.toLowerCase();
1361 if (name === "field" || name in self.tags_registry.map)
1368 var $label = $('<label/>').attr({
1370 "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
1371 "string": $field.attr('string'),
1372 "help": $field.attr('help'),
1373 "class": $field.attr('class'),
1375 $label.insertBefore($field);
1376 if (field_colspan > 1) {
1377 $field.attr('colspan', field_colspan - 1);
1381 process_field: function($field) {
1382 if ($field.parent().is('group')) {
1383 // No implicit labels for normal fields, only for <group> direct children
1384 var $label = this.preprocess_field($field);
1386 this.process($label);
1389 this.fields_to_init.push($field);
1392 process_group: function($group) {
1394 $group.children('field').each(function() {
1395 self.preprocess_field($(this));
1397 var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
1399 if ($new_group.first().is('table.oe_form_group')) {
1400 $table = $new_group;
1401 } else if ($new_group.filter('table.oe_form_group').length) {
1402 $table = $new_group.filter('table.oe_form_group').first();
1404 $table = $new_group.find('table.oe_form_group').first();
1408 cols = parseInt($group.attr('col') || 2, 10),
1412 $group.children().each(function(a,b,c) {
1413 var $child = $(this);
1414 var colspan = parseInt($child.attr('colspan') || 1, 10);
1415 var tagName = $child[0].tagName.toLowerCase();
1416 var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
1417 var newline = tagName === 'newline';
1419 // Note FME: those classes are used in layout debug mode
1420 if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
1421 $tr.addClass('oe_form_group_row_incomplete');
1423 $tr.addClass('oe_form_group_row_newline');
1430 if (!$tr || row_cols < colspan) {
1431 $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
1433 } else if (tagName==='group') {
1434 // When <group> <group/><group/> </group>, we need a spacing between the two groups
1435 $td.addClass('oe_group_right')
1437 row_cols -= colspan;
1439 // invisibility transfer
1440 var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
1441 var invisible = field_modifiers.invisible;
1442 self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
1444 $tr.append($td.append($child));
1445 children.push($child[0]);
1447 if (row_cols && $td) {
1448 $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
1450 $group.before($new_group).remove();
1452 $table.find('> tbody > tr').each(function() {
1453 var to_compute = [],
1456 $(this).children().each(function() {
1458 $child = $td.children(':first');
1459 if ($child.attr('cell-class')) {
1460 $td.addClass($child.attr('cell-class'));
1462 switch ($child[0].tagName.toLowerCase()) {
1466 if ($child.attr('for')) {
1467 $td.attr('width', '1%').addClass('oe_form_group_cell_label');
1468 row_cols-= $td.attr('colspan') || 1;
1473 var width = _.str.trim($child.attr('width') || ''),
1474 iwidth = parseInt(width, 10);
1476 if (width.substr(-1) === '%') {
1478 width = iwidth + '%';
1481 $td.css('min-width', width + 'px');
1483 $td.attr('width', width);
1484 $child.removeAttr('width');
1485 row_cols-= $td.attr('colspan') || 1;
1487 to_compute.push($td);
1493 var unit = Math.floor(total / row_cols);
1494 if (!$(this).is('.oe_form_group_row_incomplete')) {
1495 _.each(to_compute, function($td, i) {
1496 var width = parseInt($td.attr('colspan'), 10) * unit;
1497 $td.attr('width', width + '%');
1503 _.each(children, function(el) {
1504 self.process($(el));
1506 this.handle_common_properties($new_group, $group);
1509 process_notebook: function($notebook) {
1512 $notebook.find('> page').each(function() {
1513 var $page = $(this);
1514 var page_attrs = $page.getAttributes();
1515 page_attrs.id = _.uniqueId('notebook_page_');
1516 var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
1517 $page.contents().appendTo($new_page);
1518 $page.before($new_page).remove();
1519 var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
1520 page_attrs.__page = $new_page;
1521 page_attrs.__ic = ic;
1522 pages.push(page_attrs);
1524 $new_page.children().each(function() {
1525 self.process($(this));
1528 var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
1529 $notebook.contents().appendTo($new_notebook);
1530 $notebook.before($new_notebook).remove();
1531 self.process($($new_notebook.children()[0]));
1532 //tabs and invisibility handling
1533 $new_notebook.tabs();
1534 _.each(pages, function(page, i) {
1537 page.__ic.on("change:effective_invisible", null, function() {
1538 if (!page.__ic.get('effective_invisible') && page.autofocus) {
1539 $new_notebook.tabs('select', i);
1542 var current = $new_notebook.tabs("option", "selected");
1543 if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
1545 var first_visible = _.find(_.range(pages.length), function(i2) {
1546 return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
1548 if (first_visible !== undefined) {
1549 $new_notebook.tabs('select', first_visible);
1554 this.handle_common_properties($new_notebook, $notebook);
1555 return $new_notebook;
1557 process_separator: function($separator) {
1558 var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
1559 $separator.before($new_separator).remove();
1560 this.handle_common_properties($new_separator, $separator);
1561 return $new_separator;
1563 process_label: function($label) {
1564 var name = $label.attr("for"),
1565 field_orm = this.fvg.fields[name];
1567 string: $label.attr('string') || (field_orm || {}).string || '',
1568 help: $label.attr('help') || (field_orm || {}).help || '',
1569 _for: name ? _.uniqueId('oe-field-input-') : undefined,
1571 var align = parseFloat(dict.align);
1572 if (isNaN(align) || align === 1) {
1574 } else if (align === 0) {
1580 var $new_label = this.render_element('FormRenderingLabel', dict);
1581 $label.before($new_label).remove();
1582 this.handle_common_properties($new_label, $label);
1584 this.labels[name] = $new_label;
1588 handle_common_properties: function($new_element, $node) {
1589 var str_modifiers = $node.attr("modifiers") || "{}";
1590 var modifiers = JSON.parse(str_modifiers);
1592 if (modifiers.invisible !== undefined)
1593 ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
1594 $new_element.addClass($node.attr("class") || "");
1595 $new_element.attr('style', $node.attr('style'));
1596 return {invisibility_changer: ic,};
1603 If you read this documentation, it probably means that you were asked to use a form view widget outside of
1604 a form view. Before going further, you must understand that those fields were never really created for
1605 that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
1606 you to hack the system with more style.
1608 instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
1609 init: function(parent, eval_context) {
1610 this._super(parent);
1611 this.field_descs = {};
1612 this.eval_context = eval_context || {};
1614 display_invalid_fields: false,
1615 actual_mode: 'create',
1618 get_field_desc: function(field_name) {
1619 if (this.field_descs[field_name] === undefined) {
1620 this.field_descs[field_name] = {
1624 return this.field_descs[field_name];
1626 extend_field_desc: function(fields) {
1628 _.each(fields, function(v, k) {
1629 _.extend(self.get_field_desc(k), v);
1632 get_field_value: function(field_name) {
1635 set_values: function(values) {
1638 compute_domain: function(expression) {
1639 return instance.web.form.compute_domain(expression, {});
1641 build_eval_context: function() {
1642 return new instance.web.CompoundContext(this.eval_context);
1646 instance.web.form.compute_domain = function(expr, fields) {
1647 if (! (expr instanceof Array))
1650 for (var i = expr.length - 1; i >= 0; i--) {
1652 if (ex.length == 1) {
1653 var top = stack.pop();
1656 stack.push(stack.pop() || top);
1659 stack.push(stack.pop() && top);
1665 throw new Error(_.str.sprintf(
1666 _t("Unknown operator %s in domain %s"),
1667 ex, JSON.stringify(expr)));
1671 var field = fields[ex[0]];
1673 throw new Error(_.str.sprintf(
1674 _t("Unknown field %s in domain %s"),
1675 ex[0], JSON.stringify(expr)));
1677 var field_value = field.get_value ? field.get_value() : field.value;
1681 switch (op.toLowerCase()) {
1684 stack.push(_.isEqual(field_value, val));
1688 stack.push(!_.isEqual(field_value, val));
1691 stack.push(field_value < val);
1694 stack.push(field_value > val);
1697 stack.push(field_value <= val);
1700 stack.push(field_value >= val);
1703 if (!_.isArray(val)) val = [val];
1704 stack.push(_(val).contains(field_value));
1707 if (!_.isArray(val)) val = [val];
1708 stack.push(!_(val).contains(field_value));
1712 _t("Unsupported operator %s in domain %s"),
1713 op, JSON.stringify(expr));
1716 return _.all(stack, _.identity);
1719 instance.web.form.is_bin_size = function(v) {
1720 return /^\d+(\.\d*)? \w+$/.test(v);
1724 * Must be applied over an class already possessing the PropertiesMixin.
1726 * Apply the result of the "invisible" domain to this.$el.
1728 instance.web.form.InvisibilityChangerMixin = {
1729 init: function(field_manager, invisible_domain) {
1731 this._ic_field_manager = field_manager;
1732 this._ic_invisible_modifier = invisible_domain;
1733 this._ic_field_manager.on("view_content_has_changed", this, function() {
1734 var result = self._ic_invisible_modifier === undefined ? false :
1735 self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
1736 self.set({"invisible": result});
1738 this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
1739 var check = function() {
1740 if (self.get("invisible") || self.get('force_invisible')) {
1741 self.set({"effective_invisible": true});
1743 self.set({"effective_invisible": false});
1746 this.on('change:invisible', this, check);
1747 this.on('change:force_invisible', this, check);
1751 this.on("change:effective_invisible", this, this._check_visibility);
1752 this._check_visibility();
1754 _check_visibility: function() {
1755 this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
1759 instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
1760 init: function(parent, field_manager, invisible_domain, $el) {
1761 this.setParent(parent);
1762 instance.web.PropertiesMixin.init.call(this);
1763 instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
1770 Base class for all fields, custom widgets and buttons to be displayed in the form view.
1773 - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
1774 the values of the "readonly" property and the "mode" property on the field manager.
1776 instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
1778 * @constructs instance.web.form.FormWidget
1779 * @extends instance.web.Widget
1781 * @param field_manager
1784 init: function(field_manager, node) {
1785 this._super(field_manager);
1786 this.field_manager = field_manager;
1787 if (this.field_manager instanceof instance.web.FormView)
1788 this.view = this.field_manager;
1790 this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
1791 instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
1793 this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
1799 // some events to make the property "effective_readonly" sync automatically with "readonly" and
1800 // "mode" on field_manager
1802 var test_effective_readonly = function() {
1803 self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
1805 this.on("change:readonly", this, test_effective_readonly);
1806 this.field_manager.on("change:actual_mode", this, test_effective_readonly);
1807 test_effective_readonly.call(this);
1809 renderElement: function() {
1810 this.process_modifiers();
1812 this.$el.addClass(this.node.attrs["class"] || "");
1814 destroy: function() {
1816 this._super.apply(this, arguments);
1819 * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
1821 * This method is an utility method that is meant to be called by child classes.
1823 * @param {jQuery} $e jQuery object of elements to bind focus/blur on
1825 setupFocus: function ($e) {
1828 focus: function () { self.trigger('focused'); },
1829 blur: function () { self.trigger('blurred'); }
1832 process_modifiers: function() {
1834 for (var a in this.modifiers) {
1835 if (!this.modifiers.hasOwnProperty(a)) { continue; }
1836 if (!_.include(["invisible"], a)) {
1837 var val = this.field_manager.compute_domain(this.modifiers[a]);
1843 do_attach_tooltip: function(widget, trigger, options) {
1844 widget = widget || this;
1845 trigger = trigger || this.$el;
1846 options = _.extend({
1851 var template = widget.template + '.tooltip';
1852 if (!QWeb.has_template(template)) {
1853 template = 'WidgetLabel.tooltip';
1855 return QWeb.render(template, {
1856 debug: instance.session.debug,
1859 gravity: $.fn.tipsy.autoBounds(50, 'nw'),
1864 $(trigger).tipsy(options);
1867 * Builds a new context usable for operations related to fields by merging
1868 * the fields'context with the action's context.
1870 build_context: function() {
1871 // only use the model's context if there is not context on the node
1872 var v_context = this.node.attrs.context;
1874 v_context = (this.field || {}).context || {};
1877 if (v_context.__ref || true) { //TODO: remove true
1878 var fields_values = this.field_manager.build_eval_context();
1879 v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
1883 build_domain: function() {
1884 var f_domain = this.field.domain || [];
1885 var n_domain = this.node.attrs.domain || null;
1886 // if there is a domain on the node, overrides the model's domain
1887 var final_domain = n_domain !== null ? n_domain : f_domain;
1888 if (!(final_domain instanceof Array) || true) { //TODO: remove true
1889 var fields_values = this.field_manager.build_eval_context();
1890 final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
1892 return final_domain;
1896 instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
1897 template: 'WidgetButton',
1898 init: function(field_manager, node) {
1899 node.attrs.type = node.attrs['data-button-type'];
1900 this._super(field_manager, node);
1901 this.force_disabled = false;
1902 this.string = (this.node.attrs.string || '').replace(/_/g, '');
1903 if (JSON.parse(this.node.attrs.default_focus || "0")) {
1904 // TODO fme: provide enter key binding to widgets
1905 this.view.default_focus_button = this;
1907 if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
1908 this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
1912 this._super.apply(this, arguments);
1913 this.view.on('view_content_has_changed', this, this.check_disable);
1914 this.check_disable();
1915 this.$el.click(this.on_click);
1916 if (this.node.attrs.help || instance.session.debug) {
1917 this.do_attach_tooltip();
1919 this.setupFocus(this.$el);
1921 on_click: function() {
1923 this.force_disabled = true;
1924 this.check_disable();
1925 this.execute_action().always(function() {
1926 self.force_disabled = false;
1927 self.check_disable();
1930 execute_action: function() {
1932 var exec_action = function() {
1933 if (self.node.attrs.confirm) {
1934 var def = $.Deferred();
1935 var dialog = instance.web.dialog($('<div/>').text(self.node.attrs.confirm), {
1936 title: _t('Confirm'),
1939 {text: _t("Cancel"), click: function() {
1940 $(this).dialog("close");
1943 {text: _t("Ok"), click: function() {
1945 self.on_confirmed().always(function() {
1946 $(self2).dialog("close");
1951 beforeClose: function() {
1955 return def.promise();
1957 return self.on_confirmed();
1960 if (!this.node.attrs.special) {
1961 return this.view.recursive_save().then(exec_action);
1963 return exec_action();
1966 on_confirmed: function() {
1969 var context = this.build_context();
1971 return this.view.do_execute_action(
1972 _.extend({}, this.node.attrs, {context: context}),
1973 this.view.dataset, this.view.datarecord.id, function () {
1974 self.view.recursive_reload();
1977 check_disable: function() {
1978 var disabled = (this.force_disabled || !this.view.is_interactible_record());
1979 this.$el.prop('disabled', disabled);
1980 this.$el.css('color', disabled ? 'grey' : '');
1985 * Interface to be implemented by fields.
1988 * - changed_value: triggered when the value of the field has changed. This can be due
1989 * to a user interaction or a call to set_value().
1992 instance.web.form.FieldInterface = {
1994 * Constructor takes 2 arguments:
1995 * - field_manager: Implements FieldManagerMixin
1996 * - node: the "<field>" node in json form
1998 init: function(field_manager, node) {},
2000 * Called by the form view to indicate the value of the field.
2002 * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
2003 * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
2004 * before the widget is inserted into the DOM.
2006 * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
2007 * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
2008 * also be able to handle any other format commonly used in the _defaults key on the models in the addons
2009 * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
2010 * no information is ever given to know which format is used.
2012 set_value: function(value_) {},
2014 * Get the current value of the widget.
2016 * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
2017 * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
2018 * For example if the field is marked as "required", a call to get_value() can return false.
2020 * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
2021 * return a default value according to the type of field.
2023 * This method is always assumed to perform synchronously, it can not return a promise.
2025 * If there was no user interaction to modify the value of the field, it is always assumed that
2026 * get_value() return the same semantic value than the one passed in the last call to set_value(),
2027 * although the syntax can be different. This can be the case for type of fields that have a different
2028 * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
2030 get_value: function() {},
2032 * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
2035 set_input_id: function(id) {},
2037 * Returns true if is_syntax_valid() returns true and the value is semantically
2038 * valid too according to the semantic restrictions applied to the field.
2040 is_valid: function() {},
2042 * Returns true if the field holds a value which is syntactically correct, ignoring
2043 * the potential semantic restrictions applied to the field.
2045 is_syntax_valid: function() {},
2047 * Must set the focus on the field. Return false if field is not focusable.
2049 focus: function() {},
2051 * Called when the translate button is clicked.
2053 on_translate: function() {},
2055 This method is called by the form view before reading on_change values and before saving. It tells
2056 the field to save its value before reading it using get_value(). Must return a promise.
2058 commit_value: function() {},
2062 * Abstract class for classes implementing FieldInterface.
2065 * - value: useful property to hold the value of the field. By default, set_value() and get_value()
2066 * set and retrieve the value property. Changing the value property also triggers automatically
2067 * a 'changed_value' event that inform the view to trigger on_changes.
2070 instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
2072 * @constructs instance.web.form.AbstractField
2073 * @extends instance.web.form.FormWidget
2075 * @param field_manager
2078 init: function(field_manager, node) {
2080 this._super(field_manager, node);
2081 this.name = this.node.attrs.name;
2082 this.field = this.field_manager.get_field_desc(this.name);
2083 this.widget = this.node.attrs.widget;
2084 this.string = this.node.attrs.string || this.field.string || this.name;
2085 this.options = instance.web.py_eval(this.node.attrs.options || '{}');
2086 this.set({'value': false});
2088 this.on("change:value", this, function() {
2089 this.trigger('changed_value');
2090 this._check_css_flags();
2093 renderElement: function() {
2096 if (this.field.translate && this.view) {
2097 this.$el.addClass('oe_form_field_translatable');
2098 this.$el.find('.oe_field_translate').click(this.on_translate);
2100 this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
2101 if (instance.session.debug) {
2102 this.do_attach_tooltip(this, this.$label[0] || this.$el);
2103 this.$label.off('dblclick').on('dblclick', function() {
2104 console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
2106 console.log("window.w =", window.w);
2109 if (!this.disable_utility_classes) {
2110 this.off("change:required", this, this._set_required);
2111 this.on("change:required", this, this._set_required);
2112 this._set_required();
2114 this._check_visibility();
2115 this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
2116 this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
2117 this._check_css_flags();
2120 var tmp = this._super();
2121 this.on("change:value", this, function() {
2122 if (! this.no_rerender)
2123 this.render_value();
2125 this.render_value();
2128 * Private. Do not use.
2130 _set_required: function() {
2131 this.$el.toggleClass('oe_form_required', this.get("required"));
2133 set_value: function(value_) {
2134 this.set({'value': value_});
2136 get_value: function() {
2137 return this.get('value');
2140 Utility method that all implementations should use to change the
2141 value without triggering a re-rendering.
2143 internal_set_value: function(value_) {
2144 var tmp = this.no_rerender;
2145 this.no_rerender = true;
2146 this.set({'value': value_});
2147 this.no_rerender = tmp;
2150 This method is called each time the value is modified.
2152 render_value: function() {},
2153 is_valid: function() {
2154 return this.is_syntax_valid() && !(this.get('required') && this.is_false());
2156 is_syntax_valid: function() {
2160 * Method useful to implement to ease validity testing. Must return true if the current
2161 * value is similar to false in OpenERP.
2163 is_false: function() {
2164 return this.get('value') === false;
2166 _check_css_flags: function() {
2167 if (this.field.translate) {
2168 this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
2170 if (!this.disable_utility_classes) {
2171 if (this.field_manager.get('display_invalid_fields')) {
2172 this.$el.toggleClass('oe_form_invalid', !this.is_valid());
2179 set_input_id: function(id) {
2180 this.id_for_label = id;
2182 on_translate: function() {
2184 var trans = new instance.web.DataSet(this, 'ir.translation');
2185 return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
2190 set_dimensions: function (height, width) {
2196 commit_value: function() {
2202 * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
2205 instance.web.form.ReinitializeWidgetMixin = {
2207 * Default implementation of, you should not override it, use initialize_field() instead.
2210 this.initialize_field();
2213 initialize_field: function() {
2214 this.on("change:effective_readonly", this, this.reinitialize);
2215 this.initialize_content();
2217 reinitialize: function() {
2218 this.destroy_content();
2219 this.renderElement();
2220 this.initialize_content();
2223 * Called to destroy anything that could have been created previously, called before a
2224 * re-initialization.
2226 destroy_content: function() {},
2228 * Called to initialize the content.
2230 initialize_content: function() {},
2234 * A mixin to apply on any field that has to completely re-render when its readonly state
2237 instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
2238 reinitialize: function() {
2239 instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
2240 this.render_value();
2245 Some hack to make placeholders work in ie9.
2247 if ($.browser.msie && $.browser.version === "9.0") {
2248 document.addEventListener("DOMNodeInserted",function(event){
2249 var nodename = event.target.nodeName.toLowerCase();
2250 if ( nodename === "input" || nodename == "textarea" ) {
2251 $(event.target).placeholder();
2256 instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2257 template: 'FieldChar',
2258 widget_class: 'oe_form_field_char',
2260 'change input': 'store_dom_value',
2262 init: function (field_manager, node) {
2263 this._super(field_manager, node);
2264 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2266 initialize_content: function() {
2267 this.setupFocus(this.$('input'));
2269 store_dom_value: function () {
2270 if (!this.get('effective_readonly')
2271 && this.$('input').length
2272 && this.is_syntax_valid()) {
2273 this.internal_set_value(
2275 this.$('input').val()));
2278 commit_value: function () {
2279 this.store_dom_value();
2280 return this._super();
2282 render_value: function() {
2283 var show_value = this.format_value(this.get('value'), '');
2284 if (!this.get("effective_readonly")) {
2285 this.$el.find('input').val(show_value);
2287 if (this.password) {
2288 show_value = new Array(show_value.length + 1).join('*');
2290 this.$(".oe_form_char_content").text(show_value);
2293 is_syntax_valid: function() {
2294 if (!this.get("effective_readonly") && this.$("input").size() > 0) {
2296 this.parse_value(this.$('input').val(), '');
2304 parse_value: function(val, def) {
2305 return instance.web.parse_value(val, this, def);
2307 format_value: function(val, def) {
2308 return instance.web.format_value(val, this, def);
2310 is_false: function() {
2311 return this.get('value') === '' || this._super();
2314 var input = this.$('input:first')[0];
2315 return input ? input.focus() : false;
2317 set_dimensions: function (height, width) {
2318 this._super(height, width);
2319 this.$('input').css({
2326 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2327 process_modifiers: function () {
2329 this.set({ readonly: true });
2333 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2334 template: 'FieldEmail',
2335 initialize_content: function() {
2337 var $button = this.$el.find('button');
2338 $button.click(this.on_button_clicked);
2339 this.setupFocus($button);
2341 render_value: function() {
2342 if (!this.get("effective_readonly")) {
2346 .attr('href', 'mailto:' + this.get('value'))
2347 .text(this.get('value') || '');
2350 on_button_clicked: function() {
2351 if (!this.get('value') || !this.is_syntax_valid()) {
2352 this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2354 location.href = 'mailto:' + this.get('value');
2359 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2360 template: 'FieldUrl',
2361 initialize_content: function() {
2363 var $button = this.$el.find('button');
2364 $button.click(this.on_button_clicked);
2365 this.setupFocus($button);
2367 render_value: function() {
2368 if (!this.get("effective_readonly")) {
2371 var tmp = this.get('value');
2372 var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2374 tmp = "http://" + this.get('value');
2376 var text = this.get('value') ? this.node.attrs.text || tmp : '';
2377 this.$el.find('a').attr('href', tmp).text(text);
2380 on_button_clicked: function() {
2381 if (!this.get('value')) {
2382 this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2384 var url = $.trim(this.get('value'));
2385 if(/^www\./i.test(url))
2386 url = 'http://'+url;
2392 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2393 is_field_number: true,
2394 widget_class: 'oe_form_field_float',
2395 init: function (field_manager, node) {
2396 this._super(field_manager, node);
2397 this.internal_set_value(0);
2398 if (this.node.attrs.digits) {
2399 this.digits = this.node.attrs.digits;
2401 this.digits = this.field.digits;
2404 set_value: function(value_) {
2405 if (value_ === false || value_ === undefined) {
2406 // As in GTK client, floats default to 0
2409 this._super.apply(this, [value_]);
2411 focus: function () {
2412 var $input = this.$('input:first');
2413 return $input.length ? $input.select() : false;
2417 instance.web.DateTimeWidget = instance.web.Widget.extend({
2418 template: "web.datepicker",
2419 jqueryui_object: 'datetimepicker',
2420 type_of_date: "datetime",
2422 'change .oe_datepicker_master': 'change_datetime',
2424 init: function(parent) {
2425 this._super(parent);
2426 this.name = parent.name;
2430 this.$input = this.$el.find('input.oe_datepicker_master');
2431 this.$input_picker = this.$el.find('input.oe_datepicker_container');
2433 $.datepicker.setDefaults({
2434 clearText: _t('Clear'),
2435 clearStatus: _t('Erase the current date'),
2436 closeText: _t('Done'),
2437 closeStatus: _t('Close without change'),
2438 prevText: _t('<Prev'),
2439 prevStatus: _t('Show the previous month'),
2440 nextText: _t('Next>'),
2441 nextStatus: _t('Show the next month'),
2442 currentText: _t('Today'),
2443 currentStatus: _t('Show the current month'),
2444 monthNames: Date.CultureInfo.monthNames,
2445 monthNamesShort: Date.CultureInfo.abbreviatedMonthNames,
2446 monthStatus: _t('Show a different month'),
2447 yearStatus: _t('Show a different year'),
2448 weekHeader: _t('Wk'),
2449 weekStatus: _t('Week of the year'),
2450 dayNames: Date.CultureInfo.dayNames,
2451 dayNamesShort: Date.CultureInfo.abbreviatedDayNames,
2452 dayNamesMin: Date.CultureInfo.shortestDayNames,
2453 dayStatus: _t('Set DD as first week day'),
2454 dateStatus: _t('Select D, M d'),
2455 firstDay: Date.CultureInfo.firstDayOfWeek,
2456 initStatus: _t('Select a date'),
2459 $.timepicker.setDefaults({
2460 timeOnlyTitle: _t('Choose Time'),
2461 timeText: _t('Time'),
2462 hourText: _t('Hour'),
2463 minuteText: _t('Minute'),
2464 secondText: _t('Second'),
2465 currentText: _t('Now'),
2466 closeText: _t('Done')
2470 onClose: this.on_picker_select,
2471 onSelect: this.on_picker_select,
2475 showButtonPanel: true,
2476 firstDay: Date.CultureInfo.firstDayOfWeek
2478 // Some clicks in the datepicker dialog are not stopped by the
2479 // datepicker and "bubble through", unexpectedly triggering the bus's
2480 // click event. Prevent that.
2481 this.picker('widget').click(function (e) { e.stopPropagation(); });
2483 this.$el.find('img.oe_datepicker_trigger').click(function() {
2484 if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
2485 self.$input.focus();
2488 self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date());
2489 self.$input_picker.show();
2490 self.picker('show');
2491 self.$input_picker.hide();
2493 this.set_readonly(false);
2494 this.set({'value': false});
2496 picker: function() {
2497 return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
2499 on_picker_select: function(text, instance_) {
2500 var date = this.picker('getDate');
2502 .val(date ? this.format_client(date) : '')
2506 set_value: function(value_) {
2507 this.set({'value': value_});
2508 this.$input.val(value_ ? this.format_client(value_) : '');
2510 get_value: function() {
2511 return this.get('value');
2513 set_value_from_ui_: function() {
2514 var value_ = this.$input.val() || false;
2515 this.set({'value': this.parse_client(value_)});
2517 set_readonly: function(readonly) {
2518 this.readonly = readonly;
2519 this.$input.prop('readonly', this.readonly);
2520 this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
2522 is_valid_: function() {
2523 var value_ = this.$input.val();
2524 if (value_ === "") {
2528 this.parse_client(value_);
2535 parse_client: function(v) {
2536 return instance.web.parse_value(v, {"widget": this.type_of_date});
2538 format_client: function(v) {
2539 return instance.web.format_value(v, {"widget": this.type_of_date});
2541 change_datetime: function() {
2542 if (this.is_valid_()) {
2543 this.set_value_from_ui_();
2544 this.trigger("datetime_changed");
2547 commit_value: function () {
2548 this.change_datetime();
2552 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2553 jqueryui_object: 'datepicker',
2554 type_of_date: "date"
2557 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2558 template: "FieldDatetime",
2559 build_widget: function() {
2560 return new instance.web.DateTimeWidget(this);
2562 destroy_content: function() {
2563 if (this.datewidget) {
2564 this.datewidget.destroy();
2565 this.datewidget = undefined;
2568 initialize_content: function() {
2569 if (!this.get("effective_readonly")) {
2570 this.datewidget = this.build_widget();
2571 this.datewidget.on('datetime_changed', this, _.bind(function() {
2572 this.internal_set_value(this.datewidget.get_value());
2574 this.datewidget.appendTo(this.$el);
2575 this.setupFocus(this.datewidget.$input);
2578 render_value: function() {
2579 if (!this.get("effective_readonly")) {
2580 this.datewidget.set_value(this.get('value'));
2582 this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2585 is_syntax_valid: function() {
2586 if (!this.get("effective_readonly") && this.datewidget) {
2587 return this.datewidget.is_valid_();
2591 is_false: function() {
2592 return this.get('value') === '' || this._super();
2595 var input = this.datewidget && this.datewidget.$input[0];
2596 return input ? input.focus() : false;
2598 set_dimensions: function (height, width) {
2599 this._super(height, width);
2600 this.datewidget.$input.css('height', height);
2604 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2605 template: "FieldDate",
2606 build_widget: function() {
2607 return new instance.web.DateWidget(this);
2611 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2612 template: 'FieldText',
2614 'keyup': function (e) {
2615 if (e.which === $.ui.keyCode.ENTER) {
2616 e.stopPropagation();
2619 'change textarea': 'store_dom_value',
2621 initialize_content: function() {
2623 if (! this.get("effective_readonly")) {
2624 this.$textarea = this.$el.find('textarea');
2625 this.auto_sized = false;
2626 this.default_height = this.$textarea.css('height');
2627 if (this.get("effective_readonly")) {
2628 this.$textarea.attr('disabled', 'disabled');
2630 this.setupFocus(this.$textarea);
2632 this.$textarea = undefined;
2635 commit_value: function () {
2636 if (! this.get("effective_readonly") && this.$textarea) {
2637 this.store_dom_value();
2639 return this._super();
2641 store_dom_value: function () {
2642 this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2644 render_value: function() {
2645 if (! this.get("effective_readonly")) {
2646 var show_value = instance.web.format_value(this.get('value'), this, '');
2647 if (show_value === '') {
2648 this.$textarea.css('height', parseInt(this.default_height)+"px");
2650 this.$textarea.val(show_value);
2651 if (! this.auto_sized) {
2652 this.auto_sized = true;
2653 this.$textarea.autosize();
2655 this.$textarea.trigger("autosize");
2658 var txt = this.get("value") || '';
2659 this.$(".oe_form_text_content").text(txt);
2662 is_syntax_valid: function() {
2663 if (!this.get("effective_readonly") && this.$textarea) {
2665 instance.web.parse_value(this.$textarea.val(), this, '');
2673 is_false: function() {
2674 return this.get('value') === '' || this._super();
2676 focus: function($el) {
2677 var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2678 return input ? input.focus() : false;
2680 set_dimensions: function (height, width) {
2681 this._super(height, width);
2682 if (!this.get("effective_readonly") && this.$textarea) {
2683 this.$textarea.css({
2692 * FieldTextHtml Widget
2693 * Intended for FieldText widgets meant to display HTML content. This
2694 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2695 * To find more information about CLEditor configutation: go to
2696 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2698 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2699 template: 'FieldTextHtml',
2701 this._super.apply(this, arguments);
2703 initialize_content: function() {
2705 if (! this.get("effective_readonly")) {
2706 self._updating_editor = false;
2707 this.$textarea = this.$el.find('textarea');
2708 var width = ((this.node.attrs || {}).editor_width || '100%');
2709 var height = ((this.node.attrs || {}).editor_height || 250);
2710 this.$textarea.cleditor({
2711 width: width, // width not including margins, borders or padding
2712 height: height, // height not including margins, borders or padding
2713 controls: // controls to add to the toolbar
2714 "bold italic underline strikethrough " +
2715 "| removeformat | bullets numbering | outdent " +
2716 "indent | link unlink | source",
2717 bodyStyle: // style to assign to document body contained within the editor
2718 "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2720 this.$cleditor = this.$textarea.cleditor()[0];
2721 this.$cleditor.change(function() {
2722 if (! self._updating_editor) {
2723 self.$cleditor.updateTextArea();
2724 self.internal_set_value(self.$textarea.val());
2727 if (this.field.translate) {
2728 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"/>')
2729 .click(this.on_translate);
2730 this.$cleditor.$toolbar.append($img);
2734 render_value: function() {
2735 if (! this.get("effective_readonly")) {
2736 this.$textarea.val(this.get('value') || '');
2737 this._updating_editor = true;
2738 this.$cleditor.updateFrame();
2739 this._updating_editor = false;
2741 this.$el.html(this.get('value'));
2746 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2747 template: 'FieldBoolean',
2750 this.$checkbox = $("input", this.$el);
2751 this.setupFocus(this.$checkbox);
2752 this.$el.click(_.bind(function() {
2753 this.internal_set_value(this.$checkbox.is(':checked'));
2755 var check_readonly = function() {
2756 self.$checkbox.prop('disabled', self.get("effective_readonly"));
2758 this.on("change:effective_readonly", this, check_readonly);
2759 check_readonly.call(this);
2760 this._super.apply(this, arguments);
2762 render_value: function() {
2763 this.$checkbox[0].checked = this.get('value');
2766 var input = this.$checkbox && this.$checkbox[0];
2767 return input ? input.focus() : false;
2772 The progressbar field expect a float from 0 to 100.
2774 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2775 template: 'FieldProgressBar',
2776 render_value: function() {
2777 this.$el.progressbar({
2778 value: this.get('value') || 0,
2779 disabled: this.get("effective_readonly")
2781 var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
2782 this.$('span').html(formatted_value + '%');
2787 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2788 template: 'FieldSelection',
2790 'change select': 'store_dom_value',
2792 init: function(field_manager, node) {
2794 this._super(field_manager, node);
2795 this.values = _(this.field.selection).chain()
2796 .reject(function (v) { return v[0] === false && v[1] === ''; })
2797 .unshift([false, ''])
2800 initialize_content: function() {
2801 // Flag indicating whether we're in an event chain containing a change
2802 // event on the select, in order to know what to do on keyup[RETURN]:
2803 // * If the user presses [RETURN] as part of changing the value of a
2804 // selection, we should just let the value change and not let the
2805 // event broadcast further (e.g. to validating the current state of
2806 // the form in editable list view, which would lead to saving the
2807 // current row or switching to the next one)
2808 // * If the user presses [RETURN] with a select closed (side-effect:
2809 // also if the user opened the select and pressed [RETURN] without
2810 // changing the selected value), takes the action as validating the
2812 var ischanging = false;
2813 var $select = this.$el.find('select')
2814 .change(function () { ischanging = true; })
2815 .click(function () { ischanging = false; })
2816 .keyup(function (e) {
2817 if (e.which !== 13 || !ischanging) { return; }
2818 e.stopPropagation();
2821 this.setupFocus($select);
2823 commit_value: function () {
2824 this.store_dom_value();
2825 return this._super();
2827 store_dom_value: function () {
2828 if (!this.get('effective_readonly') && this.$('select').length) {
2829 this.internal_set_value(
2830 this.values[this.$('select')[0].selectedIndex][0]);
2833 set_value: function(value_) {
2834 value_ = value_ === null ? false : value_;
2835 value_ = value_ instanceof Array ? value_[0] : value_;
2836 this._super(value_);
2838 render_value: function() {
2839 if (!this.get("effective_readonly")) {
2841 for (var i = 0, ii = this.values.length; i < ii; i++) {
2842 if (this.values[i][0] === this.get('value')) index = i;
2844 this.$el.find('select')[0].selectedIndex = index;
2847 var option = _(this.values)
2848 .detect(function (record) { return record[0] === self.get('value'); });
2849 this.$el.text(option ? option[1] : this.values[0][1]);
2853 var input = this.$('select:first')[0];
2854 return input ? input.focus() : false;
2856 set_dimensions: function (height, width) {
2857 this._super(height, width);
2858 this.$('select').css({
2865 instance.web.form.FieldRadio = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2866 template: 'FieldRadio',
2868 'click input': 'click_change_value'
2870 init: function(field_manager, node) {
2871 /* Radio button widget: Attributes options:
2872 * - "horizontal" to display in column
2873 * - "no_radiolabel" don't display text values
2875 this._super(field_manager, node);
2876 this.selection = _.clone(this.field.selection) || [];
2877 this.domain = false;
2879 initialize_content: function () {
2880 this.uniqueId = _.uniqueId("radio");
2881 this.on("change:effective_readonly", this, this.render_value);
2882 this.field_manager.on("view_content_has_changed", this, this.get_selection);
2883 this.get_selection();
2885 click_change_value: function (event) {
2886 var val = $(event.target).val();
2887 val = this.field.type == "selection" ? val : +val;
2888 if (val == this.get_value()) {
2889 this.set_value(false);
2891 this.set_value(val);
2894 /** Get the selection and render it
2895 * selection: [[identifier, value_to_display], ...]
2896 * For selection fields: this is directly given by this.field.selection
2897 * For many2one fields: perform a search on the relation of the many2one field
2899 get_selection: function() {
2902 var def = $.Deferred();
2903 if (self.field.type == "many2one") {
2904 var domain = instance.web.pyeval.eval('domain', this.build_domain()) || [];
2905 if (! _.isEqual(self.domain, domain)) {
2906 self.domain = domain;
2907 var ds = new instance.web.DataSetStatic(self, self.field.relation, self.build_context());
2908 ds.call('search', [self.domain])
2909 .then(function (records) {
2910 ds.name_get(records).then(function (records) {
2911 selection = records;
2916 selection = self.selection;
2920 else if (self.field.type == "selection") {
2921 selection = self.field.selection || [];
2924 return def.then(function () {
2925 if (! _.isEqual(selection, self.selection)) {
2926 self.selection = _.clone(selection);
2927 self.renderElement();
2928 self.render_value();
2932 set_value: function (value_) {
2934 if (this.field.type == "selection") {
2935 value_ = _.find(this.field.selection, function (sel) { return sel[0] == value_});
2937 else if (!this.selection.length) {
2938 this.selection = [value_];
2941 this._super(value_);
2943 get_value: function () {
2944 var value = this.get('value');
2945 return value instanceof Array ? value[0] : value;
2947 render_value: function () {
2949 this.$el.toggleClass("oe_readonly", this.get('effective_readonly'));
2950 this.$("input:checked").prop("checked", false);
2951 if (this.get_value()) {
2952 this.$("input").filter(function () {return this.value == self.get_value()}).prop("checked", true);
2953 this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : "");
2958 // jquery autocomplete tweak to allow html and classnames
2960 var proto = $.ui.autocomplete.prototype,
2961 initSource = proto._initSource;
2963 function filter( array, term ) {
2964 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
2965 return $.grep( array, function(value_) {
2966 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
2971 _initSource: function() {
2972 if ( this.options.html && $.isArray(this.options.source) ) {
2973 this.source = function( request, response ) {
2974 response( filter( this.options.source, request.term ) );
2977 initSource.call( this );
2981 _renderItem: function( ul, item) {
2982 return $( "<li></li>" )
2983 .data( "item.autocomplete", item )
2984 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
2986 .addClass(item.classname);
2992 * A mixin containing some useful methods to handle completion inputs.
2994 instance.web.form.CompletionFieldMixin = {
2997 this.orderer = new instance.web.DropMisordered();
3000 * Call this method to search using a string.
3002 get_search_result: function(search_val) {
3005 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
3006 var blacklist = this.get_search_blacklist();
3007 this.last_query = search_val;
3009 return this.orderer.add(dataset.name_search(
3010 search_val, new instance.web.CompoundDomain(self.build_domain(), [["id", "not in", blacklist]]),
3011 'ilike', this.limit + 1, self.build_context())).then(function(data) {
3012 self.last_search = data;
3013 // possible selections for the m2o
3014 var values = _.map(data, function(x) {
3015 x[1] = x[1].split("\n")[0];
3017 label: _.str.escapeHTML(x[1]),
3024 // search more... if more results that max
3025 if (values.length > self.limit) {
3026 values = values.slice(0, self.limit);
3028 label: _t("Search More..."),
3029 action: function() {
3030 dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
3031 self._search_create_popup("search", data);
3034 classname: 'oe_m2o_dropdown_option'
3038 var raw_result = _(data.result).map(function(x) {return x[1];});
3039 if (search_val.length > 0 && !_.include(raw_result, search_val)) {
3041 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
3042 $('<span />').text(search_val).html()),
3043 action: function() {
3044 self._quick_create(search_val);
3046 classname: 'oe_m2o_dropdown_option'
3051 label: _t("Create and Edit..."),
3052 action: function() {
3053 self._search_create_popup("form", undefined, self._create_context(search_val));
3055 classname: 'oe_m2o_dropdown_option'
3061 get_search_blacklist: function() {
3064 _quick_create: function(name) {
3066 var slow_create = function () {
3067 self._search_create_popup("form", undefined, self._create_context(name));
3069 if (self.options.quick_create === undefined || self.options.quick_create) {
3070 new instance.web.DataSet(this, this.field.relation, self.build_context())
3071 .name_create(name).done(function(data) {
3072 self.add_id(data[0]);
3073 }).fail(function(error, event) {
3074 event.preventDefault();
3080 // all search/create popup handling
3081 _search_create_popup: function(view, ids, context) {
3083 var pop = new instance.web.form.SelectCreatePopup(this);
3085 self.field.relation,
3087 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3088 initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
3090 disable_multiple_selection: true
3092 self.build_domain(),
3093 new instance.web.CompoundContext(self.build_context(), context || {})
3095 pop.on("elements_selected", self, function(element_ids) {
3096 self.add_id(element_ids[0]);
3103 add_id: function(id) {},
3104 _create_context: function(name) {
3106 var field = (this.options || {}).create_name_field;
3107 if (field === undefined)
3109 if (field !== false && name && (this.options || {}).quick_create !== false)
3110 tmp["default_" + field] = name;
3115 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3116 template: "M2ODialog",
3117 init: function(parent) {
3118 this._super(parent, {
3119 title: _.str.sprintf(_t("Add %s"), parent.string),
3125 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3126 this.$("input").val(this.getParent().last_query);
3127 this.$buttons.find(".oe_form_m2o_qc_button").click(function(){
3128 self.getParent()._quick_create(self.$("input").val());
3131 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3132 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3135 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3141 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3142 template: "FieldMany2One",
3144 'keydown input': function (e) {
3146 case $.ui.keyCode.UP:
3147 case $.ui.keyCode.DOWN:
3148 e.stopPropagation();
3152 init: function(field_manager, node) {
3153 this._super(field_manager, node);
3154 instance.web.form.CompletionFieldMixin.init.call(this);
3155 this.set({'value': false});
3156 this.display_value = {};
3157 this.display_value_backup = {};
3158 this.last_search = [];
3159 this.floating = false;
3160 this.current_display = null;
3161 this.is_started = false;
3163 reinit_value: function(val) {
3164 this.internal_set_value(val);
3165 this.floating = false;
3166 if (this.is_started)
3167 this.render_value();
3169 initialize_field: function() {
3170 this.is_started = true;
3171 instance.web.bus.on('click', this, function() {
3172 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3173 this.$input.autocomplete("close");
3176 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3178 initialize_content: function() {
3179 if (!this.get("effective_readonly"))
3180 this.render_editable();
3182 destroy_content: function () {
3183 if (this.$drop_down) {
3184 this.$drop_down.off('click');
3185 delete this.$drop_down;
3188 this.$input.closest(".ui-dialog .ui-dialog-content").off('scroll');
3189 this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3190 'focus focusout change keydown');
3193 if (this.$follow_button) {
3194 this.$follow_button.off('blur focus click');
3195 delete this.$follow_button;
3198 destroy: function () {
3199 this.destroy_content();
3200 return this._super();
3202 init_error_displayer: function() {
3205 hide_error_displayer: function() {
3208 show_error_displayer: function() {
3209 new instance.web.form.M2ODialog(this).open();
3211 render_editable: function() {
3213 this.$input = this.$el.find("input");
3215 this.init_error_displayer();
3217 self.$input.on('focus', function() {
3218 self.hide_error_displayer();
3221 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3222 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3224 this.$follow_button.click(function(ev) {
3225 ev.preventDefault();
3226 if (!self.get('value')) {
3230 var pop = new instance.web.form.FormOpenPopup(self);
3232 self.field.relation,
3234 self.build_context(),
3236 title: _t("Open: ") + self.string
3239 pop.on('write_completed', self, function(){
3240 self.display_value = {};
3241 self.display_value_backup = {};
3242 self.render_value();
3244 self.view.do_onchange(self);
3248 // some behavior for input
3249 var input_changed = function() {
3250 if (self.current_display !== self.$input.val()) {
3251 self.current_display = self.$input.val();
3252 if (self.$input.val() === "") {
3253 self.internal_set_value(false);
3254 self.floating = false;
3256 self.floating = true;
3260 this.$input.keydown(input_changed);
3261 this.$input.change(input_changed);
3262 this.$drop_down.click(function() {
3263 self.$input.focus();
3264 if (self.$input.autocomplete("widget").is(":visible")) {
3265 self.$input.autocomplete("close");
3267 if (self.get("value") && ! self.floating) {
3268 self.$input.autocomplete("search", "");
3270 self.$input.autocomplete("search");
3275 // Autocomplete close on dialog content scroll
3276 var close_autocomplete = _.debounce(function() {
3277 if (self.$input.autocomplete("widget").is(":visible")) {
3278 self.$input.autocomplete("close");
3281 this.$input.closest(".ui-dialog .ui-dialog-content").on('scroll', this, close_autocomplete);
3283 self.ed_def = $.Deferred();
3284 self.uned_def = $.Deferred();
3286 var ed_duration = 15000;
3287 var anyoneLoosesFocus = function (e) {
3289 if (self.floating) {
3290 if (self.last_search.length > 0) {
3291 if (self.last_search[0][0] != self.get("value")) {
3292 self.display_value = {};
3293 self.display_value_backup = {};
3294 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3295 self.reinit_value(self.last_search[0][0]);
3298 self.render_value();
3302 self.reinit_value(false);
3304 self.floating = false;
3306 if (used && self.get("value") === false && ! self.no_ed) {
3307 self.ed_def.reject();
3308 self.uned_def.reject();
3309 self.ed_def = $.Deferred();
3310 self.ed_def.done(function() {
3311 self.show_error_displayer();
3312 ignore_blur = false;
3313 self.trigger('focused');
3316 setTimeout(function() {
3317 self.ed_def.resolve();
3318 self.uned_def.reject();
3319 self.uned_def = $.Deferred();
3320 self.uned_def.done(function() {
3321 self.hide_error_displayer();
3323 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3327 self.ed_def.reject();
3330 var ignore_blur = false;
3332 focusout: anyoneLoosesFocus,
3333 focus: function () { self.trigger('focused'); },
3334 autocompleteopen: function () { ignore_blur = true; },
3335 autocompleteclose: function () { ignore_blur = false; },
3337 // autocomplete open
3338 if (ignore_blur) { return; }
3339 if (_(self.getChildren()).any(function (child) {
3340 return child instanceof instance.web.form.AbstractFormPopup;
3342 self.trigger('blurred');
3346 var isSelecting = false;
3348 this.$input.autocomplete({
3349 source: function(req, resp) {
3350 self.get_search_result(req.term).done(function(result) {
3354 select: function(event, ui) {
3358 self.display_value = {};
3359 self.display_value_backup = {};
3360 self.display_value["" + item.id] = item.name;
3361 self.reinit_value(item.id);
3362 } else if (item.action) {
3364 // Cancel widget blurring, to avoid form blur event
3365 self.trigger('focused');
3369 focus: function(e, ui) {
3373 // disabled to solve a bug, but may cause others
3374 //close: anyoneLoosesFocus,
3378 this.$input.autocomplete("widget").openerpClass();
3379 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3380 this.$input.keyup(function(e) {
3381 if (e.which === 13) { // ENTER
3383 e.stopPropagation();
3385 isSelecting = false;
3387 this.setupFocus(this.$follow_button);
3389 render_value: function(no_recurse) {
3391 if (! this.get("value")) {
3392 this.display_string("");
3395 var display = this.display_value["" + this.get("value")];
3397 this.display_string(display);
3401 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3402 this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3403 self.display_value["" + self.get("value")] = data[0][1];
3404 self.render_value(true);
3405 }).fail( function (data, event) {
3406 // avoid displaying crash errors as many2One should be name_get compliant
3407 event.preventDefault();
3408 self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")];
3409 self.render_value(true);
3413 display_string: function(str) {
3415 if (!this.get("effective_readonly")) {
3416 this.$input.val(str.split("\n")[0]);
3417 this.current_display = this.$input.val();
3418 if (this.is_false()) {
3419 this.$('.oe_m2o_cm_button').css({'display':'none'});
3421 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3424 var lines = _.escape(str).split("\n");
3428 follow = _.rest(lines).join("<br />");
3431 var $link = this.$el.find('.oe_form_uri')
3434 if (! this.options.no_open)
3435 $link.click(function () {
3437 type: 'ir.actions.act_window',
3438 res_model: self.field.relation,
3439 res_id: self.get("value"),
3440 views: [[false, 'form']],
3442 context: self.build_context().eval(),
3446 $(".oe_form_m2o_follow", this.$el).html(follow);
3449 set_value: function(value_) {
3451 if (value_ instanceof Array) {
3452 this.display_value = {};
3453 this.display_value_backup = {}
3454 if (! this.options.always_reload) {
3455 this.display_value["" + value_[0]] = value_[1];
3458 this.display_value_backup["" + value_[0]] = value_[1];
3462 value_ = value_ || false;
3463 this.reinit_value(value_);
3465 get_displayed: function() {
3466 return this.display_value["" + this.get("value")];
3468 add_id: function(id) {
3469 this.display_value = {};
3470 this.display_value_backup = {};
3471 this.reinit_value(id);
3473 is_false: function() {
3474 return ! this.get("value");
3476 focus: function () {
3477 var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3478 return input ? input.focus() : false;
3480 _quick_create: function() {
3482 this.ed_def.reject();
3483 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3485 _search_create_popup: function() {
3487 this.ed_def.reject();
3488 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3490 set_dimensions: function (height, width) {
3491 this._super(height, width);
3492 this.$input.css('height', height);
3496 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3497 template: 'Many2OneButton',
3498 init: function(field_manager, node) {
3499 this._super.apply(this, arguments);
3502 this._super.apply(this, arguments);
3505 set_button: function() {
3508 this.$button.remove();
3511 this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3512 this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3513 this.$button.addClass('oe_link').css({'padding':'4px'});
3514 this.$el.append(this.$button);
3515 this.$button.on('click', self.on_click);
3517 on_click: function(ev) {
3519 this.popup = new instance.web.form.FormOpenPopup(this);
3520 this.popup.show_element(
3521 this.field.relation,
3523 this.build_context(),
3524 {title: this.string}
3526 this.popup.on('create_completed', self, function(r) {
3530 set_value: function(value_) {
3532 if (value_ instanceof Array) {
3535 value_ = value_ || false;
3536 this.set('value', value_);
3542 # Values: (0, 0, { fields }) create
3543 # (1, ID, { fields }) update
3544 # (2, ID) remove (delete)
3545 # (3, ID) unlink one (target id or target of relation)
3547 # (5) unlink all (only valid for one2many)
3552 'create': function (values) {
3553 return [commands.CREATE, false, values];
3555 // (1, id, {values})
3557 'update': function (id, values) {
3558 return [commands.UPDATE, id, values];
3562 'delete': function (id) {
3563 return [commands.DELETE, id, false];
3565 // (3, id[, _]) removes relation, but not linked record itself
3567 'forget': function (id) {
3568 return [commands.FORGET, id, false];
3572 'link_to': function (id) {
3573 return [commands.LINK_TO, id, false];
3577 'delete_all': function () {
3578 return [5, false, false];
3580 // (6, _, ids) replaces all linked records with provided ids
3582 'replace_with': function (ids) {
3583 return [6, false, ids];
3586 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3587 multi_selection: false,
3588 disable_utility_classes: true,
3589 init: function(field_manager, node) {
3590 this._super(field_manager, node);
3591 lazy_build_o2m_kanban_view();
3592 this.is_loaded = $.Deferred();
3593 this.initial_is_loaded = this.is_loaded;
3594 this.form_last_update = $.Deferred();
3595 this.init_form_last_update = this.form_last_update;
3596 this.is_started = false;
3597 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
3598 this.dataset.o2m = this;
3599 this.dataset.parent_view = this.view;
3600 this.dataset.child_name = this.name;
3602 this.dataset.on('dataset_changed', this, function() {
3603 self.trigger_on_change();
3608 this._super.apply(this, arguments);
3609 this.$el.addClass('oe_form_field oe_form_field_one2many');
3614 this.is_loaded.done(function() {
3615 self.on("change:effective_readonly", self, function() {
3616 self.is_loaded = self.is_loaded.then(function() {
3617 self.viewmanager.destroy();
3618 return $.when(self.load_views()).done(function() {
3619 self.reload_current_view();
3624 this.is_started = true;
3625 this.reload_current_view();
3627 trigger_on_change: function() {
3628 this.trigger('changed_value');
3630 load_views: function() {
3633 var modes = this.node.attrs.mode;
3634 modes = !!modes ? modes.split(",") : ["tree"];
3636 _.each(modes, function(mode) {
3637 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
3638 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
3642 view_type: mode == "tree" ? "list" : mode,
3645 if (self.field.views && self.field.views[mode]) {
3646 view.embedded_view = self.field.views[mode];
3648 if(view.view_type === "list") {
3649 _.extend(view.options, {
3651 selectable: self.multi_selection,
3653 import_enabled: false,
3656 if (self.get("effective_readonly")) {
3657 _.extend(view.options, {
3662 } else if (view.view_type === "form") {
3663 if (self.get("effective_readonly")) {
3664 view.view_type = 'form';
3666 _.extend(view.options, {
3667 not_interactible_on_create: true,
3669 } else if (view.view_type === "kanban") {
3670 _.extend(view.options, {
3671 confirm_on_delete: false,
3673 if (self.get("effective_readonly")) {
3674 _.extend(view.options, {
3675 action_buttons: false,
3676 quick_creatable: false,
3678 read_only_mode: true,
3686 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
3687 this.viewmanager.o2m = self;
3688 var once = $.Deferred().done(function() {
3689 self.init_form_last_update.resolve();
3691 var def = $.Deferred().done(function() {
3692 self.initial_is_loaded.resolve();
3694 this.viewmanager.on("controller_inited", self, function(view_type, controller) {
3695 controller.o2m = self;
3696 if (view_type == "list") {
3697 if (self.get("effective_readonly")) {
3698 controller.on('edit:before', self, function (e) {
3701 _(controller.columns).find(function (column) {
3702 if (!(column instanceof instance.web.list.Handle)) {
3705 column.modifiers.invisible = true;
3709 } else if (view_type === "form") {
3710 if (self.get("effective_readonly")) {
3711 $(".oe_form_buttons", controller.$el).children().remove();
3713 controller.on("load_record", self, function(){
3716 controller.on('pager_action_executed',self,self.save_any_view);
3717 } else if (view_type == "graph") {
3718 self.reload_current_view()
3722 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
3723 $.when(self.save_any_view()).done(function() {
3724 if (n_mode === "list") {
3725 $.async_when().done(function() {
3726 self.reload_current_view();
3731 $.async_when().done(function () {
3732 self.viewmanager.appendTo(self.$el);
3736 reload_current_view: function() {
3738 return self.is_loaded = self.is_loaded.then(function() {
3739 var active_view = self.viewmanager.active_view;
3740 var view = self.viewmanager.views[active_view].controller;
3741 if(active_view === "list") {
3742 return view.reload_content();
3743 } else if (active_view === "form") {
3744 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
3745 self.dataset.index = 0;
3747 var act = function() {
3748 return view.do_show();
3750 self.form_last_update = self.form_last_update.then(act, act);
3751 return self.form_last_update;
3752 } else if (view.do_search) {
3753 return view.do_search(self.build_domain(), self.dataset.get_context(), []);
3757 set_value: function(value_) {
3758 value_ = value_ || [];
3760 this.dataset.reset_ids([]);
3761 if(value_.length >= 1 && value_[0] instanceof Array) {
3763 _.each(value_, function(command) {
3764 var obj = {values: command[2]};
3765 switch (command[0]) {
3766 case commands.CREATE:
3767 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3769 self.dataset.to_create.push(obj);
3770 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3773 case commands.UPDATE:
3774 obj['id'] = command[1];
3775 self.dataset.to_write.push(obj);
3776 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3779 case commands.DELETE:
3780 self.dataset.to_delete.push({id: command[1]});
3782 case commands.LINK_TO:
3783 ids.push(command[1]);
3785 case commands.DELETE_ALL:
3786 self.dataset.delete_all = true;
3791 this.dataset.set_ids(ids);
3792 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
3794 this.dataset.delete_all = true;
3795 _.each(value_, function(command) {
3796 var obj = {values: command};
3797 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3799 self.dataset.to_create.push(obj);
3800 self.dataset.cache.push(_.clone(obj));
3804 this.dataset.set_ids(ids);
3806 this._super(value_);
3807 this.dataset.reset_ids(value_);
3809 if (this.dataset.index === null && this.dataset.ids.length > 0) {
3810 this.dataset.index = 0;
3812 this.trigger_on_change();
3813 if (this.is_started) {
3814 return self.reload_current_view();
3819 get_value: function() {
3823 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
3824 val = val.concat(_.map(this.dataset.ids, function(id) {
3825 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
3827 return commands.create(alter_order.values);
3829 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
3831 return commands.update(alter_order.id, alter_order.values);
3833 return commands.link_to(id);
3835 return val.concat(_.map(
3836 this.dataset.to_delete, function(x) {
3837 return commands['delete'](x.id);}));
3839 commit_value: function() {
3840 return this.save_any_view();
3842 save_any_view: function() {
3843 if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
3844 this.viewmanager.views[this.viewmanager.active_view] &&
3845 this.viewmanager.views[this.viewmanager.active_view].controller) {
3846 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3847 if (this.viewmanager.active_view === "form") {
3848 if (!view.is_initialized.state() === 'resolved') {
3849 return $.when(false);
3851 return $.when(view.save());
3852 } else if (this.viewmanager.active_view === "list") {
3853 return $.when(view.ensure_saved());
3856 return $.when(false);
3858 is_syntax_valid: function() {
3859 if (! this.viewmanager || ! this.viewmanager.views[this.viewmanager.active_view])
3861 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3862 switch (this.viewmanager.active_view) {
3864 return _(view.fields).chain()
3870 return view.is_valid();
3876 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
3877 template: 'One2Many.viewmanager',
3878 init: function(parent, dataset, views, flags) {
3879 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
3880 this.registry = this.registry.extend({
3881 list: 'instance.web.form.One2ManyListView',
3882 form: 'instance.web.form.One2ManyFormView',
3883 kanban: 'instance.web.form.One2ManyKanbanView',
3885 this.__ignore_blur = false;
3887 switch_mode: function(mode, unused) {
3888 if (mode !== 'form') {
3889 return this._super(mode, unused);
3892 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
3893 var pop = new instance.web.form.FormOpenPopup(this);
3894 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3895 title: _t("Open: ") + self.o2m.string,
3896 create_function: function(data, options) {
3897 return self.o2m.dataset.create(data, options).done(function(r) {
3898 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3899 self.o2m.dataset.trigger("dataset_changed", r);
3902 write_function: function(id, data, options) {
3903 return self.o2m.dataset.write(id, data, {}).done(function() {
3904 self.o2m.reload_current_view();
3907 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3908 parent_view: self.o2m.view,
3909 child_name: self.o2m.name,
3910 read_function: function() {
3911 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3913 form_view_options: {'not_interactible_on_create':true},
3914 readonly: self.o2m.get("effective_readonly")
3916 pop.on("elements_selected", self, function() {
3917 self.o2m.reload_current_view();
3922 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
3923 get_context: function() {
3924 this.context = this.o2m.build_context();
3925 return this.context;
3929 instance.web.form.One2ManyListView = instance.web.ListView.extend({
3930 _template: 'One2Many.listview',
3931 init: function (parent, dataset, view_id, options) {
3932 this._super(parent, dataset, view_id, _.extend(options || {}, {
3933 GroupsType: instance.web.form.One2ManyGroups,
3934 ListType: instance.web.form.One2ManyList
3936 this.on('edit:before', this, this.proxy('_before_edit'));
3937 this.on('edit:after', this, this.proxy('_after_edit'));
3938 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
3941 .bind('add', this.proxy("changed_records"))
3942 .bind('edit', this.proxy("changed_records"))
3943 .bind('remove', this.proxy("changed_records"));
3945 start: function () {
3946 var ret = this._super();
3948 .off('mousedown.handleButtons')
3949 .on('mousedown.handleButtons', 'table button', this.proxy('_button_down'));
3952 changed_records: function () {
3953 this.o2m.trigger_on_change();
3955 is_valid: function () {
3956 var editor = this.editor;
3957 var form = editor.form;
3958 // If no edition is pending, the listview can not be invalid (?)
3959 if (!editor.record) {
3962 // If the form has not been modified, the view can only be valid
3963 // NB: is_dirty will also be set on defaults/onchanges/whatever?
3964 // oe_form_dirty seems to only be set on actual user actions
3965 if (!form.$el.is('.oe_form_dirty')) {
3968 this.o2m._dirty_flag = true;
3970 // Otherwise validate internal form
3971 return _(form.fields).chain()
3972 .invoke(function () {
3973 this._check_css_flags();
3974 return this.is_valid();
3979 do_add_record: function () {
3980 if (this.editable()) {
3981 this._super.apply(this, arguments);
3984 var pop = new instance.web.form.SelectCreatePopup(this);
3986 self.o2m.field.relation,
3988 title: _t("Create: ") + self.o2m.string,
3989 initial_view: "form",
3990 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3991 create_function: function(data, options) {
3992 return self.o2m.dataset.create(data, options).done(function(r) {
3993 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3994 self.o2m.dataset.trigger("dataset_changed", r);
3997 read_function: function() {
3998 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4000 parent_view: self.o2m.view,
4001 child_name: self.o2m.name,
4002 form_view_options: {'not_interactible_on_create':true}
4004 self.o2m.build_domain(),
4005 self.o2m.build_context()
4007 pop.on("elements_selected", self, function() {
4008 self.o2m.reload_current_view();
4012 do_activate_record: function(index, id) {
4014 var pop = new instance.web.form.FormOpenPopup(self);
4015 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4016 title: _t("Open: ") + self.o2m.string,
4017 write_function: function(id, data) {
4018 return self.o2m.dataset.write(id, data, {}).done(function() {
4019 self.o2m.reload_current_view();
4022 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4023 parent_view: self.o2m.view,
4024 child_name: self.o2m.name,
4025 read_function: function() {
4026 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4028 form_view_options: {'not_interactible_on_create':true},
4029 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
4032 do_button_action: function (name, id, callback) {
4033 if (!_.isNumber(id)) {
4034 instance.webclient.notification.warn(
4035 _t("Action Button"),
4036 _t("The o2m record must be saved before an action can be used"));
4039 var parent_form = this.o2m.view;
4041 this.ensure_saved().then(function () {
4043 return parent_form.save();
4046 }).done(function () {
4047 if (!self.o2m.options.reload_on_button) {
4048 self.handle_button(name, id, callback);
4050 self.handle_button(name, id, function(){
4051 self.o2m.view.reload();
4057 _before_edit: function () {
4058 this.__ignore_blur = false;
4059 this.editor.form.on('blurred', this, this._on_form_blur);
4061 _after_edit: function () {
4062 // The form's blur thing may be jiggered during the edition setup,
4063 // potentially leading to the o2m instasaving the row. Cancel any
4064 // blurring triggered the edition startup here
4065 this.editor.form.widgetFocused();
4067 _before_unedit: function () {
4068 this.editor.form.off('blurred', this, this._on_form_blur);
4070 _button_down: function () {
4071 // If a button is clicked (usually some sort of action button), it's
4072 // the button's responsibility to ensure the editable list is in the
4073 // correct state -> ignore form blurring
4074 this.__ignore_blur = true;
4077 * Handles blurring of the nested form (saves the currently edited row),
4078 * unless the flag to ignore the event is set to ``true``
4080 * Makes the internal form go away
4082 _on_form_blur: function () {
4083 if (this.__ignore_blur) {
4084 this.__ignore_blur = false;
4087 // FIXME: why isn't there an API for this?
4088 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4089 this.ensure_saved();
4092 this.cancel_edition();
4094 keyup_ENTER: function () {
4095 // blurring caused by hitting the [Return] key, should skip the
4096 // autosave-on-blur and let the handler for [Return] do its thing (save
4097 // the current row *anyway*, then create a new one/edit the next one)
4098 this.__ignore_blur = true;
4099 this._super.apply(this, arguments);
4101 do_delete: function (ids) {
4102 var confirm = window.confirm;
4103 window.confirm = function () { return true; };
4105 return this._super(ids);
4107 window.confirm = confirm;
4111 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4112 setup_resequence_rows: function () {
4113 if (!this.view.o2m.get('effective_readonly')) {
4114 this._super.apply(this, arguments);
4118 instance.web.form.One2ManyList = instance.web.ListView.List.extend({
4119 pad_table_to: function (count) {
4120 if (!this.view.is_action_enabled('create')) {
4123 this._super(count > 0 ? count - 1 : 0);
4126 // magical invocation of wtf does that do
4127 if (this.view.o2m.get('effective_readonly')) {
4132 var columns = _(this.columns).filter(function (column) {
4133 return column.invisible !== '1';
4135 if (this.options.selectable) { columns++; }
4136 if (this.options.deletable) { columns++; }
4138 if (!this.view.is_action_enabled('create')) {
4142 var $cell = $('<td>', {
4144 'class': 'oe_form_field_one2many_list_row_add'
4146 $('<a>', {href: '#'}).text(_t("Add an item"))
4147 .mousedown(function () {
4148 // FIXME: needs to be an official API somehow
4149 if (self.view.editor.is_editing()) {
4150 self.view.__ignore_blur = true;
4153 .click(function (e) {
4155 e.stopPropagation();
4156 // FIXME: there should also be an API for that one
4157 if (self.view.editor.form.__blur_timeout) {
4158 clearTimeout(self.view.editor.form.__blur_timeout);
4159 self.view.editor.form.__blur_timeout = false;
4161 self.view.ensure_saved().done(function () {
4162 self.view.do_add_record();
4166 var $padding = this.$current.find('tr:not([data-id]):first');
4167 var $newrow = $('<tr>').append($cell);
4168 if ($padding.length) {
4169 $padding.before($newrow);
4171 this.$current.append($newrow)
4176 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4177 form_template: 'One2Many.formview',
4178 load_form: function(data) {
4181 this.$buttons.find('button.oe_form_button_create').click(function() {
4182 self.save().done(self.on_button_new);
4185 do_notify_change: function() {
4186 if (this.dataset.parent_view) {
4187 this.dataset.parent_view.do_notify_change();
4189 this._super.apply(this, arguments);
4194 var lazy_build_o2m_kanban_view = function() {
4195 if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
4197 instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4201 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4202 template: "FieldMany2ManyTags",
4204 this._super.apply(this, arguments);
4205 instance.web.form.CompletionFieldMixin.init.call(this);
4206 this.set({"value": []});
4207 this._display_orderer = new instance.web.DropMisordered();
4208 this._drop_shown = false;
4210 initialize_content: function() {
4211 if (this.get("effective_readonly"))
4214 var ignore_blur = false;
4215 self.$text = this.$("textarea");
4216 self.$text.textext({
4217 plugins : 'tags arrow autocomplete',
4219 render: function(suggestion) {
4220 return $('<span class="text-label"/>').
4221 data('index', suggestion['index']).html(suggestion['label']);
4226 selectFromDropdown: function() {
4227 this.trigger('hideDropdown');
4228 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4229 var data = self.search_result[index];
4231 self.add_id(data.id);
4236 this.trigger('setSuggestions', {result : []});
4240 isTagAllowed: function(tag) {
4244 removeTag: function(tag) {
4245 var id = tag.data("id");
4246 self.set({"value": _.without(self.get("value"), id)});
4248 renderTag: function(stuff) {
4249 return $.fn.textext.TextExtTags.prototype.renderTag.
4250 call(this, stuff).data("id", stuff.id);
4254 itemToString: function(item) {
4259 onSetInputData: function(e, data) {
4261 this._plugins.autocomplete._suggestions = null;
4263 this.input().val(data);
4267 }).bind('getSuggestions', function(e, data) {
4269 var str = !!data ? data.query || '' : '';
4270 self.get_search_result(str).done(function(result) {
4271 self.search_result = result;
4272 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4273 return _.extend(el, {index:i});
4276 }).bind('hideDropdown', function() {
4277 self._drop_shown = false;
4278 }).bind('showDropdown', function() {
4279 self._drop_shown = true;
4281 self.tags = self.$text.textext()[0].tags();
4283 .focusin(function () {
4284 self.trigger('focused');
4285 ignore_blur = false;
4287 .focusout(function() {
4288 self.$text.trigger("setInputData", "");
4290 self.trigger('blurred');
4292 }).keydown(function(e) {
4293 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4294 self.$text.textext()[0].autocomplete().selectFromDropdown();
4298 set_value: function(value_) {
4299 value_ = value_ || [];
4300 if (value_.length >= 1 && value_[0] instanceof Array) {
4301 value_ = value_[0][2];
4303 this._super(value_);
4305 is_false: function() {
4306 return _(this.get("value")).isEmpty();
4308 get_value: function() {
4309 var tmp = [commands.replace_with(this.get("value"))];
4312 get_search_blacklist: function() {
4313 return this.get("value");
4315 render_value: function() {
4317 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4318 var values = self.get("value");
4319 var handle_names = function(data) {
4320 if (self.isDestroyed())
4323 _.each(data, function(el) {
4324 indexed[el[0]] = el;
4326 data = _.map(values, function(el) { return indexed[el]; });
4327 if (! self.get("effective_readonly")) {
4328 self.tags.containerElement().children().remove();
4329 self.$('textarea').css("padding-left", "3px");
4330 self.tags.addTags(_.map(data, function(el) {return {name: el[1], id:el[0]};}));
4332 self.$el.html(QWeb.render("FieldMany2ManyTag", {elements: data}));
4335 if (! values || values.length > 0) {
4336 this._display_orderer.add(dataset.name_get(values)).done(handle_names);
4341 add_id: function(id) {
4342 this.set({'value': _.uniq(this.get('value').concat([id]))});
4344 focus: function () {
4345 var input = this.$text && this.$text[0];
4346 return input ? input.focus() : false;
4348 set_dimensions: function (height, width) {
4349 this._super(height, width);
4350 this.$("textarea").css({
4359 - reload_on_button: Reload the whole form view if click on a button in a list view.
4360 If you see this options, do not use it, it's basically a dirty hack to make one
4361 precise o2m to behave the way we want.
4363 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4364 multi_selection: false,
4365 disable_utility_classes: true,
4366 init: function(field_manager, node) {
4367 this._super(field_manager, node);
4368 this.is_loaded = $.Deferred();
4369 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4370 this.dataset.m2m = this;
4372 this.dataset.on('unlink', self, function(ids) {
4373 self.dataset_changed();
4376 this.list_dm = new instance.web.DropMisordered();
4377 this.render_value_dm = new instance.web.DropMisordered();
4379 initialize_content: function() {
4382 this.$el.addClass('oe_form_field oe_form_field_many2many');
4384 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4385 'addable': this.get("effective_readonly") ? null : _t("Add"),
4386 'deletable': this.get("effective_readonly") ? false : true,
4387 'selectable': this.multi_selection,
4389 'reorderable': false,
4390 'import_enabled': false,
4392 var embedded = (this.field.views || {}).tree;
4394 this.list_view.set_embedded_view(embedded);
4396 this.list_view.m2m_field = this;
4397 var loaded = $.Deferred();
4398 this.list_view.on("list_view_loaded", this, function() {
4401 this.list_view.appendTo(this.$el);
4403 var old_def = self.is_loaded;
4404 self.is_loaded = $.Deferred().done(function() {
4407 this.list_dm.add(loaded).then(function() {
4408 self.is_loaded.resolve();
4411 destroy_content: function() {
4412 this.list_view.destroy();
4413 this.list_view = undefined;
4415 set_value: function(value_) {
4416 value_ = value_ || [];
4417 if (value_.length >= 1 && value_[0] instanceof Array) {
4418 value_ = value_[0][2];
4420 this._super(value_);
4422 get_value: function() {
4423 return [commands.replace_with(this.get('value'))];
4425 is_false: function () {
4426 return _(this.get("value")).isEmpty();
4428 render_value: function() {
4430 this.dataset.set_ids(this.get("value"));
4431 this.render_value_dm.add(this.is_loaded).then(function() {
4432 return self.list_view.reload_content();
4435 dataset_changed: function() {
4436 this.internal_set_value(this.dataset.ids);
4440 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4441 get_context: function() {
4442 this.context = this.m2m.build_context();
4443 return this.context;
4449 * @extends instance.web.ListView
4451 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4452 do_add_record: function () {
4453 var pop = new instance.web.form.SelectCreatePopup(this);
4457 title: _t("Add: ") + this.m2m_field.string
4459 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4460 this.m2m_field.build_context()
4463 pop.on("elements_selected", self, function(element_ids) {
4465 _(element_ids).each(function (id) {
4466 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4467 self.dataset.set_ids(self.dataset.ids.concat([id]));
4468 self.m2m_field.dataset_changed();
4473 self.reload_content();
4477 do_activate_record: function(index, id) {
4479 var pop = new instance.web.form.FormOpenPopup(this);
4480 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4481 title: _t("Open: ") + this.m2m_field.string,
4482 readonly: this.getParent().get("effective_readonly")
4484 pop.on('write_completed', self, self.reload_content);
4486 do_button_action: function(name, id, callback) {
4488 var _sup = _.bind(this._super, this);
4489 if (! this.m2m_field.options.reload_on_button) {
4490 return _sup(name, id, callback);
4492 return this.m2m_field.view.save().then(function() {
4493 return _sup(name, id, function() {
4494 self.m2m_field.view.reload();
4499 is_action_enabled: function () { return true; },
4502 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4503 disable_utility_classes: true,
4504 init: function(field_manager, node) {
4505 this._super(field_manager, node);
4506 instance.web.form.CompletionFieldMixin.init.call(this);
4507 m2m_kanban_lazy_init();
4508 this.is_loaded = $.Deferred();
4509 this.initial_is_loaded = this.is_loaded;
4512 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4513 this.dataset.m2m = this;
4514 this.dataset.on('unlink', self, function(ids) {
4515 self.dataset_changed();
4519 this._super.apply(this, arguments);
4524 self.on("change:effective_readonly", self, function() {
4525 self.is_loaded = self.is_loaded.then(function() {
4526 self.kanban_view.destroy();
4527 return $.when(self.load_view()).done(function() {
4528 self.render_value();
4533 set_value: function(value_) {
4534 value_ = value_ || [];
4535 if (value_.length >= 1 && value_[0] instanceof Array) {
4536 value_ = value_[0][2];
4538 this._super(value_);
4540 get_value: function() {
4541 return [commands.replace_with(this.get('value'))];
4543 load_view: function() {
4545 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4546 'create_text': _t("Add"),
4547 'creatable': self.get("effective_readonly") ? false : true,
4548 'quick_creatable': self.get("effective_readonly") ? false : true,
4549 'read_only_mode': self.get("effective_readonly") ? true : false,
4550 'confirm_on_delete': false,
4552 var embedded = (this.field.views || {}).kanban;
4554 this.kanban_view.set_embedded_view(embedded);
4556 this.kanban_view.m2m = this;
4557 var loaded = $.Deferred();
4558 this.kanban_view.on("kanban_view_loaded",self,function() {
4559 self.initial_is_loaded.resolve();
4562 this.kanban_view.on('switch_mode', this, this.open_popup);
4563 $.async_when().done(function () {
4564 self.kanban_view.appendTo(self.$el);
4568 render_value: function() {
4570 this.dataset.set_ids(this.get("value"));
4571 this.is_loaded = this.is_loaded.then(function() {
4572 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
4575 dataset_changed: function() {
4576 this.set({'value': this.dataset.ids});
4578 open_popup: function(type, unused) {
4579 if (type !== "form")
4582 if (this.dataset.index === null) {
4583 var pop = new instance.web.form.SelectCreatePopup(this);
4585 this.field.relation,
4587 title: _t("Add: ") + this.string
4589 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
4590 this.build_context()
4592 pop.on("elements_selected", self, function(element_ids) {
4593 _.each(element_ids, function(one_id) {
4594 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
4595 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
4596 self.dataset_changed();
4597 self.render_value();
4602 var id = self.dataset.ids[self.dataset.index];
4603 var pop = new instance.web.form.FormOpenPopup(this);
4604 pop.show_element(self.field.relation, id, self.build_context(), {
4605 title: _t("Open: ") + self.string,
4606 write_function: function(id, data, options) {
4607 return self.dataset.write(id, data, {}).done(function() {
4608 self.render_value();
4611 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
4612 parent_view: self.view,
4613 child_name: self.name,
4614 readonly: self.get("effective_readonly")
4618 add_id: function(id) {
4619 this.quick_create.add_id(id);
4623 function m2m_kanban_lazy_init() {
4624 if (instance.web.form.Many2ManyKanbanView)
4626 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4627 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
4628 _is_quick_create_enabled: function() {
4629 return this._super() && ! this.group_by;
4632 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
4633 template: 'Many2ManyKanban.quick_create',
4636 * close_btn: If true, the widget will display a "Close" button able to trigger
4639 init: function(parent, dataset, context, buttons) {
4640 this._super(parent);
4641 this.m2m = this.getParent().view.m2m;
4642 this.m2m.quick_create = this;
4643 this._dataset = dataset;
4644 this._buttons = buttons || false;
4645 this._context = context || {};
4647 start: function () {
4649 self.$text = this.$el.find('input').css("width", "200px");
4650 self.$text.textext({
4651 plugins : 'arrow autocomplete',
4653 render: function(suggestion) {
4654 return $('<span class="text-label"/>').
4655 data('index', suggestion['index']).html(suggestion['label']);
4660 selectFromDropdown: function() {
4661 $(this).trigger('hideDropdown');
4662 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4663 var data = self.search_result[index];
4665 self.add_id(data.id);
4672 itemToString: function(item) {
4677 }).bind('getSuggestions', function(e, data) {
4679 var str = !!data ? data.query || '' : '';
4680 self.m2m.get_search_result(str).done(function(result) {
4681 self.search_result = result;
4682 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4683 return _.extend(el, {index:i});
4687 self.$text.focusout(function() {
4692 this.$text[0].focus();
4694 add_id: function(id) {
4697 self.trigger('added', id);
4698 this.m2m.dataset_changed();
4704 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
4706 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
4707 template: "AbstractFormPopup.render",
4710 * -readonly: only applicable when not in creation mode, default to false
4711 * - alternative_form_view
4718 * - form_view_options
4720 init_popup: function(model, row_id, domain, context, options) {
4721 this.row_id = row_id;
4723 this.domain = domain || [];
4724 this.context = context || {};
4725 this.options = options;
4726 _.defaults(this.options, {
4729 init_dataset: function() {
4731 this.created_elements = [];
4732 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
4733 this.dataset.read_function = this.options.read_function;
4734 this.dataset.create_function = function(data, options, sup) {
4735 var fct = self.options.create_function || sup;
4736 return fct.call(this, data, options).done(function(r) {
4737 self.trigger('create_completed saved', r);
4738 self.created_elements.push(r);
4741 this.dataset.write_function = function(id, data, options, sup) {
4742 var fct = self.options.write_function || sup;
4743 return fct.call(this, id, data, options).done(function(r) {
4744 self.trigger('write_completed saved', r);
4747 this.dataset.parent_view = this.options.parent_view;
4748 this.dataset.child_name = this.options.child_name;
4750 display_popup: function() {
4752 this.renderElement();
4753 var dialog = new instance.web.Dialog(this, {
4755 dialogClass: 'oe_act_window',
4757 self.check_exit(true);
4759 title: this.options.title || "",
4760 }, this.$el).open();
4761 this.$buttonpane = dialog.$buttons;
4764 setup_form_view: function() {
4767 this.dataset.ids = [this.row_id];
4768 this.dataset.index = 0;
4770 this.dataset.index = null;
4772 var options = _.clone(self.options.form_view_options) || {};
4773 if (this.row_id !== null) {
4774 options.initial_mode = this.options.readonly ? "view" : "edit";
4777 $buttons: this.$buttonpane,
4779 this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
4780 if (this.options.alternative_form_view) {
4781 this.view_form.set_embedded_view(this.options.alternative_form_view);
4783 this.view_form.appendTo(this.$el.find(".oe_popup_form"));
4784 this.view_form.on("form_view_loaded", self, function() {
4785 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
4786 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
4787 multi_select: multi_select,
4788 readonly: self.row_id !== null && self.options.readonly,
4790 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
4791 $snbutton.click(function() {
4792 $.when(self.view_form.save()).done(function() {
4793 self.view_form.reload_mutex.exec(function() {
4794 self.view_form.on_button_new();
4798 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
4799 $sbutton.click(function() {
4800 $.when(self.view_form.save()).done(function() {
4801 self.view_form.reload_mutex.exec(function() {
4806 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
4807 $cbutton.click(function() {
4808 self.view_form.trigger('on_button_cancel');
4811 self.view_form.do_show();
4814 select_elements: function(element_ids) {
4815 this.trigger("elements_selected", element_ids);
4817 check_exit: function(no_destroy) {
4818 if (this.created_elements.length > 0) {
4819 this.select_elements(this.created_elements);
4820 this.created_elements = [];
4822 this.trigger('closed');
4825 destroy: function () {
4826 this.trigger('closed');
4827 if (this.$el.is(":data(dialog)")) {
4828 this.$el.dialog('close');
4835 * Class to display a popup containing a form view.
4837 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
4838 show_element: function(model, row_id, context, options) {
4839 this.init_popup(model, row_id, [], context, options);
4840 _.defaults(this.options, {
4842 this.display_popup();
4846 this.init_dataset();
4847 this.setup_form_view();
4852 * Class to display a popup to display a list to search a row. It also allows
4853 * to switch to a form view to create a new row.
4855 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
4859 * - initial_view: form or search (default search)
4860 * - disable_multiple_selection
4861 * - list_view_options
4863 select_element: function(model, options, domain, context) {
4864 this.init_popup(model, null, domain, context, options);
4866 _.defaults(this.options, {
4867 initial_view: "search",
4869 this.initial_ids = this.options.initial_ids;
4870 this.display_popup();
4874 this.init_dataset();
4875 if (this.options.initial_view == "search") {
4876 instance.web.pyeval.eval_domains_and_contexts({
4878 contexts: [this.context]
4879 }).done(function (results) {
4880 var search_defaults = {};
4881 _.each(results.context, function (value_, key) {
4882 var match = /^search_default_(.*)$/.exec(key);
4884 search_defaults[match[1]] = value_;
4887 self.setup_search_view(search_defaults);
4893 setup_search_view: function(search_defaults) {
4895 if (this.searchview) {
4896 this.searchview.destroy();
4898 this.searchview = new instance.web.SearchView(this,
4899 this.dataset, false, search_defaults);
4900 this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
4901 if (self.initial_ids) {
4902 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
4903 contexts, groupbys);
4904 self.initial_ids = undefined;
4906 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
4909 this.searchview.on("search_view_loaded", self, function() {
4910 self.view_list = new instance.web.form.SelectCreateListView(self,
4911 self.dataset, false,
4912 _.extend({'deletable': false,
4913 'selectable': !self.options.disable_multiple_selection,
4914 'import_enabled': false,
4915 '$buttons': self.$buttonpane,
4916 'disable_editable_mode': true,
4917 '$pager': self.$('.oe_popup_list_pager'),
4918 }, self.options.list_view_options || {}));
4919 self.view_list.on('edit:before', self, function (e) {
4922 self.view_list.popup = self;
4923 self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
4924 self.view_list.do_show();
4925 }).then(function() {
4926 self.searchview.do_search();
4928 self.view_list.on("list_view_loaded", self, function() {
4929 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
4930 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
4931 $cbutton.click(function() {
4934 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
4935 $sbutton.click(function() {
4936 self.select_elements(self.selected_ids);
4939 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
4940 $cbutton.click(function() {
4945 this.searchview.appendTo($(".oe_popup_search", self.$el));
4947 do_search: function(domains, contexts, groupbys) {
4949 instance.web.pyeval.eval_domains_and_contexts({
4950 domains: domains || [],
4951 contexts: contexts || [],
4952 group_by_seq: groupbys || []
4953 }).done(function (results) {
4954 self.view_list.do_search(results.domain, results.context, results.group_by);
4957 on_click_element: function(ids) {
4959 this.selected_ids = ids || [];
4960 if(this.selected_ids.length > 0) {
4961 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
4963 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
4966 new_object: function() {
4967 if (this.searchview) {
4968 this.searchview.hide();
4970 if (this.view_list) {
4971 this.view_list.do_hide();
4973 this.setup_form_view();
4977 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
4978 do_add_record: function () {
4979 this.popup.new_object();
4981 select_record: function(index) {
4982 this.popup.select_elements([this.dataset.ids[index]]);
4983 this.popup.destroy();
4985 do_select: function(ids, records) {
4986 this._super(ids, records);
4987 this.popup.on_click_element(ids);
4991 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4992 template: 'FieldReference',
4993 init: function(field_manager, node) {
4994 this._super(field_manager, node);
4995 this.reference_ready = true;
4997 destroy_content: function() {
5000 this.fm = undefined;
5003 initialize_content: function() {
5005 var fm = new instance.web.form.DefaultFieldManager(this);
5007 fm.extend_field_desc({
5009 selection: this.field_manager.get_field_desc(this.name).selection,
5017 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
5019 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5021 this.selection.on("change:value", this, this.on_selection_changed);
5022 this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
5024 .on('focused', null, function () {self.trigger('focused')})
5025 .on('blurred', null, function () {self.trigger('blurred')});
5027 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
5029 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5031 this.m2o.on("change:value", this, this.data_changed);
5032 this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
5034 .on('focused', null, function () {self.trigger('focused')})
5035 .on('blurred', null, function () {self.trigger('blurred')});
5037 on_selection_changed: function() {
5038 if (this.reference_ready) {
5039 this.internal_set_value([this.selection.get_value(), false]);
5040 this.render_value();
5043 data_changed: function() {
5044 if (this.reference_ready) {
5045 this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
5048 set_value: function(val) {
5050 val = val.split(',');
5051 val[0] = val[0] || false;
5052 val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
5054 this._super(val || [false, false]);
5056 get_value: function() {
5057 return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
5059 render_value: function() {
5060 this.reference_ready = false;
5061 if (!this.get("effective_readonly")) {
5062 this.selection.set_value(this.get('value')[0]);
5064 this.m2o.field.relation = this.get('value')[0];
5065 this.m2o.set_value(this.get('value')[1]);
5066 this.m2o.$el.toggle(!!this.get('value')[0]);
5067 this.reference_ready = true;
5071 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5072 init: function(field_manager, node) {
5074 this._super(field_manager, node);
5075 this.binary_value = false;
5076 this.useFileAPI = !!window.FileReader;
5077 this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5078 if (!this.useFileAPI) {
5079 this.fileupload_id = _.uniqueId('oe_fileupload');
5080 $(window).on(this.fileupload_id, function() {
5081 var args = [].slice.call(arguments).slice(1);
5082 self.on_file_uploaded.apply(self, args);
5087 if (!this.useFileAPI) {
5088 $(window).off(this.fileupload_id);
5090 this._super.apply(this, arguments);
5092 initialize_content: function() {
5093 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5094 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5095 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5097 on_file_change: function(e) {
5099 var file_node = e.target;
5100 if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5101 if (this.useFileAPI) {
5102 var file = file_node.files[0];
5103 if (file.size > this.max_upload_size) {
5104 var msg = _t("The selected file exceed the maximum file size of %s.");
5105 instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5108 var filereader = new FileReader();
5109 filereader.readAsDataURL(file);
5110 filereader.onloadend = function(upload) {
5111 var data = upload.target.result;
5112 data = data.split(',')[1];
5113 self.on_file_uploaded(file.size, file.name, file.type, data);
5116 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5117 this.$el.find('form.oe_form_binary_form').submit();
5119 this.$el.find('.oe_form_binary_progress').show();
5120 this.$el.find('.oe_form_binary').hide();
5123 on_file_uploaded: function(size, name, content_type, file_base64) {
5124 if (size === false) {
5125 this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5126 // TODO: use openerp web crashmanager
5127 console.warn("Error while uploading file : ", name);
5129 this.filename = name;
5130 this.on_file_uploaded_and_valid.apply(this, arguments);
5132 this.$el.find('.oe_form_binary_progress').hide();
5133 this.$el.find('.oe_form_binary').show();
5135 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5137 on_save_as: function(ev) {
5138 var value = this.get('value');
5140 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5141 ev.stopPropagation();
5143 instance.web.blockUI();
5144 var c = instance.webclient.crashmanager;
5145 this.session.get_file({
5146 url: '/web/binary/saveas_ajax',
5147 data: {data: JSON.stringify({
5148 model: this.view.dataset.model,
5149 id: (this.view.datarecord.id || ''),
5151 filename_field: (this.node.attrs.filename || ''),
5152 data: instance.web.form.is_bin_size(value) ? null : value,
5153 context: this.view.dataset.get_context()
5155 complete: instance.web.unblockUI,
5156 error: c.rpc_error.bind(c)
5158 ev.stopPropagation();
5162 set_filename: function(value) {
5163 var filename = this.node.attrs.filename;
5166 tmp[filename] = value;
5167 this.field_manager.set_values(tmp);
5170 on_clear: function() {
5171 if (this.get('value') !== false) {
5172 this.binary_value = false;
5173 this.internal_set_value(false);
5179 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5180 template: 'FieldBinaryFile',
5181 initialize_content: function() {
5183 if (this.get("effective_readonly")) {
5185 this.$el.find('a').click(function(ev) {
5186 if (self.get('value')) {
5187 self.on_save_as(ev);
5193 render_value: function() {
5194 if (!this.get("effective_readonly")) {
5196 if (this.node.attrs.filename) {
5197 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5199 show_value = (this.get('value') != null && this.get('value') !== false) ? this.get('value') : '';
5201 this.$el.find('input').eq(0).val(show_value);
5203 this.$el.find('a').toggle(!!this.get('value'));
5204 if (this.get('value')) {
5205 var show_value = _t("Download")
5207 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5208 this.$el.find('a').text(show_value);
5212 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5213 this.binary_value = true;
5214 this.internal_set_value(file_base64);
5215 var show_value = name + " (" + instance.web.human_size(size) + ")";
5216 this.$el.find('input').eq(0).val(show_value);
5217 this.set_filename(name);
5219 on_clear: function() {
5220 this._super.apply(this, arguments);
5221 this.$el.find('input').eq(0).val('');
5222 this.set_filename('');
5226 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5227 template: 'FieldBinaryImage',
5228 placeholder: "/web/static/src/img/placeholder.png",
5229 render_value: function() {
5232 if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5233 url = 'data:image/png;base64,' + this.get('value');
5234 } else if (this.get('value')) {
5235 var id = JSON.stringify(this.view.datarecord.id || null);
5236 var field = this.name;
5237 if (this.options.preview_image)
5238 field = this.options.preview_image;
5239 url = this.session.url('/web/binary/image', {
5240 model: this.view.dataset.model,
5243 t: (new Date().getTime()),
5246 url = this.placeholder;
5248 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5249 this.$el.find('> img').remove();
5250 this.$el.prepend($img);
5251 $img.load(function() {
5252 if (! self.options.size)
5254 $img.css("max-width", "" + self.options.size[0] + "px");
5255 $img.css("max-height", "" + self.options.size[1] + "px");
5256 $img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px");
5257 $img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px");
5259 $img.on('error', function() {
5260 $img.attr('src', self.placeholder);
5261 instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5264 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5265 this.internal_set_value(file_base64);
5266 this.binary_value = true;
5267 this.render_value();
5268 this.set_filename(name);
5270 on_clear: function() {
5271 this._super.apply(this, arguments);
5272 this.render_value();
5273 this.set_filename('');
5278 * Widget for (many2many field) to upload one or more file in same time and display in list.
5279 * The user can delete his files.
5280 * Options on attribute ; "blockui" {Boolean} block the UI or not
5281 * during the file is uploading
5283 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
5284 template: "FieldBinaryFileUploader",
5285 init: function(field_manager, node) {
5286 this._super(field_manager, node);
5287 this.field_manager = field_manager;
5289 if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5290 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);
5294 this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5295 this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5296 $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5300 this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5302 set_value: function(value_) {
5303 value_ = value_ || [];
5304 if (value_.length >= 1 && value_[0] instanceof Array) {
5305 value_ = value_[0][2];
5307 this._super(value_);
5309 get_value: function() {
5310 var tmp = [commands.replace_with(this.get("value"))];
5313 get_file_url: function (attachment) {
5314 return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5316 read_name_values : function () {
5318 // don't reset know values
5319 var _value = _.filter(this.get('value'), function (id) { return typeof self.data[id] == 'undefined'; } );
5320 // send request for get_name
5321 if (_value.length) {
5322 return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).done(function (datas) {
5323 _.each(datas, function (data) {
5324 data.no_unlink = true;
5325 data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5326 self.data[data.id] = data;
5333 render_value: function () {
5335 this.read_name_values().then(function () {
5337 var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self}));
5338 render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5339 self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5341 // reinit input type file
5342 var $input = self.$('input.oe_form_binary_file');
5343 $input.after($input.clone(true)).remove();
5344 self.$(".oe_fileupload").show();
5348 on_file_change: function (event) {
5349 event.stopPropagation();
5351 var $target = $(event.target);
5352 if ($target.val() !== '') {
5353 var filename = $target.val().replace(/.*[\\\/]/,'');
5354 // don't uplode more of one file in same time
5355 if (self.data[0] && self.data[0].upload ) {
5358 for (var id in this.get('value')) {
5359 // if the files exits, delete the file before upload (if it's a new file)
5360 if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
5361 self.ds_file.unlink([id]);
5366 if(this.node.attrs.blockui>0) {
5367 instance.web.blockUI();
5370 // TODO : unactivate send on wizard and form
5373 this.$('form.oe_form_binary_form').submit();
5374 this.$(".oe_fileupload").hide();
5375 // add file on data result
5379 'filename': filename,
5385 on_file_loaded: function (event, result) {
5386 var files = this.get('value');
5389 if(this.node.attrs.blockui>0) {
5390 instance.web.unblockUI();
5393 if (result.error || !result.id ) {
5394 this.do_warn( _t('Uploading Error'), result.error);
5395 delete this.data[0];
5397 if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
5398 delete this.data[0];
5399 this.data[result.id] = {
5401 'name': result.name,
5402 'filename': result.filename,
5403 'url': this.get_file_url(result)
5406 this.data[result.id] = {
5408 'name': result.name,
5409 'filename': result.filename,
5410 'url': this.get_file_url(result)
5413 var values = _.clone(this.get('value'));
5414 values.push(result.id);
5415 this.set({'value': values});
5419 on_file_delete: function (event) {
5420 event.stopPropagation();
5421 var file_id=$(event.target).data("id");
5423 var files = _.filter(this.get('value'), function (id) {return id != file_id;});
5424 if(!this.data[file_id].no_unlink) {
5425 this.ds_file.unlink([file_id]);
5427 this.set({'value': files});
5432 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5433 template: "FieldStatus",
5434 init: function(field_manager, node) {
5435 this._super(field_manager, node);
5436 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5437 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5438 this.set({value: false});
5439 this.selection = [];
5440 this.set("selection", []);
5441 this.selection_dm = new instance.web.DropMisordered();
5444 this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5446 this.on("change:value", this, this.get_selection);
5447 this.on("change:evaluated_selection_domain", this, this.get_selection);
5448 this.on("change:selection", this, function() {
5449 this.selection = this.get("selection");
5450 this.render_value();
5452 this.get_selection();
5453 if (this.options.clickable) {
5454 this.$el.on('click','li',this.on_click_stage);
5456 if (this.$el.parent().is('header')) {
5457 this.$el.after('<div class="oe_clear"/>');
5461 set_value: function(value_) {
5462 if (value_ instanceof Array) {
5465 this._super(value_);
5467 render_value: function() {
5469 var content = QWeb.render("FieldStatus.content", {widget: self});
5470 self.$el.html(content);
5472 calc_domain: function() {
5473 var d = instance.web.pyeval.eval('domain', this.build_domain());
5474 var domain = []; //if there is no domain defined, fetch all the records
5477 domain = ['|',['id', '=', this.get('value')]].concat(d);
5480 if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5481 this.set("evaluated_selection_domain", domain);
5484 /** Get the selection and render it
5485 * selection: [[identifier, value_to_display], ...]
5486 * For selection fields: this is directly given by this.field.selection
5487 * For many2one fields: perform a search on the relation of the many2one field
5489 get_selection: function() {
5493 var calculation = _.bind(function() {
5494 if (this.field.type == "many2one") {
5496 var ds = new instance.web.DataSetSearch(this, this.field.relation,
5497 self.build_context(), this.get("evaluated_selection_domain"));
5498 return ds.read_slice(['name'], {}).then(function (records) {
5499 for(var i = 0; i < records.length; i++) {
5500 selection.push([records[i].id, records[i].name]);
5504 // For field type selection filter values according to
5505 // statusbar_visible attribute of the field. For example:
5506 // statusbar_visible="draft,open".
5507 var select = this.field.selection;
5508 for(var i=0; i < select.length; i++) {
5509 var key = select[i][0];
5510 if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5511 selection.push(select[i]);
5517 this.selection_dm.add(calculation()).then(function () {
5518 if (! _.isEqual(selection, self.get("selection"))) {
5519 self.set("selection", selection);
5523 on_click_stage: function (ev) {
5525 var $li = $(ev.currentTarget);
5526 var val = parseInt($li.data("id"));
5527 if (val != self.get('value')) {
5528 this.view.recursive_save().done(function() {
5530 change[self.name] = val;
5531 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
5539 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
5540 template: "FieldMonetary",
5541 widget_class: 'oe_form_field_float oe_form_field_monetary',
5543 this._super.apply(this, arguments);
5544 this.set({"currency": false});
5545 if (this.options.currency_field) {
5546 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
5547 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
5550 this.on("change:currency", this, this.get_currency_info);
5551 this.get_currency_info();
5552 this.ci_dm = new instance.web.DropMisordered();
5555 var tmp = this._super();
5556 this.on("change:currency_info", this, this.reinitialize);
5559 get_currency_info: function() {
5561 if (this.get("currency") === false) {
5562 this.set({"currency_info": null});
5565 return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
5566 .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
5567 self.set({"currency_info": res});
5570 parse_value: function(val, def) {
5571 return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5573 format_value: function(val, def) {
5574 return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5579 * Registry of form fields, called by :js:`instance.web.FormView`.
5581 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
5582 * will substitute to the <field> tags as defined in OpenERP's views.
5584 instance.web.form.widgets = new instance.web.Registry({
5585 'char' : 'instance.web.form.FieldChar',
5586 'id' : 'instance.web.form.FieldID',
5587 'email' : 'instance.web.form.FieldEmail',
5588 'url' : 'instance.web.form.FieldUrl',
5589 'text' : 'instance.web.form.FieldText',
5590 'html' : 'instance.web.form.FieldTextHtml',
5591 'date' : 'instance.web.form.FieldDate',
5592 'datetime' : 'instance.web.form.FieldDatetime',
5593 'selection' : 'instance.web.form.FieldSelection',
5594 'radio' : 'instance.web.form.FieldRadio',
5595 'many2one' : 'instance.web.form.FieldMany2One',
5596 'many2onebutton' : 'instance.web.form.Many2OneButton',
5597 'many2many' : 'instance.web.form.FieldMany2Many',
5598 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
5599 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
5600 'one2many' : 'instance.web.form.FieldOne2Many',
5601 'one2many_list' : 'instance.web.form.FieldOne2Many',
5602 'reference' : 'instance.web.form.FieldReference',
5603 'boolean' : 'instance.web.form.FieldBoolean',
5604 'float' : 'instance.web.form.FieldFloat',
5605 'integer': 'instance.web.form.FieldFloat',
5606 'float_time': 'instance.web.form.FieldFloat',
5607 'progressbar': 'instance.web.form.FieldProgressBar',
5608 'image': 'instance.web.form.FieldBinaryImage',
5609 'binary': 'instance.web.form.FieldBinaryFile',
5610 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
5611 'statusbar': 'instance.web.form.FieldStatus',
5612 'monetary': 'instance.web.form.FieldMonetary',
5616 * Registry of widgets usable in the form view that can substitute to any possible
5617 * tags defined in OpenERP's form views.
5619 * Every referenced class should extend FormWidget.
5621 instance.web.form.tags = new instance.web.Registry({
5622 'button' : 'instance.web.form.WidgetButton',
5625 instance.web.form.custom_widgets = new instance.web.Registry({
5630 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: