diff --git a/addons/web/__init__.py b/addons/web/__init__.py index 681fd6b42b3..dba47c9f45e 100644 --- a/addons/web/__init__.py +++ b/addons/web/__init__.py @@ -10,15 +10,14 @@ class Options(object): pass def wsgi_postload(): - import openerp.wsgi - import openerp.tools + import openerp import os import tempfile _logger.info("embedded mode") o = Options() o.dbfilter = openerp.tools.config['dbfilter'] o.session_storage = os.path.join(tempfile.gettempdir(), "oe-sessions") - o.addons_path = os.path.dirname(os.path.dirname(__file__)) + o.addons_path = openerp.modules.module.ad_paths o.serve_static = True o.backend = 'local' diff --git a/addons/web/__openerp__.py b/addons/web/__openerp__.py index 74633fa4969..567642d3429 100644 --- a/addons/web/__openerp__.py +++ b/addons/web/__openerp__.py @@ -2,6 +2,8 @@ "name" : "web", "depends" : [], 'active': True, + 'post_load' : 'wsgi_postload', + 'web_preload' : True, 'js' : [ "static/lib/datejs/globalization/en-US.js", "static/lib/datejs/core.js", @@ -19,6 +21,7 @@ "static/lib/jquery.ui/js/jquery-ui-1.8.9.custom.min.js", "static/lib/jquery.ui/js/jquery-ui-timepicker-addon.js", "static/lib/jquery.ui.notify/js/jquery.notify.js", + "static/lib/jquery.deferred-queue/jquery.deferred-queue.js", "static/lib/json/json2.js", "static/lib/qweb/qweb2.js", "static/lib/underscore/underscore.js", @@ -32,6 +35,7 @@ "static/src/js/views.js", "static/src/js/data.js", "static/src/js/data_export.js", + "static/src/js/data_import.js", "static/src/js/search.js", "static/src/js/view_form.js", "static/src/js/view_list.js", @@ -44,6 +48,6 @@ "static/lib/jquery.ui.notify/css/ui.notify.css", "static/src/css/base.css", "static/src/css/data_export.css", + "static/src/css/data_import.css", ], - 'post_load' : 'wsgi_postload', } diff --git a/addons/web/common/dispatch.py b/addons/web/common/dispatch.py index b0ab780cff0..7a293c8228b 100644 --- a/addons/web/common/dispatch.py +++ b/addons/web/common/dispatch.py @@ -33,7 +33,6 @@ _logger = logging.getLogger(__name__) # Globals (wont move into a pool) #----------------------------------------------------------- -applicationsession = {} addons_module = {} addons_manifest = {} controllers_class = {} @@ -52,10 +51,6 @@ class WebRequest(object): :type request: :class:`werkzeug.wrappers.BaseRequest` :param config: configuration object - .. attribute:: applicationsession - - an application-wide :class:`~collections.Mapping` - .. attribute:: httprequest the original :class:`werkzeug.wrappers.Request` object provided to the @@ -94,7 +89,6 @@ class WebRequest(object): ``bool``, indicates whether the debug mode is active on the client """ def __init__(self, request, config): - self.applicationsession = applicationsession self.httprequest = request self.httpresponse = None self.httpsession = request.session @@ -381,20 +375,21 @@ class Root(object): static URLs to the corresponding directories """ statics = {} - addons_path = self.config.addons_path - if addons_path not in sys.path: - sys.path.insert(0, addons_path) - for module in os.listdir(addons_path): - if module not in addons_module: - manifest_path = os.path.join(addons_path, module, '__openerp__.py') - path_static = os.path.join(addons_path, module, 'static') - if os.path.isfile(manifest_path) and os.path.isdir(path_static): - manifest = ast.literal_eval(open(manifest_path).read()) - _logger.info("Loading %s", module) - m = __import__(module) - addons_module[module] = m - addons_manifest[module] = manifest - statics['/%s/static' % module] = path_static + for addons_path in self.config.addons_path: + if addons_path not in sys.path: + sys.path.insert(0, addons_path) + for module in os.listdir(addons_path): + if module not in addons_module: + manifest_path = os.path.join(addons_path, module, '__openerp__.py') + path_static = os.path.join(addons_path, module, 'static') + if os.path.isfile(manifest_path) and os.path.isdir(path_static): + manifest = ast.literal_eval(open(manifest_path).read()) + manifest['addons_path'] = addons_path + _logger.info("Loading %s", module) + m = __import__(module) + addons_module[module] = m + addons_manifest[module] = manifest + statics['/%s/static' % module] = path_static for k, v in controllers_class.items(): if k not in controllers_object: o = v() diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 96db9c6fc3c..ead27b2941a 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -68,15 +68,30 @@ class Xml2Json: # OpenERP Web web Controllers #---------------------------------------------------------- -def manifest_glob(addons_path, addons, key): +def manifest_preload(): + modules = [k for k,v in openerpweb.addons_manifest.items() if v.get('web_preload')] + return modules + +def manifest_addons(addons): + if addons==None: + addons = manifest_preload() + else: + addons = addons.split(',') + return addons + +def manifest_glob(addons, key): + addons = manifest_addons(addons) files = [] for addon in addons: - globlist = openerpweb.addons_manifest.get(addon, {}).get(key, []) + manifest = openerpweb.addons_manifest.get(addon, {}) + addons_path = manifest['addons_path'] + globlist = manifest.get(key, []) for pattern in globlist: for path in glob.glob(os.path.join(addons_path, addon, pattern)): files.append(path[len(addons_path):]) return files +# TODO change into concat_file(addons,key) taking care of addons_path def concat_files(addons_path, file_list): """ Concatenate file content return (concat,timestamp) @@ -104,7 +119,7 @@ home_template = textwrap.dedent(""" %(javascript)s '%i for i in jslist]) # css tags csslist = ['/web/webclient/css'] if req.debug: - csslist = [i + '?debug=' + str(time.time()) for i in manifest_glob(req.config.addons_path, ['web'], 'css')] + csslist = [i + '?debug=' + str(time.time()) for i in manifest_glob(None, 'css')] css = "\n ".join([''%i for i in csslist]) + r = home_template % { 'javascript': js, - 'css': css + 'css': css, + 'modules': simplejson.dumps(manifest_preload()), } return r @@ -166,20 +183,21 @@ class WebClient(openerpweb.Controller): "grouping", "decimal_point", "thousands_sep"]) else: lang_obj = None - + if lang.count("_") > 0: separator = "_" else: separator = "@" langs = lang.split(separator) langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)] - + transs = {} for addon_name in mods: transl = {"messages":[]} transs[addon_name] = transl for l in langs: - f_name = os.path.join(req.config.addons_path, addon_name, "po", l + ".po") + addons_path = openerpweb.addons_manifest[addon_name]['addons_path'] + f_name = os.path.join(addons_path, addon_name, "po", l + ".po") if not os.path.exists(f_name): continue try: @@ -240,7 +258,7 @@ class Database(openerpweb.Controller): password, db = operator.itemgetter( 'drop_pwd', 'drop_db')( dict(map(operator.itemgetter('name', 'value'), fields))) - + try: return req.session.proxy("db").drop(password, db) except xmlrpclib.Fault, e: @@ -299,6 +317,7 @@ class Session(openerpweb.Controller): "context": ctx, "db": req.session._db } + @openerpweb.jsonrequest def get_session_info(self, req): req.session.assert_valid(force=True) @@ -307,6 +326,7 @@ class Session(openerpweb.Controller): "context": req.session.get_context() if req.session._uid else False, "db": req.session._db } + @openerpweb.jsonrequest def change_password (self,req,fields): old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')( @@ -322,6 +342,7 @@ class Session(openerpweb.Controller): except: return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'} return {'error': 'Error, password not changed !', 'title': 'Change Password'} + @openerpweb.jsonrequest def sc_list(self, req): return req.session.model('ir.ui.view_sc').get_sc( @@ -336,13 +357,14 @@ class Session(openerpweb.Controller): } except Exception, e: return {"error": e, "title": "Languages"} - + @openerpweb.jsonrequest def modules(self, req): # TODO query server for installed web modules mods = [] for name, manifest in openerpweb.addons_manifest.items(): - if name != 'web' and manifest.get('active', True): + # TODO replace by ir.module.module installed web + if not manifest.get('web_preload') and manifest.get('active', True): mods.append(name) return mods @@ -715,7 +737,7 @@ class DataSet(openerpweb.Controller): args[domain_id] = d if context_id and len(args) - 1 >= context_id: args[context_id] = c - + for i in xrange(len(args)): if isinstance(args[i], web.common.nonliterals.BaseContext): args[i] = req.session.eval_context(args[i]) @@ -963,7 +985,7 @@ class SearchView(View): if field.get('context'): field["context"] = self.parse_domain(field["context"], req.session) return {'fields': fields} - + @openerpweb.jsonrequest def get_filters(self, req, model): Model = req.session.model("ir.filters") @@ -972,7 +994,7 @@ class SearchView(View): filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session)) filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session)) return filters - + @openerpweb.jsonrequest def save_filter(self, req, model, name, context_to_save, domain): Model = req.session.model("ir.filters") @@ -1382,7 +1404,7 @@ class Reports(View): report_data['form'] = action['datas']['form'] if 'ids' in action['datas']: report_ids = action['datas']['ids'] - + report_id = report_srv.report( req.session._db, req.session._uid, req.session._password, action["report_name"], report_ids, @@ -1394,6 +1416,7 @@ class Reports(View): req.session._db, req.session._uid, req.session._password, report_id) if report_struct["state"]: break + time.sleep(self.POLLING_DELAY) report = base64.b64decode(report_struct['result']) @@ -1407,3 +1430,108 @@ class Reports(View): ('Content-Type', report_mimetype), ('Content-Length', len(report))], cookies={'fileToken': int(token)}) + + +class Import(View): + _cp_path = "/web/import" + + def fields_get(self, req, model): + Model = req.session.model(model) + fields = Model.fields_get(False, req.session.eval_context(req.context)) + return fields + + @openerpweb.httprequest + def detect_data(self, req, csvfile, csvsep, csvdel, csvcode, jsonp): + try: + data = list(csv.reader( + csvfile, quotechar=str(csvdel), delimiter=str(csvsep))) + except csv.Error, e: + csvfile.seek(0) + return '' % ( + jsonp, simplejson.dumps({'error': { + 'message': 'Error parsing CSV file: %s' % e, + # decodes each byte to a unicode character, which may or + # may not be printable, but decoding will succeed. + # Otherwise simplejson will try to decode the `str` using + # utf-8, which is very likely to blow up on characters out + # of the ascii range (in range [128, 256)) + 'preview': csvfile.read(200).decode('iso-8859-1')}})) + + try: + return '' % ( + jsonp, simplejson.dumps( + {'records': data[:10]}, encoding=csvcode)) + except UnicodeDecodeError: + return '' % ( + jsonp, simplejson.dumps({ + 'message': u"Failed to decode CSV file using encoding %s, " + u"try switching to a different encoding" % csvcode + })) + + @openerpweb.httprequest + def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp, + meta): + modle_obj = req.session.model(model) + skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')( + simplejson.loads(meta)) + + error = None + if not (csvdel and len(csvdel) == 1): + error = u"The CSV delimiter must be a single character" + + if not indices and fields: + error = u"You must select at least one field to import" + + if error: + return '' % ( + jsonp, simplejson.dumps({'error': {'message': error}})) + + # skip ignored records + data_record = itertools.islice( + csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)), + skip, None) + + # if only one index, itemgetter will return an atom rather than a tuple + if len(indices) == 1: mapper = lambda row: [row[indices[0]]] + else: mapper = operator.itemgetter(*indices) + + data = None + error = None + try: + # decode each data row + data = [ + [record.decode(csvcode) for record in row] + for row in itertools.imap(mapper, data_record) + # don't insert completely empty rows (can happen due to fields + # filtering in case of e.g. o2m content rows) + if any(row) + ] + except UnicodeDecodeError: + error = u"Failed to decode CSV file using encoding %s" % csvcode + except csv.Error, e: + error = u"Could not process CSV file: %s" % e + + # If the file contains nothing, + if not data: + error = u"File to import is empty" + if error: + return '' % ( + jsonp, simplejson.dumps({'error': {'message': error}})) + + try: + (code, record, message, _nope) = modle_obj.import_data( + fields, data, 'init', '', False, + req.session.eval_context(req.context)) + except xmlrpclib.Fault, e: + error = {"message": u"%s, %s" % (e.faultCode, e.faultString)} + return '' % ( + jsonp, simplejson.dumps({'error':error})) + + if code != -1: + return '' % ( + jsonp, simplejson.dumps({'success':True})) + + msg = u"Error during import: %s\n\nTrying to import record %r" % ( + message, record) + return '' % ( + jsonp, simplejson.dumps({'error': {'message':msg}})) diff --git a/addons/web/static/lib/jquery.deferred-queue/LICENSE b/addons/web/static/lib/jquery.deferred-queue/LICENSE new file mode 100644 index 00000000000..ee8cf44578c --- /dev/null +++ b/addons/web/static/lib/jquery.deferred-queue/LICENSE @@ -0,0 +1,21 @@ +Copyright 2011 Xavier Morel. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list + of conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY XAVIER MOREL ``AS IS'' AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/addons/web/static/lib/jquery.deferred-queue/README b/addons/web/static/lib/jquery.deferred-queue/README new file mode 100644 index 00000000000..71d980199c9 --- /dev/null +++ b/addons/web/static/lib/jquery.deferred-queue/README @@ -0,0 +1,59 @@ +.. -*- restructuredtext -*- + +In jQuery 1.5, jQuery has introduced a Deferred object in order to +better handle callbacks. + +Along with Deferred, it introduced ``jQuery.when`` which allows, among +other things, for multiplexing deferreds (waiting on multiple +deferreds at the same time). + +While this is very nice if all deferreds are available at the same +point, it doesn't really work if the resolution of a deferred can +generate more deferreds, or for collections of deferreds coming from +multiple sources (which may not be aware of one another). + +Deferred.queue tries to be a solution to this. It is based on the +principle of FIFO multiple-producers multiple-consumers tasks queues, +such as Python's `queue.Queue`_. Any code with a reference to the +queue can add a promise to the queue, and the queue (which is a +promise itself) will only be resolved once all promises within are +resolved. + +Quickstart +---------- + +Deferred.queue has a very simple life cycle: it is dormant when +created, it starts working as soon as promises get added to it (via +``.push``, or by providing them directly to its constructor), and as +soon as the last promise in the queue is resolved it resolves itself. + +If any promise in the queue fails, the queue itself will be rejected +(without waiting for further promises). + +Once a queue has been resolved or rejected, adding new promises to it +results in an error. + +API +--- + +``jQuery.Deferred.queue([promises...])`` + Creates a new deferred queue. Can be primed with a series of promise + objects. + +``.push(promise...)`` + Adds promises to the queue. Returns the queue itself (can be + chained). + + If the queue has already been resolved or rejected, raises an error. + +``.then([doneCallbacks][, failCallbacks])`` + Promise/A ``then`` method. + +``.done(doneCallbacks)`` + jQuery ``done`` extension to promise objects + +``.fail(failCallbacks)`` + jQuery ``fail`` extension to promise objects + +.. _queue.Queue: + http://docs.python.org/dev/library/queue.html diff --git a/addons/web/static/lib/jquery.deferred-queue/jquery.deferred-queue.js b/addons/web/static/lib/jquery.deferred-queue/jquery.deferred-queue.js new file mode 100644 index 00000000000..7ef83342f77 --- /dev/null +++ b/addons/web/static/lib/jquery.deferred-queue/jquery.deferred-queue.js @@ -0,0 +1,34 @@ +(function ($) { + "use strict"; + $.extend($.Deferred, { + queue: function () { + var queueDeferred = $.Deferred(); + var promises = 0; + + function resolve() { + if (--promises > 0) { + return; + } + setTimeout($.proxy(queueDeferred, 'resolve'), 0); + } + + var promise = $.extend(queueDeferred.promise(), { + push: function () { + if (this.isResolved() || this.isRejected()) { + throw new Error("Can not add promises to a resolved " + + "or rejected promise queue"); + } + + promises += 1; + $.when.apply(null, arguments).then( + resolve, $.proxy(queueDeferred, 'reject')); + return this; + } + }); + if (arguments.length) { + promise.push.apply(promise, arguments); + } + return promise; + } + }); +})(jQuery) diff --git a/addons/web/static/src/css/data_import.css b/addons/web/static/src/css/data_import.css new file mode 100644 index 00000000000..1fb3b22aa45 --- /dev/null +++ b/addons/web/static/src/css/data_import.css @@ -0,0 +1,30 @@ +.openerp .oe_import_grid { + border: none; + border-collapse: collapse; +} +.openerp .oe_import_grid-header .oe_import_grid-cell { + background: url(../img/gradientlinebg.gif) repeat-x #CCCCCC; + border-bottom: 1px solid #E3E3E3; + font-weight: bold; + text-align: left; +} +.openerp .oe_import_grid-row .oe_import_grid-cell { + border-bottom: 1px solid #E3E3E3; +} +.openerp .separator.horizontal { + font-weight: bold; + border-bottom-width: 1px; + margin: 6px 4px 6px 1px; + height: 20px; +} +.openerp .duplicate_fld{ + background-color:#FF6666; +} +.openerp .select_fld{ + background: none repeat scroll 0 0 white; +} +.openerp .ui-autocomplete { + max-height: 300px; + overflow-y: auto; + padding-right: 20px; +} diff --git a/addons/web/static/src/img/gradientlinebg.gif b/addons/web/static/src/img/gradientlinebg.gif new file mode 100644 index 00000000000..f40bd9c0f6a Binary files /dev/null and b/addons/web/static/src/img/gradientlinebg.gif differ diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js index b9d5180b5c4..f41185be0c6 100644 --- a/addons/web/static/src/js/boot.js +++ b/addons/web/static/src/js/boot.js @@ -19,9 +19,9 @@ /** * OpenERP instance constructor * - * @param {Boolean} skip_init if true, skips the built-in initialization + * @param {Array} modules list of modules to initialize */ - init: function(skip_init) { + init: function(modules) { var new_instance = { // links to the global openerp _openerp: openerp, @@ -35,8 +35,9 @@ web_mobile: {} }; openerp.sessions[new_instance._session_id] = new_instance; - if (!skip_init){ - openerp.web(new_instance); + modules = modules || ["web"]; + for(var i=0; i < modules.length; i++) { + openerp[modules[i]](new_instance); } return new_instance; } @@ -58,7 +59,7 @@ openerp.web = function(instance) { openerp.web.formats(instance); openerp.web.chrome(instance); openerp.web.data(instance); - var files = ["views","search","list","form","list_editable","web_mobile","view_tree","data_export"]; + var files = ["views","search","list","form","list_editable","web_mobile","view_tree","data_export","data_import"]; for(var i=0; i'}); @@ -467,10 +466,11 @@ openerp.web.Login = openerp.web.Widget.extend(/** @lends openerp.web.Login# */{ /** * @constructs openerp.web.Login * @extends openerp.web.Widget - * + * * @param parent * @param element_id */ + init: function(parent, element_id) { this._super(parent, element_id); this.has_local_storage = typeof(localStorage) != 'undefined'; @@ -496,7 +496,7 @@ openerp.web.Login = openerp.web.Widget.extend(/** @lends openerp.web.Login# */{ }, display: function() { var self = this; - + this.$element.html(QWeb.render("Login", this)); this.database = new openerp.web.Database( this, "oe_database", "oe_db_options"); @@ -574,7 +574,7 @@ openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# * /** * @constructs openerp.web.Header * @extends openerp.web.Widget - * + * * @param parent */ init: function(parent) { @@ -665,7 +665,7 @@ openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# * }); }); }, - + on_action: function(action) { }, on_preferences: function(){ @@ -713,7 +713,7 @@ openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# * action_manager.appendTo(this.dialog); action_manager.render(this.dialog); }, - + change_password :function() { var self = this; this.dialog = new openerp.web.Dialog(this,{ @@ -758,7 +758,7 @@ openerp.web.Menu = openerp.web.Widget.extend(/** @lends openerp.web.Menu# */{ /** * @constructs openerp.web.Menu * @extends openerp.web.Widget - * + * * @param parent * @param element_id * @param secondary_menu_id @@ -910,7 +910,7 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie /** * @constructs openerp.web.WebClient * @extends openerp.web.Widget - * + * * @param element_id */ init: function(element_id) { @@ -1000,7 +1000,7 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie self.execute_home_action(home_action[0], ds); }) }, - default_home: function () { + default_home: function () { }, /** * Bundles the execution of the home action diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index e5d323c8a96..00281628e75 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -608,15 +608,23 @@ openerp.web.BufferedDataSet = openerp.web.DataSetStatic.extend({ init: function() { this._super.apply(this, arguments); this.reset_ids([]); + this.last_default_get = {}; + }, + default_get: function(fields, callback) { + return this._super(fields).then(this.on_default_get).then(callback); + }, + on_default_get: function(res) { + this.last_default_get = res; }, create: function(data, callback, error_callback) { - var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data}; + var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data, + defaults: this.last_default_get}; this.to_create.push(cached); this.cache.push(cached); this.on_change(); - var to_return = $.Deferred().then(callback); - to_return.resolve({result: cached.id}); - return to_return.promise(); + var prom = $.Deferred().then(callback); + setTimeout(function() {prom.resolve({result: cached.id});}, 0); + return prom.promise(); }, write: function (id, data, options, callback) { var self = this; @@ -676,7 +684,8 @@ openerp.web.BufferedDataSet = openerp.web.DataSetStatic.extend({ var cached = _.detect(self.cache, function(x) {return x.id === id;}); var created = _.detect(self.to_create, function(x) {return x.id === id;}); if (created) { - _.each(fields, function(x) {if (cached.values[x] === undefined) cached.values[x] = false;}); + _.each(fields, function(x) {if (cached.values[x] === undefined) + cached.values[x] = created.defaults[x] || false;}); } else { if (!cached || !_.all(fields, function(x) {return cached.values[x] !== undefined})) to_get.push(id); @@ -716,6 +725,10 @@ openerp.web.BufferedDataSet = openerp.web.DataSetStatic.extend({ } }); openerp.web.ReadOnlyDataSetSearch = openerp.web.DataSetSearch.extend({ + default_get: function(fields, callback) { + return this._super(fields, callback).then(this.on_default_get); + }, + on_default_get: function(result) {}, create: function(data, callback, error_callback) { this.on_create(data); var to_return = $.Deferred().then(callback); diff --git a/addons/web/static/src/js/data_import.js b/addons/web/static/src/js/data_import.js new file mode 100644 index 00000000000..9c686846751 --- /dev/null +++ b/addons/web/static/src/js/data_import.js @@ -0,0 +1,289 @@ +openerp.web.data_import = function(openerp) { +var QWeb = openerp.web.qweb; +/** + * Safari does not deal well at all with raw JSON data being returned. As a + * result, we're going to cheat by using a pseudo-jsonp: instead of getting + * JSON data in the iframe, we're getting a ``script`` tag which consists of a + * function call and the returned data (the json dump). + * + * The function is an auto-generated name bound to ``window``, which calls + * back into the callback provided here. + * + * @param {Object} form the form element (DOM or jQuery) to use in the call + * @param {Object} attributes jquery.form attributes object + * @param {Function} callback function to call with the returned data + */ +function jsonp(form, attributes, callback) { + attributes = attributes || {}; + var options = {jsonp: _.uniqueId('import_callback_')}; + window[options.jsonp] = function () { + delete window[options.jsonp]; + callback.apply(null, arguments); + }; + if ('data' in attributes) { + _.extend(attributes.data, options); + } else { + _.extend(attributes, {data: options}); + } + $(form).ajaxSubmit(attributes); +} + +openerp.web.DataImport = openerp.web.Dialog.extend({ + template: 'ImportDataView', + dialog_title: "Import Data", + init: function(parent, dataset){ + var self = this; + this._super(parent, {}); + this.model = parent.model; + this.fields = []; + this.all_fields = []; + this.required_fields = null; + + var convert_fields = function (root, prefix) { + prefix = prefix || ''; + _(root.fields).each(function (f) { + self.all_fields.push(prefix + f.name); + if (f.fields) { + convert_fields(f, prefix + f.id + '/'); + } + }); + }; + this.ready = $.Deferred.queue().then(function () { + self.required_fields = _(self.fields).chain() + .filter(function (field) { return field.required; }) + .pluck('name') + .value(); + convert_fields(self); + self.all_fields.sort(); + }); + }, + start: function() { + var self = this; + this._super(); + this.open({ + modal: true, + width: '70%', + height: 'auto', + position: 'top', + buttons: [ + {text: "Close", click: function() { self.stop(); }}, + {text: "Import File", click: function() { self.do_import(); }, 'class': 'oe-dialog-import-button'} + ], + close: function(event, ui) { + self.stop(); + } + }); + this.toggle_import_button(false); + this.$element.find('#csvfile').change(this.on_autodetect_data); + this.$element.find('fieldset').change(this.on_autodetect_data); + this.$element.find('fieldset legend').click(function() { + $(this).next().toggle(); + }); + this.ready.push(new openerp.web.DataSet(this, this.model).call( + 'fields_get', [], function (fields) { + self.graft_fields(fields); + })); + }, + graft_fields: function (fields, parent, level) { + parent = parent || this; + level = level || 0; + + var self = this; + _(fields).each(function (field, field_name) { + var f = { + id: field_name, + name: field_name, + string: field.string, + required: field.required + }; + + switch (field.type) { + case 'many2many': + case 'many2one': + f.name += '/id'; + break; + case 'one2many': + f.name += '/id'; + f.fields = []; + // only fetch sub-fields to a depth of 2 levels + if (level < 2) { + self.ready.push(new openerp.web.DataSet(self, field.relation).call( + 'fields_get', [], function (fields) { + self.graft_fields(fields, f, level+1); + })); + } + break; + } + parent.fields.push(f); + }); + }, + toggle_import_button: function (newstate) { + this.$dialog.dialog('widget') + .find('.oe-dialog-import-button') + .button('option', 'disabled', !newstate); + }, + do_import: function() { + if(!this.$element.find('#csvfile').val()) { return; } + var lines_to_skip = parseInt(this.$element.find('#csv_skip').val(), 10); + var with_headers = this.$element.find('#file_has_headers').prop('checked'); + if (!lines_to_skip && with_headers) { + lines_to_skip = 1; + } + var indices = [], fields = []; + this.$element.find(".sel_fields").each(function(index, element) { + var val = element.value; + if (!val) { + return; + } + indices.push(index); + fields.push(val); + }); + + jsonp(this.$element.find('#import_data'), { + url: '/web/import/import_data', + data: { + model: this.model, + meta: JSON.stringify({ + skip: lines_to_skip, + indices: indices, + fields: fields + }) + } + }, this.on_import_results); + }, + on_autodetect_data: function() { + if(!this.$element.find('#csvfile').val()) { return; } + jsonp(this.$element.find('#import_data'), { + url: '/web/import/detect_data' + }, this.on_import_results); + }, + on_import_results: function(results) { + this.$element.find('#result').empty(); + var headers, result_node = this.$element.find("#result"); + + if (results['records']) { + var lines_to_skip = parseInt(this.$element.find('#csv_skip').val(), 10), + with_headers = this.$element.find('#file_has_headers').prop('checked'); + headers = with_headers ? results.records[0] : null; + + result_node.append(QWeb.render('ImportView.result', { + 'headers': headers, + 'records': lines_to_skip ? results.records.slice(lines_to_skip) + : with_headers ? results.records.slice(1) + : results.records + })); + } else if (results['error']) { + result_node.append(QWeb.render('ImportView.error', { + 'error': results['error']})); + } else if (results['success']) { + if (this.widget_parent.widget_parent.active_view == "list") { + this.widget_parent.reload_content(); + } + this.stop(); + return; + } + + var self = this; + this.ready.then(function () { + var $fields = self.$element.find('.sel_fields').bind('blur', function () { + if (this.value && !_(self.all_fields).contains(this.value)) { + this.value = ''; + } + }).autocomplete({ + minLength: 0, + source: self.all_fields, + change: self.on_check_field_values + }).focus(function () { + $(this).autocomplete('search'); + }); + // Column auto-detection + _(headers).each(function (header, index) { + var f =_(self.fields).detect(function (field) { + // TODO: levenshtein between header and field.string + return field.name === header || field.string.toLowerCase() === header; + }); + if (f) { + $fields.eq(index).val(f.name); + } + }); + self.on_check_field_values(); + }); + }, + /** + * Looks through all the field selections, and tries to find if two + * (or more) columns were matched to the same model field. + * + * Returns a map of the multiply-mapped fields to an array of offending + * columns (not actually columns, but the inputs containing the same field + * names). + * + * Also has the side-effect of marking the discovered inputs with the class + * ``duplicate_fld``. + * + * @returns {Object>} map of duplicate field matches to same-valued inputs + */ + find_duplicate_fields: function() { + // Maps values to DOM nodes, in order to discover duplicates + var values = {}, duplicates = {}; + this.$element.find(".sel_fields").each(function(index, element) { + var value = element.value; + var $element = $(element).removeClass('duplicate_fld'); + if (!value) { return; } + + if (!(value in values)) { + values[value] = element; + } else { + var same_valued_field = values[value]; + if (value in duplicates) { + duplicates[value].push(element); + } else { + duplicates[value] = [same_valued_field, element]; + } + $element.add(same_valued_field).addClass('duplicate_fld'); + } + }); + return duplicates; + }, + on_check_field_values: function () { + this.$element.find("#message, #msg").remove(); + + var required_valid = this.check_required(); + + var duplicates = this.find_duplicate_fields(); + if (_.isEmpty(duplicates)) { + this.toggle_import_button(required_valid); + } else { + var $err = $('
Destination fields should only be selected once, some fields are selected more than once:
').insertBefore(this.$element.find('#result')); + var $dupes = $('
').appendTo($err); + _(duplicates).each(function(elements, value) { + $('
').text(value).appendTo($dupes); + _(elements).each(function(element) { + var cell = $(element).closest('td'); + $('
').text(cell.parent().children().index(cell)).appendTo($dupes); + }); + }); + this.toggle_import_button(false); + } + + }, + check_required: function() { + if (!this.required_fields.length) { return true; } + + var selected_fields = _(this.$element.find('.sel_fields').get()).chain() + .pluck('value') + .compact() + .value(); + + var missing_fields = _.difference(this.required_fields, selected_fields); + if (missing_fields.length) { + this.$element.find("#result").before('
*Required Fields are not selected : ' + missing_fields + '.
'); + return false; + } + return true; + }, + stop: function() { + $(this.$dialog).remove(); + this._super(); + } +}); +}; diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 2c730d0b5af..c7ffb896186 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -345,7 +345,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# do_save: function(success, prepend_on_create) { var self = this; if (!this.ready) { - return false; + return $.Deferred().reject(); } var form_dirty = false, form_invalid = false, @@ -367,17 +367,17 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# if (form_invalid) { first_invalid_field.focus(); this.on_invalid(); - return false; + return $.Deferred().reject(); } else { console.log("About to save", values); if (!this.datarecord.id) { - return this.dataset.create(values, function(r) { - self.on_created(r, success, prepend_on_create); - }); + return this.dataset.create(values).pipe(function(r) { + return self.on_created(r, undefined, prepend_on_create); + }).then(success); } else { - return this.dataset.write(this.datarecord.id, values, {}, function(r) { - self.on_saved(r, success); - }); + return this.dataset.write(this.datarecord.id, values, {}).pipe(function(r) { + return self.on_saved(r); + }).then(success); } } }, @@ -402,11 +402,10 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# on_saved: function(r, success) { if (!r.result) { // should not happen in the server, but may happen for internal purpose + return $.Deferred().reject(); } else { - if (success) { - success(r); - } this.reload(); + return $.Deferred().then(success).resolve(r); } }, /** @@ -425,6 +424,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# on_created: function(r, success, prepend_on_create) { if (!r.result) { // should not happen in the server, but may happen for internal purpose + return $.Deferred().reject(); } else { this.datarecord.id = r.result; if (!prepend_on_create) { @@ -439,10 +439,8 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# this.sidebar.attachments.do_update(); } console.debug("The record has been created with id #" + this.datarecord.id); - if (success) { - success(_.extend(r, {created: true})); - } this.reload(); + return $.Deferred().then(success).resolve(_.extend(r, {created: true})); } }, do_search: function (domains, contexts, groupbys) { @@ -472,6 +470,13 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# get_selected_ids: function() { var id = this.dataset.ids[this.dataset.index]; return id ? [id] : []; + }, + recursive_save: function() { + var self = this; + return $.when(this.do_save()).pipe(function(res) { + if (self.dataset.parent_view) + return self.dataset.parent_view.recursive_save(); + }); } }); openerp.web.FormDialog = openerp.web.Dialog.extend({ @@ -830,30 +835,31 @@ openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({ this._super.apply(this, arguments); this.$element.click(this.on_click); }, - on_click: function(saved) { + on_click: function() { var self = this; - if ((!this.node.attrs.special && this.view.dirty_for_user && saved !== true) || !this.view.datarecord.id) { - this.view.do_save(function() { - self.on_click(true); - }); - } else { - if (this.node.attrs.confirm) { - var dialog = $('
' + this.node.attrs.confirm + '
').dialog({ + var exec_action = function() { + if (self.node.attrs.confirm) { + var dialog = $('
' + self.node.attrs.confirm + '
').dialog({ title: 'Confirm', modal: true, buttons: { Ok: function() { self.on_confirmed(); - $(this).dialog("close"); + $(self).dialog("close"); }, Cancel: function() { - $(this).dialog("close"); + $(self).dialog("close"); } } }); } else { - this.on_confirmed(); + self.on_confirmed(); } + }; + if ((!this.node.attrs.special && this.view.dirty_for_user) || !this.view.datarecord.id) { + this.view.recursive_save().then(exec_action); + } else { + exec_action(); } }, on_confirmed: function() { @@ -1467,7 +1473,7 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ this.$input = this.$element.find("input"); this.$drop_down = this.$element.find(".oe-m2o-drop-down-button"); this.$menu_btn = this.$element.find(".oe-m2o-cm-button"); - + // context menu var init_context_menu_def = $.Deferred().then(function(e) { var rdataset = new openerp.web.DataSetStatic(self, "ir.values", self.build_context()); @@ -1475,7 +1481,7 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ [[self.field.relation, false]], false, rdataset.get_context()], false, 0) .then(function(result) { self.related_entries = result; - + var $cmenu = $("#" + self.cm_id); $cmenu.append(QWeb.render("FieldMany2One.context_menu", {widget: self})); var bindings = {}; @@ -1945,11 +1951,7 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ var view = this.viewmanager.views[this.viewmanager.active_view].controller; if (this.viewmanager.active_view === "form") { var res = $.when(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()) { + if (!res.isResolved() && !res.isRejected()) { throw "Asynchronous get_value() is not supported in form view."; } return res; @@ -2002,6 +2004,7 @@ openerp.web.form.One2ManyListView = openerp.web.ListView.extend({ } else { var self = this; var pop = new openerp.web.form.SelectCreatePopup(this); + pop.on_default_get.add(self.dataset.on_default_get); pop.select_element(self.o2m.field.relation,{ initial_view: "form", alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined, @@ -2163,6 +2166,7 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope this.dataset = new openerp.web.ReadOnlyDataSetSearch(this, this.model, this.context); this.dataset.parent_view = this.options.parent_view; + this.dataset.on_default_get.add(this.on_default_get); if (this.options.initial_view == "search") { this.setup_search_view(); } else { // "form" @@ -2285,7 +2289,8 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope this.on_select_elements(this.created_elements); } this.stop(); - } + }, + on_default_get: function(res) {} }); openerp.web.form.SelectCreateListView = openerp.web.ListView.extend({ diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index 88db848118f..d31a3b05674 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -862,6 +862,8 @@ db.web.View = db.web.Widget.extend(/** @lends db.web.View# */{ console.log('Todo'); }, on_sidebar_import: function() { + var import_view = new db.web.DataImport(this, this.dataset); + import_view.start(); }, on_sidebar_export: function() { var export_view = new db.web.DataExport(this, this.dataset); diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index a451912dcb8..25d256992d7 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -1383,6 +1383,79 @@ + + + Import + + +
+ +

1. Import a .CSV file

+

Select a .CSV file to import. If you need a sample of file to import, + you should use the export tool with the "Import Compatible" option. +

+

+ + +

+

2. Check your file format

+
+
+ Import Options + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
+ + + + + + + + + + +
+
+ +
+
+ +

The import failed due to:

+ +

Here is a preview of the file we could not import:

+
+
+
+

OpenERP Web

diff --git a/addons/web_graph/__init__.py b/addons/web_graph/__init__.py index 53e20f29429..e69de29bb2d 100644 --- a/addons/web_graph/__init__.py +++ b/addons/web_graph/__init__.py @@ -1,2 +0,0 @@ -#!/usr/bin/python -import controllers \ No newline at end of file diff --git a/addons/web_graph/controllers/__init__.py b/addons/web_graph/controllers/__init__.py deleted file mode 100644 index 039d9715fab..00000000000 --- a/addons/web_graph/controllers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -import main \ No newline at end of file diff --git a/addons/web_graph/controllers/main.py b/addons/web_graph/controllers/main.py deleted file mode 100644 index cda868c0809..00000000000 --- a/addons/web_graph/controllers/main.py +++ /dev/null @@ -1,11 +0,0 @@ -from web.controllers.main import View -import web.common as openerpweb - -class GraphView(View): - _cp_path = "/web_graph/graphview" - - @openerpweb.jsonrequest - def load(self, req, model, view_id): - fields_view = self.fields_view_get(req, model, view_id, 'graph') - all_fields = req.session.model(model).fields_get() - return {'fields_view': fields_view, 'all_fields':all_fields} diff --git a/addons/web_graph/static/src/js/graph.js b/addons/web_graph/static/src/js/graph.js index 2a493b89166..59bc2c29dcf 100644 --- a/addons/web_graph/static/src/js/graph.js +++ b/addons/web_graph/static/src/js/graph.js @@ -33,8 +33,20 @@ openerp.web_graph.GraphView = openerp.web.View.extend({ this.$element.hide(); }, start: function() { + var self = this; this._super(); - return this.rpc("/web_graph/graphview/load", {"model": this.model, "view_id": this.view_id}, this.on_loaded); + return $.when( + new openerp.web.DataSet(this, this.model).call('fields_get', []), + this.rpc('/web/view/load', { + model: this.model, + view_id: this.view_id, + view_type: 'graph' + })).then(function (fields_result, view_result) { + self.on_loaded({ + all_fields: fields_result[0], + fields_view: view_result[0] + }); + }); }, on_loaded: function(data) { this.all_fields = data.all_fields; @@ -49,8 +61,6 @@ openerp.web_graph.GraphView = openerp.web.View.extend({ this.operator = []; this.group_field = []; this.orientation = this.fields_view.arch.attrs.orientation || ''; - this.elem_id = this.$element[0]['id']; - _.each(this.fields_view.arch.children, function (field) { if (field.attrs.operator) { this.operator.push(field.attrs.name); @@ -98,7 +108,7 @@ openerp.web_graph.GraphView = openerp.web.View.extend({ }, schedule_chart: function(results) { - this.$element.html(QWeb.render("GraphView", {"fields_view": this.fields_view, "chart": this.chart,'elem_id': this.elem_id})); + this.$element.html(QWeb.render("GraphView", {"fields_view": this.fields_view, "chart": this.chart,'element_id': this.element_id})); _.each(results, function (result) { _.each(result, function (field_value, field_name) { @@ -304,7 +314,7 @@ openerp.web_graph.GraphView = openerp.web.View.extend({ var bar_chart = new dhtmlXChart({ view: view_chart, - container: self.elem_id+"-barchart", + container: self.element_id+"-barchart", value:"#"+group_list[0]+"#", gradient: "3d", border: false, @@ -374,7 +384,7 @@ openerp.web_graph.GraphView = openerp.web.View.extend({ }); } bar_chart.parse(_.values(abscissa_data), "json"); - jQuery("#"+self.elem_id+"-barchart").height(jQuery("#"+self.elem_id+"-barchart").height()+50); + jQuery("#"+self.element_id+"-barchart").height(jQuery("#"+self.element_id+"-barchart").height()+50); bar_chart.attachEvent("onItemClick", function(id) { self.open_list_view(bar_chart.get(id)); }); @@ -383,7 +393,7 @@ openerp.web_graph.GraphView = openerp.web.View.extend({ var self = this; var chart = new dhtmlXChart({ view:"pie3D", - container:self.elem_id+"-piechart", + container:self.element_id+"-piechart", value:"#"+self.operator_field+"#", pieInnerText:function(obj) { var sum = chart.sum("#"+self.operator_field+"#"); diff --git a/addons/web_graph/static/src/xml/web_graph.xml b/addons/web_graph/static/src/xml/web_graph.xml index 4320b9f156e..90349623946 100644 --- a/addons/web_graph/static/src/xml/web_graph.xml +++ b/addons/web_graph/static/src/xml/web_graph.xml @@ -1,4 +1,4 @@ \ No newline at end of file diff --git a/addons/web_hello/__openerp__.py b/addons/web_hello/__openerp__.py index 7a80645f3b8..40e84db8cd5 100644 --- a/addons/web_hello/__openerp__.py +++ b/addons/web_hello/__openerp__.py @@ -5,4 +5,5 @@ "js": ["static/*/*.js", "static/*/js/*.js"], "css": [], 'active': False, + 'web_preload': True, } diff --git a/addons/web_hello/static/openerp/base_hello.js b/addons/web_hello/static/openerp/base_hello.js index 99c97006eac..91caf263196 100644 --- a/addons/web_hello/static/openerp/base_hello.js +++ b/addons/web_hello/static/openerp/base_hello.js @@ -13,6 +13,13 @@ openerp.web.SearchView = openerp.web.SearchView.extend({ // here you may tweak globals object, if any, and play with on_* or do_* callbacks on them +openerp.web.Login = openerp.web.Login.extend({ + start: function() { + console.log('Hello there'); + this._super.apply(this,arguments); + } +}); + }; // vim:et fdc=0 fdl=0: diff --git a/addons/web_kanban/static/src/css/kanban.css b/addons/web_kanban/static/src/css/kanban.css index afac6cdef47..f92203589b2 100644 --- a/addons/web_kanban/static/src/css/kanban.css +++ b/addons/web_kanban/static/src/css/kanban.css @@ -1,8 +1,21 @@ .openerp .oe_kanban_view .oe_column { - float: left; width: 100%; } +.openerp .oe_vertical_text { + writing-mode:tb-rl; + -webkit-transform:rotate(90deg); + -moz-transform:rotate(90deg); + -o-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); + display:block; + width:30px; + height:20px; + align:center; + font-size:24px; +} + .openerp .oe_kanban_view .ui-sortable-placeholder { border: 1px dotted black; visibility: visible !important; @@ -14,6 +27,10 @@ font-size: 1.5em; font-weight: bold; } +.openerp .oe_kanban_view .fold-columns-icon { + float: left; + padding: 2px 2px 0 0; +} .openerp .oe_kanban_action_button { height: 22px; margin: 0; @@ -200,4 +217,3 @@ .openerp .oe_kanban_color_alert .oe_kanban_color_border { border-color: #c00 !important; } - diff --git a/addons/web_kanban/static/src/img/minus-icon.png b/addons/web_kanban/static/src/img/minus-icon.png new file mode 100644 index 00000000000..cb655d1e503 Binary files /dev/null and b/addons/web_kanban/static/src/img/minus-icon.png differ diff --git a/addons/web_kanban/static/src/img/plus-icon.png b/addons/web_kanban/static/src/img/plus-icon.png new file mode 100644 index 00000000000..7bb9b1be856 Binary files /dev/null and b/addons/web_kanban/static/src/img/plus-icon.png differ diff --git a/addons/web_kanban/static/src/js/kanban.js b/addons/web_kanban/static/src/js/kanban.js index 8d6727facaa..46173341361 100644 --- a/addons/web_kanban/static/src/js/kanban.js +++ b/addons/web_kanban/static/src/js/kanban.js @@ -18,6 +18,8 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ this.all_display_data = false; this.groups = []; this.qweb = new QWeb2.Engine(); + this.aggregates = {}; + this.NO_OF_COLUMNS = 3; if (this.options.action_views_ids.form) { this.form_dialog = new openerp.web.FormDialog(this, {}, this.options.action_views_ids.form, dataset).start(); this.form_dialog.on_form_dialog_saved.add_last(this.on_record_saved); @@ -36,8 +38,17 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ } }, add_qweb_template: function() { + var group_operator = ["avg", "max", "min", "sum", "count"] for (var i=0, ii=this.fields_view.arch.children.length; i < ii; i++) { var child = this.fields_view.arch.children[i]; + if (child.tag === "field") { + for(j=0, jj=group_operator.length; j < jj; j++) { + if (child.attrs[group_operator[j]]) { + this.aggregates[child.attrs.name] = child.attrs[group_operator[j]]; + break; + } + } + } if (child.tag === "templates") { this.transform_qweb_template(child); this.qweb.add_template(openerp.web.json_node_to_xml(child)); @@ -118,14 +129,33 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ } } }, - on_show_data: function(data) { + sort_group: function (first, second) { + if (first.header && second.header) + { + first = first.header.toLowerCase(); + second = second.header.toLowerCase(); + if (first > second) return 1; + else if (first < second) return -1; + else return 0; + } + else return 0; + }, + on_show_data: function() { var self = this; - this.$element.html(QWeb.render("KanbanView", {"data": data})); + if (!this.group_by.length) { + this.do_record_group(); + } + self.all_display_data.sort(this.sort_group); + this.$element.html(QWeb.render("KanbanView", {"data": self.all_display_data})); this.on_reload_kanban(); + this.$element.find(".oe_vertical_text").hide(); var drag_handel = false; if (this.$element.find(".oe_kanban_draghandle").length > 0) { drag_handel = ".oe_kanban_draghandle"; } + if (!this.group_by.length) { + drag_handel = true; + } this.$element.find(".oe_column").sortable({ connectWith: ".oe_column", handle : drag_handel, @@ -134,36 +164,44 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ self.source_index['column'] = ui.item.parent().attr('id'); }, stop: self.on_receive_record, + scroll: false }); this.$element.find(".oe_column").disableSelection() this.$element.find('button.oe_kanban_button_new').click(this.do_add_record); + this.$element.find(".fold-columns-icon").click(function(event) { + self.do_fold_unfold_columns(event, this.id); + }); }, - on_button_click: function (button_attrs, record_id) { - var self = this; - if (this.groups.length) { - _.each(this.groups, function (group) { - group.list([], - function (groups) {}, - function (dataset) { - dataset.read_slice([], {}, function(records) { - var index = parseInt(_.indexOf(dataset.ids, record_id)); - if(index >= 0) { - self.on_confirm_click(dataset, button_attrs, index, record_id); - } - }); - } - ); - }); - } else { - var index = parseInt(_.indexOf(self.dataset.ids, record_id)); - if (index >= 0) { - _.extend(self.dataset, {domain: self.domain, context: self.context}); - self.on_confirm_click(self.dataset, button_attrs, index, record_id); + do_fold_unfold_columns: function(event, element_id) { + var column_id = "column_" + element_id; + var column_element = this.$element.find("#" + column_id + " .oe_fold_column"); + if (column_element.is(":hidden")) { + this.$element.find("#" + column_id).find("img.fold-columns-icon").attr('src', '/web_kanban/static/src/img/minus-icon.png'); + column_element.show(); + this.$element.find("#" + column_id + ".oe_table_column").css("width",Math.round(99 / this.all_display_data.length) + "%"); + this.$element.find("#" + column_id + ".oe_vertical_text").hide(); + } + else{ + this.$element.find("#" + column_id).find("img.fold-columns-icon").attr('src', '/web_kanban/static/src/img/plus-icon.png'); + column_element.hide(); + this.$element.find("#" + column_id + ".oe_table_column").css("width","0.5%"); + this.$element.find("#" + column_id + ".oe_vertical_text").show(); + } + + }, + do_record_group: function() { + if (this.NO_OF_COLUMNS && this.all_display_data.length > 0) { + var records = this.all_display_data[0].records; + var record_per_group = Math.round((records).length / this.NO_OF_COLUMNS); + this.all_display_data = []; + for (var i=0, ii=this.NO_OF_COLUMNS; i < ii; i++) { + this.all_display_data.push({'records': records.slice(0,record_per_group), 'value':i, 'header' : false, 'ids':[]}); + records.splice(0,record_per_group); } } }, - on_confirm_click: function (dataset, button_attrs, index, record_id) { - this.on_execute_button_click(dataset, button_attrs, record_id); + on_button_click: function (button_attrs, record_id) { + this.on_execute_button_click(this.dataset, button_attrs, record_id); }, do_add_record: function() { this.dataset.index = null; @@ -179,8 +217,7 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ }, on_record_saved: function(r) { var id = this.form_dialog.form.datarecord.id; - // TODO fme: reload record instead of all. need refactoring - this.do_actual_search(); + this.on_reload_record(id); }, do_change_color: function(record_id, $e) { var self = this, @@ -198,12 +235,41 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ var data = {}; data[$e.data('name')] = $(this).data('color'); self.dataset.write(id, data, {}, function() { - // TODO fme: reload record instead of all. need refactoring - self.do_actual_search(); + self.on_reload_record(id); }); $cpicker.remove(); }); }, + /** + Reload one record in view. + record_id : reload record id. + */ + on_reload_record: function (record_id){ + var self = this; + this.dataset.read_ids([record_id], [], function (records) { + if (records.length > 0) { + for (var i=0, ii=self.all_display_data.length; i < ii; i++) { + for(j=0, jj=self.all_display_data[i].records.length; j < jj; j++) { + if (self.all_display_data[i].records[j].id == record_id) { + _.extend(self.all_display_data[i].records[j], records[0]); + self.$element.find("#main_" + record_id).children().remove(); + self.$element.find("#main_" + record_id).append(self.qweb.render('kanban-box', { + record: self.do_transform_record(self.all_display_data[i].records[j]), + kanban_color: self.kanban_color, + kanban_gravatar: self.kanban_gravatar + })); + break; + } + } + } + self.$element.find("#main_" + record_id + " .oe_kanban_action").click(self.on_action_clicked); + self.$element.find("#main_" + record_id + " .oe_kanban_box_show_onclick_trigger").click(function() { + $(this).parent("#main_" + record_id + " .oe_kanban_box").find(".oe_kanban_box_show_onclick").toggle(); + }); + } + }); + + }, do_delete: function (id) { var self = this; return $.when(this.dataset.unlink([id])).then(function () { @@ -228,16 +294,7 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ this.do_execute_action( button_attrs, dataset, record_id, function () { - var count = 1; - _.each(self.all_display_data, function(data, index) { - self.dataset.read_ids( data.ids, [], function(records){ - self.all_display_data[index].records = records; - if(self.all_display_data.length == count) { - self.do_actual_search(); - } - count++; - }); - }); + self.do_actual_search(); } ); }, @@ -249,9 +306,8 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ if (!ui.item.attr("id")) { return false; } - // TODO fme: check what was this sequence - if (self.fields_view.fields.sequence && (self.source_index.index >= 0 && self.source_index.index != from) || - (self.source_index.column && self.source_index.column != ui.item.parent().attr('id'))) { + if (self.fields_view.fields.sequence != undefined && ((self.source_index.index >= 0 && self.source_index.index != from) || + (self.source_index.column && self.source_index.column != ui.item.parent().attr('id')))) { var child_record = ui.item.parent().children(); var data, sequence = 1, index = to; child_record.splice(0, to); @@ -338,9 +394,6 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ kanban_gravatar: self.kanban_gravatar })); }); - } else { - self.$element.find("#column_" + data.value).remove(); - self.all_display_data.splice(index, 1); } }); this.$element.find('.oe_kanban_action').click(this.on_action_clicked); @@ -377,8 +430,8 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ do_search: function (domains, contexts, group_by) { var self = this; this.rpc('/web/session/eval_domain_and_context', { - domains: domains, - contexts: contexts, + domains: [this.dataset.get_domain()].concat(domains), + contexts: [this.dataset.get_context()].concat(contexts), group_by_seq: group_by }, function (results) { self.domain = results.domain; @@ -392,23 +445,27 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ group_by = self.group_by; if (!group_by.length && this.fields_view.arch.attrs.default_group_by) { group_by = [this.fields_view.arch.attrs.default_group_by]; + self.group_by = group_by; } self.datagroup = new openerp.web.DataGroup(self, self.model, self.domain, self.context, group_by); - self.dataset.context = self.context; - self.dataset.domain = self.domain; - self.datagroup.list([], + self.datagroup.list(_.keys(self.fields_view.fields), function (groups) { self.groups = groups; - self.do_render_group(groups); + if (groups.length) { + self.do_render_group(groups); + } + else { + self.all_display_data = []; + self.on_show_data(); + } }, function (dataset) { - self.domain = dataset.domain; - self.context = dataset.context; self.groups = []; - self.dataset.read_slice([], {}, function(records) { - self.all_display_data = [{'records': records, 'value':false, 'header' : false, 'ids': self.dataset.ids}]; + self.dataset.read_slice([], {'domain': self.domain, 'context': self.context}, function(records) { + if (records.length) self.all_display_data = [{'records': records, 'value':false, 'header' : false, 'ids': self.dataset.ids}]; + else self.all_display_data = []; self.$element.find(".oe_kanban_view").remove(); - self.on_show_data(self.all_display_data); + self.on_show_data(); }); } ); @@ -417,8 +474,6 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ this.all_display_data = []; var self = this; _.each(datagroups, function (group) { - self.dataset.context = group.context; - self.dataset.domain = group.domain; var group_name = group.value; var group_value = group.value; if (!group.value) { @@ -428,11 +483,15 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ group_name = group.value[1]; group_value = group.value[0]; } - self.dataset.read_slice([], {}, function(records) { - self.all_display_data.push({"value" : group_value, "records": records, 'header':group_name, 'ids': self.dataset.ids}); + var group_aggregates = {}; + _.each(self.aggregates, function(value, key) { + group_aggregates[value] = group.aggregates[key]; + }); + self.dataset.read_slice([], {'domain': group.domain, 'context': group.context}, function(records) { + self.all_display_data.push({"value" : group_value, "records": records, 'header':group_name, 'ids': self.dataset.ids, 'aggregates': group_aggregates}); if (datagroups.length == self.all_display_data.length) { self.$element.find(".oe_kanban_view").remove(); - self.on_show_data(self.all_display_data); + self.on_show_data(); } }); }); diff --git a/addons/web_kanban/static/src/xml/web_kanban.xml b/addons/web_kanban/static/src/xml/web_kanban.xml index aa7bc73190d..8f3aaf457e9 100644 --- a/addons/web_kanban/static/src/xml/web_kanban.xml +++ b/addons/web_kanban/static/src/xml/web_kanban.xml @@ -9,15 +9,22 @@ - - + + +
+
+ +
+
+ : +
+
- -
-
-
+ +

+
diff --git a/openerp-web.py b/openerp-web.py index e5b28c83e0a..928a62e4528 100755 --- a/openerp-web.py +++ b/openerp-web.py @@ -24,7 +24,7 @@ optparser.add_option("--server-port", dest="server_port", default=8069, help="OpenERP server port", type="int", metavar="NUMBER") optparser.add_option("--db-filter", dest="dbfilter", default='.*', help="Filter listed database", metavar="REGEXP") -optparser.add_option('--addons-path', dest='addons_path', default=path_addons, +optparser.add_option('--addons-path', dest='addons_path', default=[path_addons], action='append', help="Path do addons directory", metavar="PATH") server_options = optparse.OptionGroup(optparser, "Server configuration")