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