diff --git a/addons/base/controllers/main.py b/addons/base/controllers/main.py index 8af535080b3..002b2c62d8a 100644 --- a/addons/base/controllers/main.py +++ b/addons/base/controllers/main.py @@ -439,9 +439,9 @@ class DataSet(openerpweb.Controller): return Model.unlink(ids) @openerpweb.jsonrequest - def call(self, req, model, method, ids, args): + def call(self, req, model, method, args): m = req.session.model(model) - r = getattr(m, method)(ids, *args) + r = getattr(m, method)(*args) return {'result': r} @openerpweb.jsonrequest @@ -456,9 +456,9 @@ class DataSet(openerpweb.Controller): return {'result': r} @openerpweb.jsonrequest - def name_search(self, req, model, search_str, domain=[], context={}): + def name_search(self, req, model, search_str, domain=[], context={}, limit=False): m = req.session.model(model) - r = m.name_search(search_str+'%', domain, '=ilike', context) + r = m.name_search(search_str, domain, 'ilike', context, limit) return {'result': r} class DataGroup(openerpweb.Controller): diff --git a/addons/base/static/lib/jquery.contextmenu/jquery.contextmenu.r2.packed.js b/addons/base/static/lib/jquery.contextmenu/jquery.contextmenu.r2.packed.js new file mode 100644 index 00000000000..cb456eed14c --- /dev/null +++ b/addons/base/static/lib/jquery.contextmenu/jquery.contextmenu.r2.packed.js @@ -0,0 +1,126 @@ +(function($) { + var menu,shadow,trigger,content,hash,currentTarget; + var defaults= { + menuStyle: { + listStyle:'none', + padding:'1px', + margin:'0px', + backgroundColor:'#fff', + border:'1px solid #999', + width:'100px' + }, + itemStyle: { + margin:'0px', + color:'#000', + display:'block', + cursor:'default', + padding:'3px', + border:'1px solid #fff', + backgroundColor:'transparent' + }, + itemHoverStyle: { + border:'1px solid #0a246a', + backgroundColor:'#b6bdd2' + }, + eventPosX:'pageX', + eventPosY:'pageY', + shadow:true, + onContextMenu:null, + onShowMenu:null + }; + $.fn.contextMenu= function(id,options) { + if(!menu) { + menu=$('
').hide().css({ + position:'absolute', + zIndex:'500' + }).appendTo('body').bind('click', function(e) { + e.stopPropagation() + }) + } + if(!shadow) { + shadow=$('
').css({ + backgroundColor:'#000', + position:'absolute', + opacity:0.2, + zIndex:499 + }).appendTo('body').hide() + } + hash=hash||[]; + hash.push({ + id:id, + menuStyle:$.extend({},defaults.menuStyle,options.menuStyle|| {}), + itemStyle:$.extend({},defaults.itemStyle,options.itemStyle|| {}), + itemHoverStyle:$.extend({},defaults.itemHoverStyle,options.itemHoverStyle|| {}), + bindings:options.bindings|| {}, + shadow:options.shadow||options.shadow===false?options.shadow:defaults.shadow, + onContextMenu:options.onContextMenu||defaults.onContextMenu, + onShowMenu:options.onShowMenu||defaults.onShowMenu, + eventPosX:options.eventPosX||defaults.eventPosX, + eventPosY:options.eventPosY||defaults.eventPosY + }); + var index=hash.length-1; + var callback = function(e) { + var bShowContext=(!!hash[index].onContextMenu)?hash[index].onContextMenu(e):true; + if(bShowContext) + display(index,this,e,options); + return false; + }; + $(this).bind('contextmenu', callback); + if(options.leftClickToo) { + $(this).click(callback); + } + return this + }; + function display(index,trigger,e,options) { + var cur=hash[index]; + content=$('#'+cur.id).find('ul:first').clone(true); + content.css(cur.menuStyle).find('li').css(cur.itemStyle).hover( function() { + $(this).css(cur.itemHoverStyle) + }, function() { + $(this).css(cur.itemStyle) + }).find('img').css({ + verticalAlign:'middle', + paddingRight:'2px' + }); + menu.html(content); + if(!!cur.onShowMenu) + menu=cur.onShowMenu(e,menu); + $.each(cur.bindings, function(id,func) { + $('#'+id,menu).bind('click', function(e) { + hide(); + func(trigger,currentTarget) + }) + }); + menu.css({ + 'left':e[cur.eventPosX], + 'top':e[cur.eventPosY] + }).show(); + if(cur.shadow) + shadow.css({ + width:menu.width(), + height:menu.height(), + left:e.pageX+2, + top:e.pageY+2 + }).show(); + $(document).one('click',hide) + } + + function hide() { + menu.hide(); + shadow.hide() + } + + $.contextMenu= { + defaults: function(userDefaults) { + $.each(userDefaults, function(i,val) { + if(typeof val=='object'&&defaults[i]) { + $.extend(defaults[i],val) + } else + defaults[i]=val + }) + } + } +})(jQuery); +$( function() { + $('div.contextMenu').hide() +}); \ No newline at end of file diff --git a/addons/base/static/src/base.html b/addons/base/static/src/base.html index 467bccb92ff..2761638c6d0 100644 --- a/addons/base/static/src/base.html +++ b/addons/base/static/src/base.html @@ -19,6 +19,7 @@ + @@ -28,9 +29,9 @@ + - diff --git a/addons/base/static/src/css/base.css b/addons/base/static/src/css/base.css index fff41c0f557..1c5ab90b073 100644 --- a/addons/base/static/src/css/base.css +++ b/addons/base/static/src/css/base.css @@ -27,6 +27,10 @@ body.openerp { overflow-y: scroll; } +.openerp .oe-number { + text-align: right !important; +} + /* STATES */ .openerp .on_logged { display: none; @@ -94,6 +98,12 @@ body.openerp { .openerp .login .oe_box2 td select { width: 100%; } +.openerp .login .oe_box2 td.oe_remember { + text-align:left; +} +.openerp .login .oe_box2 td.oe_remember input { + width: inherit; +} .openerp .login .oe_login_right_pane { padding:70px 35px 5px 10px; min-width: 200px; @@ -596,6 +606,7 @@ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%, padding: 0; border: none; background: none; + width: 100%; } .openerp .oe-listview .oe-field-cell button:active { opacity: 0.5; @@ -1000,60 +1011,18 @@ background: linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* W3C */ border-color: #828282; } -/* Many2one Autosearch */ -.openerp .ui_combo { - height: 19px; - top: 6px; - width: 20px; - position: relative; - margin-left: -26px; -} - -.openerp img.m2o_open { - vertical-align: middle; - cursor: pointer; -} -/*----- End Many2one Autosearch ------- */ - -/* Many2one Contextmenu */ -.openerp div.m2o_contextmenu { - background-color: #444; - border: 1px solid #000; +.openerp .oe-m2o { + padding-right: 16px; white-space: nowrap; - position: absolute; - padding: 2px; - z-index: 100; } -.openerp div.m2o_contextmenu a, -.openerp div.m2o_contextmenu span { - display: block; - text-align: left; - text-decoration: none; - white-space: nowrap; - padding: 2px; - color: #FFF; - cursor: pointer; +.openerp .oe-m2o-drop-down-button { + margin-left: -23px; } - -.openerp div.m2o_contextmenu hr { - margin: 1px 0 !important; +.openerp .oe-m2o-drop-down-button img, +.openerp .oe-m2o-cm-button img { + margin-bottom: -4px; } - -.openerp div.m2o_contextmenu td { - padding: 1px; +.openerp .oe-m2o-disabled-cm { + color: grey; } - -.openerp div.m2o_contextmenu a:hover, -.openerp div.m2o_contextmenu span:hover { - color: #FFF; - background-color: #505050; - text-decoration: none; -} - -.openerp div.m2o_contextmenu a.disabled, -.openerp div.m2o_contextmenu span.disabled { - color: #C0C0C0; -} - -/*----- End Many2one Contextmenu ------- */ diff --git a/addons/base/static/src/img/down-arrow.png b/addons/base/static/src/img/down-arrow.png new file mode 100644 index 00000000000..b1288e52682 Binary files /dev/null and b/addons/base/static/src/img/down-arrow.png differ diff --git a/addons/base/static/src/js/chrome.js b/addons/base/static/src/js/chrome.js index 6fa95266ba9..9ce0efa8d9f 100644 --- a/addons/base/static/src/js/chrome.js +++ b/addons/base/static/src/js/chrome.js @@ -149,6 +149,16 @@ openerp.base.Registry = Class.extend( /** @lends openerp.base.Registry# */ { add: function (key, object_path) { this.map[key] = object_path; return this; + }, + /** + * Creates and returns a copy of the current mapping, with the provided + * mapping argument added in (replacing existing keys if needed) + * + * @param {Object} [mapping={}] a mapping of keys to object-paths + */ + clone: function (mapping) { + return new openerp.base.Registry( + _.extend({}, this.map, mapping || {})); } }); @@ -776,6 +786,17 @@ openerp.base.Database = openerp.base.Controller.extend({ openerp.base.Login = openerp.base.Controller.extend({ init: function(session, element_id) { this._super(session, element_id); + this.has_local_storage = typeof(localStorage) != 'undefined'; + this.selected_db = null; + this.selected_login = null; + this.selected_password = null; + this.remember = false; + if (this.has_local_storage && localStorage.getItem('remember_creditentials') === 'true') { + this.remember = true; + this.selected_db = localStorage.getItem('last_db_login_success'); + this.selected_login = localStorage.getItem('last_login_login_success'); + this.selected_password = localStorage.getItem('last_password_login_success'); + } }, start: function() { var self = this; @@ -786,10 +807,10 @@ openerp.base.Login = openerp.base.Controller.extend({ self.display(); }); }, - display: function() { + display: function() { this.$element.html(QWeb.render("Login", this)); this.$element.find("form").submit(this.on_submit); - }, + }, on_login_invalid: function() { this.$element.closest(".openerp").addClass("login-mode"); }, @@ -803,10 +824,24 @@ openerp.base.Login = openerp.base.Controller.extend({ var db = $e.find("form [name=db]").val(); var login = $e.find("form input[name=login]").val(); var password = $e.find("form input[name=password]").val(); + var remember = $e.find("form input[name=remember]").attr('checked'); //$e.hide(); // Should hide then call callback this.session.session_login(db, login, password, function() { if(self.session.session_is_valid()) { + if (self.has_local_storage) { + if(remember) { + localStorage.setItem('remember_creditentials', 'true'); + localStorage.setItem('last_db_login_success', db); + localStorage.setItem('last_login_login_success', login); + localStorage.setItem('last_password_login_success', password); + } else { + localStorage.setItem('remember_creditentials', ''); + localStorage.setItem('last_db_login_success', ''); + localStorage.setItem('last_login_login_success', ''); + localStorage.setItem('last_password_login_success', ''); + } + } self.on_login_valid(); } else { self.$element.addClass("login_invalid"); diff --git a/addons/base/static/src/js/data.js b/addons/base/static/src/js/data.js index 6c0a3a354a4..57999513a54 100644 --- a/addons/base/static/src/js/data.js +++ b/addons/base/static/src/js/data.js @@ -259,7 +259,7 @@ openerp.base.DataSet = openerp.base.Controller.extend( /** @lends openerp.base. */ read_ids: function (ids, fields, callback) { var self = this; - this.rpc('/base/dataset/get', { + return this.rpc('/base/dataset/get', { model: this.model, ids: ids, fields: fields @@ -279,10 +279,10 @@ openerp.base.DataSet = openerp.base.Controller.extend( /** @lends openerp.base. */ read_index: function (fields, callback) { if (_.isEmpty(this.ids)) { - callback([]); + return $.Deferred().reject().promise(); } else { fields = fields || false; - this.read_ids([this.ids[this.index]], fields, function(records) { + return this.read_ids([this.ids[this.index]], fields, function(records) { callback(records[0]); }); } @@ -313,25 +313,21 @@ openerp.base.DataSet = openerp.base.Controller.extend( /** @lends openerp.base. // to implement in children this.notification.notify("Unlink", ids); }, - call: function (method, ids, args, callback) { - this.notification.notify( - "Calling", this.model + '#' + method + '(' + ids + ')'); - ids = ids || []; - args = args || []; + call: function (method, args, callback, error_callback) { return this.rpc('/base/dataset/call', { model: this.model, method: method, - ids: ids, - args: args - }, callback); + args: args || [] + }, callback, error_callback); }, - name_search: function (search_str, callback) { + name_search: function (search_str, limit, callback) { search_str = search_str || ''; return this.rpc('/base/dataset/name_search', { model: this.model, search_str: search_str, domain: this.domain || [], - context: this.context + context: this.context, + limit: limit || false }, callback); }, exec_workflow: function (id, signal, callback) { diff --git a/addons/base/static/src/js/form.js b/addons/base/static/src/js/form.js index 50b391c9b92..d2f23bdc262 100644 --- a/addons/base/static/src/js/form.js +++ b/addons/base/static/src/js/form.js @@ -8,12 +8,15 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV * view should be displayed (if there is one active). */ searchable: false, + template: "FormView", /** * @constructs * @param {openerp.base.Session} session the current openerp session * @param {String} element_id this view's root element id * @param {openerp.base.DataSet} dataset the dataset this view will work with * @param {String} view_id the identifier of the OpenERP view object + * + * @property {openerp.base.Registry} registry=openerp.base.form.widgets widgets registry for this form view instance */ init: function(view_manager, session, element_id, dataset, view_id) { this._super(session, element_id); @@ -29,7 +32,8 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV this.ready = false; this.show_invalid = true; this.touched = false; - this.flags = this.view_manager.flags || {}; + this.flags = this.view_manager.action.flags || {}; + this.registry = openerp.base.form.widgets; }, start: function() { //this.log('Starting FormView '+this.model+this.view_id) @@ -44,9 +48,9 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV var self = this; this.fields_view = data.fields_view; - var frame = new openerp.base.form.WidgetFrame(this, this.fields_view.arch); + var frame = new (this.registry.get_object('frame'))(this, this.fields_view.arch); - this.$element.html(QWeb.render("FormView", { 'frame': frame, 'view': this })); + this.$element.html(QWeb.render(this.template, { 'frame': frame, 'view': this })); _.each(this.widgets, function(w) { w.start(); }); @@ -220,7 +224,15 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV self.on_record_loaded(result.result); }); }, - do_save: function(success) { + /** + * Triggers saving the form's record. Chooses between creating a new + * record or saving an existing one depending on whether the record + * already has an id property. + * + * @param {Function} success callback on save success + * @param {Boolean} [prepend_on_create=false] if ``do_save`` creates a new record, should that record be inserted at the start of the dataset (by default, records are added at the end) + */ + do_save: function(success, prepend_on_create) { var self = this; if (!this.ready) { return false; @@ -243,7 +255,7 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV this.log("About to save", values); if (!this.datarecord.id) { this.dataset.create(values, function(r) { - self.on_created(r, success); + self.on_created(r, success, prepend_on_create); }); } else { this.dataset.write(this.datarecord.id, values, function(r) { @@ -281,19 +293,37 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV } } }, - on_created: function(r, success) { + /** + * Updates the form' dataset to contain the new record: + * + * * Adds the newly created record to the current dataset (at the end by + * default) + * * Selects that record (sets the dataset's index to point to the new + * record's id). + * * Updates the pager and sidebar displays + * + * @param {Object} r + * @param {Function} success callback to execute after having updated the dataset + * @param {Boolean} [prepend_on_create=false] adds the newly created record at the beginning of the dataset instead of the end + */ + on_created: function(r, success, prepend_on_create) { if (!r.result) { this.notification.warn("Record not created", "Problem while creating record."); } else { - this.datarecord.id = arguments[0].result; - this.dataset.ids.push(this.datarecord.id); - this.dataset.index = this.dataset.ids.length - 1; + this.datarecord.id = r.result; + if (!prepend_on_create) { + this.dataset.ids.push(this.datarecord.id); + this.dataset.index = this.dataset.ids.length - 1; + } else { + this.dataset.ids.unshift(this.datarecord.id); + this.dataset.index = 0; + } this.dataset.count++; this.do_update_pager(); this.do_update_sidebar(); this.notification.notify("Record created", "The record has been created with id #" + this.datarecord.id); if (success) { - success(r); + success(_.extend(r, {created: true})); } } }, @@ -422,6 +452,7 @@ openerp.base.form.compute_domain = function(expr, fields) { }; openerp.base.form.Widget = openerp.base.Controller.extend({ + template: 'Widget', init: function(view, node) { this.view = view; this.node = node; @@ -435,7 +466,6 @@ openerp.base.form.Widget = openerp.base.Controller.extend({ this.view.widgets[this.element_id] = this; this.children = node.children; this.colspan = parseInt(node.attrs.colspan || 1); - this.template = "Widget"; this.string = this.string || node.attrs.string; this.help = this.help || node.attrs.help; @@ -460,9 +490,9 @@ openerp.base.form.Widget = openerp.base.Controller.extend({ }); openerp.base.form.WidgetFrame = openerp.base.form.Widget.extend({ + template: 'WidgetFrame', init: function(view, node) { this._super(view, node); - this.template = "WidgetFrame"; this.columns = node.attrs.col || 4; this.x = 0; this.y = 0; @@ -504,9 +534,9 @@ openerp.base.form.WidgetFrame = openerp.base.form.Widget.extend({ handle_node: function(node) { var type = this.view.fields_view.fields[node.attrs.name] || {}; var widget_type = node.attrs.widget || type.type || node.tag; - var widget = new (openerp.base.form.widgets.get_object(widget_type)) (this.view, node); + var widget = new (this.view.registry.get_object(widget_type)) (this.view, node); if (node.tag == 'field' && node.attrs.nolabel != '1') { - var label = new (openerp.base.form.widgets.get_object('label')) (this.view, node); + var label = new (this.view.registry.get_object('label')) (this.view, node); label["for"] = widget; this.add_widget(label); } @@ -949,45 +979,232 @@ openerp.base.form.FieldSelection = openerp.base.form.Field.extend({ } }); -openerp.base.form.FieldMany2OneDatasSet = openerp.base.DataSetStatic.extend({ - start: function() { - }, - write: function (id, data, callback) { - this._super(id, data, callback); +// jquery autocomplete tweak to allow html +(function(x) { + var proto = $.ui.autocomplete.prototype, + initSource = proto._initSource; + + function filter( array, term ) { + var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" ); + return $.grep( array, function(value) { + return matcher.test( $( "
" ).html( value.label || value.value || value ).text() ); + }); } -}); + + $.extend( proto, { + _initSource: function() { + if ( this.options.html && $.isArray(this.options.source) ) { + this.source = function( request, response ) { + response( filter( this.options.source, request.term ) ); + }; + } else { + initSource.call( this ); + } + }, + + _renderItem: function( ul, item) { + return $( "
  • " ) + .data( "item.autocomplete", item ) + .append( $( "" )[ this.options.html ? "html" : "text" ]( item.label ) ) + .appendTo( ul ); + } + }); +})(null); openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ init: function(view, node) { this._super(view, node); this.template = "FieldMany2One"; - this.is_field_m2o = true; + this.limit = 7; + this.value = null; + this.cm_id = _.uniqueId('m2o_cm_'); + this.last_search = []; }, start: function() { - this.$element = $('#' + this.element_id); - this.dataset = new openerp.base.form.FieldMany2OneDatasSet(this.session, this.field.relation); - var views = [ [false,"list"], [false,"form"] ]; - new openerp.base.m2o(this.$element, this.field.relation, this.dataset, this.session) - this.$element.find('input').change(this.on_ui_change); - }, - set_value: function(value) { - this._super.apply(this, arguments); - var show_value = ''; - if (value != null && value !== false) { - show_value = value[1]; - this.value = value[0]; + this._super(); + var self = this; + this.$input = this.$element.find("input"); + this.$drop_down = this.$element.find(".oe-m2o-drop-down-button"); + this.$menu_btn = this.$element.find(".oe-m2o-cm-button"); + + // context menu + var bindings = {}; + bindings[this.cm_id + "_search"] = function() { + self._search_create_popup("search"); + }; + bindings[this.cm_id + "_create"] = function() { + self._search_create_popup("form"); + }; + bindings[this.cm_id + "_open"] = function() { + if (!self.value) { + return; + } + self.session.action_manager.do_action({ + "res_model": self.field.relation, + "views":[[false,"form"]], + "res_id": self.value[0], + "type":"ir.actions.act_window", + "view_type":"form", + "view_mode":"form", + "target":"new" + }); + }; + var cmenu = this.$menu_btn.contextMenu(this.cm_id, {'leftClickToo': true, + bindings: bindings, itemStyle: {"color": ""}, + onContextMenu: function() { + if(self.value) { + $("#" + self.cm_id + "_open").removeClass("oe-m2o-disabled-cm"); + } else { + $("#" + self.cm_id + "_open").addClass("oe-m2o-disabled-cm"); + } + return true; + } + }); + + // some behavior for input + this.$input.keyup(function() { + if (self.$input.val() === "") { + self._change_int_value(null); + } else if (self.value === null || (self.value && self.$input.val() !== self.value[1])) { + self._change_int_value(undefined); + } + }); + this.$drop_down.click(function() { + if (self.$input.autocomplete("widget").is(":visible")) { + self.$input.autocomplete("close"); + } else { + if (self.value) { + self.$input.autocomplete("search", ""); + } else { + self.$input.autocomplete("search"); + } + self.$input.focus(); + } + }); + var anyoneLoosesFocus = function() { + if (!self.$input.is(":focus") && + !self.$input.autocomplete("widget").is(":visible") && + !self.value) { + if(self.value === undefined && self.last_search.length > 0) { + self._change_int_ext_value(self.last_search[0]); + } else { + self._change_int_ext_value(null); + } + } } - this.$element.find('input').val(show_value); - this.$element.find('input').attr('m2o_id', this.value); + this.$input.focusout(anyoneLoosesFocus); + + // autocomplete + this.$input.autocomplete({ + source: function(req, resp) { self.get_search_result(req, resp); }, + select: function(event, ui) { + var item = ui.item; + if (item.id) { + self._change_int_value([item.id, item.name]); + } else if (item.action) { + self._change_int_value(undefined); + item.action(); + return false; + } + }, + focus: function(e, ui) { + e.preventDefault(); + }, + html: true, + close: anyoneLoosesFocus, + minLength: 0, + delay: 0 + }); + }, + // autocomplete component content handling + get_search_result: function(request, response) { + var search_val = request.term; + var self = this; + + var dataset = new openerp.base.DataSetStatic(this.session, this.field.relation, []); + + dataset.name_search(search_val, this.limit + 1, function(data) { + self.last_search = data.result; + // possible selections for the m2o + var values = _.map(data.result, function(x) { + return {label: $('').text(x[1]).html(), name:x[1], id:x[0]}; + }); + + // additional selections, actions that open popup + if (values.length > self.limit) { + values = values.slice(0, self.limit); + values.push({label: "   Search More...", action: function() { + dataset.name_search(search_val, false, function(data) { + self._change_int_value(null); + self._search_create_popup("search", data.result); + }); + }}); + } + if (self.value === undefined) { + values.push({label: '   Create "' + + $('').text(search_val).html() + '"', action: function() { + self._quick_create(search_val); + }}); + } + values.push({label: "   Create and Edit...", action: function() { + self._change_int_value(null); + self._search_create_popup("form"); + }}); + + response(values); + }); + }, + _quick_create: function(name) { + var self = this; + var dataset = new openerp.base.DataSetStatic(this.session, this.field.relation, []); + dataset.call("name_create", [name, {}], function(data) { + self._change_int_ext_value(data.result); + }, function(a, b) { + self._change_int_value(null); + self._search_create_popup("form"); + }); + }, + // all search/create popup handling + _search_create_popup: function(view, ids) { + var dataset = new openerp.base.DataSetStatic(this.session, this.field.relation, []); + var self = this; + var pop = new openerp.base.form.Many2XSelectPopup(null, self.view.session); + pop.select_element(self.field.relation, ids ? _.map(ids, function(x) {return x[0]}) : undefined, view); + pop.on_select_element.add(function(element_id) { + dataset.call("name_get", [element_id], function(data) { + self._change_int_ext_value(data.result[0]); + pop.stop(); + }); + }); + }, + _change_int_ext_value: function(value) { + this._change_int_value(value); + this.$input.val(this.value ? this.value[1] : ""); + }, + _change_int_value: function(value) { + this.value = value; + if (this.original_value === undefined || (this.value !== undefined && + ((this.original_value ? this.original_value[0] : null) !== (this.value ? this.value[0] : null)))) { + this.original_value = undefined; + this.on_ui_change(); + } + }, + set_value_from_ui: function() {}, + set_value: function(value) { + this._super(value); + this.original_value = value; + this._change_int_ext_value(value); }, - get_value: function() { - var val = this.$element.find('input').attr('m2o_id') || this.value - return val; + if (this.value === undefined) + throw "theorically unreachable state"; + return this.value ? this.value[0] : false; }, - - on_ui_change: function() { - this.touched = this.view.touched = true; + validate: function() { + this.invalid = false; + if (this.value === null) { + this.invalid = this.required; + } } }); @@ -1131,9 +1348,10 @@ openerp.base.form.Many2ManyListView = openerp.base.ListView.extend({ openerp.base.form.Many2XSelectPopup = openerp.base.BaseWidget.extend({ identifier_prefix: "many2xselectpopup", template: "Many2XSelectPopup", - select_element: function(model, dataset) { + select_element: function(model, initial_ids, initial_view) { this.model = model; - this.dataset = dataset; + this.initial_ids = initial_ids; + this.initial_view = initial_view || "search"; jQuery(this.render()).dialog({title: '', modal: true, minWidth: 800}); @@ -1141,10 +1359,12 @@ openerp.base.form.Many2XSelectPopup = openerp.base.BaseWidget.extend({ }, start: function() { this._super(); - if (!this.dataset) { - this.dataset = new openerp.base.DataSetSearch(this.session, this.model); + this.dataset = new openerp.base.DataSetSearch(this.session, this.model); + if (this.initial_view == "search") { + this.setup_search_view(); + } else { // "form" + this.new_object(); } - this.setup_search_view(); }, setup_search_view: function() { var self = this; @@ -1154,8 +1374,12 @@ openerp.base.form.Many2XSelectPopup = openerp.base.BaseWidget.extend({ this.searchview = new openerp.base.SearchView(null, this.session, this.element_id + "_search", this.dataset, false, {}); this.searchview.on_search.add(function(domains, contexts, groupbys) { - self.view_list.do_search.call( - self, domains, contexts, groupbys); + if (self.initial_ids) { + self.view_list.do_search.call(self, [[["id", "in", self.initial_ids]]], contexts, groupbys); + self.initial_ids = undefined; + } else { + self.view_list.do_search.call(self, domains, contexts, groupbys); + } }); this.searchview.on_loaded.add_last(function () { var $buttons = self.searchview.$element.find(".oe_search-view-buttons"); @@ -1173,13 +1397,8 @@ openerp.base.form.Many2XSelectPopup = openerp.base.BaseWidget.extend({ {'deletable': false}); self.view_list.popup = self; self.view_list.do_show(); - self.view_list.start(); - var tmphack = {"loaded": false}; - self.view_list.on_loaded.add_last(function() { - if ( !tmphack.loaded ) { - self.searchview.do_search(); - tmphack.loaded = true; - }; + self.view_list.start().then(function() { + self.searchview.do_search(); }); }); this.searchview.start(); @@ -1188,8 +1407,12 @@ openerp.base.form.Many2XSelectPopup = openerp.base.BaseWidget.extend({ }, new_object: function() { var self = this; - this.searchview.hide(); - this.view_list.$element.hide(); + if (this.searchview) { + this.searchview.hide(); + } + if (this.view_list) { + this.view_list.$element.hide(); + } this.dataset.index = null; this.view_form = new openerp.base.FormView(null, this.session, this.element_id + "_view_form", this.dataset, false); @@ -1369,6 +1592,7 @@ openerp.base.form.FieldBinaryImage = openerp.base.form.FieldBinary.extend({ * Registry of form widgets, called by :js:`openerp.base.FormView` */ openerp.base.form.widgets = new openerp.base.Registry({ + 'frame' : 'openerp.base.form.WidgetFrame', 'group' : 'openerp.base.form.WidgetFrame', 'notebook' : 'openerp.base.form.WidgetNotebook', 'separator' : 'openerp.base.form.WidgetSeparator', diff --git a/addons/base/static/src/js/list-editable.js b/addons/base/static/src/js/list-editable.js new file mode 100644 index 00000000000..f7eb3e27bca --- /dev/null +++ b/addons/base/static/src/js/list-editable.js @@ -0,0 +1,260 @@ +/** + * @namespace handles editability case for lists, because it depends on form and forms already depends on lists it had to be split out + */ +openerp.base.list.editable = function (openerp) { + var KEY_RETURN = 13, + KEY_ESCAPE = 27; + + // editability status of list rows + openerp.base.ListView.prototype.defaults.editable = null; + + var old_init = openerp.base.ListView.prototype.init, + old_actual_search = openerp.base.ListView.prototype.do_actual_search, + old_add_record = openerp.base.ListView.prototype.do_add_record, + old_on_loaded = openerp.base.ListView.prototype.on_loaded; + // TODO: not sure second @lends on existing item is correct, to check + _.extend(openerp.base.ListView.prototype, /** @lends openerp.base.ListView# */{ + init: function () { + var self = this; + old_init.apply(this, arguments); + $(this.groups).bind({ + 'edit': function (e, id, dataset) { + self.do_edit(dataset.index, id, dataset); + }, + 'saved': function () { + if (self.groups.get_selection().length) { + return; + } + self.compute_aggregates(); + } + }) + }, + /** + * Handles the activation of a record in editable mode (making a record + * editable), called *after* the record has become editable. + * + * The default behavior is to setup the listview's dataset to match + * whatever dataset was provided by the editing List + * + * @param {Number} index index of the record in the dataset + * @param {Object} id identifier of the record being edited + * @param {openerp.base.DataSet} dataset dataset in which the record is available + */ + do_edit: function (index, id, dataset) { + _.extend(this.dataset, dataset); + }, + /** + * Sets editability status for the list, based on defaults, view + * architecture and the provided flag, if any. + * + * @param {Boolean} [force] forces the list to editability. Sets new row edition status to "bottom". + */ + set_editable: function (force) { + // If ``force``, set editability to bottom + // otherwise rely on view default + // view' @editable is handled separately as we have not yet + // fetched and processed the view at this point. + this.options.editable = ( + (force && "bottom") + || this.defaults.editable); + }, + /** + * Replace do_actual_search to handle editability process + */ + do_actual_search: function (results) { + this.set_editable(results.context['set_editable']); + old_actual_search.call(this, results); + }, + /** + * Replace do_add_record to handle editability (and adding new record + * as an editable row at the top or bottom of the list) + */ + do_add_record: function () { + if (this.options.editable) { + this.groups.new_record(); + } else { + old_add_record.call(this); + } + }, + on_loaded: function (data, grouped) { + // tree/@editable takes priority on everything else if present. + this.options.editable = data.fields_view.arch.attrs.editable || this.options.editable; + return old_on_loaded.call(this, data, grouped); + } + }); + + _.extend(openerp.base.ListView.Groups.prototype, /** @lends openerp.base.ListView.Groups# */{ + passtrough_events: openerp.base.ListView.Groups.prototype.passtrough_events + " edit saved", + new_record: function () { + // TODO: handle multiple children + this.children[null].new_record(); + } + }); + + var old_list_row_clicked = openerp.base.ListView.List.prototype.row_clicked; + _.extend(openerp.base.ListView.List.prototype, /** @lends openerp.base.ListView.List */{ + row_clicked: function (event) { + if (!this.options.editable) { + return old_list_row_clicked.call(this, event); + } + this.edit_record(); + }, + /** + * Checks if a record is being edited, and if so cancels it + */ + cancel_pending_edition: function () { + if (!this.edition) { + return; + } + + if (this.edition_index !== null) { + this.reload_record(this.edition_index); + } + this.edition_form.stop(); + this.edition_form.$element.remove(); + delete this.edition_form; + delete this.edition_index; + delete this.edition; + }, + render_row_as_form: function (row) { + this.cancel_pending_edition(); + + var self = this; + var $new_row = $('', { + id: _.uniqueId('oe-editable-row-'), + 'class': $(row).attr('class'), + click: function (e) {e.stopPropagation();} + }) + .keyup(function (e) { + switch (e.which) { + case KEY_RETURN: + self.save_row(true); + break; + case KEY_ESCAPE: + self.cancel_edition(); + break; + default: + return; + } + }) + .delegate('button.oe-edit-row-save', 'click', function () { + self.save_row(); + }) + .delegate('button.oe-edit-row-cancel', 'click', function () { + self.cancel_edition(); + }); + if (row) { + $new_row.replaceAll(row); + } else if (this.options.editable === 'top') { + this.$current.prepend($new_row); + } else if (this.options.editable) { + this.$current.append($new_row); + } + this.edition = true; + this.edition_index = this.dataset.index; + this.edition_form = _.extend(new openerp.base.FormView( + null, this.group.view.session, $new_row.attr('id'), + this.dataset, false), { + template: 'ListView.row.form', + registry: openerp.base.list.form.widgets + }); + $.when(this.edition_form.on_loaded({fields_view: this.get_fields_view()})).then(function () { + // put in $.when just in case FormView.on_loaded becomes asynchronous + $new_row.find('td') + .addClass('oe-field-cell') + .removeAttr('width') + .end() + .find('td:first').removeClass('oe-field-cell').end() + .find('td:last').removeClass('oe-field-cell').end(); + // pad in case of groupby + _(self.columns).each(function (column) { + if (column.meta) { + $new_row.prepend(''); + } + }); + + self.edition_form.do_show(); + }); + }, + /** + * Saves the current row, and triggers the edition of its following + * sibling if asked. + * + * @param {Boolean} [edit_next=false] should the next row become editable + */ + save_row: function (edit_next) { + var self = this; + this.edition_form.do_save(function (result) { + self.reload_record(self.dataset.index, true).then(function () { + self.edition_form.stop(); + delete self.edition_form; + delete self.edition_index; + delete self.edition; + + $(self).trigger('saved', [self.dataset]); + if (!edit_next) { + return; + } + if (result.created) { + self.new_record(); + return; + } + self.dataset.next(); + self.edit_record(); + }); + }, this.options.editable === 'top'); + }, + /** + * Cancels the edition of the row for the current dataset index + */ + cancel_edition: function () { + this.cancel_pending_edition(); + }, + /** + * Edits record currently selected via dataset + */ + edit_record: function () { + this.render_row_as_form( + this.$current.children( + _.sprintf('[data-index=%d]', + this.dataset.index))); + $(this).trigger( + 'edit', + [this.rows[this.dataset.index].data.id.value, this.dataset]); + }, + new_record: function () { + this.dataset.index = null; + this.render_row_as_form(); + } + }); + openerp.base.list = {form: {}}; + openerp.base.list.form.WidgetFrame = openerp.base.form.WidgetFrame.extend({ + template: 'ListView.row.frame' + }); + var form_widgets = openerp.base.form.widgets; + openerp.base.list.form.widgets = form_widgets.clone({ + 'frame': 'openerp.base.list.form.WidgetFrame' + }); + // All form widgets inherit a problematic behavior from + // openerp.base.form.WidgetFrame: the cell itself is removed when invisible + // whether it's @invisible or @attrs[invisible]. In list view, only the + // former should completely remove the cell. We need to override update_dom + // on all widgets since we can't just hit on widget itself (I think) + var list_form_widgets = openerp.base.list.form.widgets; + _(list_form_widgets.map).each(function (widget_path, key) { + if (key === 'frame') { return; } + var new_path = 'openerp.base.list.form.' + key; + + openerp.base.list.form[key] = (form_widgets.get_object(key)).extend({ + update_dom: function () { + this.$element.children().css('visibility', ''); + if (this.invisible && this.node.attrs.invisible !== '1') { + this.$element.children().css('visibility', 'hidden'); + } else { + this._super(); + } + } + }); + list_form_widgets.add(key, new_path); + }); +}; diff --git a/addons/base/static/src/js/list.js b/addons/base/static/src/js/list.js index 7643e2b62d4..b4528de5afd 100644 --- a/addons/base/static/src/js/list.js +++ b/addons/base/static/src/js/list.js @@ -83,8 +83,8 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi 'action': function (e, action_name, id, callback) { self.do_action(action_name, id, callback); }, - 'row_link': function (e, index, id, dataset) { - self.do_activate_record(index, id, dataset); + 'row_link': function (e, id, dataset) { + self.do_activate_record(dataset.index, id, dataset); } }); }, @@ -134,9 +134,11 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi this.$element.html(QWeb.render("ListView", this)); // Head hook - this.$element.find('#oe-list-add').click(this.do_add_record); + this.$element.find('#oe-list-add') + .click(this.do_add_record) + .attr('disabled', grouped && this.options.editable); this.$element.find('#oe-list-delete') - .hide() + .attr('disabled', true) .click(this.do_delete_selected); this.$element.find('thead').delegate('th[data-id]', 'click', function (e) { e.stopPropagation(); @@ -199,18 +201,24 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi return column.invisible !== '1'; }); - this.aggregate_columns = _(this.columns).chain() - .filter(function (column) { - return column['sum'] || column['avg'];}) + this.aggregate_columns = _(this.visible_columns) .map(function (column) { - var func = column['sum'] ? 'sum' : 'avg'; + if (column.type !== 'integer' && column.type !== 'float') { + return {}; + } + var aggregation_func = column['group_operator'] || 'sum'; + + if (!column[aggregation_func]) { + return {}; + } + return { field: column.id, type: column.type, - 'function': func, - label: column[func] + 'function': aggregation_func, + label: column[aggregation_func] }; - }).value(); + }); }, /** * Used to handle a click on a table row, if no other handler caught the @@ -267,6 +275,7 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi return this.rpc('/base/listview/load', { model: this.model, view_id: this.view_id, + context: this.dataset.context, toolbar: !!this.flags.sidebar }, callback); } @@ -289,25 +298,32 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi * @returns {$.Deferred} fold request evaluation promise */ do_search: function (domains, contexts, groupbys) { - var self = this; return this.rpc('/base/session/eval_domain_and_context', { domains: domains, contexts: contexts, group_by_seq: groupbys - }, function (results) { - self.dataset.context = results.context; - self.dataset.domain = results.domain; - self.groups.datagroup = new openerp.base.DataGroup( - self.session, self.model, - results.domain, results.context, - results.group_by); + }, $.proxy(this, 'do_actual_search')); + }, + /** + * Handler for the result of eval_domain_and_context, actually perform the + * searching + * + * @param {Object} results results of evaluating domain and process for a search + */ + do_actual_search: function (results) { + this.dataset.context = results.context; + this.dataset.domain = results.domain; + this.groups.datagroup = new openerp.base.DataGroup( + this.session, this.model, + results.domain, results.context, + results.group_by); - if (_.isEmpty(results.group_by) && !results.context['group_by_no_leaf']) { - results.group_by = null; - } - self.reload_view(!!results.group_by).then( - $.proxy(self, 'reload_content')); - }); + if (_.isEmpty(results.group_by) && !results.context['group_by_no_leaf']) { + results.group_by = null; + } + + this.reload_view(!!results.group_by).then( + $.proxy(this, 'reload_content')); }, /** * Handles the signal to delete a line from the DOM @@ -349,13 +365,15 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi */ do_select: function (ids, records) { this.$element.find('#oe-list-delete') - .toggle(!!ids.length); + .attr('disabled', !ids.length); if (!records.length) { this.compute_aggregates(); return; } - this.compute_aggregates(records); + this.compute_aggregates(_(records).map(function (record) { + return {count: 1, values: record}; + })); }, /** * Handles action button signals on a record @@ -382,7 +400,7 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi * * @param {Number} index index of the record in the dataset * @param {Object} id identifier of the activated record - * @param {openobject.base.DataSet} dataset dataset in which the record is available (may not be the listview's dataset in case of nested groups) + * @param {openerp.base.DataSet} dataset dataset in which the record is available (may not be the listview's dataset in case of nested groups) */ do_activate_record: function (index, id, dataset) { var self = this; @@ -420,75 +438,56 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi * @param {Array} [records] */ compute_aggregates: function (records) { - if (_.isEmpty(this.aggregate_columns)) { - return; - } + var columns = _(this.aggregate_columns).filter(function (column) { + return column['function']; }); + + if (_.isEmpty(columns)) { return; } + if (_.isEmpty(records)) { records = this.groups.get_records(); } - var aggregator = this.build_aggregator(this.aggregate_columns); - this.display_aggregates( - _(records).reduce(aggregator, aggregator).value()); - }, - /** - * Creates a stateful callable aggregator object, which can be reduced over - * a collection of records in order to build the aggregations described - * by the parameter - * - * @param {Array} aggregation_descriptors - */ - build_aggregator: function (aggregation_descriptors) { - var values = {}; - var descriptors = {}; - _(aggregation_descriptors).each(function (descriptor) { - values[descriptor.field] = []; - descriptors[descriptor.field] = descriptor; - }); - - var aggregator = function (_i, record) { - _(values).each(function (collection, key) { - collection.push(record[key]); - }); - - return aggregator; - }; - aggregator.value = function () { - var result = {}; - - _(values).each(function (collection, key) { - var value; - switch(descriptors[key]['function']) { - case 'avg': - value = (_(collection).chain() - .filter(function (item) { - return !_.isUndefined(item); }) - .reduce(function (total, item) { - return total + item; }, 0).value() - / collection.length); - break; + var count = 0, sums = {}; + _(columns).each(function (column) { sums[column.field] = 0; }); + _(records).each(function (record) { + count += record.count || 1; + _(columns).each(function (column) { + var field = column.field; + switch (column['function']) { case 'sum': - value = (_(collection).chain() - .filter(function (item) { - return !_.isUndefined(item); }) - .reduce(function (total, item) { - return total + item; }, 0).value()); + sums[field] += record.values[field]; + break; + case 'avg': + sums[field] += record.count * record.values[field]; break; } - result[key] = value; }); + }); - return result; - }; - return aggregator; + var aggregates = {}; + _(columns).each(function (column) { + var field = column.field; + switch (column['function']) { + case 'sum': + aggregates[field] = sums[field]; + break; + case 'avg': + aggregates[field] = sums[field] / count; + break; + } + }); + + this.display_aggregates(aggregates); }, display_aggregates: function (aggregation) { - var $footer = this.$element.find('.oe-list-footer').empty(); + var $footer_cells = this.$element.find('.oe-list-footer'); _(this.aggregate_columns).each(function (column) { - $(_.sprintf( - "%s: %.2f", - column.label, aggregation[column.field])) - .appendTo($footer); + if (!column['function']) { + return; + } + var pattern = (column.type == 'integer') ? '%d' : '%.2f'; + $footer_cells.filter(_.sprintf('[data-field=%s]', column.field)) + .text(_.sprintf(pattern, aggregation[column.field])); }); } // TODO: implement reorder (drag and drop rows) @@ -521,8 +520,9 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List * @constructs * @param {Object} opts display options, identical to those of :js:class:`openerp.base.ListView` */ - init: function (opts) { + init: function (group, opts) { var self = this; + this.group = group; this.options = opts.options; this.columns = opts.columns; @@ -546,19 +546,26 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List e.stopPropagation(); var $target = $(e.currentTarget), field = $target.closest('td').data('field'), - record_id = self.row_id($target.closest('tr')); + $row = $target.closest('tr'), + record_id = self.row_id($row), + index = self.row_position($row); - $(self).trigger('action', [field, record_id]); + $(self).trigger('action', [field, record_id, function () { + self.reload_record(index, true); + }]); }) .delegate('tr', 'click', function (e) { e.stopPropagation(); - $(self).trigger( - 'row_link', - [self.row_position(e.currentTarget), - self.row_id(e.currentTarget), - self.dataset]); + self.dataset.index = self.row_position(e.currentTarget); + self.row_clicked(e); }); }, + row_clicked: function () { + $(this).trigger( + 'row_link', + [this.rows[this.dataset.index].data.id.value, + this.dataset]); + }, render: function () { if (this.$current) { this.$current.remove(); @@ -566,6 +573,18 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List this.$current = this.$_element.clone(true); this.$current.empty().append($(QWeb.render('ListView.rows', this))); }, + get_fields_view: function () { + // deep copy of view + var view = $.extend(true, {}, this.group.view.fields_view); + _(view.arch.children).each(function (widget) { + widget.attrs.nolabel = true; + if (widget.tag === 'button') { + delete widget.attrs.string; + } + }); + view.arch.attrs.col = 2 * view.arch.children.length; + return view; + }, /** * Gets the ids of all currently selected records, if any * @returns {Object} object with the keys ``ids`` and ``records``, holding respectively the ids of all selected records and the records themselves. @@ -579,7 +598,7 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List this.$current.find('th.oe-record-selector input:checked') .closest('tr').each(function () { var record = {}; - _(rows[$(this).prevAll().length].data).each(function (obj, key) { + _(rows[$(this).data('index')].data).each(function (obj, key) { record[key] = obj.value; }); result.ids.push(record.id); @@ -594,7 +613,7 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List * @returns {Number} the position of the row in this.rows */ row_position: function (row) { - return $(row).prevAll().length; + return $(row).data('index'); }, /** * Returns the identifier of the object displayed in the provided table @@ -621,13 +640,86 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List _(row.data).each(function (obj, key) { record[key] = obj.value; }); - return record; + return {count: 1, values: record}; + }); + }, + /** + * Transforms a record from what is returned by a dataset read (a simple + * mapping of ``$fieldname: $value``) to the format expected by list rows + * and form views: + * + * data: { + * $fieldname: { + * value: $value + * } + * } + * + * This format allows for the insertion of a bunch of metadata (names, + * colors, etc...) + * + * @param {Object} record original record, in dataset format + * @returns {Object} record displayable in a form or list view + */ + transform_record: function (record) { + // TODO: colors handling + var form_data = {}, + form_record = {data: form_data}; + + _(record).each(function (value, key) { + form_data[key] = {value: value}; + }); + + return form_record; + }, + /** + * Reloads the record at index ``row_index`` in the list's rows. + * + * By default, simply re-renders the record. If the ``fetch`` parameter is + * provided and ``true``, will first fetch the record anew. + * + * @param {Number} record_index index of the record to reload + * @param {Boolean} fetch fetches the record from remote before reloading it + */ + reload_record: function (record_index, fetch) { + var self = this; + var read_p = null; + if (fetch) { + // save index to restore it later, if already set + var old_index = this.dataset.index; + this.dataset.index = record_index; + read_p = this.dataset.read_index( + _.filter(_.pluck(this.columns, 'name'), _.identity), + function (record) { + var form_record = self.transform_record(record); + self.rows.splice(record_index, 1, form_record); + self.dataset.index = old_index; + } + ) + } + + return $.when(read_p).then(function () { + self.$current.children().eq(record_index) + .replaceWith(self.render_record(record_index)); }) + }, + /** + * Renders a list record to HTML + * + * @param {Number} record_index index of the record to render in ``this.rows`` + * @returns {String} QWeb rendering of the selected record + */ + render_record: function (record_index) { + return QWeb.render('ListView.row', { + columns: this.columns, + options: this.options, + row: this.rows[record_index], + row_parity: (record_index % 2 === 0) ? 'even' : 'odd', + row_index: record_index }); } // drag and drop - // editable? }); openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Groups# */{ + passtrough_events: 'action deleted row_link', /** * Grouped display for the ListView. Handles basic DOM events and interacts * with the :js:class:`~openerp.base.DataGroup` bound to it. @@ -679,24 +771,11 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr } return red_letter_tboday; }, - open_group: function (e, group) { - var row = e.currentTarget; - - if (this.children[group.value]) { - this.children[group.value].apoptosis(); - delete this.children[group.value]; - } - var prospekt = this.children[group.value] = new openerp.base.ListView.Groups(this.view, { - options: this.options, - columns: this.columns - }); - this.bind_child_events(prospekt); - prospekt.datagroup = group; - prospekt.render().insertAfter( - this.point_insertion(row)); - $(row).find('span.ui-icon') - .removeClass('ui-icon-triangle-1-e') - .addClass('ui-icon-triangle-1-s'); + open: function (point_insertion) { + this.render().insertAfter(point_insertion); + }, + close: function () { + this.apoptosis(); }, /** * Prefixes ``$node`` with floated spaces in order to indent it relative @@ -716,18 +795,32 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr var self = this; var placeholder = this.make_fragment(); _(datagroups).each(function (group) { + if (self.children[group.value]) { + self.children[group.value].apoptosis(); + delete self.children[group.value]; + } + var child = self.children[group.value] = new openerp.base.ListView.Groups(self.view, { + options: self.options, + columns: self.columns + }); + self.bind_child_events(child); + child.datagroup = group; + var $row = $(''); if (group.openable) { $row.click(function (e) { if (!$row.data('open')) { - $row.data('open', true); - self.open_group(e, group); + $row.data('open', true) + .find('span.ui-icon') + .removeClass('ui-icon-triangle-1-e') + .addClass('ui-icon-triangle-1-s'); + child.open(self.point_insertion(e.currentTarget)); } else { $row.removeData('open') .find('span.ui-icon') .removeClass('ui-icon-triangle-1-s') .addClass('ui-icon-triangle-1-e'); - _(self.children).each(function (child) {child.apoptosis();}); + child.close(); } }); } @@ -779,21 +872,7 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr // can have selections spanning multiple links var selection = self.get_selection(); $this.trigger(e, [selection.ids, selection.records]); - }).bind('action', function (e, name, id, callback) { - if (!callback) { - callback = function () { - var $prev = child.$current.prev(); - if (!$prev.is('tbody')) { - // ungrouped - $(self.elements[0]).replaceWith(self.render()); - } else { - // ghetto reload child (and its siblings) - $prev.children().last().click(); - } - }; - } - $this.trigger(e, [name, id, callback]); - }).bind('deleted row_link', function (e) { + }).bind(this.passtrough_events, function (e) { // additional positional parameters are provided to trigger as an // Array, following the event type or event object, but are // provided to the .bind event handler as *args. @@ -806,7 +885,7 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr }, render_dataset: function (dataset) { var rows = [], - list = new openerp.base.ListView.List({ + list = new openerp.base.ListView.List(this, { options: this.options, columns: this.columns, dataset: dataset, @@ -819,17 +898,8 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr _.filter(_.pluck(this.columns, 'name'), _.identity), 0, false, function (records) { - var form_records = _(records).map(function (record) { - // TODO: colors handling - var form_data = {}, - form_record = {data: form_data}; - - _(record).each(function (value, key) { - form_data[key] = {value: value}; - }); - - return form_record; - }); + var form_records = _(records).map( + $.proxy(list, 'transform_record')); rows.splice(0, rows.length); rows.push.apply(rows, form_records); @@ -881,6 +951,12 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr return this; }, get_records: function () { + if (_(this.children).isEmpty()) { + return { + count: this.datagroup.length, + values: this.datagroup.aggregates + } + } return _(this.children).chain() .map(function (child) { return child.get_records(); diff --git a/addons/base/static/src/js/m2o.js b/addons/base/static/src/js/m2o.js index fcb991dcc15..dea7e7c3b2e 100644 --- a/addons/base/static/src/js/m2o.js +++ b/addons/base/static/src/js/m2o.js @@ -1,4 +1,16 @@ openerp.base.m2o = function(openerp){ + +/* + + + + */ openerp.base.m2o = openerp.base.Controller.extend({ init: function(element_id, model, dataset, session){ diff --git a/addons/base/static/src/js/views.js b/addons/base/static/src/js/views.js index 4c259babd8f..04fb1b540ce 100644 --- a/addons/base/static/src/js/views.js +++ b/addons/base/static/src/js/views.js @@ -9,7 +9,7 @@ openerp.base.ActionManager = openerp.base.Controller.extend({ init: function(session, element_id) { this._super(session, element_id); this.viewmanager = null; - this.dialog_stack = [] + this.dialog_stack = []; // Temporary linking view_manager to session. // Will use controller_parent to find it when implementation will be done. session.action_manager = this; @@ -33,8 +33,7 @@ openerp.base.ActionManager = openerp.base.Controller.extend({ case 'ir.actions.act_window': if (action.target == 'new') { var element_id = _.uniqueId("act_window_dialog"); - var dialog = $('
    '); - dialog.dialog({ + $('
    ', {id: element_id}).dialog({ title: action.name, modal: true, width: '50%', @@ -347,7 +346,7 @@ openerp.base.Sidebar = openerp.base.BaseWidget.extend({ var action = self.sections[index[0]].elements[index[1]]; action.flags = { new_window : true - } + }; self.session.action_manager.do_action(action); e.stopPropagation(); e.preventDefault(); @@ -392,7 +391,7 @@ openerp.base.View = openerp.base.Controller.extend({ var context = _.extend({}, dataset.context, action_data.context || {}); switch(action_data.type) { case 'object': - return dataset.call(action_data.name, [record_id], [context], handler); + return dataset.call(action_data.name, [[record_id], context], handler); case 'action': return this.rpc('/base/action/load', { action_id: parseInt(action_data.name, 10) }, handler); default: diff --git a/addons/base/static/src/xml/base.xml b/addons/base/static/src/xml/base.xml index afbd4f81d91..d9901aec2e9 100644 --- a/addons/base/static/src/xml/base.xml +++ b/addons/base/static/src/xml/base.xml @@ -60,14 +60,20 @@ - - + - + + + + + - @@ -281,11 +300,13 @@ - + - +
    + @@ -75,15 +81,28 @@
    + + + + + + +
    +
    +
    @@ -297,7 +318,8 @@ - + @@ -307,8 +329,10 @@ + + t-att-class="'oe-field-cell' + (align ? ' oe-number' : '')" + t-att-data-field="column.id"> @@ -329,6 +353,9 @@ + + +

    @@ -498,15 +525,17 @@ -
    - - - +
    + + + + + +
    @@ -863,4 +892,23 @@ + + + + $(document.createElement('t')) + .append(this.contents()) + .attr({ + 't-foreach': this.attr('t-foreach'), + 't-as': this.attr('t-as') + }) + .replaceAll(this) + .after($(document.createElement('td')).append( + $(document.createElement('button')).attr({ + 'class': 'oe-edit-row-save', 'type': 'button'}).text('Save'))) + .before($(document.createElement('td')).append( + $(document.createElement('button')).attr({ + 'class': 'oe-edit-row-cancel', 'type': 'button'}).text('Cancel'))) + .unwrap(); + +