1 openerp.web.form = function (instance) {
2 var _t = instance.web._t,
3 _lt = instance.web._lt;
4 var QWeb = instance.web.qweb;
7 instance.web.form = {};
10 * Interface implemented by the form view or any other object
11 * able to provide the features necessary for the fields to work.
14 * - display_invalid_fields : if true, all fields where is_valid() return true should
15 * be displayed as invalid.
16 * - actual_mode : the current mode of the field manager. Can be "view", "edit" or "create".
18 * - view_content_has_changed : when the values of the fields have changed. When
19 * this event is triggered all fields should reprocess their modifiers.
20 * - field_changed:<field_name> : when the value of a field change, an event is triggered
21 * named "field_changed:<field_name>" with <field_name> replaced by the name of the field.
22 * This event is not related to the on_change mechanism of OpenERP and is always called
23 * when the value of a field is setted or changed. This event is only triggered when the
24 * value of the field is syntactically valid, but it can be triggered when the value
25 * is sematically invalid (ie, when a required field is false). It is possible that an event
26 * about a precise field is never triggered even if that field exists in the view, in that
27 * case the value of the field is assumed to be false.
29 instance.web.form.FieldManagerMixin = {
31 * Must return the asked field as in fields_get.
33 get_field_desc: function(field_name) {},
35 * Returns the current value of a field present in the view. See the get_value() method
36 * method in FieldInterface for further information.
38 get_field_value: function(field_name) {},
40 Gives new values for the fields contained in the view. The new values could not be setted
41 right after the call to this method. Setting new values can trigger on_changes.
43 @param (dict) values A dictonnary with key = field name and value = new value.
44 @return (Deferred) Is resolved after all the values are setted.
46 set_values: function(values) {},
48 Computes an OpenERP domain.
50 @param (list) expression An OpenERP domain.
51 @return (boolean) The computed value of the domain.
53 compute_domain: function(expression) {},
55 Builds an evaluation context for the resolution of the fields' contexts. Please note
56 the field are only supposed to use this context to evualuate their own, they should not
59 @return (CompoundContext) An OpenERP context.
61 build_eval_context: function() {},
64 instance.web.views.add('form', 'instance.web.FormView');
67 * - actual_mode: always "view", "edit" or "create". Read-only property. Determines
68 * the mode used by the view.
70 instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerMixin, {
72 * Indicates that this view is not searchable, and thus that no search
73 * view should be displayed (if there is one active).
77 display_name: _lt('Form'),
80 * @constructs instance.web.FormView
81 * @extends instance.web.View
83 * @param {instance.web.Session} session the current openerp session
84 * @param {instance.web.DataSet} dataset the dataset this view will work with
85 * @param {String} view_id the identifier of the OpenERP view object
86 * @param {Object} options
87 * - resize_textareas : [true|false|max_height]
89 * @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance
91 init: function(parent, dataset, view_id, options) {
94 this.ViewManager = parent;
95 this.set_default_options(options);
96 this.dataset = dataset;
97 this.model = dataset.model;
98 this.view_id = view_id || false;
99 this.fields_view = {};
101 this.fields_order = [];
102 this.datarecord = {};
103 this.default_focus_field = null;
104 this.default_focus_button = null;
105 this.fields_registry = instance.web.form.widgets;
106 this.tags_registry = instance.web.form.tags;
107 this.widgets_registry = instance.web.form.custom_widgets;
108 this.has_been_loaded = $.Deferred();
109 this.translatable_fields = [];
110 _.defaults(this.options, {
111 "not_interactible_on_create": false,
112 "initial_mode": "view",
113 "disable_autofocus": false,
114 "footer_to_buttons": false,
116 this.is_initialized = $.Deferred();
117 this.mutating_mutex = new $.Mutex();
118 this.on_change_list = [];
120 this.reload_mutex = new $.Mutex();
121 this.__clicked_inside = false;
122 this.__blur_timeout = null;
123 this.rendering_engine = new instance.web.form.FormRenderingEngine(this);
124 self.set({actual_mode: self.options.initial_mode});
125 this.has_been_loaded.done(function() {
126 self.on("change:actual_mode", self, self.check_actual_mode);
127 self.check_actual_mode();
128 self.on("change:actual_mode", self, self.init_pager);
131 self.on("load_record", self, self.load_record);
132 instance.web.bus.on('clear_uncommitted_changes', this, function(e) {
133 if (!this.can_be_discarded()) {
138 view_loading: function(r) {
139 return this.load_form(r);
141 destroy: function() {
142 _.each(this.get_widgets(), function(w) {
143 w.off('focused blurred');
147 this.$el.off('.formBlur');
151 load_form: function(data) {
154 throw new Error(_t("No data provided."));
157 throw "Form view does not support multiple calls to load_form";
159 this.fields_order = [];
160 this.fields_view = data;
162 this.rendering_engine.set_fields_registry(this.fields_registry);
163 this.rendering_engine.set_tags_registry(this.tags_registry);
164 this.rendering_engine.set_widgets_registry(this.widgets_registry);
165 this.rendering_engine.set_fields_view(data);
166 var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container');
167 this.rendering_engine.render_to($dest);
169 this.$el.on('mousedown.formBlur', function () {
170 self.__clicked_inside = true;
173 this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self}));
174 if (this.options.$buttons) {
175 this.$buttons.appendTo(this.options.$buttons);
177 this.$el.find('.oe_form_buttons').replaceWith(this.$buttons);
179 this.$buttons.on('click', '.oe_form_button_create',
180 this.guard_active(this.on_button_create));
181 this.$buttons.on('click', '.oe_form_button_edit',
182 this.guard_active(this.on_button_edit));
183 this.$buttons.on('click', '.oe_form_button_save',
184 this.guard_active(this.on_button_save));
185 this.$buttons.on('click', '.oe_form_button_cancel',
186 this.guard_active(this.on_button_cancel));
187 if (this.options.footer_to_buttons) {
188 this.$el.find('footer').appendTo(this.$buttons);
191 this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar');
192 if (!this.sidebar && this.options.$sidebar) {
193 this.sidebar = new instance.web.Sidebar(this);
194 this.sidebar.appendTo(this.$sidebar);
195 if (this.fields_view.toolbar) {
196 this.sidebar.add_toolbar(this.fields_view.toolbar);
198 this.sidebar.add_items('other', _.compact([
199 self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete },
200 self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate }
204 this.has_been_loaded.resolve();
206 // Add bounce effect on button 'Edit' when click on readonly page view.
207 this.$el.find(".oe_form_group_row,.oe_form_field,label").on('click', function (e) {
208 if(self.get("actual_mode") == "view") {
209 var $button = self.options.$buttons.find(".oe_form_button_edit");
210 $button.openerpBounce();
212 instance.web.bus.trigger('click', e);
215 //bounce effect on red button when click on statusbar.
216 this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) {
217 if((self.get("actual_mode") == "view")) {
218 var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'});
219 $button.openerpBounce();
223 this.trigger('form_view_loaded', data);
226 widgetFocused: function() {
227 // Clear click flag if used to focus a widget
228 this.__clicked_inside = false;
229 if (this.__blur_timeout) {
230 clearTimeout(this.__blur_timeout);
231 this.__blur_timeout = null;
234 widgetBlurred: function() {
235 if (this.__clicked_inside) {
236 // clicked in an other section of the form (than the currently
237 // focused widget) => just ignore the blurring entirely?
238 this.__clicked_inside = false;
242 // clear timeout, if any
243 this.widgetFocused();
244 this.__blur_timeout = setTimeout(function () {
245 self.trigger('blurred');
249 do_load_state: function(state, warm) {
250 if (state.id && this.datarecord.id != state.id) {
251 if (this.dataset.get_id_index(state.id) === null) {
252 this.dataset.ids.push(state.id);
254 this.dataset.select_id(state.id);
255 this.do_show({ reload: warm });
260 * @param {Object} [options]
261 * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view".
262 * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record
263 * @return {$.Deferred}
265 do_show: function (options) {
267 options = options || {};
269 this.sidebar.$el.show();
272 this.$buttons.show();
274 this.$el.show().css({
276 filter: 'alpha(opacity = 0)'
278 this.$el.add(this.$buttons).removeClass('oe_form_dirty');
280 var shown = this.has_been_loaded;
281 if (options.reload !== false) {
282 shown = shown.then(function() {
283 if (self.dataset.index === null) {
284 // null index means we should start a new record
285 return self.on_button_new();
287 var fields = _.keys(self.fields_view.fields);
288 fields.push('display_name');
289 return self.dataset.read_index(fields, {
290 context: { 'bin_size': true, 'future_display_name' : true }
291 }).then(function(r) {
292 self.trigger('load_record', r);
296 return shown.then(function() {
297 self._actualize_mode(options.mode || self.options.initial_mode);
300 filter: 'alpha(opacity = 100)'
304 do_hide: function () {
306 this.sidebar.$el.hide();
309 this.$buttons.hide();
316 load_record: function(record) {
317 var self = this, set_values = [];
319 this.set({ 'title' : undefined });
320 this.do_warn(_t("Form"), _t("The record could not be found in the database."), true);
321 return $.Deferred().reject();
323 this.datarecord = record;
324 this._actualize_mode();
325 this.set({ 'title' : record.id ? record.display_name : _t("New") });
327 _(this.fields).each(function (field, f) {
328 field._dirty_flag = false;
329 field._inhibit_on_change_flag = true;
330 var result = field.set_value(self.datarecord[f] || false);
331 field._inhibit_on_change_flag = false;
332 set_values.push(result);
334 return $.when.apply(null, set_values).then(function() {
336 // New record: Second pass in order to trigger the onchanges
337 // respecting the fields order defined in the view
338 _.each(self.fields_order, function(field_name) {
339 if (record[field_name] !== undefined) {
340 var field = self.fields[field_name];
341 field._dirty_flag = true;
342 self.do_onchange(field);
346 self.on_form_changed();
347 self.rendering_engine.init_fields();
348 self.is_initialized.resolve();
349 self.do_update_pager(record.id == null);
351 self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
354 self.do_push_state({id:record.id});
356 self.do_push_state({});
358 self.$el.add(self.$buttons).removeClass('oe_form_dirty');
363 * Loads and sets up the default values for the model as the current
366 * @return {$.Deferred}
368 load_defaults: function () {
370 var keys = _.keys(this.fields_view.fields);
372 return this.dataset.default_get(keys).then(function(r) {
373 self.trigger('load_record', r);
376 return self.trigger('load_record', {});
378 on_form_changed: function() {
379 this.trigger("view_content_has_changed");
381 do_notify_change: function() {
382 this.$el.add(this.$buttons).addClass('oe_form_dirty');
384 execute_pager_action: function(action) {
385 if (this.can_be_discarded()) {
388 this.dataset.index = 0;
391 this.dataset.previous();
397 this.dataset.index = this.dataset.ids.length - 1;
401 this.trigger('pager_action_executed');
404 init_pager: function() {
407 this.$pager.remove();
408 if (this.get("actual_mode") === "create")
410 this.$pager = $(QWeb.render("FormView.pager", {'widget':self})).hide();
411 if (this.options.$pager) {
412 this.$pager.appendTo(this.options.$pager);
414 this.$el.find('.oe_form_pager').replaceWith(this.$pager);
416 this.$pager.on('click','a[data-pager-action]',function() {
417 var action = $(this).data('pager-action');
418 self.execute_pager_action(action);
420 this.do_update_pager();
422 do_update_pager: function(hide_index) {
423 this.$pager.toggle(this.dataset.ids.length > 1);
425 $(".oe_form_pager_state", this.$pager).html("");
427 $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
430 parse_on_change: function (on_change, widget) {
432 var onchange = _.str.trim(on_change);
433 var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
435 throw new Error(_.str.sprintf( _t("Wrong on change format: %s"), onchange ));
438 var method = call[1];
439 if (!_.str.trim(call[2])) {
440 return {method: method, args: []}
443 var argument_replacement = {
444 'False': function () {return false;},
445 'True': function () {return true;},
446 'None': function () {return null;},
447 'context': function () {
448 return new instance.web.CompoundContext(
449 self.dataset.get_context(),
450 widget.build_context() ? widget.build_context() : {});
453 var parent_fields = null;
454 var args = _.map(call[2].split(','), function (a, i) {
455 var field = _.str.trim(a);
457 // literal constant or context
458 if (field in argument_replacement) {
459 return argument_replacement[field]();
462 if (/^-?\d+(\.\d+)?$/.test(field)) {
463 return Number(field);
466 if (self.fields[field]) {
467 var value_ = self.fields[field].get_value();
468 return value_ == null ? false : value_;
471 var splitted = field.split('.');
472 if (splitted.length > 1 && _.str.trim(splitted[0]) === "parent" && self.dataset.parent_view) {
473 if (parent_fields === null) {
474 parent_fields = self.dataset.parent_view.get_fields_values();
476 var p_val = parent_fields[_.str.trim(splitted[1])];
477 if (p_val !== undefined) {
478 return p_val == null ? false : p_val;
482 var first_char = field[0], last_char = field[field.length-1];
483 if ((first_char === '"' && last_char === '"')
484 || (first_char === "'" && last_char === "'")) {
485 return field.slice(1, -1);
488 throw new Error("Could not get field with name '" + field +
489 "' for onchange '" + onchange + "'");
497 do_onchange: function(widget, processed) {
499 this.on_change_list = [{widget: widget, processed: processed}].concat(this.on_change_list);
500 return this._process_operations();
502 _process_onchange: function(on_change_obj) {
504 var widget = on_change_obj.widget;
505 var processed = on_change_obj.processed;
508 processed = processed || [];
509 processed.push(widget.name);
510 var on_change = widget.node.attrs.on_change;
512 var change_spec = self.parse_on_change(on_change, widget);
514 if (self.datarecord.id && !instance.web.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) {
515 // In case of a o2m virtual id, we should pass an empty ids list
516 ids.push(self.datarecord.id);
518 def = self.alive(new instance.web.Model(self.dataset.model).call(
519 change_spec.method, [ids].concat(change_spec.args)));
523 return def.then(function(response) {
524 if (widget.field['change_default']) {
525 var fieldname = widget.name;
527 if (response.value && (fieldname in response.value)) {
528 // Use value from onchange if onchange executed
529 value_ = response.value[fieldname];
531 // otherwise get form value for field
532 value_ = self.fields[fieldname].get_value();
534 var condition = fieldname + '=' + value_;
537 return self.alive(new instance.web.Model('ir.values').call(
538 'get_defaults', [self.model, condition]
539 )).then(function (results) {
540 if (!results.length) {
543 if (!response.value) {
546 for(var i=0; i<results.length; ++i) {
547 // [whatever, key, value]
548 var triplet = results[i];
549 response.value[triplet[1]] = triplet[2];
556 }).then(function(response) {
557 return self.on_processed_onchange(response, processed);
561 instance.webclient.crashmanager.show_message(e);
562 return $.Deferred().reject();
565 on_processed_onchange: function(result, processed) {
568 this._internal_set_values(result.value, processed);
570 if (!_.isEmpty(result.warning)) {
571 instance.web.dialog($(QWeb.render("CrashManager.warning", result.warning)), {
572 title:result.warning.title,
575 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
580 var fields = this.fields;
581 _(result.domain).each(function (domain, fieldname) {
582 var field = fields[fieldname];
583 if (!field) { return; }
584 field.node.attrs.domain = domain;
587 return $.Deferred().resolve();
590 instance.webclient.crashmanager.show_message(e);
591 return $.Deferred().reject();
594 _process_operations: function() {
596 return this.mutating_mutex.exec(function() {
598 var on_change_obj = self.on_change_list.shift();
600 return self._process_onchange(on_change_obj).then(function() {
605 _.each(self.fields, function(field) {
606 defs.push(field.commit_value());
608 var args = _.toArray(arguments);
609 return $.when.apply($, defs).then(function() {
610 if (self.on_change_list.length !== 0) {
613 var save_obj = self.save_list.pop();
615 return self._process_save(save_obj).then(function() {
616 save_obj.ret = _.toArray(arguments);
619 save_obj.error = true;
628 _internal_set_values: function(values, exclude) {
629 exclude = exclude || [];
630 for (var f in values) {
631 if (!values.hasOwnProperty(f)) { continue; }
632 var field = this.fields[f];
633 // If field is not defined in the view, just ignore it
635 var value_ = values[f];
636 if (field.get_value() != value_) {
637 field._inhibit_on_change_flag = true;
638 field.set_value(value_);
639 field._inhibit_on_change_flag = false;
640 field._dirty_flag = true;
641 if (!_.contains(exclude, field.name)) {
642 this.do_onchange(field, exclude);
647 this.on_form_changed();
649 set_values: function(values) {
651 return this.mutating_mutex.exec(function() {
652 self._internal_set_values(values);
656 * Ask the view to switch to view mode if possible. The view may not do it
657 * if the current record is not yet saved. It will then stay in create mode.
659 to_view_mode: function() {
660 this._actualize_mode("view");
663 * Ask the view to switch to edit mode if possible. The view may not do it
664 * if the current record is not yet saved. It will then stay in create mode.
666 to_edit_mode: function() {
667 this._actualize_mode("edit");
670 * Ask the view to switch to a precise mode if possible. The view is free to
671 * not respect this command if the state of the dataset is not compatible with
672 * the new mode. For example, it is not possible to switch to edit mode if
673 * the current record is not yet saved in database.
675 * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
676 * undefined the view will test the actual mode to check if it is still consistent
677 * with the dataset state.
679 _actualize_mode: function(switch_to) {
680 var mode = switch_to || this.get("actual_mode");
681 if (! this.datarecord.id) {
683 } else if (mode === "create") {
686 this.set({actual_mode: mode});
688 check_actual_mode: function(source, options) {
690 if(this.get("actual_mode") === "view") {
691 self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
692 self.$buttons.find('.oe_form_buttons_edit').hide();
693 self.$buttons.find('.oe_form_buttons_view').show();
694 self.$sidebar.show();
696 self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
697 self.$buttons.find('.oe_form_buttons_edit').show();
698 self.$buttons.find('.oe_form_buttons_view').hide();
699 self.$sidebar.hide();
703 autofocus: function() {
704 if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
705 var fields_order = this.fields_order.slice(0);
706 if (this.default_focus_field) {
707 fields_order.unshift(this.default_focus_field.name);
709 for (var i = 0; i < fields_order.length; i += 1) {
710 var field = this.fields[fields_order[i]];
711 if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
712 if (field.focus() !== false) {
719 on_button_save: function() {
721 return this.save().done(function(result) {
722 self.trigger("save", result);
724 }).then(function(result) {
725 var parent = self.ViewManager.ActionManager.getParent();
727 parent.menu.do_reload_needaction();
731 on_button_cancel: function(event) {
732 if (this.can_be_discarded()) {
733 if (this.get('actual_mode') === 'create') {
734 this.trigger('history_back');
737 this.trigger('load_record', this.datarecord);
740 this.trigger('on_button_cancel');
743 on_button_new: function() {
746 return $.when(this.has_been_loaded).then(function() {
747 if (self.can_be_discarded()) {
748 return self.load_defaults();
752 on_button_edit: function() {
753 return this.to_edit_mode();
755 on_button_create: function() {
756 this.dataset.index = null;
759 on_button_duplicate: function() {
761 return this.has_been_loaded.then(function() {
762 return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
763 self.record_created(new_id);
768 on_button_delete: function() {
770 var def = $.Deferred();
771 this.has_been_loaded.done(function() {
772 if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
773 self.dataset.unlink([self.datarecord.id]).done(function() {
774 if (self.dataset.size()) {
775 self.execute_pager_action('next');
777 self.do_action('history_back');
782 $.async_when().done(function () {
787 return def.promise();
789 can_be_discarded: function() {
790 if (this.$el.is('.oe_form_dirty')) {
791 if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
794 this.$el.removeClass('oe_form_dirty');
799 * Triggers saving the form's record. Chooses between creating a new
800 * record or saving an existing one depending on whether the record
801 * already has an id property.
803 * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
804 * record, should that record be inserted at the start of the dataset (by
805 * default, records are added at the end)
807 save: function(prepend_on_create) {
809 var save_obj = {prepend_on_create: prepend_on_create, ret: null};
810 this.save_list.push(save_obj);
811 return this._process_operations().then(function() {
813 return $.Deferred().reject();
814 return $.when.apply($, save_obj.ret);
816 self.$el.removeClass('oe_form_dirty');
819 _process_save: function(save_obj) {
821 var prepend_on_create = save_obj.prepend_on_create;
823 var form_invalid = false,
825 first_invalid_field = null,
826 readonly_values = {};
827 for (var f in self.fields) {
828 if (!self.fields.hasOwnProperty(f)) { continue; }
832 if (!first_invalid_field) {
833 first_invalid_field = f;
835 } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
836 // Special case 'id' field, do not save this field
837 // on 'create' : save all non readonly fields
838 // on 'edit' : save non readonly modified fields
839 if (!f.get("readonly")) {
840 values[f.name] = f.get_value();
842 readonly_values[f.name] = f.get_value();
847 self.set({'display_invalid_fields': true});
848 first_invalid_field.focus();
850 return $.Deferred().reject();
852 self.set({'display_invalid_fields': false});
854 if (!self.datarecord.id) {
856 save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
857 return self.record_created(r, prepend_on_create);
859 } else if (_.isEmpty(values)) {
860 // Not dirty, noop save
861 save_deferral = $.Deferred().resolve({}).promise();
864 save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
865 return self.record_saved(r);
868 return save_deferral;
872 return $.Deferred().reject();
875 on_invalid: function() {
876 var warnings = _(this.fields).chain()
877 .filter(function (f) { return !f.is_valid(); })
879 return _.str.sprintf('<li>%s</li>',
882 warnings.unshift('<ul>');
883 warnings.push('</ul>');
884 this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
887 * Reload the form after saving
889 * @param {Object} r result of the write function.
891 record_saved: function(r) {
894 // should not happen in the server, but may happen for internal purpose
895 this.trigger('record_saved', r);
896 return $.Deferred().reject();
898 return $.when(this.reload()).then(function () {
899 self.trigger('record_saved', r);
905 * Updates the form' dataset to contain the new record:
907 * * Adds the newly created record to the current dataset (at the end by
909 * * Selects that record (sets the dataset's index to point to the new
911 * * Updates the pager and sidebar displays
914 * @param {Boolean} [prepend_on_create=false] adds the newly created record
915 * at the beginning of the dataset instead of the end
917 record_created: function(r, prepend_on_create) {
920 // should not happen in the server, but may happen for internal purpose
921 this.trigger('record_created', r);
922 return $.Deferred().reject();
924 this.datarecord.id = r;
925 if (!prepend_on_create) {
926 this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
927 this.dataset.index = this.dataset.ids.length - 1;
929 this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
930 this.dataset.index = 0;
932 this.do_update_pager();
934 this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
936 //openerp.log("The record has been created with id #" + this.datarecord.id);
937 return $.when(this.reload()).then(function () {
938 self.trigger('record_created', r);
939 return _.extend(r, {created: true});
943 on_action: function (action) {
944 console.debug('Executing action', action);
948 return this.reload_mutex.exec(function() {
949 if (self.dataset.index == null) {
950 self.trigger("previous_view");
951 return $.Deferred().reject().promise();
953 if (self.dataset.index == null || self.dataset.index < 0) {
954 return $.when(self.on_button_new());
956 var fields = _.keys(self.fields_view.fields);
957 fields.push('display_name');
958 return self.dataset.read_index(fields,
962 'future_display_name': true
964 }).then(function(r) {
965 self.trigger('load_record', r);
970 get_widgets: function() {
971 return _.filter(this.getChildren(), function(obj) {
972 return obj instanceof instance.web.form.FormWidget;
975 get_fields_values: function() {
977 var ids = this.get_selected_ids();
978 values["id"] = ids.length > 0 ? ids[0] : false;
979 _.each(this.fields, function(value_, key) {
980 values[key] = value_.get_value();
984 get_selected_ids: function() {
985 var id = this.dataset.ids[this.dataset.index];
986 return id ? [id] : [];
988 recursive_save: function() {
990 return $.when(this.save()).then(function(res) {
991 if (self.dataset.parent_view)
992 return self.dataset.parent_view.recursive_save();
995 recursive_reload: function() {
998 if (self.dataset.parent_view)
999 pre = self.dataset.parent_view.recursive_reload();
1000 return pre.then(function() {
1001 return self.reload();
1004 is_dirty: function() {
1005 return _.any(this.fields, function (value_) {
1006 return value_._dirty_flag;
1009 is_interactible_record: function() {
1010 var id = this.datarecord.id;
1012 if (this.options.not_interactible_on_create)
1014 } else if (typeof(id) === "string") {
1015 if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
1020 sidebar_eval_context: function () {
1021 return $.when(this.build_eval_context());
1023 open_defaults_dialog: function () {
1025 var display = function (field, value) {
1026 if (field instanceof instance.web.form.FieldSelection) {
1027 return _(field.values).find(function (option) {
1028 return option[0] === value;
1030 } else if (field instanceof instance.web.form.FieldMany2One) {
1031 return field.get_displayed();
1035 var fields = _.chain(this.fields)
1036 .map(function (field) {
1037 var value = field.get_value();
1038 // ignore fields which are empty, invisible, readonly, o2m
1041 || field.get('invisible')
1042 || field.get("readonly")
1043 || field.field.type === 'one2many'
1044 || field.field.type === 'many2many'
1045 || field.field.type === 'binary'
1046 || field.password) {
1052 string: field.string,
1054 displayed: display(field, value),
1058 .sortBy(function (field) { return field.string; })
1060 var conditions = _.chain(self.fields)
1061 .filter(function (field) { return field.field.change_default; })
1062 .map(function (field) {
1063 var value = field.get_value();
1066 string: field.string,
1068 displayed: display(field, value),
1073 var d = new instance.web.Dialog(this, {
1074 title: _t("Set Default"),
1077 conditions: conditions
1080 {text: _t("Close"), click: function () { d.close(); }},
1081 {text: _t("Save default"), click: function () {
1082 var $defaults = d.$el.find('#formview_default_fields');
1083 var field_to_set = $defaults.val();
1084 if (!field_to_set) {
1085 $defaults.parent().addClass('oe_form_invalid');
1088 var condition = d.$el.find('#formview_default_conditions').val(),
1089 all_users = d.$el.find('#formview_default_all').is(':checked');
1090 new instance.web.DataSet(self, 'ir.values').call(
1094 self.fields[field_to_set].get_value(),
1098 ]).done(function () { d.close(); });
1102 d.template = 'FormView.set_default';
1105 register_field: function(field, name) {
1106 this.fields[name] = field;
1107 this.fields_order.push(name);
1108 if (JSON.parse(field.node.attrs.default_focus || "0")) {
1109 this.default_focus_field = field;
1112 field.on('focused', null, this.proxy('widgetFocused'))
1113 .on('blurred', null, this.proxy('widgetBlurred'));
1114 if (this.get_field_desc(name).translate) {
1115 this.translatable_fields.push(field);
1117 field.on('changed_value', this, function() {
1118 if (field.is_syntax_valid()) {
1119 this.trigger('field_changed:' + name);
1121 if (field._inhibit_on_change_flag) {
1124 field._dirty_flag = true;
1125 if (field.is_syntax_valid()) {
1126 this.do_onchange(field);
1127 this.on_form_changed(true);
1128 this.do_notify_change();
1132 get_field_desc: function(field_name) {
1133 return this.fields_view.fields[field_name];
1135 get_field_value: function(field_name) {
1136 return this.fields[field_name].get_value();
1138 compute_domain: function(expression) {
1139 return instance.web.form.compute_domain(expression, this.fields);
1141 _build_view_fields_values: function() {
1142 var a_dataset = this.dataset;
1143 var fields_values = this.get_fields_values();
1144 var active_id = a_dataset.ids[a_dataset.index];
1145 _.extend(fields_values, {
1146 active_id: active_id || false,
1147 active_ids: active_id ? [active_id] : [],
1148 active_model: a_dataset.model,
1151 if (a_dataset.parent_view) {
1152 fields_values.parent = a_dataset.parent_view.get_fields_values();
1154 return fields_values;
1156 build_eval_context: function() {
1157 var a_dataset = this.dataset;
1158 return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
1163 * Interface to be implemented by rendering engines for the form view.
1165 instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
1166 set_fields_view: function(fields_view) {},
1167 set_fields_registry: function(fields_registry) {},
1168 render_to: function($el) {},
1172 * Default rendering engine for the form view.
1174 * It is necessary to set the view using set_view() before usage.
1176 instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
1177 init: function(view) {
1180 set_fields_view: function(fvg) {
1182 this.version = parseFloat(this.fvg.arch.attrs.version);
1183 if (isNaN(this.version)) {
1187 set_tags_registry: function(tags_registry) {
1188 this.tags_registry = tags_registry;
1190 set_fields_registry: function(fields_registry) {
1191 this.fields_registry = fields_registry;
1193 set_widgets_registry: function(widgets_registry) {
1194 this.widgets_registry = widgets_registry;
1196 // Backward compatibility tools, current default version: v6.1
1197 process_version: function() {
1198 if (this.version < 7.0) {
1199 this.$form.find('form:first').wrapInner('<group col="4"/>');
1200 this.$form.find('page').each(function() {
1201 if (!$(this).parents('field').length) {
1202 $(this).wrapInner('<group col="4"/>');
1207 get_arch_fragment: function() {
1208 var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
1209 // IE won't allow custom button@type and will revert it to spec default : 'submit'
1210 $('button', doc).each(function() {
1211 $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
1213 // IE's html parser is also a css parser. How convenient...
1214 $('board', doc).each(function() {
1215 $(this).attr('layout', $(this).attr('style'));
1217 return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
1219 render_to: function($target) {
1221 this.$target = $target;
1223 this.$form = this.get_arch_fragment();
1225 this.process_version();
1227 this.fields_to_init = [];
1228 this.tags_to_init = [];
1229 this.widgets_to_init = [];
1231 this.process(this.$form);
1233 this.$form.appendTo(this.$target);
1235 this.to_replace = [];
1237 _.each(this.fields_to_init, function($elem) {
1238 var name = $elem.attr("name");
1239 if (!self.fvg.fields[name]) {
1240 throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
1242 var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
1244 throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
1246 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1247 var $label = self.labels[$elem.attr("name")];
1249 w.set_input_id($label.attr("for"));
1251 self.alter_field(w);
1252 self.view.register_field(w, $elem.attr("name"));
1253 self.to_replace.push([w, $elem]);
1255 _.each(this.tags_to_init, function($elem) {
1256 var tag_name = $elem[0].tagName.toLowerCase();
1257 var obj = self.tags_registry.get_object(tag_name);
1258 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1259 self.to_replace.push([w, $elem]);
1261 _.each(this.widgets_to_init, function($elem) {
1262 var widget_type = $elem.attr("type");
1263 var obj = self.widgets_registry.get_object(widget_type);
1264 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1265 self.to_replace.push([w, $elem]);
1268 init_fields: function() {
1270 _.each(this.to_replace, function(el) {
1271 defs.push(el[0].replace(el[1]));
1273 this.to_replace = [];
1274 return $.when.apply($, defs);
1276 render_element: function(template /* dictionaries */) {
1277 var dicts = [].slice.call(arguments).slice(1);
1278 var dict = _.extend.apply(_, dicts);
1279 dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
1280 return $(QWeb.render(template, dict));
1282 alter_field: function(field) {
1284 toggle_layout_debugging: function() {
1285 if (!this.$target.has('.oe_layout_debug_cell:first').length) {
1286 this.$target.find('[title]').removeAttr('title');
1287 this.$target.find('.oe_form_group_cell').each(function() {
1288 var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
1289 $(this).attr('title', text);
1292 this.$target.toggleClass('oe_layout_debugging');
1294 process: function($tag) {
1296 var tagname = $tag[0].nodeName.toLowerCase();
1297 if (this.tags_registry.contains(tagname)) {
1298 this.tags_to_init.push($tag);
1301 var fn = self['process_' + tagname];
1303 var args = [].slice.call(arguments);
1305 return fn.apply(self, args);
1307 // generic tag handling, just process children
1308 $tag.children().each(function() {
1309 self.process($(this));
1311 self.handle_common_properties($tag, $tag);
1312 $tag.removeAttr("modifiers");
1316 process_widget: function($widget) {
1317 this.widgets_to_init.push($widget);
1320 process_sheet: function($sheet) {
1321 var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
1322 this.handle_common_properties($new_sheet, $sheet);
1323 var $dst = $new_sheet.find('.oe_form_sheet');
1324 $sheet.contents().appendTo($dst);
1325 $sheet.before($new_sheet).remove();
1326 this.process($new_sheet);
1328 process_form: function($form) {
1329 if ($form.find('> sheet').length === 0) {
1330 $form.addClass('oe_form_nosheet');
1332 var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
1333 this.handle_common_properties($new_form, $form);
1334 $form.contents().appendTo($new_form);
1335 if ($form[0] === this.$form[0]) {
1336 // If root element, replace it
1337 this.$form = $new_form;
1339 $form.before($new_form).remove();
1341 this.process($new_form);
1344 * Used by direct <field> children of a <group> tag only
1345 * This method will add the implicit <label...> for every field
1348 preprocess_field: function($field) {
1350 var name = $field.attr('name'),
1351 field_colspan = parseInt($field.attr('colspan'), 10),
1352 field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
1354 if ($field.attr('nolabel') === '1')
1356 $field.attr('nolabel', '1');
1358 this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
1359 $(el).parents().each(function(unused, tag) {
1360 var name = tag.tagName.toLowerCase();
1361 if (name === "field" || name in self.tags_registry.map)
1368 var $label = $('<label/>').attr({
1370 "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
1371 "string": $field.attr('string'),
1372 "help": $field.attr('help'),
1373 "class": $field.attr('class'),
1375 $label.insertBefore($field);
1376 if (field_colspan > 1) {
1377 $field.attr('colspan', field_colspan - 1);
1381 process_field: function($field) {
1382 if ($field.parent().is('group')) {
1383 // No implicit labels for normal fields, only for <group> direct children
1384 var $label = this.preprocess_field($field);
1386 this.process($label);
1389 this.fields_to_init.push($field);
1392 process_group: function($group) {
1394 $group.children('field').each(function() {
1395 self.preprocess_field($(this));
1397 var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
1399 if ($new_group.first().is('table.oe_form_group')) {
1400 $table = $new_group;
1401 } else if ($new_group.filter('table.oe_form_group').length) {
1402 $table = $new_group.filter('table.oe_form_group').first();
1404 $table = $new_group.find('table.oe_form_group').first();
1408 cols = parseInt($group.attr('col') || 2, 10),
1412 $group.children().each(function(a,b,c) {
1413 var $child = $(this);
1414 var colspan = parseInt($child.attr('colspan') || 1, 10);
1415 var tagName = $child[0].tagName.toLowerCase();
1416 var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
1417 var newline = tagName === 'newline';
1419 // Note FME: those classes are used in layout debug mode
1420 if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
1421 $tr.addClass('oe_form_group_row_incomplete');
1423 $tr.addClass('oe_form_group_row_newline');
1430 if (!$tr || row_cols < colspan) {
1431 $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
1433 } else if (tagName==='group') {
1434 // When <group> <group/><group/> </group>, we need a spacing between the two groups
1435 $td.addClass('oe_group_right')
1437 row_cols -= colspan;
1439 // invisibility transfer
1440 var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
1441 var invisible = field_modifiers.invisible;
1442 self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
1444 $tr.append($td.append($child));
1445 children.push($child[0]);
1447 if (row_cols && $td) {
1448 $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
1450 $group.before($new_group).remove();
1452 $table.find('> tbody > tr').each(function() {
1453 var to_compute = [],
1456 $(this).children().each(function() {
1458 $child = $td.children(':first');
1459 if ($child.attr('cell-class')) {
1460 $td.addClass($child.attr('cell-class'));
1462 switch ($child[0].tagName.toLowerCase()) {
1466 if ($child.attr('for')) {
1467 $td.attr('width', '1%').addClass('oe_form_group_cell_label');
1468 row_cols-= $td.attr('colspan') || 1;
1473 var width = _.str.trim($child.attr('width') || ''),
1474 iwidth = parseInt(width, 10);
1476 if (width.substr(-1) === '%') {
1478 width = iwidth + '%';
1481 $td.css('min-width', width + 'px');
1483 $td.attr('width', width);
1484 $child.removeAttr('width');
1485 row_cols-= $td.attr('colspan') || 1;
1487 to_compute.push($td);
1493 var unit = Math.floor(total / row_cols);
1494 if (!$(this).is('.oe_form_group_row_incomplete')) {
1495 _.each(to_compute, function($td, i) {
1496 var width = parseInt($td.attr('colspan'), 10) * unit;
1497 $td.attr('width', width + '%');
1503 _.each(children, function(el) {
1504 self.process($(el));
1506 this.handle_common_properties($new_group, $group);
1509 process_notebook: function($notebook) {
1512 $notebook.find('> page').each(function() {
1513 var $page = $(this);
1514 var page_attrs = $page.getAttributes();
1515 page_attrs.id = _.uniqueId('notebook_page_');
1516 var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
1517 $page.contents().appendTo($new_page);
1518 $page.before($new_page).remove();
1519 var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
1520 page_attrs.__page = $new_page;
1521 page_attrs.__ic = ic;
1522 pages.push(page_attrs);
1524 $new_page.children().each(function() {
1525 self.process($(this));
1528 var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
1529 $notebook.contents().appendTo($new_notebook);
1530 $notebook.before($new_notebook).remove();
1531 self.process($($new_notebook.children()[0]));
1532 //tabs and invisibility handling
1533 $new_notebook.tabs();
1534 _.each(pages, function(page, i) {
1537 page.__ic.on("change:effective_invisible", null, function() {
1538 if (!page.__ic.get('effective_invisible') && page.autofocus) {
1539 $new_notebook.tabs('select', i);
1542 var current = $new_notebook.tabs("option", "selected");
1543 if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
1545 var first_visible = _.find(_.range(pages.length), function(i2) {
1546 return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
1548 if (first_visible !== undefined) {
1549 $new_notebook.tabs('select', first_visible);
1554 this.handle_common_properties($new_notebook, $notebook);
1555 return $new_notebook;
1557 process_separator: function($separator) {
1558 var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
1559 $separator.before($new_separator).remove();
1560 this.handle_common_properties($new_separator, $separator);
1561 return $new_separator;
1563 process_label: function($label) {
1564 var name = $label.attr("for"),
1565 field_orm = this.fvg.fields[name];
1567 string: $label.attr('string') || (field_orm || {}).string || '',
1568 help: $label.attr('help') || (field_orm || {}).help || '',
1569 _for: name ? _.uniqueId('oe-field-input-') : undefined,
1571 var align = parseFloat(dict.align);
1572 if (isNaN(align) || align === 1) {
1574 } else if (align === 0) {
1580 var $new_label = this.render_element('FormRenderingLabel', dict);
1581 $label.before($new_label).remove();
1582 this.handle_common_properties($new_label, $label);
1584 this.labels[name] = $new_label;
1588 handle_common_properties: function($new_element, $node) {
1589 var str_modifiers = $node.attr("modifiers") || "{}";
1590 var modifiers = JSON.parse(str_modifiers);
1592 if (modifiers.invisible !== undefined)
1593 ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
1594 $new_element.addClass($node.attr("class") || "");
1595 $new_element.attr('style', $node.attr('style'));
1596 return {invisibility_changer: ic,};
1603 If you read this documentation, it probably means that you were asked to use a form view widget outside of
1604 a form view. Before going further, you must understand that those fields were never really created for
1605 that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
1606 you to hack the system with more style.
1608 instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
1609 init: function(parent, eval_context) {
1610 this._super(parent);
1611 this.field_descs = {};
1612 this.eval_context = eval_context || {};
1614 display_invalid_fields: false,
1615 actual_mode: 'create',
1618 get_field_desc: function(field_name) {
1619 if (this.field_descs[field_name] === undefined) {
1620 this.field_descs[field_name] = {
1624 return this.field_descs[field_name];
1626 extend_field_desc: function(fields) {
1628 _.each(fields, function(v, k) {
1629 _.extend(self.get_field_desc(k), v);
1632 get_field_value: function(field_name) {
1635 set_values: function(values) {
1638 compute_domain: function(expression) {
1639 return instance.web.form.compute_domain(expression, {});
1641 build_eval_context: function() {
1642 return new instance.web.CompoundContext(this.eval_context);
1646 instance.web.form.compute_domain = function(expr, fields) {
1647 if (! (expr instanceof Array))
1650 for (var i = expr.length - 1; i >= 0; i--) {
1652 if (ex.length == 1) {
1653 var top = stack.pop();
1656 stack.push(stack.pop() || top);
1659 stack.push(stack.pop() && top);
1665 throw new Error(_.str.sprintf(
1666 _t("Unknown operator %s in domain %s"),
1667 ex, JSON.stringify(expr)));
1671 var field = fields[ex[0]];
1673 throw new Error(_.str.sprintf(
1674 _t("Unknown field %s in domain %s"),
1675 ex[0], JSON.stringify(expr)));
1677 var field_value = field.get_value ? field.get_value() : field.value;
1681 switch (op.toLowerCase()) {
1684 stack.push(_.isEqual(field_value, val));
1688 stack.push(!_.isEqual(field_value, val));
1691 stack.push(field_value < val);
1694 stack.push(field_value > val);
1697 stack.push(field_value <= val);
1700 stack.push(field_value >= val);
1703 if (!_.isArray(val)) val = [val];
1704 stack.push(_(val).contains(field_value));
1707 if (!_.isArray(val)) val = [val];
1708 stack.push(!_(val).contains(field_value));
1712 _t("Unsupported operator %s in domain %s"),
1713 op, JSON.stringify(expr));
1716 return _.all(stack, _.identity);
1719 instance.web.form.is_bin_size = function(v) {
1720 return /^\d+(\.\d*)? \w+$/.test(v);
1724 * Must be applied over an class already possessing the PropertiesMixin.
1726 * Apply the result of the "invisible" domain to this.$el.
1728 instance.web.form.InvisibilityChangerMixin = {
1729 init: function(field_manager, invisible_domain) {
1731 this._ic_field_manager = field_manager;
1732 this._ic_invisible_modifier = invisible_domain;
1733 this._ic_field_manager.on("view_content_has_changed", this, function() {
1734 var result = self._ic_invisible_modifier === undefined ? false :
1735 self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
1736 self.set({"invisible": result});
1738 this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
1739 var check = function() {
1740 if (self.get("invisible") || self.get('force_invisible')) {
1741 self.set({"effective_invisible": true});
1743 self.set({"effective_invisible": false});
1746 this.on('change:invisible', this, check);
1747 this.on('change:force_invisible', this, check);
1751 this.on("change:effective_invisible", this, this._check_visibility);
1752 this._check_visibility();
1754 _check_visibility: function() {
1755 this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
1759 instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
1760 init: function(parent, field_manager, invisible_domain, $el) {
1761 this.setParent(parent);
1762 instance.web.PropertiesMixin.init.call(this);
1763 instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
1770 Base class for all fields, custom widgets and buttons to be displayed in the form view.
1773 - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
1774 the values of the "readonly" property and the "mode" property on the field manager.
1776 instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
1778 * @constructs instance.web.form.FormWidget
1779 * @extends instance.web.Widget
1781 * @param field_manager
1784 init: function(field_manager, node) {
1785 this._super(field_manager);
1786 this.field_manager = field_manager;
1787 if (this.field_manager instanceof instance.web.FormView)
1788 this.view = this.field_manager;
1790 this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
1791 instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
1793 this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
1799 // some events to make the property "effective_readonly" sync automatically with "readonly" and
1800 // "mode" on field_manager
1802 var test_effective_readonly = function() {
1803 self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
1805 this.on("change:readonly", this, test_effective_readonly);
1806 this.field_manager.on("change:actual_mode", this, test_effective_readonly);
1807 test_effective_readonly.call(this);
1809 renderElement: function() {
1810 this.process_modifiers();
1812 this.$el.addClass(this.node.attrs["class"] || "");
1814 destroy: function() {
1816 this._super.apply(this, arguments);
1819 * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
1821 * This method is an utility method that is meant to be called by child classes.
1823 * @param {jQuery} $e jQuery object of elements to bind focus/blur on
1825 setupFocus: function ($e) {
1828 focus: function () { self.trigger('focused'); },
1829 blur: function () { self.trigger('blurred'); }
1832 process_modifiers: function() {
1834 for (var a in this.modifiers) {
1835 if (!this.modifiers.hasOwnProperty(a)) { continue; }
1836 if (!_.include(["invisible"], a)) {
1837 var val = this.field_manager.compute_domain(this.modifiers[a]);
1843 do_attach_tooltip: function(widget, trigger, options) {
1844 widget = widget || this;
1845 trigger = trigger || this.$el;
1846 options = _.extend({
1851 var template = widget.template + '.tooltip';
1852 if (!QWeb.has_template(template)) {
1853 template = 'WidgetLabel.tooltip';
1855 return QWeb.render(template, {
1856 debug: instance.session.debug,
1859 gravity: $.fn.tipsy.autoBounds(50, 'nw'),
1864 $(trigger).tipsy(options);
1867 * Builds a new context usable for operations related to fields by merging
1868 * the fields'context with the action's context.
1870 build_context: function() {
1871 // only use the model's context if there is not context on the node
1872 var v_context = this.node.attrs.context;
1874 v_context = (this.field || {}).context || {};
1877 if (v_context.__ref || true) { //TODO: remove true
1878 var fields_values = this.field_manager.build_eval_context();
1879 v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
1883 build_domain: function() {
1884 var f_domain = this.field.domain || [];
1885 var n_domain = this.node.attrs.domain || null;
1886 // if there is a domain on the node, overrides the model's domain
1887 var final_domain = n_domain !== null ? n_domain : f_domain;
1888 if (!(final_domain instanceof Array) || true) { //TODO: remove true
1889 var fields_values = this.field_manager.build_eval_context();
1890 final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
1892 return final_domain;
1896 instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
1897 template: 'WidgetButton',
1898 init: function(field_manager, node) {
1899 node.attrs.type = node.attrs['data-button-type'];
1900 this._super(field_manager, node);
1901 this.force_disabled = false;
1902 this.string = (this.node.attrs.string || '').replace(/_/g, '');
1903 if (JSON.parse(this.node.attrs.default_focus || "0")) {
1904 // TODO fme: provide enter key binding to widgets
1905 this.view.default_focus_button = this;
1907 if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
1908 this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
1912 this._super.apply(this, arguments);
1913 this.view.on('view_content_has_changed', this, this.check_disable);
1914 this.check_disable();
1915 this.$el.click(this.on_click);
1916 if (this.node.attrs.help || instance.session.debug) {
1917 this.do_attach_tooltip();
1919 this.setupFocus(this.$el);
1921 on_click: function() {
1923 this.force_disabled = true;
1924 this.check_disable();
1925 this.execute_action().always(function() {
1926 self.force_disabled = false;
1927 self.check_disable();
1930 execute_action: function() {
1932 var exec_action = function() {
1933 if (self.node.attrs.confirm) {
1934 var def = $.Deferred();
1935 var dialog = instance.web.dialog($('<div/>').text(self.node.attrs.confirm), {
1936 title: _t('Confirm'),
1939 {text: _t("Cancel"), click: function() {
1940 $(this).dialog("close");
1943 {text: _t("Ok"), click: function() {
1945 self.on_confirmed().always(function() {
1946 $(self2).dialog("close");
1951 beforeClose: function() {
1955 return def.promise();
1957 return self.on_confirmed();
1960 if (!this.node.attrs.special) {
1961 return this.view.recursive_save().then(exec_action);
1963 return exec_action();
1966 on_confirmed: function() {
1969 var context = this.build_context();
1971 return this.view.do_execute_action(
1972 _.extend({}, this.node.attrs, {context: context}),
1973 this.view.dataset, this.view.datarecord.id, function () {
1974 self.view.recursive_reload();
1977 check_disable: function() {
1978 var disabled = (this.force_disabled || !this.view.is_interactible_record());
1979 this.$el.prop('disabled', disabled);
1980 this.$el.css('color', disabled ? 'grey' : '');
1985 * Interface to be implemented by fields.
1988 * - changed_value: triggered when the value of the field has changed. This can be due
1989 * to a user interaction or a call to set_value().
1992 instance.web.form.FieldInterface = {
1994 * Constructor takes 2 arguments:
1995 * - field_manager: Implements FieldManagerMixin
1996 * - node: the "<field>" node in json form
1998 init: function(field_manager, node) {},
2000 * Called by the form view to indicate the value of the field.
2002 * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
2003 * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
2004 * before the widget is inserted into the DOM.
2006 * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
2007 * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
2008 * also be able to handle any other format commonly used in the _defaults key on the models in the addons
2009 * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
2010 * no information is ever given to know which format is used.
2012 set_value: function(value_) {},
2014 * Get the current value of the widget.
2016 * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
2017 * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
2018 * For example if the field is marked as "required", a call to get_value() can return false.
2020 * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
2021 * return a default value according to the type of field.
2023 * This method is always assumed to perform synchronously, it can not return a promise.
2025 * If there was no user interaction to modify the value of the field, it is always assumed that
2026 * get_value() return the same semantic value than the one passed in the last call to set_value(),
2027 * although the syntax can be different. This can be the case for type of fields that have a different
2028 * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
2030 get_value: function() {},
2032 * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
2035 set_input_id: function(id) {},
2037 * Returns true if is_syntax_valid() returns true and the value is semantically
2038 * valid too according to the semantic restrictions applied to the field.
2040 is_valid: function() {},
2042 * Returns true if the field holds a value which is syntactically correct, ignoring
2043 * the potential semantic restrictions applied to the field.
2045 is_syntax_valid: function() {},
2047 * Must set the focus on the field. Return false if field is not focusable.
2049 focus: function() {},
2051 * Called when the translate button is clicked.
2053 on_translate: function() {},
2055 This method is called by the form view before reading on_change values and before saving. It tells
2056 the field to save its value before reading it using get_value(). Must return a promise.
2058 commit_value: function() {},
2062 * Abstract class for classes implementing FieldInterface.
2065 * - value: useful property to hold the value of the field. By default, set_value() and get_value()
2066 * set and retrieve the value property. Changing the value property also triggers automatically
2067 * a 'changed_value' event that inform the view to trigger on_changes.
2070 instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
2072 * @constructs instance.web.form.AbstractField
2073 * @extends instance.web.form.FormWidget
2075 * @param field_manager
2078 init: function(field_manager, node) {
2080 this._super(field_manager, node);
2081 this.name = this.node.attrs.name;
2082 this.field = this.field_manager.get_field_desc(this.name);
2083 this.widget = this.node.attrs.widget;
2084 this.string = this.node.attrs.string || this.field.string || this.name;
2085 this.options = instance.web.py_eval(this.node.attrs.options || '{}');
2086 this.set({'value': false});
2088 this.on("change:value", this, function() {
2089 this.trigger('changed_value');
2090 this._check_css_flags();
2093 renderElement: function() {
2096 if (this.field.translate && this.view) {
2097 this.$el.addClass('oe_form_field_translatable');
2098 this.$el.find('.oe_field_translate').click(this.on_translate);
2100 this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
2101 if (instance.session.debug) {
2102 this.do_attach_tooltip(this, this.$label[0] || this.$el);
2103 this.$label.off('dblclick').on('dblclick', function() {
2104 console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
2106 console.log("window.w =", window.w);
2109 if (!this.disable_utility_classes) {
2110 this.off("change:required", this, this._set_required);
2111 this.on("change:required", this, this._set_required);
2112 this._set_required();
2114 this._check_visibility();
2115 this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
2116 this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
2117 this._check_css_flags();
2120 var tmp = this._super();
2121 this.on("change:value", this, function() {
2122 if (! this.no_rerender)
2123 this.render_value();
2125 this.render_value();
2128 * Private. Do not use.
2130 _set_required: function() {
2131 this.$el.toggleClass('oe_form_required', this.get("required"));
2133 set_value: function(value_) {
2134 this.set({'value': value_});
2136 get_value: function() {
2137 return this.get('value');
2140 Utility method that all implementations should use to change the
2141 value without triggering a re-rendering.
2143 internal_set_value: function(value_) {
2144 var tmp = this.no_rerender;
2145 this.no_rerender = true;
2146 this.set({'value': value_});
2147 this.no_rerender = tmp;
2150 This method is called each time the value is modified.
2152 render_value: function() {},
2153 is_valid: function() {
2154 return this.is_syntax_valid() && !(this.get('required') && this.is_false());
2156 is_syntax_valid: function() {
2160 * Method useful to implement to ease validity testing. Must return true if the current
2161 * value is similar to false in OpenERP.
2163 is_false: function() {
2164 return this.get('value') === false;
2166 _check_css_flags: function() {
2167 if (this.field.translate) {
2168 this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
2170 if (!this.disable_utility_classes) {
2171 if (this.field_manager.get('display_invalid_fields')) {
2172 this.$el.toggleClass('oe_form_invalid', !this.is_valid());
2179 set_input_id: function(id) {
2180 this.id_for_label = id;
2182 on_translate: function() {
2184 var trans = new instance.web.DataSet(this, 'ir.translation');
2185 return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
2190 set_dimensions: function (height, width) {
2196 commit_value: function() {
2202 * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
2205 instance.web.form.ReinitializeWidgetMixin = {
2207 * Default implementation of, you should not override it, use initialize_field() instead.
2210 this.initialize_field();
2213 initialize_field: function() {
2214 this.on("change:effective_readonly", this, this.reinitialize);
2215 this.initialize_content();
2217 reinitialize: function() {
2218 this.destroy_content();
2219 this.renderElement();
2220 this.initialize_content();
2223 * Called to destroy anything that could have been created previously, called before a
2224 * re-initialization.
2226 destroy_content: function() {},
2228 * Called to initialize the content.
2230 initialize_content: function() {},
2234 * A mixin to apply on any field that has to completely re-render when its readonly state
2237 instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
2238 reinitialize: function() {
2239 instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
2240 this.render_value();
2245 Some hack to make placeholders work in ie9.
2247 if ($.browser.msie && $.browser.version === "9.0") {
2248 document.addEventListener("DOMNodeInserted",function(event){
2249 var nodename = event.target.nodeName.toLowerCase();
2250 if ( nodename === "input" || nodename == "textarea" ) {
2251 $(event.target).placeholder();
2256 instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2257 template: 'FieldChar',
2258 widget_class: 'oe_form_field_char',
2260 'change input': 'store_dom_value',
2262 init: function (field_manager, node) {
2263 this._super(field_manager, node);
2264 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2266 initialize_content: function() {
2267 this.setupFocus(this.$('input'));
2269 store_dom_value: function () {
2270 if (!this.get('effective_readonly')
2271 && this.$('input').length
2272 && this.is_syntax_valid()) {
2273 this.internal_set_value(
2275 this.$('input').val()));
2278 commit_value: function () {
2279 this.store_dom_value();
2280 return this._super();
2282 render_value: function() {
2283 var show_value = this.format_value(this.get('value'), '');
2284 if (!this.get("effective_readonly")) {
2285 this.$el.find('input').val(show_value);
2287 if (this.password) {
2288 show_value = new Array(show_value.length + 1).join('*');
2290 this.$(".oe_form_char_content").text(show_value);
2293 is_syntax_valid: function() {
2294 if (!this.get("effective_readonly") && this.$("input").size() > 0) {
2296 this.parse_value(this.$('input').val(), '');
2304 parse_value: function(val, def) {
2305 return instance.web.parse_value(val, this, def);
2307 format_value: function(val, def) {
2308 return instance.web.format_value(val, this, def);
2310 is_false: function() {
2311 return this.get('value') === '' || this._super();
2314 var input = this.$('input:first')[0];
2315 return input ? input.focus() : false;
2317 set_dimensions: function (height, width) {
2318 this._super(height, width);
2319 this.$('input').css({
2326 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2327 process_modifiers: function () {
2329 this.set({ readonly: true });
2333 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2334 template: 'FieldEmail',
2335 initialize_content: function() {
2337 var $button = this.$el.find('button');
2338 $button.click(this.on_button_clicked);
2339 this.setupFocus($button);
2341 render_value: function() {
2342 if (!this.get("effective_readonly")) {
2346 .attr('href', 'mailto:' + this.get('value'))
2347 .text(this.get('value') || '');
2350 on_button_clicked: function() {
2351 if (!this.get('value') || !this.is_syntax_valid()) {
2352 this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2354 location.href = 'mailto:' + this.get('value');
2359 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2360 template: 'FieldUrl',
2361 initialize_content: function() {
2363 var $button = this.$el.find('button');
2364 $button.click(this.on_button_clicked);
2365 this.setupFocus($button);
2367 render_value: function() {
2368 if (!this.get("effective_readonly")) {
2371 var tmp = this.get('value');
2372 var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2374 tmp = "http://" + this.get('value');
2376 var text = this.get('value') ? this.node.attrs.text || tmp : '';
2377 this.$el.find('a').attr('href', tmp).text(text);
2380 on_button_clicked: function() {
2381 if (!this.get('value')) {
2382 this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2384 var url = $.trim(this.get('value'));
2385 if(/^www\./i.test(url))
2386 url = 'http://'+url;
2392 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2393 is_field_number: true,
2394 widget_class: 'oe_form_field_float',
2395 init: function (field_manager, node) {
2396 this._super(field_manager, node);
2397 this.internal_set_value(0);
2398 if (this.node.attrs.digits) {
2399 this.digits = this.node.attrs.digits;
2401 this.digits = this.field.digits;
2404 set_value: function(value_) {
2405 if (value_ === false || value_ === undefined) {
2406 // As in GTK client, floats default to 0
2409 this._super.apply(this, [value_]);
2411 focus: function () {
2412 var $input = this.$('input:first');
2413 return $input.length ? $input.select() : false;
2417 instance.web.DateTimeWidget = instance.web.Widget.extend({
2418 template: "web.datepicker",
2419 jqueryui_object: 'datetimepicker',
2420 type_of_date: "datetime",
2422 'change .oe_datepicker_master': 'change_datetime',
2424 init: function(parent) {
2425 this._super(parent);
2426 this.name = parent.name;
2430 this.$input = this.$el.find('input.oe_datepicker_master');
2431 this.$input_picker = this.$el.find('input.oe_datepicker_container');
2433 $.datepicker.setDefaults({
2434 clearText: _t('Clear'),
2435 clearStatus: _t('Erase the current date'),
2436 closeText: _t('Done'),
2437 closeStatus: _t('Close without change'),
2438 prevText: _t('<Prev'),
2439 prevStatus: _t('Show the previous month'),
2440 nextText: _t('Next>'),
2441 nextStatus: _t('Show the next month'),
2442 currentText: _t('Today'),
2443 currentStatus: _t('Show the current month'),
2444 monthNames: Date.CultureInfo.monthNames,
2445 monthNamesShort: Date.CultureInfo.abbreviatedMonthNames,
2446 monthStatus: _t('Show a different month'),
2447 yearStatus: _t('Show a different year'),
2448 weekHeader: _t('Wk'),
2449 weekStatus: _t('Week of the year'),
2450 dayNames: Date.CultureInfo.dayNames,
2451 dayNamesShort: Date.CultureInfo.abbreviatedDayNames,
2452 dayNamesMin: Date.CultureInfo.shortestDayNames,
2453 dayStatus: _t('Set DD as first week day'),
2454 dateStatus: _t('Select D, M d'),
2455 firstDay: Date.CultureInfo.firstDayOfWeek,
2456 initStatus: _t('Select a date'),
2459 $.timepicker.setDefaults({
2460 timeOnlyTitle: _t('Choose Time'),
2461 timeText: _t('Time'),
2462 hourText: _t('Hour'),
2463 minuteText: _t('Minute'),
2464 secondText: _t('Second'),
2465 currentText: _t('Now'),
2466 closeText: _t('Done')
2470 onClose: this.on_picker_select,
2471 onSelect: this.on_picker_select,
2475 showButtonPanel: true,
2476 firstDay: Date.CultureInfo.firstDayOfWeek
2478 // Some clicks in the datepicker dialog are not stopped by the
2479 // datepicker and "bubble through", unexpectedly triggering the bus's
2480 // click event. Prevent that.
2481 this.picker('widget').click(function (e) { e.stopPropagation(); });
2483 this.$el.find('img.oe_datepicker_trigger').click(function() {
2484 if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
2485 self.$input.focus();
2488 self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date());
2489 self.$input_picker.show();
2490 self.picker('show');
2491 self.$input_picker.hide();
2493 this.set_readonly(false);
2494 this.set({'value': false});
2496 picker: function() {
2497 return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
2499 on_picker_select: function(text, instance_) {
2500 var date = this.picker('getDate');
2502 .val(date ? this.format_client(date) : '')
2506 set_value: function(value_) {
2507 this.set({'value': value_});
2508 this.$input.val(value_ ? this.format_client(value_) : '');
2510 get_value: function() {
2511 return this.get('value');
2513 set_value_from_ui_: function() {
2514 var value_ = this.$input.val() || false;
2515 this.set({'value': this.parse_client(value_)});
2517 set_readonly: function(readonly) {
2518 this.readonly = readonly;
2519 this.$input.prop('readonly', this.readonly);
2520 this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
2522 is_valid_: function() {
2523 var value_ = this.$input.val();
2524 if (value_ === "") {
2528 this.parse_client(value_);
2535 parse_client: function(v) {
2536 return instance.web.parse_value(v, {"widget": this.type_of_date});
2538 format_client: function(v) {
2539 return instance.web.format_value(v, {"widget": this.type_of_date});
2541 change_datetime: function() {
2542 if (this.is_valid_()) {
2543 this.set_value_from_ui_();
2544 this.trigger("datetime_changed");
2547 commit_value: function () {
2548 this.change_datetime();
2552 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2553 jqueryui_object: 'datepicker',
2554 type_of_date: "date"
2557 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2558 template: "FieldDatetime",
2559 build_widget: function() {
2560 return new instance.web.DateTimeWidget(this);
2562 destroy_content: function() {
2563 if (this.datewidget) {
2564 this.datewidget.destroy();
2565 this.datewidget = undefined;
2568 initialize_content: function() {
2569 if (!this.get("effective_readonly")) {
2570 this.datewidget = this.build_widget();
2571 this.datewidget.on('datetime_changed', this, _.bind(function() {
2572 this.internal_set_value(this.datewidget.get_value());
2574 this.datewidget.appendTo(this.$el);
2575 this.setupFocus(this.datewidget.$input);
2578 render_value: function() {
2579 if (!this.get("effective_readonly")) {
2580 this.datewidget.set_value(this.get('value'));
2582 this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2585 is_syntax_valid: function() {
2586 if (!this.get("effective_readonly") && this.datewidget) {
2587 return this.datewidget.is_valid_();
2591 is_false: function() {
2592 return this.get('value') === '' || this._super();
2595 var input = this.datewidget && this.datewidget.$input[0];
2596 return input ? input.focus() : false;
2598 set_dimensions: function (height, width) {
2599 this._super(height, width);
2600 this.datewidget.$input.css('height', height);
2604 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2605 template: "FieldDate",
2606 build_widget: function() {
2607 return new instance.web.DateWidget(this);
2611 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2612 template: 'FieldText',
2614 'keyup': function (e) {
2615 if (e.which === $.ui.keyCode.ENTER) {
2616 e.stopPropagation();
2619 'change textarea': 'store_dom_value',
2621 initialize_content: function() {
2623 if (! this.get("effective_readonly")) {
2624 this.$textarea = this.$el.find('textarea');
2625 this.auto_sized = false;
2626 this.default_height = this.$textarea.css('height');
2627 if (this.get("effective_readonly")) {
2628 this.$textarea.attr('disabled', 'disabled');
2630 this.setupFocus(this.$textarea);
2632 this.$textarea = undefined;
2635 commit_value: function () {
2636 if (! this.get("effective_readonly") && this.$textarea) {
2637 this.store_dom_value();
2639 return this._super();
2641 store_dom_value: function () {
2642 this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2644 render_value: function() {
2645 if (! this.get("effective_readonly")) {
2646 var show_value = instance.web.format_value(this.get('value'), this, '');
2647 if (show_value === '') {
2648 this.$textarea.css('height', parseInt(this.default_height)+"px");
2650 this.$textarea.val(show_value);
2651 if (! this.auto_sized) {
2652 this.auto_sized = true;
2653 this.$textarea.autosize();
2655 this.$textarea.trigger("autosize");
2658 var txt = this.get("value") || '';
2659 this.$(".oe_form_text_content").text(txt);
2662 is_syntax_valid: function() {
2663 if (!this.get("effective_readonly") && this.$textarea) {
2665 instance.web.parse_value(this.$textarea.val(), this, '');
2673 is_false: function() {
2674 return this.get('value') === '' || this._super();
2676 focus: function($el) {
2677 var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2678 return input ? input.focus() : false;
2680 set_dimensions: function (height, width) {
2681 this._super(height, width);
2682 if (!this.get("effective_readonly") && this.$textarea) {
2683 this.$textarea.css({
2692 * FieldTextHtml Widget
2693 * Intended for FieldText widgets meant to display HTML content. This
2694 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2695 * To find more information about CLEditor configutation: go to
2696 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2698 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2699 template: 'FieldTextHtml',
2701 this._super.apply(this, arguments);
2703 initialize_content: function() {
2705 if (! this.get("effective_readonly")) {
2706 self._updating_editor = false;
2707 this.$textarea = this.$el.find('textarea');
2708 var width = ((this.node.attrs || {}).editor_width || '100%');
2709 var height = ((this.node.attrs || {}).editor_height || 250);
2710 this.$textarea.cleditor({
2711 width: width, // width not including margins, borders or padding
2712 height: height, // height not including margins, borders or padding
2713 controls: // controls to add to the toolbar
2714 "bold italic underline strikethrough " +
2715 "| removeformat | bullets numbering | outdent " +
2716 "indent | link unlink | source",
2717 bodyStyle: // style to assign to document body contained within the editor
2718 "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2720 this.$cleditor = this.$textarea.cleditor()[0];
2721 this.$cleditor.change(function() {
2722 if (! self._updating_editor) {
2723 self.$cleditor.updateTextArea();
2724 self.internal_set_value(self.$textarea.val());
2727 if (this.field.translate) {
2728 var $img = $('<img class="oe_field_translate oe_input_icon" src="/web/static/src/img/icons/terp-translate.png" width="16" height="16" border="0"/>')
2729 .click(this.on_translate);
2730 this.$cleditor.$toolbar.append($img);
2734 render_value: function() {
2735 if (! this.get("effective_readonly")) {
2736 this.$textarea.val(this.get('value') || '');
2737 this._updating_editor = true;
2738 this.$cleditor.updateFrame();
2739 this._updating_editor = false;
2741 this.$el.html(this.get('value'));
2746 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2747 template: 'FieldBoolean',
2750 this.$checkbox = $("input", this.$el);
2751 this.setupFocus(this.$checkbox);
2752 this.$el.click(_.bind(function() {
2753 this.internal_set_value(this.$checkbox.is(':checked'));
2755 var check_readonly = function() {
2756 self.$checkbox.prop('disabled', self.get("effective_readonly"));
2758 this.on("change:effective_readonly", this, check_readonly);
2759 check_readonly.call(this);
2760 this._super.apply(this, arguments);
2762 render_value: function() {
2763 this.$checkbox[0].checked = this.get('value');
2766 var input = this.$checkbox && this.$checkbox[0];
2767 return input ? input.focus() : false;
2772 The progressbar field expect a float from 0 to 100.
2774 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2775 template: 'FieldProgressBar',
2776 render_value: function() {
2777 this.$el.progressbar({
2778 value: this.get('value') || 0,
2779 disabled: this.get("effective_readonly")
2781 var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
2782 this.$('span').html(formatted_value + '%');
2787 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2788 template: 'FieldSelection',
2790 'change select': 'store_dom_value',
2792 init: function(field_manager, node) {
2794 this._super(field_manager, node);
2795 this.values = _(this.field.selection).chain()
2796 .reject(function (v) { return v[0] === false && v[1] === ''; })
2797 .unshift([false, ''])
2800 initialize_content: function() {
2801 // Flag indicating whether we're in an event chain containing a change
2802 // event on the select, in order to know what to do on keyup[RETURN]:
2803 // * If the user presses [RETURN] as part of changing the value of a
2804 // selection, we should just let the value change and not let the
2805 // event broadcast further (e.g. to validating the current state of
2806 // the form in editable list view, which would lead to saving the
2807 // current row or switching to the next one)
2808 // * If the user presses [RETURN] with a select closed (side-effect:
2809 // also if the user opened the select and pressed [RETURN] without
2810 // changing the selected value), takes the action as validating the
2812 var ischanging = false;
2813 var $select = this.$el.find('select')
2814 .change(function () { ischanging = true; })
2815 .click(function () { ischanging = false; })
2816 .keyup(function (e) {
2817 if (e.which !== 13 || !ischanging) { return; }
2818 e.stopPropagation();
2821 this.setupFocus($select);
2823 commit_value: function () {
2824 this.store_dom_value();
2825 return this._super();
2827 store_dom_value: function () {
2828 if (!this.get('effective_readonly') && this.$('select').length) {
2829 this.internal_set_value(
2830 this.values[this.$('select')[0].selectedIndex][0]);
2833 set_value: function(value_) {
2834 value_ = value_ === null ? false : value_;
2835 value_ = value_ instanceof Array ? value_[0] : value_;
2836 this._super(value_);
2838 render_value: function() {
2839 if (!this.get("effective_readonly")) {
2841 for (var i = 0, ii = this.values.length; i < ii; i++) {
2842 if (this.values[i][0] === this.get('value')) index = i;
2844 this.$el.find('select')[0].selectedIndex = index;
2847 var option = _(this.values)
2848 .detect(function (record) { return record[0] === self.get('value'); });
2849 this.$el.text(option ? option[1] : this.values[0][1]);
2853 var input = this.$('select:first')[0];
2854 return input ? input.focus() : false;
2856 set_dimensions: function (height, width) {
2857 this._super(height, width);
2858 this.$('select').css({
2865 // jquery autocomplete tweak to allow html and classnames
2867 var proto = $.ui.autocomplete.prototype,
2868 initSource = proto._initSource;
2870 function filter( array, term ) {
2871 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
2872 return $.grep( array, function(value_) {
2873 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
2878 _initSource: function() {
2879 if ( this.options.html && $.isArray(this.options.source) ) {
2880 this.source = function( request, response ) {
2881 response( filter( this.options.source, request.term ) );
2884 initSource.call( this );
2888 _renderItem: function( ul, item) {
2889 return $( "<li></li>" )
2890 .data( "item.autocomplete", item )
2891 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
2893 .addClass(item.classname);
2899 * A mixin containing some useful methods to handle completion inputs.
2901 instance.web.form.CompletionFieldMixin = {
2904 this.orderer = new instance.web.DropMisordered();
2907 * Call this method to search using a string.
2909 get_search_result: function(search_val) {
2912 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
2913 var blacklist = this.get_search_blacklist();
2914 this.last_query = search_val;
2916 return this.orderer.add(dataset.name_search(
2917 search_val, new instance.web.CompoundDomain(self.build_domain(), [["id", "not in", blacklist]]),
2918 'ilike', this.limit + 1, self.build_context())).then(function(data) {
2919 self.last_search = data;
2920 // possible selections for the m2o
2921 var values = _.map(data, function(x) {
2922 x[1] = x[1].split("\n")[0];
2924 label: _.str.escapeHTML(x[1]),
2931 // search more... if more results that max
2932 if (values.length > self.limit) {
2933 values = values.slice(0, self.limit);
2935 label: _t("Search More..."),
2936 action: function() {
2937 dataset.name_search(search_val, self.build_domain(), 'ilike', false).done(function(data) {
2938 self._search_create_popup("search", data);
2941 classname: 'oe_m2o_dropdown_option'
2945 var raw_result = _(data.result).map(function(x) {return x[1];});
2946 if (search_val.length > 0 && !_.include(raw_result, search_val)) {
2948 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
2949 $('<span />').text(search_val).html()),
2950 action: function() {
2951 self._quick_create(search_val);
2953 classname: 'oe_m2o_dropdown_option'
2958 label: _t("Create and Edit..."),
2959 action: function() {
2960 self._search_create_popup("form", undefined, self._create_context(search_val));
2962 classname: 'oe_m2o_dropdown_option'
2968 get_search_blacklist: function() {
2971 _quick_create: function(name) {
2973 var slow_create = function () {
2974 self._search_create_popup("form", undefined, self._create_context(name));
2976 if (self.options.quick_create === undefined || self.options.quick_create) {
2977 new instance.web.DataSet(this, this.field.relation, self.build_context())
2978 .name_create(name).done(function(data) {
2979 self.add_id(data[0]);
2980 }).fail(function(error, event) {
2981 event.preventDefault();
2987 // all search/create popup handling
2988 _search_create_popup: function(view, ids, context) {
2990 var pop = new instance.web.form.SelectCreatePopup(this);
2992 self.field.relation,
2994 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
2995 initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
2997 disable_multiple_selection: true
2999 self.build_domain(),
3000 new instance.web.CompoundContext(self.build_context(), context || {})
3002 pop.on("elements_selected", self, function(element_ids) {
3003 self.add_id(element_ids[0]);
3010 add_id: function(id) {},
3011 _create_context: function(name) {
3013 var field = (this.options || {}).create_name_field;
3014 if (field === undefined)
3016 if (field !== false && name && (this.options || {}).quick_create !== false)
3017 tmp["default_" + field] = name;
3022 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3023 template: "M2ODialog",
3024 init: function(parent) {
3025 this._super(parent, {
3026 title: _.str.sprintf(_t("Add %s"), parent.string),
3032 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3033 this.$("input").val(this.getParent().last_query);
3034 this.$buttons.find(".oe_form_m2o_qc_button").click(function(){
3035 self.getParent()._quick_create(self.$("input").val());
3038 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3039 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3042 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3048 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3049 template: "FieldMany2One",
3051 'keydown input': function (e) {
3053 case $.ui.keyCode.UP:
3054 case $.ui.keyCode.DOWN:
3055 e.stopPropagation();
3059 init: function(field_manager, node) {
3060 this._super(field_manager, node);
3061 instance.web.form.CompletionFieldMixin.init.call(this);
3062 this.set({'value': false});
3063 this.display_value = {};
3064 this.last_search = [];
3065 this.floating = false;
3066 this.current_display = null;
3067 this.is_started = false;
3069 reinit_value: function(val) {
3070 this.internal_set_value(val);
3071 this.floating = false;
3072 if (this.is_started)
3073 this.render_value();
3075 initialize_field: function() {
3076 this.is_started = true;
3077 instance.web.bus.on('click', this, function() {
3078 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3079 this.$input.autocomplete("close");
3082 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3084 initialize_content: function() {
3085 if (!this.get("effective_readonly"))
3086 this.render_editable();
3088 destroy_content: function () {
3089 if (this.$drop_down) {
3090 this.$drop_down.off('click');
3091 delete this.$drop_down;
3094 this.$input.closest(".ui-dialog .ui-dialog-content").off('scroll');
3095 this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3096 'focus focusout change keydown');
3099 if (this.$follow_button) {
3100 this.$follow_button.off('blur focus click');
3101 delete this.$follow_button;
3104 destroy: function () {
3105 this.destroy_content();
3106 return this._super();
3108 init_error_displayer: function() {
3111 hide_error_displayer: function() {
3114 show_error_displayer: function() {
3115 new instance.web.form.M2ODialog(this).open();
3117 render_editable: function() {
3119 this.$input = this.$el.find("input");
3121 this.init_error_displayer();
3123 self.$input.on('focus', function() {
3124 self.hide_error_displayer();
3127 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3128 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3130 this.$follow_button.click(function(ev) {
3131 ev.preventDefault();
3132 if (!self.get('value')) {
3136 var pop = new instance.web.form.FormOpenPopup(self);
3138 self.field.relation,
3140 self.build_context(),
3142 title: _t("Open: ") + self.string
3145 pop.on('write_completed', self, function(){
3146 self.display_value = {};
3147 self.render_value();
3149 self.view.do_onchange(self);
3153 // some behavior for input
3154 var input_changed = function() {
3155 if (self.current_display !== self.$input.val()) {
3156 self.current_display = self.$input.val();
3157 if (self.$input.val() === "") {
3158 self.internal_set_value(false);
3159 self.floating = false;
3161 self.floating = true;
3165 this.$input.keydown(input_changed);
3166 this.$input.change(input_changed);
3167 this.$drop_down.click(function() {
3168 if (self.$input.autocomplete("widget").is(":visible")) {
3169 self.$input.autocomplete("close");
3170 self.$input.focus();
3172 if (self.get("value") && ! self.floating) {
3173 self.$input.autocomplete("search", "");
3175 self.$input.autocomplete("search");
3180 // Autocomplete close on dialog content scroll
3181 var close_autocomplete = _.debounce(function() {
3182 if (self.$input.autocomplete("widget").is(":visible")) {
3183 self.$input.autocomplete("close");
3186 this.$input.closest(".ui-dialog .ui-dialog-content").on('scroll', this, close_autocomplete);
3188 self.ed_def = $.Deferred();
3189 self.uned_def = $.Deferred();
3191 var ed_duration = 15000;
3192 var anyoneLoosesFocus = function (e) {
3194 if (self.floating) {
3195 if (self.last_search.length > 0) {
3196 if (self.last_search[0][0] != self.get("value")) {
3197 self.display_value = {};
3198 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3199 self.reinit_value(self.last_search[0][0]);
3202 self.render_value();
3206 self.reinit_value(false);
3208 self.floating = false;
3210 if (used && self.get("value") === false && ! self.no_ed) {
3211 self.ed_def.reject();
3212 self.uned_def.reject();
3213 self.ed_def = $.Deferred();
3214 self.ed_def.done(function() {
3215 self.show_error_displayer();
3216 ignore_blur = false;
3217 self.trigger('focused');
3220 setTimeout(function() {
3221 self.ed_def.resolve();
3222 self.uned_def.reject();
3223 self.uned_def = $.Deferred();
3224 self.uned_def.done(function() {
3225 self.hide_error_displayer();
3227 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3231 self.ed_def.reject();
3234 var ignore_blur = false;
3236 focusout: anyoneLoosesFocus,
3237 focus: function () { self.trigger('focused'); },
3238 autocompleteopen: function () { ignore_blur = true; },
3239 autocompleteclose: function () { ignore_blur = false; },
3241 // autocomplete open
3242 if (ignore_blur) { return; }
3243 if (_(self.getChildren()).any(function (child) {
3244 return child instanceof instance.web.form.AbstractFormPopup;
3246 self.trigger('blurred');
3250 var isSelecting = false;
3252 this.$input.autocomplete({
3253 source: function(req, resp) {
3254 self.get_search_result(req.term).done(function(result) {
3258 select: function(event, ui) {
3262 self.display_value = {};
3263 self.display_value["" + item.id] = item.name;
3264 self.reinit_value(item.id);
3265 } else if (item.action) {
3267 // Cancel widget blurring, to avoid form blur event
3268 self.trigger('focused');
3272 focus: function(e, ui) {
3276 // disabled to solve a bug, but may cause others
3277 //close: anyoneLoosesFocus,
3281 this.$input.autocomplete("widget").openerpClass();
3282 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3283 this.$input.keyup(function(e) {
3284 if (e.which === 13) { // ENTER
3286 e.stopPropagation();
3288 isSelecting = false;
3290 this.setupFocus(this.$follow_button);
3292 render_value: function(no_recurse) {
3294 if (! this.get("value")) {
3295 this.display_string("");
3298 var display = this.display_value["" + this.get("value")];
3300 this.display_string(display);
3304 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3305 this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3306 self.display_value["" + self.get("value")] = data[0][1];
3307 self.render_value(true);
3311 display_string: function(str) {
3313 if (!this.get("effective_readonly")) {
3314 this.$input.val(str.split("\n")[0]);
3315 this.current_display = this.$input.val();
3316 if (this.is_false()) {
3317 this.$('.oe_m2o_cm_button').css({'display':'none'});
3319 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3322 var lines = _.escape(str).split("\n");
3326 follow = _.rest(lines).join("<br />");
3329 var $link = this.$el.find('.oe_form_uri')
3332 if (! this.options.no_open)
3333 $link.click(function () {
3335 type: 'ir.actions.act_window',
3336 res_model: self.field.relation,
3337 res_id: self.get("value"),
3338 views: [[false, 'form']],
3340 context: self.build_context().eval(),
3344 $(".oe_form_m2o_follow", this.$el).html(follow);
3347 set_value: function(value_) {
3349 if (value_ instanceof Array) {
3350 this.display_value = {};
3351 if (! this.options.always_reload) {
3352 this.display_value["" + value_[0]] = value_[1];
3356 value_ = value_ || false;
3357 this.reinit_value(value_);
3359 get_displayed: function() {
3360 return this.display_value["" + this.get("value")];
3362 add_id: function(id) {
3363 this.display_value = {};
3364 this.reinit_value(id);
3366 is_false: function() {
3367 return ! this.get("value");
3369 focus: function () {
3370 var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3371 return input ? input.focus() : false;
3373 _quick_create: function() {
3375 this.ed_def.reject();
3376 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3378 _search_create_popup: function() {
3380 this.ed_def.reject();
3381 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3383 set_dimensions: function (height, width) {
3384 this._super(height, width);
3385 this.$input.css('height', height);
3389 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3390 template: 'Many2OneButton',
3391 init: function(field_manager, node) {
3392 this._super.apply(this, arguments);
3395 this._super.apply(this, arguments);
3398 set_button: function() {
3401 this.$button.remove();
3404 this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3405 this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3406 this.$button.addClass('oe_link').css({'padding':'4px'});
3407 this.$el.append(this.$button);
3408 this.$button.on('click', self.on_click);
3410 on_click: function(ev) {
3412 this.popup = new instance.web.form.FormOpenPopup(this);
3413 this.popup.show_element(
3414 this.field.relation,
3416 this.build_context(),
3417 {title: this.string}
3419 this.popup.on('create_completed', self, function(r) {
3423 set_value: function(value_) {
3425 if (value_ instanceof Array) {
3428 value_ = value_ || false;
3429 this.set('value', value_);
3435 # Values: (0, 0, { fields }) create
3436 # (1, ID, { fields }) update
3437 # (2, ID) remove (delete)
3438 # (3, ID) unlink one (target id or target of relation)
3440 # (5) unlink all (only valid for one2many)
3445 'create': function (values) {
3446 return [commands.CREATE, false, values];
3448 // (1, id, {values})
3450 'update': function (id, values) {
3451 return [commands.UPDATE, id, values];
3455 'delete': function (id) {
3456 return [commands.DELETE, id, false];
3458 // (3, id[, _]) removes relation, but not linked record itself
3460 'forget': function (id) {
3461 return [commands.FORGET, id, false];
3465 'link_to': function (id) {
3466 return [commands.LINK_TO, id, false];
3470 'delete_all': function () {
3471 return [5, false, false];
3473 // (6, _, ids) replaces all linked records with provided ids
3475 'replace_with': function (ids) {
3476 return [6, false, ids];
3479 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3480 multi_selection: false,
3481 disable_utility_classes: true,
3482 init: function(field_manager, node) {
3483 this._super(field_manager, node);
3484 lazy_build_o2m_kanban_view();
3485 this.is_loaded = $.Deferred();
3486 this.initial_is_loaded = this.is_loaded;
3487 this.form_last_update = $.Deferred();
3488 this.init_form_last_update = this.form_last_update;
3489 this.is_started = false;
3490 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
3491 this.dataset.o2m = this;
3492 this.dataset.parent_view = this.view;
3493 this.dataset.child_name = this.name;
3495 this.dataset.on('dataset_changed', this, function() {
3496 self.trigger_on_change();
3501 this._super.apply(this, arguments);
3502 this.$el.addClass('oe_form_field oe_form_field_one2many');
3507 this.is_loaded.done(function() {
3508 self.on("change:effective_readonly", self, function() {
3509 self.is_loaded = self.is_loaded.then(function() {
3510 self.viewmanager.destroy();
3511 return $.when(self.load_views()).done(function() {
3512 self.reload_current_view();
3517 this.is_started = true;
3518 this.reload_current_view();
3520 trigger_on_change: function() {
3521 this.trigger('changed_value');
3523 load_views: function() {
3526 var modes = this.node.attrs.mode;
3527 modes = !!modes ? modes.split(",") : ["tree"];
3529 _.each(modes, function(mode) {
3530 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
3531 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
3535 view_type: mode == "tree" ? "list" : mode,
3538 if (self.field.views && self.field.views[mode]) {
3539 view.embedded_view = self.field.views[mode];
3541 if(view.view_type === "list") {
3542 _.extend(view.options, {
3544 selectable: self.multi_selection,
3546 import_enabled: false,
3549 if (self.get("effective_readonly")) {
3550 _.extend(view.options, {
3555 } else if (view.view_type === "form") {
3556 if (self.get("effective_readonly")) {
3557 view.view_type = 'form';
3559 _.extend(view.options, {
3560 not_interactible_on_create: true,
3562 } else if (view.view_type === "kanban") {
3563 _.extend(view.options, {
3564 confirm_on_delete: false,
3566 if (self.get("effective_readonly")) {
3567 _.extend(view.options, {
3568 action_buttons: false,
3569 quick_creatable: false,
3571 read_only_mode: true,
3579 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
3580 this.viewmanager.o2m = self;
3581 var once = $.Deferred().done(function() {
3582 self.init_form_last_update.resolve();
3584 var def = $.Deferred().done(function() {
3585 self.initial_is_loaded.resolve();
3587 this.viewmanager.on("controller_inited", self, function(view_type, controller) {
3588 controller.o2m = self;
3589 if (view_type == "list") {
3590 if (self.get("effective_readonly")) {
3591 controller.on('edit:before', self, function (e) {
3594 _(controller.columns).find(function (column) {
3595 if (!(column instanceof instance.web.list.Handle)) {
3598 column.modifiers.invisible = true;
3602 } else if (view_type === "form") {
3603 if (self.get("effective_readonly")) {
3604 $(".oe_form_buttons", controller.$el).children().remove();
3606 controller.on("load_record", self, function(){
3609 controller.on('pager_action_executed',self,self.save_any_view);
3610 } else if (view_type == "graph") {
3611 self.reload_current_view()
3615 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
3616 $.when(self.save_any_view()).done(function() {
3617 if (n_mode === "list") {
3618 $.async_when().done(function() {
3619 self.reload_current_view();
3624 $.async_when().done(function () {
3625 self.viewmanager.appendTo(self.$el);
3629 reload_current_view: function() {
3631 return self.is_loaded = self.is_loaded.then(function() {
3632 var active_view = self.viewmanager.active_view;
3633 var view = self.viewmanager.views[active_view].controller;
3634 if(active_view === "list") {
3635 return view.reload_content();
3636 } else if (active_view === "form") {
3637 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
3638 self.dataset.index = 0;
3640 var act = function() {
3641 return view.do_show();
3643 self.form_last_update = self.form_last_update.then(act, act);
3644 return self.form_last_update;
3645 } else if (view.do_search) {
3646 return view.do_search(self.build_domain(), self.dataset.get_context(), []);
3650 set_value: function(value_) {
3651 value_ = value_ || [];
3653 this.dataset.reset_ids([]);
3654 if(value_.length >= 1 && value_[0] instanceof Array) {
3656 _.each(value_, function(command) {
3657 var obj = {values: command[2]};
3658 switch (command[0]) {
3659 case commands.CREATE:
3660 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3662 self.dataset.to_create.push(obj);
3663 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3666 case commands.UPDATE:
3667 obj['id'] = command[1];
3668 self.dataset.to_write.push(obj);
3669 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3672 case commands.DELETE:
3673 self.dataset.to_delete.push({id: command[1]});
3675 case commands.LINK_TO:
3676 ids.push(command[1]);
3678 case commands.DELETE_ALL:
3679 self.dataset.delete_all = true;
3684 this.dataset.set_ids(ids);
3685 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
3687 this.dataset.delete_all = true;
3688 _.each(value_, function(command) {
3689 var obj = {values: command};
3690 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3692 self.dataset.to_create.push(obj);
3693 self.dataset.cache.push(_.clone(obj));
3697 this.dataset.set_ids(ids);
3699 this._super(value_);
3700 this.dataset.reset_ids(value_);
3702 if (this.dataset.index === null && this.dataset.ids.length > 0) {
3703 this.dataset.index = 0;
3705 this.trigger_on_change();
3706 if (this.is_started) {
3707 return self.reload_current_view();
3712 get_value: function() {
3716 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
3717 val = val.concat(_.map(this.dataset.ids, function(id) {
3718 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
3720 return commands.create(alter_order.values);
3722 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
3724 return commands.update(alter_order.id, alter_order.values);
3726 return commands.link_to(id);
3728 return val.concat(_.map(
3729 this.dataset.to_delete, function(x) {
3730 return commands['delete'](x.id);}));
3732 commit_value: function() {
3733 return this.save_any_view();
3735 save_any_view: function() {
3736 if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
3737 this.viewmanager.views[this.viewmanager.active_view] &&
3738 this.viewmanager.views[this.viewmanager.active_view].controller) {
3739 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3740 if (this.viewmanager.active_view === "form") {
3741 if (!view.is_initialized.state() === 'resolved') {
3742 return $.when(false);
3744 return $.when(view.save());
3745 } else if (this.viewmanager.active_view === "list") {
3746 return $.when(view.ensure_saved());
3749 return $.when(false);
3751 is_syntax_valid: function() {
3752 if (! this.viewmanager || ! this.viewmanager.views[this.viewmanager.active_view])
3754 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3755 switch (this.viewmanager.active_view) {
3757 return _(view.fields).chain()
3763 return view.is_valid();
3769 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
3770 template: 'One2Many.viewmanager',
3771 init: function(parent, dataset, views, flags) {
3772 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
3773 this.registry = this.registry.extend({
3774 list: 'instance.web.form.One2ManyListView',
3775 form: 'instance.web.form.One2ManyFormView',
3776 kanban: 'instance.web.form.One2ManyKanbanView',
3778 this.__ignore_blur = false;
3780 switch_mode: function(mode, unused) {
3781 if (mode !== 'form') {
3782 return this._super(mode, unused);
3785 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
3786 var pop = new instance.web.form.FormOpenPopup(this);
3787 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3788 title: _t("Open: ") + self.o2m.string,
3789 create_function: function(data, options) {
3790 return self.o2m.dataset.create(data, options).done(function(r) {
3791 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3792 self.o2m.dataset.trigger("dataset_changed", r);
3795 write_function: function(id, data, options) {
3796 return self.o2m.dataset.write(id, data, {}).done(function() {
3797 self.o2m.reload_current_view();
3800 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3801 parent_view: self.o2m.view,
3802 child_name: self.o2m.name,
3803 read_function: function() {
3804 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3806 form_view_options: {'not_interactible_on_create':true},
3807 readonly: self.o2m.get("effective_readonly")
3809 pop.on("elements_selected", self, function() {
3810 self.o2m.reload_current_view();
3815 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
3816 get_context: function() {
3817 this.context = this.o2m.build_context();
3818 return this.context;
3822 instance.web.form.One2ManyListView = instance.web.ListView.extend({
3823 _template: 'One2Many.listview',
3824 init: function (parent, dataset, view_id, options) {
3825 this._super(parent, dataset, view_id, _.extend(options || {}, {
3826 GroupsType: instance.web.form.One2ManyGroups,
3827 ListType: instance.web.form.One2ManyList
3829 this.on('edit:before', this, this.proxy('_before_edit'));
3830 this.on('edit:after', this, this.proxy('_after_edit'));
3831 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
3834 .bind('add', this.proxy("changed_records"))
3835 .bind('edit', this.proxy("changed_records"))
3836 .bind('remove', this.proxy("changed_records"));
3838 start: function () {
3839 var ret = this._super();
3841 .off('mousedown.handleButtons')
3842 .on('mousedown.handleButtons', 'table button', this.proxy('_button_down'));
3845 changed_records: function () {
3846 this.o2m.trigger_on_change();
3848 is_valid: function () {
3849 var editor = this.editor;
3850 var form = editor.form;
3851 // If no edition is pending, the listview can not be invalid (?)
3852 if (!editor.record) {
3855 // If the form has not been modified, the view can only be valid
3856 // NB: is_dirty will also be set on defaults/onchanges/whatever?
3857 // oe_form_dirty seems to only be set on actual user actions
3858 if (!form.$el.is('.oe_form_dirty')) {
3861 this.o2m._dirty_flag = true;
3863 // Otherwise validate internal form
3864 return _(form.fields).chain()
3865 .invoke(function () {
3866 this._check_css_flags();
3867 return this.is_valid();
3872 do_add_record: function () {
3873 if (this.editable()) {
3874 this._super.apply(this, arguments);
3877 var pop = new instance.web.form.SelectCreatePopup(this);
3879 self.o2m.field.relation,
3881 title: _t("Create: ") + self.o2m.string,
3882 initial_view: "form",
3883 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3884 create_function: function(data, options) {
3885 return self.o2m.dataset.create(data, options).done(function(r) {
3886 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3887 self.o2m.dataset.trigger("dataset_changed", r);
3890 read_function: function() {
3891 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3893 parent_view: self.o2m.view,
3894 child_name: self.o2m.name,
3895 form_view_options: {'not_interactible_on_create':true}
3897 self.o2m.build_domain(),
3898 self.o2m.build_context()
3900 pop.on("elements_selected", self, function() {
3901 self.o2m.reload_current_view();
3905 do_activate_record: function(index, id) {
3907 var pop = new instance.web.form.FormOpenPopup(self);
3908 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3909 title: _t("Open: ") + self.o2m.string,
3910 write_function: function(id, data) {
3911 return self.o2m.dataset.write(id, data, {}).done(function() {
3912 self.o2m.reload_current_view();
3915 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3916 parent_view: self.o2m.view,
3917 child_name: self.o2m.name,
3918 read_function: function() {
3919 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3921 form_view_options: {'not_interactible_on_create':true},
3922 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
3925 do_button_action: function (name, id, callback) {
3926 if (!_.isNumber(id)) {
3927 instance.webclient.notification.warn(
3928 _t("Action Button"),
3929 _t("The o2m record must be saved before an action can be used"));
3932 var parent_form = this.o2m.view;
3934 this.ensure_saved().then(function () {
3936 return parent_form.save();
3939 }).done(function () {
3940 if (!self.o2m.options.reload_on_button) {
3941 self.handle_button(name, id, callback);
3943 self.handle_button(name, id, function(){
3944 self.o2m.view.reload();
3950 _before_edit: function () {
3951 this.__ignore_blur = false;
3952 this.editor.form.on('blurred', this, this._on_form_blur);
3954 _after_edit: function () {
3955 // The form's blur thing may be jiggered during the edition setup,
3956 // potentially leading to the o2m instasaving the row. Cancel any
3957 // blurring triggered the edition startup here
3958 this.editor.form.widgetFocused();
3960 _before_unedit: function () {
3961 this.editor.form.off('blurred', this, this._on_form_blur);
3963 _button_down: function () {
3964 // If a button is clicked (usually some sort of action button), it's
3965 // the button's responsibility to ensure the editable list is in the
3966 // correct state -> ignore form blurring
3967 this.__ignore_blur = true;
3970 * Handles blurring of the nested form (saves the currently edited row),
3971 * unless the flag to ignore the event is set to ``true``
3973 * Makes the internal form go away
3975 _on_form_blur: function () {
3976 if (this.__ignore_blur) {
3977 this.__ignore_blur = false;
3980 // FIXME: why isn't there an API for this?
3981 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
3982 this.ensure_saved();
3985 this.cancel_edition();
3987 keyup_ENTER: function () {
3988 // blurring caused by hitting the [Return] key, should skip the
3989 // autosave-on-blur and let the handler for [Return] do its thing (save
3990 // the current row *anyway*, then create a new one/edit the next one)
3991 this.__ignore_blur = true;
3992 this._super.apply(this, arguments);
3994 do_delete: function (ids) {
3995 var confirm = window.confirm;
3996 window.confirm = function () { return true; };
3998 return this._super(ids);
4000 window.confirm = confirm;
4004 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4005 setup_resequence_rows: function () {
4006 if (!this.view.o2m.get('effective_readonly')) {
4007 this._super.apply(this, arguments);
4011 instance.web.form.One2ManyList = instance.web.ListView.List.extend({
4012 pad_table_to: function (count) {
4013 if (!this.view.is_action_enabled('create')) {
4016 this._super(count > 0 ? count - 1 : 0);
4019 // magical invocation of wtf does that do
4020 if (this.view.o2m.get('effective_readonly')) {
4025 var columns = _(this.columns).filter(function (column) {
4026 return column.invisible !== '1';
4028 if (this.options.selectable) { columns++; }
4029 if (this.options.deletable) { columns++; }
4031 if (!this.view.is_action_enabled('create')) {
4035 var $cell = $('<td>', {
4037 'class': 'oe_form_field_one2many_list_row_add'
4039 $('<a>', {href: '#'}).text(_t("Add an item"))
4040 .mousedown(function () {
4041 // FIXME: needs to be an official API somehow
4042 if (self.view.editor.is_editing()) {
4043 self.view.__ignore_blur = true;
4046 .click(function (e) {
4048 e.stopPropagation();
4049 // FIXME: there should also be an API for that one
4050 if (self.view.editor.form.__blur_timeout) {
4051 clearTimeout(self.view.editor.form.__blur_timeout);
4052 self.view.editor.form.__blur_timeout = false;
4054 self.view.ensure_saved().done(function () {
4055 self.view.do_add_record();
4059 var $padding = this.$current.find('tr:not([data-id]):first');
4060 var $newrow = $('<tr>').append($cell);
4061 if ($padding.length) {
4062 $padding.before($newrow);
4064 this.$current.append($newrow)
4069 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4070 form_template: 'One2Many.formview',
4071 load_form: function(data) {
4074 this.$buttons.find('button.oe_form_button_create').click(function() {
4075 self.save().done(self.on_button_new);
4078 do_notify_change: function() {
4079 if (this.dataset.parent_view) {
4080 this.dataset.parent_view.do_notify_change();
4082 this._super.apply(this, arguments);
4087 var lazy_build_o2m_kanban_view = function() {
4088 if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
4090 instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4094 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4095 template: "FieldMany2ManyTags",
4097 this._super.apply(this, arguments);
4098 instance.web.form.CompletionFieldMixin.init.call(this);
4099 this.set({"value": []});
4100 this._display_orderer = new instance.web.DropMisordered();
4101 this._drop_shown = false;
4103 initialize_content: function() {
4104 if (this.get("effective_readonly"))
4107 var ignore_blur = false;
4108 self.$text = this.$("textarea");
4109 self.$text.textext({
4110 plugins : 'tags arrow autocomplete',
4112 render: function(suggestion) {
4113 return $('<span class="text-label"/>').
4114 data('index', suggestion['index']).html(suggestion['label']);
4119 selectFromDropdown: function() {
4120 this.trigger('hideDropdown');
4121 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4122 var data = self.search_result[index];
4124 self.add_id(data.id);
4129 this.trigger('setSuggestions', {result : []});
4133 isTagAllowed: function(tag) {
4137 removeTag: function(tag) {
4138 var id = tag.data("id");
4139 self.set({"value": _.without(self.get("value"), id)});
4141 renderTag: function(stuff) {
4142 return $.fn.textext.TextExtTags.prototype.renderTag.
4143 call(this, stuff).data("id", stuff.id);
4147 itemToString: function(item) {
4152 onSetInputData: function(e, data) {
4154 this._plugins.autocomplete._suggestions = null;
4156 this.input().val(data);
4160 }).bind('getSuggestions', function(e, data) {
4162 var str = !!data ? data.query || '' : '';
4163 self.get_search_result(str).done(function(result) {
4164 self.search_result = result;
4165 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4166 return _.extend(el, {index:i});
4169 }).bind('hideDropdown', function() {
4170 self._drop_shown = false;
4171 }).bind('showDropdown', function() {
4172 self._drop_shown = true;
4174 self.tags = self.$text.textext()[0].tags();
4176 .focusin(function () {
4177 self.trigger('focused');
4178 ignore_blur = false;
4180 .focusout(function() {
4181 self.$text.trigger("setInputData", "");
4183 self.trigger('blurred');
4185 }).keydown(function(e) {
4186 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4187 self.$text.textext()[0].autocomplete().selectFromDropdown();
4191 set_value: function(value_) {
4192 value_ = value_ || [];
4193 if (value_.length >= 1 && value_[0] instanceof Array) {
4194 value_ = value_[0][2];
4196 this._super(value_);
4198 is_false: function() {
4199 return _(this.get("value")).isEmpty();
4201 get_value: function() {
4202 var tmp = [commands.replace_with(this.get("value"))];
4205 get_search_blacklist: function() {
4206 return this.get("value");
4208 render_value: function() {
4210 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4211 var values = self.get("value");
4212 var handle_names = function(data) {
4213 if (self.isDestroyed())
4216 _.each(data, function(el) {
4217 indexed[el[0]] = el;
4219 data = _.map(values, function(el) { return indexed[el]; });
4220 if (! self.get("effective_readonly")) {
4221 self.tags.containerElement().children().remove();
4222 self.$('textarea').css("padding-left", "3px");
4223 self.tags.addTags(_.map(data, function(el) {return {name: el[1], id:el[0]};}));
4225 self.$el.html(QWeb.render("FieldMany2ManyTag", {elements: data}));
4228 if (! values || values.length > 0) {
4229 this._display_orderer.add(dataset.name_get(values)).done(handle_names);
4234 add_id: function(id) {
4235 this.set({'value': _.uniq(this.get('value').concat([id]))});
4237 focus: function () {
4238 var input = this.$text && this.$text[0];
4239 return input ? input.focus() : false;
4245 - reload_on_button: Reload the whole form view if click on a button in a list view.
4246 If you see this options, do not use it, it's basically a dirty hack to make one
4247 precise o2m to behave the way we want.
4249 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4250 multi_selection: false,
4251 disable_utility_classes: true,
4252 init: function(field_manager, node) {
4253 this._super(field_manager, node);
4254 this.is_loaded = $.Deferred();
4255 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4256 this.dataset.m2m = this;
4258 this.dataset.on('unlink', self, function(ids) {
4259 self.dataset_changed();
4262 this.list_dm = new instance.web.DropMisordered();
4263 this.render_value_dm = new instance.web.DropMisordered();
4265 initialize_content: function() {
4268 this.$el.addClass('oe_form_field oe_form_field_many2many');
4270 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4271 'addable': this.get("effective_readonly") ? null : _t("Add"),
4272 'deletable': this.get("effective_readonly") ? false : true,
4273 'selectable': this.multi_selection,
4275 'reorderable': false,
4276 'import_enabled': false,
4278 var embedded = (this.field.views || {}).tree;
4280 this.list_view.set_embedded_view(embedded);
4282 this.list_view.m2m_field = this;
4283 var loaded = $.Deferred();
4284 this.list_view.on("list_view_loaded", this, function() {
4287 this.list_view.appendTo(this.$el);
4289 var old_def = self.is_loaded;
4290 self.is_loaded = $.Deferred().done(function() {
4293 this.list_dm.add(loaded).then(function() {
4294 self.is_loaded.resolve();
4297 destroy_content: function() {
4298 this.list_view.destroy();
4299 this.list_view = undefined;
4301 set_value: function(value_) {
4302 value_ = value_ || [];
4303 if (value_.length >= 1 && value_[0] instanceof Array) {
4304 value_ = value_[0][2];
4306 this._super(value_);
4308 get_value: function() {
4309 return [commands.replace_with(this.get('value'))];
4311 is_false: function () {
4312 return _(this.get("value")).isEmpty();
4314 render_value: function() {
4316 this.dataset.set_ids(this.get("value"));
4317 this.render_value_dm.add(this.is_loaded).then(function() {
4318 return self.list_view.reload_content();
4321 dataset_changed: function() {
4322 this.internal_set_value(this.dataset.ids);
4326 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4327 get_context: function() {
4328 this.context = this.m2m.build_context();
4329 return this.context;
4335 * @extends instance.web.ListView
4337 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4338 do_add_record: function () {
4339 var pop = new instance.web.form.SelectCreatePopup(this);
4343 title: _t("Add: ") + this.m2m_field.string
4345 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4346 this.m2m_field.build_context()
4349 pop.on("elements_selected", self, function(element_ids) {
4351 _(element_ids).each(function (id) {
4352 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4353 self.dataset.set_ids(self.dataset.ids.concat([id]));
4354 self.m2m_field.dataset_changed();
4359 self.reload_content();
4363 do_activate_record: function(index, id) {
4365 var pop = new instance.web.form.FormOpenPopup(this);
4366 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4367 title: _t("Open: ") + this.m2m_field.string,
4368 readonly: this.getParent().get("effective_readonly")
4370 pop.on('write_completed', self, self.reload_content);
4372 do_button_action: function(name, id, callback) {
4374 var _sup = _.bind(this._super, this);
4375 if (! this.m2m_field.options.reload_on_button) {
4376 return _sup(name, id, callback);
4378 return this.m2m_field.view.save().then(function() {
4379 return _sup(name, id, function() {
4380 self.m2m_field.view.reload();
4385 is_action_enabled: function () { return true; },
4388 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4389 disable_utility_classes: true,
4390 init: function(field_manager, node) {
4391 this._super(field_manager, node);
4392 instance.web.form.CompletionFieldMixin.init.call(this);
4393 m2m_kanban_lazy_init();
4394 this.is_loaded = $.Deferred();
4395 this.initial_is_loaded = this.is_loaded;
4398 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4399 this.dataset.m2m = this;
4400 this.dataset.on('unlink', self, function(ids) {
4401 self.dataset_changed();
4405 this._super.apply(this, arguments);
4410 self.on("change:effective_readonly", self, function() {
4411 self.is_loaded = self.is_loaded.then(function() {
4412 self.kanban_view.destroy();
4413 return $.when(self.load_view()).done(function() {
4414 self.render_value();
4419 set_value: function(value_) {
4420 value_ = value_ || [];
4421 if (value_.length >= 1 && value_[0] instanceof Array) {
4422 value_ = value_[0][2];
4424 this._super(value_);
4426 get_value: function() {
4427 return [commands.replace_with(this.get('value'))];
4429 load_view: function() {
4431 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4432 'create_text': _t("Add"),
4433 'creatable': self.get("effective_readonly") ? false : true,
4434 'quick_creatable': self.get("effective_readonly") ? false : true,
4435 'read_only_mode': self.get("effective_readonly") ? true : false,
4436 'confirm_on_delete': false,
4438 var embedded = (this.field.views || {}).kanban;
4440 this.kanban_view.set_embedded_view(embedded);
4442 this.kanban_view.m2m = this;
4443 var loaded = $.Deferred();
4444 this.kanban_view.on("kanban_view_loaded",self,function() {
4445 self.initial_is_loaded.resolve();
4448 this.kanban_view.on('switch_mode', this, this.open_popup);
4449 $.async_when().done(function () {
4450 self.kanban_view.appendTo(self.$el);
4454 render_value: function() {
4456 this.dataset.set_ids(this.get("value"));
4457 this.is_loaded = this.is_loaded.then(function() {
4458 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
4461 dataset_changed: function() {
4462 this.set({'value': this.dataset.ids});
4464 open_popup: function(type, unused) {
4465 if (type !== "form")
4468 if (this.dataset.index === null) {
4469 var pop = new instance.web.form.SelectCreatePopup(this);
4471 this.field.relation,
4473 title: _t("Add: ") + this.string
4475 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
4476 this.build_context()
4478 pop.on("elements_selected", self, function(element_ids) {
4479 _.each(element_ids, function(one_id) {
4480 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
4481 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
4482 self.dataset_changed();
4483 self.render_value();
4488 var id = self.dataset.ids[self.dataset.index];
4489 var pop = new instance.web.form.FormOpenPopup(this);
4490 pop.show_element(self.field.relation, id, self.build_context(), {
4491 title: _t("Open: ") + self.string,
4492 write_function: function(id, data, options) {
4493 return self.dataset.write(id, data, {}).done(function() {
4494 self.render_value();
4497 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
4498 parent_view: self.view,
4499 child_name: self.name,
4500 readonly: self.get("effective_readonly")
4504 add_id: function(id) {
4505 this.quick_create.add_id(id);
4509 function m2m_kanban_lazy_init() {
4510 if (instance.web.form.Many2ManyKanbanView)
4512 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4513 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
4514 _is_quick_create_enabled: function() {
4515 return this._super() && ! this.group_by;
4518 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
4519 template: 'Many2ManyKanban.quick_create',
4522 * close_btn: If true, the widget will display a "Close" button able to trigger
4525 init: function(parent, dataset, context, buttons) {
4526 this._super(parent);
4527 this.m2m = this.getParent().view.m2m;
4528 this.m2m.quick_create = this;
4529 this._dataset = dataset;
4530 this._buttons = buttons || false;
4531 this._context = context || {};
4533 start: function () {
4535 self.$text = this.$el.find('input').css("width", "200px");
4536 self.$text.textext({
4537 plugins : 'arrow autocomplete',
4539 render: function(suggestion) {
4540 return $('<span class="text-label"/>').
4541 data('index', suggestion['index']).html(suggestion['label']);
4546 selectFromDropdown: function() {
4547 $(this).trigger('hideDropdown');
4548 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4549 var data = self.search_result[index];
4551 self.add_id(data.id);
4558 itemToString: function(item) {
4563 }).bind('getSuggestions', function(e, data) {
4565 var str = !!data ? data.query || '' : '';
4566 self.m2m.get_search_result(str).done(function(result) {
4567 self.search_result = result;
4568 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4569 return _.extend(el, {index:i});
4573 self.$text.focusout(function() {
4578 this.$text[0].focus();
4580 add_id: function(id) {
4583 self.trigger('added', id);
4584 this.m2m.dataset_changed();
4590 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
4592 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
4593 template: "AbstractFormPopup.render",
4596 * -readonly: only applicable when not in creation mode, default to false
4597 * - alternative_form_view
4604 * - form_view_options
4606 init_popup: function(model, row_id, domain, context, options) {
4607 this.row_id = row_id;
4609 this.domain = domain || [];
4610 this.context = context || {};
4611 this.options = options;
4612 _.defaults(this.options, {
4615 init_dataset: function() {
4617 this.created_elements = [];
4618 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
4619 this.dataset.read_function = this.options.read_function;
4620 this.dataset.create_function = function(data, options, sup) {
4621 var fct = self.options.create_function || sup;
4622 return fct.call(this, data, options).done(function(r) {
4623 self.trigger('create_completed saved', r);
4624 self.created_elements.push(r);
4627 this.dataset.write_function = function(id, data, options, sup) {
4628 var fct = self.options.write_function || sup;
4629 return fct.call(this, id, data, options).done(function(r) {
4630 self.trigger('write_completed saved', r);
4633 this.dataset.parent_view = this.options.parent_view;
4634 this.dataset.child_name = this.options.child_name;
4636 display_popup: function() {
4638 this.renderElement();
4639 var dialog = new instance.web.Dialog(this, {
4641 dialogClass: 'oe_act_window',
4643 self.check_exit(true);
4645 title: this.options.title || "",
4646 }, this.$el).open();
4647 this.$buttonpane = dialog.$buttons;
4650 setup_form_view: function() {
4653 this.dataset.ids = [this.row_id];
4654 this.dataset.index = 0;
4656 this.dataset.index = null;
4658 var options = _.clone(self.options.form_view_options) || {};
4659 if (this.row_id !== null) {
4660 options.initial_mode = this.options.readonly ? "view" : "edit";
4663 $buttons: this.$buttonpane,
4665 this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
4666 if (this.options.alternative_form_view) {
4667 this.view_form.set_embedded_view(this.options.alternative_form_view);
4669 this.view_form.appendTo(this.$el.find(".oe_popup_form"));
4670 this.view_form.on("form_view_loaded", self, function() {
4671 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
4672 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
4673 multi_select: multi_select,
4674 readonly: self.row_id !== null && self.options.readonly,
4676 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
4677 $snbutton.click(function() {
4678 $.when(self.view_form.save()).done(function() {
4679 self.view_form.reload_mutex.exec(function() {
4680 self.view_form.on_button_new();
4684 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
4685 $sbutton.click(function() {
4686 $.when(self.view_form.save()).done(function() {
4687 self.view_form.reload_mutex.exec(function() {
4692 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
4693 $cbutton.click(function() {
4694 self.view_form.trigger('on_button_cancel');
4697 self.view_form.do_show();
4700 select_elements: function(element_ids) {
4701 this.trigger("elements_selected", element_ids);
4703 check_exit: function(no_destroy) {
4704 if (this.created_elements.length > 0) {
4705 this.select_elements(this.created_elements);
4706 this.created_elements = [];
4708 this.trigger('closed');
4711 destroy: function () {
4712 this.trigger('closed');
4713 if (this.$el.is(":data(dialog)")) {
4714 this.$el.dialog('close');
4721 * Class to display a popup containing a form view.
4723 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
4724 show_element: function(model, row_id, context, options) {
4725 this.init_popup(model, row_id, [], context, options);
4726 _.defaults(this.options, {
4728 this.display_popup();
4732 this.init_dataset();
4733 this.setup_form_view();
4738 * Class to display a popup to display a list to search a row. It also allows
4739 * to switch to a form view to create a new row.
4741 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
4745 * - initial_view: form or search (default search)
4746 * - disable_multiple_selection
4747 * - list_view_options
4749 select_element: function(model, options, domain, context) {
4750 this.init_popup(model, null, domain, context, options);
4752 _.defaults(this.options, {
4753 initial_view: "search",
4755 this.initial_ids = this.options.initial_ids;
4756 this.display_popup();
4760 this.init_dataset();
4761 if (this.options.initial_view == "search") {
4762 instance.web.pyeval.eval_domains_and_contexts({
4764 contexts: [this.context]
4765 }).done(function (results) {
4766 var search_defaults = {};
4767 _.each(results.context, function (value_, key) {
4768 var match = /^search_default_(.*)$/.exec(key);
4770 search_defaults[match[1]] = value_;
4773 self.setup_search_view(search_defaults);
4779 setup_search_view: function(search_defaults) {
4781 if (this.searchview) {
4782 this.searchview.destroy();
4784 this.searchview = new instance.web.SearchView(this,
4785 this.dataset, false, search_defaults);
4786 this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
4787 if (self.initial_ids) {
4788 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
4789 contexts, groupbys);
4790 self.initial_ids = undefined;
4792 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
4795 this.searchview.on("search_view_loaded", self, function() {
4796 self.view_list = new instance.web.form.SelectCreateListView(self,
4797 self.dataset, false,
4798 _.extend({'deletable': false,
4799 'selectable': !self.options.disable_multiple_selection,
4800 'import_enabled': false,
4801 '$buttons': self.$buttonpane,
4802 'disable_editable_mode': true,
4803 '$pager': self.$('.oe_popup_list_pager'),
4804 }, self.options.list_view_options || {}));
4805 self.view_list.on('edit:before', self, function (e) {
4808 self.view_list.popup = self;
4809 self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
4810 self.view_list.do_show();
4811 }).then(function() {
4812 self.searchview.do_search();
4814 self.view_list.on("list_view_loaded", self, function() {
4815 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
4816 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
4817 $cbutton.click(function() {
4820 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
4821 $sbutton.click(function() {
4822 self.select_elements(self.selected_ids);
4825 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
4826 $cbutton.click(function() {
4831 this.searchview.appendTo($(".oe_popup_search", self.$el));
4833 do_search: function(domains, contexts, groupbys) {
4835 instance.web.pyeval.eval_domains_and_contexts({
4836 domains: domains || [],
4837 contexts: contexts || [],
4838 group_by_seq: groupbys || []
4839 }).done(function (results) {
4840 self.view_list.do_search(results.domain, results.context, results.group_by);
4843 on_click_element: function(ids) {
4845 this.selected_ids = ids || [];
4846 if(this.selected_ids.length > 0) {
4847 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
4849 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
4852 new_object: function() {
4853 if (this.searchview) {
4854 this.searchview.hide();
4856 if (this.view_list) {
4857 this.view_list.do_hide();
4859 this.setup_form_view();
4863 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
4864 do_add_record: function () {
4865 this.popup.new_object();
4867 select_record: function(index) {
4868 this.popup.select_elements([this.dataset.ids[index]]);
4869 this.popup.destroy();
4871 do_select: function(ids, records) {
4872 this._super(ids, records);
4873 this.popup.on_click_element(ids);
4877 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4878 template: 'FieldReference',
4879 init: function(field_manager, node) {
4880 this._super(field_manager, node);
4881 this.reference_ready = true;
4883 destroy_content: function() {
4886 this.fm = undefined;
4889 initialize_content: function() {
4891 var fm = new instance.web.form.DefaultFieldManager(this);
4893 fm.extend_field_desc({
4895 selection: this.field_manager.get_field_desc(this.name).selection,
4903 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
4905 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
4907 this.selection.on("change:value", this, this.on_selection_changed);
4908 this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
4910 .on('focused', null, function () {self.trigger('focused')})
4911 .on('blurred', null, function () {self.trigger('blurred')});
4913 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
4915 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
4917 this.m2o.on("change:value", this, this.data_changed);
4918 this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
4920 .on('focused', null, function () {self.trigger('focused')})
4921 .on('blurred', null, function () {self.trigger('blurred')});
4923 on_selection_changed: function() {
4924 if (this.reference_ready) {
4925 this.internal_set_value([this.selection.get_value(), false]);
4926 this.render_value();
4929 data_changed: function() {
4930 if (this.reference_ready) {
4931 this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
4934 set_value: function(val) {
4936 val = val.split(',');
4937 val[0] = val[0] || false;
4938 val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
4940 this._super(val || [false, false]);
4942 get_value: function() {
4943 return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
4945 render_value: function() {
4946 this.reference_ready = false;
4947 if (!this.get("effective_readonly")) {
4948 this.selection.set_value(this.get('value')[0]);
4950 this.m2o.field.relation = this.get('value')[0];
4951 this.m2o.set_value(this.get('value')[1]);
4952 this.m2o.$el.toggle(!!this.get('value')[0]);
4953 this.reference_ready = true;
4957 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4958 init: function(field_manager, node) {
4960 this._super(field_manager, node);
4961 this.binary_value = false;
4962 this.useFileAPI = !!window.FileReader;
4963 this.max_upload_size = 25 * 1024 * 1024; // 25Mo
4964 if (!this.useFileAPI) {
4965 this.fileupload_id = _.uniqueId('oe_fileupload');
4966 $(window).on(this.fileupload_id, function() {
4967 var args = [].slice.call(arguments).slice(1);
4968 self.on_file_uploaded.apply(self, args);
4973 if (!this.useFileAPI) {
4974 $(window).off(this.fileupload_id);
4976 this._super.apply(this, arguments);
4978 initialize_content: function() {
4979 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
4980 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
4981 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
4983 on_file_change: function(e) {
4985 var file_node = e.target;
4986 if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
4987 if (this.useFileAPI) {
4988 var file = file_node.files[0];
4989 if (file.size > this.max_upload_size) {
4990 var msg = _t("The selected file exceed the maximum file size of %s.");
4991 instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
4994 var filereader = new FileReader();
4995 filereader.readAsDataURL(file);
4996 filereader.onloadend = function(upload) {
4997 var data = upload.target.result;
4998 data = data.split(',')[1];
4999 self.on_file_uploaded(file.size, file.name, file.type, data);
5002 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5003 this.$el.find('form.oe_form_binary_form').submit();
5005 this.$el.find('.oe_form_binary_progress').show();
5006 this.$el.find('.oe_form_binary').hide();
5009 on_file_uploaded: function(size, name, content_type, file_base64) {
5010 if (size === false) {
5011 this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5012 // TODO: use openerp web crashmanager
5013 console.warn("Error while uploading file : ", name);
5015 this.filename = name;
5016 this.on_file_uploaded_and_valid.apply(this, arguments);
5018 this.$el.find('.oe_form_binary_progress').hide();
5019 this.$el.find('.oe_form_binary').show();
5021 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5023 on_save_as: function(ev) {
5024 var value = this.get('value');
5026 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5027 ev.stopPropagation();
5029 instance.web.blockUI();
5030 var c = instance.webclient.crashmanager;
5031 this.session.get_file({
5032 url: '/web/binary/saveas_ajax',
5033 data: {data: JSON.stringify({
5034 model: this.view.dataset.model,
5035 id: (this.view.datarecord.id || ''),
5037 filename_field: (this.node.attrs.filename || ''),
5038 data: instance.web.form.is_bin_size(value) ? null : value,
5039 context: this.view.dataset.get_context()
5041 complete: instance.web.unblockUI,
5042 error: c.rpc_error.bind(c)
5044 ev.stopPropagation();
5048 set_filename: function(value) {
5049 var filename = this.node.attrs.filename;
5052 tmp[filename] = value;
5053 this.field_manager.set_values(tmp);
5056 on_clear: function() {
5057 if (this.get('value') !== false) {
5058 this.binary_value = false;
5059 this.internal_set_value(false);
5065 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5066 template: 'FieldBinaryFile',
5067 initialize_content: function() {
5069 if (this.get("effective_readonly")) {
5071 this.$el.find('a').click(function(ev) {
5072 if (self.get('value')) {
5073 self.on_save_as(ev);
5079 render_value: function() {
5080 if (!this.get("effective_readonly")) {
5082 if (this.node.attrs.filename) {
5083 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5085 show_value = (this.get('value') != null && this.get('value') !== false) ? this.get('value') : '';
5087 this.$el.find('input').eq(0).val(show_value);
5089 this.$el.find('a').toggle(!!this.get('value'));
5090 if (this.get('value')) {
5091 var show_value = _t("Download")
5093 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5094 this.$el.find('a').text(show_value);
5098 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5099 this.binary_value = true;
5100 this.internal_set_value(file_base64);
5101 var show_value = name + " (" + instance.web.human_size(size) + ")";
5102 this.$el.find('input').eq(0).val(show_value);
5103 this.set_filename(name);
5105 on_clear: function() {
5106 this._super.apply(this, arguments);
5107 this.$el.find('input').eq(0).val('');
5108 this.set_filename('');
5112 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5113 template: 'FieldBinaryImage',
5114 placeholder: "/web/static/src/img/placeholder.png",
5115 render_value: function() {
5118 if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5119 url = 'data:image/png;base64,' + this.get('value');
5120 } else if (this.get('value')) {
5121 var id = JSON.stringify(this.view.datarecord.id || null);
5122 var field = this.name;
5123 if (this.options.preview_image)
5124 field = this.options.preview_image;
5125 url = this.session.url('/web/binary/image', {
5126 model: this.view.dataset.model,
5129 t: (new Date().getTime()),
5132 url = this.placeholder;
5134 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5135 this.$el.find('> img').remove();
5136 this.$el.prepend($img);
5137 $img.load(function() {
5138 if (! self.options.size)
5140 $img.css("max-width", "" + self.options.size[0] + "px");
5141 $img.css("max-height", "" + self.options.size[1] + "px");
5142 $img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px");
5143 $img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px");
5145 $img.on('error', function() {
5146 $img.attr('src', self.placeholder);
5147 instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5150 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5151 this.internal_set_value(file_base64);
5152 this.binary_value = true;
5153 this.render_value();
5154 this.set_filename(name);
5156 on_clear: function() {
5157 this._super.apply(this, arguments);
5158 this.render_value();
5159 this.set_filename('');
5164 * Widget for (one2many field) to upload one or more file in same time and display in list.
5165 * The user can delete his files.
5166 * Options on attribute ; "blockui" {Boolean} block the UI or not
5167 * during the file is uploading
5169 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
5170 template: "FieldBinaryFileUploader",
5171 init: function(field_manager, node) {
5172 this._super(field_manager, node);
5173 this.field_manager = field_manager;
5175 if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5176 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);
5178 this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5179 this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5180 $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5184 this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5186 set_value: function(value_) {
5187 var value_ = value_ || [];
5190 _.each(value_, function(command) {
5191 if (isNaN(command) && command.id == undefined) {
5192 switch (command[0]) {
5193 case commands.CREATE:
5194 ids = ids.concat(command[2]);
5196 case commands.REPLACE_WITH:
5197 ids = ids.concat(command[2]);
5199 case commands.UPDATE:
5200 ids = ids.concat(command[2]);
5202 case commands.LINK_TO:
5203 ids = ids.concat(command[1]);
5205 case commands.DELETE:
5206 ids = _.filter(ids, function (id) { return id != command[1];});
5208 case commands.DELETE_ALL:
5218 get_value: function() {
5219 return _.map(this.get('value'), function (value) { return commands.link_to( isNaN(value) ? value.id : value ); });
5221 get_file_url: function (attachment) {
5222 return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5224 read_name_values : function () {
5226 // select the list of id for a get_name
5228 _.each(this.get('value'), function (val) {
5229 if (typeof val != 'object') {
5233 // send request for get_name
5234 if (values.length) {
5235 return this.ds_file.call('read', [values, ['id', 'name', 'datas_fname']]).done(function (datas) {
5236 _.each(datas, function (data) {
5237 data.no_unlink = true;
5238 data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5240 _.each(self.get('value'), function (val, key) {
5241 if(val == data.id) {
5242 self.get('value')[key] = data;
5248 return $.when(this.get('value'));
5251 render_value: function () {
5253 this.read_name_values().then(function (datas) {
5255 var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self}));
5256 render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5257 self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5259 // reinit input type file
5260 var $input = self.$('input.oe_form_binary_file');
5261 $input.after($input.clone(true)).remove();
5262 self.$(".oe_fileupload").show();
5266 on_file_change: function (event) {
5267 event.stopPropagation();
5269 var $target = $(event.target);
5270 if ($target.val() !== '') {
5272 var filename = $target.val().replace(/.*[\\\/]/,'');
5274 // if the files is currently uploded, don't send again
5275 if( !isNaN(_.find(this.get('value'), function (file) { return (file.filename || file.name) == filename && file.upload; } )) ) {
5280 if(this.node.attrs.blockui>0) {
5281 instance.web.blockUI();
5284 // if the files exits for this answer, delete the file before upload
5285 var files = _.filter(this.get('value'), function (file) {
5286 if((file.filename || file.name) == filename) {
5287 self.ds_file.unlink([file.id]);
5294 // TODO : unactivate send on wizard and form
5297 this.$('form.oe_form_binary_form').submit();
5298 this.$(".oe_fileupload").hide();
5300 // add file on result
5304 'filename': filename,
5309 this.set({'value': files});
5312 on_file_loaded: function (event, result) {
5313 var files = this.get('value');
5316 if(this.node.attrs.blockui>0) {
5317 instance.web.unblockUI();
5320 // TODO : activate send on wizard and form
5322 if (result.error || !result.id ) {
5323 this.do_warn( _t('Uploading Error'), result.error);
5324 files = _.filter(files, function (val) { return !val.upload; });
5326 for(var i in files){
5327 if(files[i].filename == result.filename && files[i].upload) {
5330 'name': result.name,
5331 'filename': result.filename,
5332 'url': this.get_file_url(result)
5338 this.set({'value': files});
5341 on_file_delete: function (event) {
5342 event.stopPropagation();
5343 var file_id=$(event.target).data("id");
5346 for(var i in this.get('value')){
5347 if(file_id != this.get('value')[i].id){
5348 files.push(this.get('value')[i]);
5350 else if(!this.get('value')[i].no_unlink) {
5351 this.ds_file.unlink([file_id]);
5354 this.set({'value': files});
5359 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5360 template: "FieldStatus",
5361 init: function(field_manager, node) {
5362 this._super(field_manager, node);
5363 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5364 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5365 this.set({value: false});
5366 this.selection = [];
5367 this.set("selection", []);
5368 this.selection_dm = new instance.web.DropMisordered();
5371 this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5373 this.on("change:value", this, this.get_selection);
5374 this.on("change:evaluated_selection_domain", this, this.get_selection);
5375 this.on("change:selection", this, function() {
5376 this.selection = this.get("selection");
5377 this.render_value();
5379 this.get_selection();
5380 if (this.options.clickable) {
5381 this.$el.on('click','li',this.on_click_stage);
5383 if (this.$el.parent().is('header')) {
5384 this.$el.after('<div class="oe_clear"/>');
5388 set_value: function(value_) {
5389 if (value_ instanceof Array) {
5392 this._super(value_);
5394 render_value: function() {
5396 var content = QWeb.render("FieldStatus.content", {widget: self});
5397 self.$el.html(content);
5398 var colors = JSON.parse((self.node.attrs || {}).statusbar_colors || "{}");
5399 var color = colors[self.get('value')];
5401 self.$("oe_active").css("color", color);
5404 calc_domain: function() {
5405 var d = instance.web.pyeval.eval('domain', this.build_domain());
5406 var domain = []; //if there is no domain defined, fetch all the records
5409 domain = ['|',['id', '=', this.get('value')]].concat(d);
5412 if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5413 this.set("evaluated_selection_domain", domain);
5416 /** Get the selection and render it
5417 * selection: [[identifier, value_to_display], ...]
5418 * For selection fields: this is directly given by this.field.selection
5419 * For many2one fields: perform a search on the relation of the many2one field
5421 get_selection: function() {
5425 var calculation = _.bind(function() {
5426 if (this.field.type == "many2one") {
5428 var ds = new instance.web.DataSetSearch(this, this.field.relation,
5429 self.build_context(), this.get("evaluated_selection_domain"));
5430 return ds.read_slice(['name'], {}).then(function (records) {
5431 for(var i = 0; i < records.length; i++) {
5432 selection.push([records[i].id, records[i].name]);
5436 // For field type selection filter values according to
5437 // statusbar_visible attribute of the field. For example:
5438 // statusbar_visible="draft,open".
5439 var select = this.field.selection;
5440 for(var i=0; i < select.length; i++) {
5441 var key = select[i][0];
5442 if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5443 selection.push(select[i]);
5449 this.selection_dm.add(calculation()).then(function () {
5450 if (! _.isEqual(selection, self.get("selection"))) {
5451 self.set("selection", selection);
5455 on_click_stage: function (ev) {
5457 var $li = $(ev.currentTarget);
5458 var val = parseInt($li.data("id"));
5459 if (val != self.get('value')) {
5460 this.view.recursive_save().done(function() {
5462 change[self.name] = val;
5463 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
5471 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
5472 template: "FieldMonetary",
5473 widget_class: 'oe_form_field_float oe_form_field_monetary',
5475 this._super.apply(this, arguments);
5476 this.set({"currency": false});
5477 if (this.options.currency_field) {
5478 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
5479 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
5482 this.on("change:currency", this, this.get_currency_info);
5483 this.get_currency_info();
5484 this.ci_dm = new instance.web.DropMisordered();
5487 var tmp = this._super();
5488 this.on("change:currency_info", this, this.reinitialize);
5491 get_currency_info: function() {
5493 if (this.get("currency") === false) {
5494 this.set({"currency_info": null});
5497 return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
5498 .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
5499 self.set({"currency_info": res});
5502 parse_value: function(val, def) {
5503 return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5505 format_value: function(val, def) {
5506 return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5511 * Registry of form fields, called by :js:`instance.web.FormView`.
5513 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
5514 * will substitute to the <field> tags as defined in OpenERP's views.
5516 instance.web.form.widgets = new instance.web.Registry({
5517 'char' : 'instance.web.form.FieldChar',
5518 'id' : 'instance.web.form.FieldID',
5519 'email' : 'instance.web.form.FieldEmail',
5520 'url' : 'instance.web.form.FieldUrl',
5521 'text' : 'instance.web.form.FieldText',
5522 'html' : 'instance.web.form.FieldTextHtml',
5523 'date' : 'instance.web.form.FieldDate',
5524 'datetime' : 'instance.web.form.FieldDatetime',
5525 'selection' : 'instance.web.form.FieldSelection',
5526 'many2one' : 'instance.web.form.FieldMany2One',
5527 'many2onebutton' : 'instance.web.form.Many2OneButton',
5528 'many2many' : 'instance.web.form.FieldMany2Many',
5529 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
5530 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
5531 'one2many' : 'instance.web.form.FieldOne2Many',
5532 'one2many_list' : 'instance.web.form.FieldOne2Many',
5533 'reference' : 'instance.web.form.FieldReference',
5534 'boolean' : 'instance.web.form.FieldBoolean',
5535 'float' : 'instance.web.form.FieldFloat',
5536 'integer': 'instance.web.form.FieldFloat',
5537 'float_time': 'instance.web.form.FieldFloat',
5538 'progressbar': 'instance.web.form.FieldProgressBar',
5539 'image': 'instance.web.form.FieldBinaryImage',
5540 'binary': 'instance.web.form.FieldBinaryFile',
5541 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
5542 'statusbar': 'instance.web.form.FieldStatus',
5543 'monetary': 'instance.web.form.FieldMonetary',
5547 * Registry of widgets usable in the form view that can substitute to any possible
5548 * tags defined in OpenERP's form views.
5550 * Every referenced class should extend FormWidget.
5552 instance.web.form.tags = new instance.web.Registry({
5553 'button' : 'instance.web.form.WidgetButton',
5556 instance.web.form.custom_widgets = new instance.web.Registry({
5561 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: