diff --git a/addons/base/__openerp__.py b/addons/base/__openerp__.py index da2b345d217..b93e4036f8e 100644 --- a/addons/base/__openerp__.py +++ b/addons/base/__openerp__.py @@ -3,4 +3,38 @@ "version" : "2.0", "depends" : [], 'active': True, + 'js' : [ + "static/lib/datejs/date-en-US.js", + "static/lib/jquery/jquery-1.5.2.js", + "static/lib/jquery.ba-bbq/jquery.ba-bbq.js", + "static/lib/jquery.contextmenu/jquery.contextmenu.r2.packed.js", + "static/lib/jquery.superfish/js/hoverIntent.js", + "static/lib/jquery.superfish/js/superfish.js", + "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/json/json2.js", + "static/lib/qweb/qweb2.js", + "static/lib/underscore/underscore.js", + "static/lib/underscore/underscore.string.js", + "static/src/js/boot.js", + "static/src/js/core.js", + "static/src/js/dates.js", + "static/src/js/chrome.js", + "static/src/js/views.js", + "static/src/js/data.js", + "static/src/js/data_export.js", + "static/src/js/form.js", + "static/src/js/list.js", + "static/src/js/list-editable.js", + "static/src/js/search.js", + "static/src/js/view_tree.js", + ], + 'css' : [ + "static/lib/jquery.superfish/css/superfish.css", + "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 7098f7f8f90..fcf673b50d7 100644 --- a/addons/base/controllers/main.py +++ b/addons/base/controllers/main.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -import base64 -import glob, os -import pprint +import base64, glob, os, re from xml.etree import ElementTree from cStringIO import StringIO @@ -12,6 +10,8 @@ import openerpweb.ast import openerpweb.nonliterals import cherrypy +import csv +import xml.dom.minidom # Should move to openerpweb.Xml2Json class Xml2Json: @@ -56,40 +56,116 @@ class Xml2Json: # OpenERP Web base Controllers #---------------------------------------------------------- +def manifest_glob(addons, key): + files = [] + for addon in addons: + globlist = openerpweb.addons_manifest.get(addon, {}).get(key, []) + print globlist + for pattern in globlist: + for path in glob.glob(os.path.join(openerpweb.path_addons, addon, pattern)): + files.append(path[len(openerpweb.path_addons):]) + return files + +def concat_files(file_list): + """ Concatenate file content + return (concat,timestamp) + concat: concatenation of file content + timestamp: max(os.path.getmtime of file_list) + """ + root = openerpweb.path_root + files_content = [] + files_timestamp = 0 + for i in file_list: + fname = os.path.join(root, i) + ftime = os.path.getmtime(fname) + if ftime > files_timestamp: + files_timestamp = ftime + files_content = open(fname).read() + files_concat = "".join(files_content) + return files_concat + +class WebClient(openerpweb.Controller): + _cp_path = "/base/webclient" + + @openerpweb.jsonrequest + def csslist(self, req, mods='base'): + return manifest_glob(mods.split(','), 'css') + + @openerpweb.jsonrequest + def jslist(self, req, mods='base'): + return manifest_glob(mods.split(','), 'js') + + @openerpweb.httprequest + def css(self, req, mods='base'): + cherrypy.response.headers['Content-Type'] = 'text/css' + files = manifest_glob(mods.split(','), 'css') + concat = concat_files(files)[0] + # TODO request set the Date of last modif and Etag + return concat + + @openerpweb.httprequest + def js(self, req, mods='base'): + cherrypy.response.headers['Content-Type'] = 'application/javascript' + files = manifest_glob(mods.split(','), 'js') + concat = concat_files(files)[0] + # TODO request set the Date of last modif and Etag + return concat + + @openerpweb.httprequest + def home(self, req): + template =""" + + + + OpenERP + %s + + + %s + + + + + """.replace('\n'+' '*8,'\n') + + # script tags + jslist = ['/base/webclient/js'] + if 1: # debug == 1 + jslist = manifest_glob(['base'], 'js') + js = "\n ".join([''%i for i in jslist]) + + # css tags + csslist = ['/base/webclient/css'] + if 1: # debug == 1 + csslist = manifest_glob(['base'], 'css') + css = "\n ".join([''%i for i in csslist]) + r = template % (js, css) + return r + +class Database(openerpweb.Controller): + _cp_path = "/base/database" + + @openerpweb.jsonrequest + def get_databases_list(self, req): + proxy = req.session.proxy("db") + dbs = proxy.list() + h = req.httprequest.headers['Host'].split(':')[0] + d = h.split('.')[0] + r = cherrypy.config['openerp.dbfilter'].replace('%h',h).replace('%d',d) + print "h,d",h,d,r + dbs = [i for i in dbs if re.match(r,i)] + return {"db_list": dbs} + class Session(openerpweb.Controller): _cp_path = "/base/session" - def manifest_glob(self, addons, key): - files = [] - for addon in addons: - globlist = openerpweb.addons_manifest.get(addon, {}).get(key, []) - - files.extend([ - resource_path[len(openerpweb.path_addons):] - for pattern in globlist - for resource_path in glob.glob(os.path.join( - openerpweb.path_addons, addon, pattern)) - ]) - return files - - def concat_files(self, file_list): - """ Concatenate file content - return (concat,timestamp) - concat: concatenation of file content - timestamp: max(os.path.getmtime of file_list) - """ - root = openerpweb.path_root - files_content = [] - files_timestamp = 0 - for i in file_list: - fname = os.path.join(root, i) - ftime = os.path.getmtime(fname) - if ftime > files_timestamp: - files_timestamp = ftime - files_content = open(fname).read() - files_concat = "".join(files_content) - return files_concat - @openerpweb.jsonrequest def login(self, req, db, login, password): req.session.login(db, login, password) @@ -104,40 +180,14 @@ class Session(openerpweb.Controller): return req.session.model('ir.ui.view_sc').get_sc(req.session._uid, "ir.ui.menu", req.session.eval_context(req.context)) - @openerpweb.jsonrequest - def get_databases_list(self, req): - proxy = req.session.proxy("db") - dbs = proxy.list() - - return {"db_list": dbs} - @openerpweb.jsonrequest def modules(self, req): - return {"modules": [name - for name, manifest in openerpweb.addons_manifest.iteritems() - if manifest.get('active', True)]} - - @openerpweb.jsonrequest - def csslist(self, req, mods='base'): - return {'files': self.manifest_glob(mods.split(','), 'css')} - - @openerpweb.jsonrequest - def jslist(self, req, mods='base'): - return {'files': self.manifest_glob(mods.split(','), 'js')} - - def css(self, req, mods='base,base_hello'): - files = self.manifest_glob(mods.split(','), 'css') - concat = self.concat_files(files)[0] - # TODO request set the Date of last modif and Etag - return concat - css.exposed = True - - def js(self, req, mods='base,base_hello'): - files = self.manifest_glob(mods.split(','), 'js') - concat = self.concat_files(files)[0] - # TODO request set the Date of last modif and Etag - return concat - js.exposed = True + # TODO query server for installed web modules + mods = [] + for name, manifest in openerpweb.addons_manifest.items(): + if name != 'base' and manifest.get('active', True): + mods.append(name) + return mods @openerpweb.jsonrequest def eval_domain_and_context(self, req, contexts, domains, @@ -174,7 +224,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) @@ -185,7 +235,7 @@ class Session(openerpweb.Controller): group_by_sequence.append(group_by) else: group_by_sequence.extend(group_by) - + return { 'context': context, 'domain': domain, @@ -198,7 +248,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. @@ -221,7 +271,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. @@ -231,24 +281,24 @@ class Session(openerpweb.Controller): if not saved_actions: return None return saved_actions["actions"].get(key) - + def eval_context_and_domain(session, context, domain=None): e_context = session.eval_context(context) # should we give the evaluated context as an evaluation context to the domain? e_domain = session.eval_domain(domain or []) return e_context, e_domain - + def load_actions_from_ir_values(req, key, key2, models, meta, context): Values = req.session.model('ir.values') actions = Values.get(key, key2, models, meta, context) - for _, _, action in actions: - clean_action(action, req.session) - - return actions + return [(id, name, clean_action(action, req.session)) + for id, name, action in actions] def clean_action(action, session): + if action['type'] != 'ir.actions.act_window': + return action # values come from the server, we can just eval them if isinstance(action.get('context', None), basestring): action['context'] = eval( @@ -259,12 +309,49 @@ def clean_action(action, session): action['domain'] = eval( action['domain'], session.evaluation_context( - action['context'])) or [] - if not action.has_key('flags'): + action.get('context', {}))) or [] + if 'flags' not in action: # Set empty flags dictionary for web client. action['flags'] = dict() return fix_view_modes(action) +def generate_views(action): + """ + While the server generates a sequence called "views" computing dependencies + between a bunch of stuff for views coming directly from the database + (the ``ir.actions.act_window model``), it's also possible for e.g. buttons + to return custom view dictionaries generated on the fly. + + In that case, there is no ``views`` key available on the action. + + Since the web client relies on ``action['views']``, generate it here from + ``view_mode`` and ``view_id``. + + Currently handles two different cases: + + * no view_id, multiple view_mode + * single view_id, single view_mode + + :param dict action: action descriptor dictionary to generate a views key for + """ + view_id = action.get('view_id', False) + if isinstance(view_id, (list, tuple)): + view_id = view_id[0] + + # providing at least one view mode is a requirement, not an option + view_modes = action['view_mode'].split(',') + + if len(view_modes) > 1: + if view_id: + raise ValueError('Non-db action dictionaries should provide ' + 'either multiple view modes or a single view ' + 'mode and an optional view id.\n\n Got view ' + 'modes %r and view id %r for action %r' % ( + view_modes, view_id, action)) + action['views'] = [(False, mode) for mode in view_modes] + return + action['views'] = [(view_id, view_modes[0])] + def fix_view_modes(action): """ For historical reasons, OpenERP has weird dealings in relation to view_mode and the view_type attribute (on window actions): @@ -283,18 +370,17 @@ def fix_view_modes(action): :param dict action: an action descriptor :returns: nothing, the action is modified in place """ - if action.pop('view_type') != 'form': - return + if 'views' not in action: + generate_views(action) + + if action.pop('view_type') != 'form': + return action + + action['views'] = [ + [id, mode if mode != 'tree' else 'list'] + for id, mode in action['views'] + ] - if action.has_key('view_mode'): - action['view_mode'] = ','.join( - mode if mode != 'tree' else 'list' - for mode in action['view_mode'].split(',')) - if action.has_key('views'): - action['views'] = [ - [id, mode if mode != 'tree' else 'list'] - for id, mode in action['views'] - ] return action class Menu(openerpweb.Controller): @@ -320,7 +406,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: @@ -355,9 +441,10 @@ class DataSet(openerpweb.Controller): req.session.eval_context(req.context))} @openerpweb.jsonrequest - def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None): - return self.do_search_read(request, model, fields, offset, limit, domain, context, sort) - def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None): + def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, sort=None): + return self.do_search_read(request, model, fields, offset, limit, domain, sort) + def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None + , sort=None): """ Performs a search() followed by a read() (if needed) using the provided search criteria @@ -370,22 +457,33 @@ class DataSet(openerpweb.Controller): :param int limit: the maximum number of records to return :param list domain: the search domain for the query :param list sort: sorting directives - :returns: a list of result records + :returns: A structure (dict) with two keys: ids (all the ids matching + the (domain, context) pair) and records (paginated records + matching fields selection set) :rtype: list """ Model = request.session.model(model) - context, domain = eval_context_and_domain(request.session, request.context, domain) - - ids = Model.search(domain, offset or 0, limit or False, - sort or False, context) + context, domain = eval_context_and_domain( + request.session, request.context, domain) + ids = Model.search(domain, 0, False, sort or False, context) + # need to fill the dataset with all ids for the (domain, context) pair, + # so search un-paginated and paginate manually before reading + paginated_ids = ids[offset:(offset + limit if limit else None)] if fields and fields == ['id']: # shortcut read if we only want the ids - return map(lambda id: {'id': id}, ids) + return { + 'ids': ids, + 'records': map(lambda id: {'id': id}, paginated_ids) + } + + records = Model.read(paginated_ids, fields or False, context) + records.sort(key=lambda obj: ids.index(obj['id'])) + return { + 'ids': ids, + 'records': records + } - reads = Model.read(ids, fields or False, context) - reads.sort(key=lambda obj: ids.index(obj['id'])) - return reads @openerpweb.jsonrequest def get(self, request, model, ids, fields=False): @@ -415,7 +513,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) @@ -455,14 +553,14 @@ class DataSet(openerpweb.Controller): @openerpweb.jsonrequest def call(self, req, model, method, args, domain_id=None, context_id=None): - return {'result': self.call_common(req, model, method, args, domain_id, context_id)} + return self.call_common(req, model, method, args, domain_id, context_id) @openerpweb.jsonrequest def call_button(self, req, model, method, args, domain_id=None, context_id=None): action = self.call_common(req, model, method, args, domain_id, context_id) if isinstance(action, dict) and action.get('type') != '': - clean_action(action, req.session) - return {'result': action} + return {'result': clean_action(action, req.session)} + return {'result': False} @openerpweb.jsonrequest def exec_workflow(self, req, model, id, signal): @@ -471,20 +569,19 @@ class DataSet(openerpweb.Controller): @openerpweb.jsonrequest def default_get(self, req, model, fields): - m = req.session.model(model) - r = m.default_get(fields, req.session.eval_context(req.context)) - return {'result': r} + Model = req.session.model(model) + return Model.default_get(fields, req.session.eval_context(req.context)) class DataGroup(openerpweb.Controller): _cp_path = "/base/group" @openerpweb.jsonrequest - def read(self, request, model, group_by_fields, domain=None): + def read(self, request, model, fields, group_by_fields, domain=None, sort=None): Model = request.session.model(model) context, domain = eval_context_and_domain(request.session, request.context, domain) return Model.read_group( - domain or [], False, group_by_fields, 0, False, - dict(context, group_by=group_by_fields)) + domain or [], fields, group_by_fields, 0, False, + dict(context, group_by=group_by_fields), sort or False) class View(openerpweb.Controller): _cp_path = "/base/view" @@ -497,18 +594,35 @@ class View(openerpweb.Controller): # todo fme?: check that we should pass the evaluated context here self.process_view(request.session, fvg, context, transform) return fvg - + def process_view(self, session, fvg, context, transform): + # depending on how it feels, xmlrpclib.ServerProxy can translate + # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not + # enjoy unicode strings which can not be trivially converted to + # strings, and it blows up during parsing. + + # So ensure we fix this retardation by converting view xml back to + # bit strings. + if isinstance(fvg['arch'], unicode): + arch = fvg['arch'].encode('utf-8') + else: + arch = fvg['arch'] + if transform: evaluation_context = session.evaluation_context(context or {}) - xml = self.transform_view(fvg['arch'], session, evaluation_context) + xml = self.transform_view(arch, session, evaluation_context) else: - xml = ElementTree.fromstring(fvg['arch']) + xml = ElementTree.fromstring(arch) fvg['arch'] = Xml2Json.convert_element(xml) - for field in fvg['fields'].values(): - if field.has_key('views') and field['views']: - for view in field["views"].values(): + + for field in fvg['fields'].itervalues(): + if field.get('views'): + for view in field["views"].itervalues(): self.process_view(session, view, None, transform) + if field.get('domain'): + field["domain"] = self.parse_domain(field["domain"], session) + if field.get('context'): + field["context"] = self.parse_context(field["context"], session) @openerpweb.jsonrequest def add_custom(self, request, view_id, arch): @@ -534,36 +648,6 @@ class View(openerpweb.Controller): return {'result': True} return {'result': False} - def normalize_attrs(self, elem, context): - """ Normalize @attrs, @invisible, @required, @readonly and @states, so - the client only has to deal with @attrs. - - See `the discoveries pad `_ for - the rationale. - - :param elem: the current view node (Python object) - :type elem: xml.etree.ElementTree.Element - :param dict context: evaluation context - """ - # If @attrs is normalized in json by server, the eval should be replaced by simplejson.loads - attrs = openerpweb.ast.literal_eval(elem.get('attrs', '{}')) - if 'states' in elem.attrib: - attrs.setdefault('invisible', [])\ - .append(('state', 'not in', elem.attrib.pop('states').split(','))) - if attrs: - elem.set('attrs', simplejson.dumps(attrs)) - for a in ['invisible', 'readonly', 'required']: - if a in elem.attrib: - # In the XML we trust - avalue = bool(eval(elem.get(a, 'False'), - {'context': context or {}})) - if not avalue: - del elem.attrib[a] - else: - elem.attrib[a] = '1' - if a == 'invisible' and 'attrs' in elem.attrib: - del elem.attrib['attrs'] - def transform_view(self, view_string, session, context=None): # transform nodes on the fly via iterparse, instead of # doing it statically on the parsing result @@ -573,31 +657,41 @@ class View(openerpweb.Controller): if event == "start": if root is None: root = elem - self.normalize_attrs(elem, context) self.parse_domains_and_contexts(elem, session) return root - def parse_domain(self, elem, attr_name, session): - """ Parses an attribute of the provided name as a domain, transforms it + def parse_domain(self, domain, session): + """ Parses an arbitrary string containing a domain, transforms it to either a literal domain or a :class:`openerpweb.nonliterals.Domain` - :param elem: the node being parsed - :type param: xml.etree.ElementTree.Element - :param str attr_name: the name of the attribute which should be parsed + :param domain: the domain to parse, if the domain is not a string it is assumed to + be a literal domain and is returned as-is :param session: Current OpenERP session :type session: openerpweb.openerpweb.OpenERPSession """ - domain = elem.get(attr_name, '').strip() - if domain: - try: - elem.set( - attr_name, - openerpweb.ast.literal_eval( - domain)) - except ValueError: - # not a literal - elem.set(attr_name, - openerpweb.nonliterals.Domain(session, domain)) + if not isinstance(domain, (str, unicode)): + return domain + try: + return openerpweb.ast.literal_eval(domain) + 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` + + :param context: the context to parse, if the context is not a string it is assumed to + be a literal domain and is returned as-is + :param session: Current OpenERP session + :type session: openerpweb.openerpweb.OpenERPSession + """ + if not isinstance(context, (str, unicode)): + return context + try: + return openerpweb.ast.literal_eval(context) + except ValueError: + return openerpweb.nonliterals.Context(session, context) def parse_domains_and_contexts(self, elem, session): """ Converts domains and contexts from the view into Python objects, @@ -610,17 +704,14 @@ class View(openerpweb.Controller): non-literal objects :type session: openerpweb.openerpweb.OpenERPSession """ - self.parse_domain(elem, 'domain', session) - self.parse_domain(elem, 'filter_domain', session) - context_string = elem.get('context', '').strip() - if context_string: - try: - elem.set('context', - openerpweb.ast.literal_eval(context_string)) - except ValueError: - elem.set('context', - openerpweb.nonliterals.Context( - session, context_string)) + for el in ['domain', 'filter_domain']: + domain = elem.get(el, '').strip() + if domain: + elem.set(el, self.parse_domain(domain, session)) + for el in ['context', 'default_get']: + context_string = elem.get(el, '').strip() + if context_string: + elem.set(el, self.parse_context(context_string, session)) class FormView(View): _cp_path = "/base/formview" @@ -668,7 +759,41 @@ class SearchView(View): def fields_get(self, req, model): Model = req.session.model(model) fields = Model.fields_get(False, req.session.eval_context(req.context)) + for field in fields.values(): + # shouldn't convert the views too? + if field.get('domain'): + field["domain"] = self.parse_domain(field["domain"], req.session) + 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") + filters = Model.get_filters(model) + for filter in filters: + 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") + ctx = openerpweb.nonliterals.CompoundContext(context_to_save) + ctx.session = req.session + ctx = ctx.evaluate() + domain = openerpweb.nonliterals.CompoundDomain(domain) + domain.session = req.session + domain = domain.evaluate() + uid = req.session._uid + context = req.session.eval_context(req.context) + to_return = Model.create_or_replace({"context": ctx, + "domain": domain, + "model_id": model, + "name": name, + "user_id": uid + }, context) + return to_return class Binary(openerpweb.Controller): _cp_path = "/base/binary" @@ -774,3 +899,235 @@ class Action(openerpweb.Controller): if action: value = clean_action(action[0], req.session) return {'result': value} + + @openerpweb.jsonrequest + def run(self, req, action_id): + 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): + import StringIO + fp = StringIO.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): + import StringIO + 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.StringIO() + workbook.save(fp) + fp.seek(0) + data = fp.read() + fp.close() + #return data.decode('ISO-8859-1') + return unicode(data, 'utf-8', 'replace') + +def node_attributes(node): + attrs = node.attributes + + if not attrs: + return {} + # localName can be a unicode string, we're using attribute names as + # **kwargs keys and python-level kwargs don't take unicode keys kindly + # (they blow up) so we need to ensure all keys are ``str`` + return dict([(str(attrs.item(i).localName), attrs.item(i).nodeValue) + for i in range(attrs.length)]) + +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/lib/LABjs/LAB.js b/addons/base/static/lib/LABjs/LAB.js deleted file mode 100644 index 1c3c85bd7a3..00000000000 --- a/addons/base/static/lib/LABjs/LAB.js +++ /dev/null @@ -1,5 +0,0 @@ -/*! LAB.js (LABjs :: Loading And Blocking JavaScript) - v1.2.0 (c) Kyle Simpson - MIT License -*/ -(function(p){var q="string",w="head",L="body",M="script",u="readyState",j="preloaddone",x="loadtrigger",N="srcuri",E="preload",Z="complete",y="done",z="which",O="preserve",F="onreadystatechange",ba="onload",P="hasOwnProperty",bb="script/cache",Q="[object ",bw=Q+"Function]",bx=Q+"Array]",e=null,h=true,i=false,k=p.document,bc=p.location,bd=p.ActiveXObject,A=p.setTimeout,be=p.clearTimeout,R=function(a){return k.getElementsByTagName(a)},S=Object.prototype.toString,G=function(){},r={},T={},bf=/^[^?#]*\//.exec(bc.href)[0],bg=/^\w+\:\/\/\/?[^\/]+/.exec(bf)[0],by=R(M),bh=p.opera&&S.call(p.opera)==Q+"Opera]",bi=("MozAppearance"in k.documentElement.style),bj=(k.createElement(M).async===true),v={cache:!(bi||bh),order:bi||bh||bj,xhr:h,dupe:h,base:"",which:w};v[O]=i;v[E]=h;r[w]=k.head||R(w);r[L]=R(L);function B(a){return S.call(a)===bw}function U(a,b){var c=/^\w+\:\/\//,d;if(typeof a!=q)a="";if(typeof b!=q)b="";d=((/^\/\//.test(a))?bc.protocol:"")+a;d=(c.test(d)?"":b)+d;return((c.test(d)?"":(d.charAt(0)==="/"?bg:bf))+d)}function bz(a){return(U(a).indexOf(bg)===0)}function bA(a){var b,c=-1;while(b=by[++c]){if(typeof b.src==q&&a===U(b.src)&&b.type!==bb)return h}return i}function H(t,l){t=!(!t);if(l==e)l=v;var bk=i,C=t&&l[E],bl=C&&l.cache,I=C&&l.order,bm=C&&l.xhr,bB=l[O],bC=l.which,bD=l.base,bn=G,J=i,D,s=h,m={},K=[],V=e;C=bl||bm||I;function bo(a,b){if((a[u]&&a[u]!==Z&&a[u]!=="loaded")||b[y]){return i}a[ba]=a[F]=e;return h}function W(a,b,c){c=!(!c);if(!c&&!(bo(a,b)))return;b[y]=h;for(var d in m){if(m[P](d)&&!(m[d][y]))return}bk=h;bn()}function bp(a){if(B(a[x])){a[x]();a[x]=e}}function bE(a,b){if(!bo(a,b))return;b[j]=h;A(function(){r[b[z]].removeChild(a);bp(b)},0)}function bF(a,b){if(a[u]===4){a[F]=G;b[j]=h;A(function(){bp(b)},0)}}function X(b,c,d,g,f,n){var o=b[z];A(function(){if("item"in r[o]){if(!r[o][0]){A(arguments.callee,25);return}r[o]=r[o][0]}var a=k.createElement(M);if(typeof d==q)a.type=d;if(typeof g==q)a.charset=g;if(B(f)){a[ba]=a[F]=function(){f(a,b)};a.src=c;if(bj){a.async=i}}r[o].insertBefore(a,(o===w?r[o].firstChild:e));if(typeof n==q){a.text=n;W(a,b,h)}},0)}function bq(a,b,c,d){T[a[N]]=h;X(a,b,c,d,W)}function br(a,b,c,d){var g=arguments;if(s&&a[j]==e){a[j]=i;X(a,b,bb,d,bE)}else if(!s&&a[j]!=e&&!a[j]){a[x]=function(){br.apply(e,g)}}else if(!s){bq.apply(e,g)}}function bs(a,b,c,d){var g=arguments,f;if(s&&a[j]==e){a[j]=i;f=a.xhr=(bd?new bd("Microsoft.XMLHTTP"):new p.XMLHttpRequest());f[F]=function(){bF(f,a)};f.open("GET",b);f.send("")}else if(!s&&a[j]!=e&&!a[j]){a[x]=function(){bs.apply(e,g)}}else if(!s){T[a[N]]=h;X(a,b,c,d,e,a.xhr.responseText);a.xhr=e}}function bt(a){if(typeof a=="undefined"||!a)return;if(a.allowDup==e)a.allowDup=l.dupe;var b=a.src,c=a.type,d=a.charset,g=a.allowDup,f=U(b,bD),n,o=bz(f);if(typeof d!=q)d=e;g=!(!g);if(!g&&((T[f]!=e)||(s&&m[f])||bA(f))){if(m[f]!=e&&m[f][j]&&!m[f][y]&&o){W(e,m[f],h)}return}if(m[f]==e)m[f]={};n=m[f];if(n[z]==e)n[z]=bC;n[y]=i;n[N]=f;J=h;if(!I&&bm&&o)bs(n,f,c,d);else if(!I&&bl)br(n,f,c,d);else bq(n,f,c,d)}function Y(a){if(t&&!I)K.push(a);if(!t||C)a()}function bu(a){var b=[],c;for(c=-1;++c - - - - OpenERP - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/addons/base/static/src/css/base.css b/addons/base/static/src/css/base.css index 84b51b2cd54..b173386e234 100644 --- a/addons/base/static/src/css/base.css +++ b/addons/base/static/src/css/base.css @@ -30,6 +30,9 @@ body.openerp { .openerp .oe-number { text-align: right !important; } +.openerp .oe_hide { + display: none !important; +} /* STATES */ .openerp .on_logged { @@ -577,6 +580,10 @@ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%, display: none; } +.openerp .searchview_extended_delete_prop { + text-decoration: none; +} + .openerp .searchview_extended_delete_group span, .openerp .searchview_extended_delete_prop span { font-size: 0.9em; @@ -591,6 +598,10 @@ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%, border: 1px solid silver; } +.openerp .oe-listview thead table { + width: 100%; + border: none; +} .openerp .oe-listview tr.odd { background-color: #f3f3f3; } @@ -600,16 +611,27 @@ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%, .openerp .oe-listview tbody tr:hover { background-color: #eae9f0; } +.openerp .oe-listview thead table tr, +.openerp .oe-listview thead table tr:hover { + background: none; +} .openerp .oe-listview td, .openerp .oe-listview th { vertical-align: middle; text-align: left; } -.openerp .oe-listview th.oe-sortable, -.openerp .oe-listview th.oe-sortable .ui-icon { +.openerp .oe-listview td.oe-record-delete { + text-align: right; +} +.openerp .oe-listview th.oe-sortable { cursor: pointer; } +.openerp .oe-listview th.oe-sortable .ui-icon { + height: 1em; + display: inline; + display: inline-block; +} .openerp .oe-listview .oe-field-cell { cursor: pointer; @@ -632,9 +654,22 @@ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%, .openerp .oe-listview th.oe-actions { text-align: left; + white-space: nowrap; } .openerp .oe-listview th.oe-list-pager { text-align: right; + white-space: nowrap; +} +.openerp .oe-list-pager .oe-pager-state { + cursor: pointer; +} + +.openerp .oe-listview .oe-group-name { + padding-right: 1em; +} +.openerp .oe-listview .oe-group-name, +.openerp .oe-listview .oe-group-pagination { + white-space: nowrap; } .openerp .oe-listview tfoot td { @@ -653,29 +688,29 @@ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%, rounded corners are a pain on tables: need to round not only table, but also on the first and last children of the first and last row */ -.openerp .oe-listview table { +.openerp .oe-listview table.oe-listview-content { -webkit-border-radius: 7px; -moz-border-radius: 7px; border-radius: 7px; } -.openerp .oe-listview table thead tr:first-child th:first-child { +.openerp .oe-listview table.oe-listview-content thead tr:first-child th:first-child { -webkit-border-top-left-radius: 7px; -moz-border-radius-topleft: 7px; border-top-left-radius: 7px; } -.openerp .oe-listview table thead tr:first-child th:last-child { +.openerp .oe-listview table.oe-listview-content thead tr:first-child th:last-child { -webkit-border-top-right-radius: 7px; -moz-border-radius-topright: 7px; border-top-right-radius: 7px; } -.openerp .oe-listview table tfoot td:first-child, -.openerp .oe-listview table tbody:last-child tr:last-child th:first-child { +.openerp .oe-listview table.oe-listview-content tfoot td:first-child, +.openerp .oe-listview table.oe-listview-content tbody:last-child tr:last-child th:first-child { -webkit-border-bottom-left-radius: 7px; -moz-border-radius-bottomleft: 7px; border-bottom-left-radius: 7px; } -.openerp .oe-listview table tfoot td:last-child, -.openerp .oe-listview table tbody:last-child tr:last-child td:last-child { +.openerp .oe-listview table.oe-listview-content tfoot td:last-child, +.openerp .oe-listview table.oe-listview-content tbody:last-child tr:last-child td:last-child { -webkit-border-bottom-right-radius: 7px; -moz-border-radius-bottomright: 7px; border-bottom-right-radius: 7px; @@ -773,13 +808,14 @@ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%, position: relative; vertical-align: top; } -.openerp input.field_date, .openerp input.field_datetime { - background: #fff url('../img/ui/field_calendar.png') no-repeat right center; - background-origin: content-box; - -moz-background-origin: content; - -moz-background-origin: content-box; - -webkit-background-origin: content-box; +.openerp img.ui-datepicker-trigger, .openerp img.ui-datepicker-trigger { + margin-left: -20px; + vertical-align: middle; + cursor: pointer; + position: relative; + top: -1px; } + /* http://www.quirksmode.org/dom/inputfile.html * http://stackoverflow.com/questions/2855589/replace-input-type-file-by-an-image */ @@ -987,6 +1023,10 @@ background: linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* W3C */ .openerp .closed-sidebar .toggle-sidebar { border-left: none; } +.openerp li.oe_sidebar_print { + padding-left: 20px; + background: 1px 3px url(../img/icons/gtk-print.png) no-repeat; +} .openerp.kitten-mode-activated .main_table { background: url(http://placekitten.com/g/1500/800) repeat; diff --git a/addons/base/static/src/css/data_export.css b/addons/base/static/src/css/data_export.css new file mode 100644 index 00000000000..7b4b9707b39 --- /dev/null +++ b/addons/base/static/src/css/data_export.css @@ -0,0 +1,85 @@ +.openerp .oe_export_row tr{ + background-color: #FFFFFF; + font-size: 0.8em; + height: 22px; +} + +.openerp tr.ui-selected td { + background-color: #CCCCCC; +} + +.openerp .oe_export_requiredfield { + background-color: #D2D2FF; +} + +.openerp .oe_export_readonlyfield{ + background-color: #999999; +} + +.openerp .oe_export_row:hover{ + background-color: #F3F3F3; +} + +.openerp .oe_export_fields_selector_export { + width: 100%; + height: 400px; +} + +.openerp .oe_export_fields_selector_left { + width: 50%; +} + +.openerp div#left_field_panel { + overflow: scroll; + width: 100%; + height: 400px; + border: solid #999999 1px; +} + +.openerp .oe_export_fields_selector_center { + width: 102px; +} + +.openerp .oe_export_fields_selector_right { + width: 45%; + height: 400px; +} + +.openerp .oe_export_fields_selector_export select{ + width: 100%; + height: 100%; +} + +.openerp .oe_export_tree_header{ + border: 0.5px solid #E3E3E3; + text-align: left; + white-space: nowrap; + padding: 4px 5px; + background: url(/base/static/src/img/header.gif); +} + + +.openerp table.tree_grid{ + border: 1px solid #E3E3E3; + text-align: left; + white-space: nowrap; + background-color:#E3E3E3; + border-collapse: collapse; + width: 100%; +} + +.openerp table.tree_grid a:hover { + color: blue; + border: none; +} + +.openerp table.tree_grid a { + color: #5F5C5C; + border: none; + display: block; +} + +.openerp .oe_export_button_export { + border: 1px solid #006; + background-color: #F3F3F3; +} diff --git a/addons/base/static/src/img/collapse.gif b/addons/base/static/src/img/collapse.gif 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 53% rename from addons/base/static/src/js/base.js rename to addons/base/static/src/js/boot.js index 55e4929e4c0..c16f2d261f5 100644 --- a/addons/base/static/src/js/base.js +++ b/addons/base/static/src/js/boot.js @@ -1,71 +1,5 @@ -/*--------------------------------------------------------- - * John Resig Class, to be moved to openerp.base.Class - *---------------------------------------------------------*/ - -(function() { - var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; - // The base Class implementation (does nothing) - this.Class = function(){}; - - // Create a new Class that inherits from this class - Class.extend = function(prop) { - var _super = this.prototype; - - // Instantiate a base class (but only create the instance, - // don't run the init constructor) - initializing = true; - var prototype = new this(); - initializing = false; - - // Copy the properties over onto the new prototype - for (var name in prop) { - // Check if we're overwriting an existing function - prototype[name] = typeof prop[name] == "function" && - typeof _super[name] == "function" && fnTest.test(prop[name]) ? - (function(name, fn){ - return function() { - var tmp = this._super; - - // Add a new ._super() method that is the same method - // but on the super-class - this._super = _super[name]; - - // The method only need to be bound temporarily, so we - // remove it when we're done executing - var ret = fn.apply(this, arguments); - this._super = tmp; - - return ret; - }; - })(name, prop[name]) : - prop[name]; - } - - // The dummy class constructor - function Class() { - // All construction is actually done in the init method - if ( !initializing && this.init ) { - var ret = this.init.apply(this, arguments); - if (ret) { return ret; } - } - return this; - } - - // Populate our constructed prototype object - Class.prototype = prototype; - - // Enforce the constructor to be what we expect - Class.constructor = Class; - - // And make this class extendable - Class.extend = arguments.callee; - - return Class; - }; -})(); - //--------------------------------------------------------- -// OpenERP initialisation and black magic about the pool +// OpenERP Web Boostrap //--------------------------------------------------------- /** @@ -107,11 +41,13 @@ _session_id: "session" + session_counter++, screen: openerp.screen, sessions: openerp.sessions, - base: {} + base: {}, + web_mobile: {} }; openerp.sessions[new_instance._session_id] = new_instance; - if (!skip_init) + if (!skip_init){ openerp.base(new_instance); + } return new_instance; } }; @@ -121,8 +57,8 @@ // OpenERP base module split //--------------------------------------------------------- -/** @namespace */ openerp.base = function(instance) { + openerp.base.core(instance); openerp.base.dates(instance); openerp.base.chrome(instance); openerp.base.data(instance); @@ -135,9 +71,6 @@ openerp.base = function(instance) { if (openerp.base.list) { openerp.base.list(instance); } - if (openerp.base.tree) { - openerp.base.tree(instance); - } if (openerp.base. m2o) { openerp.base.m2o(instance); } @@ -147,6 +80,15 @@ openerp.base = function(instance) { if (openerp.base.list && openerp.base.list.editable) { openerp.base.list.editable(instance); } + if (openerp.web_mobile) { + openerp.web_mobile(instance); + } + if (openerp.base.view_tree) { + openerp.base.view_tree(instance); + } + if (openerp.base.data_export) { + openerp.base.data_export(instance); + } }; // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/base/static/src/js/chrome.js b/addons/base/static/src/js/chrome.js index 5e1c7432cb8..ed84c32071b 100644 --- a/addons/base/static/src/js/chrome.js +++ b/addons/base/static/src/js/chrome.js @@ -4,325 +4,15 @@ openerp.base.chrome = function(openerp) { -openerp.base.callback = function(obj, method) { - var callback = function() { - var args = Array.prototype.slice.call(arguments); - var r; - for(var i = 0; i < callback.callback_chain.length; i++) { - var c = callback.callback_chain[i]; - if(c.unique) { - callback.callback_chain.splice(i, 1); - i -= 1; - } - r = c.callback.apply(c.self, c.args.concat(args)); - // TODO special value to stop the chain - // openerp.base.callback_stop - } - return r; - }; - callback.callback_chain = []; - callback.add = function(f) { - if(typeof(f) == 'function') { - f = { callback: f, args: Array.prototype.slice.call(arguments, 1) }; - } - f.self = f.self || null; - f.args = f.args || []; - f.unique = !!f.unique; - if(f.position == 'last') { - callback.callback_chain.push(f); - } else { - callback.callback_chain.unshift(f); - } - return callback; - }; - callback.add_first = function(f) { - return callback.add.apply(null,arguments); - }; - callback.add_last = function(f) { - return callback.add({ - callback: f, - args: Array.prototype.slice.call(arguments, 1), - position: "last" - }); - }; - - return callback.add({ - callback: method, - self:obj, - args:Array.prototype.slice.call(arguments, 2) - }); -}; - -/** - * Base error for lookup failure - * - * @class - */ -openerp.base.NotFound = Class.extend( /** @lends openerp.base.NotFound# */ { -}); -openerp.base.KeyNotFound = openerp.base.NotFound.extend( /** @lends openerp.base.KeyNotFound# */ { - /** - * Thrown when a key could not be found in a mapping - * - * @constructs - * @extends openerp.base.NotFound - * @param {String} key the key which could not be found - */ - init: function (key) { - this.key = key; - }, - toString: function () { - return "The key " + this.key + " was not found"; - } -}); -openerp.base.ObjectNotFound = openerp.base.NotFound.extend( /** @lends openerp.base.ObjectNotFound# */ { - /** - * Thrown when an object path does not designate a valid class or object - * in the openerp hierarchy. - * - * @constructs - * @extends openerp.base.NotFound - * @param {String} path the invalid object path - */ - init: function (path) { - this.path = path; - }, - toString: function () { - return "Could not find any object of path " + this.path; - } -}); -openerp.base.Registry = Class.extend( /** @lends openerp.base.Registry# */ { - /** - * Stores a mapping of arbitrary key (strings) to object paths (as strings - * as well). - * - * Resolves those paths at query time in order to always fetch the correct - * object, even if those objects have been overloaded/replaced after the - * registry was created. - * - * An object path is simply a dotted name from the openerp root to the - * object pointed to (e.g. ``"openerp.base.Session"`` for an OpenERP - * session object). - * - * @constructs - * @param {Object} mapping a mapping of keys to object-paths - */ - init: function (mapping) { - this.map = mapping || {}; - }, - /** - * Retrieves the object matching the provided key string. - * - * @param {String} key the key to fetch the object for - * @returns {Class} the stored class, to initialize - * - * @throws {openerp.base.KeyNotFound} if the object was not in the mapping - * @throws {openerp.base.ObjectNotFound} if the object path was invalid - */ - get_object: function (key) { - var path_string = this.map[key]; - if (path_string === undefined) { - throw new openerp.base.KeyNotFound(key); - } - - var object_match = openerp; - var path = path_string.split('.'); - // ignore first section - for(var i=1; i').text(v).appendTo($('body')); - } - if(notify && this.notification) { - this.notification.notify("Logging:",v); - } - } - } - - } -}); - -/** - * Generates an inherited class that replaces all the methods by null methods (methods - * that does nothing and always return undefined). - * - * @param {Class} claz - * @param {dict} add Additional functions to override. - * @return {Class} - */ -openerp.base.generate_null_object_class = function(claz, add) { - var newer = {}; - var copy_proto = function(prototype) { - for (var name in prototype) { - if(typeof prototype[name] == "function") { - newer[name] = function() {}; - } - } - if (prototype.prototype) - copy_proto(prototype.prototype); - }; - copy_proto(claz.prototype); - newer.init = openerp.base.BasicController.prototype.init; - var tmpclass = claz.extend(newer); - return tmpclass.extend(add || {}); -}; - -openerp.base.Notification = openerp.base.BasicController.extend({ - init: function(element_id) { - this._super(element_id); - this.$element.notify({ - speed: 500, - expires: 1500 - }); - }, - notify: function(title, text) { - this.$element.notify('create', { - title: title, - text: text - }); - }, - warn: function(title, text) { - this.$element.notify('create', 'oe_notification_alert', { - title: title, - text: text - }); - } -}); - -openerp.base.Session = openerp.base.BasicController.extend( /** @lends openerp.base.Session# */{ +openerp.base.Session = openerp.base.Widget.extend( /** @lends openerp.base.Session# */{ /** * @constructs - * @extends openerp.base.BasicController * @param element_id to use for exception reporting * @param server * @param port */ - init: function(element_id, server, port) { - this._super(element_id); + init: function(parent, element_id, server, port) { + this._super(parent, element_id); this.server = (server == undefined) ? location.hostname : server; this.port = (port == undefined) ? location.port : port; this.rpc_mode = (server == location.hostname) ? "ajax" : "jsonp"; @@ -350,15 +40,12 @@ openerp.base.Session = openerp.base.BasicController.extend( /** @lends openerp.b * @param {Object} params call parameters * @param {Function} success_callback function to execute on RPC call success * @param {Function} error_callback function to execute on RPC call failure - * one * @returns {jQuery.Deferred} jquery-provided ajax deferred */ rpc: function(url, params, success_callback, error_callback) { var self = this; // Construct a JSON-RPC2 request, method is currently unused params.session_id = this.session_id; - // niv: wtf? - //params.context = typeof(params.context) != "undefined" ? params.context : this.context; // Call using the rpc_mode var deferred = $.Deferred(); @@ -368,7 +55,7 @@ openerp.base.Session = openerp.base.BasicController.extend( /** @lends openerp.b params: params, id:null }).then(function () {deferred.resolve.apply(deferred, arguments);}, - function(error) {deferred.reject(error, $.Event());}); + function(error) {deferred.reject(error, $.Event());}); return deferred.fail(function() { deferred.fail(function(error, event) { if (!event.isDefaultPrevented()) { @@ -401,30 +88,35 @@ openerp.base.Session = openerp.base.BasicController.extend( /** @lends openerp.b }, url); var deferred = $.Deferred(); $.ajax(ajax).done(function(response, textStatus, jqXHR) { - self.on_rpc_response(); - if (response.error) { - if (response.error.data.type == "session_invalid") { - self.uid = false; - self.on_session_invalid(function() { - self.rpc(url, payload.params, - function() {deferred.resolve.apply(deferred, arguments);}, - function() {deferred.reject.apply(deferred, arguments);}); - }); - } else { - deferred.reject(response.error); - } - } else { - deferred.resolve(response["result"], textStatus, jqXHR); - } - }).fail(function(jqXHR, textStatus, errorThrown) { - self.on_rpc_response(); - var error = { - code: -32098, - message: "XmlHttpRequestError " + errorThrown, - data: {type: "xhr"+textStatus, debug: jqXHR.responseText, objects: [jqXHR, errorThrown] } - }; - deferred.reject(error); + self.on_rpc_response(); + if (!response.error) { + deferred.resolve(response["result"], textStatus, jqXHR); + return; + } + if (response.error.data.type !== "session_invalid") { + deferred.reject(response.error); + return; + } + self.uid = false; + self.on_session_invalid(function() { + self.rpc(url, payload.params, + function() { + deferred.resolve.apply(deferred, arguments); + }, + function(error, event) { + event.preventDefault(); + deferred.reject.apply(deferred, arguments); + }); }); + }).fail(function(jqXHR, textStatus, errorThrown) { + self.on_rpc_response(); + var error = { + code: -32098, + message: "XmlHttpRequestError " + errorThrown, + data: {type: "xhr"+textStatus, debug: jqXHR.responseText, objects: [jqXHR, errorThrown] } + }; + deferred.reject(error); + }); return deferred.promise(); }, on_rpc_request: function() { @@ -499,7 +191,7 @@ openerp.base.Session = openerp.base.BasicController.extend( /** @lends openerp.b for(var i=0; i').text(v).appendTo($('body')); + } + if(notify && this.notification) { + this.notification.notify("Logging:",v); + } + } + } + } +}); + +/** + * Base class for all visual components. Provides a lot of functionalities helpful + * for the management of a part of the DOM. + * + * Widget handles: + * - Rendering with QWeb. + * - Life-cycle management and parenting (when a parent is destroyed, all its children are + * destroyed too). + * - Insertion in DOM. + * + * Widget also extends SessionAware for ease of use. + * + * Guide to create implementations of this class: + * ============================================== + * + * Here is a sample child class: + * + * MyWidget = openerp.base.Widget.extend({ + * // the name of the QWeb template to use for rendering + * template: "MyQWebTemplate", + * // identifier prefix, useful to put an obvious one for debugging + * identifier_prefix: 'my-id-prefix-', + * + * init: function(parent) { + * this._super(parent); + * // stuff that you want to init before the rendering + * }, + * start: function() { + * // stuff you want to make after the rendering, `this.$element` holds a value + * this.$element.find(".my_button").click(/* an example of event binding * /); + * + * // if you have some asynchronous operations, it's a good idea to return + * // a promise in start() + * var promise = this.rpc(...); + * return promise; + * } + * }); + * + * Now this class can simply be used with the following syntax: + * + * var my_widget = new MyWidget(this); + * my_widget.appendTo($(".some-div")); + * + * With these two lines, the MyWidget instance was inited, rendered, it was inserted into the + * DOM inside the ".some-div" div and its events were binded. + * + * And of course, when you don't need that widget anymore, just do: + * + * my_widget.stop(); + * + * That will kill the widget in a clean way and erase its content from the dom. + */ +openerp.base.Widget = openerp.base.SessionAware.extend({ + /** + * The name of the QWeb template that will be used for rendering. Must be + * redefined in subclasses or the default render() method can not be used. + * + * @type string + */ + template: null, + /** + * The prefix used to generate an id automatically. Should be redefined in + * subclasses. If it is not defined, a generic identifier will be used. + * + * @type string + */ + identifier_prefix: 'generic-identifier-', + /** + * @constructs + * Construct the widget and set its parent if a parent is given. + * + * @param {Widget} parent Binds the current instance to the given Widget instance. + * When that widget is destroyed by calling stop(), the current instance will be + * destroyed too. Can be null. + * @param {String} element_id Deprecated. Sets the element_id. Only useful when you want + * to bind the current Widget to an already existing part of the DOM, which is not compatible + * with the DOM insertion methods provided by the current implementation of Widget. So + * for new components this argument should not be provided any more. + */ + init: function(parent, /** @deprecated */ element_id) { + this._super((parent || {}).session); + // if given an element_id, try to get the associated DOM element and save + // a reference in this.$element. Else just generate a unique identifier. + this.element_id = element_id; + this.element_id = this.element_id || _.uniqueId(this.identifier_prefix); + var tmp = document.getElementById(this.element_id); + this.$element = tmp ? $(tmp) : undefined; + + this.widget_parent = parent; + this.widget_children = []; + if(parent && parent.widget_children) { + parent.widget_children.push(this); + } + // useful to know if the widget was destroyed and should not be used anymore + this.widget_is_stopped = false; + }, + /** + * Render the current widget and appends it to the given jQuery object or Widget. + * + * @param target A jQuery object or a Widget instance. + */ + appendTo: function(target) { + var self = this; + return this._render_and_insert(function(t) { + self.$element.appendTo(t); + }, target); + }, + /** + * Render the current widget and prepends it to the given jQuery object or Widget. + * + * @param target A jQuery object or a Widget instance. + */ + prependTo: function(target) { + var self = this; + return this._render_and_insert(function(t) { + self.$element.prependTo(t); + }, target); + }, + /** + * Render the current widget and inserts it after to the given jQuery object or Widget. + * + * @param target A jQuery object or a Widget instance. + */ + insertAfter: function(target) { + var self = this; + return this._render_and_insert(function(t) { + self.$element.insertAfter(t); + }, target); + }, + /** + * Render the current widget and inserts it before to the given jQuery object or Widget. + * + * @param target A jQuery object or a Widget instance. + */ + insertBefore: function(target) { + var self = this; + return this._render_and_insert(function(t) { + self.$element.insertBefore(t); + }, target); + }, + _render_and_insert: function(insertion, target) { + var rendered = this.render(); + this.$element = $(rendered); + if (target instanceof openerp.base.Widget) + target = target.$element; + insertion(target); + return this.start(); + }, + /** + * Renders the widget using QWeb, `this.template` must be defined. + * The context given to QWeb contains the "widget" key that references `this`. + * + * @param {object} additional Additional context arguments to pass to the template. + */ + render: function (additional) { + return QWeb.render(this.template, _.extend({widget: this}, additional || {})); + }, + /** + * Method called after rendering. Mostly used to bind actions, perform asynchronous + * calls, etc... + * + * By convention, the method should return a promise to inform the caller when + * this widget has been initialized. + * + * @returns {jQuery.Deferred} + */ + start: function() { + if (!this.$element) { + var tmp = document.getElementById(this.element_id); + this.$element = tmp ? $(tmp) : undefined; + } + return $.Deferred().done().promise(); + }, + /** + * Destroys the current widget, also destory all its children before destroying itself. + */ + stop: function() { + _.each(_.clone(this.widget_children), function(el) { + el.stop(); + }); + if(this.$element != null) { + this.$element.remove(); + } + if (this.widget_parent && this.widget_parent.widget_children) { + this.widget_parent.widget_children = _.without(this.widget_parent.widget_children, this); + } + this.widget_parent = null; + this.widget_is_stopped = true; + }, + /** + * Inform the action manager to do an action. Of course, this suppose that + * the action manager can be found amongst the ancestors of the current widget. + * If that's not the case this method will simply return `false`. + */ + do_action: function(action, on_finished) { + if (this.widget_parent) { + return this.widget_parent.do_action(action, on_finished); + } + return false; + }, + rpc: function(url, data, success, error) { + var def = $.Deferred().then(success, error); + var self = this; + this._super(url, data). then(function() { + if (!self.widget_is_stopped) + def.resolve.apply(def, arguments); + }, function() { + if (!self.widget_is_stopped) + def.reject.apply(def, arguments); + }); + return def.promise(); + } +}); + +/** + * @deprecated + * For retro compatibility only, the only difference with is that render() uses + * directly `this` instead of context with a "widget" key. + */ +openerp.base.OldWidget = openerp.base.Widget.extend({ + render: function (additional) { + return QWeb.render(this.template, _.extend(_.extend({}, this), additional || {})); + } +}); + +} +// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/base/static/src/js/data.js b/addons/base/static/src/js/data.js index a8cdfb7f201..5545e32df17 100644 --- a/addons/base/static/src/js/data.js +++ b/addons/base/static/src/js/data.js @@ -1,7 +1,24 @@ openerp.base.data = function(openerp) { -openerp.base.DataGroup = openerp.base.Controller.extend( /** @lends openerp.base.DataGroup# */{ +/** + * Serializes the sort criterion array of a dataset into a form which can be + * consumed by OpenERP's RPC APIs. + * + * @param {Array} criterion array of fields, from first to last criteria, prefixed with '-' for reverse sorting + * @returns {String} SQL-like sorting string (``ORDER BY``) clause + */ +openerp.base.serialize_sort = function (criterion) { + return _.map(criterion, + function (criteria) { + if (criteria[0] === '-') { + return criteria.slice(1) + ' DESC'; + } + return criteria + ' ASC'; + }).join(', '); +}; + +openerp.base.DataGroup = openerp.base.Widget.extend( /** @lends openerp.base.DataGroup# */{ /** * Management interface between views and grouped collections of OpenERP * records. @@ -13,7 +30,7 @@ openerp.base.DataGroup = openerp.base.Controller.extend( /** @lends openerp.bas * content of the current grouping level. * * @constructs - * @extends openerp.base.Controller + * @extends openerp.base.Widget * * @param {openerp.base.Session} session Current OpenERP session * @param {String} model name of the model managed by this DataGroup @@ -22,18 +39,16 @@ openerp.base.DataGroup = openerp.base.Controller.extend( /** @lends openerp.bas * @param {Array} group_by sequence of fields by which to group * @param {Number} [level=0] nesting level of the group */ - init: function(session, model, domain, context, group_by, level) { + init: function(parent, model, domain, context, group_by, level) { + this._super(parent, null); if (group_by) { if (group_by.length || context['group_by_no_leaf']) { - return new openerp.base.ContainerDataGroup( - session, model, domain, context, group_by, level); + return new openerp.base.ContainerDataGroup( this, model, domain, context, group_by, level); } else { - return new openerp.base.GrouplessDataGroup( - session, model, domain, context, level); + return new openerp.base.GrouplessDataGroup( this, model, domain, context, level); } } - this._super(session, null); this.model = model; this.context = context; this.domain = domain; @@ -42,8 +57,7 @@ openerp.base.DataGroup = openerp.base.Controller.extend( /** @lends openerp.bas }, cls: 'DataGroup' }); -openerp.base.ContainerDataGroup = openerp.base.DataGroup.extend( - /** @lends openerp.base.ContainerDataGroup# */ { +openerp.base.ContainerDataGroup = openerp.base.DataGroup.extend( /** @lends openerp.base.ContainerDataGroup# */ { /** * * @constructs @@ -56,8 +70,8 @@ openerp.base.ContainerDataGroup = openerp.base.DataGroup.extend( * @param group_by * @param level */ - init: function (session, model, domain, context, group_by, level) { - this._super(session, model, domain, context, null, level); + init: function (parent, model, domain, context, group_by, level) { + this._super(parent, model, domain, context, null, level); this.group_by = group_by; }, @@ -120,29 +134,26 @@ openerp.base.ContainerDataGroup = openerp.base.DataGroup.extend( aggregates: aggregates }; }, - fetch: function () { + fetch: function (fields) { // internal method var d = new $.Deferred(); var self = this; - // disable caching for now, not sure what I should do there - if (false && this.groups) { - d.resolveWith(this, [this.groups]); - } else { - this.rpc('/base/group/read', { - model: this.model, - context: this.context, - domain: this.domain, - group_by_fields: this.group_by - }, function () { }).then(function (response) { - var data_groups = _(response.result).map( - _.bind(self.transform_group, self)); - self.groups = data_groups; - d.resolveWith(self, [data_groups]); - }, function () { - d.rejectWith.apply(d, self, [arguments]); - }); - } + this.rpc('/base/group/read', { + model: this.model, + context: this.context, + domain: this.domain, + fields: _.uniq(this.group_by.concat(fields)), + group_by_fields: this.group_by, + sort: openerp.base.serialize_sort(this.sort) + }, function () { }).then(function (response) { + var data_groups = _(response).map( + _.bind(self.transform_group, self)); + self.groups = data_groups; + d.resolveWith(self, [data_groups]); + }, function () { + d.rejectWith.apply(d, [self, arguments]); + }); return d.promise(); }, /** @@ -163,24 +174,27 @@ openerp.base.ContainerDataGroup = openerp.base.DataGroup.extend( * records have for the current ``grouped_on`` field name). * ``aggregates`` * a mapping of other aggregation fields provided by ``read_group`` + * + * @param {Array} fields the list of fields to aggregate in each group, can be empty + * @param {Function} ifGroups function executed if any group is found (DataGroup.group_by is non-null and non-empty), called with a (potentially empty) list of groups as parameters. + * @param {Function} ifRecords function executed if there is no grouping left to perform, called with a DataSet instance as parameter */ - list: function (ifGroups, ifRecords) { + list: function (fields, ifGroups, ifRecords) { var self = this; - this.fetch().then(function (group_records) { + this.fetch(fields).then(function (group_records) { ifGroups(_(group_records).map(function (group) { var child_context = _.extend({}, self.context, group.__context); return _.extend( new openerp.base.DataGroup( - self.session, self.model, group.__domain, + self, self.model, group.__domain, child_context, child_context.group_by, self.level + 1), - group); + group, {sort: self.sort}); })); }); } }); -openerp.base.GrouplessDataGroup = openerp.base.DataGroup.extend( - /** @lends openerp.base.GrouplessDataGroup# */ { +openerp.base.GrouplessDataGroup = openerp.base.DataGroup.extend( /** @lends openerp.base.GrouplessDataGroup# */ { /** * * @constructs @@ -192,16 +206,16 @@ openerp.base.GrouplessDataGroup = openerp.base.DataGroup.extend( * @param context * @param level */ - init: function (session, model, domain, context, level) { - this._super(session, model, domain, context, null, level); + init: function (parent, model, domain, context, level) { + this._super(parent, model, domain, context, null, level); }, - list: function (ifGroups, ifRecords) { - ifRecords(new openerp.base.DataSetSearch(this.session, this.model, this.context, this.domain)); + list: function (fields, ifGroups, ifRecords) { + ifRecords(_.extend( + new openerp.base.DataSetSearch(this, this.model), + {domain: this.domain, context: this.context, _sort: this.sort})); } }); - -openerp.base.StaticDataGroup = openerp.base.GrouplessDataGroup.extend( - /** @lends openerp.base.StaticDataGroup# */ { +openerp.base.StaticDataGroup = openerp.base.GrouplessDataGroup.extend( /** @lends openerp.base.StaticDataGroup# */ { /** * A specialization of groupless data groups, relying on a single static * dataset as its records provider. @@ -213,24 +227,25 @@ openerp.base.StaticDataGroup = openerp.base.GrouplessDataGroup.extend( init: function (dataset) { this.dataset = dataset; }, - list: function (ifGroups, ifRecords) { + list: function (fields, ifGroups, ifRecords) { ifRecords(this.dataset); } }); -openerp.base.DataSet = openerp.base.Controller.extend( /** @lends openerp.base.DataSet# */{ +openerp.base.DataSet = openerp.base.Widget.extend( /** @lends openerp.base.DataSet# */{ /** * DateaManagement interface between views and the collection of selected * OpenERP records (represents the view's state?) * * @constructs - * @extends openerp.base.Controller + * @extends openerp.base.Widget * - * @param {openerp.base.Session} session current OpenERP session * @param {String} model the OpenERP model this dataset will manage */ - init: function(session, model, context) { - this._super(session); + init: function(source_controller, model, context) { + // we don't want the dataset to be a child of anything! + this._super(null); + this.session = source_controller ? source_controller.session : undefined; this.model = model; this.context = context || {}; this.index = null; @@ -276,14 +291,18 @@ openerp.base.DataSet = openerp.base.Controller.extend( /** @lends openerp.base. * Read the indexed record. */ read_index: function (fields, callback) { + var def = $.Deferred().then(callback); if (_.isEmpty(this.ids)) { - return $.Deferred().reject().promise(); + def.reject(); } else { fields = fields || false; - return this.read_ids([this.ids[this.index]], fields, function(records) { - callback(records[0]); + return this.read_ids([this.ids[this.index]], fields).then(function(records) { + def.resolve(records[0]); + }, function() { + def.reject.apply(def, arguments); }); } + return def.promise(); }, default_get: function(fields, callback) { return this.rpc('/base/dataset/default_get', { @@ -362,26 +381,24 @@ openerp.base.DataSet = openerp.base.Controller.extend( /** @lends openerp.base. return this.context; } }); - openerp.base.DataSetStatic = openerp.base.DataSet.extend({ - init: function(session, model, context, ids) { - this._super(session, model, context); + init: function(parent, model, context, ids) { + this._super(parent, model, context); // all local records this.ids = ids || []; - if (this.ids.length) { - this.index = 0; - } }, read_slice: function (fields, offset, limit, callback) { var self = this; offset = offset || 0; var end_pos = limit && limit !== -1 ? offset + limit : undefined; - this.read_ids(this.ids.slice(offset, end_pos), fields, callback); + return this.read_ids(this.ids.slice(offset, end_pos), fields, callback); }, set_ids: function (ids) { this.ids = ids; - this.index = this.index <= this.ids.length - 1 ? - this.index : (this.ids.length > 0 ? this.length - 1 : 0); + if (this.index !== null) { + this.index = this.index <= this.ids.length - 1 ? + this.index : (this.ids.length > 0 ? this.length - 1 : 0); + } }, unlink: function(ids) { this.on_unlink(ids); @@ -391,10 +408,9 @@ openerp.base.DataSetStatic = openerp.base.DataSet.extend({ this.set_ids(_.without.apply(null, [this.ids].concat(ids))); } }); - openerp.base.DataSetSearch = openerp.base.DataSet.extend({ - init: function(session, model, context, domain) { - this._super(session, model, context); + init: function(parent, model, context, domain) { + this._super(parent, model, context); this.domain = domain || []; this._sort = []; this.offset = 0; @@ -413,7 +429,7 @@ openerp.base.DataSetSearch = openerp.base.DataSet.extend({ // return read_ids(ids.slice(start,start+limit),fields,callback) } } - this.rpc('/base/dataset/search_read', { + return this.rpc('/base/dataset/search_read', { model: this.model, fields: fields, domain: this.domain, @@ -421,13 +437,12 @@ openerp.base.DataSetSearch = openerp.base.DataSet.extend({ sort: this.sort(), offset: offset, limit: limit - }, function (records) { - self.ids.splice(0, self.ids.length); + }, function (result) { + self.ids = result.ids; self.offset = offset; - for (var i=0; i < records.length; i++ ) { - self.ids.push(records[i].id); + if (callback) { + callback(result.records); } - callback(records); }); }, /** @@ -445,16 +460,12 @@ openerp.base.DataSetSearch = openerp.base.DataSet.extend({ */ sort: function (field, force_reverse) { if (!field) { - return _.map(this._sort, function (criteria) { - if (criteria[0] === '-') { - return criteria.slice(1) + ' DESC'; - } - return criteria + ' ASC'; - }).join(', '); + return openerp.base.serialize_sort(this._sort); } - var reverse = force_reverse || (this._sort[0] === field); - this._sort = _.without(this._sort, field, '-' + field); + this._sort.splice.apply( + this._sort, [0, this._sort.length].concat( + _.without(this._sort, field, '-' + field))); this._sort.unshift((reverse ? '-' : '') + field); return undefined; @@ -463,14 +474,15 @@ openerp.base.DataSetSearch = openerp.base.DataSet.extend({ var self = this; return this._super(ids, function(result) { self.ids = _.without.apply(_, [self.ids].concat(ids)); - self.index = self.index <= self.ids.length - 1 ? - self.index : (self.ids.length > 0 ? self.ids.length -1 : 0); + if (this.index !== null) { + self.index = self.index <= self.ids.length - 1 ? + self.index : (self.ids.length > 0 ? self.ids.length -1 : 0); + } if (callback) callback(result); }, error_callback); } }); - openerp.base.BufferedDataSet = openerp.base.DataSetStatic.extend({ virtual_id_prefix: "one2many_v_id_", virtual_id_regex: /one2many_v_id_.*/, @@ -483,26 +495,34 @@ openerp.base.BufferedDataSet = openerp.base.DataSetStatic.extend({ var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data}; this.to_create.push(cached); this.cache.push(cached); - this.on_change(); var to_return = $.Deferred().then(callback); - setTimeout(function() {to_return.resolve({result: cached.id});}, 0); + to_return.resolve({result: cached.id}); return to_return.promise(); }, write: function (id, data, callback) { var self = this; var record = _.detect(this.to_create, function(x) {return x.id === id;}); record = record || _.detect(this.to_write, function(x) {return x.id === id;}); + var dirty = false; if (record) { + for (k in data) { + if (record.values[k] === undefined || record.values[k] !== data[k]) { + dirty = true; + break; + } + } $.extend(record.values, data); } else { + dirty = true; record = {id: id, values: data}; self.to_write.push(record); } var cached = _.detect(this.cache, function(x) {return x.id === id;}); $.extend(cached.values, record.values); - this.on_change(); + if (dirty) + this.on_change(); var to_return = $.Deferred().then(callback); - setTimeout(function () {to_return.resolve({result: true});}, 0); + to_return.resolve({result: true}); return to_return.promise(); }, unlink: function(ids, callback, error_callback) { @@ -576,7 +596,6 @@ openerp.base.BufferedDataSet = openerp.base.DataSetStatic.extend({ return completion.promise(); } }); - openerp.base.ReadOnlyDataSetSearch = openerp.base.DataSetSearch.extend({ create: function(data, callback, error_callback) { this.on_create(data); @@ -586,12 +605,12 @@ openerp.base.ReadOnlyDataSetSearch = openerp.base.DataSetSearch.extend({ }, on_create: function(data) {}, write: function (id, data, callback) { - this.on_write(id); + this.on_write(id, data); var to_return = $.Deferred().then(callback); setTimeout(function () {to_return.resolve({"result": true});}, 0); return to_return.promise(); }, - on_write: function(id) {}, + on_write: function(id, data) {}, unlink: function(ids, callback, error_callback) { this.on_unlink(ids); var to_return = $.Deferred().then(callback); diff --git a/addons/base/static/src/js/data_export.js b/addons/base/static/src/js/data_export.js new file mode 100644 index 00000000000..ad440302c69 --- /dev/null +++ b/addons/base/static/src/js/data_export.js @@ -0,0 +1,402 @@ +openerp.base.data_export = function(openerp) { +openerp.base.DataExport = openerp.base.Dialog.extend({ + init: function(parent, dataset) { + this._super(parent); + this.dataset = dataset; + }, + start: function() { + var self = this; + self._super(false); + self.template = 'ExportTreeView'; + self.dialog_title = "Export Data"; + self.open({ + modal: true, + width: '55%', + height: 'auto', + position: 'top', + buttons : { + "Close" : function() { + self.close(); + }, + "Export To File" : function() { + self.on_click_export_data(); + } + }, + close: function(event, ui){ self.close();} + }); + self.on_show_exists_export_list(); + self.$element.removeClass('ui-dialog-content ui-widget-content'); + self.$element.find('#add_field').click(function() { + if ($('#field-tree-structure tr.ui-selected')) { + var fld = self.$element.find('#field-tree-structure tr.ui-selected').find('a'); + for (var i = 0; i < fld.length; i++) { + var id = $(fld[i]).attr('id').split('-')[1]; + var string = $(fld[i]).attr('string'); + self.add_field(id, string); + } + self.$element.find('#field-tree-structure tr').removeClass('ui-selected'); + } + }); + self.$element.find('#remove_field').click(function() { + self.$element.find('#fields_list option:selected').remove(); + }); + self.$element.find('#remove_all_field').click(function() { + self.$element.find('#fields_list option').remove(); + }); + self.$element.find('#export_new_list').click(function() { + self.on_show_save_list(); + }); + var import_comp = self.$element.find('#import_compat option:selected').val(), + params = { + import_compat: parseInt(import_comp) + }; + self.rpc('/base/export/get_fields', { model: self.dataset.model, params: params }, self.on_show_data); + + self.$element.find('#import_compat').change(function() { + self.$element.find('#fields_list option').remove(); + self.$element.find('#field-tree-structure').remove(); + var import_comp = self.$element.find("#import_compat option:selected").val(); + if (import_comp) { + var params = { + import_compat: parseInt(import_comp) + } + self.rpc("/base/export/get_fields", { model: self.dataset.model, params: params}, self.on_show_data); + } + }); + }, + on_show_exists_export_list: function() { + var self = this; + if (self.$element.find('#saved_export_list').is(':hidden')) { + self.$element.find('#ExistsExportList').show(); + } else { + this.rpc('/base/export/exist_export_lists', { 'model': this.dataset.model}, function(export_list) { + if (export_list.length) { + self.$element.find('#ExistsExportList').append(QWeb.render('Exists.ExportList', {'existing_exports': export_list})); + self.$element.find('#saved_export_list').change(function() { + self.$element.find('#fields_list option').remove(); + var export_id = self.$element.find('#saved_export_list option:selected').val(); + if (export_id) { + self.rpc('/base/export/namelist', {'model': self.dataset.model, export_id: parseInt(export_id)}, self.do_load_export_field); + } + }); + self.$element.find('#delete_export_list').click(function() { + var select_exp = self.$element.find('#saved_export_list option:selected'); + if (select_exp.val()) { + self.rpc('/base/export/delete_export', { export_id: parseInt(select_exp.val())}, {}); + select_exp.remove(); + if (self.$element.find('#saved_export_list option').length <= 1) { + self.$element.find('#ExistsExportList').hide(); + } + } + }); + } + }); + } + }, + do_load_export_field: function(field_list) { + var export_node = this.$element.find("#fields_list"); + for (var key in field_list) { + export_node.append(new Option(field_list[key], key)); + } + }, + on_show_save_list: function() { + var self = this; + var current_node = self.$element.find("#savenewlist"); + if (!(current_node.find("label")).length) { + current_node.append(QWeb.render('ExportNewList')); + current_node.find("#add_export_list").click(function() { + var value = current_node.find("#savelist_name").val(); + if (value) { + self.do_save_export_list(value); + } else { + alert("Pleae Enter Save Field List Name"); + } + }); + } else { + if (current_node.is(':hidden')) { + current_node.show(); + current_node.find("#savelist_name").val(""); + } else { + current_node.hide(); + } + } + }, + do_save_export_list: function(value) { + var self = this; + var export_field = self.get_fields(); + if (export_field.length) { + self.rpc("/base/export/save_export_lists", {"model": self.dataset.model, "name":value, "field_list":export_field}, function(exp_id) { + if (exp_id) { + if (self.$element.find("#saved_export_list").length > 0) { + self.$element.find("#saved_export_list").append(new Option(value, exp_id)); + } else { + self.on_show_exists_export_list(); + } + if (self.$element.find("#saved_export_list").is(":hidden")) { + self.on_show_exists_export_list(); + } + } + }); + self.on_show_save_list(); + self.$element.find("#fields_list option").remove(); + } + }, + on_click: function(id, result) { + var self = this; + self.field_id = id.split("-")[1]; + var is_loaded = 0; + _.each(result, function(record) { + if (record['id'] == self.field_id && (record['children']).length >= 1) { + var model = record['params']['model'], + prefix = record['params']['prefix'], + name = record['params']['name']; + $(record['children']).each(function(e, childid) { + if (self.$element.find("tr[id='treerow-" + childid + "']").length > 0) { + if (self.$element.find("tr[id='treerow-" + childid + "']").is(':hidden')) { + is_loaded = -1; + } else { + is_loaded++; + } + } + }); + if (is_loaded == 0) { + if (self.$element.find("tr[id='treerow-" + self.field_id +"']").find('img').attr('src') === '/base/static/src/img/expand.gif') { + if (model) { + var import_comp = self.$element.find("#import_compat option:selected").val(); + var params = { + import_compat: parseInt(import_comp), + parent_field_type : record['field_type'] + } + self.rpc("/base/export/get_fields", { + model: model, + prefix: prefix, + name: name, + field_parent : self.field_id, + params: params + }, function(results) { + self.on_show_data(results); + }); + } + } + } else if (is_loaded > 0) { + self.showcontent(self.field_id, true); + } else { + self.showcontent(self.field_id, false); + } + } + }); + }, + on_show_data: function(result) { + var self = this; + var imp_cmpt = parseInt(self.$element.find("#import_compat option:selected").val()); + var current_tr = self.$element.find("tr[id='treerow-" + self.field_id + "']"); + if (current_tr.length >= 1) { + current_tr.find('img').attr('src','/base/static/src/img/collapse.gif'); + current_tr.after(QWeb.render('ExportTreeView-Secondary.children', {'fields': result})); + } else { + self.$element.find('#left_field_panel').append(QWeb.render('ExportTreeView-Secondary', {'fields': result})); + } + _.each(result, function(record) { + if ((record.field_type == "one2many") && imp_cmpt) { + var o2m_fld = self.$element.find("tr[id='treerow-" + record.id + "']").find('#tree-column'); + o2m_fld.addClass("oe_export_readonlyfield"); + } + if ((record.required == true) || record.required == "True") { + var required_fld = self.$element.find("tr[id='treerow-" + record.id + "']").find('#tree-column'); + required_fld.addClass("oe_export_requiredfield"); + } + self.$element.find("img[id='parentimg-" + record.id +"']").click(function() { + self.on_click(this.id, result); + }); + + self.$element.find("tr[id='treerow-" + record.id + "']").click(function(e) { + if (e.shiftKey == true) { + var frst_click, scnd_click = ''; + if (self.row_index == 0) { + self.row_index = this.rowIndex; + frst_click = self.$element.find("tr[id^='treerow-']")[self.row_index-1]; + $(frst_click).addClass("ui-selected"); + } else { + if (this.rowIndex >=self.row_index) { + for (i = (self.row_index-1); i < this.rowIndex; i++) { + scnd_click = self.$element.find("tr[id^='treerow-']")[i]; + if (!$(scnd_click).find('#tree-column').hasClass("oe_export_readonlyfield")) { + $(scnd_click).addClass("ui-selected"); + } + } + } else { + for (i = (self.row_index-1); i >= (this.rowIndex-1); i--) { + scnd_click = self.$element.find("tr[id^='treerow-']")[i]; + if (!$(scnd_click).find('#tree-column').hasClass("oe_export_readonlyfield")) { + $(scnd_click).addClass("ui-selected"); + } + } + } + } + } + self.row_index = this.rowIndex; + + self.$element.find("tr[id='treerow-" + record.id + "']").keyup(function(e) { + self.row_index = 0; + }); + var o2m_selection = self.$element.find("tr[id='treerow-" + record.id + "']").find('#tree-column'); + if ($(o2m_selection).hasClass("oe_export_readonlyfield")) { + return false; + } + var selected = self.$element.find("tr.ui-selected"); + if ($(this).hasClass("ui-selected") && (e.ctrlKey == true)) { + $(this).find('a').blur(); + $(this).removeClass("ui-selected"); + } else if ($(this).hasClass("ui-selected") && (e.ctrlKey == false) && (e.shiftKey == false)) { + selected.find('a').blur(); + selected.removeClass("ui-selected"); + $(this).find('a').focus(); + $(this).addClass("ui-selected"); + } else if (!$(this).hasClass("ui-selected") && (e.ctrlKey == false) && (e.shiftKey == false)) { + selected.find('a').blur(); + selected.removeClass("ui-selected"); + $(this).find('a').focus(); + $(this).addClass("ui-selected"); + } else if (!$(this).hasClass("ui-selected") && (e.ctrlKey == true)) { + $(this).find('a').focus(); + $(this).addClass("ui-selected"); + } + return false; + }); + + self.$element.find("tr[id='treerow-" + record.id + "']").keydown(function(e) { + var keyCode = e.keyCode || e.which; + arrow = {left: 37, up: 38, right: 39, down: 40 }; + switch (keyCode) { + case arrow.left: + if ($(this).find('img').attr('src') === '/base/static/src/img/collapse.gif') { + self.on_click(this.id, result); + } + break; + case arrow.up: + var elem = this; + $(elem).removeClass("ui-selected"); + while ($(elem).prev().is(":visible") == false) { + elem = $(elem).prev(); + } + if (!$(elem).prev().find('#tree-column').hasClass("oe_export_readonlyfield")) { + $(elem).prev().addClass("ui-selected"); + } + $(elem).prev().find('a').focus(); + break; + case arrow.right: + if ($(this).find('img').attr('src') == '/base/static/src/img/expand.gif') { + self.on_click(this.id, result); + } + break; + case arrow.down: + var elem = this; + $(elem).removeClass("ui-selected"); + while($(elem).next().is(":visible") == false) { + elem = $(elem).next(); + } + if (!$(elem).next().find('#tree-column').hasClass("oe_export_readonlyfield")) { + $(elem).next().addClass("ui-selected"); + } + $(elem).next().find('a').focus(); + break; + } + }); + self.$element.find("tr[id='treerow-" + record.id + "']").dblclick(function(e) { + var $o2m_selection = self.$element.find("tr[id^='treerow-" + record.id + "']").find('#tree-column'); + if (!$o2m_selection.hasClass("oe_export_readonlyfield")) { + var field_id = $(this).find("a").attr("id"); + if (field_id) { + self.add_field(field_id.split('-')[1], $(this).find("a").attr("string")); + } + } + }); + }); + self.$element.find('#fields_list').mouseover(function(event) { + if (event.relatedTarget) { + if (event.relatedTarget.attributes['id'] && event.relatedTarget.attributes['string']) { + var field_id = event.relatedTarget.attributes["id"]["value"]; + if (field_id && field_id.split("-")[0] === 'export') { + if (!self.$element.find("tr[id='treerow-" + field_id.split("-")[1] + "']").find('#tree-column').hasClass("oe_export_readonlyfield")) { + self.add_field(field_id.split("-")[1], event.relatedTarget.attributes["string"]["value"]); + } + } + } + } + }); + }, + showcontent: function(id, flag) { + // show & hide the contents + var first_child = this.$element.find("tr[id='treerow-" + id + "']").find('img'); + if (flag) { + first_child.attr('src', '/base/static/src/img/expand.gif'); + } + else { + first_child.attr('src', '/base/static/src/img/collapse.gif'); + } + var child_field = this.$element.find("tr[id^='treerow-" + id +"/']"); + var child_len = (id.split("/")).length + 1; + for (var i = 0; i < child_field.length; i++) { + if (flag) { + $(child_field[i]).hide(); + } else { + if (child_len == (child_field[i].id.split("/")).length) { + if ($(child_field[i]).find('img').attr('src') == '/base/static/src/img/collapse.gif') { + $(child_field[i]).find('img').attr('src', '/base/static/src/img/expand.gif'); + } + $(child_field[i]).show(); + } + } + } + }, + add_field: function(field_id, string) { + var field_list = this.$element.find('#fields_list'); + if (this.$element.find("#fields_list option[value='" + field_id + "']") && !this.$element.find("#fields_list option[value='" + field_id + "']").length) { + field_list.append(new Option(string, field_id)); + } + }, + get_fields: function() { + var export_field = []; + this.$element.find("#fields_list option").each(function() { + export_field.push($(this).val()); + }); + if (!export_field.length) { + alert('Please select fields to save export list...'); + } + return export_field; + }, + on_click_export_data: function() { + var self = this; + var export_field = {}; + var flag = true; + self.$element.find("#fields_list option").each(function() { + export_field[$(this).val()] = $(this).text(); + flag = false; + }); + if (flag) { + alert('Please select fields to export...'); + return; + } + + var import_comp = self.$element.find("#import_compat option:selected").val(), + export_format = self.$element.find("#export_format").val(); + + self.rpc("/base/export/export_data", { + model: self.dataset.model, + fields: export_field, + ids: self.dataset.ids, + domain: self.dataset.domain, + import_compat: parseInt(import_comp), + export_format: export_format + }, function(data) { + window.location = "data:text/csv/excel;charset=utf8," + data; + self.close(); + }); + }, + close: function() { + $(this.$dialog).remove(); + this._super(); + } +}); + +}; diff --git a/addons/base/static/src/js/form.js b/addons/base/static/src/js/form.js index e4cc0172789..f8e2e91667d 100644 --- a/addons/base/static/src/js/form.js +++ b/addons/base/static/src/js/form.js @@ -1,7 +1,7 @@ openerp.base.form = function (openerp) { openerp.base.views.add('form', 'openerp.base.FormView'); -openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormView# */{ +openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormView# */{ /** * Indicates that this view is not searchable, and thus that no search * view should be displayed (if there is one active). @@ -17,9 +17,9 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV * * @property {openerp.base.Registry} registry=openerp.base.form.widgets widgets registry for this form view instance */ - init: function(view_manager, session, element_id, dataset, view_id) { - this._super(session, element_id); - this.view_manager = view_manager || new openerp.base.NullViewManager(); + init: function(parent, element_id, dataset, view_id, options) { + this._super(parent, element_id); + this.set_default_options(); this.dataset = dataset; this.model = dataset.model; this.view_id = view_id; @@ -30,27 +30,37 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV this.datarecord = {}; this.ready = false; this.show_invalid = true; - this.touched = false; - this.flags = this.view_manager.flags || {}; + this.dirty = false; this.default_focus_field = null; this.default_focus_button = null; this.registry = openerp.base.form.widgets; this.has_been_loaded = $.Deferred(); this.$form_header = null; + this.options = options || {}; + _.defaults(this.options, {"always_show_new_button": true}); }, start: function() { //this.log('Starting FormView '+this.model+this.view_id) if (this.embedded_view) { - return $.Deferred().then(this.on_loaded).resolve({fields_view: this.embedded_view}); + var def = $.Deferred().then(this.on_loaded); + var self = this; + setTimeout(function() {def.resolve({fields_view: self.embedded_view});}, 0); + return def.promise(); } else { - var context = new openerp.base.CompoundContext(this.dataset.context); - if (this.view_manager.action && this.view_manager.action.context) { - context.add(this.view_manager.action.context); - } + var context = new openerp.base.CompoundContext(this.dataset.get_context()); return this.rpc("/base/formview/load", {"model": this.model, "view_id": this.view_id, - toolbar:!!this.flags.sidebar, context: context}, this.on_loaded); + toolbar: this.options.sidebar, context: context}, this.on_loaded); } }, + stop: function() { + if (this.sidebar) { + this.sidebar.attachments.stop(); + this.sidebar.stop(); + } + _.each(this.widgets, function(w) { + w.stop(); + }); + }, on_loaded: function(data) { var self = this; this.fields_view = data.fields_view; @@ -71,22 +81,39 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV this.$form_header.find('button.oe_form_button_cancel').click(this.do_cancel); this.$form_header.find('button.oe_form_button_new').click(this.on_button_new); - this.view_manager.sidebar.set_toolbar(data.fields_view.toolbar); + this.$form_header.find('button.oe_get_xml_view').click(function() { + $('' + openerp.base.json_node_to_xml(self.fields_view.arch, true) + '').dialog({ width: '95%', height: 600}); + }); + + if (this.options.sidebar && this.options.sidebar_id) { + this.sidebar = new openerp.base.Sidebar(this, this.options.sidebar_id); + this.sidebar.start(); + this.sidebar.do_unfold(); + this.sidebar.attachments = new openerp.base.form.SidebarAttachments(this.sidebar, this.sidebar.add_section('attachments', "Attachments"), this); + this.sidebar.add_toolbar(data.fields_view.toolbar); + this.set_common_sidebar_sections(this.sidebar); + } this.has_been_loaded.resolve(); }, do_show: function () { - var self = this; + var promise; if (this.dataset.index === null) { // null index means we should start a new record - this.on_button_new(); + promise = this.on_button_new(); } else { - this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded); + promise = this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded); } - self.$element.show(); - this.view_manager.sidebar.do_refresh(true); + this.$element.show(); + if (this.sidebar) { + this.sidebar.$element.show(); + } + return promise; }, do_hide: function () { this.$element.hide(); + if (this.sidebar) { + this.sidebar.$element.hide(); + } }, on_record_loaded: function(record) { if (!record) { @@ -95,28 +122,30 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV if (!record.id) { this.$form_header.find('.oe_form_on_create').show(); this.$form_header.find('.oe_form_on_update').hide(); - this.$form_header.find('button.oe_form_button_new').hide(); + if (!this.options["always_show_new_button"]) { + this.$form_header.find('button.oe_form_button_new').hide(); + } } else { this.$form_header.find('.oe_form_on_create').hide(); this.$form_header.find('.oe_form_on_update').show(); this.$form_header.find('button.oe_form_button_new').show(); } - this.touched = false; + this.dirty = false; this.datarecord = record; for (var f in this.fields) { var field = this.fields[f]; - field.touched = false; + field.dirty = false; field.set_value(this.datarecord[f] || false); field.validate(); } if (!record.id) { // New record: Second pass in order to trigger the onchanges - this.touched = true; + this.dirty = true; this.show_invalid = false; for (var f in record) { var field = this.fields[f]; if (field) { - field.touched = true; + field.dirty = true; this.do_onchange(field); } } @@ -124,7 +153,9 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV this.on_form_changed(); this.show_invalid = this.ready = true; this.do_update_pager(record.id == null); - this.do_update_sidebar(); + if (this.sidebar) { + this.sidebar.attachments.do_update(); + } if (this.default_focus_field) { this.default_focus_field.focus(); } @@ -132,7 +163,7 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV on_form_changed: function() { for (var w in this.widgets) { w = this.widgets[w]; - w.process_attrs(); + w.process_modifiers(); w.update_dom(); } }, @@ -166,19 +197,24 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV this.ready = false; var onchange = _.trim(widget.node.attrs.on_change); var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/); - console.log("Onchange triggered for field '%s' -> %s", widget.name, onchange); if (call) { var method = call[1], args = []; + var context_index = null; var argument_replacement = { - 'False' : false, - 'True' : true, - 'None' : null + 'False' : function() {return false;}, + 'True' : function() {return true;}, + 'None' : function() {return null;}, + 'context': function(i) { + context_index = i; + var ctx = widget.build_context ? widget.build_context() : {}; + return ctx; + } } var parent_fields = null; - _.each(call[2].split(','), function(a) { + _.each(call[2].split(','), function(a, i) { var field = _.trim(a); if (field in argument_replacement) { - args.push(argument_replacement[field]); + args.push(argument_replacement[field](i)); return; } else if (self.fields[field]) { var value = self.fields[field].get_on_change_value(); @@ -192,7 +228,7 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV } var p_val = parent_fields[_.trim(splitted[1])]; if (p_val !== undefined) { - args.push(value ? value : false); + args.push(p_val == null ? false : p_val); return; } } @@ -207,7 +243,8 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV return this.rpc(ajax, { model: this.dataset.model, method: method, - args: [(this.datarecord.id == null ? [] : [this.datarecord.id])].concat(args) + args: [(this.datarecord.id == null ? [] : [this.datarecord.id])].concat(args), + context_id: context_index === null ? null : context_index + 1 }, function(response) { self.on_processed_onchange(response, processed); }); @@ -217,28 +254,26 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV } }, on_processed_onchange: function(response, processed) { - var result = response.result; + var result = response; if (result.value) { - console.log(" |-> Onchange Response :", result.value); for (var f in result.value) { var field = this.fields[f]; + // If field is not defined in the view, just ignore it if (field) { var value = result.value[f]; processed.push(field.name); if (field.get_value() != value) { - console.log(" |-> Onchange Action : change '%s' value from '%s' to '%s'", field.name, field.get_value(), value); field.set_value(value); + field.dirty = true; if (_.indexOf(processed, field.name) < 0) { this.do_onchange(field, processed); } } - } else { - this.log("warning : on_processed_onchange can't find field " + field, result); } } this.on_form_changed(); } - if (result.warning) { + if (!_.isEmpty(result.warning)) { $(QWeb.render("DialogWarning", result.warning)).dialog({ modal: true, buttons: { @@ -249,17 +284,20 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV }); } if (result.domain) { - // Will be removed ? + // TODO: } this.ready = true; }, on_button_new: function() { var self = this; + var def = $.Deferred(); $.when(this.has_been_loaded).then(function() { - self.dataset.default_get(_.keys(self.fields_view.fields), function(result) { - self.on_record_loaded(result.result); - }); + self.dataset.default_get( + _.keys(self.fields_view.fields)).then(self.on_record_loaded).then(function() { + def.resolve(); + }); }); + return def.promise(); }, /** * Triggers saving the form's record. Chooses between creating a new @@ -274,37 +312,40 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV if (!this.ready) { return false; } - var invalid = false, + var form_dirty = false, + form_invalid = false, values = {}, first_invalid_field = null; for (var f in this.fields) { f = this.fields[f]; - if (f.invalid) { - invalid = true; + if (!f.is_valid()) { + form_invalid = true; f.update_dom(); if (!first_invalid_field) { first_invalid_field = f; } - } else if (f.touched) { + } else if (f.is_dirty()) { + form_dirty = true; values[f.name] = f.get_value(); } } - if (invalid) { + if (form_invalid) { first_invalid_field.focus(); this.on_invalid(); return false; - } else { + } else if (form_dirty) { this.log("About to save", values); if (!this.datarecord.id) { - this.dataset.create(values, function(r) { + return this.dataset.create(values, function(r) { self.on_created(r, success, prepend_on_create); }); } else { - this.dataset.write(this.datarecord.id, values, function(r) { + return this.dataset.write(this.datarecord.id, values, function(r) { self.on_saved(r, success); }); } - return true; + } else { + return false; } }, do_save_edit: function() { @@ -318,7 +359,7 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV on_invalid: function() { var msg = "
    "; _.each(this.fields, function(f) { - if (f.invalid) { + if (!f.is_valid()) { msg += "
  • " + f.string + "
  • "; } }); @@ -362,7 +403,9 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV this.dataset.index = 0; } this.do_update_pager(); - this.do_update_sidebar(); + if (this.sidebar) { + this.sidebar.attachments.do_update(); + } this.notification.notify("Record created", "The record has been created with id #" + this.datarecord.id); if (success) { success(_.extend(r, {created: true})); @@ -379,34 +422,55 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV do_cancel: function () { this.notification.notify("Cancelling form"); }, - do_update_sidebar: function() { - if (this.flags.sidebar === false) { - return; + reload: function() { + if (this.datarecord.id) { + this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded); + } else { + this.on_button_new(); } - if (!this.datarecord.id) { + }, + get_fields_values: function() { + var values = {}; + _.each(this.fields, function(value, key) { + var val = value.get_value(); + values[key] = val; + }); + return values; + } +}); + +/** @namespace */ +openerp.base.form = {}; + +openerp.base.form.SidebarAttachments = openerp.base.Widget.extend({ + init: function(parent, element_id, form_view) { + this._super(parent, element_id); + this.view = form_view; + }, + do_update: function() { + if (!this.view.datarecord.id) { this.on_attachments_loaded([]); } else { - // TODO fme: modify this so it doesn't try to load attachments when there is not sidebar - /*this.rpc('/base/dataset/search_read', { - model: 'ir.attachment', - fields: ['name', 'url', 'type'], - domain: [['res_model', '=', this.dataset.model], ['res_id', '=', this.datarecord.id], ['type', 'in', ['binary', 'url']]], - context: this.dataset.context - }, this.on_attachments_loaded);*/ + (new openerp.base.DataSetSearch( + this, 'ir.attachment', this.view.dataset.get_context(), + [['res_model', '=', this.view.dataset.model], + ['res_id', '=', this.view.datarecord.id], + ['type', 'in', ['binary', 'url']]])).read_slice( + ['name', 'url', 'type'], false, false, + this.on_attachments_loaded); } }, on_attachments_loaded: function(attachments) { - this.$sidebar = this.view_manager.sidebar.$element.find('.sidebar-attachments'); this.attachments = attachments; - this.$sidebar.html(QWeb.render('FormView.sidebar.attachments', this)); - this.$sidebar.find('.oe-sidebar-attachment-delete').click(this.on_attachment_delete); - this.$sidebar.find('.oe-binary-file').change(this.on_attachment_changed); + this.$element.html(QWeb.render('FormView.sidebar.attachments', this)); + this.$element.find('.oe-binary-file').change(this.on_attachment_changed); + this.$element.find('.oe-sidebar-attachment-delete').click(this.on_attachment_delete); }, on_attachment_changed: function(e) { - window[this.element_id + '_iframe'] = this.do_update_sidebar; + window[this.element_id + '_iframe'] = this.do_update; var $e = $(e.target); if ($e.val() != '') { - this.$sidebar.find('form.oe-binary-form').submit(); + this.$element.find('form.oe-binary-form').submit(); $e.parent().find('input[type=file]').attr('disabled', 'true'); $e.parent().find('button').attr('disabled', 'true').find('img, span').toggle(); } @@ -423,26 +487,9 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV self.notification.notify("Delete an attachment", "The attachment '" + name + "' has been deleted"); }); } - }, - reload: function() { - if (this.datarecord.id) { - this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded); - } else { - this.on_button_new(); - } - }, - get_fields_values: function() { - var values = {}; - _.each(this.fields, function(value, key) { - values[key] = value.get_value(); - }); - return values; } }); -/** @namespace */ -openerp.base.form = {}; - openerp.base.form.compute_domain = function(expr, fields) { var stack = []; for (var i = expr.length - 1; i >= 0; i--) { @@ -496,23 +543,23 @@ openerp.base.form.compute_domain = function(expr, fields) { stack.push(!_(val).contains(field)); break; default: - this.log("Unsupported operator in attrs :", op); + this.log("Unsupported operator in modifiers :", op); } } return _.all(stack); }; -openerp.base.form.Widget = openerp.base.Controller.extend({ +openerp.base.form.Widget = openerp.base.Widget.extend({ template: 'Widget', init: function(view, node) { this.view = view; this.node = node; - this.attrs = JSON.parse(this.node.attrs.attrs || '{}'); + this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}'); this.type = this.type || node.tag; this.element_name = this.element_name || this.type; this.element_id = [this.view.element_id, this.element_name, this.view.widgets_counter++].join("_"); - this._super(this.view.session, this.element_id); + this._super(view, this.element_id); this.view.widgets[this.element_id] = this; this.children = node.children; @@ -520,15 +567,18 @@ openerp.base.form.Widget = openerp.base.Controller.extend({ this.string = this.string || node.attrs.string; this.help = this.help || node.attrs.help; - this.invisible = (node.attrs.invisible == '1'); + this.invisible = this.modifiers['invisible'] === true; }, start: function() { this.$element = $('#' + this.element_id); }, - process_attrs: function() { + stop: function() { + this.$element.remove(); + }, + process_modifiers: function() { var compute_domain = openerp.base.form.compute_domain; - for (var a in this.attrs) { - this[a] = compute_domain(this.attrs[a], this.view.fields); + for (var a in this.modifiers) { + this[a] = compute_domain(this.modifiers[a], this.view.fields); } }, update_dom: function() { @@ -583,7 +633,10 @@ openerp.base.form.WidgetFrame = openerp.base.form.Widget.extend({ } }, handle_node: function(node) { - var type = this.view.fields_view.fields[node.attrs.name] || {}; + var type = {}; + if (node.tag == 'field') { + type = this.view.fields_view.fields[node.attrs.name] || {}; + } var widget = new (this.view.registry.get_any( [node.attrs.widget, type.type, node.tag])) (this.view, node); if (node.tag == 'field') { @@ -593,14 +646,15 @@ openerp.base.form.WidgetFrame = openerp.base.form.Widget.extend({ if (node.attrs.nolabel != '1') { var label = new (this.view.registry.get_object('label')) (this.view, node); label["for"] = widget; - this.add_widget(label); + this.add_widget(label, widget.colspan + 1); } } this.add_widget(widget); }, - add_widget: function(widget) { + add_widget: function(widget, colspan) { + colspan = colspan || widget.colspan; var current_row = this.table[this.table.length - 1]; - if (current_row.length && (this.x + widget.colspan) > this.columns) { + if (current_row.length && (this.x + colspan) > this.columns) { current_row = this.add_row(); } current_row.push(widget); @@ -610,14 +664,14 @@ openerp.base.form.WidgetFrame = openerp.base.form.Widget.extend({ }); openerp.base.form.WidgetNotebook = openerp.base.form.Widget.extend({ + template: 'WidgetNotebook', init: function(view, node) { this._super(view, node); - this.template = "WidgetNotebook"; this.pages = []; for (var i = 0; i < node.children.length; i++) { var n = node.children[i]; if (n.tag == "page") { - var page = new openerp.base.form.WidgetFrame(this.view, n); + var page = new openerp.base.form.WidgetNotebookPage(this.view, n, this, this.pages.length); this.pages.push(page); } } @@ -628,6 +682,28 @@ openerp.base.form.WidgetNotebook = openerp.base.form.Widget.extend({ } }); +openerp.base.form.WidgetNotebookPage = openerp.base.form.WidgetFrame.extend({ + template: 'WidgetNotebookPage', + init: function(view, node, notebook, index) { + this.notebook = notebook; + this.index = index; + this.element_name = 'page_' + index; + this._super(view, node); + this.element_tab_id = this.element_id + '_tab'; + }, + start: function() { + this._super.apply(this, arguments); + this.$element_tab = $('#' + this.element_tab_id); + }, + update_dom: function() { + if (this.invisible) { + this.notebook.$element.tabs('select', 0); + } + this.$element_tab.toggle(!this.invisible); + this.$element.toggle(!this.invisible); + } +}); + openerp.base.form.WidgetSeparator = openerp.base.form.Widget.extend({ init: function(view, node) { this._super(view, node); @@ -639,6 +715,10 @@ openerp.base.form.WidgetButton = openerp.base.form.Widget.extend({ init: function(view, node) { this._super(view, node); this.template = "WidgetButton"; + if (this.string) { + // We don't have button key bindings in the webclient + this.string = this.string.replace(/_/g, ''); + } if (node.attrs.default_focus == '1') { // TODO fme: provide enter key binding to widgets this.view.default_focus_button = this; @@ -650,7 +730,7 @@ openerp.base.form.WidgetButton = openerp.base.form.Widget.extend({ }, on_click: function(saved) { var self = this; - if (!this.node.attrs.special && this.view.touched && saved !== true) { + if (!this.node.attrs.special && this.view.dirty && saved !== true) { this.view.do_save(function() { self.on_click(true); }); @@ -679,10 +759,7 @@ openerp.base.form.WidgetButton = openerp.base.form.Widget.extend({ this.view.execute_action( this.node.attrs, this.view.dataset, this.session.action_manager, - this.view.datarecord.id, function (result) { - self.log("Button returned", result); - self.view.reload(); - }, function() { + this.view.datarecord.id, function () { self.view.reload(); }); } @@ -698,11 +775,12 @@ openerp.base.form.WidgetLabel = openerp.base.form.Widget.extend({ if (this.node.tag == 'label' && this.node.attrs.colspan) { this.is_field_label = false; this.template = "WidgetParagraph"; + this.colspan = this.node.attrs.colspan; } else { this.is_field_label = true; this.template = "WidgetLabel"; + this.colspan = 1; } - this.colspan = 1; }, render: function () { if (this['for'] && this.type !== 'label') { @@ -729,12 +807,11 @@ openerp.base.form.Field = openerp.base.form.Widget.extend({ this.field = view.fields_view.fields[node.attrs.name] || {}; this.string = node.attrs.string || this.field.string; this.help = node.attrs.help || this.field.help; - this.invisible = (this.invisible || this.field.invisible == '1'); - this.nolabel = (this.field.nolabel || node.attrs.nolabel) == '1'; - this.readonly = (this.field.readonly || node.attrs.readonly) == '1'; - this.required = (this.field.required || node.attrs.required) == '1'; + this.nolabel = (this.field.nolabel || node.attrs.nolabel) === '1'; + this.readonly = this.modifiers['readonly'] === true; + this.required = this.modifiers['required'] === true; this.invalid = false; - this.touched = false; + this.dirty = false; }, set_value: function(value) { this.value = value; @@ -747,21 +824,29 @@ openerp.base.form.Field = openerp.base.form.Widget.extend({ get_value: function() { return this.value; }, + is_valid: function() { + return !this.invalid; + }, + is_dirty: function() { + return this.dirty; + }, get_on_change_value: function() { return this.get_value(); }, update_dom: function() { this._super.apply(this, arguments); - this.$element.toggleClass('disabled', this.readonly); - this.$element.toggleClass('required', this.required); - if (this.view.show_invalid) { - this.$element.toggleClass('invalid', this.invalid); + if (!this.disable_utility_classes) { + this.$element.toggleClass('disabled', this.readonly); + this.$element.toggleClass('required', this.required); + if (this.view.show_invalid) { + this.$element.toggleClass('invalid', !this.is_valid()); + } } }, on_ui_change: function() { - this.touched = this.view.touched = true; + this.dirty = this.view.dirty = true; this.validate(); - if (!this.invalid) { + if (this.is_valid()) { this.set_value_from_ui(); this.view.do_onchange(this); this.view.on_form_changed(); @@ -773,6 +858,43 @@ openerp.base.form.Field = openerp.base.form.Widget.extend({ this.invalid = false; }, focus: function() { + }, + _build_view_fields_values: function() { + var a_dataset = this.view.dataset || {}; + var fields_values = this.view.get_fields_values(); + var parent_values = a_dataset.parent_view ? a_dataset.parent_view.get_fields_values() : {}; + fields_values.parent = parent_values; + return fields_values; + }, + /** + * Builds a new context usable for operations related to fields by merging + * the fields'context with the action's context. + */ + build_context: function() { + // I previously belevied contexts should be herrited, but now I doubt it + //var a_context = this.view.dataset.get_context() || {}; + var f_context = this.field.context || null; + // maybe the default_get should only be used when we do a default_get? + var v_context1 = this.node.attrs.default_get || {}; + var v_context2 = this.node.attrs.context || {}; + var v_context = new openerp.base.CompoundContext(v_context1, v_context2); + if (v_context1.__ref || v_context2.__ref || true) { //TODO niv: remove || true + var fields_values = this._build_view_fields_values(); + v_context.set_eval_context(fields_values); + } + // if there is a context on the node, overrides the model's context + var ctx = f_context || v_context; + return ctx; + }, + build_domain: function() { + var f_domain = this.field.domain || null; + var v_domain = this.node.attrs.domain || []; + if (!(v_domain instanceof Array) || true) { //TODO niv: remove || true + var fields_values = this._build_view_fields_values(); + v_domain = new openerp.base.CompoundDomain(v_domain).set_eval_context(fields_values); + } + // if there is a domain on the node, overrides the model's domain + return f_domain || v_domain; } }); @@ -822,7 +944,7 @@ openerp.base.form.FieldEmail = openerp.base.form.FieldChar.extend({ this.$element.find('button').click(this.on_button_clicked); }, on_button_clicked: function() { - if (!this.value || this.invalid) { + if (!this.value || !this.is_valid()) { this.notification.warn("E-mail error", "Can't send email to invalid e-mail address"); } else { location.href = 'mailto:' + this.value; @@ -863,6 +985,7 @@ openerp.base.form.FieldFloat = openerp.base.form.FieldChar.extend({ if (value === false || value === undefined) { // As in GTK client, floats default to 0 value = 0; + this.dirty = true; } var show_value = value.toFixed(2); this.$element.find('input').val(show_value); @@ -872,22 +995,47 @@ openerp.base.form.FieldFloat = openerp.base.form.FieldChar.extend({ } }); +openerp.base.form.FieldInteger = openerp.base.form.FieldFloat.extend({ + init: function(view, node) { + this._super(view, node); + this.validation_regex = /^-?\d+$/; + }, + set_value: function(value) { + this._super.apply(this, [value]); + if (value === false || value === undefined) { + // TODO fme: check if GTK client default integers to 0 (like it does with floats) + value = 0; + this.dirty = true; + } + var show_value = parseInt(value, 10); + this.$element.find('input').val(show_value); + }, + set_value_from_ui: function() { + this.value = Number(this.$element.find('input').val()); + } +}); + openerp.base.form.FieldDatetime = openerp.base.form.Field.extend({ init: function(view, node) { this._super(view, node); this.template = "FieldDate"; this.jqueryui_object = 'datetimepicker'; + this.validation_regex = /^\d+-\d+-\d+( \d+:\d+(:\d+)?)?$/; }, start: function() { this._super.apply(this, arguments); this.$element.find('input').change(this.on_ui_change)[this.jqueryui_object]({ dateFormat: 'yy-mm-dd', - timeFormat: 'hh:mm:ss' + timeFormat: 'hh:mm:ss', + showOn: 'button', + buttonImage: '/base/static/src/img/ui/field_calendar.png', + buttonImageOnly: true, + constrainInput: false }); }, set_value: function(value) { this._super.apply(this, arguments); - if (value == null || value == false) { + if (!value) { this.$element.find('input').val(''); } else { this.$element.find('input').unbind('change'); @@ -902,8 +1050,20 @@ openerp.base.form.FieldDatetime = openerp.base.form.Field.extend({ this.value = this.format(this.value); } }, + update_dom: function() { + this._super.apply(this, arguments); + this.$element.find('input').datepicker(this.readonly ? 'disable' : 'enable'); + }, validate: function() { - this.invalid = this.required && !this.$element.find('input')[this.jqueryui_object]('getDate'); + this.invalid = false; + var value = this.$element.find('input').val(); + if (value === "") { + this.invalid = this.required; + } else if (this.validation_regex) { + this.invalid = !this.validation_regex.test(value); + } else { + this.invalid = !this.$element.find('input')[this.jqueryui_object]('getDate'); + } }, focus: function() { this.$element.find('input').focus(); @@ -916,6 +1076,7 @@ openerp.base.form.FieldDate = openerp.base.form.FieldDatetime.extend({ init: function(view, node) { this._super(view, node); this.jqueryui_object = 'datepicker'; + this.validation_regex = /^\d+-\d+-\d+$/; }, parse: openerp.base.parse_date, format: openerp.base.format_date @@ -931,6 +1092,7 @@ openerp.base.form.FieldFloatTime = openerp.base.form.FieldChar.extend({ if (value === false || value === undefined) { // As in GTK client, floats default to 0 value = 0; + this.dirty = true; } var show_value = _.sprintf("%02d:%02d", Math.floor(value), Math.round((value % 1) * 60)); this.$element.find('input').val(show_value); @@ -1025,7 +1187,7 @@ openerp.base.form.FieldProgressBar = openerp.base.form.Field.extend({ set_value: function(value) { this._super.apply(this, arguments); var show_value = Number(value); - if (show_value === NaN) { + if (isNaN(show_value)) { show_value = 0; } this.$element.find('div').progressbar('option', 'value', show_value).find('span').html(show_value + '%'); @@ -1040,28 +1202,54 @@ openerp.base.form.FieldSelection = openerp.base.form.Field.extend({ init: function(view, node) { this._super(view, node); this.template = "FieldSelection"; + this.field_index = _.map(this.field.selection, function(x, index) { + return {"ikey": "" + index, "ekey": x[0], "label": x[1]}; + }); }, start: function() { + // Flag indicating whether we're in an event chain containing a change + // event on the select, in order to know what to do on keyup[RETURN]: + // * If the user presses [RETURN] as part of changing the value of a + // selection, we should just let the value change and not let the + // event broadcast further (e.g. to validating the current state of + // the form in editable list view, which would lead to saving the + // current row or switching to the next one) + // * If the user presses [RETURN] with a select closed (side-effect: + // also if the user opened the select and pressed [RETURN] without + // changing the selected value), takes the action as validating the + // row + var ischanging = false; this._super.apply(this, arguments); - this.$element.find('select').change(this.on_ui_change); + this.$element.find('select') + .change(this.on_ui_change) + .change(function () { ischanging = true; }) + .click(function () { ischanging = false; }) + .keyup(function (e) { + if (e.which !== 13 || !ischanging) { return; } + e.stopPropagation(); + ischanging = false; + }); }, set_value: function(value) { - this._super.apply(this, arguments); - if (value != null && value !== false) { - this.$element.find('select').val(value); - } else { - this.$element.find('select').val('false'); - } + value = value === null ? false : value; + value = value instanceof Array ? value[0] : value; + this._super(value); + var option = _.detect(this.field_index, function(x) {return x.ekey === value;}); + this.$element.find('select').val(option === undefined ? '' : option.ikey); }, set_value_from_ui: function() { - this.value = this.$element.find('select').val(); + var ikey = this.$element.find('select').val(); + var option = _.detect(this.field_index, function(x) {return x.ikey === ikey;}); + this.value = option === undefined ? false : option.ekey; }, update_dom: function() { this._super.apply(this, arguments); this.$element.find('select').attr('disabled', this.readonly); }, validate: function() { - this.invalid = this.required && this.$element.find('select').val() === ""; + var ikey = this.$element.find('select').val(); + var option = _.detect(this.field_index, function(x) {return x.ikey === ikey;}); + this.invalid = this.required && (option === undefined || option.ekey === false); }, focus: function() { this.$element.find('select').focus(); @@ -1100,28 +1288,18 @@ openerp.base.form.FieldSelection = openerp.base.form.Field.extend({ }); })(); -var build_view_fields_values = function(view) { - var a_dataset = view.dataset || {}; - var fields_values = view.get_fields_values(); - var parent_values = a_dataset.parent_view ? a_dataset.parent_view.get_fields_values() : {}; - fields_values.parent = parent_values; - return fields_values; -} -/** - * Builds a new context usable for operations related to fields by merging - * the fields'context with the action's context. - */ -var build_relation_context = function(relation_field) { - var a_context = relation_field.view.dataset.get_context() || {}; - var fields_values = build_view_fields_values(relation_field.view); - var f_context = new openerp.base.CompoundContext(relation_field.field.context || {}).set_eval_context(fields_values); - var ctx = new openerp.base.CompoundContext(a_context, f_context); - return ctx; -} -var build_relation_domain = function(relation_field) { - var fields_values = build_view_fields_values(relation_field.view); - var f_domain = new openerp.base.CompoundDomain(relation_field.field.domain || []).set_eval_context(fields_values); - return f_domain; +openerp.base.form.dialog = function(content, options) { + options = _.extend({ + autoOpen: true, + width: '90%', + height: '90%', + min_width: '800px', + min_height: '600px' + }, options || {}); + options.autoOpen = true; + var dialog = new openerp.base.Dialog(null, options); + dialog.$dialog = $(content).dialog(dialog.options); + return dialog.$dialog; } openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ @@ -1153,15 +1331,10 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ if (!self.value) { return; } - self.session.action_manager.do_action({ - "res_model": self.field.relation, - "views":[[false,"form"]], - "res_id": self.value[0], - "type":"ir.actions.act_window", - "view_type":"form", - "view_mode":"form", - "target":"new", - "context": build_relation_context(self) + var pop = new openerp.base.form.FormOpenPopup(self.view); + pop.show_element(self.field.relation, self.value[0],self.build_context(), {}); + pop.on_write_completed.add_last(function() { + self.set_value(self.value[0]); }); }; var cmenu = this.$menu_btn.contextMenu(this.cm_id, {'leftClickToo': true, @@ -1209,10 +1382,12 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ } this.$input.focusout(anyoneLoosesFocus); + var isSelecting = false; // autocomplete this.$input.autocomplete({ source: function(req, resp) { self.get_search_result(req, resp); }, select: function(event, ui) { + isSelecting = true; var item = ui.item; if (item.id) { self._change_int_value([item.id, item.name]); @@ -1230,19 +1405,27 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ minLength: 0, delay: 0 }); + // used to correct a bug when selecting an element by pushing 'enter' in an editable list + this.$input.keyup(function(e) { + if (e.which === 13) { + if (isSelecting) + e.stopPropagation(); + } + isSelecting = false; + }); }, // autocomplete component content handling get_search_result: function(request, response) { var search_val = request.term; var self = this; - var dataset = new openerp.base.DataSetStatic(this.session, this.field.relation, build_relation_context(self)); + var dataset = new openerp.base.DataSetStatic(this, this.field.relation, self.build_context()); - dataset.name_search(search_val, build_relation_domain(self), 'ilike', + dataset.name_search(search_val, self.build_domain(), 'ilike', this.limit + 1, function(data) { - self.last_search = data.result; + self.last_search = data; // possible selections for the m2o - var values = _.map(data.result, function(x) { + var values = _.map(data, function(x) { return {label: $('').text(x[1]).html(), name:x[1], id:x[0]}; }); @@ -1250,10 +1433,10 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ if (values.length > self.limit) { values = values.slice(0, self.limit); values.push({label: "   Search More...", action: function() { - dataset.name_search(search_val, build_relation_domain(self), 'ilike' + dataset.name_search(search_val, self.build_domain(), 'ilike' , false, function(data) { self._change_int_value(null); - self._search_create_popup("search", data.result); + self._search_create_popup("search", data); }); }}); } @@ -1270,7 +1453,7 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ // create... values.push({label: "   Create and Edit...", action: function() { self._change_int_value(null); - self._search_create_popup("form"); + self._search_create_popup("form", undefined, {"default_name": search_val}); }}); response(values); @@ -1278,9 +1461,9 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ }, _quick_create: function(name) { var self = this; - var dataset = new openerp.base.DataSetStatic(this.session, this.field.relation, build_relation_context(self)); + var dataset = new openerp.base.DataSetStatic(this, this.field.relation, self.build_context()); dataset.name_create(name, function(data) { - self._change_int_ext_value(data.result); + self._change_int_ext_value(data); }).fail(function(error, event) { event.preventDefault(); self._change_int_value(null); @@ -1290,17 +1473,17 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ // all search/create popup handling _search_create_popup: function(view, ids, context) { var self = this; - var pop = new openerp.base.form.SelectCreatePopup(null, self.view.session); + var pop = new openerp.base.form.SelectCreatePopup(this); pop.select_element(self.field.relation,{ initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined, initial_view: view, disable_multiple_selection: true - }, build_relation_domain(self), - new openerp.base.CompoundContext(build_relation_context(self), context || {})); + }, self.build_domain(), + new openerp.base.CompoundContext(self.build_context(), context || {})); pop.on_select_elements.add(function(element_ids) { - var dataset = new openerp.base.DataSetStatic(this.session, this.field.relation, build_relation_context(self)); + var dataset = new openerp.base.DataSetStatic(self, self.field.relation, self.build_context()); dataset.name_get([element_ids[0]], function(data) { - self._change_int_ext_value(data.result[0]); + self._change_int_ext_value(data[0]); pop.stop(); }); }); @@ -1336,9 +1519,9 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ self._change_int_ext_value(rval); }; if(typeof(value) === "number") { - var dataset = new openerp.base.DataSetStatic(this.session, this.field.relation, build_relation_context(self)); + var dataset = new openerp.base.DataSetStatic(this, this.field.relation, self.build_context()); dataset.name_get([value], function(data) { - real_set_value(data.result[0]); + real_set_value(data[0]); }).fail(function() {self.tmp_value = undefined;}); } else { setTimeout(function() {real_set_value(value);}, 0); @@ -1409,17 +1592,20 @@ var commands = { } }; openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({ + multi_selection: false, init: function(view, node) { this._super(view, node); this.template = "FieldOne2Many"; this.is_started = $.Deferred(); + this.form_last_update = $.Deferred(); + this.disable_utility_classes = true; }, start: function() { this._super.apply(this, arguments); var self = this; - this.dataset = new openerp.base.form.One2ManyDataSet(this.session, this.field.relation); + this.dataset = new openerp.base.form.One2ManyDataSet(this, this.field.relation); this.dataset.o2m = this; this.dataset.parent_view = this.view; this.dataset.on_change.add_last(function() { @@ -1436,27 +1622,41 @@ openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({ } if(view.view_type === "list") { view.options = { + 'selectable': self.multi_selection }; } views.push(view); }); this.views = views; - this.viewmanager = new openerp.base.ViewManager(this.view.session, - this.element_id, this.dataset, views); + this.viewmanager = new openerp.base.ViewManager(this, this.element_id, this.dataset, views); this.viewmanager.registry = openerp.base.views.clone({ - list: 'openerp.base.form.One2ManyListView' + list: 'openerp.base.form.One2ManyListView', + form: 'openerp.base.form.One2ManyFormView' + }); + var once = $.Deferred().then(function() { + self.form_last_update.resolve(); }); - this.viewmanager.on_controller_inited.add_last(function(view_type, controller) { if (view_type == "list") { controller.o2m = self; } else if (view_type == "form") { - // TODO niv + controller.on_record_loaded.add_last(function() { + once.resolve(); + }); + controller.on_pager_action.add_first(function() { + self.save_form_view(); + }); + controller.$element.find(".oe_form_button_save_edit").hide(); } self.is_started.resolve(); }); - this.viewmanager.start(); + this.viewmanager.on_mode_switch.add_first(function() { + self.save_form_view(); + }); + setTimeout(function () { + self.viewmanager.start(); + }, 0); }, reload_current_view: function() { var self = this; @@ -1464,7 +1664,12 @@ openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({ if(self.viewmanager.active_view === "list") { view.reload_content(); } else if (self.viewmanager.active_view === "form") { - // TODO niv: implement + if (this.dataset.index === null && this.dataset.ids.length >= 1) { + this.dataset.index = 0; + } + this.form_last_update.then(function() { + this.form_last_update = view.do_show(); + }); } }, set_value_from_ui: function() {}, @@ -1518,6 +1723,9 @@ openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({ this._super(value); this.dataset.reset_ids(value); } + if (this.dataset.index === null && this.dataset.ids.length > 0) { + this.dataset.index = 0; + } $.when(this.is_started).then(function() { self.reload_current_view(); }); @@ -1542,47 +1750,97 @@ openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({ this.dataset.to_delete, function(x) { return commands['delete'](x.id);})); }, - get_on_change_value: function() { - var self; - return _.map(this.dataset.ids, function(id) { - var values = _.detect(self.dataset.cache, function(x) {return x.id === id}); - if (values) { - values = _.clone(values); - delete values["id"]; - return values; - } else { - console.info("trying to get value in a o2m before that value is loaded"); - return {}; + save_form_view: function() { + if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view && + this.viewmanager.views[this.viewmanager.active_view] && + this.viewmanager.views[this.viewmanager.active_view].controller) { + var view = this.viewmanager.views[this.viewmanager.active_view].controller; + if (this.viewmanager.active_view === "form") { + var res = view.do_save(); + if (res === false) { + // ignore + } else if (res.isRejected()) { + throw "Save or create on one2many dataset is not supposed to fail."; + } else if (!res.isResolved()) { + throw "Asynchronous get_value() is not supported in form view."; + } + return res; } - }); + } + return false; + }, + is_valid: function() { + this.validate(); + return this._super(); }, validate: function() { this.invalid = false; - // TODO niv + var self = this; + var view = self.viewmanager.views[self.viewmanager.active_view].controller; + if (self.viewmanager.active_view === "form") { + for (var f in view.fields) { + f = view.fields[f]; + if (!f.is_valid()) { + this.invalid = true; + return; + } + } + } + }, + is_dirty: function() { + this.save_form_view(); + return this._super(); + }, + update_dom: function() { + this._super.apply(this, arguments); + this.$element.toggleClass('disabled', this.readonly); } }); openerp.base.form.One2ManyDataSet = openerp.base.BufferedDataSet.extend({ get_context: function() { - this.context = build_relation_context(this.o2m); + this.context = this.o2m.build_context(); return this.context; } }); +openerp.base.form.One2ManyFormView = openerp.base.FormView.extend({ + +}); + openerp.base.form.One2ManyListView = openerp.base.ListView.extend({ do_add_record: function () { + if (this.options.editable) { + this._super.apply(this, arguments); + } else { + var self = this; + var pop = new openerp.base.form.SelectCreatePopup(this); + pop.select_element(self.o2m.field.relation,{ + initial_view: "form", + alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined, + auto_create: false, + parent_view: self.o2m.view + }, self.o2m.build_domain(), self.o2m.build_context()); + pop.on_create.add(function(data) { + self.o2m.dataset.create(data, function(r) { + self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r.result])); + self.o2m.dataset.on_change(); + pop.stop(); + self.o2m.reload_current_view(); + }); + }); + } + }, + do_activate_record: function(index, id) { var self = this; - var pop = new openerp.base.form.SelectCreatePopup(null, self.o2m.view.session); - pop.select_element(self.o2m.field.relation,{ - initial_view: "form", + var pop = new openerp.base.form.FormOpenPopup(self.o2m.view); + pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(),{ + auto_write: false, alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined, - auto_create: false, parent_view: self.o2m.view - }, build_relation_domain(self.o2m), build_relation_context(self.o2m)); - pop.on_create.add(function(data) { - self.o2m.dataset.create(data, function(r) { - self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r.result])); - pop.stop(); + }); + pop.on_write.add(function(id, data) { + self.o2m.dataset.write(id, data, function(r) { self.o2m.reload_current_view(); }); }); @@ -1590,6 +1848,7 @@ openerp.base.form.One2ManyListView = openerp.base.ListView.extend({ }); openerp.base.form.FieldMany2Many = openerp.base.form.Field.extend({ + multi_selection: false, init: function(view, node) { this._super(view, node); this.template = "FieldMany2Many"; @@ -1601,22 +1860,23 @@ openerp.base.form.FieldMany2Many = openerp.base.form.Field.extend({ var self = this; - this.dataset = new openerp.base.DataSetStatic( - this.session, this.field.relation); + this.dataset = new openerp.base.form.Many2ManyDataSet(this, this.field.relation); + this.dataset.m2m = this; this.dataset.on_unlink.add_last(function(ids) { - //TODO niv: should check this for other cases self.on_ui_change(); }); - this.list_view = new openerp.base.form.Many2ManyListView( - null, this.view.session, this.list_id, this.dataset, false, { - 'addable': 'Add' + this.list_view = new openerp.base.form.Many2ManyListView(new openerp.base.NullViewManager(this), this.list_id, this.dataset, false, { + 'addable': 'Add', + 'selectable': self.multi_selection }); this.list_view.m2m_field = this; this.list_view.on_loaded.add_last(function() { self.is_started.resolve(); }); - this.list_view.start(); + setTimeout(function () { + self.list_view.start(); + }, 0); }, set_value: function(value) { value = value || []; @@ -1632,19 +1892,33 @@ openerp.base.form.FieldMany2Many = openerp.base.form.Field.extend({ }, get_value: function() { return [commands.replace_with(this.dataset.ids)]; + }, + set_value_from_ui: function() {}, + validate: function() { + this.invalid = false; + // TODO niv + } +}); + +openerp.base.form.Many2ManyDataSet = openerp.base.DataSetStatic.extend({ + get_context: function() { + this.context = this.m2m.build_context(); + return this.context; } }); openerp.base.form.Many2ManyListView = openerp.base.ListView.extend({ do_add_record: function () { - var pop = new openerp.base.form.SelectCreatePopup( - null, this.m2m_field.view.session); - pop.select_element(this.model); + var pop = new openerp.base.form.SelectCreatePopup(this); + pop.select_element(this.model, {}, + new openerp.base.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]), + this.m2m_field.build_context()); var self = this; pop.on_select_elements.add(function(element_ids) { _.each(element_ids, function(element_id) { if(! _.detect(self.dataset.ids, function(x) {return x == element_id;})) { self.dataset.set_ids([].concat(self.dataset.ids, [element_id])); + self.m2m_field.on_ui_change(); self.reload_content(); } }); @@ -1652,19 +1926,16 @@ openerp.base.form.Many2ManyListView = openerp.base.ListView.extend({ }); }, do_activate_record: function(index, id) { - this.m2m_field.view.session.action_manager.do_action({ - "res_model": this.dataset.model, - "views":[[false,"form"]], - "res_id": id, - "type":"ir.actions.act_window", - "view_type":"form", - "view_mode":"form", - "target":"new" + var self = this; + var pop = new openerp.base.form.FormOpenPopup(this); + pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {}); + pop.on_write_completed.add_last(function() { + self.reload_content(); }); } }); -openerp.base.form.SelectCreatePopup = openerp.base.BaseWidget.extend({ +openerp.base.form.SelectCreatePopup = openerp.base.OldWidget.extend({ identifier_prefix: "selectcreatepopup", template: "SelectCreatePopup", /** @@ -1682,15 +1953,13 @@ openerp.base.form.SelectCreatePopup = openerp.base.BaseWidget.extend({ this.context = context || {}; this.options = _.defaults(options || {}, {"initial_view": "search", "auto_create": true}); this.initial_ids = this.options.initial_ids; - jQuery(this.render()).dialog({title: '', - modal: true, - minWidth: 800}); + openerp.base.form.dialog(this.render()); this.start(); }, start: function() { this._super(); - this.dataset = new openerp.base.ReadOnlyDataSetSearch(this.session, this.model, - this.context, this.domain); + this.dataset = new openerp.base.ReadOnlyDataSetSearch(this, this.model, + this.context); this.dataset.parent_view = this.options.parent_view; if (this.options.initial_view == "search") { this.setup_search_view(); @@ -1703,18 +1972,18 @@ openerp.base.form.SelectCreatePopup = openerp.base.BaseWidget.extend({ if (this.searchview) { this.searchview.stop(); } - this.searchview = new openerp.base.SearchView(null, this.session, + this.searchview = new openerp.base.SearchView(this, this.element_id + "_search", this.dataset, false, { "selectable": !this.options.disable_multiple_selection, "deletable": false }); this.searchview.on_search.add(function(domains, contexts, groupbys) { if (self.initial_ids) { - self.view_list.do_search.call(self, domains.concat([[["id", "in", self.initial_ids]]]), + self.view_list.do_search.call(self, domains.concat([[["id", "in", self.initial_ids]], self.domain]), contexts, groupbys); self.initial_ids = undefined; } else { - self.view_list.do_search.call(self, domains, contexts, groupbys); + self.view_list.do_search.call(self, domains.concat([self.domain]), contexts, groupbys); } }); this.searchview.on_loaded.add_last(function () { @@ -1731,7 +2000,7 @@ openerp.base.form.SelectCreatePopup = openerp.base.BaseWidget.extend({ $sbutton.click(function() { self.on_select_elements(self.selected_ids); }); - self.view_list = new openerp.base.form.SelectCreateListView( null, self.session, + self.view_list = new openerp.base.form.SelectCreateListView(self, self.element_id + "_view_list", self.dataset, false, {'deletable': false}); self.view_list.popup = self; @@ -1746,8 +2015,8 @@ openerp.base.form.SelectCreatePopup = openerp.base.BaseWidget.extend({ if (!this.options.auto_create) return; var self = this; - var wdataset = new openerp.base.DataSetSearch(this.session, this.model, this.context, this.domain); - wdataset = this.options.parent_view; + var wdataset = new openerp.base.DataSetSearch(this, this.model, this.context, this.domain); + wdataset.parent_view = this.options.parent_view; wdataset.create(data, function(r) { self.on_select_elements([r.result]); }); @@ -1771,8 +2040,7 @@ openerp.base.form.SelectCreatePopup = openerp.base.BaseWidget.extend({ this.view_list.$element.hide(); } this.dataset.index = null; - this.view_form = new openerp.base.FormView(null, this.session, - this.element_id + "_view_form", this.dataset, false); + this.view_form = new openerp.base.FormView(this, this.element_id + "_view_form", this.dataset, false); if (this.options.alternative_form_view) { this.view_form.set_embedded_view(this.options.alternative_form_view); } @@ -1807,10 +2075,74 @@ openerp.base.form.SelectCreateListView = openerp.base.ListView.extend({ } }); -openerp.base.form.FieldReference = openerp.base.form.Field.extend({ +openerp.base.form.FormOpenPopup = openerp.base.OldWidget.extend({ + identifier_prefix: "formopenpopup", + template: "FormOpenPopup", + /** + * options: + * - alternative_form_view + * - auto_write (default true) + * - parent_view + */ + show_element: function(model, row_id, context, options) { + this.model = model; + this.row_id = row_id; + this.context = context || {}; + this.options = _.defaults(options || {}, {"auto_write": true}); + jQuery(this.render()).dialog({title: '', + modal: true, + width: 960, + height: 600}); + this.start(); + }, + start: function() { + this._super(); + this.dataset = new openerp.base.ReadOnlyDataSetSearch(this, this.model, this.context); + this.dataset.ids = [this.row_id]; + this.dataset.index = 0; + this.dataset.parent_view = this.options.parent_view; + this.setup_form_view(); + }, + on_write: function(id, data) { + this.stop(); + if (!this.options.auto_write) + return; + var self = this; + var wdataset = new openerp.base.DataSetSearch(this, this.model, this.context, this.domain); + wdataset.parent_view = this.options.parent_view; + wdataset.write(id, data, function(r) { + self.on_write_completed(); + }); + }, + on_write_completed: function() {}, + setup_form_view: function() { + var self = this; + this.view_form = new openerp.base.FormView(this, this.element_id + "_view_form", this.dataset, false); + if (this.options.alternative_form_view) { + this.view_form.set_embedded_view(this.options.alternative_form_view); + } + this.view_form.start(); + this.view_form.on_loaded.add_last(function() { + var $buttons = self.view_form.$element.find(".oe_form_buttons"); + $buttons.html(QWeb.render("FormOpenPopup.form.buttons")); + var $nbutton = $buttons.find(".oe_formopenpopup-form-save"); + $nbutton.click(function() { + self.view_form.do_save(); + }); + var $cbutton = $buttons.find(".oe_formopenpopup-form-close"); + $cbutton.click(function() { + self.stop(); + }); + self.view_form.do_show(); + }); + this.dataset.on_write.add(this.on_write); + } +}); + +openerp.base.form.FieldReference = openerp.base.form.FieldChar.extend({ init: function(view, node) { this._super(view, node); - this.template = "FieldReference"; + //this.template = "FieldReference"; } }); @@ -1828,6 +2160,10 @@ openerp.base.form.FieldBinary = openerp.base.form.Field.extend({ }, set_value_from_ui: function() { }, + update_dom: function() { + this._super.apply(this, arguments); + this.$element.find('.oe-binary').toggle(!this.readonly); + }, human_filesize : function(size) { var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; var i = 0; @@ -1925,6 +2261,13 @@ openerp.base.form.FieldBinaryImage = openerp.base.form.FieldBinary.extend({ this._super.apply(this, arguments); this.$image = this.$element.find('img.oe-binary-image'); }, + set_value: function(value) { + this._super.apply(this, arguments); + this.set_image_maxwidth(); + var url = '/base/binary/image?session_id=' + this.session.session_id + '&model=' + + this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name + '&t=' + (new Date().getTime()) + this.$image.attr('src', url); + }, set_image_maxwidth: function() { this.$image.css('max-width', this.$element.width()); }, @@ -1940,13 +2283,6 @@ openerp.base.form.FieldBinaryImage = openerp.base.form.FieldBinary.extend({ on_clear: function() { this._super.apply(this, arguments); this.$image.attr('src', '/base/static/src/img/placeholder.png'); - }, - set_value: function(value) { - this._super.apply(this, arguments); - this.set_image_maxwidth(); - var url = '/base/binary/image?session_id=' + this.session.session_id + '&model=' + - this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name + '&t=' + (new Date().getTime()) - this.$image.attr('src', url); } }); @@ -1965,8 +2301,10 @@ openerp.base.form.widgets = new openerp.base.Registry({ 'url' : 'openerp.base.form.FieldUrl', 'text' : 'openerp.base.form.FieldText', 'text_wiki' : 'openerp.base.form.FieldText', - 'date' : 'openerp.base.form.FieldDate', - 'datetime' : 'openerp.base.form.FieldDatetime', +// 'date' : 'openerp.base.form.FieldDate', +// 'datetime' : 'openerp.base.form.FieldDatetime', + 'date' : 'openerp.base.form.FieldChar', + 'datetime' : 'openerp.base.form.FieldChar', 'selection' : 'openerp.base.form.FieldSelection', 'many2one' : 'openerp.base.form.FieldMany2One', 'many2many' : 'openerp.base.form.FieldMany2Many', @@ -1975,7 +2313,7 @@ openerp.base.form.widgets = new openerp.base.Registry({ 'reference' : 'openerp.base.form.FieldReference', 'boolean' : 'openerp.base.form.FieldBoolean', 'float' : 'openerp.base.form.FieldFloat', - 'integer': 'openerp.base.form.FieldFloat', + 'integer': 'openerp.base.form.FieldInteger', 'progressbar': 'openerp.base.form.FieldProgressBar', 'float_time': 'openerp.base.form.FieldFloatTime', 'image': 'openerp.base.form.FieldBinaryImage', diff --git a/addons/base/static/src/js/list-editable.js b/addons/base/static/src/js/list-editable.js index b97a35d0612..9a48889d801 100644 --- a/addons/base/static/src/js/list-editable.js +++ b/addons/base/static/src/js/list-editable.js @@ -103,80 +103,106 @@ openerp.base.list.editable = function (openerp) { * Checks if a record is being edited, and if so cancels it */ cancel_pending_edition: function () { + var self = this, cancelled = $.Deferred(); if (!this.edition) { - return; + cancelled.resolve(); + return cancelled.promise(); } if (this.edition_index !== null) { - this.reload_record(this.edition_index); + this.reload_record(this.edition_index, true).then(function () { + cancelled.resolve(); + }); + } else { + cancelled.resolve(); } - this.edition_form.stop(); - this.edition_form.$element.remove(); - delete this.edition_form; - delete this.edition_index; - delete this.edition; + cancelled.then(function () { + self.edition_form.stop(); + self.edition_form.$element.remove(); + delete self.edition_form; + delete self.edition_index; + delete self.edition; + }); + return cancelled.promise(); + }, + /** + * Adapts this list's view description to be suitable to the inner form view of a row being edited. + * + * @returns {Object} fields_view_get's view section suitable for putting into form view of editable rows. + */ + get_form_fields_view: function () { + // deep copy of view + var view = $.extend(true, {}, this.group.view.fields_view); + _(view.arch.children).each(function (widget) { + widget.attrs.nolabel = true; + if (widget.tag === 'button') { + delete widget.attrs.string; + } + }); + view.arch.attrs.col = 2 * view.arch.children.length; + return view; }, render_row_as_form: function (row) { - this.cancel_pending_edition(); - var self = this; - var $new_row = $('', { - id: _.uniqueId('oe-editable-row-'), - 'class': $(row).attr('class'), - click: function (e) {e.stopPropagation();} - }) - .delegate('button.oe-edit-row-save', 'click', function () { - self.save_row(); - }) - .delegate('button.oe-edit-row-cancel', 'click', function () { - self.cancel_edition(); - }) - .delegate('button', 'keyup', function (e) { - e.stopImmediatePropagation(); - }) - .keyup(function (e) { - switch (e.which) { - case KEY_RETURN: - self.save_row(true); - break; - case KEY_ESCAPE: - self.cancel_edition(); - break; - default: - return; - } - }); - if (row) { - $new_row.replaceAll(row); - } else if (this.options.editable === 'top') { - this.$current.prepend($new_row); - } else if (this.options.editable) { - this.$current.append($new_row); - } - this.edition = true; - this.edition_index = this.dataset.index; - this.edition_form = _.extend(new openerp.base.FormView( - null, this.group.view.session, $new_row.attr('id'), - this.dataset, false), { - template: 'ListView.row.form', - registry: openerp.base.list.form.widgets - }); - $.when(this.edition_form.on_loaded({fields_view: this.get_fields_view()})).then(function () { - // put in $.when just in case FormView.on_loaded becomes asynchronous - $new_row.find('td') - .addClass('oe-field-cell') - .removeAttr('width') - .end() - .find('td:first').removeClass('oe-field-cell').end() - .find('td:last').removeClass('oe-field-cell').end(); - // pad in case of groupby - _(self.columns).each(function (column) { - if (column.meta) { - $new_row.prepend(''); - } + this.cancel_pending_edition().then(function () { + var $new_row = $('', { + id: _.uniqueId('oe-editable-row-'), + 'class': $(row).attr('class'), + click: function (e) {e.stopPropagation();} + }) + .delegate('button.oe-edit-row-save', 'click', function () { + self.save_row(); + }) + .delegate('button.oe-edit-row-cancel', 'click', function () { + self.cancel_edition(); + }) + .delegate('button', 'keyup', function (e) { + e.stopImmediatePropagation(); + }) + .keyup(function (e) { + switch (e.which) { + case KEY_RETURN: + self.save_row(true); + break; + case KEY_ESCAPE: + self.cancel_edition(); + break; + default: + return; + } + }); + if (row) { + $new_row.replaceAll(row); + } else if (self.options.editable === 'top') { + self.$current.prepend($new_row); + } else if (self.options.editable) { + self.$current.append($new_row); + } + self.edition = true; + self.edition_index = self.dataset.index; + self.edition_form = _.extend(new openerp.base.FormView( + null, self.group.view.session, $new_row.attr('id'), + self.dataset, false), { + template: 'ListView.row.form', + registry: openerp.base.list.form.widgets }); + $.when(self.edition_form.on_loaded({fields_view: self.get_form_fields_view()})).then(function () { + // put in $.when just in case FormView.on_loaded becomes asynchronous + $new_row.find('td') + .addClass('oe-field-cell') + .removeAttr('width') + .end() + .find('td:first').removeClass('oe-field-cell').end() + .find('td:last').removeClass('oe-field-cell').end(); + // pad in case of groupby + _(self.columns).each(function (column) { + if (column.meta) { + $new_row.prepend(''); + } + }); - self.edition_form.do_show(); + self.edition_form.do_show(); + }); }); }, /** @@ -188,12 +214,7 @@ openerp.base.list.editable = function (openerp) { save_row: function (edit_next) { var self = this; this.edition_form.do_save(function (result) { - self.reload_record(self.dataset.index, true).then(function () { - self.edition_form.stop(); - delete self.edition_form; - delete self.edition_index; - delete self.edition; - + self.cancel_pending_edition().then(function () { $(self).trigger('saved', [self.dataset]); if (!edit_next) { return; @@ -230,7 +251,12 @@ openerp.base.list.editable = function (openerp) { this.render_row_as_form(); } }); - openerp.base.list = {form: {}}; + if (!openerp.base.list) { + openerp.base.list = {}; + } + if (!openerp.base.list.form) { + openerp.base.list.form = {}; + } openerp.base.list.form.WidgetFrame = openerp.base.form.WidgetFrame.extend({ template: 'ListView.row.frame' }); @@ -251,10 +277,12 @@ openerp.base.list.editable = function (openerp) { openerp.base.list.form[key] = (form_widgets.get_object(key)).extend({ update_dom: function () { this.$element.children().css('visibility', ''); - if (this.invisible && this.node.attrs.invisible !== '1') { + if (this.invisible) { this.$element.children().css('visibility', 'hidden'); } else { + this.invisible = !!this.modifiers.tree_invisible; this._super(); + this.invisible = false; } } }); diff --git a/addons/base/static/src/js/list.js b/addons/base/static/src/js/list.js index 62bedf8784d..d64d83329c0 100644 --- a/addons/base/static/src/js/list.js +++ b/addons/base/static/src/js/list.js @@ -1,5 +1,63 @@ openerp.base.list = function (openerp) { openerp.base.views.add('list', 'openerp.base.ListView'); +openerp.base.list = { + /** + * Formats the rendring of a given value based on its field type + * + * @param {Object} row_data record whose values should be displayed in the cell + * @param {Object} column column descriptor + * @param {"button"|"field"} column.tag base control type + * @param {String} column.type widget type for a field control + * @param {String} [column.string] button label + * @param {String} [column.icon] button icon + * @param {String} [value_if_empty=''] what to display if the field's value is ``false`` + */ + render_cell: function (row_data, column, value_if_empty) { + var attrs = column.modifiers_for(row_data); + if (attrs.invisible) { return ''; } + if (column.tag === 'button') { + return [ + '' + ].join('') + } + + var value = row_data[column.id].value; + + // If NaN value, display as with a `false` (empty cell) + if (typeof value === 'number' && isNaN(value)) { + value = false; + } + switch (value) { + case false: + case Infinity: + case -Infinity: + return value_if_empty === undefined ? '' : value_if_empty; + } + switch (column.widget || column.type) { + case 'integer': + return _.sprintf('%d', value); + case 'float': + var precision = column.digits ? column.digits[1] : 2; + return _.sprintf('%.' + precision + 'f', value); + case 'float_time': + return _.sprintf("%02d:%02d", + Math.floor(value), + Math.round((value % 1) * 60)); + case 'progressbar': + return _.sprintf( + '%.2f%%', + value, value); + case 'many2one': + // name_get value format + return value[1]; + default: + return value; + } + } +}; openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListView# */ { defaults: { // records can be selected one by one @@ -42,9 +100,10 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi * * @borrows openerp.base.ActionExecutor#execute_action as #execute_action */ - init: function(view_manager, session, element_id, dataset, view_id, options) { - this._super(session, element_id); - this.view_manager = view_manager || new openerp.base.NullViewManager(); + init: function(parent, element_id, dataset, view_id, options) { + this._super(parent, element_id); + this.set_default_options(); + this.view_manager = parent || new openerp.base.NullViewManager(); this.dataset = dataset; this.model = dataset.model; this.view_id = view_id; @@ -52,13 +111,30 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi this.columns = []; this.options = _.extend({}, this.defaults, options || {}); - this.flags = this.view_manager.flags || {}; this.set_groups(new openerp.base.ListView.Groups(this)); - + if (this.dataset instanceof openerp.base.DataSetStatic) { this.groups.datagroup = new openerp.base.StaticDataGroup(this.dataset); } + + this.page = 0; + }, + /** + * Retrieves the view's number of records per page (|| section) + * + * options > defaults > view_manager.action.limit > indefinite + * + * @returns {Number|null} + */ + limit: function () { + if (this._limit === undefined) { + this._limit = (this.options.limit + || this.defaults.limit + || (this.view_manager.action || {}).limit + || null); + } + return this._limit; }, /** * Set a custom Group construct as the root of the List View. @@ -129,27 +205,103 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi this.setup_columns(this.fields_view.fields, grouped); - if (!this.fields_view.sorted) { this.fields_view.sorted = {}; } - this.$element.html(QWeb.render("ListView", this)); // Head hook - this.$element.find('#oe-list-add') + this.$element.find('.oe-list-add') .click(this.do_add_record) .attr('disabled', grouped && this.options.editable); - this.$element.find('#oe-list-delete') + this.$element.find('.oe-list-delete') .attr('disabled', true) .click(this.do_delete_selected); - this.$element.find('thead').delegate('th[data-id]', 'click', function (e) { + this.$element.find('thead').delegate('th.oe-sortable[data-id]', 'click', function (e) { e.stopPropagation(); - self.dataset.sort($(this).data('id')); + var $this = $(this); + self.dataset.sort($this.data('id')); + if ($this.find('span').length) { + $this.find('span').toggleClass( + 'ui-icon-triangle-1-s ui-icon-triangle-1-n'); + } else { + $this.append('') + .siblings('.oe-sortable').find('span').remove(); + } - // TODO: should only reload content (and set the right column to a sorted display state) - self.reload_view(); + self.reload_content(); }); - this.view_manager.sidebar.set_toolbar(data.fields_view.toolbar); + this.$element.find('.oe-list-pager') + .delegate('button', 'click', function () { + var $this = $(this); + switch ($this.data('pager-action')) { + case 'first': + self.page = 0; break; + case 'last': + self.page = Math.floor( + self.dataset.ids.length / self.limit()); + break; + case 'next': + self.page += 1; break; + case 'previous': + self.page -= 1; break; + } + self.reload_content(); + }).find('.oe-pager-state') + .click(function (e) { + e.stopPropagation(); + var $this = $(this); + + var $select = $(' + - - - - - - - - - - - + t-att-value="selected_password || ''"/> @@ -114,7 +103,7 @@ @@ -154,8 +143,8 @@