diff options
| author | Fabien Meghazi <fme@openerp.com> | 2011-08-01 11:26:17 +0200 |
|---|---|---|
| committer | Fabien Meghazi <fme@openerp.com> | 2011-08-01 11:26:17 +0200 |
| commit | a291c46b27bedda79273331ca601f528bf8d0bd2 (patch) | |
| tree | c64f50b40269dca17c6d3997d6c5c7c116743fd5 /addons/base/static/src | |
| parent | 26c20c4f23724563bc56021fd661671ad35421f6 (diff) | |
| parent | c8e7b43acbc3cd5c1dc40ceffe567b12a072cf40 (diff) | |
[MERGE] Merged with trunk
bzr revid: fme@openerp.com-20110801092617-f4vcc8dhx31n5ot3
Diffstat (limited to 'addons/base/static/src')
19 files changed, 3248 insertions, 1661 deletions
diff --git a/addons/base/static/src/base.html b/addons/base/static/src/base.html deleted file mode 100644 index 2761638c6d0..00000000000 --- a/addons/base/static/src/base.html +++ /dev/null @@ -1,55 +0,0 @@ -<!DOCTYPE html> -<html style="height: 100%"> -<head> - <meta http-equiv="content-type" content="text/html; charset=utf-8" /> - <title>OpenERP</title> - <link rel="shortcut icon" href="/base/static/src/img/favicon.ico" type="image/x-icon"/> - - <script type="text/javascript" src="/base/static/lib/LABjs/LAB.js"></script> - <script type="text/javascript" src="/base/static/lib/underscore/underscore.js"></script> - <script type="text/javascript" src="/base/static/lib/underscore/underscore.string.js"></script> - <script type="text/javascript" src="/base/static/lib/qweb/qweb2.js"></script> - <!-- Uncomment in order to use legacy QWeb - <script type="text/javascript" src="/base/static/lib/qweb/qweb.js"></script> - --> - <script type="text/javascript" src="/base/static/lib/jquery/jquery-1.5.2.js"></script> - <script type="text/javascript" src="/base/static/lib/jquery.ui/js/jquery-ui-1.8.9.custom.min.js"></script> - <script type="text/javascript" src="/base/static/lib/jquery.ui/js/jquery-ui-timepicker-addon.js"></script> - <script type="text/javascript" src="/base/static/lib/jquery.ui.notify/js/jquery.notify.js"></script> - <script type="text/javascript" src="/base/static/lib/jquery.superfish/js/hoverIntent.js"></script> - <script type="text/javascript" src="/base/static/lib/jquery.superfish/js/superfish.js"></script> - <script type="text/javascript" src="/base/static/lib/jquery.ba-bbq/jquery.ba-bbq.js"></script> - <script type="text/javascript" src="/base/static/lib/jquery.contextmenu/jquery.contextmenu.r2.packed.js"></script> - <script type="text/javascript" src="/base/static/lib/datejs/date-en-US.js"></script> - <script type="text/javascript" src="/base/static/lib/json/json2.js"></script> - <script type="text/javascript" src="/base/static/src/js/base.js"></script> - <script type="text/javascript" src="/base/static/src/js/dates.js"></script> - <script type="text/javascript" src="/base/static/src/js/chrome.js"></script> - <script type="text/javascript" src="/base/static/src/js/data.js"></script> - <script type="text/javascript" src="/base/static/src/js/views.js"></script> - <script type="text/javascript" src="/base/static/src/js/form.js"></script> - <script type="text/javascript" src="/base/static/src/js/list.js"></script> - <script type="text/javascript" src="/base/static/src/js/list-editable.js"></script> - <script type="text/javascript" src="/base/static/src/js/tree.js"></script> - <script type="text/javascript" src="/base/static/src/js/search.js"></script> - - <link rel="stylesheet" type="text/css" media="screen" href="/base/static/lib/jquery.ui/css/smoothness/jquery-ui-1.8.9.custom.css" /> - <link rel="stylesheet" type="text/css" media="screen" href="/base/static/lib/jquery.ui.notify/css/ui.notify.css" /> - <link rel="stylesheet" type="text/css" href="/base/static/lib/jquery.superfish/css/superfish.css" media="screen"> - - <link rel="stylesheet" href="/base/static/src/css/base.css" type="text/css"/> - <!--[if lte IE 7]> - <link rel="stylesheet" href="/base/static/src/css/base-ie7.css" type="text/css"/> - <![endif]--> - - <script type="text/javascript"> - $(function() { - QWeb = window.QWeb || new QWeb2.Engine(); - var oe = openerp.init(); - oe.base.webclient("oe"); - }); - </script> -</head> -<body id="oe" class="openerp"> -</body> -</html> diff --git a/addons/base/static/src/css/base.css b/addons/base/static/src/css/base.css index 84b51b2cd54..b173386e234 100644 --- a/addons/base/static/src/css/base.css +++ b/addons/base/static/src/css/base.css @@ -30,6 +30,9 @@ body.openerp { .openerp .oe-number { text-align: right !important; } +.openerp .oe_hide { + display: none !important; +} /* STATES */ .openerp .on_logged { @@ -577,6 +580,10 @@ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%, display: none; } +.openerp .searchview_extended_delete_prop { + text-decoration: none; +} + .openerp .searchview_extended_delete_group span, .openerp .searchview_extended_delete_prop span { font-size: 0.9em; @@ -591,6 +598,10 @@ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%, border: 1px solid silver; } +.openerp .oe-listview thead table { + width: 100%; + border: none; +} .openerp .oe-listview tr.odd { background-color: #f3f3f3; } @@ -600,16 +611,27 @@ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%, .openerp .oe-listview tbody tr:hover { background-color: #eae9f0; } +.openerp .oe-listview thead table tr, +.openerp .oe-listview thead table tr:hover { + background: none; +} .openerp .oe-listview td, .openerp .oe-listview th { vertical-align: middle; text-align: left; } -.openerp .oe-listview th.oe-sortable, -.openerp .oe-listview th.oe-sortable .ui-icon { +.openerp .oe-listview td.oe-record-delete { + text-align: right; +} +.openerp .oe-listview th.oe-sortable { cursor: pointer; } +.openerp .oe-listview th.oe-sortable .ui-icon { + height: 1em; + display: inline; + display: inline-block; +} .openerp .oe-listview .oe-field-cell { cursor: pointer; @@ -632,9 +654,22 @@ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%, .openerp .oe-listview th.oe-actions { text-align: left; + white-space: nowrap; } .openerp .oe-listview th.oe-list-pager { text-align: right; + white-space: nowrap; +} +.openerp .oe-list-pager .oe-pager-state { + cursor: pointer; +} + +.openerp .oe-listview .oe-group-name { + padding-right: 1em; +} +.openerp .oe-listview .oe-group-name, +.openerp .oe-listview .oe-group-pagination { + white-space: nowrap; } .openerp .oe-listview tfoot td { @@ -653,29 +688,29 @@ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%, rounded corners are a pain on tables: need to round not only table, but also on the first and last children of the first and last row */ -.openerp .oe-listview table { +.openerp .oe-listview table.oe-listview-content { -webkit-border-radius: 7px; -moz-border-radius: 7px; border-radius: 7px; } -.openerp .oe-listview table thead tr:first-child th:first-child { +.openerp .oe-listview table.oe-listview-content thead tr:first-child th:first-child { -webkit-border-top-left-radius: 7px; -moz-border-radius-topleft: 7px; border-top-left-radius: 7px; } -.openerp .oe-listview table thead tr:first-child th:last-child { +.openerp .oe-listview table.oe-listview-content thead tr:first-child th:last-child { -webkit-border-top-right-radius: 7px; -moz-border-radius-topright: 7px; border-top-right-radius: 7px; } -.openerp .oe-listview table tfoot td:first-child, -.openerp .oe-listview table tbody:last-child tr:last-child th:first-child { +.openerp .oe-listview table.oe-listview-content tfoot td:first-child, +.openerp .oe-listview table.oe-listview-content tbody:last-child tr:last-child th:first-child { -webkit-border-bottom-left-radius: 7px; -moz-border-radius-bottomleft: 7px; border-bottom-left-radius: 7px; } -.openerp .oe-listview table tfoot td:last-child, -.openerp .oe-listview table tbody:last-child tr:last-child td:last-child { +.openerp .oe-listview table.oe-listview-content tfoot td:last-child, +.openerp .oe-listview table.oe-listview-content tbody:last-child tr:last-child td:last-child { -webkit-border-bottom-right-radius: 7px; -moz-border-radius-bottomright: 7px; border-bottom-right-radius: 7px; @@ -773,13 +808,14 @@ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%, position: relative; vertical-align: top; } -.openerp input.field_date, .openerp input.field_datetime { - background: #fff url('../img/ui/field_calendar.png') no-repeat right center; - background-origin: content-box; - -moz-background-origin: content; - -moz-background-origin: content-box; - -webkit-background-origin: content-box; +.openerp img.ui-datepicker-trigger, .openerp img.ui-datepicker-trigger { + margin-left: -20px; + vertical-align: middle; + cursor: pointer; + position: relative; + top: -1px; } + /* http://www.quirksmode.org/dom/inputfile.html * http://stackoverflow.com/questions/2855589/replace-input-type-file-by-an-image */ @@ -987,6 +1023,10 @@ background: linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* W3C */ .openerp .closed-sidebar .toggle-sidebar { border-left: none; } +.openerp li.oe_sidebar_print { + padding-left: 20px; + background: 1px 3px url(../img/icons/gtk-print.png) no-repeat; +} .openerp.kitten-mode-activated .main_table { background: url(http://placekitten.com/g/1500/800) repeat; diff --git a/addons/base/static/src/css/data_export.css b/addons/base/static/src/css/data_export.css new file mode 100644 index 00000000000..7b4b9707b39 --- /dev/null +++ b/addons/base/static/src/css/data_export.css @@ -0,0 +1,85 @@ +.openerp .oe_export_row tr{ + background-color: #FFFFFF; + font-size: 0.8em; + height: 22px; +} + +.openerp tr.ui-selected td { + background-color: #CCCCCC; +} + +.openerp .oe_export_requiredfield { + background-color: #D2D2FF; +} + +.openerp .oe_export_readonlyfield{ + background-color: #999999; +} + +.openerp .oe_export_row:hover{ + background-color: #F3F3F3; +} + +.openerp .oe_export_fields_selector_export { + width: 100%; + height: 400px; +} + +.openerp .oe_export_fields_selector_left { + width: 50%; +} + +.openerp div#left_field_panel { + overflow: scroll; + width: 100%; + height: 400px; + border: solid #999999 1px; +} + +.openerp .oe_export_fields_selector_center { + width: 102px; +} + +.openerp .oe_export_fields_selector_right { + width: 45%; + height: 400px; +} + +.openerp .oe_export_fields_selector_export select{ + width: 100%; + height: 100%; +} + +.openerp .oe_export_tree_header{ + border: 0.5px solid #E3E3E3; + text-align: left; + white-space: nowrap; + padding: 4px 5px; + background: url(/base/static/src/img/header.gif); +} + + +.openerp table.tree_grid{ + border: 1px solid #E3E3E3; + text-align: left; + white-space: nowrap; + background-color:#E3E3E3; + border-collapse: collapse; + width: 100%; +} + +.openerp table.tree_grid a:hover { + color: blue; + border: none; +} + +.openerp table.tree_grid a { + color: #5F5C5C; + border: none; + display: block; +} + +.openerp .oe_export_button_export { + border: 1px solid #006; + background-color: #F3F3F3; +} diff --git a/addons/base/static/src/img/collapse.gif b/addons/base/static/src/img/collapse.gif Binary files differnew file mode 100644 index 00000000000..759e72ebcfe --- /dev/null +++ b/addons/base/static/src/img/collapse.gif diff --git a/addons/base/static/src/img/expand.gif b/addons/base/static/src/img/expand.gif Binary files differnew file mode 100644 index 00000000000..ca98d0b5f2b --- /dev/null +++ b/addons/base/static/src/img/expand.gif diff --git a/addons/base/static/src/img/header.gif b/addons/base/static/src/img/header.gif Binary files differnew file mode 100644 index 00000000000..f40bd9c0f6a --- /dev/null +++ b/addons/base/static/src/img/header.gif diff --git a/addons/base/static/src/js/base.js b/addons/base/static/src/js/boot.js index 55e4929e4c0..c16f2d261f5 100644 --- a/addons/base/static/src/js/base.js +++ b/addons/base/static/src/js/boot.js @@ -1,71 +1,5 @@ -/*--------------------------------------------------------- - * John Resig Class, to be moved to openerp.base.Class - *---------------------------------------------------------*/ - -(function() { - var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; - // The base Class implementation (does nothing) - this.Class = function(){}; - - // Create a new Class that inherits from this class - Class.extend = function(prop) { - var _super = this.prototype; - - // Instantiate a base class (but only create the instance, - // don't run the init constructor) - initializing = true; - var prototype = new this(); - initializing = false; - - // Copy the properties over onto the new prototype - for (var name in prop) { - // Check if we're overwriting an existing function - prototype[name] = typeof prop[name] == "function" && - typeof _super[name] == "function" && fnTest.test(prop[name]) ? - (function(name, fn){ - return function() { - var tmp = this._super; - - // Add a new ._super() method that is the same method - // but on the super-class - this._super = _super[name]; - - // The method only need to be bound temporarily, so we - // remove it when we're done executing - var ret = fn.apply(this, arguments); - this._super = tmp; - - return ret; - }; - })(name, prop[name]) : - prop[name]; - } - - // The dummy class constructor - function Class() { - // All construction is actually done in the init method - if ( !initializing && this.init ) { - var ret = this.init.apply(this, arguments); - if (ret) { return ret; } - } - return this; - } - - // Populate our constructed prototype object - Class.prototype = prototype; - - // Enforce the constructor to be what we expect - Class.constructor = Class; - - // And make this class extendable - Class.extend = arguments.callee; - - return Class; - }; -})(); - //--------------------------------------------------------- -// OpenERP initialisation and black magic about the pool +// OpenERP Web Boostrap //--------------------------------------------------------- /** @@ -107,11 +41,13 @@ _session_id: "session" + session_counter++, screen: openerp.screen, sessions: openerp.sessions, - base: {} + base: {}, + web_mobile: {} }; openerp.sessions[new_instance._session_id] = new_instance; - if (!skip_init) + if (!skip_init){ openerp.base(new_instance); + } return new_instance; } }; @@ -121,8 +57,8 @@ // OpenERP base module split //--------------------------------------------------------- -/** @namespace */ openerp.base = function(instance) { + openerp.base.core(instance); openerp.base.dates(instance); openerp.base.chrome(instance); openerp.base.data(instance); @@ -135,9 +71,6 @@ openerp.base = function(instance) { if (openerp.base.list) { openerp.base.list(instance); } - if (openerp.base.tree) { - openerp.base.tree(instance); - } if (openerp.base. m2o) { openerp.base.m2o(instance); } @@ -147,6 +80,15 @@ openerp.base = function(instance) { if (openerp.base.list && openerp.base.list.editable) { openerp.base.list.editable(instance); } + if (openerp.web_mobile) { + openerp.web_mobile(instance); + } + if (openerp.base.view_tree) { + openerp.base.view_tree(instance); + } + if (openerp.base.data_export) { + openerp.base.data_export(instance); + } }; // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/base/static/src/js/chrome.js b/addons/base/static/src/js/chrome.js index 5e1c7432cb8..ed84c32071b 100644 --- a/addons/base/static/src/js/chrome.js +++ b/addons/base/static/src/js/chrome.js @@ -4,325 +4,15 @@ openerp.base.chrome = function(openerp) { -openerp.base.callback = function(obj, method) { - var callback = function() { - var args = Array.prototype.slice.call(arguments); - var r; - for(var i = 0; i < callback.callback_chain.length; i++) { - var c = callback.callback_chain[i]; - if(c.unique) { - callback.callback_chain.splice(i, 1); - i -= 1; - } - r = c.callback.apply(c.self, c.args.concat(args)); - // TODO special value to stop the chain - // openerp.base.callback_stop - } - return r; - }; - callback.callback_chain = []; - callback.add = function(f) { - if(typeof(f) == 'function') { - f = { callback: f, args: Array.prototype.slice.call(arguments, 1) }; - } - f.self = f.self || null; - f.args = f.args || []; - f.unique = !!f.unique; - if(f.position == 'last') { - callback.callback_chain.push(f); - } else { - callback.callback_chain.unshift(f); - } - return callback; - }; - callback.add_first = function(f) { - return callback.add.apply(null,arguments); - }; - callback.add_last = function(f) { - return callback.add({ - callback: f, - args: Array.prototype.slice.call(arguments, 1), - position: "last" - }); - }; - - return callback.add({ - callback: method, - self:obj, - args:Array.prototype.slice.call(arguments, 2) - }); -}; - -/** - * Base error for lookup failure - * - * @class - */ -openerp.base.NotFound = Class.extend( /** @lends openerp.base.NotFound# */ { -}); -openerp.base.KeyNotFound = openerp.base.NotFound.extend( /** @lends openerp.base.KeyNotFound# */ { - /** - * Thrown when a key could not be found in a mapping - * - * @constructs - * @extends openerp.base.NotFound - * @param {String} key the key which could not be found - */ - init: function (key) { - this.key = key; - }, - toString: function () { - return "The key " + this.key + " was not found"; - } -}); -openerp.base.ObjectNotFound = openerp.base.NotFound.extend( /** @lends openerp.base.ObjectNotFound# */ { +openerp.base.Session = openerp.base.Widget.extend( /** @lends openerp.base.Session# */{ /** - * Thrown when an object path does not designate a valid class or object - * in the openerp hierarchy. - * - * @constructs - * @extends openerp.base.NotFound - * @param {String} path the invalid object path - */ - init: function (path) { - this.path = path; - }, - toString: function () { - return "Could not find any object of path " + this.path; - } -}); -openerp.base.Registry = Class.extend( /** @lends openerp.base.Registry# */ { - /** - * Stores a mapping of arbitrary key (strings) to object paths (as strings - * as well). - * - * Resolves those paths at query time in order to always fetch the correct - * object, even if those objects have been overloaded/replaced after the - * registry was created. - * - * An object path is simply a dotted name from the openerp root to the - * object pointed to (e.g. ``"openerp.base.Session"`` for an OpenERP - * session object). - * * @constructs - * @param {Object} mapping a mapping of keys to object-paths - */ - init: function (mapping) { - this.map = mapping || {}; - }, - /** - * Retrieves the object matching the provided key string. - * - * @param {String} key the key to fetch the object for - * @returns {Class} the stored class, to initialize - * - * @throws {openerp.base.KeyNotFound} if the object was not in the mapping - * @throws {openerp.base.ObjectNotFound} if the object path was invalid - */ - get_object: function (key) { - var path_string = this.map[key]; - if (path_string === undefined) { - throw new openerp.base.KeyNotFound(key); - } - - var object_match = openerp; - var path = path_string.split('.'); - // ignore first section - for(var i=1; i<path.length; ++i) { - object_match = object_match[path[i]]; - - if (object_match === undefined) { - throw new openerp.base.ObjectNotFound(path_string); - } - } - return object_match; - }, - /** - * Tries a number of keys, and returns the first object matching one of - * the keys. - * - * @param {Array} keys a sequence of keys to fetch the object for - * @returns {Class} the first class found matching an object - * - * @throws {openerp.base.KeyNotFound} if none of the keys was in the mapping - * @trows {openerp.base.ObjectNotFound} if a found object path was invalid - */ - get_any: function (keys) { - for (var i=0; i<keys.length; ++i) { - try { - return this.get_object(keys[i]); - } catch (e) { - if (e instanceof openerp.base.KeyNotFound) { - continue; - } - throw e; - } - } - throw new openerp.base.KeyNotFound(keys.join(',')); - }, - /** - * Adds a new key and value to the registry. - * - * This method can be chained. - * - * @param {String} key - * @param {String} object_path fully qualified dotted object path - * @returns {openerp.base.Registry} itself - */ - 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 || {})); - } -}); - -openerp.base.BasicController = Class.extend( /** @lends openerp.base.BasicController# */{ - /** - * rpc operations, event binding and callback calling should be done in - * start() instead of init so that event can be hooked in between. - * - * @constructs - */ - init: function(element_id) { - this.element_id = element_id; - this.$element = $('#' + element_id); - if (element_id) { - openerp.screen[element_id] = this; - } - - // Transform on_* method into openerp.base.callbacks - for (var name in this) { - if(typeof(this[name]) == "function") { - this[name].debug_name = name; - // bind ALL function to this not only on_and _do ? - if((/^on_|^do_/).test(name)) { - this[name] = openerp.base.callback(this, this[name]); - } - } - } - }, - /** - * Controller start - * event binding, rpc and callback calling required to initialize the - * object can happen here - * - * Returns a promise object letting callers (subclasses and direct callers) - * know when this component is done starting - * - * @returns {jQuery.Deferred} - */ - start: function() { - // returns an already fulfilled promise. Maybe we could return nothing? - // $.when can take non-deferred and in that case it simply considers - // them all as fulfilled promises. - // But in thise case we *have* to ensure callers use $.when and don't - // try to call deferred methods on this return value. - return $.Deferred().done().promise(); - }, - stop: function() { - }, - log: function() { - var args = Array.prototype.slice.call(arguments); - var caller = arguments.callee.caller; - // TODO add support for line number using - // https://github.com/emwendelin/javascript-stacktrace/blob/master/stacktrace.js - // args.unshift("" + caller.debug_name); - this.on_log.apply(this,args); - }, - on_log: function() { - if(window.openerp.debug || (window.location.search.indexOf('?debug') !== -1)) { - var notify = false; - var body = false; - if(window.console) { - console.log(arguments); - } else { - body = true; - } - var a = Array.prototype.slice.call(arguments, 0); - for(var i = 0; i < a.length; i++) { - var v = a[i]==null ? "null" : a[i].toString(); - if(i==0) { - notify = v.match(/^not/); - body = v.match(/^bod/); - } - if(body) { - $('<pre></pre>').text(v).appendTo($('body')); - } - if(notify && this.notification) { - this.notification.notify("Logging:",v); - } - } - } - - } -}); - -/** - * Generates an inherited class that replaces all the methods by null methods (methods - * that does nothing and always return undefined). - * - * @param {Class} claz - * @param {dict} add Additional functions to override. - * @return {Class} - */ -openerp.base.generate_null_object_class = function(claz, add) { - var newer = {}; - var copy_proto = function(prototype) { - for (var name in prototype) { - if(typeof prototype[name] == "function") { - newer[name] = function() {}; - } - } - if (prototype.prototype) - copy_proto(prototype.prototype); - }; - copy_proto(claz.prototype); - newer.init = openerp.base.BasicController.prototype.init; - var tmpclass = claz.extend(newer); - return tmpclass.extend(add || {}); -}; - -openerp.base.Notification = openerp.base.BasicController.extend({ - init: function(element_id) { - this._super(element_id); - this.$element.notify({ - speed: 500, - expires: 1500 - }); - }, - notify: function(title, text) { - this.$element.notify('create', { - title: title, - text: text - }); - }, - warn: function(title, text) { - this.$element.notify('create', 'oe_notification_alert', { - title: title, - text: text - }); - } -}); - -openerp.base.Session = openerp.base.BasicController.extend( /** @lends openerp.base.Session# */{ - /** - * @constructs - * @extends openerp.base.BasicController * @param element_id to use for exception reporting * @param server * @param port */ - init: function(element_id, server, port) { - this._super(element_id); + init: function(parent, element_id, server, port) { + this._super(parent, element_id); this.server = (server == undefined) ? location.hostname : server; this.port = (port == undefined) ? location.port : port; this.rpc_mode = (server == location.hostname) ? "ajax" : "jsonp"; @@ -350,15 +40,12 @@ openerp.base.Session = openerp.base.BasicController.extend( /** @lends openerp.b * @param {Object} params call parameters * @param {Function} success_callback function to execute on RPC call success * @param {Function} error_callback function to execute on RPC call failure - * one * @returns {jQuery.Deferred} jquery-provided ajax deferred */ rpc: function(url, params, success_callback, error_callback) { var self = this; // Construct a JSON-RPC2 request, method is currently unused params.session_id = this.session_id; - // niv: wtf? - //params.context = typeof(params.context) != "undefined" ? params.context : this.context; // Call using the rpc_mode var deferred = $.Deferred(); @@ -368,7 +55,7 @@ openerp.base.Session = openerp.base.BasicController.extend( /** @lends openerp.b params: params, id:null }).then(function () {deferred.resolve.apply(deferred, arguments);}, - function(error) {deferred.reject(error, $.Event());}); + function(error) {deferred.reject(error, $.Event());}); return deferred.fail(function() { deferred.fail(function(error, event) { if (!event.isDefaultPrevented()) { @@ -401,30 +88,35 @@ openerp.base.Session = openerp.base.BasicController.extend( /** @lends openerp.b }, url); var deferred = $.Deferred(); $.ajax(ajax).done(function(response, textStatus, jqXHR) { - self.on_rpc_response(); - if (response.error) { - if (response.error.data.type == "session_invalid") { - self.uid = false; - self.on_session_invalid(function() { - self.rpc(url, payload.params, - function() {deferred.resolve.apply(deferred, arguments);}, - function() {deferred.reject.apply(deferred, arguments);}); - }); - } else { - deferred.reject(response.error); - } - } else { - deferred.resolve(response["result"], textStatus, jqXHR); - } - }).fail(function(jqXHR, textStatus, errorThrown) { - self.on_rpc_response(); - var error = { - code: -32098, - message: "XmlHttpRequestError " + errorThrown, - data: {type: "xhr"+textStatus, debug: jqXHR.responseText, objects: [jqXHR, errorThrown] } - }; - deferred.reject(error); + self.on_rpc_response(); + if (!response.error) { + deferred.resolve(response["result"], textStatus, jqXHR); + return; + } + if (response.error.data.type !== "session_invalid") { + deferred.reject(response.error); + return; + } + self.uid = false; + self.on_session_invalid(function() { + self.rpc(url, payload.params, + function() { + deferred.resolve.apply(deferred, arguments); + }, + function(error, event) { + event.preventDefault(); + deferred.reject.apply(deferred, arguments); + }); }); + }).fail(function(jqXHR, textStatus, errorThrown) { + self.on_rpc_response(); + var error = { + code: -32098, + message: "XmlHttpRequestError " + errorThrown, + data: {type: "xhr"+textStatus, debug: jqXHR.responseText, objects: [jqXHR, errorThrown] } + }; + deferred.reject(error); + }); return deferred.promise(); }, on_rpc_request: function() { @@ -499,7 +191,7 @@ openerp.base.Session = openerp.base.BasicController.extend( /** @lends openerp.b for(var i=0; i<cookies.length; ++i) { var cookie = cookies[i].replace(/^\s*/, ''); if(cookie.indexOf(nameEQ) === 0) { - return decodeURIComponent(cookie.substring(nameEQ.length)); + return JSON.parse(decodeURIComponent(cookie.substring(nameEQ.length))); } } return null; @@ -515,7 +207,7 @@ openerp.base.Session = openerp.base.BasicController.extend( /** @lends openerp.b set_cookie: function (name, value, ttl) { ttl = ttl || 24*60*60*365; document.cookie = [ - this.element_id + '|' + name + '=' + encodeURIComponent(value), + this.element_id + '|' + name + '=' + encodeURIComponent(JSON.stringify(value)), 'max-age=' + ttl, 'expires=' + new Date(new Date().getTime() + ttl*1000).toGMTString() ].join(';'); @@ -526,15 +218,20 @@ openerp.base.Session = openerp.base.BasicController.extend( /** @lends openerp.b load_modules: function() { var self = this; this.rpc('/base/session/modules', {}, function(result) { - self.module_list = result['modules']; + self.module_list = result; var modules = self.module_list.join(','); - self.rpc('/base/session/csslist', {mods: modules}, self.do_load_css); - self.rpc('/base/session/jslist', {"mods": modules}, self.debug ? self.do_load_modules_debug : self.do_load_modules_prod); + if(self.debug || true) { + self.rpc('/base/webclient/csslist', {"mods": modules}, self.do_load_css); + self.rpc('/base/webclient/jslist', {"mods": modules}, self.do_load_js); + } else { + self.do_load_css(["/base/webclient/css?mods="+modules]); + self.do_load_js(["/base/webclient/js?mods="+modules]); + } openerp._modules_loaded = true; }); }, - do_load_css: function (result) { - _.each(result.files, function (file) { + do_load_css: function (files) { + _.each(files, function (file) { $('head').append($('<link>', { 'href': file, 'rel': 'stylesheet', @@ -542,16 +239,23 @@ openerp.base.Session = openerp.base.BasicController.extend( /** @lends openerp.b })); }); }, - do_load_modules_debug: function(result) { - $LAB.setOptions({AlwaysPreserveOrder: true}) - .script(result.files) - .wait(this.on_modules_loaded); - }, - do_load_modules_prod: function() { - // load merged ones - // /base/session/css?mod=mod1,mod2,mod3 - // /base/session/js?mod=mod1,mod2,mod3 - // use $.getScript(‘your_3rd_party-script.js’); ? i want to keep lineno ! + do_load_js: function(files) { + var self = this; + if(files.length != 0) { + var file = files.shift(); + var tag = document.createElement('script'); + tag.type = 'text/javascript'; + tag.src = file; + tag.onload = tag.onreadystatechange = function() { + if ( (tag.readyState && tag.readyState != "loaded" && tag.readyState != "complete") || tag.onload_done ) + return; + tag.onload_done = true; + self.do_load_js(files); + }; + document.head.appendChild(tag); + } else { + this.on_modules_loaded(); + } }, on_modules_loaded: function() { for(var j=0; j<this.module_list.length; j++) { @@ -568,209 +272,34 @@ openerp.base.Session = openerp.base.BasicController.extend( /** @lends openerp.b } }); -// A controller takes an already existing element -// new() -// start() -openerp.base.Controller = openerp.base.BasicController.extend( /** @lends openerp.base.Controller# */{ - /** - * Controller manifest used to declare standard controller attributes - */ - controller_manifest: { - register: null, - template: "", - element_post_prefix: false - }, - /** - * Controller registry, - */ - controller_registry: { - }, - /** - * Add a new child controller - */ - controller_get: function(key) { - return this.controller_registry[key]; - // OR should build it ? setting parent correctly ? - // function construct(constructor, args) { - // function F() { - // return constructor.apply(this, args); - // } - // F.prototype = constructor.prototype; - // return new F(); - // } - // var obj = this.controller_registry[key]; - // if(obj) { - // return construct(obj, Array.prototype.slice.call(arguments, 1)); - // } - }, - controller_new: function(key) { - var self; - // OR should contrustct it ? setting parent correctly ? - function construct(constructor, args) { - function F() { - return constructor.apply(this, args); - } - F.prototype = constructor.prototype; - return new F(); - } - var obj = this.controller_registry[key]; - if(obj) { - // TODO Prepend parent - return construct(obj, Array.prototype.slice.call(arguments, 1)); - } - }, - /** - * @constructs - * @extends openerp.base.BasicController - */ - init: function(parent_or_session, element_id) { - this._super(element_id); - this.controller_parent = null; - this.controller_children = []; - if(parent_or_session) { - if(parent_or_session.session) { - this.parent = parent_or_session; - this.session = this.parent.session; - if(this.parent.children) { - this.parent.children.push(this); - } - } else { - // TODO remove Backward compatilbility - this.session = parent_or_session; - } - } - // Apply manifest options - if(this.controller_manifest) { - var register = this.controller_manifest.register; - // TODO accept a simple string - if(register) { - for(var i=0; i<register.length; i++) { - this.controller_registry[register[i]] = this; - } - } - // TODO if post prefix - //this.element_id = _.uniqueId(_.toArray(arguments).join('_')); - } - }, - /** - * Performs a JSON-RPC call - * - * @param {String} url endpoint url - * @param {Object} data RPC parameters - * @param {Function} success RPC call success callback - * @param {Function} error RPC call error callback - * @returns {jQuery.Deferred} deferred object for the RPC call - */ - rpc: function(url, data, success, error) { - // TODO: support additional arguments ? - return this.session.rpc(url, data, success, error); - } -}); - -// A widget is a controller that doesnt take an element_id -// it render its own html that you should insert into the dom -// and bind it a start() -// -// new() -// render() and insert it place it where you want -// start() -openerp.base.BaseWidget = openerp.base.Controller.extend({ - /** - * The name of the QWeb template that will be used for rendering. Must be - * redefined in subclasses or the render() method can not be used. - * - * @type string - */ - template: null, - /** - * The prefix used to generate an id automatically. Should be redefined in - * subclasses. If it is not defined, a default identifier will be used. - * - * @type string - */ - identifier_prefix: 'generic-identifier', - /** - * Base class for widgets. Handle rendering (based on a QWeb template), - * identifier generation, parenting and destruction of the widget. - * Also initialize the identifier. - * - * @constructs - * @params {openerp.base.search.BaseWidget} parent The parent widget. - */ - init: function (parent, session) { - this._super(session); - this.children = []; - this.parent = null; - this.set_parent(parent); - this.make_id(this.identifier_prefix); - }, - /** - * Sets and returns a globally unique identifier for the widget. - * - * If a prefix is appended, the identifier will be appended to it. - * - * @params sections prefix sections, empty/falsy sections will be removed - */ - make_id: function () { - this.element_id = _.uniqueId(_.toArray(arguments).join('_')); - return this.element_id; - }, - /** - * "Starts" the widgets. Called at the end of the rendering, this allows - * to get a jQuery object referring to the DOM ($element attribute). - */ - start: function () { - this._super(); - var tmp = document.getElementById(this.element_id); - this.$element = tmp ? $(tmp) : null; - }, - /** - * "Stops" the widgets. Called when the view destroys itself, this - * lets the widgets clean up after themselves. - */ - stop: function () { - var tmp_children = this.children; - this.children = []; - _.each(tmp_children, function(x) { - x.stop(); +openerp.base.Notification = openerp.base.Widget.extend({ + init: function(parent, element_id) { + this._super(parent, element_id); + this.$element.notify({ + speed: 500, + expires: 1500 }); - if(this.$element != null) { - this.$element.remove(); - } - this.set_parent(null); - this._super(); }, - /** - * Set the parent of this component, also un-register the previous parent - * if there was one. - * - * @param {openerp.base.BaseWidget} parent The new parent. - */ - set_parent: function(parent) { - if(this.parent) { - this.parent.children = _.without(this.parent.children, this); - } - this.parent = parent; - if(this.parent) { - parent.children.push(this); - } + notify: function(title, text) { + this.$element.notify('create', { + title: title, + text: text + }); }, - /** - * Render the widget. This.template must be defined. - * The content of the current object is passed as context to the template. - * - * @param {object} additional Additional context arguments to pass to the template. - */ - render: function (additional) { - return QWeb.render(this.template, _.extend({}, this, additional != null ? additional : {})); + warn: function(title, text) { + this.$element.notify('create', 'oe_notification_alert', { + title: title, + text: text + }); } }); -openerp.base.Dialog = openerp.base.BaseWidget.extend({ +openerp.base.Dialog = openerp.base.OldWidget.extend({ dialog_title: "", identifier_prefix: 'dialog', - init: function (session, options) { - this._super(null, session); + init: function (parent, options) { + var self = this; + this._super(parent); this.options = { modal: true, width: 'auto', @@ -780,7 +309,10 @@ openerp.base.Dialog = openerp.base.BaseWidget.extend({ min_height: 0, max_height: '100%', autoOpen: false, - buttons: {} + buttons: {}, + beforeClose: function () { + self.on_close(); + } }; for (var f in this) { if (f.substr(0, 10) == 'on_button_') { @@ -843,18 +375,23 @@ openerp.base.Dialog = openerp.base.BaseWidget.extend({ this.set_options(options); this.$dialog.dialog(this.options).dialog('open'); }, - close: function(options) { + close: function() { + // Closes the dialog but leave it in a state where it could be opened again. this.$dialog.dialog('close'); }, + on_close: function() { + }, stop: function () { + // Destroy widget + this.close(); this.$dialog.dialog('destroy'); } }); openerp.base.CrashManager = openerp.base.Dialog.extend({ identifier_prefix: 'dialog_crash', - init: function(session) { - this._super(session); + init: function(parent) { + this._super(parent); this.session.on_rpc_error.add(this.on_rpc_error); }, on_button_Ok: function() { @@ -887,12 +424,9 @@ openerp.base.CrashManager = openerp.base.Dialog.extend({ } }); -openerp.base.Loading = openerp.base.Controller.extend({ - controller_manifest: { - register: ["Loading"] - }, - init: function(session, element_id) { - this._super(session, element_id); +openerp.base.Loading = openerp.base.Widget.extend({ + init: function(parent, element_id) { + this._super(parent, element_id); this.count = 0; this.session.on_rpc_request.add_first(this.on_rpc_event, 1); this.session.on_rpc_response.add_last(this.on_rpc_event, -1); @@ -909,27 +443,29 @@ openerp.base.Loading = openerp.base.Controller.extend({ } }); -openerp.base.Database = openerp.base.Controller.extend({ +openerp.base.Database = openerp.base.Widget.extend({ }); -openerp.base.Login = openerp.base.Controller.extend({ - init: function(session, element_id) { - this._super(session, element_id); +openerp.base.Login = openerp.base.Widget.extend({ + remember_creditentials: true, + init: function(parent, element_id) { + this._super(parent, 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; + if (this.has_local_storage && this.remember_creditentials) { 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'); + } + if (jQuery.deparam(jQuery.param.querystring()).debug != undefined) { + this.selected_db = this.selected_db || "trunk"; + this.selected_login = this.selected_login || "admin"; + this.selected_password = this.selected_password || "a"; } }, start: function() { var self = this; - this.rpc("/base/session/get_databases_list", {}, function(result) { + this.rpc("/base/database/get_databases_list", {}, function(result) { self.db_list = result.db_list; self.display(); }, function() { @@ -953,22 +489,17 @@ 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'); + if(self.remember_creditentials) { 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(); @@ -993,9 +524,9 @@ openerp.base.Login = openerp.base.Controller.extend({ } }); -openerp.base.Header = openerp.base.Controller.extend({ - init: function(session, element_id) { - this._super(session, element_id); +openerp.base.Header = openerp.base.Widget.extend({ + init: function(parent, element_id) { + this._super(parent, element_id); }, start: function() { this.do_update(); @@ -1007,9 +538,9 @@ openerp.base.Header = openerp.base.Controller.extend({ on_logout: function() {} }); -openerp.base.Menu = openerp.base.Controller.extend({ - init: function(session, element_id, secondary_menu_id) { - this._super(session, element_id); +openerp.base.Menu = openerp.base.Widget.extend({ + init: function(parent, element_id, secondary_menu_id) { + this._super(parent, element_id); this.secondary_menu_id = secondary_menu_id; this.$secondary_menu = $("#" + secondary_menu_id); this.menu = false; @@ -1088,45 +619,43 @@ openerp.base.Menu = openerp.base.Controller.extend({ } }); -openerp.base.Homepage = openerp.base.Controller.extend({ +openerp.base.Homepage = openerp.base.Widget.extend({ }); -openerp.base.Preferences = openerp.base.Controller.extend({ +openerp.base.Preferences = openerp.base.Widget.extend({ }); -openerp.base.ImportExport = openerp.base.Controller.extend({ +openerp.base.ImportExport = openerp.base.Widget.extend({ }); -openerp.base.WebClient = openerp.base.Controller.extend({ +openerp.base.WebClient = openerp.base.Widget.extend({ init: function(element_id) { - var self = this; this._super(null, element_id); - QWeb.add_template("xml/base.xml"); + QWeb.add_template("/base/static/src/xml/base.xml"); var params = {}; - if(jQuery.param != undefined && - jQuery.deparam(jQuery.param.querystring()).kitten != undefined) { + if(jQuery.param != undefined && jQuery.deparam(jQuery.param.querystring()).kitten != undefined) { this.$element.addClass("kitten-mode-activated"); } this.$element.html(QWeb.render("Interface", params)); - this.session = new openerp.base.Session("oe_errors"); - this.loading = new openerp.base.Loading(this.session, "oe_loading"); - this.crashmanager = new openerp.base.CrashManager(this.session); + this.session = new openerp.base.Session(this,"oe_errors"); + this.loading = new openerp.base.Loading(this,"oe_loading"); + this.crashmanager = new openerp.base.CrashManager(this); this.crashmanager.start(false); - // Do you autorize this ? - openerp.base.Controller.prototype.notification = new openerp.base.Notification("oe_notification"); + // Do you autorize this ? will be replaced by notify() in controller + openerp.base.Widget.prototype.notification = new openerp.base.Notification(this, "oe_notification"); - this.header = new openerp.base.Header(this.session, "oe_header"); - this.login = new openerp.base.Login(this.session, "oe_login"); + this.header = new openerp.base.Header(this, "oe_header"); + this.login = new openerp.base.Login(this, "oe_login"); this.header.on_logout.add(this.login.on_logout); this.session.on_session_invalid.add(this.login.do_ask_login); this.session.on_session_valid.add_last(this.header.do_update); this.session.on_session_valid.add_last(this.on_logged); - this.menu = new openerp.base.Menu(this.session, "oe_menu", "oe_secondary_menu"); + this.menu = new openerp.base.Menu(this, "oe_menu", "oe_secondary_menu"); this.menu.on_action.add(this.on_menu_action); }, start: function() { @@ -1137,9 +666,9 @@ openerp.base.WebClient = openerp.base.Controller.extend({ this.notification.notify("OpenERP Client", "The openerp client has been initialized."); }, on_logged: function() { - this.action_manager = new openerp.base.ActionManager(this.session, "oe_app"); + this.action_manager = new openerp.base.ActionManager(this, "oe_app"); this.action_manager.start(); - + // if using saved actions, load the action and give it to action manager var parameters = jQuery.deparam(jQuery.param.querystring()); if(parameters["s_action"] != undefined) { diff --git a/addons/base/static/src/js/core.js b/addons/base/static/src/js/core.js new file mode 100644 index 00000000000..790542be7e4 --- /dev/null +++ b/addons/base/static/src/js/core.js @@ -0,0 +1,574 @@ +/*--------------------------------------------------------- + * OpenERP controller framework + *--------------------------------------------------------*/ + +openerp.base.core = function(openerp) { +/** + * John Resig Class with factory improvement + */ +(function() { + var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; + // The base Class implementation (does nothing) + this.Class = function(){}; + + // Create a new Class that inherits from this class + Class.extend = function(prop) { + var _super = this.prototype; + + // Instantiate a base class (but only create the instance, + // don't run the init constructor) + initializing = true; + var prototype = new this(); + initializing = false; + + // Copy the properties over onto the new prototype + for (var name in prop) { + // Check if we're overwriting an existing function + prototype[name] = typeof prop[name] == "function" && + typeof _super[name] == "function" && fnTest.test(prop[name]) ? + (function(name, fn){ + return function() { + var tmp = this._super; + + // Add a new ._super() method that is the same method + // but on the super-class + this._super = _super[name]; + + // The method only need to be bound temporarily, so we + // remove it when we're done executing + var ret = fn.apply(this, arguments); + this._super = tmp; + + return ret; + }; + })(name, prop[name]) : + prop[name]; + } + + // The dummy class constructor + function Class() { + // All construction is actually done in the init method + if ( !initializing && this.init ) { + var ret = this.init.apply(this, arguments); + if (ret) { return ret; } + } + return this; + } + + // Populate our constructed prototype object + Class.prototype = prototype; + + // Enforce the constructor to be what we expect + Class.constructor = Class; + + // And make this class extendable + Class.extend = arguments.callee; + + return Class; + }; +})(); + +// todo change john resig class to keep window clean +openerp.base.Class = window.Class + +openerp.base.callback = function(obj, method) { + var callback = function() { + var args = Array.prototype.slice.call(arguments); + var r; + for(var i = 0; i < callback.callback_chain.length; i++) { + var c = callback.callback_chain[i]; + if(c.unique) { + callback.callback_chain.splice(i, 1); + i -= 1; + } + r = c.callback.apply(c.self, c.args.concat(args)); + // TODO special value to stop the chain + // openerp.base.callback_stop + } + return r; + }; + callback.callback_chain = []; + callback.add = function(f) { + if(typeof(f) == 'function') { + f = { callback: f, args: Array.prototype.slice.call(arguments, 1) }; + } + f.self = f.self || null; + f.args = f.args || []; + f.unique = !!f.unique; + if(f.position == 'last') { + callback.callback_chain.push(f); + } else { + callback.callback_chain.unshift(f); + } + return callback; + }; + callback.add_first = function(f) { + return callback.add.apply(null,arguments); + }; + callback.add_last = function(f) { + return callback.add({ + callback: f, + args: Array.prototype.slice.call(arguments, 1), + position: "last" + }); + }; + + return callback.add({ + callback: method, + self:obj, + args:Array.prototype.slice.call(arguments, 2) + }); +}; + +/** + * Generates an inherited class that replaces all the methods by null methods (methods + * that does nothing and always return undefined). + * + * @param {Class} claz + * @param {dict} add Additional functions to override. + * @return {Class} + */ +openerp.base.generate_null_object_class = function(claz, add) { + var newer = {}; + var copy_proto = function(prototype) { + for (var name in prototype) { + if(typeof prototype[name] == "function") { + newer[name] = function() {}; + } + } + if (prototype.prototype) + copy_proto(prototype.prototype); + }; + copy_proto(claz.prototype); + newer.init = openerp.base.Widget.prototype.init; + var tmpclass = claz.extend(newer); + return tmpclass.extend(add || {}); +}; + +/** + * Base error for lookup failure + * + * @class + */ +openerp.base.NotFound = openerp.base.Class.extend( /** @lends openerp.base.NotFound# */ { +}); +openerp.base.KeyNotFound = openerp.base.NotFound.extend( /** @lends openerp.base.KeyNotFound# */ { + /** + * Thrown when a key could not be found in a mapping + * + * @constructs + * @extends openerp.base.NotFound + * @param {String} key the key which could not be found + */ + init: function (key) { + this.key = key; + }, + toString: function () { + return "The key " + this.key + " was not found"; + } +}); +openerp.base.ObjectNotFound = openerp.base.NotFound.extend( /** @lends openerp.base.ObjectNotFound# */ { + /** + * Thrown when an object path does not designate a valid class or object + * in the openerp hierarchy. + * + * @constructs + * @extends openerp.base.NotFound + * @param {String} path the invalid object path + */ + init: function (path) { + this.path = path; + }, + toString: function () { + return "Could not find any object of path " + this.path; + } +}); +openerp.base.Registry = openerp.base.Class.extend( /** @lends openerp.base.Registry# */ { + /** + * Stores a mapping of arbitrary key (strings) to object paths (as strings + * as well). + * + * Resolves those paths at query time in order to always fetch the correct + * object, even if those objects have been overloaded/replaced after the + * registry was created. + * + * An object path is simply a dotted name from the openerp root to the + * object pointed to (e.g. ``"openerp.base.Session"`` for an OpenERP + * session object). + * + * @constructs + * @param {Object} mapping a mapping of keys to object-paths + */ + init: function (mapping) { + this.map = mapping || {}; + }, + /** + * Retrieves the object matching the provided key string. + * + * @param {String} key the key to fetch the object for + * @returns {Class} the stored class, to initialize + * + * @throws {openerp.base.KeyNotFound} if the object was not in the mapping + * @throws {openerp.base.ObjectNotFound} if the object path was invalid + */ + get_object: function (key) { + var path_string = this.map[key]; + if (path_string === undefined) { + throw new openerp.base.KeyNotFound(key); + } + + var object_match = openerp; + var path = path_string.split('.'); + // ignore first section + for(var i=1; i<path.length; ++i) { + object_match = object_match[path[i]]; + + if (object_match === undefined) { + throw new openerp.base.ObjectNotFound(path_string); + } + } + return object_match; + }, + /** + * Tries a number of keys, and returns the first object matching one of + * the keys. + * + * @param {Array} keys a sequence of keys to fetch the object for + * @returns {Class} the first class found matching an object + * + * @throws {openerp.base.KeyNotFound} if none of the keys was in the mapping + * @trows {openerp.base.ObjectNotFound} if a found object path was invalid + */ + get_any: function (keys) { + for (var i=0; i<keys.length; ++i) { + try { + return this.get_object(keys[i]); + } catch (e) { + if (e instanceof openerp.base.KeyNotFound) { + continue; + } + throw e; + } + } + throw new openerp.base.KeyNotFound(keys.join(',')); + }, + /** + * Adds a new key and value to the registry. + * + * This method can be chained. + * + * @param {String} key + * @param {String} object_path fully qualified dotted object path + * @returns {openerp.base.Registry} itself + */ + 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 || {})); + } +}); + +/** + * Utility class that any class is allowed to extend to easy common manipulations. + * + * It provides rpc calls, callback on all methods preceded by "on_" or "do_" and a + * logging facility. + */ +openerp.base.SessionAware = openerp.base.Class.extend({ + init: function(session) { + this.session = session; + + // Transform on_* method into openerp.base.callbacks + for (var name in this) { + if(typeof(this[name]) == "function") { + this[name].debug_name = name; + // bind ALL function to this not only on_and _do ? + if((/^on_|^do_/).test(name)) { + this[name] = openerp.base.callback(this, this[name]); + } + } + } + }, + /** + * Performs a JSON-RPC call + * + * @param {String} url endpoint url + * @param {Object} data RPC parameters + * @param {Function} success RPC call success callback + * @param {Function} error RPC call error callback + * @returns {jQuery.Deferred} deferred object for the RPC call + */ + rpc: function(url, data, success, error) { + return this.session.rpc(url, data, success, error); + }, + log: function() { + var args = Array.prototype.slice.call(arguments); + var caller = arguments.callee.caller; + // TODO add support for line number using + // https://github.com/emwendelin/javascript-stacktrace/blob/master/stacktrace.js + // args.unshift("" + caller.debug_name); + this.on_log.apply(this,args); + }, + on_log: function() { + if(window.openerp.debug || (window.location.search.indexOf('?debug') !== -1)) { + var notify = false; + var body = false; + if(window.console) { + console.log(arguments); + } else { + body = true; + } + var a = Array.prototype.slice.call(arguments, 0); + for(var i = 0; i < a.length; i++) { + var v = a[i]==null ? "null" : a[i].toString(); + if(i==0) { + notify = v.match(/^not/); + body = v.match(/^bod/); + } + if(body) { + $('<pre></pre>').text(v).appendTo($('body')); + } + if(notify && this.notification) { + this.notification.notify("Logging:",v); + } + } + } + } +}); + +/** + * Base class for all visual components. Provides a lot of functionalities helpful + * for the management of a part of the DOM. + * + * Widget handles: + * - Rendering with QWeb. + * - Life-cycle management and parenting (when a parent is destroyed, all its children are + * destroyed too). + * - Insertion in DOM. + * + * Widget also extends SessionAware for ease of use. + * + * Guide to create implementations of this class: + * ============================================== + * + * Here is a sample child class: + * + * MyWidget = openerp.base.Widget.extend({ + * // the name of the QWeb template to use for rendering + * template: "MyQWebTemplate", + * // identifier prefix, useful to put an obvious one for debugging + * identifier_prefix: 'my-id-prefix-', + * + * init: function(parent) { + * this._super(parent); + * // stuff that you want to init before the rendering + * }, + * start: function() { + * // stuff you want to make after the rendering, `this.$element` holds a value + * this.$element.find(".my_button").click(/* an example of event binding * /); + * + * // if you have some asynchronous operations, it's a good idea to return + * // a promise in start() + * var promise = this.rpc(...); + * return promise; + * } + * }); + * + * Now this class can simply be used with the following syntax: + * + * var my_widget = new MyWidget(this); + * my_widget.appendTo($(".some-div")); + * + * With these two lines, the MyWidget instance was inited, rendered, it was inserted into the + * DOM inside the ".some-div" div and its events were binded. + * + * And of course, when you don't need that widget anymore, just do: + * + * my_widget.stop(); + * + * That will kill the widget in a clean way and erase its content from the dom. + */ +openerp.base.Widget = openerp.base.SessionAware.extend({ + /** + * The name of the QWeb template that will be used for rendering. Must be + * redefined in subclasses or the default render() method can not be used. + * + * @type string + */ + template: null, + /** + * The prefix used to generate an id automatically. Should be redefined in + * subclasses. If it is not defined, a generic identifier will be used. + * + * @type string + */ + identifier_prefix: 'generic-identifier-', + /** + * @constructs + * Construct the widget and set its parent if a parent is given. + * + * @param {Widget} parent Binds the current instance to the given Widget instance. + * When that widget is destroyed by calling stop(), the current instance will be + * destroyed too. Can be null. + * @param {String} element_id Deprecated. Sets the element_id. Only useful when you want + * to bind the current Widget to an already existing part of the DOM, which is not compatible + * with the DOM insertion methods provided by the current implementation of Widget. So + * for new components this argument should not be provided any more. + */ + init: function(parent, /** @deprecated */ element_id) { + this._super((parent || {}).session); + // if given an element_id, try to get the associated DOM element and save + // a reference in this.$element. Else just generate a unique identifier. + this.element_id = element_id; + this.element_id = this.element_id || _.uniqueId(this.identifier_prefix); + var tmp = document.getElementById(this.element_id); + this.$element = tmp ? $(tmp) : undefined; + + this.widget_parent = parent; + this.widget_children = []; + if(parent && parent.widget_children) { + parent.widget_children.push(this); + } + // useful to know if the widget was destroyed and should not be used anymore + this.widget_is_stopped = false; + }, + /** + * Render the current widget and appends it to the given jQuery object or Widget. + * + * @param target A jQuery object or a Widget instance. + */ + appendTo: function(target) { + var self = this; + return this._render_and_insert(function(t) { + self.$element.appendTo(t); + }, target); + }, + /** + * Render the current widget and prepends it to the given jQuery object or Widget. + * + * @param target A jQuery object or a Widget instance. + */ + prependTo: function(target) { + var self = this; + return this._render_and_insert(function(t) { + self.$element.prependTo(t); + }, target); + }, + /** + * Render the current widget and inserts it after to the given jQuery object or Widget. + * + * @param target A jQuery object or a Widget instance. + */ + insertAfter: function(target) { + var self = this; + return this._render_and_insert(function(t) { + self.$element.insertAfter(t); + }, target); + }, + /** + * Render the current widget and inserts it before to the given jQuery object or Widget. + * + * @param target A jQuery object or a Widget instance. + */ + insertBefore: function(target) { + var self = this; + return this._render_and_insert(function(t) { + self.$element.insertBefore(t); + }, target); + }, + _render_and_insert: function(insertion, target) { + var rendered = this.render(); + this.$element = $(rendered); + if (target instanceof openerp.base.Widget) + target = target.$element; + insertion(target); + return this.start(); + }, + /** + * Renders the widget using QWeb, `this.template` must be defined. + * The context given to QWeb contains the "widget" key that references `this`. + * + * @param {object} additional Additional context arguments to pass to the template. + */ + render: function (additional) { + return QWeb.render(this.template, _.extend({widget: this}, additional || {})); + }, + /** + * Method called after rendering. Mostly used to bind actions, perform asynchronous + * calls, etc... + * + * By convention, the method should return a promise to inform the caller when + * this widget has been initialized. + * + * @returns {jQuery.Deferred} + */ + start: function() { + if (!this.$element) { + var tmp = document.getElementById(this.element_id); + this.$element = tmp ? $(tmp) : undefined; + } + return $.Deferred().done().promise(); + }, + /** + * Destroys the current widget, also destory all its children before destroying itself. + */ + stop: function() { + _.each(_.clone(this.widget_children), function(el) { + el.stop(); + }); + if(this.$element != null) { + this.$element.remove(); + } + if (this.widget_parent && this.widget_parent.widget_children) { + this.widget_parent.widget_children = _.without(this.widget_parent.widget_children, this); + } + this.widget_parent = null; + this.widget_is_stopped = true; + }, + /** + * Inform the action manager to do an action. Of course, this suppose that + * the action manager can be found amongst the ancestors of the current widget. + * If that's not the case this method will simply return `false`. + */ + do_action: function(action, on_finished) { + if (this.widget_parent) { + return this.widget_parent.do_action(action, on_finished); + } + return false; + }, + rpc: function(url, data, success, error) { + var def = $.Deferred().then(success, error); + var self = this; + this._super(url, data). then(function() { + if (!self.widget_is_stopped) + def.resolve.apply(def, arguments); + }, function() { + if (!self.widget_is_stopped) + def.reject.apply(def, arguments); + }); + return def.promise(); + } +}); + +/** + * @deprecated + * For retro compatibility only, the only difference with is that render() uses + * directly `this` instead of context with a "widget" key. + */ +openerp.base.OldWidget = openerp.base.Widget.extend({ + render: function (additional) { + return QWeb.render(this.template, _.extend(_.extend({}, this), additional || {})); + } +}); + +} +// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/base/static/src/js/data.js b/addons/base/static/src/js/data.js index a8cdfb7f201..5545e32df17 100644 --- a/addons/base/static/src/js/data.js +++ b/addons/base/static/src/js/data.js @@ -1,7 +1,24 @@ openerp.base.data = function(openerp) { -openerp.base.DataGroup = openerp.base.Controller.extend( /** @lends openerp.base.DataGroup# */{ +/** + * Serializes the sort criterion array of a dataset into a form which can be + * consumed by OpenERP's RPC APIs. + * + * @param {Array} criterion array of fields, from first to last criteria, prefixed with '-' for reverse sorting + * @returns {String} SQL-like sorting string (``ORDER BY``) clause + */ +openerp.base.serialize_sort = function (criterion) { + return _.map(criterion, + function (criteria) { + if (criteria[0] === '-') { + return criteria.slice(1) + ' DESC'; + } + return criteria + ' ASC'; + }).join(', '); +}; + +openerp.base.DataGroup = openerp.base.Widget.extend( /** @lends openerp.base.DataGroup# */{ /** * Management interface between views and grouped collections of OpenERP * records. @@ -13,7 +30,7 @@ openerp.base.DataGroup = openerp.base.Controller.extend( /** @lends openerp.bas * content of the current grouping level. * * @constructs - * @extends openerp.base.Controller + * @extends openerp.base.Widget * * @param {openerp.base.Session} session Current OpenERP session * @param {String} model name of the model managed by this DataGroup @@ -22,18 +39,16 @@ openerp.base.DataGroup = openerp.base.Controller.extend( /** @lends openerp.bas * @param {Array} group_by sequence of fields by which to group * @param {Number} [level=0] nesting level of the group */ - init: function(session, model, domain, context, group_by, level) { + init: function(parent, model, domain, context, group_by, level) { + this._super(parent, null); if (group_by) { if (group_by.length || context['group_by_no_leaf']) { - return new openerp.base.ContainerDataGroup( - session, model, domain, context, group_by, level); + return new openerp.base.ContainerDataGroup( this, model, domain, context, group_by, level); } else { - return new openerp.base.GrouplessDataGroup( - session, model, domain, context, level); + return new openerp.base.GrouplessDataGroup( this, model, domain, context, level); } } - this._super(session, null); this.model = model; this.context = context; this.domain = domain; @@ -42,8 +57,7 @@ openerp.base.DataGroup = openerp.base.Controller.extend( /** @lends openerp.bas }, cls: 'DataGroup' }); -openerp.base.ContainerDataGroup = openerp.base.DataGroup.extend( - /** @lends openerp.base.ContainerDataGroup# */ { +openerp.base.ContainerDataGroup = openerp.base.DataGroup.extend( /** @lends openerp.base.ContainerDataGroup# */ { /** * * @constructs @@ -56,8 +70,8 @@ openerp.base.ContainerDataGroup = openerp.base.DataGroup.extend( * @param group_by * @param level */ - init: function (session, model, domain, context, group_by, level) { - this._super(session, model, domain, context, null, level); + init: function (parent, model, domain, context, group_by, level) { + this._super(parent, model, domain, context, null, level); this.group_by = group_by; }, @@ -120,29 +134,26 @@ openerp.base.ContainerDataGroup = openerp.base.DataGroup.extend( aggregates: aggregates }; }, - fetch: function () { + fetch: function (fields) { // internal method var d = new $.Deferred(); var self = this; - // disable caching for now, not sure what I should do there - if (false && this.groups) { - d.resolveWith(this, [this.groups]); - } else { - this.rpc('/base/group/read', { - model: this.model, - context: this.context, - domain: this.domain, - group_by_fields: this.group_by - }, function () { }).then(function (response) { - var data_groups = _(response.result).map( - _.bind(self.transform_group, self)); - self.groups = data_groups; - d.resolveWith(self, [data_groups]); - }, function () { - d.rejectWith.apply(d, self, [arguments]); - }); - } + this.rpc('/base/group/read', { + model: this.model, + context: this.context, + domain: this.domain, + fields: _.uniq(this.group_by.concat(fields)), + group_by_fields: this.group_by, + sort: openerp.base.serialize_sort(this.sort) + }, function () { }).then(function (response) { + var data_groups = _(response).map( + _.bind(self.transform_group, self)); + self.groups = data_groups; + d.resolveWith(self, [data_groups]); + }, function () { + d.rejectWith.apply(d, [self, arguments]); + }); return d.promise(); }, /** @@ -163,24 +174,27 @@ openerp.base.ContainerDataGroup = openerp.base.DataGroup.extend( * records have for the current ``grouped_on`` field name). * ``aggregates`` * a mapping of other aggregation fields provided by ``read_group`` + * + * @param {Array} fields the list of fields to aggregate in each group, can be empty + * @param {Function} ifGroups function executed if any group is found (DataGroup.group_by is non-null and non-empty), called with a (potentially empty) list of groups as parameters. + * @param {Function} ifRecords function executed if there is no grouping left to perform, called with a DataSet instance as parameter */ - list: function (ifGroups, ifRecords) { + list: function (fields, ifGroups, ifRecords) { var self = this; - this.fetch().then(function (group_records) { + this.fetch(fields).then(function (group_records) { ifGroups(_(group_records).map(function (group) { var child_context = _.extend({}, self.context, group.__context); return _.extend( new openerp.base.DataGroup( - self.session, self.model, group.__domain, + self, self.model, group.__domain, child_context, child_context.group_by, self.level + 1), - group); + group, {sort: self.sort}); })); }); } }); -openerp.base.GrouplessDataGroup = openerp.base.DataGroup.extend( - /** @lends openerp.base.GrouplessDataGroup# */ { +openerp.base.GrouplessDataGroup = openerp.base.DataGroup.extend( /** @lends openerp.base.GrouplessDataGroup# */ { /** * * @constructs @@ -192,16 +206,16 @@ openerp.base.GrouplessDataGroup = openerp.base.DataGroup.extend( * @param context * @param level */ - init: function (session, model, domain, context, level) { - this._super(session, model, domain, context, null, level); + init: function (parent, model, domain, context, level) { + this._super(parent, model, domain, context, null, level); }, - list: function (ifGroups, ifRecords) { - ifRecords(new openerp.base.DataSetSearch(this.session, this.model, this.context, this.domain)); + list: function (fields, ifGroups, ifRecords) { + ifRecords(_.extend( + new openerp.base.DataSetSearch(this, this.model), + {domain: this.domain, context: this.context, _sort: this.sort})); } }); - -openerp.base.StaticDataGroup = openerp.base.GrouplessDataGroup.extend( - /** @lends openerp.base.StaticDataGroup# */ { +openerp.base.StaticDataGroup = openerp.base.GrouplessDataGroup.extend( /** @lends openerp.base.StaticDataGroup# */ { /** * A specialization of groupless data groups, relying on a single static * dataset as its records provider. @@ -213,24 +227,25 @@ openerp.base.StaticDataGroup = openerp.base.GrouplessDataGroup.extend( init: function (dataset) { this.dataset = dataset; }, - list: function (ifGroups, ifRecords) { + list: function (fields, ifGroups, ifRecords) { ifRecords(this.dataset); } }); -openerp.base.DataSet = openerp.base.Controller.extend( /** @lends openerp.base.DataSet# */{ +openerp.base.DataSet = openerp.base.Widget.extend( /** @lends openerp.base.DataSet# */{ /** * DateaManagement interface between views and the collection of selected * OpenERP records (represents the view's state?) * * @constructs - * @extends openerp.base.Controller + * @extends openerp.base.Widget * - * @param {openerp.base.Session} session current OpenERP session * @param {String} model the OpenERP model this dataset will manage */ - init: function(session, model, context) { - this._super(session); + init: function(source_controller, model, context) { + // we don't want the dataset to be a child of anything! + this._super(null); + this.session = source_controller ? source_controller.session : undefined; this.model = model; this.context = context || {}; this.index = null; @@ -276,14 +291,18 @@ openerp.base.DataSet = openerp.base.Controller.extend( /** @lends openerp.base. * Read the indexed record. */ read_index: function (fields, callback) { + var def = $.Deferred().then(callback); if (_.isEmpty(this.ids)) { - return $.Deferred().reject().promise(); + def.reject(); } else { fields = fields || false; - return this.read_ids([this.ids[this.index]], fields, function(records) { - callback(records[0]); + return this.read_ids([this.ids[this.index]], fields).then(function(records) { + def.resolve(records[0]); + }, function() { + def.reject.apply(def, arguments); }); } + return def.promise(); }, default_get: function(fields, callback) { return this.rpc('/base/dataset/default_get', { @@ -362,26 +381,24 @@ openerp.base.DataSet = openerp.base.Controller.extend( /** @lends openerp.base. return this.context; } }); - openerp.base.DataSetStatic = openerp.base.DataSet.extend({ - init: function(session, model, context, ids) { - this._super(session, model, context); + init: function(parent, model, context, ids) { + this._super(parent, model, context); // all local records this.ids = ids || []; - if (this.ids.length) { - this.index = 0; - } }, read_slice: function (fields, offset, limit, callback) { var self = this; offset = offset || 0; var end_pos = limit && limit !== -1 ? offset + limit : undefined; - this.read_ids(this.ids.slice(offset, end_pos), fields, callback); + return this.read_ids(this.ids.slice(offset, end_pos), fields, callback); }, set_ids: function (ids) { this.ids = ids; - this.index = this.index <= this.ids.length - 1 ? - this.index : (this.ids.length > 0 ? this.length - 1 : 0); + if (this.index !== null) { + this.index = this.index <= this.ids.length - 1 ? + this.index : (this.ids.length > 0 ? this.length - 1 : 0); + } }, unlink: function(ids) { this.on_unlink(ids); @@ -391,10 +408,9 @@ openerp.base.DataSetStatic = openerp.base.DataSet.extend({ this.set_ids(_.without.apply(null, [this.ids].concat(ids))); } }); - openerp.base.DataSetSearch = openerp.base.DataSet.extend({ - init: function(session, model, context, domain) { - this._super(session, model, context); + init: function(parent, model, context, domain) { + this._super(parent, model, context); this.domain = domain || []; this._sort = []; this.offset = 0; @@ -413,7 +429,7 @@ openerp.base.DataSetSearch = openerp.base.DataSet.extend({ // return read_ids(ids.slice(start,start+limit),fields,callback) } } - this.rpc('/base/dataset/search_read', { + return this.rpc('/base/dataset/search_read', { model: this.model, fields: fields, domain: this.domain, @@ -421,13 +437,12 @@ openerp.base.DataSetSearch = openerp.base.DataSet.extend({ sort: this.sort(), offset: offset, limit: limit - }, function (records) { - self.ids.splice(0, self.ids.length); + }, function (result) { + self.ids = result.ids; self.offset = offset; - for (var i=0; i < records.length; i++ ) { - self.ids.push(records[i].id); + if (callback) { + callback(result.records); } - callback(records); }); }, /** @@ -445,16 +460,12 @@ openerp.base.DataSetSearch = openerp.base.DataSet.extend({ */ sort: function (field, force_reverse) { if (!field) { - return _.map(this._sort, function (criteria) { - if (criteria[0] === '-') { - return criteria.slice(1) + ' DESC'; - } - return criteria + ' ASC'; - }).join(', '); + return openerp.base.serialize_sort(this._sort); } - var reverse = force_reverse || (this._sort[0] === field); - this._sort = _.without(this._sort, field, '-' + field); + this._sort.splice.apply( + this._sort, [0, this._sort.length].concat( + _.without(this._sort, field, '-' + field))); this._sort.unshift((reverse ? '-' : '') + field); return undefined; @@ -463,14 +474,15 @@ openerp.base.DataSetSearch = openerp.base.DataSet.extend({ var self = this; return this._super(ids, function(result) { self.ids = _.without.apply(_, [self.ids].concat(ids)); - self.index = self.index <= self.ids.length - 1 ? - self.index : (self.ids.length > 0 ? self.ids.length -1 : 0); + if (this.index !== null) { + self.index = self.index <= self.ids.length - 1 ? + self.index : (self.ids.length > 0 ? self.ids.length -1 : 0); + } if (callback) callback(result); }, error_callback); } }); - openerp.base.BufferedDataSet = openerp.base.DataSetStatic.extend({ virtual_id_prefix: "one2many_v_id_", virtual_id_regex: /one2many_v_id_.*/, @@ -483,26 +495,34 @@ openerp.base.BufferedDataSet = openerp.base.DataSetStatic.extend({ var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data}; this.to_create.push(cached); this.cache.push(cached); - this.on_change(); var to_return = $.Deferred().then(callback); - setTimeout(function() {to_return.resolve({result: cached.id});}, 0); + to_return.resolve({result: cached.id}); return to_return.promise(); }, write: function (id, data, callback) { var self = this; var record = _.detect(this.to_create, function(x) {return x.id === id;}); record = record || _.detect(this.to_write, function(x) {return x.id === id;}); + var dirty = false; if (record) { + for (k in data) { + if (record.values[k] === undefined || record.values[k] !== data[k]) { + dirty = true; + break; + } + } $.extend(record.values, data); } else { + dirty = true; record = {id: id, values: data}; self.to_write.push(record); } var cached = _.detect(this.cache, function(x) {return x.id === id;}); $.extend(cached.values, record.values); - this.on_change(); + if (dirty) + this.on_change(); var to_return = $.Deferred().then(callback); - setTimeout(function () {to_return.resolve({result: true});}, 0); + to_return.resolve({result: true}); return to_return.promise(); }, unlink: function(ids, callback, error_callback) { @@ -576,7 +596,6 @@ openerp.base.BufferedDataSet = openerp.base.DataSetStatic.extend({ return completion.promise(); } }); - openerp.base.ReadOnlyDataSetSearch = openerp.base.DataSetSearch.extend({ create: function(data, callback, error_callback) { this.on_create(data); @@ -586,12 +605,12 @@ openerp.base.ReadOnlyDataSetSearch = openerp.base.DataSetSearch.extend({ }, on_create: function(data) {}, write: function (id, data, callback) { - this.on_write(id); + this.on_write(id, data); var to_return = $.Deferred().then(callback); setTimeout(function () {to_return.resolve({"result": true});}, 0); return to_return.promise(); }, - on_write: function(id) {}, + on_write: function(id, data) {}, unlink: function(ids, callback, error_callback) { this.on_unlink(ids); var to_return = $.Deferred().then(callback); diff --git a/addons/base/static/src/js/data_export.js b/addons/base/static/src/js/data_export.js new file mode 100644 index 00000000000..ad440302c69 --- /dev/null +++ b/addons/base/static/src/js/data_export.js @@ -0,0 +1,402 @@ +openerp.base.data_export = function(openerp) { +openerp.base.DataExport = openerp.base.Dialog.extend({ + init: function(parent, dataset) { + this._super(parent); + this.dataset = dataset; + }, + start: function() { + var self = this; + self._super(false); + self.template = 'ExportTreeView'; + self.dialog_title = "Export Data"; + self.open({ + modal: true, + width: '55%', + height: 'auto', + position: 'top', + buttons : { + "Close" : function() { + self.close(); + }, + "Export To File" : function() { + self.on_click_export_data(); + } + }, + close: function(event, ui){ self.close();} + }); + self.on_show_exists_export_list(); + self.$element.removeClass('ui-dialog-content ui-widget-content'); + self.$element.find('#add_field').click(function() { + if ($('#field-tree-structure tr.ui-selected')) { + var fld = self.$element.find('#field-tree-structure tr.ui-selected').find('a'); + for (var i = 0; i < fld.length; i++) { + var id = $(fld[i]).attr('id').split('-')[1]; + var string = $(fld[i]).attr('string'); + self.add_field(id, string); + } + self.$element.find('#field-tree-structure tr').removeClass('ui-selected'); + } + }); + self.$element.find('#remove_field').click(function() { + self.$element.find('#fields_list option:selected').remove(); + }); + self.$element.find('#remove_all_field').click(function() { + self.$element.find('#fields_list option').remove(); + }); + self.$element.find('#export_new_list').click(function() { + self.on_show_save_list(); + }); + var import_comp = self.$element.find('#import_compat option:selected').val(), + params = { + import_compat: parseInt(import_comp) + }; + self.rpc('/base/export/get_fields', { model: self.dataset.model, params: params }, self.on_show_data); + + self.$element.find('#import_compat').change(function() { + self.$element.find('#fields_list option').remove(); + self.$element.find('#field-tree-structure').remove(); + var import_comp = self.$element.find("#import_compat option:selected").val(); + if (import_comp) { + var params = { + import_compat: parseInt(import_comp) + } + self.rpc("/base/export/get_fields", { model: self.dataset.model, params: params}, self.on_show_data); + } + }); + }, + on_show_exists_export_list: function() { + var self = this; + if (self.$element.find('#saved_export_list').is(':hidden')) { + self.$element.find('#ExistsExportList').show(); + } else { + this.rpc('/base/export/exist_export_lists', { 'model': this.dataset.model}, function(export_list) { + if (export_list.length) { + self.$element.find('#ExistsExportList').append(QWeb.render('Exists.ExportList', {'existing_exports': export_list})); + self.$element.find('#saved_export_list').change(function() { + self.$element.find('#fields_list option').remove(); + var export_id = self.$element.find('#saved_export_list option:selected').val(); + if (export_id) { + self.rpc('/base/export/namelist', {'model': self.dataset.model, export_id: parseInt(export_id)}, self.do_load_export_field); + } + }); + self.$element.find('#delete_export_list').click(function() { + var select_exp = self.$element.find('#saved_export_list option:selected'); + if (select_exp.val()) { + self.rpc('/base/export/delete_export', { export_id: parseInt(select_exp.val())}, {}); + select_exp.remove(); + if (self.$element.find('#saved_export_list option').length <= 1) { + self.$element.find('#ExistsExportList').hide(); + } + } + }); + } + }); + } + }, + do_load_export_field: function(field_list) { + var export_node = this.$element.find("#fields_list"); + for (var key in field_list) { + export_node.append(new Option(field_list[key], key)); + } + }, + on_show_save_list: function() { + var self = this; + var current_node = self.$element.find("#savenewlist"); + if (!(current_node.find("label")).length) { + current_node.append(QWeb.render('ExportNewList')); + current_node.find("#add_export_list").click(function() { + var value = current_node.find("#savelist_name").val(); + if (value) { + self.do_save_export_list(value); + } else { + alert("Pleae Enter Save Field List Name"); + } + }); + } else { + if (current_node.is(':hidden')) { + current_node.show(); + current_node.find("#savelist_name").val(""); + } else { + current_node.hide(); + } + } + }, + do_save_export_list: function(value) { + var self = this; + var export_field = self.get_fields(); + if (export_field.length) { + self.rpc("/base/export/save_export_lists", {"model": self.dataset.model, "name":value, "field_list":export_field}, function(exp_id) { + if (exp_id) { + if (self.$element.find("#saved_export_list").length > 0) { + self.$element.find("#saved_export_list").append(new Option(value, exp_id)); + } else { + self.on_show_exists_export_list(); + } + if (self.$element.find("#saved_export_list").is(":hidden")) { + self.on_show_exists_export_list(); + } + } + }); + self.on_show_save_list(); + self.$element.find("#fields_list option").remove(); + } + }, + on_click: function(id, result) { + var self = this; + self.field_id = id.split("-")[1]; + var is_loaded = 0; + _.each(result, function(record) { + if (record['id'] == self.field_id && (record['children']).length >= 1) { + var model = record['params']['model'], + prefix = record['params']['prefix'], + name = record['params']['name']; + $(record['children']).each(function(e, childid) { + if (self.$element.find("tr[id='treerow-" + childid + "']").length > 0) { + if (self.$element.find("tr[id='treerow-" + childid + "']").is(':hidden')) { + is_loaded = -1; + } else { + is_loaded++; + } + } + }); + if (is_loaded == 0) { + if (self.$element.find("tr[id='treerow-" + self.field_id +"']").find('img').attr('src') === '/base/static/src/img/expand.gif') { + if (model) { + var import_comp = self.$element.find("#import_compat option:selected").val(); + var params = { + import_compat: parseInt(import_comp), + parent_field_type : record['field_type'] + } + self.rpc("/base/export/get_fields", { + model: model, + prefix: prefix, + name: name, + field_parent : self.field_id, + params: params + }, function(results) { + self.on_show_data(results); + }); + } + } + } else if (is_loaded > 0) { + self.showcontent(self.field_id, true); + } else { + self.showcontent(self.field_id, false); + } + } + }); + }, + on_show_data: function(result) { + var self = this; + var imp_cmpt = parseInt(self.$element.find("#import_compat option:selected").val()); + var current_tr = self.$element.find("tr[id='treerow-" + self.field_id + "']"); + if (current_tr.length >= 1) { + current_tr.find('img').attr('src','/base/static/src/img/collapse.gif'); + current_tr.after(QWeb.render('ExportTreeView-Secondary.children', {'fields': result})); + } else { + self.$element.find('#left_field_panel').append(QWeb.render('ExportTreeView-Secondary', {'fields': result})); + } + _.each(result, function(record) { + if ((record.field_type == "one2many") && imp_cmpt) { + var o2m_fld = self.$element.find("tr[id='treerow-" + record.id + "']").find('#tree-column'); + o2m_fld.addClass("oe_export_readonlyfield"); + } + if ((record.required == true) || record.required == "True") { + var required_fld = self.$element.find("tr[id='treerow-" + record.id + "']").find('#tree-column'); + required_fld.addClass("oe_export_requiredfield"); + } + self.$element.find("img[id='parentimg-" + record.id +"']").click(function() { + self.on_click(this.id, result); + }); + + self.$element.find("tr[id='treerow-" + record.id + "']").click(function(e) { + if (e.shiftKey == true) { + var frst_click, scnd_click = ''; + if (self.row_index == 0) { + self.row_index = this.rowIndex; + frst_click = self.$element.find("tr[id^='treerow-']")[self.row_index-1]; + $(frst_click).addClass("ui-selected"); + } else { + if (this.rowIndex >=self.row_index) { + for (i = (self.row_index-1); i < this.rowIndex; i++) { + scnd_click = self.$element.find("tr[id^='treerow-']")[i]; + if (!$(scnd_click).find('#tree-column').hasClass("oe_export_readonlyfield")) { + $(scnd_click).addClass("ui-selected"); + } + } + } else { + for (i = (self.row_index-1); i >= (this.rowIndex-1); i--) { + scnd_click = self.$element.find("tr[id^='treerow-']")[i]; + if (!$(scnd_click).find('#tree-column').hasClass("oe_export_readonlyfield")) { + $(scnd_click).addClass("ui-selected"); + } + } + } + } + } + self.row_index = this.rowIndex; + + self.$element.find("tr[id='treerow-" + record.id + "']").keyup(function(e) { + self.row_index = 0; + }); + var o2m_selection = self.$element.find("tr[id='treerow-" + record.id + "']").find('#tree-column'); + if ($(o2m_selection).hasClass("oe_export_readonlyfield")) { + return false; + } + var selected = self.$element.find("tr.ui-selected"); + if ($(this).hasClass("ui-selected") && (e.ctrlKey == true)) { + $(this).find('a').blur(); + $(this).removeClass("ui-selected"); + } else if ($(this).hasClass("ui-selected") && (e.ctrlKey == false) && (e.shiftKey == false)) { + selected.find('a').blur(); + selected.removeClass("ui-selected"); + $(this).find('a').focus(); + $(this).addClass("ui-selected"); + } else if (!$(this).hasClass("ui-selected") && (e.ctrlKey == false) && (e.shiftKey == false)) { + selected.find('a').blur(); + selected.removeClass("ui-selected"); + $(this).find('a').focus(); + $(this).addClass("ui-selected"); + } else if (!$(this).hasClass("ui-selected") && (e.ctrlKey == true)) { + $(this).find('a').focus(); + $(this).addClass("ui-selected"); + } + return false; + }); + + self.$element.find("tr[id='treerow-" + record.id + "']").keydown(function(e) { + var keyCode = e.keyCode || e.which; + arrow = {left: 37, up: 38, right: 39, down: 40 }; + switch (keyCode) { + case arrow.left: + if ($(this).find('img').attr('src') === '/base/static/src/img/collapse.gif') { + self.on_click(this.id, result); + } + break; + case arrow.up: + var elem = this; + $(elem).removeClass("ui-selected"); + while ($(elem).prev().is(":visible") == false) { + elem = $(elem).prev(); + } + if (!$(elem).prev().find('#tree-column').hasClass("oe_export_readonlyfield")) { + $(elem).prev().addClass("ui-selected"); + } + $(elem).prev().find('a').focus(); + break; + case arrow.right: + if ($(this).find('img').attr('src') == '/base/static/src/img/expand.gif') { + self.on_click(this.id, result); + } + break; + case arrow.down: + var elem = this; + $(elem).removeClass("ui-selected"); + while($(elem).next().is(":visible") == false) { + elem = $(elem).next(); + } + if (!$(elem).next().find('#tree-column').hasClass("oe_export_readonlyfield")) { + $(elem).next().addClass("ui-selected"); + } + $(elem).next().find('a').focus(); + break; + } + }); + self.$element.find("tr[id='treerow-" + record.id + "']").dblclick(function(e) { + var $o2m_selection = self.$element.find("tr[id^='treerow-" + record.id + "']").find('#tree-column'); + if (!$o2m_selection.hasClass("oe_export_readonlyfield")) { + var field_id = $(this).find("a").attr("id"); + if (field_id) { + self.add_field(field_id.split('-')[1], $(this).find("a").attr("string")); + } + } + }); + }); + self.$element.find('#fields_list').mouseover(function(event) { + if (event.relatedTarget) { + if (event.relatedTarget.attributes['id'] && event.relatedTarget.attributes['string']) { + var field_id = event.relatedTarget.attributes["id"]["value"]; + if (field_id && field_id.split("-")[0] === 'export') { + if (!self.$element.find("tr[id='treerow-" + field_id.split("-")[1] + "']").find('#tree-column').hasClass("oe_export_readonlyfield")) { + self.add_field(field_id.split("-")[1], event.relatedTarget.attributes["string"]["value"]); + } + } + } + } + }); + }, + showcontent: function(id, flag) { + // show & hide the contents + var first_child = this.$element.find("tr[id='treerow-" + id + "']").find('img'); + if (flag) { + first_child.attr('src', '/base/static/src/img/expand.gif'); + } + else { + first_child.attr('src', '/base/static/src/img/collapse.gif'); + } + var child_field = this.$element.find("tr[id^='treerow-" + id +"/']"); + var child_len = (id.split("/")).length + 1; + for (var i = 0; i < child_field.length; i++) { + if (flag) { + $(child_field[i]).hide(); + } else { + if (child_len == (child_field[i].id.split("/")).length) { + if ($(child_field[i]).find('img').attr('src') == '/base/static/src/img/collapse.gif') { + $(child_field[i]).find('img').attr('src', '/base/static/src/img/expand.gif'); + } + $(child_field[i]).show(); + } + } + } + }, + add_field: function(field_id, string) { + var field_list = this.$element.find('#fields_list'); + if (this.$element.find("#fields_list option[value='" + field_id + "']") && !this.$element.find("#fields_list option[value='" + field_id + "']").length) { + field_list.append(new Option(string, field_id)); + } + }, + get_fields: function() { + var export_field = []; + this.$element.find("#fields_list option").each(function() { + export_field.push($(this).val()); + }); + if (!export_field.length) { + alert('Please select fields to save export list...'); + } + return export_field; + }, + on_click_export_data: function() { + var self = this; + var export_field = {}; + var flag = true; + self.$element.find("#fields_list option").each(function() { + export_field[$(this).val()] = $(this).text(); + flag = false; + }); + if (flag) { + alert('Please select fields to export...'); + return; + } + + var import_comp = self.$element.find("#import_compat option:selected").val(), + export_format = self.$element.find("#export_format").val(); + + self.rpc("/base/export/export_data", { + model: self.dataset.model, + fields: export_field, + ids: self.dataset.ids, + domain: self.dataset.domain, + import_compat: parseInt(import_comp), + export_format: export_format + }, function(data) { + window.location = "data:text/csv/excel;charset=utf8," + data; + self.close(); + }); + }, + close: function() { + $(this.$dialog).remove(); + this._super(); + } +}); + +}; diff --git a/addons/base/static/src/js/form.js b/addons/base/static/src/js/form.js index e4cc0172789..f8e2e91667d 100644 --- a/addons/base/static/src/js/form.js +++ b/addons/base/static/src/js/form.js @@ -1,7 +1,7 @@ openerp.base.form = function (openerp) { openerp.base.views.add('form', 'openerp.base.FormView'); -openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormView# */{ +openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormView# */{ /** * Indicates that this view is not searchable, and thus that no search * view should be displayed (if there is one active). @@ -17,9 +17,9 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV * * @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); - this.view_manager = view_manager || new openerp.base.NullViewManager(); + init: function(parent, element_id, dataset, view_id, options) { + this._super(parent, element_id); + this.set_default_options(); this.dataset = dataset; this.model = dataset.model; this.view_id = view_id; @@ -30,26 +30,36 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV this.datarecord = {}; this.ready = false; this.show_invalid = true; - this.touched = false; - this.flags = this.view_manager.flags || {}; + this.dirty = false; this.default_focus_field = null; this.default_focus_button = null; this.registry = openerp.base.form.widgets; this.has_been_loaded = $.Deferred(); this.$form_header = null; + this.options = options || {}; + _.defaults(this.options, {"always_show_new_button": true}); }, start: function() { //this.log('Starting FormView '+this.model+this.view_id) if (this.embedded_view) { - return $.Deferred().then(this.on_loaded).resolve({fields_view: this.embedded_view}); + var def = $.Deferred().then(this.on_loaded); + var self = this; + setTimeout(function() {def.resolve({fields_view: self.embedded_view});}, 0); + return def.promise(); } else { - var context = new openerp.base.CompoundContext(this.dataset.context); - if (this.view_manager.action && this.view_manager.action.context) { - context.add(this.view_manager.action.context); - } + var context = new openerp.base.CompoundContext(this.dataset.get_context()); return this.rpc("/base/formview/load", {"model": this.model, "view_id": this.view_id, - toolbar:!!this.flags.sidebar, context: context}, this.on_loaded); + toolbar: this.options.sidebar, context: context}, this.on_loaded); + } + }, + stop: function() { + if (this.sidebar) { + this.sidebar.attachments.stop(); + this.sidebar.stop(); } + _.each(this.widgets, function(w) { + w.stop(); + }); }, on_loaded: function(data) { var self = this; @@ -71,22 +81,39 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV this.$form_header.find('button.oe_form_button_cancel').click(this.do_cancel); this.$form_header.find('button.oe_form_button_new').click(this.on_button_new); - this.view_manager.sidebar.set_toolbar(data.fields_view.toolbar); + this.$form_header.find('button.oe_get_xml_view').click(function() { + $('<xmp>' + openerp.base.json_node_to_xml(self.fields_view.arch, true) + '</xmp>').dialog({ width: '95%', height: 600}); + }); + + if (this.options.sidebar && this.options.sidebar_id) { + this.sidebar = new openerp.base.Sidebar(this, this.options.sidebar_id); + this.sidebar.start(); + this.sidebar.do_unfold(); + this.sidebar.attachments = new openerp.base.form.SidebarAttachments(this.sidebar, this.sidebar.add_section('attachments', "Attachments"), this); + this.sidebar.add_toolbar(data.fields_view.toolbar); + this.set_common_sidebar_sections(this.sidebar); + } this.has_been_loaded.resolve(); }, do_show: function () { - var self = this; + var promise; if (this.dataset.index === null) { // null index means we should start a new record - this.on_button_new(); + promise = this.on_button_new(); } else { - this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded); + promise = this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded); } - self.$element.show(); - this.view_manager.sidebar.do_refresh(true); + this.$element.show(); + if (this.sidebar) { + this.sidebar.$element.show(); + } + return promise; }, do_hide: function () { this.$element.hide(); + if (this.sidebar) { + this.sidebar.$element.hide(); + } }, on_record_loaded: function(record) { if (!record) { @@ -95,28 +122,30 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV if (!record.id) { this.$form_header.find('.oe_form_on_create').show(); this.$form_header.find('.oe_form_on_update').hide(); - this.$form_header.find('button.oe_form_button_new').hide(); + if (!this.options["always_show_new_button"]) { + this.$form_header.find('button.oe_form_button_new').hide(); + } } else { this.$form_header.find('.oe_form_on_create').hide(); this.$form_header.find('.oe_form_on_update').show(); this.$form_header.find('button.oe_form_button_new').show(); } - this.touched = false; + this.dirty = false; this.datarecord = record; for (var f in this.fields) { var field = this.fields[f]; - field.touched = false; + field.dirty = false; field.set_value(this.datarecord[f] || false); field.validate(); } if (!record.id) { // New record: Second pass in order to trigger the onchanges - this.touched = true; + this.dirty = true; this.show_invalid = false; for (var f in record) { var field = this.fields[f]; if (field) { - field.touched = true; + field.dirty = true; this.do_onchange(field); } } @@ -124,7 +153,9 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV this.on_form_changed(); this.show_invalid = this.ready = true; this.do_update_pager(record.id == null); - this.do_update_sidebar(); + if (this.sidebar) { + this.sidebar.attachments.do_update(); + } if (this.default_focus_field) { this.default_focus_field.focus(); } @@ -132,7 +163,7 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV on_form_changed: function() { for (var w in this.widgets) { w = this.widgets[w]; - w.process_attrs(); + w.process_modifiers(); w.update_dom(); } }, @@ -166,19 +197,24 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV this.ready = false; var onchange = _.trim(widget.node.attrs.on_change); var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/); - console.log("Onchange triggered for field '%s' -> %s", widget.name, onchange); if (call) { var method = call[1], args = []; + var context_index = null; var argument_replacement = { - 'False' : false, - 'True' : true, - 'None' : null + 'False' : function() {return false;}, + 'True' : function() {return true;}, + 'None' : function() {return null;}, + 'context': function(i) { + context_index = i; + var ctx = widget.build_context ? widget.build_context() : {}; + return ctx; + } } var parent_fields = null; - _.each(call[2].split(','), function(a) { + _.each(call[2].split(','), function(a, i) { var field = _.trim(a); if (field in argument_replacement) { - args.push(argument_replacement[field]); + args.push(argument_replacement[field](i)); return; } else if (self.fields[field]) { var value = self.fields[field].get_on_change_value(); @@ -192,7 +228,7 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV } var p_val = parent_fields[_.trim(splitted[1])]; if (p_val !== undefined) { - args.push(value ? value : false); + args.push(p_val == null ? false : p_val); return; } } @@ -207,7 +243,8 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV return this.rpc(ajax, { model: this.dataset.model, method: method, - args: [(this.datarecord.id == null ? [] : [this.datarecord.id])].concat(args) + args: [(this.datarecord.id == null ? [] : [this.datarecord.id])].concat(args), + context_id: context_index === null ? null : context_index + 1 }, function(response) { self.on_processed_onchange(response, processed); }); @@ -217,28 +254,26 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV } }, on_processed_onchange: function(response, processed) { - var result = response.result; + var result = response; if (result.value) { - console.log(" |-> Onchange Response :", result.value); for (var f in result.value) { var field = this.fields[f]; + // If field is not defined in the view, just ignore it if (field) { var value = result.value[f]; processed.push(field.name); if (field.get_value() != value) { - console.log(" |-> Onchange Action : change '%s' value from '%s' to '%s'", field.name, field.get_value(), value); field.set_value(value); + field.dirty = true; if (_.indexOf(processed, field.name) < 0) { this.do_onchange(field, processed); } } - } else { - this.log("warning : on_processed_onchange can't find field " + field, result); } } this.on_form_changed(); } - if (result.warning) { + if (!_.isEmpty(result.warning)) { $(QWeb.render("DialogWarning", result.warning)).dialog({ modal: true, buttons: { @@ -249,17 +284,20 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV }); } if (result.domain) { - // Will be removed ? + // TODO: } this.ready = true; }, on_button_new: function() { var self = this; + var def = $.Deferred(); $.when(this.has_been_loaded).then(function() { - self.dataset.default_get(_.keys(self.fields_view.fields), function(result) { - self.on_record_loaded(result.result); - }); + self.dataset.default_get( + _.keys(self.fields_view.fields)).then(self.on_record_loaded).then(function() { + def.resolve(); + }); }); + return def.promise(); }, /** * Triggers saving the form's record. Chooses between creating a new @@ -274,37 +312,40 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV if (!this.ready) { return false; } - var invalid = false, + var form_dirty = false, + form_invalid = false, values = {}, first_invalid_field = null; for (var f in this.fields) { f = this.fields[f]; - if (f.invalid) { - invalid = true; + if (!f.is_valid()) { + form_invalid = true; f.update_dom(); if (!first_invalid_field) { first_invalid_field = f; } - } else if (f.touched) { + } else if (f.is_dirty()) { + form_dirty = true; values[f.name] = f.get_value(); } } - if (invalid) { + if (form_invalid) { first_invalid_field.focus(); this.on_invalid(); return false; - } else { + } else if (form_dirty) { this.log("About to save", values); if (!this.datarecord.id) { - this.dataset.create(values, function(r) { + return this.dataset.create(values, function(r) { self.on_created(r, success, prepend_on_create); }); } else { - this.dataset.write(this.datarecord.id, values, function(r) { + return this.dataset.write(this.datarecord.id, values, function(r) { self.on_saved(r, success); }); } - return true; + } else { + return false; } }, do_save_edit: function() { @@ -318,7 +359,7 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV on_invalid: function() { var msg = "<ul>"; _.each(this.fields, function(f) { - if (f.invalid) { + if (!f.is_valid()) { msg += "<li>" + f.string + "</li>"; } }); @@ -362,7 +403,9 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV this.dataset.index = 0; } this.do_update_pager(); - this.do_update_sidebar(); + if (this.sidebar) { + this.sidebar.attachments.do_update(); + } this.notification.notify("Record created", "The record has been created with id #" + this.datarecord.id); if (success) { success(_.extend(r, {created: true})); @@ -379,34 +422,55 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV do_cancel: function () { this.notification.notify("Cancelling form"); }, - do_update_sidebar: function() { - if (this.flags.sidebar === false) { - return; + reload: function() { + if (this.datarecord.id) { + this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded); + } else { + this.on_button_new(); } - if (!this.datarecord.id) { + }, + get_fields_values: function() { + var values = {}; + _.each(this.fields, function(value, key) { + var val = value.get_value(); + values[key] = val; + }); + return values; + } +}); + +/** @namespace */ +openerp.base.form = {}; + +openerp.base.form.SidebarAttachments = openerp.base.Widget.extend({ + init: function(parent, element_id, form_view) { + this._super(parent, element_id); + this.view = form_view; + }, + do_update: function() { + if (!this.view.datarecord.id) { this.on_attachments_loaded([]); } else { - // TODO fme: modify this so it doesn't try to load attachments when there is not sidebar - /*this.rpc('/base/dataset/search_read', { - model: 'ir.attachment', - fields: ['name', 'url', 'type'], - domain: [['res_model', '=', this.dataset.model], ['res_id', '=', this.datarecord.id], ['type', 'in', ['binary', 'url']]], - context: this.dataset.context - }, this.on_attachments_loaded);*/ + (new openerp.base.DataSetSearch( + this, 'ir.attachment', this.view.dataset.get_context(), + [['res_model', '=', this.view.dataset.model], + ['res_id', '=', this.view.datarecord.id], + ['type', 'in', ['binary', 'url']]])).read_slice( + ['name', 'url', 'type'], false, false, + this.on_attachments_loaded); } }, on_attachments_loaded: function(attachments) { - this.$sidebar = this.view_manager.sidebar.$element.find('.sidebar-attachments'); this.attachments = attachments; - this.$sidebar.html(QWeb.render('FormView.sidebar.attachments', this)); - this.$sidebar.find('.oe-sidebar-attachment-delete').click(this.on_attachment_delete); - this.$sidebar.find('.oe-binary-file').change(this.on_attachment_changed); + this.$element.html(QWeb.render('FormView.sidebar.attachments', this)); + this.$element.find('.oe-binary-file').change(this.on_attachment_changed); + this.$element.find('.oe-sidebar-attachment-delete').click(this.on_attachment_delete); }, on_attachment_changed: function(e) { - window[this.element_id + '_iframe'] = this.do_update_sidebar; + window[this.element_id + '_iframe'] = this.do_update; var $e = $(e.target); if ($e.val() != '') { - this.$sidebar.find('form.oe-binary-form').submit(); + this.$element.find('form.oe-binary-form').submit(); $e.parent().find('input[type=file]').attr('disabled', 'true'); $e.parent().find('button').attr('disabled', 'true').find('img, span').toggle(); } @@ -423,26 +487,9 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV self.notification.notify("Delete an attachment", "The attachment '" + name + "' has been deleted"); }); } - }, - reload: function() { - if (this.datarecord.id) { - this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded); - } else { - this.on_button_new(); - } - }, - get_fields_values: function() { - var values = {}; - _.each(this.fields, function(value, key) { - values[key] = value.get_value(); - }); - return values; } }); -/** @namespace */ -openerp.base.form = {}; - openerp.base.form.compute_domain = function(expr, fields) { var stack = []; for (var i = expr.length - 1; i >= 0; i--) { @@ -496,23 +543,23 @@ openerp.base.form.compute_domain = function(expr, fields) { stack.push(!_(val).contains(field)); break; default: - this.log("Unsupported operator in attrs :", op); + this.log("Unsupported operator in modifiers :", op); } } return _.all(stack); }; -openerp.base.form.Widget = openerp.base.Controller.extend({ +openerp.base.form.Widget = openerp.base.Widget.extend({ template: 'Widget', init: function(view, node) { this.view = view; this.node = node; - this.attrs = JSON.parse(this.node.attrs.attrs || '{}'); + this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}'); this.type = this.type || node.tag; this.element_name = this.element_name || this.type; this.element_id = [this.view.element_id, this.element_name, this.view.widgets_counter++].join("_"); - this._super(this.view.session, this.element_id); + this._super(view, this.element_id); this.view.widgets[this.element_id] = this; this.children = node.children; @@ -520,15 +567,18 @@ openerp.base.form.Widget = openerp.base.Controller.extend({ this.string = this.string || node.attrs.string; this.help = this.help || node.attrs.help; - this.invisible = (node.attrs.invisible == '1'); + this.invisible = this.modifiers['invisible'] === true; }, start: function() { this.$element = $('#' + this.element_id); }, - process_attrs: function() { + stop: function() { + this.$element.remove(); + }, + process_modifiers: function() { var compute_domain = openerp.base.form.compute_domain; - for (var a in this.attrs) { - this[a] = compute_domain(this.attrs[a], this.view.fields); + for (var a in this.modifiers) { + this[a] = compute_domain(this.modifiers[a], this.view.fields); } }, update_dom: function() { @@ -583,7 +633,10 @@ openerp.base.form.WidgetFrame = openerp.base.form.Widget.extend({ } }, handle_node: function(node) { - var type = this.view.fields_view.fields[node.attrs.name] || {}; + var type = {}; + if (node.tag == 'field') { + type = this.view.fields_view.fields[node.attrs.name] || {}; + } var widget = new (this.view.registry.get_any( [node.attrs.widget, type.type, node.tag])) (this.view, node); if (node.tag == 'field') { @@ -593,14 +646,15 @@ openerp.base.form.WidgetFrame = openerp.base.form.Widget.extend({ if (node.attrs.nolabel != '1') { var label = new (this.view.registry.get_object('label')) (this.view, node); label["for"] = widget; - this.add_widget(label); + this.add_widget(label, widget.colspan + 1); } } this.add_widget(widget); }, - add_widget: function(widget) { + add_widget: function(widget, colspan) { + colspan = colspan || widget.colspan; var current_row = this.table[this.table.length - 1]; - if (current_row.length && (this.x + widget.colspan) > this.columns) { + if (current_row.length && (this.x + colspan) > this.columns) { current_row = this.add_row(); } current_row.push(widget); @@ -610,14 +664,14 @@ openerp.base.form.WidgetFrame = openerp.base.form.Widget.extend({ }); openerp.base.form.WidgetNotebook = openerp.base.form.Widget.extend({ + template: 'WidgetNotebook', init: function(view, node) { this._super(view, node); - this.template = "WidgetNotebook"; this.pages = []; for (var i = 0; i < node.children.length; i++) { var n = node.children[i]; if (n.tag == "page") { - var page = new openerp.base.form.WidgetFrame(this.view, n); + var page = new openerp.base.form.WidgetNotebookPage(this.view, n, this, this.pages.length); this.pages.push(page); } } @@ -628,6 +682,28 @@ openerp.base.form.WidgetNotebook = openerp.base.form.Widget.extend({ } }); +openerp.base.form.WidgetNotebookPage = openerp.base.form.WidgetFrame.extend({ + template: 'WidgetNotebookPage', + init: function(view, node, notebook, index) { + this.notebook = notebook; + this.index = index; + this.element_name = 'page_' + index; + this._super(view, node); + this.element_tab_id = this.element_id + '_tab'; + }, + start: function() { + this._super.apply(this, arguments); + this.$element_tab = $('#' + this.element_tab_id); + }, + update_dom: function() { + if (this.invisible) { + this.notebook.$element.tabs('select', 0); + } + this.$element_tab.toggle(!this.invisible); + this.$element.toggle(!this.invisible); + } +}); + openerp.base.form.WidgetSeparator = openerp.base.form.Widget.extend({ init: function(view, node) { this._super(view, node); @@ -639,6 +715,10 @@ openerp.base.form.WidgetButton = openerp.base.form.Widget.extend({ init: function(view, node) { this._super(view, node); this.template = "WidgetButton"; + if (this.string) { + // We don't have button key bindings in the webclient + this.string = this.string.replace(/_/g, ''); + } if (node.attrs.default_focus == '1') { // TODO fme: provide enter key binding to widgets this.view.default_focus_button = this; @@ -650,7 +730,7 @@ openerp.base.form.WidgetButton = openerp.base.form.Widget.extend({ }, on_click: function(saved) { var self = this; - if (!this.node.attrs.special && this.view.touched && saved !== true) { + if (!this.node.attrs.special && this.view.dirty && saved !== true) { this.view.do_save(function() { self.on_click(true); }); @@ -679,10 +759,7 @@ openerp.base.form.WidgetButton = openerp.base.form.Widget.extend({ this.view.execute_action( this.node.attrs, this.view.dataset, this.session.action_manager, - this.view.datarecord.id, function (result) { - self.log("Button returned", result); - self.view.reload(); - }, function() { + this.view.datarecord.id, function () { self.view.reload(); }); } @@ -698,11 +775,12 @@ openerp.base.form.WidgetLabel = openerp.base.form.Widget.extend({ if (this.node.tag == 'label' && this.node.attrs.colspan) { this.is_field_label = false; this.template = "WidgetParagraph"; + this.colspan = this.node.attrs.colspan; } else { this.is_field_label = true; this.template = "WidgetLabel"; + this.colspan = 1; } - this.colspan = 1; }, render: function () { if (this['for'] && this.type !== 'label') { @@ -729,12 +807,11 @@ openerp.base.form.Field = openerp.base.form.Widget.extend({ this.field = view.fields_view.fields[node.attrs.name] || {}; this.string = node.attrs.string || this.field.string; this.help = node.attrs.help || this.field.help; - this.invisible = (this.invisible || this.field.invisible == '1'); - this.nolabel = (this.field.nolabel || node.attrs.nolabel) == '1'; - this.readonly = (this.field.readonly || node.attrs.readonly) == '1'; - this.required = (this.field.required || node.attrs.required) == '1'; + this.nolabel = (this.field.nolabel || node.attrs.nolabel) === '1'; + this.readonly = this.modifiers['readonly'] === true; + this.required = this.modifiers['required'] === true; this.invalid = false; - this.touched = false; + this.dirty = false; }, set_value: function(value) { this.value = value; @@ -747,21 +824,29 @@ openerp.base.form.Field = openerp.base.form.Widget.extend({ get_value: function() { return this.value; }, + is_valid: function() { + return !this.invalid; + }, + is_dirty: function() { + return this.dirty; + }, get_on_change_value: function() { return this.get_value(); }, update_dom: function() { this._super.apply(this, arguments); - this.$element.toggleClass('disabled', this.readonly); - this.$element.toggleClass('required', this.required); - if (this.view.show_invalid) { - this.$element.toggleClass('invalid', this.invalid); + if (!this.disable_utility_classes) { + this.$element.toggleClass('disabled', this.readonly); + this.$element.toggleClass('required', this.required); + if (this.view.show_invalid) { + this.$element.toggleClass('invalid', !this.is_valid()); + } } }, on_ui_change: function() { - this.touched = this.view.touched = true; + this.dirty = this.view.dirty = true; this.validate(); - if (!this.invalid) { + if (this.is_valid()) { this.set_value_from_ui(); this.view.do_onchange(this); this.view.on_form_changed(); @@ -773,6 +858,43 @@ openerp.base.form.Field = openerp.base.form.Widget.extend({ this.invalid = false; }, focus: function() { + }, + _build_view_fields_values: function() { + var a_dataset = this.view.dataset || {}; + var fields_values = this.view.get_fields_values(); + var parent_values = a_dataset.parent_view ? a_dataset.parent_view.get_fields_values() : {}; + fields_values.parent = parent_values; + return fields_values; + }, + /** + * Builds a new context usable for operations related to fields by merging + * the fields'context with the action's context. + */ + build_context: function() { + // I previously belevied contexts should be herrited, but now I doubt it + //var a_context = this.view.dataset.get_context() || {}; + var f_context = this.field.context || null; + // maybe the default_get should only be used when we do a default_get? + var v_context1 = this.node.attrs.default_get || {}; + var v_context2 = this.node.attrs.context || {}; + var v_context = new openerp.base.CompoundContext(v_context1, v_context2); + if (v_context1.__ref || v_context2.__ref || true) { //TODO niv: remove || true + var fields_values = this._build_view_fields_values(); + v_context.set_eval_context(fields_values); + } + // if there is a context on the node, overrides the model's context + var ctx = f_context || v_context; + return ctx; + }, + build_domain: function() { + var f_domain = this.field.domain || null; + var v_domain = this.node.attrs.domain || []; + if (!(v_domain instanceof Array) || true) { //TODO niv: remove || true + var fields_values = this._build_view_fields_values(); + v_domain = new openerp.base.CompoundDomain(v_domain).set_eval_context(fields_values); + } + // if there is a domain on the node, overrides the model's domain + return f_domain || v_domain; } }); @@ -822,7 +944,7 @@ openerp.base.form.FieldEmail = openerp.base.form.FieldChar.extend({ this.$element.find('button').click(this.on_button_clicked); }, on_button_clicked: function() { - if (!this.value || this.invalid) { + if (!this.value || !this.is_valid()) { this.notification.warn("E-mail error", "Can't send email to invalid e-mail address"); } else { location.href = 'mailto:' + this.value; @@ -863,6 +985,7 @@ openerp.base.form.FieldFloat = openerp.base.form.FieldChar.extend({ if (value === false || value === undefined) { // As in GTK client, floats default to 0 value = 0; + this.dirty = true; } var show_value = value.toFixed(2); this.$element.find('input').val(show_value); @@ -872,22 +995,47 @@ openerp.base.form.FieldFloat = openerp.base.form.FieldChar.extend({ } }); +openerp.base.form.FieldInteger = openerp.base.form.FieldFloat.extend({ + init: function(view, node) { + this._super(view, node); + this.validation_regex = /^-?\d+$/; + }, + set_value: function(value) { + this._super.apply(this, [value]); + if (value === false || value === undefined) { + // TODO fme: check if GTK client default integers to 0 (like it does with floats) + value = 0; + this.dirty = true; + } + var show_value = parseInt(value, 10); + this.$element.find('input').val(show_value); + }, + set_value_from_ui: function() { + this.value = Number(this.$element.find('input').val()); + } +}); + openerp.base.form.FieldDatetime = openerp.base.form.Field.extend({ init: function(view, node) { this._super(view, node); this.template = "FieldDate"; this.jqueryui_object = 'datetimepicker'; + this.validation_regex = /^\d+-\d+-\d+( \d+:\d+(:\d+)?)?$/; }, start: function() { this._super.apply(this, arguments); this.$element.find('input').change(this.on_ui_change)[this.jqueryui_object]({ dateFormat: 'yy-mm-dd', - timeFormat: 'hh:mm:ss' + timeFormat: 'hh:mm:ss', + showOn: 'button', + buttonImage: '/base/static/src/img/ui/field_calendar.png', + buttonImageOnly: true, + constrainInput: false }); }, set_value: function(value) { this._super.apply(this, arguments); - if (value == null || value == false) { + if (!value) { this.$element.find('input').val(''); } else { this.$element.find('input').unbind('change'); @@ -902,8 +1050,20 @@ openerp.base.form.FieldDatetime = openerp.base.form.Field.extend({ this.value = this.format(this.value); } }, + update_dom: function() { + this._super.apply(this, arguments); + this.$element.find('input').datepicker(this.readonly ? 'disable' : 'enable'); + }, validate: function() { - this.invalid = this.required && !this.$element.find('input')[this.jqueryui_object]('getDate'); + this.invalid = false; + var value = this.$element.find('input').val(); + if (value === "") { + this.invalid = this.required; + } else if (this.validation_regex) { + this.invalid = !this.validation_regex.test(value); + } else { + this.invalid = !this.$element.find('input')[this.jqueryui_object]('getDate'); + } }, focus: function() { this.$element.find('input').focus(); @@ -916,6 +1076,7 @@ openerp.base.form.FieldDate = openerp.base.form.FieldDatetime.extend({ init: function(view, node) { this._super(view, node); this.jqueryui_object = 'datepicker'; + this.validation_regex = /^\d+-\d+-\d+$/; }, parse: openerp.base.parse_date, format: openerp.base.format_date @@ -931,6 +1092,7 @@ openerp.base.form.FieldFloatTime = openerp.base.form.FieldChar.extend({ if (value === false || value === undefined) { // As in GTK client, floats default to 0 value = 0; + this.dirty = true; } var show_value = _.sprintf("%02d:%02d", Math.floor(value), Math.round((value % 1) * 60)); this.$element.find('input').val(show_value); @@ -1025,7 +1187,7 @@ openerp.base.form.FieldProgressBar = openerp.base.form.Field.extend({ set_value: function(value) { this._super.apply(this, arguments); var show_value = Number(value); - if (show_value === NaN) { + if (isNaN(show_value)) { show_value = 0; } this.$element.find('div').progressbar('option', 'value', show_value).find('span').html(show_value + '%'); @@ -1040,28 +1202,54 @@ openerp.base.form.FieldSelection = openerp.base.form.Field.extend({ init: function(view, node) { this._super(view, node); this.template = "FieldSelection"; + this.field_index = _.map(this.field.selection, function(x, index) { + return {"ikey": "" + index, "ekey": x[0], "label": x[1]}; + }); }, start: function() { + // Flag indicating whether we're in an event chain containing a change + // event on the select, in order to know what to do on keyup[RETURN]: + // * If the user presses [RETURN] as part of changing the value of a + // selection, we should just let the value change and not let the + // event broadcast further (e.g. to validating the current state of + // the form in editable list view, which would lead to saving the + // current row or switching to the next one) + // * If the user presses [RETURN] with a select closed (side-effect: + // also if the user opened the select and pressed [RETURN] without + // changing the selected value), takes the action as validating the + // row + var ischanging = false; this._super.apply(this, arguments); - this.$element.find('select').change(this.on_ui_change); + this.$element.find('select') + .change(this.on_ui_change) + .change(function () { ischanging = true; }) + .click(function () { ischanging = false; }) + .keyup(function (e) { + if (e.which !== 13 || !ischanging) { return; } + e.stopPropagation(); + ischanging = false; + }); }, set_value: function(value) { - this._super.apply(this, arguments); - if (value != null && value !== false) { - this.$element.find('select').val(value); - } else { - this.$element.find('select').val('false'); - } + value = value === null ? false : value; + value = value instanceof Array ? value[0] : value; + this._super(value); + var option = _.detect(this.field_index, function(x) {return x.ekey === value;}); + this.$element.find('select').val(option === undefined ? '' : option.ikey); }, set_value_from_ui: function() { - this.value = this.$element.find('select').val(); + var ikey = this.$element.find('select').val(); + var option = _.detect(this.field_index, function(x) {return x.ikey === ikey;}); + this.value = option === undefined ? false : option.ekey; }, update_dom: function() { this._super.apply(this, arguments); this.$element.find('select').attr('disabled', this.readonly); }, validate: function() { - this.invalid = this.required && this.$element.find('select').val() === ""; + var ikey = this.$element.find('select').val(); + var option = _.detect(this.field_index, function(x) {return x.ikey === ikey;}); + this.invalid = this.required && (option === undefined || option.ekey === false); }, focus: function() { this.$element.find('select').focus(); @@ -1100,28 +1288,18 @@ openerp.base.form.FieldSelection = openerp.base.form.Field.extend({ }); })(); -var build_view_fields_values = function(view) { - var a_dataset = view.dataset || {}; - var fields_values = view.get_fields_values(); - var parent_values = a_dataset.parent_view ? a_dataset.parent_view.get_fields_values() : {}; - fields_values.parent = parent_values; - return fields_values; -} -/** - * Builds a new context usable for operations related to fields by merging - * the fields'context with the action's context. - */ -var build_relation_context = function(relation_field) { - var a_context = relation_field.view.dataset.get_context() || {}; - var fields_values = build_view_fields_values(relation_field.view); - var f_context = new openerp.base.CompoundContext(relation_field.field.context || {}).set_eval_context(fields_values); - var ctx = new openerp.base.CompoundContext(a_context, f_context); - return ctx; -} -var build_relation_domain = function(relation_field) { - var fields_values = build_view_fields_values(relation_field.view); - var f_domain = new openerp.base.CompoundDomain(relation_field.field.domain || []).set_eval_context(fields_values); - return f_domain; +openerp.base.form.dialog = function(content, options) { + options = _.extend({ + autoOpen: true, + width: '90%', + height: '90%', + min_width: '800px', + min_height: '600px' + }, options || {}); + options.autoOpen = true; + var dialog = new openerp.base.Dialog(null, options); + dialog.$dialog = $(content).dialog(dialog.options); + return dialog.$dialog; } openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ @@ -1153,15 +1331,10 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ 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", - "context": build_relation_context(self) + var pop = new openerp.base.form.FormOpenPopup(self.view); + pop.show_element(self.field.relation, self.value[0],self.build_context(), {}); + pop.on_write_completed.add_last(function() { + self.set_value(self.value[0]); }); }; var cmenu = this.$menu_btn.contextMenu(this.cm_id, {'leftClickToo': true, @@ -1209,10 +1382,12 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ } this.$input.focusout(anyoneLoosesFocus); + var isSelecting = false; // autocomplete this.$input.autocomplete({ source: function(req, resp) { self.get_search_result(req, resp); }, select: function(event, ui) { + isSelecting = true; var item = ui.item; if (item.id) { self._change_int_value([item.id, item.name]); @@ -1230,19 +1405,27 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ minLength: 0, delay: 0 }); + // used to correct a bug when selecting an element by pushing 'enter' in an editable list + this.$input.keyup(function(e) { + if (e.which === 13) { + if (isSelecting) + e.stopPropagation(); + } + isSelecting = false; + }); }, // 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, build_relation_context(self)); + var dataset = new openerp.base.DataSetStatic(this, this.field.relation, self.build_context()); - dataset.name_search(search_val, build_relation_domain(self), 'ilike', + dataset.name_search(search_val, self.build_domain(), 'ilike', this.limit + 1, function(data) { - self.last_search = data.result; + self.last_search = data; // possible selections for the m2o - var values = _.map(data.result, function(x) { + var values = _.map(data, function(x) { return {label: $('<span />').text(x[1]).html(), name:x[1], id:x[0]}; }); @@ -1250,10 +1433,10 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ if (values.length > self.limit) { values = values.slice(0, self.limit); values.push({label: "<em> Search More...</em>", action: function() { - dataset.name_search(search_val, build_relation_domain(self), 'ilike' + dataset.name_search(search_val, self.build_domain(), 'ilike' , false, function(data) { self._change_int_value(null); - self._search_create_popup("search", data.result); + self._search_create_popup("search", data); }); }}); } @@ -1270,7 +1453,7 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ // create... values.push({label: "<em> Create and Edit...</em>", action: function() { self._change_int_value(null); - self._search_create_popup("form"); + self._search_create_popup("form", undefined, {"default_name": search_val}); }}); response(values); @@ -1278,9 +1461,9 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ }, _quick_create: function(name) { var self = this; - var dataset = new openerp.base.DataSetStatic(this.session, this.field.relation, build_relation_context(self)); + var dataset = new openerp.base.DataSetStatic(this, this.field.relation, self.build_context()); dataset.name_create(name, function(data) { - self._change_int_ext_value(data.result); + self._change_int_ext_value(data); }).fail(function(error, event) { event.preventDefault(); self._change_int_value(null); @@ -1290,17 +1473,17 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ // all search/create popup handling _search_create_popup: function(view, ids, context) { var self = this; - var pop = new openerp.base.form.SelectCreatePopup(null, self.view.session); + var pop = new openerp.base.form.SelectCreatePopup(this); pop.select_element(self.field.relation,{ initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined, initial_view: view, disable_multiple_selection: true - }, build_relation_domain(self), - new openerp.base.CompoundContext(build_relation_context(self), context || {})); + }, self.build_domain(), + new openerp.base.CompoundContext(self.build_context(), context || {})); pop.on_select_elements.add(function(element_ids) { - var dataset = new openerp.base.DataSetStatic(this.session, this.field.relation, build_relation_context(self)); + var dataset = new openerp.base.DataSetStatic(self, self.field.relation, self.build_context()); dataset.name_get([element_ids[0]], function(data) { - self._change_int_ext_value(data.result[0]); + self._change_int_ext_value(data[0]); pop.stop(); }); }); @@ -1336,9 +1519,9 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ self._change_int_ext_value(rval); }; if(typeof(value) === "number") { - var dataset = new openerp.base.DataSetStatic(this.session, this.field.relation, build_relation_context(self)); + var dataset = new openerp.base.DataSetStatic(this, this.field.relation, self.build_context()); dataset.name_get([value], function(data) { - real_set_value(data.result[0]); + real_set_value(data[0]); }).fail(function() {self.tmp_value = undefined;}); } else { setTimeout(function() {real_set_value(value);}, 0); @@ -1409,17 +1592,20 @@ var commands = { } }; openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({ + multi_selection: false, init: function(view, node) { this._super(view, node); this.template = "FieldOne2Many"; this.is_started = $.Deferred(); + this.form_last_update = $.Deferred(); + this.disable_utility_classes = true; }, start: function() { this._super.apply(this, arguments); var self = this; - this.dataset = new openerp.base.form.One2ManyDataSet(this.session, this.field.relation); + this.dataset = new openerp.base.form.One2ManyDataSet(this, this.field.relation); this.dataset.o2m = this; this.dataset.parent_view = this.view; this.dataset.on_change.add_last(function() { @@ -1436,27 +1622,41 @@ openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({ } if(view.view_type === "list") { view.options = { + 'selectable': self.multi_selection }; } views.push(view); }); this.views = views; - this.viewmanager = new openerp.base.ViewManager(this.view.session, - this.element_id, this.dataset, views); + this.viewmanager = new openerp.base.ViewManager(this, this.element_id, this.dataset, views); this.viewmanager.registry = openerp.base.views.clone({ - list: 'openerp.base.form.One2ManyListView' + list: 'openerp.base.form.One2ManyListView', + form: 'openerp.base.form.One2ManyFormView' + }); + var once = $.Deferred().then(function() { + self.form_last_update.resolve(); }); - this.viewmanager.on_controller_inited.add_last(function(view_type, controller) { if (view_type == "list") { controller.o2m = self; } else if (view_type == "form") { - // TODO niv + controller.on_record_loaded.add_last(function() { + once.resolve(); + }); + controller.on_pager_action.add_first(function() { + self.save_form_view(); + }); + controller.$element.find(".oe_form_button_save_edit").hide(); } self.is_started.resolve(); }); - this.viewmanager.start(); + this.viewmanager.on_mode_switch.add_first(function() { + self.save_form_view(); + }); + setTimeout(function () { + self.viewmanager.start(); + }, 0); }, reload_current_view: function() { var self = this; @@ -1464,7 +1664,12 @@ openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({ if(self.viewmanager.active_view === "list") { view.reload_content(); } else if (self.viewmanager.active_view === "form") { - // TODO niv: implement + if (this.dataset.index === null && this.dataset.ids.length >= 1) { + this.dataset.index = 0; + } + this.form_last_update.then(function() { + this.form_last_update = view.do_show(); + }); } }, set_value_from_ui: function() {}, @@ -1518,6 +1723,9 @@ openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({ this._super(value); this.dataset.reset_ids(value); } + if (this.dataset.index === null && this.dataset.ids.length > 0) { + this.dataset.index = 0; + } $.when(this.is_started).then(function() { self.reload_current_view(); }); @@ -1542,47 +1750,97 @@ openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({ this.dataset.to_delete, function(x) { return commands['delete'](x.id);})); }, - get_on_change_value: function() { - var self; - return _.map(this.dataset.ids, function(id) { - var values = _.detect(self.dataset.cache, function(x) {return x.id === id}); - if (values) { - values = _.clone(values); - delete values["id"]; - return values; - } else { - console.info("trying to get value in a o2m before that value is loaded"); - return {}; + save_form_view: function() { + if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view && + this.viewmanager.views[this.viewmanager.active_view] && + this.viewmanager.views[this.viewmanager.active_view].controller) { + var view = this.viewmanager.views[this.viewmanager.active_view].controller; + if (this.viewmanager.active_view === "form") { + var res = view.do_save(); + if (res === false) { + // ignore + } else if (res.isRejected()) { + throw "Save or create on one2many dataset is not supposed to fail."; + } else if (!res.isResolved()) { + throw "Asynchronous get_value() is not supported in form view."; + } + return res; } - }); + } + return false; + }, + is_valid: function() { + this.validate(); + return this._super(); }, validate: function() { this.invalid = false; - // TODO niv + var self = this; + var view = self.viewmanager.views[self.viewmanager.active_view].controller; + if (self.viewmanager.active_view === "form") { + for (var f in view.fields) { + f = view.fields[f]; + if (!f.is_valid()) { + this.invalid = true; + return; + } + } + } + }, + is_dirty: function() { + this.save_form_view(); + return this._super(); + }, + update_dom: function() { + this._super.apply(this, arguments); + this.$element.toggleClass('disabled', this.readonly); } }); openerp.base.form.One2ManyDataSet = openerp.base.BufferedDataSet.extend({ get_context: function() { - this.context = build_relation_context(this.o2m); + this.context = this.o2m.build_context(); return this.context; } }); +openerp.base.form.One2ManyFormView = openerp.base.FormView.extend({ + +}); + openerp.base.form.One2ManyListView = openerp.base.ListView.extend({ do_add_record: function () { + if (this.options.editable) { + this._super.apply(this, arguments); + } else { + var self = this; + var pop = new openerp.base.form.SelectCreatePopup(this); + pop.select_element(self.o2m.field.relation,{ + initial_view: "form", + alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined, + auto_create: false, + parent_view: self.o2m.view + }, self.o2m.build_domain(), self.o2m.build_context()); + pop.on_create.add(function(data) { + self.o2m.dataset.create(data, function(r) { + self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r.result])); + self.o2m.dataset.on_change(); + pop.stop(); + self.o2m.reload_current_view(); + }); + }); + } + }, + do_activate_record: function(index, id) { var self = this; - var pop = new openerp.base.form.SelectCreatePopup(null, self.o2m.view.session); - pop.select_element(self.o2m.field.relation,{ - initial_view: "form", + var pop = new openerp.base.form.FormOpenPopup(self.o2m.view); + pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(),{ + auto_write: false, alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined, - auto_create: false, parent_view: self.o2m.view - }, build_relation_domain(self.o2m), build_relation_context(self.o2m)); - pop.on_create.add(function(data) { - self.o2m.dataset.create(data, function(r) { - self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r.result])); - pop.stop(); + }); + pop.on_write.add(function(id, data) { + self.o2m.dataset.write(id, data, function(r) { self.o2m.reload_current_view(); }); }); @@ -1590,6 +1848,7 @@ openerp.base.form.One2ManyListView = openerp.base.ListView.extend({ }); openerp.base.form.FieldMany2Many = openerp.base.form.Field.extend({ + multi_selection: false, init: function(view, node) { this._super(view, node); this.template = "FieldMany2Many"; @@ -1601,22 +1860,23 @@ openerp.base.form.FieldMany2Many = openerp.base.form.Field.extend({ var self = this; - this.dataset = new openerp.base.DataSetStatic( - this.session, this.field.relation); + this.dataset = new openerp.base.form.Many2ManyDataSet(this, this.field.relation); + this.dataset.m2m = this; this.dataset.on_unlink.add_last(function(ids) { - //TODO niv: should check this for other cases self.on_ui_change(); }); - this.list_view = new openerp.base.form.Many2ManyListView( - null, this.view.session, this.list_id, this.dataset, false, { - 'addable': 'Add' + this.list_view = new openerp.base.form.Many2ManyListView(new openerp.base.NullViewManager(this), this.list_id, this.dataset, false, { + 'addable': 'Add', + 'selectable': self.multi_selection }); this.list_view.m2m_field = this; this.list_view.on_loaded.add_last(function() { self.is_started.resolve(); }); - this.list_view.start(); + setTimeout(function () { + self.list_view.start(); + }, 0); }, set_value: function(value) { value = value || []; @@ -1632,19 +1892,33 @@ openerp.base.form.FieldMany2Many = openerp.base.form.Field.extend({ }, get_value: function() { return [commands.replace_with(this.dataset.ids)]; + }, + set_value_from_ui: function() {}, + validate: function() { + this.invalid = false; + // TODO niv + } +}); + +openerp.base.form.Many2ManyDataSet = openerp.base.DataSetStatic.extend({ + get_context: function() { + this.context = this.m2m.build_context(); + return this.context; } }); openerp.base.form.Many2ManyListView = openerp.base.ListView.extend({ do_add_record: function () { - var pop = new openerp.base.form.SelectCreatePopup( - null, this.m2m_field.view.session); - pop.select_element(this.model); + var pop = new openerp.base.form.SelectCreatePopup(this); + pop.select_element(this.model, {}, + new openerp.base.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]), + this.m2m_field.build_context()); var self = this; pop.on_select_elements.add(function(element_ids) { _.each(element_ids, function(element_id) { if(! _.detect(self.dataset.ids, function(x) {return x == element_id;})) { self.dataset.set_ids([].concat(self.dataset.ids, [element_id])); + self.m2m_field.on_ui_change(); self.reload_content(); } }); @@ -1652,19 +1926,16 @@ openerp.base.form.Many2ManyListView = openerp.base.ListView.extend({ }); }, do_activate_record: function(index, id) { - this.m2m_field.view.session.action_manager.do_action({ - "res_model": this.dataset.model, - "views":[[false,"form"]], - "res_id": id, - "type":"ir.actions.act_window", - "view_type":"form", - "view_mode":"form", - "target":"new" + var self = this; + var pop = new openerp.base.form.FormOpenPopup(this); + pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {}); + pop.on_write_completed.add_last(function() { + self.reload_content(); }); } }); -openerp.base.form.SelectCreatePopup = openerp.base.BaseWidget.extend({ +openerp.base.form.SelectCreatePopup = openerp.base.OldWidget.extend({ identifier_prefix: "selectcreatepopup", template: "SelectCreatePopup", /** @@ -1682,15 +1953,13 @@ openerp.base.form.SelectCreatePopup = openerp.base.BaseWidget.extend({ this.context = context || {}; this.options = _.defaults(options || {}, {"initial_view": "search", "auto_create": true}); this.initial_ids = this.options.initial_ids; - jQuery(this.render()).dialog({title: '', - modal: true, - minWidth: 800}); + openerp.base.form.dialog(this.render()); this.start(); }, start: function() { this._super(); - this.dataset = new openerp.base.ReadOnlyDataSetSearch(this.session, this.model, - this.context, this.domain); + this.dataset = new openerp.base.ReadOnlyDataSetSearch(this, this.model, + this.context); this.dataset.parent_view = this.options.parent_view; if (this.options.initial_view == "search") { this.setup_search_view(); @@ -1703,18 +1972,18 @@ openerp.base.form.SelectCreatePopup = openerp.base.BaseWidget.extend({ if (this.searchview) { this.searchview.stop(); } - this.searchview = new openerp.base.SearchView(null, this.session, + this.searchview = new openerp.base.SearchView(this, this.element_id + "_search", this.dataset, false, { "selectable": !this.options.disable_multiple_selection, "deletable": false }); this.searchview.on_search.add(function(domains, contexts, groupbys) { if (self.initial_ids) { - self.view_list.do_search.call(self, domains.concat([[["id", "in", self.initial_ids]]]), + self.view_list.do_search.call(self, domains.concat([[["id", "in", self.initial_ids]], self.domain]), contexts, groupbys); self.initial_ids = undefined; } else { - self.view_list.do_search.call(self, domains, contexts, groupbys); + self.view_list.do_search.call(self, domains.concat([self.domain]), contexts, groupbys); } }); this.searchview.on_loaded.add_last(function () { @@ -1731,7 +2000,7 @@ openerp.base.form.SelectCreatePopup = openerp.base.BaseWidget.extend({ $sbutton.click(function() { self.on_select_elements(self.selected_ids); }); - self.view_list = new openerp.base.form.SelectCreateListView( null, self.session, + self.view_list = new openerp.base.form.SelectCreateListView(self, self.element_id + "_view_list", self.dataset, false, {'deletable': false}); self.view_list.popup = self; @@ -1746,8 +2015,8 @@ openerp.base.form.SelectCreatePopup = openerp.base.BaseWidget.extend({ if (!this.options.auto_create) return; var self = this; - var wdataset = new openerp.base.DataSetSearch(this.session, this.model, this.context, this.domain); - wdataset = this.options.parent_view; + var wdataset = new openerp.base.DataSetSearch(this, this.model, this.context, this.domain); + wdataset.parent_view = this.options.parent_view; wdataset.create(data, function(r) { self.on_select_elements([r.result]); }); @@ -1771,8 +2040,7 @@ openerp.base.form.SelectCreatePopup = openerp.base.BaseWidget.extend({ 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); + this.view_form = new openerp.base.FormView(this, this.element_id + "_view_form", this.dataset, false); if (this.options.alternative_form_view) { this.view_form.set_embedded_view(this.options.alternative_form_view); } @@ -1807,10 +2075,74 @@ openerp.base.form.SelectCreateListView = openerp.base.ListView.extend({ } }); -openerp.base.form.FieldReference = openerp.base.form.Field.extend({ +openerp.base.form.FormOpenPopup = openerp.base.OldWidget.extend({ + identifier_prefix: "formopenpopup", + template: "FormOpenPopup", + /** + * options: + * - alternative_form_view + * - auto_write (default true) + * - parent_view + */ + show_element: function(model, row_id, context, options) { + this.model = model; + this.row_id = row_id; + this.context = context || {}; + this.options = _.defaults(options || {}, {"auto_write": true}); + jQuery(this.render()).dialog({title: '', + modal: true, + width: 960, + height: 600}); + this.start(); + }, + start: function() { + this._super(); + this.dataset = new openerp.base.ReadOnlyDataSetSearch(this, this.model, this.context); + this.dataset.ids = [this.row_id]; + this.dataset.index = 0; + this.dataset.parent_view = this.options.parent_view; + this.setup_form_view(); + }, + on_write: function(id, data) { + this.stop(); + if (!this.options.auto_write) + return; + var self = this; + var wdataset = new openerp.base.DataSetSearch(this, this.model, this.context, this.domain); + wdataset.parent_view = this.options.parent_view; + wdataset.write(id, data, function(r) { + self.on_write_completed(); + }); + }, + on_write_completed: function() {}, + setup_form_view: function() { + var self = this; + this.view_form = new openerp.base.FormView(this, this.element_id + "_view_form", this.dataset, false); + if (this.options.alternative_form_view) { + this.view_form.set_embedded_view(this.options.alternative_form_view); + } + this.view_form.start(); + this.view_form.on_loaded.add_last(function() { + var $buttons = self.view_form.$element.find(".oe_form_buttons"); + $buttons.html(QWeb.render("FormOpenPopup.form.buttons")); + var $nbutton = $buttons.find(".oe_formopenpopup-form-save"); + $nbutton.click(function() { + self.view_form.do_save(); + }); + var $cbutton = $buttons.find(".oe_formopenpopup-form-close"); + $cbutton.click(function() { + self.stop(); + }); + self.view_form.do_show(); + }); + this.dataset.on_write.add(this.on_write); + } +}); + +openerp.base.form.FieldReference = openerp.base.form.FieldChar.extend({ init: function(view, node) { this._super(view, node); - this.template = "FieldReference"; + //this.template = "FieldReference"; } }); @@ -1828,6 +2160,10 @@ openerp.base.form.FieldBinary = openerp.base.form.Field.extend({ }, set_value_from_ui: function() { }, + update_dom: function() { + this._super.apply(this, arguments); + this.$element.find('.oe-binary').toggle(!this.readonly); + }, human_filesize : function(size) { var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; var i = 0; @@ -1925,6 +2261,13 @@ openerp.base.form.FieldBinaryImage = openerp.base.form.FieldBinary.extend({ this._super.apply(this, arguments); this.$image = this.$element.find('img.oe-binary-image'); }, + set_value: function(value) { + this._super.apply(this, arguments); + this.set_image_maxwidth(); + var url = '/base/binary/image?session_id=' + this.session.session_id + '&model=' + + this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name + '&t=' + (new Date().getTime()) + this.$image.attr('src', url); + }, set_image_maxwidth: function() { this.$image.css('max-width', this.$element.width()); }, @@ -1940,13 +2283,6 @@ openerp.base.form.FieldBinaryImage = openerp.base.form.FieldBinary.extend({ on_clear: function() { this._super.apply(this, arguments); this.$image.attr('src', '/base/static/src/img/placeholder.png'); - }, - set_value: function(value) { - this._super.apply(this, arguments); - this.set_image_maxwidth(); - var url = '/base/binary/image?session_id=' + this.session.session_id + '&model=' + - this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name + '&t=' + (new Date().getTime()) - this.$image.attr('src', url); } }); @@ -1965,8 +2301,10 @@ openerp.base.form.widgets = new openerp.base.Registry({ 'url' : 'openerp.base.form.FieldUrl', 'text' : 'openerp.base.form.FieldText', 'text_wiki' : 'openerp.base.form.FieldText', - 'date' : 'openerp.base.form.FieldDate', - 'datetime' : 'openerp.base.form.FieldDatetime', +// 'date' : 'openerp.base.form.FieldDate', +// 'datetime' : 'openerp.base.form.FieldDatetime', + 'date' : 'openerp.base.form.FieldChar', + 'datetime' : 'openerp.base.form.FieldChar', 'selection' : 'openerp.base.form.FieldSelection', 'many2one' : 'openerp.base.form.FieldMany2One', 'many2many' : 'openerp.base.form.FieldMany2Many', @@ -1975,7 +2313,7 @@ openerp.base.form.widgets = new openerp.base.Registry({ 'reference' : 'openerp.base.form.FieldReference', 'boolean' : 'openerp.base.form.FieldBoolean', 'float' : 'openerp.base.form.FieldFloat', - 'integer': 'openerp.base.form.FieldFloat', + 'integer': 'openerp.base.form.FieldInteger', 'progressbar': 'openerp.base.form.FieldProgressBar', 'float_time': 'openerp.base.form.FieldFloatTime', 'image': 'openerp.base.form.FieldBinaryImage', diff --git a/addons/base/static/src/js/list-editable.js b/addons/base/static/src/js/list-editable.js index b97a35d0612..9a48889d801 100644 --- a/addons/base/static/src/js/list-editable.js +++ b/addons/base/static/src/js/list-editable.js @@ -103,80 +103,106 @@ openerp.base.list.editable = function (openerp) { * Checks if a record is being edited, and if so cancels it */ cancel_pending_edition: function () { + var self = this, cancelled = $.Deferred(); if (!this.edition) { - return; + cancelled.resolve(); + return cancelled.promise(); } if (this.edition_index !== null) { - this.reload_record(this.edition_index); + this.reload_record(this.edition_index, true).then(function () { + cancelled.resolve(); + }); + } else { + cancelled.resolve(); } - this.edition_form.stop(); - this.edition_form.$element.remove(); - delete this.edition_form; - delete this.edition_index; - delete this.edition; + cancelled.then(function () { + self.edition_form.stop(); + self.edition_form.$element.remove(); + delete self.edition_form; + delete self.edition_index; + delete self.edition; + }); + return cancelled.promise(); + }, + /** + * Adapts this list's view description to be suitable to the inner form view of a row being edited. + * + * @returns {Object} fields_view_get's view section suitable for putting into form view of editable rows. + */ + get_form_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; }, render_row_as_form: function (row) { - this.cancel_pending_edition(); - var self = this; - var $new_row = $('<tr>', { - id: _.uniqueId('oe-editable-row-'), - 'class': $(row).attr('class'), - click: function (e) {e.stopPropagation();} - }) - .delegate('button.oe-edit-row-save', 'click', function () { - self.save_row(); - }) - .delegate('button.oe-edit-row-cancel', 'click', function () { - self.cancel_edition(); - }) - .delegate('button', 'keyup', function (e) { - e.stopImmediatePropagation(); - }) - .keyup(function (e) { - switch (e.which) { - case KEY_RETURN: - self.save_row(true); - break; - case KEY_ESCAPE: - self.cancel_edition(); - break; - default: - return; - } - }); - 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('<td>'); - } + this.cancel_pending_edition().then(function () { + var $new_row = $('<tr>', { + id: _.uniqueId('oe-editable-row-'), + 'class': $(row).attr('class'), + click: function (e) {e.stopPropagation();} + }) + .delegate('button.oe-edit-row-save', 'click', function () { + self.save_row(); + }) + .delegate('button.oe-edit-row-cancel', 'click', function () { + self.cancel_edition(); + }) + .delegate('button', 'keyup', function (e) { + e.stopImmediatePropagation(); + }) + .keyup(function (e) { + switch (e.which) { + case KEY_RETURN: + self.save_row(true); + break; + case KEY_ESCAPE: + self.cancel_edition(); + break; + default: + return; + } + }); + if (row) { + $new_row.replaceAll(row); + } else if (self.options.editable === 'top') { + self.$current.prepend($new_row); + } else if (self.options.editable) { + self.$current.append($new_row); + } + self.edition = true; + self.edition_index = self.dataset.index; + self.edition_form = _.extend(new openerp.base.FormView( + null, self.group.view.session, $new_row.attr('id'), + self.dataset, false), { + template: 'ListView.row.form', + registry: openerp.base.list.form.widgets }); + $.when(self.edition_form.on_loaded({fields_view: self.get_form_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('<td>'); + } + }); - self.edition_form.do_show(); + self.edition_form.do_show(); + }); }); }, /** @@ -188,12 +214,7 @@ openerp.base.list.editable = function (openerp) { 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.cancel_pending_edition().then(function () { $(self).trigger('saved', [self.dataset]); if (!edit_next) { return; @@ -230,7 +251,12 @@ openerp.base.list.editable = function (openerp) { this.render_row_as_form(); } }); - openerp.base.list = {form: {}}; + if (!openerp.base.list) { + openerp.base.list = {}; + } + if (!openerp.base.list.form) { + openerp.base.list.form = {}; + } openerp.base.list.form.WidgetFrame = openerp.base.form.WidgetFrame.extend({ template: 'ListView.row.frame' }); @@ -251,10 +277,12 @@ openerp.base.list.editable = function (openerp) { 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') { + if (this.invisible) { this.$element.children().css('visibility', 'hidden'); } else { + this.invisible = !!this.modifiers.tree_invisible; this._super(); + this.invisible = false; } } }); diff --git a/addons/base/static/src/js/list.js b/addons/base/static/src/js/list.js index 62bedf8784d..d64d83329c0 100644 --- a/addons/base/static/src/js/list.js +++ b/addons/base/static/src/js/list.js @@ -1,5 +1,63 @@ openerp.base.list = function (openerp) { openerp.base.views.add('list', 'openerp.base.ListView'); +openerp.base.list = { + /** + * Formats the rendring of a given value based on its field type + * + * @param {Object} row_data record whose values should be displayed in the cell + * @param {Object} column column descriptor + * @param {"button"|"field"} column.tag base control type + * @param {String} column.type widget type for a field control + * @param {String} [column.string] button label + * @param {String} [column.icon] button icon + * @param {String} [value_if_empty=''] what to display if the field's value is ``false`` + */ + render_cell: function (row_data, column, value_if_empty) { + var attrs = column.modifiers_for(row_data); + if (attrs.invisible) { return ''; } + if (column.tag === 'button') { + return [ + '<button type="button" title="', column.string || '', '">', + '<img src="/base/static/src/img/icons/', column.icon, '.png"', + ' alt="', column.string || '', '"/>', + '</button>' + ].join('') + } + + var value = row_data[column.id].value; + + // If NaN value, display as with a `false` (empty cell) + if (typeof value === 'number' && isNaN(value)) { + value = false; + } + switch (value) { + case false: + case Infinity: + case -Infinity: + return value_if_empty === undefined ? '' : value_if_empty; + } + switch (column.widget || column.type) { + case 'integer': + return _.sprintf('%d', value); + case 'float': + var precision = column.digits ? column.digits[1] : 2; + return _.sprintf('%.' + precision + 'f', value); + case 'float_time': + return _.sprintf("%02d:%02d", + Math.floor(value), + Math.round((value % 1) * 60)); + case 'progressbar': + return _.sprintf( + '<progress value="%.2f" max="100.0">%.2f%%</progress>', + value, value); + case 'many2one': + // name_get value format + return value[1]; + default: + return value; + } + } +}; openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListView# */ { defaults: { // records can be selected one by one @@ -42,9 +100,10 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi * * @borrows openerp.base.ActionExecutor#execute_action as #execute_action */ - init: function(view_manager, session, element_id, dataset, view_id, options) { - this._super(session, element_id); - this.view_manager = view_manager || new openerp.base.NullViewManager(); + init: function(parent, element_id, dataset, view_id, options) { + this._super(parent, element_id); + this.set_default_options(); + this.view_manager = parent || new openerp.base.NullViewManager(); this.dataset = dataset; this.model = dataset.model; this.view_id = view_id; @@ -52,13 +111,30 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi this.columns = []; this.options = _.extend({}, this.defaults, options || {}); - this.flags = this.view_manager.flags || {}; this.set_groups(new openerp.base.ListView.Groups(this)); - + if (this.dataset instanceof openerp.base.DataSetStatic) { this.groups.datagroup = new openerp.base.StaticDataGroup(this.dataset); } + + this.page = 0; + }, + /** + * Retrieves the view's number of records per page (|| section) + * + * options > defaults > view_manager.action.limit > indefinite + * + * @returns {Number|null} + */ + limit: function () { + if (this._limit === undefined) { + this._limit = (this.options.limit + || this.defaults.limit + || (this.view_manager.action || {}).limit + || null); + } + return this._limit; }, /** * Set a custom Group construct as the root of the List View. @@ -129,27 +205,103 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi this.setup_columns(this.fields_view.fields, grouped); - if (!this.fields_view.sorted) { this.fields_view.sorted = {}; } - this.$element.html(QWeb.render("ListView", this)); // Head hook - this.$element.find('#oe-list-add') + this.$element.find('.oe-list-add') .click(this.do_add_record) .attr('disabled', grouped && this.options.editable); - this.$element.find('#oe-list-delete') + this.$element.find('.oe-list-delete') .attr('disabled', true) .click(this.do_delete_selected); - this.$element.find('thead').delegate('th[data-id]', 'click', function (e) { + this.$element.find('thead').delegate('th.oe-sortable[data-id]', 'click', function (e) { e.stopPropagation(); - self.dataset.sort($(this).data('id')); + var $this = $(this); + self.dataset.sort($this.data('id')); + if ($this.find('span').length) { + $this.find('span').toggleClass( + 'ui-icon-triangle-1-s ui-icon-triangle-1-n'); + } else { + $this.append('<span class="ui-icon ui-icon-triangle-1-s">') + .siblings('.oe-sortable').find('span').remove(); + } - // TODO: should only reload content (and set the right column to a sorted display state) - self.reload_view(); + self.reload_content(); }); - this.view_manager.sidebar.set_toolbar(data.fields_view.toolbar); + this.$element.find('.oe-list-pager') + .delegate('button', 'click', function () { + var $this = $(this); + switch ($this.data('pager-action')) { + case 'first': + self.page = 0; break; + case 'last': + self.page = Math.floor( + self.dataset.ids.length / self.limit()); + break; + case 'next': + self.page += 1; break; + case 'previous': + self.page -= 1; break; + } + self.reload_content(); + }).find('.oe-pager-state') + .click(function (e) { + e.stopPropagation(); + var $this = $(this); + + var $select = $('<select>') + .appendTo($this.empty()) + .click(function (e) {e.stopPropagation();}) + .append('<option value="80">80</option>' + + '<option value="100">100</option>' + + '<option value="200">200</option>' + + '<option value="500">500</option>' + + '<option value="NaN">Unlimited</option>') + .change(function () { + var val = parseInt($select.val(), 10); + self._limit = (isNaN(val) ? null : val); + self.page = 0; + self.reload_content(); + }) + .val(self._limit || 'NaN'); + }); + if (this.options.sidebar && this.options.sidebar_id) { + this.sidebar = new openerp.base.Sidebar(this, this.options.sidebar_id); + this.sidebar.start(); + this.sidebar.add_toolbar(data.fields_view.toolbar); + this.set_common_sidebar_sections(this.sidebar); + } + }, + /** + * Configures the ListView pager based on the provided dataset's information + * + * Horrifying side-effect: sets the dataset's data on this.dataset? + * + * @param {openerp.base.DataSet} dataset + */ + configure_pager: function (dataset) { + this.dataset.ids = dataset.ids; + + var limit = this.limit(), + total = dataset.ids.length, + first = (this.page * limit), + last; + if (!limit || (total - first) < limit) { + last = total; + } else { + last = first + limit; + } + this.$element.find('span.oe-pager-state').empty().text(_.sprintf( + "[%d to %d] of %d", first + 1, last, total)); + + this.$element + .find('button[data-pager-action=first], button[data-pager-action=previous]') + .attr('disabled', this.page === 0) + .end() + .find('button[data-pager-action=last], button[data-pager-action=next]') + .attr('disabled', last === total); }, /** * Sets up the listview's columns: merges view and fields data, move @@ -167,22 +319,26 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi var name = field.attrs.name; var column = _.extend({id: name, tag: field.tag}, field.attrs, fields[name]); - // attrs computer - if (column.attrs) { - var attrs = JSON.parse(column.attrs); - column.attrs_for = function (fields) { - var result = {}; - for (var attr in attrs) { - result[attr] = domain_computer(attrs[attr], fields); + // modifiers computer + if (column.modifiers) { + var modifiers = JSON.parse(column.modifiers); + column.modifiers_for = function (fields) { + if (!modifiers.invisible) { + return {}; } - return result; + return { + 'invisible': domain_computer(modifiers.invisible, fields) + }; }; + if (modifiers['tree_invisible']) { + column.invisible = '1'; + } } else { - column.attrs_for = noop; + column.modifiers_for = noop; } return column; }; - + this.columns.splice(0, this.columns.length); this.columns.push.apply( this.columns, @@ -190,10 +346,10 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi if (grouped) { this.columns.unshift({ id: '_group', tag: '', string: "Group", meta: true, - attrs_for: function () { return {}; } + modifiers_for: function () { return {}; } }, { id: '_count', tag: '', string: '#', meta: true, - attrs_for: function () { return {}; } + modifiers_for: function () { return {}; } }); } @@ -208,16 +364,10 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi } var aggregation_func = column['group_operator'] || 'sum'; - if (!column[aggregation_func]) { - return {}; - } - - return { - field: column.id, - type: column.type, + return _.extend({}, column, { 'function': aggregation_func, label: column[aggregation_func] - }; + }); }); }, /** @@ -246,15 +396,20 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi }, do_show: function () { this.$element.show(); + if (this.sidebar) { + this.sidebar.$element.show(); + } if (this.hidden) { - this.$element.find('table').append( + this.$element.find('.oe-listview-content').append( this.groups.apoptosis().render()); this.hidden = false; } - this.view_manager.sidebar.do_refresh(true); }, do_hide: function () { this.$element.hide(); + if (this.sidebar) { + this.sidebar.$element.hide(); + } this.hidden = true; }, /** @@ -264,10 +419,8 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi */ reload_view: function (grouped) { var self = this; - this.dataset.offset = 0; - this.dataset.limit = false; var callback = function (field_view_get) { - self.on_loaded(field_view_get, grouped); + self.on_loaded(field_view_get, grouped); }; if (this.embedded_view) { return $.Deferred().then(callback).resolve({fields_view: this.embedded_view}); @@ -275,8 +428,8 @@ 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 + context: this.dataset.get_context(), + toolbar: this.options.sidebar }, callback); } }, @@ -284,7 +437,7 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi * re-renders the content of the list view */ reload_content: function () { - this.$element.find('table').append( + this.$element.find('.oe-listview-content').append( this.groups.apoptosis().render( $.proxy(this, 'compute_aggregates'))); }, @@ -314,9 +467,10 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi this.dataset.context = results.context; this.dataset.domain = results.domain; this.groups.datagroup = new openerp.base.DataGroup( - this.session, this.model, + this, this.model, results.domain, results.context, results.group_by); + this.groups.datagroup.sort = this.dataset._sort; if (_.isEmpty(results.group_by) && !results.context['group_by_no_leaf']) { results.group_by = null; @@ -326,9 +480,9 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi $.proxy(this, 'reload_content')); }, /** - * Handles the signal to delete a line from the DOM + * Handles the signal to delete lines from the records list * - * @param {Array} ids the id of the object to delete + * @param {Array} ids the ids of the records to delete */ do_delete: function (ids) { if (!ids.length) { @@ -336,7 +490,8 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi } var self = this; return $.when(this.dataset.unlink(ids)).then(function () { - self.reload_content(); + self.groups.drop_records(ids); + self.compute_aggregates(); }); }, /** @@ -346,7 +501,7 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi * @param {Array} records selected record values */ do_select: function (ids, records) { - this.$element.find('#oe-list-delete') + this.$element.find('.oe-list-delete') .attr('disabled', !ids.length); if (!records.length) { @@ -365,16 +520,16 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi * @param {Function} callback should be called after the action is executed, if non-null */ do_action: function (name, id, callback) { - var action = _.detect(this.columns, function (field) { + var self = this, + action = _.detect(this.columns, function (field) { return field.name === name; }); if (!action) { return; } this.execute_action( - action, this.dataset, this.session.action_manager, - id, function () { - if (callback) { - callback(); - } + action, this.dataset, this.session.action_manager, id, function () { + $.when(callback.apply(this, arguments).then(function () { + self.compute_aggregates(); + })); }); }, /** @@ -388,7 +543,7 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi var self = this; _.extend(this.dataset, { domain: dataset.domain, - context: dataset.context + context: dataset.get_context() }).read_slice([], 0, false, function () { self.select_record(index); }); @@ -422,7 +577,6 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi compute_aggregates: function (records) { var columns = _(this.aggregate_columns).filter(function (column) { return column['function']; }); - if (_.isEmpty(columns)) { return; } if (_.isEmpty(records)) { @@ -430,17 +584,39 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi } var count = 0, sums = {}; - _(columns).each(function (column) { sums[column.field] = 0; }); + _(columns).each(function (column) { + switch (column['function']) { + case 'max': + sums[column.id] = -Infinity; + break; + case 'min': + sums[column.id] = Infinity; + break; + default: + sums[column.id] = 0; + } + }); _(records).each(function (record) { count += record.count || 1; _(columns).each(function (column) { - var field = column.field; + var field = column.id, + value = record.values[field]; switch (column['function']) { case 'sum': - sums[field] += record.values[field]; + sums[field] += value; break; case 'avg': - sums[field] += record.count * record.values[field]; + sums[field] += record.count * value; + break; + case 'min': + if (sums[field] > value) { + sums[field] = value; + } + break; + case 'max': + if (sums[field] < value) { + sums[field] = value; + } break; } }); @@ -448,14 +624,13 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi var aggregates = {}; _(columns).each(function (column) { - var field = column.field; + var field = column.id; switch (column['function']) { - case 'sum': - aggregates[field] = sums[field]; - break; case 'avg': - aggregates[field] = sums[field] / count; + aggregates[field] = {value: sums[field] / count}; break; + default: + aggregates[field] = {value: sums[field]}; } }); @@ -467,14 +642,14 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi 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])); + + $footer_cells.filter(_.sprintf('[data-field=%s]', column.id)) + .html(openerp.base.list.render_cell(aggregation, column)); }); } // TODO: implement reorder (drag and drop rows) }); -openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List# */{ +openerp.base.ListView.List = openerp.base.Class.extend( /** @lends openerp.base.ListView.List# */{ /** * List display for the ListView, handles basic DOM events and transforms * them in the relevant higher-level events, to which the list view (or @@ -505,6 +680,7 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List init: function (group, opts) { var self = this; this.group = group; + this.view = group.view; this.options = opts.options; this.columns = opts.columns; @@ -533,7 +709,7 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List index = self.row_position($row); $(self).trigger('action', [field, record_id, function () { - self.reload_record(index, true); + return self.reload_record(index, true); }]); }) .delegate('tr', 'click', function (e) { @@ -553,19 +729,9 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List this.$current.remove(); } 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; + this.$current.empty().append( + QWeb.render('ListView.rows', _.extend({ + render_cell: openerp.base.list.render_cell}, this))); }, /** * Gets the ids of all currently selected records, if any @@ -681,7 +847,7 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List return $.when(read_p).then(function () { self.$current.children().eq(record_index) - .replaceWith(self.render_record(record_index)); }) + .replaceWith(self.render_record(record_index)); }); }, /** * Renders a list record to HTML @@ -695,12 +861,31 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List options: this.options, row: this.rows[record_index], row_parity: (record_index % 2 === 0) ? 'even' : 'odd', - row_index: record_index + row_index: record_index, + render_cell: openerp.base.list.render_cell }); + }, + /** + * Stops displaying the records matching the provided ids. + * + * @param {Array} ids identifiers of the records to remove + */ + drop_records: function (ids) { + var self = this; + _(this.rows).chain() + .map(function (record, index) { + return {index: index, id: record.data.id.value}; + }).filter(function (record) { + return _(ids).contains(record.id); + }).reverse() + .each(function (record) { + self.$current.find('tr:eq(' + record.index + ')').remove(); + self.rows.splice(record.index, 1); + }) } // drag and drop }); -openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Groups# */{ +openerp.base.ListView.Groups = openerp.base.Class.extend( /** @lends openerp.base.ListView.Groups# */{ passtrough_events: 'action deleted row_link', /** * Grouped display for the ListView. Handles basic DOM events and interacts @@ -715,13 +900,10 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr this.columns = view.columns; this.datagroup = null; - this.sections = []; + this.$row = null; this.children = {}; - }, - pad: function ($row) { - if (this.options.selectable) { - $row.append('<td>'); - } + + this.page = 0; }, make_fragment: function () { return document.createDocumentFragment(); @@ -753,10 +935,35 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr } return red_letter_tboday; }, + make_paginator: function () { + var self = this; + var $prev = $('<button type="button" data-pager-action="previous"><</button>') + .click(function (e) { + e.stopPropagation(); + self.page -= 1; + + self.$row.closest('tbody').next() + .replaceWith(self.render()); + }); + var $next = $('<button type="button" data-pager-action="next">></button>') + .click(function (e) { + e.stopPropagation(); + self.page += 1; + + self.$row.closest('tbody').next() + .replaceWith(self.render()); + }); + this.$row.children().last() + .append($prev) + .append('<span class="oe-pager-state"></span>') + .append($next); + }, open: function (point_insertion) { this.render().insertAfter(point_insertion); + this.make_paginator(); }, close: function () { + this.$row.children().last().empty(); this.apoptosis(); }, /** @@ -788,7 +995,7 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr self.bind_child_events(child); child.datagroup = group; - var $row = $('<tr>'); + var $row = child.$row = $('<tr>'); if (group.openable) { $row.click(function (e) { if (!$row.data('open')) { @@ -808,11 +1015,16 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr } placeholder.appendChild($row[0]); - var $group_column = $('<th>').appendTo($row); + var $group_column = $('<th class="oe-group-name">').appendTo($row); + // Don't fill this if group_by_no_leaf but no group_by if (group.grouped_on) { - // Don't fill this if group_by_no_leaf but no group_by - $group_column - .text((group.value instanceof Array ? group.value[1] : group.value)); + var row_data = {}; + row_data[group.grouped_on] = group; + var group_column = _(self.columns).detect(function (column) { + return column.id === group.grouped_on; }); + $group_column.html(openerp.base.list.render_cell( + row_data, group_column, "Undefined" + )); if (group.openable) { // Make openable if not terminal group & group_by_no_leaf $group_column @@ -822,8 +1034,10 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr self.indent($group_column, group.level); // count column $('<td>').text(group.length).appendTo($row); - - self.pad($row); + + if (self.options.selectable) { + $row.append('<td>'); + } _(self.columns).chain() .filter(function (column) {return !column.invisible;}) .each(function (column) { @@ -844,6 +1058,9 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr $row.append('<td>'); } }); + if (self.options.deletable) { + $row.append('<td class="oe-group-pagination">'); + } }); return placeholder; }, @@ -867,6 +1084,7 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr }, render_dataset: function (dataset) { var rows = [], + self = this, list = new openerp.base.ListView.List(this, { options: this.options, columns: this.columns, @@ -875,11 +1093,30 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr }); this.bind_child_events(list); - var d = new $.Deferred(); + var view = this.view, + limit = view.limit(), + d = new $.Deferred(), + page = this.datagroup.openable ? this.page : view.page; + dataset.read_slice( _.filter(_.pluck(_.select(this.columns, function(x) {return x.tag == "field";}), 'name'), _.identity), - 0, false, + page * limit, limit, function (records) { + if (!self.datagroup.openable) { + view.configure_pager(dataset); + } else { + var pages = Math.ceil(dataset.ids.length / limit); + self.$row + .find('.oe-pager-state') + .text(_.sprintf('%d/%d', page + 1, pages)) + .end() + .find('button[data-pager-action=previous]') + .attr('disabled', page === 0) + .end() + .find('button[data-pager-action=next]') + .attr('disabled', page === pages - 1); + } + var form_records = _(records).map( $.proxy(list, 'transform_record')); @@ -890,22 +1127,67 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr }); return d.promise(); }, + setup_resequence_rows: function (list, dataset) { + // drag and drop enabled if list is not sorted and there is a + // "sequence" column in the view. + if ((dataset.sort && dataset.sort()) + || !_(this.columns).any(function (column) { + return column.name === 'sequence'; })) { + return; + } + // ondrop, move relevant record & fix sequences + list.$current.sortable({ + stop: function (event, ui) { + var from = ui.item.data('index'), + to = ui.item.prev().data('index') || 0; + if (from === to) { return; } + list.rows.splice(to, 0, list.rows.splice(from, 1)[0]); + + ui.item.parent().children().each(function (i, e) { + // reset record-index accelerators on rows and even/odd + var even = i%2 === 0; + $(e).data('index', i) + .toggleClass('even', even) + .toggleClass('odd', !even); + }); + + // resequencing time! + var data, index = to, + // if drag to 1st row (to = 0), start sequencing from 0 + // (exclusive lower bound) + seq = to ? list.rows[to - 1].data.sequence.value : 0; + while (++seq, data = list.rows[index++].data) { + data.sequence.value = seq; + // write are independent from one another, so we can just + // launch them all at the same time and we don't really + // give a fig about when they're done + dataset.write(data.id.value, {sequence: seq}); + } + } + }); + }, render: function (post_render) { var self = this; var $element = $('<tbody>'); this.elements = [$element[0]]; - this.datagroup.list(function (groups) { - $element[0].appendChild( - self.render_groups(groups)); - if (post_render) { post_render(); } - }, function (dataset) { - self.render_dataset(dataset).then(function (list) { - self.children[null] = list; - self.elements = - [list.$current.replaceAll($element)[0]]; + + this.datagroup.list( + _(this.view.visible_columns).chain() + .filter(function (column) { return column.tag === 'field' }) + .pluck('name').value(), + function (groups) { + $element[0].appendChild( + self.render_groups(groups)); if (post_render) { post_render(); } + }, function (dataset) { + self.render_dataset(dataset).then(function (list) { + self.children[null] = list; + self.elements = + [list.$current.replaceAll($element)[0]]; + self.setup_resequence_rows(list, dataset); + if (post_render) { post_render(); } + }); }); - }); return $element; }, /** @@ -943,6 +1225,19 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr .map(function (child) { return child.get_records(); }).flatten().value(); + }, + /** + * Stops displaying the records with the linked ids, assumes these records + * were deleted from the DB. + * + * This is the up-signal from the `deleted` event on groups and lists. + * + * @param {Array} ids list of identifier of the records to remove. + */ + drop_records: function (ids) { + _.each(this.children, function (child) { + child.drop_records(ids); + }); } }); }; diff --git a/addons/base/static/src/js/search.js b/addons/base/static/src/js/search.js index 6efa6988fe8..763746bb0bf 100644 --- a/addons/base/static/src/js/search.js +++ b/addons/base/static/src/js/search.js @@ -1,9 +1,9 @@ openerp.base.search = function(openerp) { -openerp.base.SearchView = openerp.base.Controller.extend({ - init: function(view_manager, session, element_id, dataset, view_id, defaults) { - this._super(session, element_id); - this.view_manager = view_manager || new openerp.base.NullViewManager(); +openerp.base.SearchView = openerp.base.Widget.extend({ + init: function(parent, element_id, dataset, view_id, defaults) { + this._super(parent, element_id); + this.view_manager = parent || new openerp.base.NullViewManager(); this.dataset = dataset; this.model = dataset.model; this.view_id = view_id; @@ -14,10 +14,13 @@ openerp.base.SearchView = openerp.base.Controller.extend({ this.enabled_filters = []; this.has_focus = false; + + this.ready = $.Deferred(); }, start: function() { //this.log('Starting SearchView '+this.model+this.view_id) - return this.rpc("/base/searchview/load", {"model": this.model, "view_id":this.view_id}, this.on_loaded); + this.rpc("/base/searchview/load", {"model": this.model, "view_id":this.view_id}, this.on_loaded); + return this.ready.promise(); }, show: function () { this.$element.show(); @@ -107,12 +110,13 @@ openerp.base.SearchView = openerp.base.Controller.extend({ } }, on_loaded: function(data) { - var lines = this.make_widgets( - data.fields_view['arch'].children, - data.fields_view.fields); + var self = this, + lines = this.make_widgets( + data.fields_view['arch'].children, + data.fields_view.fields); // for extended search view - var ext = new openerp.base.search.ExtendedSearch(null, this.session, this.model); + var ext = new openerp.base.search.ExtendedSearch(this, this.model); lines.push([ext]); this.inputs.push(ext); @@ -121,6 +125,7 @@ openerp.base.SearchView = openerp.base.Controller.extend({ 'lines': lines, 'defaults': this.defaults }); + // We don't understand why the following commented line does not work in Chrome but // the non-commented line does. As far as we investigated, only God knows. //this.$element.html(render); @@ -132,30 +137,90 @@ openerp.base.SearchView = openerp.base.Controller.extend({ .submit(this.do_search) .bind('reset', this.do_clear); // start() all the widgets - _(lines).chain().flatten().each(function (widget) { - widget.start(); + var widget_starts = _(lines).chain().flatten().map(function (widget) { + return widget.start(); + }).value(); + + $.when.apply(null, widget_starts).then(function () { + self.ready.resolve(); }); - // filters management - this.$element.find(".oe_search-view-filters-management").change(this.on_filters_management); + this.reload_managed_filters(); + }, + reload_managed_filters: function() { + var self = this; + return this.rpc('/base/searchview/get_filters', { + model: this.dataset.model + }).then(function(result) { + self.managed_filters = result; + var filters = self.$element.find(".oe_search-view-filters-management"); + filters.html(QWeb.render("SearchView.managed-filters", {filters: result})); + filters.change(self.on_filters_management); + }); }, /** * Handle event when the user make a selection in the filters management select box. */ on_filters_management: function(e) { + var self = this; var select = this.$element.find(".oe_search-view-filters-management"); var val = select.val(); - select.val("_filters"); - if (val.slice(0,1) == "_") // useless action + if (val.slice(0,1) == "_") { // useless action + select.val("_filters"); return; + } if (val.slice(0, "get:".length) == "get:") { val = val.slice("get:".length); - //TODO niv + val = parseInt(val); + var filter = this.managed_filters[val]; + this.on_search([filter.domain], [filter.context], []); } else if (val == "save_filter") { - //TODO niv + select.val("_filters"); + var data = this.build_search_data(); + var context = new openerp.base.CompoundContext(); + _.each(data.contexts, function(x) { + context.add(x); + }); + var domain = new openerp.base.CompoundDomain(); + _.each(data.domains, function(x) { + domain.add(x); + }); + var dial_html = QWeb.render("SearchView.managed-filters.add"); + var $dial = $(dial_html); + $dial.dialog({ + modal: true, + title: "Filter Entry", + buttons: { + Cancel: function() { + $(this).dialog("close"); + }, + OK: function() { + $(this).dialog("close"); + var name = $(this).find("input").val(); + self.rpc('/base/searchview/save_filter', { + model: self.dataset.model, + context_to_save: context, + domain: domain, + name: name + }).then(function(result) { + self.reload_managed_filters(); + }); + }, + } + }); } else { // manage_filters - //TODO niv + select.val("_filters"); + this.do_action({ + res_model: 'ir.filters', + views: [[false, 'list'], [false, 'form']], + type: 'ir.actions.act_window', + context: {"search_default_user_id": this.session.uid, + "search_default_model_id": this.dataset.model}, + target: "current", + limit : 80, + auto_search : true + }); } }, /** @@ -170,11 +235,25 @@ openerp.base.SearchView = openerp.base.Controller.extend({ * @param e jQuery event object coming from the "Search" button */ do_search: function (e) { + // reset filters management + var select = this.$element.find(".oe_search-view-filters-management"); + select.val("_filters"); + if (e && e.preventDefault) { e.preventDefault(); } - var domains = [], contexts = []; + var data = this.build_search_data(); + + if (data.errors.length) { + this.on_invalid(data.errors); + return; + } - var errors = []; + this.on_search(data.domains, data.contexts, data.groupbys); + }, + build_search_data: function() { + var domains = [], + contexts = [], + errors = []; _.each(this.inputs, function (input) { try { @@ -196,19 +275,13 @@ openerp.base.SearchView = openerp.base.Controller.extend({ } }); - 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); + return {domains: domains, contexts: contexts, errors: errors, groupbys: groupbys}; }, /** * Triggered after the SearchView has collected all relevant domains and @@ -293,8 +366,7 @@ openerp.base.search.fields = new openerp.base.Registry({ 'many2one': 'openerp.base.search.ManyToOneField', 'many2many': 'openerp.base.search.ManyToManyField' }); -openerp.base.search.Invalid = Class.extend( - /** @lends openerp.base.search.Invalid# */{ +openerp.base.search.Invalid = openerp.base.Class.extend( /** @lends openerp.base.search.Invalid# */{ /** * Exception thrown by search widgets when they hold invalid values, * which they can not return when asked. @@ -314,14 +386,13 @@ openerp.base.search.Invalid = Class.extend( ': [' + this.value + '] is ' + this.message); } }); -openerp.base.search.Widget = openerp.base.Controller.extend( - /** @lends openerp.base.search.Widget# */{ +openerp.base.search.Widget = openerp.base.Widget.extend( /** @lends openerp.base.search.Widget# */{ template: null, /** * Root class of all search widgets * * @constructs - * @extends openerp.base.Controller + * @extends openerp.base.Widget * * @param view the ancestor view of this widget */ @@ -395,8 +466,6 @@ openerp.base.search.add_expand_listener = function($root) { }; 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; @@ -414,8 +483,7 @@ openerp.base.search.Group = openerp.base.search.Widget.extend({ } }); -openerp.base.search.Input = openerp.base.search.Widget.extend( - /** @lends openerp.base.search.Input# */{ +openerp.base.search.Input = openerp.base.search.Widget.extend( /** @lends openerp.base.search.Input# */{ /** * @constructs * @extends openerp.base.search.Widget @@ -437,7 +505,6 @@ openerp.base.search.Input = openerp.base.search.Widget.extend( }); openerp.base.search.Filter = openerp.base.search.Input.extend({ template: 'SearchView.filter', - // TODO: force rendering init: function (node, view) { this._super(view); this.attrs = node.attrs; @@ -486,13 +553,9 @@ openerp.base.search.Filter = openerp.base.search.Input.extend({ return this.attrs.domain; } }); -openerp.base.search.Field = openerp.base.search.Input.extend( - /** @lends openerp.base.search.Field# */ { +openerp.base.search.Field = openerp.base.search.Input.extend( /** @lends openerp.base.search.Field# */ { template: 'SearchView.field', default_operator: '=', - // TODO: set default values - // TODO: get context, domain - // TODO: holds Filters /** * @constructs * @extends openerp.base.search.Input @@ -530,9 +593,7 @@ openerp.base.search.Field = openerp.base.search.Input.extend( }, get_domain: function () { var val = this.get_value(); - - var has_value = (val !== null && val !== ''); - if(!has_value) { + if (val === null || val === '') { return; } @@ -544,9 +605,7 @@ openerp.base.search.Field = openerp.base.search.Input.extend( this.get_value() ]]; } - return _.extend( - {}, domain, - {own_values: {self: val}}); + return _.extend({}, domain, {own_values: {self: val}}); } }); /** @@ -559,8 +618,7 @@ openerp.base.search.Field = openerp.base.search.Input.extend( * @class * @extends openerp.base.search.Field */ -openerp.base.search.CharField = openerp.base.search.Field.extend( - /** @lends openerp.base.search.CharField# */ { +openerp.base.search.CharField = openerp.base.search.Field.extend( /** @lends openerp.base.search.CharField# */ { default_operator: 'ilike', get_value: function () { return this.$element.val(); @@ -599,32 +657,40 @@ openerp.base.search.BooleanField = openerp.base.search.Field.extend({ } } }); -openerp.base.search.IntegerField = openerp.base.search.Field.extend({ +openerp.base.search.NumberField = openerp.base.search.Field.extend(/** @lends openerp.base.search.NumberField# */{ get_value: function () { if (!this.$element.val()) { return null; } - var val = parseInt(this.$element.val()); - var check = Number(this.$element.val()); - if (isNaN(check) || val !== check) { + var val = this.parse(this.$element.val()), + check = Number(this.$element.val()); + if (isNaN(val) || val !== check) { this.$element.addClass('error'); throw new openerp.base.search.Invalid( - this.attrs.name, this.$element.val(), "not a valid integer"); + this.attrs.name, this.$element.val(), this.error_message); } this.$element.removeClass('error'); return val; } }); -openerp.base.search.FloatField = openerp.base.search.Field.extend({ - get_value: function () { - var val = Number(this.$element.val()); - if (isNaN(val)) { - this.$element.addClass('error'); - throw new openerp.base.search.Invalid( - this.attrs.name, this.$element.val(), "not a valid number"); - } - this.$element.removeClass('error'); - return val; +/** + * @class + * @extends openerp.base.search.NumberField + */ +openerp.base.search.IntegerField = openerp.base.search.NumberField.extend(/** @lends openerp.base.search.IntegerField# */{ + error_message: "not a valid integer", + parse: function (value) { + return parseInt(value, 10); + } +}); +/** + * @class + * @extends openerp.base.search.NumberField + */ +openerp.base.search.FloatField = openerp.base.search.NumberField.extend(/** @lends openerp.base.search.FloatField# */{ + error_message: "not a valid number", + parse: function (value) { + return parseFloat(value); } }); openerp.base.search.SelectionField = openerp.base.search.Field.extend({ @@ -633,12 +699,7 @@ openerp.base.search.SelectionField = openerp.base.search.Field.extend({ return this.$element.val(); } }); -/** - * @class - * @extends openerp.base.search.Field - */ -openerp.base.search.DateField = openerp.base.search.Field.extend( - /** @lends openerp.base.search.DateField# */{ +openerp.base.search.DateField = openerp.base.search.Field.extend( /** @lends openerp.base.search.DateField# */{ template: 'SearchView.fields.date', /** * enables date picker on the HTML widgets @@ -663,8 +724,12 @@ openerp.base.search.DateField = openerp.base.search.Field.extend( get_values: function () { var values_array = this.$element.find('input').serializeArray(); - var from = values_array[0].value; - var to = values_array[1].value; + if (!values_array || !values_array[0]) { + throw new openerp.base.search.Invalid( + this.attrs.name, null, "widget not ready"); + } + var from = values_array[0].value, + to = values_array[1].value; var field_values = {}; if (from) { @@ -709,33 +774,100 @@ openerp.base.search.DateField = openerp.base.search.Field.extend( openerp.base.search.DateTimeField = openerp.base.search.DateField.extend({ // TODO: time? }); -openerp.base.search.OneToManyField = openerp.base.search.IntegerField.extend({ +openerp.base.search.OneToManyField = openerp.base.search.CharField.extend({ // TODO: .relation, .context, .domain }); -openerp.base.search.ManyToOneField = openerp.base.search.IntegerField.extend({ +openerp.base.search.ManyToOneField = openerp.base.search.CharField.extend({ // TODO: @widget // TODO: .relation, .selection, .context, .domain + init: function (view_section, field, view) { + this._super(view_section, field, view); + var self = this; + this.got_name = $.Deferred().then(function () { + self.$element.val(self.name); + }); + this.dataset = new openerp.base.DataSet( + this.view, this.attrs['relation']); + }, + start: function () { + this._super(); + this.setup_autocomplete(); + var started = $.Deferred(); + this.got_name.then(function () { started.resolve();}, + function () { started.resolve(); }); + return started.promise(); + }, + setup_autocomplete: function () { + var self = this; + this.$element.autocomplete({ + source: function (req, resp) { + self.dataset.name_search( + req.term, self.attrs.domain, 'ilike', 8, function (data) { + resp(_.map(data, function (result) { + return {id: result[0], label: result[1]} + })); + }); + }, + select: function (event, ui) { + self.id = ui.item.id; + self.name = ui.item.label; + }, + delay: 0 + }) + }, + on_name_get: function (name_get) { + if (!name_get.length) { + delete this.id; + this.got_name.reject(); + return; + } + this.name = name_get[0][1]; + this.got_name.resolve(); + }, + render: function (defaults) { + if (defaults[this.attrs.name]) { + this.id = defaults[this.attrs.name]; + // TODO: maybe this should not be completely removed + delete defaults[this.attrs.name]; + this.dataset.name_get([this.id], $.proxy(this, 'on_name_get')); + } else { + this.got_name.reject(); + } + return this._super(defaults); + }, + get_domain: function () { + if (this.id && this.name) { + if (this.$element.val() === this.name) { + return [[this.attrs.name, '=', this.id]]; + } else { + delete this.id; + delete this.name; + } + } + return this._super(); + } }); -openerp.base.search.ManyToManyField = openerp.base.search.IntegerField.extend({ +openerp.base.search.ManyToManyField = openerp.base.search.CharField.extend({ // TODO: .related_columns (Array), .context, .domain }); -openerp.base.search.ExtendedSearch = openerp.base.BaseWidget.extend({ +openerp.base.search.ExtendedSearch = openerp.base.OldWidget.extend({ template: 'SearchView.extended_search', identifier_prefix: 'extended-search', - init: function (parent, session, model) { - this._super(parent, session); + init: function (parent, model) { + this._super(parent); this.model = model; }, add_group: function() { var group = new openerp.base.search.ExtendedSearchGroup(this, this.fields); - var render = group.render(); - this.$element.find('.searchview_extended_groups_list').append(render); - group.start(); + group.appendTo(this.$element.find('.searchview_extended_groups_list')); this.check_last_element(); }, start: function () { this._super(); + if (!this.$element) { + return; // not a logical state but sometimes it happens + } this.$element.closest("table.oe-searchview-render-line").css("display", "none"); var self = this; this.rpc("/base/searchview/fields_get", @@ -751,10 +883,13 @@ openerp.base.search.ExtendedSearch = openerp.base.BaseWidget.extend({ return null; }, get_domain: function() { + if (!this.$element) { + return null; // not a logical state but sometimes it happens + } if(this.$element.closest("table.oe-searchview-render-line").css("display") == "none") { return null; } - return _.reduce(this.children, + return _.reduce(this.widget_children, function(mem, x) { return mem.concat(x.get_domain());}, []); }, on_activate: function() { @@ -773,12 +908,14 @@ openerp.base.search.ExtendedSearch = openerp.base.BaseWidget.extend({ } }, check_last_element: function() { - _.each(this.children, function(x) {x.set_last_group(false);}); - this.children[this.children.length - 1].set_last_group(true); + _.each(this.widget_children, function(x) {x.set_last_group(false);}); + if (this.widget_children.length >= 1) { + this.widget_children[this.widget_children.length - 1].set_last_group(true); + } } }); -openerp.base.search.ExtendedSearchGroup = openerp.base.BaseWidget.extend({ +openerp.base.search.ExtendedSearchGroup = openerp.base.OldWidget.extend({ template: 'SearchView.extended_search.group', identifier_prefix: 'extended-search-group', init: function (parent, fields) { @@ -787,7 +924,7 @@ openerp.base.search.ExtendedSearchGroup = openerp.base.BaseWidget.extend({ }, add_prop: function() { var prop = new openerp.base.search.ExtendedSearchProposition(this, this.fields); - var render = prop.render({'index': this.children.length - 1}); + var render = prop.render({'index': this.widget_children.length - 1}); this.$element.find('.searchview_extended_propositions_list').append(render); prop.start(); }, @@ -804,7 +941,7 @@ openerp.base.search.ExtendedSearchGroup = openerp.base.BaseWidget.extend({ }); }, get_domain: function() { - var props = _(this.children).chain().map(function(x) { + var props = _(this.widget_children).chain().map(function(x) { return x.get_proposition(); }).compact().value(); var choice = this.$element.find(".searchview_extended_group_choice").val(); @@ -814,9 +951,9 @@ openerp.base.search.ExtendedSearchGroup = openerp.base.BaseWidget.extend({ props); }, stop: function() { - var parent = this.parent; - if (this.parent.children.length == 1) - this.parent.hide(); + var parent = this.widget_parent; + if (this.widget_parent.widget_children.length == 1) + this.widget_parent.hide(); this._super(); parent.check_last_element(); }, @@ -828,7 +965,7 @@ openerp.base.search.ExtendedSearchGroup = openerp.base.BaseWidget.extend({ } }); -openerp.base.search.ExtendedSearchProposition = openerp.base.BaseWidget.extend({ +openerp.base.search.ExtendedSearchProposition = openerp.base.OldWidget.extend({ template: 'SearchView.extended_search.proposition', identifier_prefix: 'extended-search-proposition', init: function (parent, fields) { @@ -853,9 +990,12 @@ openerp.base.search.ExtendedSearchProposition = openerp.base.BaseWidget.extend({ }); }, stop: function() { - if (this.parent.children.length == 1) - this.parent.stop(); + var parent; + if (this.widget_parent.widget_children.length == 1) + parent = this.widget_parent; this._super(); + if (parent) + parent.stop(); }, changed: function() { var nval = this.$element.find(".searchview_extended_prop_field").val(); @@ -887,7 +1027,7 @@ openerp.base.search.ExtendedSearchProposition = openerp.base.BaseWidget.extend({ if (! e instanceof openerp.base.KeyNotFound) { throw e; } - var type = "char"; + type = "char"; this.log('Unknow field type ' + e.key); } this.value = new (openerp.base.search.custom_filters.get_object(type)) @@ -915,7 +1055,7 @@ openerp.base.search.ExtendedSearchProposition = openerp.base.BaseWidget.extend({ } }); -openerp.base.search.ExtendedSearchProposition.Char = openerp.base.BaseWidget.extend({ +openerp.base.search.ExtendedSearchProposition.Char = openerp.base.OldWidget.extend({ template: 'SearchView.extended_search.proposition.char', identifier_prefix: 'extended-search-proposition-char', operators: [ @@ -932,7 +1072,7 @@ openerp.base.search.ExtendedSearchProposition.Char = openerp.base.BaseWidget.ext return this.$element.val(); } }); -openerp.base.search.ExtendedSearchProposition.DateTime = openerp.base.BaseWidget.extend({ +openerp.base.search.ExtendedSearchProposition.DateTime = openerp.base.OldWidget.extend({ template: 'SearchView.extended_search.proposition.datetime', identifier_prefix: 'extended-search-proposition-datetime', operators: [ @@ -954,7 +1094,7 @@ openerp.base.search.ExtendedSearchProposition.DateTime = openerp.base.BaseWidget }); } }); -openerp.base.search.ExtendedSearchProposition.Date = openerp.base.BaseWidget.extend({ +openerp.base.search.ExtendedSearchProposition.Date = openerp.base.OldWidget.extend({ template: 'SearchView.extended_search.proposition.date', identifier_prefix: 'extended-search-proposition-date', operators: [ @@ -976,7 +1116,7 @@ openerp.base.search.ExtendedSearchProposition.Date = openerp.base.BaseWidget.ext }); } }); -openerp.base.search.ExtendedSearchProposition.Integer = openerp.base.BaseWidget.extend({ +openerp.base.search.ExtendedSearchProposition.Integer = openerp.base.OldWidget.extend({ template: 'SearchView.extended_search.proposition.integer', identifier_prefix: 'extended-search-proposition-integer', operators: [ @@ -988,15 +1128,14 @@ openerp.base.search.ExtendedSearchProposition.Integer = openerp.base.BaseWidget. {value: "<=", text: "less or equal than"} ], get_value: function() { - val = this.$element.val(); - val2 = parseFloat(val); - if(val2 != 0 && !val2) { + var value = parseFloat(this.$element.val()); + if(value != 0 && !value) { return ""; } - return Math.round(val2); + return Math.round(value); } }); -openerp.base.search.ExtendedSearchProposition.Float = openerp.base.BaseWidget.extend({ +openerp.base.search.ExtendedSearchProposition.Float = openerp.base.OldWidget.extend({ template: 'SearchView.extended_search.proposition.float', identifier_prefix: 'extended-search-proposition-float', operators: [ @@ -1008,15 +1147,14 @@ openerp.base.search.ExtendedSearchProposition.Float = openerp.base.BaseWidget.ex {value: "<=", text: "less or equal than"} ], get_value: function() { - val = this.$element.val(); - val2 = parseFloat(val); - if(val2 != 0 && !val2) { + var value = parseFloat(this.$element.val()); + if(value != 0 && !value) { return ""; } - return val2; + return value; } }); -openerp.base.search.ExtendedSearchProposition.Selection = openerp.base.BaseWidget.extend({ +openerp.base.search.ExtendedSearchProposition.Selection = openerp.base.OldWidget.extend({ template: 'SearchView.extended_search.proposition.selection', identifier_prefix: 'extended-search-proposition-selection', operators: [ @@ -1030,7 +1168,7 @@ openerp.base.search.ExtendedSearchProposition.Selection = openerp.base.BaseWidge return this.$element.val(); } }); -openerp.base.search.ExtendedSearchProposition.Boolean = openerp.base.BaseWidget.extend({ +openerp.base.search.ExtendedSearchProposition.Boolean = openerp.base.OldWidget.extend({ template: 'SearchView.extended_search.proposition.boolean', identifier_prefix: 'extended-search-proposition-boolean', operators: [ diff --git a/addons/base/static/src/js/view_help.js b/addons/base/static/src/js/view_help.js new file mode 100644 index 00000000000..aa98f59c3ab --- /dev/null +++ b/addons/base/static/src/js/view_help.js @@ -0,0 +1,15 @@ +/*--------------------------------------------------------- + * OpenERP base library + *---------------------------------------------------------*/ + +openerp.base.view_help = function(openerp) { + +openerp.base.ProcessView = openerp.base.Widget.extend({ +}); + +openerp.base.HelpView = openerp.base.Widget.extend({ +}); + +}; + +// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/base/static/src/js/tree.js b/addons/base/static/src/js/view_tree.js index e333c295473..90972f84973 100644 --- a/addons/base/static/src/js/tree.js +++ b/addons/base/static/src/js/view_tree.js @@ -2,10 +2,10 @@ * OpenERP base library *---------------------------------------------------------*/ -openerp.base.tree = function(openerp) { +openerp.base.view_tree = function(openerp) { openerp.base.views.add('tree', 'openerp.base.TreeView'); -openerp.base.TreeView = openerp.base.Controller.extend({ +openerp.base.TreeView = openerp.base.Widget.extend({ /** * Genuine tree view (the one displayed as a tree, not the list) */ diff --git a/addons/base/static/src/js/views.js b/addons/base/static/src/js/views.js index ee4388c8971..7e95ff67f83 100644 --- a/addons/base/static/src/js/views.js +++ b/addons/base/static/src/js/views.js @@ -4,84 +4,117 @@ openerp.base.views = function(openerp) { -openerp.base.ActionManager = openerp.base.Controller.extend({ +openerp.base.ActionManager = openerp.base.Widget.extend({ // process all kind of actions - init: function(session, element_id) { - this._super(session, element_id); + init: function(parent, element_id) { + this._super(parent, element_id); this.viewmanager = null; - this.dialog_stack = []; + this.current_dialog = null; // Temporary linking view_manager to session. - // Will use controller_parent to find it when implementation will be done. - session.action_manager = this; + // Will use parent to find it when implementation will be done. + this.session.action_manager = this; }, /** * Process an action * Supported actions: act_window */ + action_window: function() { + }, + action_window_close: function() { + }, + action_server: function() { + }, + action_url: function() { + }, + action_report: function() { + }, + action_client: function() { + }, do_action: function(action, on_closed) { var self = this; action.flags = _.extend({ sidebar : action.target != 'new', - search_view : true, + search_view : action.target != 'new', new_window : false, - views_switcher : true, - action_buttons : true, - pager : true + views_switcher : action.target != 'new', + action_buttons : action.target != 'new', + pager : action.target != 'new' }, action.flags || {}); // instantiate the right controllers by understanding the action switch (action.type) { case 'ir.actions.act_window': - if (!action.target && this.dialog_stack.length) { + if (!action.target && this.current_dialog) { action.flags.new_window = true; } if (action.target == 'new') { - var element_id = _.uniqueId("act_window_dialog"); - $('<div>', {id: element_id}).dialog({ - title: action.name, - modal: true, - width: '50%', - height: 'auto' - }).bind('dialogclose', function(event) { - // When dialog is closed with ESC key or close manually, branch to act_window_close logic - self.do_action({ type: 'ir.actions.act_window_close' }); - }); - var viewmanager = new openerp.base.ViewManagerAction(this.session, element_id, action); + var dialog = this.current_dialog = new openerp.base.ActionDialog(this, { title: action.name, width: '90%' }); + if (on_closed) { + dialog.close_callback = on_closed; + } + dialog.start(false); + var viewmanager = dialog.viewmanager = new openerp.base.ViewManagerAction(this, dialog.element_id, action); viewmanager.start(); - viewmanager.on_act_window_closed.add(on_closed); - viewmanager.is_dialog = true; - this.dialog_stack.push(viewmanager); + dialog.open(); } else if (action.flags.new_window) { action.flags.new_window = false; this.rpc("/base/session/save_session_action", { the_action : action}, function(key) { var url = window.location.protocol + "//" + window.location.host + window.location.pathname + "?" + jQuery.param({ s_action : "" + key }); window.open(url); + if (on_closed) { + on_closed(); + } }); } else { if (this.viewmanager) { this.viewmanager.stop(); } - this.viewmanager = new openerp.base.ViewManagerAction(this.session, this.element_id, action); + this.viewmanager = new openerp.base.ViewManagerAction(this, this.element_id, action); this.viewmanager.start(); } break; case 'ir.actions.act_window_close': - var dialog = this.dialog_stack.pop(); - if (!action.special) { - dialog.on_act_window_closed(); - } - dialog.$element.dialog('destroy'); - dialog.stop(); + this.close_dialog(); + break; + case 'ir.actions.server': + this.rpc('/base/action/run', { + action_id: action.id, + context: {active_id: 66, active_ids: [66], active_model: 'ir.ui.menu'} + }).then(function (action) { + self.do_action(action, on_closed) + }); break; default: console.log("Action manager can't handle action of type " + action.type, action); } + }, + close_dialog: function() { + if (this.current_dialog) { + this.current_dialog.stop(); + this.current_dialog = null; + } + } +}); + +openerp.base.ActionDialog = openerp.base.Dialog.extend({ + identifier_prefix: 'action_dialog', + on_close: function() { + this._super(this, arguments); + if (this.close_callback) { + this.close_callback(); + } + }, + stop: function() { + this._super(this, arguments); + if (this.viewmanager) { + this.viewmanager.stop(); + } } }); -openerp.base.ViewManager = openerp.base.Controller.extend({ - init: function(session, element_id, dataset, views) { - this._super(session, element_id); +openerp.base.ViewManager = openerp.base.Widget.extend({ + init: function(parent, element_id, dataset, views) { + this._super(parent, element_id); this.model = dataset.model; this.dataset = dataset; this.searchview = null; @@ -90,14 +123,13 @@ openerp.base.ViewManager = openerp.base.Controller.extend({ {return x instanceof Array? {view_id: x[0], view_type: x[1]} : x;}); this.views = {}; this.flags = this.flags || {}; - this.sidebar = new openerp.base.NullSidebar(); this.registry = openerp.base.views; - this.is_dialog = false; }, /** * @returns {jQuery.Deferred} initial view loading promise */ start: function() { + this._super(); var self = this; this.dataset.start(); this.$element.html(QWeb.render("ViewManager", {"prefix": this.element_id, views: this.views_src})); @@ -105,7 +137,12 @@ openerp.base.ViewManager = openerp.base.Controller.extend({ self.on_mode_switch($(this).data('view-type')); }); _.each(this.views_src, function(view) { - self.views[view.view_type] = $.extend({}, view, {controller: null}); + self.views[view.view_type] = $.extend({}, view, { + controller : null, + options : _.extend({ + sidebar_id : self.element_id + '_sidebar_' + view.view_type + }, self.flags) + }); }); if (this.flags.views_switcher === false) { this.$element.find('.oe_vm_switch').hide(); @@ -129,7 +166,7 @@ openerp.base.ViewManager = openerp.base.Controller.extend({ if (!view.controller) { // Lazy loading of views var controllerclass = this.registry.get_object(view_type); - var controller = new controllerclass( this, this.session, this.element_id + "_view_" + view_type, + var controller = new controllerclass(this, this.element_id + '_view_' + view_type, this.dataset, view.view_id, view.options); if (view.embedded_view) { controller.set_embedded_view(view.embedded_view); @@ -183,7 +220,7 @@ openerp.base.ViewManager = openerp.base.Controller.extend({ }, /** * Event launched when a controller has been inited. - * + * * @param {String} view_type type of view * @param {String} view the inited controller */ @@ -199,7 +236,7 @@ openerp.base.ViewManager = openerp.base.Controller.extend({ if (this.searchview) { this.searchview.stop(); } - this.searchview = new openerp.base.SearchView(this, this.session, this.element_id + "_search", this.dataset, view_id, search_defaults); + this.searchview = new openerp.base.SearchView(this, this.element_id + "_search", this.dataset, view_id, search_defaults); if (this.flags.search_view === false) { this.searchview.hide(); } @@ -211,11 +248,6 @@ openerp.base.ViewManager = openerp.base.Controller.extend({ return this.searchview.start(); }, /** - * Called when this view manager has been created by an action 'act_window@target=new' is closed - */ - on_act_window_closed : function() { - }, - /** * Called when one of the view want to execute an action */ on_action: function(action) { @@ -246,45 +278,39 @@ openerp.base.ViewManager = openerp.base.Controller.extend({ }); openerp.base.NullViewManager = openerp.base.generate_null_object_class(openerp.base.ViewManager, { - init: function() { - this._super(); + init: function(parent) { + this._super(parent); + if(parent) + this.session = parent.session; this.action = {flags: {}}; - this.sidebar = new openerp.base.NullSidebar(); } }); +// TODO Will move to action Manager openerp.base.ViewManagerAction = openerp.base.ViewManager.extend({ - init: function(session, element_id, action) { + init: function(parent, element_id, action) { + this.session = parent.session; var dataset; if (!action.res_id) { - dataset = new openerp.base.DataSetSearch(session, action.res_model, action.context || null); + dataset = new openerp.base.DataSetSearch(this, action.res_model, action.context || null); } else { - dataset = new openerp.base.DataSetStatic(session, action.res_model, {}, [action.res_id]); + dataset = new openerp.base.DataSetStatic(this, action.res_model, {}, [action.res_id]); if (action.context) { // TODO fme: should normalize all DataSets constructors to (session, model, context, ...) dataset.context = action.context; } } - this._super(session, element_id, dataset, action.views); + this._super(parent, element_id, dataset, action.views); this.action = action; this.flags = this.action.flags || {}; if (action.res_model == 'board.board' && action.views.length == 1 && action.views) { // Not elegant but allows to avoid flickering of SearchView#do_hide this.flags.search_view = this.flags.pager = this.flags.sidebar = this.flags.action_buttons = false; } - if (this.flags.sidebar) { - this.sidebar = new openerp.base.Sidebar(null, this); - } }, start: function() { var inital_view_loaded = this._super(); - // init sidebar - if (this.flags.sidebar) { - this.$element.find('.view-manager-main-sidebar').html(this.sidebar.render()); - this.sidebar.start(); - } - var search_defaults = {}; _.each(this.action.context, function (value, key) { var match = /^search_default_(.*)$/.exec(key); @@ -308,8 +334,7 @@ openerp.base.ViewManagerAction = openerp.base.ViewManager.extend({ } }, stop: function() { - // should be replaced by automatic destruction implemented in BaseWidget - this.sidebar.stop(); + // should be replaced by automatic destruction implemented in Widget this._super(); }, /** @@ -336,82 +361,101 @@ openerp.base.ViewManagerAction = openerp.base.ViewManager.extend({ } }); -openerp.base.Sidebar = openerp.base.BaseWidget.extend({ - template: "ViewManager.sidebar", - init: function(parent, view_manager) { - this._super(parent, view_manager.session); - this.view_manager = view_manager; - this.sections = []; - }, - set_toolbar: function(toolbar) { - this.sections = []; - var self = this; - _.each([["print", "Reports"], ["action", "Actions"], ["relate", "Links"]], function(type) { - if (toolbar[type[0]].length == 0) - return; - var section = {elements:toolbar[type[0]], label:type[1]}; - self.sections.push(section); - }); - this.do_refresh(true); +openerp.base.Sidebar = openerp.base.Widget.extend({ + init: function(parent, element_id) { + this._super(parent, element_id); + this.items = {}; + this.sections = {}; }, - do_refresh: function(new_view) { - var view = this.view_manager.active_view; - var the_condition = this.sections.length > 0 && _.detect(this.sections, - function(x) {return x.elements.length > 0;}) != undefined - && (!new_view || view != 'list'); - - this.$element.toggleClass('open-sidebar', the_condition) - .toggleClass('closed-sidebar', !the_condition); - - this.$element.html(QWeb.render("ViewManager.sidebar.internal", { sidebar: this, view: view })); - + start: function() { var self = this; + this._super(this, arguments); + this.$element.html(QWeb.render('Sidebar')); this.$element.find(".toggle-sidebar").click(function(e) { - self.$element.toggleClass('open-sidebar closed-sidebar'); - e.stopPropagation(); - e.preventDefault(); + self.do_toggle(); }); - - this.$element.find("a.oe_sidebar_action_a").click(function(e) { - var $this = jQuery(this); - var index = $this.attr("data-index").split('-'); - 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(); + }, + add_toolbar: function(toolbar) { + var self = this; + _.each([['print', "Reports"], ['action', "Actions"], ['relate', "Links"]], function(type) { + var items = toolbar[type[0]]; + if (items.length) { + for (var i = 0; i < items.length; i++) { + items[i] = { + label: items[i]['name'], + action: items[i], + classname: 'oe_sidebar_' + type[0] + } + } + self.add_section(type[0], type[1], items); + } }); }, - start: function() { - this._super(); - this.do_refresh(false); - } -}); - -openerp.base.NullSidebar = openerp.base.generate_null_object_class(openerp.base.Sidebar); - -openerp.base.Export = openerp.base.Dialog.extend({ - dialog_title: "Export", - template: 'ExportDialog', - identifier_prefix: 'export_dialog', - init: function (session, model, domain) { - this._super(); + add_section: function(code, name, items) { + // For each section, we pass a name/label and optionally an array of items. + // If no items are passed, then the section will be created as a custom section + // returning back an element_id to be used by a custom controller. + // Else, the section is a standard section with items displayed as links. + // An item is a dictonary : { + // label: label to be displayed for the link, + // action: action to be launch when the link is clicked, + // callback: a function to be executed when the link is clicked, + // classname: optional dom class name for the line, + // title: optional title for the link + // } + // Note: The item should have one action or/and a callback + var self = this, + section_id = _.uniqueId(this.element_id + '_section_' + code + '_'); + if (items) { + for (var i = 0; i < items.length; i++) { + items[i].element_id = _.uniqueId(section_id + '_item_'); + this.items[items[i].element_id] = items[i]; + } + } + var $section = $(QWeb.render("Sidebar.section", { + section_id: section_id, + name: name, + classname: 'oe_sidebar_' + code, + items: items + })); + if (items) { + $section.find('a.oe_sidebar_action_a').click(function() { + var item = self.items[$(this).attr('id')]; + if (item.callback) { + item.callback(); + } + if (item.action) { + item.action.flags = item.action.flags || {}; + item.action.flags.new_window = true; + self.do_action(item.action); + } + return false; + }); + } + $section.appendTo(this.$element.find('div.sidebar-actions')); + this.sections[code] = $section; + return section_id; }, - start: function () { - this._super(); - this.$element.html(this.render()); + do_fold: function() { + this.$element.addClass('closed-sidebar').removeClass('open-sidebar'); }, - on_button_Export: function() { - console.log("Export") + do_unfold: function() { + this.$element.addClass('open-sidebar').removeClass('closed-sidebar'); }, - on_button_Cancel: function() { - this.$element.dialog("close"); + do_toggle: function() { + this.$element.toggleClass('open-sidebar closed-sidebar'); } }); -openerp.base.View = openerp.base.Controller.extend({ +openerp.base.View = openerp.base.Widget.extend({ + set_default_options: function(options) { + this.options = options || {}; + _.defaults(this.options, { + // All possible views options should be defaulted here + sidebar_id: null, + sidebar: true + }); + }, /** * Fetches and executes the action identified by ``action_data``. * @@ -423,11 +467,15 @@ openerp.base.View = openerp.base.Controller.extend({ * @param {openerp.base.DataSet} dataset a dataset object used to communicate with the server * @param {openerp.base.ActionManager} action_manager object able to actually execute the action, if any is fetched * @param {Object} [record_id] the identifier of the object on which the action is to be applied - * @param {Function} on_no_action callback to execute if the action does not generate any result (no new action) + * @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action) */ - execute_action: function (action_data, dataset, action_manager, record_id, on_no_action, on_closed) { + execute_action: function (action_data, dataset, action_manager, record_id, on_closed) { var self = this; + if (action_manager.current_dialog) { + on_closed = action_manager.current_dialog.close_callback; + } var handler = function (r) { + action_manager.close_dialog(); var action = r.result; if (action && action.constructor == Object) { action.context = action.context || {}; @@ -437,29 +485,16 @@ openerp.base.View = openerp.base.Controller.extend({ active_model: dataset.model }); action.flags = { - sidebar : false, - search_view : false, - views_switcher : false, - action_buttons : false, - pager : false + new_window: true }; action_manager.do_action(action, on_closed); - if (self.view_manager.is_dialog && action.type != 'ir.actions.act_window_close') { - handler({ - result : { type: 'ir.actions.act_window_close' } - }); - } - } else { - on_no_action(action); + } else if (on_closed) { + on_closed(action); } }; - if (action_data.special) { - handler({ - result : { type: 'ir.actions.act_window_close', special: action_data.special } - }); - } else { - var context = new openerp.base.CompoundContext(dataset.context, action_data.context || {}); + if (!action_data.special) { + var context = new openerp.base.CompoundContext(dataset.get_context(), action_data.context || {}); switch(action_data.type) { case 'object': return dataset.call_button(action_data.name, [[record_id], context], handler); @@ -468,17 +503,73 @@ openerp.base.View = openerp.base.Controller.extend({ default: return dataset.exec_workflow(record_id, action_data.name, handler); } + } else { + action_manager.close_dialog(); } }, /** * Directly set a view to use instead of calling fields_view_get. This method must * be called before start(). When an embedded view is set, underlying implementations * of openerp.base.View must use the provided view instead of any other one. - * + * * @param embedded_view A view. */ set_embedded_view: function(embedded_view) { this.embedded_view = embedded_view; + }, + set_common_sidebar_sections: function(sidebar) { + sidebar.add_section('customize', "Customize", [ + { + label: "Manage Views", + callback: this.on_sidebar_manage_view, + title: "Manage views of the current object" + }, { + label: "Edit Workflow", + callback: this.on_sidebar_edit_workflow, + title: "Manage views of the current object", + classname: 'oe_hide oe_sidebar_edit_workflow' + }, { + label: "Customize Object", + callback: this.on_sidebar_customize_object, + title: "Manage views of the current object" + } + ]); + sidebar.add_section('other', "Other Options", [ + { + label: "Import", + callback: this.on_sidebar_import + }, { + label: "Export", + callback: this.on_sidebar_export + }, { + label: "Translate", + callback: this.on_sidebar_translate, + classname: 'oe_hide oe_sidebar_translate' + }, { + label: "View Log", + callback: this.on_sidebar_view_log, + classname: 'oe_hide oe_sidebar_view_log' + } + ]); + }, + on_sidebar_manage_view: function() { + console.log('Todo'); + }, + on_sidebar_edit_workflow: function() { + console.log('Todo'); + }, + on_sidebar_customize_object: function() { + console.log('Todo'); + }, + on_sidebar_import: function() { + }, + on_sidebar_export: function() { + var export_view = new openerp.base.DataExport(this, this.dataset); + export_view.start(false); + }, + on_sidebar_translate: function() { + }, + on_sidebar_view_log: function() { } }); @@ -487,12 +578,6 @@ openerp.base.View = openerp.base.Controller.extend({ */ openerp.base.views = new openerp.base.Registry(); -openerp.base.ProcessView = openerp.base.Controller.extend({ -}); - -openerp.base.HelpView = openerp.base.Controller.extend({ -}); - openerp.base.json_node_to_xml = function(node, single_quote, indent) { // For debugging purpose, this function will convert a json node back to xml // Maybe usefull for xml view editor diff --git a/addons/base/static/src/xml/base.xml b/addons/base/static/src/xml/base.xml index 9bef00147d3..c51284ad63a 100644 --- a/addons/base/static/src/xml/base.xml +++ b/addons/base/static/src/xml/base.xml @@ -62,7 +62,7 @@ <td><label for="db">Database:</label></td> <td> <t t-if="!db_list"> - <input type="text" name="db" value="trunk" autofocus="true"/> + <input type="text" name="db" t-att-value="selected_db || ''" autofocus="true"/> </t> <t t-if="db_list"> <select name="db"> @@ -87,18 +87,7 @@ <tr> <td><label for="password">Password:</label></td> <td><input type="password" name="password" - t-att-value="selected_password || ''"/></td> - </tr> - <tr t-if="has_local_storage"> - <td><label for="remember">Remember password:</label></td> - <td class="oe_remember"> - <t t-if="remember"> - <input type="checkbox" name="remember" checked="yes"/> - </t> - <t t-if="!remember"> - <input type="checkbox" name="remember"/> - </t> - </td> + t-att-value="selected_password || ''"/></td> </tr> <tr> <td></td> @@ -114,7 +103,7 @@ <div class="oe_login_right_pane"> <p>We think that daily job activities can be more intuitive, efficient, automated, .. and even fun.</p> <h3>OpenERP's vision to be:</h3> - + <table cellpadding="0" cellspacing="0" width="100%" style="border:none;"> <tbody> <tr> @@ -146,7 +135,7 @@ </tr> </tbody> </table> - + </div> </t> <t t-name="Header"> @@ -154,8 +143,8 @@ <div class="company_logo" /> </a> <h1 class="header_title" t-if="session.session_is_valid()"> - <span class="company">$company</span> - (<span class="database">$database</span>)<br/> - <small class="username">$username session_id: <t t-esc="session.session_id"/></small> + <span class="database"><t t-esc="session.db"/></span> - <t t-esc="session.login"/> <br/> + <small class="username">rpc_session_id: <t t-esc="session.session_id"/></small> </h1> <div class="header_corner"> <ul class="block" t-if="session.session_is_valid()"> @@ -243,42 +232,66 @@ </t> </td> <td class="view-manager-main-sidebar" height="100%"> + <t t-foreach="views" t-as="view"> + <div t-attf-id="#{prefix}_sidebar_#{view.view_type}" class="sidebar-main-div closed-sidebar" style="display: none"/> + </t> </td> </tr> </table> </t> -<table t-name="ListView"> +<t t-name="Sidebar"> + <a class="toggle-sidebar"></a> + <div class="sidebar-content"> + <div class="sidebar-actions"> + </div> + </div> +</t> +<t t-name="Sidebar.section"> + <h2><t t-esc="name"/></h2> + <div t-att-id="section_id" t-att-class="classname"> + <ul t-if="items"> + <li t-foreach="items" t-as="item" t-att-class="item.classname"> + <a class="oe_sidebar_action_a" t-att-id="item.element_id" t-att-title="item.title" href="#"> + <t t-esc="item.label"/> + </a> + </li> + </ul> + </div> +</t> +<table t-name="ListView" class="oe-listview-content"> <t t-set="columns_count" t-value="visible_columns.length + (options.selectable ? 1 : 0) + (options.deletable ? 1 : 0)"/> - <t t-set="actions_span" t-value="Math.floor((options.deletable or options.addable) ? columns_count/2 : 0)"/> <thead class="ui-widget-header"> - <tr> - <th t-if="actions_span" t-att-colspan="actions_span" - class="oe-actions"> - <t t-if="flags.action_buttons !== false"> - <button type="button" id="oe-list-add" - t-if="options.addable"> - <t t-esc="options.addable"/> - </button> - <button type="button" id="oe-list-delete" - t-if="options.selectable and options.deletable"> - Delete - </button> - </t> - </th> - <th t-att-colspan="columns_count - actions_span" - class="oe-list-pager"> - <t t-if="flags.pager !== false"> - <button type="button" data-pager-action="first">First</button> - <button type="button" data-pager-action="previous" - ><<</button> + <tr t-if="options.action_buttons !== false or options.pager !== false"> + <th t-att-colspan="columns_count"> + <table> + <tr> + <td t-if="options.action_buttons !== false" class="oe-actions"> + <button type="button" class="oe-list-add" + t-if="options.addable"> + <t t-esc="options.addable"/> + </button> + <button type="button" class="oe-list-delete" + t-if="options.selectable and options.deletable"> + Delete + </button> + </td> + <th t-if="options.pager !== false" class="oe-list-pager"> + <button type="button" disabled="disabled" + data-pager-action="first">First</button> + <button type="button" disabled="disabled" + data-pager-action="previous" + ><</button> - <span class="oe-pager-first">1</span> - to <span class="oe-pager-last">1</span> - of <span class="oe-pager-total">1</span> + <span class="oe-pager-state"> + </span> - <button type="button" data-pager-action="next">>></button> - <button type="button" data-pager-action="last">Last</button> - </t> + <button type="button" disabled="disabled" + data-pager-action="next">></button> + <button type="button" disabled="disabled" + data-pager-action="last">Last</button> + </th> + </tr> + </table> </th> </tr> <tr t-if="options.header"> @@ -287,24 +300,22 @@ <t t-esc="column.string"/> </th> </t> - <th t-if="options.selectable"/> + <th t-if="options.selectable" width="1"/> <t t-foreach="columns" t-as="column"> <th t-if="!column.meta and column.invisible !== '1'" t-att-data-id="column.id" t-att-class="((options.sortable and column.tag !== 'button') ? 'oe-sortable' : null)"> - <t t-if="column.tag !== 'button'"> - <t t-esc="column.string"/> - <span t-att-class="(fields_view.sorted.field === column.id) ? ('ui-icon' + (fields_view.sorted.reversed ? ' ui-icon-triangle-1-n' : ' ui-icon-triangle-1-s')) : ''"/> - </t> + <t t-if="column.tag !== 'button'" + ><t t-esc="column.string"/></t> </th> </t> - <th t-if="options.deletable"/> + <th t-if="options.deletable" width="1"/> </tr> </thead> <tfoot class="ui-widget-header"> <tr> <td t-if="options.selectable"/> <td t-foreach="aggregate_columns" t-as="column" class="oe-list-footer oe-number" - t-att-data-field="column.field" t-att-title="column.label"> + t-att-data-field="column.id" t-att-title="column.label"> </td> <td t-if="options.deletable"/> </tr> @@ -322,10 +333,10 @@ t-att-data-index="row_index"> <t t-foreach="columns" t-as="column"> <td t-if="column.meta"> - + </td> </t> - <th t-if="options.selectable" class="oe-record-selector"> + <th t-if="options.selectable" class="oe-record-selector" width="1"> <input type="checkbox"/> </th> <t t-foreach="columns" t-as="column"> @@ -333,23 +344,10 @@ <td t-if="!column.meta and column.invisible !== '1'" t-att-title="column.help" t-att-class="'oe-field-cell' + (align ? ' oe-number' : '')" t-att-data-field="column.id"> - <t t-set="attrs" t-value="column.attrs_for(row.data)"/> - <t t-if="!attrs.invisible"> - <t t-set="is_button" t-value="column.tag === 'button'"/> - <!-- TODO: get correct widget from form --> - <t t-if="!is_button and row['data'][column.id].value !== false"> - <t t-set="value" t-value="row['data'][column.id].value"/> - <t t-esc="value instanceof Array ? value[1] : value"/> - </t> - <button type="button" t-att-title="column.string" - t-if="is_button"> - <img t-att-src="'/base/static/src/img/icons/' + column.icon + '.png'" - t-att-alt="column.string"/> - </button> - </t> + <t t-raw="render_cell(row.data, column)"/> </td> </t> - <td t-if="options.deletable" class='oe-record-delete'> + <td t-if="options.deletable" class='oe-record-delete' width="1"> <button type="button" name="delete">♻</button> </td> </tr> @@ -357,9 +355,9 @@ <t t-raw="frame.render()"/> </t> <t t-name="FormView"> - <h2 class="oe_view_title"><t t-esc="view.fields_view.arch.attrs.string"/></h2> <div class="oe_form_header" t-att-id="view.element_id + '_header'"> - <div class="oe_form_buttons" t-if="view.flags.action_buttons !== false"> + <h2 class="oe_view_title"><t t-esc="view.fields_view.arch.attrs.string"/> <button class="oe_get_xml_view">xml</button></h2> + <div class="oe_form_buttons" t-if="view.options.action_buttons !== false"> <!--<button type="button" class="oe_form_button_save"> <span class="oe_form_on_update">Save</span> <span class="oe_form_on_create">Create</span> @@ -371,7 +369,7 @@ <!--<button type="button" class="oe_form_button_cancel">Cancel</button>--> <button type="button" class="oe_form_button_new">New</button> </div> - <div class="oe_form_pager" t-if="view.flags.pager !== false"> + <div class="oe_form_pager" t-if="view.options.pager !== false"> <button type="button" data-pager-action="first">First</button> <button type="button" data-pager-action="previous"><<</button> @@ -384,21 +382,20 @@ <t t-raw="frame.render()"/> </t> <t t-name="FormView.sidebar.attachments"> - <h2>Attachments</h2> <div class="oe-sidebar-attachments-toolbar"> <div class="oe-binary-file-set" style="float: right"> <form class="oe-binary-form" t-attf-target="#{element_id}_iframe" method="post" enctype="multipart/form-data" action="/base/binary/upload_attachment"> <input type="hidden" name="session_id" t-att-value="session.session_id"/> <input type="hidden" name="callback" t-attf-value="#{element_id}_iframe"/> - <input type="hidden" name="model" t-att-value="dataset.model"/> - <input type="hidden" name="id" t-att-value="datarecord.id"/> + <input type="hidden" name="model" t-att-value="view.dataset.model"/> + <input type="hidden" name="id" t-att-value="view.datarecord.id"/> <button class="button" type="button"> <img src="/base/static/src/img/throbber.gif" width="16" height="16" style="display: none"/> <span>Add</span> </button> <input type="file" class="oe-binary-file" name="ufile" title="Add attachment" - t-att-onclick="datarecord.id ? null : 'alert(\'No record selected ! You can only attach to existing record.\'); return false;'"/> + t-att-onclick="view.datarecord.id ? null : 'alert(\'No record selected ! You can only attach to existing record.\'); return false;'"/> </form> <iframe t-attf-id="#{element_id}_iframe" t-attf-name="#{element_id}_iframe" style="display: none"> </iframe> </div> @@ -408,7 +405,7 @@ <li t-foreach="attachments" t-as="attachment"> <t t-if="attachment.type == 'binary'" t-set="attachment.url" t-value="'/base/binary/saveas?session_id=' + session.session_id + '&model=ir.attachment&id=' + attachment.id - + '&field=datas' + '&fieldname=name' + '&t=' + (new Date().getTime())"/> + + '&field=datas&fieldname=name&t=' + (new Date().getTime())"/> <a class="oe-sidebar-attachments-link" t-att-href="attachment.url" target="_blank"> <t t-esc="attachment.name"/> </a> @@ -441,18 +438,21 @@ </t> <t t-name="WidgetNotebook"> <ul> - <li t-foreach="widget.pages" t-as="page"> - <a t-att-href="'#' + widget.element_id + '-' + page_index"> + <li t-foreach="widget.pages" t-as="page" t-att-id="page.element_tab_id"> + <a t-att-href="'#' + page.element_id"> <t t-esc="page.string"/> </a> </li> </ul> <t t-foreach="widget.pages" t-as="page"> - <div t-att-id="widget.element_id + '-' + page_index"> - <t t-raw="page.render()"/> - </div> + <t t-raw="page.render()"/> </t> </t> +<t t-name="WidgetNotebookPage"> + <div t-att-id="widget.element_id"> + <t t-call="WidgetFrame"/> + </div> +</t> <t t-name="WidgetSeparator"> <div t-att-class="'separator ' + (widget.node.attrs.orientation || 'horizontal')"> <t t-esc="widget.string"/> @@ -526,9 +526,9 @@ t-att-id="widget.element_id + '_field'" t-att-class="'field_' + widget.type" style="width: 100%"> - <t t-foreach="widget.field.selection" t-as="options"> - <option t-att-value="options[0]"> - <t t-esc="options[1]"/> + <t t-foreach="widget.field_index" t-as="options"> + <option t-att-value="options.ikey"> + <t t-esc="options.label"/> </option> </t> </select> @@ -541,9 +541,9 @@ <span class="oe-m2o-cm-button" t-att-id="widget.name + '_open'"> <img src="/base/static/src/img/icons/gtk-index.png"/></span> <div t-att-id="widget.cm_id" class="contextMenu" style="display:none"><ul> - <li t-att-id="widget.cm_id + '_search'">Search</li> - <li t-att-id="widget.cm_id + '_create'">Create New</li> - <li t-att-id="widget.cm_id + '_open'" style="color:grey">Open</li> + <li t-att-id="widget.cm_id + '_open'" style="color:grey">Open...</li> + <li t-att-id="widget.cm_id + '_create'">Create...</li> + <li t-att-id="widget.cm_id + '_search'">Search...</li> </ul></div> </div> </t> @@ -685,14 +685,28 @@ <input type="reset" value="Clear"/> <button class="oe_search-view-custom-filter-btn"><span>Advanced Filter</span></button> <select class="oe_search-view-filters-management"> - <option value="_filters">-- Filters --</option> - <option value="_actions">-- Actions --</option> - <option value="save_filter">Save Filter</option> - <option value="manage_filters">Manage Filters</option> </select> </div> </form> </t> +<t t-name="SearchView.managed-filters"> + <option value="_filters">-- Filters --</option> + <t t-set="i" t-value="0"/> + <t t-foreach="filters" t-as="filter"> + <option t-att-value="'get:' + i"><t t-esc="filter.name"/></option> + <t t-set="i" t-value="i+1"/> + </t> + <option value="_actions">-- Actions --</option> + <option value="save_filter">Save Filter</option> + <option value="manage_filters">Manage Filters</option> +</t> +<t t-name="SearchView.managed-filters.add"> + <div> + <p>Filter Name:</p> + <input type="text"/> + <p>(Any existing filter with the same name will be replaced)</p> + </div> +</t> <t t-name="SearchView.render_lines"> <table class="oe-searchview-render-line" border="0" cellspacing="0" cellpadding="0" t-foreach="lines" t-as="line"> @@ -803,11 +817,11 @@ <t t-name="SearchView.extended_search.group"> <div t-att-id="element_id" class="searchview_extended_group"> <select class="searchview_extended_group_choice"> - <option value="all">All the following conditions must match</option> <option value="any">Any of the following conditions must match</option> + <option value="all">All the following conditions must match</option> <option value="none">None of the following conditions must match</option> </select> - <a class="searchview_extended_delete_group" + <a class="searchview_extended_delete_group" href="javascript:void(0)"><span></span></a> <div class="searchview_extended_propositions_list"> </div> @@ -829,7 +843,7 @@ <select class="searchview_extended_prop_op"/> <span class="searchview_extended_prop_value"/> <a class="searchview_extended_delete_prop" - href="javascript:void(0)"><span></span></a> + href="javascript:void(0)"><span> </span></a> </div> </t> <t t-name="SearchView.extended_search.proposition.char"> @@ -856,29 +870,6 @@ </t> </select> </t> -<t t-name="ViewManager.sidebar"> - <div t-att-id="element_id" class="sidebar-main-div closed-sidebar"> - - </div> -</t> -<t t-name="ViewManager.sidebar.internal"> - <a class="toggle-sidebar"></a> - <div class="sidebar-content"> - <div class="sidebar-attachments" t-if="view == 'form'"> </div> - <div class="sidebar-actions"> - <t t-foreach="sidebar.sections" t-as="section" t-if="section.elements.length"> - <h2><t t-esc="section.label"/></h2> - <ul> - <li t-foreach="section.elements" t-as="element"> - <a class="oe_sidebar_action_a" t-attf-data-index="#{section_index}-#{element_index}" href="#"> - <t t-esc="element.name"/> - </a> - </li> - </ul> - </t> - </div> - </div> -</t> <t t-name="DialogWarning"> <table cellspacing="0" cellpadding="0" border="0" class="oe-dialog-warning"> <tr> @@ -886,7 +877,9 @@ <td> <p> <t t-js="d"> - d.html_error = context.engine.tools.html_escape(d.error.data.fault_code).replace(/\n/g, '<br/>'); + var message = d.message ? d.message : d.error.data.fault_code; + d.html_error = context.engine.tools.html_escape(message) + .replace(/\n/g, '<br/>'); </t> <t t-raw="html_error"/> </p> @@ -901,9 +894,19 @@ </t> <t t-name="SelectCreatePopup"> <div t-att-id="element_id"> - <div t-att-id="element_id + '_search'"></div> - <div t-att-id="element_id + '_view_list'"></div> - <div t-att-id="element_id + '_view_form'"></div> + <table style="width:100%"> + <tr style="width:100%"> + <td style="width:100%"> + <div t-att-id="element_id + '_search'" style="width:100%"></div> + </td> + </tr> + <tr style="width:100%"> + <td style="width:100%"> + <div t-att-id="element_id + '_view_list'" style="width:100%"></div> + </td> + </tr> + </table> + <div t-att-id="element_id + '_view_form'" style="width:100%"></div> </div> </t> <t t-name="SelectCreatePopup.search.buttons"> @@ -914,7 +917,15 @@ <button type="button" class="oe_selectcreatepopup-form-save">Save</button> <button type="button" class="oe_selectcreatepopup-form-close">Close</button> </t> - +<t t-name="FormOpenPopup"> + <div t-att-id="element_id"> + <div t-att-id="element_id + '_view_form'" style="width:100%"></div> + </div> +</t> +<t t-name="FormOpenPopup.form.buttons"> + <button type="button" class="oe_formopenpopup-form-save">Save</button> + <button type="button" class="oe_formopenpopup-form-close">Close</button> +</t> <t t-name="ListView.row.frame" t-extend="WidgetFrame"> <t t-jquery="tr"> $(document.createElement('t')) @@ -933,4 +944,145 @@ .unwrap(); </t> </t> + +<t t-name="ExportView"> + <a id="exportview" href="javascript: void(0)" style="text-decoration: none;color: #3D3D3D;">Export</a> +</t> + +<t t-name="ExportTreeView"> + <table class="view" style="background-color: #F3F3F3;"> + <tr> + <td align="left"> + This wizard will export all data that matches the current search criteria to a CSV file. + You can export all data or only the fields that can be reimported after modification. + </td> + </tr> + <tr> + <td> + <table> + <tr> + <td class="label"><label>Export Type:</label></td> + <td> + <select id="import_compat" name="import_compat"> + <option value="1">Import Compatible Export</option> + <option value="0">Export all Data</option> + </select> + </td> + <td class="label"><label>Export Format</label></td> + <td> + <select id="export_format" name="export_format"> + <option value="csv">CSV</option> + <option value="xls">Excel</option> + </select> + </td> + + </tr> + </table> + </td> + </tr> + + <tr> + <td> + <table class="oe_export_fields_selector_export"> + <tr> + <th class="oe_view_title" valign="bottom">Available fields</th> + <th class="oe_view_title"></th> + <th class="oe_view_title">Fields to export + <a style="color: blue; text-decoration: none;" href="#" id="export_new_list">Save fields list</a> + <div id="savenewlist"></div> + <div id="ExistsExportList"></div> + </th> + </tr> + <tr> + <td class="oe_export_fields_selector_left"> + <div id="left_field_panel"> + </div> + </td> + <td> + <table class="oe_export_fields_selector_center"> + <tr> + <td align="center"> + <button id="add_field" class="oe_export_button_export">Add</button> + </td> + </tr> + <tr> + <td align="center"> + <button id="remove_field" class="oe_export_button_export">Remove</button> + </td> + </tr> + <tr> + <td align="center"> + <button id="remove_all_field" class="oe_export_button_export">Remove All</button> + </td> + </tr> + </table> + </td> + <td class="oe_export_fields_selector_right"> + <select name="fields_list" id="fields_list" multiple="multiple"></select> + </td> + </tr> + </table> + </td> + </tr> + </table> +</t> + +<t t-name="ExportTreeView-Secondary"> + <table id="field-tree-structure" class="oe_export_fields_selector_export" cellspacing="0" cellpadding="0"> + <tr><th class="oe_export_tree_header"> Name </th></tr> + <t t-call="ExportTreeView-Secondary.children"/> + </table> +</t> +<t t-name="ExportTreeView-Secondary.children"> + <t t-foreach="fields" t-as="field" > + <tr t-att-id="'treerow-' + field.id" class="oe_export_row"> + <td> + <table class="tree_grid" border="0"> + <tr class="oe_export_row"> + <t t-foreach="(field.id).split('/')" t-as="level" > + <t t-if="(field.id).split('/')[0] != level"> + <td width="18">&nbsp;</td> + </t> + </t> + <td valign="top" align="left" style="cursor: pointer;" width="18"> + <t t-if="(field.children).length >= 1"> + <t t-if="(field.id).split('/').length != 3"> + <img t-att-id="'parentimg-' + field.id" src="/base/static/src/img/expand.gif" width="16" height="16" border="0"/> + </t> + </t> + </td> + <td id="tree-column" valign="middle" align="left" style="cursor: pointer;"> + <a t-att-id="'export-' + field.id" t-att-string="field.string" href="javascript: void(0);" style="text-decoration: none;"> + <t t-esc="field.string"/> + </a> + </td> + </tr> + </table> + </td> + </tr> + </t> +</t> + +<t t-name="ExportNewList"> + <tr> + <th><label>Save as:</label></th> + <td><input size="10" type="text" id="savelist_name"/></td> + <td><button class="oe_export_button_export" id="add_export_list">Ok</button></td> + </tr> +</t> + +<t t-name="Exists.ExportList"> + <tr><th align="right"><label >Saved exports:</label></th></tr> + <tr align="left"> + <td> + <select id="saved_export_list" style="width: 100%;"> + <option></option> + <t t-foreach="existing_exports" t-as="export"> + <option t-att-value="export.id"><t t-esc="export.name"/></option> + </t> + </select> + </td> + <td><button class="oe_export_button_export" id="delete_export_list">Delete</button></td> + </tr> +</t> </templates> |
