/*--------------------------------------------------------- * OpenERP base library *---------------------------------------------------------*/ openerp.base$views = function(openerp) { // process all kind of actions openerp.base.ActionManager = openerp.base.Controller.extend({ init: function(session, element_id) { this._super(session, element_id); this.action = null; this.viewmanager = null; }, /** * Process an action * Supported actions: act_window */ do_action: function(action) { // instantiate the right controllers by understanding the action this.action = action; if(action.type == "ir.actions.act_window") { this.viewmanager = new openerp.base.ViewManager(this.session,this.element_id); this.viewmanager.do_action_window(action); this.viewmanager.start(); } } }); // This will be ViewManager Abstract/Common openerp.base.ViewManager = openerp.base.Controller.extend({ init: function(session, element_id) { this._super(session, element_id); this.action = null; this.dataset = null; this.searchview_id = false; this.searchview = null; this.search_visible = true; // this.views = { "list": { "view_id":1234, "controller": instance} } this.views = {}; }, start: function() { }, on_mode_switch: function(view_type) { for (var i in this.views) { this.views[i].controller.$element.toggle(i === view_type); } }, /** * Extract search view defaults from the current action's context. * * These defaults are of the form {search_default_*: value} * * @returns {Object} a clean defaults mapping of {field_name: value} */ search_defaults: function () { var defaults = {}; _.each(this.action.context, function (value, key) { var match = /^search_default_(.*)$/.exec(key); if (match) { defaults[match[1]] = value; } }); return defaults; }, do_action_window: function(action) { var self = this; var prefix_id = "#" + this.element_id; this.action = action; this.dataset = new openerp.base.DataSet(this.session, action.res_model); this.dataset.start(); this.$element.html(QWeb.render("ViewManager", {"prefix": this.element_id, views: action.views})); this.searchview_id = false; if(this.search_visible && action.search_view_id) { this.searchview_id = action.search_view_id[0]; var searchview = this.searchview = new openerp.base.SearchView( this.session, this.element_id + "_search", this.dataset, this.searchview_id, this.search_defaults()); searchview.on_search.add(this.do_search); searchview.start(); if (action['auto_search']) { searchview.on_loaded.add_last( searchview.do_search); } } for(var i = 0; i < action.views.length; i++) { var view_id, controller; view_id = action.views[i][0]; if(action.views[i][1] == "tree") { controller = new openerp.base.ListView(this.session, this.element_id + "_view_tree", this.dataset, view_id); controller.start(); this.views.tree = { view_id: view_id, controller: controller }; this.$element.find(prefix_id + "_button_tree").bind('click',function(){ self.on_mode_switch("tree"); }); } else if(action.views[i][1] == "form") { controller = new openerp.base.FormView(this.session, this.element_id + "_view_form", this.dataset, view_id); controller.start(); this.views.form = { view_id: view_id, controller: controller }; this.$element.find(prefix_id + "_button_form").bind('click',function(){ self.on_mode_switch("form"); }); } } // switch to the first one in sequence this.on_mode_switch("tree"); }, // create when root, also add to parent when o2m on_create: function() { }, on_remove: function() { }, on_edit: function() { }, do_search: function (domains, contexts, groupbys) { var self = this; this.rpc('/base/session/eval_domain_and_context', { domains: domains, contexts: contexts, group_by_seq: groupbys }, function (results) { // TODO: handle non-empty results.group_by with read_group self.dataset.set({ context: results.context, domain: results.domain }).fetch(0, self.action.limit); }); } }); // Extends view manager openerp.base.ViewManagerRoot = openerp.base.Controller.extend({ }); // Extends view manager openerp.base.ViewManagerUsedAsAMany2One = openerp.base.Controller.extend({ }); /** * Management interface between views and the collection of selected OpenERP * records (represents the view's state?) */ openerp.base.DataSet = openerp.base.Controller.extend({ init: function(session, model) { this._super(session); this.model = model; this._fields = null; this._ids = []; this._active_ids = null; this._active_id_index = 0; this._sort = []; this._domain = []; this._context = {}; }, start: function() { // TODO: fields_view_get fields selection? this.rpc("/base/dataset/fields", {"model":this.model}, this.on_fields); }, on_fields: function(result) { this._fields = result._fields; this.on_ready(); }, /** * Fetch all the records selected by this DataSet, based on its domain * and context. * * Fires the on_ids event. * * @param {Number} [offset=0] The index from which selected records should be returned * @param {Number} [limit=null] The maximum number of records to return * @returns itself */ fetch: function (offset, limit) { offset = offset || 0; limit = limit || null; this.rpc('/base/dataset/find', { model: this.model, fields: this._fields, domain: this._domain, context: this._context, sort: this._sort, offset: offset, limit: limit }, _.bind(function (records) { var data_records = _.map( records, function (record) { return new openerp.base.DataRecord( this.session, this.model, this._fields, record); }, this); this.on_fetch(data_records, { offset: offset, limit: limit, domain: this._domain, context: this._context, sort: this._sort }); }, this)); return this; }, /** * @event * * Fires after the DataSet fetched the records matching its internal ids selection * * @param {Array} records An array of the DataRecord fetched * @param event The on_fetch event object * @param {Number} event.offset the offset with which the original DataSet#fetch call was performed * @param {Number} event.limit the limit set on the original DataSet#fetch call * @param {Array} event.domain the domain set on the DataSet before DataSet#fetch was called * @param {Object} event.context the context set on the DataSet before DataSet#fetch was called * @param {Array} event.sort the sorting criteria used to get the ids */ on_fetch: function (records, event) { }, /** * Fetch all the currently active records for this DataSet (records selected via DataSet#select) * * @returns itself */ active_ids: function () { this.rpc('/base/dataset/get', { ids: this.get_active_ids(), model: this.model }, _.bind(function (records) { this.on_active_ids(_.map( records, function (record) { return new openerp.base.DataRecord( this.session, this.model, this._fields, record); }, this)); }, this)); return this; }, /** * @event * * Fires after the DataSet fetched the records matching its internal active ids selection * * @param {Array} records An array of the DataRecord fetched */ on_active_ids: function (records) { }, /** * Fetches the current active record for this DataSet * * @returns itself */ active_id: function () { this.rpc('/base/dataset/get', { ids: [this.get_active_id()], model: this.model }, _.bind(function (records) { var record = records[0]; this.on_active_id( record && new openerp.base.DataRecord( this.session, this.model, this._fields, record)); }, this)); return this; }, /** * Fires after the DataSet fetched the record matching the current active record * * @param record the record matching the provided id, or null if there is no record for this id */ on_active_id: function (record) { }, /** * Configures the DataSet * * @param options DataSet options * @param {Array} options.domain the domain to assign to this DataSet for filtering * @param {Object} options.context the context this DataSet should use during its calls * @param {Array} options.sort the sorting criteria for this DataSet * @returns itself */ set: function (options) { if (options.domain) { this._domain = _.clone(options.domain); } if (options.context) { this._context = _.clone(options.context); } if (options.sort) { this._sort = _.clone(options.sort); } return this; }, /** * Activates the previous id in the active sequence. If there is no previous id, wraps around to the last one * @returns itself */ prev: function () { this._active_id_index -= 1; if (this._active_id_index < 0) { this._active_id_index = this._active_ids.length - 1; } return this; }, /** * Activates the next id in the active sequence. If there is no next id, wraps around to the first one * @returns itself */ next: function () { this._active_id_index += 1; if (this._active_id_index >= this._active_ids.length) { this._active_id_index = 0; } return this; }, /** * Sets active_ids by value: * * * Activates all ids part of the current selection * * Sets active_id to be the first id of the selection * * @param {Array} ids the list of ids to activate * @returns itself */ select: function (ids) { this._active_ids = ids; this._active_id_index = 0; return this; }, /** * Fetches the ids of the currently selected records, if any. */ get_active_ids: function () { return this._active_ids; }, /** * Sets the current active_id by value * * If there are no active_ids selected, selects the provided id as the sole active_id * * If there are ids selected and the provided id is not in them, raise an error * * @param {Object} id the id to activate * @returns itself */ activate: function (id) { if(!this._active_ids) { this._active_ids = [id]; this._active_id_index = 0; } else { var index = _.indexOf(this._active_ids, id); if (index == -1) { throw new Error( "Could not find id " + id + " in array [" + this._active_ids.join(', ') + "]"); } this._active_id_index = index; } return this; }, /** * Fetches the id of the current active record, if any. * * @returns record? record id or null */ get_active_id: function () { if (!this._active_ids) { return null; } return this._active_ids[this._active_id_index]; } }); openerp.base.DataRecord = openerp.base.Controller.extend({ init: function(session, model, fields, values) { this._super(session, null); this.model = model; this.id = values.id || null; this.fields = fields; this.values = values; }, on_change: function() { }, on_reload: function() { } }); openerp.base.SearchView = openerp.base.Controller.extend({ init: function(session, element_id, dataset, view_id, defaults) { this._super(session, element_id); this.dataset = dataset; this.model = dataset.model; this.view_id = view_id; this.defaults = defaults || {}; this.inputs = []; this.enabled_filters = []; }, start: function() { //this.log('Starting SearchView '+this.model+this.view_id) this.rpc("/base/searchview/load", {"model": this.model, "view_id":this.view_id}, this.on_loaded); }, /** * Builds a list of widget rows (each row is an array of widgets) * * @param {Array} items a list of nodes to convert to widgets * @param {Object} fields a mapping of field names to (ORM) field attributes * @returns Array */ make_widgets: function (items, fields) { var rows = [], row = []; rows.push(row); var filters = []; _.each(items, function (item) { if (filters.length && item.tag !== 'filter') { row.push( new openerp.base.search.FilterGroup( filters, this)); filters = []; } if (item.tag === 'newline') { row = []; rows.push(row); } else if (item.tag === 'filter') { filters.push( new openerp.base.search.Filter( item, this)); } else if (item.tag === 'separator') { // a separator is a no-op } else { if (item.tag === 'group') { // TODO: group and field should be fetched from registries, maybe even filters row.push( new openerp.base.search.Group( item, this, fields)); } else if (item.tag === 'field') { row.push( this.make_field( item, fields[item['attrs'].name])); } } }, this); if (filters.length) { row.push(new openerp.base.search.FilterGroup(filters, this)); } return rows; }, /** * Creates a field for the provided field descriptor item (which comes * from fields_view_get) * * @param {Object} item fields_view_get node for the field * @param {Object} field fields_get result for the field * @returns openerp.base.search.Field */ make_field: function (item, field) { // TODO: should fetch from an actual registry // TODO: register fields in self? switch (field.type) { case 'char': case 'text': return new openerp.base.search.CharField( item, field, this); case 'boolean': return new openerp.base.search.BooleanField( item, field, this); case 'integer': return new openerp.base.search.IntegerField( item, field, this); case 'float': return new openerp.base.search.FloatField( item, field, this); case 'selection': return new openerp.base.search.SelectionField( item, field, this); case 'datetime': return new openerp.base.search.DateTimeField( item, field, this); case 'date': return new openerp.base.search.DateField( item, field, this); case 'one2many': return new openerp.base.search.OneToManyField( item, field, this); case 'many2one': return new openerp.base.search.ManyToOneField( item, field, this); case 'many2many': return new openerp.base.search.ManyToManyField( item, field, this); default: console.group('Unknown field type ' + field.type); console.error('View node', item); console.info('View field', field); console.info('In view', this); console.groupEnd(); return null; } }, on_loaded: function(data) { var lines = this.make_widgets( data.fields_view['arch'].children, data.fields_view.fields); // for extended search view lines.push([new openerp.base.search.ExtendedSearch(this, data.fields_view.fields)]); var render = QWeb.render("SearchView", { 'view': data.fields_view['arch'], 'lines': lines, 'defaults': this.defaults }); this.$element.html(render); this.$element.find('form') .submit(this.do_search) .bind('reset', this.do_clear); // start() all the widgets _(lines).chain().flatten().each(function (widget) { widget.start(); }); }, /** * Performs the search view collection of widget data. * * If the collection went well (all fields are valid), then triggers * :js:func:`openerp.base.SearchView.on_search`. * * If at least one field failed its validation, triggers * :js:func:`openerp.base.SearchView.on_invalid` instead. * * @param e jQuery event object coming from the "Search" button */ do_search: function (e) { if (e && e.preventDefault) { e.preventDefault(); } var domains = [], contexts = []; var errors = []; _.each(this.inputs, function (input) { try { var domain = input.get_domain(); if (domain) { domains.push(domain); } var context = input.get_context(); if (context) { contexts.push(context); } } catch (e) { if (e instanceof openerp.base.search.Invalid) { errors.push(e); } else { throw e; } } }); if (errors.length) { this.on_invalid(errors); return; } // TODO: do we need to handle *fields* with group_by in their context? var groupbys = _(this.enabled_filters) .chain() .map(function (filter) { return filter.get_context();}) .compact() .value(); this.on_search(domains, contexts, groupbys); }, /** * Triggered after the SearchView has collected all relevant domains and * contexts. * * It is provided with an Array of domains and an Array of contexts, which * may or may not be evaluated (each item can be either a valid domain or * context, or a string to evaluate in order in the sequence) * * It is also passed an array of contexts used for group_by (they are in * the correct order for group_by evaluation, which contexts may not be) * * @event * @param {Array} domains an array of literal domains or domain references * @param {Array} contexts an array of literal contexts or context refs * @param {Array} groupbys ordered contexts which may or may not have group_by keys */ on_search: function (domains, contexts, groupbys) { }, /** * Triggered after a validation error in the SearchView fields. * * Error objects have three keys: * * ``field`` is the name of the invalid field * * ``value`` is the invalid value * * ``message`` is the (in)validation message provided by the field * * @event * @param {Array} errors a never-empty array of error objects */ on_invalid: function (errors) { }, do_clear: function (e) { if (e && e.preventDefault) { e.preventDefault(); } this.on_clear(); }, /** * Triggered when the search view gets cleared * * @event */ on_clear: function () { }, /** * Called by a filter propagating its state changes * * @param {openerp.base.search.Filter} filter a filter which got toggled * @param {Boolean} default_enabled filter got enabled through the default values, at render time. */ do_toggle_filter: function (filter, default_enabled) { if (default_enabled || filter.is_enabled()) { this.enabled_filters.push(filter); } else { this.enabled_filters = _.without( this.enabled_filters, filter); } if (!default_enabled) { // selecting a filter after initial loading automatically // triggers refresh this.$element.find('form').submit(); } } }); openerp.base.search = {}; openerp.base.search.Invalid = Class.extend({ init: function (field, value, message) { this.field = field; this.value = value; this.message = message; }, toString: function () { return ('Incorrect value for field ' + this.field + ': [' + this.value + '] is ' + this.message); } }); openerp.base.search.Widget = openerp.base.Controller.extend({ template: null, init: function (view) { this.view = view; }, /** * Sets and returns a globally unique identifier for the widget. * * If a prefix is specified, the identifier will be appended to it. * * @params prefix prefix sections, empty/falsy sections will be removed */ make_id: function () { this.element_id = _.uniqueId( ['search'].concat( _.compact(_.toArray(arguments)), ['']).join('_')); return this.element_id; }, /** * "Starts" the widgets. Called at the end of the rendering, this allows * widgets to hook themselves to their view sections. * * On widgets, if they kept a reference to a view and have an element_id, * will fetch and set their root element on $element. */ start: function () { this._super(); if (this.view && this.element_id) { // id is unique, and no getElementById on elements this.$element = $(document.getElementById( this.element_id)); } }, /** * "Stops" the widgets. Called when the view destroys itself, this * lets the widgets clean up after themselves. */ stop: function () { delete this.view; this.$element.remove(); this._super(); }, render: function (defaults) { return QWeb.render( this.template, _.extend(this, { defaults: defaults })); } }); openerp.base.search.FilterGroup = openerp.base.search.Widget.extend({ template: 'SearchView.filters', init: function (filters, view) { this._super(view); this.filters = filters; }, start: function () { this._super(); _.each(this.filters, function (filter) { filter.start(); }); } }); add_expand_listener = function($root) { $root.find('a.searchview_group_string').click(function (e) { $root.toggleClass('folded expanded'); e.stopPropagation(); e.preventDefault(); }); }; openerp.base.search.Group = openerp.base.search.Widget.extend({ template: 'SearchView.group', // TODO: contain stuff // TODO: @expand init: function (view_section, view, fields) { this._super(view); this.attrs = view_section.attrs; this.lines = view.make_widgets( view_section.children, fields); this.make_id('group'); }, start: function () { this._super(); _(this.lines) .chain() .flatten() .each(function (widget) { widget.start(); }); add_expand_listener(this.$element); } }); openerp.base.search.ExtendedSearch = openerp.base.search.Widget.extend({ template: 'SearchView.extended_search', init: function (view, fields) { this._super(view); this.make_id('extended-search'); this.fields = fields; this.groups_list = []; }, add_group: function(group) { var group = new openerp.base.search.ExtendedSearchGroup(this.view, this, this.fields); var $root = this.$element; this.groups_list.push(group); var render = group.render({}); var groups_div = $root.find('.searchview_extended_groups_list'); groups_div.html(groups_div.html() + render); group.start(); }, remove: function(group) { this.groups_list = _.select(this.groups_list, function(x) {return ! x === group}); group.stop(); }, stop: function() { _.each(this.groups_list, function(el) { el.stop(); }); this._super(); }, start: function () { this._super(); var $root = this.$element; var $this = this; add_expand_listener($root); this.add_group(); $root.find('.searchview_extended_add_group').click(function (e) { $this.add_group(); e.stopPropagation(); e.preventDefault(); }); } }); openerp.base.search.ExtendedSearchGroup = openerp.base.search.Widget.extend({ template: 'SearchView.extended_search.group', init: function (view, parent, fields) { this._super(view); this.parent = parent; this.make_id('extended-search-group'); this.fields = fields; this.propositions_list = []; }, add_prop: function() { var $root = this.$element; var prop = new openerp.base.search.ExtendedSearchProposition(this.view, this, this.fields); this.propositions_list.push(prop); var render = prop.render({}); var propositions_div = $root.find('.searchview_extended_propositions_list'); propositions_div.html(propositions_div.html() + render); prop.start(); }, stop: function() { _.each(this.propositions_list, function(el) { el.stop(); }); this._super(); }, remove: function(prop) { this.propositions_list = _.select(this.propositions_list, function(x) {return ! x === prop}); prop.stop(); }, start: function () { this._super(); var $root = this.$element; var $this = this; this.add_prop(); $root.find('.searchview_extended_add_proposition').click(function (e) { $this.add_prop(); e.stopPropagation(); e.preventDefault(); }); $root.find('.searchview_extended_delete_group').click(function (e) { $this.parent.remove($this); }); } }); extended_filters_types = { char: { operators: [ {value: "ilike", text: "contains"}, {value: "not like", text: "doesn't contain"}, {value: "=", text: "is equal to"}, {value: "!=", text: "is not equal to"}, {value: ">", text: "greater than"}, {value: "<", text: "less than"}, {value: ">=", text: "greater or equal than"}, {value: "<=", text: "less or equal than"}, ], build_component: function(view) { return new openerp.base.search.ExtendedSearchProposition.Char(view); }, } } openerp.base.search.ExtendedSearchProposition = openerp.base.search.Widget.extend({ template: 'SearchView.extended_search.proposition', init: function (view, parent, fields) { this._super(view); this.parent = parent; this.make_id('extended-search-proposition'); this.fields = _(fields).chain() .map(function(key,val) {return {name:val, obj:key};}) .sortBy(function(x) {return x.name;}).value(); this.attrs = {_: _, fields: this.fields, selected: null}; this.value_component = null; }, start: function () { this._super(); this.set_selected(this.fields.length > 0 ? this.fields[0] : null); $this = this; this.$element.find(".searchview_extended_prop_field").change(function(e) { $this.changed(); e.stopPropagation(); e.preventDefault(); }); this.$element.find('.searchview_extended_delete_prop').click(function (e) { $this.parent.remove($this); }); }, changed: function() { var nval = this.$element.find(".searchview_extended_prop_field").val(); if(this.attrs.selected == null || nval != this.attrs.selected.name) { this.set_selected(_.detect(this.fields, function(x) {return x.name == nval})); } }, stop: function() { if(this.value_component != null) { this.value_component.stop(); }; this._super(); }, set_selected: function(selected) { var $root = this.$element; var tmp = $root.find('.searchview_extended_prop_op'); if(this.attrs.selected != null) { this.value_component.stop(); this.value_component = null; $root.find('.searchview_extended_prop_op').html(''); } this.attrs.selected = selected; if(selected == null) { return; } var type = selected.obj.type; type = type in extended_filters_types ? type : "char"; _.each(extended_filters_types[type].operators, function(operator) { option = jQuery('