1 # -*- coding: utf-8 -*-
19 from xml.etree import ElementTree
20 from cStringIO import StringIO
22 import babel.messages.pofile
24 import werkzeug.wrappers
31 openerpweb = common.http
33 #----------------------------------------------------------
34 # OpenERP Web web Controllers
35 #----------------------------------------------------------
38 def concat_xml(file_list):
39 """Concatenate xml files
41 :param list(str) file_list: list of files to check
42 :returns: (concatenation_result, checksum)
45 checksum = hashlib.new('sha1')
47 return '', checksum.hexdigest()
50 for fname in file_list:
51 with open(fname, 'rb') as fp:
53 checksum.update(contents)
55 xml = ElementTree.parse(fp).getroot()
58 root = ElementTree.Element(xml.tag)
59 #elif root.tag != xml.tag:
60 # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
62 for child in xml.getchildren():
64 return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
67 def concat_files(file_list, reader=None, intersperse=""):
68 """ Concatenates contents of all provided files
70 :param list(str) file_list: list of files to check
71 :param function reader: reading procedure for each file
72 :param str intersperse: string to intersperse between file contents
73 :returns: (concatenation_result, checksum)
76 checksum = hashlib.new('sha1')
78 return '', checksum.hexdigest()
86 for fname in file_list:
87 contents = reader(fname)
88 checksum.update(contents)
89 files_content.append(contents)
91 files_concat = intersperse.join(files_content)
92 return files_concat, checksum.hexdigest()
94 html_template = """<!DOCTYPE html>
95 <html style="height: 100%%">
97 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
98 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
99 <title>OpenERP</title>
100 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
101 <link rel="stylesheet" href="/web/static/src/css/full.css" />
104 <script type="text/javascript">
106 var s = new openerp.init(%(modules)s);
107 var wc = new s.web.WebClient();
108 wc.appendTo($(document.body));
117 # Validated by diff -u of sass2scss against:
118 # sass-convert -F sass -T scss openerp.sass openerp.scss
121 reComment = re.compile(r'//.*$')
122 reIndent = re.compile(r'^\s+')
123 reIgnore = re.compile(r'^\s*(//.*)?$')
124 reFixes = { re.compile(r'\(\((.*)\)\)') : r'(\1)', }
127 for l in src.split('\n'):
129 if reIgnore.search(l): continue
130 l = reComment.sub('', l)
132 indent = reIndent.match(l)
133 level = indent.end() if indent else 0
136 prevBlocks[lastLevel] = block
138 block[-1] = (block[-1], newBlock)
140 elif level<lastLevel:
141 block = prevBlocks[level]
145 for ereg, repl in reFixes.items():
146 l = ereg.sub(repl if type(repl)==str else repl(), l)
149 def write(sass, level=-1):
152 if type(sass)==tuple:
154 out += indent+sass[0]+" {\n"
156 out += write(e, level+1)
158 out = out.rstrip(" \n")
163 out += indent+sass+";\n"
167 class WebClient(openerpweb.Controller):
168 _cp_path = "/web/webclient"
170 def server_wide_modules(self, req):
171 addons = [i for i in req.config.server_wide_modules if i in openerpweb.addons_manifest]
174 def manifest_glob(self, req, addons, key):
176 addons = self.server_wide_modules(req)
178 addons = addons.split(',')
181 manifest = openerpweb.addons_manifest.get(addon, None)
184 # ensure does not ends with /
185 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
186 globlist = manifest.get(key, [])
187 for pattern in globlist:
188 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
189 r.append( (path, path[len(addons_path):]))
192 def manifest_list(self, req, mods, extension):
194 path = '/web/webclient/' + extension
196 path += '?mods=' + mods
198 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in self.manifest_glob(req, mods, extension)]
200 @openerpweb.jsonrequest
201 def csslist(self, req, mods=None):
202 return self.manifest_list(req, mods, 'css')
204 @openerpweb.jsonrequest
205 def jslist(self, req, mods=None):
206 return self.manifest_list(req, mods, 'js')
208 @openerpweb.jsonrequest
209 def qweblist(self, req, mods=None):
210 return self.manifest_list(req, mods, 'qweb')
212 def get_last_modified(self, files):
213 """ Returns the modification time of the most recently modified
216 :param list(str) files: names of files to check
217 :return: most recent modification time amongst the fileset
218 :rtype: datetime.datetime
222 return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
224 return datetime.datetime(1970, 1, 1)
226 def make_conditional(self, req, response, last_modified=None, etag=None):
227 """ Makes the provided response conditional based upon the request,
228 and mandates revalidation from clients
230 Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
231 setting ``last_modified`` and ``etag`` correctly on the response object
233 :param req: OpenERP request
234 :type req: web.common.http.WebRequest
235 :param response: Werkzeug response
236 :type response: werkzeug.wrappers.Response
237 :param datetime.datetime last_modified: last modification date of the response content
238 :param str etag: some sort of checksum of the content (deep etag)
239 :return: the response object provided
240 :rtype: werkzeug.wrappers.Response
242 response.cache_control.must_revalidate = True
243 response.cache_control.max_age = 0
245 response.last_modified = last_modified
247 response.set_etag(etag)
248 return response.make_conditional(req.httprequest)
250 @openerpweb.httprequest
251 def css(self, req, mods=None):
252 files = list(self.manifest_glob(req, mods, 'css'))
253 last_modified = self.get_last_modified(f[0] for f in files)
254 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
255 return werkzeug.wrappers.Response(status=304)
257 file_map = dict(files)
259 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
260 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://)""", re.U)
264 """read the a css file and absolutify all relative uris"""
269 # convert FS path into web path
270 web_dir = '/'.join(os.path.dirname(path).split(os.path.sep))
274 r"""@import \1%s/""" % (web_dir,),
280 r"""url(\1%s/""" % (web_dir,),
285 content, checksum = concat_files((f[0] for f in files), reader)
287 return self.make_conditional(
288 req, req.make_response(content, [('Content-Type', 'text/css')]),
289 last_modified, checksum)
291 @openerpweb.httprequest
292 def js(self, req, mods=None):
293 files = [f[0] for f in self.manifest_glob(req, mods, 'js')]
294 last_modified = self.get_last_modified(files)
295 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
296 return werkzeug.wrappers.Response(status=304)
298 content, checksum = concat_files(files, intersperse=';')
300 return self.make_conditional(
301 req, req.make_response(content, [('Content-Type', 'application/javascript')]),
302 last_modified, checksum)
304 @openerpweb.httprequest
305 def qweb(self, req, mods=None):
306 files = [f[0] for f in self.manifest_glob(req, mods, 'qweb')]
307 last_modified = self.get_last_modified(files)
308 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
309 return werkzeug.wrappers.Response(status=304)
311 content,checksum = concat_xml(files)
313 return self.make_conditional(
314 req, req.make_response(content, [('Content-Type', 'text/xml')]),
315 last_modified, checksum)
317 @openerpweb.httprequest
318 def home(self, req, s_action=None, **kw):
319 js = "\n ".join('<script type="text/javascript" src="%s"></script>'%i for i in self.manifest_list(req, None, 'js'))
320 css = "\n ".join('<link rel="stylesheet" href="%s">'%i for i in self.manifest_list(req, None, 'css'))
322 r = html_template % {
325 'modules': simplejson.dumps(self.server_wide_modules(req)),
329 @openerpweb.httprequest
330 def login(self, req, db, login, key):
331 req.session.authenticate(db, login, key, {})
332 redirect = werkzeug.utils.redirect('/web/webclient/home', 303)
333 cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
334 redirect.set_cookie('session0|session_id', cookie_val)
337 @openerpweb.jsonrequest
338 def translations(self, req, mods, lang):
339 lang_model = req.session.model('res.lang')
340 ids = lang_model.search([("code", "=", lang)])
342 lang_obj = lang_model.read(ids[0], ["direction", "date_format", "time_format",
343 "grouping", "decimal_point", "thousands_sep"])
351 langs = lang.split(separator)
352 langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
355 for addon_name in mods:
356 transl = {"messages":[]}
357 transs[addon_name] = transl
358 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
360 f_name = os.path.join(addons_path, addon_name, "i18n", l + ".po")
361 if not os.path.exists(f_name):
364 with open(f_name) as t_file:
365 po = babel.messages.pofile.read_po(t_file)
369 if x.id and x.string and "openerp-web" in x.auto_comments:
370 transl["messages"].append({'id': x.id, 'string': x.string})
371 return {"modules": transs,
372 "lang_parameters": lang_obj}
374 @openerpweb.jsonrequest
375 def version_info(self, req):
377 "version": common.release.version
380 class Proxy(openerpweb.Controller):
381 _cp_path = '/web/proxy'
383 @openerpweb.jsonrequest
384 def load(self, req, path):
385 """ Proxies an HTTP request through a JSON request.
387 It is strongly recommended to not request binary files through this,
388 as the result will be a binary data blob as well.
390 :param req: OpenERP request
391 :param path: actual request path
392 :return: file content
394 from werkzeug.test import Client
395 from werkzeug.wrappers import BaseResponse
397 return Client(req.httprequest.app, BaseResponse).get(path).data
399 class Database(openerpweb.Controller):
400 _cp_path = "/web/database"
402 @openerpweb.jsonrequest
403 def get_list(self, req):
404 proxy = req.session.proxy("db")
406 h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
408 r = req.config.dbfilter.replace('%h', h).replace('%d', d)
409 dbs = [i for i in dbs if re.match(r, i)]
410 return {"db_list": dbs}
412 @openerpweb.jsonrequest
413 def create(self, req, fields):
414 params = dict(map(operator.itemgetter('name', 'value'), fields))
416 params['super_admin_pwd'],
418 bool(params.get('demo_data')),
420 params['create_admin_pwd']
423 return req.session.proxy("db").create_database(*create_attrs)
425 @openerpweb.jsonrequest
426 def drop(self, req, fields):
427 password, db = operator.itemgetter(
428 'drop_pwd', 'drop_db')(
429 dict(map(operator.itemgetter('name', 'value'), fields)))
432 return req.session.proxy("db").drop(password, db)
433 except xmlrpclib.Fault, e:
434 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
435 return {'error': e.faultCode, 'title': 'Drop Database'}
436 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
438 @openerpweb.httprequest
439 def backup(self, req, backup_db, backup_pwd, token):
440 db_dump = base64.b64decode(
441 req.session.proxy("db").dump(backup_pwd, backup_db))
442 filename = "%(db)s_%(timestamp)s.dump" % {
444 'timestamp': datetime.datetime.utcnow().strftime(
445 "%Y-%m-%d_%H-%M-%SZ")
447 return req.make_response(db_dump,
448 [('Content-Type', 'application/octet-stream; charset=binary'),
449 ('Content-Disposition', 'attachment; filename="' + filename + '"')],
450 {'fileToken': int(token)}
453 @openerpweb.httprequest
454 def restore(self, req, db_file, restore_pwd, new_db):
456 data = base64.b64encode(db_file.read())
457 req.session.proxy("db").restore(restore_pwd, new_db, data)
459 except xmlrpclib.Fault, e:
460 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
461 raise Exception("AccessDenied")
463 @openerpweb.jsonrequest
464 def change_password(self, req, fields):
465 old_password, new_password = operator.itemgetter(
466 'old_pwd', 'new_pwd')(
467 dict(map(operator.itemgetter('name', 'value'), fields)))
469 return req.session.proxy("db").change_admin_password(old_password, new_password)
470 except xmlrpclib.Fault, e:
471 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
472 return {'error': e.faultCode, 'title': 'Change Password'}
473 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
475 def topological_sort(modules):
476 """ Return a list of module names sorted so that their dependencies of the
477 modules are listed before the module itself
479 modules is a dict of {module_name: dependencies}
481 :param modules: modules to sort
486 dependencies = set(itertools.chain.from_iterable(modules.itervalues()))
487 # incoming edge: dependency on other module (if a depends on b, a has an
488 # incoming edge from b, aka there's an edge from b to a)
489 # outgoing edge: other module depending on this one
491 # [Tarjan 1976], http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
492 #L ← Empty list that will contain the sorted nodes
494 #S ← Set of all nodes with no outgoing edges (modules on which no other
496 S = set(module for module in modules if module not in dependencies)
499 #function visit(node n)
501 #if n has not been visited yet then
505 #change: n not web module, can not be resolved, ignore
506 if n not in modules: return
507 #for each node m with an edge from m to n do (dependencies of n)
513 #for each node n in S do
519 class Session(openerpweb.Controller):
520 _cp_path = "/web/session"
522 def session_info(self, req):
523 req.session.ensure_valid()
525 "session_id": req.session_id,
526 "uid": req.session._uid,
527 "context": req.session.get_context() if req.session._uid else {},
528 "db": req.session._db,
529 "login": req.session._login,
530 "openerp_entreprise": req.session.openerp_entreprise(),
533 @openerpweb.jsonrequest
534 def get_session_info(self, req):
535 return self.session_info(req)
537 @openerpweb.jsonrequest
538 def authenticate(self, req, db, login, password, base_location=None):
539 wsgienv = req.httprequest.environ
540 release = common.release
542 base_location=base_location,
543 HTTP_HOST=wsgienv['HTTP_HOST'],
544 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
545 user_agent="%s / %s" % (release.name, release.version),
547 req.session.authenticate(db, login, password, env)
549 return self.session_info(req)
551 @openerpweb.jsonrequest
552 def change_password (self,req,fields):
553 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
554 dict(map(operator.itemgetter('name', 'value'), fields)))
555 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
556 return {'error':'All passwords have to be filled.','title': 'Change Password'}
557 if new_password != confirm_password:
558 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
560 if req.session.model('res.users').change_password(
561 old_password, new_password):
562 return {'new_password':new_password}
564 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
565 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
567 @openerpweb.jsonrequest
568 def sc_list(self, req):
569 return req.session.model('ir.ui.view_sc').get_sc(
570 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
572 @openerpweb.jsonrequest
573 def get_lang_list(self, req):
576 'lang_list': (req.session.proxy("db").list_lang() or []),
580 return {"error": e, "title": "Languages"}
582 @openerpweb.jsonrequest
583 def modules(self, req):
584 # Compute available candidates module
585 loadable = openerpweb.addons_manifest
586 loaded = set(req.config.server_wide_modules)
587 candidates = [mod for mod in loadable if mod not in loaded]
589 # already installed modules have no dependencies
590 modules = dict.fromkeys(loaded, [])
592 # Compute auto_install modules that might be on the web side only
593 modules.update((name, openerpweb.addons_manifest[name].get('depends', []))
594 for name in candidates
595 if openerpweb.addons_manifest[name].get('auto_install'))
597 # Retrieve database installed modules
598 Modules = req.session.model('ir.module.module')
599 for module in Modules.search_read(
600 [('state','=','installed'), ('name','in', candidates)],
601 ['name', 'dependencies_id']):
602 deps = module.get('dependencies_id')
605 operator.itemgetter('name'),
606 req.session.model('ir.module.module.dependency').read(deps, ['name']))
607 modules[module['name']] = list(
608 set(modules.get(module['name'], []) + dependencies))
610 sorted_modules = topological_sort(modules)
611 return [module for module in sorted_modules if module not in loaded]
613 @openerpweb.jsonrequest
614 def eval_domain_and_context(self, req, contexts, domains,
616 """ Evaluates sequences of domains and contexts, composing them into
617 a single context, domain or group_by sequence.
619 :param list contexts: list of contexts to merge together. Contexts are
620 evaluated in sequence, all previous contexts
621 are part of their own evaluation context
622 (starting at the session context).
623 :param list domains: list of domains to merge together. Domains are
624 evaluated in sequence and appended to one another
625 (implicit AND), their evaluation domain is the
626 result of merging all contexts.
627 :param list group_by_seq: list of domains (which may be in a different
628 order than the ``contexts`` parameter),
629 evaluated in sequence, their ``'group_by'``
630 key is extracted if they have one.
635 the global context created by merging all of
639 the concatenation of all domains
642 a list of fields to group by, potentially empty (in which case
643 no group by should be performed)
645 context, domain = eval_context_and_domain(req.session,
646 common.nonliterals.CompoundContext(*(contexts or [])),
647 common.nonliterals.CompoundDomain(*(domains or [])))
649 group_by_sequence = []
650 for candidate in (group_by_seq or []):
651 ctx = req.session.eval_context(candidate, context)
652 group_by = ctx.get('group_by')
655 elif isinstance(group_by, basestring):
656 group_by_sequence.append(group_by)
658 group_by_sequence.extend(group_by)
663 'group_by': group_by_sequence
666 @openerpweb.jsonrequest
667 def save_session_action(self, req, the_action):
669 This method store an action object in the session object and returns an integer
670 identifying that action. The method get_session_action() can be used to get
673 :param the_action: The action to save in the session.
674 :type the_action: anything
675 :return: A key identifying the saved action.
678 saved_actions = req.httpsession.get('saved_actions')
679 if not saved_actions:
680 saved_actions = {"next":0, "actions":{}}
681 req.httpsession['saved_actions'] = saved_actions
682 # we don't allow more than 10 stored actions
683 if len(saved_actions["actions"]) >= 10:
684 del saved_actions["actions"][min(saved_actions["actions"])]
685 key = saved_actions["next"]
686 saved_actions["actions"][key] = the_action
687 saved_actions["next"] = key + 1
690 @openerpweb.jsonrequest
691 def get_session_action(self, req, key):
693 Gets back a previously saved action. This method can return None if the action
694 was saved since too much time (this case should be handled in a smart way).
696 :param key: The key given by save_session_action()
698 :return: The saved action or None.
701 saved_actions = req.httpsession.get('saved_actions')
702 if not saved_actions:
704 return saved_actions["actions"].get(key)
706 @openerpweb.jsonrequest
707 def check(self, req):
708 req.session.assert_valid()
711 @openerpweb.jsonrequest
712 def destroy(self, req):
713 req.session._suicide = True
715 def eval_context_and_domain(session, context, domain=None):
716 e_context = session.eval_context(context)
717 # should we give the evaluated context as an evaluation context to the domain?
718 e_domain = session.eval_domain(domain or [])
720 return e_context, e_domain
722 def load_actions_from_ir_values(req, key, key2, models, meta):
723 context = req.session.eval_context(req.context)
724 Values = req.session.model('ir.values')
725 actions = Values.get(key, key2, models, meta, context)
727 return [(id, name, clean_action(req, action))
728 for id, name, action in actions]
730 def clean_action(req, action, do_not_eval=False):
731 action.setdefault('flags', {})
733 context = req.session.eval_context(req.context)
734 eval_ctx = req.session.evaluation_context(context)
737 # values come from the server, we can just eval them
738 if isinstance(action.get('context'), basestring):
739 action['context'] = eval( action['context'], eval_ctx ) or {}
741 if isinstance(action.get('domain'), basestring):
742 action['domain'] = eval( action['domain'], eval_ctx ) or []
744 if 'context' in action:
745 action['context'] = parse_context(action['context'], req.session)
746 if 'domain' in action:
747 action['domain'] = parse_domain(action['domain'], req.session)
749 action_type = action.setdefault('type', 'ir.actions.act_window_close')
750 if action_type == 'ir.actions.act_window':
751 return fix_view_modes(action)
754 # I think generate_views,fix_view_modes should go into js ActionManager
755 def generate_views(action):
757 While the server generates a sequence called "views" computing dependencies
758 between a bunch of stuff for views coming directly from the database
759 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
760 to return custom view dictionaries generated on the fly.
762 In that case, there is no ``views`` key available on the action.
764 Since the web client relies on ``action['views']``, generate it here from
765 ``view_mode`` and ``view_id``.
767 Currently handles two different cases:
769 * no view_id, multiple view_mode
770 * single view_id, single view_mode
772 :param dict action: action descriptor dictionary to generate a views key for
774 view_id = action.get('view_id', False)
775 if isinstance(view_id, (list, tuple)):
778 # providing at least one view mode is a requirement, not an option
779 view_modes = action['view_mode'].split(',')
781 if len(view_modes) > 1:
783 raise ValueError('Non-db action dictionaries should provide '
784 'either multiple view modes or a single view '
785 'mode and an optional view id.\n\n Got view '
786 'modes %r and view id %r for action %r' % (
787 view_modes, view_id, action))
788 action['views'] = [(False, mode) for mode in view_modes]
790 action['views'] = [(view_id, view_modes[0])]
792 def fix_view_modes(action):
793 """ For historical reasons, OpenERP has weird dealings in relation to
794 view_mode and the view_type attribute (on window actions):
796 * one of the view modes is ``tree``, which stands for both list views
798 * the choice is made by checking ``view_type``, which is either
799 ``form`` for a list view or ``tree`` for an actual tree view
801 This methods simply folds the view_type into view_mode by adding a
802 new view mode ``list`` which is the result of the ``tree`` view_mode
803 in conjunction with the ``form`` view_type.
805 This method also adds a ``page`` view mode in case there is a ``form`` in
808 TODO: this should go into the doc, some kind of "peculiarities" section
810 :param dict action: an action descriptor
811 :returns: nothing, the action is modified in place
813 if not action.get('views'):
814 generate_views(action)
817 for index, (id, mode) in enumerate(action['views']):
821 if id_form is not None:
822 action['views'].insert(index + 1, (id_form, 'page'))
824 if action.pop('view_type', 'form') != 'form':
828 [id, mode if mode != 'tree' else 'list']
829 for id, mode in action['views']
834 class Menu(openerpweb.Controller):
835 _cp_path = "/web/menu"
837 @openerpweb.jsonrequest
839 return {'data': self.do_load(req)}
841 @openerpweb.jsonrequest
842 def get_user_roots(self, req):
843 return self.do_get_user_roots(req)
845 def do_get_user_roots(self, req):
846 """ Return all root menu ids visible for the session user.
848 :param req: A request object, with an OpenERP session attribute
849 :type req: < session -> OpenERPSession >
850 :return: the root menu ids
854 context = s.eval_context(req.context)
855 Menus = s.model('ir.ui.menu')
856 # If a menu action is defined use its domain to get the root menu items
857 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
859 menu_domain = [('parent_id', '=', False)]
861 domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
863 menu_domain = ast.literal_eval(domain_string)
865 return Menus.search(menu_domain, 0, False, False, context)
867 def do_load(self, req):
868 """ Loads all menu items (all applications and their sub-menus).
870 :param req: A request object, with an OpenERP session attribute
871 :type req: < session -> OpenERPSession >
872 :return: the menu root
873 :rtype: dict('children': menu_nodes)
875 context = req.session.eval_context(req.context)
876 Menus = req.session.model('ir.ui.menu')
878 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action'], context)
879 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
881 # menus are loaded fully unlike a regular tree view, cause there are a
882 # limited number of items (752 when all 6.1 addons are installed)
883 menu_ids = Menus.search([], 0, False, False, context)
884 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action'], context)
885 # adds roots at the end of the sequence, so that they will overwrite
886 # equivalent menu items from full menu read when put into id:item
887 # mapping, resulting in children being correctly set on the roots.
888 menu_items.extend(menu_roots)
890 # make a tree using parent_id
891 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
892 for menu_item in menu_items:
893 if menu_item['parent_id']:
894 parent = menu_item['parent_id'][0]
897 if parent in menu_items_map:
898 menu_items_map[parent].setdefault(
899 'children', []).append(menu_item)
901 # sort by sequence a tree using parent_id
902 for menu_item in menu_items:
903 menu_item.setdefault('children', []).sort(
904 key=operator.itemgetter('sequence'))
908 @openerpweb.jsonrequest
909 def action(self, req, menu_id):
910 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
911 [('ir.ui.menu', menu_id)], False)
912 return {"action": actions}
914 class DataSet(openerpweb.Controller):
915 _cp_path = "/web/dataset"
917 @openerpweb.jsonrequest
918 def fields(self, req, model):
919 return {'fields': req.session.model(model).fields_get(False,
920 req.session.eval_context(req.context))}
922 @openerpweb.jsonrequest
923 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
924 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
925 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
927 """ Performs a search() followed by a read() (if needed) using the
928 provided search criteria
930 :param req: a JSON-RPC request object
931 :type req: openerpweb.JsonRequest
932 :param str model: the name of the model to search on
933 :param fields: a list of the fields to return in the result records
935 :param int offset: from which index should the results start being returned
936 :param int limit: the maximum number of records to return
937 :param list domain: the search domain for the query
938 :param list sort: sorting directives
939 :returns: A structure (dict) with two keys: ids (all the ids matching
940 the (domain, context) pair) and records (paginated records
941 matching fields selection set)
944 Model = req.session.model(model)
946 context, domain = eval_context_and_domain(
947 req.session, req.context, domain)
949 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
950 if limit and len(ids) == limit:
951 length = Model.search_count(domain, context)
953 length = len(ids) + (offset or 0)
954 if fields and fields == ['id']:
955 # shortcut read if we only want the ids
959 'records': [{'id': id} for id in ids]
962 records = Model.read(ids, fields or False, context)
963 records.sort(key=lambda obj: ids.index(obj['id']))
971 @openerpweb.jsonrequest
972 def read(self, req, model, ids, fields=False):
973 return self.do_search_read(req, model, ids, fields)
975 @openerpweb.jsonrequest
976 def get(self, req, model, ids, fields=False):
977 return self.do_get(req, model, ids, fields)
979 def do_get(self, req, model, ids, fields=False):
980 """ Fetches and returns the records of the model ``model`` whose ids
983 The results are in the same order as the inputs, but elements may be
984 missing (if there is no record left for the id)
986 :param req: the JSON-RPC2 request object
987 :type req: openerpweb.JsonRequest
988 :param model: the model to read from
990 :param ids: a list of identifiers
992 :param fields: a list of fields to fetch, ``False`` or empty to fetch
993 all fields in the model
994 :type fields: list | False
995 :returns: a list of records, in the same order as the list of ids
998 Model = req.session.model(model)
999 records = Model.read(ids, fields, req.session.eval_context(req.context))
1001 record_map = dict((record['id'], record) for record in records)
1003 return [record_map[id] for id in ids if record_map.get(id)]
1005 @openerpweb.jsonrequest
1006 def load(self, req, model, id, fields):
1007 m = req.session.model(model)
1009 r = m.read([id], False, req.session.eval_context(req.context))
1012 return {'value': value}
1014 @openerpweb.jsonrequest
1015 def create(self, req, model, data):
1016 m = req.session.model(model)
1017 r = m.create(data, req.session.eval_context(req.context))
1018 return {'result': r}
1020 @openerpweb.jsonrequest
1021 def save(self, req, model, id, data):
1022 m = req.session.model(model)
1023 r = m.write([id], data, req.session.eval_context(req.context))
1024 return {'result': r}
1026 @openerpweb.jsonrequest
1027 def unlink(self, req, model, ids=()):
1028 Model = req.session.model(model)
1029 return Model.unlink(ids, req.session.eval_context(req.context))
1031 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1032 has_domain = domain_id is not None and domain_id < len(args)
1033 has_context = context_id is not None and context_id < len(args)
1035 domain = args[domain_id] if has_domain else []
1036 context = args[context_id] if has_context else {}
1037 c, d = eval_context_and_domain(req.session, context, domain)
1041 args[context_id] = c
1043 return self._call_kw(req, model, method, args, {})
1045 def _call_kw(self, req, model, method, args, kwargs):
1046 for i in xrange(len(args)):
1047 if isinstance(args[i], common.nonliterals.BaseContext):
1048 args[i] = req.session.eval_context(args[i])
1049 elif isinstance(args[i], common.nonliterals.BaseDomain):
1050 args[i] = req.session.eval_domain(args[i])
1051 for k in kwargs.keys():
1052 if isinstance(kwargs[k], common.nonliterals.BaseContext):
1053 kwargs[k] = req.session.eval_context(kwargs[k])
1054 elif isinstance(kwargs[k], common.nonliterals.BaseDomain):
1055 kwargs[k] = req.session.eval_domain(kwargs[k])
1057 return getattr(req.session.model(model), method)(*args, **kwargs)
1059 @openerpweb.jsonrequest
1060 def onchange(self, req, model, method, args, context_id=None):
1061 """ Support method for handling onchange calls: behaves much like call
1062 with the following differences:
1064 * Does not take a domain_id
1065 * Is aware of the return value's structure, and will parse the domains
1066 if needed in order to return either parsed literal domains (in JSON)
1067 or non-literal domain instances, allowing those domains to be used
1071 :type req: web.common.http.JsonRequest
1072 :param str model: object type on which to call the method
1073 :param str method: name of the onchange handler method
1074 :param list args: arguments to call the onchange handler with
1075 :param int context_id: index of the context object in the list of
1077 :return: result of the onchange call with all domains parsed
1079 result = self.call_common(req, model, method, args, context_id=context_id)
1080 if not result or 'domain' not in result:
1083 result['domain'] = dict(
1084 (k, parse_domain(v, req.session))
1085 for k, v in result['domain'].iteritems())
1089 @openerpweb.jsonrequest
1090 def call(self, req, model, method, args, domain_id=None, context_id=None):
1091 return self.call_common(req, model, method, args, domain_id, context_id)
1093 @openerpweb.jsonrequest
1094 def call_kw(self, req, model, method, args, kwargs):
1095 return self._call_kw(req, model, method, args, kwargs)
1097 @openerpweb.jsonrequest
1098 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1099 action = self.call_common(req, model, method, args, domain_id, context_id)
1100 if isinstance(action, dict) and action.get('type') != '':
1101 return {'result': clean_action(req, action)}
1102 return {'result': False}
1104 @openerpweb.jsonrequest
1105 def exec_workflow(self, req, model, id, signal):
1106 r = req.session.exec_workflow(model, id, signal)
1107 return {'result': r}
1109 @openerpweb.jsonrequest
1110 def default_get(self, req, model, fields):
1111 Model = req.session.model(model)
1112 return Model.default_get(fields, req.session.eval_context(req.context))
1114 @openerpweb.jsonrequest
1115 def name_search(self, req, model, search_str, domain=[], context={}):
1116 m = req.session.model(model)
1117 r = m.name_search(search_str+'%', domain, '=ilike', context)
1118 return {'result': r}
1120 class DataGroup(openerpweb.Controller):
1121 _cp_path = "/web/group"
1122 @openerpweb.jsonrequest
1123 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
1124 Model = req.session.model(model)
1125 context, domain = eval_context_and_domain(req.session, req.context, domain)
1127 return Model.read_group(
1128 domain or [], fields, group_by_fields, 0, False,
1129 dict(context, group_by=group_by_fields), sort or False)
1131 class View(openerpweb.Controller):
1132 _cp_path = "/web/view"
1134 def fields_view_get(self, req, model, view_id, view_type,
1135 transform=True, toolbar=False, submenu=False):
1136 Model = req.session.model(model)
1137 context = req.session.eval_context(req.context)
1138 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1139 # todo fme?: check that we should pass the evaluated context here
1140 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1141 if toolbar and transform:
1142 self.process_toolbar(req, fvg['toolbar'])
1145 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1146 # depending on how it feels, xmlrpclib.ServerProxy can translate
1147 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1148 # enjoy unicode strings which can not be trivially converted to
1149 # strings, and it blows up during parsing.
1151 # So ensure we fix this retardation by converting view xml back to
1153 if isinstance(fvg['arch'], unicode):
1154 arch = fvg['arch'].encode('utf-8')
1157 fvg['arch_string'] = arch
1160 evaluation_context = session.evaluation_context(context or {})
1161 xml = self.transform_view(arch, session, evaluation_context)
1163 xml = ElementTree.fromstring(arch)
1164 fvg['arch'] = common.xml2json.from_elementtree(xml, preserve_whitespaces)
1166 if 'id' in fvg['fields']:
1167 # Special case for id's
1168 id_field = fvg['fields']['id']
1169 id_field['original_type'] = id_field['type']
1170 id_field['type'] = 'id'
1172 for field in fvg['fields'].itervalues():
1173 if field.get('views'):
1174 for view in field["views"].itervalues():
1175 self.process_view(session, view, None, transform)
1176 if field.get('domain'):
1177 field["domain"] = parse_domain(field["domain"], session)
1178 if field.get('context'):
1179 field["context"] = parse_context(field["context"], session)
1181 def process_toolbar(self, req, toolbar):
1183 The toolbar is a mapping of section_key: [action_descriptor]
1185 We need to clean all those actions in order to ensure correct
1188 for actions in toolbar.itervalues():
1189 for action in actions:
1190 if 'context' in action:
1191 action['context'] = parse_context(
1192 action['context'], req.session)
1193 if 'domain' in action:
1194 action['domain'] = parse_domain(
1195 action['domain'], req.session)
1197 @openerpweb.jsonrequest
1198 def add_custom(self, req, view_id, arch):
1199 CustomView = req.session.model('ir.ui.view.custom')
1201 'user_id': req.session._uid,
1204 }, req.session.eval_context(req.context))
1205 return {'result': True}
1207 @openerpweb.jsonrequest
1208 def undo_custom(self, req, view_id, reset=False):
1209 CustomView = req.session.model('ir.ui.view.custom')
1210 context = req.session.eval_context(req.context)
1211 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1212 0, False, False, context)
1215 CustomView.unlink(vcustom, context)
1217 CustomView.unlink([vcustom[0]], context)
1218 return {'result': True}
1219 return {'result': False}
1221 def transform_view(self, view_string, session, context=None):
1222 # transform nodes on the fly via iterparse, instead of
1223 # doing it statically on the parsing result
1224 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1226 for event, elem in parser:
1227 if event == "start":
1230 self.parse_domains_and_contexts(elem, session)
1233 def parse_domains_and_contexts(self, elem, session):
1234 """ Converts domains and contexts from the view into Python objects,
1235 either literals if they can be parsed by literal_eval or a special
1236 placeholder object if the domain or context refers to free variables.
1238 :param elem: the current node being parsed
1239 :type param: xml.etree.ElementTree.Element
1240 :param session: OpenERP session object, used to store and retrieve
1242 :type session: openerpweb.openerpweb.OpenERPSession
1244 for el in ['domain', 'filter_domain']:
1245 domain = elem.get(el, '').strip()
1247 elem.set(el, parse_domain(domain, session))
1248 elem.set(el + '_string', domain)
1249 for el in ['context', 'default_get']:
1250 context_string = elem.get(el, '').strip()
1252 elem.set(el, parse_context(context_string, session))
1253 elem.set(el + '_string', context_string)
1255 @openerpweb.jsonrequest
1256 def load(self, req, model, view_id, view_type, toolbar=False):
1257 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1259 def parse_domain(domain, session):
1260 """ Parses an arbitrary string containing a domain, transforms it
1261 to either a literal domain or a :class:`common.nonliterals.Domain`
1263 :param domain: the domain to parse, if the domain is not a string it
1264 is assumed to be a literal domain and is returned as-is
1265 :param session: Current OpenERP session
1266 :type session: openerpweb.openerpweb.OpenERPSession
1268 if not isinstance(domain, basestring):
1271 return ast.literal_eval(domain)
1274 return common.nonliterals.Domain(session, domain)
1276 def parse_context(context, session):
1277 """ Parses an arbitrary string containing a context, transforms it
1278 to either a literal context or a :class:`common.nonliterals.Context`
1280 :param context: the context to parse, if the context is not a string it
1281 is assumed to be a literal domain and is returned as-is
1282 :param session: Current OpenERP session
1283 :type session: openerpweb.openerpweb.OpenERPSession
1285 if not isinstance(context, basestring):
1288 return ast.literal_eval(context)
1290 return common.nonliterals.Context(session, context)
1292 class ListView(View):
1293 _cp_path = "/web/listview"
1295 def process_colors(self, view, row, context):
1296 colors = view['arch']['attrs'].get('colors')
1303 for pair in colors.split(';')
1304 if eval(pair.split(':')[1], dict(context, **row))
1309 elif len(color) == 1:
1313 class TreeView(View):
1314 _cp_path = "/web/treeview"
1316 @openerpweb.jsonrequest
1317 def action(self, req, model, id):
1318 return load_actions_from_ir_values(
1319 req,'action', 'tree_but_open',[(model, id)],
1322 class SearchView(View):
1323 _cp_path = "/web/searchview"
1325 @openerpweb.jsonrequest
1326 def load(self, req, model, view_id):
1327 fields_view = self.fields_view_get(req, model, view_id, 'search')
1328 return {'fields_view': fields_view}
1330 @openerpweb.jsonrequest
1331 def fields_get(self, req, model):
1332 Model = req.session.model(model)
1333 fields = Model.fields_get(False, req.session.eval_context(req.context))
1334 for field in fields.values():
1335 # shouldn't convert the views too?
1336 if field.get('domain'):
1337 field["domain"] = parse_domain(field["domain"], req.session)
1338 if field.get('context'):
1339 field["context"] = parse_context(field["context"], req.session)
1340 return {'fields': fields}
1342 @openerpweb.jsonrequest
1343 def get_filters(self, req, model):
1344 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1345 Model = req.session.model("ir.filters")
1346 filters = Model.get_filters(model)
1347 for filter in filters:
1349 parsed_context = parse_context(filter["context"], req.session)
1350 filter["context"] = (parsed_context
1351 if not isinstance(parsed_context, common.nonliterals.BaseContext)
1352 else req.session.eval_context(parsed_context))
1354 parsed_domain = parse_domain(filter["domain"], req.session)
1355 filter["domain"] = (parsed_domain
1356 if not isinstance(parsed_domain, common.nonliterals.BaseDomain)
1357 else req.session.eval_domain(parsed_domain))
1359 logger.exception("Failed to parse custom filter %s in %s",
1360 filter['name'], model)
1361 filter['disabled'] = True
1362 del filter['context']
1363 del filter['domain']
1366 @openerpweb.jsonrequest
1367 def save_filter(self, req, model, name, context_to_save, domain):
1368 Model = req.session.model("ir.filters")
1369 ctx = common.nonliterals.CompoundContext(context_to_save)
1370 ctx.session = req.session
1371 ctx = ctx.evaluate()
1372 domain = common.nonliterals.CompoundDomain(domain)
1373 domain.session = req.session
1374 domain = domain.evaluate()
1375 uid = req.session._uid
1376 context = req.session.eval_context(req.context)
1377 to_return = Model.create_or_replace({"context": ctx,
1385 @openerpweb.jsonrequest
1386 def add_to_dashboard(self, req, menu_id, action_id, context_to_save, domain, view_mode, name=''):
1387 ctx = common.nonliterals.CompoundContext(context_to_save)
1388 ctx.session = req.session
1389 ctx = ctx.evaluate()
1390 ctx['dashboard_merge_domains_contexts'] = False # TODO: replace this 6.1 workaround by attribute on <action/>
1391 domain = common.nonliterals.CompoundDomain(domain)
1392 domain.session = req.session
1393 domain = domain.evaluate()
1395 dashboard_action = load_actions_from_ir_values(req, 'action', 'tree_but_open',
1396 [('ir.ui.menu', menu_id)], False)
1397 if dashboard_action:
1398 action = dashboard_action[0][2]
1399 if action['res_model'] == 'board.board' and action['views'][0][1] == 'form':
1400 # Maybe should check the content instead of model board.board ?
1401 view_id = action['views'][0][0]
1402 board = req.session.model(action['res_model']).fields_view_get(view_id, 'form')
1403 if board and 'arch' in board:
1404 xml = ElementTree.fromstring(board['arch'])
1405 column = xml.find('./board/column')
1406 if column is not None:
1407 new_action = ElementTree.Element('action', {
1408 'name' : str(action_id),
1410 'view_mode' : view_mode,
1411 'context' : str(ctx),
1412 'domain' : str(domain)
1414 column.insert(0, new_action)
1415 arch = ElementTree.tostring(xml, 'utf-8')
1416 return req.session.model('ir.ui.view.custom').create({
1417 'user_id': req.session._uid,
1420 }, req.session.eval_context(req.context))
1424 class Binary(openerpweb.Controller):
1425 _cp_path = "/web/binary"
1427 @openerpweb.httprequest
1428 def image(self, req, model, id, field, **kw):
1429 Model = req.session.model(model)
1430 context = req.session.eval_context(req.context)
1434 res = Model.default_get([field], context).get(field)
1436 res = Model.read([int(id)], [field], context)[0].get(field)
1437 image_data = base64.b64decode(res)
1438 except (TypeError, xmlrpclib.Fault):
1439 image_data = self.placeholder(req)
1440 return req.make_response(image_data, [
1441 ('Content-Type', 'image/png'), ('Content-Length', len(image_data))])
1442 def placeholder(self, req):
1443 addons_path = openerpweb.addons_manifest['web']['addons_path']
1444 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1446 @openerpweb.httprequest
1447 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1448 """ Download link for files stored as binary fields.
1450 If the ``id`` parameter is omitted, fetches the default value for the
1451 binary field (via ``default_get``), otherwise fetches the field for
1452 that precise record.
1454 :param req: OpenERP request
1455 :type req: :class:`web.common.http.HttpRequest`
1456 :param str model: name of the model to fetch the binary from
1457 :param str field: binary field
1458 :param str id: id of the record from which to fetch the binary
1459 :param str filename_field: field holding the file's name, if any
1460 :returns: :class:`werkzeug.wrappers.Response`
1462 Model = req.session.model(model)
1463 context = req.session.eval_context(req.context)
1466 fields.append(filename_field)
1468 res = Model.read([int(id)], fields, context)[0]
1470 res = Model.default_get(fields, context)
1471 filecontent = base64.b64decode(res.get(field, ''))
1473 return req.not_found()
1475 filename = '%s_%s' % (model.replace('.', '_'), id)
1477 filename = res.get(filename_field, '') or filename
1478 return req.make_response(filecontent,
1479 [('Content-Type', 'application/octet-stream'),
1480 ('Content-Disposition', 'attachment; filename="%s"' % filename)])
1482 @openerpweb.httprequest
1483 def saveas_ajax(self, req, data, token):
1484 jdata = simplejson.loads(data)
1485 model = jdata['model']
1486 field = jdata['field']
1487 id = jdata.get('id', None)
1488 filename_field = jdata.get('filename_field', None)
1489 context = jdata.get('context', dict())
1491 context = req.session.eval_context(context)
1492 Model = req.session.model(model)
1495 fields.append(filename_field)
1497 res = Model.read([int(id)], fields, context)[0]
1499 res = Model.default_get(fields, context)
1500 filecontent = base64.b64decode(res.get(field, ''))
1502 raise ValueError("No content found for field '%s' on '%s:%s'" %
1505 filename = '%s_%s' % (model.replace('.', '_'), id)
1507 filename = res.get(filename_field, '') or filename
1508 return req.make_response(filecontent,
1509 headers=[('Content-Type', 'application/octet-stream'),
1510 ('Content-Disposition', 'attachment; filename="%s"' % filename)],
1511 cookies={'fileToken': int(token)})
1513 @openerpweb.httprequest
1514 def upload(self, req, callback, ufile):
1515 # TODO: might be useful to have a configuration flag for max-length file uploads
1517 out = """<script language="javascript" type="text/javascript">
1518 var win = window.top.window,
1520 if (typeof(callback) === 'function') {
1521 callback.apply(this, %s);
1523 win.jQuery('#oe_notification', win.document).notify('create', {
1524 title: "Ajax File Upload",
1525 text: "Could not find callback"
1530 args = [len(data), ufile.filename,
1531 ufile.content_type, base64.b64encode(data)]
1532 except Exception, e:
1533 args = [False, e.message]
1534 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1536 @openerpweb.httprequest
1537 def upload_attachment(self, req, callback, model, id, ufile):
1538 context = req.session.eval_context(req.context)
1539 Model = req.session.model('ir.attachment')
1541 out = """<script language="javascript" type="text/javascript">
1542 var win = window.top.window,
1544 if (typeof(callback) === 'function') {
1545 callback.call(this, %s);
1548 attachment_id = Model.create({
1549 'name': ufile.filename,
1550 'datas': base64.encodestring(ufile.read()),
1551 'datas_fname': ufile.filename,
1556 'filename': ufile.filename,
1559 except Exception, e:
1560 args = { 'error': e.message }
1561 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1563 class Action(openerpweb.Controller):
1564 _cp_path = "/web/action"
1566 @openerpweb.jsonrequest
1567 def load(self, req, action_id, do_not_eval=False):
1568 Actions = req.session.model('ir.actions.actions')
1570 context = req.session.eval_context(req.context)
1571 action_type = Actions.read([action_id], ['type'], context)
1574 if action_type[0]['type'] == 'ir.actions.report.xml':
1575 ctx.update({'bin_size': True})
1577 action = req.session.model(action_type[0]['type']).read([action_id], False, ctx)
1579 value = clean_action(req, action[0], do_not_eval)
1580 return {'result': value}
1582 @openerpweb.jsonrequest
1583 def run(self, req, action_id):
1584 return clean_action(req, req.session.model('ir.actions.server').run(
1585 [action_id], req.session.eval_context(req.context)))
1588 _cp_path = "/web/export"
1590 @openerpweb.jsonrequest
1591 def formats(self, req):
1592 """ Returns all valid export formats
1594 :returns: for each export format, a pair of identifier and printable name
1595 :rtype: [(str, str)]
1599 for path, controller in openerpweb.controllers_path.iteritems()
1600 if path.startswith(self._cp_path)
1601 if hasattr(controller, 'fmt')
1602 ], key=operator.itemgetter("label"))
1604 def fields_get(self, req, model):
1605 Model = req.session.model(model)
1606 fields = Model.fields_get(False, req.session.eval_context(req.context))
1609 @openerpweb.jsonrequest
1610 def get_fields(self, req, model, prefix='', parent_name= '',
1611 import_compat=True, parent_field_type=None,
1614 if import_compat and parent_field_type == "many2one":
1617 fields = self.fields_get(req, model)
1620 fields.pop('id', None)
1622 fields['.id'] = fields.pop('id', {'string': 'ID'})
1624 fields_sequence = sorted(fields.iteritems(),
1625 key=lambda field: field[1].get('string', ''))
1628 for field_name, field in fields_sequence:
1630 if exclude and field_name in exclude:
1632 if field.get('readonly'):
1633 # If none of the field's states unsets readonly, skip the field
1634 if all(dict(attrs).get('readonly', True)
1635 for attrs in field.get('states', {}).values()):
1638 id = prefix + (prefix and '/'or '') + field_name
1639 name = parent_name + (parent_name and '/' or '') + field['string']
1640 record = {'id': id, 'string': name,
1641 'value': id, 'children': False,
1642 'field_type': field.get('type'),
1643 'required': field.get('required'),
1644 'relation_field': field.get('relation_field')}
1645 records.append(record)
1647 if len(name.split('/')) < 3 and 'relation' in field:
1648 ref = field.pop('relation')
1649 record['value'] += '/id'
1650 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1652 if not import_compat or field['type'] == 'one2many':
1653 # m2m field in import_compat is childless
1654 record['children'] = True
1658 @openerpweb.jsonrequest
1659 def namelist(self,req, model, export_id):
1660 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1661 export = req.session.model("ir.exports").read([export_id])[0]
1662 export_fields_list = req.session.model("ir.exports.line").read(
1663 export['export_fields'])
1665 fields_data = self.fields_info(
1666 req, model, map(operator.itemgetter('name'), export_fields_list))
1669 {'name': field['name'], 'label': fields_data[field['name']]}
1670 for field in export_fields_list
1673 def fields_info(self, req, model, export_fields):
1675 fields = self.fields_get(req, model)
1677 # To make fields retrieval more efficient, fetch all sub-fields of a
1678 # given field at the same time. Because the order in the export list is
1679 # arbitrary, this requires ordering all sub-fields of a given field
1680 # together so they can be fetched at the same time
1682 # Works the following way:
1683 # * sort the list of fields to export, the default sorting order will
1684 # put the field itself (if present, for xmlid) and all of its
1685 # sub-fields right after it
1686 # * then, group on: the first field of the path (which is the same for
1687 # a field and for its subfields and the length of splitting on the
1688 # first '/', which basically means grouping the field on one side and
1689 # all of the subfields on the other. This way, we have the field (for
1690 # the xmlid) with length 1, and all of the subfields with the same
1691 # base but a length "flag" of 2
1692 # * if we have a normal field (length 1), just add it to the info
1693 # mapping (with its string) as-is
1694 # * otherwise, recursively call fields_info via graft_subfields.
1695 # all graft_subfields does is take the result of fields_info (on the
1696 # field's model) and prepend the current base (current field), which
1697 # rebuilds the whole sub-tree for the field
1699 # result: because we're not fetching the fields_get for half the
1700 # database models, fetching a namelist with a dozen fields (including
1701 # relational data) falls from ~6s to ~300ms (on the leads model).
1702 # export lists with no sub-fields (e.g. import_compatible lists with
1703 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1704 # there's a single fields_get to execute)
1705 for (base, length), subfields in itertools.groupby(
1706 sorted(export_fields),
1707 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1708 subfields = list(subfields)
1710 # subfields is a seq of $base/*rest, and not loaded yet
1711 info.update(self.graft_subfields(
1712 req, fields[base]['relation'], base, fields[base]['string'],
1716 info[base] = fields[base]['string']
1720 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1721 export_fields = [field.split('/', 1)[1] for field in fields]
1723 (prefix + '/' + k, prefix_string + '/' + v)
1724 for k, v in self.fields_info(req, model, export_fields).iteritems())
1726 #noinspection PyPropertyDefinition
1728 def content_type(self):
1729 """ Provides the format's content type """
1730 raise NotImplementedError()
1732 def filename(self, base):
1733 """ Creates a valid filename for the format (with extension) from the
1734 provided base name (exension-less)
1736 raise NotImplementedError()
1738 def from_data(self, fields, rows):
1739 """ Conversion method from OpenERP's export data to whatever the
1740 current export class outputs
1742 :params list fields: a list of fields to export
1743 :params list rows: a list of records to export
1747 raise NotImplementedError()
1749 @openerpweb.httprequest
1750 def index(self, req, data, token):
1751 model, fields, ids, domain, import_compat = \
1752 operator.itemgetter('model', 'fields', 'ids', 'domain',
1754 simplejson.loads(data))
1756 context = req.session.eval_context(req.context)
1757 Model = req.session.model(model)
1758 ids = ids or Model.search(domain, 0, False, False, context)
1760 field_names = map(operator.itemgetter('name'), fields)
1761 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1764 columns_headers = field_names
1766 columns_headers = [val['label'].strip() for val in fields]
1769 return req.make_response(self.from_data(columns_headers, import_data),
1770 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1771 ('Content-Type', self.content_type)],
1772 cookies={'fileToken': int(token)})
1774 class CSVExport(Export):
1775 _cp_path = '/web/export/csv'
1776 fmt = {'tag': 'csv', 'label': 'CSV'}
1779 def content_type(self):
1780 return 'text/csv;charset=utf8'
1782 def filename(self, base):
1783 return base + '.csv'
1785 def from_data(self, fields, rows):
1787 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1789 writer.writerow([name.encode('utf-8') for name in fields])
1794 if isinstance(d, basestring):
1795 d = d.replace('\n',' ').replace('\t',' ')
1797 d = d.encode('utf-8')
1798 except UnicodeError:
1800 if d is False: d = None
1802 writer.writerow(row)
1809 class ExcelExport(Export):
1810 _cp_path = '/web/export/xls'
1814 'error': None if xlwt else "XLWT required"
1818 def content_type(self):
1819 return 'application/vnd.ms-excel'
1821 def filename(self, base):
1822 return base + '.xls'
1824 def from_data(self, fields, rows):
1825 workbook = xlwt.Workbook()
1826 worksheet = workbook.add_sheet('Sheet 1')
1828 for i, fieldname in enumerate(fields):
1829 worksheet.write(0, i, fieldname)
1830 worksheet.col(i).width = 8000 # around 220 pixels
1832 style = xlwt.easyxf('align: wrap yes')
1834 for row_index, row in enumerate(rows):
1835 for cell_index, cell_value in enumerate(row):
1836 if isinstance(cell_value, basestring):
1837 cell_value = re.sub("\r", " ", cell_value)
1838 if cell_value is False: cell_value = None
1839 worksheet.write(row_index + 1, cell_index, cell_value, style)
1848 class Reports(View):
1849 _cp_path = "/web/report"
1850 POLLING_DELAY = 0.25
1852 'doc': 'application/vnd.ms-word',
1853 'html': 'text/html',
1854 'odt': 'application/vnd.oasis.opendocument.text',
1855 'pdf': 'application/pdf',
1856 'sxw': 'application/vnd.sun.xml.writer',
1857 'xls': 'application/vnd.ms-excel',
1860 @openerpweb.httprequest
1861 def index(self, req, action, token):
1862 action = simplejson.loads(action)
1864 report_srv = req.session.proxy("report")
1865 context = req.session.eval_context(
1866 common.nonliterals.CompoundContext(
1867 req.context or {}, action[ "context"]))
1870 report_ids = context["active_ids"]
1871 if 'report_type' in action:
1872 report_data['report_type'] = action['report_type']
1873 if 'datas' in action:
1874 if 'ids' in action['datas']:
1875 report_ids = action['datas'].pop('ids')
1876 report_data.update(action['datas'])
1878 report_id = report_srv.report(
1879 req.session._db, req.session._uid, req.session._password,
1880 action["report_name"], report_ids,
1881 report_data, context)
1883 report_struct = None
1885 report_struct = report_srv.report_get(
1886 req.session._db, req.session._uid, req.session._password, report_id)
1887 if report_struct["state"]:
1890 time.sleep(self.POLLING_DELAY)
1892 report = base64.b64decode(report_struct['result'])
1893 if report_struct.get('code') == 'zlib':
1894 report = zlib.decompress(report)
1895 report_mimetype = self.TYPES_MAPPING.get(
1896 report_struct['format'], 'octet-stream')
1897 return req.make_response(report,
1899 ('Content-Disposition', 'attachment; filename="%s.%s"' % (action['report_name'], report_struct['format'])),
1900 ('Content-Type', report_mimetype),
1901 ('Content-Length', len(report))],
1902 cookies={'fileToken': int(token)})
1905 _cp_path = "/web/import"
1907 def fields_get(self, req, model):
1908 Model = req.session.model(model)
1909 fields = Model.fields_get(False, req.session.eval_context(req.context))
1912 @openerpweb.httprequest
1913 def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1915 data = list(csv.reader(
1916 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1917 except csv.Error, e:
1919 return '<script>window.top.%s(%s);</script>' % (
1920 jsonp, simplejson.dumps({'error': {
1921 'message': 'Error parsing CSV file: %s' % e,
1922 # decodes each byte to a unicode character, which may or
1923 # may not be printable, but decoding will succeed.
1924 # Otherwise simplejson will try to decode the `str` using
1925 # utf-8, which is very likely to blow up on characters out
1926 # of the ascii range (in range [128, 256))
1927 'preview': csvfile.read(200).decode('iso-8859-1')}}))
1930 return '<script>window.top.%s(%s);</script>' % (
1931 jsonp, simplejson.dumps(
1932 {'records': data[:10]}, encoding=csvcode))
1933 except UnicodeDecodeError:
1934 return '<script>window.top.%s(%s);</script>' % (
1935 jsonp, simplejson.dumps({
1936 'message': u"Failed to decode CSV file using encoding %s, "
1937 u"try switching to a different encoding" % csvcode
1940 @openerpweb.httprequest
1941 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
1943 modle_obj = req.session.model(model)
1944 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
1945 simplejson.loads(meta))
1948 if not (csvdel and len(csvdel) == 1):
1949 error = u"The CSV delimiter must be a single character"
1951 if not indices and fields:
1952 error = u"You must select at least one field to import"
1955 return '<script>window.top.%s(%s);</script>' % (
1956 jsonp, simplejson.dumps({'error': {'message': error}}))
1958 # skip ignored records
1959 data_record = itertools.islice(
1960 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
1963 # if only one index, itemgetter will return an atom rather than a tuple
1964 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
1965 else: mapper = operator.itemgetter(*indices)
1970 # decode each data row
1972 [record.decode(csvcode) for record in row]
1973 for row in itertools.imap(mapper, data_record)
1974 # don't insert completely empty rows (can happen due to fields
1975 # filtering in case of e.g. o2m content rows)
1978 except UnicodeDecodeError:
1979 error = u"Failed to decode CSV file using encoding %s" % csvcode
1980 except csv.Error, e:
1981 error = u"Could not process CSV file: %s" % e
1983 # If the file contains nothing,
1985 error = u"File to import is empty"
1987 return '<script>window.top.%s(%s);</script>' % (
1988 jsonp, simplejson.dumps({'error': {'message': error}}))
1991 (code, record, message, _nope) = modle_obj.import_data(
1992 fields, data, 'init', '', False,
1993 req.session.eval_context(req.context))
1994 except xmlrpclib.Fault, e:
1995 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
1996 return '<script>window.top.%s(%s);</script>' % (
1997 jsonp, simplejson.dumps({'error':error}))
2000 return '<script>window.top.%s(%s);</script>' % (
2001 jsonp, simplejson.dumps({'success':True}))
2003 msg = u"Error during import: %s\n\nTrying to import record %r" % (
2005 return '<script>window.top.%s(%s);</script>' % (
2006 jsonp, simplejson.dumps({'error': {'message':msg}}))