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()
82 with open(f, 'rb') as fp:
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);
116 # Validated by diff -u of sass2scss against:
117 # sass-convert -F sass -T scss openerp.sass openerp.scss
120 reComment = re.compile(r'//.*$')
121 reIndent = re.compile(r'^\s+')
122 reIgnore = re.compile(r'^\s*(//.*)?$')
123 reFixes = { re.compile(r'\(\((.*)\)\)') : r'(\1)', }
126 for l in src.split('\n'):
128 if reIgnore.search(l): continue
129 l = reComment.sub('', l)
131 indent = reIndent.match(l)
132 level = indent.end() if indent else 0
135 prevBlocks[lastLevel] = block
137 block[-1] = (block[-1], newBlock)
139 elif level<lastLevel:
140 block = prevBlocks[level]
144 for ereg, repl in reFixes.items():
145 l = ereg.sub(repl if type(repl)==str else repl(), l)
148 def write(sass, level=-1):
151 if type(sass)==tuple:
153 out += indent+sass[0]+" {\n"
155 out += write(e, level+1)
157 out = out.rstrip(" \n")
162 out += indent+sass+";\n"
166 class WebClient(openerpweb.Controller):
167 _cp_path = "/web/webclient"
169 def server_wide_modules(self, req):
170 addons = [i for i in req.config.server_wide_modules if i in openerpweb.addons_manifest]
173 def manifest_glob(self, req, addons, key):
175 addons = self.server_wide_modules(req)
177 addons = addons.split(',')
180 manifest = openerpweb.addons_manifest.get(addon, None)
183 # ensure does not ends with /
184 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
185 globlist = manifest.get(key, [])
186 for pattern in globlist:
187 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
188 r.append( (path, path[len(addons_path):]))
191 def manifest_list(self, req, mods, extension):
193 path = '/web/webclient/' + extension
195 path += '?mods=' + mods
197 no_sugar = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1
198 no_sugar = no_sugar or req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
200 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in self.manifest_glob(req, mods, extension)]
202 return [el[1] for el in self.manifest_glob(req, mods, extension)]
204 @openerpweb.jsonrequest
205 def csslist(self, req, mods=None):
206 return self.manifest_list(req, mods, 'css')
208 @openerpweb.jsonrequest
209 def jslist(self, req, mods=None):
210 return self.manifest_list(req, mods, 'js')
212 @openerpweb.jsonrequest
213 def qweblist(self, req, mods=None):
214 return self.manifest_list(req, mods, 'qweb')
216 def get_last_modified(self, files):
217 """ Returns the modification time of the most recently modified
220 :param list(str) files: names of files to check
221 :return: most recent modification time amongst the fileset
222 :rtype: datetime.datetime
226 return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
228 return datetime.datetime(1970, 1, 1)
230 def make_conditional(self, req, response, last_modified=None, etag=None):
231 """ Makes the provided response conditional based upon the request,
232 and mandates revalidation from clients
234 Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
235 setting ``last_modified`` and ``etag`` correctly on the response object
237 :param req: OpenERP request
238 :type req: web.common.http.WebRequest
239 :param response: Werkzeug response
240 :type response: werkzeug.wrappers.Response
241 :param datetime.datetime last_modified: last modification date of the response content
242 :param str etag: some sort of checksum of the content (deep etag)
243 :return: the response object provided
244 :rtype: werkzeug.wrappers.Response
246 response.cache_control.must_revalidate = True
247 response.cache_control.max_age = 0
249 response.last_modified = last_modified
251 response.set_etag(etag)
252 return response.make_conditional(req.httprequest)
254 @openerpweb.httprequest
255 def css(self, req, mods=None):
256 files = list(self.manifest_glob(req, mods, 'css'))
257 last_modified = self.get_last_modified(f[0] for f in files)
258 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
259 return werkzeug.wrappers.Response(status=304)
261 file_map = dict(files)
263 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
264 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
267 """read the a css file and absolutify all relative uris"""
268 with open(f, 'rb') as fp:
269 data = fp.read().decode('utf-8')
272 # convert FS path into web path
273 web_dir = '/'.join(os.path.dirname(path).split(os.path.sep))
277 r"""@import \1%s/""" % (web_dir,),
283 r"""url(\1%s/""" % (web_dir,),
286 return data.encode('utf-8')
288 content, checksum = concat_files((f[0] for f in files), reader)
290 return self.make_conditional(
291 req, req.make_response(content, [('Content-Type', 'text/css')]),
292 last_modified, checksum)
294 @openerpweb.httprequest
295 def js(self, req, mods=None):
296 files = [f[0] for f in self.manifest_glob(req, mods, 'js')]
297 last_modified = self.get_last_modified(files)
298 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
299 return werkzeug.wrappers.Response(status=304)
301 content, checksum = concat_files(files, intersperse=';')
303 return self.make_conditional(
304 req, req.make_response(content, [('Content-Type', 'application/javascript')]),
305 last_modified, checksum)
307 @openerpweb.httprequest
308 def qweb(self, req, mods=None):
309 files = [f[0] for f in self.manifest_glob(req, mods, 'qweb')]
310 last_modified = self.get_last_modified(files)
311 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
312 return werkzeug.wrappers.Response(status=304)
314 content,checksum = concat_xml(files)
316 return self.make_conditional(
317 req, req.make_response(content, [('Content-Type', 'text/xml')]),
318 last_modified, checksum)
320 @openerpweb.httprequest
321 def home(self, req, s_action=None, **kw):
322 js = "\n ".join('<script type="text/javascript" src="%s"></script>'%i for i in self.manifest_list(req, None, 'js'))
323 css = "\n ".join('<link rel="stylesheet" href="%s">'%i for i in self.manifest_list(req, None, 'css'))
325 r = html_template % {
328 'modules': simplejson.dumps(self.server_wide_modules(req)),
329 'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
333 @openerpweb.httprequest
334 def login(self, req, db, login, key):
335 req.session.authenticate(db, login, key, {})
336 redirect = werkzeug.utils.redirect('/web/webclient/home', 303)
337 cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
338 redirect.set_cookie('instance0|session_id', cookie_val)
341 @openerpweb.jsonrequest
342 def translations(self, req, mods, lang):
343 lang_model = req.session.model('res.lang')
344 ids = lang_model.search([("code", "=", lang)])
346 lang_obj = lang_model.read(ids[0], ["direction", "date_format", "time_format",
347 "grouping", "decimal_point", "thousands_sep"])
355 langs = lang.split(separator)
356 langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
359 for addon_name in mods:
360 transl = {"messages":[]}
361 transs[addon_name] = transl
362 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
364 f_name = os.path.join(addons_path, addon_name, "i18n", l + ".po")
365 if not os.path.exists(f_name):
368 with open(f_name) as t_file:
369 po = babel.messages.pofile.read_po(t_file)
373 if x.id and x.string and "openerp-web" in x.auto_comments:
374 transl["messages"].append({'id': x.id, 'string': x.string})
375 return {"modules": transs,
376 "lang_parameters": lang_obj}
378 @openerpweb.jsonrequest
379 def version_info(self, req):
381 "version": common.release.version
384 class Proxy(openerpweb.Controller):
385 _cp_path = '/web/proxy'
387 @openerpweb.jsonrequest
388 def load(self, req, path):
389 """ Proxies an HTTP request through a JSON request.
391 It is strongly recommended to not request binary files through this,
392 as the result will be a binary data blob as well.
394 :param req: OpenERP request
395 :param path: actual request path
396 :return: file content
398 from werkzeug.test import Client
399 from werkzeug.wrappers import BaseResponse
401 return Client(req.httprequest.app, BaseResponse).get(path).data
403 class Database(openerpweb.Controller):
404 _cp_path = "/web/database"
406 @openerpweb.jsonrequest
407 def get_list(self, req):
408 proxy = req.session.proxy("db")
410 h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
412 r = req.config.dbfilter.replace('%h', h).replace('%d', d)
413 dbs = [i for i in dbs if re.match(r, i)]
414 return {"db_list": dbs}
416 @openerpweb.jsonrequest
417 def create(self, req, fields):
418 params = dict(map(operator.itemgetter('name', 'value'), fields))
420 params['super_admin_pwd'],
422 bool(params.get('demo_data')),
424 params['create_admin_pwd']
427 return req.session.proxy("db").create_database(*create_attrs)
429 @openerpweb.jsonrequest
430 def drop(self, req, fields):
431 password, db = operator.itemgetter(
432 'drop_pwd', 'drop_db')(
433 dict(map(operator.itemgetter('name', 'value'), fields)))
436 return req.session.proxy("db").drop(password, db)
437 except xmlrpclib.Fault, e:
438 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
439 return {'error': e.faultCode, 'title': 'Drop Database'}
440 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
442 @openerpweb.httprequest
443 def backup(self, req, backup_db, backup_pwd, token):
445 db_dump = base64.b64decode(
446 req.session.proxy("db").dump(backup_pwd, backup_db))
447 filename = "%(db)s_%(timestamp)s.dump" % {
449 'timestamp': datetime.datetime.utcnow().strftime(
450 "%Y-%m-%d_%H-%M-%SZ")
452 return req.make_response(db_dump,
453 [('Content-Type', 'application/octet-stream; charset=binary'),
454 ('Content-Disposition', 'attachment; filename="' + filename + '"')],
455 {'fileToken': int(token)}
457 except xmlrpclib.Fault, e:
458 return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
460 @openerpweb.httprequest
461 def restore(self, req, db_file, restore_pwd, new_db):
463 data = base64.b64encode(db_file.read())
464 req.session.proxy("db").restore(restore_pwd, new_db, data)
466 except xmlrpclib.Fault, e:
467 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
468 raise Exception("AccessDenied")
470 @openerpweb.jsonrequest
471 def change_password(self, req, fields):
472 old_password, new_password = operator.itemgetter(
473 'old_pwd', 'new_pwd')(
474 dict(map(operator.itemgetter('name', 'value'), fields)))
476 return req.session.proxy("db").change_admin_password(old_password, new_password)
477 except xmlrpclib.Fault, e:
478 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
479 return {'error': e.faultCode, 'title': 'Change Password'}
480 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
482 def topological_sort(modules):
483 """ Return a list of module names sorted so that their dependencies of the
484 modules are listed before the module itself
486 modules is a dict of {module_name: dependencies}
488 :param modules: modules to sort
493 dependencies = set(itertools.chain.from_iterable(modules.itervalues()))
494 # incoming edge: dependency on other module (if a depends on b, a has an
495 # incoming edge from b, aka there's an edge from b to a)
496 # outgoing edge: other module depending on this one
498 # [Tarjan 1976], http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
499 #L ← Empty list that will contain the sorted nodes
501 #S ← Set of all nodes with no outgoing edges (modules on which no other
503 S = set(module for module in modules if module not in dependencies)
506 #function visit(node n)
508 #if n has not been visited yet then
512 #change: n not web module, can not be resolved, ignore
513 if n not in modules: return
514 #for each node m with an edge from m to n do (dependencies of n)
520 #for each node n in S do
526 class Session(openerpweb.Controller):
527 _cp_path = "/web/session"
529 def session_info(self, req):
530 req.session.ensure_valid()
532 "session_id": req.session_id,
533 "uid": req.session._uid,
534 "context": req.session.get_context() if req.session._uid else {},
535 "db": req.session._db,
536 "login": req.session._login,
537 "openerp_entreprise": req.session.openerp_entreprise(),
540 @openerpweb.jsonrequest
541 def get_session_info(self, req):
542 return self.session_info(req)
544 @openerpweb.jsonrequest
545 def authenticate(self, req, db, login, password, base_location=None):
546 wsgienv = req.httprequest.environ
547 release = common.release
549 base_location=base_location,
550 HTTP_HOST=wsgienv['HTTP_HOST'],
551 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
552 user_agent="%s / %s" % (release.name, release.version),
554 req.session.authenticate(db, login, password, env)
556 return self.session_info(req)
558 @openerpweb.jsonrequest
559 def change_password (self,req,fields):
560 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
561 dict(map(operator.itemgetter('name', 'value'), fields)))
562 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
563 return {'error':'All passwords have to be filled.','title': 'Change Password'}
564 if new_password != confirm_password:
565 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
567 if req.session.model('res.users').change_password(
568 old_password, new_password):
569 return {'new_password':new_password}
571 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
572 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
574 @openerpweb.jsonrequest
575 def sc_list(self, req):
576 return req.session.model('ir.ui.view_sc').get_sc(
577 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
579 @openerpweb.jsonrequest
580 def get_lang_list(self, req):
583 'lang_list': (req.session.proxy("db").list_lang() or []),
587 return {"error": e, "title": "Languages"}
589 @openerpweb.jsonrequest
590 def modules(self, req):
591 # Compute available candidates module
592 loadable = openerpweb.addons_manifest
593 loaded = set(req.config.server_wide_modules)
594 candidates = [mod for mod in loadable if mod not in loaded]
596 # already installed modules have no dependencies
597 modules = dict.fromkeys(loaded, [])
599 # Compute auto_install modules that might be on the web side only
600 modules.update((name, openerpweb.addons_manifest[name].get('depends', []))
601 for name in candidates
602 if openerpweb.addons_manifest[name].get('auto_install'))
604 # Retrieve database installed modules
605 Modules = req.session.model('ir.module.module')
606 for module in Modules.search_read(
607 [('state','=','installed'), ('name','in', candidates)],
608 ['name', 'dependencies_id']):
609 deps = module.get('dependencies_id')
612 operator.itemgetter('name'),
613 req.session.model('ir.module.module.dependency').read(deps, ['name']))
614 modules[module['name']] = list(
615 set(modules.get(module['name'], []) + dependencies))
617 sorted_modules = topological_sort(modules)
618 return [module for module in sorted_modules if module not in loaded]
620 @openerpweb.jsonrequest
621 def eval_domain_and_context(self, req, contexts, domains,
623 """ Evaluates sequences of domains and contexts, composing them into
624 a single context, domain or group_by sequence.
626 :param list contexts: list of contexts to merge together. Contexts are
627 evaluated in sequence, all previous contexts
628 are part of their own evaluation context
629 (starting at the session context).
630 :param list domains: list of domains to merge together. Domains are
631 evaluated in sequence and appended to one another
632 (implicit AND), their evaluation domain is the
633 result of merging all contexts.
634 :param list group_by_seq: list of domains (which may be in a different
635 order than the ``contexts`` parameter),
636 evaluated in sequence, their ``'group_by'``
637 key is extracted if they have one.
642 the global context created by merging all of
646 the concatenation of all domains
649 a list of fields to group by, potentially empty (in which case
650 no group by should be performed)
652 context, domain = eval_context_and_domain(req.session,
653 common.nonliterals.CompoundContext(*(contexts or [])),
654 common.nonliterals.CompoundDomain(*(domains or [])))
656 group_by_sequence = []
657 for candidate in (group_by_seq or []):
658 ctx = req.session.eval_context(candidate, context)
659 group_by = ctx.get('group_by')
662 elif isinstance(group_by, basestring):
663 group_by_sequence.append(group_by)
665 group_by_sequence.extend(group_by)
670 'group_by': group_by_sequence
673 @openerpweb.jsonrequest
674 def save_session_action(self, req, the_action):
676 This method store an action object in the session object and returns an integer
677 identifying that action. The method get_session_action() can be used to get
680 :param the_action: The action to save in the session.
681 :type the_action: anything
682 :return: A key identifying the saved action.
685 saved_actions = req.httpsession.get('saved_actions')
686 if not saved_actions:
687 saved_actions = {"next":0, "actions":{}}
688 req.httpsession['saved_actions'] = saved_actions
689 # we don't allow more than 10 stored actions
690 if len(saved_actions["actions"]) >= 10:
691 del saved_actions["actions"][min(saved_actions["actions"])]
692 key = saved_actions["next"]
693 saved_actions["actions"][key] = the_action
694 saved_actions["next"] = key + 1
697 @openerpweb.jsonrequest
698 def get_session_action(self, req, key):
700 Gets back a previously saved action. This method can return None if the action
701 was saved since too much time (this case should be handled in a smart way).
703 :param key: The key given by save_session_action()
705 :return: The saved action or None.
708 saved_actions = req.httpsession.get('saved_actions')
709 if not saved_actions:
711 return saved_actions["actions"].get(key)
713 @openerpweb.jsonrequest
714 def check(self, req):
715 req.session.assert_valid()
718 @openerpweb.jsonrequest
719 def destroy(self, req):
720 req.session._suicide = True
722 def eval_context_and_domain(session, context, domain=None):
723 e_context = session.eval_context(context)
724 # should we give the evaluated context as an evaluation context to the domain?
725 e_domain = session.eval_domain(domain or [])
727 return e_context, e_domain
729 def load_actions_from_ir_values(req, key, key2, models, meta):
730 context = req.session.eval_context(req.context)
731 Values = req.session.model('ir.values')
732 actions = Values.get(key, key2, models, meta, context)
734 return [(id, name, clean_action(req, action))
735 for id, name, action in actions]
737 def clean_action(req, action, do_not_eval=False):
738 action.setdefault('flags', {})
740 context = req.session.eval_context(req.context)
741 eval_ctx = req.session.evaluation_context(context)
744 # values come from the server, we can just eval them
745 if action.get('context') and isinstance(action.get('context'), basestring):
746 action['context'] = eval( action['context'], eval_ctx ) or {}
748 if action.get('domain') and isinstance(action.get('domain'), basestring):
749 action['domain'] = eval( action['domain'], eval_ctx ) or []
751 if 'context' in action:
752 action['context'] = parse_context(action['context'], req.session)
753 if 'domain' in action:
754 action['domain'] = parse_domain(action['domain'], req.session)
756 action_type = action.setdefault('type', 'ir.actions.act_window_close')
757 if action_type == 'ir.actions.act_window':
758 return fix_view_modes(action)
761 # I think generate_views,fix_view_modes should go into js ActionManager
762 def generate_views(action):
764 While the server generates a sequence called "views" computing dependencies
765 between a bunch of stuff for views coming directly from the database
766 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
767 to return custom view dictionaries generated on the fly.
769 In that case, there is no ``views`` key available on the action.
771 Since the web client relies on ``action['views']``, generate it here from
772 ``view_mode`` and ``view_id``.
774 Currently handles two different cases:
776 * no view_id, multiple view_mode
777 * single view_id, single view_mode
779 :param dict action: action descriptor dictionary to generate a views key for
781 view_id = action.get('view_id') or False
782 if isinstance(view_id, (list, tuple)):
785 # providing at least one view mode is a requirement, not an option
786 view_modes = action['view_mode'].split(',')
788 if len(view_modes) > 1:
790 raise ValueError('Non-db action dictionaries should provide '
791 'either multiple view modes or a single view '
792 'mode and an optional view id.\n\n Got view '
793 'modes %r and view id %r for action %r' % (
794 view_modes, view_id, action))
795 action['views'] = [(False, mode) for mode in view_modes]
797 action['views'] = [(view_id, view_modes[0])]
799 def fix_view_modes(action):
800 """ For historical reasons, OpenERP has weird dealings in relation to
801 view_mode and the view_type attribute (on window actions):
803 * one of the view modes is ``tree``, which stands for both list views
805 * the choice is made by checking ``view_type``, which is either
806 ``form`` for a list view or ``tree`` for an actual tree view
808 This methods simply folds the view_type into view_mode by adding a
809 new view mode ``list`` which is the result of the ``tree`` view_mode
810 in conjunction with the ``form`` view_type.
812 TODO: this should go into the doc, some kind of "peculiarities" section
814 :param dict action: an action descriptor
815 :returns: nothing, the action is modified in place
817 if not action.get('views'):
818 generate_views(action)
821 for index, (id, mode) in enumerate(action['views']):
826 if action.pop('view_type', 'form') != 'form':
830 [id, mode if mode != 'tree' else 'list']
831 for id, mode in action['views']
836 class Menu(openerpweb.Controller):
837 _cp_path = "/web/menu"
839 @openerpweb.jsonrequest
841 return {'data': self.do_load(req)}
843 @openerpweb.jsonrequest
844 def get_user_roots(self, req):
845 return self.do_get_user_roots(req)
847 def do_get_user_roots(self, req):
848 """ Return all root menu ids visible for the session user.
850 :param req: A request object, with an OpenERP session attribute
851 :type req: < session -> OpenERPSession >
852 :return: the root menu ids
856 context = s.eval_context(req.context)
857 Menus = s.model('ir.ui.menu')
858 # If a menu action is defined use its domain to get the root menu items
859 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
861 menu_domain = [('parent_id', '=', False)]
863 domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
865 menu_domain = ast.literal_eval(domain_string)
867 return Menus.search(menu_domain, 0, False, False, context)
869 def do_load(self, req):
870 """ Loads all menu items (all applications and their sub-menus).
872 :param req: A request object, with an OpenERP session attribute
873 :type req: < session -> OpenERPSession >
874 :return: the menu root
875 :rtype: dict('children': menu_nodes)
877 context = req.session.eval_context(req.context)
878 Menus = req.session.model('ir.ui.menu')
880 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
881 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
883 # menus are loaded fully unlike a regular tree view, cause there are a
884 # limited number of items (752 when all 6.1 addons are installed)
885 menu_ids = Menus.search([], 0, False, False, context)
886 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
887 # adds roots at the end of the sequence, so that they will overwrite
888 # equivalent menu items from full menu read when put into id:item
889 # mapping, resulting in children being correctly set on the roots.
890 menu_items.extend(menu_roots)
892 # make a tree using parent_id
893 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
894 for menu_item in menu_items:
895 if menu_item['parent_id']:
896 parent = menu_item['parent_id'][0]
899 if parent in menu_items_map:
900 menu_items_map[parent].setdefault(
901 'children', []).append(menu_item)
903 # sort by sequence a tree using parent_id
904 for menu_item in menu_items:
905 menu_item.setdefault('children', []).sort(
906 key=operator.itemgetter('sequence'))
910 @openerpweb.jsonrequest
911 def action(self, req, menu_id):
912 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
913 [('ir.ui.menu', menu_id)], False)
914 return {"action": actions}
916 class DataSet(openerpweb.Controller):
917 _cp_path = "/web/dataset"
919 @openerpweb.jsonrequest
920 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
921 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
922 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
924 """ Performs a search() followed by a read() (if needed) using the
925 provided search criteria
927 :param req: a JSON-RPC request object
928 :type req: openerpweb.JsonRequest
929 :param str model: the name of the model to search on
930 :param fields: a list of the fields to return in the result records
932 :param int offset: from which index should the results start being returned
933 :param int limit: the maximum number of records to return
934 :param list domain: the search domain for the query
935 :param list sort: sorting directives
936 :returns: A structure (dict) with two keys: ids (all the ids matching
937 the (domain, context) pair) and records (paginated records
938 matching fields selection set)
941 Model = req.session.model(model)
943 context, domain = eval_context_and_domain(
944 req.session, req.context, domain)
946 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
947 if limit and len(ids) == limit:
948 length = Model.search_count(domain, context)
950 length = len(ids) + (offset or 0)
951 if fields and fields == ['id']:
952 # shortcut read if we only want the ids
955 'records': [{'id': id} for id in ids]
958 records = Model.read(ids, fields or False, context)
959 records.sort(key=lambda obj: ids.index(obj['id']))
965 @openerpweb.jsonrequest
966 def load(self, req, model, id, fields):
967 m = req.session.model(model)
969 r = m.read([id], False, req.session.eval_context(req.context))
972 return {'value': value}
974 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
975 has_domain = domain_id is not None and domain_id < len(args)
976 has_context = context_id is not None and context_id < len(args)
978 domain = args[domain_id] if has_domain else []
979 context = args[context_id] if has_context else {}
980 c, d = eval_context_and_domain(req.session, context, domain)
986 return self._call_kw(req, model, method, args, {})
988 def _call_kw(self, req, model, method, args, kwargs):
989 for i in xrange(len(args)):
990 if isinstance(args[i], common.nonliterals.BaseContext):
991 args[i] = req.session.eval_context(args[i])
992 elif isinstance(args[i], common.nonliterals.BaseDomain):
993 args[i] = req.session.eval_domain(args[i])
994 for k in kwargs.keys():
995 if isinstance(kwargs[k], common.nonliterals.BaseContext):
996 kwargs[k] = req.session.eval_context(kwargs[k])
997 elif isinstance(kwargs[k], common.nonliterals.BaseDomain):
998 kwargs[k] = req.session.eval_domain(kwargs[k])
1000 # Temporary implements future display_name special field for model#read()
1001 if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1002 if 'display_name' in args[1]:
1003 names = req.session.model(model).name_get(args[0], **kwargs)
1004 args[1].remove('display_name')
1005 r = getattr(req.session.model(model), method)(*args, **kwargs)
1006 for i in range(len(r)):
1007 r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1010 return getattr(req.session.model(model), method)(*args, **kwargs)
1012 @openerpweb.jsonrequest
1013 def onchange(self, req, model, method, args, context_id=None):
1014 """ Support method for handling onchange calls: behaves much like call
1015 with the following differences:
1017 * Does not take a domain_id
1018 * Is aware of the return value's structure, and will parse the domains
1019 if needed in order to return either parsed literal domains (in JSON)
1020 or non-literal domain instances, allowing those domains to be used
1024 :type req: web.common.http.JsonRequest
1025 :param str model: object type on which to call the method
1026 :param str method: name of the onchange handler method
1027 :param list args: arguments to call the onchange handler with
1028 :param int context_id: index of the context object in the list of
1030 :return: result of the onchange call with all domains parsed
1032 result = self.call_common(req, model, method, args, context_id=context_id)
1033 if not result or 'domain' not in result:
1036 result['domain'] = dict(
1037 (k, parse_domain(v, req.session))
1038 for k, v in result['domain'].iteritems())
1042 @openerpweb.jsonrequest
1043 def call(self, req, model, method, args, domain_id=None, context_id=None):
1044 return self.call_common(req, model, method, args, domain_id, context_id)
1046 @openerpweb.jsonrequest
1047 def call_kw(self, req, model, method, args, kwargs):
1048 return self._call_kw(req, model, method, args, kwargs)
1050 @openerpweb.jsonrequest
1051 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1052 action = self.call_common(req, model, method, args, domain_id, context_id)
1053 if isinstance(action, dict) and action.get('type') != '':
1054 return {'result': clean_action(req, action)}
1055 return {'result': False}
1057 @openerpweb.jsonrequest
1058 def exec_workflow(self, req, model, id, signal):
1059 return req.session.exec_workflow(model, id, signal)
1061 class DataGroup(openerpweb.Controller):
1062 _cp_path = "/web/group"
1063 @openerpweb.jsonrequest
1064 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
1065 Model = req.session.model(model)
1066 context, domain = eval_context_and_domain(req.session, req.context, domain)
1068 return Model.read_group(
1069 domain or [], fields, group_by_fields, 0, False,
1070 dict(context, group_by=group_by_fields), sort or False)
1072 class View(openerpweb.Controller):
1073 _cp_path = "/web/view"
1075 def fields_view_get(self, req, model, view_id, view_type,
1076 transform=True, toolbar=False, submenu=False):
1077 Model = req.session.model(model)
1078 context = req.session.eval_context(req.context)
1079 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1080 # todo fme?: check that we should pass the evaluated context here
1081 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1082 if toolbar and transform:
1083 self.process_toolbar(req, fvg['toolbar'])
1086 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1087 # depending on how it feels, xmlrpclib.ServerProxy can translate
1088 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1089 # enjoy unicode strings which can not be trivially converted to
1090 # strings, and it blows up during parsing.
1092 # So ensure we fix this retardation by converting view xml back to
1094 if isinstance(fvg['arch'], unicode):
1095 arch = fvg['arch'].encode('utf-8')
1098 fvg['arch_string'] = arch
1101 evaluation_context = session.evaluation_context(context or {})
1102 xml = self.transform_view(arch, session, evaluation_context)
1104 xml = ElementTree.fromstring(arch)
1105 fvg['arch'] = common.xml2json.from_elementtree(xml, preserve_whitespaces)
1107 if 'id' in fvg['fields']:
1108 # Special case for id's
1109 id_field = fvg['fields']['id']
1110 id_field['original_type'] = id_field['type']
1111 id_field['type'] = 'id'
1113 for field in fvg['fields'].itervalues():
1114 if field.get('views'):
1115 for view in field["views"].itervalues():
1116 self.process_view(session, view, None, transform)
1117 if field.get('domain'):
1118 field["domain"] = parse_domain(field["domain"], session)
1119 if field.get('context'):
1120 field["context"] = parse_context(field["context"], session)
1122 def process_toolbar(self, req, toolbar):
1124 The toolbar is a mapping of section_key: [action_descriptor]
1126 We need to clean all those actions in order to ensure correct
1129 for actions in toolbar.itervalues():
1130 for action in actions:
1131 if 'context' in action:
1132 action['context'] = parse_context(
1133 action['context'], req.session)
1134 if 'domain' in action:
1135 action['domain'] = parse_domain(
1136 action['domain'], req.session)
1138 @openerpweb.jsonrequest
1139 def add_custom(self, req, view_id, arch):
1140 CustomView = req.session.model('ir.ui.view.custom')
1142 'user_id': req.session._uid,
1145 }, req.session.eval_context(req.context))
1146 return {'result': True}
1148 @openerpweb.jsonrequest
1149 def undo_custom(self, req, view_id, reset=False):
1150 CustomView = req.session.model('ir.ui.view.custom')
1151 context = req.session.eval_context(req.context)
1152 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1153 0, False, False, context)
1156 CustomView.unlink(vcustom, context)
1158 CustomView.unlink([vcustom[0]], context)
1159 return {'result': True}
1160 return {'result': False}
1162 def transform_view(self, view_string, session, context=None):
1163 # transform nodes on the fly via iterparse, instead of
1164 # doing it statically on the parsing result
1165 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1167 for event, elem in parser:
1168 if event == "start":
1171 self.parse_domains_and_contexts(elem, session)
1174 def parse_domains_and_contexts(self, elem, session):
1175 """ Converts domains and contexts from the view into Python objects,
1176 either literals if they can be parsed by literal_eval or a special
1177 placeholder object if the domain or context refers to free variables.
1179 :param elem: the current node being parsed
1180 :type param: xml.etree.ElementTree.Element
1181 :param session: OpenERP session object, used to store and retrieve
1183 :type session: openerpweb.openerpweb.OpenERPSession
1185 for el in ['domain', 'filter_domain']:
1186 domain = elem.get(el, '').strip()
1188 elem.set(el, parse_domain(domain, session))
1189 elem.set(el + '_string', domain)
1190 for el in ['context', 'default_get']:
1191 context_string = elem.get(el, '').strip()
1193 elem.set(el, parse_context(context_string, session))
1194 elem.set(el + '_string', context_string)
1196 @openerpweb.jsonrequest
1197 def load(self, req, model, view_id, view_type, toolbar=False):
1198 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1200 def parse_domain(domain, session):
1201 """ Parses an arbitrary string containing a domain, transforms it
1202 to either a literal domain or a :class:`common.nonliterals.Domain`
1204 :param domain: the domain to parse, if the domain is not a string it
1205 is assumed to be a literal domain and is returned as-is
1206 :param session: Current OpenERP session
1207 :type session: openerpweb.openerpweb.OpenERPSession
1209 if not isinstance(domain, basestring):
1212 return ast.literal_eval(domain)
1215 return common.nonliterals.Domain(session, domain)
1217 def parse_context(context, session):
1218 """ Parses an arbitrary string containing a context, transforms it
1219 to either a literal context or a :class:`common.nonliterals.Context`
1221 :param context: the context to parse, if the context is not a string it
1222 is assumed to be a literal domain and is returned as-is
1223 :param session: Current OpenERP session
1224 :type session: openerpweb.openerpweb.OpenERPSession
1226 if not isinstance(context, basestring):
1229 return ast.literal_eval(context)
1231 return common.nonliterals.Context(session, context)
1233 class ListView(View):
1234 _cp_path = "/web/listview"
1236 def process_colors(self, view, row, context):
1237 colors = view['arch']['attrs'].get('colors')
1244 for pair in colors.split(';')
1245 if eval(pair.split(':')[1], dict(context, **row))
1250 elif len(color) == 1:
1254 class TreeView(View):
1255 _cp_path = "/web/treeview"
1257 @openerpweb.jsonrequest
1258 def action(self, req, model, id):
1259 return load_actions_from_ir_values(
1260 req,'action', 'tree_but_open',[(model, id)],
1263 class SearchView(View):
1264 _cp_path = "/web/searchview"
1266 @openerpweb.jsonrequest
1267 def load(self, req, model, view_id):
1268 fields_view = self.fields_view_get(req, model, view_id, 'search')
1269 return {'fields_view': fields_view}
1271 @openerpweb.jsonrequest
1272 def fields_get(self, req, model):
1273 Model = req.session.model(model)
1274 fields = Model.fields_get(False, req.session.eval_context(req.context))
1275 for field in fields.values():
1276 # shouldn't convert the views too?
1277 if field.get('domain'):
1278 field["domain"] = parse_domain(field["domain"], req.session)
1279 if field.get('context'):
1280 field["context"] = parse_context(field["context"], req.session)
1281 return {'fields': fields}
1283 @openerpweb.jsonrequest
1284 def get_filters(self, req, model):
1285 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1286 Model = req.session.model("ir.filters")
1287 filters = Model.get_filters(model)
1288 for filter in filters:
1290 parsed_context = parse_context(filter["context"], req.session)
1291 filter["context"] = (parsed_context
1292 if not isinstance(parsed_context, common.nonliterals.BaseContext)
1293 else req.session.eval_context(parsed_context))
1295 parsed_domain = parse_domain(filter["domain"], req.session)
1296 filter["domain"] = (parsed_domain
1297 if not isinstance(parsed_domain, common.nonliterals.BaseDomain)
1298 else req.session.eval_domain(parsed_domain))
1300 logger.exception("Failed to parse custom filter %s in %s",
1301 filter['name'], model)
1302 filter['disabled'] = True
1303 del filter['context']
1304 del filter['domain']
1308 @openerpweb.jsonrequest
1309 def add_to_dashboard(self, req, menu_id, action_id, context_to_save, domain, view_mode, name=''):
1310 to_eval = common.nonliterals.CompoundContext(context_to_save)
1311 to_eval.session = req.session
1312 ctx = dict((k, v) for k, v in to_eval.evaluate().iteritems()
1313 if not k.startswith('search_default_'))
1314 ctx['dashboard_merge_domains_contexts'] = False # TODO: replace this 6.1 workaround by attribute on <action/>
1315 domain = common.nonliterals.CompoundDomain(domain)
1316 domain.session = req.session
1317 domain = domain.evaluate()
1319 dashboard_action = load_actions_from_ir_values(req, 'action', 'tree_but_open',
1320 [('ir.ui.menu', menu_id)], False)
1321 if dashboard_action:
1322 action = dashboard_action[0][2]
1323 if action['res_model'] == 'board.board' and action['views'][0][1] == 'form':
1324 # Maybe should check the content instead of model board.board ?
1325 view_id = action['views'][0][0]
1326 board = req.session.model(action['res_model']).fields_view_get(view_id, 'form')
1327 if board and 'arch' in board:
1328 xml = ElementTree.fromstring(board['arch'])
1329 column = xml.find('./board/column')
1330 if column is not None:
1331 new_action = ElementTree.Element('action', {
1332 'name' : str(action_id),
1334 'view_mode' : view_mode,
1335 'context' : str(ctx),
1336 'domain' : str(domain)
1338 column.insert(0, new_action)
1339 arch = ElementTree.tostring(xml, 'utf-8')
1340 return req.session.model('ir.ui.view.custom').create({
1341 'user_id': req.session._uid,
1344 }, req.session.eval_context(req.context))
1348 class Binary(openerpweb.Controller):
1349 _cp_path = "/web/binary"
1351 @openerpweb.httprequest
1352 def image(self, req, model, id, field, **kw):
1353 last_update = '__last_update'
1354 Model = req.session.model(model)
1355 context = req.session.eval_context(req.context)
1356 headers = [('Content-Type', 'image/png')]
1357 etag = req.httprequest.headers.get('If-None-Match')
1358 hashed_session = hashlib.md5(req.session_id).hexdigest()
1360 if not id and hashed_session == etag:
1361 return werkzeug.wrappers.Response(status=304)
1363 date = Model.read([int(id)], [last_update], context)[0].get(last_update)
1364 if hashlib.md5(date).hexdigest() == etag:
1365 return werkzeug.wrappers.Response(status=304)
1367 retag = hashed_session
1370 res = Model.default_get([field], context).get(field)
1371 image_data = base64.b64decode(res)
1373 res = Model.read([int(id)], [last_update, field], context)[0]
1374 retag = hashlib.md5(res.get(last_update)).hexdigest()
1375 image_data = base64.b64decode(res.get(field))
1376 except (TypeError, xmlrpclib.Fault):
1377 image_data = self.placeholder(req)
1378 headers.append(('ETag', retag))
1379 headers.append(('Content-Length', len(image_data)))
1381 ncache = int(kw.get('cache'))
1382 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1385 return req.make_response(image_data, headers)
1386 def placeholder(self, req):
1387 addons_path = openerpweb.addons_manifest['web']['addons_path']
1388 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1389 def content_disposition(self, filename, req):
1390 filename = filename.encode('utf8')
1391 escaped = urllib2.quote(filename)
1392 browser = req.httprequest.user_agent.browser
1393 version = int((req.httprequest.user_agent.version or '0').split('.')[0])
1394 if browser == 'msie' and version < 9:
1395 return "attachment; filename=%s" % escaped
1396 elif browser == 'safari':
1397 return "attachment; filename=%s" % filename
1399 return "attachment; filename*=UTF-8''%s" % escaped
1401 @openerpweb.httprequest
1402 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1403 """ Download link for files stored as binary fields.
1405 If the ``id`` parameter is omitted, fetches the default value for the
1406 binary field (via ``default_get``), otherwise fetches the field for
1407 that precise record.
1409 :param req: OpenERP request
1410 :type req: :class:`web.common.http.HttpRequest`
1411 :param str model: name of the model to fetch the binary from
1412 :param str field: binary field
1413 :param str id: id of the record from which to fetch the binary
1414 :param str filename_field: field holding the file's name, if any
1415 :returns: :class:`werkzeug.wrappers.Response`
1417 Model = req.session.model(model)
1418 context = req.session.eval_context(req.context)
1421 fields.append(filename_field)
1423 res = Model.read([int(id)], fields, context)[0]
1425 res = Model.default_get(fields, context)
1426 filecontent = base64.b64decode(res.get(field, ''))
1428 return req.not_found()
1430 filename = '%s_%s' % (model.replace('.', '_'), id)
1432 filename = res.get(filename_field, '') or filename
1433 return req.make_response(filecontent,
1434 [('Content-Type', 'application/octet-stream'),
1435 ('Content-Disposition', self.content_disposition(filename, req))])
1437 @openerpweb.httprequest
1438 def saveas_ajax(self, req, data, token):
1439 jdata = simplejson.loads(data)
1440 model = jdata['model']
1441 field = jdata['field']
1442 id = jdata.get('id', None)
1443 filename_field = jdata.get('filename_field', None)
1444 context = jdata.get('context', dict())
1446 context = req.session.eval_context(context)
1447 Model = req.session.model(model)
1450 fields.append(filename_field)
1452 res = Model.read([int(id)], fields, context)[0]
1454 res = Model.default_get(fields, context)
1455 filecontent = base64.b64decode(res.get(field, ''))
1457 raise ValueError("No content found for field '%s' on '%s:%s'" %
1460 filename = '%s_%s' % (model.replace('.', '_'), id)
1462 filename = res.get(filename_field, '') or filename
1463 return req.make_response(filecontent,
1464 headers=[('Content-Type', 'application/octet-stream'),
1465 ('Content-Disposition', self.content_disposition(filename, req))],
1466 cookies={'fileToken': int(token)})
1468 @openerpweb.httprequest
1469 def upload(self, req, callback, ufile):
1470 # TODO: might be useful to have a configuration flag for max-length file uploads
1472 out = """<script language="javascript" type="text/javascript">
1473 var win = window.top.window;
1474 win.jQuery(win).trigger(%s, %s);
1477 args = [len(data), ufile.filename,
1478 ufile.content_type, base64.b64encode(data)]
1479 except Exception, e:
1480 args = [False, e.message]
1481 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1483 @openerpweb.httprequest
1484 def upload_attachment(self, req, callback, model, id, ufile):
1485 context = req.session.eval_context(req.context)
1486 Model = req.session.model('ir.attachment')
1488 out = """<script language="javascript" type="text/javascript">
1489 var win = window.top.window;
1490 win.jQuery(win).trigger(%s, %s);
1492 attachment_id = Model.create({
1493 'name': ufile.filename,
1494 'datas': base64.encodestring(ufile.read()),
1495 'datas_fname': ufile.filename,
1500 'filename': ufile.filename,
1503 except Exception, e:
1504 args = { 'error': e.message }
1505 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1507 class Action(openerpweb.Controller):
1508 _cp_path = "/web/action"
1510 # For most actions, the type attribute and the model name are the same, but
1511 # there are exceptions. This dict is used to remap action type attributes
1512 # to the "real" model name when they differ.
1514 "ir.actions.act_url": "ir.actions.url",
1517 @openerpweb.jsonrequest
1518 def load(self, req, action_id, do_not_eval=False):
1519 Actions = req.session.model('ir.actions.actions')
1521 context = req.session.eval_context(req.context)
1524 action_id = int(action_id)
1527 module, xmlid = action_id.split('.', 1)
1528 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1529 assert model.startswith('ir.actions.')
1531 action_id = 0 # force failed read
1533 base_action = Actions.read([action_id], ['type'], context)
1536 action_type = base_action[0]['type']
1537 if action_type == 'ir.actions.report.xml':
1538 ctx.update({'bin_size': True})
1540 action_model = self.action_mapping.get(action_type, action_type)
1541 action = req.session.model(action_model).read([action_id], False, ctx)
1543 value = clean_action(req, action[0], do_not_eval)
1544 return {'result': value}
1546 @openerpweb.jsonrequest
1547 def run(self, req, action_id):
1548 return_action = req.session.model('ir.actions.server').run(
1549 [action_id], req.session.eval_context(req.context))
1551 return clean_action(req, return_action)
1556 _cp_path = "/web/export"
1558 @openerpweb.jsonrequest
1559 def formats(self, req):
1560 """ Returns all valid export formats
1562 :returns: for each export format, a pair of identifier and printable name
1563 :rtype: [(str, str)]
1567 for path, controller in openerpweb.controllers_path.iteritems()
1568 if path.startswith(self._cp_path)
1569 if hasattr(controller, 'fmt')
1570 ], key=operator.itemgetter("label"))
1572 def fields_get(self, req, model):
1573 Model = req.session.model(model)
1574 fields = Model.fields_get(False, req.session.eval_context(req.context))
1577 @openerpweb.jsonrequest
1578 def get_fields(self, req, model, prefix='', parent_name= '',
1579 import_compat=True, parent_field_type=None,
1582 if import_compat and parent_field_type == "many2one":
1585 fields = self.fields_get(req, model)
1588 fields.pop('id', None)
1590 fields['.id'] = fields.pop('id', {'string': 'ID'})
1592 fields_sequence = sorted(fields.iteritems(),
1593 key=lambda field: field[1].get('string', ''))
1596 for field_name, field in fields_sequence:
1598 if exclude and field_name in exclude:
1600 if field.get('readonly'):
1601 # If none of the field's states unsets readonly, skip the field
1602 if all(dict(attrs).get('readonly', True)
1603 for attrs in field.get('states', {}).values()):
1606 id = prefix + (prefix and '/'or '') + field_name
1607 name = parent_name + (parent_name and '/' or '') + field['string']
1608 record = {'id': id, 'string': name,
1609 'value': id, 'children': False,
1610 'field_type': field.get('type'),
1611 'required': field.get('required'),
1612 'relation_field': field.get('relation_field')}
1613 records.append(record)
1615 if len(name.split('/')) < 3 and 'relation' in field:
1616 ref = field.pop('relation')
1617 record['value'] += '/id'
1618 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1620 if not import_compat or field['type'] == 'one2many':
1621 # m2m field in import_compat is childless
1622 record['children'] = True
1626 @openerpweb.jsonrequest
1627 def namelist(self,req, model, export_id):
1628 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1629 export = req.session.model("ir.exports").read([export_id])[0]
1630 export_fields_list = req.session.model("ir.exports.line").read(
1631 export['export_fields'])
1633 fields_data = self.fields_info(
1634 req, model, map(operator.itemgetter('name'), export_fields_list))
1637 {'name': field['name'], 'label': fields_data[field['name']]}
1638 for field in export_fields_list
1641 def fields_info(self, req, model, export_fields):
1643 fields = self.fields_get(req, model)
1645 # To make fields retrieval more efficient, fetch all sub-fields of a
1646 # given field at the same time. Because the order in the export list is
1647 # arbitrary, this requires ordering all sub-fields of a given field
1648 # together so they can be fetched at the same time
1650 # Works the following way:
1651 # * sort the list of fields to export, the default sorting order will
1652 # put the field itself (if present, for xmlid) and all of its
1653 # sub-fields right after it
1654 # * then, group on: the first field of the path (which is the same for
1655 # a field and for its subfields and the length of splitting on the
1656 # first '/', which basically means grouping the field on one side and
1657 # all of the subfields on the other. This way, we have the field (for
1658 # the xmlid) with length 1, and all of the subfields with the same
1659 # base but a length "flag" of 2
1660 # * if we have a normal field (length 1), just add it to the info
1661 # mapping (with its string) as-is
1662 # * otherwise, recursively call fields_info via graft_subfields.
1663 # all graft_subfields does is take the result of fields_info (on the
1664 # field's model) and prepend the current base (current field), which
1665 # rebuilds the whole sub-tree for the field
1667 # result: because we're not fetching the fields_get for half the
1668 # database models, fetching a namelist with a dozen fields (including
1669 # relational data) falls from ~6s to ~300ms (on the leads model).
1670 # export lists with no sub-fields (e.g. import_compatible lists with
1671 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1672 # there's a single fields_get to execute)
1673 for (base, length), subfields in itertools.groupby(
1674 sorted(export_fields),
1675 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1676 subfields = list(subfields)
1678 # subfields is a seq of $base/*rest, and not loaded yet
1679 info.update(self.graft_subfields(
1680 req, fields[base]['relation'], base, fields[base]['string'],
1684 info[base] = fields[base]['string']
1688 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1689 export_fields = [field.split('/', 1)[1] for field in fields]
1691 (prefix + '/' + k, prefix_string + '/' + v)
1692 for k, v in self.fields_info(req, model, export_fields).iteritems())
1694 #noinspection PyPropertyDefinition
1696 def content_type(self):
1697 """ Provides the format's content type """
1698 raise NotImplementedError()
1700 def filename(self, base):
1701 """ Creates a valid filename for the format (with extension) from the
1702 provided base name (exension-less)
1704 raise NotImplementedError()
1706 def from_data(self, fields, rows):
1707 """ Conversion method from OpenERP's export data to whatever the
1708 current export class outputs
1710 :params list fields: a list of fields to export
1711 :params list rows: a list of records to export
1715 raise NotImplementedError()
1717 @openerpweb.httprequest
1718 def index(self, req, data, token):
1719 model, fields, ids, domain, import_compat = \
1720 operator.itemgetter('model', 'fields', 'ids', 'domain',
1722 simplejson.loads(data))
1724 context = req.session.eval_context(req.context)
1725 Model = req.session.model(model)
1726 ids = ids or Model.search(domain, 0, False, False, context)
1728 field_names = map(operator.itemgetter('name'), fields)
1729 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1732 columns_headers = field_names
1734 columns_headers = [val['label'].strip() for val in fields]
1737 return req.make_response(self.from_data(columns_headers, import_data),
1738 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1739 ('Content-Type', self.content_type)],
1740 cookies={'fileToken': int(token)})
1742 class CSVExport(Export):
1743 _cp_path = '/web/export/csv'
1744 fmt = {'tag': 'csv', 'label': 'CSV'}
1747 def content_type(self):
1748 return 'text/csv;charset=utf8'
1750 def filename(self, base):
1751 return base + '.csv'
1753 def from_data(self, fields, rows):
1755 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1757 writer.writerow([name.encode('utf-8') for name in fields])
1762 if isinstance(d, basestring):
1763 d = d.replace('\n',' ').replace('\t',' ')
1765 d = d.encode('utf-8')
1766 except UnicodeError:
1768 if d is False: d = None
1770 writer.writerow(row)
1777 class ExcelExport(Export):
1778 _cp_path = '/web/export/xls'
1782 'error': None if xlwt else "XLWT required"
1786 def content_type(self):
1787 return 'application/vnd.ms-excel'
1789 def filename(self, base):
1790 return base + '.xls'
1792 def from_data(self, fields, rows):
1793 workbook = xlwt.Workbook()
1794 worksheet = workbook.add_sheet('Sheet 1')
1796 for i, fieldname in enumerate(fields):
1797 worksheet.write(0, i, fieldname)
1798 worksheet.col(i).width = 8000 # around 220 pixels
1800 style = xlwt.easyxf('align: wrap yes')
1802 for row_index, row in enumerate(rows):
1803 for cell_index, cell_value in enumerate(row):
1804 if isinstance(cell_value, basestring):
1805 cell_value = re.sub("\r", " ", cell_value)
1806 if cell_value is False: cell_value = None
1807 worksheet.write(row_index + 1, cell_index, cell_value, style)
1816 class Reports(View):
1817 _cp_path = "/web/report"
1818 POLLING_DELAY = 0.25
1820 'doc': 'application/vnd.ms-word',
1821 'html': 'text/html',
1822 'odt': 'application/vnd.oasis.opendocument.text',
1823 'pdf': 'application/pdf',
1824 'sxw': 'application/vnd.sun.xml.writer',
1825 'xls': 'application/vnd.ms-excel',
1828 @openerpweb.httprequest
1829 def index(self, req, action, token):
1830 action = simplejson.loads(action)
1832 report_srv = req.session.proxy("report")
1833 context = req.session.eval_context(
1834 common.nonliterals.CompoundContext(
1835 req.context or {}, action[ "context"]))
1838 report_ids = context["active_ids"]
1839 if 'report_type' in action:
1840 report_data['report_type'] = action['report_type']
1841 if 'datas' in action:
1842 if 'ids' in action['datas']:
1843 report_ids = action['datas'].pop('ids')
1844 report_data.update(action['datas'])
1846 report_id = report_srv.report(
1847 req.session._db, req.session._uid, req.session._password,
1848 action["report_name"], report_ids,
1849 report_data, context)
1851 report_struct = None
1853 report_struct = report_srv.report_get(
1854 req.session._db, req.session._uid, req.session._password, report_id)
1855 if report_struct["state"]:
1858 time.sleep(self.POLLING_DELAY)
1860 report = base64.b64decode(report_struct['result'])
1861 if report_struct.get('code') == 'zlib':
1862 report = zlib.decompress(report)
1863 report_mimetype = self.TYPES_MAPPING.get(
1864 report_struct['format'], 'octet-stream')
1866 if 'name' not in action:
1867 reports = req.session.model('ir.actions.report.xml')
1868 res_id = reports.search([('report_name', '=', action['report_name']),],
1869 0, False, False, context)
1871 file_name = reports.read(res_id[0], ['name'], context)['name']
1873 file_name = action['report_name']
1875 return req.make_response(report,
1877 # maybe we should take of what characters can appear in a file name?
1878 ('Content-Disposition', 'attachment; filename="%s.%s"' % (file_name, report_struct['format'])),
1879 ('Content-Type', report_mimetype),
1880 ('Content-Length', len(report))],
1881 cookies={'fileToken': int(token)})
1884 _cp_path = "/web/import"
1886 def fields_get(self, req, model):
1887 Model = req.session.model(model)
1888 fields = Model.fields_get(False, req.session.eval_context(req.context))
1891 @openerpweb.httprequest
1892 def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1894 data = list(csv.reader(
1895 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1896 except csv.Error, e:
1898 return '<script>window.top.%s(%s);</script>' % (
1899 jsonp, simplejson.dumps({'error': {
1900 'message': 'Error parsing CSV file: %s' % e,
1901 # decodes each byte to a unicode character, which may or
1902 # may not be printable, but decoding will succeed.
1903 # Otherwise simplejson will try to decode the `str` using
1904 # utf-8, which is very likely to blow up on characters out
1905 # of the ascii range (in range [128, 256))
1906 'preview': csvfile.read(200).decode('iso-8859-1')}}))
1909 return '<script>window.top.%s(%s);</script>' % (
1910 jsonp, simplejson.dumps(
1911 {'records': data[:10]}, encoding=csvcode))
1912 except UnicodeDecodeError:
1913 return '<script>window.top.%s(%s);</script>' % (
1914 jsonp, simplejson.dumps({
1915 'message': u"Failed to decode CSV file using encoding %s, "
1916 u"try switching to a different encoding" % csvcode
1919 @openerpweb.httprequest
1920 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
1922 modle_obj = req.session.model(model)
1923 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
1924 simplejson.loads(meta))
1927 if not (csvdel and len(csvdel) == 1):
1928 error = u"The CSV delimiter must be a single character"
1930 if not indices and fields:
1931 error = u"You must select at least one field to import"
1934 return '<script>window.top.%s(%s);</script>' % (
1935 jsonp, simplejson.dumps({'error': {'message': error}}))
1937 # skip ignored records (@skip parameter)
1938 # then skip empty lines (not valid csv)
1939 # nb: should these operations be reverted?
1940 rows_to_import = itertools.ifilter(
1943 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
1946 # if only one index, itemgetter will return an atom rather than a tuple
1947 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
1948 else: mapper = operator.itemgetter(*indices)
1953 # decode each data row
1955 [record.decode(csvcode) for record in row]
1956 for row in itertools.imap(mapper, rows_to_import)
1957 # don't insert completely empty rows (can happen due to fields
1958 # filtering in case of e.g. o2m content rows)
1961 except UnicodeDecodeError:
1962 error = u"Failed to decode CSV file using encoding %s" % csvcode
1963 except csv.Error, e:
1964 error = u"Could not process CSV file: %s" % e
1966 # If the file contains nothing,
1968 error = u"File to import is empty"
1970 return '<script>window.top.%s(%s);</script>' % (
1971 jsonp, simplejson.dumps({'error': {'message': error}}))
1974 (code, record, message, _nope) = modle_obj.import_data(
1975 fields, data, 'init', '', False,
1976 req.session.eval_context(req.context))
1977 except xmlrpclib.Fault, e:
1978 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
1979 return '<script>window.top.%s(%s);</script>' % (
1980 jsonp, simplejson.dumps({'error':error}))
1983 return '<script>window.top.%s(%s);</script>' % (
1984 jsonp, simplejson.dumps({'success':True}))
1986 msg = u"Error during import: %s\n\nTrying to import record %r" % (
1988 return '<script>window.top.%s(%s);</script>' % (
1989 jsonp, simplejson.dumps({'error': {'message':msg}}))