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;
624 self.save_list.pop();
631 _internal_set_values: function(values, exclude) {
632 exclude = exclude || [];
633 for (var f in values) {
634 if (!values.hasOwnProperty(f)) { continue; }
635 var field = this.fields[f];
636 // If field is not defined in the view, just ignore it
638 var value_ = values[f];
639 if (field.get_value() != value_) {
640 field._inhibit_on_change_flag = true;
641 field.set_value(value_);
642 field._inhibit_on_change_flag = false;
643 field._dirty_flag = true;
644 if (!_.contains(exclude, field.name)) {
645 this.do_onchange(field, exclude);
650 this.on_form_changed();
652 set_values: function(values) {
654 return this.mutating_mutex.exec(function() {
655 self._internal_set_values(values);
659 * Ask the view to switch to view mode if possible. The view may not do it
660 * if the current record is not yet saved. It will then stay in create mode.
662 to_view_mode: function() {
663 this._actualize_mode("view");
666 * Ask the view to switch to edit mode if possible. The view may not do it
667 * if the current record is not yet saved. It will then stay in create mode.
669 to_edit_mode: function() {
670 this._actualize_mode("edit");
673 * Ask the view to switch to a precise mode if possible. The view is free to
674 * not respect this command if the state of the dataset is not compatible with
675 * the new mode. For example, it is not possible to switch to edit mode if
676 * the current record is not yet saved in database.
678 * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
679 * undefined the view will test the actual mode to check if it is still consistent
680 * with the dataset state.
682 _actualize_mode: function(switch_to) {
683 var mode = switch_to || this.get("actual_mode");
684 if (! this.datarecord.id) {
686 } else if (mode === "create") {
689 this.set({actual_mode: mode});
691 check_actual_mode: function(source, options) {
693 if(this.get("actual_mode") === "view") {
694 self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
695 self.$buttons.find('.oe_form_buttons_edit').hide();
696 self.$buttons.find('.oe_form_buttons_view').show();
697 self.$sidebar.show();
699 self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
700 self.$buttons.find('.oe_form_buttons_edit').show();
701 self.$buttons.find('.oe_form_buttons_view').hide();
702 self.$sidebar.hide();
706 autofocus: function() {
707 if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
708 var fields_order = this.fields_order.slice(0);
709 if (this.default_focus_field) {
710 fields_order.unshift(this.default_focus_field.name);
712 for (var i = 0; i < fields_order.length; i += 1) {
713 var field = this.fields[fields_order[i]];
714 if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
715 if (field.focus() !== false) {
722 on_button_save: function(e) {
724 $(e.target).attr("disabled", true);
725 return this.save().done(function(result) {
726 self.trigger("save", result);
727 self.reload().then(function() {
729 var parent = self.ViewManager.ActionManager.getParent();
731 parent.menu.do_reload_needaction();
734 }).always(function(){
735 $(e.target).attr("disabled", false);
738 on_button_cancel: function(event) {
739 if (this.can_be_discarded()) {
740 if (this.get('actual_mode') === 'create') {
741 this.trigger('history_back');
744 this.trigger('load_record', this.datarecord);
747 this.trigger('on_button_cancel');
750 on_button_new: function() {
753 return $.when(this.has_been_loaded).then(function() {
754 if (self.can_be_discarded()) {
755 return self.load_defaults();
759 on_button_edit: function() {
760 return this.to_edit_mode();
762 on_button_create: function() {
763 this.dataset.index = null;
766 on_button_duplicate: function() {
768 return this.has_been_loaded.then(function() {
769 return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
770 self.record_created(new_id);
775 on_button_delete: function() {
777 var def = $.Deferred();
778 this.has_been_loaded.done(function() {
779 if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
780 self.dataset.unlink([self.datarecord.id]).done(function() {
781 if (self.dataset.size()) {
782 self.execute_pager_action('next');
784 self.do_action('history_back');
789 $.async_when().done(function () {
794 return def.promise();
796 can_be_discarded: function() {
797 if (this.$el.is('.oe_form_dirty')) {
798 if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
801 this.$el.removeClass('oe_form_dirty');
806 * Triggers saving the form's record. Chooses between creating a new
807 * record or saving an existing one depending on whether the record
808 * already has an id property.
810 * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
811 * record, should that record be inserted at the start of the dataset (by
812 * default, records are added at the end)
814 save: function(prepend_on_create) {
816 var save_obj = {prepend_on_create: prepend_on_create, ret: null};
817 this.save_list.push(save_obj);
818 return this._process_operations().then(function() {
820 return $.Deferred().reject();
821 return $.when.apply($, save_obj.ret);
823 self.$el.removeClass('oe_form_dirty');
826 _process_save: function(save_obj) {
828 var prepend_on_create = save_obj.prepend_on_create;
830 var form_invalid = false,
832 first_invalid_field = null,
833 readonly_values = {};
834 for (var f in self.fields) {
835 if (!self.fields.hasOwnProperty(f)) { continue; }
839 if (!first_invalid_field) {
840 first_invalid_field = f;
842 } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
843 // Special case 'id' field, do not save this field
844 // on 'create' : save all non readonly fields
845 // on 'edit' : save non readonly modified fields
846 if (!f.get("readonly")) {
847 values[f.name] = f.get_value();
849 readonly_values[f.name] = f.get_value();
854 self.set({'display_invalid_fields': true});
855 first_invalid_field.focus();
857 return $.Deferred().reject();
859 self.set({'display_invalid_fields': false});
861 if (!self.datarecord.id) {
863 save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
864 return self.record_created(r, prepend_on_create);
866 } else if (_.isEmpty(values)) {
867 // Not dirty, noop save
868 save_deferral = $.Deferred().resolve({}).promise();
871 save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
872 return self.record_saved(r);
875 return save_deferral;
879 return $.Deferred().reject();
882 on_invalid: function() {
883 var warnings = _(this.fields).chain()
884 .filter(function (f) { return !f.is_valid(); })
886 return _.str.sprintf('<li>%s</li>',
889 warnings.unshift('<ul>');
890 warnings.push('</ul>');
891 this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
894 * Reload the form after saving
896 * @param {Object} r result of the write function.
898 record_saved: function(r) {
899 this.trigger('record_saved', r);
901 // should not happen in the server, but may happen for internal purpose
902 return $.Deferred().reject();
907 * Updates the form' dataset to contain the new record:
909 * * Adds the newly created record to the current dataset (at the end by
911 * * Selects that record (sets the dataset's index to point to the new
913 * * Updates the pager and sidebar displays
916 * @param {Boolean} [prepend_on_create=false] adds the newly created record
917 * at the beginning of the dataset instead of the end
919 record_created: function(r, prepend_on_create) {
922 // should not happen in the server, but may happen for internal purpose
923 this.trigger('record_created', r);
924 return $.Deferred().reject();
926 this.datarecord.id = r;
927 if (!prepend_on_create) {
928 this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
929 this.dataset.index = this.dataset.ids.length - 1;
931 this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
932 this.dataset.index = 0;
934 this.do_update_pager();
936 this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
938 //openerp.log("The record has been created with id #" + this.datarecord.id);
939 return $.when(this.reload()).then(function () {
940 self.trigger('record_created', r);
941 return _.extend(r, {created: true});
945 on_action: function (action) {
946 console.debug('Executing action', action);
950 return this.reload_mutex.exec(function() {
951 if (self.dataset.index == null) {
952 self.trigger("previous_view");
953 return $.Deferred().reject().promise();
955 if (self.dataset.index == null || self.dataset.index < 0) {
956 return $.when(self.on_button_new());
958 var fields = _.keys(self.fields_view.fields);
959 fields.push('display_name');
960 return self.dataset.read_index(fields,
964 'future_display_name': true
966 }).then(function(r) {
967 self.trigger('load_record', r);
972 get_widgets: function() {
973 return _.filter(this.getChildren(), function(obj) {
974 return obj instanceof instance.web.form.FormWidget;
977 get_fields_values: function() {
979 var ids = this.get_selected_ids();
980 values["id"] = ids.length > 0 ? ids[0] : false;
981 _.each(this.fields, function(value_, key) {
982 values[key] = value_.get_value();
986 get_selected_ids: function() {
987 var id = this.dataset.ids[this.dataset.index];
988 return id ? [id] : [];
990 recursive_save: function() {
992 return $.when(this.save()).then(function(res) {
993 if (self.dataset.parent_view)
994 return self.dataset.parent_view.recursive_save();
997 recursive_reload: function() {
1000 if (self.dataset.parent_view)
1001 pre = self.dataset.parent_view.recursive_reload();
1002 return pre.then(function() {
1003 return self.reload();
1006 is_dirty: function() {
1007 return _.any(this.fields, function (value_) {
1008 return value_._dirty_flag;
1011 is_interactible_record: function() {
1012 var id = this.datarecord.id;
1014 if (this.options.not_interactible_on_create)
1016 } else if (typeof(id) === "string") {
1017 if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
1022 sidebar_eval_context: function () {
1023 return $.when(this.build_eval_context());
1025 open_defaults_dialog: function () {
1027 var display = function (field, value) {
1028 if (field instanceof instance.web.form.FieldSelection) {
1029 return _(field.values).find(function (option) {
1030 return option[0] === value;
1032 } else if (field instanceof instance.web.form.FieldMany2One) {
1033 return field.get_displayed();
1037 var fields = _.chain(this.fields)
1038 .map(function (field) {
1039 var value = field.get_value();
1040 // ignore fields which are empty, invisible, readonly, o2m
1043 || field.get('invisible')
1044 || field.get("readonly")
1045 || field.field.type === 'one2many'
1046 || field.field.type === 'many2many'
1047 || field.field.type === 'binary'
1048 || field.password) {
1054 string: field.string,
1056 displayed: display(field, value),
1060 .sortBy(function (field) { return field.string; })
1062 var conditions = _.chain(self.fields)
1063 .filter(function (field) { return field.field.change_default; })
1064 .map(function (field) {
1065 var value = field.get_value();
1068 string: field.string,
1070 displayed: display(field, value),
1075 var d = new instance.web.Dialog(this, {
1076 title: _t("Set Default"),
1079 conditions: conditions
1082 {text: _t("Close"), click: function () { d.close(); }},
1083 {text: _t("Save default"), click: function () {
1084 var $defaults = d.$el.find('#formview_default_fields');
1085 var field_to_set = $defaults.val();
1086 if (!field_to_set) {
1087 $defaults.parent().addClass('oe_form_invalid');
1090 var condition = d.$el.find('#formview_default_conditions').val(),
1091 all_users = d.$el.find('#formview_default_all').is(':checked');
1092 new instance.web.DataSet(self, 'ir.values').call(
1096 self.fields[field_to_set].get_value(),
1100 ]).done(function () { d.close(); });
1104 d.template = 'FormView.set_default';
1107 register_field: function(field, name) {
1108 this.fields[name] = field;
1109 this.fields_order.push(name);
1110 if (JSON.parse(field.node.attrs.default_focus || "0")) {
1111 this.default_focus_field = field;
1114 field.on('focused', null, this.proxy('widgetFocused'))
1115 .on('blurred', null, this.proxy('widgetBlurred'));
1116 if (this.get_field_desc(name).translate) {
1117 this.translatable_fields.push(field);
1119 field.on('changed_value', this, function() {
1120 if (field.is_syntax_valid()) {
1121 this.trigger('field_changed:' + name);
1123 if (field._inhibit_on_change_flag) {
1126 field._dirty_flag = true;
1127 if (field.is_syntax_valid()) {
1128 this.do_onchange(field);
1129 this.on_form_changed(true);
1130 this.do_notify_change();
1134 get_field_desc: function(field_name) {
1135 return this.fields_view.fields[field_name];
1137 get_field_value: function(field_name) {
1138 return this.fields[field_name].get_value();
1140 compute_domain: function(expression) {
1141 return instance.web.form.compute_domain(expression, this.fields);
1143 _build_view_fields_values: function() {
1144 var a_dataset = this.dataset;
1145 var fields_values = this.get_fields_values();
1146 var active_id = a_dataset.ids[a_dataset.index];
1147 _.extend(fields_values, {
1148 active_id: active_id || false,
1149 active_ids: active_id ? [active_id] : [],
1150 active_model: a_dataset.model,
1153 if (a_dataset.parent_view) {
1154 fields_values.parent = a_dataset.parent_view.get_fields_values();
1156 return fields_values;
1158 build_eval_context: function() {
1159 var a_dataset = this.dataset;
1160 return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
1165 * Interface to be implemented by rendering engines for the form view.
1167 instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
1168 set_fields_view: function(fields_view) {},
1169 set_fields_registry: function(fields_registry) {},
1170 render_to: function($el) {},
1174 * Default rendering engine for the form view.
1176 * It is necessary to set the view using set_view() before usage.
1178 instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
1179 init: function(view) {
1182 set_fields_view: function(fvg) {
1184 this.version = parseFloat(this.fvg.arch.attrs.version);
1185 if (isNaN(this.version)) {
1189 set_tags_registry: function(tags_registry) {
1190 this.tags_registry = tags_registry;
1192 set_fields_registry: function(fields_registry) {
1193 this.fields_registry = fields_registry;
1195 set_widgets_registry: function(widgets_registry) {
1196 this.widgets_registry = widgets_registry;
1198 // Backward compatibility tools, current default version: v6.1
1199 process_version: function() {
1200 if (this.version < 7.0) {
1201 this.$form.find('form:first').wrapInner('<group col="4"/>');
1202 this.$form.find('page').each(function() {
1203 if (!$(this).parents('field').length) {
1204 $(this).wrapInner('<group col="4"/>');
1209 get_arch_fragment: function() {
1210 var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
1211 // IE won't allow custom button@type and will revert it to spec default : 'submit'
1212 $('button', doc).each(function() {
1213 $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
1215 // IE's html parser is also a css parser. How convenient...
1216 $('board', doc).each(function() {
1217 $(this).attr('layout', $(this).attr('style'));
1219 return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
1221 render_to: function($target) {
1223 this.$target = $target;
1225 this.$form = this.get_arch_fragment();
1227 this.process_version();
1229 this.fields_to_init = [];
1230 this.tags_to_init = [];
1231 this.widgets_to_init = [];
1233 this.process(this.$form);
1235 this.$form.appendTo(this.$target);
1237 this.to_replace = [];
1239 _.each(this.fields_to_init, function($elem) {
1240 var name = $elem.attr("name");
1241 if (!self.fvg.fields[name]) {
1242 throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
1244 var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
1246 throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
1248 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1249 var $label = self.labels[$elem.attr("name")];
1251 w.set_input_id($label.attr("for"));
1253 self.alter_field(w);
1254 self.view.register_field(w, $elem.attr("name"));
1255 self.to_replace.push([w, $elem]);
1257 _.each(this.tags_to_init, function($elem) {
1258 var tag_name = $elem[0].tagName.toLowerCase();
1259 var obj = self.tags_registry.get_object(tag_name);
1260 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1261 self.to_replace.push([w, $elem]);
1263 _.each(this.widgets_to_init, function($elem) {
1264 var widget_type = $elem.attr("type");
1265 var obj = self.widgets_registry.get_object(widget_type);
1266 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1267 self.to_replace.push([w, $elem]);
1270 init_fields: function() {
1272 _.each(this.to_replace, function(el) {
1273 defs.push(el[0].replace(el[1]));
1275 this.to_replace = [];
1276 return $.when.apply($, defs);
1278 render_element: function(template /* dictionaries */) {
1279 var dicts = [].slice.call(arguments).slice(1);
1280 var dict = _.extend.apply(_, dicts);
1281 dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
1282 return $(QWeb.render(template, dict));
1284 alter_field: function(field) {
1286 toggle_layout_debugging: function() {
1287 if (!this.$target.has('.oe_layout_debug_cell:first').length) {
1288 this.$target.find('[title]').removeAttr('title');
1289 this.$target.find('.oe_form_group_cell').each(function() {
1290 var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
1291 $(this).attr('title', text);
1294 this.$target.toggleClass('oe_layout_debugging');
1296 process: function($tag) {
1298 var tagname = $tag[0].nodeName.toLowerCase();
1299 if (this.tags_registry.contains(tagname)) {
1300 this.tags_to_init.push($tag);
1303 var fn = self['process_' + tagname];
1305 var args = [].slice.call(arguments);
1307 return fn.apply(self, args);
1309 // generic tag handling, just process children
1310 $tag.children().each(function() {
1311 self.process($(this));
1313 self.handle_common_properties($tag, $tag);
1314 $tag.removeAttr("modifiers");
1318 process_widget: function($widget) {
1319 this.widgets_to_init.push($widget);
1322 process_sheet: function($sheet) {
1323 var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
1324 this.handle_common_properties($new_sheet, $sheet);
1325 var $dst = $new_sheet.find('.oe_form_sheet');
1326 $sheet.contents().appendTo($dst);
1327 $sheet.before($new_sheet).remove();
1328 this.process($new_sheet);
1330 process_form: function($form) {
1331 if ($form.find('> sheet').length === 0) {
1332 $form.addClass('oe_form_nosheet');
1334 var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
1335 this.handle_common_properties($new_form, $form);
1336 $form.contents().appendTo($new_form);
1337 if ($form[0] === this.$form[0]) {
1338 // If root element, replace it
1339 this.$form = $new_form;
1341 $form.before($new_form).remove();
1343 this.process($new_form);
1346 * Used by direct <field> children of a <group> tag only
1347 * This method will add the implicit <label...> for every field
1350 preprocess_field: function($field) {
1352 var name = $field.attr('name'),
1353 field_colspan = parseInt($field.attr('colspan'), 10),
1354 field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
1356 if ($field.attr('nolabel') === '1')
1358 $field.attr('nolabel', '1');
1360 this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
1361 $(el).parents().each(function(unused, tag) {
1362 var name = tag.tagName.toLowerCase();
1363 if (name === "field" || name in self.tags_registry.map)
1370 var $label = $('<label/>').attr({
1372 "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
1373 "string": $field.attr('string'),
1374 "help": $field.attr('help'),
1375 "class": $field.attr('class'),
1377 $label.insertBefore($field);
1378 if (field_colspan > 1) {
1379 $field.attr('colspan', field_colspan - 1);
1383 process_field: function($field) {
1384 if ($field.parent().is('group')) {
1385 // No implicit labels for normal fields, only for <group> direct children
1386 var $label = this.preprocess_field($field);
1388 this.process($label);
1391 this.fields_to_init.push($field);
1394 process_group: function($group) {
1396 $group.children('field').each(function() {
1397 self.preprocess_field($(this));
1399 var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
1401 if ($new_group.first().is('table.oe_form_group')) {
1402 $table = $new_group;
1403 } else if ($new_group.filter('table.oe_form_group').length) {
1404 $table = $new_group.filter('table.oe_form_group').first();
1406 $table = $new_group.find('table.oe_form_group').first();
1410 cols = parseInt($group.attr('col') || 2, 10),
1414 $group.children().each(function(a,b,c) {
1415 var $child = $(this);
1416 var colspan = parseInt($child.attr('colspan') || 1, 10);
1417 var tagName = $child[0].tagName.toLowerCase();
1418 var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
1419 var newline = tagName === 'newline';
1421 // Note FME: those classes are used in layout debug mode
1422 if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
1423 $tr.addClass('oe_form_group_row_incomplete');
1425 $tr.addClass('oe_form_group_row_newline');
1432 if (!$tr || row_cols < colspan) {
1433 $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
1435 } else if (tagName==='group') {
1436 // When <group> <group/><group/> </group>, we need a spacing between the two groups
1437 $td.addClass('oe_group_right')
1439 row_cols -= colspan;
1441 // invisibility transfer
1442 var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
1443 var invisible = field_modifiers.invisible;
1444 self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
1446 $tr.append($td.append($child));
1447 children.push($child[0]);
1449 if (row_cols && $td) {
1450 $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
1452 $group.before($new_group).remove();
1454 $table.find('> tbody > tr').each(function() {
1455 var to_compute = [],
1458 $(this).children().each(function() {
1460 $child = $td.children(':first');
1461 if ($child.attr('cell-class')) {
1462 $td.addClass($child.attr('cell-class'));
1464 switch ($child[0].tagName.toLowerCase()) {
1468 if ($child.attr('for')) {
1469 $td.attr('width', '1%').addClass('oe_form_group_cell_label');
1470 row_cols-= $td.attr('colspan') || 1;
1475 var width = _.str.trim($child.attr('width') || ''),
1476 iwidth = parseInt(width, 10);
1478 if (width.substr(-1) === '%') {
1480 width = iwidth + '%';
1483 $td.css('min-width', width + 'px');
1485 $td.attr('width', width);
1486 $child.removeAttr('width');
1487 row_cols-= $td.attr('colspan') || 1;
1489 to_compute.push($td);
1495 var unit = Math.floor(total / row_cols);
1496 if (!$(this).is('.oe_form_group_row_incomplete')) {
1497 _.each(to_compute, function($td, i) {
1498 var width = parseInt($td.attr('colspan'), 10) * unit;
1499 $td.attr('width', width + '%');
1505 _.each(children, function(el) {
1506 self.process($(el));
1508 this.handle_common_properties($new_group, $group);
1511 process_notebook: function($notebook) {
1514 $notebook.find('> page').each(function() {
1515 var $page = $(this);
1516 var page_attrs = $page.getAttributes();
1517 page_attrs.id = _.uniqueId('notebook_page_');
1518 var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
1519 $page.contents().appendTo($new_page);
1520 $page.before($new_page).remove();
1521 var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
1522 page_attrs.__page = $new_page;
1523 page_attrs.__ic = ic;
1524 pages.push(page_attrs);
1526 $new_page.children().each(function() {
1527 self.process($(this));
1530 var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
1531 $notebook.contents().appendTo($new_notebook);
1532 $notebook.before($new_notebook).remove();
1533 self.process($($new_notebook.children()[0]));
1534 //tabs and invisibility handling
1535 $new_notebook.tabs();
1536 _.each(pages, function(page, i) {
1539 page.__ic.on("change:effective_invisible", null, function() {
1540 if (!page.__ic.get('effective_invisible') && page.autofocus) {
1541 $new_notebook.tabs('select', i);
1544 var current = $new_notebook.tabs("option", "selected");
1545 if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
1547 var first_visible = _.find(_.range(pages.length), function(i2) {
1548 return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
1550 if (first_visible !== undefined) {
1551 $new_notebook.tabs('select', first_visible);
1556 this.handle_common_properties($new_notebook, $notebook);
1557 return $new_notebook;
1559 process_separator: function($separator) {
1560 var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
1561 $separator.before($new_separator).remove();
1562 this.handle_common_properties($new_separator, $separator);
1563 return $new_separator;
1565 process_label: function($label) {
1566 var name = $label.attr("for"),
1567 field_orm = this.fvg.fields[name];
1569 string: $label.attr('string') || (field_orm || {}).string || '',
1570 help: $label.attr('help') || (field_orm || {}).help || '',
1571 _for: name ? _.uniqueId('oe-field-input-') : undefined,
1573 var align = parseFloat(dict.align);
1574 if (isNaN(align) || align === 1) {
1576 } else if (align === 0) {
1582 var $new_label = this.render_element('FormRenderingLabel', dict);
1583 $label.before($new_label).remove();
1584 this.handle_common_properties($new_label, $label);
1586 this.labels[name] = $new_label;
1590 handle_common_properties: function($new_element, $node) {
1591 var str_modifiers = $node.attr("modifiers") || "{}";
1592 var modifiers = JSON.parse(str_modifiers);
1594 if (modifiers.invisible !== undefined)
1595 ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
1596 $new_element.addClass($node.attr("class") || "");
1597 $new_element.attr('style', $node.attr('style'));
1598 return {invisibility_changer: ic,};
1605 If you read this documentation, it probably means that you were asked to use a form view widget outside of
1606 a form view. Before going further, you must understand that those fields were never really created for
1607 that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
1608 you to hack the system with more style.
1610 instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
1611 init: function(parent, eval_context) {
1612 this._super(parent);
1613 this.field_descs = {};
1614 this.eval_context = eval_context || {};
1616 display_invalid_fields: false,
1617 actual_mode: 'create',
1620 get_field_desc: function(field_name) {
1621 if (this.field_descs[field_name] === undefined) {
1622 this.field_descs[field_name] = {
1626 return this.field_descs[field_name];
1628 extend_field_desc: function(fields) {
1630 _.each(fields, function(v, k) {
1631 _.extend(self.get_field_desc(k), v);
1634 get_field_value: function(field_name) {
1637 set_values: function(values) {
1640 compute_domain: function(expression) {
1641 return instance.web.form.compute_domain(expression, {});
1643 build_eval_context: function() {
1644 return new instance.web.CompoundContext(this.eval_context);
1648 instance.web.form.compute_domain = function(expr, fields) {
1649 if (! (expr instanceof Array))
1652 for (var i = expr.length - 1; i >= 0; i--) {
1654 if (ex.length == 1) {
1655 var top = stack.pop();
1658 stack.push(stack.pop() || top);
1661 stack.push(stack.pop() && top);
1667 throw new Error(_.str.sprintf(
1668 _t("Unknown operator %s in domain %s"),
1669 ex, JSON.stringify(expr)));
1673 var field = fields[ex[0]];
1675 throw new Error(_.str.sprintf(
1676 _t("Unknown field %s in domain %s"),
1677 ex[0], JSON.stringify(expr)));
1679 var field_value = field.get_value ? field.get_value() : field.value;
1683 switch (op.toLowerCase()) {
1686 stack.push(_.isEqual(field_value, val));
1690 stack.push(!_.isEqual(field_value, val));
1693 stack.push(field_value < val);
1696 stack.push(field_value > val);
1699 stack.push(field_value <= val);
1702 stack.push(field_value >= val);
1705 if (!_.isArray(val)) val = [val];
1706 stack.push(_(val).contains(field_value));
1709 if (!_.isArray(val)) val = [val];
1710 stack.push(!_(val).contains(field_value));
1714 _t("Unsupported operator %s in domain %s"),
1715 op, JSON.stringify(expr));
1718 return _.all(stack, _.identity);
1721 instance.web.form.is_bin_size = function(v) {
1722 return /^\d+(\.\d*)? \w+$/.test(v);
1726 * Must be applied over an class already possessing the PropertiesMixin.
1728 * Apply the result of the "invisible" domain to this.$el.
1730 instance.web.form.InvisibilityChangerMixin = {
1731 init: function(field_manager, invisible_domain) {
1733 this._ic_field_manager = field_manager;
1734 this._ic_invisible_modifier = invisible_domain;
1735 this._ic_field_manager.on("view_content_has_changed", this, function() {
1736 var result = self._ic_invisible_modifier === undefined ? false :
1737 self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
1738 self.set({"invisible": result});
1740 this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
1741 var check = function() {
1742 if (self.get("invisible") || self.get('force_invisible')) {
1743 self.set({"effective_invisible": true});
1745 self.set({"effective_invisible": false});
1748 this.on('change:invisible', this, check);
1749 this.on('change:force_invisible', this, check);
1753 this.on("change:effective_invisible", this, this._check_visibility);
1754 this._check_visibility();
1756 _check_visibility: function() {
1757 this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
1761 instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
1762 init: function(parent, field_manager, invisible_domain, $el) {
1763 this.setParent(parent);
1764 instance.web.PropertiesMixin.init.call(this);
1765 instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
1772 Base class for all fields, custom widgets and buttons to be displayed in the form view.
1775 - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
1776 the values of the "readonly" property and the "mode" property on the field manager.
1778 instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
1780 * @constructs instance.web.form.FormWidget
1781 * @extends instance.web.Widget
1783 * @param field_manager
1786 init: function(field_manager, node) {
1787 this._super(field_manager);
1788 this.field_manager = field_manager;
1789 if (this.field_manager instanceof instance.web.FormView)
1790 this.view = this.field_manager;
1792 this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
1793 instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
1795 this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
1801 // some events to make the property "effective_readonly" sync automatically with "readonly" and
1802 // "mode" on field_manager
1804 var test_effective_readonly = function() {
1805 self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
1807 this.on("change:readonly", this, test_effective_readonly);
1808 this.field_manager.on("change:actual_mode", this, test_effective_readonly);
1809 test_effective_readonly.call(this);
1811 renderElement: function() {
1812 this.process_modifiers();
1814 this.$el.addClass(this.node.attrs["class"] || "");
1816 destroy: function() {
1818 this._super.apply(this, arguments);
1821 * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
1823 * This method is an utility method that is meant to be called by child classes.
1825 * @param {jQuery} $e jQuery object of elements to bind focus/blur on
1827 setupFocus: function ($e) {
1830 focus: function () { self.trigger('focused'); },
1831 blur: function () { self.trigger('blurred'); }
1834 process_modifiers: function() {
1836 for (var a in this.modifiers) {
1837 if (!this.modifiers.hasOwnProperty(a)) { continue; }
1838 if (!_.include(["invisible"], a)) {
1839 var val = this.field_manager.compute_domain(this.modifiers[a]);
1845 do_attach_tooltip: function(widget, trigger, options) {
1846 widget = widget || this;
1847 trigger = trigger || this.$el;
1848 options = _.extend({
1853 var template = widget.template + '.tooltip';
1854 if (!QWeb.has_template(template)) {
1855 template = 'WidgetLabel.tooltip';
1857 return QWeb.render(template, {
1858 debug: instance.session.debug,
1861 gravity: $.fn.tipsy.autoBounds(50, 'nw'),
1866 $(trigger).tipsy(options);
1869 * Builds a new context usable for operations related to fields by merging
1870 * the fields'context with the action's context.
1872 build_context: function() {
1873 // only use the model's context if there is not context on the node
1874 var v_context = this.node.attrs.context;
1876 v_context = (this.field || {}).context || {};
1879 if (v_context.__ref || true) { //TODO: remove true
1880 var fields_values = this.field_manager.build_eval_context();
1881 v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
1885 build_domain: function() {
1886 var f_domain = this.field.domain || [];
1887 var n_domain = this.node.attrs.domain || null;
1888 // if there is a domain on the node, overrides the model's domain
1889 var final_domain = n_domain !== null ? n_domain : f_domain;
1890 if (!(final_domain instanceof Array) || true) { //TODO: remove true
1891 var fields_values = this.field_manager.build_eval_context();
1892 final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
1894 return final_domain;
1898 instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
1899 template: 'WidgetButton',
1900 init: function(field_manager, node) {
1901 node.attrs.type = node.attrs['data-button-type'];
1902 this._super(field_manager, node);
1903 this.force_disabled = false;
1904 this.string = (this.node.attrs.string || '').replace(/_/g, '');
1905 if (JSON.parse(this.node.attrs.default_focus || "0")) {
1906 // TODO fme: provide enter key binding to widgets
1907 this.view.default_focus_button = this;
1909 if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
1910 this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
1914 this._super.apply(this, arguments);
1915 this.view.on('view_content_has_changed', this, this.check_disable);
1916 this.check_disable();
1917 this.$el.click(this.on_click);
1918 if (this.node.attrs.help || instance.session.debug) {
1919 this.do_attach_tooltip();
1921 this.setupFocus(this.$el);
1923 on_click: function() {
1925 this.force_disabled = true;
1926 this.check_disable();
1927 this.execute_action().always(function() {
1928 self.force_disabled = false;
1929 self.check_disable();
1932 execute_action: function() {
1934 var exec_action = function() {
1935 if (self.node.attrs.confirm) {
1936 var def = $.Deferred();
1937 var dialog = instance.web.dialog($('<div/>').text(self.node.attrs.confirm), {
1938 title: _t('Confirm'),
1941 {text: _t("Cancel"), click: function() {
1942 $(this).dialog("close");
1945 {text: _t("Ok"), click: function() {
1947 self.on_confirmed().always(function() {
1948 $(self2).dialog("close");
1953 beforeClose: function() {
1957 return def.promise();
1959 return self.on_confirmed();
1962 if (!this.node.attrs.special) {
1963 return this.view.recursive_save().then(exec_action);
1965 return exec_action();
1968 on_confirmed: function() {
1971 var context = this.build_context();
1973 return this.view.do_execute_action(
1974 _.extend({}, this.node.attrs, {context: context}),
1975 this.view.dataset, this.view.datarecord.id, function () {
1976 self.view.recursive_reload();
1979 check_disable: function() {
1980 var disabled = (this.force_disabled || !this.view.is_interactible_record());
1981 this.$el.prop('disabled', disabled);
1982 this.$el.css('color', disabled ? 'grey' : '');
1987 * Interface to be implemented by fields.
1990 * - changed_value: triggered when the value of the field has changed. This can be due
1991 * to a user interaction or a call to set_value().
1994 instance.web.form.FieldInterface = {
1996 * Constructor takes 2 arguments:
1997 * - field_manager: Implements FieldManagerMixin
1998 * - node: the "<field>" node in json form
2000 init: function(field_manager, node) {},
2002 * Called by the form view to indicate the value of the field.
2004 * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
2005 * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
2006 * before the widget is inserted into the DOM.
2008 * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
2009 * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
2010 * also be able to handle any other format commonly used in the _defaults key on the models in the addons
2011 * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
2012 * no information is ever given to know which format is used.
2014 set_value: function(value_) {},
2016 * Get the current value of the widget.
2018 * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
2019 * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
2020 * For example if the field is marked as "required", a call to get_value() can return false.
2022 * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
2023 * return a default value according to the type of field.
2025 * This method is always assumed to perform synchronously, it can not return a promise.
2027 * If there was no user interaction to modify the value of the field, it is always assumed that
2028 * get_value() return the same semantic value than the one passed in the last call to set_value(),
2029 * although the syntax can be different. This can be the case for type of fields that have a different
2030 * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
2032 get_value: function() {},
2034 * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
2037 set_input_id: function(id) {},
2039 * Returns true if is_syntax_valid() returns true and the value is semantically
2040 * valid too according to the semantic restrictions applied to the field.
2042 is_valid: function() {},
2044 * Returns true if the field holds a value which is syntactically correct, ignoring
2045 * the potential semantic restrictions applied to the field.
2047 is_syntax_valid: function() {},
2049 * Must set the focus on the field. Return false if field is not focusable.
2051 focus: function() {},
2053 * Called when the translate button is clicked.
2055 on_translate: function() {},
2057 This method is called by the form view before reading on_change values and before saving. It tells
2058 the field to save its value before reading it using get_value(). Must return a promise.
2060 commit_value: function() {},
2064 * Abstract class for classes implementing FieldInterface.
2067 * - value: useful property to hold the value of the field. By default, set_value() and get_value()
2068 * set and retrieve the value property. Changing the value property also triggers automatically
2069 * a 'changed_value' event that inform the view to trigger on_changes.
2072 instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
2074 * @constructs instance.web.form.AbstractField
2075 * @extends instance.web.form.FormWidget
2077 * @param field_manager
2080 init: function(field_manager, node) {
2082 this._super(field_manager, node);
2083 this.name = this.node.attrs.name;
2084 this.field = this.field_manager.get_field_desc(this.name);
2085 this.widget = this.node.attrs.widget;
2086 this.string = this.node.attrs.string || this.field.string || this.name;
2087 this.options = instance.web.py_eval(this.node.attrs.options || '{}');
2088 this.set({'value': false});
2090 this.on("change:value", this, function() {
2091 this.trigger('changed_value');
2092 this._check_css_flags();
2095 renderElement: function() {
2098 if (this.field.translate && this.view) {
2099 this.$el.addClass('oe_form_field_translatable');
2100 this.$el.find('.oe_field_translate').click(this.on_translate);
2102 this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
2103 if (instance.session.debug) {
2104 this.do_attach_tooltip(this, this.$label[0] || this.$el);
2105 this.$label.off('dblclick').on('dblclick', function() {
2106 console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
2108 console.log("window.w =", window.w);
2111 if (!this.disable_utility_classes) {
2112 this.off("change:required", this, this._set_required);
2113 this.on("change:required", this, this._set_required);
2114 this._set_required();
2116 this._check_visibility();
2117 this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
2118 this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
2119 this._check_css_flags();
2122 var tmp = this._super();
2123 this.on("change:value", this, function() {
2124 if (! this.no_rerender)
2125 this.render_value();
2127 this.render_value();
2130 * Private. Do not use.
2132 _set_required: function() {
2133 this.$el.toggleClass('oe_form_required', this.get("required"));
2135 set_value: function(value_) {
2136 this.set({'value': value_});
2138 get_value: function() {
2139 return this.get('value');
2142 Utility method that all implementations should use to change the
2143 value without triggering a re-rendering.
2145 internal_set_value: function(value_) {
2146 var tmp = this.no_rerender;
2147 this.no_rerender = true;
2148 this.set({'value': value_});
2149 this.no_rerender = tmp;
2152 This method is called each time the value is modified.
2154 render_value: function() {},
2155 is_valid: function() {
2156 return this.is_syntax_valid() && !(this.get('required') && this.is_false());
2158 is_syntax_valid: function() {
2162 * Method useful to implement to ease validity testing. Must return true if the current
2163 * value is similar to false in OpenERP.
2165 is_false: function() {
2166 return this.get('value') === false;
2168 _check_css_flags: function() {
2169 if (this.field.translate) {
2170 this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
2172 if (!this.disable_utility_classes) {
2173 if (this.field_manager.get('display_invalid_fields')) {
2174 this.$el.toggleClass('oe_form_invalid', !this.is_valid());
2181 set_input_id: function(id) {
2182 this.id_for_label = id;
2184 on_translate: function() {
2186 var trans = new instance.web.DataSet(this, 'ir.translation');
2187 return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
2192 set_dimensions: function (height, width) {
2198 commit_value: function() {
2204 * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
2207 instance.web.form.ReinitializeWidgetMixin = {
2209 * Default implementation of, you should not override it, use initialize_field() instead.
2212 this.initialize_field();
2215 initialize_field: function() {
2216 this.on("change:effective_readonly", this, this.reinitialize);
2217 this.initialize_content();
2219 reinitialize: function() {
2220 this.destroy_content();
2221 this.renderElement();
2222 this.initialize_content();
2225 * Called to destroy anything that could have been created previously, called before a
2226 * re-initialization.
2228 destroy_content: function() {},
2230 * Called to initialize the content.
2232 initialize_content: function() {},
2236 * A mixin to apply on any field that has to completely re-render when its readonly state
2239 instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
2240 reinitialize: function() {
2241 instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
2242 this.render_value();
2247 Some hack to make placeholders work in ie9.
2249 if ($.browser.msie && $.browser.version === "9.0") {
2250 document.addEventListener("DOMNodeInserted",function(event){
2251 var nodename = event.target.nodeName.toLowerCase();
2252 if ( nodename === "input" || nodename == "textarea" ) {
2253 $(event.target).placeholder();
2258 instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2259 template: 'FieldChar',
2260 widget_class: 'oe_form_field_char',
2262 'change input': 'store_dom_value',
2264 init: function (field_manager, node) {
2265 this._super(field_manager, node);
2266 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2268 initialize_content: function() {
2269 this.setupFocus(this.$('input'));
2271 store_dom_value: function () {
2272 if (!this.get('effective_readonly')
2273 && this.$('input').length
2274 && this.is_syntax_valid()) {
2275 this.internal_set_value(
2277 this.$('input').val()));
2280 commit_value: function () {
2281 this.store_dom_value();
2282 return this._super();
2284 render_value: function() {
2285 var show_value = this.format_value(this.get('value'), '');
2286 if (!this.get("effective_readonly")) {
2287 this.$el.find('input').val(show_value);
2289 if (this.password) {
2290 show_value = new Array(show_value.length + 1).join('*');
2292 this.$(".oe_form_char_content").text(show_value);
2295 is_syntax_valid: function() {
2296 if (!this.get("effective_readonly") && this.$("input").size() > 0) {
2298 this.parse_value(this.$('input').val(), '');
2306 parse_value: function(val, def) {
2307 return instance.web.parse_value(val, this, def);
2309 format_value: function(val, def) {
2310 return instance.web.format_value(val, this, def);
2312 is_false: function() {
2313 return this.get('value') === '' || this._super();
2316 var input = this.$('input:first')[0];
2317 return input ? input.focus() : false;
2319 set_dimensions: function (height, width) {
2320 this._super(height, width);
2321 this.$('input').css({
2328 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2329 process_modifiers: function () {
2331 this.set({ readonly: true });
2335 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2336 template: 'FieldEmail',
2337 initialize_content: function() {
2339 var $button = this.$el.find('button');
2340 $button.click(this.on_button_clicked);
2341 this.setupFocus($button);
2343 render_value: function() {
2344 if (!this.get("effective_readonly")) {
2348 .attr('href', 'mailto:' + this.get('value'))
2349 .text(this.get('value') || '');
2352 on_button_clicked: function() {
2353 if (!this.get('value') || !this.is_syntax_valid()) {
2354 this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2356 location.href = 'mailto:' + this.get('value');
2361 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2362 template: 'FieldUrl',
2363 initialize_content: function() {
2365 var $button = this.$el.find('button');
2366 $button.click(this.on_button_clicked);
2367 this.setupFocus($button);
2369 render_value: function() {
2370 if (!this.get("effective_readonly")) {
2373 var tmp = this.get('value');
2374 var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2376 tmp = "http://" + this.get('value');
2378 var text = this.get('value') ? this.node.attrs.text || tmp : '';
2379 this.$el.find('a').attr('href', tmp).text(text);
2382 on_button_clicked: function() {
2383 if (!this.get('value')) {
2384 this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2386 var url = $.trim(this.get('value'));
2387 if(/^www\./i.test(url))
2388 url = 'http://'+url;
2394 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2395 is_field_number: true,
2396 widget_class: 'oe_form_field_float',
2397 init: function (field_manager, node) {
2398 this._super(field_manager, node);
2399 this.internal_set_value(0);
2400 if (this.node.attrs.digits) {
2401 this.digits = this.node.attrs.digits;
2403 this.digits = this.field.digits;
2406 set_value: function(value_) {
2407 if (value_ === false || value_ === undefined) {
2408 // As in GTK client, floats default to 0
2411 this._super.apply(this, [value_]);
2413 focus: function () {
2414 var $input = this.$('input:first');
2415 return $input.length ? $input.select() : false;
2419 instance.web.DateTimeWidget = instance.web.Widget.extend({
2420 template: "web.datepicker",
2421 jqueryui_object: 'datetimepicker',
2422 type_of_date: "datetime",
2424 'change .oe_datepicker_master': 'change_datetime',
2425 'keypress .oe_datepicker_master': 'change_datetime',
2427 init: function(parent) {
2428 this._super(parent);
2429 this.name = parent.name;
2433 this.$input = this.$el.find('input.oe_datepicker_master');
2434 this.$input_picker = this.$el.find('input.oe_datepicker_container');
2436 $.datepicker.setDefaults({
2437 clearText: _t('Clear'),
2438 clearStatus: _t('Erase the current date'),
2439 closeText: _t('Done'),
2440 closeStatus: _t('Close without change'),
2441 prevText: _t('<Prev'),
2442 prevStatus: _t('Show the previous month'),
2443 nextText: _t('Next>'),
2444 nextStatus: _t('Show the next month'),
2445 currentText: _t('Today'),
2446 currentStatus: _t('Show the current month'),
2447 monthNames: Date.CultureInfo.monthNames,
2448 monthNamesShort: Date.CultureInfo.abbreviatedMonthNames,
2449 monthStatus: _t('Show a different month'),
2450 yearStatus: _t('Show a different year'),
2451 weekHeader: _t('Wk'),
2452 weekStatus: _t('Week of the year'),
2453 dayNames: Date.CultureInfo.dayNames,
2454 dayNamesShort: Date.CultureInfo.abbreviatedDayNames,
2455 dayNamesMin: Date.CultureInfo.shortestDayNames,
2456 dayStatus: _t('Set DD as first week day'),
2457 dateStatus: _t('Select D, M d'),
2458 firstDay: Date.CultureInfo.firstDayOfWeek,
2459 initStatus: _t('Select a date'),
2462 $.timepicker.setDefaults({
2463 timeOnlyTitle: _t('Choose Time'),
2464 timeText: _t('Time'),
2465 hourText: _t('Hour'),
2466 minuteText: _t('Minute'),
2467 secondText: _t('Second'),
2468 currentText: _t('Now'),
2469 closeText: _t('Done')
2473 onClose: this.on_picker_select,
2474 onSelect: this.on_picker_select,
2478 showButtonPanel: true,
2479 firstDay: Date.CultureInfo.firstDayOfWeek
2481 // Some clicks in the datepicker dialog are not stopped by the
2482 // datepicker and "bubble through", unexpectedly triggering the bus's
2483 // click event. Prevent that.
2484 this.picker('widget').click(function (e) { e.stopPropagation(); });
2486 this.$el.find('img.oe_datepicker_trigger').click(function() {
2487 if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
2488 self.$input.focus();
2491 self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date());
2492 self.$input_picker.show();
2493 self.picker('show');
2494 self.$input_picker.hide();
2496 this.set_readonly(false);
2497 this.set({'value': false});
2499 picker: function() {
2500 return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
2502 on_picker_select: function(text, instance_) {
2503 var date = this.picker('getDate');
2505 .val(date ? this.format_client(date) : '')
2509 set_value: function(value_) {
2510 this.set({'value': value_});
2511 this.$input.val(value_ ? this.format_client(value_) : '');
2513 get_value: function() {
2514 return this.get('value');
2516 set_value_from_ui_: function() {
2517 var value_ = this.$input.val() || false;
2518 this.set({'value': this.parse_client(value_)});
2520 set_readonly: function(readonly) {
2521 this.readonly = readonly;
2522 this.$input.prop('readonly', this.readonly);
2523 this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
2525 is_valid_: function() {
2526 var value_ = this.$input.val();
2527 if (value_ === "") {
2531 this.parse_client(value_);
2538 parse_client: function(v) {
2539 return instance.web.parse_value(v, {"widget": this.type_of_date});
2541 format_client: function(v) {
2542 return instance.web.format_value(v, {"widget": this.type_of_date});
2544 change_datetime: function(e) {
2545 if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
2546 this.set_value_from_ui_();
2547 this.trigger("datetime_changed");
2550 commit_value: function () {
2551 this.change_datetime();
2555 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2556 jqueryui_object: 'datepicker',
2557 type_of_date: "date"
2560 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2561 template: "FieldDatetime",
2562 build_widget: function() {
2563 return new instance.web.DateTimeWidget(this);
2565 destroy_content: function() {
2566 if (this.datewidget) {
2567 this.datewidget.destroy();
2568 this.datewidget = undefined;
2571 initialize_content: function() {
2572 if (!this.get("effective_readonly")) {
2573 this.datewidget = this.build_widget();
2574 this.datewidget.on('datetime_changed', this, _.bind(function() {
2575 this.internal_set_value(this.datewidget.get_value());
2577 this.datewidget.appendTo(this.$el);
2578 this.setupFocus(this.datewidget.$input);
2581 render_value: function() {
2582 if (!this.get("effective_readonly")) {
2583 this.datewidget.set_value(this.get('value'));
2585 this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2588 is_syntax_valid: function() {
2589 if (!this.get("effective_readonly") && this.datewidget) {
2590 return this.datewidget.is_valid_();
2594 is_false: function() {
2595 return this.get('value') === '' || this._super();
2598 var input = this.datewidget && this.datewidget.$input[0];
2599 return input ? input.focus() : false;
2601 set_dimensions: function (height, width) {
2602 this._super(height, width);
2603 if (!this.get("effective_readonly")) {
2604 this.datewidget.$input.css('height', height);
2609 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2610 template: "FieldDate",
2611 build_widget: function() {
2612 return new instance.web.DateWidget(this);
2616 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2617 template: 'FieldText',
2619 'keyup': function (e) {
2620 if (e.which === $.ui.keyCode.ENTER) {
2621 e.stopPropagation();
2624 'keypress': function (e) {
2625 if (e.which === $.ui.keyCode.ENTER) {
2626 e.stopPropagation();
2629 'change textarea': 'store_dom_value',
2631 initialize_content: function() {
2633 if (! this.get("effective_readonly")) {
2634 this.$textarea = this.$el.find('textarea');
2635 this.auto_sized = false;
2636 this.default_height = this.$textarea.css('height');
2637 if (this.get("effective_readonly")) {
2638 this.$textarea.attr('disabled', 'disabled');
2640 this.setupFocus(this.$textarea);
2642 this.$textarea = undefined;
2645 commit_value: function () {
2646 if (! this.get("effective_readonly") && this.$textarea) {
2647 this.store_dom_value();
2649 return this._super();
2651 store_dom_value: function () {
2652 this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2654 render_value: function() {
2655 if (! this.get("effective_readonly")) {
2656 var show_value = instance.web.format_value(this.get('value'), this, '');
2657 if (show_value === '') {
2658 this.$textarea.css('height', parseInt(this.default_height)+"px");
2660 this.$textarea.val(show_value);
2661 if (! this.auto_sized) {
2662 this.auto_sized = true;
2663 this.$textarea.autosize();
2665 this.$textarea.trigger("autosize");
2668 var txt = this.get("value") || '';
2669 this.$(".oe_form_text_content").text(txt);
2672 is_syntax_valid: function() {
2673 if (!this.get("effective_readonly") && this.$textarea) {
2675 instance.web.parse_value(this.$textarea.val(), this, '');
2683 is_false: function() {
2684 return this.get('value') === '' || this._super();
2686 focus: function($el) {
2687 var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2688 return input ? input.focus() : false;
2690 set_dimensions: function (height, width) {
2691 this._super(height, width);
2692 if (!this.get("effective_readonly") && this.$textarea) {
2693 this.$textarea.css({
2702 * FieldTextHtml Widget
2703 * Intended for FieldText widgets meant to display HTML content. This
2704 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2705 * To find more information about CLEditor configutation: go to
2706 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2708 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2709 template: 'FieldTextHtml',
2711 this._super.apply(this, arguments);
2713 initialize_content: function() {
2715 if (! this.get("effective_readonly")) {
2716 self._updating_editor = false;
2717 this.$textarea = this.$el.find('textarea');
2718 var width = ((this.node.attrs || {}).editor_width || '100%');
2719 var height = ((this.node.attrs || {}).editor_height || 250);
2720 this.$textarea.cleditor({
2721 width: width, // width not including margins, borders or padding
2722 height: height, // height not including margins, borders or padding
2723 controls: // controls to add to the toolbar
2724 "bold italic underline strikethrough " +
2725 "| removeformat | bullets numbering | outdent " +
2726 "indent | link unlink | source",
2727 bodyStyle: // style to assign to document body contained within the editor
2728 "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2730 this.$cleditor = this.$textarea.cleditor()[0];
2731 this.$cleditor.change(function() {
2732 if (! self._updating_editor) {
2733 self.$cleditor.updateTextArea();
2734 self.internal_set_value(self.$textarea.val());
2737 if (this.field.translate) {
2738 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"/>')
2739 .click(this.on_translate);
2740 this.$cleditor.$toolbar.append($img);
2744 render_value: function() {
2745 if (! this.get("effective_readonly")) {
2746 this.$textarea.val(this.get('value') || '');
2747 this._updating_editor = true;
2748 this.$cleditor.updateFrame();
2749 this._updating_editor = false;
2751 this.$el.html(this.get('value'));
2756 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2757 template: 'FieldBoolean',
2760 this.$checkbox = $("input", this.$el);
2761 this.setupFocus(this.$checkbox);
2762 this.$el.click(_.bind(function() {
2763 this.internal_set_value(this.$checkbox.is(':checked'));
2765 var check_readonly = function() {
2766 self.$checkbox.prop('disabled', self.get("effective_readonly"));
2768 this.on("change:effective_readonly", this, check_readonly);
2769 check_readonly.call(this);
2770 this._super.apply(this, arguments);
2772 render_value: function() {
2773 this.$checkbox[0].checked = this.get('value');
2776 var input = this.$checkbox && this.$checkbox[0];
2777 return input ? input.focus() : false;
2782 The progressbar field expect a float from 0 to 100.
2784 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2785 template: 'FieldProgressBar',
2786 render_value: function() {
2787 this.$el.progressbar({
2788 value: this.get('value') || 0,
2789 disabled: this.get("effective_readonly")
2791 var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
2792 this.$('span').html(formatted_value + '%');
2797 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2798 template: 'FieldSelection',
2800 'change select': 'store_dom_value',
2802 init: function(field_manager, node) {
2804 this._super(field_manager, node);
2805 this.values = _(this.field.selection).chain()
2806 .reject(function (v) { return v[0] === false && v[1] === ''; })
2807 .unshift([false, ''])
2810 initialize_content: function() {
2811 // Flag indicating whether we're in an event chain containing a change
2812 // event on the select, in order to know what to do on keyup[RETURN]:
2813 // * If the user presses [RETURN] as part of changing the value of a
2814 // selection, we should just let the value change and not let the
2815 // event broadcast further (e.g. to validating the current state of
2816 // the form in editable list view, which would lead to saving the
2817 // current row or switching to the next one)
2818 // * If the user presses [RETURN] with a select closed (side-effect:
2819 // also if the user opened the select and pressed [RETURN] without
2820 // changing the selected value), takes the action as validating the
2822 var ischanging = false;
2823 var $select = this.$el.find('select')
2824 .change(function () { ischanging = true; })
2825 .click(function () { ischanging = false; })
2826 .keyup(function (e) {
2827 if (e.which !== 13 || !ischanging) { return; }
2828 e.stopPropagation();
2831 this.setupFocus($select);
2833 commit_value: function () {
2834 this.store_dom_value();
2835 return this._super();
2837 store_dom_value: function () {
2838 if (!this.get('effective_readonly') && this.$('select').length) {
2839 this.internal_set_value(
2840 this.values[this.$('select')[0].selectedIndex][0]);
2843 set_value: function(value_) {
2844 value_ = value_ === null ? false : value_;
2845 value_ = value_ instanceof Array ? value_[0] : value_;
2846 this._super(value_);
2848 render_value: function() {
2849 if (!this.get("effective_readonly")) {
2851 for (var i = 0, ii = this.values.length; i < ii; i++) {
2852 if (this.values[i][0] === this.get('value')) index = i;
2854 this.$el.find('select')[0].selectedIndex = index;
2857 var option = _(this.values)
2858 .detect(function (record) { return record[0] === self.get('value'); });
2859 this.$el.text(option ? option[1] : this.values[0][1]);
2863 var input = this.$('select:first')[0];
2864 return input ? input.focus() : false;
2866 set_dimensions: function (height, width) {
2867 this._super(height, width);
2868 this.$('select').css({
2875 // jquery autocomplete tweak to allow html and classnames
2877 var proto = $.ui.autocomplete.prototype,
2878 initSource = proto._initSource;
2880 function filter( array, term ) {
2881 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
2882 return $.grep( array, function(value_) {
2883 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
2888 _initSource: function() {
2889 if ( this.options.html && $.isArray(this.options.source) ) {
2890 this.source = function( request, response ) {
2891 response( filter( this.options.source, request.term ) );
2894 initSource.call( this );
2898 _renderItem: function( ul, item) {
2899 return $( "<li></li>" )
2900 .data( "item.autocomplete", item )
2901 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
2903 .addClass(item.classname);
2909 * A mixin containing some useful methods to handle completion inputs.
2911 instance.web.form.CompletionFieldMixin = {
2914 this.orderer = new instance.web.DropMisordered();
2917 * Call this method to search using a string.
2919 get_search_result: function(search_val) {
2922 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
2923 var blacklist = this.get_search_blacklist();
2924 this.last_query = search_val;
2926 return this.orderer.add(dataset.name_search(
2927 search_val, new instance.web.CompoundDomain(self.build_domain(), [["id", "not in", blacklist]]),
2928 'ilike', this.limit + 1, self.build_context())).then(function(data) {
2929 self.last_search = data;
2930 // possible selections for the m2o
2931 var values = _.map(data, function(x) {
2932 x[1] = x[1].split("\n")[0];
2934 label: _.str.escapeHTML(x[1]),
2941 // search more... if more results that max
2942 if (values.length > self.limit) {
2943 values = values.slice(0, self.limit);
2945 label: _t("Search More..."),
2946 action: function() {
2947 dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
2948 self._search_create_popup("search", data);
2951 classname: 'oe_m2o_dropdown_option'
2955 var raw_result = _(data.result).map(function(x) {return x[1];});
2956 if (search_val.length > 0 && !_.include(raw_result, search_val)) {
2958 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
2959 $('<span />').text(search_val).html()),
2960 action: function() {
2961 self._quick_create(search_val);
2963 classname: 'oe_m2o_dropdown_option'
2968 label: _t("Create and Edit..."),
2969 action: function() {
2970 self._search_create_popup("form", undefined, self._create_context(search_val));
2972 classname: 'oe_m2o_dropdown_option'
2978 get_search_blacklist: function() {
2981 _quick_create: function(name) {
2983 var slow_create = function () {
2984 self._search_create_popup("form", undefined, self._create_context(name));
2986 if (self.options.quick_create === undefined || self.options.quick_create) {
2987 new instance.web.DataSet(this, this.field.relation, self.build_context())
2988 .name_create(name).done(function(data) {
2989 if (!self.get('effective_readonly'))
2990 self.add_id(data[0]);
2991 }).fail(function(error, event) {
2992 event.preventDefault();
2998 // all search/create popup handling
2999 _search_create_popup: function(view, ids, context) {
3001 var pop = new instance.web.form.SelectCreatePopup(this);
3003 self.field.relation,
3005 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3006 initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
3008 disable_multiple_selection: true
3010 self.build_domain(),
3011 new instance.web.CompoundContext(self.build_context(), context || {})
3013 pop.on("elements_selected", self, function(element_ids) {
3014 self.add_id(element_ids[0]);
3021 add_id: function(id) {},
3022 _create_context: function(name) {
3024 var field = (this.options || {}).create_name_field;
3025 if (field === undefined)
3027 if (field !== false && name && (this.options || {}).quick_create !== false)
3028 tmp["default_" + field] = name;
3033 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3034 template: "M2ODialog",
3035 init: function(parent) {
3036 this._super(parent, {
3037 title: _.str.sprintf(_t("Add %s"), parent.string),
3043 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3044 this.$("input").val(this.getParent().last_query);
3045 this.$buttons.find(".oe_form_m2o_qc_button").click(function(){
3046 self.getParent()._quick_create(self.$("input").val());
3049 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3050 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3053 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3059 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3060 template: "FieldMany2One",
3062 'keydown input': function (e) {
3064 case $.ui.keyCode.UP:
3065 case $.ui.keyCode.DOWN:
3066 e.stopPropagation();
3070 init: function(field_manager, node) {
3071 this._super(field_manager, node);
3072 instance.web.form.CompletionFieldMixin.init.call(this);
3073 this.set({'value': false});
3074 this.display_value = {};
3075 this.last_search = [];
3076 this.floating = false;
3077 this.current_display = null;
3078 this.is_started = false;
3080 reinit_value: function(val) {
3081 this.internal_set_value(val);
3082 this.floating = false;
3083 if (this.is_started)
3084 this.render_value();
3086 initialize_field: function() {
3087 this.is_started = true;
3088 instance.web.bus.on('click', this, function() {
3089 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3090 this.$input.autocomplete("close");
3093 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3095 initialize_content: function() {
3096 if (!this.get("effective_readonly"))
3097 this.render_editable();
3099 destroy_content: function () {
3100 if (this.$drop_down) {
3101 this.$drop_down.off('click');
3102 delete this.$drop_down;
3105 this.$input.closest(".ui-dialog .ui-dialog-content").off('scroll');
3106 this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3107 'focus focusout change keydown');
3110 if (this.$follow_button) {
3111 this.$follow_button.off('blur focus click');
3112 delete this.$follow_button;
3115 destroy: function () {
3116 this.destroy_content();
3117 return this._super();
3119 init_error_displayer: function() {
3122 hide_error_displayer: function() {
3125 show_error_displayer: function() {
3126 new instance.web.form.M2ODialog(this).open();
3128 render_editable: function() {
3130 this.$input = this.$el.find("input");
3132 this.init_error_displayer();
3134 self.$input.on('focus', function() {
3135 self.hide_error_displayer();
3138 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3139 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3141 this.$follow_button.click(function(ev) {
3142 ev.preventDefault();
3143 if (!self.get('value')) {
3147 var pop = new instance.web.form.FormOpenPopup(self);
3149 self.field.relation,
3151 self.build_context(),
3153 title: _t("Open: ") + self.string
3156 pop.on('write_completed', self, function(){
3157 self.display_value = {};
3158 self.render_value();
3160 self.trigger('changed_value');
3164 // some behavior for input
3165 var input_changed = function() {
3166 if (self.current_display !== self.$input.val()) {
3167 self.current_display = self.$input.val();
3168 if (self.$input.val() === "") {
3169 self.internal_set_value(false);
3170 self.floating = false;
3172 self.floating = true;
3176 this.$input.keydown(input_changed);
3177 this.$input.change(input_changed);
3178 this.$drop_down.click(function() {
3179 if (self.$input.autocomplete("widget").is(":visible")) {
3180 self.$input.autocomplete("close");
3181 self.$input.focus();
3183 if (self.get("value") && ! self.floating) {
3184 self.$input.autocomplete("search", "");
3186 self.$input.autocomplete("search");
3191 // Autocomplete close on dialog content scroll
3192 var close_autocomplete = _.debounce(function() {
3193 if (self.$input.autocomplete("widget").is(":visible")) {
3194 self.$input.autocomplete("close");
3197 this.$input.closest(".ui-dialog .ui-dialog-content").on('scroll', this, close_autocomplete);
3199 self.ed_def = $.Deferred();
3200 self.uned_def = $.Deferred();
3202 var ed_duration = 15000;
3203 var anyoneLoosesFocus = function (e) {
3205 if (self.floating) {
3206 if (self.last_search.length > 0) {
3207 if (self.last_search[0][0] != self.get("value")) {
3208 self.display_value = {};
3209 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3210 self.reinit_value(self.last_search[0][0]);
3213 self.render_value();
3217 self.reinit_value(false);
3219 self.floating = false;
3221 if (used && self.get("value") === false && ! self.no_ed) {
3222 self.ed_def.reject();
3223 self.uned_def.reject();
3224 self.ed_def = $.Deferred();
3225 self.ed_def.done(function() {
3226 self.show_error_displayer();
3227 ignore_blur = false;
3228 self.trigger('focused');
3231 setTimeout(function() {
3232 self.ed_def.resolve();
3233 self.uned_def.reject();
3234 self.uned_def = $.Deferred();
3235 self.uned_def.done(function() {
3236 self.hide_error_displayer();
3238 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3242 self.ed_def.reject();
3245 var ignore_blur = false;
3247 focusout: anyoneLoosesFocus,
3248 focus: function () { self.trigger('focused'); },
3249 autocompleteopen: function () { ignore_blur = true; },
3250 autocompleteclose: function () { ignore_blur = false; },
3252 // autocomplete open
3253 if (ignore_blur) { return; }
3254 if (_(self.getChildren()).any(function (child) {
3255 return child instanceof instance.web.form.AbstractFormPopup;
3257 self.trigger('blurred');
3261 var isSelecting = false;
3263 this.$input.autocomplete({
3264 source: function(req, resp) {
3265 self.get_search_result(req.term).done(function(result) {
3269 select: function(event, ui) {
3273 self.display_value = {};
3274 self.display_value["" + item.id] = item.name;
3275 self.reinit_value(item.id);
3276 } else if (item.action) {
3278 // Cancel widget blurring, to avoid form blur event
3279 self.trigger('focused');
3283 focus: function(e, ui) {
3287 // disabled to solve a bug, but may cause others
3288 //close: anyoneLoosesFocus,
3292 this.$input.autocomplete("widget").openerpClass();
3293 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3294 this.$input.keyup(function(e) {
3295 if (e.which === 13) { // ENTER
3297 e.stopPropagation();
3299 isSelecting = false;
3301 this.setupFocus(this.$follow_button);
3303 render_value: function(no_recurse) {
3305 if (! this.get("value")) {
3306 this.display_string("");
3309 var display = this.display_value["" + this.get("value")];
3311 this.display_string(display);
3315 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3316 this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3318 self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
3321 self.display_value["" + self.get("value")] = data[0][1];
3322 self.render_value(true);
3326 display_string: function(str) {
3328 if (!this.get("effective_readonly")) {
3329 this.$input.val(str.split("\n")[0]);
3330 this.current_display = this.$input.val();
3331 if (this.is_false()) {
3332 this.$('.oe_m2o_cm_button').css({'display':'none'});
3334 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3337 var lines = _.escape(str).split("\n");
3341 follow = _.rest(lines).join("<br />");
3344 var $link = this.$el.find('.oe_form_uri')
3347 if (! this.options.no_open)
3348 $link.click(function () {
3350 type: 'ir.actions.act_window',
3351 res_model: self.field.relation,
3352 res_id: self.get("value"),
3353 views: [[false, 'form']],
3355 context: self.build_context().eval(),
3359 $(".oe_form_m2o_follow", this.$el).html(follow);
3362 set_value: function(value_) {
3364 if (value_ instanceof Array) {
3365 this.display_value = {};
3366 if (! this.options.always_reload) {
3367 this.display_value["" + value_[0]] = value_[1];
3371 value_ = value_ || false;
3372 this.reinit_value(value_);
3374 get_displayed: function() {
3375 return this.display_value["" + this.get("value")];
3377 add_id: function(id) {
3378 this.display_value = {};
3379 this.reinit_value(id);
3381 is_false: function() {
3382 return ! this.get("value");
3384 focus: function () {
3385 var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3386 return input ? input.focus() : false;
3388 _quick_create: function() {
3390 this.ed_def.reject();
3391 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3393 _search_create_popup: function() {
3395 this.ed_def.reject();
3396 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3398 set_dimensions: function (height, width) {
3399 this._super(height, width);
3400 if (!this.get("effective_readonly") && this.$input)
3401 this.$input.css('height', height);
3405 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3406 template: 'Many2OneButton',
3407 init: function(field_manager, node) {
3408 this._super.apply(this, arguments);
3411 this._super.apply(this, arguments);
3414 set_button: function() {
3417 this.$button.remove();
3420 this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3421 this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3422 this.$button.addClass('oe_link').css({'padding':'4px'});
3423 this.$el.append(this.$button);
3424 this.$button.on('click', self.on_click);
3426 on_click: function(ev) {
3428 this.popup = new instance.web.form.FormOpenPopup(this);
3429 this.popup.show_element(
3430 this.field.relation,
3432 this.build_context(),
3433 {title: this.string}
3435 this.popup.on('create_completed', self, function(r) {
3439 set_value: function(value_) {
3441 if (value_ instanceof Array) {
3444 value_ = value_ || false;
3445 this.set('value', value_);
3451 # Values: (0, 0, { fields }) create
3452 # (1, ID, { fields }) update
3453 # (2, ID) remove (delete)
3454 # (3, ID) unlink one (target id or target of relation)
3456 # (5) unlink all (only valid for one2many)
3461 'create': function (values) {
3462 return [commands.CREATE, false, values];
3464 // (1, id, {values})
3466 'update': function (id, values) {
3467 return [commands.UPDATE, id, values];
3471 'delete': function (id) {
3472 return [commands.DELETE, id, false];
3474 // (3, id[, _]) removes relation, but not linked record itself
3476 'forget': function (id) {
3477 return [commands.FORGET, id, false];
3481 'link_to': function (id) {
3482 return [commands.LINK_TO, id, false];
3486 'delete_all': function () {
3487 return [5, false, false];
3489 // (6, _, ids) replaces all linked records with provided ids
3491 'replace_with': function (ids) {
3492 return [6, false, ids];
3495 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3496 multi_selection: false,
3497 disable_utility_classes: true,
3498 init: function(field_manager, node) {
3499 this._super(field_manager, node);
3500 lazy_build_o2m_kanban_view();
3501 this.is_loaded = $.Deferred();
3502 this.initial_is_loaded = this.is_loaded;
3503 this.form_last_update = $.Deferred();
3504 this.init_form_last_update = this.form_last_update;
3505 this.is_started = false;
3506 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
3507 this.dataset.o2m = this;
3508 this.dataset.parent_view = this.view;
3509 this.dataset.child_name = this.name;
3511 this.dataset.on('dataset_changed', this, function() {
3512 self.trigger_on_change();
3517 this._super.apply(this, arguments);
3518 this.$el.addClass('oe_form_field oe_form_field_one2many');
3523 this.is_loaded.done(function() {
3524 self.on("change:effective_readonly", self, function() {
3525 self.is_loaded = self.is_loaded.then(function() {
3526 self.viewmanager.destroy();
3527 return $.when(self.load_views()).done(function() {
3528 self.reload_current_view();
3533 this.is_started = true;
3534 this.reload_current_view();
3536 trigger_on_change: function() {
3537 this.trigger('changed_value');
3539 load_views: function() {
3542 var modes = this.node.attrs.mode;
3543 modes = !!modes ? modes.split(",") : ["tree"];
3545 _.each(modes, function(mode) {
3546 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
3547 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
3551 view_type: mode == "tree" ? "list" : mode,
3554 if (self.field.views && self.field.views[mode]) {
3555 view.embedded_view = self.field.views[mode];
3557 if(view.view_type === "list") {
3558 _.extend(view.options, {
3560 selectable: self.multi_selection,
3562 import_enabled: false,
3565 if (self.get("effective_readonly")) {
3566 _.extend(view.options, {
3571 } else if (view.view_type === "form") {
3572 if (self.get("effective_readonly")) {
3573 view.view_type = 'form';
3575 _.extend(view.options, {
3576 not_interactible_on_create: true,
3578 } else if (view.view_type === "kanban") {
3579 _.extend(view.options, {
3580 confirm_on_delete: false,
3582 if (self.get("effective_readonly")) {
3583 _.extend(view.options, {
3584 action_buttons: false,
3585 quick_creatable: false,
3587 read_only_mode: true,
3595 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
3596 this.viewmanager.o2m = self;
3597 var once = $.Deferred().done(function() {
3598 self.init_form_last_update.resolve();
3600 var def = $.Deferred().done(function() {
3601 self.initial_is_loaded.resolve();
3603 this.viewmanager.on("controller_inited", self, function(view_type, controller) {
3604 controller.o2m = self;
3605 if (view_type == "list") {
3606 if (self.get("effective_readonly")) {
3607 controller.on('edit:before', self, function (e) {
3610 _(controller.columns).find(function (column) {
3611 if (!(column instanceof instance.web.list.Handle)) {
3614 column.modifiers.invisible = true;
3618 } else if (view_type === "form") {
3619 if (self.get("effective_readonly")) {
3620 $(".oe_form_buttons", controller.$el).children().remove();
3622 controller.on("load_record", self, function(){
3625 controller.on('pager_action_executed',self,self.save_any_view);
3626 } else if (view_type == "graph") {
3627 self.reload_current_view()
3631 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
3632 $.when(self.save_any_view()).done(function() {
3633 if (n_mode === "list") {
3634 $.async_when().done(function() {
3635 self.reload_current_view();
3640 $.async_when().done(function () {
3641 self.viewmanager.appendTo(self.$el);
3645 reload_current_view: function() {
3647 return self.is_loaded = self.is_loaded.then(function() {
3648 var active_view = self.viewmanager.active_view;
3649 var view = self.viewmanager.views[active_view].controller;
3650 if(active_view === "list") {
3651 return view.reload_content();
3652 } else if (active_view === "form") {
3653 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
3654 self.dataset.index = 0;
3656 var act = function() {
3657 return view.do_show();
3659 self.form_last_update = self.form_last_update.then(act, act);
3660 return self.form_last_update;
3661 } else if (view.do_search) {
3662 return view.do_search(self.build_domain(), self.dataset.get_context(), []);
3666 set_value: function(value_) {
3667 value_ = value_ || [];
3669 this.dataset.reset_ids([]);
3670 if(value_.length >= 1 && value_[0] instanceof Array) {
3672 _.each(value_, function(command) {
3673 var obj = {values: command[2]};
3674 switch (command[0]) {
3675 case commands.CREATE:
3676 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3678 self.dataset.to_create.push(obj);
3679 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3682 case commands.UPDATE:
3683 obj['id'] = command[1];
3684 self.dataset.to_write.push(obj);
3685 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3688 case commands.DELETE:
3689 self.dataset.to_delete.push({id: command[1]});
3691 case commands.LINK_TO:
3692 ids.push(command[1]);
3694 case commands.DELETE_ALL:
3695 self.dataset.delete_all = true;
3700 this.dataset.set_ids(ids);
3701 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
3703 this.dataset.delete_all = true;
3704 _.each(value_, function(command) {
3705 var obj = {values: command};
3706 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3708 self.dataset.to_create.push(obj);
3709 self.dataset.cache.push(_.clone(obj));
3713 this.dataset.set_ids(ids);
3715 this._super(value_);
3716 this.dataset.reset_ids(value_);
3718 if (this.dataset.index === null && this.dataset.ids.length > 0) {
3719 this.dataset.index = 0;
3721 this.trigger_on_change();
3722 if (this.is_started) {
3723 return self.reload_current_view();
3728 get_value: function() {
3732 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
3733 val = val.concat(_.map(this.dataset.ids, function(id) {
3734 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
3736 return commands.create(alter_order.values);
3738 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
3740 return commands.update(alter_order.id, alter_order.values);
3742 return commands.link_to(id);
3744 return val.concat(_.map(
3745 this.dataset.to_delete, function(x) {
3746 return commands['delete'](x.id);}));
3748 commit_value: function() {
3749 return this.save_any_view();
3751 save_any_view: function() {
3752 if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
3753 this.viewmanager.views[this.viewmanager.active_view] &&
3754 this.viewmanager.views[this.viewmanager.active_view].controller) {
3755 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3756 if (this.viewmanager.active_view === "form") {
3757 if (!view.is_initialized.state() === 'resolved') {
3758 return $.when(false);
3760 return $.when(view.save());
3761 } else if (this.viewmanager.active_view === "list") {
3762 return $.when(view.ensure_saved());
3765 return $.when(false);
3767 is_syntax_valid: function() {
3768 if (! this.viewmanager || ! this.viewmanager.views[this.viewmanager.active_view])
3770 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3771 switch (this.viewmanager.active_view) {
3773 return _(view.fields).chain()
3779 return view.is_valid();
3785 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
3786 template: 'One2Many.viewmanager',
3787 init: function(parent, dataset, views, flags) {
3788 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
3789 this.registry = this.registry.extend({
3790 list: 'instance.web.form.One2ManyListView',
3791 form: 'instance.web.form.One2ManyFormView',
3792 kanban: 'instance.web.form.One2ManyKanbanView',
3794 this.__ignore_blur = false;
3796 switch_mode: function(mode, unused) {
3797 if (mode !== 'form') {
3798 return this._super(mode, unused);
3801 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
3802 var pop = new instance.web.form.FormOpenPopup(this);
3803 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3804 title: _t("Open: ") + self.o2m.string,
3805 create_function: function(data, options) {
3806 return self.o2m.dataset.create(data, options).done(function(r) {
3807 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3808 self.o2m.dataset.trigger("dataset_changed", r);
3811 write_function: function(id, data, options) {
3812 return self.o2m.dataset.write(id, data, {}).done(function() {
3813 self.o2m.reload_current_view();
3816 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3817 parent_view: self.o2m.view,
3818 child_name: self.o2m.name,
3819 read_function: function() {
3820 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3822 form_view_options: {'not_interactible_on_create':true},
3823 readonly: self.o2m.get("effective_readonly")
3825 pop.on("elements_selected", self, function() {
3826 self.o2m.reload_current_view();
3831 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
3832 get_context: function() {
3833 this.context = this.o2m.build_context();
3834 return this.context;
3838 instance.web.form.One2ManyListView = instance.web.ListView.extend({
3839 _template: 'One2Many.listview',
3840 init: function (parent, dataset, view_id, options) {
3841 this._super(parent, dataset, view_id, _.extend(options || {}, {
3842 GroupsType: instance.web.form.One2ManyGroups,
3843 ListType: instance.web.form.One2ManyList
3845 this.on('edit:after', this, this.proxy('_after_edit'));
3846 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
3849 .bind('add', this.proxy("changed_records"))
3850 .bind('edit', this.proxy("changed_records"))
3851 .bind('remove', this.proxy("changed_records"));
3853 start: function () {
3854 var ret = this._super();
3856 .off('mousedown.handleButtons')
3857 .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
3860 changed_records: function () {
3861 this.o2m.trigger_on_change();
3863 is_valid: function () {
3864 var editor = this.editor;
3865 var form = editor.form;
3866 // If no edition is pending, the listview can not be invalid (?)
3867 if (!editor.record) {
3870 // If the form has not been modified, the view can only be valid
3871 // NB: is_dirty will also be set on defaults/onchanges/whatever?
3872 // oe_form_dirty seems to only be set on actual user actions
3873 if (!form.$el.is('.oe_form_dirty')) {
3876 this.o2m._dirty_flag = true;
3878 // Otherwise validate internal form
3879 return _(form.fields).chain()
3880 .invoke(function () {
3881 this._check_css_flags();
3882 return this.is_valid();
3887 do_add_record: function () {
3888 if (this.editable()) {
3889 this._super.apply(this, arguments);
3892 var pop = new instance.web.form.SelectCreatePopup(this);
3894 self.o2m.field.relation,
3896 title: _t("Create: ") + self.o2m.string,
3897 initial_view: "form",
3898 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3899 create_function: function(data, options) {
3900 return self.o2m.dataset.create(data, options).done(function(r) {
3901 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3902 self.o2m.dataset.trigger("dataset_changed", r);
3905 read_function: function() {
3906 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3908 parent_view: self.o2m.view,
3909 child_name: self.o2m.name,
3910 form_view_options: {'not_interactible_on_create':true}
3912 self.o2m.build_domain(),
3913 self.o2m.build_context()
3915 pop.on("elements_selected", self, function() {
3916 self.o2m.reload_current_view();
3920 do_activate_record: function(index, id) {
3922 var pop = new instance.web.form.FormOpenPopup(self);
3923 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3924 title: _t("Open: ") + self.o2m.string,
3925 write_function: function(id, data) {
3926 return self.o2m.dataset.write(id, data, {}).done(function() {
3927 self.o2m.reload_current_view();
3930 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3931 parent_view: self.o2m.view,
3932 child_name: self.o2m.name,
3933 read_function: function() {
3934 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3936 form_view_options: {'not_interactible_on_create':true},
3937 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
3940 do_button_action: function (name, id, callback) {
3941 if (!_.isNumber(id)) {
3942 instance.webclient.notification.warn(
3943 _t("Action Button"),
3944 _t("The o2m record must be saved before an action can be used"));
3947 var parent_form = this.o2m.view;
3949 this.ensure_saved().then(function () {
3951 return parent_form.save();
3954 }).done(function () {
3955 var ds = self.o2m.dataset;
3956 var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
3957 return value.length;
3959 if (!self.o2m.options.reload_on_button && !cached_records) {
3960 self.handle_button(name, id, callback);
3962 self.handle_button(name, id, function(){
3963 self.o2m.view.reload();
3969 _after_edit: function () {
3970 this.__ignore_blur = false;
3971 this.editor.form.on('blurred', this, this._on_form_blur);
3973 // The form's blur thing may be jiggered during the edition setup,
3974 // potentially leading to the o2m instasaving the row. Cancel any
3975 // blurring triggered the edition startup here
3976 this.editor.form.widgetFocused();
3978 _before_unedit: function () {
3979 this.editor.form.off('blurred', this, this._on_form_blur);
3981 _button_down: function () {
3982 // If a button is clicked (usually some sort of action button), it's
3983 // the button's responsibility to ensure the editable list is in the
3984 // correct state -> ignore form blurring
3985 this.__ignore_blur = true;
3988 * Handles blurring of the nested form (saves the currently edited row),
3989 * unless the flag to ignore the event is set to ``true``
3991 * Makes the internal form go away
3993 _on_form_blur: function () {
3994 if (this.__ignore_blur) {
3995 this.__ignore_blur = false;
3998 // FIXME: why isn't there an API for this?
3999 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4000 this.ensure_saved();
4003 this.cancel_edition();
4005 keypress_ENTER: function () {
4006 // blurring caused by hitting the [Return] key, should skip the
4007 // autosave-on-blur and let the handler for [Return] do its thing (save
4008 // the current row *anyway*, then create a new one/edit the next one)
4009 this.__ignore_blur = true;
4010 this._super.apply(this, arguments);
4012 do_delete: function (ids) {
4013 var confirm = window.confirm;
4014 window.confirm = function () { return true; };
4016 return this._super(ids);
4018 window.confirm = confirm;
4021 reload_record: function (record) {
4022 // Evict record.id from cache to ensure it will be reloaded correctly
4023 this.dataset.evict_record(record.get('id'));
4025 return this._super(record);
4028 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4029 setup_resequence_rows: function () {
4030 if (!this.view.o2m.get('effective_readonly')) {
4031 this._super.apply(this, arguments);
4035 instance.web.form.One2ManyList = instance.web.ListView.List.extend({
4036 pad_table_to: function (count) {
4037 if (!this.view.is_action_enabled('create')) {
4040 this._super(count > 0 ? count - 1 : 0);
4043 // magical invocation of wtf does that do
4044 if (this.view.o2m.get('effective_readonly')) {
4049 var columns = _(this.columns).filter(function (column) {
4050 return column.invisible !== '1';
4052 if (this.options.selectable) { columns++; }
4053 if (this.options.deletable) { columns++; }
4055 if (!this.view.is_action_enabled('create')) {
4059 var $cell = $('<td>', {
4061 'class': 'oe_form_field_one2many_list_row_add'
4063 $('<a>', {href: '#'}).text(_t("Add an item"))
4064 .mousedown(function () {
4065 // FIXME: needs to be an official API somehow
4066 if (self.view.editor.is_editing()) {
4067 self.view.__ignore_blur = true;
4070 .click(function (e) {
4072 e.stopPropagation();
4073 // FIXME: there should also be an API for that one
4074 if (self.view.editor.form.__blur_timeout) {
4075 clearTimeout(self.view.editor.form.__blur_timeout);
4076 self.view.editor.form.__blur_timeout = false;
4078 self.view.ensure_saved().done(function () {
4079 self.view.do_add_record();
4083 var $padding = this.$current.find('tr:not([data-id]):first');
4084 var $newrow = $('<tr>').append($cell);
4085 if ($padding.length) {
4086 $padding.before($newrow);
4088 this.$current.append($newrow)
4093 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4094 form_template: 'One2Many.formview',
4095 load_form: function(data) {
4098 this.$buttons.find('button.oe_form_button_create').click(function() {
4099 self.save().done(self.on_button_new);
4102 do_notify_change: function() {
4103 if (this.dataset.parent_view) {
4104 this.dataset.parent_view.do_notify_change();
4106 this._super.apply(this, arguments);
4111 var lazy_build_o2m_kanban_view = function() {
4112 if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
4114 instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4118 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4119 template: "FieldMany2ManyTags",
4121 this._super.apply(this, arguments);
4122 instance.web.form.CompletionFieldMixin.init.call(this);
4123 this.set({"value": []});
4124 this._display_orderer = new instance.web.DropMisordered();
4125 this._drop_shown = false;
4127 initialize_content: function() {
4128 if (this.get("effective_readonly"))
4131 var ignore_blur = false;
4132 self.$text = this.$("textarea");
4133 self.$text.textext({
4134 plugins : 'tags arrow autocomplete',
4136 render: function(suggestion) {
4137 return $('<span class="text-label"/>').
4138 data('index', suggestion['index']).html(suggestion['label']);
4143 selectFromDropdown: function() {
4144 this.trigger('hideDropdown');
4145 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4146 var data = self.search_result[index];
4148 self.add_id(data.id);
4153 this.trigger('setSuggestions', {result : []});
4157 isTagAllowed: function(tag) {
4161 removeTag: function(tag) {
4162 var id = tag.data("id");
4163 self.set({"value": _.without(self.get("value"), id)});
4165 renderTag: function(stuff) {
4166 return $.fn.textext.TextExtTags.prototype.renderTag.
4167 call(this, stuff).data("id", stuff.id);
4171 itemToString: function(item) {
4176 onSetInputData: function(e, data) {
4178 this._plugins.autocomplete._suggestions = null;
4180 this.input().val(data);
4184 }).bind('getSuggestions', function(e, data) {
4186 var str = !!data ? data.query || '' : '';
4187 self.get_search_result(str).done(function(result) {
4188 self.search_result = result;
4189 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4190 return _.extend(el, {index:i});
4193 }).bind('hideDropdown', function() {
4194 self._drop_shown = false;
4195 }).bind('showDropdown', function() {
4196 self._drop_shown = true;
4198 self.tags = self.$text.textext()[0].tags();
4200 .focusin(function () {
4201 self.trigger('focused');
4202 ignore_blur = false;
4204 .focusout(function() {
4205 self.$text.trigger("setInputData", "");
4207 self.trigger('blurred');
4209 }).keydown(function(e) {
4210 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4211 self.$text.textext()[0].autocomplete().selectFromDropdown();
4215 set_value: function(value_) {
4216 value_ = value_ || [];
4217 if (value_.length >= 1 && value_[0] instanceof Array) {
4218 value_ = value_[0][2];
4220 this._super(value_);
4222 is_false: function() {
4223 return _(this.get("value")).isEmpty();
4225 get_value: function() {
4226 var tmp = [commands.replace_with(this.get("value"))];
4229 get_search_blacklist: function() {
4230 return this.get("value");
4232 render_value: function() {
4234 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4235 var values = self.get("value");
4236 var handle_names = function(data) {
4237 if (self.isDestroyed())
4240 _.each(data, function(el) {
4241 indexed[el[0]] = el;
4243 data = _.map(values, function(el) { return indexed[el]; });
4244 if (! self.get("effective_readonly")) {
4245 self.tags.containerElement().children().remove();
4246 self.$('textarea').css("padding-left", "3px");
4247 self.tags.addTags(_.map(data, function(el) {return {name: el[1], id:el[0]};}));
4249 self.$el.html(QWeb.render("FieldMany2ManyTag", {elements: data}));
4252 if (! values || values.length > 0) {
4253 this._display_orderer.add(dataset.name_get(values)).done(handle_names);
4258 add_id: function(id) {
4259 this.set({'value': _.uniq(this.get('value').concat([id]))});
4261 focus: function () {
4262 var input = this.$text && this.$text[0];
4263 return input ? input.focus() : false;
4269 - reload_on_button: Reload the whole form view if click on a button in a list view.
4270 If you see this options, do not use it, it's basically a dirty hack to make one
4271 precise o2m to behave the way we want.
4273 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4274 multi_selection: false,
4275 disable_utility_classes: true,
4276 init: function(field_manager, node) {
4277 this._super(field_manager, node);
4278 this.is_loaded = $.Deferred();
4279 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4280 this.dataset.m2m = this;
4282 this.dataset.on('unlink', self, function(ids) {
4283 self.dataset_changed();
4286 this.list_dm = new instance.web.DropMisordered();
4287 this.render_value_dm = new instance.web.DropMisordered();
4289 initialize_content: function() {
4292 this.$el.addClass('oe_form_field oe_form_field_many2many');
4294 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4295 'addable': this.get("effective_readonly") ? null : _t("Add"),
4296 'deletable': this.get("effective_readonly") ? false : true,
4297 'selectable': this.multi_selection,
4299 'reorderable': false,
4300 'import_enabled': false,
4302 var embedded = (this.field.views || {}).tree;
4304 this.list_view.set_embedded_view(embedded);
4306 this.list_view.m2m_field = this;
4307 var loaded = $.Deferred();
4308 this.list_view.on("list_view_loaded", this, function() {
4311 this.list_view.appendTo(this.$el);
4313 var old_def = self.is_loaded;
4314 self.is_loaded = $.Deferred().done(function() {
4317 this.list_dm.add(loaded).then(function() {
4318 self.is_loaded.resolve();
4321 destroy_content: function() {
4322 this.list_view.destroy();
4323 this.list_view = undefined;
4325 set_value: function(value_) {
4326 value_ = value_ || [];
4327 if (value_.length >= 1 && value_[0] instanceof Array) {
4328 value_ = value_[0][2];
4330 this._super(value_);
4332 get_value: function() {
4333 return [commands.replace_with(this.get('value'))];
4335 is_false: function () {
4336 return _(this.get("value")).isEmpty();
4338 render_value: function() {
4340 this.dataset.set_ids(this.get("value"));
4341 this.render_value_dm.add(this.is_loaded).then(function() {
4342 return self.list_view.reload_content();
4345 dataset_changed: function() {
4346 this.internal_set_value(this.dataset.ids);
4350 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4351 get_context: function() {
4352 this.context = this.m2m.build_context();
4353 return this.context;
4359 * @extends instance.web.ListView
4361 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4362 do_add_record: function () {
4363 var pop = new instance.web.form.SelectCreatePopup(this);
4367 title: _t("Add: ") + this.m2m_field.string,
4368 no_create: this.m2m_field.options.no_create,
4370 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4371 this.m2m_field.build_context()
4374 pop.on("elements_selected", self, function(element_ids) {
4376 _(element_ids).each(function (id) {
4377 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4378 self.dataset.set_ids(self.dataset.ids.concat([id]));
4379 self.m2m_field.dataset_changed();
4384 self.reload_content();
4388 do_activate_record: function(index, id) {
4390 var pop = new instance.web.form.FormOpenPopup(this);
4391 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4392 title: _t("Open: ") + this.m2m_field.string,
4393 readonly: this.getParent().get("effective_readonly")
4395 pop.on('write_completed', self, self.reload_content);
4397 do_button_action: function(name, id, callback) {
4399 var _sup = _.bind(this._super, this);
4400 if (! this.m2m_field.options.reload_on_button) {
4401 return _sup(name, id, callback);
4403 return this.m2m_field.view.save().then(function() {
4404 return _sup(name, id, function() {
4405 self.m2m_field.view.reload();
4410 is_action_enabled: function () { return true; },
4413 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4414 disable_utility_classes: true,
4415 init: function(field_manager, node) {
4416 this._super(field_manager, node);
4417 instance.web.form.CompletionFieldMixin.init.call(this);
4418 m2m_kanban_lazy_init();
4419 this.is_loaded = $.Deferred();
4420 this.initial_is_loaded = this.is_loaded;
4423 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4424 this.dataset.m2m = this;
4425 this.dataset.on('unlink', self, function(ids) {
4426 self.dataset_changed();
4430 this._super.apply(this, arguments);
4435 self.on("change:effective_readonly", self, function() {
4436 self.is_loaded = self.is_loaded.then(function() {
4437 self.kanban_view.destroy();
4438 return $.when(self.load_view()).done(function() {
4439 self.render_value();
4444 set_value: function(value_) {
4445 value_ = value_ || [];
4446 if (value_.length >= 1 && value_[0] instanceof Array) {
4447 value_ = value_[0][2];
4449 this._super(value_);
4451 get_value: function() {
4452 return [commands.replace_with(this.get('value'))];
4454 load_view: function() {
4456 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4457 'create_text': _t("Add"),
4458 'creatable': self.get("effective_readonly") ? false : true,
4459 'quick_creatable': self.get("effective_readonly") ? false : true,
4460 'read_only_mode': self.get("effective_readonly") ? true : false,
4461 'confirm_on_delete': false,
4463 var embedded = (this.field.views || {}).kanban;
4465 this.kanban_view.set_embedded_view(embedded);
4467 this.kanban_view.m2m = this;
4468 var loaded = $.Deferred();
4469 this.kanban_view.on("kanban_view_loaded",self,function() {
4470 self.initial_is_loaded.resolve();
4473 this.kanban_view.on('switch_mode', this, this.open_popup);
4474 $.async_when().done(function () {
4475 self.kanban_view.appendTo(self.$el);
4479 render_value: function() {
4481 this.dataset.set_ids(this.get("value"));
4482 this.is_loaded = this.is_loaded.then(function() {
4483 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
4486 dataset_changed: function() {
4487 this.set({'value': this.dataset.ids});
4489 open_popup: function(type, unused) {
4490 if (type !== "form")
4493 if (this.dataset.index === null) {
4494 var pop = new instance.web.form.SelectCreatePopup(this);
4496 this.field.relation,
4498 title: _t("Add: ") + this.string
4500 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
4501 this.build_context()
4503 pop.on("elements_selected", self, function(element_ids) {
4504 _.each(element_ids, function(one_id) {
4505 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
4506 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
4507 self.dataset_changed();
4508 self.render_value();
4513 var id = self.dataset.ids[self.dataset.index];
4514 var pop = new instance.web.form.FormOpenPopup(this);
4515 pop.show_element(self.field.relation, id, self.build_context(), {
4516 title: _t("Open: ") + self.string,
4517 write_function: function(id, data, options) {
4518 return self.dataset.write(id, data, {}).done(function() {
4519 self.render_value();
4522 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
4523 parent_view: self.view,
4524 child_name: self.name,
4525 readonly: self.get("effective_readonly")
4529 add_id: function(id) {
4530 this.quick_create.add_id(id);
4534 function m2m_kanban_lazy_init() {
4535 if (instance.web.form.Many2ManyKanbanView)
4537 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4538 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
4539 _is_quick_create_enabled: function() {
4540 return this._super() && ! this.group_by;
4543 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
4544 template: 'Many2ManyKanban.quick_create',
4547 * close_btn: If true, the widget will display a "Close" button able to trigger
4550 init: function(parent, dataset, context, buttons) {
4551 this._super(parent);
4552 this.m2m = this.getParent().view.m2m;
4553 this.m2m.quick_create = this;
4554 this._dataset = dataset;
4555 this._buttons = buttons || false;
4556 this._context = context || {};
4558 start: function () {
4560 self.$text = this.$el.find('input').css("width", "200px");
4561 self.$text.textext({
4562 plugins : 'arrow autocomplete',
4564 render: function(suggestion) {
4565 return $('<span class="text-label"/>').
4566 data('index', suggestion['index']).html(suggestion['label']);
4571 selectFromDropdown: function() {
4572 $(this).trigger('hideDropdown');
4573 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4574 var data = self.search_result[index];
4576 self.add_id(data.id);
4583 itemToString: function(item) {
4588 }).bind('getSuggestions', function(e, data) {
4590 var str = !!data ? data.query || '' : '';
4591 self.m2m.get_search_result(str).done(function(result) {
4592 self.search_result = result;
4593 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4594 return _.extend(el, {index:i});
4598 self.$text.focusout(function() {
4603 this.$text[0].focus();
4605 add_id: function(id) {
4608 self.trigger('added', id);
4609 this.m2m.dataset_changed();
4615 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
4617 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
4618 template: "AbstractFormPopup.render",
4621 * -readonly: only applicable when not in creation mode, default to false
4622 * - alternative_form_view
4629 * - form_view_options
4631 init_popup: function(model, row_id, domain, context, options) {
4632 this.row_id = row_id;
4634 this.domain = domain || [];
4635 this.context = context || {};
4636 this.options = options;
4637 _.defaults(this.options, {
4640 init_dataset: function() {
4642 this.created_elements = [];
4643 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
4644 this.dataset.read_function = this.options.read_function;
4645 this.dataset.create_function = function(data, options, sup) {
4646 var fct = self.options.create_function || sup;
4647 return fct.call(this, data, options).done(function(r) {
4648 self.trigger('create_completed saved', r);
4649 self.created_elements.push(r);
4652 this.dataset.write_function = function(id, data, options, sup) {
4653 var fct = self.options.write_function || sup;
4654 return fct.call(this, id, data, options).done(function(r) {
4655 self.trigger('write_completed saved', r);
4658 this.dataset.parent_view = this.options.parent_view;
4659 this.dataset.child_name = this.options.child_name;
4661 display_popup: function() {
4663 this.renderElement();
4664 var dialog = new instance.web.Dialog(this, {
4666 dialogClass: 'oe_act_window',
4668 self.check_exit(true);
4670 title: this.options.title || "",
4671 }, this.$el).open();
4672 this.$buttonpane = dialog.$buttons;
4675 setup_form_view: function() {
4678 this.dataset.ids = [this.row_id];
4679 this.dataset.index = 0;
4681 this.dataset.index = null;
4683 var options = _.clone(self.options.form_view_options) || {};
4684 if (this.row_id !== null) {
4685 options.initial_mode = this.options.readonly ? "view" : "edit";
4688 $buttons: this.$buttonpane,
4690 this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
4691 if (this.options.alternative_form_view) {
4692 this.view_form.set_embedded_view(this.options.alternative_form_view);
4694 this.view_form.appendTo(this.$el.find(".oe_popup_form"));
4695 this.view_form.on("form_view_loaded", self, function() {
4696 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
4697 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
4698 multi_select: multi_select,
4699 readonly: self.row_id !== null && self.options.readonly,
4701 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
4702 $snbutton.click(function() {
4703 $.when(self.view_form.save()).done(function() {
4704 self.view_form.reload_mutex.exec(function() {
4705 self.view_form.on_button_new();
4709 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
4710 $sbutton.click(function() {
4711 $.when(self.view_form.save()).done(function() {
4712 self.view_form.reload_mutex.exec(function() {
4717 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
4718 $cbutton.click(function() {
4719 self.view_form.trigger('on_button_cancel');
4722 self.view_form.do_show();
4725 select_elements: function(element_ids) {
4726 this.trigger("elements_selected", element_ids);
4728 check_exit: function(no_destroy) {
4729 if (this.created_elements.length > 0) {
4730 this.select_elements(this.created_elements);
4731 this.created_elements = [];
4733 this.trigger('closed');
4736 destroy: function () {
4737 this.trigger('closed');
4738 if (this.$el.is(":data(dialog)")) {
4739 this.$el.dialog('close');
4746 * Class to display a popup containing a form view.
4748 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
4749 show_element: function(model, row_id, context, options) {
4750 this.init_popup(model, row_id, [], context, options);
4751 _.defaults(this.options, {
4753 this.display_popup();
4757 this.init_dataset();
4758 this.setup_form_view();
4763 * Class to display a popup to display a list to search a row. It also allows
4764 * to switch to a form view to create a new row.
4766 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
4770 * - initial_view: form or search (default search)
4771 * - disable_multiple_selection
4772 * - list_view_options
4774 select_element: function(model, options, domain, context) {
4775 this.init_popup(model, null, domain, context, options);
4777 _.defaults(this.options, {
4778 initial_view: "search",
4780 this.initial_ids = this.options.initial_ids;
4781 this.display_popup();
4785 this.init_dataset();
4786 if (this.options.initial_view == "search") {
4787 instance.web.pyeval.eval_domains_and_contexts({
4789 contexts: [this.context]
4790 }).done(function (results) {
4791 var search_defaults = {};
4792 _.each(results.context, function (value_, key) {
4793 var match = /^search_default_(.*)$/.exec(key);
4795 search_defaults[match[1]] = value_;
4798 self.setup_search_view(search_defaults);
4804 setup_search_view: function(search_defaults) {
4806 if (this.searchview) {
4807 this.searchview.destroy();
4809 this.searchview = new instance.web.SearchView(this,
4810 this.dataset, false, search_defaults);
4811 this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
4812 if (self.initial_ids) {
4813 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
4814 contexts.concat(self.context), groupbys);
4815 self.initial_ids = undefined;
4817 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
4820 this.searchview.on("search_view_loaded", self, function() {
4821 self.view_list = new instance.web.form.SelectCreateListView(self,
4822 self.dataset, false,
4823 _.extend({'deletable': false,
4824 'selectable': !self.options.disable_multiple_selection,
4825 'import_enabled': false,
4826 '$buttons': self.$buttonpane,
4827 'disable_editable_mode': true,
4828 '$pager': self.$('.oe_popup_list_pager'),
4829 }, self.options.list_view_options || {}));
4830 self.view_list.on('edit:before', self, function (e) {
4833 self.view_list.popup = self;
4834 self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
4835 self.view_list.do_show();
4836 }).then(function() {
4837 self.searchview.do_search();
4839 self.view_list.on("list_view_loaded", self, function() {
4840 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
4841 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
4842 $cbutton.click(function() {
4845 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
4846 $sbutton.click(function() {
4847 self.select_elements(self.selected_ids);
4850 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
4851 $cbutton.click(function() {
4856 this.searchview.appendTo($(".oe_popup_search", self.$el));
4858 do_search: function(domains, contexts, groupbys) {
4860 instance.web.pyeval.eval_domains_and_contexts({
4861 domains: domains || [],
4862 contexts: contexts || [],
4863 group_by_seq: groupbys || []
4864 }).done(function (results) {
4865 self.view_list.do_search(results.domain, results.context, results.group_by);
4868 on_click_element: function(ids) {
4870 this.selected_ids = ids || [];
4871 if(this.selected_ids.length > 0) {
4872 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
4874 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
4877 new_object: function() {
4878 if (this.searchview) {
4879 this.searchview.hide();
4881 if (this.view_list) {
4882 this.view_list.do_hide();
4884 this.setup_form_view();
4888 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
4889 do_add_record: function () {
4890 this.popup.new_object();
4892 select_record: function(index) {
4893 this.popup.select_elements([this.dataset.ids[index]]);
4894 this.popup.destroy();
4896 do_select: function(ids, records) {
4897 this._super(ids, records);
4898 this.popup.on_click_element(ids);
4902 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4903 template: 'FieldReference',
4904 init: function(field_manager, node) {
4905 this._super(field_manager, node);
4906 this.reference_ready = true;
4908 destroy_content: function() {
4911 this.fm = undefined;
4914 initialize_content: function() {
4916 var fm = new instance.web.form.DefaultFieldManager(this);
4918 fm.extend_field_desc({
4920 selection: this.field_manager.get_field_desc(this.name).selection,
4928 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
4930 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
4932 this.selection.on("change:value", this, this.on_selection_changed);
4933 this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
4935 .on('focused', null, function () {self.trigger('focused')})
4936 .on('blurred', null, function () {self.trigger('blurred')});
4938 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
4939 name: 'Referenced Document',
4940 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
4942 this.m2o.on("change:value", this, this.data_changed);
4943 this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
4945 .on('focused', null, function () {self.trigger('focused')})
4946 .on('blurred', null, function () {self.trigger('blurred')});
4948 on_selection_changed: function() {
4949 if (this.reference_ready) {
4950 this.internal_set_value([this.selection.get_value(), false]);
4951 this.render_value();
4954 data_changed: function() {
4955 if (this.reference_ready) {
4956 this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
4959 set_value: function(val) {
4961 val = val.split(',');
4962 val[0] = val[0] || false;
4963 val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
4965 this._super(val || [false, false]);
4967 get_value: function() {
4968 return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
4970 render_value: function() {
4971 this.reference_ready = false;
4972 if (!this.get("effective_readonly")) {
4973 this.selection.set_value(this.get('value')[0]);
4975 this.m2o.field.relation = this.get('value')[0];
4976 this.m2o.set_value(this.get('value')[1]);
4977 this.m2o.$el.toggle(!!this.get('value')[0]);
4978 this.reference_ready = true;
4982 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4983 init: function(field_manager, node) {
4985 this._super(field_manager, node);
4986 this.binary_value = false;
4987 this.useFileAPI = !!window.FileReader;
4988 this.max_upload_size = 25 * 1024 * 1024; // 25Mo
4989 if (!this.useFileAPI) {
4990 this.fileupload_id = _.uniqueId('oe_fileupload');
4991 $(window).on(this.fileupload_id, function() {
4992 var args = [].slice.call(arguments).slice(1);
4993 self.on_file_uploaded.apply(self, args);
4998 if (!this.useFileAPI) {
4999 $(window).off(this.fileupload_id);
5001 this._super.apply(this, arguments);
5003 initialize_content: function() {
5004 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5005 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5006 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5008 on_file_change: function(e) {
5010 var file_node = e.target;
5011 if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5012 if (this.useFileAPI) {
5013 var file = file_node.files[0];
5014 if (file.size > this.max_upload_size) {
5015 var msg = _t("The selected file exceed the maximum file size of %s.");
5016 instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5019 var filereader = new FileReader();
5020 filereader.readAsDataURL(file);
5021 filereader.onloadend = function(upload) {
5022 var data = upload.target.result;
5023 data = data.split(',')[1];
5024 self.on_file_uploaded(file.size, file.name, file.type, data);
5027 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5028 this.$el.find('form.oe_form_binary_form').submit();
5030 this.$el.find('.oe_form_binary_progress').show();
5031 this.$el.find('.oe_form_binary').hide();
5034 on_file_uploaded: function(size, name, content_type, file_base64) {
5035 if (size === false) {
5036 this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5037 // TODO: use openerp web crashmanager
5038 console.warn("Error while uploading file : ", name);
5040 this.filename = name;
5041 this.on_file_uploaded_and_valid.apply(this, arguments);
5043 this.$el.find('.oe_form_binary_progress').hide();
5044 this.$el.find('.oe_form_binary').show();
5046 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5048 on_save_as: function(ev) {
5049 var value = this.get('value');
5051 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5052 ev.stopPropagation();
5054 instance.web.blockUI();
5055 var c = instance.webclient.crashmanager;
5056 this.session.get_file({
5057 url: '/web/binary/saveas_ajax',
5058 data: {data: JSON.stringify({
5059 model: this.view.dataset.model,
5060 id: (this.view.datarecord.id || ''),
5062 filename_field: (this.node.attrs.filename || ''),
5063 data: instance.web.form.is_bin_size(value) ? null : value,
5064 context: this.view.dataset.get_context()
5066 complete: instance.web.unblockUI,
5067 error: c.rpc_error.bind(c)
5069 ev.stopPropagation();
5073 set_filename: function(value) {
5074 var filename = this.node.attrs.filename;
5077 tmp[filename] = value;
5078 this.field_manager.set_values(tmp);
5081 on_clear: function() {
5082 if (this.get('value') !== false) {
5083 this.binary_value = false;
5084 this.internal_set_value(false);
5090 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5091 template: 'FieldBinaryFile',
5092 initialize_content: function() {
5094 if (this.get("effective_readonly")) {
5096 this.$el.find('a').click(function(ev) {
5097 if (self.get('value')) {
5098 self.on_save_as(ev);
5104 render_value: function() {
5105 if (!this.get("effective_readonly")) {
5107 if (this.node.attrs.filename) {
5108 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5110 show_value = (this.get('value') != null && this.get('value') !== false) ? this.get('value') : '';
5112 this.$el.find('input').eq(0).val(show_value);
5114 this.$el.find('a').toggle(!!this.get('value'));
5115 if (this.get('value')) {
5116 var show_value = _t("Download")
5118 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5119 this.$el.find('a').text(show_value);
5123 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5124 this.binary_value = true;
5125 this.internal_set_value(file_base64);
5126 var show_value = name + " (" + instance.web.human_size(size) + ")";
5127 this.$el.find('input').eq(0).val(show_value);
5128 this.set_filename(name);
5130 on_clear: function() {
5131 this._super.apply(this, arguments);
5132 this.$el.find('input').eq(0).val('');
5133 this.set_filename('');
5137 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5138 template: 'FieldBinaryImage',
5139 placeholder: "/web/static/src/img/placeholder.png",
5140 render_value: function() {
5143 if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5144 url = 'data:image/png;base64,' + this.get('value');
5145 } else if (this.get('value')) {
5146 var id = JSON.stringify(this.view.datarecord.id || null);
5147 var field = this.name;
5148 if (this.options.preview_image)
5149 field = this.options.preview_image;
5150 url = this.session.url('/web/binary/image', {
5151 model: this.view.dataset.model,
5154 t: (new Date().getTime()),
5157 url = this.placeholder;
5159 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5160 this.$el.find('> img').remove();
5161 this.$el.prepend($img);
5162 $img.load(function() {
5163 if (! self.options.size)
5165 $img.css("max-width", "" + self.options.size[0] + "px");
5166 $img.css("max-height", "" + self.options.size[1] + "px");
5167 $img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px");
5168 $img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px");
5170 $img.on('error', function() {
5171 $img.attr('src', self.placeholder);
5172 instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5175 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5176 this.internal_set_value(file_base64);
5177 this.binary_value = true;
5178 this.render_value();
5179 this.set_filename(name);
5181 on_clear: function() {
5182 this._super.apply(this, arguments);
5183 this.render_value();
5184 this.set_filename('');
5189 * Widget for (one2many field) to upload one or more file in same time and display in list.
5190 * The user can delete his files.
5191 * Options on attribute ; "blockui" {Boolean} block the UI or not
5192 * during the file is uploading
5194 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
5195 template: "FieldBinaryFileUploader",
5196 init: function(field_manager, node) {
5197 this._super(field_manager, node);
5198 this.field_manager = field_manager;
5200 if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5201 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);
5203 this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5204 this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5205 $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5209 this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5211 set_value: function(value_) {
5212 var value_ = value_ || [];
5215 _.each(value_, function(command) {
5216 if (isNaN(command) && command.id == undefined) {
5217 switch (command[0]) {
5218 case commands.CREATE:
5219 ids = ids.concat(command[2]);
5221 case commands.REPLACE_WITH:
5222 ids = ids.concat(command[2]);
5224 case commands.UPDATE:
5225 ids = ids.concat(command[2]);
5227 case commands.LINK_TO:
5228 ids = ids.concat(command[1]);
5230 case commands.DELETE:
5231 ids = _.filter(ids, function (id) { return id != command[1];});
5233 case commands.DELETE_ALL:
5243 get_value: function() {
5244 return _.map(this.get('value'), function (value) { return commands.link_to( isNaN(value) ? value.id : value ); });
5246 get_file_url: function (attachment) {
5247 return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5249 read_name_values : function () {
5251 // select the list of id for a get_name
5253 _.each(this.get('value'), function (val) {
5254 if (typeof val != 'object') {
5258 // send request for get_name
5259 if (values.length) {
5260 return this.ds_file.call('read', [values, ['id', 'name', 'datas_fname']]).done(function (datas) {
5261 _.each(datas, function (data) {
5262 data.no_unlink = true;
5263 data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5265 _.each(self.get('value'), function (val, key) {
5266 if(val == data.id) {
5267 self.get('value')[key] = data;
5273 return $.when(this.get('value'));
5276 render_value: function () {
5278 this.read_name_values().then(function (datas) {
5280 var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self}));
5281 render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5282 self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5284 // reinit input type file
5285 var $input = self.$('input.oe_form_binary_file');
5286 $input.after($input.clone(true)).remove();
5287 self.$(".oe_fileupload").show();
5291 on_file_change: function (event) {
5292 event.stopPropagation();
5294 var $target = $(event.target);
5295 if ($target.val() !== '') {
5297 var filename = $target.val().replace(/.*[\\\/]/,'');
5299 // if the files is currently uploded, don't send again
5300 if( !isNaN(_.find(this.get('value'), function (file) { return (file.filename || file.name) == filename && file.upload; } )) ) {
5305 if(this.node.attrs.blockui>0) {
5306 instance.web.blockUI();
5309 // if the files exits for this answer, delete the file before upload
5310 var files = _.filter(this.get('value'), function (file) {
5311 if((file.filename || file.name) == filename) {
5312 self.ds_file.unlink([file.id]);
5319 // TODO : unactivate send on wizard and form
5322 this.$('form.oe_form_binary_form').submit();
5323 this.$(".oe_fileupload").hide();
5325 // add file on result
5329 'filename': filename,
5334 this.set({'value': files});
5337 on_file_loaded: function (event, result) {
5338 var files = this.get('value');
5341 if(this.node.attrs.blockui>0) {
5342 instance.web.unblockUI();
5345 // TODO : activate send on wizard and form
5347 if (result.error || !result.id ) {
5348 this.do_warn( _t('Uploading Error'), result.error);
5349 files = _.filter(files, function (val) { return !val.upload; });
5351 for(var i in files){
5352 if(files[i].filename == result.filename && files[i].upload) {
5355 'name': result.name,
5356 'filename': result.filename,
5357 'url': this.get_file_url(result)
5363 this.set({'value': files});
5366 on_file_delete: function (event) {
5367 event.stopPropagation();
5368 var file_id=$(event.target).data("id");
5371 for(var i in this.get('value')){
5372 if(file_id != this.get('value')[i].id){
5373 files.push(this.get('value')[i]);
5375 else if(!this.get('value')[i].no_unlink) {
5376 this.ds_file.unlink([file_id]);
5379 this.set({'value': files});
5384 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5385 template: "FieldStatus",
5386 init: function(field_manager, node) {
5387 this._super(field_manager, node);
5388 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5389 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5390 this.set({value: false});
5391 this.selection = [];
5392 this.set("selection", []);
5393 this.selection_dm = new instance.web.DropMisordered();
5396 this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5398 this.on("change:value", this, this.get_selection);
5399 this.on("change:evaluated_selection_domain", this, this.get_selection);
5400 this.on("change:selection", this, function() {
5401 this.selection = this.get("selection");
5402 this.render_value();
5404 this.get_selection();
5405 if (this.options.clickable) {
5406 this.$el.on('click','li',this.on_click_stage);
5410 set_value: function(value_) {
5411 if (value_ instanceof Array) {
5414 this._super(value_);
5416 render_value: function() {
5418 var content = QWeb.render("FieldStatus.content", {widget: self});
5419 self.$el.html(content);
5420 var colors = JSON.parse((self.node.attrs || {}).statusbar_colors || "{}");
5421 var color = colors[self.get('value')];
5423 self.$("oe_active").css("color", color);
5426 calc_domain: function() {
5427 var d = instance.web.pyeval.eval('domain', this.build_domain());
5428 var domain = []; //if there is no domain defined, fetch all the records
5431 domain = ['|',['id', '=', this.get('value')]].concat(d);
5434 if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5435 this.set("evaluated_selection_domain", domain);
5438 /** Get the selection and render it
5439 * selection: [[identifier, value_to_display], ...]
5440 * For selection fields: this is directly given by this.field.selection
5441 * For many2one fields: perform a search on the relation of the many2one field
5443 get_selection: function() {
5447 var calculation = _.bind(function() {
5448 if (this.field.type == "many2one") {
5450 var ds = new instance.web.DataSetSearch(this, this.field.relation,
5451 self.build_context(), this.get("evaluated_selection_domain"));
5452 return ds.read_slice(['name'], {}).then(function (records) {
5453 for(var i = 0; i < records.length; i++) {
5454 selection.push([records[i].id, records[i].name]);
5458 // For field type selection filter values according to
5459 // statusbar_visible attribute of the field. For example:
5460 // statusbar_visible="draft,open".
5461 var select = this.field.selection;
5462 for(var i=0; i < select.length; i++) {
5463 var key = select[i][0];
5464 if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5465 selection.push(select[i]);
5471 this.selection_dm.add(calculation()).then(function () {
5472 if (! _.isEqual(selection, self.get("selection"))) {
5473 self.set("selection", selection);
5477 on_click_stage: function (ev) {
5479 var $li = $(ev.currentTarget);
5480 var val = parseInt($li.data("id"));
5481 if (val != self.get('value')) {
5482 this.view.recursive_save().done(function() {
5484 change[self.name] = val;
5485 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
5493 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
5494 template: "FieldMonetary",
5495 widget_class: 'oe_form_field_float oe_form_field_monetary',
5497 this._super.apply(this, arguments);
5498 this.set({"currency": false});
5499 if (this.options.currency_field) {
5500 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
5501 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
5504 this.on("change:currency", this, this.get_currency_info);
5505 this.get_currency_info();
5506 this.ci_dm = new instance.web.DropMisordered();
5509 var tmp = this._super();
5510 this.on("change:currency_info", this, this.reinitialize);
5513 get_currency_info: function() {
5515 if (this.get("currency") === false) {
5516 this.set({"currency_info": null});
5519 return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
5520 .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
5521 self.set({"currency_info": res});
5524 parse_value: function(val, def) {
5525 return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5527 format_value: function(val, def) {
5528 return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5533 * Registry of form fields, called by :js:`instance.web.FormView`.
5535 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
5536 * will substitute to the <field> tags as defined in OpenERP's views.
5538 instance.web.form.widgets = new instance.web.Registry({
5539 'char' : 'instance.web.form.FieldChar',
5540 'id' : 'instance.web.form.FieldID',
5541 'email' : 'instance.web.form.FieldEmail',
5542 'url' : 'instance.web.form.FieldUrl',
5543 'text' : 'instance.web.form.FieldText',
5544 'html' : 'instance.web.form.FieldTextHtml',
5545 'date' : 'instance.web.form.FieldDate',
5546 'datetime' : 'instance.web.form.FieldDatetime',
5547 'selection' : 'instance.web.form.FieldSelection',
5548 'many2one' : 'instance.web.form.FieldMany2One',
5549 'many2onebutton' : 'instance.web.form.Many2OneButton',
5550 'many2many' : 'instance.web.form.FieldMany2Many',
5551 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
5552 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
5553 'one2many' : 'instance.web.form.FieldOne2Many',
5554 'one2many_list' : 'instance.web.form.FieldOne2Many',
5555 'reference' : 'instance.web.form.FieldReference',
5556 'boolean' : 'instance.web.form.FieldBoolean',
5557 'float' : 'instance.web.form.FieldFloat',
5558 'integer': 'instance.web.form.FieldFloat',
5559 'float_time': 'instance.web.form.FieldFloat',
5560 'progressbar': 'instance.web.form.FieldProgressBar',
5561 'image': 'instance.web.form.FieldBinaryImage',
5562 'binary': 'instance.web.form.FieldBinaryFile',
5563 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
5564 'statusbar': 'instance.web.form.FieldStatus',
5565 'monetary': 'instance.web.form.FieldMonetary',
5569 * Registry of widgets usable in the form view that can substitute to any possible
5570 * tags defined in OpenERP's form views.
5572 * Every referenced class should extend FormWidget.
5574 instance.web.form.tags = new instance.web.Registry({
5575 'button' : 'instance.web.form.WidgetButton',
5578 instance.web.form.custom_widgets = new instance.web.Registry({
5583 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: