[IMP] improved custom filters.
[odoo/odoo.git] / addons / base / static / src / js / search.js
1 openerp.base.search = function(openerp) {
2
3 openerp.base.SearchView = openerp.base.Controller.extend({
4     init: function(view_manager, session, element_id, dataset, view_id, defaults) {
5         this._super(session, element_id);
6         this.view_manager = view_manager;
7         this.dataset = dataset;
8         this.model = dataset.model;
9         this.view_id = view_id;
10
11         this.defaults = defaults || {};
12
13         this.inputs = [];
14         this.enabled_filters = [];
15
16         this.has_focus = false;
17     },
18     start: function() {
19         //this.log('Starting SearchView '+this.model+this.view_id)
20         return this.rpc("/base/searchview/load", {"model": this.model, "view_id":this.view_id}, this.on_loaded);
21     },
22     show: function () {
23         this.$element.show();
24     },
25     hide: function () {
26         this.$element.hide();
27     },
28     /**
29      * Builds a list of widget rows (each row is an array of widgets)
30      *
31      * @param {Array} items a list of nodes to convert to widgets
32      * @param {Object} fields a mapping of field names to (ORM) field attributes
33      * @returns Array
34      */
35     make_widgets: function (items, fields) {
36         var rows = [],
37             row = [];
38         rows.push(row);
39         var filters = [];
40         _.each(items, function (item) {
41             if (filters.length && item.tag !== 'filter') {
42                 row.push(
43                     new openerp.base.search.FilterGroup(
44                         filters, this));
45                 filters = [];
46             }
47
48             if (item.tag === 'newline') {
49                 row = [];
50                 rows.push(row);
51             } else if (item.tag === 'filter') {
52                 if (!this.has_focus) {
53                     item.attrs.default_focus = '1';
54                     this.has_focus = true;
55                 }
56                 filters.push(
57                     new openerp.base.search.Filter(
58                         item, this));
59             } else if (item.tag === 'separator') {
60                 // a separator is a no-op
61             } else {
62                 if (item.tag === 'group') {
63                     // TODO: group and field should be fetched from registries, maybe even filters
64                     row.push(
65                         new openerp.base.search.Group(
66                             item, this, fields));
67                 } else if (item.tag === 'field') {
68                     if (!this.has_focus) {
69                         item.attrs.default_focus = '1';
70                         this.has_focus = true;
71                     }
72                     row.push(
73                         this.make_field(
74                             item, fields[item['attrs'].name]));
75                 }
76             }
77         }, this);
78         if (filters.length) {
79             row.push(new openerp.base.search.FilterGroup(filters, this));
80         }
81
82         return rows;
83     },
84     /**
85      * Creates a field for the provided field descriptor item (which comes
86      * from fields_view_get)
87      *
88      * @param {Object} item fields_view_get node for the field
89      * @param {Object} field fields_get result for the field
90      * @returns openerp.base.search.Field
91      */
92     make_field: function (item, field) {
93         try {
94             return new (openerp.base.search.fields.get_object(field.type))
95                         (item, field, this);
96         } catch (e) {
97             if (! e instanceof openerp.base.KeyNotFound) {
98                 throw e;
99             }
100             // KeyNotFound means unknown field type
101             console.group('Unknown field type ' + field.type);
102             console.error('View node', item);
103             console.info('View field', field);
104             console.info('In view', this);
105             console.groupEnd();
106             return null;
107         }
108     },
109     on_loaded: function(data) {
110         var lines = this.make_widgets(
111             data.fields_view['arch'].children,
112             data.fields_view.fields);
113
114         // for extended search view
115         var ext = new openerp.base.search.ExtendedSearch(null, this.session, this.model);
116         lines.push([ext]);
117         this.inputs.push(ext);
118         
119         var render = QWeb.render("SearchView", {
120             'view': data.fields_view['arch'],
121             'lines': lines,
122             'defaults': this.defaults
123         });
124         // We don't understand why the following commented line does not work in Chrome but
125         // the non-commented line does. As far as we investigated, only God knows.
126         //this.$element.html(render);
127         jQuery(render).appendTo(this.$element);
128         this.$element.find(".oe_search-view-custom-filter-btn").click(ext.on_activate);
129
130         var f = this.$element.find('form');
131         this.$element.find('form')
132                 .submit(this.do_search)
133                 .bind('reset', this.do_clear);
134         // start() all the widgets
135         _(lines).chain().flatten().each(function (widget) {
136             widget.start();
137         });
138     },
139     /**
140      * Performs the search view collection of widget data.
141      *
142      * If the collection went well (all fields are valid), then triggers
143      * :js:func:`openerp.base.SearchView.on_search`.
144      *
145      * If at least one field failed its validation, triggers
146      * :js:func:`openerp.base.SearchView.on_invalid` instead.
147      *
148      * @param e jQuery event object coming from the "Search" button
149      */
150     do_search: function (e) {
151         if (e && e.preventDefault) { e.preventDefault(); }
152
153         var domains = [], contexts = [];
154
155         var errors = [];
156
157         _.each(this.inputs, function (input) {
158             try {
159                 var domain = input.get_domain();
160                 if (domain) {
161                     domains.push(domain);
162                 }
163
164                 var context = input.get_context();
165                 if (context) {
166                     contexts.push(context);
167                 }
168             } catch (e) {
169                 if (e instanceof openerp.base.search.Invalid) {
170                     errors.push(e);
171                 } else {
172                     throw e;
173                 }
174             }
175         });
176
177         if (errors.length) {
178             this.on_invalid(errors);
179             return;
180         }
181
182         // TODO: do we need to handle *fields* with group_by in their context?
183         var groupbys = _(this.enabled_filters)
184                 .chain()
185                 .map(function (filter) { return filter.get_context();})
186                 .compact()
187                 .value();
188
189         this.on_search(domains, contexts, groupbys);
190     },
191     /**
192      * Triggered after the SearchView has collected all relevant domains and
193      * contexts.
194      *
195      * It is provided with an Array of domains and an Array of contexts, which
196      * may or may not be evaluated (each item can be either a valid domain or
197      * context, or a string to evaluate in order in the sequence)
198      *
199      * It is also passed an array of contexts used for group_by (they are in
200      * the correct order for group_by evaluation, which contexts may not be)
201      *
202      * @event
203      * @param {Array} domains an array of literal domains or domain references
204      * @param {Array} contexts an array of literal contexts or context refs
205      * @param {Array} groupbys ordered contexts which may or may not have group_by keys
206      */
207     on_search: function (domains, contexts, groupbys) {
208     },
209     /**
210      * Triggered after a validation error in the SearchView fields.
211      *
212      * Error objects have three keys:
213      * * ``field`` is the name of the invalid field
214      * * ``value`` is the invalid value
215      * * ``message`` is the (in)validation message provided by the field
216      *
217      * @event
218      * @param {Array} errors a never-empty array of error objects
219      */
220     on_invalid: function (errors) {
221         this.notification.notify("Invalid Search", "triggered from search view");
222     },
223     do_clear: function (e) {
224         if (e && e.preventDefault) { e.preventDefault(); }
225         this.on_clear();
226     },
227     /**
228      * Triggered when the search view gets cleared
229      *
230      * @event
231      */
232     on_clear: function () {  },
233     /**
234      * Called by a filter propagating its state changes
235      *
236      * @param {openerp.base.search.Filter} filter a filter which got toggled
237      * @param {Boolean} default_enabled filter got enabled through the default values, at render time.
238      */
239     do_toggle_filter: function (filter, default_enabled) {
240         if (default_enabled || filter.is_enabled()) {
241             this.enabled_filters.push(filter);
242         } else {
243             this.enabled_filters = _.without(
244                 this.enabled_filters, filter);
245         }
246
247         if (!default_enabled) {
248             // selecting a filter after initial loading automatically
249             // triggers refresh
250             this.$element.find('form').submit();
251         }
252     }
253 });
254
255 /** @namespace */
256 openerp.base.search = {};
257 /**
258  * Registry of search fields, called by :js:class:`openerp.base.SearchView` to
259  * find and instantiate its field widgets.
260  */
261 openerp.base.search.fields = new openerp.base.Registry({
262     'char': 'openerp.base.search.CharField',
263     'text': 'openerp.base.search.CharField',
264     'boolean': 'openerp.base.search.BooleanField',
265     'integer': 'openerp.base.search.IntegerField',
266     'float': 'openerp.base.search.FloatField',
267     'selection': 'openerp.base.search.SelectionField',
268     'datetime': 'openerp.base.search.DateTimeField',
269     'date': 'openerp.base.search.DateField',
270     'one2many': 'openerp.base.search.OneToManyField',
271     'many2one': 'openerp.base.search.ManyToOneField',
272     'many2many': 'openerp.base.search.ManyToManyField'
273 });
274 openerp.base.search.Invalid = Class.extend(
275     /** @lends openerp.base.search.Invalid# */{
276     /**
277      * Exception thrown by search widgets when they hold invalid values,
278      * which they can not return when asked.
279      *
280      * @constructs
281      * @param field the name of the field holding an invalid value
282      * @param value the invalid value
283      * @param message validation failure message
284      */
285     init: function (field, value, message) {
286         this.field = field;
287         this.value = value;
288         this.message = message;
289     },
290     toString: function () {
291         return ('Incorrect value for field ' + this.field +
292                 ': [' + this.value + '] is ' + this.message);
293     }
294 });
295 openerp.base.search.Widget = openerp.base.Controller.extend(
296     /** @lends openerp.base.search.Widget# */{
297     template: null,
298     /**
299      * Root class of all search widgets
300      *
301      * @constructs
302      * @extends openerp.base.Controller
303      *
304      * @param view the ancestor view of this widget
305      */
306     init: function (view) {
307         this.view = view;
308     },
309     /**
310      * Sets and returns a globally unique identifier for the widget.
311      *
312      * If a prefix is specified, the identifier will be appended to it.
313      *
314      * @params prefix prefix sections, empty/falsy sections will be removed
315      */
316     make_id: function () {
317         this.element_id = _.uniqueId(
318             ['search'].concat(
319                 _.compact(_.toArray(arguments)),
320                 ['']).join('_'));
321         return this.element_id;
322     },
323     /**
324      * "Starts" the widgets. Called at the end of the rendering, this allows
325      * widgets to hook themselves to their view sections.
326      *
327      * On widgets, if they kept a reference to a view and have an element_id,
328      * will fetch and set their root element on $element.
329      */
330     start: function () {
331         this._super();
332         if (this.view && this.element_id) {
333             // id is unique, and no getElementById on elements
334             this.$element = $(document.getElementById(
335                 this.element_id));
336         }
337     },
338     /**
339      * "Stops" the widgets. Called when the view destroys itself, this
340      * lets the widgets clean up after themselves.
341      */
342     stop: function () {
343         delete this.view;
344         this._super();
345     },
346     render: function (defaults) {
347         return QWeb.render(
348             this.template, _.extend(this, {
349                 defaults: defaults
350         }));
351     }
352 });
353 openerp.base.search.FilterGroup = openerp.base.search.Widget.extend({
354     template: 'SearchView.filters',
355     init: function (filters, view) {
356         this._super(view);
357         this.filters = filters;
358     },
359     start: function () {
360         this._super();
361         _.each(this.filters, function (filter) {
362             filter.start();
363         });
364     }
365 });
366 openerp.base.search.add_expand_listener = function($root) {
367     $root.find('a.searchview_group_string').click(function (e) {
368         $root.toggleClass('folded expanded');
369         e.stopPropagation();
370         e.preventDefault();
371     });
372 };
373 openerp.base.search.Group = openerp.base.search.Widget.extend({
374     template: 'SearchView.group',
375     // TODO: contain stuff
376     // TODO: @expand
377     init: function (view_section, view, fields) {
378         this._super(view);
379         this.attrs = view_section.attrs;
380         this.lines = view.make_widgets(
381             view_section.children, fields);
382         this.make_id('group');
383     },
384     start: function () {
385         this._super();
386         _(this.lines)
387             .chain()
388             .flatten()
389             .each(function (widget) { widget.start(); });
390         openerp.base.search.add_expand_listener(this.$element);
391     }
392 });
393
394 openerp.base.search.Input = openerp.base.search.Widget.extend(
395     /** @lends openerp.base.search.Input# */{
396     /**
397      * @constructs
398      * @extends openerp.base.search.Widget
399      *
400      * @param view
401      */
402     init: function (view) {
403         this._super(view);
404         this.view.inputs.push(this);
405     },
406     get_context: function () {
407         throw new Error(
408             "get_context not implemented for widget " + this.attrs.type);
409     },
410     get_domain: function () {
411         throw new Error(
412             "get_domain not implemented for widget " + this.attrs.type);
413     }
414 });
415 openerp.base.search.Filter = openerp.base.search.Input.extend({
416     template: 'SearchView.filter',
417     // TODO: force rendering
418     init: function (node, view) {
419         this._super(view);
420         this.attrs = node.attrs;
421         this.classes = [this.attrs.string ? 'filter_label' : 'filter_icon'];
422         this.make_id('filter', this.attrs.name);
423     },
424     start: function () {
425         this._super();
426         var self = this;
427         this.$element.click(function (e) {
428             $(this).toggleClass('enabled');
429             self.view.do_toggle_filter(self);
430         });
431     },
432     /**
433      * Returns whether the filter is currently enabled (in use) or not.
434      *
435      * @returns a boolean
436      */
437     is_enabled:function () {
438         return this.$element.hasClass('enabled');
439     },
440     /**
441      * If the filter is present in the defaults (and has a truthy value),
442      * enable the filter.
443      *
444      * @param {Object} defaults the search view's default values
445      */
446     render: function (defaults) {
447         if (this.attrs.name && defaults[this.attrs.name]) {
448             this.classes.push('enabled');
449             this.view.do_toggle_filter(this, true);
450         }
451         return this._super(defaults);
452     },
453     get_context: function () {
454         if (!this.is_enabled()) {
455             return;
456         }
457         return this.attrs.context;
458     },
459     get_domain: function () {
460         if (!this.is_enabled()) {
461             return;
462         }
463         return this.attrs.domain;
464     }
465 });
466 openerp.base.search.Field = openerp.base.search.Input.extend(
467     /** @lends openerp.base.search.Field# */ {
468     template: 'SearchView.field',
469     default_operator: '=',
470     // TODO: set default values
471     // TODO: get context, domain
472     // TODO: holds Filters
473     /**
474      * @constructs
475      * @extends openerp.base.search.Input
476      *
477      * @param view_section
478      * @param field
479      * @param view
480      */
481     init: function (view_section, field, view) {
482         this._super(view);
483         this.attrs = _.extend({}, field, view_section.attrs);
484         this.filters = new openerp.base.search.FilterGroup(_.map(
485             view_section.children, function (filter_node) {
486                 return new openerp.base.search.Filter(
487                     filter_node, view);
488         }), view);
489         this.make_id('input', field.type, this.attrs.name);
490     },
491     start: function () {
492         this._super();
493         this.filters.start();
494     },
495     get_context: function () {
496         var val = this.get_value();
497         // A field needs a value to be "active", and a context to send when
498         // active
499         var has_value = (val !== null && val !== '');
500         var context = this.attrs.context;
501         if (!(has_value && context)) {
502             return;
503         }
504         return _.extend(
505             {}, context,
506             {own_values: {self: val}});
507     },
508     get_domain: function () {
509         var val = this.get_value();
510
511         var has_value = (val !== null && val !== '');
512         if(!has_value) {
513             return;
514         }
515
516         var domain = this.attrs['filter_domain'];
517         if (!domain) {
518             return [[
519                 this.attrs.name,
520                 this.attrs.operator || this.default_operator,
521                 this.get_value()
522             ]];
523         }
524         return _.extend(
525             {}, domain,
526             {own_values: {self: val}});
527     }
528 });
529 /**
530  * Implementation of the ``char`` OpenERP field type:
531  *
532  * * Default operator is ``ilike`` rather than ``=``
533  *
534  * * The Javascript and the HTML values are identical (strings)
535  *
536  * @class
537  * @extends openerp.base.search.Field
538  */
539 openerp.base.search.CharField = openerp.base.search.Field.extend(
540     /** @lends openerp.base.search.CharField# */ {
541     default_operator: 'ilike',
542     get_value: function () {
543         return this.$element.val();
544     }
545 });
546 openerp.base.search.BooleanField = openerp.base.search.Field.extend({
547     template: 'SearchView.field.selection',
548     init: function () {
549         this._super.apply(this, arguments);
550         this.attrs.selection = [
551             ['true', 'Yes'],
552             ['false', 'No']
553         ];
554     },
555     /**
556      * Search defaults likely to be boolean values (for a boolean field).
557      *
558      * In the HTML, we only get strings, and our strings here are
559      * <code>'true'</code> and <code>'false'</code>, so ensure we get only
560      * those by truth-testing the default value.
561      *
562      * @param {Object} defaults default values for this search view
563      */
564     render: function (defaults) {
565         var name = this.attrs.name;
566         if (name in defaults) {
567             defaults[name] = defaults[name] ? "true" : "false";
568         }
569         return this._super(defaults);
570     },
571     get_value: function () {
572         switch (this.$element.val()) {
573             case 'false': return false;
574             case 'true': return true;
575             default: return null;
576         }
577     }
578 });
579 openerp.base.search.IntegerField = openerp.base.search.Field.extend({
580     get_value: function () {
581         if (!this.$element.val()) {
582             return null;
583         }
584         var val = parseInt(this.$element.val());
585         var check = Number(this.$element.val());
586         if (isNaN(check) || val !== check) {
587             this.$element.addClass('error');
588             throw new openerp.base.search.Invalid(
589                 this.attrs.name, this.$element.val(), "not a valid integer");
590         }
591         this.$element.removeClass('error');
592         return val;
593     }
594 });
595 openerp.base.search.FloatField = openerp.base.search.Field.extend({
596     get_value: function () {
597         var val = Number(this.$element.val());
598         if (isNaN(val)) {
599             this.$element.addClass('error');
600             throw new openerp.base.search.Invalid(
601                 this.attrs.name, this.$element.val(), "not a valid number");
602         }
603         this.$element.removeClass('error');
604         return val;
605     }
606 });
607 openerp.base.search.SelectionField = openerp.base.search.Field.extend({
608     template: 'SearchView.field.selection',
609     get_value: function () {
610         return this.$element.val();
611     }
612 });
613 /**
614  * @class
615  * @extends openerp.base.search.Field
616  */
617 openerp.base.search.DateField = openerp.base.search.Field.extend(
618     /** @lends openerp.base.search.DateField# */{
619     template: 'SearchView.fields.date',
620     /**
621      * enables date picker on the HTML widgets
622      */
623     start: function () {
624         this._super();
625         this.$element.find('input').datepicker({
626             dateFormat: 'yy-mm-dd'
627         });
628     },
629     stop: function () {
630         this.$element.find('input').datepicker('destroy');
631     },
632     /**
633      * Returns an object with two optional keys ``from`` and ``to`` providing
634      * the values for resp. the from and to sections of the date widget.
635      *
636      * If a key is absent, then the corresponding field was not filled.
637      *
638      * @returns {Object}
639      */
640     get_values: function () {
641         var values_array = this.$element.find('input').serializeArray();
642
643         var from = values_array[0].value;
644         var to = values_array[1].value;
645
646         var field_values = {};
647         if (from) {
648             field_values.from = from;
649         }
650         if (to) {
651             field_values.to = to;
652         }
653         return field_values;
654     },
655     get_context: function () {
656         var values = this.get_values();
657         if (!this.attrs.context || _.isEmpty(values)) {
658             return null;
659         }
660         return _.extend(
661             {}, this.attrs.context,
662             {own_values: {self: values}});
663     },
664     get_domain: function () {
665         var values = this.get_values();
666         if (_.isEmpty(values)) {
667             return null;
668         }
669         var domain = this.attrs['filter_domain'];
670         if (!domain) {
671             domain = [];
672             if (values.from) {
673                 domain.push([this.attrs.name, '>=', values.from]);
674             }
675             if (values.to) {
676                 domain.push([this.attrs.name, '<=', values.to]);
677             }
678             return domain;
679         }
680
681         return _.extend(
682                 {}, domain,
683                 {own_values: {self: values}});
684     }
685 });
686 openerp.base.search.DateTimeField = openerp.base.search.DateField.extend({
687     // TODO: time?
688 });
689 openerp.base.search.OneToManyField = openerp.base.search.IntegerField.extend({
690     // TODO: .relation, .context, .domain
691 });
692 openerp.base.search.ManyToOneField = openerp.base.search.IntegerField.extend({
693     // TODO: @widget
694     // TODO: .relation, .selection, .context, .domain
695 });
696 openerp.base.search.ManyToManyField = openerp.base.search.IntegerField.extend({
697     // TODO: .related_columns (Array), .context, .domain
698 });
699
700 openerp.base.search.ExtendedSearch = openerp.base.BaseWidget.extend({
701     template: 'SearchView.extended_search',
702     identifier_prefix: 'extended-search',
703     init: function (parent, session, model) {
704         this._super(parent, session);
705         this.model = model;
706     },
707     add_group: function() {
708         var group = new openerp.base.search.ExtendedSearchGroup(this, this.fields);
709         var render = group.render({'index': this.children.length - 1});
710         this.$element.find('.searchview_extended_groups_list').append(render);
711         group.start();
712     },
713     start: function () {
714         this._super();
715         this.$element.closest("table.oe-searchview-render-line").css("display", "none");
716         var self = this;
717         this.rpc("/base/searchview/fields_get",
718             {"model": this.model}, function(data) {
719             self.fields = data.fields;
720             openerp.base.search.add_expand_listener(self.$element);
721             self.add_group();
722             self.$element.find('.searchview_extended_add_group').click(function (e) {
723                 self.add_group();
724             });
725         });
726     },
727     get_context: function() {
728         return null;
729     },
730     get_domain: function() {
731         if(this.$element.closest("table.oe-searchview-render-line").css("display") == "none") {
732             return null;
733         }
734         return _.reduce(this.children,
735             function(mem, x) { return mem.concat(x.get_domain());}, []);
736     },
737     on_activate: function() {
738         var table = this.$element.closest("table.oe-searchview-render-line");
739         if (table.css("display") == "none") {
740             table.css("display", "");
741             if(this.$element.hasClass("folded")) {
742                 this.$element.toggleClass("folded expanded");
743             }
744         } else {
745             table.css("display", "none");
746             if(this.$element.hasClass("expanded")) {
747                 this.$element.toggleClass("folded expanded");
748             }
749         }
750     }
751 });
752
753 openerp.base.search.ExtendedSearchGroup = openerp.base.BaseWidget.extend({
754     template: 'SearchView.extended_search.group',
755     identifier_prefix: 'extended-search-group',
756     init: function (parent, fields) {
757         this._super(parent);
758         this.fields = fields;
759     },
760     add_prop: function() {
761         var prop = new openerp.base.search.ExtendedSearchProposition(this, this.fields);
762         var render = prop.render({'index': this.children.length - 1});
763         this.$element.find('.searchview_extended_propositions_list').append(render);
764         prop.start();
765     },
766     start: function () {
767         this._super();
768         var _this = this;
769         this.add_prop();
770         this.$element.find('.searchview_extended_add_proposition').click(function (e) {
771             _this.add_prop();
772         });
773         var delete_btn = this.$element.find('.searchview_extended_delete_group');
774         delete_btn.click(function (e) {
775             _this.stop();
776         });
777     },
778     get_domain: function() {
779         var props = _(this.children).chain().map(function(x) {
780             return x.get_proposition();
781         }).compact().value();
782         var choice = this.$element.find(".searchview_extended_group_choice").val();
783         var op = choice == "all" ? "&" : "|";
784         return [].concat(choice == "none" ? ['!'] : [],
785             _.map(_.range(_.max([0,props.length - 1])), function() { return op; }),
786             props);
787     }
788 });
789
790 openerp.base.search.ExtendedSearchProposition = openerp.base.BaseWidget.extend({
791     template: 'SearchView.extended_search.proposition',
792     identifier_prefix: 'extended-search-proposition',
793     init: function (parent, fields) {
794         this._super(parent);
795         this.fields = _(fields).chain()
796             .map(function(val, key) { return _.extend({}, val, {'name': key}); })
797             .sortBy(function(field) {return field.string;})
798             .value();
799         this.attrs = {_: _, fields: this.fields, selected: null};
800         this.value = null;
801     },
802     start: function () {
803         this._super();
804         this.select_field(this.fields.length > 0 ? this.fields[0] : null);
805         var _this = this;
806         this.$element.find(".searchview_extended_prop_field").change(function() {
807             _this.changed();
808         });
809         var delete_btn = this.$element.find('.searchview_extended_delete_prop');
810         delete_btn.click(function (e) {
811             _this.stop();
812         });
813     },
814     changed: function() {
815         var nval = this.$element.find(".searchview_extended_prop_field").val();
816         if(this.attrs.selected == null || nval != this.attrs.selected.name) {
817             this.select_field(_.detect(this.fields, function(x) {return x.name == nval;}));
818         }
819     },
820     /**
821      * Selects the provided field object
822      *
823      * @param field a field descriptor object (as returned by fields_get, augmented by the field name)
824      */
825     select_field: function(field) {
826         var _this = this;
827         if(this.attrs.selected != null) {
828             this.value.stop();
829             this.value = null;
830             this.$element.find('.searchview_extended_prop_op').html('');
831         }
832         this.attrs.selected = field;
833         if(field == null) {
834             return;
835         }
836
837         var type = field.type;
838         try {
839             openerp.base.search.custom_filters.get_object(type);
840         } catch (e) {
841             if (! e instanceof openerp.base.KeyNotFound) {
842                 throw e;
843             }
844             var type = "char";
845             this.log('Unknow field type ' + e.key);
846         }
847         this.value = new (openerp.base.search.custom_filters.get_object(type))
848                           (this);
849         if(this.value.set_field) {
850             this.value.set_field(field);
851         }
852         _.each(this.value.operators, function(operator) {
853             var option = jQuery('<option>', {value: operator.value})
854                 .text(operator.text)
855                 .appendTo(_this.$element.find('.searchview_extended_prop_op'));
856         });
857         this.$element.find('.searchview_extended_prop_value').html(
858             this.value.render({}));
859         this.value.start();
860         
861     },
862     get_proposition: function() {
863         if ( this.attrs.selected == null)
864             return null;
865         var field = this.attrs.selected.name;
866         var op =  this.$element.find('.searchview_extended_prop_op').val();
867         var value = this.value.get_value();
868         return [field, op, value];
869     }
870 });
871
872 openerp.base.search.ExtendedSearchProposition.Char = openerp.base.BaseWidget.extend({
873     template: 'SearchView.extended_search.proposition.char',
874     identifier_prefix: 'extended-search-proposition-char',
875     operators: [
876         {value: "ilike", text: "contains"},
877         {value: "not ilike", text: "doesn't contain"},
878         {value: "=", text: "is equal to"},
879         {value: "!=", text: "is not equal to"},
880         {value: ">", text: "greater than"},
881         {value: "<", text: "less than"},
882         {value: ">=", text: "greater or equal than"},
883         {value: "<=", text: "less or equal than"}
884     ],
885     get_value: function() {
886         return this.$element.val();
887     }
888 });
889 openerp.base.search.ExtendedSearchProposition.DateTime = openerp.base.BaseWidget.extend({
890     template: 'SearchView.extended_search.proposition.datetime',
891     identifier_prefix: 'extended-search-proposition-datetime',
892     operators: [
893         {value: "=", text: "is equal to"},
894         {value: "!=", text: "is not equal to"},
895         {value: ">", text: "greater than"},
896         {value: "<", text: "less than"},
897         {value: ">=", text: "greater or equal than"},
898         {value: "<=", text: "less or equal than"}
899     ],
900     get_value: function() {
901         return this.$element.val();
902     },
903     start: function() {
904         this._super();
905         this.$element.datetimepicker({
906             dateFormat: 'yy-mm-dd',
907             timeFormat: 'hh:mm:ss'
908         });
909     }
910 });
911 openerp.base.search.ExtendedSearchProposition.Date = openerp.base.BaseWidget.extend({
912     template: 'SearchView.extended_search.proposition.date',
913     identifier_prefix: 'extended-search-proposition-date',
914     operators: [
915         {value: "=", text: "is equal to"},
916         {value: "!=", text: "is not equal to"},
917         {value: ">", text: "greater than"},
918         {value: "<", text: "less than"},
919         {value: ">=", text: "greater or equal than"},
920         {value: "<=", text: "less or equal than"}
921     ],
922     get_value: function() {
923         return this.$element.val();
924     },
925     start: function() {
926         this._super();
927         this.$element.datepicker({
928             dateFormat: 'yy-mm-dd',
929             timeFormat: 'hh:mm:ss'
930         });
931     }
932 });
933 openerp.base.search.ExtendedSearchProposition.Integer = openerp.base.BaseWidget.extend({
934     template: 'SearchView.extended_search.proposition.integer',
935     identifier_prefix: 'extended-search-proposition-integer',
936     operators: [
937         {value: "=", text: "is equal to"},
938         {value: "!=", text: "is not equal to"},
939         {value: ">", text: "greater than"},
940         {value: "<", text: "less than"},
941         {value: ">=", text: "greater or equal than"},
942         {value: "<=", text: "less or equal than"}
943     ],
944     get_value: function() {
945         val = this.$element.val();
946         val2 = parseFloat(val);
947         if(val2 != 0 && !val2) {
948             return "";
949         }
950         return Math.round(val2);
951     }
952 });
953 openerp.base.search.ExtendedSearchProposition.Float = openerp.base.BaseWidget.extend({
954     template: 'SearchView.extended_search.proposition.float',
955     identifier_prefix: 'extended-search-proposition-float',
956     operators: [
957         {value: "=", text: "is equal to"},
958         {value: "!=", text: "is not equal to"},
959         {value: ">", text: "greater than"},
960         {value: "<", text: "less than"},
961         {value: ">=", text: "greater or equal than"},
962         {value: "<=", text: "less or equal than"}
963     ],
964     get_value: function() {
965         val = this.$element.val();
966         val2 = parseFloat(val);
967         if(val2 != 0 && !val2) {
968             return "";
969         }
970         return val2;
971     }
972 });
973 openerp.base.search.ExtendedSearchProposition.Selection = openerp.base.BaseWidget.extend({
974     template: 'SearchView.extended_search.proposition.selection',
975     identifier_prefix: 'extended-search-proposition-selection',
976     operators: [
977         {value: "=", text: "is"},
978         {value: "!=", text: "is not"}
979     ],
980     set_field: function(field) {
981         this.field = field;
982     },
983     get_value: function() {
984         return this.$element.val();
985     }
986 });
987 openerp.base.search.ExtendedSearchProposition.Boolean = openerp.base.BaseWidget.extend({
988     template: 'SearchView.extended_search.proposition.boolean',
989     identifier_prefix: 'extended-search-proposition-boolean',
990     operators: [
991         {value: "=", text: "is true"},
992         {value: "!=", text: "is false"}
993     ],
994     get_value: function() {
995         return true;
996     }
997 });
998
999 openerp.base.search.custom_filters = new openerp.base.Registry({
1000     'char': 'openerp.base.search.ExtendedSearchProposition.Char',
1001     'text': 'openerp.base.search.ExtendedSearchProposition.Char',
1002     'one2many': 'openerp.base.search.ExtendedSearchProposition.Char',
1003     'many2one': 'openerp.base.search.ExtendedSearchProposition.Char',
1004     'many2many': 'openerp.base.search.ExtendedSearchProposition.Char',
1005     
1006     'datetime': 'openerp.base.search.ExtendedSearchProposition.DateTime',
1007     'date': 'openerp.base.search.ExtendedSearchProposition.Date',
1008     'integer': 'openerp.base.search.ExtendedSearchProposition.Integer',
1009     'float': 'openerp.base.search.ExtendedSearchProposition.Float',
1010     'boolean': 'openerp.base.search.ExtendedSearchProposition.Boolean',
1011     'selection': 'openerp.base.search.ExtendedSearchProposition.Selection'
1012 });
1013
1014 };
1015
1016 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: