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.set_default_options(options);
95 this.dataset = dataset;
96 this.model = dataset.model;
97 this.view_id = view_id || false;
98 this.fields_view = {};
100 this.fields_order = [];
101 this.datarecord = {};
102 this.default_focus_field = null;
103 this.default_focus_button = null;
104 this.fields_registry = instance.web.form.widgets;
105 this.tags_registry = instance.web.form.tags;
106 this.widgets_registry = instance.web.form.custom_widgets;
107 this.has_been_loaded = $.Deferred();
108 this.translatable_fields = [];
109 _.defaults(this.options, {
110 "not_interactible_on_create": false,
111 "initial_mode": "view",
112 "disable_autofocus": false,
113 "footer_to_buttons": false,
115 this.is_initialized = $.Deferred();
116 this.mutating_mutex = new $.Mutex();
117 this.on_change_list = [];
119 this.reload_mutex = new $.Mutex();
120 this.__clicked_inside = false;
121 this.__blur_timeout = null;
122 this.rendering_engine = new instance.web.form.FormRenderingEngine(this);
123 self.set({actual_mode: self.options.initial_mode});
124 this.has_been_loaded.done(function() {
125 self.on("change:actual_mode", self, self.check_actual_mode);
126 self.check_actual_mode();
127 self.on("change:actual_mode", self, self.init_pager);
130 self.on("load_record", self, self.load_record);
131 instance.web.bus.on('clear_uncommitted_changes', this, function(e) {
132 if (!this.can_be_discarded()) {
137 view_loading: function(r) {
138 return this.load_form(r);
140 destroy: function() {
141 _.each(this.get_widgets(), function(w) {
142 w.off('focused blurred');
146 this.$el.off('.formBlur');
150 load_form: function(data) {
153 throw new Error(_t("No data provided."));
156 throw "Form view does not support multiple calls to load_form";
158 this.fields_order = [];
159 this.fields_view = data;
161 this.rendering_engine.set_fields_registry(this.fields_registry);
162 this.rendering_engine.set_tags_registry(this.tags_registry);
163 this.rendering_engine.set_widgets_registry(this.widgets_registry);
164 this.rendering_engine.set_fields_view(data);
165 var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container');
166 this.rendering_engine.render_to($dest);
168 this.$el.on('mousedown.formBlur', function () {
169 self.__clicked_inside = true;
172 this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self}));
173 if (this.options.$buttons) {
174 this.$buttons.appendTo(this.options.$buttons);
176 this.$el.find('.oe_form_buttons').replaceWith(this.$buttons);
178 this.$buttons.on('click', '.oe_form_button_create',
179 this.guard_active(this.on_button_create));
180 this.$buttons.on('click', '.oe_form_button_edit',
181 this.guard_active(this.on_button_edit));
182 this.$buttons.on('click', '.oe_form_button_save',
183 this.guard_active(this.on_button_save));
184 this.$buttons.on('click', '.oe_form_button_cancel',
185 this.guard_active(this.on_button_cancel));
186 if (this.options.footer_to_buttons) {
187 this.$el.find('footer').appendTo(this.$buttons);
190 this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar');
191 if (!this.sidebar && this.options.$sidebar) {
192 this.sidebar = new instance.web.Sidebar(this);
193 this.sidebar.appendTo(this.$sidebar);
194 if (this.fields_view.toolbar) {
195 this.sidebar.add_toolbar(this.fields_view.toolbar);
197 this.sidebar.add_items('other', _.compact([
198 self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete },
199 self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate }
203 this.has_been_loaded.resolve();
205 // Add bounce effect on button 'Edit' when click on readonly page view.
206 this.$el.find(".oe_form_group_row,.oe_form_field,label").on('click', function (e) {
207 if(self.get("actual_mode") == "view") {
208 var $button = self.options.$buttons.find(".oe_form_button_edit");
209 $button.openerpBounce();
211 instance.web.bus.trigger('click', e);
214 //bounce effect on red button when click on statusbar.
215 this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) {
216 if((self.get("actual_mode") == "view")) {
217 var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'});
218 $button.openerpBounce();
222 this.trigger('form_view_loaded', data);
225 widgetFocused: function() {
226 // Clear click flag if used to focus a widget
227 this.__clicked_inside = false;
228 if (this.__blur_timeout) {
229 clearTimeout(this.__blur_timeout);
230 this.__blur_timeout = null;
233 widgetBlurred: function() {
234 if (this.__clicked_inside) {
235 // clicked in an other section of the form (than the currently
236 // focused widget) => just ignore the blurring entirely?
237 this.__clicked_inside = false;
241 // clear timeout, if any
242 this.widgetFocused();
243 this.__blur_timeout = setTimeout(function () {
244 self.trigger('blurred');
248 do_load_state: function(state, warm) {
249 if (state.id && this.datarecord.id != state.id) {
250 if (!this.dataset.get_id_index(state.id)) {
251 this.dataset.ids.push(state.id);
253 this.dataset.select_id(state.id);
261 * @param {Object} [options]
262 * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view".
263 * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record
264 * @return {$.Deferred}
266 do_show: function (options) {
268 options = options || {};
270 this.sidebar.$el.show();
273 this.$buttons.show();
275 this.$el.show().css({
277 filter: 'alpha(opacity = 0)'
279 this.$el.add(this.$buttons).removeClass('oe_form_dirty');
281 var shown = this.has_been_loaded;
282 if (options.reload !== false) {
283 shown = shown.then(function() {
284 if (self.dataset.index === null) {
285 // null index means we should start a new record
286 return self.on_button_new();
288 var fields = _.keys(self.fields_view.fields);
289 fields.push('display_name');
290 return self.dataset.read_index(fields, {
291 context: { 'bin_size': true, 'future_display_name' : true }
292 }).then(function(r) {
293 self.trigger('load_record', r);
297 return shown.then(function() {
298 self._actualize_mode(options.mode || self.options.initial_mode);
301 filter: 'alpha(opacity = 100)'
305 do_hide: function () {
307 this.sidebar.$el.hide();
310 this.$buttons.hide();
317 load_record: function(record) {
318 var self = this, set_values = [];
320 this.set({ 'title' : undefined });
321 this.do_warn(_t("Form"), _t("The record could not be found in the database."), true);
322 return $.Deferred().reject();
324 this.datarecord = record;
325 this._actualize_mode();
326 this.set({ 'title' : record.id ? record.display_name : _t("New") });
328 _(this.fields).each(function (field, f) {
329 field._dirty_flag = false;
330 field._inhibit_on_change_flag = true;
331 var result = field.set_value(self.datarecord[f] || false);
332 field._inhibit_on_change_flag = false;
333 set_values.push(result);
335 return $.when.apply(null, set_values).then(function() {
337 // New record: Second pass in order to trigger the onchanges
338 // respecting the fields order defined in the view
339 _.each(self.fields_order, function(field_name) {
340 if (record[field_name] !== undefined) {
341 var field = self.fields[field_name];
342 field._dirty_flag = true;
343 self.do_onchange(field);
347 self.on_form_changed();
348 self.rendering_engine.init_fields();
349 self.is_initialized.resolve();
350 self.do_update_pager(record.id == null);
352 self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
355 self.do_push_state({id:record.id});
357 self.do_push_state({});
359 self.$el.add(self.$buttons).removeClass('oe_form_dirty');
364 * Loads and sets up the default values for the model as the current
367 * @return {$.Deferred}
369 load_defaults: function () {
371 var keys = _.keys(this.fields_view.fields);
373 return this.dataset.default_get(keys).then(function(r) {
374 self.trigger('load_record', r);
377 return self.trigger('load_record', {});
379 on_form_changed: function() {
380 this.trigger("view_content_has_changed");
382 do_notify_change: function() {
383 this.$el.add(this.$buttons).addClass('oe_form_dirty');
385 execute_pager_action: function(action) {
386 if (this.can_be_discarded()) {
389 this.dataset.index = 0;
392 this.dataset.previous();
398 this.dataset.index = this.dataset.ids.length - 1;
402 this.trigger('pager_action_executed');
405 init_pager: function() {
408 this.$pager.remove();
409 if (this.get("actual_mode") === "create")
411 this.$pager = $(QWeb.render("FormView.pager", {'widget':self})).hide();
412 if (this.options.$pager) {
413 this.$pager.appendTo(this.options.$pager);
415 this.$el.find('.oe_form_pager').replaceWith(this.$pager);
417 this.$pager.on('click','a[data-pager-action]',function() {
418 var action = $(this).data('pager-action');
419 self.execute_pager_action(action);
421 this.do_update_pager();
423 do_update_pager: function(hide_index) {
424 this.$pager.toggle(this.dataset.ids.length > 1);
426 $(".oe_form_pager_state", this.$pager).html("");
428 $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
431 parse_on_change: function (on_change, widget) {
433 var onchange = _.str.trim(on_change);
434 var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
436 throw new Error(_.str.sprintf( _t("Wrong on change format: %s"), onchange ));
439 var method = call[1];
440 if (!_.str.trim(call[2])) {
441 return {method: method, args: []}
444 var argument_replacement = {
445 'False': function () {return false;},
446 'True': function () {return true;},
447 'None': function () {return null;},
448 'context': function () {
449 return new instance.web.CompoundContext(
450 self.dataset.get_context(),
451 widget.build_context() ? widget.build_context() : {});
454 var parent_fields = null;
455 var args = _.map(call[2].split(','), function (a, i) {
456 var field = _.str.trim(a);
458 // literal constant or context
459 if (field in argument_replacement) {
460 return argument_replacement[field]();
463 if (/^-?\d+(\.\d+)?$/.test(field)) {
464 return Number(field);
467 if (self.fields[field]) {
468 var value_ = self.fields[field].get_value();
469 return value_ == null ? false : value_;
472 var splitted = field.split('.');
473 if (splitted.length > 1 && _.str.trim(splitted[0]) === "parent" && self.dataset.parent_view) {
474 if (parent_fields === null) {
475 parent_fields = self.dataset.parent_view.get_fields_values();
477 var p_val = parent_fields[_.str.trim(splitted[1])];
478 if (p_val !== undefined) {
479 return p_val == null ? false : p_val;
483 var first_char = field[0], last_char = field[field.length-1];
484 if ((first_char === '"' && last_char === '"')
485 || (first_char === "'" && last_char === "'")) {
486 return field.slice(1, -1);
489 throw new Error("Could not get field with name '" + field +
490 "' for onchange '" + onchange + "'");
498 do_onchange: function(widget, processed) {
500 this.on_change_list = [{widget: widget, processed: processed}].concat(this.on_change_list);
501 return this._process_operations();
503 _process_onchange: function(on_change_obj) {
505 var widget = on_change_obj.widget;
506 var processed = on_change_obj.processed;
509 processed = processed || [];
510 processed.push(widget.name);
511 var on_change = widget.node.attrs.on_change;
513 var change_spec = self.parse_on_change(on_change, widget);
514 var id = [self.datarecord.id == null ? [] : [self.datarecord.id]];
515 def = new instance.web.Model(self.dataset.model).call(
516 change_spec.method, id.concat(change_spec.args));
520 return def.then(function(response) {
521 if (widget.field['change_default']) {
522 var fieldname = widget.name;
524 if (response.value && (fieldname in response.value)) {
525 // Use value from onchange if onchange executed
526 value_ = response.value[fieldname];
528 // otherwise get form value for field
529 value_ = self.fields[fieldname].get_value();
531 var condition = fieldname + '=' + value_;
534 return new instance.web.Model('ir.values').call(
535 'get_defaults', [self.model, condition]
536 ).then(function (results) {
537 if (!results.length) {
540 if (!response.value) {
543 for(var i=0; i<results.length; ++i) {
544 // [whatever, key, value]
545 var triplet = results[i];
546 response.value[triplet[1]] = triplet[2];
553 }).then(function(response) {
554 return self.on_processed_onchange(response, processed);
558 instance.webclient.crashmanager.show_message(e);
559 return $.Deferred().reject();
562 on_processed_onchange: function(result, processed) {
565 this._internal_set_values(result.value, processed);
567 if (!_.isEmpty(result.warning)) {
568 instance.web.dialog($(QWeb.render("CrashManager.warning", result.warning)), {
569 title:result.warning.title,
572 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
577 function edit_domain(node) {
578 if (typeof node !== "object") {
581 var new_domain = result.domain[node.attrs.name];
583 node.attrs.domain = new_domain;
585 _(node.children).each(edit_domain);
587 edit_domain(this.fields_view.arch);
589 return $.Deferred().resolve();
592 instance.webclient.crashmanager.show_message(e);
593 return $.Deferred().reject();
596 _process_operations: function() {
598 return this.mutating_mutex.exec(function() {
600 var on_change_obj = self.on_change_list.shift();
602 return self._process_onchange(on_change_obj).then(function() {
607 _.each(self.fields, function(field) {
608 defs.push(field.commit_value());
610 var args = _.toArray(arguments);
611 return $.when.apply($, defs).then(function() {
612 if (self.on_change_list.length !== 0) {
615 var save_obj = self.save_list.pop();
617 return self._process_save(save_obj).then(function() {
618 save_obj.ret = _.toArray(arguments);
621 save_obj.error = true;
630 _internal_set_values: function(values, exclude) {
631 exclude = exclude || [];
632 for (var f in values) {
633 if (!values.hasOwnProperty(f)) { continue; }
634 var field = this.fields[f];
635 // If field is not defined in the view, just ignore it
637 var value_ = values[f];
638 if (field.get_value() != value_) {
639 field._inhibit_on_change_flag = true;
640 field.set_value(value_);
641 field._inhibit_on_change_flag = false;
642 field._dirty_flag = true;
643 if (!_.contains(exclude, field.name)) {
644 this.do_onchange(field, exclude);
649 this.on_form_changed();
651 set_values: function(values) {
653 return this.mutating_mutex.exec(function() {
654 self._internal_set_values(values);
658 * Ask the view to switch to view mode if possible. The view may not do it
659 * if the current record is not yet saved. It will then stay in create mode.
661 to_view_mode: function() {
662 this._actualize_mode("view");
665 * Ask the view to switch to edit mode if possible. The view may not do it
666 * if the current record is not yet saved. It will then stay in create mode.
668 to_edit_mode: function() {
669 this._actualize_mode("edit");
672 * Ask the view to switch to a precise mode if possible. The view is free to
673 * not respect this command if the state of the dataset is not compatible with
674 * the new mode. For example, it is not possible to switch to edit mode if
675 * the current record is not yet saved in database.
677 * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
678 * undefined the view will test the actual mode to check if it is still consistent
679 * with the dataset state.
681 _actualize_mode: function(switch_to) {
682 var mode = switch_to || this.get("actual_mode");
683 if (! this.datarecord.id) {
685 } else if (mode === "create") {
688 this.set({actual_mode: mode});
690 check_actual_mode: function(source, options) {
692 if(this.get("actual_mode") === "view") {
693 self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
694 self.$buttons.find('.oe_form_buttons_edit').hide();
695 self.$buttons.find('.oe_form_buttons_view').show();
696 self.$sidebar.show();
698 self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
699 self.$buttons.find('.oe_form_buttons_edit').show();
700 self.$buttons.find('.oe_form_buttons_view').hide();
701 self.$sidebar.hide();
705 autofocus: function() {
706 if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
707 var fields_order = this.fields_order.slice(0);
708 if (this.default_focus_field) {
709 fields_order.unshift(this.default_focus_field.name);
711 for (var i = 0; i < fields_order.length; i += 1) {
712 var field = this.fields[fields_order[i]];
713 if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
714 if (field.focus() !== false) {
721 on_button_save: function() {
723 return this.save().done(function(result) {
724 self.trigger("save", result);
728 on_button_cancel: function(event) {
729 if (this.can_be_discarded()) {
730 if (this.get('actual_mode') === 'create') {
731 this.trigger('history_back');
734 this.trigger('load_record', this.datarecord);
737 this.trigger('on_button_cancel');
740 on_button_new: function() {
743 return $.when(this.has_been_loaded).then(function() {
744 if (self.can_be_discarded()) {
745 return self.load_defaults();
749 on_button_edit: function() {
750 return this.to_edit_mode();
752 on_button_create: function() {
753 this.dataset.index = null;
756 on_button_duplicate: function() {
758 return this.has_been_loaded.then(function() {
759 return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
760 self.record_created(new_id);
765 on_button_delete: function() {
767 var def = $.Deferred();
768 this.has_been_loaded.done(function() {
769 if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
770 self.dataset.unlink([self.datarecord.id]).done(function() {
771 self.execute_pager_action('next');
775 $.async_when().done(function () {
780 return def.promise();
782 can_be_discarded: function() {
783 if (this.$el.is('.oe_form_dirty')) {
784 if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
787 this.$el.removeClass('oe_form_dirty');
792 * Triggers saving the form's record. Chooses between creating a new
793 * record or saving an existing one depending on whether the record
794 * already has an id property.
796 * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
797 * record, should that record be inserted at the start of the dataset (by
798 * default, records are added at the end)
800 save: function(prepend_on_create) {
802 var save_obj = {prepend_on_create: prepend_on_create, ret: null};
803 this.save_list.push(save_obj);
804 return this._process_operations().then(function() {
806 return $.Deferred().reject();
807 return $.when.apply($, save_obj.ret);
810 _process_save: function(save_obj) {
812 var prepend_on_create = save_obj.prepend_on_create;
814 var form_invalid = false,
816 first_invalid_field = null;
817 for (var f in self.fields) {
818 if (!self.fields.hasOwnProperty(f)) { continue; }
822 if (!first_invalid_field) {
823 first_invalid_field = f;
825 } else if (f.name !== 'id' && !f.get("readonly") && (!self.datarecord.id || f._dirty_flag)) {
826 // Special case 'id' field, do not save this field
827 // on 'create' : save all non readonly fields
828 // on 'edit' : save non readonly modified fields
829 values[f.name] = f.get_value();
833 self.set({'display_invalid_fields': true});
834 first_invalid_field.focus();
836 return $.Deferred().reject();
838 self.set({'display_invalid_fields': false});
840 if (!self.datarecord.id) {
842 save_deferral = self.dataset.create(values).then(function(r) {
843 return self.record_created(r, prepend_on_create);
845 } else if (_.isEmpty(values) && ! self.force_dirty) {
846 // Not dirty, noop save
847 save_deferral = $.Deferred().resolve({}).promise();
849 self.force_dirty = false;
851 save_deferral = self.dataset.write(self.datarecord.id, values, {}).then(function(r) {
852 return self.record_saved(r);
855 return save_deferral;
859 return $.Deferred().reject();
862 on_invalid: function() {
863 var warnings = _(this.fields).chain()
864 .filter(function (f) { return !f.is_valid(); })
866 return _.str.sprintf('<li>%s</li>',
869 warnings.unshift('<ul>');
870 warnings.push('</ul>');
871 this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
874 * Reload the form after saving
876 * @param {Object} r result of the write function.
878 record_saved: function(r) {
881 // should not happen in the server, but may happen for internal purpose
882 this.trigger('record_saved', r);
883 return $.Deferred().reject();
885 return $.when(this.reload()).then(function () {
886 self.trigger('record_saved', r);
892 * Updates the form' dataset to contain the new record:
894 * * Adds the newly created record to the current dataset (at the end by
896 * * Selects that record (sets the dataset's index to point to the new
898 * * Updates the pager and sidebar displays
901 * @param {Boolean} [prepend_on_create=false] adds the newly created record
902 * at the beginning of the dataset instead of the end
904 record_created: function(r, prepend_on_create) {
907 // should not happen in the server, but may happen for internal purpose
908 this.trigger('record_created', r);
909 return $.Deferred().reject();
911 this.datarecord.id = r;
912 if (!prepend_on_create) {
913 this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
914 this.dataset.index = this.dataset.ids.length - 1;
916 this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
917 this.dataset.index = 0;
919 this.do_update_pager();
921 this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
923 //openerp.log("The record has been created with id #" + this.datarecord.id);
924 return $.when(this.reload()).then(function () {
925 self.trigger('record_created', r);
926 return _.extend(r, {created: true});
930 on_action: function (action) {
931 console.debug('Executing action', action);
935 return this.reload_mutex.exec(function() {
936 if (self.dataset.index == null) {
937 self.trigger("previous_view");
938 return $.Deferred().reject().promise();
940 if (self.dataset.index == null || self.dataset.index < 0) {
941 return $.when(self.on_button_new());
943 var fields = _.keys(self.fields_view.fields);
944 fields.push('display_name');
945 return self.dataset.read_index(fields,
949 'future_display_name': true
951 }).then(function(r) {
952 self.trigger('load_record', r);
957 get_widgets: function() {
958 return _.filter(this.getChildren(), function(obj) {
959 return obj instanceof instance.web.form.FormWidget;
962 get_fields_values: function() {
964 var ids = this.get_selected_ids();
965 values["id"] = ids.length > 0 ? ids[0] : false;
966 _.each(this.fields, function(value_, key) {
967 values[key] = value_.get_value();
971 get_selected_ids: function() {
972 var id = this.dataset.ids[this.dataset.index];
973 return id ? [id] : [];
975 recursive_save: function() {
977 return $.when(this.save()).then(function(res) {
978 if (self.dataset.parent_view)
979 return self.dataset.parent_view.recursive_save();
982 recursive_reload: function() {
985 if (self.dataset.parent_view)
986 pre = self.dataset.parent_view.recursive_reload();
987 return pre.then(function() {
988 return self.reload();
991 is_dirty: function() {
992 return _.any(this.fields, function (value_) {
993 return value_._dirty_flag;
996 is_interactible_record: function() {
997 var id = this.datarecord.id;
999 if (this.options.not_interactible_on_create)
1001 } else if (typeof(id) === "string") {
1002 if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
1007 sidebar_eval_context: function () {
1008 return $.when(this.build_eval_context());
1010 open_defaults_dialog: function () {
1012 var display = function (field, value) {
1013 if (field instanceof instance.web.form.FieldSelection) {
1014 return _(field.values).find(function (option) {
1015 return option[0] === value;
1017 } else if (field instanceof instance.web.form.FieldMany2One) {
1018 return field.get_displayed();
1022 var fields = _.chain(this.fields)
1023 .map(function (field, name) {
1024 var value = field.get_value();
1025 // ignore fields which are empty, invisible, readonly, o2m
1028 || field.get('invisible')
1029 || field.get("readonly")
1030 || field.field.type === 'one2many'
1031 || field.field.type === 'many2many'
1032 || field.field.type === 'binary'
1033 || field.password) {
1039 string: field.string,
1041 displayed: display(field, value),
1045 .sortBy(function (field) { return field.string; })
1047 var conditions = _.chain(self.fields)
1048 .filter(function (field) { return field.field.change_default; })
1049 .map(function (field, name) {
1050 var value = field.get_value();
1053 string: field.string,
1055 displayed: display(field, value),
1060 var d = new instance.web.Dialog(this, {
1061 title: _t("Set Default"),
1064 conditions: conditions
1067 {text: _t("Close"), click: function () { d.close(); }},
1068 {text: _t("Save default"), click: function () {
1069 var $defaults = d.$el.find('#formview_default_fields');
1070 var field_to_set = $defaults.val();
1071 if (!field_to_set) {
1072 $defaults.parent().addClass('oe_form_invalid');
1075 var condition = d.$el.find('#formview_default_conditions').val(),
1076 all_users = d.$el.find('#formview_default_all').is(':checked');
1077 new instance.web.DataSet(self, 'ir.values').call(
1081 self.fields[field_to_set].get_value(),
1085 ]).done(function () { d.close(); });
1089 d.template = 'FormView.set_default';
1092 register_field: function(field, name) {
1093 this.fields[name] = field;
1094 this.fields_order.push(name);
1095 if (JSON.parse(field.node.attrs.default_focus || "0")) {
1096 this.default_focus_field = field;
1099 field.on('focused', null, this.proxy('widgetFocused'))
1100 .on('blurred', null, this.proxy('widgetBlurred'));
1101 if (this.get_field_desc(name).translate) {
1102 this.translatable_fields.push(field);
1104 field.on('changed_value', this, function() {
1105 if (field.is_syntax_valid()) {
1106 this.trigger('field_changed:' + name);
1108 if (field._inhibit_on_change_flag) {
1111 field._dirty_flag = true;
1112 if (field.is_syntax_valid()) {
1113 this.do_onchange(field);
1114 this.on_form_changed(true);
1115 this.do_notify_change();
1119 get_field_desc: function(field_name) {
1120 return this.fields_view.fields[field_name];
1122 get_field_value: function(field_name) {
1123 return this.fields[field_name].get_value();
1125 compute_domain: function(expression) {
1126 return instance.web.form.compute_domain(expression, this.fields);
1128 _build_view_fields_values: function() {
1129 var a_dataset = this.dataset;
1130 var fields_values = this.get_fields_values();
1131 var active_id = a_dataset.ids[a_dataset.index];
1132 _.extend(fields_values, {
1133 active_id: active_id || false,
1134 active_ids: active_id ? [active_id] : [],
1135 active_model: a_dataset.model,
1138 if (a_dataset.parent_view) {
1139 fields_values.parent = a_dataset.parent_view.get_fields_values();
1141 return fields_values;
1143 build_eval_context: function() {
1144 var a_dataset = this.dataset;
1145 return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
1150 * Interface to be implemented by rendering engines for the form view.
1152 instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
1153 set_fields_view: function(fields_view) {},
1154 set_fields_registry: function(fields_registry) {},
1155 render_to: function($el) {},
1159 * Default rendering engine for the form view.
1161 * It is necessary to set the view using set_view() before usage.
1163 instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
1164 init: function(view) {
1167 set_fields_view: function(fvg) {
1169 this.version = parseFloat(this.fvg.arch.attrs.version);
1170 if (isNaN(this.version)) {
1174 set_tags_registry: function(tags_registry) {
1175 this.tags_registry = tags_registry;
1177 set_fields_registry: function(fields_registry) {
1178 this.fields_registry = fields_registry;
1180 set_widgets_registry: function(widgets_registry) {
1181 this.widgets_registry = widgets_registry;
1183 // Backward compatibility tools, current default version: v6.1
1184 process_version: function() {
1185 if (this.version < 7.0) {
1186 this.$form.find('form:first').wrapInner('<group col="4"/>');
1187 this.$form.find('page').each(function() {
1188 if (!$(this).parents('field').length) {
1189 $(this).wrapInner('<group col="4"/>');
1194 get_arch_fragment: function() {
1195 var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
1196 // IE won't allow custom button@type and will revert it to spec default : 'submit'
1197 $('button', doc).each(function() {
1198 $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
1200 return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
1202 render_to: function($target) {
1204 this.$target = $target;
1206 this.$form = this.get_arch_fragment();
1208 this.process_version();
1210 this.fields_to_init = [];
1211 this.tags_to_init = [];
1212 this.widgets_to_init = [];
1214 this.process(this.$form);
1216 this.$form.appendTo(this.$target);
1218 this.to_replace = [];
1220 _.each(this.fields_to_init, function($elem) {
1221 var name = $elem.attr("name");
1222 if (!self.fvg.fields[name]) {
1223 throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
1225 var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
1227 throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
1229 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1230 var $label = self.labels[$elem.attr("name")];
1232 w.set_input_id($label.attr("for"));
1234 self.alter_field(w);
1235 self.view.register_field(w, $elem.attr("name"));
1236 self.to_replace.push([w, $elem]);
1238 _.each(this.tags_to_init, function($elem) {
1239 var tag_name = $elem[0].tagName.toLowerCase();
1240 var obj = self.tags_registry.get_object(tag_name);
1241 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1242 self.to_replace.push([w, $elem]);
1244 _.each(this.widgets_to_init, function($elem) {
1245 var widget_type = $elem.attr("type");
1246 var obj = self.widgets_registry.get_object(widget_type);
1247 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1248 self.to_replace.push([w, $elem]);
1251 init_fields: function() {
1253 _.each(this.to_replace, function(el) {
1254 defs.push(el[0].replace(el[1]));
1256 this.to_replace = [];
1257 return $.when.apply($, defs);
1259 render_element: function(template /* dictionaries */) {
1260 var dicts = [].slice.call(arguments).slice(1);
1261 var dict = _.extend.apply(_, dicts);
1262 dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
1263 return $(QWeb.render(template, dict));
1265 alter_field: function(field) {
1267 toggle_layout_debugging: function() {
1268 if (!this.$target.has('.oe_layout_debug_cell:first').length) {
1269 this.$target.find('[title]').removeAttr('title');
1270 this.$target.find('.oe_form_group_cell').each(function() {
1271 var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
1272 $(this).attr('title', text);
1275 this.$target.toggleClass('oe_layout_debugging');
1277 process: function($tag) {
1279 var tagname = $tag[0].nodeName.toLowerCase();
1280 if (this.tags_registry.contains(tagname)) {
1281 this.tags_to_init.push($tag);
1284 var fn = self['process_' + tagname];
1286 var args = [].slice.call(arguments);
1288 return fn.apply(self, args);
1290 // generic tag handling, just process children
1291 $tag.children().each(function() {
1292 self.process($(this));
1294 self.handle_common_properties($tag, $tag);
1295 $tag.removeAttr("modifiers");
1299 process_widget: function($widget) {
1300 this.widgets_to_init.push($widget);
1303 process_sheet: function($sheet) {
1304 var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
1305 this.handle_common_properties($new_sheet, $sheet);
1306 var $dst = $new_sheet.find('.oe_form_sheet');
1307 $sheet.contents().appendTo($dst);
1308 $sheet.before($new_sheet).remove();
1309 this.process($new_sheet);
1311 process_form: function($form) {
1312 if ($form.find('> sheet').length === 0) {
1313 $form.addClass('oe_form_nosheet');
1315 var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
1316 this.handle_common_properties($new_form, $form);
1317 $form.contents().appendTo($new_form);
1318 if ($form[0] === this.$form[0]) {
1319 // If root element, replace it
1320 this.$form = $new_form;
1322 $form.before($new_form).remove();
1324 this.process($new_form);
1327 * Used by direct <field> children of a <group> tag only
1328 * This method will add the implicit <label...> for every field
1331 preprocess_field: function($field) {
1333 var name = $field.attr('name'),
1334 field_colspan = parseInt($field.attr('colspan'), 10),
1335 field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
1337 if ($field.attr('nolabel') === '1')
1339 $field.attr('nolabel', '1');
1341 this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
1342 $(el).parents().each(function(unused, tag) {
1343 var name = tag.tagName.toLowerCase();
1344 if (name === "field" || name in self.tags_registry.map)
1351 var $label = $('<label/>').attr({
1353 "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
1354 "string": $field.attr('string'),
1355 "help": $field.attr('help'),
1356 "class": $field.attr('class'),
1358 $label.insertBefore($field);
1359 if (field_colspan > 1) {
1360 $field.attr('colspan', field_colspan - 1);
1364 process_field: function($field) {
1365 if ($field.parent().is('group')) {
1366 // No implicit labels for normal fields, only for <group> direct children
1367 var $label = this.preprocess_field($field);
1369 this.process($label);
1372 this.fields_to_init.push($field);
1375 process_group: function($group) {
1377 $group.children('field').each(function() {
1378 self.preprocess_field($(this));
1380 var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
1382 if ($new_group.first().is('table.oe_form_group')) {
1383 $table = $new_group;
1384 } else if ($new_group.filter('table.oe_form_group').length) {
1385 $table = $new_group.filter('table.oe_form_group').first();
1387 $table = $new_group.find('table.oe_form_group').first();
1391 cols = parseInt($group.attr('col') || 2, 10),
1395 $group.children().each(function(a,b,c) {
1396 var $child = $(this);
1397 var colspan = parseInt($child.attr('colspan') || 1, 10);
1398 var tagName = $child[0].tagName.toLowerCase();
1399 var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
1400 var newline = tagName === 'newline';
1402 // Note FME: those classes are used in layout debug mode
1403 if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
1404 $tr.addClass('oe_form_group_row_incomplete');
1406 $tr.addClass('oe_form_group_row_newline');
1413 if (!$tr || row_cols < colspan) {
1414 $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
1416 } else if (tagName==='group') {
1417 // When <group> <group/><group/> </group>, we need a spacing between the two groups
1418 $td.addClass('oe_group_right')
1420 row_cols -= colspan;
1422 // invisibility transfer
1423 var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
1424 var invisible = field_modifiers.invisible;
1425 self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
1427 $tr.append($td.append($child));
1428 children.push($child[0]);
1430 if (row_cols && $td) {
1431 $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
1433 $group.before($new_group).remove();
1435 $table.find('> tbody > tr').each(function() {
1436 var to_compute = [],
1439 $(this).children().each(function() {
1441 $child = $td.children(':first');
1442 switch ($child[0].tagName.toLowerCase()) {
1446 if ($child.attr('for')) {
1447 $td.attr('width', '1%').addClass('oe_form_group_cell_label');
1448 row_cols-= $td.attr('colspan') || 1;
1453 var width = _.str.trim($child.attr('width') || ''),
1454 iwidth = parseInt(width, 10);
1456 if (width.substr(-1) === '%') {
1458 width = iwidth + '%';
1461 $td.css('min-width', width + 'px');
1463 $td.attr('width', width);
1464 $child.removeAttr('width');
1465 row_cols-= $td.attr('colspan') || 1;
1467 to_compute.push($td);
1473 var unit = Math.floor(total / row_cols);
1474 if (!$(this).is('.oe_form_group_row_incomplete')) {
1475 _.each(to_compute, function($td, i) {
1476 var width = parseInt($td.attr('colspan'), 10) * unit;
1477 $td.attr('width', width + '%');
1483 _.each(children, function(el) {
1484 self.process($(el));
1486 this.handle_common_properties($new_group, $group);
1489 process_notebook: function($notebook) {
1492 $notebook.find('> page').each(function() {
1493 var $page = $(this);
1494 var page_attrs = $page.getAttributes();
1495 page_attrs.id = _.uniqueId('notebook_page_');
1496 var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
1497 $page.contents().appendTo($new_page);
1498 $page.before($new_page).remove();
1499 var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
1500 page_attrs.__page = $new_page;
1501 page_attrs.__ic = ic;
1502 pages.push(page_attrs);
1504 $new_page.children().each(function() {
1505 self.process($(this));
1508 var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
1509 $notebook.contents().appendTo($new_notebook);
1510 $notebook.before($new_notebook).remove();
1511 self.process($($new_notebook.children()[0]));
1512 //tabs and invisibility handling
1513 $new_notebook.tabs();
1514 _.each(pages, function(page, i) {
1517 page.__ic.on("change:effective_invisible", null, function() {
1518 if (!page.__ic.get('effective_invisible')) {
1519 $new_notebook.tabs('select', i);
1522 var current = $new_notebook.tabs("option", "selected");
1523 if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
1525 var first_visible = _.find(_.range(pages.length), function(i2) {
1526 return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
1528 if (first_visible !== undefined) {
1529 $new_notebook.tabs('select', first_visible);
1534 this.handle_common_properties($new_notebook, $notebook);
1535 return $new_notebook;
1537 process_separator: function($separator) {
1538 var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
1539 $separator.before($new_separator).remove();
1540 this.handle_common_properties($new_separator, $separator);
1541 return $new_separator;
1543 process_label: function($label) {
1544 var name = $label.attr("for"),
1545 field_orm = this.fvg.fields[name];
1547 string: $label.attr('string') || (field_orm || {}).string || '',
1548 help: $label.attr('help') || (field_orm || {}).help || '',
1549 _for: name ? _.uniqueId('oe-field-input-') : undefined,
1551 var align = parseFloat(dict.align);
1552 if (isNaN(align) || align === 1) {
1554 } else if (align === 0) {
1560 var $new_label = this.render_element('FormRenderingLabel', dict);
1561 $label.before($new_label).remove();
1562 this.handle_common_properties($new_label, $label);
1564 this.labels[name] = $new_label;
1568 handle_common_properties: function($new_element, $node) {
1569 var str_modifiers = $node.attr("modifiers") || "{}";
1570 var modifiers = JSON.parse(str_modifiers);
1572 if (modifiers.invisible !== undefined)
1573 ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
1574 $new_element.addClass($node.attr("class") || "");
1575 $new_element.attr('style', $node.attr('style'));
1576 return {invisibility_changer: ic,};
1583 If you read this documentation, it probably means that you were asked to use a form view widget outside of
1584 a form view. Before going further, you must understand that those fields were never really created for
1585 that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
1586 you to hack the system with more style.
1588 instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
1589 init: function(parent, eval_context) {
1590 this._super(parent);
1591 this.field_descs = {};
1592 this.eval_context = eval_context || {};
1594 display_invalid_fields: false,
1595 actual_mode: 'create',
1598 get_field_desc: function(field_name) {
1599 if (this.field_descs[field_name] === undefined) {
1600 this.field_descs[field_name] = {
1604 return this.field_descs[field_name];
1606 extend_field_desc: function(fields) {
1608 _.each(fields, function(v, k) {
1609 _.extend(self.get_field_desc(k), v);
1612 get_field_value: function(field_name) {
1615 set_values: function(values) {
1618 compute_domain: function(expression) {
1619 return instance.web.form.compute_domain(expression, {});
1621 build_eval_context: function() {
1622 return new instance.web.CompoundContext(this.eval_context);
1626 instance.web.form.compute_domain = function(expr, fields) {
1627 if (! (expr instanceof Array))
1630 for (var i = expr.length - 1; i >= 0; i--) {
1632 if (ex.length == 1) {
1633 var top = stack.pop();
1636 stack.push(stack.pop() || top);
1639 stack.push(stack.pop() && top);
1645 throw new Error(_.str.sprintf(
1646 _t("Unknown operator %s in domain %s"),
1647 ex, JSON.stringify(expr)));
1651 var field = fields[ex[0]];
1653 throw new Error(_.str.sprintf(
1654 _t("Unknown field %s in domain %s"),
1655 ex[0], JSON.stringify(expr)));
1657 var field_value = field.get_value ? field.get_value() : field.value;
1661 switch (op.toLowerCase()) {
1664 stack.push(_.isEqual(field_value, val));
1668 stack.push(!_.isEqual(field_value, val));
1671 stack.push(field_value < val);
1674 stack.push(field_value > val);
1677 stack.push(field_value <= val);
1680 stack.push(field_value >= val);
1683 if (!_.isArray(val)) val = [val];
1684 stack.push(_(val).contains(field_value));
1687 if (!_.isArray(val)) val = [val];
1688 stack.push(!_(val).contains(field_value));
1692 _t("Unsupported operator %s in domain %s"),
1693 op, JSON.stringify(expr));
1696 return _.all(stack, _.identity);
1699 instance.web.form.is_bin_size = function(v) {
1700 return /^\d+(\.\d*)? \w+$/.test(v);
1704 * Must be applied over an class already possessing the PropertiesMixin.
1706 * Apply the result of the "invisible" domain to this.$el.
1708 instance.web.form.InvisibilityChangerMixin = {
1709 init: function(field_manager, invisible_domain) {
1711 this._ic_field_manager = field_manager;
1712 this._ic_invisible_modifier = invisible_domain;
1713 this._ic_field_manager.on("view_content_has_changed", this, function() {
1714 var result = self._ic_invisible_modifier === undefined ? false :
1715 self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
1716 self.set({"invisible": result});
1718 this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
1719 var check = function() {
1720 if (self.get("invisible") || self.get('force_invisible')) {
1721 self.set({"effective_invisible": true});
1723 self.set({"effective_invisible": false});
1726 this.on('change:invisible', this, check);
1727 this.on('change:force_invisible', this, check);
1731 this.on("change:effective_invisible", this, this._check_visibility);
1732 this._check_visibility();
1734 _check_visibility: function() {
1735 this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
1739 instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
1740 init: function(parent, field_manager, invisible_domain, $el) {
1741 this.setParent(parent);
1742 instance.web.PropertiesMixin.init.call(this);
1743 instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
1750 Base class for all fields, custom widgets and buttons to be displayed in the form view.
1753 - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
1754 the values of the "readonly" property and the "mode" property on the field manager.
1756 instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
1758 * @constructs instance.web.form.FormWidget
1759 * @extends instance.web.Widget
1761 * @param field_manager
1764 init: function(field_manager, node) {
1765 this._super(field_manager);
1766 this.field_manager = field_manager;
1767 if (this.field_manager instanceof instance.web.FormView)
1768 this.view = this.field_manager;
1770 this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
1771 instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
1773 this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
1779 // some events to make the property "effective_readonly" sync automatically with "readonly" and
1780 // "mode" on field_manager
1782 var test_effective_readonly = function() {
1783 self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
1785 this.on("change:readonly", this, test_effective_readonly);
1786 this.field_manager.on("change:actual_mode", this, test_effective_readonly);
1787 test_effective_readonly.call(this);
1789 renderElement: function() {
1790 this.process_modifiers();
1792 this.$el.addClass(this.node.attrs["class"] || "");
1794 destroy: function() {
1796 this._super.apply(this, arguments);
1799 * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
1801 * This method is an utility method that is meant to be called by child classes.
1803 * @param {jQuery} $e jQuery object of elements to bind focus/blur on
1805 setupFocus: function ($e) {
1808 focus: function () { self.trigger('focused'); },
1809 blur: function () { self.trigger('blurred'); }
1812 process_modifiers: function() {
1814 for (var a in this.modifiers) {
1815 if (!this.modifiers.hasOwnProperty(a)) { continue; }
1816 if (!_.include(["invisible"], a)) {
1817 var val = this.field_manager.compute_domain(this.modifiers[a]);
1823 do_attach_tooltip: function(widget, trigger, options) {
1824 widget = widget || this;
1825 trigger = trigger || this.$el;
1826 options = _.extend({
1831 var template = widget.template + '.tooltip';
1832 if (!QWeb.has_template(template)) {
1833 template = 'WidgetLabel.tooltip';
1835 return QWeb.render(template, {
1836 debug: instance.session.debug,
1839 gravity: $.fn.tipsy.autoBounds(50, 'nw'),
1844 $(trigger).tipsy(options);
1847 * Builds a new context usable for operations related to fields by merging
1848 * the fields'context with the action's context.
1850 build_context: function() {
1851 // only use the model's context if there is not context on the node
1852 var v_context = this.node.attrs.context;
1854 v_context = (this.field || {}).context || {};
1857 if (v_context.__ref || true) { //TODO: remove true
1858 var fields_values = this.field_manager.build_eval_context();
1859 v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
1863 build_domain: function() {
1864 var f_domain = this.field.domain || [];
1865 var n_domain = this.node.attrs.domain || null;
1866 // if there is a domain on the node, overrides the model's domain
1867 var final_domain = n_domain !== null ? n_domain : f_domain;
1868 if (!(final_domain instanceof Array) || true) { //TODO: remove true
1869 var fields_values = this.field_manager.build_eval_context();
1870 final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
1872 return final_domain;
1876 instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
1877 template: 'WidgetButton',
1878 init: function(field_manager, node) {
1879 node.attrs.type = node.attrs['data-button-type'];
1880 this._super(field_manager, node);
1881 this.force_disabled = false;
1882 this.string = (this.node.attrs.string || '').replace(/_/g, '');
1883 if (JSON.parse(this.node.attrs.default_focus || "0")) {
1884 // TODO fme: provide enter key binding to widgets
1885 this.view.default_focus_button = this;
1887 if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
1888 this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
1890 this.view.on('view_content_has_changed', this, this.check_disable);
1893 this._super.apply(this, arguments);
1894 this.$el.click(this.on_click);
1895 if (this.node.attrs.help || instance.session.debug) {
1896 this.do_attach_tooltip();
1898 this.setupFocus(this.$el);
1900 on_click: function() {
1902 this.force_disabled = true;
1903 this.check_disable();
1904 this.execute_action().always(function() {
1905 self.force_disabled = false;
1906 self.check_disable();
1909 execute_action: function() {
1911 var exec_action = function() {
1912 if (self.node.attrs.confirm) {
1913 var def = $.Deferred();
1914 var dialog = instance.web.dialog($('<div/>').text(self.node.attrs.confirm), {
1915 title: _t('Confirm'),
1918 {text: _t("Cancel"), click: function() {
1920 $(this).dialog("close");
1923 {text: _t("Ok"), click: function() {
1924 self.on_confirmed().done(function() {
1927 $(this).dialog("close");
1932 return def.promise();
1934 return self.on_confirmed();
1937 if (!this.node.attrs.special) {
1938 this.view.force_dirty = true;
1939 return this.view.recursive_save().then(exec_action);
1941 return exec_action();
1944 on_confirmed: function() {
1947 var context = this.build_context();
1949 return this.view.do_execute_action(
1950 _.extend({}, this.node.attrs, {context: context}),
1951 this.view.dataset, this.view.datarecord.id, function () {
1952 self.view.recursive_reload();
1955 check_disable: function() {
1956 var disabled = (this.force_disabled || !this.view.is_interactible_record());
1957 this.$el.prop('disabled', disabled);
1958 this.$el.css('color', disabled ? 'grey' : '');
1963 * Interface to be implemented by fields.
1966 * - changed_value: triggered when the value of the field has changed. This can be due
1967 * to a user interaction or a call to set_value().
1970 instance.web.form.FieldInterface = {
1972 * Constructor takes 2 arguments:
1973 * - field_manager: Implements FieldManagerMixin
1974 * - node: the "<field>" node in json form
1976 init: function(field_manager, node) {},
1978 * Called by the form view to indicate the value of the field.
1980 * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
1981 * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
1982 * before the widget is inserted into the DOM.
1984 * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
1985 * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
1986 * also be able to handle any other format commonly used in the _defaults key on the models in the addons
1987 * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
1988 * no information is ever given to know which format is used.
1990 set_value: function(value_) {},
1992 * Get the current value of the widget.
1994 * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
1995 * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
1996 * For example if the field is marked as "required", a call to get_value() can return false.
1998 * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
1999 * return a default value according to the type of field.
2001 * This method is always assumed to perform synchronously, it can not return a promise.
2003 * If there was no user interaction to modify the value of the field, it is always assumed that
2004 * get_value() return the same semantic value than the one passed in the last call to set_value(),
2005 * although the syntax can be different. This can be the case for type of fields that have a different
2006 * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
2008 get_value: function() {},
2010 * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
2013 set_input_id: function(id) {},
2015 * Returns true if is_syntax_valid() returns true and the value is semantically
2016 * valid too according to the semantic restrictions applied to the field.
2018 is_valid: function() {},
2020 * Returns true if the field holds a value which is syntactically correct, ignoring
2021 * the potential semantic restrictions applied to the field.
2023 is_syntax_valid: function() {},
2025 * Must set the focus on the field. Return false if field is not focusable.
2027 focus: function() {},
2029 * Called when the translate button is clicked.
2031 on_translate: function() {},
2033 This method is called by the form view before reading on_change values and before saving. It tells
2034 the field to save its value before reading it using get_value(). Must return a promise.
2036 commit_value: function() {},
2040 * Abstract class for classes implementing FieldInterface.
2043 * - value: useful property to hold the value of the field. By default, set_value() and get_value()
2044 * set and retrieve the value property. Changing the value property also triggers automatically
2045 * a 'changed_value' event that inform the view to trigger on_changes.
2048 instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
2050 * @constructs instance.web.form.AbstractField
2051 * @extends instance.web.form.FormWidget
2053 * @param field_manager
2056 init: function(field_manager, node) {
2058 this._super(field_manager, node);
2059 this.name = this.node.attrs.name;
2060 this.field = this.field_manager.get_field_desc(this.name);
2061 this.widget = this.node.attrs.widget;
2062 this.string = this.node.attrs.string || this.field.string || this.name;
2063 this.options = instance.web.py_eval(this.node.attrs.options || '{}');
2064 this.set({'value': false});
2066 this.on("change:value", this, function() {
2067 this.trigger('changed_value');
2068 this._check_css_flags();
2071 renderElement: function() {
2074 if (this.field.translate && this.view) {
2075 this.$el.addClass('oe_form_field_translatable');
2076 this.$el.find('.oe_field_translate').click(this.on_translate);
2078 this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
2079 if (instance.session.debug) {
2080 this.do_attach_tooltip(this, this.$label[0] || this.$el);
2081 this.$label.off('dblclick').on('dblclick', function() {
2082 console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
2084 console.log("window.w =", window.w);
2087 if (!this.disable_utility_classes) {
2088 this.off("change:required", this, this._set_required);
2089 this.on("change:required", this, this._set_required);
2090 this._set_required();
2092 this._check_visibility();
2093 this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
2094 this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
2095 this._check_css_flags();
2098 var tmp = this._super();
2099 this.on("change:value", this, function() {
2100 if (! this.no_rerender)
2101 this.render_value();
2103 this.render_value();
2106 * Private. Do not use.
2108 _set_required: function() {
2109 this.$el.toggleClass('oe_form_required', this.get("required"));
2111 set_value: function(value_) {
2112 this.set({'value': value_});
2114 get_value: function() {
2115 return this.get('value');
2118 Utility method that all implementations should use to change the
2119 value without triggering a re-rendering.
2121 internal_set_value: function(value_) {
2122 var tmp = this.no_render;
2123 this.no_rerender = true;
2124 this.set({'value': value_});
2125 this.no_rerender = tmp;
2128 This method is called each time the value is modified.
2130 render_value: function() {},
2131 is_valid: function() {
2132 return this.is_syntax_valid() && !(this.get('required') && this.is_false());
2134 is_syntax_valid: function() {
2138 * Method useful to implement to ease validity testing. Must return true if the current
2139 * value is similar to false in OpenERP.
2141 is_false: function() {
2142 return this.get('value') === false;
2144 _check_css_flags: function() {
2145 if (this.field.translate) {
2146 this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
2148 if (!this.disable_utility_classes) {
2149 if (this.field_manager.get('display_invalid_fields')) {
2150 this.$el.toggleClass('oe_form_invalid', !this.is_valid());
2157 set_input_id: function(id) {
2158 this.id_for_label = id;
2160 on_translate: function() {
2162 var trans = new instance.web.DataSet(this, 'ir.translation');
2163 return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
2168 set_dimensions: function (height, width) {
2174 commit_value: function() {
2180 * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
2183 instance.web.form.ReinitializeWidgetMixin = {
2185 * Default implementation of, you should not override it, use initialize_field() instead.
2188 this.initialize_field();
2191 initialize_field: function() {
2192 this.on("change:effective_readonly", this, this.reinitialize);
2193 this.initialize_content();
2195 reinitialize: function() {
2196 this.destroy_content();
2197 this.renderElement();
2198 this.initialize_content();
2201 * Called to destroy anything that could have been created previously, called before a
2202 * re-initialization.
2204 destroy_content: function() {},
2206 * Called to initialize the content.
2208 initialize_content: function() {},
2212 * A mixin to apply on any field that has to completely re-render when its readonly state
2215 instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
2216 reinitialize: function() {
2217 instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
2218 this.render_value();
2222 instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2223 template: 'FieldChar',
2224 widget_class: 'oe_form_field_char',
2226 'change input': 'store_dom_value',
2228 init: function (field_manager, node) {
2229 this._super(field_manager, node);
2230 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2232 initialize_content: function() {
2233 this.setupFocus(this.$('input'));
2235 store_dom_value: function () {
2236 if (!this.get('effective_readonly')
2237 && this.$('input').length
2238 && this.is_syntax_valid()) {
2239 this.internal_set_value(
2241 this.$('input').val()));
2244 commit_value: function () {
2245 this.store_dom_value();
2246 return this._super();
2248 render_value: function() {
2249 var show_value = this.format_value(this.get('value'), '');
2250 if (!this.get("effective_readonly")) {
2251 this.$el.find('input').val(show_value);
2253 if (this.password) {
2254 show_value = new Array(show_value.length + 1).join('*');
2256 this.$(".oe_form_char_content").text(show_value);
2259 is_syntax_valid: function() {
2260 if (!this.get("effective_readonly") && this.$("input").size() > 0) {
2262 this.parse_value(this.$('input').val(), '');
2270 parse_value: function(val, def) {
2271 return instance.web.parse_value(val, this, def);
2273 format_value: function(val, def) {
2274 return instance.web.format_value(val, this, def);
2276 is_false: function() {
2277 return this.get('value') === '' || this._super();
2280 this.$('input:first')[0].focus();
2282 set_dimensions: function (height, width) {
2283 this._super(height, width);
2284 this.$('input').css({
2291 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2292 process_modifiers: function () {
2294 this.set({ readonly: true });
2298 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2299 template: 'FieldEmail',
2300 initialize_content: function() {
2302 var $button = this.$el.find('button');
2303 $button.click(this.on_button_clicked);
2304 this.setupFocus($button);
2306 render_value: function() {
2307 if (!this.get("effective_readonly")) {
2311 .attr('href', 'mailto:' + this.get('value'))
2312 .text(this.get('value') || '');
2315 on_button_clicked: function() {
2316 if (!this.get('value') || !this.is_syntax_valid()) {
2317 this.do_warn(_t("E-mail error"), _t("Can't send email to invalid e-mail address"));
2319 location.href = 'mailto:' + this.get('value');
2324 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2325 template: 'FieldUrl',
2326 initialize_content: function() {
2328 var $button = this.$el.find('button');
2329 $button.click(this.on_button_clicked);
2330 this.setupFocus($button);
2332 render_value: function() {
2333 if (!this.get("effective_readonly")) {
2336 var tmp = this.get('value');
2337 var s = /(\w+):(.+)/.exec(tmp);
2339 tmp = "http://" + this.get('value');
2341 this.$el.find('a').attr('href', tmp).text(this.get('value') ? tmp : '');
2344 on_button_clicked: function() {
2345 if (!this.get('value')) {
2346 this.do_warn(_t("Resource error"), _t("This resource is empty"));
2348 var url = $.trim(this.get('value'));
2349 if(/^www\./i.test(url))
2350 url = 'http://'+url;
2356 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2357 is_field_number: true,
2358 widget_class: 'oe_form_field_float',
2359 init: function (field_manager, node) {
2360 this._super(field_manager, node);
2361 this.internal_set_value(0);
2362 if (this.node.attrs.digits) {
2363 this.digits = this.node.attrs.digits;
2365 this.digits = this.field.digits;
2368 set_value: function(value_) {
2369 if (value_ === false || value_ === undefined) {
2370 // As in GTK client, floats default to 0
2373 this._super.apply(this, [value_]);
2375 focus: function () {
2376 this.$('input:first').select();
2380 instance.web.DateTimeWidget = instance.web.Widget.extend({
2381 template: "web.datepicker",
2382 jqueryui_object: 'datetimepicker',
2383 type_of_date: "datetime",
2385 'change .oe_datepicker_master': 'change_datetime',
2387 init: function(parent) {
2388 this._super(parent);
2389 this.name = parent.name;
2393 this.$input = this.$el.find('input.oe_datepicker_master');
2394 this.$input_picker = this.$el.find('input.oe_datepicker_container');
2397 onClose: this.on_picker_select,
2398 onSelect: this.on_picker_select,
2402 showButtonPanel: true,
2403 firstDay: Date.CultureInfo.firstDayOfWeek
2405 this.$el.find('img.oe_datepicker_trigger').click(function() {
2406 if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
2407 self.$input.focus();
2410 self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date());
2411 self.$input_picker.show();
2412 self.picker('show');
2413 self.$input_picker.hide();
2415 this.set_readonly(false);
2416 this.set({'value': false});
2418 picker: function() {
2419 return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
2421 on_picker_select: function(text, instance_) {
2422 var date = this.picker('getDate');
2424 .val(date ? this.format_client(date) : '')
2428 set_value: function(value_) {
2429 this.set({'value': value_});
2430 this.$input.val(value_ ? this.format_client(value_) : '');
2432 get_value: function() {
2433 return this.get('value');
2435 set_value_from_ui_: function() {
2436 var value_ = this.$input.val() || false;
2437 this.set({'value': this.parse_client(value_)});
2439 set_readonly: function(readonly) {
2440 this.readonly = readonly;
2441 this.$input.prop('readonly', this.readonly);
2442 this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
2444 is_valid_: function() {
2445 var value_ = this.$input.val();
2446 if (value_ === "") {
2450 this.parse_client(value_);
2457 parse_client: function(v) {
2458 return instance.web.parse_value(v, {"widget": this.type_of_date});
2460 format_client: function(v) {
2461 return instance.web.format_value(v, {"widget": this.type_of_date});
2463 change_datetime: function() {
2464 if (this.is_valid_()) {
2465 this.set_value_from_ui_();
2466 this.trigger("datetime_changed");
2469 commit_value: function () {
2470 this.change_datetime();
2474 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2475 jqueryui_object: 'datepicker',
2476 type_of_date: "date"
2479 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2480 template: "FieldDatetime",
2481 build_widget: function() {
2482 return new instance.web.DateTimeWidget(this);
2484 destroy_content: function() {
2485 if (this.datewidget) {
2486 this.datewidget.destroy();
2487 this.datewidget = undefined;
2490 initialize_content: function() {
2491 if (!this.get("effective_readonly")) {
2492 this.datewidget = this.build_widget();
2493 this.datewidget.on('datetime_changed', this, _.bind(function() {
2494 this.internal_set_value(this.datewidget.get_value());
2496 this.datewidget.appendTo(this.$el);
2497 this.setupFocus(this.datewidget.$input);
2500 render_value: function() {
2501 if (!this.get("effective_readonly")) {
2502 this.datewidget.set_value(this.get('value'));
2504 this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2507 is_syntax_valid: function() {
2508 if (!this.get("effective_readonly") && this.datewidget) {
2509 return this.datewidget.is_valid_();
2513 is_false: function() {
2514 return this.get('value') === '' || this._super();
2517 if (this.datewidget && this.datewidget.$input) {
2518 this.datewidget.$input[0].focus();
2521 set_dimensions: function (height, width) {
2522 this._super(height, width);
2523 this.datewidget.$input.css('height', height);
2527 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2528 template: "FieldDate",
2529 build_widget: function() {
2530 return new instance.web.DateWidget(this);
2534 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2535 template: 'FieldText',
2537 'keyup': function (e) {
2538 if (e.which === $.ui.keyCode.ENTER) {
2539 e.stopPropagation();
2542 'change textarea': 'store_dom_value',
2544 init: function (field_manager, node) {
2545 this._super(field_manager, node);
2547 initialize_content: function() {
2549 this.$textarea = this.$el.find('textarea');
2550 this.auto_sized = false;
2551 this.default_height = this.$textarea.css('height');
2552 if (this.get("effective_readonly")) {
2553 this.$textarea.attr('disabled', 'disabled');
2555 this.setupFocus(this.$textarea);
2557 commit_value: function () {
2558 this.store_dom_value();
2559 return this._super();
2561 store_dom_value: function () {
2562 if (!this.get('effective_readonly') && this.$('textarea').length) {
2563 this.internal_set_value(
2564 instance.web.parse_value(
2565 this.$textarea.val(),
2569 render_value: function() {
2570 var show_value = instance.web.format_value(this.get('value'), this, '');
2571 if (show_value === '') {
2572 this.$textarea.css('height', parseInt(this.default_height)+"px");
2574 this.$textarea.val(show_value);
2575 if (! this.auto_sized) {
2576 this.auto_sized = true;
2577 this.$textarea.autosize();
2579 this.$textarea.trigger("autosize");
2582 is_syntax_valid: function() {
2583 if (!this.get("effective_readonly") && this.$textarea) {
2585 instance.web.parse_value(this.$textarea.val(), this, '');
2593 is_false: function() {
2594 return this.get('value') === '' || this._super();
2596 focus: function($el) {
2597 this.$textarea[0].focus();
2599 set_dimensions: function (height, width) {
2600 this._super(height, width);
2601 this.$textarea.css({
2609 * FieldTextHtml Widget
2610 * Intended for FieldText widgets meant to display HTML content. This
2611 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2612 * To find more information about CLEditor configutation: go to
2613 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2615 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2616 template: 'FieldTextHtml',
2618 this._super.apply(this, arguments);
2620 initialize_content: function() {
2622 if (! this.get("effective_readonly")) {
2623 self._updating_editor = false;
2624 this.$textarea = this.$el.find('textarea');
2625 var width = ((this.node.attrs || {}).editor_width || '100%');
2626 var height = ((this.node.attrs || {}).editor_height || 250);
2627 this.$textarea.cleditor({
2628 width: width, // width not including margins, borders or padding
2629 height: height, // height not including margins, borders or padding
2630 controls: // controls to add to the toolbar
2631 "bold italic underline strikethrough " +
2632 "| removeformat | bullets numbering | outdent " +
2633 "indent | link unlink | source",
2634 bodyStyle: // style to assign to document body contained within the editor
2635 "margin:4px; font:12px monospace; cursor:text; color:#1F1F1F"
2637 this.$cleditor = this.$textarea.cleditor()[0];
2638 this.$cleditor.change(function() {
2639 if (! self._updating_editor) {
2640 self.$cleditor.updateTextArea();
2641 self.internal_set_value(self.$textarea.val());
2646 render_value: function() {
2647 if (! this.get("effective_readonly")) {
2648 this.$textarea.val(this.get('value') || '');
2649 this._updating_editor = true;
2650 this.$cleditor.updateFrame();
2651 this._updating_editor = false;
2653 this.$el.html(this.get('value'));
2658 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2659 template: 'FieldBoolean',
2662 this.$checkbox = $("input", this.$el);
2663 this.setupFocus(this.$checkbox);
2664 this.$el.click(_.bind(function() {
2665 this.internal_set_value(this.$checkbox.is(':checked'));
2667 var check_readonly = function() {
2668 self.$checkbox.prop('disabled', self.get("effective_readonly"));
2670 this.on("change:effective_readonly", this, check_readonly);
2671 check_readonly.call(this);
2672 this._super.apply(this, arguments);
2674 render_value: function() {
2675 this.$checkbox[0].checked = this.get('value');
2678 this.$checkbox[0].focus();
2683 The progressbar field expect a float from 0 to 100.
2685 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2686 template: 'FieldProgressBar',
2687 render_value: function() {
2688 this.$el.progressbar({
2689 value: this.get('value') || 0,
2690 disabled: this.get("effective_readonly")
2692 var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
2693 this.$('span').html(formatted_value + '%');
2698 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2699 template: 'FieldSelection',
2701 'change select': 'store_dom_value',
2703 init: function(field_manager, node) {
2705 this._super(field_manager, node);
2706 this.values = _(this.field.selection).chain()
2707 .reject(function (v) { return v[0] === false && v[1] === ''; })
2708 .unshift([false, ''])
2711 initialize_content: function() {
2712 // Flag indicating whether we're in an event chain containing a change
2713 // event on the select, in order to know what to do on keyup[RETURN]:
2714 // * If the user presses [RETURN] as part of changing the value of a
2715 // selection, we should just let the value change and not let the
2716 // event broadcast further (e.g. to validating the current state of
2717 // the form in editable list view, which would lead to saving the
2718 // current row or switching to the next one)
2719 // * If the user presses [RETURN] with a select closed (side-effect:
2720 // also if the user opened the select and pressed [RETURN] without
2721 // changing the selected value), takes the action as validating the
2723 var ischanging = false;
2724 var $select = this.$el.find('select')
2725 .change(function () { ischanging = true; })
2726 .click(function () { ischanging = false; })
2727 .keyup(function (e) {
2728 if (e.which !== 13 || !ischanging) { return; }
2729 e.stopPropagation();
2732 this.setupFocus($select);
2734 commit_value: function () {
2735 this.store_dom_value();
2736 return this._super();
2738 store_dom_value: function () {
2739 if (!this.get('effective_readonly') && this.$('select').length) {
2740 this.internal_set_value(
2741 this.values[this.$('select')[0].selectedIndex][0]);
2744 set_value: function(value_) {
2745 value_ = value_ === null ? false : value_;
2746 value_ = value_ instanceof Array ? value_[0] : value_;
2747 this._super(value_);
2749 render_value: function() {
2750 if (!this.get("effective_readonly")) {
2752 for (var i = 0, ii = this.values.length; i < ii; i++) {
2753 if (this.values[i][0] === this.get('value')) index = i;
2755 this.$el.find('select')[0].selectedIndex = index;
2758 var option = _(this.values)
2759 .detect(function (record) { return record[0] === self.get('value'); });
2760 this.$el.text(option ? option[1] : this.values[0][1]);
2764 this.$('select:first')[0].focus();
2766 set_dimensions: function (height, width) {
2767 this._super(height, width);
2768 this.$('select').css({
2775 // jquery autocomplete tweak to allow html and classnames
2777 var proto = $.ui.autocomplete.prototype,
2778 initSource = proto._initSource;
2780 function filter( array, term ) {
2781 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
2782 return $.grep( array, function(value_) {
2783 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
2788 _initSource: function() {
2789 if ( this.options.html && $.isArray(this.options.source) ) {
2790 this.source = function( request, response ) {
2791 response( filter( this.options.source, request.term ) );
2794 initSource.call( this );
2798 _renderItem: function( ul, item) {
2799 return $( "<li></li>" )
2800 .data( "item.autocomplete", item )
2801 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
2803 .addClass(item.classname);
2809 * A mixin containing some useful methods to handle completion inputs.
2811 instance.web.form.CompletionFieldMixin = {
2814 this.orderer = new instance.web.DropMisordered();
2817 * Call this method to search using a string.
2819 get_search_result: function(search_val) {
2822 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
2823 var blacklist = this.get_search_blacklist();
2824 this.last_query = search_val;
2826 return this.orderer.add(dataset.name_search(
2827 search_val, new instance.web.CompoundDomain(self.build_domain(), [["id", "not in", blacklist]]),
2828 'ilike', this.limit + 1, self.build_context())).then(function(data) {
2829 self.last_search = data;
2830 // possible selections for the m2o
2831 var values = _.map(data, function(x) {
2832 x[1] = x[1].split("\n")[0];
2834 label: _.str.escapeHTML(x[1]),
2841 // search more... if more results that max
2842 if (values.length > self.limit) {
2843 values = values.slice(0, self.limit);
2845 label: _t("Search More..."),
2846 action: function() {
2847 dataset.name_search(search_val, self.build_domain(), 'ilike', false).done(function(data) {
2848 self._search_create_popup("search", data);
2851 classname: 'oe_m2o_dropdown_option'
2855 var raw_result = _(data.result).map(function(x) {return x[1];});
2856 if (search_val.length > 0 && !_.include(raw_result, search_val)) {
2858 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
2859 $('<span />').text(search_val).html()),
2860 action: function() {
2861 self._quick_create(search_val);
2863 classname: 'oe_m2o_dropdown_option'
2868 label: _t("Create and Edit..."),
2869 action: function() {
2870 self._search_create_popup("form", undefined, self._create_context(search_val));
2872 classname: 'oe_m2o_dropdown_option'
2878 get_search_blacklist: function() {
2881 _quick_create: function(name) {
2883 var slow_create = function () {
2884 self._search_create_popup("form", undefined, self._create_context(name));
2886 if (self.options.quick_create === undefined || self.options.quick_create) {
2887 new instance.web.DataSet(this, this.field.relation, self.build_context())
2888 .name_create(name).done(function(data) {
2889 self.add_id(data[0]);
2890 }).fail(function(error, event) {
2891 event.preventDefault();
2897 // all search/create popup handling
2898 _search_create_popup: function(view, ids, context) {
2900 var pop = new instance.web.form.SelectCreatePopup(this);
2902 self.field.relation,
2904 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
2905 initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
2907 disable_multiple_selection: true
2909 self.build_domain(),
2910 new instance.web.CompoundContext(self.build_context(), context || {})
2912 pop.on("elements_selected", self, function(element_ids) {
2913 self.add_id(element_ids[0]);
2920 add_id: function(id) {},
2921 _create_context: function(name) {
2923 var field = (this.options || {}).create_name_field;
2924 if (field === undefined)
2926 if (field !== false && name && (this.options || {}).quick_create !== false)
2927 tmp["default_" + field] = name;
2932 instance.web.form.M2ODialog = instance.web.Dialog.extend({
2933 template: "M2ODialog",
2934 init: function(parent) {
2935 this._super(parent, {
2936 title: _.str.sprintf(_t("Add %s"), parent.string),
2942 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
2943 this.$("input").val(this.getParent().last_query);
2944 this.$buttons.find(".oe_form_m2o_qc_button").click(function(){
2945 self.getParent()._quick_create(self.$("input").val());
2948 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
2949 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
2952 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
2958 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
2959 template: "FieldMany2One",
2961 'keydown input': function (e) {
2963 case $.ui.keyCode.UP:
2964 case $.ui.keyCode.DOWN:
2965 e.stopPropagation();
2969 init: function(field_manager, node) {
2970 this._super(field_manager, node);
2971 instance.web.form.CompletionFieldMixin.init.call(this);
2972 this.set({'value': false});
2973 this.display_value = {};
2974 this.last_search = [];
2975 this.floating = false;
2976 this.current_display = null;
2977 this.is_started = false;
2979 reinit_value: function(val) {
2980 this.internal_set_value(val);
2981 this.floating = false;
2982 if (this.is_started)
2983 this.render_value();
2985 initialize_field: function() {
2986 this.is_started = true;
2987 instance.web.bus.on('click', this, function() {
2988 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
2989 this.$input.autocomplete("close");
2992 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
2994 initialize_content: function() {
2995 if (!this.get("effective_readonly"))
2996 this.render_editable();
2998 init_error_displayer: function() {
3001 hide_error_displayer: function() {
3004 show_error_displayer: function() {
3005 new instance.web.form.M2ODialog(this).open();
3007 render_editable: function() {
3009 this.$input = this.$el.find("input");
3011 this.init_error_displayer();
3013 self.$input.on('focus', function() {
3014 self.hide_error_displayer();
3017 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3018 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3020 this.$follow_button.click(function(ev) {
3021 ev.preventDefault();
3022 if (!self.get('value')) {
3026 var pop = new instance.web.form.FormOpenPopup(self);
3028 self.field.relation,
3030 self.build_context(),
3032 title: _t("Open: ") + self.string
3035 pop.on('write_completed', self, function(){
3036 self.display_value = {};
3037 self.render_value();
3042 // some behavior for input
3043 var input_changed = function() {
3044 if (self.current_display !== self.$input.val()) {
3045 self.current_display = self.$input.val();
3046 if (self.$input.val() === "") {
3047 self.internal_set_value(false);
3048 self.floating = false;
3050 self.floating = true;
3054 this.$input.keydown(input_changed);
3055 this.$input.change(input_changed);
3056 this.$drop_down.click(function() {
3057 if (self.$input.autocomplete("widget").is(":visible")) {
3058 self.$input.autocomplete("close");
3059 self.$input.focus();
3061 if (self.get("value") && ! self.floating) {
3062 self.$input.autocomplete("search", "");
3064 self.$input.autocomplete("search");
3068 self.ed_def = $.Deferred();
3069 self.uned_def = $.Deferred();
3071 var ed_duration = 15000;
3072 var anyoneLoosesFocus = function (e) {
3074 if (self.floating) {
3075 if (self.last_search.length > 0) {
3076 if (self.last_search[0][0] != self.get("value")) {
3077 self.display_value = {};
3078 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3079 self.reinit_value(self.last_search[0][0]);
3082 self.render_value();
3086 self.reinit_value(false);
3088 self.floating = false;
3090 if (used && self.get("value") === false && ! self.no_ed) {
3091 self.ed_def.reject();
3092 self.uned_def.reject();
3093 self.ed_def = $.Deferred();
3094 self.ed_def.done(function() {
3095 self.show_error_displayer();
3096 ignore_blur = false;
3097 self.trigger('focused');
3100 setTimeout(function() {
3101 self.ed_def.resolve();
3102 self.uned_def.reject();
3103 self.uned_def = $.Deferred();
3104 self.uned_def.done(function() {
3105 self.hide_error_displayer();
3107 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3111 self.ed_def.reject();
3114 var ignore_blur = false;
3116 focusout: anyoneLoosesFocus,
3117 focus: function () { self.trigger('focused'); },
3118 autocompleteopen: function () { ignore_blur = true; },
3119 autocompleteclose: function () { ignore_blur = false; },
3121 // autocomplete open
3122 if (ignore_blur) { return; }
3123 if (_(self.getChildren()).any(function (child) {
3124 return child instanceof instance.web.form.AbstractFormPopup;
3126 self.trigger('blurred');
3130 var isSelecting = false;
3132 this.$input.autocomplete({
3133 source: function(req, resp) {
3134 self.get_search_result(req.term).done(function(result) {
3138 select: function(event, ui) {
3142 self.display_value = {};
3143 self.display_value["" + item.id] = item.name;
3144 self.reinit_value(item.id);
3145 } else if (item.action) {
3147 // Cancel widget blurring, to avoid form blur event
3148 self.trigger('focused');
3152 focus: function(e, ui) {
3156 // disabled to solve a bug, but may cause others
3157 //close: anyoneLoosesFocus,
3161 this.$input.autocomplete("widget").openerpClass();
3162 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3163 this.$input.keyup(function(e) {
3164 if (e.which === 13) { // ENTER
3166 e.stopPropagation();
3168 isSelecting = false;
3170 this.setupFocus(this.$follow_button);
3172 render_value: function(no_recurse) {
3174 if (! this.get("value")) {
3175 this.display_string("");
3178 var display = this.display_value["" + this.get("value")];
3180 this.display_string(display);
3184 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3185 dataset.name_get([self.get("value")]).done(function(data) {
3186 self.display_value["" + self.get("value")] = data[0][1];
3187 self.render_value(true);
3191 display_string: function(str) {
3193 if (!this.get("effective_readonly")) {
3194 this.$input.val(str.split("\n")[0]);
3195 this.current_display = this.$input.val();
3196 if (this.is_false()) {
3197 this.$('.oe_m2o_cm_button').css({'display':'none'});
3199 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3202 var lines = _.escape(str).split("\n");
3206 follow = _.rest(lines).join("<br />");
3209 var $link = this.$el.find('.oe_form_uri')
3212 if (! this.options.no_open)
3213 $link.click(function () {
3215 type: 'ir.actions.act_window',
3216 res_model: self.field.relation,
3217 res_id: self.get("value"),
3218 views: [[false, 'form']],
3223 $(".oe_form_m2o_follow", this.$el).html(follow);
3226 set_value: function(value_) {
3228 if (value_ instanceof Array) {
3229 this.display_value = {};
3230 if (! this.options.always_reload) {
3231 this.display_value["" + value_[0]] = value_[1];
3235 value_ = value_ || false;
3236 this.reinit_value(value_);
3238 get_displayed: function() {
3239 return this.display_value["" + this.get("value")];
3241 add_id: function(id) {
3242 this.display_value = {};
3243 this.reinit_value(id);
3245 is_false: function() {
3246 return ! this.get("value");
3248 focus: function () {
3249 if (!this.get('effective_readonly')) {
3250 this.$input[0].focus();
3253 _quick_create: function() {
3255 this.ed_def.reject();
3256 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3258 _search_create_popup: function() {
3260 this.ed_def.reject();
3261 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3263 set_dimensions: function (height, width) {
3264 this._super(height, width);
3265 this.$input.css('height', height);
3269 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3270 template: 'Many2OneButton',
3271 init: function(field_manager, node) {
3272 this._super.apply(this, arguments);
3275 this._super.apply(this, arguments);
3278 set_button: function() {
3281 this.$button.remove();
3284 this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3285 this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3286 this.$button.addClass('oe_link').css({'padding':'4px'});
3287 this.$el.append(this.$button);
3288 this.$button.on('click', self.on_click);
3290 on_click: function(ev) {
3292 this.popup = new instance.web.form.FormOpenPopup(this);
3293 this.popup.show_element(
3294 this.field.relation,
3296 this.build_context(),
3297 {title: this.string}
3299 this.popup.on('create_completed', self, function(r) {
3303 set_value: function(value_) {
3305 if (value_ instanceof Array) {
3308 value_ = value_ || false;
3309 this.set('value', value_);
3315 # Values: (0, 0, { fields }) create
3316 # (1, ID, { fields }) update
3317 # (2, ID) remove (delete)
3318 # (3, ID) unlink one (target id or target of relation)
3320 # (5) unlink all (only valid for one2many)
3325 'create': function (values) {
3326 return [commands.CREATE, false, values];
3328 // (1, id, {values})
3330 'update': function (id, values) {
3331 return [commands.UPDATE, id, values];
3335 'delete': function (id) {
3336 return [commands.DELETE, id, false];
3338 // (3, id[, _]) removes relation, but not linked record itself
3340 'forget': function (id) {
3341 return [commands.FORGET, id, false];
3345 'link_to': function (id) {
3346 return [commands.LINK_TO, id, false];
3350 'delete_all': function () {
3351 return [5, false, false];
3353 // (6, _, ids) replaces all linked records with provided ids
3355 'replace_with': function (ids) {
3356 return [6, false, ids];
3359 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3360 multi_selection: false,
3361 disable_utility_classes: true,
3362 init: function(field_manager, node) {
3363 this._super(field_manager, node);
3364 lazy_build_o2m_kanban_view();
3365 this.is_loaded = $.Deferred();
3366 this.initial_is_loaded = this.is_loaded;
3367 this.form_last_update = $.Deferred();
3368 this.init_form_last_update = this.form_last_update;
3369 this.is_started = false;
3370 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
3371 this.dataset.o2m = this;
3372 this.dataset.parent_view = this.view;
3373 this.dataset.child_name = this.name;
3375 this.dataset.on('dataset_changed', this, function() {
3376 self.trigger_on_change();
3381 this._super.apply(this, arguments);
3382 this.$el.addClass('oe_form_field oe_form_field_one2many');
3387 this.is_loaded.done(function() {
3388 self.on("change:effective_readonly", self, function() {
3389 self.is_loaded = self.is_loaded.then(function() {
3390 self.viewmanager.destroy();
3391 return $.when(self.load_views()).done(function() {
3392 self.reload_current_view();
3397 this.is_started = true;
3398 this.reload_current_view();
3400 trigger_on_change: function() {
3401 this.trigger('changed_value');
3403 load_views: function() {
3406 var modes = this.node.attrs.mode;
3407 modes = !!modes ? modes.split(",") : ["tree"];
3409 _.each(modes, function(mode) {
3410 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
3411 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
3415 view_type: mode == "tree" ? "list" : mode,
3418 if (self.field.views && self.field.views[mode]) {
3419 view.embedded_view = self.field.views[mode];
3421 if(view.view_type === "list") {
3422 _.extend(view.options, {
3424 selectable: self.multi_selection,
3426 import_enabled: false,
3429 if (self.get("effective_readonly")) {
3430 _.extend(view.options, {
3435 } else if (view.view_type === "form") {
3436 if (self.get("effective_readonly")) {
3437 view.view_type = 'form';
3439 _.extend(view.options, {
3440 not_interactible_on_create: true,
3442 } else if (view.view_type === "kanban") {
3443 _.extend(view.options, {
3444 confirm_on_delete: false,
3446 if (self.get("effective_readonly")) {
3447 _.extend(view.options, {
3448 action_buttons: false,
3449 quick_creatable: false,
3451 read_only_mode: true,
3459 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
3460 this.viewmanager.o2m = self;
3461 var once = $.Deferred().done(function() {
3462 self.init_form_last_update.resolve();
3464 var def = $.Deferred().done(function() {
3465 self.initial_is_loaded.resolve();
3467 this.viewmanager.on("controller_inited", self, function(view_type, controller) {
3468 controller.o2m = self;
3469 if (view_type == "list") {
3470 if (self.get("effective_readonly")) {
3471 controller.on('edit:before', self, function (e) {
3474 _(controller.columns).find(function (column) {
3475 if (!(column instanceof instance.web.list.Handle)) {
3478 column.modifiers.invisible = true;
3482 } else if (view_type === "form") {
3483 if (self.get("effective_readonly")) {
3484 $(".oe_form_buttons", controller.$el).children().remove();
3486 controller.on("load_record", self, function(){
3489 controller.on('pager_action_executed',self,self.save_any_view);
3490 } else if (view_type == "graph") {
3491 self.reload_current_view()
3495 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
3496 $.when(self.save_any_view()).done(function() {
3497 if (n_mode === "list") {
3498 $.async_when().done(function() {
3499 self.reload_current_view();
3504 $.async_when().done(function () {
3505 self.viewmanager.appendTo(self.$el);
3509 reload_current_view: function() {
3511 return self.is_loaded = self.is_loaded.then(function() {
3512 var active_view = self.viewmanager.active_view;
3513 var view = self.viewmanager.views[active_view].controller;
3514 if(active_view === "list") {
3515 return view.reload_content();
3516 } else if (active_view === "form") {
3517 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
3518 self.dataset.index = 0;
3520 var act = function() {
3521 return view.do_show();
3523 self.form_last_update = self.form_last_update.then(act, act);
3524 return self.form_last_update;
3525 } else if (view.do_search) {
3526 return view.do_search(self.build_domain(), self.dataset.get_context(), []);
3530 set_value: function(value_) {
3531 value_ = value_ || [];
3533 this.dataset.reset_ids([]);
3534 if(value_.length >= 1 && value_[0] instanceof Array) {
3536 _.each(value_, function(command) {
3537 var obj = {values: command[2]};
3538 switch (command[0]) {
3539 case commands.CREATE:
3540 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3542 self.dataset.to_create.push(obj);
3543 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3546 case commands.UPDATE:
3547 obj['id'] = command[1];
3548 self.dataset.to_write.push(obj);
3549 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3552 case commands.DELETE:
3553 self.dataset.to_delete.push({id: command[1]});
3555 case commands.LINK_TO:
3556 ids.push(command[1]);
3558 case commands.DELETE_ALL:
3559 self.dataset.delete_all = true;
3564 this.dataset.set_ids(ids);
3565 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
3567 this.dataset.delete_all = true;
3568 _.each(value_, function(command) {
3569 var obj = {values: command};
3570 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3572 self.dataset.to_create.push(obj);
3573 self.dataset.cache.push(_.clone(obj));
3577 this.dataset.set_ids(ids);
3579 this._super(value_);
3580 this.dataset.reset_ids(value_);
3582 if (this.dataset.index === null && this.dataset.ids.length > 0) {
3583 this.dataset.index = 0;
3585 this.trigger_on_change();
3586 if (this.is_started) {
3587 return self.reload_current_view();
3592 get_value: function() {
3596 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
3597 val = val.concat(_.map(this.dataset.ids, function(id) {
3598 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
3600 return commands.create(alter_order.values);
3602 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
3604 return commands.update(alter_order.id, alter_order.values);
3606 return commands.link_to(id);
3608 return val.concat(_.map(
3609 this.dataset.to_delete, function(x) {
3610 return commands['delete'](x.id);}));
3612 commit_value: function() {
3613 return this.save_any_view();
3615 save_any_view: function() {
3616 if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
3617 this.viewmanager.views[this.viewmanager.active_view] &&
3618 this.viewmanager.views[this.viewmanager.active_view].controller) {
3619 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3620 if (this.viewmanager.active_view === "form") {
3621 if (!view.is_initialized.state() === 'resolved') {
3622 return $.when(false);
3624 return $.when(view.save());
3625 } else if (this.viewmanager.active_view === "list") {
3626 return $.when(view.ensure_saved());
3629 return $.when(false);
3631 is_syntax_valid: function() {
3632 if (! this.viewmanager || ! this.viewmanager.views[this.viewmanager.active_view])
3634 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3635 switch (this.viewmanager.active_view) {
3637 return _(view.fields).chain()
3643 return view.is_valid();
3649 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
3650 template: 'One2Many.viewmanager',
3651 init: function(parent, dataset, views, flags) {
3652 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
3653 this.registry = this.registry.extend({
3654 list: 'instance.web.form.One2ManyListView',
3655 form: 'instance.web.form.One2ManyFormView',
3656 kanban: 'instance.web.form.One2ManyKanbanView',
3658 this.__ignore_blur = false;
3660 switch_mode: function(mode, unused) {
3661 if (mode !== 'form') {
3662 return this._super(mode, unused);
3665 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
3666 var pop = new instance.web.form.FormOpenPopup(this);
3667 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3668 title: _t("Open: ") + self.o2m.string,
3669 create_function: function(data) {
3670 return self.o2m.dataset.create(data).done(function(r) {
3671 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3672 self.o2m.dataset.trigger("dataset_changed", r);
3675 write_function: function(id, data, options) {
3676 return self.o2m.dataset.write(id, data, {}).done(function() {
3677 self.o2m.reload_current_view();
3680 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3681 parent_view: self.o2m.view,
3682 child_name: self.o2m.name,
3683 read_function: function() {
3684 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3686 form_view_options: {'not_interactible_on_create':true},
3687 readonly: self.o2m.get("effective_readonly")
3689 pop.on("elements_selected", self, function() {
3690 self.o2m.reload_current_view();
3695 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
3696 get_context: function() {
3697 this.context = this.o2m.build_context();
3698 return this.context;
3702 instance.web.form.One2ManyListView = instance.web.ListView.extend({
3703 _template: 'One2Many.listview',
3704 init: function (parent, dataset, view_id, options) {
3705 this._super(parent, dataset, view_id, _.extend(options || {}, {
3706 GroupsType: instance.web.form.One2ManyGroups,
3707 ListType: instance.web.form.One2ManyList
3709 this.on('edit:before', this, this.proxy('_before_edit'));
3710 this.on('edit:after', this, this.proxy('_after_edit'));
3711 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
3714 .bind('add', this.proxy("changed_records"))
3715 .bind('edit', this.proxy("changed_records"))
3716 .bind('remove', this.proxy("changed_records"));
3718 start: function () {
3719 var ret = this._super();
3721 .off('mousedown.handleButtons')
3722 .on('mousedown.handleButtons', 'table button', this.proxy('_button_down'));
3725 changed_records: function () {
3726 this.o2m.trigger_on_change();
3728 is_valid: function () {
3729 var form = this.editor.form;
3731 // If the form has not been modified, the view can only be valid
3732 // NB: is_dirty will also be set on defaults/onchanges/whatever?
3733 // oe_form_dirty seems to only be set on actual user actions
3734 if (!form.$el.is('.oe_form_dirty')) {
3737 this.o2m._dirty_flag = true;
3739 // Otherwise validate internal form
3740 return _(form.fields).chain()
3741 .invoke(function () {
3742 this._check_css_flags();
3743 return this.is_valid();
3748 do_add_record: function () {
3749 if (this.editable()) {
3750 this._super.apply(this, arguments);
3753 var pop = new instance.web.form.SelectCreatePopup(this);
3755 self.o2m.field.relation,
3757 title: _t("Create: ") + self.o2m.string,
3758 initial_view: "form",
3759 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3760 create_function: function(data, callback, error_callback) {
3761 return self.o2m.dataset.create(data).done(function(r) {
3762 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3763 self.o2m.dataset.trigger("dataset_changed", r);
3764 }).done(callback).fail(error_callback);
3766 read_function: function() {
3767 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3769 parent_view: self.o2m.view,
3770 child_name: self.o2m.name,
3771 form_view_options: {'not_interactible_on_create':true}
3773 self.o2m.build_domain(),
3774 self.o2m.build_context()
3776 pop.on("elements_selected", self, function() {
3777 self.o2m.reload_current_view();
3781 do_activate_record: function(index, id) {
3783 var pop = new instance.web.form.FormOpenPopup(self);
3784 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3785 title: _t("Open: ") + self.o2m.string,
3786 write_function: function(id, data) {
3787 return self.o2m.dataset.write(id, data, {}).done(function() {
3788 self.o2m.reload_current_view();
3791 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3792 parent_view: self.o2m.view,
3793 child_name: self.o2m.name,
3794 read_function: function() {
3795 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3797 form_view_options: {'not_interactible_on_create':true},
3798 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
3801 do_button_action: function (name, id, callback) {
3802 if (!_.isNumber(id)) {
3803 instance.webclient.notification.warn(
3804 _t("Action Button"),
3805 _t("The o2m record must be saved before an action can be used"));
3808 var parent_form = this.o2m.view;
3810 this.ensure_saved().then(function () {
3812 return parent_form.save();
3815 }).done(function () {
3816 self.handle_button(name, id, callback);
3820 _before_edit: function () {
3821 this.__ignore_blur = false;
3822 this.editor.form.on('blurred', this, this._on_form_blur);
3824 _after_edit: function () {
3825 // The form's blur thing may be jiggered during the edition setup,
3826 // potentially leading to the o2m instasaving the row. Cancel any
3827 // blurring triggered the edition startup here
3828 this.editor.form.widgetFocused();
3830 _before_unedit: function () {
3831 this.editor.form.off('blurred', this, this._on_form_blur);
3833 _button_down: function () {
3834 // If a button is clicked (usually some sort of action button), it's
3835 // the button's responsibility to ensure the editable list is in the
3836 // correct state -> ignore form blurring
3837 this.__ignore_blur = true;
3840 * Handles blurring of the nested form (saves the currently edited row),
3841 * unless the flag to ignore the event is set to ``true``
3843 * Makes the internal form go away
3845 _on_form_blur: function () {
3846 if (this.__ignore_blur) {
3847 this.__ignore_blur = false;
3850 // FIXME: why isn't there an API for this?
3851 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
3852 this.ensure_saved();
3855 this.cancel_edition();
3857 keyup_ENTER: function () {
3858 // blurring caused by hitting the [Return] key, should skip the
3859 // autosave-on-blur and let the handler for [Return] do its thing (save
3860 // the current row *anyway*, then create a new one/edit the next one)
3861 this.__ignore_blur = true;
3862 this._super.apply(this, arguments);
3864 do_delete: function (ids) {
3865 var confirm = window.confirm;
3866 window.confirm = function () { return true; };
3868 return this._super(ids);
3870 window.confirm = confirm;
3874 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
3875 setup_resequence_rows: function () {
3876 if (!this.view.o2m.get('effective_readonly')) {
3877 this._super.apply(this, arguments);
3881 instance.web.form.One2ManyList = instance.web.ListView.List.extend({
3882 pad_table_to: function (count) {
3883 if (!this.view.is_action_enabled('create')) {
3886 this._super(count > 0 ? count - 1 : 0);
3889 // magical invocation of wtf does that do
3890 if (this.view.o2m.get('effective_readonly')) {
3895 var columns = _(this.columns).filter(function (column) {
3896 return column.invisible !== '1';
3898 if (this.options.selectable) { columns++; }
3899 if (this.options.deletable) { columns++; }
3901 if (!this.view.is_action_enabled('create')) {
3905 var $cell = $('<td>', {
3907 'class': 'oe_form_field_one2many_list_row_add'
3909 $('<a>', {href: '#'}).text(_t("Add an item"))
3910 .mousedown(function () {
3911 // FIXME: needs to be an official API somehow
3912 if (self.view.editor.is_editing()) {
3913 self.view.__ignore_blur = true;
3916 .click(function (e) {
3918 e.stopPropagation();
3919 // FIXME: there should also be an API for that one
3920 if (self.view.editor.form.__blur_timeout) {
3921 clearTimeout(self.view.editor.form.__blur_timeout);
3922 self.view.editor.form.__blur_timeout = false;
3924 self.view.ensure_saved().done(function () {
3925 self.view.do_add_record();
3929 var $padding = this.$current.find('tr:not([data-id]):first');
3930 var $newrow = $('<tr>').append($cell);
3931 if ($padding.length) {
3932 $padding.before($newrow);
3934 this.$current.append($newrow)
3939 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
3940 form_template: 'One2Many.formview',
3941 load_form: function(data) {
3944 this.$buttons.find('button.oe_form_button_create').click(function() {
3945 self.save().done(self.on_button_new);
3948 do_notify_change: function() {
3949 if (this.dataset.parent_view) {
3950 this.dataset.parent_view.do_notify_change();
3952 this._super.apply(this, arguments);
3957 var lazy_build_o2m_kanban_view = function() {
3958 if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
3960 instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
3964 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3965 template: "FieldMany2ManyTags",
3967 this._super.apply(this, arguments);
3968 instance.web.form.CompletionFieldMixin.init.call(this);
3969 this.set({"value": []});
3970 this._display_orderer = new instance.web.DropMisordered();
3971 this._drop_shown = false;
3973 initialize_content: function() {
3974 if (this.get("effective_readonly"))
3977 self.$text = this.$("textarea");
3978 self.$text.textext({
3979 plugins : 'tags arrow autocomplete',
3981 render: function(suggestion) {
3982 return $('<span class="text-label"/>').
3983 data('index', suggestion['index']).html(suggestion['label']);
3988 selectFromDropdown: function() {
3989 $(this).trigger('hideDropdown');
3990 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
3991 var data = self.search_result[index];
3993 self.add_id(data.id);
4000 isTagAllowed: function(tag) {
4004 removeTag: function(tag) {
4005 var id = tag.data("id");
4006 self.set({"value": _.without(self.get("value"), id)});
4008 renderTag: function(stuff) {
4009 return $.fn.textext.TextExtTags.prototype.renderTag.
4010 call(this, stuff).data("id", stuff.id);
4014 itemToString: function(item) {
4019 onSetInputData: function(e, data) {
4021 this._plugins.autocomplete._suggestions = null;
4023 this.input().val(data);
4027 }).bind('getSuggestions', function(e, data) {
4029 var str = !!data ? data.query || '' : '';
4030 self.get_search_result(str).done(function(result) {
4031 self.search_result = result;
4032 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4033 return _.extend(el, {index:i});
4036 }).bind('hideDropdown', function() {
4037 self._drop_shown = false;
4038 }).bind('showDropdown', function() {
4039 self._drop_shown = true;
4041 self.tags = self.$text.textext()[0].tags();
4043 .focusin(function () {
4044 self.trigger('focused');
4046 .focusout(function() {
4047 self.$text.trigger("setInputData", "");
4048 self.trigger('blurred');
4049 }).keydown(function(e) {
4050 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4051 self.$text.textext()[0].autocomplete().selectFromDropdown();
4055 set_value: function(value_) {
4056 value_ = value_ || [];
4057 if (value_.length >= 1 && value_[0] instanceof Array) {
4058 value_ = value_[0][2];
4060 this._super(value_);
4062 is_false: function() {
4063 return _(this.get("value")).isEmpty();
4065 get_value: function() {
4066 var tmp = [commands.replace_with(this.get("value"))];
4069 get_search_blacklist: function() {
4070 return this.get("value");
4072 render_value: function() {
4074 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4075 var values = self.get("value");
4076 var handle_names = function(data) {
4077 if (self.isDestroyed())
4080 _.each(data, function(el) {
4081 indexed[el[0]] = el;
4083 data = _.map(values, function(el) { return indexed[el]; });
4084 if (! self.get("effective_readonly")) {
4085 self.tags.containerElement().children().remove();
4086 self.$('textarea').css("padding-left", "3px");
4087 self.tags.addTags(_.map(data, function(el) {return {name: el[1], id:el[0]};}));
4089 self.$el.html(QWeb.render("FieldMany2ManyTag", {elements: data}));
4092 if (! values || values.length > 0) {
4093 this._display_orderer.add(dataset.name_get(values)).done(handle_names);
4098 add_id: function(id) {
4099 this.set({'value': _.uniq(this.get('value').concat([id]))});
4101 focus: function () {
4102 this.$text[0].focus();
4108 - reload_on_button: Reload the whole form view if click on a button in a list view.
4109 If you see this options, do not use it, it's basically a dirty hack to make one
4110 precise o2m to behave the way we want.
4112 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4113 multi_selection: false,
4114 disable_utility_classes: true,
4115 init: function(field_manager, node) {
4116 this._super(field_manager, node);
4117 this.is_loaded = $.Deferred();
4118 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4119 this.dataset.m2m = this;
4121 this.dataset.on('unlink', self, function(ids) {
4122 self.dataset_changed();
4125 this.list_dm = new instance.web.DropMisordered();
4126 this.render_value_dm = new instance.web.DropMisordered();
4128 initialize_content: function() {
4131 this.$el.addClass('oe_form_field oe_form_field_many2many');
4133 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4134 'addable': this.get("effective_readonly") ? null : _t("Add"),
4135 'deletable': this.get("effective_readonly") ? false : true,
4136 'selectable': this.multi_selection,
4138 'reorderable': false,
4139 'import_enabled': false,
4141 var embedded = (this.field.views || {}).tree;
4143 this.list_view.set_embedded_view(embedded);
4145 this.list_view.m2m_field = this;
4146 var loaded = $.Deferred();
4147 this.list_view.on("list_view_loaded", this, function() {
4150 this.list_view.appendTo(this.$el);
4152 var old_def = self.is_loaded;
4153 self.is_loaded = $.Deferred().done(function() {
4156 this.list_dm.add(loaded).then(function() {
4157 self.is_loaded.resolve();
4160 destroy_content: function() {
4161 this.list_view.destroy();
4162 this.list_view = undefined;
4164 set_value: function(value_) {
4165 value_ = value_ || [];
4166 if (value_.length >= 1 && value_[0] instanceof Array) {
4167 value_ = value_[0][2];
4169 this._super(value_);
4171 get_value: function() {
4172 return [commands.replace_with(this.get('value'))];
4174 is_false: function () {
4175 return _(this.get("value")).isEmpty();
4177 render_value: function() {
4179 this.dataset.set_ids(this.get("value"));
4180 this.render_value_dm.add(this.is_loaded).then(function() {
4181 return self.list_view.reload_content();
4184 dataset_changed: function() {
4185 this.internal_set_value(this.dataset.ids);
4189 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4190 get_context: function() {
4191 this.context = this.m2m.build_context();
4192 return this.context;
4198 * @extends instance.web.ListView
4200 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4201 do_add_record: function () {
4202 var pop = new instance.web.form.SelectCreatePopup(this);
4206 title: _t("Add: ") + this.m2m_field.string
4208 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4209 this.m2m_field.build_context()
4212 pop.on("elements_selected", self, function(element_ids) {
4214 _(element_ids).each(function (id) {
4215 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4216 self.dataset.set_ids(self.dataset.ids.concat([id]));
4217 self.m2m_field.dataset_changed();
4222 self.reload_content();
4226 do_activate_record: function(index, id) {
4228 var pop = new instance.web.form.FormOpenPopup(this);
4229 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4230 title: _t("Open: ") + this.m2m_field.string,
4231 readonly: this.getParent().get("effective_readonly")
4233 pop.on('write_completed', self, self.reload_content);
4235 do_button_action: function(name, id, callback) {
4237 var _sup = _.bind(this._super, this);
4238 if (! this.m2m_field.options.reload_on_button) {
4239 return _sup(name, id, callback);
4241 return this.m2m_field.view.save().then(function() {
4242 return _sup(name, id, function() {
4243 self.m2m_field.view.reload();
4250 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4251 disable_utility_classes: true,
4252 init: function(field_manager, node) {
4253 this._super(field_manager, node);
4254 instance.web.form.CompletionFieldMixin.init.call(this);
4255 m2m_kanban_lazy_init();
4256 this.is_loaded = $.Deferred();
4257 this.initial_is_loaded = this.is_loaded;
4260 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4261 this.dataset.m2m = this;
4262 this.dataset.on('unlink', self, function(ids) {
4263 self.dataset_changed();
4267 this._super.apply(this, arguments);
4272 self.on("change:effective_readonly", self, function() {
4273 self.is_loaded = self.is_loaded.then(function() {
4274 self.kanban_view.destroy();
4275 return $.when(self.load_view()).done(function() {
4276 self.render_value();
4281 set_value: function(value_) {
4282 value_ = value_ || [];
4283 if (value_.length >= 1 && value_[0] instanceof Array) {
4284 value_ = value_[0][2];
4286 this._super(value_);
4288 get_value: function() {
4289 return [commands.replace_with(this.get('value'))];
4291 load_view: function() {
4293 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4294 'create_text': _t("Add"),
4295 'creatable': self.get("effective_readonly") ? false : true,
4296 'quick_creatable': self.get("effective_readonly") ? false : true,
4297 'read_only_mode': self.get("effective_readonly") ? true : false,
4298 'confirm_on_delete': false,
4300 var embedded = (this.field.views || {}).kanban;
4302 this.kanban_view.set_embedded_view(embedded);
4304 this.kanban_view.m2m = this;
4305 var loaded = $.Deferred();
4306 this.kanban_view.on("kanban_view_loaded",self,function() {
4307 self.initial_is_loaded.resolve();
4310 this.kanban_view.on('switch_mode', this, this.open_popup);
4311 $.async_when().done(function () {
4312 self.kanban_view.appendTo(self.$el);
4316 render_value: function() {
4318 this.dataset.set_ids(this.get("value"));
4319 this.is_loaded = this.is_loaded.then(function() {
4320 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
4323 dataset_changed: function() {
4324 this.set({'value': this.dataset.ids});
4326 open_popup: function(type, unused) {
4327 if (type !== "form")
4330 if (this.dataset.index === null) {
4331 var pop = new instance.web.form.SelectCreatePopup(this);
4333 this.field.relation,
4335 title: _t("Add: ") + this.string
4337 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
4338 this.build_context()
4340 pop.on("elements_selected", self, function(element_ids) {
4341 _.each(element_ids, function(one_id) {
4342 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
4343 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
4344 self.dataset_changed();
4345 self.render_value();
4350 var id = self.dataset.ids[self.dataset.index];
4351 var pop = new instance.web.form.FormOpenPopup(this);
4352 pop.show_element(self.field.relation, id, self.build_context(), {
4353 title: _t("Open: ") + self.string,
4354 write_function: function(id, data, options) {
4355 return self.dataset.write(id, data, {}).done(function() {
4356 self.render_value();
4359 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
4360 parent_view: self.view,
4361 child_name: self.name,
4362 readonly: self.get("effective_readonly")
4366 add_id: function(id) {
4367 this.quick_create.add_id(id);
4371 function m2m_kanban_lazy_init() {
4372 if (instance.web.form.Many2ManyKanbanView)
4374 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4375 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
4376 _is_quick_create_enabled: function() {
4377 return this._super() && ! this.group_by;
4380 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
4381 template: 'Many2ManyKanban.quick_create',
4384 * close_btn: If true, the widget will display a "Close" button able to trigger
4387 init: function(parent, dataset, context, buttons) {
4388 this._super(parent);
4389 this.m2m = this.getParent().view.m2m;
4390 this.m2m.quick_create = this;
4391 this._dataset = dataset;
4392 this._buttons = buttons || false;
4393 this._context = context || {};
4395 start: function () {
4397 self.$text = this.$el.find('input').css("width", "200px");
4398 self.$text.textext({
4399 plugins : 'arrow autocomplete',
4401 render: function(suggestion) {
4402 return $('<span class="text-label"/>').
4403 data('index', suggestion['index']).html(suggestion['label']);
4408 selectFromDropdown: function() {
4409 $(this).trigger('hideDropdown');
4410 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4411 var data = self.search_result[index];
4413 self.add_id(data.id);
4420 itemToString: function(item) {
4425 }).bind('getSuggestions', function(e, data) {
4427 var str = !!data ? data.query || '' : '';
4428 self.m2m.get_search_result(str).done(function(result) {
4429 self.search_result = result;
4430 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4431 return _.extend(el, {index:i});
4435 self.$text.focusout(function() {
4440 this.$text[0].focus();
4442 add_id: function(id) {
4445 self.trigger('added', id);
4446 this.m2m.dataset_changed();
4452 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
4454 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
4455 template: "AbstractFormPopup.render",
4458 * -readonly: only applicable when not in creation mode, default to false
4459 * - alternative_form_view
4465 * - form_view_options
4467 init_popup: function(model, row_id, domain, context, options) {
4468 this.row_id = row_id;
4470 this.domain = domain || [];
4471 this.context = context || {};
4472 this.options = options;
4473 _.defaults(this.options, {
4476 init_dataset: function() {
4478 this.created_elements = [];
4479 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
4480 this.dataset.read_function = this.options.read_function;
4481 this.dataset.create_function = function(data, sup) {
4482 var fct = self.options.create_function || sup;
4483 return fct.call(this, data).done(function(r) {
4484 self.trigger('create_completed saved', r);
4485 self.created_elements.push(r);
4488 this.dataset.write_function = function(id, data, options, sup) {
4489 var fct = self.options.write_function || sup;
4490 return fct.call(this, id, data, options).done(function(r) {
4491 self.trigger('write_completed saved', r);
4494 this.dataset.parent_view = this.options.parent_view;
4495 this.dataset.child_name = this.options.child_name;
4497 display_popup: function() {
4499 this.renderElement();
4500 var dialog = new instance.web.Dialog(this, {
4502 dialogClass: 'oe_act_window',
4504 self.check_exit(true);
4506 title: this.options.title || "",
4507 }, this.$el).open();
4508 this.$buttonpane = dialog.$buttons;
4511 setup_form_view: function() {
4514 this.dataset.ids = [this.row_id];
4515 this.dataset.index = 0;
4517 this.dataset.index = null;
4519 var options = _.clone(self.options.form_view_options) || {};
4520 if (this.row_id !== null) {
4521 options.initial_mode = this.options.readonly ? "view" : "edit";
4524 $buttons: this.$buttonpane,
4526 this.view_form = new instance.web.FormView(this, this.dataset, false, options);
4527 if (this.options.alternative_form_view) {
4528 this.view_form.set_embedded_view(this.options.alternative_form_view);
4530 this.view_form.appendTo(this.$el.find(".oe_popup_form"));
4531 this.view_form.on("form_view_loaded", self, function() {
4532 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
4533 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
4534 multi_select: multi_select,
4535 readonly: self.row_id !== null && self.options.readonly,
4537 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
4538 $snbutton.click(function() {
4539 $.when(self.view_form.save()).done(function() {
4540 self.view_form.reload_mutex.exec(function() {
4541 self.view_form.on_button_new();
4545 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
4546 $sbutton.click(function() {
4547 $.when(self.view_form.save()).done(function() {
4548 self.view_form.reload_mutex.exec(function() {
4553 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
4554 $cbutton.click(function() {
4557 self.view_form.do_show();
4560 select_elements: function(element_ids) {
4561 this.trigger("elements_selected", element_ids);
4563 check_exit: function(no_destroy) {
4564 if (this.created_elements.length > 0) {
4565 this.select_elements(this.created_elements);
4566 this.created_elements = [];
4568 this.trigger('closed');
4571 destroy: function () {
4572 this.trigger('closed');
4573 if (this.$el.is(":data(dialog)")) {
4574 this.$el.dialog('close');
4581 * Class to display a popup containing a form view.
4583 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
4584 show_element: function(model, row_id, context, options) {
4585 this.init_popup(model, row_id, [], context, options);
4586 _.defaults(this.options, {
4588 this.display_popup();
4592 this.init_dataset();
4593 this.setup_form_view();
4598 * Class to display a popup to display a list to search a row. It also allows
4599 * to switch to a form view to create a new row.
4601 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
4605 * - initial_view: form or search (default search)
4606 * - disable_multiple_selection
4607 * - list_view_options
4609 select_element: function(model, options, domain, context) {
4610 this.init_popup(model, null, domain, context, options);
4612 _.defaults(this.options, {
4613 initial_view: "search",
4615 this.initial_ids = this.options.initial_ids;
4616 this.display_popup();
4620 this.init_dataset();
4621 if (this.options.initial_view == "search") {
4622 instance.web.pyeval.eval_domains_and_contexts({
4624 contexts: [this.context]
4625 }).done(function (results) {
4626 var search_defaults = {};
4627 _.each(results.context, function (value_, key) {
4628 var match = /^search_default_(.*)$/.exec(key);
4630 search_defaults[match[1]] = value_;
4633 self.setup_search_view(search_defaults);
4639 setup_search_view: function(search_defaults) {
4641 if (this.searchview) {
4642 this.searchview.destroy();
4644 this.searchview = new instance.web.SearchView(this,
4645 this.dataset, false, search_defaults);
4646 this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
4647 if (self.initial_ids) {
4648 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
4649 contexts, groupbys);
4650 self.initial_ids = undefined;
4652 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
4655 this.searchview.on("search_view_loaded", self, function() {
4656 self.view_list = new instance.web.form.SelectCreateListView(self,
4657 self.dataset, false,
4658 _.extend({'deletable': false,
4659 'selectable': !self.options.disable_multiple_selection,
4660 'import_enabled': false,
4661 '$buttons': self.$buttonpane,
4662 'disable_editable_mode': true,
4663 '$pager': self.$('.oe_popup_list_pager'),
4664 }, self.options.list_view_options || {}));
4665 self.view_list.on('edit:before', self, function (e) {
4668 self.view_list.popup = self;
4669 self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
4670 self.view_list.do_show();
4671 }).then(function() {
4672 self.searchview.do_search();
4674 self.view_list.on("list_view_loaded", self, function() {
4675 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
4676 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
4677 $cbutton.click(function() {
4680 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
4681 $sbutton.click(function() {
4682 self.select_elements(self.selected_ids);
4685 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
4686 $cbutton.click(function() {
4691 this.searchview.appendTo($(".oe_popup_search", self.$el));
4693 do_search: function(domains, contexts, groupbys) {
4695 instance.web.pyeval.eval_domains_and_contexts({
4696 domains: domains || [],
4697 contexts: contexts || [],
4698 group_by_seq: groupbys || []
4699 }).done(function (results) {
4700 self.view_list.do_search(results.domain, results.context, results.group_by);
4703 on_click_element: function(ids) {
4705 this.selected_ids = ids || [];
4706 if(this.selected_ids.length > 0) {
4707 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
4709 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
4712 new_object: function() {
4713 if (this.searchview) {
4714 this.searchview.hide();
4716 if (this.view_list) {
4717 this.view_list.$el.hide();
4719 this.setup_form_view();
4723 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
4724 do_add_record: function () {
4725 this.popup.new_object();
4727 select_record: function(index) {
4728 this.popup.select_elements([this.dataset.ids[index]]);
4729 this.popup.destroy();
4731 do_select: function(ids, records) {
4732 this._super(ids, records);
4733 this.popup.on_click_element(ids);
4737 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4738 template: 'FieldReference',
4739 init: function(field_manager, node) {
4740 this._super(field_manager, node);
4741 this.reference_ready = true;
4743 destroy_content: function() {
4746 this.fm = undefined;
4749 initialize_content: function() {
4751 var fm = new instance.web.form.DefaultFieldManager(this);
4753 fm.extend_field_desc({
4755 selection: this.field_manager.get_field_desc(this.name).selection,
4763 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
4765 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
4767 this.selection.on("change:value", this, this.on_selection_changed);
4768 this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
4770 .on('focused', null, function () {self.trigger('focused')})
4771 .on('blurred', null, function () {self.trigger('blurred')});
4773 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
4775 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
4777 this.m2o.on("change:value", this, this.data_changed);
4778 this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
4780 .on('focused', null, function () {self.trigger('focused')})
4781 .on('blurred', null, function () {self.trigger('blurred')});
4783 on_selection_changed: function() {
4784 if (this.reference_ready) {
4785 this.internal_set_value([this.selection.get_value(), false]);
4786 this.render_value();
4789 data_changed: function() {
4790 if (this.reference_ready) {
4791 this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
4794 set_value: function(val) {
4796 val = val.split(',');
4797 val[0] = val[0] || false;
4798 val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
4800 this._super(val || [false, false]);
4802 get_value: function() {
4803 return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
4805 render_value: function() {
4806 this.reference_ready = false;
4807 if (!this.get("effective_readonly")) {
4808 this.selection.set_value(this.get('value')[0]);
4810 this.m2o.field.relation = this.get('value')[0];
4811 this.m2o.set_value(this.get('value')[1]);
4812 this.m2o.$el.toggle(!!this.get('value')[0]);
4813 this.reference_ready = true;
4817 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4818 init: function(field_manager, node) {
4820 this._super(field_manager, node);
4821 this.binary_value = false;
4822 this.useFileAPI = !!window.FileReader;
4823 this.max_upload_size = 25 * 1024 * 1024; // 25Mo
4824 if (!this.useFileAPI) {
4825 this.fileupload_id = _.uniqueId('oe_fileupload');
4826 $(window).on(this.fileupload_id, function() {
4827 var args = [].slice.call(arguments).slice(1);
4828 self.on_file_uploaded.apply(self, args);
4833 if (!this.useFileAPI) {
4834 $(window).off(this.fileupload_id);
4836 this._super.apply(this, arguments);
4838 initialize_content: function() {
4839 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
4840 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
4841 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
4843 on_file_change: function(e) {
4845 var file_node = e.target;
4846 if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
4847 if (this.useFileAPI) {
4848 var file = file_node.files[0];
4849 if (file.size > this.max_upload_size) {
4850 var msg = _t("The selected file exceed the maximum file size of %s.");
4851 instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
4854 var filereader = new FileReader();
4855 filereader.readAsDataURL(file);
4856 filereader.onloadend = function(upload) {
4857 var data = upload.target.result;
4858 data = data.split(',')[1];
4859 self.on_file_uploaded(file.size, file.name, file.type, data);
4862 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
4863 this.$el.find('form.oe_form_binary_form').submit();
4865 this.$el.find('.oe_form_binary_progress').show();
4866 this.$el.find('.oe_form_binary').hide();
4869 on_file_uploaded: function(size, name, content_type, file_base64) {
4870 if (size === false) {
4871 this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
4872 // TODO: use openerp web crashmanager
4873 console.warn("Error while uploading file : ", name);
4875 this.filename = name;
4876 this.on_file_uploaded_and_valid.apply(this, arguments);
4878 this.$el.find('.oe_form_binary_progress').hide();
4879 this.$el.find('.oe_form_binary').show();
4881 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
4883 on_save_as: function(ev) {
4884 var value = this.get('value');
4886 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
4887 ev.stopPropagation();
4889 instance.web.blockUI();
4890 var c = instance.webclient.crashmanager;
4891 this.session.get_file({
4892 url: '/web/binary/saveas_ajax',
4893 data: {data: JSON.stringify({
4894 model: this.view.dataset.model,
4895 id: (this.view.datarecord.id || ''),
4897 filename_field: (this.node.attrs.filename || ''),
4898 data: instance.web.form.is_bin_size(value) ? null : value,
4899 context: this.view.dataset.get_context()
4901 complete: instance.web.unblockUI,
4902 error: c.rpc_error.bind(c)
4904 ev.stopPropagation();
4908 set_filename: function(value) {
4909 var filename = this.node.attrs.filename;
4912 tmp[filename] = value;
4913 this.field_manager.set_values(tmp);
4916 on_clear: function() {
4917 if (this.get('value') !== false) {
4918 this.binary_value = false;
4919 this.internal_set_value(false);
4925 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
4926 template: 'FieldBinaryFile',
4927 initialize_content: function() {
4929 if (this.get("effective_readonly")) {
4931 this.$el.find('a').click(function(ev) {
4932 if (self.get('value')) {
4933 self.on_save_as(ev);
4939 render_value: function() {
4940 if (!this.get("effective_readonly")) {
4942 if (this.node.attrs.filename) {
4943 show_value = this.view.datarecord[this.node.attrs.filename] || '';
4945 show_value = (this.get('value') != null && this.get('value') !== false) ? this.get('value') : '';
4947 this.$el.find('input').eq(0).val(show_value);
4949 this.$el.find('a').show(!!this.get('value'));
4950 if (this.get('value')) {
4951 var show_value = _t("Download")
4953 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
4954 this.$el.find('a').text(show_value);
4958 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
4959 this.binary_value = true;
4960 this.internal_set_value(file_base64);
4961 var show_value = name + " (" + instance.web.human_size(size) + ")";
4962 this.$el.find('input').eq(0).val(show_value);
4963 this.set_filename(name);
4965 on_clear: function() {
4966 this._super.apply(this, arguments);
4967 this.$el.find('input').eq(0).val('');
4968 this.set_filename('');
4972 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
4973 template: 'FieldBinaryImage',
4974 placeholder: "/web/static/src/img/placeholder.png",
4975 render_value: function() {
4978 if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
4979 url = 'data:image/png;base64,' + this.get('value');
4980 } else if (this.get('value')) {
4981 var id = JSON.stringify(this.view.datarecord.id || null);
4982 var field = this.name;
4983 if (this.options.preview_image)
4984 field = this.options.preview_image;
4985 url = this.session.url('/web/binary/image', {
4986 model: this.view.dataset.model,
4989 t: (new Date().getTime()),
4992 url = this.placeholder;
4994 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
4995 this.$el.find('> img').remove();
4996 this.$el.prepend($img);
4997 $img.load(function() {
4998 if (! self.options.size)
5000 $img.css("max-width", "" + self.options.size[0] + "px");
5001 $img.css("max-height", "" + self.options.size[1] + "px");
5002 $img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px");
5003 $img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px");
5005 $img.on('error', function() {
5006 $img.attr('src', self.placeholder);
5007 instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5010 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5011 this.internal_set_value(file_base64);
5012 this.binary_value = true;
5013 this.render_value();
5014 this.set_filename(name);
5016 on_clear: function() {
5017 this._super.apply(this, arguments);
5018 this.render_value();
5019 this.set_filename('');
5024 * Widget for (many2many field) to upload one or more file in same time and display in list.
5025 * The user can delete his files.
5026 * Options on attribute ; "blockui" {Boolean} block the UI or not
5027 * during the file is uploading
5029 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
5030 template: "FieldBinaryFileUploader",
5031 init: function(field_manager, node) {
5032 this._super(field_manager, node);
5033 this.field_manager = field_manager;
5035 if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5036 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);
5040 this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5041 this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5042 $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5046 this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5048 set_value: function(value_) {
5050 value_ = value_ || [];
5051 if (value_.length >= 1 && value_[0] instanceof Array) {
5052 value_ = value_[0][2];
5053 } else if (value_.length >= 1 && value_[0] instanceof Object) {
5054 value_ = _.map(value_, function (value) { self.data[value.id] = value; return value.id;});
5056 this._super(value_);
5058 get_value: function() {
5059 var tmp = [commands.replace_with(this.get("value"))];
5062 get_file_url: function (attachment) {
5063 return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5065 read_name_values : function () {
5067 // don't reset know values
5068 var _value = _.filter(this.get('value'), function (id) { return typeof self.data[id] == 'undefined'; } );
5069 // send request for get_name
5070 if (_value.length) {
5071 return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).done(function (datas) {
5072 _.each(datas, function (data) {
5073 data.no_unlink = true;
5074 data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5075 self.data[data.id] = data;
5082 render_value: function () {
5084 this.read_name_values().then(function () {
5086 var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self}));
5087 render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5088 self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5090 // reinit input type file
5091 var $input = self.$('input.oe_form_binary_file');
5092 $input.after($input.clone(true)).remove();
5093 self.$(".oe_fileupload").show();
5097 on_file_change: function (event) {
5098 event.stopPropagation();
5100 var $target = $(event.target);
5101 if ($target.val() !== '') {
5102 var filename = $target.val().replace(/.*[\\\/]/,'');
5103 // don't uplode more of one file in same time
5104 if (self.data[0] && self.data[0].upload ) {
5107 for (var id in this.get('value')) {
5108 // if the files exits, delete the file before upload (if it's a new file)
5109 if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
5110 self.ds_file.unlink([id]);
5115 if(this.node.attrs.blockui>0) {
5116 instance.web.blockUI();
5119 // TODO : unactivate send on wizard and form
5122 this.$('form.oe_form_binary_form').submit();
5123 this.$(".oe_fileupload").hide();
5124 // add file on data result
5128 'filename': filename,
5134 on_file_loaded: function (event, result) {
5135 var files = this.get('value');
5138 if(this.node.attrs.blockui>0) {
5139 instance.web.unblockUI();
5142 if (result.error || !result.id ) {
5143 this.do_warn( _t('Uploading error'), result.error);
5144 delete this.data[0];
5146 if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
5147 delete this.data[0];
5148 this.data[result.id] = {
5150 'name': result.name,
5151 'filename': result.filename,
5152 'url': this.get_file_url(result)
5155 this.data[result.id] = {
5157 'name': result.name,
5158 'filename': result.filename,
5159 'url': this.get_file_url(result)
5162 var values = _.clone(this.get('value'));
5163 values.push(result.id);
5164 this.set({'value': values});
5168 on_file_delete: function (event) {
5169 event.stopPropagation();
5170 var file_id=$(event.target).data("id");
5172 var files = _.filter(this.get('value'), function (id) {return id != file_id;});
5173 if(!this.data[file_id].no_unlink) {
5174 this.ds_file.unlink([file_id]);
5176 this.set({'value': files});
5181 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5182 template: "FieldStatus",
5183 init: function(field_manager, node) {
5184 this._super(field_manager, node);
5185 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5186 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5187 this.set({value: false});
5190 if (this.options.clickable) {
5191 this.$el.on('click','li',this.on_click_stage);
5193 if (this.$el.parent().is('header')) {
5194 this.$el.after('<div class="oe_clear"/>');
5198 set_value: function(value_) {
5199 if (value_ instanceof Array) {
5202 this._super(value_);
5204 render_value: function() {
5206 self.get_selection().done(function() {
5207 var content = QWeb.render("FieldStatus.content", {widget: self});
5208 self.$el.html(content);
5209 var colors = JSON.parse((self.node.attrs || {}).statusbar_colors || "{}");
5210 var color = colors[self.get('value')];
5212 self.$("oe_active").css("color", color);
5216 /** Get the selection and render it
5217 * selection: [[identifier, value_to_display], ...]
5218 * For selection fields: this is directly given by this.field.selection
5219 * For many2one fields: perform a search on the relation of the many2one field
5221 get_selection: function() {
5223 self.selection = [];
5224 if (this.field.type == "many2one") {
5226 if(!_.isEmpty(this.field.domain) || !_.isEmpty(this.node.attrs.domain)) {
5227 var d = instance.web.pyeval.eval('domain', self.build_domain());
5228 domain = ['|', ['id', '=', self.get('value')]].concat(d);
5230 var ds = new instance.web.DataSetSearch(this, this.field.relation, self.build_context(), domain);
5231 return ds.read_slice(['name'], {}).then(function (records) {
5232 for(var i = 0; i < records.length; i++) {
5233 self.selection.push([records[i].id, records[i].name]);
5237 // For field type selection filter values according to
5238 // statusbar_visible attribute of the field. For example:
5239 // statusbar_visible="draft,open".
5240 var selection = this.field.selection;
5241 for(var i=0; i < selection.length; i++) {
5242 var key = selection[i][0];
5243 if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5244 this.selection.push(selection[i]);
5250 on_click_stage: function (ev) {
5252 var $li = $(ev.currentTarget);
5253 var val = parseInt($li.data("id"));
5254 if (val != self.get('value')) {
5255 this.view.recursive_save().done(function() {
5257 change[self.name] = val;
5258 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
5266 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
5267 template: "FieldMonetary",
5268 widget_class: 'oe_form_field_float oe_form_field_monetary',
5270 this._super.apply(this, arguments);
5271 this.set({"currency": false});
5272 if (this.options.currency_field) {
5273 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
5274 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
5277 this.on("change:currency", this, this.get_currency_info);
5278 this.get_currency_info();
5279 this.ci_dm = new instance.web.DropMisordered();
5282 var tmp = this._super();
5283 this.on("change:currency_info", this, this.reinitialize);
5286 get_currency_info: function() {
5288 if (this.get("currency") === false) {
5289 this.set({"currency_info": null});
5292 return this.ci_dm.add(new instance.web.Model("res.currency").query(["symbol", "position"])
5293 .filter([["id", "=", self.get("currency")]]).first()).then(function(res) {
5294 self.set({"currency_info": res});
5297 parse_value: function(val, def) {
5298 return instance.web.parse_value(val, {type: "float"}, def);
5300 format_value: function(val, def) {
5301 return instance.web.format_value(val, {type: "float"}, def);
5306 * Registry of form fields, called by :js:`instance.web.FormView`.
5308 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
5309 * will substitute to the <field> tags as defined in OpenERP's views.
5311 instance.web.form.widgets = new instance.web.Registry({
5312 'char' : 'instance.web.form.FieldChar',
5313 'id' : 'instance.web.form.FieldID',
5314 'email' : 'instance.web.form.FieldEmail',
5315 'url' : 'instance.web.form.FieldUrl',
5316 'text' : 'instance.web.form.FieldText',
5317 'html' : 'instance.web.form.FieldTextHtml',
5318 'date' : 'instance.web.form.FieldDate',
5319 'datetime' : 'instance.web.form.FieldDatetime',
5320 'selection' : 'instance.web.form.FieldSelection',
5321 'many2one' : 'instance.web.form.FieldMany2One',
5322 'many2onebutton' : 'instance.web.form.Many2OneButton',
5323 'many2many' : 'instance.web.form.FieldMany2Many',
5324 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
5325 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
5326 'one2many' : 'instance.web.form.FieldOne2Many',
5327 'one2many_list' : 'instance.web.form.FieldOne2Many',
5328 'reference' : 'instance.web.form.FieldReference',
5329 'boolean' : 'instance.web.form.FieldBoolean',
5330 'float' : 'instance.web.form.FieldFloat',
5331 'integer': 'instance.web.form.FieldFloat',
5332 'float_time': 'instance.web.form.FieldFloat',
5333 'progressbar': 'instance.web.form.FieldProgressBar',
5334 'image': 'instance.web.form.FieldBinaryImage',
5335 'binary': 'instance.web.form.FieldBinaryFile',
5336 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
5337 'statusbar': 'instance.web.form.FieldStatus',
5338 'monetary': 'instance.web.form.FieldMonetary',
5342 * Registry of widgets usable in the form view that can substitute to any possible
5343 * tags defined in OpenERP's form views.
5345 * Every referenced class should extend FormWidget.
5347 instance.web.form.tags = new instance.web.Registry({
5348 'button' : 'instance.web.form.WidgetButton',
5351 instance.web.form.custom_widgets = new instance.web.Registry({
5356 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: