1 openerp.base.search = function(openerp) {
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;
11 this.defaults = defaults || {};
14 this.enabled_filters = [];
16 this.has_focus = false;
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);
29 * Builds a list of widget rows (each row is an array of widgets)
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
35 make_widgets: function (items, fields) {
40 _.each(items, function (item) {
41 if (filters.length && item.tag !== 'filter') {
43 new openerp.base.search.FilterGroup(
48 if (item.tag === 'newline') {
51 } else if (item.tag === 'filter') {
52 if (!this.has_focus) {
53 item.attrs.default_focus = '1';
54 this.has_focus = true;
57 new openerp.base.search.Filter(
59 } else if (item.tag === 'separator') {
60 // a separator is a no-op
62 if (item.tag === 'group') {
63 // TODO: group and field should be fetched from registries, maybe even filters
65 new openerp.base.search.Group(
67 } else if (item.tag === 'field') {
68 if (!this.has_focus) {
69 item.attrs.default_focus = '1';
70 this.has_focus = true;
74 item, fields[item['attrs'].name]));
79 row.push(new openerp.base.search.FilterGroup(filters, this));
85 * Creates a field for the provided field descriptor item (which comes
86 * from fields_view_get)
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
92 make_field: function (item, field) {
94 return new (openerp.base.search.fields.get_object(field.type))
97 if (! e instanceof openerp.base.KeyNotFound) {
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);
109 on_loaded: function(data) {
110 var lines = this.make_widgets(
111 data.fields_view['arch'].children,
112 data.fields_view.fields);
114 // for extended search view
115 var ext = new openerp.base.search.ExtendedSearch(null, this.session, this.model);
117 this.inputs.push(ext);
119 var render = QWeb.render("SearchView", {
120 'view': data.fields_view['arch'],
122 'defaults': this.defaults
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);
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) {
140 * Performs the search view collection of widget data.
142 * If the collection went well (all fields are valid), then triggers
143 * :js:func:`openerp.base.SearchView.on_search`.
145 * If at least one field failed its validation, triggers
146 * :js:func:`openerp.base.SearchView.on_invalid` instead.
148 * @param e jQuery event object coming from the "Search" button
150 do_search: function (e) {
151 if (e && e.preventDefault) { e.preventDefault(); }
153 var domains = [], contexts = [];
157 _.each(this.inputs, function (input) {
159 var domain = input.get_domain();
161 domains.push(domain);
164 var context = input.get_context();
166 contexts.push(context);
169 if (e instanceof openerp.base.search.Invalid) {
178 this.on_invalid(errors);
182 // TODO: do we need to handle *fields* with group_by in their context?
183 var groupbys = _(this.enabled_filters)
185 .map(function (filter) { return filter.get_context();})
189 this.on_search(domains, contexts, groupbys);
192 * Triggered after the SearchView has collected all relevant domains and
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)
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)
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
207 on_search: function (domains, contexts, groupbys) {
210 * Triggered after a validation error in the SearchView fields.
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
218 * @param {Array} errors a never-empty array of error objects
220 on_invalid: function (errors) {
221 this.notification.notify("Invalid Search", "triggered from search view");
223 do_clear: function (e) {
224 if (e && e.preventDefault) { e.preventDefault(); }
228 * Triggered when the search view gets cleared
232 on_clear: function () { },
234 * Called by a filter propagating its state changes
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.
239 do_toggle_filter: function (filter, default_enabled) {
240 if (default_enabled || filter.is_enabled()) {
241 this.enabled_filters.push(filter);
243 this.enabled_filters = _.without(
244 this.enabled_filters, filter);
247 if (!default_enabled) {
248 // selecting a filter after initial loading automatically
250 this.$element.find('form').submit();
256 openerp.base.search = {};
258 * Registry of search fields, called by :js:class:`openerp.base.SearchView` to
259 * find and instantiate its field widgets.
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'
274 openerp.base.search.Invalid = Class.extend(
275 /** @lends openerp.base.search.Invalid# */{
277 * Exception thrown by search widgets when they hold invalid values,
278 * which they can not return when asked.
281 * @param field the name of the field holding an invalid value
282 * @param value the invalid value
283 * @param message validation failure message
285 init: function (field, value, message) {
288 this.message = message;
290 toString: function () {
291 return ('Incorrect value for field ' + this.field +
292 ': [' + this.value + '] is ' + this.message);
295 openerp.base.search.Widget = openerp.base.Controller.extend(
296 /** @lends openerp.base.search.Widget# */{
299 * Root class of all search widgets
302 * @extends openerp.base.Controller
304 * @param view the ancestor view of this widget
306 init: function (view) {
310 * Sets and returns a globally unique identifier for the widget.
312 * If a prefix is specified, the identifier will be appended to it.
314 * @params prefix prefix sections, empty/falsy sections will be removed
316 make_id: function () {
317 this.element_id = _.uniqueId(
319 _.compact(_.toArray(arguments)),
321 return this.element_id;
324 * "Starts" the widgets. Called at the end of the rendering, this allows
325 * widgets to hook themselves to their view sections.
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.
332 if (this.view && this.element_id) {
333 // id is unique, and no getElementById on elements
334 this.$element = $(document.getElementById(
339 * "Stops" the widgets. Called when the view destroys itself, this
340 * lets the widgets clean up after themselves.
346 render: function (defaults) {
348 this.template, _.extend(this, {
353 openerp.base.search.FilterGroup = openerp.base.search.Widget.extend({
354 template: 'SearchView.filters',
355 init: function (filters, view) {
357 this.filters = filters;
361 _.each(this.filters, function (filter) {
366 openerp.base.search.add_expand_listener = function($root) {
367 $root.find('a.searchview_group_string').click(function (e) {
368 $root.toggleClass('folded expanded');
373 openerp.base.search.Group = openerp.base.search.Widget.extend({
374 template: 'SearchView.group',
375 // TODO: contain stuff
377 init: function (view_section, view, fields) {
379 this.attrs = view_section.attrs;
380 this.lines = view.make_widgets(
381 view_section.children, fields);
382 this.make_id('group');
389 .each(function (widget) { widget.start(); });
390 openerp.base.search.add_expand_listener(this.$element);
394 openerp.base.search.Input = openerp.base.search.Widget.extend(
395 /** @lends openerp.base.search.Input# */{
398 * @extends openerp.base.search.Widget
402 init: function (view) {
404 this.view.inputs.push(this);
406 get_context: function () {
408 "get_context not implemented for widget " + this.attrs.type);
410 get_domain: function () {
412 "get_domain not implemented for widget " + this.attrs.type);
415 openerp.base.search.Filter = openerp.base.search.Input.extend({
416 template: 'SearchView.filter',
417 // TODO: force rendering
418 init: function (node, view) {
420 this.attrs = node.attrs;
421 this.classes = [this.attrs.string ? 'filter_label' : 'filter_icon'];
422 this.make_id('filter', this.attrs.name);
427 this.$element.click(function (e) {
428 $(this).toggleClass('enabled');
429 self.view.do_toggle_filter(self);
433 * Returns whether the filter is currently enabled (in use) or not.
437 is_enabled:function () {
438 return this.$element.hasClass('enabled');
441 * If the filter is present in the defaults (and has a truthy value),
444 * @param {Object} defaults the search view's default values
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);
451 return this._super(defaults);
453 get_context: function () {
454 if (!this.is_enabled()) {
457 return this.attrs.context;
459 get_domain: function () {
460 if (!this.is_enabled()) {
463 return this.attrs.domain;
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
475 * @extends openerp.base.search.Input
477 * @param view_section
481 init: function (view_section, field, 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(
489 this.make_id('input', field.type, this.attrs.name);
493 this.filters.start();
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
499 var has_value = (val !== null && val !== '');
500 var context = this.attrs.context;
501 if (!(has_value && context)) {
506 {own_values: {self: val}});
508 get_domain: function () {
509 var val = this.get_value();
511 var has_value = (val !== null && val !== '');
516 var domain = this.attrs['filter_domain'];
520 this.attrs.operator || this.default_operator,
526 {own_values: {self: val}});
530 * Implementation of the ``char`` OpenERP field type:
532 * * Default operator is ``ilike`` rather than ``=``
534 * * The Javascript and the HTML values are identical (strings)
537 * @extends openerp.base.search.Field
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();
546 openerp.base.search.BooleanField = openerp.base.search.Field.extend({
547 template: 'SearchView.field.selection',
549 this._super.apply(this, arguments);
550 this.attrs.selection = [
556 * Search defaults likely to be boolean values (for a boolean field).
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.
562 * @param {Object} defaults default values for this search view
564 render: function (defaults) {
565 var name = this.attrs.name;
566 if (name in defaults) {
567 defaults[name] = defaults[name] ? "true" : "false";
569 return this._super(defaults);
571 get_value: function () {
572 switch (this.$element.val()) {
573 case 'false': return false;
574 case 'true': return true;
575 default: return null;
579 openerp.base.search.IntegerField = openerp.base.search.Field.extend({
580 get_value: function () {
581 if (!this.$element.val()) {
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");
591 this.$element.removeClass('error');
595 openerp.base.search.FloatField = openerp.base.search.Field.extend({
596 get_value: function () {
597 var val = Number(this.$element.val());
599 this.$element.addClass('error');
600 throw new openerp.base.search.Invalid(
601 this.attrs.name, this.$element.val(), "not a valid number");
603 this.$element.removeClass('error');
607 openerp.base.search.SelectionField = openerp.base.search.Field.extend({
608 template: 'SearchView.field.selection',
609 get_value: function () {
610 return this.$element.val();
615 * @extends openerp.base.search.Field
617 openerp.base.search.DateField = openerp.base.search.Field.extend(
618 /** @lends openerp.base.search.DateField# */{
619 template: 'SearchView.fields.date',
621 * enables date picker on the HTML widgets
625 this.$element.find('input').datepicker({
626 dateFormat: 'yy-mm-dd'
630 this.$element.find('input').datepicker('destroy');
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.
636 * If a key is absent, then the corresponding field was not filled.
640 get_values: function () {
641 var values_array = this.$element.find('input').serializeArray();
643 var from = values_array[0].value;
644 var to = values_array[1].value;
646 var field_values = {};
648 field_values.from = from;
651 field_values.to = to;
655 get_context: function () {
656 var values = this.get_values();
657 if (!this.attrs.context || _.isEmpty(values)) {
661 {}, this.attrs.context,
662 {own_values: {self: values}});
664 get_domain: function () {
665 var values = this.get_values();
666 if (_.isEmpty(values)) {
669 var domain = this.attrs['filter_domain'];
673 domain.push([this.attrs.name, '>=', values.from]);
676 domain.push([this.attrs.name, '<=', values.to]);
683 {own_values: {self: values}});
686 openerp.base.search.DateTimeField = openerp.base.search.DateField.extend({
689 openerp.base.search.OneToManyField = openerp.base.search.IntegerField.extend({
690 // TODO: .relation, .context, .domain
692 openerp.base.search.ManyToOneField = openerp.base.search.IntegerField.extend({
694 // TODO: .relation, .selection, .context, .domain
696 openerp.base.search.ManyToManyField = openerp.base.search.IntegerField.extend({
697 // TODO: .related_columns (Array), .context, .domain
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);
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);
715 this.$element.closest("table.oe-searchview-render-line").css("display", "none");
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);
722 self.$element.find('.searchview_extended_add_group').click(function (e) {
727 get_context: function() {
730 get_domain: function() {
731 if(this.$element.closest("table.oe-searchview-render-line").css("display") == "none") {
734 return _.reduce(this.children,
735 function(mem, x) { return mem.concat(x.get_domain());}, []);
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");
745 table.css("display", "none");
746 if(this.$element.hasClass("expanded")) {
747 this.$element.toggleClass("folded expanded");
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) {
758 this.fields = fields;
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);
770 this.$element.find('.searchview_extended_add_proposition').click(function (e) {
773 var delete_btn = this.$element.find('.searchview_extended_delete_group');
774 delete_btn.click(function (e) {
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; }),
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) {
795 this.fields = _(fields).chain()
796 .map(function(val, key) { return _.extend({}, val, {'name': key}); })
797 .sortBy(function(field) {return field.string;})
799 this.attrs = {_: _, fields: this.fields, selected: null};
804 this.select_field(this.fields.length > 0 ? this.fields[0] : null);
806 this.$element.find(".searchview_extended_prop_field").change(function() {
809 var delete_btn = this.$element.find('.searchview_extended_delete_prop');
810 delete_btn.click(function (e) {
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;}));
821 * Selects the provided field object
823 * @param field a field descriptor object (as returned by fields_get, augmented by the field name)
825 select_field: function(field) {
827 if(this.attrs.selected != null) {
830 this.$element.find('.searchview_extended_prop_op').html('');
832 this.attrs.selected = field;
837 var type = field.type;
839 openerp.base.search.custom_filters.get_object(type);
841 if (! e instanceof openerp.base.KeyNotFound) {
845 this.log('Unknow field type ' + e.key);
847 this.value = new (openerp.base.search.custom_filters.get_object(type))
849 if(this.value.set_field) {
850 this.value.set_field(field);
852 _.each(this.value.operators, function(operator) {
853 var option = jQuery('<option>', {value: operator.value})
855 .appendTo(_this.$element.find('.searchview_extended_prop_op'));
857 this.$element.find('.searchview_extended_prop_value').html(
858 this.value.render({}));
862 get_proposition: function() {
863 if ( this.attrs.selected == 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];
872 openerp.base.search.ExtendedSearchProposition.Char = openerp.base.BaseWidget.extend({
873 template: 'SearchView.extended_search.proposition.char',
874 identifier_prefix: 'extended-search-proposition-char',
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"}
885 get_value: function() {
886 return this.$element.val();
889 openerp.base.search.ExtendedSearchProposition.DateTime = openerp.base.BaseWidget.extend({
890 template: 'SearchView.extended_search.proposition.datetime',
891 identifier_prefix: 'extended-search-proposition-datetime',
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"}
900 get_value: function() {
901 return this.$element.val();
905 this.$element.datetimepicker({
906 dateFormat: 'yy-mm-dd',
907 timeFormat: 'hh:mm:ss'
911 openerp.base.search.ExtendedSearchProposition.Date = openerp.base.BaseWidget.extend({
912 template: 'SearchView.extended_search.proposition.date',
913 identifier_prefix: 'extended-search-proposition-date',
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"}
922 get_value: function() {
923 return this.$element.val();
927 this.$element.datepicker({
928 dateFormat: 'yy-mm-dd',
929 timeFormat: 'hh:mm:ss'
933 openerp.base.search.ExtendedSearchProposition.Integer = openerp.base.BaseWidget.extend({
934 template: 'SearchView.extended_search.proposition.integer',
935 identifier_prefix: 'extended-search-proposition-integer',
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"}
944 get_value: function() {
945 val = this.$element.val();
946 val2 = parseFloat(val);
947 if(val2 != 0 && !val2) {
950 return Math.round(val2);
953 openerp.base.search.ExtendedSearchProposition.Float = openerp.base.BaseWidget.extend({
954 template: 'SearchView.extended_search.proposition.float',
955 identifier_prefix: 'extended-search-proposition-float',
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"}
964 get_value: function() {
965 val = this.$element.val();
966 val2 = parseFloat(val);
967 if(val2 != 0 && !val2) {
973 openerp.base.search.ExtendedSearchProposition.Selection = openerp.base.BaseWidget.extend({
974 template: 'SearchView.extended_search.proposition.selection',
975 identifier_prefix: 'extended-search-proposition-selection',
977 {value: "=", text: "is"},
978 {value: "!=", text: "is not"}
980 set_field: function(field) {
983 get_value: function() {
984 return this.$element.val();
987 openerp.base.search.ExtendedSearchProposition.Boolean = openerp.base.BaseWidget.extend({
988 template: 'SearchView.extended_search.proposition.boolean',
989 identifier_prefix: 'extended-search-proposition-boolean',
991 {value: "=", text: "is true"},
992 {value: "!=", text: "is false"}
994 get_value: function() {
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',
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'
1016 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: