diff --git a/addons/base/__openerp__.py b/addons/base/__openerp__.py index 7fa373d1ba0..8e8cb5285b0 100644 --- a/addons/base/__openerp__.py +++ b/addons/base/__openerp__.py @@ -20,12 +20,13 @@ "static/lib/qweb/qweb2.js", "static/lib/underscore/underscore.js", "static/lib/underscore/underscore.string.js", - "static/src/js/base.js", + "static/src/js/boot.js", + "static/src/js/core.js", + "static/src/js/dates.js", "static/src/js/chrome.js", - "static/src/js/controller.js", "static/src/js/views.js", "static/src/js/data.js", - "static/src/js/dates.js", + "static/src/js/data_export.js", "static/src/js/form.js", "static/src/js/list.js", "static/src/js/list-editable.js", @@ -37,5 +38,6 @@ "static/lib/jquery.ui/css/smoothness/jquery-ui-1.8.9.custom.css", "static/lib/jquery.ui.notify/css/ui.notify.css", "static/src/css/base.css", + "static/src/css/data_export.css", ], } diff --git a/addons/base/controllers/main.py b/addons/base/controllers/main.py index 6f37989c728..358f6fb4601 100644 --- a/addons/base/controllers/main.py +++ b/addons/base/controllers/main.py @@ -13,6 +13,7 @@ import openerpweb.nonliterals import cherrypy import xmlrpclib +import csv # Should move to openerpweb.Xml2Json class Xml2Json: @@ -305,7 +306,7 @@ class Session(openerpweb.Controller): context, domain = eval_context_and_domain(req.session, openerpweb.nonliterals.CompoundContext(*(contexts or [])), openerpweb.nonliterals.CompoundDomain(*(domains or []))) - + group_by_sequence = [] for candidate in (group_by_seq or []): ctx = req.session.eval_context(candidate, context) @@ -316,7 +317,7 @@ class Session(openerpweb.Controller): group_by_sequence.append(group_by) else: group_by_sequence.extend(group_by) - + return { 'context': context, 'domain': domain, @@ -329,7 +330,7 @@ class Session(openerpweb.Controller): This method store an action object in the session object and returns an integer identifying that action. The method get_session_action() can be used to get back the action. - + :param the_action: The action to save in the session. :type the_action: anything :return: A key identifying the saved action. @@ -352,7 +353,7 @@ class Session(openerpweb.Controller): """ Gets back a previously saved action. This method can return None if the action was saved since too much time (this case should be handled in a smart way). - + :param key: The key given by save_session_action() :type key: integer :return: The saved action or None. @@ -487,7 +488,7 @@ class Menu(openerpweb.Controller): menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context) menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']} menu_items.append(menu_root) - + # make a tree using parent_id menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items) for menu_item in menu_items: @@ -594,7 +595,7 @@ class DataSet(openerpweb.Controller): record_map = dict((record['id'], record) for record in records) return [record_map[id] for id in ids if record_map.get(id)] - + @openerpweb.jsonrequest def load(self, req, model, id, fields): m = req.session.model(model) @@ -757,7 +758,7 @@ class View(openerpweb.Controller): except ValueError: # not a literal return openerpweb.nonliterals.Domain(session, domain) - + def parse_context(self, context, session): """ Parses an arbitrary string containing a context, transforms it to either a literal context or a :class:`openerpweb.nonliterals.Context` @@ -986,4 +987,216 @@ class Action(openerpweb.Controller): return clean_action(req.session.model('ir.actions.server').run( [action_id], req.session.eval_context(req.context)), req.session) -# +def export_csv(fields, result): + fp = StringIO() + writer = csv.writer(fp, quoting=csv.QUOTE_ALL) + + writer.writerow(fields) + + for data in result: + row = [] + for d in data: + if isinstance(d, basestring): + d = d.replace('\n',' ').replace('\t',' ') + try: + d = d.encode('utf-8') + except: + pass + if d is False: d = None + row.append(d) + writer.writerow(row) + + fp.seek(0) + data = fp.read() + fp.close() + return data + +def export_xls(fieldnames, table): + try: + import xlwt + except ImportError: + common.error(_('Import Error.'), _('Please install xlwt library to export to MS Excel.')) + + workbook = xlwt.Workbook() + worksheet = workbook.add_sheet('Sheet 1') + + for i, fieldname in enumerate(fieldnames): + worksheet.write(0, i, str(fieldname)) + worksheet.col(i).width = 8000 # around 220 pixels + + style = xlwt.easyxf('align: wrap yes') + + for row_index, row in enumerate(table): + for cell_index, cell_value in enumerate(row): + cell_value = str(cell_value) + cell_value = re.sub("\r", " ", cell_value) + worksheet.write(row_index + 1, cell_index, cell_value, style) + + + fp = StringIO() + workbook.save(fp) + fp.seek(0) + data = fp.read() + fp.close() + #return data.decode('ISO-8859-1') + return unicode(data, 'utf-8', 'replace') + +class Export(View): + _cp_path = "/base/export" + + 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.jsonrequest + def get_fields(self, req, model, prefix='', name= '', field_parent=None, params={}): + import_compat = params.get("import_compat", False) + + fields = self.fields_get(req, model) + field_parent_type = params.get("parent_field_type",False) + + if import_compat and field_parent_type and field_parent_type == "many2one": + fields = {} + + fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}}) + records = [] + fields_order = fields.keys() + fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', ''))) + + for index, field in enumerate(fields_order): + value = fields[field] + record = {} + if import_compat and value.get('readonly', False): + ok = False + for sl in value.get('states', {}).values(): + for s in sl: + ok = ok or (s==['readonly',False]) + if not ok: continue + + id = prefix + (prefix and '/'or '') + field + nm = name + (name and '/' or '') + value['string'] + record.update(id=id, string= nm, action='javascript: void(0)', + target=None, icon=None, children=[], field_type=value.get('type',False), required=value.get('required', False)) + records.append(record) + + if len(nm.split('/')) < 3 and value.get('relation', False): + if import_compat: + ref = value.pop('relation') + cfields = self.fields_get(req, ref) + if (value['type'] == 'many2many'): + record['children'] = [] + record['params'] = {'model': ref, 'prefix': id, 'name': nm} + + elif value['type'] == 'many2one': + record['children'] = [id + '/id', id + '/.id'] + record['params'] = {'model': ref, 'prefix': id, 'name': nm} + + else: + cfields_order = cfields.keys() + cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', ''))) + children = [] + for j, fld in enumerate(cfields_order): + cid = id + '/' + fld + cid = cid.replace(' ', '_') + children.append(cid) + record['children'] = children or [] + record['params'] = {'model': ref, 'prefix': id, 'name': nm} + else: + ref = value.pop('relation') + cfields = self.fields_get(req, ref) + cfields_order = cfields.keys() + cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', ''))) + children = [] + for j, fld in enumerate(cfields_order): + cid = id + '/' + fld + cid = cid.replace(' ', '_') + children.append(cid) + record['children'] = children or [] + record['params'] = {'model': ref, 'prefix': id, 'name': nm} + + records.reverse() + return records + + @openerpweb.jsonrequest + def save_export_lists(self, req, name, model, field_list): + result = {'resource':model, 'name':name, 'export_fields': []} + for field in field_list: + result['export_fields'].append((0, 0, {'name': field})) + return req.session.model("ir.exports").create(result, req.session.eval_context(req.context)) + + @openerpweb.jsonrequest + def exist_export_lists(self, req, model): + export_model = req.session.model("ir.exports") + return export_model.read(export_model.search([('resource', '=', model)]), ['name']) + + @openerpweb.jsonrequest + def delete_export(self, req, export_id): + req.session.model("ir.exports").unlink(export_id, req.session.eval_context(req.context)) + return True + + @openerpweb.jsonrequest + def namelist(self,req, model, export_id): + + result = self.get_data(req, model, req.session.eval_context(req.context)) + ir_export_obj = req.session.model("ir.exports") + ir_export_line_obj = req.session.model("ir.exports.line") + + field = ir_export_obj.read(export_id) + fields = ir_export_line_obj.read(field['export_fields']) + + name_list = {} + [name_list.update({field['name']: result.get(field['name'])}) for field in fields] + return name_list + + def get_data(self, req, model, context=None): + ids = [] + context = context or {} + fields_data = {} + proxy = req.session.model(model) + fields = self.fields_get(req, model) + if not ids: + f1 = proxy.fields_view_get(False, 'tree', context)['fields'] + f2 = proxy.fields_view_get(False, 'form', context)['fields'] + + fields = dict(f1) + fields.update(f2) + fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}}) + + def rec(fields): + _fields = {'id': 'ID' , '.id': 'Database ID' } + def model_populate(fields, prefix_node='', prefix=None, prefix_value='', level=2): + fields_order = fields.keys() + fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', ''))) + + for field in fields_order: + fields_data[prefix_node+field] = fields[field] + if prefix_node: + fields_data[prefix_node + field]['string'] = '%s%s' % (prefix_value, fields_data[prefix_node + field]['string']) + st_name = fields[field]['string'] or field + _fields[prefix_node+field] = st_name + if fields[field].get('relation', False) and level>0: + fields2 = self.fields_get(req, fields[field]['relation']) + fields2.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}}) + model_populate(fields2, prefix_node+field+'/', None, st_name+'/', level-1) + model_populate(fields) + return _fields + return rec(fields) + + @openerpweb.jsonrequest + def export_data(self, req, model, fields, ids, domain, import_compat=False, export_format="csv", context=None): + context = req.session.eval_context(req.context) + modle_obj = req.session.model(model) + ids = ids or modle_obj.search(domain, context=context) + + field = fields.keys() + result = modle_obj.export_data(ids, field , context).get('datas',[]) + + if not import_compat: + field = [val.strip() for val in fields.values()] + + if export_format == 'xls': + return export_xls(field, result) + else: + return export_csv(field, result) + diff --git a/addons/base/static/src/css/base.css b/addons/base/static/src/css/base.css index 483a433eb99..dce17649310 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, @@ -1169,3 +1172,10 @@ background: linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* W3C */ font-size: 1.2em; font-weight: bold; } + +.openerp .dhx_mini_calendar { + -moz-box-shadow: none; + -khtml-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} 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 new file mode 100644 index 00000000000..759e72ebcfe Binary files /dev/null and b/addons/base/static/src/img/collapse.gif differ diff --git a/addons/base/static/src/img/expand.gif b/addons/base/static/src/img/expand.gif new file mode 100644 index 00000000000..ca98d0b5f2b Binary files /dev/null and b/addons/base/static/src/img/expand.gif differ diff --git a/addons/base/static/src/img/header.gif b/addons/base/static/src/img/header.gif new file mode 100644 index 00000000000..f40bd9c0f6a Binary files /dev/null and b/addons/base/static/src/img/header.gif differ diff --git a/addons/base/static/src/js/base.js b/addons/base/static/src/js/boot.js similarity index 95% rename from addons/base/static/src/js/base.js rename to addons/base/static/src/js/boot.js index 28022f8b764..c16f2d261f5 100644 --- a/addons/base/static/src/js/base.js +++ b/addons/base/static/src/js/boot.js @@ -1,5 +1,5 @@ //--------------------------------------------------------- -// OpenERP initialisation and black magic about the pool +// OpenERP Web Boostrap //--------------------------------------------------------- /** @@ -57,9 +57,8 @@ // OpenERP base module split //--------------------------------------------------------- -/** @namespace */ openerp.base = function(instance) { - openerp.base.controller(instance); + openerp.base.core(instance); openerp.base.dates(instance); openerp.base.chrome(instance); openerp.base.data(instance); @@ -87,6 +86,9 @@ openerp.base = function(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 2019776932f..c456996f6ba 100644 --- a/addons/base/static/src/js/chrome.js +++ b/addons/base/static/src/js/chrome.js @@ -4,241 +4,7 @@ openerp.base.chrome = function(openerp) { -/** - * 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').text(v).appendTo($('body')); - } - if(notify && this.notification) { - this.notification.notify("Logging:",v); - } - } - } - - } -}); - -}; - -// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/base/static/src/js/core.js b/addons/base/static/src/js/core.js new file mode 100644 index 00000000000..e57d79e40bb --- /dev/null +++ b/addons/base/static/src/js/core.js @@ -0,0 +1,572 @@ +/*--------------------------------------------------------- + * 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) + openerp.base.Class = function(){}; + + // Create a new Class that inherits from this class + openerp.base.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.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').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 the Widget 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, it is 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() { + * this._super(); + * // stuff you want to make after the rendering, `this.$element` holds a correct 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-', + /** + * Construct the widget and set its parent if a parent is given. + * + * @constructs + * @param {openerp.base.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 2e89dacceb8..5545e32df17 100644 --- a/addons/base/static/src/js/data.js +++ b/addons/base/static/src/js/data.js @@ -18,7 +18,7 @@ openerp.base.serialize_sort = function (criterion) { }).join(', '); }; -openerp.base.DataGroup = openerp.base.Controller.extend( /** @lends openerp.base.DataGroup# */{ +openerp.base.DataGroup = openerp.base.Widget.extend( /** @lends openerp.base.DataGroup# */{ /** * Management interface between views and grouped collections of OpenERP * records. @@ -30,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 @@ -232,13 +232,13 @@ openerp.base.StaticDataGroup = openerp.base.GrouplessDataGroup.extend( /** @lend } }); -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 {String} model the OpenERP model this dataset will manage */ @@ -291,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', { @@ -492,24 +496,33 @@ openerp.base.BufferedDataSet = openerp.base.DataSetStatic.extend({ this.to_create.push(cached); this.cache.push(cached); 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) { 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 e66a073643c..f8e2e91667d 100644 --- a/addons/base/static/src/js/form.js +++ b/addons/base/static/src/js/form.js @@ -20,7 +20,6 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormVi 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; @@ -31,8 +30,7 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormVi 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; @@ -50,9 +48,6 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormVi return def.promise(); } else { var context = new openerp.base.CompoundContext(this.dataset.get_context()); - if (this.view_manager.action && this.view_manager.action.context) { - context.add(this.view_manager.action.context); - } return this.rpc("/base/formview/load", {"model": this.model, "view_id": this.view_id, toolbar: this.options.sidebar, context: context}, this.on_loaded); } @@ -93,9 +88,10 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormVi if (this.options.sidebar && this.options.sidebar_id) { this.sidebar = new openerp.base.Sidebar(this, this.options.sidebar_id); this.sidebar.start(); - this.sidebar.attachments = new openerp.base.form.SidebarAttachments(this.sidebar, this.sidebar.add_section("Attachments"), this); - this.sidebar.add_toolbar(data.fields_view.toolbar); 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(); }, @@ -134,22 +130,22 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormVi 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); } } @@ -268,7 +264,7 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormVi processed.push(field.name); if (field.get_value() != value) { field.set_value(value); - field.touched = true; + field.dirty = true; if (_.indexOf(processed, field.name) < 0) { this.do_onchange(field, processed); } @@ -316,37 +312,40 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormVi 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() { @@ -360,7 +359,7 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormVi on_invalid: function() { var msg = "