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).
13 form_template: "FormView",
14 identifier_prefix: 'formview-',
16 * @constructs openerp.web.FormView
17 * @extends openerp.web.View
19 * @param {openerp.web.Session} session the current openerp session
20 * @param {openerp.web.DataSet} dataset the dataset this view will work with
21 * @param {String} view_id the identifier of the OpenERP view object
23 * @property {openerp.web.Registry} registry=openerp.web.form.widgets widgets registry for this form view instance
25 init: function(parent, dataset, view_id, options) {
27 this.set_default_options(options);
28 this.dataset = dataset;
29 this.model = dataset.model;
30 this.view_id = view_id || false;
31 this.fields_view = {};
33 this.widgets_counter = 0;
36 this.show_invalid = true;
37 this.dirty_for_user = false;
38 this.default_focus_field = null;
39 this.default_focus_button = null;
40 this.registry = 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.do_save);
105 this.$form_header.find('button.oe_form_button_cancel').click(this.do_cancel);
106 this.$form_header.find('button.oe_form_button_new').click(this.on_button_new);
107 this.$form_header.find('button.oe_form_button_duplicate').click(this.on_button_duplicate);
108 this.$form_header.find('button.oe_form_button_toggle').click(this.on_toggle_readonly);
110 if (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.registry = self.registry === openerp.web.form.widgets
127 ? openerp.web.form.readonly
128 : openerp.web.form.widgets;
129 self.on_loaded(self.fields_view);
132 do_show: function () {
134 if (this.dataset.index === null) {
135 // null index means we should start a new record
136 promise = this.on_button_new();
138 promise = this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded);
140 this.$element.show();
142 this.sidebar.$element.show();
146 do_hide: function () {
147 this.$element.hide();
149 this.sidebar.$element.hide();
152 on_record_loaded: function(record) {
154 throw("Form: No record received");
157 this.$form_header.find('.oe_form_on_create').show();
158 this.$form_header.find('.oe_form_on_update').hide();
159 if (!this.options["always_show_new_button"]) {
160 this.$form_header.find('button.oe_form_button_new').hide();
163 this.$form_header.find('.oe_form_on_create').hide();
164 this.$form_header.find('.oe_form_on_update').show();
165 this.$form_header.find('button.oe_form_button_new').show();
167 this.dirty_for_user = false;
168 this.datarecord = record;
169 for (var f in this.fields) {
170 var field = this.fields[f];
172 field.set_value(this.datarecord[f] || false);
176 // New record: Second pass in order to trigger the onchanges
177 this.show_invalid = false;
178 for (var f in record) {
179 var field = this.fields[f];
182 this.do_onchange(field);
186 this.on_form_changed();
187 this.initial_mutating_lock.resolve();
188 this.show_invalid = true;
189 this.do_update_pager(record.id == null);
191 this.sidebar.attachments.do_update();
192 this.sidebar.$element.find('.oe_sidebar_translate').toggleClass('oe_hide', !record.id);
194 if (this.default_focus_field && !this.embedded_view) {
195 this.default_focus_field.focus();
198 on_form_changed: function() {
199 for (var w in this.widgets) {
201 w.process_modifiers();
205 on_pager_action: function(action) {
206 if (this.can_be_discarded()) {
209 this.dataset.index = 0;
212 this.dataset.previous();
218 this.dataset.index = this.dataset.ids.length - 1;
224 do_update_pager: function(hide_index) {
225 var $pager = this.$form_header.find('div.oe_form_pager');
226 var index = hide_index ? '-' : this.dataset.index + 1;
227 $pager.find('span.oe_pager_index').html(index);
228 $pager.find('span.oe_pager_count').html(this.dataset.ids.length);
230 do_onchange: function(widget, processed) {
232 var act = function() {
234 processed = processed || [];
235 if (widget.node.attrs.on_change) {
236 var onchange = _.trim(widget.node.attrs.on_change);
237 var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
239 var method = call[1], args = [];
240 var context_index = null;
241 var argument_replacement = {
242 'False' : function() {return false;},
243 'True' : function() {return true;},
244 'None' : function() {return null;},
245 'context': function(i) {
247 var ctx = widget.build_context ? widget.build_context() : {};
251 var parent_fields = null;
252 _.each(call[2].split(','), function(a, i) {
253 var field = _.trim(a);
254 if (field in argument_replacement) {
255 args.push(argument_replacement[field](i));
257 } else if (self.fields[field]) {
258 var value = self.fields[field].get_on_change_value();
259 args.push(value == null ? false : value);
262 var splitted = field.split('.');
263 if (splitted.length > 1 && _.trim(splitted[0]) === "parent" && self.dataset.parent_view) {
264 if (parent_fields === null) {
265 parent_fields = self.dataset.parent_view.get_fields_values();
267 var p_val = parent_fields[_.trim(splitted[1])];
268 if (p_val !== undefined) {
269 args.push(p_val == null ? false : p_val);
274 throw "Could not get field with name '" + field +
275 "' for onchange '" + onchange + "'";
278 url: '/web/dataset/call',
281 return self.rpc(ajax, {
282 model: self.dataset.model,
284 args: [(self.datarecord.id == null ? [] : [self.datarecord.id])].concat(args),
285 context_id: context_index === null ? null : context_index + 1
286 }).pipe(function(response) {
287 return self.on_processed_onchange(response, processed);
290 console.log("Wrong on_change format", on_change);
295 return $.Deferred().reject();
298 this.on_change_lock = this.on_change_lock.pipe(act, act);
299 return this.on_change_lock;
301 on_processed_onchange: function(response, processed) {
303 var result = response;
305 for (var f in result.value) {
306 var field = this.fields[f];
307 // If field is not defined in the view, just ignore it
309 var value = result.value[f];
310 processed.push(field.name);
311 if (field.get_value() != value) {
312 field.set_value(value);
313 field.dirty = this.dirty_for_user = true;
314 if (_.indexOf(processed, field.name) < 0) {
315 this.do_onchange(field, processed);
320 this.on_form_changed();
322 if (!_.isEmpty(result.warning)) {
323 $(QWeb.render("DialogWarning", result.warning)).dialog({
327 $(this).dialog("close");
335 return $.Deferred().resolve();
338 return $.Deferred().reject();
341 on_button_new: function() {
343 var def = $.Deferred();
344 $.when(this.has_been_loaded).then(function() {
345 if (self.can_be_discarded()) {
346 var keys = _.keys(self.fields_view.fields);
348 self.dataset.default_get(keys).then(self.on_record_loaded).then(function() {
352 self.on_record_loaded({});
357 return def.promise();
359 on_button_duplicate: function() {
361 var def = $.Deferred();
362 $.when(this.has_been_loaded).then(function() {
363 if (self.can_be_discarded()) {
364 self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
365 return self.on_created({ result : new_id });
371 return def.promise();
373 can_be_discarded: function() {
374 return !this.dirty_for_user || confirm(_t("Warning, the record has been modified, your changes will be discarded."));
377 * Triggers saving the form's record. Chooses between creating a new
378 * record or saving an existing one depending on whether the record
379 * already has an id property.
381 * @param {Function} success callback on save success
382 * @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)
384 do_save: function(success, prepend_on_create) {
386 var action = function() {
388 if (!self.initial_mutating_lock.isResolved() && !self.initial_mutating_lock.isRejected())
390 var form_dirty = false,
391 form_invalid = false,
393 first_invalid_field = null;
394 for (var f in self.fields) {
399 if (!first_invalid_field) {
400 first_invalid_field = f;
402 } else if (f.is_dirty()) {
404 values[f.name] = f.get_value();
408 first_invalid_field.focus();
410 return $.Deferred().reject();
412 console.log("About to save", values);
413 if (!self.datarecord.id) {
414 return self.dataset.create(values).pipe(function(r) {
415 return self.on_created(r, undefined, prepend_on_create);
418 return self.dataset.write(self.datarecord.id, values, {}).pipe(function(r) {
419 return self.on_saved(r);
425 return $.Deferred().reject();
428 this.mutating_lock = this.mutating_lock.pipe(action, action);
429 return this.mutating_lock;
431 switch_readonly: function() {
433 switch_editable: function() {
435 on_invalid: function() {
437 _.each(this.fields, function(f) {
439 msg += "<li>" + f.string + "</li>";
443 this.do_warn("The following fields are invalid :", msg);
445 on_saved: function(r, success) {
447 // should not happen in the server, but may happen for internal purpose
448 return $.Deferred().reject();
451 return $.when(r).then(success);
455 * Updates the form' dataset to contain the new record:
457 * * Adds the newly created record to the current dataset (at the end by
459 * * Selects that record (sets the dataset's index to point to the new
461 * * Updates the pager and sidebar displays
464 * @param {Function} success callback to execute after having updated the dataset
465 * @param {Boolean} [prepend_on_create=false] adds the newly created record at the beginning of the dataset instead of the end
467 on_created: function(r, success, prepend_on_create) {
469 // should not happen in the server, but may happen for internal purpose
470 return $.Deferred().reject();
472 this.datarecord.id = r.result;
473 if (!prepend_on_create) {
474 this.dataset.ids.push(this.datarecord.id);
475 this.dataset.index = this.dataset.ids.length - 1;
477 this.dataset.ids.unshift(this.datarecord.id);
478 this.dataset.index = 0;
480 this.do_update_pager();
482 this.sidebar.attachments.do_update();
484 console.debug("The record has been created with id #" + this.datarecord.id);
486 return $.when(_.extend(r, {created: true})).then(success);
489 on_action: function (action) {
490 console.debug('Executing action', action);
492 do_cancel: function () {
493 console.debug("Cancelling form");
497 var act = function() {
498 if (self.dataset.index == null || self.dataset.index < 0) {
499 return $.when(self.on_button_new());
501 return self.dataset.read_index(_.keys(self.fields_view.fields), self.on_record_loaded);
504 this.reload_lock = this.reload_lock.pipe(act, act);
505 return this.reload_lock;
507 get_fields_values: function() {
509 _.each(this.fields, function(value, key) {
510 var val = value.get_value();
515 get_selected_ids: function() {
516 var id = this.dataset.ids[this.dataset.index];
517 return id ? [id] : [];
519 recursive_save: function() {
521 return $.when(this.do_save()).pipe(function(res) {
522 if (self.dataset.parent_view)
523 return self.dataset.parent_view.recursive_save();
526 is_interactible_record: function() {
527 var id = this.datarecord.id;
529 if (this.options.not_interactible_on_create)
531 } else if (typeof(id) === "string") {
532 if(openerp.web.BufferedDataSet.virtual_id_regex.test(id))
538 openerp.web.FormDialog = openerp.web.Dialog.extend({
539 init: function(parent, options, view_id, dataset) {
540 this._super(parent, options);
541 this.dataset = dataset;
542 this.view_id = view_id;
547 this.form = new openerp.web.FormView(this, this.dataset, this.view_id, {
551 this.form.appendTo(this.$element);
552 this.form.on_created.add_last(this.on_form_dialog_saved);
553 this.form.on_saved.add_last(this.on_form_dialog_saved);
556 select_id: function(id) {
557 if (this.form.dataset.select_id(id)) {
558 return this.form.do_show();
560 this.do_warn("Could not find id in dataset");
561 return $.Deferred().reject();
564 on_form_dialog_saved: function(r) {
570 openerp.web.form = {};
572 openerp.web.form.SidebarAttachments = openerp.web.Widget.extend({
573 init: function(parent, form_view) {
574 var $section = parent.add_section(_t('Attachments'), 'attachments');
575 this.$div = $('<div class="oe-sidebar-attachments"></div>');
576 $section.append(this.$div);
578 this._super(parent, $section.attr('id'));
579 this.view = form_view;
581 do_update: function() {
582 if (!this.view.datarecord.id) {
583 this.on_attachments_loaded([]);
585 (new openerp.web.DataSetSearch(
586 this, 'ir.attachment', this.view.dataset.get_context(),
588 ['res_model', '=', this.view.dataset.model],
589 ['res_id', '=', this.view.datarecord.id],
590 ['type', 'in', ['binary', 'url']]
591 ])).read_slice(['name', 'url', 'type'], {}, this.on_attachments_loaded);
594 on_attachments_loaded: function(attachments) {
595 this.attachments = attachments;
596 this.$div.html(QWeb.render('FormView.sidebar.attachments', this));
597 this.$element.find('.oe-binary-file').change(this.on_attachment_changed);
598 this.$element.find('.oe-sidebar-attachment-delete').click(this.on_attachment_delete);
600 on_attachment_changed: function(e) {
601 window[this.element_id + '_iframe'] = this.do_update;
602 var $e = $(e.target);
603 if ($e.val() != '') {
604 this.$element.find('form.oe-binary-form').submit();
605 $e.parent().find('input[type=file]').attr('disabled', 'true');
606 $e.parent().find('button').attr('disabled', 'true').find('img, span').toggle();
609 on_attachment_delete: function(e) {
610 var self = this, $e = $(e.currentTarget);
611 var name = _.trim($e.parent().find('a.oe-sidebar-attachments-link').text());
612 if (confirm("Do you really want to delete the attachment " + name + " ?")) {
613 this.rpc('/web/dataset/unlink', {
614 model: 'ir.attachment',
615 ids: [parseInt($e.attr('data-id'))]
617 $e.parent().remove();
618 self.do_notify("Delete an attachment", "The attachment '" + name + "' has been deleted");
624 openerp.web.form.compute_domain = function(expr, fields) {
626 for (var i = expr.length - 1; i >= 0; i--) {
628 if (ex.length == 1) {
629 var top = stack.pop();
632 stack.push(stack.pop() || top);
635 stack.push(stack.pop() && top);
641 throw new Error('Unknown domain operator ' + ex);
645 var field = fields[ex[0]];
647 throw new Error("Domain references unknown field : " + ex[0]);
649 var field_value = field.get_value ? fields[ex[0]].get_value() : fields[ex[0]].value;
653 switch (op.toLowerCase()) {
656 stack.push(field_value == val);
660 stack.push(field_value != val);
663 stack.push(field_value < val);
666 stack.push(field_value > val);
669 stack.push(field_value <= val);
672 stack.push(field_value >= val);
675 stack.push(_(val).contains(field_value));
678 stack.push(!_(val).contains(field_value));
681 console.log("Unsupported operator in modifiers :", op);
684 return _.all(stack, _.identity);
687 openerp.web.form.Widget = openerp.web.Widget.extend(/** @lends openerp.web.form.Widget# */{
689 identifier_prefix: 'formview-widget-',
691 * @constructs openerp.web.form.Widget
692 * @extends openerp.web.Widget
697 init: function(view, node) {
700 this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
701 this.always_invisible = (this.modifiers.invisible && this.modifiers.invisible === true);
702 this.type = this.type || node.tag;
703 this.element_name = this.element_name || this.type;
704 this.element_class = [
705 'formview', this.view.view_id, this.element_name,
706 this.view.widgets_counter++].join("_");
710 this.view.widgets[this.element_class] = this;
711 this.children = node.children;
712 this.colspan = parseInt(node.attrs.colspan || 1, 10);
713 this.decrease_max_width = 0;
715 this.string = this.string || node.attrs.string;
716 this.help = this.help || node.attrs.help;
717 this.invisible = this.modifiers['invisible'] === true;
718 this.classname = 'oe_form_' + this.type;
720 this.align = parseFloat(this.node.attrs.align);
721 if (isNaN(this.align) || this.align === 1) {
722 this.align = 'right';
723 } else if (this.align === 0) {
726 this.align = 'center';
730 this.width = this.node.attrs.width;
733 this.$element = this.view.$element.find(
734 '.' + this.element_class.replace(/[^\r\n\f0-9A-Za-z_-]/g, "\\$&"));
736 process_modifiers: function() {
737 var compute_domain = openerp.web.form.compute_domain;
738 for (var a in this.modifiers) {
739 this[a] = compute_domain(this.modifiers[a], this.view.fields);
742 update_dom: function() {
743 this.$element.toggle(!this.invisible);
746 var template = this.template;
747 return QWeb.render(template, { "widget": this });
749 _build_view_fields_values: function() {
750 var a_dataset = this.view.dataset;
751 var fields_values = this.view.get_fields_values();
752 var parent_values = a_dataset.parent_view ? a_dataset.parent_view.get_fields_values() : {};
753 fields_values.parent = parent_values;
754 return fields_values;
756 _build_eval_context: function() {
757 var a_dataset = this.view.dataset;
758 return new openerp.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
761 * Builds a new context usable for operations related to fields by merging
762 * the fields'context with the action's context.
764 build_context: function() {
765 var f_context = (this.field || {}).context || {};
766 if (!!f_context.__ref) {
767 var fields_values = this._build_eval_context();
768 f_context = new openerp.web.CompoundDomain(f_context).set_eval_context(fields_values);
770 // maybe the default_get should only be used when we do a default_get?
771 var v_contexts = _.compact([this.node.attrs.default_get || null,
772 this.node.attrs.context || null]);
773 var v_context = new openerp.web.CompoundContext();
774 _.each(v_contexts, function(x) {v_context.add(x);});
775 if (_.detect(v_contexts, function(x) {return !!x.__ref;})) {
776 var fields_values = this._build_eval_context();
777 v_context.set_eval_context(fields_values);
779 // if there is a context on the node, overrides the model's context
780 var ctx = v_contexts.length > 0 ? v_context : f_context;
783 build_domain: function() {
784 var f_domain = this.field.domain || [];
785 var n_domain = this.node.attrs.domain || null;
786 // if there is a domain on the node, overrides the model's domain
787 var final_domain = n_domain !== null ? n_domain : f_domain;
788 if (!(final_domain instanceof Array)) {
789 var fields_values = this._build_eval_context();
790 final_domain = new openerp.web.CompoundDomain(final_domain).set_eval_context(fields_values);
796 openerp.web.form.WidgetFrame = openerp.web.form.Widget.extend({
797 template: 'WidgetFrame',
798 init: function(view, node) {
799 this._super(view, node);
800 this.columns = parseInt(node.attrs.col || 4, 10);
805 for (var i = 0; i < node.children.length; i++) {
806 var n = node.children[i];
807 if (n.tag == "newline") {
813 this.set_row_cells_with(this.table[this.table.length - 1]);
816 if (this.table.length) {
817 this.set_row_cells_with(this.table[this.table.length - 1]);
820 this.table.push(row);
825 set_row_cells_with: function(row) {
828 row_length = row.length;
829 for (var i = 0; i < row.length; i++) {
830 if (row[i].always_invisible) {
833 bypass += row[i].width === undefined ? 0 : 1;
834 max_width -= row[i].decrease_max_width;
837 var size_unit = Math.round(max_width / (this.columns - bypass)),
839 for (var i = 0; i < row.length; i++) {
841 if (w.always_invisible) {
844 colspan_sum += w.colspan;
845 if (w.width === undefined) {
846 var width = (i === row_length - 1 && colspan_sum === this.columns) ? max_width : Math.round(size_unit * w.colspan);
848 w.width = width + '%';
852 handle_node: function(node) {
854 if (node.tag == 'field') {
855 type = this.view.fields_view.fields[node.attrs.name] || {};
856 if (node.attrs.widget == 'statusbar') {
857 // This way we can retain backward compatibility between addons and old clients
858 node.attrs.nolabel = '1';
861 var widget = new (this.view.registry.get_any(
862 [node.attrs.widget, type.type, node.tag])) (this.view, node);
863 if (node.tag == 'field') {
864 if (!this.view.default_focus_field || node.attrs.default_focus == '1') {
865 this.view.default_focus_field = widget;
867 if (node.attrs.nolabel != '1') {
868 var label = new (this.view.registry.get_object('label')) (this.view, node);
869 label["for"] = widget;
870 this.add_widget(label, widget.colspan + 1);
873 this.add_widget(widget);
875 add_widget: function(widget, colspan) {
876 var current_row = this.table[this.table.length - 1];
877 if (!widget.always_invisible) {
878 colspan = colspan || widget.colspan;
879 if (current_row.length && (this.x + colspan) > this.columns) {
880 current_row = this.add_row();
882 this.x += widget.colspan;
884 current_row.push(widget);
889 openerp.web.form.WidgetNotebook = openerp.web.form.Widget.extend({
890 template: 'WidgetNotebook',
891 init: function(view, node) {
892 this._super(view, node);
894 for (var i = 0; i < node.children.length; i++) {
895 var n = node.children[i];
896 if (n.tag == "page") {
897 var page = new (this.view.registry.get_object('notebookpage'))(
898 this.view, n, this, this.pages.length);
899 this.pages.push(page);
905 this._super.apply(this, arguments);
906 this.$element.find('> ul > li').each(function (index, tab_li) {
907 var page = self.pages[index],
908 id = _.uniqueId(self.element_name + '-');
909 page.element_id = id;
910 $(tab_li).find('a').attr('href', '#' + id);
912 this.$element.find('> div').each(function (index, page) {
913 page.id = self.pages[index].element_id;
915 this.$element.tabs();
916 this.view.on_button_new.add_first(this.do_select_first_visible_tab);
918 do_select_first_visible_tab: function() {
919 for (var i = 0; i < this.pages.length; i++) {
920 var page = this.pages[i];
921 if (page.invisible === false) {
922 this.$element.tabs('select', page.index);
929 openerp.web.form.WidgetNotebookPage = openerp.web.form.WidgetFrame.extend({
930 template: 'WidgetNotebookPage',
931 init: function(view, node, notebook, index) {
932 this.notebook = notebook;
934 this.element_name = 'page_' + index;
935 this._super(view, node);
938 this._super.apply(this, arguments);
939 this.$element_tab = this.notebook.$element.find(
940 '> ul > li:eq(' + this.index + ')');
942 update_dom: function() {
943 if (this.invisible && this.index === this.notebook.$element.tabs('option', 'selected')) {
944 this.notebook.do_select_first_visible_tab();
946 this.$element_tab.toggle(!this.invisible);
947 this.$element.toggle(!this.invisible);
951 openerp.web.form.WidgetSeparator = openerp.web.form.Widget.extend({
952 template: 'WidgetSeparator',
953 init: function(view, node) {
954 this._super(view, node);
955 this.orientation = node.attrs.orientation || 'horizontal';
956 if (this.orientation === 'vertical') {
959 this.classname += '_' + this.orientation;
963 openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({
964 template: 'WidgetButton',
965 init: function(view, node) {
966 this._super(view, node);
967 this.force_disabled = false;
969 // We don't have button key bindings in the webclient
970 this.string = this.string.replace(/_/g, '');
972 if (node.attrs.default_focus == '1') {
973 // TODO fme: provide enter key binding to widgets
974 this.view.default_focus_button = this;
978 this._super.apply(this, arguments);
979 this.$element.find("button").click(this.on_click);
981 on_click: function() {
983 this.force_disabled = true;
984 this.check_disable();
985 this.execute_action().always(function() {
986 self.force_disabled = false;
987 self.check_disable();
990 execute_action: function() {
992 var exec_action = function() {
993 if (self.node.attrs.confirm) {
994 var def = $.Deferred();
995 var dialog = $('<div>' + self.node.attrs.confirm + '</div>').dialog({
1000 self.on_confirmed().then(function() {
1003 $(this).dialog("close");
1005 Cancel: function() {
1007 $(this).dialog("close");
1011 return def.promise();
1013 return self.on_confirmed();
1016 if (!this.node.attrs.special) {
1017 return this.view.recursive_save().pipe(exec_action);
1019 return exec_action();
1022 on_confirmed: function() {
1025 var context = this.node.attrs.context;
1026 if (context && context.__ref) {
1027 context = new openerp.web.CompoundContext(context);
1028 context.set_eval_context(this._build_eval_context());
1031 return this.view.do_execute_action(
1032 _.extend({}, this.node.attrs, {context: context}),
1033 this.view.dataset, this.view.datarecord.id, function () {
1037 update_dom: function() {
1039 this.check_disable();
1041 check_disable: function() {
1042 if (this.readonly || this.force_disabled || !this.view.is_interactible_record()) {
1043 this.$element.find("button").attr("disabled", "disabled");
1044 this.$element.find("button").css("color", "grey");
1046 this.$element.find("button").removeAttr("disabled");
1047 this.$element.find("button").css("color", "");
1052 openerp.web.form.WidgetLabel = openerp.web.form.Widget.extend({
1053 template: 'WidgetLabel',
1054 init: function(view, node) {
1055 this.element_name = 'label_' + node.attrs.name;
1057 this._super(view, node);
1059 if (this.node.tag == 'label' && (this.align === 'left' || this.node.attrs.colspan || (this.string && this.string.length > 32))) {
1060 this.template = "WidgetParagraph";
1061 this.colspan = parseInt(this.node.attrs.colspan || 1, 10);
1065 this.decrease_max_width = 1;
1069 render: function () {
1070 if (this['for'] && this.type !== 'label') {
1071 return QWeb.render(this.template, {widget: this['for']});
1073 // Actual label widgets should not have a false and have type label
1074 return QWeb.render(this.template, {widget: this});
1079 this.$element.find("label").dblclick(function() {
1080 var widget = self['for'] || self;
1081 console.log(widget.element_class , widget);
1087 openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.form.Field# */{
1089 * @constructs openerp.web.form.Field
1090 * @extends openerp.web.form.Widget
1095 init: function(view, node) {
1096 this.name = node.attrs.name;
1097 this.value = undefined;
1098 view.fields[this.name] = this;
1099 this.type = node.attrs.widget || view.fields_view.fields[node.attrs.name].type;
1100 this.element_name = "field_" + this.name + "_" + this.type;
1102 this._super(view, node);
1104 if (node.attrs.nolabel != '1' && this.colspan > 1) {
1107 this.field = view.fields_view.fields[node.attrs.name] || {};
1108 this.string = node.attrs.string || this.field.string;
1109 this.help = node.attrs.help || this.field.help;
1110 this.nolabel = (this.field.nolabel || node.attrs.nolabel) === '1';
1111 this.readonly = this.modifiers['readonly'] === true;
1112 this.required = this.modifiers['required'] === true;
1113 this.invalid = false;
1116 this.classname = 'oe_form_field_' + this.type;
1119 this._super.apply(this, arguments);
1120 if (this.field.translate) {
1121 this.view.translatable_fields.push(this);
1122 this.$element.find('.oe_field_translate').click(this.on_translate);
1125 set_value: function(value) {
1127 this.invalid = false;
1129 this.on_value_changed();
1131 set_value_from_ui: function() {
1132 this.on_value_changed();
1134 on_value_changed: function() {
1136 on_translate: function() {
1137 this.view.open_translate_dialog(this);
1139 get_value: function() {
1142 is_valid: function() {
1143 return !this.invalid;
1145 is_dirty: function() {
1146 return this.dirty && !this.readonly;
1148 get_on_change_value: function() {
1149 return this.get_value();
1151 update_dom: function() {
1152 this._super.apply(this, arguments);
1153 if (this.field.translate) {
1154 this.$element.find('.oe_field_translate').toggle(!!this.view.datarecord.id);
1156 if (!this.disable_utility_classes) {
1157 this.$element.toggleClass('disabled', this.readonly);
1158 this.$element.toggleClass('required', this.required);
1159 if (this.view.show_invalid) {
1160 this.$element.toggleClass('invalid', !this.is_valid());
1164 on_ui_change: function() {
1165 this.dirty = this.view.dirty_for_user = true;
1167 if (this.is_valid()) {
1168 this.set_value_from_ui();
1169 this.view.do_onchange(this);
1170 this.view.on_form_changed();
1175 validate: function() {
1176 this.invalid = false;
1182 openerp.web.form.FieldChar = openerp.web.form.Field.extend({
1183 template: 'FieldChar',
1184 init: function (view, node) {
1185 this._super(view, node);
1186 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
1189 this._super.apply(this, arguments);
1190 this.$element.find('input').change(this.on_ui_change);
1192 set_value: function(value) {
1193 this._super.apply(this, arguments);
1194 var show_value = openerp.web.format_value(value, this, '');
1195 this.$element.find('input').val(show_value);
1198 update_dom: function() {
1199 this._super.apply(this, arguments);
1200 this.$element.find('input').attr('disabled', this.readonly);
1202 set_value_from_ui: function() {
1203 this.value = openerp.web.parse_value(this.$element.find('input').val(), this);
1206 validate: function() {
1207 this.invalid = false;
1209 var value = openerp.web.parse_value(this.$element.find('input').val(), this, '');
1210 this.invalid = this.required && value === '';
1212 this.invalid = true;
1216 this.$element.find('input').focus();
1220 openerp.web.form.FieldEmail = openerp.web.form.FieldChar.extend({
1221 template: 'FieldEmail',
1223 this._super.apply(this, arguments);
1224 this.$element.find('button').click(this.on_button_clicked);
1226 on_button_clicked: function() {
1227 if (!this.value || !this.is_valid()) {
1228 this.do_warn("E-mail error", "Can't send email to invalid e-mail address");
1230 location.href = 'mailto:' + this.value;
1235 openerp.web.form.FieldUrl = openerp.web.form.FieldChar.extend({
1236 template: 'FieldUrl',
1238 this._super.apply(this, arguments);
1239 this.$element.find('button').click(this.on_button_clicked);
1241 on_button_clicked: function() {
1243 this.do_warn("Resource error", "This resource is empty");
1245 window.open(this.value);
1250 openerp.web.form.FieldFloat = openerp.web.form.FieldChar.extend({
1251 init: function (view, node) {
1252 this._super(view, node);
1253 if (node.attrs.digits) {
1254 this.parse_digits(node.attrs.digits);
1256 this.digits = view.fields_view.fields[node.attrs.name].digits;
1259 parse_digits: function (digits_attr) {
1260 // could use a Python parser instead.
1261 var match = /^\s*[\(\[](\d+),\s*(\d+)/.exec(digits_attr);
1262 return [parseInt(match[1], 10), parseInt(match[2], 10)];
1264 set_value: function(value) {
1265 if (value === false || value === undefined) {
1266 // As in GTK client, floats default to 0
1270 this._super.apply(this, [value]);
1274 openerp.web.DateTimeWidget = openerp.web.Widget.extend({
1275 template: "web.datetimepicker",
1276 jqueryui_object: 'datetimepicker',
1277 type_of_date: "datetime",
1280 this.$element.find('input').change(this.on_change);
1282 onSelect: this.on_picker_select,
1286 showButtonPanel: false
1288 this.$element.find('img.oe_datepicker_trigger').click(function() {
1289 if (!self.readonly) {
1290 self.picker('setDate', self.value ? openerp.web.auto_str_to_date(self.value) : new Date());
1291 self.$element.find('.oe_datepicker').toggle();
1294 this.$element.find('.ui-datepicker-inline').removeClass('ui-widget-content ui-corner-all');
1295 this.$element.find('button.oe_datepicker_close').click(function() {
1296 self.$element.find('.oe_datepicker').hide();
1298 this.set_readonly(false);
1301 picker: function() {
1302 return $.fn[this.jqueryui_object].apply(this.$element.find('.oe_datepicker_container'), arguments);
1304 on_picker_select: function(text, instance) {
1305 var date = this.picker('getDate');
1306 this.$element.find('input').val(date ? this.format_client(date) : '').change();
1308 set_value: function(value) {
1310 this.$element.find('input').val(value ? this.format_client(value) : '');
1312 get_value: function() {
1315 set_value_from_ui: function() {
1316 var value = this.$element.find('input').val() || false;
1317 this.value = this.parse_client(value);
1319 set_readonly: function(readonly) {
1320 this.readonly = readonly;
1321 this.$element.find('input').attr('disabled', this.readonly);
1322 this.$element.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
1324 is_valid: function(required) {
1325 var value = this.$element.find('input').val();
1330 this.parse_client(value);
1338 this.$element.find('input').focus();
1340 parse_client: function(v) {
1341 return openerp.web.parse_value(v, {"widget": this.type_of_date});
1343 format_client: function(v) {
1344 return openerp.web.format_value(v, {"widget": this.type_of_date});
1346 on_change: function() {
1347 if (this.is_valid()) {
1348 this.set_value_from_ui();
1353 openerp.web.DateWidget = openerp.web.DateTimeWidget.extend({
1354 jqueryui_object: 'datepicker',
1355 type_of_date: "date",
1356 on_picker_select: function(text, instance) {
1357 this._super(text, instance);
1358 this.$element.find('.oe_datepicker').hide();
1362 openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({
1363 template: "EmptyComponent",
1364 build_widget: function() {
1365 return new openerp.web.DateTimeWidget(this);
1369 this._super.apply(this, arguments);
1370 this.datewidget = this.build_widget();
1371 this.datewidget.on_change.add(this.on_ui_change);
1372 this.datewidget.appendTo(this.$element);
1374 set_value: function(value) {
1376 this.datewidget.set_value(value);
1378 get_value: function() {
1379 return this.datewidget.get_value();
1381 update_dom: function() {
1382 this._super.apply(this, arguments);
1383 this.datewidget.set_readonly(this.readonly);
1385 validate: function() {
1386 this.invalid = !this.datewidget.is_valid(this.required);
1389 this.datewidget.focus();
1393 openerp.web.form.FieldDate = openerp.web.form.FieldDatetime.extend({
1394 build_widget: function() {
1395 return new openerp.web.DateWidget(this);
1399 openerp.web.form.FieldText = openerp.web.form.Field.extend({
1400 template: 'FieldText',
1402 this._super.apply(this, arguments);
1403 this.$element.find('textarea').change(this.on_ui_change);
1405 set_value: function(value) {
1406 this._super.apply(this, arguments);
1407 var show_value = openerp.web.format_value(value, this, '');
1408 this.$element.find('textarea').val(show_value);
1410 update_dom: function() {
1411 this._super.apply(this, arguments);
1412 this.$element.find('textarea').attr('disabled', this.readonly);
1414 set_value_from_ui: function() {
1415 this.value = openerp.web.parse_value(this.$element.find('textarea').val(), this);
1418 validate: function() {
1419 this.invalid = false;
1421 var value = openerp.web.parse_value(this.$element.find('textarea').val(), this, '');
1422 this.invalid = this.required && value === '';
1424 this.invalid = true;
1428 this.$element.find('textarea').focus();
1432 openerp.web.form.FieldBoolean = openerp.web.form.Field.extend({
1433 template: 'FieldBoolean',
1436 this._super.apply(this, arguments);
1437 this.$element.find('input').click(self.on_ui_change);
1439 set_value: function(value) {
1440 this._super.apply(this, arguments);
1441 this.$element.find('input')[0].checked = value;
1443 set_value_from_ui: function() {
1444 this.value = this.$element.find('input').is(':checked');
1447 update_dom: function() {
1448 this._super.apply(this, arguments);
1449 this.$element.find('input').attr('disabled', this.readonly);
1452 this.$element.find('input').focus();
1456 openerp.web.form.FieldProgressBar = openerp.web.form.Field.extend({
1457 template: 'FieldProgressBar',
1459 this._super.apply(this, arguments);
1460 this.$element.find('div').progressbar({
1462 disabled: this.readonly
1465 set_value: function(value) {
1466 this._super.apply(this, arguments);
1467 var show_value = Number(value);
1468 if (isNaN(show_value)) {
1471 this.$element.find('div').progressbar('option', 'value', show_value).find('span').html(show_value + '%');
1475 openerp.web.form.FieldTextXml = openerp.web.form.Field.extend({
1476 // to replace view editor
1479 openerp.web.form.FieldSelection = openerp.web.form.Field.extend({
1480 template: 'FieldSelection',
1481 init: function(view, node) {
1483 this._super(view, node);
1484 this.values = this.field.selection;
1485 _.each(this.values, function(v, i) {
1486 if (v[0] === false && v[1] === '') {
1487 self.values.splice(i, 1);
1490 this.values.unshift([false, '']);
1493 // Flag indicating whether we're in an event chain containing a change
1494 // event on the select, in order to know what to do on keyup[RETURN]:
1495 // * If the user presses [RETURN] as part of changing the value of a
1496 // selection, we should just let the value change and not let the
1497 // event broadcast further (e.g. to validating the current state of
1498 // the form in editable list view, which would lead to saving the
1499 // current row or switching to the next one)
1500 // * If the user presses [RETURN] with a select closed (side-effect:
1501 // also if the user opened the select and pressed [RETURN] without
1502 // changing the selected value), takes the action as validating the
1504 var ischanging = false;
1505 this._super.apply(this, arguments);
1506 this.$element.find('select')
1507 .change(this.on_ui_change)
1508 .change(function () { ischanging = true; })
1509 .click(function () { ischanging = false; })
1510 .keyup(function (e) {
1511 if (e.which !== 13 || !ischanging) { return; }
1512 e.stopPropagation();
1516 set_value: function(value) {
1517 value = value === null ? false : value;
1518 value = value instanceof Array ? value[0] : value;
1521 for (var i = 0, ii = this.values.length; i < ii; i++) {
1522 if (this.values[i][0] === value) index = i;
1524 this.$element.find('select')[0].selectedIndex = index;
1526 set_value_from_ui: function() {
1527 this.value = this.values[this.$element.find('select')[0].selectedIndex][0];
1530 update_dom: function() {
1531 this._super.apply(this, arguments);
1532 this.$element.find('select').attr('disabled', this.readonly);
1534 validate: function() {
1535 var value = this.values[this.$element.find('select')[0].selectedIndex];
1536 this.invalid = !(value && !(this.required && value[0] === false));
1539 this.$element.find('select').focus();
1543 // jquery autocomplete tweak to allow html
1545 var proto = $.ui.autocomplete.prototype,
1546 initSource = proto._initSource;
1548 function filter( array, term ) {
1549 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
1550 return $.grep( array, function(value) {
1551 return matcher.test( $( "<div>" ).html( value.label || value.value || value ).text() );
1556 _initSource: function() {
1557 if ( this.options.html && $.isArray(this.options.source) ) {
1558 this.source = function( request, response ) {
1559 response( filter( this.options.source, request.term ) );
1562 initSource.call( this );
1566 _renderItem: function( ul, item) {
1567 return $( "<li></li>" )
1568 .data( "item.autocomplete", item )
1569 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
1575 openerp.web.form.dialog = function(content, options) {
1576 options = _.extend({
1583 options.autoOpen = true;
1584 var dialog = new openerp.web.Dialog(null, options);
1585 dialog.$dialog = $(content).dialog(dialog.dialog_options);
1586 return dialog.$dialog;
1589 openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({
1590 template: 'FieldMany2One',
1591 init: function(view, node) {
1592 this._super(view, node);
1595 this.cm_id = _.uniqueId('m2o_cm_');
1596 this.last_search = [];
1597 this.tmp_value = undefined;
1602 this.$input = this.$element.find("input");
1603 this.$drop_down = this.$element.find(".oe-m2o-drop-down-button");
1604 this.$menu_btn = this.$element.find(".oe-m2o-cm-button");
1607 var init_context_menu_def = $.Deferred().then(function(e) {
1608 var rdataset = new openerp.web.DataSetStatic(self, "ir.values", self.build_context());
1609 rdataset.call("get", ['action', 'client_action_relate',
1610 [[self.field.relation, false]], false, rdataset.get_context()], false, 0)
1611 .then(function(result) {
1612 self.related_entries = result;
1614 var $cmenu = $("#" + self.cm_id);
1615 $cmenu.append(QWeb.render("FieldMany2One.context_menu", {widget: self}));
1617 bindings[self.cm_id + "_search"] = function() {
1618 self._search_create_popup("search");
1620 bindings[self.cm_id + "_create"] = function() {
1621 self._search_create_popup("form");
1623 bindings[self.cm_id + "_open"] = function() {
1627 var pop = new openerp.web.form.FormOpenPopup(self.view);
1628 pop.show_element(self.field.relation, self.value[0],self.build_context(), {});
1629 pop.on_write_completed.add_last(function() {
1630 self.set_value(self.value[0]);
1633 _.each(_.range(self.related_entries.length), function(i) {
1634 bindings[self.cm_id + "_related_" + i] = function() {
1635 self.open_related(self.related_entries[i]);
1638 var cmenu = self.$menu_btn.contextMenu(self.cm_id, {'leftClickToo': true,
1639 bindings: bindings, itemStyle: {"color": ""},
1640 onContextMenu: function() {
1642 $("#" + self.cm_id + " .oe_m2o_menu_item_mandatory").removeClass("oe-m2o-disabled-cm");
1644 $("#" + self.cm_id + " .oe_m2o_menu_item_mandatory").addClass("oe-m2o-disabled-cm");
1646 if (!self.readonly) {
1647 $("#" + self.cm_id + " .oe_m2o_menu_item_noreadonly").removeClass("oe-m2o-disabled-cm");
1649 $("#" + self.cm_id + " .oe_m2o_menu_item_noreadonly").addClass("oe-m2o-disabled-cm");
1652 }, menuStyle: {width: "200px"}
1654 setTimeout(function() {self.$menu_btn.trigger(e);}, 0);
1657 var ctx_callback = function(e) {init_context_menu_def.resolve(e); e.preventDefault()};
1658 this.$menu_btn.bind('contextmenu', ctx_callback);
1659 this.$menu_btn.click(ctx_callback);
1661 // some behavior for input
1662 this.$input.keyup(function() {
1663 if (self.$input.val() === "") {
1664 self._change_int_value(null);
1665 } else if (self.value === null || (self.value && self.$input.val() !== self.value[1])) {
1666 self._change_int_value(undefined);
1669 this.$drop_down.click(function() {
1672 if (self.$input.autocomplete("widget").is(":visible")) {
1673 self.$input.autocomplete("close");
1676 self.$input.autocomplete("search", "");
1678 self.$input.autocomplete("search");
1680 self.$input.focus();
1683 var anyoneLoosesFocus = function() {
1684 if (!self.$input.is(":focus") &&
1685 !self.$input.autocomplete("widget").is(":visible") &&
1687 if (self.value === undefined && self.last_search.length > 0) {
1688 self._change_int_ext_value(self.last_search[0]);
1690 self._change_int_ext_value(null);
1694 this.$input.focusout(anyoneLoosesFocus);
1696 var isSelecting = false;
1698 this.$input.autocomplete({
1699 source: function(req, resp) { self.get_search_result(req, resp); },
1700 select: function(event, ui) {
1704 self._change_int_value([item.id, item.name]);
1705 } else if (item.action) {
1706 self._change_int_value(undefined);
1711 focus: function(e, ui) {
1715 close: anyoneLoosesFocus,
1719 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
1720 this.$input.keyup(function(e) {
1721 if (e.which === 13) {
1723 e.stopPropagation();
1725 isSelecting = false;
1728 // autocomplete component content handling
1729 get_search_result: function(request, response) {
1730 var search_val = request.term;
1733 var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context());
1735 dataset.name_search(search_val, self.build_domain(), 'ilike',
1736 this.limit + 1, function(data) {
1737 self.last_search = data;
1738 // possible selections for the m2o
1739 var values = _.map(data, function(x) {
1740 return {label: $('<span />').text(x[1]).html(), name:x[1], id:x[0]};
1743 // search more... if more results that max
1744 if (values.length > self.limit) {
1745 values = values.slice(0, self.limit);
1746 values.push({label: _t("<em>Â Â Â Search More...</em>"), action: function() {
1747 dataset.name_search(search_val, self.build_domain(), 'ilike'
1748 , false, function(data) {
1749 self._change_int_value(null);
1750 self._search_create_popup("search", data);
1755 var raw_result = _(data.result).map(function(x) {return x[1];});
1756 if (search_val.length > 0 &&
1757 !_.include(raw_result, search_val) &&
1758 (!self.value || search_val !== self.value[1])) {
1759 values.push({label: _.sprintf(_t('<em>Â Â Â Create "<strong>%s</strong>"</em>'),
1760 $('<span />').text(search_val).html()), action: function() {
1761 self._quick_create(search_val);
1765 values.push({label: _t("<em>Â Â Â Create and Edit...</em>"), action: function() {
1766 self._change_int_value(null);
1767 self._search_create_popup("form", undefined, {"default_name": search_val});
1773 _quick_create: function(name) {
1775 var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context());
1776 dataset.name_create(name, function(data) {
1777 self._change_int_ext_value(data);
1778 }).fail(function(error, event) {
1779 event.preventDefault();
1780 self._change_int_value(null);
1781 self._search_create_popup("form", undefined, {"default_name": name});
1784 // all search/create popup handling
1785 _search_create_popup: function(view, ids, context) {
1787 var pop = new openerp.web.form.SelectCreatePopup(this);
1788 pop.select_element(self.field.relation,{
1789 initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
1791 disable_multiple_selection: true
1792 }, self.build_domain(),
1793 new openerp.web.CompoundContext(self.build_context(), context || {}));
1794 pop.on_select_elements.add(function(element_ids) {
1795 var dataset = new openerp.web.DataSetStatic(self, self.field.relation, self.build_context());
1796 dataset.name_get([element_ids[0]], function(data) {
1797 self._change_int_ext_value(data[0]);
1801 _change_int_ext_value: function(value) {
1802 this._change_int_value(value);
1803 this.$input.val(this.value ? this.value[1] : "");
1805 _change_int_value: function(value) {
1807 var back_orig_value = this.original_value;
1808 if (this.value === null || this.value) {
1809 this.original_value = this.value;
1811 if (back_orig_value === undefined) { // first use after a set_value()
1814 if (this.value !== undefined && ((back_orig_value ? back_orig_value[0] : null)
1815 !== (this.value ? this.value[0] : null))) {
1816 this.on_ui_change();
1819 set_value: function(value) {
1820 value = value || null;
1821 this.invalid = false;
1823 this.tmp_value = value;
1825 self.on_value_changed();
1826 var real_set_value = function(rval) {
1827 self.tmp_value = undefined;
1829 self.original_value = undefined;
1830 self._change_int_ext_value(rval);
1832 if(!(typeof(value) instanceof Array)) {
1833 var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context());
1834 dataset.name_get([value], function(data) {
1835 real_set_value(data[0]);
1836 }).fail(function() {self.tmp_value = undefined;});
1838 setTimeout(function() {real_set_value(value);}, 0);
1841 get_value: function() {
1842 if (this.tmp_value !== undefined) {
1843 if (this.tmp_value instanceof Array) {
1844 return this.tmp_value[0];
1846 return this.tmp_value ? this.tmp_value : false;
1848 if (this.value === undefined)
1849 return this.original_value ? this.original_value[0] : false;
1850 return this.value ? this.value[0] : false;
1852 validate: function() {
1853 this.invalid = false;
1854 var val = this.tmp_value !== undefined ? this.tmp_value : this.value;
1856 this.invalid = this.required;
1859 open_related: function(related) {
1863 var additional_context = {
1864 active_id: self.value[0],
1865 active_ids: [self.value[0]],
1866 active_model: self.field.relation
1868 self.rpc("/web/action/load", {
1869 action_id: related[2].id,
1870 context: additional_context
1871 }, function(result) {
1872 result.result.context = _.extend(result.result.context || {}, additional_context);
1873 self.do_action(result.result);
1876 focus: function () {
1877 this.$input.focus();
1879 update_dom: function() {
1880 this._super.apply(this, arguments);
1881 this.$input.attr('disabled', this.readonly);
1886 # Values: (0, 0, { fields }) create
1887 # (1, ID, { fields }) update
1888 # (2, ID) remove (delete)
1889 # (3, ID) unlink one (target id or target of relation)
1891 # (5) unlink all (only valid for one2many)
1896 'create': function (values) {
1897 return [commands.CREATE, false, values];
1899 // (1, id, {values})
1901 'update': function (id, values) {
1902 return [commands.UPDATE, id, values];
1906 'delete': function (id) {
1907 return [commands.DELETE, id, false];
1909 // (3, id[, _]) removes relation, but not linked record itself
1911 'forget': function (id) {
1912 return [commands.FORGET, id, false];
1916 'link_to': function (id) {
1917 return [commands.LINK_TO, id, false];
1921 'delete_all': function () {
1922 return [5, false, false];
1924 // (6, _, ids) replaces all linked records with provided ids
1926 'replace_with': function (ids) {
1927 return [6, false, ids];
1930 openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({
1931 template: 'FieldOne2Many',
1932 multi_selection: false,
1933 init: function(view, node) {
1934 this._super(view, node);
1935 this.is_loaded = $.Deferred();
1936 this.initial_is_loaded = this.is_loaded;
1937 this.is_setted = $.Deferred();
1938 this.form_last_update = $.Deferred();
1939 this.init_form_last_update = this.form_last_update;
1940 this.disable_utility_classes = true;
1943 this._super.apply(this, arguments);
1947 this.dataset = new openerp.web.form.One2ManyDataSet(this, this.field.relation);
1948 this.dataset.o2m = this;
1949 this.dataset.parent_view = this.view;
1950 this.dataset.on_change.add_last(function() {
1951 self.on_ui_change();
1954 this.is_setted.then(function() {
1958 is_readonly: function() {
1959 return this.readonly || this.force_readonly;
1961 load_views: function() {
1964 var modes = this.node.attrs.mode;
1965 modes = !!modes ? modes.split(",") : ["tree"];
1967 _.each(modes, function(mode) {
1970 view_type: mode == "tree" ? "list" : mode,
1971 options: { sidebar : false }
1973 if (self.field.views && self.field.views[mode]) {
1974 view.embedded_view = self.field.views[mode];
1976 if(view.view_type === "list") {
1977 view.options.selectable = self.multi_selection;
1978 if (self.is_readonly()) {
1979 view.options.addable = null;
1980 view.options.deletable = null;
1982 } else if (view.view_type === "form") {
1983 view.options.not_interactible_on_create = true;
1989 this.viewmanager = new openerp.web.ViewManager(this, this.dataset, views);
1990 this.viewmanager.registry = openerp.web.views.clone({
1991 list: 'openerp.web.form.One2ManyListView',
1992 form: 'openerp.web.FormView'
1994 var once = $.Deferred().then(function() {
1995 self.init_form_last_update.resolve();
1997 var def = $.Deferred().then(function() {
1998 self.initial_is_loaded.resolve();
2000 this.viewmanager.on_controller_inited.add_last(function(view_type, controller) {
2001 if (view_type == "list") {
2002 controller.o2m = self;
2003 if (self.is_readonly())
2004 controller.set_editable(false);
2005 } else if (view_type == "form") {
2006 if (self.is_readonly()) {
2007 controller.on_toggle_readonly();
2008 $(controller.$element.find(".oe_form_buttons")[0]).children().remove();
2010 controller.on_record_loaded.add_last(function() {
2013 controller.on_pager_action.add_first(function() {
2014 self.save_form_view();
2016 controller.$element.find(".oe_form_button_save").hide();
2017 } else if (view_type == "graph") {
2018 self.reload_current_view()
2022 this.viewmanager.on_mode_switch.add_first(function(n_mode, b, c, d, e) {
2023 $.when(self.save_form_view()).then(function() {
2024 if(n_mode === "list")
2025 setTimeout(function() {self.reload_current_view();}, 0);
2028 this.is_setted.then(function() {
2029 setTimeout(function () {
2030 self.viewmanager.appendTo(self.$element);
2035 reload_current_view: function() {
2037 self.is_loaded = self.is_loaded.pipe(function() {
2038 var view = self.viewmanager.views[self.viewmanager.active_view].controller;
2039 if(self.viewmanager.active_view === "list") {
2040 return view.reload_content();
2041 } else if (self.viewmanager.active_view === "form") {
2042 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
2043 self.dataset.index = 0;
2045 var act = function() {
2046 return view.do_show();
2048 self.form_last_update = self.form_last_update.pipe(act, act);
2049 return self.form_last_update;
2050 } else if (self.viewmanager.active_view === "graph") {
2051 return view.do_search(self.build_domain(), self.dataset.get_context(), []);
2055 set_value: function(value) {
2056 value = value || [];
2058 this.dataset.reset_ids([]);
2059 if(value.length >= 1 && value[0] instanceof Array) {
2061 _.each(value, function(command) {
2062 var obj = {values: command[2]};
2063 switch (command[0]) {
2064 case commands.CREATE:
2065 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
2067 self.dataset.to_create.push(obj);
2068 self.dataset.cache.push(_.clone(obj));
2071 case commands.UPDATE:
2072 obj['id'] = command[1];
2073 self.dataset.to_write.push(obj);
2074 self.dataset.cache.push(_.clone(obj));
2077 case commands.DELETE:
2078 self.dataset.to_delete.push({id: command[1]});
2080 case commands.LINK_TO:
2081 ids.push(command[1]);
2083 case commands.DELETE_ALL:
2084 self.dataset.delete_all = true;
2089 this.dataset.set_ids(ids);
2090 } else if (value.length >= 1 && typeof(value[0]) === "object") {
2092 this.dataset.delete_all = true;
2093 _.each(value, function(command) {
2094 var obj = {values: command};
2095 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
2097 self.dataset.to_create.push(obj);
2098 self.dataset.cache.push(_.clone(obj));
2102 this.dataset.set_ids(ids);
2105 this.dataset.reset_ids(value);
2107 if (this.dataset.index === null && this.dataset.ids.length > 0) {
2108 this.dataset.index = 0;
2110 self.reload_current_view();
2111 this.is_setted.resolve();
2113 get_value: function() {
2117 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
2118 val = val.concat(_.map(this.dataset.ids, function(id) {
2119 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
2121 return commands.create(alter_order.values);
2123 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
2125 return commands.update(alter_order.id, alter_order.values);
2127 return commands.link_to(id);
2129 return val.concat(_.map(
2130 this.dataset.to_delete, function(x) {
2131 return commands['delete'](x.id);}));
2133 save_form_view: function() {
2134 if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
2135 this.viewmanager.views[this.viewmanager.active_view] &&
2136 this.viewmanager.views[this.viewmanager.active_view].controller) {
2137 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
2138 if (this.viewmanager.active_view === "form") {
2139 var res = $.when(view.do_save());
2140 // it seems line there are some cases when this happens
2141 /*if (!res.isResolved() && !res.isRejected()) {
2142 console.warn("Asynchronous get_value() is not supported in form view.");
2149 is_valid: function() {
2151 return this._super();
2153 validate: function() {
2154 this.invalid = false;
2155 if (!this.viewmanager.views[this.viewmanager.active_view])
2157 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
2158 if (this.viewmanager.active_view === "form") {
2159 for (var f in view.fields) {
2161 if (!f.is_valid()) {
2162 this.invalid = true;
2168 is_dirty: function() {
2169 this.save_form_view();
2170 return this._super();
2172 update_dom: function() {
2173 this._super.apply(this, arguments);
2175 if (this.previous_readonly !== this.readonly) {
2176 this.previous_readonly = this.readonly;
2177 if (this.viewmanager) {
2178 this.is_loaded = this.is_loaded.pipe(function() {
2179 self.viewmanager.stop();
2180 return $.when(self.load_views()).then(function() {
2181 self.reload_current_view();
2189 openerp.web.form.One2ManyDataSet = openerp.web.BufferedDataSet.extend({
2190 get_context: function() {
2191 this.context = this.o2m.build_context();
2192 return this.context;
2196 openerp.web.form.One2ManyListView = openerp.web.ListView.extend({
2197 do_add_record: function () {
2198 if (this.options.editable) {
2199 this._super.apply(this, arguments);
2202 var pop = new openerp.web.form.SelectCreatePopup(this);
2203 pop.on_default_get.add(self.dataset.on_default_get);
2204 pop.select_element(self.o2m.field.relation,{
2205 initial_view: "form",
2206 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
2207 create_function: function(data, callback, error_callback) {
2208 return self.o2m.dataset.create(data).then(function(r) {
2209 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r.result]));
2210 self.o2m.dataset.on_change();
2211 }).then(callback, error_callback);
2213 read_function: function() {
2214 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
2216 parent_view: self.o2m.view,
2217 form_view_options: {'not_interactible_on_create':true}
2218 }, self.o2m.build_domain(), self.o2m.build_context());
2219 pop.on_select_elements.add_last(function() {
2220 self.o2m.reload_current_view();
2224 do_activate_record: function(index, id) {
2226 var pop = new openerp.web.form.FormOpenPopup(self.o2m.view);
2227 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(),{
2229 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
2230 parent_view: self.o2m.view,
2231 read_function: function() {
2232 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
2234 form_view_options: {'not_interactible_on_create':true},
2235 readonly: self.o2m.is_readonly()
2237 pop.on_write.add(function(id, data) {
2238 self.o2m.dataset.write(id, data, {}, function(r) {
2239 self.o2m.reload_current_view();
2245 openerp.web.form.FieldMany2Many = openerp.web.form.Field.extend({
2246 template: 'FieldMany2Many',
2247 multi_selection: false,
2248 init: function(view, node) {
2249 this._super(view, node);
2250 this.list_id = _.uniqueId("many2many");
2251 this.is_loaded = $.Deferred();
2252 this.initial_is_loaded = this.is_loaded;
2253 this.is_setted = $.Deferred();
2256 this._super.apply(this, arguments);
2260 this.dataset = new openerp.web.form.Many2ManyDataSet(this, this.field.relation);
2261 this.dataset.m2m = this;
2262 this.dataset.on_unlink.add_last(function(ids) {
2263 self.on_ui_change();
2266 this.is_setted.then(function() {
2270 set_value: function(value) {
2271 value = value || [];
2272 if (value.length >= 1 && value[0] instanceof Array) {
2273 value = value[0][2];
2276 this.dataset.set_ids(value);
2278 self.reload_content();
2279 this.is_setted.resolve();
2281 get_value: function() {
2282 return [commands.replace_with(this.dataset.ids)];
2284 validate: function() {
2285 this.invalid = false;
2287 is_readonly: function() {
2288 return this.readonly || this.force_readonly;
2290 load_view: function() {
2292 this.list_view = new openerp.web.form.Many2ManyListView(this, this.dataset, false, {
2293 'addable': self.is_readonly() ? null : 'Add',
2294 'deletable': self.is_readonly() ? false : true,
2295 'selectable': self.multi_selection
2297 this.list_view.m2m_field = this;
2298 var loaded = $.Deferred();
2299 this.list_view.on_loaded.add_last(function() {
2300 self.initial_is_loaded.resolve();
2303 setTimeout(function () {
2304 self.list_view.appendTo($("#" + self.list_id));
2308 reload_content: function() {
2310 this.is_loaded = this.is_loaded.pipe(function() {
2311 return self.list_view.reload_content();
2314 update_dom: function() {
2315 this._super.apply(this, arguments);
2317 if (this.previous_readonly !== this.readonly) {
2318 this.previous_readonly = this.readonly;
2319 if (this.list_view) {
2320 this.is_loaded = this.is_loaded.pipe(function() {
2321 self.list_view.stop();
2322 return $.when(self.load_view()).then(function() {
2323 self.reload_content();
2331 openerp.web.form.Many2ManyDataSet = openerp.web.DataSetStatic.extend({
2332 get_context: function() {
2333 this.context = this.m2m.build_context();
2334 return this.context;
2340 * @extends openerp.web.ListView
2342 openerp.web.form.Many2ManyListView = openerp.web.ListView.extend(/** @lends openerp.web.form.Many2ManyListView# */{
2343 do_add_record: function () {
2344 var pop = new openerp.web.form.SelectCreatePopup(this);
2345 pop.select_element(this.model, {},
2346 new openerp.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
2347 this.m2m_field.build_context());
2349 pop.on_select_elements.add(function(element_ids) {
2350 _.each(element_ids, function(element_id) {
2351 if(! _.detect(self.dataset.ids, function(x) {return x == element_id;})) {
2352 self.dataset.set_ids([].concat(self.dataset.ids, [element_id]));
2353 self.m2m_field.on_ui_change();
2354 self.reload_content();
2359 do_activate_record: function(index, id) {
2361 var pop = new openerp.web.form.FormOpenPopup(this);
2362 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {});
2363 pop.on_write_completed.add_last(function() {
2364 self.reload_content();
2371 * @extends openerp.web.OldWidget
2373 openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends openerp.web.form.SelectCreatePopup# */{
2374 identifier_prefix: "selectcreatepopup",
2375 template: "SelectCreatePopup",
2379 * - initial_view: form or search (default search)
2380 * - disable_multiple_selection
2381 * - alternative_form_view
2382 * - create_function (defaults to a naive saving behavior)
2384 * - form_view_options
2385 * - list_view_options
2388 select_element: function(model, options, domain, context) {
2391 this.domain = domain || [];
2392 this.context = context || {};
2393 this.options = _.defaults(options || {}, {"initial_view": "search", "create_function": function() {
2394 return self.create_row.apply(self, arguments);
2395 }, read_function: null});
2396 this.initial_ids = this.options.initial_ids;
2397 this.created_elements = [];
2398 openerp.web.form.dialog(this.render(), {close:function() {
2406 this.dataset = new openerp.web.ProxyDataSet(this, this.model,
2408 this.dataset.create_function = function() {
2409 return self.options.create_function.apply(null, arguments).then(function(r) {
2410 self.created_elements.push(r.result);
2413 this.dataset.write_function = function() {
2414 return self.write_row.apply(self, arguments);
2416 this.dataset.read_function = this.options.read_function;
2417 this.dataset.parent_view = this.options.parent_view;
2418 this.dataset.on_default_get.add(this.on_default_get);
2419 if (this.options.initial_view == "search") {
2420 this.setup_search_view();
2425 setup_search_view: function() {
2427 if (this.searchview) {
2428 this.searchview.stop();
2430 this.searchview = new openerp.web.SearchView(this,
2431 this.dataset, false, {
2432 "selectable": !this.options.disable_multiple_selection,
2435 this.searchview.on_search.add(function(domains, contexts, groupbys) {
2436 if (self.initial_ids) {
2437 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
2438 contexts, groupbys);
2439 self.initial_ids = undefined;
2441 self.do_search(domains.concat([self.domain]), contexts, groupbys);
2444 this.searchview.on_loaded.add_last(function () {
2445 var $buttons = self.searchview.$element.find(".oe_search-view-buttons");
2446 $buttons.append(QWeb.render("SelectCreatePopup.search.buttons"));
2447 var $cbutton = $buttons.find(".oe_selectcreatepopup-search-close");
2448 $cbutton.click(function() {
2451 var $sbutton = $buttons.find(".oe_selectcreatepopup-search-select");
2452 if(self.options.disable_multiple_selection) {
2455 $sbutton.click(function() {
2456 self.on_select_elements(self.selected_ids);
2459 self.view_list = new openerp.web.form.SelectCreateListView(self,
2460 self.dataset, false,
2461 _.extend({'deletable': false}, self.options.list_view_options || {}));
2462 self.view_list.popup = self;
2463 self.view_list.appendTo($("#" + self.element_id + "_view_list")).pipe(function() {
2464 self.view_list.do_show();
2465 }).pipe(function() {
2466 self.searchview.do_search();
2469 this.searchview.appendTo($("#" + this.element_id + "_search"));
2471 do_search: function(domains, contexts, groupbys) {
2473 this.rpc('/web/session/eval_domain_and_context', {
2474 domains: domains || [],
2475 contexts: contexts || [],
2476 group_by_seq: groupbys || []
2477 }, function (results) {
2478 self.view_list.do_search(results.domain, results.context, results.group_by);
2481 create_row: function() {
2483 var wdataset = new openerp.web.DataSetSearch(this, this.model, this.context, this.domain);
2484 wdataset.parent_view = this.options.parent_view;
2485 return wdataset.create.apply(wdataset, arguments);
2487 write_row: function() {
2489 var wdataset = new openerp.web.DataSetSearch(this, this.model, this.context, this.domain);
2490 wdataset.parent_view = this.options.parent_view;
2491 return wdataset.write.apply(wdataset, arguments);
2493 on_select_elements: function(element_ids) {
2495 on_click_element: function(ids) {
2496 this.selected_ids = ids || [];
2497 if(this.selected_ids.length > 0) {
2498 this.$element.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
2500 this.$element.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
2503 new_object: function() {
2505 if (this.searchview) {
2506 this.searchview.hide();
2508 if (this.view_list) {
2509 this.view_list.$element.hide();
2511 this.dataset.index = null;
2512 this.view_form = new openerp.web.FormView(this, this.dataset, false, self.options.form_view_options);
2513 if (this.options.alternative_form_view) {
2514 this.view_form.set_embedded_view(this.options.alternative_form_view);
2516 this.view_form.appendTo(this.$element.find("#" + this.element_id + "_view_form"));
2517 this.view_form.on_loaded.add_last(function() {
2518 var $buttons = self.view_form.$element.find(".oe_form_buttons");
2519 $buttons.html(QWeb.render("SelectCreatePopup.form.buttons", {widget:self}));
2520 var $nbutton = $buttons.find(".oe_selectcreatepopup-form-save-new");
2521 $nbutton.click(function() {
2522 $.when(self.view_form.do_save()).then(function() {
2523 self.view_form.reload_lock.then(function() {
2524 self.view_form.on_button_new();
2528 var $nbutton = $buttons.find(".oe_selectcreatepopup-form-save");
2529 $nbutton.click(function() {
2530 $.when(self.view_form.do_save()).then(function() {
2531 self.view_form.reload_lock.then(function() {
2536 var $cbutton = $buttons.find(".oe_selectcreatepopup-form-close");
2537 $cbutton.click(function() {
2541 this.view_form.do_show();
2543 check_exit: function() {
2544 if (this.created_elements.length > 0) {
2545 this.on_select_elements(this.created_elements);
2549 on_default_get: function(res) {}
2552 openerp.web.form.SelectCreateListView = openerp.web.ListView.extend({
2553 do_add_record: function () {
2554 this.popup.new_object();
2556 select_record: function(index) {
2557 this.popup.on_select_elements([this.dataset.ids[index]]);
2560 do_select: function(ids, records) {
2561 this._super(ids, records);
2562 this.popup.on_click_element(ids);
2568 * @extends openerp.web.OldWidget
2570 openerp.web.form.FormOpenPopup = openerp.web.OldWidget.extend(/** @lends openerp.web.form.FormOpenPopup# */{
2571 identifier_prefix: "formopenpopup",
2572 template: "FormOpenPopup",
2575 * - alternative_form_view
2576 * - auto_write (default true)
2579 * - form_view_options
2582 show_element: function(model, row_id, context, options) {
2584 this.row_id = row_id;
2585 this.context = context || {};
2586 this.options = _.defaults(options || {}, {"auto_write": true});
2587 jQuery(this.render()).dialog({title: '',
2595 this.dataset = new openerp.web.form.FormOpenDataset(this, this.model, this.context);
2596 this.dataset.fop = this;
2597 this.dataset.ids = [this.row_id];
2598 this.dataset.index = 0;
2599 this.dataset.parent_view = this.options.parent_view;
2600 this.setup_form_view();
2602 on_write: function(id, data) {
2603 if (!this.options.auto_write)
2606 var wdataset = new openerp.web.DataSetSearch(this, this.model, this.context, this.domain);
2607 wdataset.parent_view = this.options.parent_view;
2608 wdataset.write(id, data, {}, function(r) {
2609 self.on_write_completed();
2612 on_write_completed: function() {},
2613 setup_form_view: function() {
2615 this.view_form = new openerp.web.FormView(this, this.dataset, false, self.options.form_view_options);
2616 if (this.options.alternative_form_view) {
2617 this.view_form.set_embedded_view(this.options.alternative_form_view);
2619 this.view_form.appendTo(this.$element.find("#" + this.element_id + "_view_form"));
2620 var once = $.Deferred().then(function() {
2621 if (self.options.readonly) {
2622 self.view_form.on_toggle_readonly();
2625 this.view_form.on_loaded.add_last(function() {
2627 var $buttons = self.view_form.$element.find(".oe_form_buttons");
2628 $buttons.html(QWeb.render("FormOpenPopup.form.buttons"));
2629 var $nbutton = $buttons.find(".oe_formopenpopup-form-save");
2630 $nbutton.click(function() {
2631 self.view_form.do_save().then(function() {
2635 if (self.options.readonly) {
2638 var $cbutton = $buttons.find(".oe_formopenpopup-form-close");
2639 $cbutton.click(function() {
2642 self.view_form.do_show();
2644 this.dataset.on_write.add(this.on_write);
2648 openerp.web.form.FormOpenDataset = openerp.web.ProxyDataSet.extend({
2649 read_ids: function() {
2650 if (this.fop.options.read_function) {
2651 return this.fop.options.read_function.apply(null, arguments);
2653 return this._super.apply(this, arguments);
2658 openerp.web.form.FieldReference = openerp.web.form.Field.extend({
2659 template: 'FieldReference',
2660 init: function(view, node) {
2661 this._super(view, node);
2662 this.fields_view = {
2665 selection: view.fields_view.fields[this.name].selection
2672 this.get_fields_values = view.get_fields_values;
2673 this.do_onchange = this.on_form_changed = this.on_nop;
2674 this.dataset = this.view.dataset;
2675 this.widgets_counter = 0;
2676 this.view_id = 'reference_' + _.uniqueId();
2679 this.selection = new openerp.web.form.FieldSelection(this, { attrs: {
2683 this.selection.on_value_changed.add_last(this.on_selection_changed);
2684 this.m2o = new openerp.web.form.FieldMany2One(this, { attrs: {
2689 on_nop: function() {
2691 on_selection_changed: function() {
2692 var sel = this.selection.get_value();
2693 this.m2o.field.relation = sel;
2694 this.m2o.set_value(null);
2695 this.m2o.$element.toggle(sel !== false);
2699 this.selection.start();
2702 is_valid: function() {
2703 return this.required === false || typeof(this.get_value()) === 'string';
2705 is_dirty: function() {
2706 return this.selection.is_dirty() || this.m2o.is_dirty();
2708 set_value: function(value) {
2710 if (typeof(value) === 'string') {
2711 var vals = value.split(',');
2712 this.selection.set_value(vals[0]);
2713 this.m2o.set_value(parseInt(vals[1], 10));
2716 get_value: function() {
2717 var model = this.selection.get_value(),
2718 id = this.m2o.get_value();
2719 if (typeof(model) === 'string' && typeof(id) === 'number') {
2720 return model + ',' + id;
2727 openerp.web.form.FieldBinary = openerp.web.form.Field.extend({
2728 init: function(view, node) {
2729 this._super(view, node);
2730 this.iframe = this.element_id + '_iframe';
2731 this.binary_value = false;
2734 this._super.apply(this, arguments);
2735 this.$element.find('input.oe-binary-file').change(this.on_file_change);
2736 this.$element.find('button.oe-binary-file-save').click(this.on_save_as);
2737 this.$element.find('.oe-binary-file-clear').click(this.on_clear);
2739 human_filesize : function(size) {
2740 var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
2742 while (size >= 1024) {
2746 return size.toFixed(2) + ' ' + units[i];
2748 on_file_change: function(e) {
2749 // TODO: on modern browsers, we could directly read the file locally on client ready to be used on image cropper
2750 // http://www.html5rocks.com/tutorials/file/dndfiles/
2751 // http://deepliquid.com/projects/Jcrop/demos.php?demo=handler
2752 window[this.iframe] = this.on_file_uploaded;
2753 if ($(e.target).val() != '') {
2754 this.$element.find('form.oe-binary-form input[name=session_id]').val(this.session.session_id);
2755 this.$element.find('form.oe-binary-form').submit();
2756 this.$element.find('.oe-binary-progress').show();
2757 this.$element.find('.oe-binary').hide();
2760 on_file_uploaded: function(size, name, content_type, file_base64) {
2761 delete(window[this.iframe]);
2762 if (size === false) {
2763 this.do_warn("File Upload", "There was a problem while uploading your file");
2764 // TODO: use openerp web crashmanager
2765 console.warn("Error while uploading file : ", name);
2767 this.on_file_uploaded_and_valid.apply(this, arguments);
2768 this.on_ui_change();
2770 this.$element.find('.oe-binary-progress').hide();
2771 this.$element.find('.oe-binary').show();
2773 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
2775 on_save_as: function() {
2776 var url = '/web/binary/saveas?session_id=' + this.session.session_id + '&model=' +
2777 this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name +
2778 '&fieldname=' + (this.node.attrs.filename || '') + '&t=' + (new Date().getTime());
2781 on_clear: function() {
2782 if (this.value !== false) {
2784 this.binary_value = false;
2785 this.on_ui_change();
2791 openerp.web.form.FieldBinaryFile = openerp.web.form.FieldBinary.extend({
2792 template: 'FieldBinaryFile',
2793 update_dom: function() {
2794 this._super.apply(this, arguments);
2795 this.$element.find('.oe-binary-file-set, .oe-binary-file-clear').toggle(!this.readonly);
2796 this.$element.find('input[type=text]').attr('disabled', this.readonly);
2798 set_value: function(value) {
2799 this._super.apply(this, arguments);
2800 var show_value = (value != null && value !== false) ? value : '';
2801 this.$element.find('input').eq(0).val(show_value);
2803 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
2804 this.value = file_base64;
2805 this.binary_value = true;
2806 var show_value = this.human_filesize(size);
2807 this.$element.find('input').eq(0).val(show_value);
2808 this.set_filename(name);
2810 set_filename: function(value) {
2811 var filename = this.node.attrs.filename;
2812 if (this.view.fields[filename]) {
2813 this.view.fields[filename].set_value(value);
2814 this.view.fields[filename].on_ui_change();
2817 on_clear: function() {
2818 this._super.apply(this, arguments);
2819 this.$element.find('input').eq(0).val('');
2820 this.set_filename('');
2824 openerp.web.form.FieldBinaryImage = openerp.web.form.FieldBinary.extend({
2825 template: 'FieldBinaryImage',
2827 this._super.apply(this, arguments);
2828 this.$image = this.$element.find('img.oe-binary-image');
2830 update_dom: function() {
2831 this._super.apply(this, arguments);
2832 this.$element.find('.oe-binary').toggle(!this.readonly);
2834 set_value: function(value) {
2835 this._super.apply(this, arguments);
2836 this.set_image_maxwidth();
2837 var url = '/web/binary/image?session_id=' + this.session.session_id + '&model=' +
2838 this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name + '&t=' + (new Date().getTime());
2839 this.$image.attr('src', url);
2841 set_image_maxwidth: function() {
2842 this.$image.css('max-width', this.$element.width());
2844 on_file_change: function() {
2845 this.set_image_maxwidth();
2846 this._super.apply(this, arguments);
2848 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
2849 this.value = file_base64;
2850 this.binary_value = true;
2851 this.$image.attr('src', 'data:' + (content_type || 'image/png') + ';base64,' + file_base64);
2853 on_clear: function() {
2854 this._super.apply(this, arguments);
2855 this.$image.attr('src', '/web/static/src/img/placeholder.png');
2859 openerp.web.form.FieldStatus = openerp.web.form.Field.extend({
2860 template: "EmptyComponent",
2863 this.selected_value = null;
2867 set_value: function(value) {
2869 this.selected_value = value;
2873 render_list: function() {
2875 var shown = _.map(((this.node.attrs || {}).statusbar_visible || "").split(","),
2876 function(x) { return _.trim(x); });
2877 shown = _.select(shown, function(x) { return x.length > 0; });
2879 if (shown.length == 0) {
2880 this.to_show = this.field.selection;
2882 this.to_show = _.select(this.field.selection, function(x) {
2883 return _.indexOf(shown, x[0]) !== -1 || x[0] === self.selected_value;
2887 var content = openerp.web.qweb.render("FieldStatus.content", {widget: this, _:_});
2888 this.$element.html(content);
2890 var colors = JSON.parse((this.node.attrs || {}).statusbar_colors || "{}");
2891 var color = colors[this.selected_value];
2893 var elem = this.$element.find("li.oe-arrow-list-selected span");
2894 elem.css("border-color", color);
2895 if (this.check_white(color))
2896 elem.css("color", "white");
2897 elem = this.$element.find("li.oe-arrow-list-selected .oe-arrow-list-before");
2898 elem.css("border-left-color", "rgba(0,0,0,0)");
2899 elem = this.$element.find("li.oe-arrow-list-selected .oe-arrow-list-after");
2900 elem.css("border-color", "rgba(0,0,0,0)");
2901 elem.css("border-left-color", color);
2904 check_white: function(color) {
2905 var div = $("<div></div>");
2906 div.css("display", "none");
2907 div.css("color", color);
2908 div.appendTo($("body"));
2909 var ncolor = div.css("color");
2911 var res = /^\s*rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/.exec(ncolor);
2915 var comps = [parseInt(res[1]), parseInt(res[2]), parseInt(res[3])];
2916 var lum = comps[0] * 0.3 + comps[1] * 0.59 + comps[1] * 0.11;
2924 openerp.web.form.FieldReadonly = openerp.web.form.Field.extend({
2927 openerp.web.form.FieldCharReadonly = openerp.web.form.FieldReadonly.extend({
2928 template: 'FieldChar.readonly',
2929 init: function(view, node) {
2930 this._super(view, node);
2931 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2933 set_value: function (value) {
2934 this._super.apply(this, arguments);
2935 var show_value = openerp.web.format_value(value, this, '');
2936 if (this.password) {
2937 show_value = new Array(show_value.length + 1).join('*');
2939 this.$element.find('div').text(show_value);
2943 openerp.web.form.FieldURIReadonly = openerp.web.form.FieldCharReadonly.extend({
2944 template: 'FieldURI.readonly',
2946 set_value: function (value) {
2947 var displayed = this._super.apply(this, arguments);
2948 this.$element.find('a')
2949 .attr('href', this.scheme + ':' + displayed)
2953 openerp.web.form.FieldEmailReadonly = openerp.web.form.FieldURIReadonly.extend({
2956 openerp.web.form.FieldUrlReadonly = openerp.web.form.FieldURIReadonly.extend({
2957 set_value: function (value) {
2958 var s = /(\w+):(.+)/.exec(value);
2959 if (!s || !(s[1] === 'http' || s[1] === 'https')) { return; }
2964 openerp.web.form.FieldBooleanReadonly = openerp.web.form.FieldCharReadonly.extend({
2965 set_value: function (value) {
2966 this._super(value ? '\u2611' : '\u2610');
2969 openerp.web.form.FieldSelectionReadonly = openerp.web.form.FieldReadonly.extend({
2970 template: 'FieldChar.readonly',
2971 init: function(view, node) {
2972 // lifted straight from r/w version
2974 this._super(view, node);
2975 this.values = this.field.selection;
2976 _.each(this.values, function(v, i) {
2977 if (v[0] === false && v[1] === '') {
2978 self.values.splice(i, 1);
2981 this.values.unshift([false, '']);
2983 set_value: function (value) {
2984 value = value === null ? false : value;
2985 value = value instanceof Array ? value[0] : value;
2986 var option = _(this.values)
2987 .detect(function (record) { return record[0] === value; });
2989 this.$element.find('div').text(option ? option[1] : this.values[0][1]);
2992 openerp.web.form.FieldMany2OneReadonly = openerp.web.form.FieldCharReadonly.extend({
2993 set_value: function (value) {
2994 value = value || null;
2995 this.invalid = false;
2999 self.on_value_changed();
3000 var real_set_value = function(rval) {
3001 self.$element.find('div').text(rval ? rval[1] : '');
3003 if(!(typeof(value) instanceof Array)) {
3004 var dataset = new openerp.web.DataSetStatic(
3005 this, this.field.relation, self.build_context());
3006 dataset.name_get([value], function(data) {
3007 real_set_value(data[0]);
3008 }).fail(function() {self.tmp_value = undefined;});
3010 setTimeout(function() {real_set_value(value);}, 0);
3016 * Registry of form widgets, called by :js:`openerp.web.FormView`
3018 openerp.web.form.widgets = new openerp.web.Registry({
3019 'frame' : 'openerp.web.form.WidgetFrame',
3020 'group' : 'openerp.web.form.WidgetFrame',
3021 'notebook' : 'openerp.web.form.WidgetNotebook',
3022 'notebookpage' : 'openerp.web.form.WidgetNotebookPage',
3023 'separator' : 'openerp.web.form.WidgetSeparator',
3024 'label' : 'openerp.web.form.WidgetLabel',
3025 'button' : 'openerp.web.form.WidgetButton',
3026 'char' : 'openerp.web.form.FieldChar',
3027 'email' : 'openerp.web.form.FieldEmail',
3028 'url' : 'openerp.web.form.FieldUrl',
3029 'text' : 'openerp.web.form.FieldText',
3030 'text_wiki' : 'openerp.web.form.FieldText',
3031 'date' : 'openerp.web.form.FieldDate',
3032 'datetime' : 'openerp.web.form.FieldDatetime',
3033 'selection' : 'openerp.web.form.FieldSelection',
3034 'many2one' : 'openerp.web.form.FieldMany2One',
3035 'many2many' : 'openerp.web.form.FieldMany2Many',
3036 'one2many' : 'openerp.web.form.FieldOne2Many',
3037 'one2many_list' : 'openerp.web.form.FieldOne2Many',
3038 'reference' : 'openerp.web.form.FieldReference',
3039 'boolean' : 'openerp.web.form.FieldBoolean',
3040 'float' : 'openerp.web.form.FieldFloat',
3041 'integer': 'openerp.web.form.FieldFloat',
3042 'float_time': 'openerp.web.form.FieldFloat',
3043 'progressbar': 'openerp.web.form.FieldProgressBar',
3044 'image': 'openerp.web.form.FieldBinaryImage',
3045 'binary': 'openerp.web.form.FieldBinaryFile',
3046 'statusbar': 'openerp.web.form.FieldStatus'
3049 openerp.web.form.FieldMany2ManyReadonly = openerp.web.form.FieldMany2Many.extend({
3050 force_readonly: true
3052 openerp.web.form.FieldOne2ManyReadonly = openerp.web.form.FieldOne2Many.extend({
3053 force_readonly: true
3055 openerp.web.form.readonly = openerp.web.form.widgets.clone({
3056 'char': 'openerp.web.form.FieldCharReadonly',
3057 'email': 'openerp.web.form.FieldEmailReadonly',
3058 'url': 'openerp.web.form.FieldUrlReadonly',
3059 'text': 'openerp.web.form.FieldCharReadonly',
3060 'text_wiki' : 'openerp.web.form.FieldCharReadonly',
3061 'date': 'openerp.web.form.FieldCharReadonly',
3062 'datetime': 'openerp.web.form.FieldCharReadonly',
3063 'selection' : 'openerp.web.form.FieldSelectionReadonly',
3064 'many2one': 'openerp.web.form.FieldMany2OneReadonly',
3065 'many2many' : 'openerp.web.form.FieldMany2ManyReadonly',
3066 'one2many' : 'openerp.web.form.FieldOne2ManyReadonly',
3067 'one2many_list' : 'openerp.web.form.FieldOne2ManyReadonly',
3068 'boolean': 'openerp.web.form.FieldBooleanReadonly',
3069 'float': 'openerp.web.form.FieldCharReadonly',
3070 'integer': 'openerp.web.form.FieldCharReadonly',
3071 'float_time': 'openerp.web.form.FieldCharReadonly'
3076 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: