5520799527d85ad4dc6a8b1d76a96fb4569080cb
[odoo/odoo.git] / addons / web / static / src / js / search.js
1 openerp.web.search = function(openerp) {
2 var QWeb = openerp.web.qweb,
3       _t =  openerp.web._t;
4
5 openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.SearchView# */{
6     template: "EmptyComponent",
7     /**
8      * @constructs openerp.web.SearchView
9      * @extends openerp.web.Widget
10      *
11      * @param parent
12      * @param element_id
13      * @param dataset
14      * @param view_id
15      * @param defaults
16      */
17     init: function(parent, dataset, view_id, defaults, hidden) {
18         this._super(parent);
19         this.dataset = dataset;
20         this.model = dataset.model;
21         this.view_id = view_id;
22
23         this.defaults = defaults || {};
24         this.has_defaults = !_.isEmpty(this.defaults);
25
26         this.inputs = [];
27         this.enabled_filters = [];
28
29         this.has_focus = false;
30
31         this.hidden = !!hidden;
32         this.headless = this.hidden && !this.has_defaults;
33
34         this.ready = $.Deferred();
35     },
36     start: function() {
37         this._super();
38         if (this.hidden) {
39             this.$element.hide();
40         }
41         if (this.headless) {
42             this.ready.resolve();
43         } else {
44             this.rpc("/web/searchview/load", {
45                 model: this.model,
46                 view_id: this.view_id,
47                 context: this.dataset.get_context()
48             }, this.on_loaded);
49         }
50         return this.ready.promise();
51     },
52     show: function () {
53         this.$element.show();
54     },
55     hide: function () {
56         this.$element.hide();
57     },
58     /**
59      * Builds a list of widget rows (each row is an array of widgets)
60      *
61      * @param {Array} items a list of nodes to convert to widgets
62      * @param {Object} fields a mapping of field names to (ORM) field attributes
63      * @returns Array
64      */
65     make_widgets: function (items, fields) {
66         var rows = [],
67             row = [];
68         rows.push(row);
69         var filters = [];
70         _.each(items, function (item) {
71             if (item.attrs.modifiers) {
72                 var modifiers = item.attrs.modifiers = JSON.parse(
73                         item.attrs.modifiers);
74                 if (modifiers.invisible) { return; }
75             }
76             if (filters.length && item.tag !== 'filter') {
77                 row.push(
78                     new openerp.web.search.FilterGroup(
79                         filters, this));
80                 filters = [];
81             }
82
83             if (item.tag === 'newline') {
84                 row = [];
85                 rows.push(row);
86             } else if (item.tag === 'filter') {
87                 if (!this.has_focus) {
88                     item.attrs.default_focus = '1';
89                     this.has_focus = true;
90                 }
91                 filters.push(
92                     new openerp.web.search.Filter(
93                         item, this));
94             } else if (item.tag === 'separator') {
95                 // a separator is a no-op
96             } else {
97                 if (item.tag === 'group') {
98                     // TODO: group and field should be fetched from registries, maybe even filters
99                     row.push(
100                         new openerp.web.search.Group(
101                             item, this, fields));
102                 } else if (item.tag === 'field') {
103                     if (!this.has_focus) {
104                         item.attrs.default_focus = '1';
105                         this.has_focus = true;
106                     }
107                     row.push(
108                         this.make_field(
109                             item, fields[item['attrs'].name]));
110                 }
111             }
112         }, this);
113         if (filters.length) {
114             row.push(new openerp.web.search.FilterGroup(filters, this));
115         }
116
117         return rows;
118     },
119     /**
120      * Creates a field for the provided field descriptor item (which comes
121      * from fields_view_get)
122      *
123      * @param {Object} item fields_view_get node for the field
124      * @param {Object} field fields_get result for the field
125      * @returns openerp.web.search.Field
126      */
127     make_field: function (item, field) {
128         try {
129             return new (openerp.web.search.fields.get_any(
130                     [item.attrs.widget, field.type]))
131                 (item, field, this);
132         } catch (e) {
133             if (! e instanceof openerp.web.KeyNotFound) {
134                 throw e;
135             }
136             // KeyNotFound means unknown field type
137             console.group('Unknown field type ' + field.type);
138             console.error('View node', item);
139             console.info('View field', field);
140             console.info('In view', this);
141             console.groupEnd();
142             return null;
143         }
144     },
145     on_loaded: function(data) {
146         this.fields_view = data.fields_view;
147         if (data.fields_view.type !== 'search' ||
148             data.fields_view.arch.tag !== 'search') {
149                 throw new Error(_.str.sprintf(
150                     "Got non-search view after asking for a search view: type %s, arch root %s",
151                     data.fields_view.type, data.fields_view.arch.tag));
152         }
153         var self = this,
154            lines = this.make_widgets(
155                 data.fields_view['arch'].children,
156                 data.fields_view.fields);
157
158         // for extended search view
159         var ext = new openerp.web.search.ExtendedSearch(this, this.model);
160         lines.push([ext]);
161         this.inputs.push(ext);
162         this.extended_search = ext;
163
164         var render = QWeb.render("SearchView", {
165             'view': data.fields_view['arch'],
166             'lines': lines,
167             'defaults': this.defaults
168         });
169
170         this.$element.html(render);
171
172         var f = this.$element.find('form');
173         this.$element.find('form')
174                 .submit(this.do_search)
175                 .bind('reset', this.do_clear);
176         // start() all the widgets
177         var widget_starts = _(lines).chain().flatten().map(function (widget) {
178             return widget.start();
179         }).value();
180
181         $.when.apply(null, widget_starts).then(function () {
182             self.ready.resolve();
183         });
184
185         this.reload_managed_filters();
186     },
187     reload_managed_filters: function() {
188         var self = this;
189         return this.rpc('/web/searchview/get_filters', {
190             model: this.dataset.model
191         }).then(function(result) {
192             self.managed_filters = result;
193             var filters = self.$element.find(".oe_search-view-filters-management");
194             filters.html(QWeb.render("SearchView.managed-filters", {filters: result}));
195             filters.change(self.on_filters_management);
196         });
197     },
198     /**
199      * Handle event when the user make a selection in the filters management select box.
200      */
201     on_filters_management: function(e) {
202         var self = this;
203         var select = this.$element.find(".oe_search-view-filters-management");
204         var val = select.val();
205         switch(val) {
206         case 'advanced_filter':
207             this.extended_search.on_activate();
208             break;
209         case 'add_to_dashboard':
210             this.on_add_to_dashboard();
211             break;
212         case 'manage_filters':
213             this.do_action({
214                 res_model: 'ir.filters',
215                 views: [[false, 'list'], [false, 'form']],
216                 type: 'ir.actions.act_window',
217                 context: {"search_default_user_id": this.session.uid,
218                 "search_default_model_id": this.dataset.model},
219                 target: "current",
220                 limit : 80
221             });
222             break;
223         case 'save_filter':
224             var data = this.build_search_data();
225             var context = new openerp.web.CompoundContext();
226             _.each(data.contexts, function(x) {
227                 context.add(x);
228             });
229             var domain = new openerp.web.CompoundDomain();
230             _.each(data.domains, function(x) {
231                 domain.add(x);
232             });
233             var dial_html = QWeb.render("SearchView.managed-filters.add");
234             var $dial = $(dial_html);
235             $dial.dialog({
236                 modal: true,
237                 title: _t("Filter Entry"),
238                 buttons: [
239                     {text: _t("Cancel"), click: function() {
240                         $(this).dialog("close");
241                     }},
242                     {text: _t("OK"), click: function() {
243                         $(this).dialog("close");
244                         var name = $(this).find("input").val();
245                         self.rpc('/web/searchview/save_filter', {
246                             model: self.dataset.model,
247                             context_to_save: context,
248                             domain: domain,
249                             name: name
250                         }).then(function() {
251                             self.reload_managed_filters();
252                         });
253                     }}
254                 ]
255             });
256             break;
257         }
258         if (val.slice(0, 4) == "get:") {
259             val = val.slice(4);
260             val = parseInt(val, 10);
261             var filter = this.managed_filters[val];
262             this.on_search([filter.domain], [filter.context], []);
263         } else {
264             select.val('');
265         }
266     },
267     on_add_to_dashboard: function() {
268         this.$element.find(".oe_search-view-filters-management")[0].selectedIndex = 0;
269         var self = this,
270             menu = openerp.webclient.menu,
271             $dialog = $(QWeb.render("SearchView.add_to_dashboard", {
272                 dashboards : menu.data.data.children,
273                 selected_menu_id : menu.$element.find('a.active').data('menu')
274             }));
275         $dialog.find('input').val(this.fields_view.name);
276         $dialog.dialog({
277             modal: true,
278             title: _t("Add to Dashboard"),
279             buttons: [
280                 {text: _t("Cancel"), click: function() {
281                     $(this).dialog("close");
282                 }},
283                 {text: _t("OK"), click: function() {
284                     $(this).dialog("close");
285                     var menu_id = $(this).find("select").val(),
286                         title = $(this).find("input").val(),
287                         data = self.build_search_data(),
288                         context = new openerp.web.CompoundContext(),
289                         domain = new openerp.web.CompoundDomain();
290                     _.each(data.contexts, function(x) {
291                         context.add(x);
292                     });
293                     _.each(data.domains, function(x) {
294                            domain.add(x);
295                     });
296                     self.rpc('/web/searchview/add_to_dashboard', {
297                         menu_id: menu_id,
298                         action_id: self.widget_parent.action.id,
299                         context_to_save: context,
300                         domain: domain,
301                         view_mode: self.widget_parent.active_view,
302                         name: title
303                     }, function(r) {
304                         if (r === false) {
305                             self.do_warn("Could not add filter to dashboard");
306                         } else {
307                             self.do_notify("Filter added to dashboard", '');
308                         }
309                     });
310                 }}
311             ]
312         });
313     },
314     /**
315      * Performs the search view collection of widget data.
316      *
317      * If the collection went well (all fields are valid), then triggers
318      * :js:func:`openerp.web.SearchView.on_search`.
319      *
320      * If at least one field failed its validation, triggers
321      * :js:func:`openerp.web.SearchView.on_invalid` instead.
322      *
323      * @param e jQuery event object coming from the "Search" button
324      */
325     do_search: function (e) {
326         if (this.headless && !this.has_defaults) {
327             return this.on_search([], [], []);
328         }
329         // reset filters management
330         var select = this.$element.find(".oe_search-view-filters-management");
331         select.val("_filters");
332
333         if (e && e.preventDefault) { e.preventDefault(); }
334
335         var data = this.build_search_data();
336
337         if (data.errors.length) {
338             this.on_invalid(data.errors);
339             return;
340         }
341
342         this.on_search(data.domains, data.contexts, data.groupbys);
343     },
344     build_search_data: function() {
345         var domains = [],
346            contexts = [],
347              errors = [];
348
349         _.each(this.inputs, function (input) {
350             try {
351                 var domain = input.get_domain();
352                 if (domain) {
353                     domains.push(domain);
354                 }
355
356                 var context = input.get_context();
357                 if (context) {
358                     contexts.push(context);
359                 }
360             } catch (e) {
361                 if (e instanceof openerp.web.search.Invalid) {
362                     errors.push(e);
363                 } else {
364                     throw e;
365                 }
366             }
367         });
368
369         // TODO: do we need to handle *fields* with group_by in their context?
370         var groupbys = _(this.enabled_filters)
371                 .chain()
372                 .map(function (filter) { return filter.get_context();})
373                 .compact()
374                 .value();
375         return {domains: domains, contexts: contexts, errors: errors, groupbys: groupbys};
376     },
377     /**
378      * Triggered after the SearchView has collected all relevant domains and
379      * contexts.
380      *
381      * It is provided with an Array of domains and an Array of contexts, which
382      * may or may not be evaluated (each item can be either a valid domain or
383      * context, or a string to evaluate in order in the sequence)
384      *
385      * It is also passed an array of contexts used for group_by (they are in
386      * the correct order for group_by evaluation, which contexts may not be)
387      *
388      * @event
389      * @param {Array} domains an array of literal domains or domain references
390      * @param {Array} contexts an array of literal contexts or context refs
391      * @param {Array} groupbys ordered contexts which may or may not have group_by keys
392      */
393     on_search: function (domains, contexts, groupbys) {
394     },
395     /**
396      * Triggered after a validation error in the SearchView fields.
397      *
398      * Error objects have three keys:
399      * * ``field`` is the name of the invalid field
400      * * ``value`` is the invalid value
401      * * ``message`` is the (in)validation message provided by the field
402      *
403      * @event
404      * @param {Array} errors a never-empty array of error objects
405      */
406     on_invalid: function (errors) {
407         this.do_notify(_t("Invalid Search"), _t("triggered from search view"));
408     },
409     do_clear: function () {
410         this.$element.find('.filter_label, .filter_icon').removeClass('enabled');
411         this.enabled_filters.splice(0);
412         var string = $('a.searchview_group_string');
413         _.each(string, function(str){
414             $(str).closest('div.searchview_group').removeClass("expanded").addClass('folded');
415          });
416         this.$element.find('table:last').hide();
417
418         $('.searchview_extended_groups_list').empty();
419         _.each(this.inputs, function (input) {
420             if(input.datewidget && input.datewidget.value) {
421                 input.datewidget.set_value(false);
422             }
423         });
424         setTimeout(this.on_clear, 0);
425     },
426     /**
427      * Triggered when the search view gets cleared
428      *
429      * @event
430      */
431     on_clear: function () {
432         this.do_search();
433     },
434     /**
435      * Called by a filter propagating its state changes
436      *
437      * @param {openerp.web.search.Filter} filter a filter which got toggled
438      * @param {Boolean} default_enabled filter got enabled through the default values, at render time.
439      */
440     do_toggle_filter: function (filter, default_enabled) {
441         if (default_enabled || filter.is_enabled()) {
442             this.enabled_filters.push(filter);
443         } else {
444             this.enabled_filters = _.without(
445                 this.enabled_filters, filter);
446         }
447
448         if (!default_enabled) {
449             // selecting a filter after initial loading automatically
450             // triggers refresh
451             this.$element.find('form').submit();
452         }
453     }
454 });
455
456 /** @namespace */
457 openerp.web.search = {};
458 /**
459  * Registry of search fields, called by :js:class:`openerp.web.SearchView` to
460  * find and instantiate its field widgets.
461  */
462 openerp.web.search.fields = new openerp.web.Registry({
463     'char': 'openerp.web.search.CharField',
464     'text': 'openerp.web.search.CharField',
465     'boolean': 'openerp.web.search.BooleanField',
466     'integer': 'openerp.web.search.IntegerField',
467     'float': 'openerp.web.search.FloatField',
468     'selection': 'openerp.web.search.SelectionField',
469     'datetime': 'openerp.web.search.DateTimeField',
470     'date': 'openerp.web.search.DateField',
471     'many2one': 'openerp.web.search.ManyToOneField',
472     'many2many': 'openerp.web.search.CharField',
473     'one2many': 'openerp.web.search.CharField'
474 });
475 openerp.web.search.Invalid = openerp.web.Class.extend( /** @lends openerp.web.search.Invalid# */{
476     /**
477      * Exception thrown by search widgets when they hold invalid values,
478      * which they can not return when asked.
479      *
480      * @constructs openerp.web.search.Invalid
481      * @extends openerp.web.Class
482      *
483      * @param field the name of the field holding an invalid value
484      * @param value the invalid value
485      * @param message validation failure message
486      */
487     init: function (field, value, message) {
488         this.field = field;
489         this.value = value;
490         this.message = message;
491     },
492     toString: function () {
493         return _.str.sprintf(
494             _t("Incorrect value for field %(fieldname)s: [%(value)s] is %(message)s"),
495             {fieldname: this.field, value: this.value, message: this.message}
496         );
497     }
498 });
499 openerp.web.search.Widget = openerp.web.Widget.extend( /** @lends openerp.web.search.Widget# */{
500     template: null,
501     /**
502      * Root class of all search widgets
503      *
504      * @constructs openerp.web.search.Widget
505      * @extends openerp.web.Widget
506      *
507      * @param view the ancestor view of this widget
508      */
509     init: function (view) {
510         this._super(view);
511         this.view = view;
512     },
513     /**
514      * Sets and returns a globally unique identifier for the widget.
515      *
516      * If a prefix is specified, the identifier will be appended to it.
517      *
518      * @params prefix prefix sections, empty/falsy sections will be removed
519      */
520     make_id: function () {
521         this.element_id = _.uniqueId(
522             ['search'].concat(
523                 _.compact(_.toArray(arguments)),
524                 ['']).join('_'));
525         return this.element_id;
526     },
527     /**
528      * "Starts" the widgets. Called at the end of the rendering, this allows
529      * widgets to hook themselves to their view sections.
530      *
531      * On widgets, if they kept a reference to a view and have an element_id,
532      * will fetch and set their root element on $element.
533      */
534     start: function () {
535         this._super();
536         if (this.view && this.element_id) {
537             // id is unique, and no getElementById on elements
538             this.$element = $(document.getElementById(
539                 this.element_id));
540         }
541     },
542     /**
543      * "Stops" the widgets. Called when the view destroys itself, this
544      * lets the widgets clean up after themselves.
545      */
546     stop: function () {
547         delete this.view;
548         this._super();
549     },
550     render: function (defaults) {
551         // FIXME
552         return this._super(_.extend(this, {defaults: defaults}));
553     }
554 });
555 openerp.web.search.add_expand_listener = function($root) {
556     $root.find('a.searchview_group_string').click(function (e) {
557         $root.toggleClass('folded expanded');
558         e.stopPropagation();
559         e.preventDefault();
560     });
561 };
562 openerp.web.search.Group = openerp.web.search.Widget.extend({
563     template: 'SearchView.group',
564     init: function (view_section, view, fields) {
565         this._super(view);
566         this.attrs = view_section.attrs;
567         this.lines = view.make_widgets(
568             view_section.children, fields);
569         this.make_id('group');
570     },
571     start: function () {
572         this._super();
573         openerp.web.search.add_expand_listener(this.$element);
574         var widget_starts = _(this.lines).chain().flatten()
575                 .map(function (widget) { return widget.start(); })
576             .value();
577         return $.when.apply(null, widget_starts);
578     }
579 });
580
581 openerp.web.search.Input = openerp.web.search.Widget.extend( /** @lends openerp.web.search.Input# */{
582     /**
583      * @constructs openerp.web.search.Input
584      * @extends openerp.web.search.Widget
585      *
586      * @param view
587      */
588     init: function (view) {
589         this._super(view);
590         this.view.inputs.push(this);
591     },
592     get_context: function () {
593         throw new Error(
594             "get_context not implemented for widget " + this.attrs.type);
595     },
596     get_domain: function () {
597         throw new Error(
598             "get_domain not implemented for widget " + this.attrs.type);
599     }
600 });
601 openerp.web.search.FilterGroup = openerp.web.search.Input.extend(/** @lends openerp.web.search.FilterGroup# */{
602     template: 'SearchView.filters',
603     /**
604      * Inclusive group of filters, creates a continuous "button" with clickable
605      * sections (the normal display for filters is to be a self-contained button)
606      *
607      * @constructs openerp.web.search.FilterGroup
608      * @extends openerp.web.search.Input
609      *
610      * @param {Array<openerp.web.search.Filter>} filters elements of the group
611      * @param {openerp.web.SearchView} view view in which the filters are contained
612      */
613     init: function (filters, view) {
614         this._super(view);
615         this.filters = filters;
616         this.length = filters.length;
617     },
618     start: function () {
619         this._super();
620         _.each(this.filters, function (filter) {
621             filter.start();
622         });
623     },
624     get_context: function () { },
625     /**
626      * Handles domains-fetching for all the filters within it: groups them.
627      */
628     get_domain: function () {
629         var domains = _(this.filters).chain()
630             .filter(function (filter) { return filter.is_enabled(); })
631             .map(function (filter) { return filter.attrs.domain; })
632             .reject(_.isEmpty)
633             .value();
634
635         if (!domains.length) { return; }
636         if (domains.length === 1) { return domains[0]; }
637         for (var i=domains.length; --i;) {
638             domains.unshift(['|']);
639         }
640         return _.extend(new openerp.web.CompoundDomain(), {
641             __domains: domains
642         });
643     }
644 });
645 openerp.web.search.Filter = openerp.web.search.Input.extend(/** @lends openerp.web.search.Filter# */{
646     template: 'SearchView.filter',
647     /**
648      * Implementation of the OpenERP filters (button with a context and/or
649      * a domain sent as-is to the search view)
650      *
651      * @constructs openerp.web.search.Filter
652      * @extends openerp.web.search.Input
653      *
654      * @param node
655      * @param view
656      */
657     init: function (node, view) {
658         this._super(view);
659         this.attrs = node.attrs;
660         this.classes = [this.attrs.string ? 'filter_label' : 'filter_icon'];
661         this.make_id('filter', this.attrs.name);
662     },
663     start: function () {
664         this._super();
665         var self = this;
666         this.$element.click(function (e) {
667             $(this).toggleClass('enabled');
668             self.view.do_toggle_filter(self);
669         });
670     },
671     /**
672      * Returns whether the filter is currently enabled (in use) or not.
673      *
674      * @returns a boolean
675      */
676     is_enabled:function () {
677         return this.$element.hasClass('enabled');
678     },
679     /**
680      * If the filter is present in the defaults (and has a truthy value),
681      * enable the filter.
682      *
683      * @param {Object} defaults the search view's default values
684      */
685     render: function (defaults) {
686         if (this.attrs.name && defaults[this.attrs.name]) {
687             this.classes.push('enabled');
688             this.view.do_toggle_filter(this, true);
689         }
690         return this._super(defaults);
691     },
692     get_context: function () {
693         if (!this.is_enabled()) {
694             return;
695         }
696         return this.attrs.context;
697     },
698     /**
699      * Does not return anything: filter domain is handled at the FilterGroup
700      * level
701      */
702     get_domain: function () { }
703 });
704 openerp.web.search.Field = openerp.web.search.Input.extend( /** @lends openerp.web.search.Field# */ {
705     template: 'SearchView.field',
706     default_operator: '=',
707     /**
708      * @constructs openerp.web.search.Field
709      * @extends openerp.web.search.Input
710      *
711      * @param view_section
712      * @param field
713      * @param view
714      */
715     init: function (view_section, field, view) {
716         this._super(view);
717         this.attrs = _.extend({}, field, view_section.attrs);
718         this.filters = new openerp.web.search.FilterGroup(_.compact(_.map(
719             view_section.children, function (filter_node) {
720                 if (filter_node.attrs.modifiers) {
721                     var modifiers = filter_node.attrs.modifiers = JSON.parse(
722                             filter_node.attrs.modifiers);
723                     if (modifiers.invisible) { return; }
724                 }
725                 if (filter_node.attrs.string &&
726                         typeof console !== 'undefined' && console.debug) {
727                     console.debug("Filter-in-field with a 'string' attribute "
728                                 + "in view", view);
729                 }
730                 delete filter_node.attrs.string;
731                 return new openerp.web.search.Filter(
732                     filter_node, view);
733         })), view);
734         this.make_id('input', field.type, this.attrs.name);
735     },
736     start: function () {
737         this._super();
738         this.filters.start();
739     },
740     get_context: function () {
741         var val = this.get_value();
742         // A field needs a value to be "active", and a context to send when
743         // active
744         var has_value = (val !== null && val !== '');
745         var context = this.attrs.context;
746         if (!(has_value && context)) {
747             return;
748         }
749         return _.extend(
750             {}, context,
751             {own_values: {self: val}});
752     },
753     /**
754      * Function creating the returned domain for the field, override this
755      * methods in children if you only need to customize the field's domain
756      * without more complex alterations or tests (and without the need to
757      * change override the handling of filter_domain)
758      *
759      * @param {String} name the field's name
760      * @param {String} operator the field's operator (either attribute-specified or default operator for the field
761      * @param {Number|String} value parsed value for the field
762      * @returns {Array<Array>} domain to include in the resulting search
763      */
764     make_domain: function (name, operator, value) {
765         return [[name, operator, value]];
766     },
767     get_domain: function () {
768         var val = this.get_value();
769         if (val === null || val === '') {
770             return;
771         }
772
773         var domain = this.attrs['filter_domain'];
774         if (!domain) {
775             return this.make_domain(
776                 this.attrs.name,
777                 this.attrs.operator || this.default_operator,
778                 val);
779         }
780         return _.extend({}, domain, {own_values: {self: val}});
781     }
782 });
783 /**
784  * Implementation of the ``char`` OpenERP field type:
785  *
786  * * Default operator is ``ilike`` rather than ``=``
787  *
788  * * The Javascript and the HTML values are identical (strings)
789  *
790  * @class
791  * @extends openerp.web.search.Field
792  */
793 openerp.web.search.CharField = openerp.web.search.Field.extend( /** @lends openerp.web.search.CharField# */ {
794     default_operator: 'ilike',
795     get_value: function () {
796         return this.$element.val();
797     }
798 });
799 openerp.web.search.NumberField = openerp.web.search.Field.extend(/** @lends openerp.web.search.NumberField# */{
800     get_value: function () {
801         if (!this.$element.val()) {
802             return null;
803         }
804         var val = this.parse(this.$element.val()),
805           check = Number(this.$element.val());
806         if (isNaN(val) || val !== check) {
807             this.$element.addClass('error');
808             throw new openerp.web.search.Invalid(
809                 this.attrs.name, this.$element.val(), this.error_message);
810         }
811         this.$element.removeClass('error');
812         return val;
813     }
814 });
815 /**
816  * @class
817  * @extends openerp.web.search.NumberField
818  */
819 openerp.web.search.IntegerField = openerp.web.search.NumberField.extend(/** @lends openerp.web.search.IntegerField# */{
820     error_message: _t("not a valid integer"),
821     parse: function (value) {
822         try {
823             return openerp.web.parse_value(value, {'widget': 'integer'});
824         } catch (e) {
825             return NaN;
826         }
827     }
828 });
829 /**
830  * @class
831  * @extends openerp.web.search.NumberField
832  */
833 openerp.web.search.FloatField = openerp.web.search.NumberField.extend(/** @lends openerp.web.search.FloatField# */{
834     error_message: _t("not a valid number"),
835     parse: function (value) {
836         try {
837             return openerp.web.parse_value(value, {'widget': 'float'});
838         } catch (e) {
839             return NaN;
840         }
841     }
842 });
843 /**
844  * @class
845  * @extends openerp.web.search.Field
846  */
847 openerp.web.search.SelectionField = openerp.web.search.Field.extend(/** @lends openerp.web.search.SelectionField# */{
848     // This implementation is a basic <select> field, but it may have to be
849     // altered to be more in line with the GTK client, which uses a combo box
850     // (~ jquery.autocomplete):
851     // * If an option was selected in the list, behave as currently
852     // * If something which is not in the list was entered (via the text input),
853     //   the default domain should become (`ilike` string_value) but **any
854     //   ``context`` or ``filter_domain`` becomes falsy, idem if ``@operator``
855     //   is specified. So at least get_domain needs to be quite a bit
856     //   overridden (if there's no @value and there is no filter_domain and
857     //   there is no @operator, return [[name, 'ilike', str_val]]
858     template: 'SearchView.field.selection',
859     init: function () {
860         this._super.apply(this, arguments);
861         // prepend empty option if there is no empty option in the selection list
862         this.prepend_empty = !_(this.attrs.selection).detect(function (item) {
863             return !item[1];
864         });
865     },
866     get_value: function () {
867         var index = parseInt(this.$element.val(), 10);
868         if (isNaN(index)) { return null; }
869         var value = this.attrs.selection[index][0];
870         if (value === false) { return null; }
871         return value;
872     },
873     /**
874      * The selection field needs a default ``false`` value in case none is
875      * provided, so that selector options with a ``false`` value (convention
876      * for explicitly empty options) get selected by default rather than the
877      * first (value-holding) option in the selection.
878      *
879      * @param {Object} defaults search default values
880      */
881     render: function (defaults) {
882         if (!defaults[this.attrs.name]) {
883             defaults[this.attrs.name] = false;
884         }
885         return this._super(defaults);
886     }
887 });
888 openerp.web.search.BooleanField = openerp.web.search.SelectionField.extend(/** @lends openerp.web.search.BooleanField# */{
889     /**
890      * @constructs openerp.web.search.BooleanField
891      * @extends openerp.web.search.BooleanField
892      */
893     init: function () {
894         this._super.apply(this, arguments);
895         this.attrs.selection = [
896             ['true', 'Yes'],
897             ['false', 'No']
898         ];
899     },
900     /**
901      * Search defaults likely to be boolean values (for a boolean field).
902      *
903      * In the HTML, we only want/get strings, and our strings here are ``true``
904      * and ``false``, so ensure we use precisely those by truth-testing the
905      * default value (iif there is one in the view's defaults).
906      *
907      * @param {Object} defaults default values for this search view
908      * @returns {String} rendered boolean field
909      */
910     render: function (defaults) {
911         var name = this.attrs.name;
912         if (name in defaults) {
913             defaults[name] = defaults[name] ? "true" : "false";
914         }
915         return this._super(defaults);
916     },
917     get_value: function () {
918         switch (this.$element.val()) {
919             case 'false': return false;
920             case 'true': return true;
921             default: return null;
922         }
923     }
924 });
925 /**
926  * @class
927  * @extends openerp.web.search.DateField
928  */
929 openerp.web.search.DateField = openerp.web.search.Field.extend(/** @lends openerp.web.search.DateField# */{
930     template: "SearchView.date",
931     start: function () {
932         this._super();
933         this.datewidget = new openerp.web.DateWidget(this);
934         this.datewidget.prependTo(this.$element);
935         this.datewidget.$element.find("input").attr("size", 15);
936         this.datewidget.$element.find("input").attr("autofocus",
937             this.attrs.default_focus === '1' ? 'autofocus' : undefined);
938         this.datewidget.set_value(this.defaults[this.attrs.name] || false);
939     },
940     get_value: function () {
941         return this.datewidget.get_value() || null;
942     }
943 });
944 /**
945  * Implementation of the ``datetime`` openerp field type:
946  *
947  * * Uses the same widget as the ``date`` field type (a simple date)
948  *
949  * * Builds a slighly more complex, it's a datetime range (includes time)
950  *   spanning the whole day selected by the date widget
951  *
952  * @class
953  * @extends openerp.web.DateField
954  */
955 openerp.web.search.DateTimeField = openerp.web.search.DateField.extend(/** @lends openerp.web.search.DateTimeField# */{
956     make_domain: function (name, operator, value) {
957         return ['&', [name, '>=', value + ' 00:00:00'],
958                      [name, '<=', value + ' 23:59:59']];
959     }
960 });
961 openerp.web.search.ManyToOneField = openerp.web.search.CharField.extend({
962     init: function (view_section, field, view) {
963         this._super(view_section, field, view);
964         var self = this;
965         this.got_name = $.Deferred().then(function () {
966             self.$element.val(self.name);
967         });
968         this.dataset = new openerp.web.DataSet(
969                 this.view, this.attrs['relation']);
970     },
971     start: function () {
972         this._super();
973         this.setup_autocomplete();
974         var started = $.Deferred();
975         this.got_name.then(function () { started.resolve();},
976                            function () { started.resolve(); });
977         return started.promise();
978     },
979     setup_autocomplete: function () {
980         var self = this;
981         this.$element.autocomplete({
982             source: function (req, resp) {
983                 self.dataset.name_search(
984                     req.term, self.attrs.domain, 'ilike', 8, function (data) {
985                         resp(_.map(data, function (result) {
986                             return {id: result[0], label: result[1]}
987                         }));
988                 });
989             },
990             select: function (event, ui) {
991                 self.id = ui.item.id;
992                 self.name = ui.item.label;
993             },
994             delay: 0
995         })
996     },
997     on_name_get: function (name_get) {
998         if (!name_get.length) {
999             delete this.id;
1000             this.got_name.reject();
1001             return;
1002         }
1003         this.name = name_get[0][1];
1004         this.got_name.resolve();
1005     },
1006     render: function (defaults) {
1007         if (defaults[this.attrs.name]) {
1008             this.id = defaults[this.attrs.name];
1009             if (this.id instanceof Array)
1010                 this.id = this.id[0];
1011             // TODO: maybe this should not be completely removed
1012             delete defaults[this.attrs.name];
1013             this.dataset.name_get([this.id], $.proxy(this, 'on_name_get'));
1014         } else {
1015             this.got_name.reject();
1016         }
1017         return this._super(defaults);
1018     },
1019     make_domain: function (name, operator, value) {
1020         if (this.id && this.name) {
1021             if (value === this.name) {
1022                 return [[name, '=', this.id]];
1023             } else {
1024                 delete this.id;
1025                 delete this.name;
1026             }
1027         }
1028         return this._super(name, operator, value);
1029     }
1030 });
1031
1032 openerp.web.search.ExtendedSearch = openerp.web.OldWidget.extend({
1033     template: 'SearchView.extended_search',
1034     identifier_prefix: 'extended-search',
1035     init: function (parent, model) {
1036         this._super(parent);
1037         this.model = model;
1038     },
1039     add_group: function() {
1040         var group = new openerp.web.search.ExtendedSearchGroup(this, this.fields);
1041         group.appendTo(this.$element.find('.searchview_extended_groups_list'));
1042         this.check_last_element();
1043     },
1044     start: function () {
1045         this.$element = $("#" + this.element_id);
1046         this.$element.closest("table.oe-searchview-render-line").css("display", "none");
1047         var self = this;
1048         this.rpc("/web/searchview/fields_get",
1049             {"model": this.model}, function(data) {
1050             self.fields = data.fields;
1051             if (!('id' in self.fields)) {
1052                 self.fields.id = {
1053                     string: 'ID',
1054                     type: 'id'
1055                 }
1056             }
1057             openerp.web.search.add_expand_listener(self.$element);
1058             self.$element.find('.searchview_extended_add_group').click(function (e) {
1059                 self.add_group();
1060             });
1061         });
1062     },
1063     get_context: function() {
1064         return null;
1065     },
1066     get_domain: function() {
1067         if (!this.$element) {
1068             return null; // not a logical state but sometimes it happens
1069         }
1070         if(this.$element.closest("table.oe-searchview-render-line").css("display") == "none") {
1071             return null;
1072         }
1073         return _.reduce(this.widget_children,
1074             function(mem, x) { return mem.concat(x.get_domain());}, []);
1075     },
1076     on_activate: function() {
1077         this.add_group();
1078         var table = this.$element.closest("table.oe-searchview-render-line");
1079         table.css("display", "");
1080         if(this.$element.hasClass("folded")) {
1081             this.$element.toggleClass("folded expanded");
1082         }
1083     },
1084     hide: function() {
1085         var table = this.$element.closest("table.oe-searchview-render-line");
1086         table.css("display", "none");
1087         if(this.$element.hasClass("expanded")) {
1088             this.$element.toggleClass("folded expanded");
1089         }
1090     },
1091     check_last_element: function() {
1092         _.each(this.widget_children, function(x) {x.set_last_group(false);});
1093         if (this.widget_children.length >= 1) {
1094             this.widget_children[this.widget_children.length - 1].set_last_group(true);
1095         }
1096     }
1097 });
1098
1099 openerp.web.search.ExtendedSearchGroup = openerp.web.OldWidget.extend({
1100     template: 'SearchView.extended_search.group',
1101     identifier_prefix: 'extended-search-group',
1102     init: function (parent, fields) {
1103         this._super(parent);
1104         this.fields = fields;
1105     },
1106     add_prop: function() {
1107         var prop = new openerp.web.search.ExtendedSearchProposition(this, this.fields);
1108         var render = prop.render({'index': this.widget_children.length - 1});
1109         this.$element.find('.searchview_extended_propositions_list').append(render);
1110         prop.start();
1111     },
1112     start: function () {
1113         this.$element = $("#" + this.element_id);
1114         var _this = this;
1115         this.add_prop();
1116         this.$element.find('.searchview_extended_add_proposition').click(function () {
1117             _this.add_prop();
1118         });
1119         this.$element.find('.searchview_extended_delete_group').click(function () {
1120             _this.stop();
1121         });
1122     },
1123     get_domain: function() {
1124         var props = _(this.widget_children).chain().map(function(x) {
1125             return x.get_proposition();
1126         }).compact().value();
1127         var choice = this.$element.find(".searchview_extended_group_choice").val();
1128         var op = choice == "all" ? "&" : "|";
1129         return choice == "none" ? ['!'] : [].concat(
1130             _.map(_.range(_.max([0,props.length - 1])), function() { return op; }),
1131             props);
1132     },
1133     stop: function() {
1134         var parent = this.widget_parent;
1135         if (this.widget_parent.widget_children.length == 1)
1136             this.widget_parent.hide();
1137         this._super();
1138         parent.check_last_element();
1139     },
1140     set_last_group: function(is_last) {
1141         this.$element.toggleClass('last_group', is_last);
1142     }
1143 });
1144
1145 openerp.web.search.ExtendedSearchProposition = openerp.web.OldWidget.extend(/** @lends openerp.web.search.ExtendedSearchProposition# */{
1146     template: 'SearchView.extended_search.proposition',
1147     identifier_prefix: 'extended-search-proposition',
1148     /**
1149      * @constructs openerp.web.search.ExtendedSearchProposition
1150      * @extends openerp.web.OldWidget
1151      *
1152      * @param parent
1153      * @param fields
1154      */
1155     init: function (parent, fields) {
1156         this._super(parent);
1157         this.fields = _(fields).chain()
1158             .map(function(val, key) { return _.extend({}, val, {'name': key}); })
1159             .sortBy(function(field) {return field.string;})
1160             .value();
1161         this.attrs = {_: _, fields: this.fields, selected: null};
1162         this.value = null;
1163     },
1164     start: function () {
1165         this.$element = $("#" + this.element_id);
1166         this.select_field(this.fields.length > 0 ? this.fields[0] : null);
1167         var _this = this;
1168         this.$element.find(".searchview_extended_prop_field").change(function() {
1169             _this.changed();
1170         });
1171         this.$element.find('.searchview_extended_delete_prop').click(function () {
1172             _this.stop();
1173         });
1174     },
1175     stop: function() {
1176         var parent;
1177         if (this.widget_parent.widget_children.length == 1)
1178             parent = this.widget_parent;
1179         this._super();
1180         if (parent)
1181             parent.stop();
1182     },
1183     changed: function() {
1184         var nval = this.$element.find(".searchview_extended_prop_field").val();
1185         if(this.attrs.selected == null || nval != this.attrs.selected.name) {
1186             this.select_field(_.detect(this.fields, function(x) {return x.name == nval;}));
1187         }
1188     },
1189     /**
1190      * Selects the provided field object
1191      *
1192      * @param field a field descriptor object (as returned by fields_get, augmented by the field name)
1193      */
1194     select_field: function(field) {
1195         var _this = this;
1196         if(this.attrs.selected != null) {
1197             this.value.stop();
1198             this.value = null;
1199             this.$element.find('.searchview_extended_prop_op').html('');
1200         }
1201         this.attrs.selected = field;
1202         if(field == null) {
1203             return;
1204         }
1205
1206         var type = field.type;
1207         try {
1208             openerp.web.search.custom_filters.get_object(type);
1209         } catch (e) {
1210             if (! e instanceof openerp.web.KeyNotFound) {
1211                 throw e;
1212             }
1213             type = "char";
1214             console.log('Unknow field type ' + e.key);
1215         }
1216         this.value = new (openerp.web.search.custom_filters.get_object(type))
1217                           (this);
1218         if(this.value.set_field) {
1219             this.value.set_field(field);
1220         }
1221         _.each(this.value.operators, function(operator) {
1222             var option = jQuery('<option>', {value: operator.value})
1223                 .text(operator.text)
1224                 .appendTo(_this.$element.find('.searchview_extended_prop_op'));
1225         });
1226         this.$element.find('.searchview_extended_prop_value').html(
1227             this.value.render({}));
1228         this.value.start();
1229
1230     },
1231     get_proposition: function() {
1232         if ( this.attrs.selected == null)
1233             return null;
1234         var field = this.attrs.selected.name;
1235         var op =  this.$element.find('.searchview_extended_prop_op').val();
1236         var value = this.value.get_value();
1237         return [field, op, value];
1238     }
1239 });
1240
1241 openerp.web.search.ExtendedSearchProposition.Field = openerp.web.OldWidget.extend({
1242     start: function () {
1243         this.$element = $("#" + this.element_id);
1244     }
1245 });
1246 openerp.web.search.ExtendedSearchProposition.Char = openerp.web.search.ExtendedSearchProposition.Field.extend({
1247     template: 'SearchView.extended_search.proposition.char',
1248     identifier_prefix: 'extended-search-proposition-char',
1249     operators: [
1250         {value: "ilike", text: _t("contains")},
1251         {value: "not ilike", text: _t("doesn't contain")},
1252         {value: "=", text: _t("is equal to")},
1253         {value: "!=", text: _t("is not equal to")},
1254         {value: ">", text: _t("greater than")},
1255         {value: "<", text: _t("less than")},
1256         {value: ">=", text: _t("greater or equal than")},
1257         {value: "<=", text: _t("less or equal than")}
1258     ],
1259     get_value: function() {
1260         return this.$element.val();
1261     }
1262 });
1263 openerp.web.search.ExtendedSearchProposition.DateTime = openerp.web.search.ExtendedSearchProposition.Field.extend({
1264     template: 'SearchView.extended_search.proposition.empty',
1265     identifier_prefix: 'extended-search-proposition-datetime',
1266     operators: [
1267         {value: "=", text: _t("is equal to")},
1268         {value: "!=", text: _t("is not equal to")},
1269         {value: ">", text: _t("greater than")},
1270         {value: "<", text: _t("less than")},
1271         {value: ">=", text: _t("greater or equal than")},
1272         {value: "<=", text: _t("less or equal than")}
1273     ],
1274     get_value: function() {
1275         return this.datewidget.get_value();
1276     },
1277     start: function() {
1278         this._super();
1279         this.datewidget = new openerp.web.DateTimeWidget(this);
1280         this.datewidget.prependTo(this.$element);
1281     }
1282 });
1283 openerp.web.search.ExtendedSearchProposition.Date = openerp.web.search.ExtendedSearchProposition.Field.extend({
1284     template: 'SearchView.extended_search.proposition.empty',
1285     identifier_prefix: 'extended-search-proposition-date',
1286     operators: [
1287         {value: "=", text: _t("is equal to")},
1288         {value: "!=", text: _t("is not equal to")},
1289         {value: ">", text: _t("greater than")},
1290         {value: "<", text: _t("less than")},
1291         {value: ">=", text: _t("greater or equal than")},
1292         {value: "<=", text: _t("less or equal than")}
1293     ],
1294     get_value: function() {
1295         return this.datewidget.get_value();
1296     },
1297     start: function() {
1298         this._super();
1299         this.datewidget = new openerp.web.DateWidget(this);
1300         this.datewidget.prependTo(this.$element);
1301     }
1302 });
1303 openerp.web.search.ExtendedSearchProposition.Integer = openerp.web.search.ExtendedSearchProposition.Field.extend({
1304     template: 'SearchView.extended_search.proposition.integer',
1305     identifier_prefix: 'extended-search-proposition-integer',
1306     operators: [
1307         {value: "=", text: _t("is equal to")},
1308         {value: "!=", text: _t("is not equal to")},
1309         {value: ">", text: _t("greater than")},
1310         {value: "<", text: _t("less than")},
1311         {value: ">=", text: _t("greater or equal than")},
1312         {value: "<=", text: _t("less or equal than")}
1313     ],
1314     get_value: function() {
1315         try {
1316             return openerp.web.parse_value(this.$element.val(), {'widget': 'integer'});
1317         } catch (e) {
1318             return "";
1319         }
1320     }
1321 });
1322 openerp.web.search.ExtendedSearchProposition.Id = openerp.web.search.ExtendedSearchProposition.Integer.extend({
1323     operators: [{value: "=", text: _t("is")}]
1324 });
1325 openerp.web.search.ExtendedSearchProposition.Float = openerp.web.search.ExtendedSearchProposition.Field.extend({
1326     template: 'SearchView.extended_search.proposition.float',
1327     identifier_prefix: 'extended-search-proposition-float',
1328     operators: [
1329         {value: "=", text: _t("is equal to")},
1330         {value: "!=", text: _t("is not equal to")},
1331         {value: ">", text: _t("greater than")},
1332         {value: "<", text: _t("less than")},
1333         {value: ">=", text: _t("greater or equal than")},
1334         {value: "<=", text: _t("less or equal than")}
1335     ],
1336     get_value: function() {
1337         try {
1338             return openerp.web.parse_value(this.$element.val(), {'widget': 'float'});
1339         } catch (e) {
1340             return "";
1341         }
1342     }
1343 });
1344 openerp.web.search.ExtendedSearchProposition.Selection = openerp.web.search.ExtendedSearchProposition.Field.extend({
1345     template: 'SearchView.extended_search.proposition.selection',
1346     identifier_prefix: 'extended-search-proposition-selection',
1347     operators: [
1348         {value: "=", text: _t("is")},
1349         {value: "!=", text: _t("is not")}
1350     ],
1351     set_field: function(field) {
1352         this.field = field;
1353     },
1354     get_value: function() {
1355         return this.$element.val();
1356     }
1357 });
1358 openerp.web.search.ExtendedSearchProposition.Boolean = openerp.web.search.ExtendedSearchProposition.Field.extend({
1359     template: 'SearchView.extended_search.proposition.boolean',
1360     identifier_prefix: 'extended-search-proposition-boolean',
1361     operators: [
1362         {value: "=", text: _t("is true")},
1363         {value: "!=", text: _t("is false")}
1364     ],
1365     get_value: function() {
1366         return true;
1367     }
1368 });
1369
1370 openerp.web.search.custom_filters = new openerp.web.Registry({
1371     'char': 'openerp.web.search.ExtendedSearchProposition.Char',
1372     'text': 'openerp.web.search.ExtendedSearchProposition.Char',
1373     'one2many': 'openerp.web.search.ExtendedSearchProposition.Char',
1374     'many2one': 'openerp.web.search.ExtendedSearchProposition.Char',
1375     'many2many': 'openerp.web.search.ExtendedSearchProposition.Char',
1376
1377     'datetime': 'openerp.web.search.ExtendedSearchProposition.DateTime',
1378     'date': 'openerp.web.search.ExtendedSearchProposition.Date',
1379     'integer': 'openerp.web.search.ExtendedSearchProposition.Integer',
1380     'float': 'openerp.web.search.ExtendedSearchProposition.Float',
1381     'boolean': 'openerp.web.search.ExtendedSearchProposition.Boolean',
1382     'selection': 'openerp.web.search.ExtendedSearchProposition.Selection',
1383
1384     'id': 'openerp.web.search.ExtendedSearchProposition.Id'
1385 });
1386
1387 };
1388
1389 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: