1 openerp.web.form = function (instance) {
2 var _t = instance.web._t,
3 _lt = instance.web._lt;
4 var QWeb = instance.web.qweb;
7 instance.web.form = {};
10 * Interface implemented by the form view or any other object
11 * able to provide the features necessary for the fields to work.
14 * - display_invalid_fields : if true, all fields where is_valid() return true should
15 * be displayed as invalid.
16 * - actual_mode : the current mode of the field manager. Can be "view", "edit" or "create".
18 * - view_content_has_changed : when the values of the fields have changed. When
19 * this event is triggered all fields should reprocess their modifiers.
20 * - field_changed:<field_name> : when the value of a field change, an event is triggered
21 * named "field_changed:<field_name>" with <field_name> replaced by the name of the field.
22 * This event is not related to the on_change mechanism of OpenERP and is always called
23 * when the value of a field is setted or changed. This event is only triggered when the
24 * value of the field is syntactically valid, but it can be triggered when the value
25 * is sematically invalid (ie, when a required field is false). It is possible that an event
26 * about a precise field is never triggered even if that field exists in the view, in that
27 * case the value of the field is assumed to be false.
29 instance.web.form.FieldManagerMixin = {
31 * Must return the asked field as in fields_get.
33 get_field_desc: function(field_name) {},
35 * Returns the current value of a field present in the view. See the get_value() method
36 * method in FieldInterface for further information.
38 get_field_value: function(field_name) {},
40 Gives new values for the fields contained in the view. The new values could not be setted
41 right after the call to this method. Setting new values can trigger on_changes.
43 @param (dict) values A dictonnary with key = field name and value = new value.
44 @return (Deferred) Is resolved after all the values are setted.
46 set_values: function(values) {},
48 Computes an OpenERP domain.
50 @param (list) expression An OpenERP domain.
51 @return (boolean) The computed value of the domain.
53 compute_domain: function(expression) {},
55 Builds an evaluation context for the resolution of the fields' contexts. Please note
56 the field are only supposed to use this context to evualuate their own, they should not
59 @return (CompoundContext) An OpenERP context.
61 build_eval_context: function() {},
64 instance.web.views.add('form', 'instance.web.FormView');
67 * - actual_mode: always "view", "edit" or "create". Read-only property. Determines
68 * the mode used by the view.
70 instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerMixin, {
72 * Indicates that this view is not searchable, and thus that no search
73 * view should be displayed (if there is one active).
77 display_name: _lt('Form'),
80 * @constructs instance.web.FormView
81 * @extends instance.web.View
83 * @param {instance.web.Session} session the current openerp session
84 * @param {instance.web.DataSet} dataset the dataset this view will work with
85 * @param {String} view_id the identifier of the OpenERP view object
86 * @param {Object} options
87 * - resize_textareas : [true|false|max_height]
89 * @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance
91 init: function(parent, dataset, view_id, options) {
94 this.ViewManager = parent;
95 this.set_default_options(options);
96 this.dataset = dataset;
97 this.model = dataset.model;
98 this.view_id = view_id || false;
99 this.fields_view = {};
101 this.fields_order = [];
102 this.datarecord = {};
103 this.default_focus_field = null;
104 this.default_focus_button = null;
105 this.fields_registry = instance.web.form.widgets;
106 this.tags_registry = instance.web.form.tags;
107 this.widgets_registry = instance.web.form.custom_widgets;
108 this.has_been_loaded = $.Deferred();
109 this.translatable_fields = [];
110 _.defaults(this.options, {
111 "not_interactible_on_create": false,
112 "initial_mode": "view",
113 "disable_autofocus": false,
114 "footer_to_buttons": false,
116 this.is_initialized = $.Deferred();
117 this.mutating_mutex = new $.Mutex();
118 this.on_change_list = [];
120 this.render_value_defs = [];
121 this.reload_mutex = new $.Mutex();
122 this.__clicked_inside = false;
123 this.__blur_timeout = null;
124 this.rendering_engine = new instance.web.form.FormRenderingEngine(this);
125 self.set({actual_mode: self.options.initial_mode});
126 this.has_been_loaded.done(function() {
127 self.on("change:actual_mode", self, self.check_actual_mode);
128 self.check_actual_mode();
129 self.on("change:actual_mode", self, self.init_pager);
132 self.on("load_record", self, self.load_record);
133 instance.web.bus.on('clear_uncommitted_changes', this, function(e) {
134 if (!this.can_be_discarded()) {
139 view_loading: function(r) {
140 return this.load_form(r);
142 destroy: function() {
143 _.each(this.get_widgets(), function(w) {
144 w.off('focused blurred');
148 this.$el.off('.formBlur');
152 load_form: function(data) {
155 throw new Error(_t("No data provided."));
158 throw "Form view does not support multiple calls to load_form";
160 this.fields_order = [];
161 this.fields_view = data;
163 this.rendering_engine.set_fields_registry(this.fields_registry);
164 this.rendering_engine.set_tags_registry(this.tags_registry);
165 this.rendering_engine.set_widgets_registry(this.widgets_registry);
166 this.rendering_engine.set_fields_view(data);
167 var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container');
168 this.rendering_engine.render_to($dest);
170 this.$el.on('mousedown.formBlur', function () {
171 self.__clicked_inside = true;
174 this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self}));
175 if (this.options.$buttons) {
176 this.$buttons.appendTo(this.options.$buttons);
178 this.$el.find('.oe_form_buttons').replaceWith(this.$buttons);
180 this.$buttons.on('click', '.oe_form_button_create',
181 this.guard_active(this.on_button_create));
182 this.$buttons.on('click', '.oe_form_button_edit',
183 this.guard_active(this.on_button_edit));
184 this.$buttons.on('click', '.oe_form_button_save',
185 this.guard_active(this.on_button_save));
186 this.$buttons.on('click', '.oe_form_button_cancel',
187 this.guard_active(this.on_button_cancel));
188 if (this.options.footer_to_buttons) {
189 this.$el.find('footer').appendTo(this.$buttons);
192 this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar');
193 if (!this.sidebar && this.options.$sidebar) {
194 this.sidebar = new instance.web.Sidebar(this);
195 this.sidebar.appendTo(this.$sidebar);
196 if (this.fields_view.toolbar) {
197 this.sidebar.add_toolbar(this.fields_view.toolbar);
199 this.sidebar.add_items('other', _.compact([
200 self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete },
201 self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate }
205 this.has_been_loaded.resolve();
207 // Add bounce effect on button 'Edit' when click on readonly page view.
208 this.$el.find(".oe_form_group_row,.oe_form_field,label").on('click', function (e) {
209 if(self.get("actual_mode") == "view") {
210 var $button = self.options.$buttons.find(".oe_form_button_edit");
211 $button.openerpBounce();
213 instance.web.bus.trigger('click', e);
216 //bounce effect on red button when click on statusbar.
217 this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) {
218 if((self.get("actual_mode") == "view")) {
219 var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'});
220 $button.openerpBounce();
224 this.trigger('form_view_loaded', data);
227 widgetFocused: function() {
228 // Clear click flag if used to focus a widget
229 this.__clicked_inside = false;
230 if (this.__blur_timeout) {
231 clearTimeout(this.__blur_timeout);
232 this.__blur_timeout = null;
235 widgetBlurred: function() {
236 if (this.__clicked_inside) {
237 // clicked in an other section of the form (than the currently
238 // focused widget) => just ignore the blurring entirely?
239 this.__clicked_inside = false;
243 // clear timeout, if any
244 this.widgetFocused();
245 this.__blur_timeout = setTimeout(function () {
246 self.trigger('blurred');
250 do_load_state: function(state, warm) {
251 if (state.id && this.datarecord.id != state.id) {
252 if (this.dataset.get_id_index(state.id) === null) {
253 this.dataset.ids.push(state.id);
255 this.dataset.select_id(state.id);
256 this.do_show({ reload: warm });
261 * @param {Object} [options]
262 * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view".
263 * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record
264 * @return {$.Deferred}
266 do_show: function (options) {
268 options = options || {};
270 this.sidebar.$el.show();
273 this.$buttons.show();
275 this.$el.show().css({
277 filter: 'alpha(opacity = 0)'
279 this.$el.add(this.$buttons).removeClass('oe_form_dirty');
281 var shown = this.has_been_loaded;
282 if (options.reload !== false) {
283 shown = shown.then(function() {
284 if (self.dataset.index === null) {
285 // null index means we should start a new record
286 return self.on_button_new();
288 var fields = _.keys(self.fields_view.fields);
289 fields.push('display_name');
290 return self.dataset.read_index(fields, {
291 context: { 'bin_size': true, 'future_display_name' : true }
292 }).then(function(r) {
293 self.trigger('load_record', r);
297 return shown.then(function() {
298 self._actualize_mode(options.mode || self.options.initial_mode);
301 filter: 'alpha(opacity = 100)'
305 do_hide: function () {
307 this.sidebar.$el.hide();
310 this.$buttons.hide();
317 load_record: function(record) {
318 var self = this, set_values = [];
320 this.set({ 'title' : undefined });
321 this.do_warn(_t("Form"), _t("The record could not be found in the database."), true);
322 return $.Deferred().reject();
324 this.datarecord = record;
325 this._actualize_mode();
326 this.set({ 'title' : record.id ? record.display_name : _t("New") });
328 _(this.fields).each(function (field, f) {
329 field._dirty_flag = false;
330 field._inhibit_on_change_flag = true;
331 var result = field.set_value(self.datarecord[f] || false);
332 field._inhibit_on_change_flag = false;
333 set_values.push(result);
335 return $.when.apply(null, set_values).then(function() {
337 // New record: Second pass in order to trigger the onchanges
338 // respecting the fields order defined in the view
339 _.each(self.fields_order, function(field_name) {
340 if (record[field_name] !== undefined) {
341 var field = self.fields[field_name];
342 field._dirty_flag = true;
343 self.do_onchange(field);
347 self.on_form_changed();
348 self.rendering_engine.init_fields();
349 self.is_initialized.resolve();
350 self.do_update_pager(record.id == null);
352 self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
355 self.do_push_state({id:record.id});
357 self.do_push_state({});
359 self.$el.add(self.$buttons).removeClass('oe_form_dirty');
364 * Loads and sets up the default values for the model as the current
367 * @return {$.Deferred}
369 load_defaults: function () {
371 var keys = _.keys(this.fields_view.fields);
373 return this.dataset.default_get(keys).then(function(r) {
374 self.trigger('load_record', r);
377 return self.trigger('load_record', {});
379 on_form_changed: function() {
380 this.trigger("view_content_has_changed");
382 do_notify_change: function() {
383 this.$el.add(this.$buttons).addClass('oe_form_dirty');
385 execute_pager_action: function(action) {
386 if (this.can_be_discarded()) {
389 this.dataset.index = 0;
392 this.dataset.previous();
398 this.dataset.index = this.dataset.ids.length - 1;
402 this.trigger('pager_action_executed');
405 init_pager: function() {
408 this.$pager.remove();
409 if (this.get("actual_mode") === "create")
411 this.$pager = $(QWeb.render("FormView.pager", {'widget':self})).hide();
412 if (this.options.$pager) {
413 this.$pager.appendTo(this.options.$pager);
415 this.$el.find('.oe_form_pager').replaceWith(this.$pager);
417 this.$pager.on('click','a[data-pager-action]',function() {
418 var action = $(this).data('pager-action');
419 self.execute_pager_action(action);
421 this.do_update_pager();
423 do_update_pager: function(hide_index) {
424 this.$pager.toggle(this.dataset.ids.length > 1);
426 $(".oe_form_pager_state", this.$pager).html("");
428 $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
431 parse_on_change: function (on_change, widget) {
433 var onchange = _.str.trim(on_change);
434 var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
436 throw new Error(_.str.sprintf( _t("Wrong on change format: %s"), onchange ));
439 var method = call[1];
440 if (!_.str.trim(call[2])) {
441 return {method: method, args: []}
444 var argument_replacement = {
445 'False': function () {return false;},
446 'True': function () {return true;},
447 'None': function () {return null;},
448 'context': function () {
449 return new instance.web.CompoundContext(
450 self.dataset.get_context(),
451 widget.build_context() ? widget.build_context() : {});
454 var parent_fields = null;
455 var args = _.map(call[2].split(','), function (a, i) {
456 var field = _.str.trim(a);
458 // literal constant or context
459 if (field in argument_replacement) {
460 return argument_replacement[field]();
463 if (/^-?\d+(\.\d+)?$/.test(field)) {
464 return Number(field);
467 if (self.fields[field]) {
468 var value_ = self.fields[field].get_value();
469 return value_ == null ? false : value_;
472 var splitted = field.split('.');
473 if (splitted.length > 1 && _.str.trim(splitted[0]) === "parent" && self.dataset.parent_view) {
474 if (parent_fields === null) {
475 parent_fields = self.dataset.parent_view.get_fields_values();
477 var p_val = parent_fields[_.str.trim(splitted[1])];
478 if (p_val !== undefined) {
479 return p_val == null ? false : p_val;
483 var first_char = field[0], last_char = field[field.length-1];
484 if ((first_char === '"' && last_char === '"')
485 || (first_char === "'" && last_char === "'")) {
486 return field.slice(1, -1);
489 throw new Error("Could not get field with name '" + field +
490 "' for onchange '" + onchange + "'");
498 do_onchange: function(widget, processed) {
500 this.on_change_list = [{widget: widget, processed: processed}].concat(this.on_change_list);
501 return this._process_operations();
503 _process_onchange: function(on_change_obj) {
505 var widget = on_change_obj.widget;
506 var processed = on_change_obj.processed;
509 processed = processed || [];
510 processed.push(widget.name);
511 var on_change = widget.node.attrs.on_change;
513 var change_spec = self.parse_on_change(on_change, widget);
515 if (self.datarecord.id && !instance.web.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) {
516 // In case of a o2m virtual id, we should pass an empty ids list
517 ids.push(self.datarecord.id);
519 def = self.alive(new instance.web.Model(self.dataset.model).call(
520 change_spec.method, [ids].concat(change_spec.args)));
524 return def.then(function(response) {
525 if (widget.field['change_default']) {
526 var fieldname = widget.name;
528 if (response.value && (fieldname in response.value)) {
529 // Use value from onchange if onchange executed
530 value_ = response.value[fieldname];
532 // otherwise get form value for field
533 value_ = self.fields[fieldname].get_value();
535 var condition = fieldname + '=' + value_;
538 return self.alive(new instance.web.Model('ir.values').call(
539 'get_defaults', [self.model, condition]
540 )).then(function (results) {
541 if (!results.length) {
544 if (!response.value) {
547 for(var i=0; i<results.length; ++i) {
548 // [whatever, key, value]
549 var triplet = results[i];
550 response.value[triplet[1]] = triplet[2];
557 }).then(function(response) {
558 return self.on_processed_onchange(response, processed);
562 instance.webclient.crashmanager.show_message(e);
563 return $.Deferred().reject();
566 on_processed_onchange: function(result, processed) {
569 this._internal_set_values(result.value, processed);
571 if (!_.isEmpty(result.warning)) {
572 instance.web.dialog($(QWeb.render("CrashManager.warning", result.warning)), {
573 title:result.warning.title,
576 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
581 var fields = this.fields;
582 _(result.domain).each(function (domain, fieldname) {
583 var field = fields[fieldname];
584 if (!field) { return; }
585 field.node.attrs.domain = domain;
588 return $.Deferred().resolve();
591 instance.webclient.crashmanager.show_message(e);
592 return $.Deferred().reject();
595 _process_operations: function() {
597 return this.mutating_mutex.exec(function() {
599 var on_change_obj = self.on_change_list.shift();
601 return self._process_onchange(on_change_obj).then(function() {
606 _.each(self.fields, function(field) {
607 defs.push(field.commit_value());
609 var args = _.toArray(arguments);
610 return $.when.apply($, defs).then(function() {
611 if (self.on_change_list.length !== 0) {
614 var save_obj = self.save_list.pop();
616 return self._process_save(save_obj).then(function() {
617 save_obj.ret = _.toArray(arguments);
620 save_obj.error = true;
625 self.save_list.pop();
632 _internal_set_values: function(values, exclude) {
633 exclude = exclude || [];
634 for (var f in values) {
635 if (!values.hasOwnProperty(f)) { continue; }
636 var field = this.fields[f];
637 // If field is not defined in the view, just ignore it
639 var value_ = values[f];
640 if (field.get_value() != value_) {
641 field._inhibit_on_change_flag = true;
642 field.set_value(value_);
643 field._inhibit_on_change_flag = false;
644 field._dirty_flag = true;
645 if (!_.contains(exclude, field.name)) {
646 this.do_onchange(field, exclude);
651 this.on_form_changed();
653 set_values: function(values) {
655 return this.mutating_mutex.exec(function() {
656 self._internal_set_values(values);
660 * Ask the view to switch to view mode if possible. The view may not do it
661 * if the current record is not yet saved. It will then stay in create mode.
663 to_view_mode: function() {
664 this._actualize_mode("view");
667 * Ask the view to switch to edit mode if possible. The view may not do it
668 * if the current record is not yet saved. It will then stay in create mode.
670 to_edit_mode: function() {
671 this._actualize_mode("edit");
674 * Ask the view to switch to a precise mode if possible. The view is free to
675 * not respect this command if the state of the dataset is not compatible with
676 * the new mode. For example, it is not possible to switch to edit mode if
677 * the current record is not yet saved in database.
679 * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
680 * undefined the view will test the actual mode to check if it is still consistent
681 * with the dataset state.
683 _actualize_mode: function(switch_to) {
684 var mode = switch_to || this.get("actual_mode");
685 if (! this.datarecord.id) {
687 } else if (mode === "create") {
690 this.render_value_defs = [];
691 this.set({actual_mode: mode});
693 check_actual_mode: function(source, options) {
695 if(this.get("actual_mode") === "view") {
696 self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
697 self.$buttons.find('.oe_form_buttons_edit').hide();
698 self.$buttons.find('.oe_form_buttons_view').show();
699 self.$sidebar.show();
701 self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
702 self.$buttons.find('.oe_form_buttons_edit').show();
703 self.$buttons.find('.oe_form_buttons_view').hide();
704 self.$sidebar.hide();
708 autofocus: function() {
709 if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
710 var fields_order = this.fields_order.slice(0);
711 if (this.default_focus_field) {
712 fields_order.unshift(this.default_focus_field.name);
714 for (var i = 0; i < fields_order.length; i += 1) {
715 var field = this.fields[fields_order[i]];
716 if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
717 if (field.focus() !== false) {
724 on_button_save: function(e) {
726 $(e.target).attr("disabled", true);
727 return this.save().done(function(result) {
728 self.trigger("save", result);
729 self.reload().then(function() {
731 var parent = self.ViewManager.ActionManager.getParent();
733 parent.menu.do_reload_needaction();
736 }).always(function(){
737 $(e.target).attr("disabled", false);
740 on_button_cancel: function(event) {
742 if (this.can_be_discarded()) {
743 if (this.get('actual_mode') === 'create') {
744 this.trigger('history_back');
747 $.when.apply(null, this.render_value_defs).then(function(){
748 self.trigger('load_record', self.datarecord);
752 this.trigger('on_button_cancel');
755 on_button_new: function() {
758 return $.when(this.has_been_loaded).then(function() {
759 if (self.can_be_discarded()) {
760 return self.load_defaults();
764 on_button_edit: function() {
765 return this.to_edit_mode();
767 on_button_create: function() {
768 this.dataset.index = null;
771 on_button_duplicate: function() {
773 return this.has_been_loaded.then(function() {
774 return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
775 self.record_created(new_id);
780 on_button_delete: function() {
782 var def = $.Deferred();
783 this.has_been_loaded.done(function() {
784 if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
785 self.dataset.unlink([self.datarecord.id]).done(function() {
786 if (self.dataset.size()) {
787 self.execute_pager_action('next');
789 self.do_action('history_back');
794 $.async_when().done(function () {
799 return def.promise();
801 can_be_discarded: function() {
802 if (this.$el.is('.oe_form_dirty')) {
803 if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
806 this.$el.removeClass('oe_form_dirty');
811 * Triggers saving the form's record. Chooses between creating a new
812 * record or saving an existing one depending on whether the record
813 * already has an id property.
815 * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
816 * record, should that record be inserted at the start of the dataset (by
817 * default, records are added at the end)
819 save: function(prepend_on_create) {
821 var save_obj = {prepend_on_create: prepend_on_create, ret: null};
822 this.save_list.push(save_obj);
823 return this._process_operations().then(function() {
825 return $.Deferred().reject();
826 return $.when.apply($, save_obj.ret);
828 self.$el.removeClass('oe_form_dirty');
831 _process_save: function(save_obj) {
833 var prepend_on_create = save_obj.prepend_on_create;
835 var form_invalid = false,
837 first_invalid_field = null,
838 readonly_values = {};
839 for (var f in self.fields) {
840 if (!self.fields.hasOwnProperty(f)) { continue; }
844 if (!first_invalid_field) {
845 first_invalid_field = f;
847 } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
848 // Special case 'id' field, do not save this field
849 // on 'create' : save all non readonly fields
850 // on 'edit' : save non readonly modified fields
851 if (!f.get("readonly")) {
852 values[f.name] = f.get_value();
854 readonly_values[f.name] = f.get_value();
859 self.set({'display_invalid_fields': true});
860 first_invalid_field.focus();
862 return $.Deferred().reject();
864 self.set({'display_invalid_fields': false});
866 if (!self.datarecord.id) {
868 save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
869 return self.record_created(r, prepend_on_create);
871 } else if (_.isEmpty(values)) {
872 // Not dirty, noop save
873 save_deferral = $.Deferred().resolve({}).promise();
876 save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
877 return self.record_saved(r);
880 return save_deferral;
884 return $.Deferred().reject();
887 on_invalid: function() {
888 var warnings = _(this.fields).chain()
889 .filter(function (f) { return !f.is_valid(); })
891 return _.str.sprintf('<li>%s</li>',
894 warnings.unshift('<ul>');
895 warnings.push('</ul>');
896 this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
899 * Reload the form after saving
901 * @param {Object} r result of the write function.
903 record_saved: function(r) {
904 this.trigger('record_saved', r);
906 // should not happen in the server, but may happen for internal purpose
907 return $.Deferred().reject();
912 * Updates the form' dataset to contain the new record:
914 * * Adds the newly created record to the current dataset (at the end by
916 * * Selects that record (sets the dataset's index to point to the new
918 * * Updates the pager and sidebar displays
921 * @param {Boolean} [prepend_on_create=false] adds the newly created record
922 * at the beginning of the dataset instead of the end
924 record_created: function(r, prepend_on_create) {
927 // should not happen in the server, but may happen for internal purpose
928 this.trigger('record_created', r);
929 return $.Deferred().reject();
931 this.datarecord.id = r;
932 if (!prepend_on_create) {
933 this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
934 this.dataset.index = this.dataset.ids.length - 1;
936 this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
937 this.dataset.index = 0;
939 this.do_update_pager();
941 this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
943 //openerp.log("The record has been created with id #" + this.datarecord.id);
944 return $.when(this.reload()).then(function () {
945 self.trigger('record_created', r);
946 return _.extend(r, {created: true});
950 on_action: function (action) {
951 console.debug('Executing action', action);
955 return this.reload_mutex.exec(function() {
956 if (self.dataset.index == null) {
957 self.trigger("previous_view");
958 return $.Deferred().reject().promise();
960 if (self.dataset.index == null || self.dataset.index < 0) {
961 return $.when(self.on_button_new());
963 var fields = _.keys(self.fields_view.fields);
964 fields.push('display_name');
965 return self.dataset.read_index(fields,
969 'future_display_name': true
971 check_access_rule: true
972 }).then(function(r) {
973 self.trigger('load_record', r);
975 self.do_action('history_back');
980 get_widgets: function() {
981 return _.filter(this.getChildren(), function(obj) {
982 return obj instanceof instance.web.form.FormWidget;
985 get_fields_values: function() {
987 var ids = this.get_selected_ids();
988 values["id"] = ids.length > 0 ? ids[0] : false;
989 _.each(this.fields, function(value_, key) {
990 values[key] = value_.get_value();
994 get_selected_ids: function() {
995 var id = this.dataset.ids[this.dataset.index];
996 return id ? [id] : [];
998 recursive_save: function() {
1000 return $.when(this.save()).then(function(res) {
1001 if (self.dataset.parent_view)
1002 return self.dataset.parent_view.recursive_save();
1005 recursive_reload: function() {
1008 if (self.dataset.parent_view)
1009 pre = self.dataset.parent_view.recursive_reload();
1010 return pre.then(function() {
1011 return self.reload();
1014 is_dirty: function() {
1015 return _.any(this.fields, function (value_) {
1016 return value_._dirty_flag;
1019 is_interactible_record: function() {
1020 var id = this.datarecord.id;
1022 if (this.options.not_interactible_on_create)
1024 } else if (typeof(id) === "string") {
1025 if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
1030 sidebar_eval_context: function () {
1031 return $.when(this.build_eval_context());
1033 open_defaults_dialog: function () {
1035 var display = function (field, value) {
1036 if (field instanceof instance.web.form.FieldSelection) {
1037 return _(field.values).find(function (option) {
1038 return option[0] === value;
1040 } else if (field instanceof instance.web.form.FieldMany2One) {
1041 return field.get_displayed();
1045 var fields = _.chain(this.fields)
1046 .map(function (field) {
1047 var value = field.get_value();
1048 // ignore fields which are empty, invisible, readonly, o2m
1051 || field.get('invisible')
1052 || field.get("readonly")
1053 || field.field.type === 'one2many'
1054 || field.field.type === 'many2many'
1055 || field.field.type === 'binary'
1056 || field.password) {
1062 string: field.string,
1064 displayed: display(field, value),
1068 .sortBy(function (field) { return field.string; })
1070 var conditions = _.chain(self.fields)
1071 .filter(function (field) { return field.field.change_default; })
1072 .map(function (field) {
1073 var value = field.get_value();
1076 string: field.string,
1078 displayed: display(field, value),
1083 var d = new instance.web.Dialog(this, {
1084 title: _t("Set Default"),
1087 conditions: conditions
1090 {text: _t("Close"), click: function () { d.close(); }},
1091 {text: _t("Save default"), click: function () {
1092 var $defaults = d.$el.find('#formview_default_fields');
1093 var field_to_set = $defaults.val();
1094 if (!field_to_set) {
1095 $defaults.parent().addClass('oe_form_invalid');
1098 var condition = d.$el.find('#formview_default_conditions').val(),
1099 all_users = d.$el.find('#formview_default_all').is(':checked');
1100 new instance.web.DataSet(self, 'ir.values').call(
1104 self.fields[field_to_set].get_value(),
1108 ]).done(function () { d.close(); });
1112 d.template = 'FormView.set_default';
1115 register_field: function(field, name) {
1116 this.fields[name] = field;
1117 this.fields_order.push(name);
1118 if (JSON.parse(field.node.attrs.default_focus || "0")) {
1119 this.default_focus_field = field;
1122 field.on('focused', null, this.proxy('widgetFocused'))
1123 .on('blurred', null, this.proxy('widgetBlurred'));
1124 if (this.get_field_desc(name).translate) {
1125 this.translatable_fields.push(field);
1127 field.on('changed_value', this, function() {
1128 if (field.is_syntax_valid()) {
1129 this.trigger('field_changed:' + name);
1131 if (field._inhibit_on_change_flag) {
1134 field._dirty_flag = true;
1135 if (field.is_syntax_valid()) {
1136 this.do_onchange(field);
1137 this.on_form_changed(true);
1138 this.do_notify_change();
1142 get_field_desc: function(field_name) {
1143 return this.fields_view.fields[field_name];
1145 get_field_value: function(field_name) {
1146 return this.fields[field_name].get_value();
1148 compute_domain: function(expression) {
1149 return instance.web.form.compute_domain(expression, this.fields);
1151 _build_view_fields_values: function() {
1152 var a_dataset = this.dataset;
1153 var fields_values = this.get_fields_values();
1154 var active_id = a_dataset.ids[a_dataset.index];
1155 _.extend(fields_values, {
1156 active_id: active_id || false,
1157 active_ids: active_id ? [active_id] : [],
1158 active_model: a_dataset.model,
1161 if (a_dataset.parent_view) {
1162 fields_values.parent = a_dataset.parent_view.get_fields_values();
1164 return fields_values;
1166 build_eval_context: function() {
1167 var a_dataset = this.dataset;
1168 return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
1173 * Interface to be implemented by rendering engines for the form view.
1175 instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
1176 set_fields_view: function(fields_view) {},
1177 set_fields_registry: function(fields_registry) {},
1178 render_to: function($el) {},
1182 * Default rendering engine for the form view.
1184 * It is necessary to set the view using set_view() before usage.
1186 instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
1187 init: function(view) {
1190 set_fields_view: function(fvg) {
1192 this.version = parseFloat(this.fvg.arch.attrs.version);
1193 if (isNaN(this.version)) {
1197 set_tags_registry: function(tags_registry) {
1198 this.tags_registry = tags_registry;
1200 set_fields_registry: function(fields_registry) {
1201 this.fields_registry = fields_registry;
1203 set_widgets_registry: function(widgets_registry) {
1204 this.widgets_registry = widgets_registry;
1206 // Backward compatibility tools, current default version: v6.1
1207 process_version: function() {
1208 if (this.version < 7.0) {
1209 this.$form.find('form:first').wrapInner('<group col="4"/>');
1210 this.$form.find('page').each(function() {
1211 if (!$(this).parents('field').length) {
1212 $(this).wrapInner('<group col="4"/>');
1217 get_arch_fragment: function() {
1218 var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
1219 // IE won't allow custom button@type and will revert it to spec default : 'submit'
1220 $('button', doc).each(function() {
1221 $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
1223 // IE's html parser is also a css parser. How convenient...
1224 $('board', doc).each(function() {
1225 $(this).attr('layout', $(this).attr('style'));
1227 return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
1229 render_to: function($target) {
1231 this.$target = $target;
1233 this.$form = this.get_arch_fragment();
1235 this.process_version();
1237 this.fields_to_init = [];
1238 this.tags_to_init = [];
1239 this.widgets_to_init = [];
1241 this.process(this.$form);
1243 this.$form.appendTo(this.$target);
1245 this.to_replace = [];
1247 _.each(this.fields_to_init, function($elem) {
1248 var name = $elem.attr("name");
1249 if (!self.fvg.fields[name]) {
1250 throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
1252 var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
1254 throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
1256 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1257 var $label = self.labels[$elem.attr("name")];
1259 w.set_input_id($label.attr("for"));
1261 self.alter_field(w);
1262 self.view.register_field(w, $elem.attr("name"));
1263 self.to_replace.push([w, $elem]);
1265 _.each(this.tags_to_init, function($elem) {
1266 var tag_name = $elem[0].tagName.toLowerCase();
1267 var obj = self.tags_registry.get_object(tag_name);
1268 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1269 self.to_replace.push([w, $elem]);
1271 _.each(this.widgets_to_init, function($elem) {
1272 var widget_type = $elem.attr("type");
1273 var obj = self.widgets_registry.get_object(widget_type);
1274 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1275 self.to_replace.push([w, $elem]);
1278 init_fields: function() {
1280 _.each(this.to_replace, function(el) {
1281 defs.push(el[0].replace(el[1]));
1283 this.to_replace = [];
1284 return $.when.apply($, defs);
1286 render_element: function(template /* dictionaries */) {
1287 var dicts = [].slice.call(arguments).slice(1);
1288 var dict = _.extend.apply(_, dicts);
1289 dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
1290 return $(QWeb.render(template, dict));
1292 alter_field: function(field) {
1294 toggle_layout_debugging: function() {
1295 if (!this.$target.has('.oe_layout_debug_cell:first').length) {
1296 this.$target.find('[title]').removeAttr('title');
1297 this.$target.find('.oe_form_group_cell').each(function() {
1298 var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
1299 $(this).attr('title', text);
1302 this.$target.toggleClass('oe_layout_debugging');
1304 process: function($tag) {
1306 var tagname = $tag[0].nodeName.toLowerCase();
1307 if (this.tags_registry.contains(tagname)) {
1308 this.tags_to_init.push($tag);
1311 var fn = self['process_' + tagname];
1313 var args = [].slice.call(arguments);
1315 return fn.apply(self, args);
1317 // generic tag handling, just process children
1318 $tag.children().each(function() {
1319 self.process($(this));
1321 self.handle_common_properties($tag, $tag);
1322 $tag.removeAttr("modifiers");
1326 process_widget: function($widget) {
1327 this.widgets_to_init.push($widget);
1330 process_sheet: function($sheet) {
1331 var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
1332 this.handle_common_properties($new_sheet, $sheet);
1333 var $dst = $new_sheet.find('.oe_form_sheet');
1334 $sheet.contents().appendTo($dst);
1335 $sheet.before($new_sheet).remove();
1336 this.process($new_sheet);
1338 process_form: function($form) {
1339 if ($form.find('> sheet').length === 0) {
1340 $form.addClass('oe_form_nosheet');
1342 var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
1343 this.handle_common_properties($new_form, $form);
1344 $form.contents().appendTo($new_form);
1345 if ($form[0] === this.$form[0]) {
1346 // If root element, replace it
1347 this.$form = $new_form;
1349 $form.before($new_form).remove();
1351 this.process($new_form);
1354 * Used by direct <field> children of a <group> tag only
1355 * This method will add the implicit <label...> for every field
1358 preprocess_field: function($field) {
1360 var name = $field.attr('name'),
1361 field_colspan = parseInt($field.attr('colspan'), 10),
1362 field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
1364 if ($field.attr('nolabel') === '1')
1366 $field.attr('nolabel', '1');
1368 this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
1369 $(el).parents().each(function(unused, tag) {
1370 var name = tag.tagName.toLowerCase();
1371 if (name === "field" || name in self.tags_registry.map)
1378 var $label = $('<label/>').attr({
1380 "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
1381 "string": $field.attr('string'),
1382 "help": $field.attr('help'),
1383 "class": $field.attr('class'),
1385 $label.insertBefore($field);
1386 if (field_colspan > 1) {
1387 $field.attr('colspan', field_colspan - 1);
1391 process_field: function($field) {
1392 if ($field.parent().is('group')) {
1393 // No implicit labels for normal fields, only for <group> direct children
1394 var $label = this.preprocess_field($field);
1396 this.process($label);
1399 this.fields_to_init.push($field);
1402 process_group: function($group) {
1404 $group.children('field').each(function() {
1405 self.preprocess_field($(this));
1407 var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
1409 if ($new_group.first().is('table.oe_form_group')) {
1410 $table = $new_group;
1411 } else if ($new_group.filter('table.oe_form_group').length) {
1412 $table = $new_group.filter('table.oe_form_group').first();
1414 $table = $new_group.find('table.oe_form_group').first();
1418 cols = parseInt($group.attr('col') || 2, 10),
1422 $group.children().each(function(a,b,c) {
1423 var $child = $(this);
1424 var colspan = parseInt($child.attr('colspan') || 1, 10);
1425 var tagName = $child[0].tagName.toLowerCase();
1426 var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
1427 var newline = tagName === 'newline';
1429 // Note FME: those classes are used in layout debug mode
1430 if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
1431 $tr.addClass('oe_form_group_row_incomplete');
1433 $tr.addClass('oe_form_group_row_newline');
1440 if (!$tr || row_cols < colspan) {
1441 $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
1443 } else if (tagName==='group') {
1444 // When <group> <group/><group/> </group>, we need a spacing between the two groups
1445 $td.addClass('oe_group_right')
1447 row_cols -= colspan;
1449 // invisibility transfer
1450 var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
1451 var invisible = field_modifiers.invisible;
1452 self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
1454 $tr.append($td.append($child));
1455 children.push($child[0]);
1457 if (row_cols && $td) {
1458 $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
1460 $group.before($new_group).remove();
1462 $table.find('> tbody > tr').each(function() {
1463 var to_compute = [],
1466 $(this).children().each(function() {
1468 $child = $td.children(':first');
1469 if ($child.attr('cell-class')) {
1470 $td.addClass($child.attr('cell-class'));
1472 switch ($child[0].tagName.toLowerCase()) {
1476 if ($child.attr('for')) {
1477 $td.attr('width', '1%').addClass('oe_form_group_cell_label');
1478 row_cols-= $td.attr('colspan') || 1;
1483 var width = _.str.trim($child.attr('width') || ''),
1484 iwidth = parseInt(width, 10);
1486 if (width.substr(-1) === '%') {
1488 width = iwidth + '%';
1491 $td.css('min-width', width + 'px');
1493 $td.attr('width', width);
1494 $child.removeAttr('width');
1495 row_cols-= $td.attr('colspan') || 1;
1497 to_compute.push($td);
1503 var unit = Math.floor(total / row_cols);
1504 if (!$(this).is('.oe_form_group_row_incomplete')) {
1505 _.each(to_compute, function($td, i) {
1506 var width = parseInt($td.attr('colspan'), 10) * unit;
1507 $td.attr('width', width + '%');
1513 _.each(children, function(el) {
1514 self.process($(el));
1516 this.handle_common_properties($new_group, $group);
1519 process_notebook: function($notebook) {
1522 $notebook.find('> page').each(function() {
1523 var $page = $(this);
1524 var page_attrs = $page.getAttributes();
1525 page_attrs.id = _.uniqueId('notebook_page_');
1526 var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
1527 $page.contents().appendTo($new_page);
1528 $page.before($new_page).remove();
1529 var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
1530 page_attrs.__page = $new_page;
1531 page_attrs.__ic = ic;
1532 pages.push(page_attrs);
1534 $new_page.children().each(function() {
1535 self.process($(this));
1538 var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
1539 $notebook.contents().appendTo($new_notebook);
1540 $notebook.before($new_notebook).remove();
1541 self.process($($new_notebook.children()[0]));
1542 //tabs and invisibility handling
1543 $new_notebook.tabs();
1544 _.each(pages, function(page, i) {
1547 page.__ic.on("change:effective_invisible", null, function() {
1548 if (!page.__ic.get('effective_invisible') && page.autofocus) {
1549 $new_notebook.tabs('select', i);
1552 var current = $new_notebook.tabs("option", "selected");
1553 if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
1555 var first_visible = _.find(_.range(pages.length), function(i2) {
1556 return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
1558 if (first_visible !== undefined) {
1559 $new_notebook.tabs('select', first_visible);
1564 this.handle_common_properties($new_notebook, $notebook);
1565 return $new_notebook;
1567 process_separator: function($separator) {
1568 var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
1569 $separator.before($new_separator).remove();
1570 this.handle_common_properties($new_separator, $separator);
1571 return $new_separator;
1573 process_label: function($label) {
1574 var name = $label.attr("for"),
1575 field_orm = this.fvg.fields[name];
1577 string: $label.attr('string') || (field_orm || {}).string || '',
1578 help: $label.attr('help') || (field_orm || {}).help || '',
1579 _for: name ? _.uniqueId('oe-field-input-') : undefined,
1581 var align = parseFloat(dict.align);
1582 if (isNaN(align) || align === 1) {
1584 } else if (align === 0) {
1590 var $new_label = this.render_element('FormRenderingLabel', dict);
1591 $label.before($new_label).remove();
1592 this.handle_common_properties($new_label, $label);
1594 this.labels[name] = $new_label;
1598 handle_common_properties: function($new_element, $node) {
1599 var str_modifiers = $node.attr("modifiers") || "{}";
1600 var modifiers = JSON.parse(str_modifiers);
1602 if (modifiers.invisible !== undefined)
1603 ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
1604 $new_element.addClass($node.attr("class") || "");
1605 $new_element.attr('style', $node.attr('style'));
1606 return {invisibility_changer: ic,};
1613 If you read this documentation, it probably means that you were asked to use a form view widget outside of
1614 a form view. Before going further, you must understand that those fields were never really created for
1615 that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
1616 you to hack the system with more style.
1618 instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
1619 init: function(parent, eval_context) {
1620 this._super(parent);
1621 this.field_descs = {};
1622 this.eval_context = eval_context || {};
1624 display_invalid_fields: false,
1625 actual_mode: 'create',
1628 get_field_desc: function(field_name) {
1629 if (this.field_descs[field_name] === undefined) {
1630 this.field_descs[field_name] = {
1634 return this.field_descs[field_name];
1636 extend_field_desc: function(fields) {
1638 _.each(fields, function(v, k) {
1639 _.extend(self.get_field_desc(k), v);
1642 get_field_value: function(field_name) {
1645 set_values: function(values) {
1648 compute_domain: function(expression) {
1649 return instance.web.form.compute_domain(expression, {});
1651 build_eval_context: function() {
1652 return new instance.web.CompoundContext(this.eval_context);
1656 instance.web.form.compute_domain = function(expr, fields) {
1657 if (! (expr instanceof Array))
1660 for (var i = expr.length - 1; i >= 0; i--) {
1662 if (ex.length == 1) {
1663 var top = stack.pop();
1666 stack.push(stack.pop() || top);
1669 stack.push(stack.pop() && top);
1675 throw new Error(_.str.sprintf(
1676 _t("Unknown operator %s in domain %s"),
1677 ex, JSON.stringify(expr)));
1681 var field = fields[ex[0]];
1683 throw new Error(_.str.sprintf(
1684 _t("Unknown field %s in domain %s"),
1685 ex[0], JSON.stringify(expr)));
1687 var field_value = field.get_value ? field.get_value() : field.value;
1691 switch (op.toLowerCase()) {
1694 stack.push(_.isEqual(field_value, val));
1698 stack.push(!_.isEqual(field_value, val));
1701 stack.push(field_value < val);
1704 stack.push(field_value > val);
1707 stack.push(field_value <= val);
1710 stack.push(field_value >= val);
1713 if (!_.isArray(val)) val = [val];
1714 stack.push(_(val).contains(field_value));
1717 if (!_.isArray(val)) val = [val];
1718 stack.push(!_(val).contains(field_value));
1722 _t("Unsupported operator %s in domain %s"),
1723 op, JSON.stringify(expr));
1726 return _.all(stack, _.identity);
1729 instance.web.form.is_bin_size = function(v) {
1730 return /^\d+(\.\d*)? \w+$/.test(v);
1734 * Must be applied over an class already possessing the PropertiesMixin.
1736 * Apply the result of the "invisible" domain to this.$el.
1738 instance.web.form.InvisibilityChangerMixin = {
1739 init: function(field_manager, invisible_domain) {
1741 this._ic_field_manager = field_manager;
1742 this._ic_invisible_modifier = invisible_domain;
1743 this._ic_field_manager.on("view_content_has_changed", this, function() {
1744 var result = self._ic_invisible_modifier === undefined ? false :
1745 self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
1746 self.set({"invisible": result});
1748 this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
1749 var check = function() {
1750 if (self.get("invisible") || self.get('force_invisible')) {
1751 self.set({"effective_invisible": true});
1753 self.set({"effective_invisible": false});
1756 this.on('change:invisible', this, check);
1757 this.on('change:force_invisible', this, check);
1761 this.on("change:effective_invisible", this, this._check_visibility);
1762 this._check_visibility();
1764 _check_visibility: function() {
1765 this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
1769 instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
1770 init: function(parent, field_manager, invisible_domain, $el) {
1771 this.setParent(parent);
1772 instance.web.PropertiesMixin.init.call(this);
1773 instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
1780 Base class for all fields, custom widgets and buttons to be displayed in the form view.
1783 - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
1784 the values of the "readonly" property and the "mode" property on the field manager.
1786 instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
1788 * @constructs instance.web.form.FormWidget
1789 * @extends instance.web.Widget
1791 * @param field_manager
1794 init: function(field_manager, node) {
1795 this._super(field_manager);
1796 this.field_manager = field_manager;
1797 if (this.field_manager instanceof instance.web.FormView)
1798 this.view = this.field_manager;
1800 this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
1801 instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
1803 this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
1809 // some events to make the property "effective_readonly" sync automatically with "readonly" and
1810 // "mode" on field_manager
1812 var test_effective_readonly = function() {
1813 self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
1815 this.on("change:readonly", this, test_effective_readonly);
1816 this.field_manager.on("change:actual_mode", this, test_effective_readonly);
1817 test_effective_readonly.call(this);
1819 renderElement: function() {
1820 this.process_modifiers();
1822 this.$el.addClass(this.node.attrs["class"] || "");
1824 destroy: function() {
1826 this._super.apply(this, arguments);
1829 * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
1831 * This method is an utility method that is meant to be called by child classes.
1833 * @param {jQuery} $e jQuery object of elements to bind focus/blur on
1835 setupFocus: function ($e) {
1838 focus: function () { self.trigger('focused'); },
1839 blur: function () { self.trigger('blurred'); }
1842 process_modifiers: function() {
1844 for (var a in this.modifiers) {
1845 if (!this.modifiers.hasOwnProperty(a)) { continue; }
1846 if (!_.include(["invisible"], a)) {
1847 var val = this.field_manager.compute_domain(this.modifiers[a]);
1853 do_attach_tooltip: function(widget, trigger, options) {
1854 widget = widget || this;
1855 trigger = trigger || this.$el;
1856 options = _.extend({
1861 var template = widget.template + '.tooltip';
1862 if (!QWeb.has_template(template)) {
1863 template = 'WidgetLabel.tooltip';
1865 return QWeb.render(template, {
1866 debug: instance.session.debug,
1869 gravity: $.fn.tipsy.autoBounds(50, 'nw'),
1874 $(trigger).tipsy(options);
1877 * Builds a new context usable for operations related to fields by merging
1878 * the fields'context with the action's context.
1880 build_context: function() {
1881 // only use the model's context if there is not context on the node
1882 var v_context = this.node.attrs.context;
1884 v_context = (this.field || {}).context || {};
1887 if (v_context.__ref || true) { //TODO: remove true
1888 var fields_values = this.field_manager.build_eval_context();
1889 v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
1893 build_domain: function() {
1894 var f_domain = this.field.domain || [];
1895 var n_domain = this.node.attrs.domain || null;
1896 // if there is a domain on the node, overrides the model's domain
1897 var final_domain = n_domain !== null ? n_domain : f_domain;
1898 if (!(final_domain instanceof Array) || true) { //TODO: remove true
1899 var fields_values = this.field_manager.build_eval_context();
1900 final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
1902 return final_domain;
1906 instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
1907 template: 'WidgetButton',
1908 init: function(field_manager, node) {
1909 node.attrs.type = node.attrs['data-button-type'];
1910 this._super(field_manager, node);
1911 this.force_disabled = false;
1912 this.string = (this.node.attrs.string || '').replace(/_/g, '');
1913 if (JSON.parse(this.node.attrs.default_focus || "0")) {
1914 // TODO fme: provide enter key binding to widgets
1915 this.view.default_focus_button = this;
1917 if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
1918 this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
1922 this._super.apply(this, arguments);
1923 this.view.on('view_content_has_changed', this, this.check_disable);
1924 this.check_disable();
1925 this.$el.click(this.on_click);
1926 if (this.node.attrs.help || instance.session.debug) {
1927 this.do_attach_tooltip();
1929 this.setupFocus(this.$el);
1931 on_click: function() {
1933 this.force_disabled = true;
1934 this.check_disable();
1935 this.execute_action().always(function() {
1936 self.force_disabled = false;
1937 self.check_disable();
1940 execute_action: function() {
1942 var exec_action = function() {
1943 if (self.node.attrs.confirm) {
1944 var def = $.Deferred();
1945 var dialog = instance.web.dialog($('<div/>').text(self.node.attrs.confirm), {
1946 title: _t('Confirm'),
1949 {text: _t("Cancel"), click: function() {
1950 $(this).dialog("close");
1953 {text: _t("Ok"), click: function() {
1955 self.on_confirmed().always(function() {
1956 $(self2).dialog("close");
1961 beforeClose: function() {
1965 return def.promise();
1967 return self.on_confirmed();
1970 if (!this.node.attrs.special) {
1971 return this.view.recursive_save().then(exec_action);
1973 return exec_action();
1976 on_confirmed: function() {
1979 var context = this.build_context();
1981 return this.view.do_execute_action(
1982 _.extend({}, this.node.attrs, {context: context}),
1983 this.view.dataset, this.view.datarecord.id, function (reason) {
1984 if (!_.isObject(reason)) {
1985 self.view.recursive_reload();
1989 check_disable: function() {
1990 var disabled = (this.force_disabled || !this.view.is_interactible_record());
1991 this.$el.prop('disabled', disabled);
1992 this.$el.css('color', disabled ? 'grey' : '');
1997 * Interface to be implemented by fields.
2000 * - changed_value: triggered when the value of the field has changed. This can be due
2001 * to a user interaction or a call to set_value().
2004 instance.web.form.FieldInterface = {
2006 * Constructor takes 2 arguments:
2007 * - field_manager: Implements FieldManagerMixin
2008 * - node: the "<field>" node in json form
2010 init: function(field_manager, node) {},
2012 * Called by the form view to indicate the value of the field.
2014 * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
2015 * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
2016 * before the widget is inserted into the DOM.
2018 * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
2019 * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
2020 * also be able to handle any other format commonly used in the _defaults key on the models in the addons
2021 * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
2022 * no information is ever given to know which format is used.
2024 set_value: function(value_) {},
2026 * Get the current value of the widget.
2028 * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
2029 * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
2030 * For example if the field is marked as "required", a call to get_value() can return false.
2032 * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
2033 * return a default value according to the type of field.
2035 * This method is always assumed to perform synchronously, it can not return a promise.
2037 * If there was no user interaction to modify the value of the field, it is always assumed that
2038 * get_value() return the same semantic value than the one passed in the last call to set_value(),
2039 * although the syntax can be different. This can be the case for type of fields that have a different
2040 * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
2042 get_value: function() {},
2044 * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
2047 set_input_id: function(id) {},
2049 * Returns true if is_syntax_valid() returns true and the value is semantically
2050 * valid too according to the semantic restrictions applied to the field.
2052 is_valid: function() {},
2054 * Returns true if the field holds a value which is syntactically correct, ignoring
2055 * the potential semantic restrictions applied to the field.
2057 is_syntax_valid: function() {},
2059 * Must set the focus on the field. Return false if field is not focusable.
2061 focus: function() {},
2063 * Called when the translate button is clicked.
2065 on_translate: function() {},
2067 This method is called by the form view before reading on_change values and before saving. It tells
2068 the field to save its value before reading it using get_value(). Must return a promise.
2070 commit_value: function() {},
2074 * Abstract class for classes implementing FieldInterface.
2077 * - value: useful property to hold the value of the field. By default, set_value() and get_value()
2078 * set and retrieve the value property. Changing the value property also triggers automatically
2079 * a 'changed_value' event that inform the view to trigger on_changes.
2082 instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
2084 * @constructs instance.web.form.AbstractField
2085 * @extends instance.web.form.FormWidget
2087 * @param field_manager
2090 init: function(field_manager, node) {
2092 this._super(field_manager, node);
2093 this.name = this.node.attrs.name;
2094 this.field = this.field_manager.get_field_desc(this.name);
2095 this.widget = this.node.attrs.widget;
2096 this.string = this.node.attrs.string || this.field.string || this.name;
2097 this.options = instance.web.py_eval(this.node.attrs.options || '{}');
2098 this.set({'value': false});
2100 this.on("change:value", this, function() {
2101 this.trigger('changed_value');
2102 this._check_css_flags();
2105 renderElement: function() {
2108 if (this.field.translate && this.view) {
2109 this.$el.addClass('oe_form_field_translatable');
2110 this.$el.find('.oe_field_translate').click(this.on_translate);
2112 this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
2113 if (instance.session.debug) {
2114 this.do_attach_tooltip(this, this.$label[0] || this.$el);
2115 this.$label.off('dblclick').on('dblclick', function() {
2116 console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
2118 console.log("window.w =", window.w);
2121 if (!this.disable_utility_classes) {
2122 this.off("change:required", this, this._set_required);
2123 this.on("change:required", this, this._set_required);
2124 this._set_required();
2126 this._check_visibility();
2127 this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
2128 this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
2129 this._check_css_flags();
2132 var tmp = this._super();
2133 this.on("change:value", this, function() {
2134 if (! this.no_rerender)
2135 this.render_value();
2137 this.render_value();
2140 * Private. Do not use.
2142 _set_required: function() {
2143 this.$el.toggleClass('oe_form_required', this.get("required"));
2145 set_value: function(value_) {
2146 this.set({'value': value_});
2148 get_value: function() {
2149 return this.get('value');
2152 Utility method that all implementations should use to change the
2153 value without triggering a re-rendering.
2155 internal_set_value: function(value_) {
2156 var tmp = this.no_rerender;
2157 this.no_rerender = true;
2158 this.set({'value': value_});
2159 this.no_rerender = tmp;
2162 This method is called each time the value is modified.
2164 render_value: function() {},
2165 is_valid: function() {
2166 return this.is_syntax_valid() && !(this.get('required') && this.is_false());
2168 is_syntax_valid: function() {
2172 * Method useful to implement to ease validity testing. Must return true if the current
2173 * value is similar to false in OpenERP.
2175 is_false: function() {
2176 return this.get('value') === false;
2178 _check_css_flags: function() {
2179 if (this.field.translate) {
2180 this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
2182 if (!this.disable_utility_classes) {
2183 if (this.field_manager.get('display_invalid_fields')) {
2184 this.$el.toggleClass('oe_form_invalid', !this.is_valid());
2191 set_input_id: function(id) {
2192 this.id_for_label = id;
2194 on_translate: function() {
2196 var trans = new instance.web.DataSet(this, 'ir.translation');
2197 return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
2202 set_dimensions: function (height, width) {
2208 commit_value: function() {
2214 * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
2217 instance.web.form.ReinitializeWidgetMixin = {
2219 * Default implementation of, you should not override it, use initialize_field() instead.
2222 this.initialize_field();
2225 initialize_field: function() {
2226 this.on("change:effective_readonly", this, this.reinitialize);
2227 this.initialize_content();
2229 reinitialize: function() {
2230 this.destroy_content();
2231 this.renderElement();
2232 this.initialize_content();
2235 * Called to destroy anything that could have been created previously, called before a
2236 * re-initialization.
2238 destroy_content: function() {},
2240 * Called to initialize the content.
2242 initialize_content: function() {},
2246 * A mixin to apply on any field that has to completely re-render when its readonly state
2249 instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
2250 reinitialize: function() {
2251 instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
2252 this.view.render_value_defs.push(this.render_value());
2257 Some hack to make placeholders work in ie9.
2259 if ($.browser.msie && $.browser.version === "9.0") {
2260 document.addEventListener("DOMNodeInserted",function(event){
2261 var nodename = event.target.nodeName.toLowerCase();
2262 if ( nodename === "input" || nodename == "textarea" ) {
2263 $(event.target).placeholder();
2268 instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2269 template: 'FieldChar',
2270 widget_class: 'oe_form_field_char',
2272 'change input': 'store_dom_value',
2274 init: function (field_manager, node) {
2275 this._super(field_manager, node);
2276 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2278 initialize_content: function() {
2279 this.setupFocus(this.$('input'));
2281 store_dom_value: function () {
2282 if (!this.get('effective_readonly')
2283 && this.$('input').length
2284 && this.is_syntax_valid()) {
2285 this.internal_set_value(
2287 this.$('input').val()));
2290 commit_value: function () {
2291 this.store_dom_value();
2292 return this._super();
2294 render_value: function() {
2295 var show_value = this.format_value(this.get('value'), '');
2296 if (!this.get("effective_readonly")) {
2297 this.$el.find('input').val(show_value);
2299 if (this.password) {
2300 show_value = new Array(show_value.length + 1).join('*');
2302 this.$(".oe_form_char_content").text(show_value);
2305 is_syntax_valid: function() {
2306 if (!this.get("effective_readonly") && this.$("input").size() > 0) {
2308 this.parse_value(this.$('input').val(), '');
2316 parse_value: function(val, def) {
2317 return instance.web.parse_value(val, this, def);
2319 format_value: function(val, def) {
2320 return instance.web.format_value(val, this, def);
2322 is_false: function() {
2323 return this.get('value') === '' || this._super();
2326 var input = this.$('input:first')[0];
2327 return input ? input.focus() : false;
2329 set_dimensions: function (height, width) {
2330 this._super(height, width);
2331 this.$('input').css({
2338 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2339 process_modifiers: function () {
2341 this.set({ readonly: true });
2345 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2346 template: 'FieldEmail',
2347 initialize_content: function() {
2349 var $button = this.$el.find('button');
2350 $button.click(this.on_button_clicked);
2351 this.setupFocus($button);
2353 render_value: function() {
2354 if (!this.get("effective_readonly")) {
2358 .attr('href', 'mailto:' + this.get('value'))
2359 .text(this.get('value') || '');
2362 on_button_clicked: function() {
2363 if (!this.get('value') || !this.is_syntax_valid()) {
2364 this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2366 location.href = 'mailto:' + this.get('value');
2371 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2372 template: 'FieldUrl',
2373 initialize_content: function() {
2375 var $button = this.$el.find('button');
2376 $button.click(this.on_button_clicked);
2377 this.setupFocus($button);
2379 render_value: function() {
2380 if (!this.get("effective_readonly")) {
2383 var tmp = this.get('value');
2384 var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2386 tmp = "http://" + this.get('value');
2388 var text = this.get('value') ? this.node.attrs.text || tmp : '';
2389 this.$el.find('a').attr('href', tmp).text(text);
2392 on_button_clicked: function() {
2393 if (!this.get('value')) {
2394 this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2396 var url = $.trim(this.get('value'));
2397 if(/^www\./i.test(url))
2398 url = 'http://'+url;
2404 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2405 is_field_number: true,
2406 widget_class: 'oe_form_field_float',
2407 init: function (field_manager, node) {
2408 this._super(field_manager, node);
2409 this.internal_set_value(0);
2410 if (this.node.attrs.digits) {
2411 this.digits = this.node.attrs.digits;
2413 this.digits = this.field.digits;
2416 set_value: function(value_) {
2417 if (value_ === false || value_ === undefined) {
2418 // As in GTK client, floats default to 0
2421 this._super.apply(this, [value_]);
2423 focus: function () {
2424 var $input = this.$('input:first');
2425 return $input.length ? $input.select() : false;
2429 instance.web.DateTimeWidget = instance.web.Widget.extend({
2430 template: "web.datepicker",
2431 jqueryui_object: 'datetimepicker',
2432 type_of_date: "datetime",
2434 'change .oe_datepicker_master': 'change_datetime',
2435 'keypress .oe_datepicker_master': 'change_datetime',
2437 init: function(parent) {
2438 this._super(parent);
2439 this.name = parent.name;
2443 this.$input = this.$el.find('input.oe_datepicker_master');
2444 this.$input_picker = this.$el.find('input.oe_datepicker_container');
2446 $.datepicker.setDefaults({
2447 clearText: _t('Clear'),
2448 clearStatus: _t('Erase the current date'),
2449 closeText: _t('Done'),
2450 closeStatus: _t('Close without change'),
2451 prevText: _t('<Prev'),
2452 prevStatus: _t('Show the previous month'),
2453 nextText: _t('Next>'),
2454 nextStatus: _t('Show the next month'),
2455 currentText: _t('Today'),
2456 currentStatus: _t('Show the current month'),
2457 monthNames: Date.CultureInfo.monthNames,
2458 monthNamesShort: Date.CultureInfo.abbreviatedMonthNames,
2459 monthStatus: _t('Show a different month'),
2460 yearStatus: _t('Show a different year'),
2461 weekHeader: _t('Wk'),
2462 weekStatus: _t('Week of the year'),
2463 dayNames: Date.CultureInfo.dayNames,
2464 dayNamesShort: Date.CultureInfo.abbreviatedDayNames,
2465 dayNamesMin: Date.CultureInfo.shortestDayNames,
2466 dayStatus: _t('Set DD as first week day'),
2467 dateStatus: _t('Select D, M d'),
2468 firstDay: Date.CultureInfo.firstDayOfWeek,
2469 initStatus: _t('Select a date'),
2472 $.timepicker.setDefaults({
2473 timeOnlyTitle: _t('Choose Time'),
2474 timeText: _t('Time'),
2475 hourText: _t('Hour'),
2476 minuteText: _t('Minute'),
2477 secondText: _t('Second'),
2478 currentText: _t('Now'),
2479 closeText: _t('Done')
2483 onClose: this.on_picker_select,
2484 onSelect: this.on_picker_select,
2488 showButtonPanel: true,
2489 firstDay: Date.CultureInfo.firstDayOfWeek
2491 // Some clicks in the datepicker dialog are not stopped by the
2492 // datepicker and "bubble through", unexpectedly triggering the bus's
2493 // click event. Prevent that.
2494 this.picker('widget').click(function (e) { e.stopPropagation(); });
2496 this.$el.find('img.oe_datepicker_trigger').click(function() {
2497 if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
2498 self.$input.focus();
2501 self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date());
2502 self.$input_picker.show();
2503 self.picker('show');
2504 self.$input_picker.hide();
2506 this.set_readonly(false);
2507 this.set({'value': false});
2509 picker: function() {
2510 return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
2512 on_picker_select: function(text, instance_) {
2513 var date = this.picker('getDate');
2515 .val(date ? this.format_client(date) : '')
2519 set_value: function(value_) {
2520 this.set({'value': value_});
2521 this.$input.val(value_ ? this.format_client(value_) : '');
2523 get_value: function() {
2524 return this.get('value');
2526 set_value_from_ui_: function() {
2527 var value_ = this.$input.val() || false;
2528 this.set({'value': this.parse_client(value_)});
2530 set_readonly: function(readonly) {
2531 this.readonly = readonly;
2532 this.$input.prop('readonly', this.readonly);
2533 this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
2535 is_valid_: function() {
2536 var value_ = this.$input.val();
2537 if (value_ === "") {
2541 this.parse_client(value_);
2548 parse_client: function(v) {
2549 return instance.web.parse_value(v, {"widget": this.type_of_date});
2551 format_client: function(v) {
2552 return instance.web.format_value(v, {"widget": this.type_of_date});
2554 change_datetime: function(e) {
2555 if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
2556 this.set_value_from_ui_();
2557 this.trigger("datetime_changed");
2560 commit_value: function () {
2561 this.change_datetime();
2565 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2566 jqueryui_object: 'datepicker',
2567 type_of_date: "date"
2570 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2571 template: "FieldDatetime",
2572 build_widget: function() {
2573 return new instance.web.DateTimeWidget(this);
2575 destroy_content: function() {
2576 if (this.datewidget) {
2577 this.datewidget.destroy();
2578 this.datewidget = undefined;
2581 initialize_content: function() {
2582 if (!this.get("effective_readonly")) {
2583 this.datewidget = this.build_widget();
2584 this.datewidget.on('datetime_changed', this, _.bind(function() {
2585 this.internal_set_value(this.datewidget.get_value());
2587 this.datewidget.appendTo(this.$el);
2588 this.setupFocus(this.datewidget.$input);
2591 render_value: function() {
2592 if (!this.get("effective_readonly")) {
2593 this.datewidget.set_value(this.get('value'));
2595 this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2598 is_syntax_valid: function() {
2599 if (!this.get("effective_readonly") && this.datewidget) {
2600 return this.datewidget.is_valid_();
2604 is_false: function() {
2605 return this.get('value') === '' || this._super();
2608 var input = this.datewidget && this.datewidget.$input[0];
2609 return input ? input.focus() : false;
2611 set_dimensions: function (height, width) {
2612 this._super(height, width);
2613 if (!this.get("effective_readonly")) {
2614 this.datewidget.$input.css('height', height);
2619 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2620 template: "FieldDate",
2621 build_widget: function() {
2622 return new instance.web.DateWidget(this);
2626 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2627 template: 'FieldText',
2629 'keyup': function (e) {
2630 if (e.which === $.ui.keyCode.ENTER) {
2631 e.stopPropagation();
2634 'keypress': function (e) {
2635 if (e.which === $.ui.keyCode.ENTER) {
2636 e.stopPropagation();
2639 'change textarea': 'store_dom_value',
2641 initialize_content: function() {
2643 if (! this.get("effective_readonly")) {
2644 this.$textarea = this.$el.find('textarea');
2645 this.auto_sized = false;
2646 this.default_height = this.$textarea.css('height');
2647 if (this.get("effective_readonly")) {
2648 this.$textarea.attr('disabled', 'disabled');
2650 this.setupFocus(this.$textarea);
2652 this.$textarea = undefined;
2655 commit_value: function () {
2656 if (! this.get("effective_readonly") && this.$textarea) {
2657 this.store_dom_value();
2659 return this._super();
2661 store_dom_value: function () {
2662 this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2664 render_value: function() {
2665 if (! this.get("effective_readonly")) {
2666 var show_value = instance.web.format_value(this.get('value'), this, '');
2667 if (show_value === '') {
2668 this.$textarea.css('height', parseInt(this.default_height)+"px");
2670 this.$textarea.val(show_value);
2671 if (! this.auto_sized) {
2672 this.auto_sized = true;
2673 this.$textarea.autosize();
2675 this.$textarea.trigger("autosize");
2678 var txt = this.get("value") || '';
2679 this.$(".oe_form_text_content").text(txt);
2682 is_syntax_valid: function() {
2683 if (!this.get("effective_readonly") && this.$textarea) {
2685 instance.web.parse_value(this.$textarea.val(), this, '');
2693 is_false: function() {
2694 return this.get('value') === '' || this._super();
2696 focus: function($el) {
2697 var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2698 return input ? input.focus() : false;
2700 set_dimensions: function (height, width) {
2701 this._super(height, width);
2702 if (!this.get("effective_readonly") && this.$textarea) {
2703 this.$textarea.css({
2712 * FieldTextHtml Widget
2713 * Intended for FieldText widgets meant to display HTML content. This
2714 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2715 * To find more information about CLEditor configutation: go to
2716 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2718 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2719 template: 'FieldTextHtml',
2721 this._super.apply(this, arguments);
2723 initialize_content: function() {
2725 if (! this.get("effective_readonly")) {
2726 self._updating_editor = false;
2727 this.$textarea = this.$el.find('textarea');
2728 var width = ((this.node.attrs || {}).editor_width || 'calc(100% - 4px)');
2729 var height = ((this.node.attrs || {}).editor_height || 250);
2730 this.$textarea.cleditor({
2731 width: width, // width not including margins, borders or padding
2732 height: height, // height not including margins, borders or padding
2733 controls: // controls to add to the toolbar
2734 "bold italic underline strikethrough " +
2735 "| removeformat | bullets numbering | outdent " +
2736 "indent | link unlink | source",
2737 bodyStyle: // style to assign to document body contained within the editor
2738 "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2740 this.$cleditor = this.$textarea.cleditor()[0];
2741 this.$cleditor.change(function() {
2742 if (! self._updating_editor) {
2743 self.$cleditor.updateTextArea();
2744 self.internal_set_value(self.$textarea.val());
2747 if (this.field.translate) {
2748 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"/>')
2749 .click(this.on_translate);
2750 this.$cleditor.$toolbar.append($img);
2754 render_value: function() {
2755 if (! this.get("effective_readonly")) {
2756 this.$textarea.val(this.get('value') || '');
2757 this._updating_editor = true;
2758 this.$cleditor.updateFrame();
2759 this._updating_editor = false;
2761 this.$el.html(this.get('value'));
2766 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2767 template: 'FieldBoolean',
2770 this.$checkbox = $("input", this.$el);
2771 this.setupFocus(this.$checkbox);
2772 this.$el.click(_.bind(function() {
2773 this.internal_set_value(this.$checkbox.is(':checked'));
2775 var check_readonly = function() {
2776 self.$checkbox.prop('disabled', self.get("effective_readonly"));
2778 this.on("change:effective_readonly", this, check_readonly);
2779 check_readonly.call(this);
2780 this._super.apply(this, arguments);
2782 render_value: function() {
2783 this.$checkbox[0].checked = this.get('value');
2786 var input = this.$checkbox && this.$checkbox[0];
2787 return input ? input.focus() : false;
2792 The progressbar field expect a float from 0 to 100.
2794 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2795 template: 'FieldProgressBar',
2796 render_value: function() {
2797 this.$el.progressbar({
2798 value: this.get('value') || 0,
2799 disabled: this.get("effective_readonly")
2801 var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
2802 this.$('span').html(formatted_value + '%');
2807 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2808 template: 'FieldSelection',
2810 'change select': 'store_dom_value',
2812 init: function(field_manager, node) {
2814 this._super(field_manager, node);
2815 this.values = _(this.field.selection).chain()
2816 .reject(function (v) { return v[0] === false && v[1] === ''; })
2817 .unshift([false, ''])
2820 initialize_content: function() {
2821 // Flag indicating whether we're in an event chain containing a change
2822 // event on the select, in order to know what to do on keyup[RETURN]:
2823 // * If the user presses [RETURN] as part of changing the value of a
2824 // selection, we should just let the value change and not let the
2825 // event broadcast further (e.g. to validating the current state of
2826 // the form in editable list view, which would lead to saving the
2827 // current row or switching to the next one)
2828 // * If the user presses [RETURN] with a select closed (side-effect:
2829 // also if the user opened the select and pressed [RETURN] without
2830 // changing the selected value), takes the action as validating the
2832 var ischanging = false;
2833 var $select = this.$el.find('select')
2834 .change(function () { ischanging = true; })
2835 .click(function () { ischanging = false; })
2836 .keyup(function (e) {
2837 if (e.which !== 13 || !ischanging) { return; }
2838 e.stopPropagation();
2841 this.setupFocus($select);
2843 commit_value: function () {
2844 this.store_dom_value();
2845 return this._super();
2847 store_dom_value: function () {
2848 if (!this.get('effective_readonly') && this.$('select').length) {
2849 this.internal_set_value(
2850 this.values[this.$('select')[0].selectedIndex][0]);
2853 set_value: function(value_) {
2854 value_ = value_ === null ? false : value_;
2855 value_ = value_ instanceof Array ? value_[0] : value_;
2856 this._super(value_);
2858 render_value: function() {
2859 if (!this.get("effective_readonly")) {
2861 for (var i = 0, ii = this.values.length; i < ii; i++) {
2862 if (this.values[i][0] === this.get('value')) index = i;
2864 this.$el.find('select')[0].selectedIndex = index;
2867 var option = _(this.values)
2868 .detect(function (record) { return record[0] === self.get('value'); });
2869 this.$el.text(option ? option[1] : this.values[0][1]);
2873 var input = this.$('select:first')[0];
2874 return input ? input.focus() : false;
2876 set_dimensions: function (height, width) {
2877 this._super(height, width);
2878 this.$('select').css({
2885 // jquery autocomplete tweak to allow html and classnames
2887 var proto = $.ui.autocomplete.prototype,
2888 initSource = proto._initSource;
2890 function filter( array, term ) {
2891 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
2892 return $.grep( array, function(value_) {
2893 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
2898 _initSource: function() {
2899 if ( this.options.html && $.isArray(this.options.source) ) {
2900 this.source = function( request, response ) {
2901 response( filter( this.options.source, request.term ) );
2904 initSource.call( this );
2908 _renderItem: function( ul, item) {
2909 return $( "<li></li>" )
2910 .data( "item.autocomplete", item )
2911 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
2913 .addClass(item.classname);
2919 * A mixin containing some useful methods to handle completion inputs.
2921 instance.web.form.CompletionFieldMixin = {
2924 this.orderer = new instance.web.DropMisordered();
2927 * Call this method to search using a string.
2929 get_search_result: function(search_val) {
2932 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
2933 var blacklist = this.get_search_blacklist();
2934 this.last_query = search_val;
2936 return this.orderer.add(dataset.name_search(
2937 search_val, new instance.web.CompoundDomain(self.build_domain(), [["id", "not in", blacklist]]),
2938 'ilike', this.limit + 1, self.build_context())).then(function(data) {
2939 self.last_search = data;
2940 // possible selections for the m2o
2941 var values = _.map(data, function(x) {
2942 x[1] = x[1].split("\n")[0];
2944 label: _.str.escapeHTML(x[1]),
2951 // search more... if more results that max
2952 if (values.length > self.limit) {
2953 values = values.slice(0, self.limit);
2955 label: _t("Search More..."),
2956 action: function() {
2957 dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
2958 self._search_create_popup("search", data);
2961 classname: 'oe_m2o_dropdown_option'
2965 var raw_result = _(data.result).map(function(x) {return x[1];});
2966 if (search_val.length > 0 && !_.include(raw_result, search_val)) {
2968 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
2969 $('<span />').text(search_val).html()),
2970 action: function() {
2971 self._quick_create(search_val);
2973 classname: 'oe_m2o_dropdown_option'
2978 label: _t("Create and Edit..."),
2979 action: function() {
2980 self._search_create_popup("form", undefined, self._create_context(search_val));
2982 classname: 'oe_m2o_dropdown_option'
2988 get_search_blacklist: function() {
2991 _quick_create: function(name) {
2993 var slow_create = function () {
2994 self._search_create_popup("form", undefined, self._create_context(name));
2996 if (self.options.quick_create === undefined || self.options.quick_create) {
2997 new instance.web.DataSet(this, this.field.relation, self.build_context())
2998 .name_create(name).done(function(data) {
2999 if (!self.get('effective_readonly'))
3000 self.add_id(data[0]);
3001 }).fail(function(error, event) {
3002 event.preventDefault();
3008 // all search/create popup handling
3009 _search_create_popup: function(view, ids, context) {
3011 var pop = new instance.web.form.SelectCreatePopup(this);
3013 self.field.relation,
3015 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3016 initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
3018 disable_multiple_selection: true
3020 self.build_domain(),
3021 new instance.web.CompoundContext(self.build_context(), context || {})
3023 pop.on("elements_selected", self, function(element_ids) {
3024 self.add_id(element_ids[0]);
3031 add_id: function(id) {},
3032 _create_context: function(name) {
3034 var field = (this.options || {}).create_name_field;
3035 if (field === undefined)
3037 if (field !== false && name && (this.options || {}).quick_create !== false)
3038 tmp["default_" + field] = name;
3043 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3044 template: "M2ODialog",
3045 init: function(parent) {
3046 this._super(parent, {
3047 title: _.str.sprintf(_t("Add %s"), parent.string),
3053 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3054 this.$("input").val(this.getParent().last_query);
3055 this.$buttons.find(".oe_form_m2o_qc_button").click(function(){
3056 self.getParent()._quick_create(self.$("input").val());
3059 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3060 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3063 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3069 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3070 template: "FieldMany2One",
3072 'keydown input': function (e) {
3074 case $.ui.keyCode.UP:
3075 case $.ui.keyCode.DOWN:
3076 e.stopPropagation();
3080 init: function(field_manager, node) {
3081 this._super(field_manager, node);
3082 instance.web.form.CompletionFieldMixin.init.call(this);
3083 this.set({'value': false});
3084 this.display_value = {};
3085 this.last_search = [];
3086 this.floating = false;
3087 this.current_display = null;
3088 this.is_started = false;
3089 this.ignore_focusout = false;
3091 reinit_value: function(val) {
3092 this.internal_set_value(val);
3093 this.floating = false;
3094 if (this.is_started)
3095 this.render_value();
3097 initialize_field: function() {
3098 this.is_started = true;
3099 instance.web.bus.on('click', this, function() {
3100 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3101 this.$input.autocomplete("close");
3104 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3106 initialize_content: function() {
3107 if (!this.get("effective_readonly"))
3108 this.render_editable();
3110 destroy_content: function () {
3111 if (this.$drop_down) {
3112 this.$drop_down.off('click');
3113 delete this.$drop_down;
3116 this.$input.closest(".ui-dialog .ui-dialog-content").off('scroll');
3117 this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3118 'focus focusout change keydown');
3121 if (this.$follow_button) {
3122 this.$follow_button.off('blur focus click');
3123 delete this.$follow_button;
3126 destroy: function () {
3127 this.destroy_content();
3128 return this._super();
3130 init_error_displayer: function() {
3133 hide_error_displayer: function() {
3136 show_error_displayer: function() {
3137 new instance.web.form.M2ODialog(this).open();
3139 render_editable: function() {
3141 this.$input = this.$el.find("input");
3143 this.init_error_displayer();
3145 self.$input.on('focus', function() {
3146 self.hide_error_displayer();
3149 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3150 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3152 this.$follow_button.click(function(ev) {
3153 ev.preventDefault();
3154 if (!self.get('value')) {
3158 var pop = new instance.web.form.FormOpenPopup(self);
3160 self.field.relation,
3162 self.build_context(),
3164 title: _t("Open: ") + self.string
3167 pop.on('write_completed', self, function(){
3168 self.display_value = {};
3169 self.render_value();
3171 self.trigger('changed_value');
3175 // some behavior for input
3176 var input_changed = function() {
3177 if (self.current_display !== self.$input.val()) {
3178 self.current_display = self.$input.val();
3179 if (self.$input.val() === "") {
3180 self.internal_set_value(false);
3181 self.floating = false;
3183 self.floating = true;
3187 this.$input.keydown(input_changed);
3188 this.$input.change(input_changed);
3189 this.$drop_down.click(function() {
3190 if (self.$input.autocomplete("widget").is(":visible")) {
3191 self.$input.autocomplete("close");
3192 self.$input.focus();
3194 if (self.get("value") && ! self.floating) {
3195 self.$input.autocomplete("search", "");
3197 self.$input.autocomplete("search");
3202 // Autocomplete close on dialog content scroll
3203 var close_autocomplete = _.debounce(function() {
3204 if (self.$input.autocomplete("widget").is(":visible")) {
3205 self.$input.autocomplete("close");
3208 this.$input.closest(".ui-dialog .ui-dialog-content").on('scroll', this, close_autocomplete);
3210 self.ed_def = $.Deferred();
3211 self.uned_def = $.Deferred();
3213 var ed_duration = 15000;
3214 var anyoneLoosesFocus = function (e) {
3215 if (self.ignore_focusout) { return; }
3217 if (self.floating) {
3218 if (self.last_search.length > 0) {
3219 if (self.last_search[0][0] != self.get("value")) {
3220 self.display_value = {};
3221 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3222 self.reinit_value(self.last_search[0][0]);
3225 self.render_value();
3229 self.reinit_value(false);
3231 self.floating = false;
3233 if (used && self.get("value") === false && ! self.no_ed) {
3234 self.ed_def.reject();
3235 self.uned_def.reject();
3236 self.ed_def = $.Deferred();
3237 self.ed_def.done(function() {
3238 self.show_error_displayer();
3239 ignore_blur = false;
3240 self.trigger('focused');
3243 setTimeout(function() {
3244 self.ed_def.resolve();
3245 self.uned_def.reject();
3246 self.uned_def = $.Deferred();
3247 self.uned_def.done(function() {
3248 self.hide_error_displayer();
3250 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3254 self.ed_def.reject();
3257 var ignore_blur = false;
3259 focusout: anyoneLoosesFocus,
3260 focus: function () { self.trigger('focused'); },
3261 autocompleteopen: function () { ignore_blur = true; },
3262 autocompleteclose: function () { ignore_blur = false; },
3264 // autocomplete open
3265 if (ignore_blur) { return; }
3266 if (_(self.getChildren()).any(function (child) {
3267 return child instanceof instance.web.form.AbstractFormPopup;
3269 self.trigger('blurred');
3273 var isSelecting = false;
3275 this.$input.autocomplete({
3276 source: function(req, resp) {
3277 self.get_search_result(req.term).done(function(result) {
3281 select: function(event, ui) {
3285 self.display_value = {};
3286 self.display_value["" + item.id] = item.name;
3287 self.reinit_value(item.id);
3288 } else if (item.action) {
3290 // Cancel widget blurring, to avoid form blur event
3291 self.trigger('focused');
3295 focus: function(e, ui) {
3299 // disabled to solve a bug, but may cause others
3300 //close: anyoneLoosesFocus,
3304 this.$input.autocomplete("widget").openerpClass();
3305 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3306 this.$input.keyup(function(e) {
3307 if (e.which === 13) { // ENTER
3309 e.stopPropagation();
3311 isSelecting = false;
3313 this.setupFocus(this.$follow_button);
3315 render_value: function(no_recurse) {
3317 if (! this.get("value")) {
3318 this.display_string("");
3321 var display = this.display_value["" + this.get("value")];
3323 this.display_string(display);
3327 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3328 this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3330 self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
3333 self.display_value["" + self.get("value")] = data[0][1];
3334 self.render_value(true);
3338 display_string: function(str) {
3340 if (!this.get("effective_readonly")) {
3341 this.$input.val(str.split("\n")[0]);
3342 this.current_display = this.$input.val();
3343 if (this.is_false()) {
3344 this.$('.oe_m2o_cm_button').css({'display':'none'});
3346 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3349 var lines = _.escape(str).split("\n");
3353 follow = _.rest(lines).join("<br />");
3356 var $link = this.$el.find('.oe_form_uri')
3359 if (! this.options.no_open)
3360 $link.click(function () {
3362 type: 'ir.actions.act_window',
3363 res_model: self.field.relation,
3364 res_id: self.get("value"),
3365 views: [[false, 'form']],
3367 context: self.build_context().eval(),
3371 $(".oe_form_m2o_follow", this.$el).html(follow);
3374 set_value: function(value_) {
3376 if (value_ instanceof Array) {
3377 this.display_value = {};
3378 if (! this.options.always_reload) {
3379 this.display_value["" + value_[0]] = value_[1];
3383 value_ = value_ || false;
3384 this.reinit_value(value_);
3386 get_displayed: function() {
3387 return this.display_value["" + this.get("value")];
3389 add_id: function(id) {
3390 this.display_value = {};
3391 this.reinit_value(id);
3393 is_false: function() {
3394 return ! this.get("value");
3396 focus: function () {
3397 var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3398 return input ? input.focus() : false;
3400 _quick_create: function() {
3402 this.ed_def.reject();
3403 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3405 _search_create_popup: function() {
3407 this.ed_def.reject();
3408 this.ignore_focusout = true;
3409 this.reinit_value(false);
3410 var res = instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3411 this.ignore_focusout = false;
3415 set_dimensions: function (height, width) {
3416 this._super(height, width);
3417 if (!this.get("effective_readonly") && this.$input)
3418 this.$input.css('height', height);
3422 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3423 template: 'Many2OneButton',
3424 init: function(field_manager, node) {
3425 this._super.apply(this, arguments);
3428 this._super.apply(this, arguments);
3431 set_button: function() {
3434 this.$button.remove();
3437 this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3438 this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3439 this.$button.addClass('oe_link').css({'padding':'4px'});
3440 this.$el.append(this.$button);
3441 this.$button.on('click', self.on_click);
3443 on_click: function(ev) {
3445 this.popup = new instance.web.form.FormOpenPopup(this);
3446 this.popup.show_element(
3447 this.field.relation,
3449 this.build_context(),
3450 {title: this.string}
3452 this.popup.on('create_completed', self, function(r) {
3456 set_value: function(value_) {
3458 if (value_ instanceof Array) {
3461 value_ = value_ || false;
3462 this.set('value', value_);
3468 # Values: (0, 0, { fields }) create
3469 # (1, ID, { fields }) update
3470 # (2, ID) remove (delete)
3471 # (3, ID) unlink one (target id or target of relation)
3473 # (5) unlink all (only valid for one2many)
3478 'create': function (values) {
3479 return [commands.CREATE, false, values];
3481 // (1, id, {values})
3483 'update': function (id, values) {
3484 return [commands.UPDATE, id, values];
3488 'delete': function (id) {
3489 return [commands.DELETE, id, false];
3491 // (3, id[, _]) removes relation, but not linked record itself
3493 'forget': function (id) {
3494 return [commands.FORGET, id, false];
3498 'link_to': function (id) {
3499 return [commands.LINK_TO, id, false];
3503 'delete_all': function () {
3504 return [5, false, false];
3506 // (6, _, ids) replaces all linked records with provided ids
3508 'replace_with': function (ids) {
3509 return [6, false, ids];
3512 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3513 multi_selection: false,
3514 disable_utility_classes: true,
3515 init: function(field_manager, node) {
3516 this._super(field_manager, node);
3517 lazy_build_o2m_kanban_view();
3518 this.is_loaded = $.Deferred();
3519 this.initial_is_loaded = this.is_loaded;
3520 this.form_last_update = $.Deferred();
3521 this.init_form_last_update = this.form_last_update;
3522 this.is_started = false;
3523 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
3524 this.dataset.o2m = this;
3525 this.dataset.parent_view = this.view;
3526 this.dataset.child_name = this.name;
3528 this.dataset.on('dataset_changed', this, function() {
3529 self.trigger_on_change();
3534 this._super.apply(this, arguments);
3535 this.$el.addClass('oe_form_field oe_form_field_one2many');
3540 this.is_loaded.done(function() {
3541 self.on("change:effective_readonly", self, function() {
3542 self.is_loaded = self.is_loaded.then(function() {
3543 self.viewmanager.destroy();
3544 return $.when(self.load_views()).done(function() {
3545 self.reload_current_view();
3550 this.is_started = true;
3551 this.reload_current_view();
3553 trigger_on_change: function() {
3554 this.trigger('changed_value');
3556 load_views: function() {
3559 var modes = this.node.attrs.mode;
3560 modes = !!modes ? modes.split(",") : ["tree"];
3562 _.each(modes, function(mode) {
3563 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
3564 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
3568 view_type: mode == "tree" ? "list" : mode,
3571 if (self.field.views && self.field.views[mode]) {
3572 view.embedded_view = self.field.views[mode];
3574 if(view.view_type === "list") {
3575 _.extend(view.options, {
3577 selectable: self.multi_selection,
3579 import_enabled: false,
3582 if (self.get("effective_readonly")) {
3583 _.extend(view.options, {
3588 } else if (view.view_type === "form") {
3589 if (self.get("effective_readonly")) {
3590 view.view_type = 'form';
3592 _.extend(view.options, {
3593 not_interactible_on_create: true,
3595 } else if (view.view_type === "kanban") {
3596 _.extend(view.options, {
3597 confirm_on_delete: false,
3599 if (self.get("effective_readonly")) {
3600 _.extend(view.options, {
3601 action_buttons: false,
3602 quick_creatable: false,
3604 read_only_mode: true,
3612 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
3613 this.viewmanager.o2m = self;
3614 var once = $.Deferred().done(function() {
3615 self.init_form_last_update.resolve();
3617 var def = $.Deferred().done(function() {
3618 self.initial_is_loaded.resolve();
3620 this.viewmanager.on("controller_inited", self, function(view_type, controller) {
3621 controller.o2m = self;
3622 if (view_type == "list") {
3623 if (self.get("effective_readonly")) {
3624 controller.on('edit:before', self, function (e) {
3627 _(controller.columns).find(function (column) {
3628 if (!(column instanceof instance.web.list.Handle)) {
3631 column.modifiers.invisible = true;
3635 } else if (view_type === "form") {
3636 if (self.get("effective_readonly")) {
3637 $(".oe_form_buttons", controller.$el).children().remove();
3639 controller.on("load_record", self, function(){
3642 controller.on('pager_action_executed',self,self.save_any_view);
3643 } else if (view_type == "graph") {
3644 self.reload_current_view()
3648 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
3649 $.when(self.save_any_view()).done(function() {
3650 if (n_mode === "list") {
3651 $.async_when().done(function() {
3652 self.reload_current_view();
3657 $.async_when().done(function () {
3658 self.viewmanager.appendTo(self.$el);
3662 reload_current_view: function() {
3664 return self.is_loaded = self.is_loaded.then(function() {
3665 var active_view = self.viewmanager.active_view;
3666 var view = self.viewmanager.views[active_view].controller;
3667 if(active_view === "list") {
3668 return view.reload_content();
3669 } else if (active_view === "form") {
3670 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
3671 self.dataset.index = 0;
3673 var act = function() {
3674 return view.do_show();
3676 self.form_last_update = self.form_last_update.then(act, act);
3677 return self.form_last_update;
3678 } else if (view.do_search) {
3679 return view.do_search(self.build_domain(), self.dataset.get_context(), []);
3683 set_value: function(value_) {
3684 value_ = value_ || [];
3686 this.dataset.reset_ids([]);
3687 if(value_.length >= 1 && value_[0] instanceof Array) {
3689 _.each(value_, function(command) {
3690 var obj = {values: command[2]};
3691 switch (command[0]) {
3692 case commands.CREATE:
3693 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3695 self.dataset.to_create.push(obj);
3696 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3699 case commands.UPDATE:
3700 obj['id'] = command[1];
3701 self.dataset.to_write.push(obj);
3702 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3705 case commands.DELETE:
3706 self.dataset.to_delete.push({id: command[1]});
3708 case commands.LINK_TO:
3709 ids.push(command[1]);
3711 case commands.DELETE_ALL:
3712 self.dataset.delete_all = true;
3717 this.dataset.set_ids(ids);
3718 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
3720 this.dataset.delete_all = true;
3721 _.each(value_, function(command) {
3722 var obj = {values: command};
3723 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3725 self.dataset.to_create.push(obj);
3726 self.dataset.cache.push(_.clone(obj));
3730 this.dataset.set_ids(ids);
3732 this._super(value_);
3733 this.dataset.reset_ids(value_);
3735 if (this.dataset.index === null && this.dataset.ids.length > 0) {
3736 this.dataset.index = 0;
3738 this.trigger_on_change();
3739 if (this.is_started) {
3740 return self.reload_current_view();
3745 get_value: function() {
3749 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
3750 val = val.concat(_.map(this.dataset.ids, function(id) {
3751 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
3753 return commands.create(alter_order.values);
3755 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
3757 return commands.update(alter_order.id, alter_order.values);
3759 return commands.link_to(id);
3761 return val.concat(_.map(
3762 this.dataset.to_delete, function(x) {
3763 return commands['delete'](x.id);}));
3765 commit_value: function() {
3766 return this.save_any_view();
3768 save_any_view: function() {
3769 if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
3770 this.viewmanager.views[this.viewmanager.active_view] &&
3771 this.viewmanager.views[this.viewmanager.active_view].controller) {
3772 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3773 if (this.viewmanager.active_view === "form") {
3774 if (!view.is_initialized.state() === 'resolved') {
3775 return $.when(false);
3777 return $.when(view.save());
3778 } else if (this.viewmanager.active_view === "list") {
3779 return $.when(view.ensure_saved());
3782 return $.when(false);
3784 is_syntax_valid: function() {
3785 if (! this.viewmanager || ! this.viewmanager.views[this.viewmanager.active_view])
3787 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3788 switch (this.viewmanager.active_view) {
3790 return _(view.fields).chain()
3796 return view.is_valid();
3802 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
3803 template: 'One2Many.viewmanager',
3804 init: function(parent, dataset, views, flags) {
3805 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
3806 this.registry = this.registry.extend({
3807 list: 'instance.web.form.One2ManyListView',
3808 form: 'instance.web.form.One2ManyFormView',
3809 kanban: 'instance.web.form.One2ManyKanbanView',
3811 this.__ignore_blur = false;
3813 switch_mode: function(mode, unused) {
3814 if (mode !== 'form') {
3815 return this._super(mode, unused);
3818 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
3819 var pop = new instance.web.form.FormOpenPopup(this);
3820 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3821 title: _t("Open: ") + self.o2m.string,
3822 create_function: function(data, options) {
3823 return self.o2m.dataset.create(data, options).done(function(r) {
3824 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3825 self.o2m.dataset.trigger("dataset_changed", r);
3828 write_function: function(id, data, options) {
3829 return self.o2m.dataset.write(id, data, {}).done(function() {
3830 self.o2m.reload_current_view();
3833 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3834 parent_view: self.o2m.view,
3835 child_name: self.o2m.name,
3836 read_function: function() {
3837 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3839 form_view_options: {'not_interactible_on_create':true},
3840 readonly: self.o2m.get("effective_readonly")
3842 pop.on("elements_selected", self, function() {
3843 self.o2m.reload_current_view();
3848 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
3849 get_context: function() {
3850 this.context = this.o2m.build_context();
3851 return this.context;
3855 instance.web.form.One2ManyListView = instance.web.ListView.extend({
3856 _template: 'One2Many.listview',
3857 init: function (parent, dataset, view_id, options) {
3858 this._super(parent, dataset, view_id, _.extend(options || {}, {
3859 GroupsType: instance.web.form.One2ManyGroups,
3860 ListType: instance.web.form.One2ManyList
3862 this.on('edit:after', this, this.proxy('_after_edit'));
3863 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
3866 .bind('add', this.proxy("changed_records"))
3867 .bind('edit', this.proxy("changed_records"))
3868 .bind('remove', this.proxy("changed_records"));
3870 start: function () {
3871 var ret = this._super();
3873 .off('mousedown.handleButtons')
3874 .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
3877 changed_records: function () {
3878 this.o2m.trigger_on_change();
3880 is_valid: function () {
3881 var editor = this.editor;
3882 var form = editor.form;
3883 // If no edition is pending, the listview can not be invalid (?)
3884 if (!editor.record) {
3887 // If the form has not been modified, the view can only be valid
3888 // NB: is_dirty will also be set on defaults/onchanges/whatever?
3889 // oe_form_dirty seems to only be set on actual user actions
3890 if (!form.$el.is('.oe_form_dirty')) {
3893 this.o2m._dirty_flag = true;
3895 // Otherwise validate internal form
3896 return _(form.fields).chain()
3897 .invoke(function () {
3898 this._check_css_flags();
3899 return this.is_valid();
3904 do_add_record: function () {
3905 if (this.editable()) {
3906 this._super.apply(this, arguments);
3909 var pop = new instance.web.form.SelectCreatePopup(this);
3911 self.o2m.field.relation,
3913 title: _t("Create: ") + self.o2m.string,
3914 initial_view: "form",
3915 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3916 create_function: function(data, options) {
3917 return self.o2m.dataset.create(data, options).done(function(r) {
3918 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3919 self.o2m.dataset.trigger("dataset_changed", r);
3922 read_function: function() {
3923 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3925 parent_view: self.o2m.view,
3926 child_name: self.o2m.name,
3927 form_view_options: {'not_interactible_on_create':true}
3929 self.o2m.build_domain(),
3930 self.o2m.build_context()
3932 pop.on("elements_selected", self, function() {
3933 self.o2m.reload_current_view();
3937 do_activate_record: function(index, id) {
3939 var pop = new instance.web.form.FormOpenPopup(self);
3940 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3941 title: _t("Open: ") + self.o2m.string,
3942 write_function: function(id, data) {
3943 return self.o2m.dataset.write(id, data, {}).done(function() {
3944 self.o2m.reload_current_view();
3947 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3948 parent_view: self.o2m.view,
3949 child_name: self.o2m.name,
3950 read_function: function() {
3951 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3953 form_view_options: {'not_interactible_on_create':true},
3954 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
3957 do_button_action: function (name, id, callback) {
3958 if (!_.isNumber(id)) {
3959 instance.webclient.notification.warn(
3960 _t("Action Button"),
3961 _t("The o2m record must be saved before an action can be used"));
3964 var parent_form = this.o2m.view;
3966 this.ensure_saved().then(function () {
3968 return parent_form.save();
3971 }).done(function () {
3972 var ds = self.o2m.dataset;
3973 var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
3974 return value.length;
3976 if (!self.o2m.options.reload_on_button && !cached_records) {
3977 self.handle_button(name, id, callback);
3979 self.handle_button(name, id, function(){
3980 self.o2m.view.reload();
3986 _after_edit: function () {
3987 this.__ignore_blur = false;
3988 this.editor.form.on('blurred', this, this._on_form_blur);
3990 // The form's blur thing may be jiggered during the edition setup,
3991 // potentially leading to the o2m instasaving the row. Cancel any
3992 // blurring triggered the edition startup here
3993 this.editor.form.widgetFocused();
3995 _before_unedit: function () {
3996 this.editor.form.off('blurred', this, this._on_form_blur);
3998 _button_down: function () {
3999 // If a button is clicked (usually some sort of action button), it's
4000 // the button's responsibility to ensure the editable list is in the
4001 // correct state -> ignore form blurring
4002 this.__ignore_blur = true;
4005 * Handles blurring of the nested form (saves the currently edited row),
4006 * unless the flag to ignore the event is set to ``true``
4008 * Makes the internal form go away
4010 _on_form_blur: function () {
4011 if (this.__ignore_blur) {
4012 this.__ignore_blur = false;
4015 // FIXME: why isn't there an API for this?
4016 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4017 this.ensure_saved();
4020 this.cancel_edition();
4022 keypress_ENTER: function () {
4023 // blurring caused by hitting the [Return] key, should skip the
4024 // autosave-on-blur and let the handler for [Return] do its thing (save
4025 // the current row *anyway*, then create a new one/edit the next one)
4026 this.__ignore_blur = true;
4027 this._super.apply(this, arguments);
4029 do_delete: function (ids) {
4030 var confirm = window.confirm;
4031 window.confirm = function () { return true; };
4033 return this._super(ids);
4035 window.confirm = confirm;
4038 reload_record: function (record) {
4039 // Evict record.id from cache to ensure it will be reloaded correctly
4040 this.dataset.evict_record(record.get('id'));
4042 return this._super(record);
4045 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4046 setup_resequence_rows: function () {
4047 if (!this.view.o2m.get('effective_readonly')) {
4048 this._super.apply(this, arguments);
4052 instance.web.form.One2ManyList = instance.web.ListView.List.extend({
4053 pad_table_to: function (count) {
4054 if (!this.view.is_action_enabled('create')) {
4057 this._super(count > 0 ? count - 1 : 0);
4060 // magical invocation of wtf does that do
4061 if (this.view.o2m.get('effective_readonly')) {
4066 var columns = _(this.columns).filter(function (column) {
4067 return column.invisible !== '1';
4069 if (this.options.selectable) { columns++; }
4070 if (this.options.deletable) { columns++; }
4072 if (!this.view.is_action_enabled('create')) {
4076 var $cell = $('<td>', {
4078 'class': 'oe_form_field_one2many_list_row_add'
4080 $('<a>', {href: '#'}).text(_t("Add an item"))
4081 .mousedown(function () {
4082 // FIXME: needs to be an official API somehow
4083 if (self.view.editor.is_editing()) {
4084 self.view.__ignore_blur = true;
4087 .click(function (e) {
4089 e.stopPropagation();
4090 // FIXME: there should also be an API for that one
4091 if (self.view.editor.form.__blur_timeout) {
4092 clearTimeout(self.view.editor.form.__blur_timeout);
4093 self.view.editor.form.__blur_timeout = false;
4095 self.view.ensure_saved().done(function () {
4096 self.view.do_add_record();
4100 var $padding = this.$current.find('tr:not([data-id]):first');
4101 var $newrow = $('<tr>').append($cell);
4102 if ($padding.length) {
4103 $padding.before($newrow);
4105 this.$current.append($newrow)
4110 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4111 form_template: 'One2Many.formview',
4112 load_form: function(data) {
4115 this.$buttons.find('button.oe_form_button_create').click(function() {
4116 self.save().done(self.on_button_new);
4119 do_notify_change: function() {
4120 if (this.dataset.parent_view) {
4121 this.dataset.parent_view.do_notify_change();
4123 this._super.apply(this, arguments);
4128 var lazy_build_o2m_kanban_view = function() {
4129 if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
4131 instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4135 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4136 template: "FieldMany2ManyTags",
4138 this._super.apply(this, arguments);
4139 instance.web.form.CompletionFieldMixin.init.call(this);
4140 this.set({"value": []});
4141 this._display_orderer = new instance.web.DropMisordered();
4142 this._drop_shown = false;
4144 initialize_content: function() {
4145 if (this.get("effective_readonly"))
4148 var ignore_blur = false;
4149 self.$text = this.$("textarea");
4150 self.$text.textext({
4151 plugins : 'tags arrow autocomplete',
4153 render: function(suggestion) {
4154 return $('<span class="text-label"/>').
4155 data('index', suggestion['index']).html(suggestion['label']);
4160 selectFromDropdown: function() {
4161 this.trigger('hideDropdown');
4162 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4163 var data = self.search_result[index];
4165 self.add_id(data.id);
4170 this.trigger('setSuggestions', {result : []});
4174 isTagAllowed: function(tag) {
4178 removeTag: function(tag) {
4179 var id = tag.data("id");
4180 self.set({"value": _.without(self.get("value"), id)});
4182 renderTag: function(stuff) {
4183 return $.fn.textext.TextExtTags.prototype.renderTag.
4184 call(this, stuff).data("id", stuff.id);
4188 itemToString: function(item) {
4193 onSetInputData: function(e, data) {
4195 this._plugins.autocomplete._suggestions = null;
4197 this.input().val(data);
4201 }).bind('getSuggestions', function(e, data) {
4203 var str = !!data ? data.query || '' : '';
4204 self.get_search_result(str).done(function(result) {
4205 self.search_result = result;
4206 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4207 return _.extend(el, {index:i});
4210 }).bind('hideDropdown', function() {
4211 self._drop_shown = false;
4212 }).bind('showDropdown', function() {
4213 self._drop_shown = true;
4215 self.tags = self.$text.textext()[0].tags();
4217 .focusin(function () {
4218 self.trigger('focused');
4219 ignore_blur = false;
4221 .focusout(function() {
4222 self.$text.trigger("setInputData", "");
4224 self.trigger('blurred');
4226 }).keydown(function(e) {
4227 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4228 self.$text.textext()[0].autocomplete().selectFromDropdown();
4232 set_value: function(value_) {
4233 value_ = value_ || [];
4234 if (value_.length >= 1 && value_[0] instanceof Array) {
4235 value_ = value_[0][2];
4237 this._super(value_);
4239 is_false: function() {
4240 return _(this.get("value")).isEmpty();
4242 get_value: function() {
4243 var tmp = [commands.replace_with(this.get("value"))];
4246 get_search_blacklist: function() {
4247 return this.get("value");
4249 render_value: function() {
4251 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4252 var values = self.get("value");
4253 var handle_names = function(data) {
4254 if (self.isDestroyed())
4257 _.each(data, function(el) {
4258 indexed[el[0]] = el;
4260 data = _.map(values, function(el) { return indexed[el]; });
4261 if (! self.get("effective_readonly")) {
4262 self.tags.containerElement().children().remove();
4263 self.$('textarea').css("padding-left", "3px");
4264 self.tags.addTags(_.map(data, function(el) {return {name: el[1], id:el[0]};}));
4266 self.$el.html(QWeb.render("FieldMany2ManyTag", {elements: data}));
4269 if (! values || values.length > 0) {
4270 return this._display_orderer.add(dataset.name_get(values)).done(handle_names);
4275 add_id: function(id) {
4276 this.set({'value': _.uniq(this.get('value').concat([id]))});
4278 focus: function () {
4279 var input = this.$text && this.$text[0];
4280 return input ? input.focus() : false;
4286 - reload_on_button: Reload the whole form view if click on a button in a list view.
4287 If you see this options, do not use it, it's basically a dirty hack to make one
4288 precise o2m to behave the way we want.
4290 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4291 multi_selection: false,
4292 disable_utility_classes: true,
4293 init: function(field_manager, node) {
4294 this._super(field_manager, node);
4295 this.is_loaded = $.Deferred();
4296 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4297 this.dataset.m2m = this;
4299 this.dataset.on('unlink', self, function(ids) {
4300 self.dataset_changed();
4303 this.list_dm = new instance.web.DropMisordered();
4304 this.render_value_dm = new instance.web.DropMisordered();
4306 initialize_content: function() {
4309 this.$el.addClass('oe_form_field oe_form_field_many2many');
4311 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4312 'addable': this.get("effective_readonly") ? null : _t("Add"),
4313 'deletable': this.get("effective_readonly") ? false : true,
4314 'selectable': this.multi_selection,
4316 'reorderable': false,
4317 'import_enabled': false,
4319 var embedded = (this.field.views || {}).tree;
4321 this.list_view.set_embedded_view(embedded);
4323 this.list_view.m2m_field = this;
4324 var loaded = $.Deferred();
4325 this.list_view.on("list_view_loaded", this, function() {
4328 this.list_view.appendTo(this.$el);
4330 var old_def = self.is_loaded;
4331 self.is_loaded = $.Deferred().done(function() {
4334 this.list_dm.add(loaded).then(function() {
4335 self.is_loaded.resolve();
4338 destroy_content: function() {
4339 this.list_view.destroy();
4340 this.list_view = undefined;
4342 set_value: function(value_) {
4343 value_ = value_ || [];
4344 if (value_.length >= 1 && value_[0] instanceof Array) {
4345 value_ = value_[0][2];
4347 this._super(value_);
4349 get_value: function() {
4350 return [commands.replace_with(this.get('value'))];
4352 is_false: function () {
4353 return _(this.get("value")).isEmpty();
4355 render_value: function() {
4357 this.dataset.set_ids(this.get("value"));
4358 this.render_value_dm.add(this.is_loaded).then(function() {
4359 return self.list_view.reload_content();
4362 dataset_changed: function() {
4363 this.internal_set_value(this.dataset.ids);
4367 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4368 get_context: function() {
4369 this.context = this.m2m.build_context();
4370 return this.context;
4376 * @extends instance.web.ListView
4378 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4379 do_add_record: function () {
4380 var pop = new instance.web.form.SelectCreatePopup(this);
4384 title: _t("Add: ") + this.m2m_field.string,
4385 no_create: this.m2m_field.options.no_create,
4387 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4388 this.m2m_field.build_context()
4391 pop.on("elements_selected", self, function(element_ids) {
4393 _(element_ids).each(function (id) {
4394 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4395 self.dataset.set_ids(self.dataset.ids.concat([id]));
4396 self.m2m_field.dataset_changed();
4401 self.reload_content();
4405 do_activate_record: function(index, id) {
4407 var pop = new instance.web.form.FormOpenPopup(this);
4408 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4409 title: _t("Open: ") + this.m2m_field.string,
4410 readonly: this.getParent().get("effective_readonly")
4412 pop.on('write_completed', self, self.reload_content);
4414 do_button_action: function(name, id, callback) {
4416 var _sup = _.bind(this._super, this);
4417 if (! this.m2m_field.options.reload_on_button) {
4418 return _sup(name, id, callback);
4420 return this.m2m_field.view.save().then(function() {
4421 return _sup(name, id, function() {
4422 self.m2m_field.view.reload();
4427 is_action_enabled: function () { return true; },
4430 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4431 disable_utility_classes: true,
4432 init: function(field_manager, node) {
4433 this._super(field_manager, node);
4434 instance.web.form.CompletionFieldMixin.init.call(this);
4435 m2m_kanban_lazy_init();
4436 this.is_loaded = $.Deferred();
4437 this.initial_is_loaded = this.is_loaded;
4440 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4441 this.dataset.m2m = this;
4442 this.dataset.on('unlink', self, function(ids) {
4443 self.dataset_changed();
4447 this._super.apply(this, arguments);
4452 self.on("change:effective_readonly", self, function() {
4453 self.is_loaded = self.is_loaded.then(function() {
4454 self.kanban_view.destroy();
4455 return $.when(self.load_view()).done(function() {
4456 self.render_value();
4461 set_value: function(value_) {
4462 value_ = value_ || [];
4463 if (value_.length >= 1 && value_[0] instanceof Array) {
4464 value_ = value_[0][2];
4466 this._super(value_);
4468 get_value: function() {
4469 return [commands.replace_with(this.get('value'))];
4471 load_view: function() {
4473 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4474 'create_text': _t("Add"),
4475 'creatable': self.get("effective_readonly") ? false : true,
4476 'quick_creatable': self.get("effective_readonly") ? false : true,
4477 'read_only_mode': self.get("effective_readonly") ? true : false,
4478 'confirm_on_delete': false,
4480 var embedded = (this.field.views || {}).kanban;
4482 this.kanban_view.set_embedded_view(embedded);
4484 this.kanban_view.m2m = this;
4485 var loaded = $.Deferred();
4486 this.kanban_view.on("kanban_view_loaded",self,function() {
4487 self.initial_is_loaded.resolve();
4490 this.kanban_view.on('switch_mode', this, this.open_popup);
4491 $.async_when().done(function () {
4492 self.kanban_view.appendTo(self.$el);
4496 render_value: function() {
4498 this.dataset.set_ids(this.get("value"));
4499 this.is_loaded = this.is_loaded.then(function() {
4500 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
4503 dataset_changed: function() {
4504 this.set({'value': this.dataset.ids});
4506 open_popup: function(type, unused) {
4507 if (type !== "form")
4510 if (this.dataset.index === null) {
4511 var pop = new instance.web.form.SelectCreatePopup(this);
4513 this.field.relation,
4515 title: _t("Add: ") + this.string
4517 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
4518 this.build_context()
4520 pop.on("elements_selected", self, function(element_ids) {
4521 _.each(element_ids, function(one_id) {
4522 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
4523 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
4524 self.dataset_changed();
4525 self.render_value();
4530 var id = self.dataset.ids[self.dataset.index];
4531 var pop = new instance.web.form.FormOpenPopup(this);
4532 pop.show_element(self.field.relation, id, self.build_context(), {
4533 title: _t("Open: ") + self.string,
4534 write_function: function(id, data, options) {
4535 return self.dataset.write(id, data, {}).done(function() {
4536 self.render_value();
4539 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
4540 parent_view: self.view,
4541 child_name: self.name,
4542 readonly: self.get("effective_readonly")
4546 add_id: function(id) {
4547 this.quick_create.add_id(id);
4551 function m2m_kanban_lazy_init() {
4552 if (instance.web.form.Many2ManyKanbanView)
4554 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4555 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
4556 _is_quick_create_enabled: function() {
4557 return this._super() && ! this.group_by;
4560 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
4561 template: 'Many2ManyKanban.quick_create',
4564 * close_btn: If true, the widget will display a "Close" button able to trigger
4567 init: function(parent, dataset, context, buttons) {
4568 this._super(parent);
4569 this.m2m = this.getParent().view.m2m;
4570 this.m2m.quick_create = this;
4571 this._dataset = dataset;
4572 this._buttons = buttons || false;
4573 this._context = context || {};
4575 start: function () {
4577 self.$text = this.$el.find('input').css("width", "200px");
4578 self.$text.textext({
4579 plugins : 'arrow autocomplete',
4581 render: function(suggestion) {
4582 return $('<span class="text-label"/>').
4583 data('index', suggestion['index']).html(suggestion['label']);
4588 selectFromDropdown: function() {
4589 $(this).trigger('hideDropdown');
4590 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4591 var data = self.search_result[index];
4593 self.add_id(data.id);
4600 itemToString: function(item) {
4605 }).bind('getSuggestions', function(e, data) {
4607 var str = !!data ? data.query || '' : '';
4608 self.m2m.get_search_result(str).done(function(result) {
4609 self.search_result = result;
4610 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4611 return _.extend(el, {index:i});
4615 self.$text.focusout(function() {
4620 this.$text[0].focus();
4622 add_id: function(id) {
4625 self.trigger('added', id);
4626 this.m2m.dataset_changed();
4632 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
4634 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
4635 template: "AbstractFormPopup.render",
4638 * -readonly: only applicable when not in creation mode, default to false
4639 * - alternative_form_view
4646 * - form_view_options
4648 init_popup: function(model, row_id, domain, context, options) {
4649 this.row_id = row_id;
4651 this.domain = domain || [];
4652 this.context = context || {};
4653 this.options = options;
4654 _.defaults(this.options, {
4657 init_dataset: function() {
4659 this.created_elements = [];
4660 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
4661 this.dataset.read_function = this.options.read_function;
4662 this.dataset.create_function = function(data, options, sup) {
4663 var fct = self.options.create_function || sup;
4664 return fct.call(this, data, options).done(function(r) {
4665 self.trigger('create_completed saved', r);
4666 self.created_elements.push(r);
4669 this.dataset.write_function = function(id, data, options, sup) {
4670 var fct = self.options.write_function || sup;
4671 return fct.call(this, id, data, options).done(function(r) {
4672 self.trigger('write_completed saved', r);
4675 this.dataset.parent_view = this.options.parent_view;
4676 this.dataset.child_name = this.options.child_name;
4678 display_popup: function() {
4680 this.renderElement();
4681 var dialog = new instance.web.Dialog(this, {
4683 dialogClass: 'oe_act_window',
4685 self.check_exit(true);
4687 title: this.options.title || "",
4688 }, this.$el).open();
4689 this.$buttonpane = dialog.$buttons;
4692 setup_form_view: function() {
4695 this.dataset.ids = [this.row_id];
4696 this.dataset.index = 0;
4698 this.dataset.index = null;
4700 var options = _.clone(self.options.form_view_options) || {};
4701 if (this.row_id !== null) {
4702 options.initial_mode = this.options.readonly ? "view" : "edit";
4705 $buttons: this.$buttonpane,
4707 this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
4708 if (this.options.alternative_form_view) {
4709 this.view_form.set_embedded_view(this.options.alternative_form_view);
4711 this.view_form.appendTo(this.$el.find(".oe_popup_form"));
4712 this.view_form.on("form_view_loaded", self, function() {
4713 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
4714 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
4715 multi_select: multi_select,
4716 readonly: self.row_id !== null && self.options.readonly,
4718 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
4719 $snbutton.click(function() {
4720 $.when(self.view_form.save()).done(function() {
4721 self.view_form.reload_mutex.exec(function() {
4722 self.view_form.on_button_new();
4726 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
4727 $sbutton.click(function() {
4728 $.when(self.view_form.save()).done(function() {
4729 self.view_form.reload_mutex.exec(function() {
4734 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
4735 $cbutton.click(function() {
4736 self.view_form.trigger('on_button_cancel');
4739 self.view_form.do_show();
4742 select_elements: function(element_ids) {
4743 this.trigger("elements_selected", element_ids);
4745 check_exit: function(no_destroy) {
4746 if (this.created_elements.length > 0) {
4747 this.select_elements(this.created_elements);
4748 this.created_elements = [];
4750 this.trigger('closed');
4753 destroy: function () {
4754 this.trigger('closed');
4755 if (this.$el.is(":data(dialog)")) {
4756 this.$el.dialog('close');
4763 * Class to display a popup containing a form view.
4765 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
4766 show_element: function(model, row_id, context, options) {
4767 this.init_popup(model, row_id, [], context, options);
4768 _.defaults(this.options, {
4770 this.display_popup();
4774 this.init_dataset();
4775 this.setup_form_view();
4780 * Class to display a popup to display a list to search a row. It also allows
4781 * to switch to a form view to create a new row.
4783 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
4787 * - initial_view: form or search (default search)
4788 * - disable_multiple_selection
4789 * - list_view_options
4791 select_element: function(model, options, domain, context) {
4792 this.init_popup(model, null, domain, context, options);
4794 _.defaults(this.options, {
4795 initial_view: "search",
4797 this.initial_ids = this.options.initial_ids;
4798 this.display_popup();
4802 this.init_dataset();
4803 if (this.options.initial_view == "search") {
4804 instance.web.pyeval.eval_domains_and_contexts({
4806 contexts: [this.context]
4807 }).done(function (results) {
4808 var search_defaults = {};
4809 _.each(results.context, function (value_, key) {
4810 var match = /^search_default_(.*)$/.exec(key);
4812 search_defaults[match[1]] = value_;
4815 self.setup_search_view(search_defaults);
4821 setup_search_view: function(search_defaults) {
4823 if (this.searchview) {
4824 this.searchview.destroy();
4826 this.searchview = new instance.web.SearchView(this,
4827 this.dataset, false, search_defaults);
4828 this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
4829 if (self.initial_ids) {
4830 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
4831 contexts.concat(self.context), groupbys);
4832 self.initial_ids = undefined;
4834 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
4837 this.searchview.on("search_view_loaded", self, function() {
4838 self.view_list = new instance.web.form.SelectCreateListView(self,
4839 self.dataset, false,
4840 _.extend({'deletable': false,
4841 'selectable': !self.options.disable_multiple_selection,
4842 'import_enabled': false,
4843 '$buttons': self.$buttonpane,
4844 'disable_editable_mode': true,
4845 '$pager': self.$('.oe_popup_list_pager'),
4846 }, self.options.list_view_options || {}));
4847 self.view_list.on('edit:before', self, function (e) {
4850 self.view_list.popup = self;
4851 self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
4852 self.view_list.do_show();
4853 }).then(function() {
4854 self.searchview.do_search();
4856 self.view_list.on("list_view_loaded", self, function() {
4857 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
4858 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
4859 $cbutton.click(function() {
4862 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
4863 $sbutton.click(function() {
4864 self.select_elements(self.selected_ids);
4867 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
4868 $cbutton.click(function() {
4873 this.searchview.appendTo($(".oe_popup_search", self.$el));
4875 do_search: function(domains, contexts, groupbys) {
4877 instance.web.pyeval.eval_domains_and_contexts({
4878 domains: domains || [],
4879 contexts: contexts || [],
4880 group_by_seq: groupbys || []
4881 }).done(function (results) {
4882 self.view_list.do_search(results.domain, results.context, results.group_by);
4885 on_click_element: function(ids) {
4887 this.selected_ids = ids || [];
4888 if(this.selected_ids.length > 0) {
4889 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
4891 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
4894 new_object: function() {
4895 if (this.searchview) {
4896 this.searchview.hide();
4898 if (this.view_list) {
4899 this.view_list.do_hide();
4901 this.setup_form_view();
4905 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
4906 do_add_record: function () {
4907 this.popup.new_object();
4909 select_record: function(index) {
4910 this.popup.select_elements([this.dataset.ids[index]]);
4911 this.popup.destroy();
4913 do_select: function(ids, records) {
4914 this._super(ids, records);
4915 this.popup.on_click_element(ids);
4919 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4920 template: 'FieldReference',
4921 init: function(field_manager, node) {
4922 this._super(field_manager, node);
4923 this.reference_ready = true;
4925 destroy_content: function() {
4928 this.fm = undefined;
4931 initialize_content: function() {
4933 var fm = new instance.web.form.DefaultFieldManager(this);
4935 fm.extend_field_desc({
4937 selection: this.field_manager.get_field_desc(this.name).selection,
4945 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
4947 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
4949 this.selection.on("change:value", this, this.on_selection_changed);
4950 this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
4952 .on('focused', null, function () {self.trigger('focused')})
4953 .on('blurred', null, function () {self.trigger('blurred')});
4955 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
4956 name: 'Referenced Document',
4957 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
4959 this.m2o.on("change:value", this, this.data_changed);
4960 this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
4962 .on('focused', null, function () {self.trigger('focused')})
4963 .on('blurred', null, function () {self.trigger('blurred')});
4965 on_selection_changed: function() {
4966 if (this.reference_ready) {
4967 this.internal_set_value([this.selection.get_value(), false]);
4968 this.render_value();
4971 data_changed: function() {
4972 if (this.reference_ready) {
4973 this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
4976 set_value: function(val) {
4978 val = val.split(',');
4979 val[0] = val[0] || false;
4980 val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
4982 this._super(val || [false, false]);
4984 get_value: function() {
4985 return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
4987 render_value: function() {
4988 this.reference_ready = false;
4989 if (!this.get("effective_readonly")) {
4990 this.selection.set_value(this.get('value')[0]);
4992 this.m2o.field.relation = this.get('value')[0];
4993 this.m2o.set_value(this.get('value')[1]);
4994 this.m2o.$el.toggle(!!this.get('value')[0]);
4995 this.reference_ready = true;
4999 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5000 init: function(field_manager, node) {
5002 this._super(field_manager, node);
5003 this.binary_value = false;
5004 this.useFileAPI = !!window.FileReader;
5005 this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5006 if (!this.useFileAPI) {
5007 this.fileupload_id = _.uniqueId('oe_fileupload');
5008 $(window).on(this.fileupload_id, function() {
5009 var args = [].slice.call(arguments).slice(1);
5010 self.on_file_uploaded.apply(self, args);
5015 if (!this.useFileAPI) {
5016 $(window).off(this.fileupload_id);
5018 this._super.apply(this, arguments);
5020 initialize_content: function() {
5021 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5022 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5023 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5025 on_file_change: function(e) {
5027 var file_node = e.target;
5028 if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5029 if (this.useFileAPI) {
5030 var file = file_node.files[0];
5031 if (file.size > this.max_upload_size) {
5032 var msg = _t("The selected file exceed the maximum file size of %s.");
5033 instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5036 var filereader = new FileReader();
5037 filereader.readAsDataURL(file);
5038 filereader.onloadend = function(upload) {
5039 var data = upload.target.result;
5040 data = data.split(',')[1];
5041 self.on_file_uploaded(file.size, file.name, file.type, data);
5044 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5045 this.$el.find('form.oe_form_binary_form').submit();
5047 this.$el.find('.oe_form_binary_progress').show();
5048 this.$el.find('.oe_form_binary').hide();
5051 on_file_uploaded: function(size, name, content_type, file_base64) {
5052 if (size === false) {
5053 this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5054 // TODO: use openerp web crashmanager
5055 console.warn("Error while uploading file : ", name);
5057 this.filename = name;
5058 this.on_file_uploaded_and_valid.apply(this, arguments);
5060 this.$el.find('.oe_form_binary_progress').hide();
5061 this.$el.find('.oe_form_binary').show();
5063 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5065 on_save_as: function(ev) {
5066 var value = this.get('value');
5068 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5069 ev.stopPropagation();
5071 instance.web.blockUI();
5072 var c = instance.webclient.crashmanager;
5073 this.session.get_file({
5074 url: '/web/binary/saveas_ajax',
5075 data: {data: JSON.stringify({
5076 model: this.view.dataset.model,
5077 id: (this.view.datarecord.id || ''),
5079 filename_field: (this.node.attrs.filename || ''),
5080 data: instance.web.form.is_bin_size(value) ? null : value,
5081 context: this.view.dataset.get_context()
5083 complete: instance.web.unblockUI,
5084 error: c.rpc_error.bind(c)
5086 ev.stopPropagation();
5090 set_filename: function(value) {
5091 var filename = this.node.attrs.filename;
5094 tmp[filename] = value;
5095 this.field_manager.set_values(tmp);
5098 on_clear: function() {
5099 if (this.get('value') !== false) {
5100 this.binary_value = false;
5101 this.internal_set_value(false);
5107 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5108 template: 'FieldBinaryFile',
5109 initialize_content: function() {
5111 if (this.get("effective_readonly")) {
5113 this.$el.find('a').click(function(ev) {
5114 if (self.get('value')) {
5115 self.on_save_as(ev);
5121 render_value: function() {
5122 if (!this.get("effective_readonly")) {
5124 if (this.node.attrs.filename) {
5125 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5127 show_value = (this.get('value') != null && this.get('value') !== false) ? this.get('value') : '';
5129 this.$el.find('input').eq(0).val(show_value);
5131 this.$el.find('a').toggle(!!this.get('value'));
5132 if (this.get('value')) {
5133 var show_value = _t("Download")
5135 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5136 this.$el.find('a').text(show_value);
5140 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5141 this.binary_value = true;
5142 this.internal_set_value(file_base64);
5143 var show_value = name + " (" + instance.web.human_size(size) + ")";
5144 this.$el.find('input').eq(0).val(show_value);
5145 this.set_filename(name);
5147 on_clear: function() {
5148 this._super.apply(this, arguments);
5149 this.$el.find('input').eq(0).val('');
5150 this.set_filename('');
5154 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5155 template: 'FieldBinaryImage',
5156 placeholder: "/web/static/src/img/placeholder.png",
5157 render_value: function() {
5160 if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5161 url = 'data:image/png;base64,' + this.get('value');
5162 } else if (this.get('value')) {
5163 var id = JSON.stringify(this.view.datarecord.id || null);
5164 var field = this.name;
5165 if (this.options.preview_image)
5166 field = this.options.preview_image;
5167 url = this.session.url('/web/binary/image', {
5168 model: this.view.dataset.model,
5171 t: (new Date().getTime()),
5174 url = this.placeholder;
5176 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5177 this.$el.find('> img').remove();
5178 this.$el.prepend($img);
5179 $img.load(function() {
5180 if (! self.options.size)
5182 $img.css("max-width", "" + self.options.size[0] + "px");
5183 $img.css("max-height", "" + self.options.size[1] + "px");
5184 $img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px");
5185 $img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px");
5187 $img.on('error', function() {
5188 $img.attr('src', self.placeholder);
5189 instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5192 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5193 this.internal_set_value(file_base64);
5194 this.binary_value = true;
5195 this.render_value();
5196 this.set_filename(name);
5198 on_clear: function() {
5199 this._super.apply(this, arguments);
5200 this.render_value();
5201 this.set_filename('');
5203 set_value: function(value_){
5204 var changed = value_ !== this.get_value();
5205 this._super.apply(this, arguments);
5206 // By default, on binary images read, the server returns the binary size
5207 // This is possible that two images have the exact same size
5208 // Therefore we trigger the change in case the image value hasn't changed
5209 // So the image is re-rendered correctly
5211 this.trigger("change:value", this, {
5220 * Widget for (one2many field) to upload one or more file in same time and display in list.
5221 * The user can delete his files.
5222 * Options on attribute ; "blockui" {Boolean} block the UI or not
5223 * during the file is uploading
5225 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
5226 template: "FieldBinaryFileUploader",
5227 init: function(field_manager, node) {
5228 this._super(field_manager, node);
5229 this.field_manager = field_manager;
5231 if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5232 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);
5234 this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5235 this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5236 $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5240 this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5242 set_value: function(value_) {
5243 var value_ = value_ || [];
5246 _.each(value_, function(command) {
5247 if (isNaN(command) && command.id == undefined) {
5248 switch (command[0]) {
5249 case commands.CREATE:
5250 ids = ids.concat(command[2]);
5252 case commands.REPLACE_WITH:
5253 ids = ids.concat(command[2]);
5255 case commands.UPDATE:
5256 ids = ids.concat(command[2]);
5258 case commands.LINK_TO:
5259 ids = ids.concat(command[1]);
5261 case commands.DELETE:
5262 ids = _.filter(ids, function (id) { return id != command[1];});
5264 case commands.DELETE_ALL:
5274 get_value: function() {
5275 return _.map(this.get('value'), function (value) { return commands.link_to( isNaN(value) ? value.id : value ); });
5277 get_file_url: function (attachment) {
5278 return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5280 read_name_values : function () {
5282 // select the list of id for a get_name
5284 _.each(this.get('value'), function (val) {
5285 if (typeof val != 'object') {
5289 // send request for get_name
5290 if (values.length) {
5291 return this.ds_file.call('read', [values, ['id', 'name', 'datas_fname']]).done(function (datas) {
5292 _.each(datas, function (data) {
5293 data.no_unlink = true;
5294 data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5296 _.each(self.get('value'), function (val, key) {
5297 if(val == data.id) {
5298 self.get('value')[key] = data;
5304 return $.when(this.get('value'));
5307 render_value: function () {
5309 this.read_name_values().then(function (datas) {
5311 var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self}));
5312 render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5313 self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5315 // reinit input type file
5316 var $input = self.$('input.oe_form_binary_file');
5317 $input.after($input.clone(true)).remove();
5318 self.$(".oe_fileupload").show();
5322 on_file_change: function (event) {
5323 event.stopPropagation();
5325 var $target = $(event.target);
5326 if ($target.val() !== '') {
5328 var filename = $target.val().replace(/.*[\\\/]/,'');
5330 // if the files is currently uploded, don't send again
5331 if( !isNaN(_.find(this.get('value'), function (file) { return (file.filename || file.name) == filename && file.upload; } )) ) {
5336 if(this.node.attrs.blockui>0) {
5337 instance.web.blockUI();
5340 // if the files exits for this answer, delete the file before upload
5341 var files = _.filter(this.get('value'), function (file) {
5342 if((file.filename || file.name) == filename) {
5343 self.ds_file.unlink([file.id]);
5350 // TODO : unactivate send on wizard and form
5353 this.$('form.oe_form_binary_form').submit();
5354 this.$(".oe_fileupload").hide();
5356 // add file on result
5360 'filename': filename,
5365 this.set({'value': files});
5368 on_file_loaded: function (event, result) {
5369 var files = this.get('value');
5372 if(this.node.attrs.blockui>0) {
5373 instance.web.unblockUI();
5376 // TODO : activate send on wizard and form
5378 if (result.error || !result.id ) {
5379 this.do_warn( _t('Uploading Error'), result.error);
5380 files = _.filter(files, function (val) { return !val.upload; });
5382 for(var i in files){
5383 if(files[i].filename == result.filename && files[i].upload) {
5386 'name': result.name,
5387 'filename': result.filename,
5388 'url': this.get_file_url(result)
5394 this.set({'value': files});
5397 on_file_delete: function (event) {
5398 event.stopPropagation();
5399 var file_id=$(event.target).data("id");
5402 for(var i in this.get('value')){
5403 if(file_id != this.get('value')[i].id){
5404 files.push(this.get('value')[i]);
5406 else if(!this.get('value')[i].no_unlink) {
5407 this.ds_file.unlink([file_id]);
5410 this.set({'value': files});
5415 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5416 template: "FieldStatus",
5417 init: function(field_manager, node) {
5418 this._super(field_manager, node);
5419 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5420 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5421 this.set({value: false});
5422 this.selection = [];
5423 this.set("selection", []);
5424 this.selection_dm = new instance.web.DropMisordered();
5427 this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5429 this.on("change:value", this, this.get_selection);
5430 this.on("change:evaluated_selection_domain", this, this.get_selection);
5431 this.on("change:selection", this, function() {
5432 this.selection = this.get("selection");
5433 this.render_value();
5435 this.get_selection();
5436 if (this.options.clickable) {
5437 this.$el.on('click','li',this.on_click_stage);
5441 set_value: function(value_) {
5442 if (value_ instanceof Array) {
5445 this._super(value_);
5447 render_value: function() {
5449 var content = QWeb.render("FieldStatus.content", {widget: self});
5450 self.$el.html(content);
5451 var colors = JSON.parse((self.node.attrs || {}).statusbar_colors || "{}");
5452 var color = colors[self.get('value')];
5454 self.$("oe_active").css("color", color);
5457 calc_domain: function() {
5458 var d = instance.web.pyeval.eval('domain', this.build_domain());
5459 var domain = []; //if there is no domain defined, fetch all the records
5462 domain = ['|',['id', '=', this.get('value')]].concat(d);
5465 if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5466 this.set("evaluated_selection_domain", domain);
5469 /** Get the selection and render it
5470 * selection: [[identifier, value_to_display], ...]
5471 * For selection fields: this is directly given by this.field.selection
5472 * For many2one fields: perform a search on the relation of the many2one field
5474 get_selection: function() {
5478 var calculation = _.bind(function() {
5479 if (this.field.type == "many2one") {
5481 var ds = new instance.web.DataSetSearch(this, this.field.relation,
5482 self.build_context(), this.get("evaluated_selection_domain"));
5483 return ds.read_slice(['name'], {}).then(function (records) {
5484 for(var i = 0; i < records.length; i++) {
5485 selection.push([records[i].id, records[i].name]);
5489 // For field type selection filter values according to
5490 // statusbar_visible attribute of the field. For example:
5491 // statusbar_visible="draft,open".
5492 var select = this.field.selection;
5493 for(var i=0; i < select.length; i++) {
5494 var key = select[i][0];
5495 if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5496 selection.push(select[i]);
5502 this.selection_dm.add(calculation()).then(function () {
5503 if (! _.isEqual(selection, self.get("selection"))) {
5504 self.set("selection", selection);
5508 on_click_stage: function (ev) {
5510 var $li = $(ev.currentTarget);
5511 if (this.field.type == "many2one") {
5512 var val = parseInt($li.data("id"));
5515 var val = $li.data("id");
5517 if (val != self.get('value')) {
5518 this.view.recursive_save().done(function() {
5520 change[self.name] = val;
5521 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
5529 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
5530 template: "FieldMonetary",
5531 widget_class: 'oe_form_field_float oe_form_field_monetary',
5533 this._super.apply(this, arguments);
5534 this.set({"currency": false});
5535 if (this.options.currency_field) {
5536 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
5537 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
5540 this.on("change:currency", this, this.get_currency_info);
5541 this.get_currency_info();
5542 this.ci_dm = new instance.web.DropMisordered();
5545 var tmp = this._super();
5546 this.on("change:currency_info", this, this.reinitialize);
5549 get_currency_info: function() {
5551 if (this.get("currency") === false) {
5552 this.set({"currency_info": null});
5555 return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
5556 .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
5557 self.set({"currency_info": res});
5560 parse_value: function(val, def) {
5561 return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5563 format_value: function(val, def) {
5564 return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5569 * Registry of form fields, called by :js:`instance.web.FormView`.
5571 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
5572 * will substitute to the <field> tags as defined in OpenERP's views.
5574 instance.web.form.widgets = new instance.web.Registry({
5575 'char' : 'instance.web.form.FieldChar',
5576 'id' : 'instance.web.form.FieldID',
5577 'email' : 'instance.web.form.FieldEmail',
5578 'url' : 'instance.web.form.FieldUrl',
5579 'text' : 'instance.web.form.FieldText',
5580 'html' : 'instance.web.form.FieldTextHtml',
5581 'date' : 'instance.web.form.FieldDate',
5582 'datetime' : 'instance.web.form.FieldDatetime',
5583 'selection' : 'instance.web.form.FieldSelection',
5584 'many2one' : 'instance.web.form.FieldMany2One',
5585 'many2onebutton' : 'instance.web.form.Many2OneButton',
5586 'many2many' : 'instance.web.form.FieldMany2Many',
5587 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
5588 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
5589 'one2many' : 'instance.web.form.FieldOne2Many',
5590 'one2many_list' : 'instance.web.form.FieldOne2Many',
5591 'reference' : 'instance.web.form.FieldReference',
5592 'boolean' : 'instance.web.form.FieldBoolean',
5593 'float' : 'instance.web.form.FieldFloat',
5594 'integer': 'instance.web.form.FieldFloat',
5595 'float_time': 'instance.web.form.FieldFloat',
5596 'progressbar': 'instance.web.form.FieldProgressBar',
5597 'image': 'instance.web.form.FieldBinaryImage',
5598 'binary': 'instance.web.form.FieldBinaryFile',
5599 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
5600 'statusbar': 'instance.web.form.FieldStatus',
5601 'monetary': 'instance.web.form.FieldMonetary',
5605 * Registry of widgets usable in the form view that can substitute to any possible
5606 * tags defined in OpenERP's form views.
5608 * Every referenced class should extend FormWidget.
5610 instance.web.form.tags = new instance.web.Registry({
5611 'button' : 'instance.web.form.WidgetButton',
5614 instance.web.form.custom_widgets = new instance.web.Registry({
5619 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: