1 openerp.web.form = function (instance) {
2 var _t = instance.web._t,
3 _lt = instance.web._lt;
4 var QWeb = instance.web.qweb;
7 instance.web.form = {};
10 * Interface implemented by the form view or any other object
11 * able to provide the features necessary for the fields to work.
14 * - display_invalid_fields : if true, all fields where is_valid() return true should
15 * be displayed as invalid.
16 * - actual_mode : the current mode of the field manager. Can be "view", "edit" or "create".
18 * - view_content_has_changed : when the values of the fields have changed. When
19 * this event is triggered all fields should reprocess their modifiers.
20 * - field_changed:<field_name> : when the value of a field change, an event is triggered
21 * named "field_changed:<field_name>" with <field_name> replaced by the name of the field.
22 * This event is not related to the on_change mechanism of OpenERP and is always called
23 * when the value of a field is setted or changed. This event is only triggered when the
24 * value of the field is syntactically valid, but it can be triggered when the value
25 * is sematically invalid (ie, when a required field is false). It is possible that an event
26 * about a precise field is never triggered even if that field exists in the view, in that
27 * case the value of the field is assumed to be false.
29 instance.web.form.FieldManagerMixin = {
31 * Must return the asked field as in fields_get.
33 get_field_desc: function(field_name) {},
35 * Returns the current value of a field present in the view. See the get_value() method
36 * method in FieldInterface for further information.
38 get_field_value: function(field_name) {},
40 Gives new values for the fields contained in the view. The new values could not be setted
41 right after the call to this method. Setting new values can trigger on_changes.
43 @param (dict) values A dictonnary with key = field name and value = new value.
44 @return (Deferred) Is resolved after all the values are setted.
46 set_values: function(values) {},
48 Computes an OpenERP domain.
50 @param (list) expression An OpenERP domain.
51 @return (boolean) The computed value of the domain.
53 compute_domain: function(expression) {},
55 Builds an evaluation context for the resolution of the fields' contexts. Please note
56 the field are only supposed to use this context to evualuate their own, they should not
59 @return (CompoundContext) An OpenERP context.
61 build_eval_context: function() {},
64 instance.web.views.add('form', 'instance.web.FormView');
67 * - actual_mode: always "view", "edit" or "create". Read-only property. Determines
68 * the mode used by the view.
70 instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerMixin, {
72 * Indicates that this view is not searchable, and thus that no search
73 * view should be displayed (if there is one active).
77 display_name: _lt('Form'),
80 * @constructs instance.web.FormView
81 * @extends instance.web.View
83 * @param {instance.web.Session} session the current openerp session
84 * @param {instance.web.DataSet} dataset the dataset this view will work with
85 * @param {String} view_id the identifier of the OpenERP view object
86 * @param {Object} options
87 * - resize_textareas : [true|false|max_height]
89 * @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance
91 init: function(parent, dataset, view_id, options) {
94 this.set_default_options(options);
95 this.dataset = dataset;
96 this.model = dataset.model;
97 this.view_id = view_id || false;
98 this.fields_view = {};
100 this.fields_order = [];
101 this.datarecord = {};
102 this.default_focus_field = null;
103 this.default_focus_button = null;
104 this.fields_registry = instance.web.form.widgets;
105 this.tags_registry = instance.web.form.tags;
106 this.widgets_registry = instance.web.form.custom_widgets;
107 this.has_been_loaded = $.Deferred();
108 this.translatable_fields = [];
109 _.defaults(this.options, {
110 "not_interactible_on_create": false,
111 "initial_mode": "view",
112 "disable_autofocus": false,
114 this.is_initialized = $.Deferred();
115 this.mutating_mutex = new $.Mutex();
116 this.on_change_mutex = new $.Mutex();
117 this.reload_mutex = new $.Mutex();
118 this.__clicked_inside = false;
119 this.__blur_timeout = null;
120 this.rendering_engine = new instance.web.form.FormRenderingEngine(this);
121 this.qweb = null; // A QWeb instance will be created if the view is a QWeb template
122 self.set({actual_mode: self.options.initial_mode});
123 this.has_been_loaded.then(function() {
124 self.on("change:actual_mode", self, self.check_actual_mode);
125 self.check_actual_mode();
126 self.on("change:actual_mode", self, self.init_pager);
129 self.on("load_record", self, self.load_record);
130 instance.web.bus.on('clear_uncommitted_changes', this, function(e) {
131 if (!this.can_be_discarded()) {
136 destroy: function() {
137 _.each(this.get_widgets(), function(w) {
138 w.off('focused blurred');
142 this.$el.off('.formBlur');
146 on_loaded: function(data) {
149 throw new Error("No data provided.");
152 throw "Form view does not support multiple calls to on_loaded";
154 this.fields_order = [];
155 this.fields_view = data;
157 this.rendering_engine.set_fields_registry(this.fields_registry);
158 this.rendering_engine.set_tags_registry(this.tags_registry);
159 this.rendering_engine.set_widgets_registry(this.widgets_registry);
160 if (!this.extract_qweb_template(data)) {
161 this.rendering_engine.set_fields_view(data);
162 var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container');
163 this.rendering_engine.render_to($dest);
166 this.$el.on('mousedown.formBlur', function () {
167 self.__clicked_inside = true;
170 this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self}));
171 if (this.options.$buttons) {
172 this.$buttons.appendTo(this.options.$buttons);
174 this.$el.find('.oe_form_buttons').replaceWith(this.$buttons);
176 this.$buttons.on('click', '.oe_form_button_create', this.on_button_create);
177 this.$buttons.on('click', '.oe_form_button_edit', this.on_button_edit);
178 this.$buttons.on('click', '.oe_form_button_save', this.on_button_save);
179 this.$buttons.on('click', '.oe_form_button_cancel', this.on_button_cancel);
181 this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar');
182 if (!this.sidebar && this.options.$sidebar) {
183 this.sidebar = new instance.web.Sidebar(this);
184 this.sidebar.appendTo(this.$sidebar);
185 if (this.fields_view.toolbar) {
186 this.sidebar.add_toolbar(this.fields_view.toolbar);
188 this.sidebar.add_items('other', _.compact([
189 self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete },
190 self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate },
191 { label: _t('Set Default'), callback: function (item) { self.open_defaults_dialog(); } }
195 this.has_been_loaded.resolve();
197 // Add bounce effect on button 'Edit' when click on readonly page view.
198 this.$el.find(".oe_form_group_row,.oe_form_field,label").on('click', function (e) {
199 if(self.get("actual_mode") == "view") {
200 var $button = self.options.$buttons.find(".oe_form_button_edit");
201 $button.effect('bounce', {distance: 18, times: 5}, 150);
203 instance.web.bus.trigger('click', e);
206 //bounce effect on red button when click on statusbar.
207 this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) {
208 if((self.get("actual_mode") == "view")) {
209 var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'});
210 $button.effect('bounce', {distance:18, times: 5}, 150);
217 extract_qweb_template: function(fvg) {
218 for (var i=0, ii=fvg.arch.children.length; i < ii; i++) {
219 var child = fvg.arch.children[i];
220 if (child.tag === "templates") {
221 this.qweb = new QWeb2.Engine();
222 this.qweb.add_template(instance.web.json_node_to_xml(child));
223 if (!this.qweb.has_template('form')) {
224 throw new Error("No QWeb template found for form view");
232 get_fvg_from_qweb: function(record) {
233 var view = this.qweb.render('form', this.get_qweb_context(record));
234 var fvg = _.clone(this.fields_view);
235 fvg.arch = instance.web.xml_to_json(instance.web.str_to_xml(view).firstChild);
238 get_qweb_context: function(record) {
241 _.each(record, function(value_, name) {
242 var r = _.clone(self.fields_view.fields[name] || {});
243 if ((r.type === 'date' || r.type === 'datetime') && value_) {
244 r.raw_value = instance.web.auto_str_to_date(value_);
246 r.raw_value = value_;
248 r.value = instance.web.format_value(value_, r);
249 new_record[name] = r;
253 new_record : !record.id
256 kill_current_form: function() {
257 _.each(this.getChildren(), function(el) {
261 this.fields_order = [];
262 this.default_focus_field = null;
263 this.default_focus_button = null;
264 this.translatable_fields = [];
265 this.$el.find('.oe_form_container').empty();
268 widgetFocused: function() {
269 // Clear click flag if used to focus a widget
270 this.__clicked_inside = false;
271 if (this.__blur_timeout) {
272 clearTimeout(this.__blur_timeout);
273 this.__blur_timeout = null;
276 widgetBlurred: function() {
277 if (this.__clicked_inside) {
278 // clicked in an other section of the form (than the currently
279 // focused widget) => just ignore the blurring entirely?
280 this.__clicked_inside = false;
284 // clear timeout, if any
285 this.widgetFocused();
286 this.__blur_timeout = setTimeout(function () {
287 self.trigger('blurred');
291 do_load_state: function(state, warm) {
292 if (state.id && this.datarecord.id != state.id) {
293 if (!this.dataset.get_id_index(state.id)) {
294 this.dataset.ids.push(state.id);
296 this.dataset.select_id(state.id);
304 * @param {Object} [options]
305 * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view".
306 * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record
307 * @return {$.Deferred}
309 do_show: function (options) {
311 options = options || {};
313 this.sidebar.$el.show();
316 this.$buttons.show();
318 this.$el.show().css({
320 filter: 'alpha(opacity = 0)'
322 this.$el.add(this.$buttons).removeClass('oe_form_dirty');
324 var shown = this.has_been_loaded;
325 if (options.reload !== false) {
326 shown = shown.pipe(function() {
327 if (self.dataset.index === null) {
328 // null index means we should start a new record
329 return self.on_button_new();
331 var fields = _.keys(self.fields_view.fields);
332 fields.push('display_name');
333 return self.dataset.read_index(fields, {
334 context: { 'bin_size': true, 'future_display_name' : true }
335 }).pipe(function(r) {
336 self.trigger('load_record', r);
340 return shown.pipe(function() {
341 self._actualize_mode(options.mode || self.options.initial_mode);
344 filter: 'alpha(opacity = 100)'
348 do_hide: function () {
350 this.sidebar.$el.hide();
353 this.$buttons.hide();
360 load_record: function(record) {
361 var self = this, set_values = [];
363 this.set({ 'title' : undefined });
364 this.do_warn("Form", "The record could not be found in the database.", true);
365 return $.Deferred().reject();
367 this.datarecord = record;
368 this._actualize_mode();
369 this.set({ 'title' : record.id ? record.display_name : "New" });
372 this.kill_current_form();
373 this.rendering_engine.set_fields_view(this.get_fvg_from_qweb(record));
374 var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container');
375 this.rendering_engine.render_to($dest);
378 _(this.fields).each(function (field, f) {
379 field._dirty_flag = false;
380 field._inhibit_on_change_flag = true;
381 var result = field.set_value(self.datarecord[f] || false);
382 field._inhibit_on_change_flag = false;
383 set_values.push(result);
385 return $.when.apply(null, set_values).pipe(function() {
387 // New record: Second pass in order to trigger the onchanges
388 // respecting the fields order defined in the view
389 _.each(self.fields_order, function(field_name) {
390 if (record[field_name] !== undefined) {
391 var field = self.fields[field_name];
392 field._dirty_flag = true;
393 self.do_onchange(field);
397 self.on_form_changed();
398 self.is_initialized.resolve();
399 self.do_update_pager(record.id == null);
401 self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
404 self.do_push_state({id:record.id});
406 self.do_push_state({});
408 self.$el.add(self.$buttons).removeClass('oe_form_dirty');
413 * Loads and sets up the default values for the model as the current
416 * @return {$.Deferred}
418 load_defaults: function () {
420 var keys = _.keys(this.fields_view.fields);
422 return this.dataset.default_get(keys).pipe(function(r) {
423 self.trigger('load_record', r);
426 return self.trigger('load_record', {});
428 on_form_changed: function() {
429 this.trigger("view_content_has_changed");
431 do_notify_change: function() {
432 this.$el.add(this.$buttons).addClass('oe_form_dirty');
434 on_pager_action: function(action) {
435 if (this.can_be_discarded()) {
438 this.dataset.index = 0;
441 this.dataset.previous();
447 this.dataset.index = this.dataset.ids.length - 1;
453 init_pager: function() {
456 this.$pager.remove();
457 if (this.get("actual_mode") === "create")
459 this.$pager = $(QWeb.render("FormView.pager", {'widget':self})).hide();
460 if (this.options.$pager) {
461 this.$pager.appendTo(this.options.$pager);
463 this.$el.find('.oe_form_pager').replaceWith(this.$pager);
465 this.$pager.on('click','a[data-pager-action]',function() {
466 var action = $(this).data('pager-action');
467 self.on_pager_action(action);
469 this.do_update_pager();
471 do_update_pager: function(hide_index) {
472 this.$pager.toggle(this.dataset.ids.length > 1);
474 $(".oe_form_pager_state", this.$pager).html("");
476 $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
479 parse_on_change: function (on_change, widget) {
481 var onchange = _.str.trim(on_change);
482 var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
487 var method = call[1];
488 if (!_.str.trim(call[2])) {
489 return {method: method, args: [], context_index: null}
492 var argument_replacement = {
493 'False': function () {return false;},
494 'True': function () {return true;},
495 'None': function () {return null;},
496 'context': function (i) {
498 var ctx = new instance.web.CompoundContext(self.dataset.get_context(), widget.build_context() ? widget.build_context() : {});
502 var parent_fields = null, context_index = null;
503 var args = _.map(call[2].split(','), function (a, i) {
504 var field = _.str.trim(a);
506 // literal constant or context
507 if (field in argument_replacement) {
508 return argument_replacement[field](i);
511 if (/^-?\d+(\.\d+)?$/.test(field)) {
512 return Number(field);
515 if (self.fields[field]) {
516 var value_ = self.fields[field].get_value();
517 return value_ == null ? false : value_;
520 var splitted = field.split('.');
521 if (splitted.length > 1 && _.str.trim(splitted[0]) === "parent" && self.dataset.parent_view) {
522 if (parent_fields === null) {
523 parent_fields = self.dataset.parent_view.get_fields_values([self.dataset.child_name]);
525 var p_val = parent_fields[_.str.trim(splitted[1])];
526 if (p_val !== undefined) {
527 return p_val == null ? false : p_val;
531 var first_char = field[0], last_char = field[field.length-1];
532 if ((first_char === '"' && last_char === '"')
533 || (first_char === "'" && last_char === "'")) {
534 return field.slice(1, -1);
537 throw new Error("Could not get field with name '" + field +
538 "' for onchange '" + onchange + "'");
544 context_index: context_index
547 do_onchange: function(widget, processed) {
549 return this.on_change_mutex.exec(function() {
551 var response = {}, can_process_onchange = $.Deferred();
552 processed = processed || [];
553 processed.push(widget.name);
554 var on_change = widget.node.attrs.on_change;
556 var change_spec = self.parse_on_change(on_change, widget);
559 url: '/web/dataset/onchange',
562 can_process_onchange = self.rpc(ajax, {
563 model: self.dataset.model,
564 method: change_spec.method,
565 args: [(self.datarecord.id == null ? [] : [self.datarecord.id])].concat(change_spec.args),
566 context_id: change_spec.context_index == undefined ? null : change_spec.context_index + 1
567 }).then(function(r) {
568 _.extend(response, r);
571 console.warn("Wrong on_change format", on_change);
574 // fail if onchange failed
575 if (can_process_onchange.isRejected()) {
576 return can_process_onchange;
579 if (widget.field['change_default']) {
580 var fieldname = widget.name, value_;
581 if (response.value && (fieldname in response.value)) {
582 // Use value from onchange if onchange executed
583 value_ = response.value[fieldname];
585 // otherwise get form value for field
586 value_ = self.fields[fieldname].get_value();
588 var condition = fieldname + '=' + value_;
591 can_process_onchange = self.rpc({
592 url: '/web/dataset/call',
596 method: 'get_defaults',
597 args: [self.model, condition]
598 }).then(function (results) {
599 if (!results.length) { return; }
600 if (!response.value) {
603 for(var i=0; i<results.length; ++i) {
604 // [whatever, key, value]
605 var triplet = results[i];
606 response.value[triplet[1]] = triplet[2];
611 if (can_process_onchange.isRejected()) {
612 return can_process_onchange;
615 return self.on_processed_onchange(response, processed);
618 instance.webclient.crashmanager.show_message(e);
619 return $.Deferred().reject();
623 on_processed_onchange: function(result, processed) {
626 this._internal_set_values(result.value, processed);
628 if (!_.isEmpty(result.warning)) {
629 instance.web.dialog($(QWeb.render("CrashManager.warning", result.warning)), {
630 title:result.warning.title,
633 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
638 function edit_domain(node) {
639 if (typeof node !== "object") {
642 var new_domain = result.domain[node.attrs.name];
644 node.attrs.domain = new_domain;
646 _(node.children).each(edit_domain);
648 edit_domain(this.fields_view.arch);
650 return $.Deferred().resolve();
653 instance.webclient.crashmanager.show_message(e);
654 return $.Deferred().reject();
657 _internal_set_values: function(values, exclude) {
658 exclude = exclude || [];
659 for (var f in values) {
660 if (!values.hasOwnProperty(f)) { continue; }
661 var field = this.fields[f];
662 // If field is not defined in the view, just ignore it
664 var value_ = values[f];
665 if (field.get_value() != value_) {
666 field._inhibit_on_change_flag = true;
667 field.set_value(value_);
668 field._inhibit_on_change_flag = false;
669 field._dirty_flag = true;
670 if (!_.contains(exclude, field.name)) {
671 this.do_onchange(field, exclude);
676 this.on_form_changed();
678 set_values: function(values) {
680 return this.on_change_mutex.exec(function() {
681 self._internal_set_values(values);
685 * Ask the view to switch to view mode if possible. The view may not do it
686 * if the current record is not yet saved. It will then stay in create mode.
688 to_view_mode: function() {
689 this._actualize_mode("view");
692 * Ask the view to switch to edit mode if possible. The view may not do it
693 * if the current record is not yet saved. It will then stay in create mode.
695 to_edit_mode: function() {
696 this._actualize_mode("edit");
699 * Ask the view to switch to a precise mode if possible. The view is free to
700 * not respect this command if the state of the dataset is not compatible with
701 * the new mode. For example, it is not possible to switch to edit mode if
702 * the current record is not yet saved in database.
704 * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
705 * undefined the view will test the actual mode to check if it is still consistent
706 * with the dataset state.
708 _actualize_mode: function(switch_to) {
709 var mode = switch_to || this.get("actual_mode");
710 if (! this.datarecord.id) {
712 } else if (mode === "create") {
715 this.set({actual_mode: mode});
717 check_actual_mode: function(source, options) {
719 if(this.get("actual_mode") === "view") {
720 self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
721 self.$buttons.find('.oe_form_buttons_edit').hide();
722 self.$buttons.find('.oe_form_buttons_view').show();
723 self.$sidebar.show();
725 self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
726 self.$buttons.find('.oe_form_buttons_edit').show();
727 self.$buttons.find('.oe_form_buttons_view').hide();
728 self.$sidebar.hide();
732 autofocus: function() {
733 if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
734 var fields_order = this.fields_order.slice(0);
735 if (this.default_focus_field) {
736 fields_order.unshift(this.default_focus_field.name);
738 for (var i = 0; i < fields_order.length; i += 1) {
739 var field = this.fields[fields_order[i]];
740 if (!field.get('effective_invisible') && !field.get('effective_readonly')) {
741 if (field.focus() !== false) {
748 on_button_save: function() {
750 return this.do_save().then(function(result) {
754 on_button_cancel: function(event) {
755 if (this.can_be_discarded()) {
756 if (this.get('actual_mode') === 'create') {
757 this.trigger('history_back');
760 this.trigger('load_record', this.datarecord);
763 this.trigger('on_button_cancel');
766 on_button_new: function() {
769 return $.when(this.has_been_loaded).pipe(function() {
770 if (self.can_be_discarded()) {
771 return self.load_defaults();
775 on_button_edit: function() {
776 return this.to_edit_mode();
778 on_button_create: function() {
779 this.dataset.index = null;
782 on_button_duplicate: function() {
784 var def = $.Deferred();
785 $.when(this.has_been_loaded).then(function() {
786 self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
787 return self.record_created({ result : new_id });
789 return self.to_edit_mode();
794 return def.promise();
796 on_button_delete: function() {
798 var def = $.Deferred();
799 $.when(this.has_been_loaded).then(function() {
800 if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
801 self.dataset.unlink([self.datarecord.id]).then(function() {
802 self.on_pager_action('next');
806 $.async_when().then(function () {
811 return def.promise();
813 can_be_discarded: function() {
814 if (this.$el.is('.oe_form_dirty')) {
815 if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
818 this.$el.removeClass('oe_form_dirty');
823 * Triggers saving the form's record. Chooses between creating a new
824 * record or saving an existing one depending on whether the record
825 * already has an id property.
827 * @param {Boolean} [prepend_on_create=false] if ``do_save`` creates a new
828 * record, should that record be inserted at the start of the dataset (by
829 * default, records are added at the end)
831 do_save: function(prepend_on_create) {
833 return this.mutating_mutex.exec(function() { return self.is_initialized.pipe(function() {
835 var form_invalid = false,
837 first_invalid_field = null;
838 for (var f in self.fields) {
839 if (!self.fields.hasOwnProperty(f)) { continue; }
843 if (!first_invalid_field) {
844 first_invalid_field = f;
846 } else if (f.name !== 'id' && (!self.datarecord.id || (!f.get("readonly") && f._dirty_flag))) {
847 // Special case 'id' field, do not save this field
848 // on 'create' : save all non readonly fields
849 // on 'edit' : save non readonly modified fields
850 values[f.name] = f.get_value();
854 self.set({'display_invalid_fields': true});
855 first_invalid_field.focus();
857 return $.Deferred().reject();
859 self.set({'display_invalid_fields': false});
861 if (!self.datarecord.id) {
863 save_deferral = self.dataset.create(values).pipe(function(r) {
864 return self.record_created(r, prepend_on_create);
866 } else if (_.isEmpty(values) && ! self.force_dirty) {
867 // Not dirty, noop save
868 save_deferral = $.Deferred().resolve({}).promise();
870 self.force_dirty = false;
872 save_deferral = self.dataset.write(self.datarecord.id, values, {}).pipe(function(r) {
873 return self.record_saved(r);
876 return save_deferral;
880 return $.Deferred().reject();
884 on_invalid: function() {
885 var warnings = _(this.fields).chain()
886 .filter(function (f) { return !f.is_valid(); })
888 return _.str.sprintf('<li>%s</li>',
891 warnings.unshift('<ul>');
892 warnings.push('</ul>');
893 this.do_warn("The following fields are invalid :", warnings.join(''));
896 * Reload the form after saving
898 * @param {Object} r result of the write function.
900 record_saved: function(r) {
903 // should not happen in the server, but may happen for internal purpose
904 this.trigger('record_saved', r);
905 return $.Deferred().reject();
907 return $.when(this.reload()).pipe(function () {
908 self.trigger('record_saved', r);
914 * Updates the form' dataset to contain the new record:
916 * * Adds the newly created record to the current dataset (at the end by
918 * * Selects that record (sets the dataset's index to point to the new
920 * * Updates the pager and sidebar displays
923 * @param {Boolean} [prepend_on_create=false] adds the newly created record
924 * at the beginning of the dataset instead of the end
926 record_created: function(r, prepend_on_create) {
929 // should not happen in the server, but may happen for internal purpose
930 this.trigger('record_created', r);
931 return $.Deferred().reject();
933 this.datarecord.id = r;
934 if (!prepend_on_create) {
935 this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
936 this.dataset.index = this.dataset.ids.length - 1;
938 this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
939 this.dataset.index = 0;
941 this.do_update_pager();
943 this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
945 //openerp.log("The record has been created with id #" + this.datarecord.id);
946 return $.when(this.reload()).pipe(function () {
947 self.trigger('record_created', r);
948 return _.extend(r, {created: true});
952 on_action: function (action) {
953 console.debug('Executing action', action);
957 return this.reload_mutex.exec(function() {
958 if (self.dataset.index == null) {
960 return $.Deferred().reject().promise();
962 if (self.dataset.index == null || self.dataset.index < 0) {
963 return $.when(self.on_button_new());
965 var fields = _.keys(self.fields_view.fields);
966 fields.push('display_name');
967 return self.dataset.read_index(fields,
971 'future_display_name': true
973 }).pipe(function(r) {
974 self.trigger('load_record', r);
979 get_widgets: function() {
980 return _.filter(this.getChildren(), function(obj) {
981 return obj instanceof instance.web.form.FormWidget;
984 get_fields_values: function(blacklist) {
985 blacklist = blacklist || [];
987 var ids = this.get_selected_ids();
988 values["id"] = ids.length > 0 ? ids[0] : false;
989 _.each(this.fields, function(value_, key) {
990 if (_.include(blacklist, key)) {
993 values[key] = value_.get_value();
997 get_selected_ids: function() {
998 var id = this.dataset.ids[this.dataset.index];
999 return id ? [id] : [];
1001 recursive_save: function() {
1003 return $.when(this.do_save()).pipe(function(res) {
1004 if (self.dataset.parent_view)
1005 return self.dataset.parent_view.recursive_save();
1008 recursive_reload: function() {
1011 if (self.dataset.parent_view)
1012 pre = self.dataset.parent_view.recursive_reload();
1013 return pre.pipe(function() {
1014 return self.reload();
1017 is_dirty: function() {
1018 return _.any(this.fields, function (value_) {
1019 return value_._dirty_flag;
1022 is_interactible_record: function() {
1023 var id = this.datarecord.id;
1025 if (this.options.not_interactible_on_create)
1027 } else if (typeof(id) === "string") {
1028 if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
1033 sidebar_context: function () {
1034 return this.do_save().pipe(_.bind(function() {return this.get_fields_values();}, this));
1036 open_defaults_dialog: function () {
1038 var fields = _.chain(this.fields)
1039 .map(function (field, name) {
1040 var value = field.get_value();
1041 // ignore fields which are empty, invisible, readonly, o2m
1044 || field.get('invisible')
1045 || field.get("readonly")
1046 || field.field.type === 'one2many'
1047 || field.field.type === 'many2many'
1048 || field.field.type === 'binary') {
1051 var displayed = value;
1052 if (field instanceof instance.web.form.FieldSelection) {
1053 displayed = _(field.values).find(function (option) {
1054 return option[0] === value;
1056 } else if (field instanceof instance.web.form.FieldMany2One) {
1057 displayed = field.get_displayed();
1062 string: field.string,
1064 displayed: displayed,
1065 // convert undefined to false
1066 change_default: !!field.field.change_default
1070 .sortBy(function (field) { return field.string; })
1072 var conditions = _.chain(fields)
1073 .filter(function (field) { return field.change_default; })
1076 var d = new instance.web.Dialog(this, {
1077 title: _t("Set Default"),
1080 conditions: conditions
1083 {text: _t("Close"), click: function () { d.close(); }},
1084 {text: _t("Save default"), click: function () {
1085 var $defaults = d.$el.find('#formview_default_fields');
1086 var field_to_set = $defaults.val();
1087 if (!field_to_set) {
1088 $defaults.parent().addClass('oe_form_invalid');
1091 var condition = d.$el.find('#formview_default_conditions').val(),
1092 all_users = d.$el.find('#formview_default_all').is(':checked');
1093 new instance.web.DataSet(self, 'ir.values').call(
1097 self.fields[field_to_set].get_value(),
1101 ]).then(function () { d.close(); });
1105 d.template = 'FormView.set_default';
1108 register_field: function(field, name) {
1109 this.fields[name] = field;
1110 this.fields_order.push(name);
1111 if (JSON.parse(field.node.attrs.default_focus || "0")) {
1112 this.default_focus_field = field;
1115 field.on('focused', null, this.proxy('widgetFocused'))
1116 .on('blurred', null, this.proxy('widgetBlurred'));
1117 if (this.get_field_desc(name).translate) {
1118 this.translatable_fields.push(field);
1120 field.on('changed_value', this, function() {
1121 if (field.is_syntax_valid()) {
1122 this.trigger('field_changed:' + name);
1124 if (field._inhibit_on_change_flag) {
1127 field._dirty_flag = true;
1128 if (field.is_syntax_valid()) {
1129 this.do_onchange(field);
1130 this.on_form_changed(true);
1131 this.do_notify_change();
1135 get_field_desc: function(field_name) {
1136 return this.fields_view.fields[field_name];
1138 get_field_value: function(field_name) {
1139 return this.fields[field_name].get_value();
1141 compute_domain: function(expression) {
1142 return instance.web.form.compute_domain(expression, this.fields);
1144 _build_view_fields_values: function(blacklist) {
1145 var a_dataset = this.dataset;
1146 var fields_values = this.get_fields_values(blacklist);
1147 var active_id = a_dataset.ids[a_dataset.index];
1148 _.extend(fields_values, {
1149 active_id: active_id || false,
1150 active_ids: active_id ? [active_id] : [],
1151 active_model: a_dataset.model,
1154 if (a_dataset.parent_view) {
1155 fields_values.parent = a_dataset.parent_view.get_fields_values([a_dataset.child_name]);
1157 return fields_values;
1159 build_eval_context: function(blacklist) {
1160 var a_dataset = this.dataset;
1161 return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values(blacklist));
1166 * Interface to be implemented by rendering engines for the form view.
1168 instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
1169 set_fields_view: function(fields_view) {},
1170 set_fields_registry: function(fields_registry) {},
1171 render_to: function($el) {},
1175 * Default rendering engine for the form view.
1177 * It is necessary to set the view using set_view() before usage.
1179 instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
1180 init: function(view) {
1183 set_fields_view: function(fvg) {
1185 this.version = parseFloat(this.fvg.arch.attrs.version);
1186 if (isNaN(this.version)) {
1190 set_tags_registry: function(tags_registry) {
1191 this.tags_registry = tags_registry;
1193 set_fields_registry: function(fields_registry) {
1194 this.fields_registry = fields_registry;
1196 set_widgets_registry: function(widgets_registry) {
1197 this.widgets_registry = widgets_registry;
1199 // Backward compatibility tools, current default version: v6.1
1200 process_version: function() {
1201 if (this.version < 7.0) {
1202 this.$form.find('form:first').wrapInner('<group col="4"/>');
1203 this.$form.find('page').each(function() {
1204 if (!$(this).parents('field').length) {
1205 $(this).wrapInner('<group col="4"/>');
1210 render_to: function($target) {
1212 this.$target = $target;
1214 // TODO: I know this will save the world and all the kitten for a moment,
1215 // but one day, we will have to get rid of xml2json
1216 var xml = instance.web.json_node_to_xml(this.fvg.arch);
1217 this.$form = $('<div class="oe_form">' + xml + '</div>');
1219 this.process_version();
1221 this.fields_to_init = [];
1222 this.tags_to_init = [];
1223 this.widgets_to_init = [];
1225 this.process(this.$form);
1227 this.$form.appendTo(this.$target);
1229 var ws = _.map(this.fields_to_init, function($elem) {
1230 var name = $elem.attr("name");
1231 if (!self.fvg.fields[name]) {
1232 throw new Error("Field '" + name + "' specified in view could not be found.");
1234 var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
1236 throw new Error("Widget type '"+ $elem.attr('widget') + "' is not implemented");
1238 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1239 var $label = self.labels[$elem.attr("name")];
1241 w.set_input_id($label.attr("for"));
1243 self.alter_field(w);
1244 self.view.register_field(w, $elem.attr("name"));
1247 _.each(ws, function(w) {
1250 _.each(this.tags_to_init, function($elem) {
1251 var tag_name = $elem[0].tagName.toLowerCase();
1252 var obj = self.tags_registry.get_object(tag_name);
1253 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1256 _.each(this.widgets_to_init, function($elem) {
1257 var widget_type = $elem.attr("type");
1258 var obj = self.widgets_registry.get_object(widget_type);
1259 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1262 // TODO: return a deferred
1264 render_element: function(template /* dictionaries */) {
1265 var dicts = [].slice.call(arguments).slice(1);
1266 var dict = _.extend.apply(_, dicts);
1267 dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
1268 return $(QWeb.render(template, dict));
1270 alter_field: function(field) {
1272 toggle_layout_debugging: function() {
1273 if (!this.$target.has('.oe_layout_debug_cell:first').length) {
1274 this.$target.find('[title]').removeAttr('title');
1275 this.$target.find('.oe_form_group_cell').each(function() {
1276 var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
1277 $(this).attr('title', text);
1280 this.$target.toggleClass('oe_layout_debugging');
1282 process: function($tag) {
1284 var tagname = $tag[0].nodeName.toLowerCase();
1285 if (this.tags_registry.contains(tagname)) {
1286 this.tags_to_init.push($tag);
1289 var fn = self['process_' + tagname];
1291 var args = [].slice.call(arguments);
1293 return fn.apply(self, args);
1295 // generic tag handling, just process children
1296 $tag.children().each(function() {
1297 self.process($(this));
1299 self.handle_common_properties($tag, $tag);
1300 $tag.removeAttr("modifiers");
1304 process_widget: function($widget) {
1305 this.widgets_to_init.push($widget);
1308 process_sheet: function($sheet) {
1309 var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
1310 this.handle_common_properties($new_sheet, $sheet);
1311 var $dst = $new_sheet.find('.oe_form_sheet');
1312 $sheet.contents().appendTo($dst);
1313 $sheet.before($new_sheet).remove();
1314 this.process($new_sheet);
1316 process_form: function($form) {
1317 if ($form.find('> sheet').length === 0) {
1318 $form.addClass('oe_form_nosheet');
1320 var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
1321 this.handle_common_properties($new_form, $form);
1322 $form.contents().appendTo($new_form);
1323 if ($form[0] === this.$form[0]) {
1324 // If root element, replace it
1325 this.$form = $new_form;
1327 $form.before($new_form).remove();
1329 this.process($new_form);
1332 * Used by direct <field> children of a <group> tag only
1333 * This method will add the implicit <label...> for every field
1336 preprocess_field: function($field) {
1338 var name = $field.attr('name'),
1339 field_colspan = parseInt($field.attr('colspan'), 10),
1340 field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
1342 if ($field.attr('nolabel') === '1')
1344 $field.attr('nolabel', '1');
1346 this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
1347 $(el).parents().each(function(unused, tag) {
1348 var name = tag.tagName.toLowerCase();
1349 if (name === "field" || name in self.tags_registry.map)
1356 var $label = $('<label/>').attr({
1358 "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
1359 "string": $field.attr('string'),
1360 "help": $field.attr('help'),
1361 "class": $field.attr('class'),
1363 $label.insertBefore($field);
1364 if (field_colspan > 1) {
1365 $field.attr('colspan', field_colspan - 1);
1369 process_field: function($field) {
1370 if ($field.parent().is('group')) {
1371 // No implicit labels for normal fields, only for <group> direct children
1372 var $label = this.preprocess_field($field);
1374 this.process($label);
1377 this.fields_to_init.push($field);
1380 process_group: function($group) {
1382 $group.children('field').each(function() {
1383 self.preprocess_field($(this));
1385 var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
1387 if ($new_group.first().is('table.oe_form_group')) {
1388 $table = $new_group;
1389 } else if ($new_group.filter('table.oe_form_group').length) {
1390 $table = $new_group.filter('table.oe_form_group').first();
1392 $table = $new_group.find('table.oe_form_group').first();
1396 cols = parseInt($group.attr('col') || 2, 10),
1400 $group.children().each(function(a,b,c) {
1401 var $child = $(this);
1402 var colspan = parseInt($child.attr('colspan') || 1, 10);
1403 var tagName = $child[0].tagName.toLowerCase();
1404 var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
1405 var newline = tagName === 'newline';
1407 // Note FME: those classes are used in layout debug mode
1408 if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
1409 $tr.addClass('oe_form_group_row_incomplete');
1411 $tr.addClass('oe_form_group_row_newline');
1418 if (!$tr || row_cols < colspan) {
1419 $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
1421 } else if (tagName==='group') {
1422 // When <group> <group/><group/> </group>, we need a spacing between the two groups
1423 $td.addClass('oe_group_right')
1425 row_cols -= colspan;
1427 // invisibility transfer
1428 var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
1429 var invisible = field_modifiers.invisible;
1430 self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
1432 $tr.append($td.append($child));
1433 children.push($child[0]);
1435 if (row_cols && $td) {
1436 $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
1438 $group.before($new_group).remove();
1440 $table.find('> tbody > tr').each(function() {
1441 var to_compute = [],
1444 $(this).children().each(function() {
1446 $child = $td.children(':first');
1447 switch ($child[0].tagName.toLowerCase()) {
1451 if ($child.attr('for')) {
1452 $td.attr('width', '1%').addClass('oe_form_group_cell_label');
1453 row_cols-= $td.attr('colspan') || 1;
1458 var width = _.str.trim($child.attr('width') || ''),
1459 iwidth = parseInt(width, 10);
1461 if (width.substr(-1) === '%') {
1463 width = iwidth + '%';
1466 $td.css('min-width', width + 'px');
1468 $td.attr('width', width);
1469 $child.removeAttr('width');
1470 row_cols-= $td.attr('colspan') || 1;
1472 to_compute.push($td);
1478 var unit = Math.floor(total / row_cols);
1479 if (!$(this).is('.oe_form_group_row_incomplete')) {
1480 _.each(to_compute, function($td, i) {
1481 var width = parseInt($td.attr('colspan'), 10) * unit;
1482 $td.attr('width', width + '%');
1488 _.each(children, function(el) {
1489 self.process($(el));
1491 this.handle_common_properties($new_group, $group);
1494 process_notebook: function($notebook) {
1497 $notebook.find('> page').each(function() {
1498 var $page = $(this);
1499 var page_attrs = $page.getAttributes();
1500 page_attrs.id = _.uniqueId('notebook_page_');
1501 var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
1502 $page.contents().appendTo($new_page);
1503 $page.before($new_page).remove();
1504 var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
1505 page_attrs.__page = $new_page;
1506 page_attrs.__ic = ic;
1507 pages.push(page_attrs);
1509 $new_page.children().each(function() {
1510 self.process($(this));
1513 var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
1514 $notebook.contents().appendTo($new_notebook);
1515 $notebook.before($new_notebook).remove();
1516 self.process($($new_notebook.children()[0]));
1517 //tabs and invisibility handling
1518 $new_notebook.tabs();
1519 _.each(pages, function(page, i) {
1522 page.__ic.on("change:effective_invisible", null, function() {
1523 if (!page.__ic.get('effective_invisible')) {
1524 $new_notebook.tabs('select', i);
1527 var current = $new_notebook.tabs("option", "selected");
1528 if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
1530 var first_visible = _.find(_.range(pages.length), function(i2) {
1531 return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
1533 if (first_visible !== undefined) {
1534 $new_notebook.tabs('select', first_visible);
1539 this.handle_common_properties($new_notebook, $notebook);
1540 return $new_notebook;
1542 process_separator: function($separator) {
1543 var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
1544 $separator.before($new_separator).remove();
1545 this.handle_common_properties($new_separator, $separator);
1546 return $new_separator;
1548 process_label: function($label) {
1549 var name = $label.attr("for"),
1550 field_orm = this.fvg.fields[name];
1552 string: $label.attr('string') || (field_orm || {}).string || '',
1553 help: $label.attr('help') || (field_orm || {}).help || '',
1554 _for: name ? _.uniqueId('oe-field-input-') : undefined,
1556 var align = parseFloat(dict.align);
1557 if (isNaN(align) || align === 1) {
1559 } else if (align === 0) {
1565 var $new_label = this.render_element('FormRenderingLabel', dict);
1566 $label.before($new_label).remove();
1567 this.handle_common_properties($new_label, $label);
1569 this.labels[name] = $new_label;
1573 handle_common_properties: function($new_element, $node) {
1574 var str_modifiers = $node.attr("modifiers") || "{}";
1575 var modifiers = JSON.parse(str_modifiers);
1577 if (modifiers.invisible !== undefined)
1578 ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
1579 $new_element.addClass($node.attr("class") || "");
1580 $new_element.attr('style', $node.attr('style'));
1581 return {invisibility_changer: ic,};
1588 If you read this documentation, it probably means that you were asked to use a form view widget outside of
1589 a form view. Before going further, you must understand that those fields were never really created for
1590 that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
1591 you to hack the system with more style.
1593 instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
1594 init: function(parent, eval_context) {
1595 this._super(parent);
1596 this.field_descs = {};
1597 this.eval_context = eval_context || {};
1599 display_invalid_fields: false,
1600 actual_mode: 'create',
1603 get_field_desc: function(field_name) {
1604 if (this.field_descs[field_name] === undefined) {
1605 this.field_descs[field_name] = {
1609 return this.field_descs[field_name];
1611 extend_field_desc: function(fields) {
1613 _.each(fields, function(v, k) {
1614 _.extend(self.get_field_desc(k), v);
1617 get_field_value: function(field_name) {
1620 set_values: function(values) {
1623 compute_domain: function(expression) {
1624 return instance.web.form.compute_domain(expression, {});
1626 build_eval_context: function() {
1627 return new instance.web.CompoundContext(this.eval_context);
1631 instance.web.form.FormDialog = instance.web.Dialog.extend({
1632 init: function(parent, options, view_id, dataset) {
1633 this._super(parent, options);
1634 this.dataset = dataset;
1635 this.view_id = view_id;
1641 this.form = new instance.web.FormView(this, this.dataset, this.view_id, {
1644 this.form.appendTo(this.$el);
1645 this.form.on('record_created', self, this.on_form_dialog_saved);
1646 this.form.on('record_saved', this, this.on_form_dialog_saved);
1649 select_id: function(id) {
1650 if (this.form.dataset.select_id(id)) {
1651 return this.form.do_show();
1653 this.do_warn("Could not find id in dataset");
1654 return $.Deferred().reject();
1657 on_form_dialog_saved: function(r) {
1662 instance.web.form.compute_domain = function(expr, fields) {
1664 for (var i = expr.length - 1; i >= 0; i--) {
1666 if (ex.length == 1) {
1667 var top = stack.pop();
1670 stack.push(stack.pop() || top);
1673 stack.push(stack.pop() && top);
1679 throw new Error(_.str.sprintf(
1680 _t("Unknown operator %s in domain %s"),
1681 ex, JSON.stringify(expr)));
1685 var field = fields[ex[0]];
1687 throw new Error(_.str.sprintf(
1688 _t("Unknown field %s in domain %s"),
1689 ex[0], JSON.stringify(expr)));
1691 var field_value = field.get_value ? field.get_value() : field.value;
1695 switch (op.toLowerCase()) {
1698 stack.push(field_value == val);
1702 stack.push(field_value != val);
1705 stack.push(field_value < val);
1708 stack.push(field_value > val);
1711 stack.push(field_value <= val);
1714 stack.push(field_value >= val);
1717 if (!_.isArray(val)) val = [val];
1718 stack.push(_(val).contains(field_value));
1721 if (!_.isArray(val)) val = [val];
1722 stack.push(!_(val).contains(field_value));
1726 _t("Unsupported operator %s in domain %s"),
1727 op, JSON.stringify(expr));
1730 return _.all(stack, _.identity);
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(blacklist) {
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(blacklist);
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 this._super(field_manager, node);
1910 this.force_disabled = false;
1911 this.string = (this.node.attrs.string || '').replace(/_/g, '');
1912 if (JSON.parse(this.node.attrs.default_focus || "0")) {
1913 // TODO fme: provide enter key binding to widgets
1914 this.view.default_focus_button = this;
1916 if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
1917 this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
1919 this.view.on('view_content_has_changed', this, this.check_disable);
1922 this._super.apply(this, arguments);
1923 this.$el.click(this.on_click);
1924 if (this.node.attrs.help || instance.session.debug) {
1925 this.do_attach_tooltip();
1927 this.setupFocus(this.$el);
1929 on_click: function() {
1931 this.force_disabled = true;
1932 this.check_disable();
1933 this.execute_action().always(function() {
1934 self.force_disabled = false;
1935 self.check_disable();
1938 execute_action: function() {
1940 var exec_action = function() {
1941 if (self.node.attrs.confirm) {
1942 var def = $.Deferred();
1943 var dialog = instance.web.dialog($('<div/>').text(self.node.attrs.confirm), {
1944 title: _t('Confirm'),
1947 {text: _t("Cancel"), click: function() {
1949 $(this).dialog("close");
1952 {text: _t("Ok"), click: function() {
1953 self.on_confirmed().then(function() {
1956 $(this).dialog("close");
1961 return def.promise();
1963 return self.on_confirmed();
1966 if (!this.node.attrs.special) {
1967 this.view.force_dirty = true;
1968 return this.view.recursive_save().pipe(exec_action);
1970 return exec_action();
1973 on_confirmed: function() {
1976 var context = this.build_context();
1978 return this.view.do_execute_action(
1979 _.extend({}, this.node.attrs, {context: context}),
1980 this.view.dataset, this.view.datarecord.id, function () {
1981 self.view.recursive_reload();
1984 check_disable: function() {
1985 var disabled = (this.force_disabled || !this.view.is_interactible_record());
1986 this.$el.prop('disabled', disabled);
1987 this.$el.css('color', disabled ? 'grey' : '');
1992 * Interface to be implemented by fields.
1995 * - changed_value: triggered when the value of the field has changed. This can be due
1996 * to a user interaction or a call to set_value().
1999 instance.web.form.FieldInterface = {
2001 * Constructor takes 2 arguments:
2002 * - field_manager: Implements FieldManagerMixin
2003 * - node: the "<field>" node in json form
2005 init: function(field_manager, node) {},
2007 * Called by the form view to indicate the value of the field.
2009 * set_value() may return an object that can be passed to $.when() that represents the moment when
2010 * the field has finished all operations necessary before the user can effectively use the widget.
2012 * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
2013 * regardless of any asynchronous operation currently running and the status of any promise that a
2014 * previous call to set_value() could have returned.
2016 * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
2017 * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
2018 * also be able to handle any other format commonly used in the _defaults key on the models in the addons
2019 * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
2020 * no information is ever given to know which format is used.
2022 set_value: function(value_) {},
2024 * Get the current value of the widget.
2026 * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
2027 * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
2028 * For example if the field is marked as "required", a call to get_value() can return false.
2030 * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
2031 * return a default value according to the type of field.
2033 * This method is always assumed to perform synchronously, it can not return a promise.
2035 * If there was no user interaction to modify the value of the field, it is always assumed that
2036 * get_value() return the same semantic value than the one passed in the last call to set_value(),
2037 * although the syntax can be different. This can be the case for type of fields that have a different
2038 * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
2040 get_value: function() {},
2042 * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
2045 set_input_id: function(id) {},
2047 * Returns true if is_syntax_valid() returns true and the value is semantically
2048 * valid too according to the semantic restrictions applied to the field.
2050 is_valid: function() {},
2052 * Returns true if the field holds a value which is syntactically correct, ignoring
2053 * the potential semantic restrictions applied to the field.
2055 is_syntax_valid: function() {},
2057 * Must set the focus on the field. Return false if field is not focusable.
2059 focus: function() {},
2061 * Called when the translate button is clicked.
2063 on_translate: function() {},
2067 * Abstract class for classes implementing FieldInterface.
2070 * - value: useful property to hold the value of the field. By default, set_value() and get_value()
2071 * set and retrieve the value property. Changing the value property also triggers automatically
2072 * a 'changed_value' event that inform the view to trigger on_changes.
2075 instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
2077 * @constructs instance.web.form.AbstractField
2078 * @extends instance.web.form.FormWidget
2080 * @param field_manager
2083 init: function(field_manager, node) {
2085 this._super(field_manager, node);
2086 this.name = this.node.attrs.name;
2087 this.field = this.field_manager.get_field_desc(this.name);
2088 this.widget = this.node.attrs.widget;
2089 this.string = this.node.attrs.string || this.field.string || this.name;
2091 this.options = JSON.parse(this.node.attrs.options || '{}');
2093 throw new Error(_.str.sprintf(_t("Widget options for field '%s' are not valid JSON."), this.name));
2095 this.set({'value': false});
2097 this.on("change:value", this, function() {
2098 this.trigger('changed_value');
2099 this._check_css_flags();
2102 renderElement: function() {
2105 if (this.field.translate && this.view) {
2106 this.$el.addClass('oe_form_field_translatable');
2107 this.$el.find('.oe_field_translate').click(this.on_translate);
2109 this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
2110 if (instance.session.debug) {
2111 this.do_attach_tooltip(this, this.$label[0] || this.$el);
2112 this.$label.off('dblclick').on('dblclick', function() {
2113 console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
2115 console.log("window.w =", window.w);
2118 if (!this.disable_utility_classes) {
2119 this.off("change:required", this, this._set_required);
2120 this.on("change:required", this, this._set_required);
2121 this._set_required();
2123 this._check_visibility();
2124 this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
2125 this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
2126 this._check_css_flags();
2129 * Private. Do not use.
2131 _set_required: function() {
2132 this.$el.toggleClass('oe_form_required', this.get("required"));
2134 set_value: function(value_) {
2135 this.set({'value': value_});
2137 get_value: function() {
2138 return this.get('value');
2140 is_valid: function() {
2141 return this.is_syntax_valid() && !(this.get('required') && this.is_false());
2143 is_syntax_valid: function() {
2147 * Method useful to implement to ease validity testing. Must return true if the current
2148 * value is similar to false in OpenERP.
2150 is_false: function() {
2151 return this.get('value') === false;
2153 _check_css_flags: function() {
2154 if (this.field.translate) {
2155 this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
2157 if (!this.disable_utility_classes) {
2158 if (this.field_manager.get('display_invalid_fields')) {
2159 this.$el.toggleClass('oe_form_invalid', !this.is_valid());
2166 set_input_id: function(id) {
2167 this.id_for_label = id;
2169 on_translate: function() {
2171 var trans = new instance.web.DataSet(this, 'ir.translation');
2172 return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).then(function(r) {
2179 * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
2182 instance.web.form.ReinitializeWidgetMixin = {
2184 * Default implementation of start(), use it or call explicitly initialize_field().
2188 this.initialize_field();
2190 initialize_field: function() {
2191 this.on("change:effective_readonly", this, this.reinitialize);
2192 this.initialize_content();
2194 reinitialize: function() {
2195 this.destroy_content();
2196 this.renderElement();
2197 this.initialize_content();
2200 * Called to destroy anything that could have been created previously, called before a
2201 * re-initialization.
2203 destroy_content: function() {},
2205 * Called to initialize the content.
2207 initialize_content: function() {},
2211 * A mixin to apply on any field that has to completely re-render when its readonly state
2214 instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
2215 initialize_field: function() {
2216 instance.web.form.ReinitializeWidgetMixin.initialize_field.call(this);
2217 this.render_value();
2219 reinitialize: function() {
2220 instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
2221 this.render_value();
2224 * Called to render the value. Should also be explicitly called at the end of a set_value().
2226 render_value: function() {},
2229 instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2230 template: 'FieldChar',
2231 widget_class: 'oe_form_field_char',
2232 init: function (field_manager, node) {
2233 this._super(field_manager, node);
2234 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2236 initialize_content: function() {
2238 var $input = this.$el.find('input');
2239 $input.change(function() {
2240 self.set({'value': self.parse_value($input.val())});
2242 this.setupFocus($input);
2244 set_value: function(value_) {
2245 this._super(value_);
2246 this.render_value();
2248 render_value: function() {
2249 var show_value = this.format_value(this.get('value'), '');
2250 if (!this.get("effective_readonly")) {
2251 this.$el.find('input').val(show_value);
2253 if (this.password) {
2254 show_value = new Array(show_value.length + 1).join('*');
2256 this.$(".oe_form_char_content").text(show_value);
2259 is_syntax_valid: function() {
2260 if (!this.get("effective_readonly")) {
2262 var value_ = this.parse_value(this.$el.find('input').val(), '');
2270 parse_value: function(val, def) {
2271 return instance.web.parse_value(val, this, def);
2273 format_value: function(val, def) {
2274 return instance.web.format_value(val, this, def);
2276 is_false: function() {
2277 return this.get('value') === '' || this._super();
2280 this.$('input:first').focus();
2284 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2288 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2289 template: 'FieldEmail',
2290 initialize_content: function() {
2292 var $button = this.$el.find('button');
2293 $button.click(this.on_button_clicked);
2294 this.setupFocus($button);
2296 render_value: function() {
2297 if (!this.get("effective_readonly")) {
2301 .attr('href', 'mailto:' + this.get('value'))
2302 .text(this.get('value') || '');
2305 on_button_clicked: function() {
2306 if (!this.get('value') || !this.is_syntax_valid()) {
2307 this.do_warn("E-mail error", "Can't send email to invalid e-mail address");
2309 location.href = 'mailto:' + this.get('value');
2314 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2315 template: 'FieldUrl',
2316 initialize_content: function() {
2318 var $button = this.$el.find('button');
2319 $button.click(this.on_button_clicked);
2320 this.setupFocus($button);
2322 render_value: function() {
2323 if (!this.get("effective_readonly")) {
2326 var tmp = this.get('value');
2327 var s = /(\w+):(.+)/.exec(tmp);
2329 tmp = "http://" + this.get('value');
2331 this.$el.find('a').attr('href', tmp).text(this.get('value') ? tmp : '');
2334 on_button_clicked: function() {
2335 if (!this.get('value')) {
2336 this.do_warn("Resource error", "This resource is empty");
2338 var url = $.trim(this.get('value'));
2339 if(/^www\./i.test(url))
2340 url = 'http://'+url;
2346 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2347 is_field_number: true,
2348 widget_class: 'oe_form_field_float',
2349 init: function (field_manager, node) {
2350 this._super(field_manager, node);
2351 this.set({'value': 0});
2352 if (this.node.attrs.digits) {
2353 this.digits = this.node.attrs.digits;
2355 this.digits = this.field.digits;
2358 set_value: function(value_) {
2359 if (value_ === false || value_ === undefined) {
2360 // As in GTK client, floats default to 0
2363 this._super.apply(this, [value_]);
2365 focus: function () {
2366 this.$('input:first').select();
2370 instance.web.DateTimeWidget = instance.web.Widget.extend({
2371 template: "web.datepicker",
2372 jqueryui_object: 'datetimepicker',
2373 type_of_date: "datetime",
2374 init: function(parent) {
2375 this._super(parent);
2376 this.name = parent.name;
2380 this.$input = this.$el.find('input.oe_datepicker_master');
2381 this.$input_picker = this.$el.find('input.oe_datepicker_container');
2382 this.$input.change(this.on_change);
2384 onClose: this.on_picker_select,
2385 onSelect: this.on_picker_select,
2389 showButtonPanel: true,
2390 firstDay: Date.CultureInfo.firstDayOfWeek
2392 this.$el.find('img.oe_datepicker_trigger').click(function() {
2393 if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
2394 self.$input.focus();
2397 self.picker('setDate', self.value ? instance.web.auto_str_to_date(self.value) : new Date());
2398 self.$input_picker.show();
2399 self.picker('show');
2400 self.$input_picker.hide();
2402 this.set_readonly(false);
2403 this.set({'value': false});
2405 picker: function() {
2406 return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
2408 on_picker_select: function(text, instance_) {
2409 var date = this.picker('getDate');
2411 .val(date ? this.format_client(date) : '')
2415 set_value: function(value_) {
2416 this.set({'value': value_});
2417 this.$input.val(value_ ? this.format_client(value_) : '');
2419 get_value: function() {
2420 return this.get('value');
2422 set_value_from_ui_: function() {
2423 var value_ = this.$input.val() || false;
2424 this.set({'value': this.parse_client(value_)});
2426 set_readonly: function(readonly) {
2427 this.readonly = readonly;
2428 this.$input.prop('readonly', this.readonly);
2429 this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
2431 is_valid_: function() {
2432 var value_ = this.$input.val();
2433 if (value_ === "") {
2437 this.parse_client(value_);
2444 parse_client: function(v) {
2445 return instance.web.parse_value(v, {"widget": this.type_of_date});
2447 format_client: function(v) {
2448 return instance.web.format_value(v, {"widget": this.type_of_date});
2450 on_change: function() {
2451 if (this.is_valid_()) {
2452 this.set_value_from_ui_();
2457 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2458 jqueryui_object: 'datepicker',
2459 type_of_date: "date"
2462 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2463 template: "FieldDatetime",
2464 build_widget: function() {
2465 return new instance.web.DateTimeWidget(this);
2467 destroy_content: function() {
2468 if (this.datewidget) {
2469 this.datewidget.destroy();
2470 this.datewidget = undefined;
2473 initialize_content: function() {
2474 if (!this.get("effective_readonly")) {
2475 this.datewidget = this.build_widget();
2476 this.datewidget.on_change.add_last(_.bind(function() {
2477 this.set({'value': this.datewidget.get_value()});
2479 this.datewidget.appendTo(this.$el);
2480 this.setupFocus(this.datewidget.$input);
2483 set_value: function(value_) {
2484 this._super(value_);
2485 this.render_value();
2487 render_value: function() {
2488 if (!this.get("effective_readonly")) {
2489 this.datewidget.set_value(this.get('value'));
2491 this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2494 is_syntax_valid: function() {
2495 if (!this.get("effective_readonly")) {
2496 return this.datewidget.is_valid_();
2500 is_false: function() {
2501 return this.get('value') === '' || this._super();
2504 if (this.datewidget && this.datewidget.$input) {
2505 this.datewidget.$input.focus();
2510 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2511 template: "FieldDate",
2512 build_widget: function() {
2513 return new instance.web.DateWidget(this);
2517 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2518 template: 'FieldText',
2519 init: function (field_manager, node) {
2520 this._super(field_manager, node);
2522 initialize_content: function() {
2524 this.$textarea = this.$el.find('textarea');
2525 this.default_height = this.$textarea.css('height');
2526 if (!this.get("effective_readonly")) {
2527 this.$textarea.change(_.bind(function() {
2528 self.set({'value': instance.web.parse_value(self.$textarea.val(), self)});
2531 this.$textarea.attr('disabled', 'disabled');
2533 this.$el.keyup(function (e) {
2534 if (e.which === $.ui.keyCode.ENTER) {
2535 e.stopPropagation();
2538 this.setupFocus(this.$textarea);
2540 set_value: function(value_) {
2541 this._super(value_);
2542 this.render_value();
2545 render_value: function() {
2546 var show_value = instance.web.format_value(this.get('value'), this, '');
2547 if (show_value === '') {
2548 this.$textarea.css('height', parseInt(this.default_height)+"px");
2550 this.$textarea.val(show_value);
2551 this.$textarea.autosize();
2553 is_syntax_valid: function() {
2554 if (!this.get("effective_readonly")) {
2556 var value_ = instance.web.parse_value(this.$textarea.val(), this, '');
2564 is_false: function() {
2565 return this.get('value') === '' || this._super();
2567 focus: function($el) {
2568 this.$textarea.focus();
2573 * FieldTextHtml Widget
2574 * Intended for FieldText widgets meant to display HTML content. This
2575 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2576 * To find more information about CLEditor configutation: go to
2577 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2579 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2580 template: 'FieldTextHtml',
2582 this._super.apply(this, arguments);
2584 initialize_content: function() {
2586 if (! this.get("effective_readonly")) {
2587 self._updating_editor = false;
2588 this.$textarea = this.$el.find('textarea');
2589 var width = ((this.node.attrs || {}).editor_width || 468);
2590 var height = ((this.node.attrs || {}).editor_height || 250);
2591 this.$textarea.cleditor({
2592 width: width, // width not including margins, borders or padding
2593 height: height, // height not including margins, borders or padding
2594 controls: // controls to add to the toolbar
2595 "bold italic underline strikethrough " +
2596 "| removeformat | bullets numbering | outdent " +
2597 "indent | link unlink | source",
2598 bodyStyle: // style to assign to document body contained within the editor
2599 "margin:4px; font:12px monospace; cursor:text; color:#1F1F1F"
2601 this.$cleditor = this.$textarea.cleditor()[0];
2602 this.$cleditor.change(function() {
2603 if (! self._updating_editor) {
2604 self.$cleditor.updateTextArea();
2605 self.set({'value': self.$textarea.val()});
2610 set_value: function(value_) {
2611 this._super.apply(this, arguments);
2612 this.render_value();
2614 render_value: function() {
2615 if (! this.get("effective_readonly")) {
2616 this.$textarea.val(this.get('value') || '');
2617 this._updating_editor = true;
2618 this.$cleditor.updateFrame();
2619 this._updating_editor = false;
2621 this.$el.html(this.get('value'));
2626 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2627 template: 'FieldBoolean',
2630 this._super.apply(this, arguments);
2631 this.$checkbox = $("input", this.$el);
2632 this.setupFocus(this.$checkbox);
2633 this.$el.click(_.bind(function() {
2634 this.set({'value': this.$checkbox.is(':checked')});
2636 var check_readonly = function() {
2637 self.$checkbox.prop('disabled', self.get("effective_readonly"));
2639 this.on("change:effective_readonly", this, check_readonly);
2640 check_readonly.call(this);
2642 set_value: function(value_) {
2643 this._super.apply(this, arguments);
2644 this.$checkbox[0].checked = value_;
2647 this.$checkbox.focus();
2651 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2652 template: 'FieldProgressBar',
2654 this._super.apply(this, arguments);
2655 this.$el.progressbar({
2656 value: this.get('value'),
2657 disabled: this.get("effective_readonly")
2660 set_value: function(value_) {
2661 this._super.apply(this, arguments);
2662 var show_value = Number(value_);
2663 if (isNaN(show_value)) {
2666 var formatted_value = instance.web.format_value(show_value, { type : 'float' }, '0');
2667 this.$el.progressbar('option', 'value', show_value).find('span').html(formatted_value + '%');
2672 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2673 template: 'FieldSelection',
2674 init: function(field_manager, node) {
2676 this._super(field_manager, node);
2677 this.values = _.clone(this.field.selection);
2678 _.each(this.values, function(v, i) {
2679 if (v[0] === false && v[1] === '') {
2680 self.values.splice(i, 1);
2683 this.values.unshift([false, '']);
2685 initialize_content: function() {
2686 // Flag indicating whether we're in an event chain containing a change
2687 // event on the select, in order to know what to do on keyup[RETURN]:
2688 // * If the user presses [RETURN] as part of changing the value of a
2689 // selection, we should just let the value change and not let the
2690 // event broadcast further (e.g. to validating the current state of
2691 // the form in editable list view, which would lead to saving the
2692 // current row or switching to the next one)
2693 // * If the user presses [RETURN] with a select closed (side-effect:
2694 // also if the user opened the select and pressed [RETURN] without
2695 // changing the selected value), takes the action as validating the
2697 var ischanging = false;
2698 var $select = this.$el.find('select')
2699 .change(_.bind(function() {
2700 this.set({'value': this.values[this.$el.find('select')[0].selectedIndex][0]});
2702 .change(function () { ischanging = true; })
2703 .click(function () { ischanging = false; })
2704 .keyup(function (e) {
2705 if (e.which !== 13 || !ischanging) { return; }
2706 e.stopPropagation();
2709 this.setupFocus($select);
2711 set_value: function(value_) {
2712 value_ = value_ === null ? false : value_;
2713 value_ = value_ instanceof Array ? value_[0] : value_;
2714 this._super(value_);
2715 this.render_value();
2717 render_value: function() {
2718 if (!this.get("effective_readonly")) {
2720 for (var i = 0, ii = this.values.length; i < ii; i++) {
2721 if (this.values[i][0] === this.get('value')) index = i;
2723 this.$el.find('select')[0].selectedIndex = index;
2726 var option = _(this.values)
2727 .detect(function (record) { return record[0] === self.get('value'); });
2728 this.$el.text(option ? option[1] : this.values[0][1]);
2731 is_syntax_valid: function() {
2732 if (this.get("effective_readonly")) {
2735 var value_ = this.values[this.$el.find('select')[0].selectedIndex];
2739 this.$el.find('select:first').focus();
2743 // jquery autocomplete tweak to allow html and classnames
2745 var proto = $.ui.autocomplete.prototype,
2746 initSource = proto._initSource;
2748 function filter( array, term ) {
2749 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
2750 return $.grep( array, function(value_) {
2751 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
2756 _initSource: function() {
2757 if ( this.options.html && $.isArray(this.options.source) ) {
2758 this.source = function( request, response ) {
2759 response( filter( this.options.source, request.term ) );
2762 initSource.call( this );
2766 _renderItem: function( ul, item) {
2767 return $( "<li></li>" )
2768 .data( "item.autocomplete", item )
2769 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
2771 .addClass(item.classname);
2777 * A mixin containing some useful methods to handle completion inputs.
2779 instance.web.form.CompletionFieldMixin = {
2782 this.orderer = new instance.web.DropMisordered();
2785 * Call this method to search using a string.
2787 get_search_result: function(search_val) {
2790 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
2791 var blacklist = this.get_search_blacklist();
2792 this.last_query = search_val;
2794 return this.orderer.add(dataset.name_search(
2795 search_val, new instance.web.CompoundDomain(self.build_domain(), [["id", "not in", blacklist]]),
2796 'ilike', this.limit + 1, self.build_context())).pipe(function(data) {
2797 self.last_search = data;
2798 // possible selections for the m2o
2799 var values = _.map(data, function(x) {
2800 x[1] = x[1].split("\n")[0];
2802 label: _.str.escapeHTML(x[1]),
2809 // search more... if more results that max
2810 if (values.length > self.limit) {
2811 values = values.slice(0, self.limit);
2813 label: _t("Search More..."),
2814 action: function() {
2815 dataset.name_search(search_val, self.build_domain(), 'ilike', false).then(function(data) {
2816 self._search_create_popup("search", data);
2819 classname: 'oe_m2o_dropdown_option'
2823 var raw_result = _(data.result).map(function(x) {return x[1];});
2824 if (search_val.length > 0 && !_.include(raw_result, search_val)) {
2826 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
2827 $('<span />').text(search_val).html()),
2828 action: function() {
2829 self._quick_create(search_val);
2831 classname: 'oe_m2o_dropdown_option'
2836 label: _t("Create and Edit..."),
2837 action: function() {
2838 self._search_create_popup("form", undefined, self._create_context(search_val));
2840 classname: 'oe_m2o_dropdown_option'
2846 get_search_blacklist: function() {
2849 _quick_create: function(name) {
2851 var slow_create = function () {
2852 self._search_create_popup("form", undefined, self._create_context(name));
2854 if (self.options.quick_create === undefined || self.options.quick_create) {
2855 new instance.web.DataSet(this, this.field.relation, self.build_context())
2856 .name_create(name).then(function(data) {
2857 self.add_id(data[0]);
2858 }).fail(function(error, event) {
2859 event.preventDefault();
2865 // all search/create popup handling
2866 _search_create_popup: function(view, ids, context) {
2868 var pop = new instance.web.form.SelectCreatePopup(this);
2870 self.field.relation,
2872 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
2873 initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
2875 disable_multiple_selection: true
2877 self.build_domain(),
2878 new instance.web.CompoundContext(self.build_context(), context || {})
2880 pop.on_select_elements.add(function(element_ids) {
2881 self.add_id(element_ids[0]);
2888 add_id: function(id) {},
2889 _create_context: function(name) {
2891 var field = (this.options || {}).create_name_field;
2892 if (field === undefined)
2894 if (field !== false && name && (this.options || {}).quick_create !== false)
2895 tmp["default_" + field] = name;
2900 instance.web.form.M2ODialog = instance.web.Dialog.extend({
2901 template: "M2ODialog",
2902 init: function(parent) {
2903 this._super(parent, {
2904 title: _.str.sprintf(_t("Add %s"), parent.string),
2910 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
2911 this.$("input").val(this.getParent().last_query);
2912 this.$buttons.find(".oe_form_m2o_qc_button").click(function(){
2913 self.getParent()._quick_create(self.$("input").val());
2916 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
2917 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
2920 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
2926 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
2927 template: "FieldMany2One",
2928 init: function(field_manager, node) {
2929 this._super(field_manager, node);
2930 instance.web.form.CompletionFieldMixin.init.call(this);
2931 this.set({'value': false});
2932 this.display_value = {};
2933 this.last_search = [];
2934 this.floating = false;
2935 this.inhibit_on_change = false;
2936 this.current_display = null;
2940 instance.web.form.ReinitializeFieldMixin.start.call(this);
2941 this.on("change:value", this, function() {
2942 this.floating = false;
2943 this.render_value();
2945 instance.web.bus.on('click', this, function() {
2946 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
2947 this.$input.autocomplete("close");
2951 initialize_content: function() {
2952 if (!this.get("effective_readonly"))
2953 this.render_editable();
2954 this.render_value();
2956 init_error_displayer: function() {
2959 hide_error_displayer: function() {
2962 show_error_displayer: function() {
2963 new instance.web.form.M2ODialog(this).open();
2965 render_editable: function() {
2967 this.$input = this.$el.find("input");
2969 this.init_error_displayer();
2971 self.$input.on('focus', function() {
2972 self.hide_error_displayer();
2975 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
2976 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
2978 this.$follow_button.click(function(ev) {
2979 ev.preventDefault();
2980 if (!self.get('value')) {
2984 var pop = new instance.web.form.FormOpenPopup(self);
2986 self.field.relation,
2988 self.build_context(),
2990 title: _t("Open: ") + self.string
2993 pop.on('write_completed', self, function(){
2994 self.display_value = {};
2995 self.render_value();
3000 // some behavior for input
3001 var input_changed = function() {
3002 if (self.current_display !== self.$input.val()) {
3003 self.current_display = self.$input.val();
3004 if (self.$input.val() === "") {
3005 self.set({value: false});
3006 self.floating = false;
3008 self.floating = true;
3012 this.$input.keydown(input_changed);
3013 this.$input.change(input_changed);
3014 this.$drop_down.click(function() {
3015 if (self.$input.autocomplete("widget").is(":visible")) {
3016 self.$input.autocomplete("close");
3017 self.$input.focus();
3019 if (self.get("value") && ! self.floating) {
3020 self.$input.autocomplete("search", "");
3022 self.$input.autocomplete("search");
3026 self.ed_def = $.Deferred();
3027 self.uned_def = $.Deferred();
3029 var ed_duration = 15000;
3030 var anyoneLoosesFocus = function() {
3032 if (self.floating) {
3033 if (self.last_search.length > 0) {
3034 if (self.last_search[0][0] != self.get("value")) {
3035 self.display_value = {};
3036 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3037 self.set({value: self.last_search[0][0]});
3040 self.render_value();
3044 self.set({value: false});
3045 self.render_value();
3047 self.floating = false;
3049 if (used && self.get("value") === false && ! self.no_ed) {
3050 self.ed_def.reject();
3051 self.uned_def.reject();
3052 self.ed_def = $.Deferred();
3053 self.ed_def.then(function() {
3054 self.show_error_displayer();
3056 setTimeout(function() {
3057 self.ed_def.resolve();
3058 self.uned_def.reject();
3059 self.uned_def = $.Deferred();
3060 self.uned_def.then(function() {
3061 self.hide_error_displayer();
3063 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3067 self.ed_def.reject();
3070 var ignore_blur = false;
3072 focusout: anyoneLoosesFocus,
3073 focus: function () { self.trigger('focused'); },
3074 autocompleteopen: function () { ignore_blur = true; },
3075 autocompleteclose: function () { ignore_blur = false; },
3077 // autocomplete open
3078 if (ignore_blur) { return; }
3079 if (_(self.getChildren()).any(function (child) {
3080 return child instanceof instance.web.form.AbstractFormPopup;
3082 self.trigger('blurred');
3086 var isSelecting = false;
3088 this.$input.autocomplete({
3089 source: function(req, resp) {
3090 self.get_search_result(req.term).then(function(result) {
3094 select: function(event, ui) {
3098 self.display_value = {};
3099 self.display_value["" + item.id] = item.name;
3100 self.set({value: item.id});
3101 } else if (item.action) {
3103 // Cancel widget blurring, to avoid form blur event
3104 self.trigger('focused');
3108 focus: function(e, ui) {
3112 // disabled to solve a bug, but may cause others
3113 //close: anyoneLoosesFocus,
3117 this.$input.autocomplete("widget").addClass("openerp");
3118 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3119 this.$input.keyup(function(e) {
3120 if (e.which === 13) { // ENTER
3122 e.stopPropagation();
3124 isSelecting = false;
3126 this.setupFocus(this.$follow_button);
3128 render_value: function(no_recurse) {
3130 if (! this.get("value")) {
3131 this.display_string("");
3134 var display = this.display_value["" + this.get("value")];
3136 this.display_string(display);
3140 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3141 dataset.name_get([self.get("value")]).then(function(data) {
3142 self.display_value["" + self.get("value")] = data[0][1];
3143 self.render_value(true);
3147 display_string: function(str) {
3149 if (!this.get("effective_readonly")) {
3150 this.$input.val(str.split("\n")[0]);
3151 this.current_display = this.$input.val();
3152 if(this.is_false()){
3153 this.$('.oe_m2o_cm_button').css({'display':'none'});
3156 var lines = _.escape(str).split("\n");
3160 follow = _.rest(lines).join("<br />");
3163 var $link = this.$el.find('.oe_form_uri')
3166 if (! this.options.no_open)
3167 $link.click(function () {
3169 type: 'ir.actions.act_window',
3170 res_model: self.field.relation,
3171 res_id: self.get("value"),
3172 context: self.build_context(),
3173 views: [[false, 'form']],
3178 $(".oe_form_m2o_follow", this.$el).html(follow);
3181 set_value: function(value_) {
3183 if (value_ instanceof Array) {
3184 this.display_value = {};
3185 if (! this.options.always_reload) {
3186 this.display_value["" + value_[0]] = value_[1];
3190 value_ = value_ || false;
3191 this.inhibit_on_change = true;
3192 this._super(value_);
3193 this.inhibit_on_change = false;
3195 get_displayed: function() {
3196 return this.display_value["" + this.get("value")];
3198 add_id: function(id) {
3199 this.display_value = {};
3200 this.set({value: id});
3202 is_false: function() {
3203 return ! this.get("value");
3205 focus: function () {
3206 this.$input.focus();
3208 _quick_create: function() {
3210 this.ed_def.reject();
3211 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3213 _search_create_popup: function() {
3215 this.ed_def.reject();
3216 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3221 # Values: (0, 0, { fields }) create
3222 # (1, ID, { fields }) update
3223 # (2, ID) remove (delete)
3224 # (3, ID) unlink one (target id or target of relation)
3226 # (5) unlink all (only valid for one2many)
3231 'create': function (values) {
3232 return [commands.CREATE, false, values];
3234 // (1, id, {values})
3236 'update': function (id, values) {
3237 return [commands.UPDATE, id, values];
3241 'delete': function (id) {
3242 return [commands.DELETE, id, false];
3244 // (3, id[, _]) removes relation, but not linked record itself
3246 'forget': function (id) {
3247 return [commands.FORGET, id, false];
3251 'link_to': function (id) {
3252 return [commands.LINK_TO, id, false];
3256 'delete_all': function () {
3257 return [5, false, false];
3259 // (6, _, ids) replaces all linked records with provided ids
3261 'replace_with': function (ids) {
3262 return [6, false, ids];
3265 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3266 multi_selection: false,
3267 disable_utility_classes: true,
3268 init: function(field_manager, node) {
3269 this._super(field_manager, node);
3270 lazy_build_o2m_kanban_view();
3271 this.is_loaded = $.Deferred();
3272 this.initial_is_loaded = this.is_loaded;
3273 this.is_setted = $.Deferred();
3274 this.form_last_update = $.Deferred();
3275 this.init_form_last_update = this.form_last_update;
3278 this._super.apply(this, arguments);
3279 this.$el.addClass('oe_form_field oe_form_field_one2many');
3283 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
3284 this.dataset.o2m = this;
3285 this.dataset.parent_view = this.view;
3286 this.dataset.child_name = this.name;
3287 this.dataset.on_change.add_last(function() {
3288 self.trigger_on_change();
3291 this.is_setted.then(function() {
3294 this.is_loaded.then(function() {
3295 self.on("change:effective_readonly", self, function() {
3296 self.is_loaded = self.is_loaded.pipe(function() {
3297 self.viewmanager.destroy();
3298 return $.when(self.load_views()).then(function() {
3299 self.reload_current_view();
3305 trigger_on_change: function() {
3306 var tmp = this.doing_on_change;
3307 this.doing_on_change = true;
3308 this.trigger('changed_value');
3309 this.doing_on_change = tmp;
3311 load_views: function() {
3314 var modes = this.node.attrs.mode;
3315 modes = !!modes ? modes.split(",") : ["tree"];
3317 _.each(modes, function(mode) {
3318 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
3319 throw new Error(_.str.sprintf("View type '%s' is not supported in One2Many.", mode));
3323 view_type: mode == "tree" ? "list" : mode,
3326 if (self.field.views && self.field.views[mode]) {
3327 view.embedded_view = self.field.views[mode];
3329 if(view.view_type === "list") {
3330 _.extend(view.options, {
3332 selectable: self.multi_selection,
3334 import_enabled: false,
3337 if (self.get("effective_readonly")) {
3338 _.extend(view.options, {
3343 } else if (view.view_type === "form") {
3344 if (self.get("effective_readonly")) {
3345 view.view_type = 'form';
3347 _.extend(view.options, {
3348 not_interactible_on_create: true,
3350 } else if (view.view_type === "kanban") {
3351 _.extend(view.options, {
3352 confirm_on_delete: false,
3354 if (self.get("effective_readonly")) {
3355 _.extend(view.options, {
3356 action_buttons: false,
3357 quick_creatable: false,
3359 read_only_mode: true,
3367 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
3368 this.viewmanager.o2m = self;
3369 var once = $.Deferred().then(function() {
3370 self.init_form_last_update.resolve();
3372 var def = $.Deferred().then(function() {
3373 self.initial_is_loaded.resolve();
3375 this.viewmanager.on_controller_inited.add_last(function(view_type, controller) {
3376 controller.o2m = self;
3377 if (view_type == "list") {
3378 if (self.get("effective_readonly")) {
3379 controller.on('edit:before', self, function (e) {
3383 } else if (view_type === "form") {
3384 if (self.get("effective_readonly")) {
3385 $(".oe_form_buttons", controller.$el).children().remove();
3387 controller.on("load_record", self, function(){
3390 controller.on_pager_action.add_first(function() {
3391 self.save_any_view();
3393 } else if (view_type == "graph") {
3394 self.reload_current_view()
3398 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
3399 $.when(self.save_any_view()).then(function() {
3400 if (n_mode === "list") {
3401 $.async_when().then(function() {
3402 self.reload_current_view();
3407 this.is_setted.then(function() {
3408 $.async_when().then(function () {
3409 self.viewmanager.appendTo(self.$el);
3414 reload_current_view: function() {
3416 return self.is_loaded = self.is_loaded.pipe(function() {
3417 var active_view = self.viewmanager.active_view;
3418 var view = self.viewmanager.views[active_view].controller;
3419 if(active_view === "list") {
3420 return view.reload_content();
3421 } else if (active_view === "form") {
3422 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
3423 self.dataset.index = 0;
3425 var act = function() {
3426 return view.do_show();
3428 self.form_last_update = self.form_last_update.pipe(act, act);
3429 return self.form_last_update;
3430 } else if (view.do_search) {
3431 return view.do_search(self.build_domain(), self.dataset.get_context(), []);
3435 set_value: function(value_) {
3436 value_ = value_ || [];
3438 this.dataset.reset_ids([]);
3439 if(value_.length >= 1 && value_[0] instanceof Array) {
3441 _.each(value_, function(command) {
3442 var obj = {values: command[2]};
3443 switch (command[0]) {
3444 case commands.CREATE:
3445 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3447 self.dataset.to_create.push(obj);
3448 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3451 case commands.UPDATE:
3452 obj['id'] = command[1];
3453 self.dataset.to_write.push(obj);
3454 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3457 case commands.DELETE:
3458 self.dataset.to_delete.push({id: command[1]});
3460 case commands.LINK_TO:
3461 ids.push(command[1]);
3463 case commands.DELETE_ALL:
3464 self.dataset.delete_all = true;
3469 this.dataset.set_ids(ids);
3470 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
3472 this.dataset.delete_all = true;
3473 _.each(value_, function(command) {
3474 var obj = {values: command};
3475 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3477 self.dataset.to_create.push(obj);
3478 self.dataset.cache.push(_.clone(obj));
3482 this.dataset.set_ids(ids);
3484 this._super(value_);
3485 this.dataset.reset_ids(value_);
3487 if (this.dataset.index === null && this.dataset.ids.length > 0) {
3488 this.dataset.index = 0;
3490 self.is_setted.resolve();
3491 this.trigger_on_change();
3492 return self.reload_current_view();
3494 get_value: function() {
3498 this.save_any_view();
3499 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
3500 val = val.concat(_.map(this.dataset.ids, function(id) {
3501 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
3503 return commands.create(alter_order.values);
3505 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
3507 return commands.update(alter_order.id, alter_order.values);
3509 return commands.link_to(id);
3511 return val.concat(_.map(
3512 this.dataset.to_delete, function(x) {
3513 return commands['delete'](x.id);}));
3515 save_any_view: function() {
3516 if (this.doing_on_change)
3518 return this.session.synchronized_mode(_.bind(function() {
3519 if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
3520 this.viewmanager.views[this.viewmanager.active_view] &&
3521 this.viewmanager.views[this.viewmanager.active_view].controller) {
3522 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3523 if (this.viewmanager.active_view === "form") {
3524 if (!view.is_initialized.isResolved()) {
3527 var res = $.when(view.do_save());
3528 if (!res.isResolved() && !res.isRejected()) {
3529 console.warn("Asynchronous get_value() is not supported in form view.");
3532 } else if (this.viewmanager.active_view === "list") {
3533 var res = $.when(view.ensure_saved());
3534 if (!res.isResolved() && !res.isRejected()) {
3535 console.warn("Asynchronous get_value() is not supported in list view.");
3543 is_syntax_valid: function() {
3544 if (! this.viewmanager || ! this.viewmanager.views[this.viewmanager.active_view])
3546 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3547 switch (this.viewmanager.active_view) {
3549 return _(view.fields).chain()
3555 return view.is_valid();
3561 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
3562 template: 'One2Many.viewmanager',
3563 init: function(parent, dataset, views, flags) {
3564 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
3565 this.registry = this.registry.extend({
3566 list: 'instance.web.form.One2ManyListView',
3567 form: 'instance.web.form.One2ManyFormView',
3568 kanban: 'instance.web.form.One2ManyKanbanView',
3570 this.__ignore_blur = false;
3572 switch_mode: function(mode, unused) {
3573 if (mode !== 'form') {
3574 return this._super(mode, unused);
3577 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
3578 var pop = new instance.web.form.FormOpenPopup(this);
3579 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3580 title: _t("Open: ") + self.o2m.string,
3581 create_function: function(data) {
3582 return self.o2m.dataset.create(data).then(function(r) {
3583 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3584 self.o2m.dataset.on_change();
3587 write_function: function(id, data, options) {
3588 return self.o2m.dataset.write(id, data, {}).then(function() {
3589 self.o2m.reload_current_view();
3592 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3593 parent_view: self.o2m.view,
3594 child_name: self.o2m.name,
3595 read_function: function() {
3596 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3598 form_view_options: {'not_interactible_on_create':true},
3599 readonly: self.o2m.get("effective_readonly")
3601 pop.on_select_elements.add_last(function() {
3602 self.o2m.reload_current_view();
3607 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
3608 get_context: function() {
3609 this.context = this.o2m.build_context([this.o2m.name]);
3610 return this.context;
3614 instance.web.form.One2ManyListView = instance.web.ListView.extend({
3615 _template: 'One2Many.listview',
3616 init: function (parent, dataset, view_id, options) {
3617 this._super(parent, dataset, view_id, _.extend(options || {}, {
3618 ListType: instance.web.form.One2ManyList
3620 this.on('edit:before', this, this.proxy('_before_edit'));
3621 this.on('edit:after', this, this.proxy('_after_edit'));
3622 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
3625 .bind('add', this.proxy("changed_records"))
3626 .bind('edit', this.proxy("changed_records"))
3627 .bind('remove', this.proxy("changed_records"));
3629 start: function () {
3630 var ret = this._super();
3632 .off('mousedown.handleButtons')
3633 .on('mousedown.handleButtons', 'table button', this.proxy('_button_down'));
3636 changed_records: function () {
3637 this.o2m.trigger_on_change();
3639 is_valid: function () {
3640 var form = this.editor.form;
3642 // If the form has not been modified, the view can only be valid
3643 // NB: is_dirty will also be set on defaults/onchanges/whatever?
3644 // oe_form_dirty seems to only be set on actual user actions
3645 if (!form.$el.is('.oe_form_dirty')) {
3648 this.o2m._dirty_flag = true;
3650 // Otherwise validate internal form
3651 return _(form.fields).chain()
3652 .invoke(function () {
3653 this._check_css_flags();
3654 return this.is_valid();
3659 do_add_record: function () {
3660 if (this.editable()) {
3661 this._super.apply(this, arguments);
3664 var pop = new instance.web.form.SelectCreatePopup(this);
3666 self.o2m.field.relation,
3668 title: _t("Create: ") + self.o2m.string,
3669 initial_view: "form",
3670 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3671 create_function: function(data, callback, error_callback) {
3672 return self.o2m.dataset.create(data).then(function(r) {
3673 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3674 self.o2m.dataset.on_change();
3675 }).then(callback, error_callback);
3677 read_function: function() {
3678 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3680 parent_view: self.o2m.view,
3681 child_name: self.o2m.name,
3682 form_view_options: {'not_interactible_on_create':true}
3684 self.o2m.build_domain(),
3685 self.o2m.build_context()
3687 pop.on_select_elements.add_last(function() {
3688 self.o2m.reload_current_view();
3692 do_activate_record: function(index, id) {
3694 var pop = new instance.web.form.FormOpenPopup(self);
3695 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3696 title: _t("Open: ") + self.o2m.string,
3697 write_function: function(id, data) {
3698 return self.o2m.dataset.write(id, data, {}).then(function() {
3699 self.o2m.reload_current_view();
3702 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3703 parent_view: self.o2m.view,
3704 child_name: self.o2m.name,
3705 read_function: function() {
3706 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3708 form_view_options: {'not_interactible_on_create':true},
3709 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
3712 do_button_action: function (name, id, callback) {
3713 if (!_.isNumber(id)) {
3714 instance.webclient.notification.warn(
3715 _t("Action Button"),
3716 _t("The o2m record must be saved before an action can be used"));
3719 var parent_form = this.o2m.view;
3721 this.ensure_saved().pipe(function () {
3723 return parent_form.do_save();
3726 }).then(function () {
3727 self.handle_button(name, id, callback);
3731 _before_edit: function () {
3732 this.__ignore_blur = false;
3733 this.editor.form.on('blurred', this, this._on_form_blur);
3735 _after_edit: function () {
3736 // The form's blur thing may be jiggered during the edition setup,
3737 // potentially leading to the o2m instasaving the row. Cancel any
3738 // blurring triggered the edition startup here
3739 this.editor.form.widgetFocused();
3741 _before_unedit: function () {
3742 this.editor.form.off('blurred', this, this._on_form_blur);
3744 _button_down: function () {
3745 // If a button is clicked (usually some sort of action button), it's
3746 // the button's responsibility to ensure the editable list is in the
3747 // correct state -> ignore form blurring
3748 this.__ignore_blur = true;
3751 * Handles blurring of the nested form (saves the currently edited row),
3752 * unless the flag to ignore the event is set to ``true``
3754 * Makes the internal form go away
3756 _on_form_blur: function () {
3757 if (this.__ignore_blur) {
3758 this.__ignore_blur = false;
3761 // FIXME: why isn't there an API for this?
3762 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
3763 this.save_edition();
3766 this.cancel_edition();
3768 keyup_ENTER: function () {
3769 // blurring caused by hitting the [Return] key, should skip the
3770 // autosave-on-blur and let the handler for [Return] do its thing (save
3771 // the current row *anyway*, then create a new one/edit the next one)
3772 this.__ignore_blur = true;
3773 this._super.apply(this, arguments);
3775 do_delete: function (ids) {
3777 var next = $.when();
3778 var _super = this._super;
3779 // handle deletion of an item which does not exist
3780 // TODO: better handle that in the editable list?
3781 var false_id_index = _(ids).indexOf(false);
3782 if (false_id_index !== -1) {
3783 ids.splice(false_id_index, 1);
3784 next = this.cancel_edition(true);
3786 return next.pipe(function () {
3788 var confirm = window.confirm;
3789 window.confirm = function () { return true; };
3791 return _super.call(self, ids);
3793 window.confirm = confirm;
3798 instance.web.form.One2ManyList = instance.web.ListView.List.extend({
3799 pad_table_to: function (count) {
3800 if (!this.view.is_action_enabled('create')) {
3803 this._super(count > 0 ? count - 1 : 0);
3806 // magical invocation of wtf does that do
3807 if (this.view.o2m.get('effective_readonly')) {
3812 var columns = _(this.columns).filter(function (column) {
3813 return column.invisible !== '1';
3815 if (this.options.selectable) { columns++; }
3816 if (this.options.deletable) { columns++; }
3818 if (!this.view.is_action_enabled('create')) {
3822 var $cell = $('<td>', {
3824 'class': 'oe_form_field_one2many_list_row_add'
3826 $('<a>', {href: '#'}).text(_t("Add an item"))
3827 .mousedown(function () {
3828 // FIXME: needs to be an official API somehow
3829 if (self.view.editor.is_editing()) {
3830 self.view.__ignore_blur = true;
3833 .click(function (e) {
3835 e.stopPropagation();
3836 // FIXME: there should also be an API for that one
3837 if (self.view.editor.form.__blur_timeout) {
3838 clearTimeout(self.view.editor.form.__blur_timeout);
3839 self.view.editor.form.__blur_timeout = false;
3841 self.view.ensure_saved().then(function () {
3842 self.view.do_add_record();
3846 var $padding = this.$current.find('tr:not([data-id]):first');
3847 var $newrow = $('<tr>').append($cell);
3848 if ($padding.length) {
3849 $padding.before($newrow);
3851 this.$current.append($newrow)
3856 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
3857 form_template: 'One2Many.formview',
3858 on_loaded: function(data) {
3861 this.$buttons.find('button.oe_form_button_create').click(function() {
3862 self.do_save().then(self.on_button_new);
3865 do_notify_change: function() {
3866 if (this.dataset.parent_view) {
3867 this.dataset.parent_view.do_notify_change();
3869 this._super.apply(this, arguments);
3874 var lazy_build_o2m_kanban_view = function() {
3875 if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
3877 instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
3881 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3882 template: "FieldMany2ManyTags",
3884 this._super.apply(this, arguments);
3885 instance.web.form.CompletionFieldMixin.init.call(this);
3886 this.set({"value": []});
3887 this._display_orderer = new instance.web.DropMisordered();
3888 this._drop_shown = false;
3892 instance.web.form.ReinitializeFieldMixin.start.call(this);
3893 this.on("change:value", this, this.render_value);
3895 initialize_content: function() {
3896 if (this.get("effective_readonly"))
3899 self.$text = $("textarea", this.$el);
3900 self.$text.textext({
3901 plugins : 'tags arrow autocomplete',
3903 render: function(suggestion) {
3904 return $('<span class="text-label"/>').
3905 data('index', suggestion['index']).html(suggestion['label']);
3910 selectFromDropdown: function() {
3911 $(this).trigger('hideDropdown');
3912 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
3913 var data = self.search_result[index];
3915 self.add_id(data.id);
3922 isTagAllowed: function(tag) {
3927 removeTag: function(tag) {
3928 var id = tag.data("id");
3929 self.set({"value": _.without(self.get("value"), id)});
3931 renderTag: function(stuff) {
3932 return $.fn.textext.TextExtTags.prototype.renderTag.
3933 call(this, stuff).data("id", stuff.id);
3937 itemToString: function(item) {
3942 }).bind('getSuggestions', function(e, data) {
3944 var str = !!data ? data.query || '' : '';
3945 self.get_search_result(str).then(function(result) {
3946 self.search_result = result;
3947 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
3948 return _.extend(el, {index:i});
3951 }).bind('hideDropdown', function() {
3952 self._drop_shown = false;
3953 }).bind('showDropdown', function() {
3954 self._drop_shown = true;
3956 self.tags = self.$text.textext()[0].tags();
3957 $("textarea", this.$el).focusout(function() {
3958 self.$text.trigger("setInputData", "");
3959 }).keydown(function(e) {
3960 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
3961 self.$text.textext()[0].autocomplete().selectFromDropdown();
3965 set_value: function(value_) {
3966 value_ = value_ || [];
3967 if (value_.length >= 1 && value_[0] instanceof Array) {
3968 value_ = value_[0][2];
3970 this._super(value_);
3972 get_value: function() {
3973 var tmp = [commands.replace_with(this.get("value"))];
3976 get_search_blacklist: function() {
3977 return this.get("value");
3979 render_value: function() {
3981 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3982 var values = self.get("value")
3983 var handle_names = function(data) {
3985 _.each(data, function(el) {
3986 indexed[el[0]] = el;
3988 data = _.map(values, function(el) { return indexed[el]; });
3989 if (! self.get("effective_readonly")) {
3990 self.tags.containerElement().children().remove();
3991 $("textarea", self.$el).css("padding-left", "3px");
3992 self.tags.addTags(_.map(data, function(el) {return {name: el[1], id:el[0]};}));
3994 self.$el.html(QWeb.render("FieldMany2ManyTag", {elements: data}));
3997 if (! values || values.length > 0) {
3998 this._display_orderer.add(dataset.name_get(values)).then(handle_names);
4003 add_id: function(id) {
4004 this.set({'value': _.uniq(this.get('value').concat([id]))});
4009 * TODO niv: clean those deferred stuff, it could be better
4011 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({
4012 multi_selection: false,
4013 disable_utility_classes: true,
4014 init: function(field_manager, node) {
4015 this._super(field_manager, node);
4016 this.set({"value": []});
4017 this.is_loaded = $.Deferred();
4018 this.initial_is_loaded = this.is_loaded;
4019 this.is_setted = $.Deferred();
4022 this._super.apply(this, arguments);
4023 this.$el.addClass('oe_form_field oe_form_field_many2many');
4027 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4028 this.dataset.m2m = this;
4029 this.dataset.on('unlink', self, function(ids) {
4030 self.dataset_changed();
4033 this.is_setted.then(function() {
4036 this.is_loaded.then(function() {
4037 self.on("change:effective_readonly", self, function() {
4038 self.is_loaded = self.is_loaded.pipe(function() {
4039 self.list_view.destroy();
4040 return $.when(self.load_view()).then(function() {
4041 self.reload_content();
4047 set_value: function(value_) {
4048 value_ = value_ || [];
4049 if (value_.length >= 1 && value_[0] instanceof Array) {
4050 value_ = value_[0][2];
4052 this._super(value_);
4053 this.dataset.set_ids(value_);
4055 self.reload_content();
4056 this.is_setted.resolve();
4058 get_value: function() {
4059 return [commands.replace_with(this.get('value'))];
4062 is_false: function () {
4063 return _(this.dataset.ids).isEmpty();
4065 load_view: function() {
4067 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4068 'addable': self.get("effective_readonly") ? null : _t("Add"),
4069 'deletable': self.get("effective_readonly") ? false : true,
4070 'selectable': self.multi_selection,
4072 'reorderable': false,
4073 'import_enabled': false,
4075 var embedded = (this.field.views || {}).tree;
4077 this.list_view.set_embedded_view(embedded);
4079 this.list_view.m2m_field = this;
4080 var loaded = $.Deferred();
4081 this.list_view.on_loaded.add_last(function() {
4082 self.initial_is_loaded.resolve();
4085 $.async_when().then(function () {
4086 self.list_view.appendTo(self.$el);
4090 reload_content: function() {
4092 this.is_loaded = this.is_loaded.pipe(function() {
4093 return self.list_view.reload_content();
4096 dataset_changed: function() {
4097 this.set({'value': this.dataset.ids});
4101 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4102 get_context: function() {
4103 this.context = this.m2m.build_context();
4104 return this.context;
4110 * @extends instance.web.ListView
4112 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4113 do_add_record: function () {
4114 var pop = new instance.web.form.SelectCreatePopup(this);
4118 title: _t("Add: ") + this.m2m_field.string
4120 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4121 this.m2m_field.build_context()
4124 pop.on_select_elements.add(function(element_ids) {
4125 _.each(element_ids, function(one_id) {
4126 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
4127 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
4128 self.m2m_field.dataset_changed();
4129 self.reload_content();
4134 do_activate_record: function(index, id) {
4136 var pop = new instance.web.form.FormOpenPopup(this);
4137 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4138 title: _t("Open: ") + this.m2m_field.string,
4139 readonly: this.getParent().get("effective_readonly")
4141 pop.on('write_completed', self, self.reload_content);
4145 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4146 disable_utility_classes: true,
4147 init: function(field_manager, node) {
4148 this._super(field_manager, node);
4149 instance.web.form.CompletionFieldMixin.init.call(this);
4150 m2m_kanban_lazy_init();
4151 this.is_loaded = $.Deferred();
4152 this.initial_is_loaded = this.is_loaded;
4153 this.is_setted = $.Deferred();
4156 this._super.apply(this, arguments);
4160 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4161 this.dataset.m2m = this;
4162 this.dataset.on('unlink', self, function(ids) {
4163 self.dataset_changed();
4166 this.is_setted.then(function() {
4169 this.is_loaded.then(function() {
4170 self.on("change:effective_readonly", self, function() {
4171 self.is_loaded = self.is_loaded.pipe(function() {
4172 self.kanban_view.destroy();
4173 return $.when(self.load_view()).then(function() {
4174 self.reload_content();
4180 set_value: function(value_) {
4181 value_ = value_ || [];
4182 if (value_.length >= 1 && value_[0] instanceof Array) {
4183 value_ = value_[0][2];
4185 this._super(value_);
4186 this.dataset.set_ids(value_);
4188 self.reload_content();
4189 this.is_setted.resolve();
4191 load_view: function() {
4193 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4194 'create_text': _t("Add"),
4195 'creatable': self.get("effective_readonly") ? false : true,
4196 'quick_creatable': self.get("effective_readonly") ? false : true,
4197 'read_only_mode': self.get("effective_readonly") ? true : false,
4198 'confirm_on_delete': false,
4200 var embedded = (this.field.views || {}).kanban;
4202 this.kanban_view.set_embedded_view(embedded);
4204 this.kanban_view.m2m = this;
4205 var loaded = $.Deferred();
4206 this.kanban_view.on_loaded.add_last(function() {
4207 self.initial_is_loaded.resolve();
4210 this.kanban_view.on('switch_mode', this, this.open_popup);
4211 $.async_when().then(function () {
4212 self.kanban_view.appendTo(self.$el);
4216 reload_content: function() {
4218 this.is_loaded = this.is_loaded.pipe(function() {
4219 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
4222 dataset_changed: function() {
4223 this.set({'value': [commands.replace_with(this.dataset.ids)]});
4225 open_popup: function(type, unused) {
4226 if (type !== "form")
4229 if (this.dataset.index === null) {
4230 var pop = new instance.web.form.SelectCreatePopup(this);
4232 this.field.relation,
4234 title: _t("Add: ") + this.string
4236 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
4237 this.build_context()
4239 pop.on_select_elements.add(function(element_ids) {
4240 _.each(element_ids, function(one_id) {
4241 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
4242 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
4243 self.dataset_changed();
4244 self.reload_content();
4249 var id = self.dataset.ids[self.dataset.index];
4250 var pop = new instance.web.form.FormOpenPopup(this);
4251 pop.show_element(self.field.relation, id, self.build_context(), {
4252 title: _t("Open: ") + self.string,
4253 write_function: function(id, data, options) {
4254 return self.dataset.write(id, data, {}).then(function() {
4255 self.reload_content();
4258 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
4259 parent_view: self.view,
4260 child_name: self.name,
4261 readonly: self.get("effective_readonly")
4265 add_id: function(id) {
4266 this.quick_create.add_id(id);
4270 function m2m_kanban_lazy_init() {
4271 if (instance.web.form.Many2ManyKanbanView)
4273 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4274 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
4275 _is_quick_create_enabled: function() {
4276 return this._super() && ! this.group_by;
4279 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
4280 template: 'Many2ManyKanban.quick_create',
4283 * close_btn: If true, the widget will display a "Close" button able to trigger
4286 init: function(parent, dataset, context, buttons) {
4287 this._super(parent);
4288 this.m2m = this.getParent().view.m2m;
4289 this.m2m.quick_create = this;
4290 this._dataset = dataset;
4291 this._buttons = buttons || false;
4292 this._context = context || {};
4294 start: function () {
4296 self.$text = this.$el.find('input').css("width", "200px");
4297 self.$text.textext({
4298 plugins : 'arrow autocomplete',
4300 render: function(suggestion) {
4301 return $('<span class="text-label"/>').
4302 data('index', suggestion['index']).html(suggestion['label']);
4307 selectFromDropdown: function() {
4308 $(this).trigger('hideDropdown');
4309 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4310 var data = self.search_result[index];
4312 self.add_id(data.id);
4319 itemToString: function(item) {
4324 }).bind('getSuggestions', function(e, data) {
4326 var str = !!data ? data.query || '' : '';
4327 self.m2m.get_search_result(str).then(function(result) {
4328 self.search_result = result;
4329 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4330 return _.extend(el, {index:i});
4334 self.$text.focusout(function() {
4341 add_id: function(id) {
4344 self.trigger('added', id);
4345 this.m2m.dataset_changed();
4351 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
4353 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
4354 template: "AbstractFormPopup.render",
4357 * -readonly: only applicable when not in creation mode, default to false
4358 * - alternative_form_view
4364 * - form_view_options
4366 init_popup: function(model, row_id, domain, context, options) {
4367 this.row_id = row_id;
4369 this.domain = domain || [];
4370 this.context = context || {};
4371 this.options = options;
4372 _.defaults(this.options, {
4375 init_dataset: function() {
4377 this.created_elements = [];
4378 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
4379 this.dataset.read_function = this.options.read_function;
4380 this.dataset.create_function = function(data, sup) {
4381 var fct = self.options.create_function || sup;
4382 return fct.call(this, data).then(function(r) {
4383 self.created_elements.push(r);
4386 this.dataset.write_function = function(id, data, options, sup) {
4387 var fct = self.options.write_function || sup;
4388 return fct.call(this, id, data, options).then(function() {
4389 self.trigger('write_completed');
4392 this.dataset.parent_view = this.options.parent_view;
4393 this.dataset.child_name = this.options.child_name;
4395 display_popup: function() {
4397 this.renderElement();
4398 var dialog = new instance.web.Dialog(this, {
4400 dialogClass: 'oe_act_window',
4402 self.check_exit(true);
4404 title: this.options.title || "",
4405 buttons: [{text:"tmp"}],
4406 }, this.$el).open();
4407 this.$buttonpane = dialog.$el.dialog("widget").find(".ui-dialog-buttonpane").html("");
4410 setup_form_view: function() {
4413 this.dataset.ids = [this.row_id];
4414 this.dataset.index = 0;
4416 this.dataset.index = null;
4418 var options = _.clone(self.options.form_view_options) || {};
4419 if (this.row_id !== null) {
4420 options.initial_mode = this.options.readonly ? "view" : "edit";
4423 $buttons: this.$buttonpane,
4425 this.view_form = new instance.web.FormView(this, this.dataset, false, options);
4426 if (this.options.alternative_form_view) {
4427 this.view_form.set_embedded_view(this.options.alternative_form_view);
4429 this.view_form.appendTo(this.$el.find(".oe_popup_form"));
4430 this.view_form.on_loaded.add_last(function() {
4431 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
4432 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
4433 multi_select: multi_select,
4434 readonly: self.row_id !== null && self.options.readonly,
4436 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
4437 $snbutton.click(function() {
4438 $.when(self.view_form.do_save()).then(function() {
4439 self.view_form.reload_mutex.exec(function() {
4440 self.view_form.on_button_new();
4444 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
4445 $sbutton.click(function() {
4446 $.when(self.view_form.do_save()).then(function() {
4447 self.view_form.reload_mutex.exec(function() {
4452 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
4453 $cbutton.click(function() {
4456 self.view_form.do_show();
4459 on_select_elements: function(element_ids) {
4461 check_exit: function(no_destroy) {
4462 if (this.created_elements.length > 0) {
4463 this.on_select_elements(this.created_elements);
4464 this.created_elements = [];
4468 destroy: function () {
4469 this.$el.dialog('close');
4475 * Class to display a popup containing a form view.
4477 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
4478 show_element: function(model, row_id, context, options) {
4479 this.init_popup(model, row_id, [], context, options);
4480 _.defaults(this.options, {
4482 this.display_popup();
4486 this.init_dataset();
4487 this.setup_form_view();
4492 * Class to display a popup to display a list to search a row. It also allows
4493 * to switch to a form view to create a new row.
4495 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
4499 * - initial_view: form or search (default search)
4500 * - disable_multiple_selection
4501 * - list_view_options
4503 select_element: function(model, options, domain, context) {
4504 this.init_popup(model, null, domain, context, options);
4506 _.defaults(this.options, {
4507 initial_view: "search",
4509 this.initial_ids = this.options.initial_ids;
4510 this.display_popup();
4514 this.init_dataset();
4515 if (this.options.initial_view == "search") {
4516 self.rpc('/web/session/eval_domain_and_context', {
4518 contexts: [this.context]
4519 }).then(function (results) {
4520 var search_defaults = {};
4521 _.each(results.context, function (value_, key) {
4522 var match = /^search_default_(.*)$/.exec(key);
4524 search_defaults[match[1]] = value_;
4527 self.setup_search_view(search_defaults);
4533 setup_search_view: function(search_defaults) {
4535 if (this.searchview) {
4536 this.searchview.destroy();
4538 this.searchview = new instance.web.SearchView(this,
4539 this.dataset, false, search_defaults);
4540 this.searchview.on_search.add(function(domains, contexts, groupbys) {
4541 if (self.initial_ids) {
4542 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
4543 contexts, groupbys);
4544 self.initial_ids = undefined;
4546 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
4549 this.searchview.on_loaded.add_last(function () {
4550 self.view_list = new instance.web.form.SelectCreateListView(self,
4551 self.dataset, false,
4552 _.extend({'deletable': false,
4553 'selectable': !self.options.disable_multiple_selection,
4554 'import_enabled': false,
4555 '$buttons': self.$buttonpane,
4556 }, self.options.list_view_options || {}));
4557 self.view_list.on('edit:before', self, function (e) {
4560 self.view_list.popup = self;
4561 self.view_list.appendTo($(".oe_popup_list", self.$el)).pipe(function() {
4562 self.view_list.do_show();
4563 }).pipe(function() {
4564 self.searchview.do_search();
4566 self.view_list.on_loaded.add_last(function() {
4567 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
4568 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
4569 $cbutton.click(function() {
4572 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
4573 $sbutton.click(function() {
4574 self.on_select_elements(self.selected_ids);
4579 this.searchview.appendTo($(".oe_popup_list", self.$el));
4581 do_search: function(domains, contexts, groupbys) {
4583 this.rpc('/web/session/eval_domain_and_context', {
4584 domains: domains || [],
4585 contexts: contexts || [],
4586 group_by_seq: groupbys || []
4587 }).then(function (results) {
4588 self.view_list.do_search(results.domain, results.context, results.group_by);
4591 on_click_element: function(ids) {
4593 this.selected_ids = ids || [];
4594 if(this.selected_ids.length > 0) {
4595 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
4597 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
4600 new_object: function() {
4601 if (this.searchview) {
4602 this.searchview.hide();
4604 if (this.view_list) {
4605 this.view_list.$el.hide();
4607 this.setup_form_view();
4611 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
4612 do_add_record: function () {
4613 this.popup.new_object();
4615 select_record: function(index) {
4616 this.popup.on_select_elements([this.dataset.ids[index]]);
4617 this.popup.destroy();
4619 do_select: function(ids, records) {
4620 this._super(ids, records);
4621 this.popup.on_click_element(ids);
4625 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4626 template: 'FieldReference',
4627 init: function(field_manager, node) {
4628 this._super(field_manager, node);
4629 this.reference_ready = true;
4631 on_nop: function() {
4633 on_selection_changed: function() {
4634 if (this.reference_ready) {
4635 var sel = this.selection.get_value();
4636 this.m2o.field.relation = sel;
4637 this.m2o.set_value(false);
4638 this.m2o.$el.toggle(sel !== false);
4641 destroy_content: function() {
4646 initialize_content: function() {
4648 var fm = new instance.web.form.DefaultFieldManager(this);
4650 fm.extend_field_desc({
4652 selection: this.field_manager.get_field_desc(this.name).selection,
4660 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
4662 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
4664 this.selection.on("change:value", this, this.on_selection_changed);
4665 this.selection.setElement(this.$(".oe_form_view_reference_selection"));
4666 this.selection.renderElement();
4667 this.selection.start();
4669 .on('focused', null, function () {self.trigger('focused')})
4670 .on('blurred', null, function () {self.trigger('blurred')});
4672 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
4674 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
4676 this.m2o.on("change:value", this, this.data_changed);
4677 this.m2o.setElement(this.$(".oe_form_view_reference_m2o"));
4678 this.m2o.renderElement();
4681 .on('focused', null, function () {self.trigger('focused')})
4682 .on('blurred', null, function () {self.trigger('blurred')});
4684 is_false: function() {
4685 return typeof(this.get_value()) !== 'string';
4687 set_value: function(value_) {
4688 this._super(value_);
4689 this.render_value();
4691 render_value: function() {
4692 this.reference_ready = false;
4693 var vals = [], sel_val, m2o_val;
4694 if (typeof(this.get('value')) === 'string') {
4695 vals = this.get('value').split(',');
4697 sel_val = vals[0] || false;
4698 m2o_val = vals[1] ? parseInt(vals[1], 10) : vals[1];
4699 if (!this.get("effective_readonly")) {
4700 this.selection.set_value(sel_val);
4702 this.m2o.field.relation = sel_val;
4703 this.m2o.set_value(m2o_val);
4704 this.reference_ready = true;
4706 data_changed: function() {
4707 var model = this.selection.get_value(),
4708 id = this.m2o.get_value();
4709 if (typeof(model) === 'string' && typeof(id) === 'number') {
4710 this.set({'value': model + ',' + id});
4712 this.set({'value': false});
4717 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4718 init: function(field_manager, node) {
4720 this._super(field_manager, node);
4721 this.binary_value = false;
4722 this.fileupload_id = _.uniqueId('oe_fileupload');
4723 $(window).on(this.fileupload_id, function() {
4724 var args = [].slice.call(arguments).slice(1);
4725 self.on_file_uploaded.apply(self, args);
4729 $(window).off(this.fileupload_id);
4730 this._super.apply(this, arguments);
4732 initialize_content: function() {
4733 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
4734 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
4735 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
4737 human_filesize : function(size) {
4738 var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
4740 while (size >= 1024) {
4744 return size.toFixed(2) + ' ' + units[i];
4746 on_file_change: function(e) {
4747 // TODO: on modern browsers, we could directly read the file locally on client ready to be used on image cropper
4748 // http://www.html5rocks.com/tutorials/file/dndfiles/
4749 // http://deepliquid.com/projects/Jcrop/demos.php?demo=handler
4751 if ($(e.target).val() !== '') {
4752 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
4753 this.$el.find('form.oe_form_binary_form').submit();
4754 this.$el.find('.oe_form_binary_progress').show();
4755 this.$el.find('.oe_form_binary').hide();
4758 on_file_uploaded: function(size, name, content_type, file_base64) {
4759 if (size === false) {
4760 this.do_warn("File Upload", "There was a problem while uploading your file");
4761 // TODO: use openerp web crashmanager
4762 console.warn("Error while uploading file : ", name);
4764 this.filename = name;
4765 this.on_file_uploaded_and_valid.apply(this, arguments);
4767 this.$el.find('.oe_form_binary_progress').hide();
4768 this.$el.find('.oe_form_binary').show();
4770 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
4772 on_save_as: function(ev) {
4773 var value = this.get('value');
4775 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
4776 ev.stopPropagation();
4777 } else if (this._dirty_flag) {
4778 var link = this.$('.oe_form_binary_file_save_data')[0];
4779 link.download = this.filename || "download.bin"; // Works on only on Google Chrome
4780 //link.target = '_blank';
4781 link.href = "data:application/octet-stream;base64," + value;
4783 instance.web.blockUI();
4784 this.session.get_file({
4785 url: '/web/binary/saveas_ajax',
4786 data: {data: JSON.stringify({
4787 model: this.view.dataset.model,
4788 id: (this.view.datarecord.id || ''),
4790 filename_field: (this.node.attrs.filename || ''),
4791 context: this.view.dataset.get_context()
4793 complete: instance.web.unblockUI,
4794 error: instance.webclient.crashmanager.on_rpc_error
4796 ev.stopPropagation();
4800 set_filename: function(value) {
4801 var filename = this.node.attrs.filename;
4804 tmp[filename] = value;
4805 this.field_manager.set_values(tmp);
4808 on_clear: function() {
4809 if (this.get('value') !== false) {
4810 this.binary_value = false;
4811 this.set({'value': false});
4817 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
4818 template: 'FieldBinaryFile',
4819 initialize_content: function() {
4821 if (this.get("effective_readonly")) {
4823 this.$el.find('a').click(function(ev) {
4824 if (self.get('value')) {
4825 self.on_save_as(ev);
4831 set_value: function(value_) {
4832 this._super.apply(this, arguments);
4833 this.render_value();
4835 render_value: function() {
4836 if (!this.get("effective_readonly")) {
4838 if (this.node.attrs.filename) {
4839 show_value = this.view.datarecord[this.node.attrs.filename] || '';
4841 show_value = (this.get('value') != null && this.get('value') !== false) ? this.get('value') : '';
4843 this.$el.find('input').eq(0).val(show_value);
4845 this.$el.find('a').show(!!this.get('value'));
4846 if (this.get('value')) {
4847 var show_value = _t("Download")
4849 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
4850 this.$el.find('a').text(show_value);
4854 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
4855 this.binary_value = true;
4856 this.set({'value': file_base64});
4857 var show_value = name + " (" + this.human_filesize(size) + ")";
4858 this.$el.find('input').eq(0).val(show_value);
4859 this.set_filename(name);
4861 on_clear: function() {
4862 this._super.apply(this, arguments);
4863 this.$el.find('input').eq(0).val('');
4864 this.set_filename('');
4868 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
4869 template: 'FieldBinaryImage',
4870 set_value: function(value_) {
4871 this._super.apply(this, arguments);
4872 this.render_value();
4874 render_value: function() {
4877 if (this.get('value') && ! /^\d+(\.\d*)? \w+$/.test(this.get('value'))) {
4878 url = 'data:image/png;base64,' + this.get('value');
4879 } else if (this.get('value')) {
4880 var id = escape(JSON.stringify(this.view.datarecord.id || null));
4881 var field = this.name;
4882 if (this.options.preview_image)
4883 field = this.options.preview_image;
4884 url = '/web/binary/image?session_id=' + this.session.session_id + '&model=' +
4885 this.view.dataset.model +'&id=' + id + '&field=' + field + '&t=' + (new Date().getTime());
4887 url = "/web/static/src/img/placeholder.png";
4889 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
4890 this.$el.find('> img').remove();
4891 this.$el.prepend($img);
4892 $img.load(function() {
4893 if (! self.options.size)
4895 $img.css("max-width", "" + self.options.size[0] + "px");
4896 $img.css("max-height", "" + self.options.size[1] + "px");
4897 $img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px");
4898 $img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px");
4901 on_file_change: function() {
4902 this.render_value();
4903 this._super.apply(this, arguments);
4905 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
4906 this.set({'value': file_base64});
4907 this.binary_value = true;
4908 this.render_value();
4909 this.set_filename(name);
4911 on_clear: function() {
4912 this._super.apply(this, arguments);
4913 this.render_value();
4914 this.set_filename('');
4918 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
4919 template: "FieldStatus",
4920 init: function(field_manager, node) {
4921 this._super(field_manager, node);
4922 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
4923 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
4924 this.selected_value = null;
4928 // backward compatibility
4929 this.loaded = new $.Deferred();
4930 if (this.options.clickable) {
4931 this.$el.on('click','li',this.on_click_stage);
4933 // TODO move the following into css :after
4934 if (this.$el.parent().is('header')) {
4935 this.$el.after('<div class="oe_clear"/>');
4938 set_value: function(value_) {
4940 this._super(value_);
4941 // find selected value:
4942 // - many2one: [2, "New"] -> 2
4943 // - selection: new -> new
4944 if (this.field.type == "many2one") {
4945 this.selected_value = value_[0];
4947 this.selected_value = value_;
4949 // trick to be sure all values are loaded in the form, therefore
4950 // enabling the evaluation of dynamic domains
4951 self.selection = [];
4952 $.async_when().then(function() {
4953 self.get_selection();
4957 /** Get the selection and render it
4958 * selection: [[identifier, value_to_display], ...]
4959 * For selection fields: this is directly given by this.field.selection
4960 * For many2one fields: perform a search on the relation of the many2one field
4962 get_selection: function() {
4964 if (this.field.type == "many2one") {
4966 if(!_.isEmpty(this.field.domain) || !_.isEmpty(this.node.attrs.domain)) {
4967 domain = new instance.web.CompoundDomain(['|'], self.build_domain(), [['id', '=', self.selected_value]]);
4969 var ds = new instance.web.DataSetSearch(this, this.field.relation, self.build_context(), domain);
4970 ds.read_slice(['name'], {}).done( function (records) {
4971 for(var i = 0; i < records.length; i++) {
4972 self.selection.push([records[i].id, records[i].name]);
4974 self.render_elements();
4975 self.loaded.resolve();
4978 this.loaded.resolve();
4979 // For field type selection filter values according to
4980 // statusbar_visible attribute of the field. For example:
4981 // statusbar_visible="draft,open".
4982 var selection = this.field.selection;
4983 for(var i=0; i< selection.length; i++) {
4984 var key = selection[i][0];
4985 if(key == this.selected_value || !this.options.visible || this.options.visible.indexOf(key) != -1) {
4986 this.selection.push(selection[i]);
4989 this.render_elements();
4992 /** Renders the widget. This function also checks for statusbar_colors='{"pending": "blue"}'
4993 * attribute in the widget. This allows to set a given color to a given
4994 * state (given by the key of (key, label)).
4996 render_elements: function () {
4998 var content = instance.web.qweb.render("FieldStatus.content", {widget: this});
4999 this.$el.html(content);
5000 var colors = JSON.parse((this.node.attrs || {}).statusbar_colors || "{}");
5001 var color = colors[this.selected_value];
5003 this.$("oe_active").css("color", color);
5006 on_click_stage: function (ev) {
5008 var $li = $(ev.currentTarget);
5009 var val = parseInt($li.data("id"));
5010 if (val != self.selected_value) {
5011 this.view.recursive_save().then(function() {
5013 change[self.name] = val;
5014 self.view.dataset.write(self.view.datarecord.id, change).then(function() {
5022 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
5023 template: "FieldMonetary",
5025 this._super.apply(this, arguments);
5026 this.set({"currency": false});
5027 if (this.options.currency_field) {
5028 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
5029 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
5032 this.on("change:currency", this, this.get_currency_info);
5033 this.get_currency_info();
5034 this.ci_dm = new instance.web.DropMisordered();
5037 var tmp = this._super();
5038 this.on("change:currency_info", this, this.reinitialize);
5041 get_currency_info: function() {
5043 if (this.get("currency") === false) {
5044 this.set({"currency_info": null});
5047 return this.ci_dm.add(new instance.web.Model("res.currency").query(["symbol", "position"])
5048 .filter([["id", "=", self.get("currency")]]).first()).pipe(function(res) {
5049 self.set({"currency_info": res});
5052 parse_value: function(val, def) {
5053 return instance.web.parse_value(val, {type: "float"}, def);
5055 format_value: function(val, def) {
5056 return instance.web.format_value(val, {type: "float"}, def);
5061 * Registry of form fields, called by :js:`instance.web.FormView`.
5063 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
5064 * will substitute to the <field> tags as defined in OpenERP's views.
5066 instance.web.form.widgets = new instance.web.Registry({
5067 'char' : 'instance.web.form.FieldChar',
5068 'id' : 'instance.web.form.FieldID',
5069 'email' : 'instance.web.form.FieldEmail',
5070 'url' : 'instance.web.form.FieldUrl',
5071 'text' : 'instance.web.form.FieldText',
5072 'html' : 'instance.web.form.FieldTextHtml',
5073 'date' : 'instance.web.form.FieldDate',
5074 'datetime' : 'instance.web.form.FieldDatetime',
5075 'selection' : 'instance.web.form.FieldSelection',
5076 'many2one' : 'instance.web.form.FieldMany2One',
5077 'many2many' : 'instance.web.form.FieldMany2Many',
5078 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
5079 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
5080 'one2many' : 'instance.web.form.FieldOne2Many',
5081 'one2many_list' : 'instance.web.form.FieldOne2Many',
5082 'reference' : 'instance.web.form.FieldReference',
5083 'boolean' : 'instance.web.form.FieldBoolean',
5084 'float' : 'instance.web.form.FieldFloat',
5085 'integer': 'instance.web.form.FieldFloat',
5086 'float_time': 'instance.web.form.FieldFloat',
5087 'progressbar': 'instance.web.form.FieldProgressBar',
5088 'image': 'instance.web.form.FieldBinaryImage',
5089 'binary': 'instance.web.form.FieldBinaryFile',
5090 'statusbar': 'instance.web.form.FieldStatus',
5091 'monetary': 'instance.web.form.FieldMonetary',
5095 * Registry of widgets usable in the form view that can substitute to any possible
5096 * tags defined in OpenERP's form views.
5098 * Every referenced class should extend FormWidget.
5100 instance.web.form.tags = new instance.web.Registry({
5101 'button' : 'instance.web.form.WidgetButton',
5104 instance.web.form.custom_widgets = new instance.web.Registry({
5109 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: