1 openerp.web.form = function (openerp) {
3 var _t = openerp.web._t;
4 var QWeb = openerp.web.qweb;
6 openerp.web.views.add('form', 'openerp.web.FormView');
7 openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# */{
9 * Indicates that this view is not searchable, and thus that no search
10 * view should be displayed (if there is one active).
14 form_template: "FormView",
15 identifier_prefix: 'formview-',
17 * @constructs openerp.web.FormView
18 * @extends openerp.web.View
20 * @param {openerp.web.Session} session the current openerp session
21 * @param {openerp.web.DataSet} dataset the dataset this view will work with
22 * @param {String} view_id the identifier of the OpenERP view object
24 * @property {openerp.web.Registry} registry=openerp.web.form.widgets widgets registry for this form view instance
26 init: function(parent, dataset, view_id, options) {
28 this.set_default_options(options);
29 this.dataset = dataset;
30 this.model = dataset.model;
31 this.view_id = view_id || false;
32 this.fields_view = {};
34 this.widgets_counter = 0;
37 this.show_invalid = true;
38 this.default_focus_field = null;
39 this.default_focus_button = null;
40 this.registry = this.readonly ? openerp.web.form.readonly : openerp.web.form.widgets;
41 this.has_been_loaded = $.Deferred();
42 this.$form_header = null;
43 this.translatable_fields = [];
44 _.defaults(this.options, {"always_show_new_button": true,
45 "not_interactible_on_create": false});
46 this.mutating_lock = $.Deferred();
47 this.initial_mutating_lock = this.mutating_lock;
48 this.on_change_lock = $.Deferred().resolve();
49 this.reload_lock = $.Deferred().resolve();
53 return this.init_view();
55 init_view: function() {
56 if (this.embedded_view) {
57 var def = $.Deferred().then(this.on_loaded);
59 setTimeout(function() {def.resolve(self.embedded_view);}, 0);
62 var context = new openerp.web.CompoundContext(this.dataset.get_context());
63 return this.rpc("/web/view/load", {
65 "view_id": this.view_id,
67 toolbar: this.options.sidebar,
74 this.sidebar.attachments.stop();
77 _.each(this.widgets, function(w) {
82 reposition: function ($e) {
86 on_loaded: function(data) {
89 this.fields_view = data;
90 var frame = new (this.registry.get_object('frame'))(this, this.fields_view.arch);
92 this.rendered = QWeb.render(this.form_template, { 'frame': frame, 'widget': this });
94 this.$element.html(this.rendered);
95 _.each(this.widgets, function(w) {
98 this.$form_header = this.$element.find('.oe_form_header:first');
99 this.$form_header.find('div.oe_form_pager button[data-pager-action]').click(function() {
100 var action = $(this).data('pager-action');
101 self.on_pager_action(action);
104 this.$form_header.find('button.oe_form_button_save').click(this.on_button_save);
105 this.$form_header.find('button.oe_form_button_new').click(this.on_button_new);
106 this.$form_header.find('button.oe_form_button_duplicate').click(this.on_button_duplicate);
107 this.$form_header.find('button.oe_form_button_delete').click(this.on_button_delete);
108 this.$form_header.find('button.oe_form_button_toggle').click(this.on_toggle_readonly);
110 if (!this.sidebar && this.options.sidebar && this.options.sidebar_id) {
111 this.sidebar = new openerp.web.Sidebar(this, this.options.sidebar_id);
112 this.sidebar.start();
113 this.sidebar.do_unfold();
114 this.sidebar.attachments = new openerp.web.form.SidebarAttachments(this.sidebar, this);
115 this.sidebar.add_toolbar(this.fields_view.toolbar);
116 this.set_common_sidebar_sections(this.sidebar);
118 this.has_been_loaded.resolve();
120 on_toggle_readonly: function() {
122 self.translatable_fields = [];
125 self.$form_header.find('button').unbind('click');
126 self.readonly = !self.readonly;
127 self.registry = self.readonly ? openerp.web.form.readonly : openerp.web.form.widgets;
128 self.on_loaded(self.fields_view);
129 return self.reload();
131 do_set_readonly: function() {
132 return this.readonly ? $.Deferred().resolve() : this.on_toggle_readonly();
134 do_set_editable: function() {
135 return !this.readonly ? $.Deferred().resolve() : this.on_toggle_readonly();
137 do_show: function () {
139 if (this.dataset.index === null) {
140 // null index means we should start a new record
141 promise = this.on_button_new();
143 promise = this.dataset.read_index(_.keys(this.fields_view.fields)).pipe(this.on_record_loaded);
145 this.$element.show();
147 this.sidebar.$element.show();
151 do_hide: function () {
152 this.$element.hide();
154 this.sidebar.$element.hide();
157 on_record_loaded: function(record) {
159 deferred_stack = $.Deferred.queue();
161 throw("Form: No record received");
164 this.$form_header.find('.oe_form_on_create').show();
165 this.$form_header.find('.oe_form_on_update').hide();
166 if (!this.options["always_show_new_button"]) {
167 this.$form_header.find('button.oe_form_button_new').hide();
170 this.$form_header.find('.oe_form_on_create').hide();
171 this.$form_header.find('.oe_form_on_update').show();
172 this.$form_header.find('button.oe_form_button_new').show();
174 this.$form_header.find('.oe_form_on_readonly').toggle(this.readonly);
175 this.$form_header.find('.oe_form_on_editable').toggle(!this.readonly);
176 this.datarecord = record;
178 _(this.fields).each(function (field, f) {
180 var result = field.set_value(self.datarecord[f] || false);
181 if (result && _.isFunction(result.promise)) {
182 deferred_stack.push(result);
184 $.when(result).then(function() {
188 deferred_stack.push('force resolution if no fields');
189 return deferred_stack.then(function() {
191 // New record: Second pass in order to trigger the onchanges
192 self.show_invalid = false;
193 for (var f in record) {
194 var field = self.fields[f];
197 self.do_onchange(field);
201 self.on_form_changed();
202 self.initial_mutating_lock.resolve();
203 self.show_invalid = true;
204 self.do_update_pager(record.id == null);
206 self.sidebar.attachments.do_update();
208 if (self.default_focus_field && !self.embedded_view) {
209 self.default_focus_field.focus();
213 on_form_changed: function() {
214 for (var w in this.widgets) {
216 w.process_modifiers();
220 on_pager_action: function(action) {
221 if (this.can_be_discarded()) {
224 this.dataset.index = 0;
227 this.dataset.previous();
233 this.dataset.index = this.dataset.ids.length - 1;
239 do_update_pager: function(hide_index) {
240 var $pager = this.$form_header.find('div.oe_form_pager');
241 var index = hide_index ? '-' : this.dataset.index + 1;
242 $pager.find('span.oe_pager_index').html(index);
243 $pager.find('span.oe_pager_count').html(this.dataset.ids.length);
245 parse_on_change: function (on_change, widget) {
247 var onchange = _.str.trim(on_change);
248 var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
253 var method = call[1];
254 if (!_.str.trim(call[2])) {
255 return {method: method, args: [], context_index: null}
258 var argument_replacement = {
259 'False': function () {return false;},
260 'True': function () {return true;},
261 'None': function () {return null;},
262 'context': function (i) {
264 var ctx = widget.build_context ? widget.build_context() : {};
268 var parent_fields = null, context_index = null;
269 var args = _.map(call[2].split(','), function (a, i) {
270 var field = _.str.trim(a);
272 // literal constant or context
273 if (field in argument_replacement) {
274 return argument_replacement[field](i);
277 if (/^-?\d+(\.\d+)?$/.test(field)) {
278 return Number(field);
281 if (self.fields[field]) {
282 var value = self.fields[field].get_on_change_value();
283 return value == null ? false : value;
286 var splitted = field.split('.');
287 if (splitted.length > 1 && _.str.trim(splitted[0]) === "parent" && self.dataset.parent_view) {
288 if (parent_fields === null) {
289 parent_fields = self.dataset.parent_view.get_fields_values();
291 var p_val = parent_fields[_.str.trim(splitted[1])];
292 if (p_val !== undefined) {
293 return p_val == null ? false : p_val;
297 var first_char = field[0], last_char = field[field.length-1];
298 if ((first_char === '"' && last_char === '"')
299 || (first_char === "'" && last_char === "'")) {
300 return field.slice(1, -1);
303 throw new Error("Could not get field with name '" + field +
304 "' for onchange '" + onchange + "'");
310 context_index: context_index
313 do_onchange: function(widget, processed) {
315 var act = function() {
317 processed = processed || [];
318 var on_change = widget.node.attrs.on_change;
320 var change_spec = self.parse_on_change(on_change, widget);
323 url: '/web/dataset/call',
326 return self.rpc(ajax, {
327 model: self.dataset.model,
328 method: change_spec.method,
329 args: [(self.datarecord.id == null ? [] : [self.datarecord.id])].concat(change_spec.args),
330 context_id: change_spec.context_index == undefined ? null : change_spec.context_index + 1
331 }).pipe(function(response) {
332 return self.on_processed_onchange(response, processed);
335 console.warn("Wrong on_change format", on_change);
340 return $.Deferred().reject();
343 this.on_change_lock = this.on_change_lock.pipe(act, act);
344 return this.on_change_lock;
346 on_processed_onchange: function(response, processed) {
348 var result = response;
350 for (var f in result.value) {
351 var field = this.fields[f];
352 // If field is not defined in the view, just ignore it
354 var value = result.value[f];
355 processed.push(field.name);
356 if (field.get_value() != value) {
357 field.set_value(value);
359 if (_.indexOf(processed, field.name) < 0) {
360 this.do_onchange(field, processed);
365 this.on_form_changed();
367 if (!_.isEmpty(result.warning)) {
368 $(QWeb.render("DialogWarning", result.warning)).dialog({
372 $(this).dialog("close");
380 return $.Deferred().resolve();
383 return $.Deferred().reject();
386 on_button_save: function() {
387 return this.do_save().then(this.do_set_readonly);
389 on_button_new: function() {
391 var def = $.Deferred();
392 $.when(this.has_been_loaded).then(function() {
393 if (self.can_be_discarded()) {
394 var keys = _.keys(self.fields_view.fields);
395 $.when(self.do_set_editable()).then(function() {
397 self.dataset.default_get(keys).pipe(self.on_record_loaded).then(function() {
401 self.on_record_loaded({}).then(function() {
408 return def.promise();
410 on_button_duplicate: function() {
412 var def = $.Deferred();
413 $.when(this.has_been_loaded).then(function() {
414 if (self.can_be_discarded()) {
415 self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
416 return self.on_created({ result : new_id });
417 }).then(self.do_set_editable).then(function() {
422 return def.promise();
424 on_button_delete: function() {
426 var def = $.Deferred();
427 $.when(this.has_been_loaded).then(function() {
428 if (self.can_be_discarded() && self.datarecord.id) {
429 if (confirm(_t("Do you really want to delete this record?"))) {
430 self.dataset.unlink([self.datarecord.id]).then(function() {
431 self.on_pager_action('next');
435 setTimeout(function () {
441 return def.promise();
443 can_be_discarded: function() {
444 return !this.is_dirty() || confirm(_t("Warning, the record has been modified, your changes will be discarded."));
447 * Triggers saving the form's record. Chooses between creating a new
448 * record or saving an existing one depending on whether the record
449 * already has an id property.
451 * @param {Function} success callback on save success
452 * @param {Boolean} [prepend_on_create=false] if ``do_save`` creates a new record, should that record be inserted at the start of the dataset (by default, records are added at the end)
454 do_save: function(success, prepend_on_create) {
456 var action = function() {
458 if (!self.initial_mutating_lock.isResolved() && !self.initial_mutating_lock.isRejected())
460 var form_invalid = false,
462 first_invalid_field = null;
463 for (var f in self.fields) {
468 if (!first_invalid_field) {
469 first_invalid_field = f;
471 } else if (f.name !== 'id' && !f.readonly && (!self.datarecord.id || f.is_dirty())) {
472 // Special case 'id' field, do not save this field
473 // on 'create' : save all non readonly fields
474 // on 'edit' : save non readonly modified fields
475 values[f.name] = f.get_value();
479 first_invalid_field.focus();
481 return $.Deferred().reject();
483 if (!self.datarecord.id) {
484 openerp.log("FormView(", self, ") : About to create", values);
485 return self.dataset.create(values).pipe(function(r) {
486 return self.on_created(r, undefined, prepend_on_create);
488 } else if (_.isEmpty(values)) {
489 openerp.log("FormView(", self, ") : Nothing to save");
494 openerp.log("FormView(", self, ") : About to save", values);
495 return self.dataset.write(self.datarecord.id, values, {}).pipe(function(r) {
496 return self.on_saved(r);
502 return $.Deferred().reject();
505 this.mutating_lock = this.mutating_lock.pipe(action, action);
506 return this.mutating_lock;
508 on_invalid: function() {
510 _.each(this.fields, function(f) {
512 msg += "<li>" + f.string + "</li>";
516 this.do_warn("The following fields are invalid :", msg);
518 on_saved: function(r, success) {
520 // should not happen in the server, but may happen for internal purpose
521 return $.Deferred().reject();
523 return $.when(this.reload()).pipe(function () {
524 return $.when(r).then(success); }, null);
528 * Updates the form' dataset to contain the new record:
530 * * Adds the newly created record to the current dataset (at the end by
532 * * Selects that record (sets the dataset's index to point to the new
534 * * Updates the pager and sidebar displays
537 * @param {Function} success callback to execute after having updated the dataset
538 * @param {Boolean} [prepend_on_create=false] adds the newly created record at the beginning of the dataset instead of the end
540 on_created: function(r, success, prepend_on_create) {
542 // should not happen in the server, but may happen for internal purpose
543 return $.Deferred().reject();
545 this.datarecord.id = r.result;
546 if (!prepend_on_create) {
547 this.dataset.ids.push(this.datarecord.id);
548 this.dataset.index = this.dataset.ids.length - 1;
550 this.dataset.ids.unshift(this.datarecord.id);
551 this.dataset.index = 0;
553 this.do_update_pager();
555 this.sidebar.attachments.do_update();
557 openerp.log("The record has been created with id #" + this.datarecord.id);
559 return $.when(_.extend(r, {created: true})).then(success);
562 on_action: function (action) {
563 console.debug('Executing action', action);
567 var act = function() {
568 if (self.dataset.index == null || self.dataset.index < 0) {
569 return $.when(self.on_button_new());
571 return self.dataset.read_index(_.keys(self.fields_view.fields)).pipe(self.on_record_loaded);
574 this.reload_lock = this.reload_lock.pipe(act, act);
575 return this.reload_lock;
577 get_fields_values: function() {
579 _.each(this.fields, function(value, key) {
580 var val = value.get_value();
585 get_selected_ids: function() {
586 var id = this.dataset.ids[this.dataset.index];
587 return id ? [id] : [];
589 recursive_save: function() {
591 return $.when(this.do_save()).pipe(function(res) {
592 if (self.dataset.parent_view)
593 return self.dataset.parent_view.recursive_save();
596 is_dirty: function() {
597 return _.any(this.fields, function (value) {
598 return value.is_dirty();
601 is_interactible_record: function() {
602 var id = this.datarecord.id;
604 if (this.options.not_interactible_on_create)
606 } else if (typeof(id) === "string") {
607 if(openerp.web.BufferedDataSet.virtual_id_regex.test(id))
612 sidebar_context: function () {
613 return this.do_save().pipe($.proxy(this, 'get_fields_values'));
616 openerp.web.FormDialog = openerp.web.Dialog.extend({
617 init: function(parent, options, view_id, dataset) {
618 this._super(parent, options);
619 this.dataset = dataset;
620 this.view_id = view_id;
625 this.form = new openerp.web.FormView(this, this.dataset, this.view_id, {
629 this.form.appendTo(this.$element);
630 this.form.on_created.add_last(this.on_form_dialog_saved);
631 this.form.on_saved.add_last(this.on_form_dialog_saved);
634 select_id: function(id) {
635 if (this.form.dataset.select_id(id)) {
636 return this.form.do_show();
638 this.do_warn("Could not find id in dataset");
639 return $.Deferred().reject();
642 on_form_dialog_saved: function(r) {
648 openerp.web.form = {};
650 openerp.web.form.SidebarAttachments = openerp.web.Widget.extend({
651 init: function(parent, form_view) {
652 var $section = parent.add_section(_t('Attachments'), 'attachments');
653 this.$div = $('<div class="oe-sidebar-attachments"></div>');
654 $section.append(this.$div);
656 this._super(parent, $section.attr('id'));
657 this.view = form_view;
659 do_update: function() {
660 if (!this.view.datarecord.id) {
661 this.on_attachments_loaded([]);
663 (new openerp.web.DataSetSearch(
664 this, 'ir.attachment', this.view.dataset.get_context(),
666 ['res_model', '=', this.view.dataset.model],
667 ['res_id', '=', this.view.datarecord.id],
668 ['type', 'in', ['binary', 'url']]
669 ])).read_slice(['name', 'url', 'type'], {}, this.on_attachments_loaded);
672 on_attachments_loaded: function(attachments) {
673 this.attachments = attachments;
674 this.$div.html(QWeb.render('FormView.sidebar.attachments', this));
675 this.$element.find('.oe-binary-file').change(this.on_attachment_changed);
676 this.$element.find('.oe-sidebar-attachment-delete').click(this.on_attachment_delete);
678 on_attachment_changed: function(e) {
679 window[this.element_id + '_iframe'] = this.do_update;
680 var $e = $(e.target);
681 if ($e.val() != '') {
682 this.$element.find('form.oe-binary-form').submit();
683 $e.parent().find('input[type=file]').attr('disabled', 'true');
684 $e.parent().find('button').attr('disabled', 'true').find('img, span').toggle();
687 on_attachment_delete: function(e) {
688 var self = this, $e = $(e.currentTarget);
689 var name = _.str.trim($e.parent().find('a.oe-sidebar-attachments-link').text());
690 if (confirm("Do you really want to delete the attachment " + name + " ?")) {
691 this.rpc('/web/dataset/unlink', {
692 model: 'ir.attachment',
693 ids: [parseInt($e.attr('data-id'))]
695 $e.parent().remove();
696 self.do_notify("Delete an attachment", "The attachment '" + name + "' has been deleted");
702 openerp.web.form.compute_domain = function(expr, fields) {
704 for (var i = expr.length - 1; i >= 0; i--) {
706 if (ex.length == 1) {
707 var top = stack.pop();
710 stack.push(stack.pop() || top);
713 stack.push(stack.pop() && top);
719 throw new Error('Unknown domain operator ' + ex);
723 var field = fields[ex[0]];
725 throw new Error("Domain references unknown field : " + ex[0]);
727 var field_value = field.get_value ? fields[ex[0]].get_value() : fields[ex[0]].value;
731 switch (op.toLowerCase()) {
734 stack.push(field_value == val);
738 stack.push(field_value != val);
741 stack.push(field_value < val);
744 stack.push(field_value > val);
747 stack.push(field_value <= val);
750 stack.push(field_value >= val);
753 stack.push(_(val).contains(field_value));
756 stack.push(!_(val).contains(field_value));
759 console.warn("Unsupported operator in modifiers :", op);
762 return _.all(stack, _.identity);
765 openerp.web.form.Widget = openerp.web.Widget.extend(/** @lends openerp.web.form.Widget# */{
767 identifier_prefix: 'formview-widget-',
769 * @constructs openerp.web.form.Widget
770 * @extends openerp.web.Widget
775 init: function(view, node) {
778 this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
779 this.always_invisible = (this.modifiers.invisible && this.modifiers.invisible === true);
780 this.type = this.type || node.tag;
781 this.element_name = this.element_name || this.type;
782 this.element_class = [
783 'formview', this.view.view_id, this.element_name,
784 this.view.widgets_counter++].join("_");
788 this.view.widgets[this.element_class] = this;
789 this.children = node.children;
790 this.colspan = parseInt(node.attrs.colspan || 1, 10);
791 this.decrease_max_width = 0;
793 this.string = this.string || node.attrs.string;
794 this.help = this.help || node.attrs.help;
795 this.invisible = this.modifiers['invisible'] === true;
796 this.classname = 'oe_form_' + this.type;
798 this.align = parseFloat(this.node.attrs.align);
799 if (isNaN(this.align) || this.align === 1) {
800 this.align = 'right';
801 } else if (this.align === 0) {
804 this.align = 'center';
808 this.width = this.node.attrs.width;
811 this.$element = this.view.$element.find(
812 '.' + this.element_class.replace(/[^\r\n\f0-9A-Za-z_-]/g, "\\$&"));
814 process_modifiers: function() {
815 var compute_domain = openerp.web.form.compute_domain;
816 for (var a in this.modifiers) {
817 this[a] = compute_domain(this.modifiers[a], this.view.fields);
820 update_dom: function() {
821 this.$element.toggle(!this.invisible);
824 var template = this.template;
825 return QWeb.render(template, { "widget": this });
827 do_attach_tooltip: function(widget, trigger, options) {
828 widget = widget || this;
829 trigger = trigger || this.$element;
832 maxWidth: openerp.connection.debug ? '300px' : '200px',
833 content: function() {
834 var template = widget.template + '.tooltip';
835 if (!QWeb.has_template(template)) {
836 template = 'WidgetLabel.tooltip';
838 return QWeb.render(template, {
839 debug: openerp.connection.debug,
844 trigger.tipTip(options);
846 _build_view_fields_values: function() {
847 var a_dataset = this.view.dataset;
848 var fields_values = this.view.get_fields_values();
849 var active_id = a_dataset.ids[a_dataset.index];
850 _.extend(fields_values, {
851 active_id: active_id || false,
852 active_ids: active_id ? [active_id] : [],
853 active_model: a_dataset.model,
854 parent: a_dataset.parent_view ? a_dataset.parent_view.get_fields_values() : {}
856 return fields_values;
858 _build_eval_context: function() {
859 var a_dataset = this.view.dataset;
860 return new openerp.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
863 * Builds a new context usable for operations related to fields by merging
864 * the fields'context with the action's context.
866 build_context: function() {
867 var f_context = (this.field || {}).context || {};
868 if (!!f_context.__ref) {
869 var fields_values = this._build_eval_context();
870 f_context = new openerp.web.CompoundDomain(f_context).set_eval_context(fields_values);
872 // maybe the default_get should only be used when we do a default_get?
873 var v_contexts = _.compact([this.node.attrs.default_get || null,
874 this.node.attrs.context || null]);
875 var v_context = new openerp.web.CompoundContext();
876 _.each(v_contexts, function(x) {v_context.add(x);});
877 if (_.detect(v_contexts, function(x) {return !!x.__ref;})) {
878 var fields_values = this._build_eval_context();
879 v_context.set_eval_context(fields_values);
881 // if there is a context on the node, overrides the model's context
882 var ctx = v_contexts.length > 0 ? v_context : f_context;
885 build_domain: function() {
886 var f_domain = this.field.domain || [];
887 var n_domain = this.node.attrs.domain || null;
888 // if there is a domain on the node, overrides the model's domain
889 var final_domain = n_domain !== null ? n_domain : f_domain;
890 if (!(final_domain instanceof Array)) {
891 var fields_values = this._build_eval_context();
892 final_domain = new openerp.web.CompoundDomain(final_domain).set_eval_context(fields_values);
898 openerp.web.form.WidgetFrame = openerp.web.form.Widget.extend({
899 template: 'WidgetFrame',
900 init: function(view, node) {
901 this._super(view, node);
902 this.columns = parseInt(node.attrs.col || 4, 10);
907 for (var i = 0; i < node.children.length; i++) {
908 var n = node.children[i];
909 if (n.tag == "newline") {
915 this.set_row_cells_with(this.table[this.table.length - 1]);
918 if (this.table.length) {
919 this.set_row_cells_with(this.table[this.table.length - 1]);
922 this.table.push(row);
927 set_row_cells_with: function(row) {
930 row_length = row.length;
931 for (var i = 0; i < row.length; i++) {
932 if (row[i].always_invisible) {
935 bypass += row[i].width === undefined ? 0 : 1;
936 max_width -= row[i].decrease_max_width;
939 var size_unit = Math.round(max_width / (this.columns - bypass)),
941 for (var i = 0; i < row.length; i++) {
943 if (w.always_invisible) {
946 colspan_sum += w.colspan;
947 if (w.width === undefined) {
948 var width = (i === row_length - 1 && colspan_sum === this.columns) ? max_width : Math.round(size_unit * w.colspan);
950 w.width = width + '%';
954 handle_node: function(node) {
956 if (node.tag == 'field') {
957 type = this.view.fields_view.fields[node.attrs.name] || {};
958 if (node.attrs.widget == 'statusbar' && node.attrs.nolabel !== '1') {
959 // This way we can retain backward compatibility between addons and old clients
960 node.attrs.colspan = (parseInt(node.attrs.colspan, 10) || 1) + 1;
961 node.attrs.nolabel = '1';
964 var widget = new (this.view.registry.get_any(
965 [node.attrs.widget, type.type, node.tag])) (this.view, node);
966 if (node.tag == 'field') {
967 if (!this.view.default_focus_field || node.attrs.default_focus == '1') {
968 this.view.default_focus_field = widget;
970 if (node.attrs.nolabel != '1') {
971 var label = new (this.view.registry.get_object('label')) (this.view, node);
972 label["for"] = widget;
973 this.add_widget(label, widget.colspan + 1);
976 this.add_widget(widget);
978 add_widget: function(widget, colspan) {
979 var current_row = this.table[this.table.length - 1];
980 if (!widget.always_invisible) {
981 colspan = colspan || widget.colspan;
982 if (current_row.length && (this.x + colspan) > this.columns) {
983 current_row = this.add_row();
985 this.x += widget.colspan;
987 current_row.push(widget);
992 openerp.web.form.WidgetGroup = openerp.web.form.WidgetFrame.extend({
993 template: 'WidgetGroup'
996 openerp.web.form.WidgetNotebook = openerp.web.form.Widget.extend({
997 template: 'WidgetNotebook',
998 init: function(view, node) {
999 this._super(view, node);
1001 for (var i = 0; i < node.children.length; i++) {
1002 var n = node.children[i];
1003 if (n.tag == "page") {
1004 var page = new (this.view.registry.get_object('notebookpage'))(
1005 this.view, n, this, this.pages.length);
1006 this.pages.push(page);
1012 this._super.apply(this, arguments);
1013 this.$element.find('> ul > li').each(function (index, tab_li) {
1014 var page = self.pages[index],
1015 id = _.uniqueId(self.element_name + '-');
1016 page.element_id = id;
1017 $(tab_li).find('a').attr('href', '#' + id);
1019 this.$element.find('> div').each(function (index, page) {
1020 page.id = self.pages[index].element_id;
1022 this.$element.tabs();
1023 this.view.on_button_new.add_first(this.do_select_first_visible_tab);
1024 if (openerp.connection.debug) {
1025 this.do_attach_tooltip(this, this.$element.find('ul:first'), {
1026 defaultPosition: 'top'
1030 do_select_first_visible_tab: function() {
1031 for (var i = 0; i < this.pages.length; i++) {
1032 var page = this.pages[i];
1033 if (page.invisible === false) {
1034 this.$element.tabs('select', page.index);
1041 openerp.web.form.WidgetNotebookPage = openerp.web.form.WidgetFrame.extend({
1042 template: 'WidgetNotebookPage',
1043 init: function(view, node, notebook, index) {
1044 this.notebook = notebook;
1046 this.element_name = 'page_' + index;
1047 this._super(view, node);
1050 this._super.apply(this, arguments);
1051 this.$element_tab = this.notebook.$element.find(
1052 '> ul > li:eq(' + this.index + ')');
1054 update_dom: function() {
1055 if (this.invisible && this.index === this.notebook.$element.tabs('option', 'selected')) {
1056 this.notebook.do_select_first_visible_tab();
1058 this.$element_tab.toggle(!this.invisible);
1059 this.$element.toggle(!this.invisible);
1063 openerp.web.form.WidgetSeparator = openerp.web.form.Widget.extend({
1064 template: 'WidgetSeparator',
1065 init: function(view, node) {
1066 this._super(view, node);
1067 this.orientation = node.attrs.orientation || 'horizontal';
1068 if (this.orientation === 'vertical') {
1071 this.classname += '_' + this.orientation;
1075 openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({
1076 template: 'WidgetButton',
1077 init: function(view, node) {
1078 this._super(view, node);
1079 this.force_disabled = false;
1081 // We don't have button key bindings in the webclient
1082 this.string = this.string.replace(/_/g, '');
1084 if (node.attrs.default_focus == '1') {
1085 // TODO fme: provide enter key binding to widgets
1086 this.view.default_focus_button = this;
1090 this._super.apply(this, arguments);
1091 this.$element.find("button").click(this.on_click);
1092 if (this.help || openerp.connection.debug) {
1093 this.do_attach_tooltip();
1096 on_click: function() {
1098 this.force_disabled = true;
1099 this.check_disable();
1100 this.execute_action().always(function() {
1101 self.force_disabled = false;
1102 self.check_disable();
1105 execute_action: function() {
1107 var exec_action = function() {
1108 if (self.node.attrs.confirm) {
1109 var def = $.Deferred();
1110 var dialog = $('<div>' + self.node.attrs.confirm + '</div>').dialog({
1115 self.on_confirmed().then(function() {
1118 $(this).dialog("close");
1120 Cancel: function() {
1122 $(this).dialog("close");
1126 return def.promise();
1128 return self.on_confirmed();
1131 if (!this.node.attrs.special) {
1132 return this.view.recursive_save().pipe(exec_action);
1134 return exec_action();
1137 on_confirmed: function() {
1140 var context = this.node.attrs.context;
1141 if (context && context.__ref) {
1142 context = new openerp.web.CompoundContext(context);
1143 context.set_eval_context(this._build_eval_context());
1146 return this.view.do_execute_action(
1147 _.extend({}, this.node.attrs, {context: context}),
1148 this.view.dataset, this.view.datarecord.id, function () {
1152 update_dom: function() {
1154 this.check_disable();
1156 check_disable: function() {
1157 if (this.readonly || this.force_disabled || !this.view.is_interactible_record()) {
1158 this.$element.find("button").attr("disabled", "disabled");
1159 this.$element.find("button").css("color", "grey");
1161 this.$element.find("button").removeAttr("disabled");
1162 this.$element.find("button").css("color", "");
1167 openerp.web.form.WidgetLabel = openerp.web.form.Widget.extend({
1168 template: 'WidgetLabel',
1169 init: function(view, node) {
1170 this.element_name = 'label_' + node.attrs.name;
1172 this._super(view, node);
1174 if (this.node.tag == 'label' && (this.align === 'left' || this.node.attrs.colspan || (this.string && this.string.length > 32))) {
1175 this.template = "WidgetParagraph";
1176 this.colspan = parseInt(this.node.attrs.colspan || 1, 10);
1177 // Widgets default to right-aligned, but paragraph defaults to
1179 if (isNaN(parseFloat(this.node.attrs.align))) {
1180 this.align = 'left';
1185 this.decrease_max_width = 1;
1189 render: function () {
1190 if (this['for'] && this.type !== 'label') {
1191 return QWeb.render(this.template, {widget: this['for']});
1193 // Actual label widgets should not have a false and have type label
1194 return QWeb.render(this.template, {widget: this});
1199 if (this['for'] && (this['for'].help || openerp.connection.debug)) {
1200 this.do_attach_tooltip(self['for']);
1202 this.$element.find("label").dblclick(function() {
1203 var widget = self['for'] || self;
1204 openerp.log(widget.element_class , widget);
1210 openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.form.Field# */{
1212 * @constructs openerp.web.form.Field
1213 * @extends openerp.web.form.Widget
1218 init: function(view, node) {
1219 this.name = node.attrs.name;
1220 this.value = undefined;
1221 view.fields[this.name] = this;
1222 this.type = node.attrs.widget || view.fields_view.fields[node.attrs.name].type;
1223 this.element_name = "field_" + this.name + "_" + this.type;
1225 this._super(view, node);
1227 if (node.attrs.nolabel != '1' && this.colspan > 1) {
1230 this.field = view.fields_view.fields[node.attrs.name] || {};
1231 this.string = node.attrs.string || this.field.string;
1232 this.help = node.attrs.help || this.field.help;
1233 this.nolabel = (this.field.nolabel || node.attrs.nolabel) === '1';
1234 this.readonly = this.modifiers['readonly'] === true;
1235 this.required = this.modifiers['required'] === true;
1236 this.invalid = this.dirty = false;
1238 this.classname = 'oe_form_field_' + this.type;
1241 this._super.apply(this, arguments);
1242 if (this.field.translate) {
1243 this.view.translatable_fields.push(this);
1244 this.$element.find('.oe_field_translate').click(this.on_translate);
1246 if (this.nolabel && openerp.connection.debug) {
1247 this.do_attach_tooltip(this, this.$element, {
1248 defaultPosition: 'top'
1252 set_value: function(value) {
1254 this.invalid = false;
1256 this.on_value_changed();
1258 set_value_from_ui: function() {
1259 this.on_value_changed();
1261 on_value_changed: function() {
1263 on_translate: function() {
1264 this.view.open_translate_dialog(this);
1266 get_value: function() {
1269 is_valid: function() {
1270 return !this.invalid;
1272 is_dirty: function() {
1273 return this.dirty && !this.readonly;
1275 get_on_change_value: function() {
1276 return this.get_value();
1278 update_dom: function() {
1279 this._super.apply(this, arguments);
1280 if (this.field.translate) {
1281 this.$element.find('.oe_field_translate').toggle(!!this.view.datarecord.id);
1283 if (!this.disable_utility_classes) {
1284 this.$element.toggleClass('disabled', this.readonly);
1285 this.$element.toggleClass('required', this.required);
1286 if (this.view.show_invalid) {
1287 this.$element.toggleClass('invalid', !this.is_valid());
1291 on_ui_change: function() {
1294 if (this.is_valid()) {
1295 this.set_value_from_ui();
1296 this.view.do_onchange(this);
1297 this.view.on_form_changed();
1302 validate: function() {
1303 this.invalid = false;
1312 openerp.web.form.FieldChar = openerp.web.form.Field.extend({
1313 template: 'FieldChar',
1314 init: function (view, node) {
1315 this._super(view, node);
1316 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
1319 this._super.apply(this, arguments);
1320 this.$element.find('input').change(this.on_ui_change);
1322 set_value: function(value) {
1323 this._super.apply(this, arguments);
1324 var show_value = openerp.web.format_value(value, this, '');
1325 this.$element.find('input').val(show_value);
1328 update_dom: function() {
1329 this._super.apply(this, arguments);
1330 this.$element.find('input').attr('disabled', this.readonly);
1332 set_value_from_ui: function() {
1333 this.value = openerp.web.parse_value(this.$element.find('input').val(), this);
1336 validate: function() {
1337 this.invalid = false;
1339 var value = openerp.web.parse_value(this.$element.find('input').val(), this, '');
1340 this.invalid = this.required && value === '';
1342 this.invalid = true;
1346 this.$element.find('input').focus();
1350 openerp.web.form.FieldEmail = openerp.web.form.FieldChar.extend({
1351 template: 'FieldEmail',
1353 this._super.apply(this, arguments);
1354 this.$element.find('button').click(this.on_button_clicked);
1356 on_button_clicked: function() {
1357 if (!this.value || !this.is_valid()) {
1358 this.do_warn("E-mail error", "Can't send email to invalid e-mail address");
1360 location.href = 'mailto:' + this.value;
1365 openerp.web.form.FieldUrl = openerp.web.form.FieldChar.extend({
1366 template: 'FieldUrl',
1368 this._super.apply(this, arguments);
1369 this.$element.find('button').click(this.on_button_clicked);
1371 on_button_clicked: function() {
1373 this.do_warn("Resource error", "This resource is empty");
1375 window.open(this.value);
1380 openerp.web.form.FieldFloat = openerp.web.form.FieldChar.extend({
1381 init: function (view, node) {
1382 this._super(view, node);
1383 if (node.attrs.digits) {
1384 this.parse_digits(node.attrs.digits);
1386 this.digits = view.fields_view.fields[node.attrs.name].digits;
1389 parse_digits: function (digits_attr) {
1390 // could use a Python parser instead.
1391 var match = /^\s*[\(\[](\d+),\s*(\d+)/.exec(digits_attr);
1392 return [parseInt(match[1], 10), parseInt(match[2], 10)];
1394 set_value: function(value) {
1395 if (value === false || value === undefined) {
1396 // As in GTK client, floats default to 0
1399 this._super.apply(this, [value]);
1403 openerp.web.DateTimeWidget = openerp.web.Widget.extend({
1404 template: "web.datetimepicker",
1405 jqueryui_object: 'datetimepicker',
1406 type_of_date: "datetime",
1407 init: function(parent) {
1408 this._super(parent);
1409 this.name = parent.name;
1413 this.$input = this.$element.find('input.oe_datepicker_master');
1414 this.$input_picker = this.$element.find('input.oe_datepicker_container');
1415 this.$input.change(this.on_change);
1417 onSelect: this.on_picker_select,
1421 showButtonPanel: true
1423 this.$element.find('img.oe_datepicker_trigger').click(function() {
1424 if (!self.readonly) {
1425 self.picker('setDate', self.value ? openerp.web.auto_str_to_date(self.value) : new Date());
1426 self.$input_picker.show();
1427 self.picker('show');
1428 self.$input_picker.hide();
1431 this.set_readonly(false);
1434 picker: function() {
1435 return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
1437 on_picker_select: function(text, instance) {
1438 var date = this.picker('getDate');
1439 this.$input.val(date ? this.format_client(date) : '').change();
1441 set_value: function(value) {
1443 this.$input.val(value ? this.format_client(value) : '');
1445 get_value: function() {
1448 set_value_from_ui: function() {
1449 var value = this.$input.val() || false;
1450 this.value = this.parse_client(value);
1452 set_readonly: function(readonly) {
1453 this.readonly = readonly;
1454 this.$input.attr('disabled', this.readonly);
1455 this.$element.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
1457 is_valid: function(required) {
1458 var value = this.$input.val();
1463 this.parse_client(value);
1471 this.$input.focus();
1473 parse_client: function(v) {
1474 return openerp.web.parse_value(v, {"widget": this.type_of_date});
1476 format_client: function(v) {
1477 return openerp.web.format_value(v, {"widget": this.type_of_date});
1479 on_change: function() {
1480 if (this.is_valid()) {
1481 this.set_value_from_ui();
1486 openerp.web.DateWidget = openerp.web.DateTimeWidget.extend({
1487 jqueryui_object: 'datepicker',
1488 type_of_date: "date"
1491 openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({
1492 template: "EmptyComponent",
1493 build_widget: function() {
1494 return new openerp.web.DateTimeWidget(this);
1498 this._super.apply(this, arguments);
1499 this.datewidget = this.build_widget();
1500 this.datewidget.on_change.add_last(this.on_ui_change);
1501 this.datewidget.appendTo(this.$element);
1503 set_value: function(value) {
1505 this.datewidget.set_value(value);
1507 get_value: function() {
1508 return this.datewidget.get_value();
1510 update_dom: function() {
1511 this._super.apply(this, arguments);
1512 this.datewidget.set_readonly(this.readonly);
1514 validate: function() {
1515 this.invalid = !this.datewidget.is_valid(this.required);
1518 this.datewidget.focus();
1522 openerp.web.form.FieldDate = openerp.web.form.FieldDatetime.extend({
1523 build_widget: function() {
1524 return new openerp.web.DateWidget(this);
1528 openerp.web.form.FieldText = openerp.web.form.Field.extend({
1529 template: 'FieldText',
1531 this._super.apply(this, arguments);
1532 this.$element.find('textarea').change(this.on_ui_change);
1534 set_value: function(value) {
1535 this._super.apply(this, arguments);
1536 var show_value = openerp.web.format_value(value, this, '');
1537 this.$element.find('textarea').val(show_value);
1539 update_dom: function() {
1540 this._super.apply(this, arguments);
1541 this.$element.find('textarea').attr('disabled', this.readonly);
1543 set_value_from_ui: function() {
1544 this.value = openerp.web.parse_value(this.$element.find('textarea').val(), this);
1547 validate: function() {
1548 this.invalid = false;
1550 var value = openerp.web.parse_value(this.$element.find('textarea').val(), this, '');
1551 this.invalid = this.required && value === '';
1553 this.invalid = true;
1557 this.$element.find('textarea').focus();
1561 openerp.web.form.FieldBoolean = openerp.web.form.Field.extend({
1562 template: 'FieldBoolean',
1565 this._super.apply(this, arguments);
1566 this.$element.find('input').click(self.on_ui_change);
1568 set_value: function(value) {
1569 this._super.apply(this, arguments);
1570 this.$element.find('input')[0].checked = value;
1572 set_value_from_ui: function() {
1573 this.value = this.$element.find('input').is(':checked');
1576 update_dom: function() {
1577 this._super.apply(this, arguments);
1578 this.$element.find('input').attr('disabled', this.readonly);
1581 this.$element.find('input').focus();
1585 openerp.web.form.FieldProgressBar = openerp.web.form.Field.extend({
1586 template: 'FieldProgressBar',
1588 this._super.apply(this, arguments);
1589 this.$element.find('div').progressbar({
1591 disabled: this.readonly
1594 set_value: function(value) {
1595 this._super.apply(this, arguments);
1596 var show_value = Number(value);
1597 if (isNaN(show_value)) {
1600 this.$element.find('div').progressbar('option', 'value', show_value).find('span').html(show_value + '%');
1604 openerp.web.form.FieldTextXml = openerp.web.form.Field.extend({
1605 // to replace view editor
1608 openerp.web.form.FieldSelection = openerp.web.form.Field.extend({
1609 template: 'FieldSelection',
1610 init: function(view, node) {
1612 this._super(view, node);
1613 this.values = _.clone(this.field.selection);
1614 _.each(this.values, function(v, i) {
1615 if (v[0] === false && v[1] === '') {
1616 self.values.splice(i, 1);
1619 this.values.unshift([false, '']);
1622 // Flag indicating whether we're in an event chain containing a change
1623 // event on the select, in order to know what to do on keyup[RETURN]:
1624 // * If the user presses [RETURN] as part of changing the value of a
1625 // selection, we should just let the value change and not let the
1626 // event broadcast further (e.g. to validating the current state of
1627 // the form in editable list view, which would lead to saving the
1628 // current row or switching to the next one)
1629 // * If the user presses [RETURN] with a select closed (side-effect:
1630 // also if the user opened the select and pressed [RETURN] without
1631 // changing the selected value), takes the action as validating the
1633 var ischanging = false;
1634 this._super.apply(this, arguments);
1635 this.$element.find('select')
1636 .change(this.on_ui_change)
1637 .change(function () { ischanging = true; })
1638 .click(function () { ischanging = false; })
1639 .keyup(function (e) {
1640 if (e.which !== 13 || !ischanging) { return; }
1641 e.stopPropagation();
1645 set_value: function(value) {
1646 value = value === null ? false : value;
1647 value = value instanceof Array ? value[0] : value;
1650 for (var i = 0, ii = this.values.length; i < ii; i++) {
1651 if (this.values[i][0] === value) index = i;
1653 this.$element.find('select')[0].selectedIndex = index;
1655 set_value_from_ui: function() {
1656 this.value = this.values[this.$element.find('select')[0].selectedIndex][0];
1659 update_dom: function() {
1660 this._super.apply(this, arguments);
1661 this.$element.find('select').attr('disabled', this.readonly);
1663 validate: function() {
1664 var value = this.values[this.$element.find('select')[0].selectedIndex];
1665 this.invalid = !(value && !(this.required && value[0] === false));
1668 this.$element.find('select').focus();
1672 // jquery autocomplete tweak to allow html
1674 var proto = $.ui.autocomplete.prototype,
1675 initSource = proto._initSource;
1677 function filter( array, term ) {
1678 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
1679 return $.grep( array, function(value) {
1680 return matcher.test( $( "<div>" ).html( value.label || value.value || value ).text() );
1685 _initSource: function() {
1686 if ( this.options.html && $.isArray(this.options.source) ) {
1687 this.source = function( request, response ) {
1688 response( filter( this.options.source, request.term ) );
1691 initSource.call( this );
1695 _renderItem: function( ul, item) {
1696 return $( "<li></li>" )
1697 .data( "item.autocomplete", item )
1698 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
1704 openerp.web.form.dialog = function(content, options) {
1705 options = _.extend({
1712 options.autoOpen = true;
1713 var dialog = new openerp.web.Dialog(null, options);
1714 dialog.$dialog = $(content).dialog(dialog.dialog_options);
1715 return dialog.$dialog;
1718 openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({
1719 template: 'FieldMany2One',
1720 init: function(view, node) {
1721 this._super(view, node);
1724 this.cm_id = _.uniqueId('m2o_cm_');
1725 this.last_search = [];
1726 this.tmp_value = undefined;
1731 this.$input = this.$element.find("input");
1732 this.$drop_down = this.$element.find(".oe-m2o-drop-down-button");
1733 this.$menu_btn = this.$element.find(".oe-m2o-cm-button");
1736 var init_context_menu_def = $.Deferred().then(function(e) {
1737 var rdataset = new openerp.web.DataSetStatic(self, "ir.values", self.build_context());
1738 rdataset.call("get", ['action', 'client_action_relate',
1739 [[self.field.relation, false]], false, rdataset.get_context()], false, 0)
1740 .then(function(result) {
1741 self.related_entries = result;
1743 var $cmenu = $("#" + self.cm_id);
1744 $cmenu.append(QWeb.render("FieldMany2One.context_menu", {widget: self}));
1746 bindings[self.cm_id + "_search"] = function() {
1747 self._search_create_popup("search");
1749 bindings[self.cm_id + "_create"] = function() {
1750 self._search_create_popup("form");
1752 bindings[self.cm_id + "_open"] = function() {
1756 var pop = new openerp.web.form.FormOpenPopup(self.view);
1757 pop.show_element(self.field.relation, self.value[0],self.build_context(), {});
1758 pop.on_write_completed.add_last(function() {
1759 self.set_value(self.value[0]);
1762 _.each(_.range(self.related_entries.length), function(i) {
1763 bindings[self.cm_id + "_related_" + i] = function() {
1764 self.open_related(self.related_entries[i]);
1767 var cmenu = self.$menu_btn.contextMenu(self.cm_id, {'leftClickToo': true,
1768 bindings: bindings, itemStyle: {"color": ""},
1769 onContextMenu: function() {
1771 $("#" + self.cm_id + " .oe_m2o_menu_item_mandatory").removeClass("oe-m2o-disabled-cm");
1773 $("#" + self.cm_id + " .oe_m2o_menu_item_mandatory").addClass("oe-m2o-disabled-cm");
1775 if (!self.readonly) {
1776 $("#" + self.cm_id + " .oe_m2o_menu_item_noreadonly").removeClass("oe-m2o-disabled-cm");
1778 $("#" + self.cm_id + " .oe_m2o_menu_item_noreadonly").addClass("oe-m2o-disabled-cm");
1781 }, menuStyle: {width: "200px"}
1783 setTimeout(function() {self.$menu_btn.trigger(e);}, 0);
1786 var ctx_callback = function(e) {init_context_menu_def.resolve(e); e.preventDefault()};
1787 this.$menu_btn.bind('contextmenu', ctx_callback);
1788 this.$menu_btn.click(ctx_callback);
1790 // some behavior for input
1791 this.$input.keyup(function() {
1792 if (self.$input.val() === "") {
1793 self._change_int_value(null);
1794 } else if (self.value === null || (self.value && self.$input.val() !== self.value[1])) {
1795 self._change_int_value(undefined);
1798 this.$drop_down.click(function() {
1801 if (self.$input.autocomplete("widget").is(":visible")) {
1802 self.$input.autocomplete("close");
1805 self.$input.autocomplete("search", "");
1807 self.$input.autocomplete("search");
1809 self.$input.focus();
1812 var anyoneLoosesFocus = function() {
1813 if (!self.$input.is(":focus") &&
1814 !self.$input.autocomplete("widget").is(":visible") &&
1816 if (self.value === undefined && self.last_search.length > 0) {
1817 self._change_int_ext_value(self.last_search[0]);
1819 self._change_int_ext_value(null);
1823 this.$input.focusout(anyoneLoosesFocus);
1825 var isSelecting = false;
1827 this.$input.autocomplete({
1828 source: function(req, resp) { self.get_search_result(req, resp); },
1829 select: function(event, ui) {
1833 self._change_int_value([item.id, item.name]);
1834 } else if (item.action) {
1835 self._change_int_value(undefined);
1840 focus: function(e, ui) {
1844 close: anyoneLoosesFocus,
1848 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
1849 this.$input.keyup(function(e) {
1850 if (e.which === 13) {
1852 e.stopPropagation();
1854 isSelecting = false;
1857 // autocomplete component content handling
1858 get_search_result: function(request, response) {
1859 var search_val = request.term;
1862 var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context());
1864 dataset.name_search(search_val, self.build_domain(), 'ilike',
1865 this.limit + 1, function(data) {
1866 self.last_search = data;
1867 // possible selections for the m2o
1868 var values = _.map(data, function(x) {
1869 return {label: $('<span />').text(x[1]).html(), name:x[1], id:x[0]};
1872 // search more... if more results that max
1873 if (values.length > self.limit) {
1874 values = values.slice(0, self.limit);
1875 values.push({label: _t("<em>Â Â Â Search More...</em>"), action: function() {
1876 dataset.name_search(search_val, self.build_domain(), 'ilike'
1877 , false, function(data) {
1878 self._change_int_value(null);
1879 self._search_create_popup("search", data);
1884 var raw_result = _(data.result).map(function(x) {return x[1];});
1885 if (search_val.length > 0 &&
1886 !_.include(raw_result, search_val) &&
1887 (!self.value || search_val !== self.value[1])) {
1888 values.push({label: _.str.sprintf(_t('<em>Â Â Â Create "<strong>%s</strong>"</em>'),
1889 $('<span />').text(search_val).html()), action: function() {
1890 self._quick_create(search_val);
1894 values.push({label: _t("<em>Â Â Â Create and Edit...</em>"), action: function() {
1895 self._change_int_value(null);
1896 self._search_create_popup("form", undefined, {"default_name": search_val});
1902 _quick_create: function(name) {
1904 var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context());
1905 dataset.name_create(name, function(data) {
1906 self._change_int_ext_value(data);
1907 }).fail(function(error, event) {
1908 event.preventDefault();
1909 self._change_int_value(null);
1910 self._search_create_popup("form", undefined, {"default_name": name});
1913 // all search/create popup handling
1914 _search_create_popup: function(view, ids, context) {
1916 var pop = new openerp.web.form.SelectCreatePopup(this);
1917 pop.select_element(self.field.relation,{
1918 initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
1920 disable_multiple_selection: true
1921 }, self.build_domain(),
1922 new openerp.web.CompoundContext(self.build_context(), context || {}));
1923 pop.on_select_elements.add(function(element_ids) {
1924 var dataset = new openerp.web.DataSetStatic(self, self.field.relation, self.build_context());
1925 dataset.name_get([element_ids[0]], function(data) {
1926 self._change_int_ext_value(data[0]);
1930 _change_int_ext_value: function(value) {
1931 this._change_int_value(value);
1932 this.$input.val(this.value ? this.value[1] : "");
1934 _change_int_value: function(value) {
1936 var back_orig_value = this.original_value;
1937 if (this.value === null || this.value) {
1938 this.original_value = this.value;
1940 if (back_orig_value === undefined) { // first use after a set_value()
1943 if (this.value !== undefined && ((back_orig_value ? back_orig_value[0] : null)
1944 !== (this.value ? this.value[0] : null))) {
1945 this.on_ui_change();
1948 set_value: function(value) {
1949 value = value || null;
1950 this.invalid = false;
1952 this.tmp_value = value;
1954 self.on_value_changed();
1955 var real_set_value = function(rval) {
1956 self.tmp_value = undefined;
1958 self.original_value = undefined;
1959 self._change_int_ext_value(rval);
1961 if (value && !(value instanceof Array)) {
1962 var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context());
1963 dataset.name_get([value], function(data) {
1964 real_set_value(data[0]);
1965 }).fail(function() {self.tmp_value = undefined;});
1967 setTimeout(function() {real_set_value(value);}, 0);
1970 get_value: function() {
1971 if (this.tmp_value !== undefined) {
1972 if (this.tmp_value instanceof Array) {
1973 return this.tmp_value[0];
1975 return this.tmp_value ? this.tmp_value : false;
1977 if (this.value === undefined)
1978 return this.original_value ? this.original_value[0] : false;
1979 return this.value ? this.value[0] : false;
1981 validate: function() {
1982 this.invalid = false;
1983 var val = this.tmp_value !== undefined ? this.tmp_value : this.value;
1985 this.invalid = this.required;
1988 open_related: function(related) {
1992 var additional_context = {
1993 active_id: self.value[0],
1994 active_ids: [self.value[0]],
1995 active_model: self.field.relation
1997 self.rpc("/web/action/load", {
1998 action_id: related[2].id,
1999 context: additional_context
2000 }, function(result) {
2001 result.result.context = _.extend(result.result.context || {}, additional_context);
2002 self.do_action(result.result);
2005 focus: function () {
2006 this.$input.focus();
2008 update_dom: function() {
2009 this._super.apply(this, arguments);
2010 this.$input.attr('disabled', this.readonly);
2015 # Values: (0, 0, { fields }) create
2016 # (1, ID, { fields }) update
2017 # (2, ID) remove (delete)
2018 # (3, ID) unlink one (target id or target of relation)
2020 # (5) unlink all (only valid for one2many)
2025 'create': function (values) {
2026 return [commands.CREATE, false, values];
2028 // (1, id, {values})
2030 'update': function (id, values) {
2031 return [commands.UPDATE, id, values];
2035 'delete': function (id) {
2036 return [commands.DELETE, id, false];
2038 // (3, id[, _]) removes relation, but not linked record itself
2040 'forget': function (id) {
2041 return [commands.FORGET, id, false];
2045 'link_to': function (id) {
2046 return [commands.LINK_TO, id, false];
2050 'delete_all': function () {
2051 return [5, false, false];
2053 // (6, _, ids) replaces all linked records with provided ids
2055 'replace_with': function (ids) {
2056 return [6, false, ids];
2059 openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({
2060 template: 'FieldOne2Many',
2061 multi_selection: false,
2062 init: function(view, node) {
2063 this._super(view, node);
2064 this.is_loaded = $.Deferred();
2065 this.initial_is_loaded = this.is_loaded;
2066 this.is_setted = $.Deferred();
2067 this.form_last_update = $.Deferred();
2068 this.init_form_last_update = this.form_last_update;
2069 this.disable_utility_classes = true;
2072 this._super.apply(this, arguments);
2076 this.dataset = new openerp.web.form.One2ManyDataSet(this, this.field.relation);
2077 this.dataset.o2m = this;
2078 this.dataset.parent_view = this.view;
2079 this.dataset.on_change.add_last(function() {
2080 self.on_ui_change();
2083 this.is_setted.then(function() {
2087 is_readonly: function() {
2088 return this.readonly || this.force_readonly;
2090 load_views: function() {
2093 var modes = this.node.attrs.mode;
2094 modes = !!modes ? modes.split(",") : ["tree"];
2096 _.each(modes, function(mode) {
2099 view_type: mode == "tree" ? "list" : mode,
2100 options: { sidebar : false }
2102 if (self.field.views && self.field.views[mode]) {
2103 view.embedded_view = self.field.views[mode];
2105 if(view.view_type === "list") {
2106 view.options.selectable = self.multi_selection;
2107 if (self.is_readonly()) {
2108 view.options.addable = null;
2109 view.options.deletable = null;
2111 } else if (view.view_type === "form") {
2112 view.options.not_interactible_on_create = true;
2118 this.viewmanager = new openerp.web.ViewManager(this, this.dataset, views);
2119 this.viewmanager.registry = openerp.web.views.clone({
2120 list: 'openerp.web.form.One2ManyListView',
2121 form: 'openerp.web.FormView'
2123 var once = $.Deferred().then(function() {
2124 self.init_form_last_update.resolve();
2126 var def = $.Deferred().then(function() {
2127 self.initial_is_loaded.resolve();
2129 this.viewmanager.on_controller_inited.add_last(function(view_type, controller) {
2130 if (view_type == "list") {
2131 controller.o2m = self;
2132 if (self.is_readonly())
2133 controller.set_editable(false);
2134 } else if (view_type == "form") {
2135 if (self.is_readonly()) {
2136 controller.on_toggle_readonly();
2137 $(controller.$element.find(".oe_form_buttons")[0]).children().remove();
2139 controller.on_record_loaded.add_last(function() {
2142 controller.on_pager_action.add_first(function() {
2143 self.save_any_view();
2145 controller.$element.find(".oe_form_button_save").hide();
2146 } else if (view_type == "graph") {
2147 self.reload_current_view()
2151 this.viewmanager.on_mode_switch.add_first(function(n_mode, b, c, d, e) {
2152 $.when(self.save_any_view()).then(function() {
2153 if(n_mode === "list")
2154 setTimeout(function() {self.reload_current_view();}, 0);
2157 this.is_setted.then(function() {
2158 setTimeout(function () {
2159 self.viewmanager.appendTo(self.$element);
2164 reload_current_view: function() {
2166 return self.is_loaded = self.is_loaded.pipe(function() {
2167 var view = self.viewmanager.views[self.viewmanager.active_view].controller;
2168 if(self.viewmanager.active_view === "list") {
2169 return view.reload_content();
2170 } else if (self.viewmanager.active_view === "form") {
2171 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
2172 self.dataset.index = 0;
2174 var act = function() {
2175 return view.do_show();
2177 self.form_last_update = self.form_last_update.pipe(act, act);
2178 return self.form_last_update;
2179 } else if (self.viewmanager.active_view === "graph") {
2180 return view.do_search(self.build_domain(), self.dataset.get_context(), []);
2184 set_value: function(value) {
2185 value = value || [];
2187 this.dataset.reset_ids([]);
2188 if(value.length >= 1 && value[0] instanceof Array) {
2190 _.each(value, function(command) {
2191 var obj = {values: command[2]};
2192 switch (command[0]) {
2193 case commands.CREATE:
2194 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
2196 self.dataset.to_create.push(obj);
2197 self.dataset.cache.push(_.clone(obj));
2200 case commands.UPDATE:
2201 obj['id'] = command[1];
2202 self.dataset.to_write.push(obj);
2203 self.dataset.cache.push(_.clone(obj));
2206 case commands.DELETE:
2207 self.dataset.to_delete.push({id: command[1]});
2209 case commands.LINK_TO:
2210 ids.push(command[1]);
2212 case commands.DELETE_ALL:
2213 self.dataset.delete_all = true;
2218 this.dataset.set_ids(ids);
2219 } else if (value.length >= 1 && typeof(value[0]) === "object") {
2221 this.dataset.delete_all = true;
2222 _.each(value, function(command) {
2223 var obj = {values: command};
2224 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
2226 self.dataset.to_create.push(obj);
2227 self.dataset.cache.push(_.clone(obj));
2231 this.dataset.set_ids(ids);
2234 this.dataset.reset_ids(value);
2236 if (this.dataset.index === null && this.dataset.ids.length > 0) {
2237 this.dataset.index = 0;
2239 self.is_setted.resolve();
2240 return self.reload_current_view();
2242 get_value: function() {
2246 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
2247 val = val.concat(_.map(this.dataset.ids, function(id) {
2248 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
2250 return commands.create(alter_order.values);
2252 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
2254 return commands.update(alter_order.id, alter_order.values);
2256 return commands.link_to(id);
2258 return val.concat(_.map(
2259 this.dataset.to_delete, function(x) {
2260 return commands['delete'](x.id);}));
2262 save_any_view: function() {
2263 if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
2264 this.viewmanager.views[this.viewmanager.active_view] &&
2265 this.viewmanager.views[this.viewmanager.active_view].controller) {
2266 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
2267 if (this.viewmanager.active_view === "form") {
2268 var res = $.when(view.do_save());
2269 // it seems line there are some cases when this happens
2270 /*if (!res.isResolved() && !res.isRejected()) {
2271 console.warn("Asynchronous get_value() is not supported in form view.");
2274 } else if (this.viewmanager.active_view === "list") {
2275 var res = $.when(view.ensure_saved());
2276 // it seems line there are some cases when this happens
2277 /*if (!res.isResolved() && !res.isRejected()) {
2278 console.warn("Asynchronous get_value() is not supported in list view.");
2285 is_valid: function() {
2287 return this._super();
2289 validate: function() {
2290 this.invalid = false;
2291 if (!this.viewmanager.views[this.viewmanager.active_view])
2293 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
2294 if (this.viewmanager.active_view === "form") {
2295 for (var f in view.fields) {
2297 if (!f.is_valid()) {
2298 this.invalid = true;
2304 is_dirty: function() {
2305 this.save_any_view();
2306 return this._super();
2308 update_dom: function() {
2309 this._super.apply(this, arguments);
2311 if (this.previous_readonly !== this.readonly) {
2312 this.previous_readonly = this.readonly;
2313 if (this.viewmanager) {
2314 this.is_loaded = this.is_loaded.pipe(function() {
2315 self.viewmanager.stop();
2316 return $.when(self.load_views()).then(function() {
2317 self.reload_current_view();
2325 openerp.web.form.One2ManyDataSet = openerp.web.BufferedDataSet.extend({
2326 get_context: function() {
2327 this.context = this.o2m.build_context();
2328 return this.context;
2332 openerp.web.form.One2ManyListView = openerp.web.ListView.extend({
2333 do_add_record: function () {
2334 if (this.options.editable) {
2335 this._super.apply(this, arguments);
2338 var pop = new openerp.web.form.SelectCreatePopup(this);
2339 pop.on_default_get.add(self.dataset.on_default_get);
2340 pop.select_element(self.o2m.field.relation,{
2341 initial_view: "form",
2342 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
2343 create_function: function(data, callback, error_callback) {
2344 return self.o2m.dataset.create(data).then(function(r) {
2345 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r.result]));
2346 self.o2m.dataset.on_change();
2347 }).then(callback, error_callback);
2349 read_function: function() {
2350 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
2352 parent_view: self.o2m.view,
2353 form_view_options: {'not_interactible_on_create':true}
2354 }, self.o2m.build_domain(), self.o2m.build_context());
2355 pop.on_select_elements.add_last(function() {
2356 self.o2m.reload_current_view();
2360 do_activate_record: function(index, id) {
2362 var pop = new openerp.web.form.FormOpenPopup(self.o2m.view);
2363 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(),{
2365 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
2366 parent_view: self.o2m.view,
2367 read_function: function() {
2368 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
2370 form_view_options: {'not_interactible_on_create':true},
2371 readonly: self.o2m.is_readonly()
2373 pop.on_write.add(function(id, data) {
2374 self.o2m.dataset.write(id, data, {}, function(r) {
2375 self.o2m.reload_current_view();
2381 openerp.web.form.FieldMany2Many = openerp.web.form.Field.extend({
2382 template: 'FieldMany2Many',
2383 multi_selection: false,
2384 init: function(view, node) {
2385 this._super(view, node);
2386 this.list_id = _.uniqueId("many2many");
2387 this.is_loaded = $.Deferred();
2388 this.initial_is_loaded = this.is_loaded;
2389 this.is_setted = $.Deferred();
2392 this._super.apply(this, arguments);
2396 this.dataset = new openerp.web.form.Many2ManyDataSet(this, this.field.relation);
2397 this.dataset.m2m = this;
2398 this.dataset.on_unlink.add_last(function(ids) {
2399 self.on_ui_change();
2402 this.is_setted.then(function() {
2406 set_value: function(value) {
2407 value = value || [];
2408 if (value.length >= 1 && value[0] instanceof Array) {
2409 value = value[0][2];
2412 this.dataset.set_ids(value);
2414 self.reload_content();
2415 this.is_setted.resolve();
2417 get_value: function() {
2418 return [commands.replace_with(this.dataset.ids)];
2420 validate: function() {
2421 this.invalid = false;
2423 is_readonly: function() {
2424 return this.readonly || this.force_readonly;
2426 load_view: function() {
2428 this.list_view = new openerp.web.form.Many2ManyListView(this, this.dataset, false, {
2429 'addable': self.is_readonly() ? null : 'Add',
2430 'deletable': self.is_readonly() ? false : true,
2431 'selectable': self.multi_selection
2433 var embedded = (this.field.views || {}).tree;
2435 this.list_view.set_embedded_view(embedded);
2437 this.list_view.m2m_field = this;
2438 var loaded = $.Deferred();
2439 this.list_view.on_loaded.add_last(function() {
2440 self.initial_is_loaded.resolve();
2443 setTimeout(function () {
2444 self.list_view.appendTo($("#" + self.list_id));
2448 reload_content: function() {
2450 this.is_loaded = this.is_loaded.pipe(function() {
2451 return self.list_view.reload_content();
2454 update_dom: function() {
2455 this._super.apply(this, arguments);
2457 if (this.previous_readonly !== this.readonly) {
2458 this.previous_readonly = this.readonly;
2459 if (this.list_view) {
2460 this.is_loaded = this.is_loaded.pipe(function() {
2461 self.list_view.stop();
2462 return $.when(self.load_view()).then(function() {
2463 self.reload_content();
2471 openerp.web.form.Many2ManyDataSet = openerp.web.DataSetStatic.extend({
2472 get_context: function() {
2473 this.context = this.m2m.build_context();
2474 return this.context;
2480 * @extends openerp.web.ListView
2482 openerp.web.form.Many2ManyListView = openerp.web.ListView.extend(/** @lends openerp.web.form.Many2ManyListView# */{
2483 do_add_record: function () {
2484 var pop = new openerp.web.form.SelectCreatePopup(this);
2485 pop.select_element(this.model, {},
2486 new openerp.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
2487 this.m2m_field.build_context());
2489 pop.on_select_elements.add(function(element_ids) {
2490 _.each(element_ids, function(element_id) {
2491 if(! _.detect(self.dataset.ids, function(x) {return x == element_id;})) {
2492 self.dataset.set_ids([].concat(self.dataset.ids, [element_id]));
2493 self.m2m_field.on_ui_change();
2494 self.reload_content();
2499 do_activate_record: function(index, id) {
2501 var pop = new openerp.web.form.FormOpenPopup(this);
2502 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {});
2503 pop.on_write_completed.add_last(function() {
2504 self.reload_content();
2511 * @extends openerp.web.OldWidget
2513 openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends openerp.web.form.SelectCreatePopup# */{
2514 identifier_prefix: "selectcreatepopup",
2515 template: "SelectCreatePopup",
2519 * - initial_view: form or search (default search)
2520 * - disable_multiple_selection
2521 * - alternative_form_view
2522 * - create_function (defaults to a naive saving behavior)
2524 * - form_view_options
2525 * - list_view_options
2528 select_element: function(model, options, domain, context) {
2531 this.domain = domain || [];
2532 this.context = context || {};
2533 this.options = _.defaults(options || {}, {"initial_view": "search", "create_function": function() {
2534 return self.create_row.apply(self, arguments);
2535 }, read_function: null});
2536 this.initial_ids = this.options.initial_ids;
2537 this.created_elements = [];
2538 this.render_element();
2539 openerp.web.form.dialog(this.$element, {close:function() {
2547 this.dataset = new openerp.web.ProxyDataSet(this, this.model,
2549 this.dataset.create_function = function() {
2550 return self.options.create_function.apply(null, arguments).then(function(r) {
2551 self.created_elements.push(r.result);
2554 this.dataset.write_function = function() {
2555 return self.write_row.apply(self, arguments);
2557 this.dataset.read_function = this.options.read_function;
2558 this.dataset.parent_view = this.options.parent_view;
2559 this.dataset.on_default_get.add(this.on_default_get);
2560 if (this.options.initial_view == "search") {
2561 self.rpc('/web/session/eval_domain_and_context', {
2563 contexts: [this.context]
2564 }, function (results) {
2565 var search_defaults = {};
2566 _.each(results.context, function (value, key) {
2567 var match = /^search_default_(.*)$/.exec(key);
2569 search_defaults[match[1]] = value;
2572 self.setup_search_view(search_defaults);
2578 setup_search_view: function(search_defaults) {
2580 if (this.searchview) {
2581 this.searchview.stop();
2583 this.searchview = new openerp.web.SearchView(this,
2584 this.dataset, false, search_defaults);
2585 this.searchview.on_search.add(function(domains, contexts, groupbys) {
2586 if (self.initial_ids) {
2587 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
2588 contexts, groupbys);
2589 self.initial_ids = undefined;
2591 self.do_search(domains.concat([self.domain]), contexts, groupbys);
2594 this.searchview.on_loaded.add_last(function () {
2595 var $buttons = self.searchview.$element.find(".oe_search-view-buttons");
2596 $buttons.append(QWeb.render("SelectCreatePopup.search.buttons"));
2597 var $cbutton = $buttons.find(".oe_selectcreatepopup-search-close");
2598 $cbutton.click(function() {
2601 var $sbutton = $buttons.find(".oe_selectcreatepopup-search-select");
2602 if(self.options.disable_multiple_selection) {
2605 $sbutton.click(function() {
2606 self.on_select_elements(self.selected_ids);
2609 self.view_list = new openerp.web.form.SelectCreateListView(self,
2610 self.dataset, false,
2611 _.extend({'deletable': false,
2612 'selectable': !self.options.disable_multiple_selection
2613 }, self.options.list_view_options || {}));
2614 self.view_list.popup = self;
2615 self.view_list.appendTo($("#" + self.element_id + "_view_list")).pipe(function() {
2616 self.view_list.do_show();
2617 }).pipe(function() {
2618 self.searchview.do_search();
2621 this.searchview.appendTo($("#" + this.element_id + "_search"));
2623 do_search: function(domains, contexts, groupbys) {
2625 this.rpc('/web/session/eval_domain_and_context', {
2626 domains: domains || [],
2627 contexts: contexts || [],
2628 group_by_seq: groupbys || []
2629 }, function (results) {
2630 self.view_list.do_search(results.domain, results.context, results.group_by);
2633 create_row: function() {
2635 var wdataset = new openerp.web.DataSetSearch(this, this.model, this.context, this.domain);
2636 wdataset.parent_view = this.options.parent_view;
2637 return wdataset.create.apply(wdataset, arguments);
2639 write_row: function() {
2641 var wdataset = new openerp.web.DataSetSearch(this, this.model, this.context, this.domain);
2642 wdataset.parent_view = this.options.parent_view;
2643 return wdataset.write.apply(wdataset, arguments);
2645 on_select_elements: function(element_ids) {
2647 on_click_element: function(ids) {
2648 this.selected_ids = ids || [];
2649 if(this.selected_ids.length > 0) {
2650 this.$element.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
2652 this.$element.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
2655 new_object: function() {
2657 if (this.searchview) {
2658 this.searchview.hide();
2660 if (this.view_list) {
2661 this.view_list.$element.hide();
2663 this.dataset.index = null;
2664 this.view_form = new openerp.web.FormView(this, this.dataset, false, self.options.form_view_options);
2665 if (this.options.alternative_form_view) {
2666 this.view_form.set_embedded_view(this.options.alternative_form_view);
2668 this.view_form.appendTo(this.$element.find("#" + this.element_id + "_view_form"));
2669 this.view_form.on_loaded.add_last(function() {
2670 var $buttons = self.view_form.$element.find(".oe_form_buttons");
2671 $buttons.html(QWeb.render("SelectCreatePopup.form.buttons", {widget:self}));
2672 var $nbutton = $buttons.find(".oe_selectcreatepopup-form-save-new");
2673 $nbutton.click(function() {
2674 $.when(self.view_form.do_save()).then(function() {
2675 self.view_form.reload_lock.then(function() {
2676 self.view_form.on_button_new();
2680 var $nbutton = $buttons.find(".oe_selectcreatepopup-form-save");
2681 $nbutton.click(function() {
2682 $.when(self.view_form.do_save()).then(function() {
2683 self.view_form.reload_lock.then(function() {
2688 var $cbutton = $buttons.find(".oe_selectcreatepopup-form-close");
2689 $cbutton.click(function() {
2693 this.view_form.do_show();
2695 check_exit: function() {
2696 if (this.created_elements.length > 0) {
2697 this.on_select_elements(this.created_elements);
2701 on_default_get: function(res) {}
2704 openerp.web.form.SelectCreateListView = openerp.web.ListView.extend({
2705 do_add_record: function () {
2706 this.popup.new_object();
2708 select_record: function(index) {
2709 this.popup.on_select_elements([this.dataset.ids[index]]);
2712 do_select: function(ids, records) {
2713 this._super(ids, records);
2714 this.popup.on_click_element(ids);
2720 * @extends openerp.web.OldWidget
2722 openerp.web.form.FormOpenPopup = openerp.web.OldWidget.extend(/** @lends openerp.web.form.FormOpenPopup# */{
2723 identifier_prefix: "formopenpopup",
2724 template: "FormOpenPopup",
2727 * - alternative_form_view
2728 * - auto_write (default true)
2731 * - form_view_options
2734 show_element: function(model, row_id, context, options) {
2736 this.row_id = row_id;
2737 this.context = context || {};
2738 this.options = _.defaults(options || {}, {"auto_write": true});
2739 this.render_element();
2740 this.$element.dialog({title: '',
2748 this.dataset = new openerp.web.form.FormOpenDataset(this, this.model, this.context);
2749 this.dataset.fop = this;
2750 this.dataset.ids = [this.row_id];
2751 this.dataset.index = 0;
2752 this.dataset.parent_view = this.options.parent_view;
2753 this.setup_form_view();
2755 on_write: function(id, data) {
2756 if (!this.options.auto_write)
2759 var wdataset = new openerp.web.DataSetSearch(this, this.model, this.context, this.domain);
2760 wdataset.parent_view = this.options.parent_view;
2761 wdataset.write(id, data, {}, function(r) {
2762 self.on_write_completed();
2765 on_write_completed: function() {},
2766 setup_form_view: function() {
2768 this.view_form = new openerp.web.FormView(this, this.dataset, false, self.options.form_view_options);
2769 if (this.options.alternative_form_view) {
2770 this.view_form.set_embedded_view(this.options.alternative_form_view);
2772 this.view_form.appendTo(this.$element.find("#" + this.element_id + "_view_form"));
2773 var once = $.Deferred().then(function() {
2774 if (self.options.readonly) {
2775 self.view_form.on_toggle_readonly();
2778 this.view_form.on_loaded.add_last(function() {
2780 var $buttons = self.view_form.$element.find(".oe_form_buttons");
2781 $buttons.html(QWeb.render("FormOpenPopup.form.buttons"));
2782 var $nbutton = $buttons.find(".oe_formopenpopup-form-save");
2783 $nbutton.click(function() {
2784 self.view_form.do_save().then(function() {
2788 var $cbutton = $buttons.find(".oe_formopenpopup-form-close");
2789 $cbutton.click(function() {
2792 if (self.options.readonly) {
2794 $cbutton.text(_t("Close"));
2796 self.view_form.do_show();
2798 this.dataset.on_write.add(this.on_write);
2802 openerp.web.form.FormOpenDataset = openerp.web.ProxyDataSet.extend({
2803 read_ids: function() {
2804 if (this.fop.options.read_function) {
2805 return this.fop.options.read_function.apply(null, arguments);
2807 return this._super.apply(this, arguments);
2812 openerp.web.form.FieldReference = openerp.web.form.Field.extend({
2813 template: 'FieldReference',
2814 init: function(view, node) {
2815 this._super(view, node);
2816 this.fields_view = {
2819 selection: view.fields_view.fields[this.name].selection
2826 this.get_fields_values = view.get_fields_values;
2827 this.do_onchange = this.on_form_changed = this.on_nop;
2828 this.dataset = this.view.dataset;
2829 this.widgets_counter = 0;
2830 this.view_id = 'reference_' + _.uniqueId();
2833 this.selection = new openerp.web.form.FieldSelection(this, { attrs: {
2837 this.selection.on_value_changed.add_last(this.on_selection_changed);
2838 this.m2o = new openerp.web.form.FieldMany2One(this, { attrs: {
2843 on_nop: function() {
2845 on_selection_changed: function() {
2846 var sel = this.selection.get_value();
2847 this.m2o.field.relation = sel;
2848 this.m2o.set_value(null);
2849 this.m2o.$element.toggle(sel !== false);
2853 this.selection.start();
2856 is_valid: function() {
2857 return this.required === false || typeof(this.get_value()) === 'string';
2859 is_dirty: function() {
2860 return this.selection.is_dirty() || this.m2o.is_dirty();
2862 set_value: function(value) {
2864 if (typeof(value) === 'string') {
2865 var vals = value.split(',');
2866 this.selection.set_value(vals[0]);
2867 this.m2o.set_value(parseInt(vals[1], 10));
2870 get_value: function() {
2871 var model = this.selection.get_value(),
2872 id = this.m2o.get_value();
2873 if (typeof(model) === 'string' && typeof(id) === 'number') {
2874 return model + ',' + id;
2881 openerp.web.form.FieldBinary = openerp.web.form.Field.extend({
2882 init: function(view, node) {
2883 this._super(view, node);
2884 this.iframe = this.element_id + '_iframe';
2885 this.binary_value = false;
2888 this._super.apply(this, arguments);
2889 this.$element.find('input.oe-binary-file').change(this.on_file_change);
2890 this.$element.find('button.oe-binary-file-save').click(this.on_save_as);
2891 this.$element.find('.oe-binary-file-clear').click(this.on_clear);
2893 human_filesize : function(size) {
2894 var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
2896 while (size >= 1024) {
2900 return size.toFixed(2) + ' ' + units[i];
2902 on_file_change: function(e) {
2903 // TODO: on modern browsers, we could directly read the file locally on client ready to be used on image cropper
2904 // http://www.html5rocks.com/tutorials/file/dndfiles/
2905 // http://deepliquid.com/projects/Jcrop/demos.php?demo=handler
2906 window[this.iframe] = this.on_file_uploaded;
2907 if ($(e.target).val() != '') {
2908 this.$element.find('form.oe-binary-form input[name=session_id]').val(this.session.session_id);
2909 this.$element.find('form.oe-binary-form').submit();
2910 this.$element.find('.oe-binary-progress').show();
2911 this.$element.find('.oe-binary').hide();
2914 on_file_uploaded: function(size, name, content_type, file_base64) {
2915 delete(window[this.iframe]);
2916 if (size === false) {
2917 this.do_warn("File Upload", "There was a problem while uploading your file");
2918 // TODO: use openerp web crashmanager
2919 console.warn("Error while uploading file : ", name);
2921 this.on_file_uploaded_and_valid.apply(this, arguments);
2922 this.on_ui_change();
2924 this.$element.find('.oe-binary-progress').hide();
2925 this.$element.find('.oe-binary').show();
2927 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
2929 on_save_as: function() {
2930 var url = '/web/binary/saveas?session_id=' + this.session.session_id + '&model=' +
2931 this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name +
2932 '&fieldname=' + (this.node.attrs.filename || '') + '&t=' + (new Date().getTime());
2935 on_clear: function() {
2936 if (this.value !== false) {
2938 this.binary_value = false;
2939 this.on_ui_change();
2945 openerp.web.form.FieldBinaryFile = openerp.web.form.FieldBinary.extend({
2946 template: 'FieldBinaryFile',
2947 update_dom: function() {
2948 this._super.apply(this, arguments);
2949 this.$element.find('.oe-binary-file-set, .oe-binary-file-clear').toggle(!this.readonly);
2950 this.$element.find('input[type=text]').attr('disabled', this.readonly);
2952 set_value: function(value) {
2953 this._super.apply(this, arguments);
2954 var show_value = (value != null && value !== false) ? value : '';
2955 this.$element.find('input').eq(0).val(show_value);
2957 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
2958 this.value = file_base64;
2959 this.binary_value = true;
2960 var show_value = this.human_filesize(size);
2961 this.$element.find('input').eq(0).val(show_value);
2962 this.set_filename(name);
2964 set_filename: function(value) {
2965 var filename = this.node.attrs.filename;
2966 if (this.view.fields[filename]) {
2967 this.view.fields[filename].set_value(value);
2968 this.view.fields[filename].on_ui_change();
2971 on_clear: function() {
2972 this._super.apply(this, arguments);
2973 this.$element.find('input').eq(0).val('');
2974 this.set_filename('');
2978 openerp.web.form.FieldBinaryImage = openerp.web.form.FieldBinary.extend({
2979 template: 'FieldBinaryImage',
2981 this._super.apply(this, arguments);
2982 this.$image = this.$element.find('img.oe-binary-image');
2984 update_dom: function() {
2985 this._super.apply(this, arguments);
2986 this.$element.find('.oe-binary').toggle(!this.readonly);
2988 set_value: function(value) {
2989 this._super.apply(this, arguments);
2990 this.set_image_maxwidth();
2991 var url = '/web/binary/image?session_id=' + this.session.session_id + '&model=' +
2992 this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name + '&t=' + (new Date().getTime());
2993 this.$image.attr('src', url);
2995 set_image_maxwidth: function() {
2996 this.$image.css('max-width', this.$element.width());
2998 on_file_change: function() {
2999 this.set_image_maxwidth();
3000 this._super.apply(this, arguments);
3002 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
3003 this.value = file_base64;
3004 this.binary_value = true;
3005 this.$image.attr('src', 'data:' + (content_type || 'image/png') + ';base64,' + file_base64);
3007 on_clear: function() {
3008 this._super.apply(this, arguments);
3009 this.$image.attr('src', '/web/static/src/img/placeholder.png');
3013 openerp.web.form.FieldStatus = openerp.web.form.Field.extend({
3014 template: "EmptyComponent",
3017 this.selected_value = null;
3021 set_value: function(value) {
3023 this.selected_value = value;
3027 render_list: function() {
3029 var shown = _.map(((this.node.attrs || {}).statusbar_visible || "").split(","),
3030 function(x) { return _.str.trim(x); });
3031 shown = _.select(shown, function(x) { return x.length > 0; });
3033 if (shown.length == 0) {
3034 this.to_show = this.field.selection;
3036 this.to_show = _.select(this.field.selection, function(x) {
3037 return _.indexOf(shown, x[0]) !== -1 || x[0] === self.selected_value;
3041 var content = openerp.web.qweb.render("FieldStatus.content", {widget: this, _:_});
3042 this.$element.html(content);
3044 var colors = JSON.parse((this.node.attrs || {}).statusbar_colors || "{}");
3045 var color = colors[this.selected_value];
3047 var elem = this.$element.find("li.oe-arrow-list-selected span");
3048 elem.css("border-color", color);
3049 if (this.check_white(color))
3050 elem.css("color", "white");
3051 elem = this.$element.find("li.oe-arrow-list-selected .oe-arrow-list-before");
3052 elem.css("border-left-color", "rgba(0,0,0,0)");
3053 elem = this.$element.find("li.oe-arrow-list-selected .oe-arrow-list-after");
3054 elem.css("border-color", "rgba(0,0,0,0)");
3055 elem.css("border-left-color", color);
3058 check_white: function(color) {
3059 var div = $("<div></div>");
3060 div.css("display", "none");
3061 div.css("color", color);
3062 div.appendTo($("body"));
3063 var ncolor = div.css("color");
3065 var res = /^\s*rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/.exec(ncolor);
3069 var comps = [parseInt(res[1]), parseInt(res[2]), parseInt(res[3])];
3070 var lum = comps[0] * 0.3 + comps[1] * 0.59 + comps[1] * 0.11;
3078 openerp.web.form.FieldReadonly = openerp.web.form.Field.extend({
3081 openerp.web.form.WidgetFrameReadonly = openerp.web.form.WidgetFrame.extend({
3082 template: 'WidgetFrame.readonly'
3084 openerp.web.form.FieldCharReadonly = openerp.web.form.FieldReadonly.extend({
3085 template: 'FieldChar.readonly',
3086 init: function(view, node) {
3087 this._super(view, node);
3088 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
3090 set_value: function (value) {
3091 this._super.apply(this, arguments);
3092 var show_value = openerp.web.format_value(value, this, '');
3093 if (this.password) {
3094 show_value = new Array(show_value.length + 1).join('*');
3096 this.$element.find('div').text(show_value);
3100 openerp.web.form.FieldURIReadonly = openerp.web.form.FieldCharReadonly.extend({
3101 template: 'FieldURI.readonly',
3103 set_value: function (value) {
3104 var displayed = this._super.apply(this, arguments);
3105 this.$element.find('a')
3106 .attr('href', this.scheme + ':' + displayed)
3110 openerp.web.form.FieldEmailReadonly = openerp.web.form.FieldURIReadonly.extend({
3113 openerp.web.form.FieldUrlReadonly = openerp.web.form.FieldURIReadonly.extend({
3114 set_value: function (value) {
3115 var s = /(\w+):(.+)/.exec(value);
3116 if (!s || !(s[1] === 'http' || s[1] === 'https')) { return; }
3121 openerp.web.form.FieldBooleanReadonly = openerp.web.form.FieldCharReadonly.extend({
3122 set_value: function (value) {
3123 this._super(value ? '\u2611' : '\u2610');
3126 openerp.web.form.FieldSelectionReadonly = openerp.web.form.FieldReadonly.extend({
3127 template: 'FieldChar.readonly',
3128 init: function(view, node) {
3129 // lifted straight from r/w version
3131 this._super(view, node);
3132 this.values = _.clone(this.field.selection);
3133 _.each(this.values, function(v, i) {
3134 if (v[0] === false && v[1] === '') {
3135 self.values.splice(i, 1);
3138 this.values.unshift([false, '']);
3140 set_value: function (value) {
3141 value = value === null ? false : value;
3142 value = value instanceof Array ? value[0] : value;
3143 var option = _(this.values)
3144 .detect(function (record) { return record[0] === value; });
3146 this.$element.find('div').text(option ? option[1] : this.values[0][1]);
3149 openerp.web.form.FieldMany2OneReadonly = openerp.web.form.FieldURIReadonly.extend({
3150 set_value: function (value) {
3151 value = value || null;
3152 this.invalid = false;
3156 self.on_value_changed();
3157 var real_set_value = function(rval) {
3159 self.$element.find('a')
3161 .text(rval ? rval[1] : '')
3162 .click(function () {
3164 type: 'ir.actions.act_window',
3165 res_model: self.field.relation,
3166 res_id: self.value[0],
3167 context: self.build_context(),
3168 views: [[false, 'form']],
3174 if (value && !(value instanceof Array)) {
3175 new openerp.web.DataSetStatic(
3176 this, this.field.relation, self.build_context())
3177 .name_get([value], function(data) {
3178 real_set_value(data[0]);
3181 setTimeout(function() {real_set_value(value);}, 0);
3184 get_value: function() {
3187 } else if (this.value instanceof Array) {
3188 return this.value[0];
3196 * Registry of form widgets, called by :js:`openerp.web.FormView`
3198 openerp.web.form.widgets = new openerp.web.Registry({
3199 'frame' : 'openerp.web.form.WidgetFrame',
3200 'group' : 'openerp.web.form.WidgetGroup',
3201 'notebook' : 'openerp.web.form.WidgetNotebook',
3202 'notebookpage' : 'openerp.web.form.WidgetNotebookPage',
3203 'separator' : 'openerp.web.form.WidgetSeparator',
3204 'label' : 'openerp.web.form.WidgetLabel',
3205 'button' : 'openerp.web.form.WidgetButton',
3206 'char' : 'openerp.web.form.FieldChar',
3207 'email' : 'openerp.web.form.FieldEmail',
3208 'url' : 'openerp.web.form.FieldUrl',
3209 'text' : 'openerp.web.form.FieldText',
3210 'text_wiki' : 'openerp.web.form.FieldText',
3211 'date' : 'openerp.web.form.FieldDate',
3212 'datetime' : 'openerp.web.form.FieldDatetime',
3213 'selection' : 'openerp.web.form.FieldSelection',
3214 'many2one' : 'openerp.web.form.FieldMany2One',
3215 'many2many' : 'openerp.web.form.FieldMany2Many',
3216 'one2many' : 'openerp.web.form.FieldOne2Many',
3217 'one2many_list' : 'openerp.web.form.FieldOne2Many',
3218 'reference' : 'openerp.web.form.FieldReference',
3219 'boolean' : 'openerp.web.form.FieldBoolean',
3220 'float' : 'openerp.web.form.FieldFloat',
3221 'integer': 'openerp.web.form.FieldFloat',
3222 'float_time': 'openerp.web.form.FieldFloat',
3223 'progressbar': 'openerp.web.form.FieldProgressBar',
3224 'image': 'openerp.web.form.FieldBinaryImage',
3225 'binary': 'openerp.web.form.FieldBinaryFile',
3226 'statusbar': 'openerp.web.form.FieldStatus'
3229 openerp.web.form.FieldMany2ManyReadonly = openerp.web.form.FieldMany2Many.extend({
3230 force_readonly: true
3232 openerp.web.form.FieldOne2ManyReadonly = openerp.web.form.FieldOne2Many.extend({
3233 force_readonly: true
3235 openerp.web.form.readonly = openerp.web.form.widgets.clone({
3236 'frame': 'openerp.web.form.WidgetFrameReadonly',
3237 'char': 'openerp.web.form.FieldCharReadonly',
3238 'email': 'openerp.web.form.FieldEmailReadonly',
3239 'url': 'openerp.web.form.FieldUrlReadonly',
3240 'text': 'openerp.web.form.FieldCharReadonly',
3241 'text_wiki' : 'openerp.web.form.FieldCharReadonly',
3242 'date': 'openerp.web.form.FieldCharReadonly',
3243 'datetime': 'openerp.web.form.FieldCharReadonly',
3244 'selection' : 'openerp.web.form.FieldSelectionReadonly',
3245 'many2one': 'openerp.web.form.FieldMany2OneReadonly',
3246 'many2many' : 'openerp.web.form.FieldMany2ManyReadonly',
3247 'one2many' : 'openerp.web.form.FieldOne2ManyReadonly',
3248 'one2many_list' : 'openerp.web.form.FieldOne2ManyReadonly',
3249 'boolean': 'openerp.web.form.FieldBooleanReadonly',
3250 'float': 'openerp.web.form.FieldCharReadonly',
3251 'integer': 'openerp.web.form.FieldCharReadonly',
3252 'float_time': 'openerp.web.form.FieldCharReadonly'
3257 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: