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