1 # -*- coding: utf-8 -*-
12 from xml.etree import ElementTree
13 from cStringIO import StringIO
19 import openerpweb.nonliterals
21 from babel.messages.pofile import read_po
23 # Should move to openerpweb.Xml2Json
26 # Simple and straightforward XML-to-JSON converter in Python
29 # URL: http://code.google.com/p/xml2json-direct/
31 def convert_to_json(s):
32 return simplejson.dumps(
33 Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
36 def convert_to_structure(s):
37 root = ElementTree.fromstring(s)
38 return Xml2Json.convert_element(root)
41 def convert_element(el, skip_whitespaces=True):
44 ns, name = el.tag.rsplit("}", 1)
46 res["namespace"] = ns[1:]
50 for k, v in el.items():
53 if el.text and (not skip_whitespaces or el.text.strip() != ''):
56 kids.append(Xml2Json.convert_element(kid))
57 if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''):
59 res["children"] = kids
62 #----------------------------------------------------------
63 # OpenERP Web base Controllers
64 #----------------------------------------------------------
66 def manifest_glob(addons, key):
69 globlist = openerpweb.addons_manifest.get(addon, {}).get(key, [])
71 for pattern in globlist:
72 for path in glob.glob(os.path.join(openerpweb.path_addons, addon, pattern)):
73 files.append(path[len(openerpweb.path_addons):])
76 def concat_files(file_list):
77 """ Concatenate file content
78 return (concat,timestamp)
79 concat: concatenation of file content
80 timestamp: max(os.path.getmtime of file_list)
82 root = openerpweb.path_root
86 fname = os.path.join(root, i)
87 ftime = os.path.getmtime(fname)
88 if ftime > files_timestamp:
89 files_timestamp = ftime
90 files_content = open(fname).read()
91 files_concat = "".join(files_content)
94 home_template = textwrap.dedent("""<!DOCTYPE html>
95 <html style="height: 100%%">
97 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
98 <title>OpenERP</title>
100 <script type="text/javascript">
102 QWeb = new QWeb2.Engine();
103 openerp.init().base.webclient("oe");
106 <link rel="shortcut icon" href="/base/static/src/img/favicon.ico" type="image/x-icon"/>
109 <link rel="stylesheet" href="/base/static/src/css/base-ie7.css" type="text/css"/>
112 <body id="oe" class="openerp"></body>
115 class WebClient(openerpweb.Controller):
116 _cp_path = "/base/webclient"
118 @openerpweb.jsonrequest
119 def csslist(self, req, mods='base'):
120 return manifest_glob(mods.split(','), 'css')
122 @openerpweb.jsonrequest
123 def jslist(self, req, mods='base'):
124 return manifest_glob(mods.split(','), 'js')
126 @openerpweb.httprequest
127 def css(self, req, mods='base'):
128 cherrypy.response.headers['Content-Type'] = 'text/css'
129 files = manifest_glob(mods.split(','), 'css')
130 concat = concat_files(files)[0]
131 # TODO request set the Date of last modif and Etag
134 @openerpweb.httprequest
135 def js(self, req, mods='base'):
136 cherrypy.response.headers['Content-Type'] = 'application/javascript'
137 files = manifest_glob(mods.split(','), 'js')
138 concat = concat_files(files)[0]
139 # TODO request set the Date of last modif and Etag
142 @openerpweb.httprequest
143 def home(self, req, s_action=None):
145 jslist = ['/base/webclient/js']
147 jslist = manifest_glob(['base'], 'js')
148 js = "\n ".join(['<script type="text/javascript" src="%s"></script>'%i for i in jslist])
151 csslist = ['/base/webclient/css']
153 csslist = manifest_glob(['base'], 'css')
154 css = "\n ".join(['<link rel="stylesheet" href="%s">'%i for i in csslist])
155 r = home_template % {
161 @openerpweb.jsonrequest
162 def translations(self, req, mods, lang):
164 for addon_name in mods:
165 transl = {"messages":[]}
166 transs[addon_name] = transl
167 f_name = os.path.join(openerpweb.path_addons, addon_name, "po", lang + ".po")
168 if not os.path.exists(f_name):
171 with open(f_name) as t_file:
177 transl["messages"].append({'id': x.id, 'string': x.string})
178 return {"modules": transs}
181 class Database(openerpweb.Controller):
182 _cp_path = "/base/database"
184 @openerpweb.jsonrequest
185 def get_list(self, req):
186 proxy = req.session.proxy("db")
188 h = req.httprequest.headers['Host'].split(':')[0]
190 r = cherrypy.config['openerp.dbfilter'].replace('%h', h).replace('%d', d)
191 dbs = [i for i in dbs if re.match(r, i)]
192 return {"db_list": dbs}
194 @openerpweb.jsonrequest
195 def progress(self, req, password, id):
196 return req.session.proxy('db').get_progress(password, id)
198 @openerpweb.jsonrequest
199 def create(self, req, fields):
201 params = dict(map(operator.itemgetter('name', 'value'), fields))
203 params['super_admin_pwd'],
205 bool(params.get('demo_data')),
207 params['create_admin_pwd']
211 return req.session.proxy("db").create(*create_attrs)
212 except xmlrpclib.Fault, e:
213 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
214 return {'error': e.faultCode, 'title': 'Create Database'}
215 return {'error': 'Could not create database !', 'title': 'Create Database'}
217 @openerpweb.jsonrequest
218 def drop(self, req, fields):
219 password, db = operator.itemgetter(
220 'drop_pwd', 'drop_db')(
221 dict(map(operator.itemgetter('name', 'value'), fields)))
224 return req.session.proxy("db").drop(password, db)
225 except xmlrpclib.Fault, e:
226 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
227 return {'error': e.faultCode, 'title': 'Drop Database'}
228 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
230 @openerpweb.httprequest
231 def backup(self, req, backup_db, backup_pwd, token):
233 db_dump = base64.decodestring(
234 req.session.proxy("db").dump(backup_pwd, backup_db))
235 cherrypy.response.headers['Content-Type'] = "application/octet-stream; charset=binary"
236 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename="' + backup_db + '.dump"'
237 cherrypy.response.cookie['fileToken'] = token
238 cherrypy.response.cookie['fileToken']['path'] = '/'
240 except xmlrpclib.Fault, e:
241 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
242 return 'Backup Database|' + e.faultCode
243 return 'Backup Database|Could not generate database backup'
245 @openerpweb.httprequest
246 def restore(self, req, db_file, restore_pwd, new_db):
248 data = base64.encodestring(db_file.file.read())
249 req.session.proxy("db").restore(restore_pwd, new_db, data)
251 except xmlrpclib.Fault, e:
252 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
253 raise cherrypy.HTTPError(403)
255 raise cherrypy.HTTPError()
257 @openerpweb.jsonrequest
258 def change_password(self, req, fields):
259 old_password, new_password = operator.itemgetter(
260 'old_pwd', 'new_pwd')(
261 dict(map(operator.itemgetter('name', 'value'), fields)))
263 return req.session.proxy("db").change_admin_password(old_password, new_password)
264 except xmlrpclib.Fault, e:
265 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
266 return {'error': e.faultCode, 'title': 'Change Password'}
267 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
269 class Session(openerpweb.Controller):
270 _cp_path = "/base/session"
272 @openerpweb.jsonrequest
273 def login(self, req, db, login, password):
274 req.session.login(db, login, password)
277 "session_id": req.session_id,
278 "uid": req.session._uid,
281 @openerpweb.jsonrequest
282 def sc_list(self, req):
283 return req.session.model('ir.ui.view_sc').get_sc(req.session._uid, "ir.ui.menu",
284 req.session.eval_context(req.context))
286 @openerpweb.jsonrequest
287 def get_lang_list(self, req):
290 'lang_list': (req.session.proxy("db").list_lang() or []),
294 return {"error": e, "title": "Languages"}
296 @openerpweb.jsonrequest
297 def modules(self, req):
298 # TODO query server for installed web modules
300 for name, manifest in openerpweb.addons_manifest.items():
301 if name != 'base' and manifest.get('active', True):
305 @openerpweb.jsonrequest
306 def eval_domain_and_context(self, req, contexts, domains,
308 """ Evaluates sequences of domains and contexts, composing them into
309 a single context, domain or group_by sequence.
311 :param list contexts: list of contexts to merge together. Contexts are
312 evaluated in sequence, all previous contexts
313 are part of their own evaluation context
314 (starting at the session context).
315 :param list domains: list of domains to merge together. Domains are
316 evaluated in sequence and appended to one another
317 (implicit AND), their evaluation domain is the
318 result of merging all contexts.
319 :param list group_by_seq: list of domains (which may be in a different
320 order than the ``contexts`` parameter),
321 evaluated in sequence, their ``'group_by'``
322 key is extracted if they have one.
327 the global context created by merging all of
331 the concatenation of all domains
334 a list of fields to group by, potentially empty (in which case
335 no group by should be performed)
337 context, domain = eval_context_and_domain(req.session,
338 openerpweb.nonliterals.CompoundContext(*(contexts or [])),
339 openerpweb.nonliterals.CompoundDomain(*(domains or [])))
341 group_by_sequence = []
342 for candidate in (group_by_seq or []):
343 ctx = req.session.eval_context(candidate, context)
344 group_by = ctx.get('group_by')
347 elif isinstance(group_by, basestring):
348 group_by_sequence.append(group_by)
350 group_by_sequence.extend(group_by)
355 'group_by': group_by_sequence
358 @openerpweb.jsonrequest
359 def save_session_action(self, req, the_action):
361 This method store an action object in the session object and returns an integer
362 identifying that action. The method get_session_action() can be used to get
365 :param the_action: The action to save in the session.
366 :type the_action: anything
367 :return: A key identifying the saved action.
370 saved_actions = cherrypy.session.get('saved_actions')
371 if not saved_actions:
372 saved_actions = {"next":0, "actions":{}}
373 cherrypy.session['saved_actions'] = saved_actions
374 # we don't allow more than 10 stored actions
375 if len(saved_actions["actions"]) >= 10:
376 del saved_actions["actions"][min(saved_actions["actions"].keys())]
377 key = saved_actions["next"]
378 saved_actions["actions"][key] = the_action
379 saved_actions["next"] = key + 1
382 @openerpweb.jsonrequest
383 def get_session_action(self, req, key):
385 Gets back a previously saved action. This method can return None if the action
386 was saved since too much time (this case should be handled in a smart way).
388 :param key: The key given by save_session_action()
390 :return: The saved action or None.
393 saved_actions = cherrypy.session.get('saved_actions')
394 if not saved_actions:
396 return saved_actions["actions"].get(key)
398 @openerpweb.jsonrequest
399 def check(self, req):
400 req.session.assert_valid()
403 def eval_context_and_domain(session, context, domain=None):
404 e_context = session.eval_context(context)
405 # should we give the evaluated context as an evaluation context to the domain?
406 e_domain = session.eval_domain(domain or [])
408 return e_context, e_domain
410 def load_actions_from_ir_values(req, key, key2, models, meta, context):
411 Values = req.session.model('ir.values')
412 actions = Values.get(key, key2, models, meta, context)
414 return [(id, name, clean_action(action, req.session, context=context))
415 for id, name, action in actions]
417 def clean_action(action, session, context=None):
418 action.setdefault('flags', {})
419 if action['type'] != 'ir.actions.act_window':
421 # values come from the server, we can just eval them
422 if isinstance(action.get('context'), basestring):
423 action['context'] = eval(
425 session.evaluation_context(context=context)) or {}
427 if isinstance(action.get('domain'), basestring):
428 action['domain'] = eval(
430 session.evaluation_context(
431 action.get('context', {}))) or []
433 return fix_view_modes(action)
435 def generate_views(action):
437 While the server generates a sequence called "views" computing dependencies
438 between a bunch of stuff for views coming directly from the database
439 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
440 to return custom view dictionaries generated on the fly.
442 In that case, there is no ``views`` key available on the action.
444 Since the web client relies on ``action['views']``, generate it here from
445 ``view_mode`` and ``view_id``.
447 Currently handles two different cases:
449 * no view_id, multiple view_mode
450 * single view_id, single view_mode
452 :param dict action: action descriptor dictionary to generate a views key for
454 view_id = action.get('view_id', False)
455 if isinstance(view_id, (list, tuple)):
458 # providing at least one view mode is a requirement, not an option
459 view_modes = action['view_mode'].split(',')
461 if len(view_modes) > 1:
463 raise ValueError('Non-db action dictionaries should provide '
464 'either multiple view modes or a single view '
465 'mode and an optional view id.\n\n Got view '
466 'modes %r and view id %r for action %r' % (
467 view_modes, view_id, action))
468 action['views'] = [(False, mode) for mode in view_modes]
470 action['views'] = [(view_id, view_modes[0])]
472 def fix_view_modes(action):
473 """ For historical reasons, OpenERP has weird dealings in relation to
474 view_mode and the view_type attribute (on window actions):
476 * one of the view modes is ``tree``, which stands for both list views
478 * the choice is made by checking ``view_type``, which is either
479 ``form`` for a list view or ``tree`` for an actual tree view
481 This methods simply folds the view_type into view_mode by adding a
482 new view mode ``list`` which is the result of the ``tree`` view_mode
483 in conjunction with the ``form`` view_type.
485 TODO: this should go into the doc, some kind of "peculiarities" section
487 :param dict action: an action descriptor
488 :returns: nothing, the action is modified in place
490 if 'views' not in action:
491 generate_views(action)
493 if action.pop('view_type') != 'form':
497 [id, mode if mode != 'tree' else 'list']
498 for id, mode in action['views']
503 class Menu(openerpweb.Controller):
504 _cp_path = "/base/menu"
506 @openerpweb.jsonrequest
508 return {'data': self.do_load(req)}
510 def do_load(self, req):
511 """ Loads all menu items (all applications and their sub-menus).
513 :param req: A request object, with an OpenERP session attribute
514 :type req: < session -> OpenERPSession >
515 :return: the menu root
516 :rtype: dict('children': menu_nodes)
518 Menus = req.session.model('ir.ui.menu')
519 # menus are loaded fully unlike a regular tree view, cause there are
520 # less than 512 items
521 context = req.session.eval_context(req.context)
522 menu_ids = Menus.search([], 0, False, False, context)
523 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
524 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
525 menu_items.append(menu_root)
527 # make a tree using parent_id
528 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
529 for menu_item in menu_items:
530 if menu_item['parent_id']:
531 parent = menu_item['parent_id'][0]
534 if parent in menu_items_map:
535 menu_items_map[parent].setdefault(
536 'children', []).append(menu_item)
538 # sort by sequence a tree using parent_id
539 for menu_item in menu_items:
540 menu_item.setdefault('children', []).sort(
541 key=lambda x:x["sequence"])
545 @openerpweb.jsonrequest
546 def action(self, req, menu_id):
547 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
548 [('ir.ui.menu', menu_id)], False,
549 req.session.eval_context(req.context))
550 return {"action": actions}
552 class DataSet(openerpweb.Controller):
553 _cp_path = "/base/dataset"
555 @openerpweb.jsonrequest
556 def fields(self, req, model):
557 return {'fields': req.session.model(model).fields_get(False,
558 req.session.eval_context(req.context))}
560 @openerpweb.jsonrequest
561 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, sort=None):
562 return self.do_search_read(request, model, fields, offset, limit, domain, sort)
563 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None
565 """ Performs a search() followed by a read() (if needed) using the
566 provided search criteria
568 :param request: a JSON-RPC request object
569 :type request: openerpweb.JsonRequest
570 :param str model: the name of the model to search on
571 :param fields: a list of the fields to return in the result records
573 :param int offset: from which index should the results start being returned
574 :param int limit: the maximum number of records to return
575 :param list domain: the search domain for the query
576 :param list sort: sorting directives
577 :returns: A structure (dict) with two keys: ids (all the ids matching
578 the (domain, context) pair) and records (paginated records
579 matching fields selection set)
582 Model = request.session.model(model)
584 context, domain = eval_context_and_domain(
585 request.session, request.context, domain)
587 ids = Model.search(domain, 0, False, sort or False, context)
588 # need to fill the dataset with all ids for the (domain, context) pair,
589 # so search un-paginated and paginate manually before reading
590 paginated_ids = ids[offset:(offset + limit if limit else None)]
591 if fields and fields == ['id']:
592 # shortcut read if we only want the ids
595 'records': map(lambda id: {'id': id}, paginated_ids)
598 records = Model.read(paginated_ids, fields or False, context)
599 records.sort(key=lambda obj: ids.index(obj['id']))
606 @openerpweb.jsonrequest
607 def read(self, request, model, ids, fields=False):
608 return self.do_search_read(request, model, ids, fields)
610 @openerpweb.jsonrequest
611 def get(self, request, model, ids, fields=False):
612 return self.do_get(request, model, ids, fields)
614 def do_get(self, request, model, ids, fields=False):
615 """ Fetches and returns the records of the model ``model`` whose ids
618 The results are in the same order as the inputs, but elements may be
619 missing (if there is no record left for the id)
621 :param request: the JSON-RPC2 request object
622 :type request: openerpweb.JsonRequest
623 :param model: the model to read from
625 :param ids: a list of identifiers
627 :param fields: a list of fields to fetch, ``False`` or empty to fetch
628 all fields in the model
629 :type fields: list | False
630 :returns: a list of records, in the same order as the list of ids
633 Model = request.session.model(model)
634 records = Model.read(ids, fields, request.session.eval_context(request.context))
636 record_map = dict((record['id'], record) for record in records)
638 return [record_map[id] for id in ids if record_map.get(id)]
640 @openerpweb.jsonrequest
641 def load(self, req, model, id, fields):
642 m = req.session.model(model)
644 r = m.read([id], False, req.session.eval_context(req.context))
647 return {'value': value}
649 @openerpweb.jsonrequest
650 def create(self, req, model, data):
651 m = req.session.model(model)
652 r = m.create(data, req.session.eval_context(req.context))
655 @openerpweb.jsonrequest
656 def save(self, req, model, id, data):
657 m = req.session.model(model)
658 r = m.write([id], data, req.session.eval_context(req.context))
661 @openerpweb.jsonrequest
662 def unlink(self, request, model, ids=()):
663 Model = request.session.model(model)
664 return Model.unlink(ids, request.session.eval_context(request.context))
666 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
667 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
668 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
669 c, d = eval_context_and_domain(req.session, context, domain)
670 if domain_id and len(args) - 1 >= domain_id:
672 if context_id and len(args) - 1 >= context_id:
675 return getattr(req.session.model(model), method)(*args)
677 @openerpweb.jsonrequest
678 def call(self, req, model, method, args, domain_id=None, context_id=None):
679 return self.call_common(req, model, method, args, domain_id, context_id)
681 @openerpweb.jsonrequest
682 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
683 action = self.call_common(req, model, method, args, domain_id, context_id)
684 if isinstance(action, dict) and action.get('type') != '':
685 return {'result': clean_action(action, req.session)}
686 return {'result': False}
688 @openerpweb.jsonrequest
689 def exec_workflow(self, req, model, id, signal):
690 r = req.session.exec_workflow(model, id, signal)
693 @openerpweb.jsonrequest
694 def default_get(self, req, model, fields):
695 Model = req.session.model(model)
696 return Model.default_get(fields, req.session.eval_context(req.context))
698 @openerpweb.jsonrequest
699 def name_search(self, req, model, search_str, domain=[], context={}):
700 m = req.session.model(model)
701 r = m.name_search(search_str+'%', domain, '=ilike', context)
704 class DataGroup(openerpweb.Controller):
705 _cp_path = "/base/group"
706 @openerpweb.jsonrequest
707 def read(self, request, model, fields, group_by_fields, domain=None, sort=None):
708 Model = request.session.model(model)
709 context, domain = eval_context_and_domain(request.session, request.context, domain)
711 return Model.read_group(
712 domain or [], fields, group_by_fields, 0, False,
713 dict(context, group_by=group_by_fields), sort or False)
715 class View(openerpweb.Controller):
716 _cp_path = "/base/view"
718 def fields_view_get(self, request, model, view_id, view_type,
719 transform=True, toolbar=False, submenu=False):
720 Model = request.session.model(model)
721 context = request.session.eval_context(request.context)
722 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
723 # todo fme?: check that we should pass the evaluated context here
724 self.process_view(request.session, fvg, context, transform)
727 def process_view(self, session, fvg, context, transform):
728 # depending on how it feels, xmlrpclib.ServerProxy can translate
729 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
730 # enjoy unicode strings which can not be trivially converted to
731 # strings, and it blows up during parsing.
733 # So ensure we fix this retardation by converting view xml back to
735 if isinstance(fvg['arch'], unicode):
736 arch = fvg['arch'].encode('utf-8')
741 evaluation_context = session.evaluation_context(context or {})
742 xml = self.transform_view(arch, session, evaluation_context)
744 xml = ElementTree.fromstring(arch)
745 fvg['arch'] = Xml2Json.convert_element(xml)
747 for field in fvg['fields'].itervalues():
748 if field.get('views'):
749 for view in field["views"].itervalues():
750 self.process_view(session, view, None, transform)
751 if field.get('domain'):
752 field["domain"] = self.parse_domain(field["domain"], session)
753 if field.get('context'):
754 field["context"] = self.parse_context(field["context"], session)
756 @openerpweb.jsonrequest
757 def add_custom(self, request, view_id, arch):
758 CustomView = request.session.model('ir.ui.view.custom')
760 'user_id': request.session._uid,
763 }, request.session.eval_context(request.context))
764 return {'result': True}
766 @openerpweb.jsonrequest
767 def undo_custom(self, request, view_id, reset=False):
768 CustomView = request.session.model('ir.ui.view.custom')
769 context = request.session.eval_context(request.context)
770 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
771 0, False, False, context)
774 CustomView.unlink(vcustom, context)
776 CustomView.unlink([vcustom[0]], context)
777 return {'result': True}
778 return {'result': False}
780 def transform_view(self, view_string, session, context=None):
781 # transform nodes on the fly via iterparse, instead of
782 # doing it statically on the parsing result
783 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
785 for event, elem in parser:
789 self.parse_domains_and_contexts(elem, session)
792 def parse_domain(self, domain, session):
793 """ Parses an arbitrary string containing a domain, transforms it
794 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
796 :param domain: the domain to parse, if the domain is not a string it
797 is assumed to be a literal domain and is returned as-is
798 :param session: Current OpenERP session
799 :type session: openerpweb.openerpweb.OpenERPSession
801 if not isinstance(domain, (str, unicode)):
804 return openerpweb.ast.literal_eval(domain)
807 return openerpweb.nonliterals.Domain(session, domain)
809 def parse_context(self, context, session):
810 """ Parses an arbitrary string containing a context, transforms it
811 to either a literal context or a :class:`openerpweb.nonliterals.Context`
813 :param context: the context to parse, if the context is not a string it
814 is assumed to be a literal domain and is returned as-is
815 :param session: Current OpenERP session
816 :type session: openerpweb.openerpweb.OpenERPSession
818 if not isinstance(context, (str, unicode)):
821 return openerpweb.ast.literal_eval(context)
823 return openerpweb.nonliterals.Context(session, context)
825 def parse_domains_and_contexts(self, elem, session):
826 """ Converts domains and contexts from the view into Python objects,
827 either literals if they can be parsed by literal_eval or a special
828 placeholder object if the domain or context refers to free variables.
830 :param elem: the current node being parsed
831 :type param: xml.etree.ElementTree.Element
832 :param session: OpenERP session object, used to store and retrieve
834 :type session: openerpweb.openerpweb.OpenERPSession
836 for el in ['domain', 'filter_domain']:
837 domain = elem.get(el, '').strip()
839 elem.set(el, self.parse_domain(domain, session))
840 for el in ['context', 'default_get']:
841 context_string = elem.get(el, '').strip()
843 elem.set(el, self.parse_context(context_string, session))
845 class FormView(View):
846 _cp_path = "/base/formview"
848 @openerpweb.jsonrequest
849 def load(self, req, model, view_id, toolbar=False):
850 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
851 return {'fields_view': fields_view}
853 class ListView(View):
854 _cp_path = "/base/listview"
856 @openerpweb.jsonrequest
857 def load(self, req, model, view_id, toolbar=False):
858 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
859 return {'fields_view': fields_view}
861 def process_colors(self, view, row, context):
862 colors = view['arch']['attrs'].get('colors')
869 for pair in colors.split(';')
870 if eval(pair.split(':')[1], dict(context, **row))
875 elif len(color) == 1:
879 class SearchView(View):
880 _cp_path = "/base/searchview"
882 @openerpweb.jsonrequest
883 def load(self, req, model, view_id):
884 fields_view = self.fields_view_get(req, model, view_id, 'search')
885 return {'fields_view': fields_view}
887 @openerpweb.jsonrequest
888 def fields_get(self, req, model):
889 Model = req.session.model(model)
890 fields = Model.fields_get(False, req.session.eval_context(req.context))
891 for field in fields.values():
892 # shouldn't convert the views too?
893 if field.get('domain'):
894 field["domain"] = self.parse_domain(field["domain"], req.session)
895 if field.get('context'):
896 field["context"] = self.parse_domain(field["context"], req.session)
897 return {'fields': fields}
899 @openerpweb.jsonrequest
900 def get_filters(self, req, model):
901 Model = req.session.model("ir.filters")
902 filters = Model.get_filters(model)
903 for filter in filters:
904 filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session))
905 filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session))
908 @openerpweb.jsonrequest
909 def save_filter(self, req, model, name, context_to_save, domain):
910 Model = req.session.model("ir.filters")
911 ctx = openerpweb.nonliterals.CompoundContext(context_to_save)
912 ctx.session = req.session
914 domain = openerpweb.nonliterals.CompoundDomain(domain)
915 domain.session = req.session
916 domain = domain.evaluate()
917 uid = req.session._uid
918 context = req.session.eval_context(req.context)
919 to_return = Model.create_or_replace({"context": ctx,
927 class Binary(openerpweb.Controller):
928 _cp_path = "/base/binary"
930 @openerpweb.httprequest
931 def image(self, request, session_id, model, id, field, **kw):
932 cherrypy.response.headers['Content-Type'] = 'image/png'
933 Model = request.session.model(model)
934 context = request.session.eval_context(request.context)
937 res = Model.default_get([field], context).get(field, '')
939 res = Model.read([int(id)], [field], context)[0].get(field, '')
940 return base64.decodestring(res)
941 except: # TODO: what's the exception here?
942 return self.placeholder()
943 def placeholder(self):
944 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
946 @openerpweb.httprequest
947 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
948 Model = request.session.model(model)
949 context = request.session.eval_context(request.context)
950 res = Model.read([int(id)], [field, fieldname], context)[0]
951 filecontent = res.get(field, '')
953 raise cherrypy.NotFound
955 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
956 filename = '%s_%s' % (model.replace('.', '_'), id)
958 filename = res.get(fieldname, '') or filename
959 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
960 return base64.decodestring(filecontent)
962 @openerpweb.httprequest
963 def upload(self, request, session_id, callback, ufile=None):
964 cherrypy.response.timeout = 500
966 for key, val in cherrypy.request.headers.iteritems():
967 headers[key.lower()] = val
968 size = int(headers.get('content-length', 0))
969 # TODO: might be useful to have a configuration flag for max-length file uploads
971 out = """<script language="javascript" type="text/javascript">
972 var win = window.top.window,
974 if (typeof(callback) === 'function') {
975 callback.apply(this, %s);
977 win.jQuery('#oe_notification', win.document).notify('create', {
978 title: "Ajax File Upload",
979 text: "Could not find callback"
983 data = ufile.file.read()
984 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
986 args = [False, e.message]
987 return out % (simplejson.dumps(callback), simplejson.dumps(args))
989 @openerpweb.httprequest
990 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
991 cherrypy.response.timeout = 500
992 context = request.session.eval_context(request.context)
993 Model = request.session.model('ir.attachment')
995 out = """<script language="javascript" type="text/javascript">
996 var win = window.top.window,
998 if (typeof(callback) === 'function') {
999 callback.call(this, %s);
1002 attachment_id = Model.create({
1003 'name': ufile.filename,
1004 'datas': base64.encodestring(ufile.file.read()),
1009 'filename': ufile.filename,
1012 except Exception, e:
1013 args = { 'error': e.message }
1014 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1016 class Action(openerpweb.Controller):
1017 _cp_path = "/base/action"
1019 @openerpweb.jsonrequest
1020 def load(self, req, action_id):
1021 Actions = req.session.model('ir.actions.actions')
1023 context = req.session.eval_context(req.context)
1024 action_type = Actions.read([action_id], ['type'], context)
1026 action = req.session.model(action_type[0]['type']).read([action_id], False,
1029 value = clean_action(action[0], req.session)
1030 return {'result': value}
1032 @openerpweb.jsonrequest
1033 def run(self, req, action_id):
1034 return clean_action(req.session.model('ir.actions.server').run(
1035 [action_id], req.session.eval_context(req.context)), req.session)
1037 class TreeView(View):
1038 _cp_path = "/base/treeview"
1040 @openerpweb.jsonrequest
1041 def load(self, req, model, view_id, toolbar=False):
1042 return self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
1044 @openerpweb.jsonrequest
1045 def action(self, req, model, id):
1046 return load_actions_from_ir_values(
1047 req,'action', 'tree_but_open',[(model, id)],
1048 False, req.session.eval_context(req.context))
1050 def export_csv(fields, result):
1052 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1054 writer.writerow(fields)
1059 if isinstance(d, basestring):
1060 d = d.replace('\n',' ').replace('\t',' ')
1062 d = d.encode('utf-8')
1065 if d is False: d = None
1067 writer.writerow(row)
1074 def export_xls(fieldnames, table):
1078 common.error(_('Import Error.'), _('Please install xlwt library to export to MS Excel.'))
1080 workbook = xlwt.Workbook()
1081 worksheet = workbook.add_sheet('Sheet 1')
1083 for i, fieldname in enumerate(fieldnames):
1084 worksheet.write(0, i, str(fieldname))
1085 worksheet.col(i).width = 8000 # around 220 pixels
1087 style = xlwt.easyxf('align: wrap yes')
1089 for row_index, row in enumerate(table):
1090 for cell_index, cell_value in enumerate(row):
1091 cell_value = str(cell_value)
1092 cell_value = re.sub("\r", " ", cell_value)
1093 worksheet.write(row_index + 1, cell_index, cell_value, style)
1101 #return data.decode('ISO-8859-1')
1102 return unicode(data, 'utf-8', 'replace')
1105 _cp_path = "/base/export"
1107 def fields_get(self, req, model):
1108 Model = req.session.model(model)
1109 fields = Model.fields_get(False, req.session.eval_context(req.context))
1112 @openerpweb.jsonrequest
1113 def get_fields(self, req, model, prefix='', name= '', field_parent=None, params={}):
1114 import_compat = params.get("import_compat", False)
1116 fields = self.fields_get(req, model)
1117 field_parent_type = params.get("parent_field_type",False)
1119 if import_compat and field_parent_type and field_parent_type == "many2one":
1122 fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1124 fields_order = fields.keys()
1125 fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1127 for index, field in enumerate(fields_order):
1128 value = fields[field]
1130 if import_compat and value.get('readonly', False):
1132 for sl in value.get('states', {}).values():
1134 ok = ok or (s==['readonly',False])
1137 id = prefix + (prefix and '/'or '') + field
1138 nm = name + (name and '/' or '') + value['string']
1139 record.update(id=id, string= nm, action='javascript: void(0)',
1140 target=None, icon=None, children=[], field_type=value.get('type',False), required=value.get('required', False))
1141 records.append(record)
1143 if len(nm.split('/')) < 3 and value.get('relation', False):
1145 ref = value.pop('relation')
1146 cfields = self.fields_get(req, ref)
1147 if (value['type'] == 'many2many'):
1148 record['children'] = []
1149 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1151 elif value['type'] == 'many2one':
1152 record['children'] = [id + '/id', id + '/.id']
1153 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1156 cfields_order = cfields.keys()
1157 cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1159 for j, fld in enumerate(cfields_order):
1160 cid = id + '/' + fld
1161 cid = cid.replace(' ', '_')
1162 children.append(cid)
1163 record['children'] = children or []
1164 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1166 ref = value.pop('relation')
1167 cfields = self.fields_get(req, ref)
1168 cfields_order = cfields.keys()
1169 cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1171 for j, fld in enumerate(cfields_order):
1172 cid = id + '/' + fld
1173 cid = cid.replace(' ', '_')
1174 children.append(cid)
1175 record['children'] = children or []
1176 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1181 @openerpweb.jsonrequest
1182 def save_export_lists(self, req, name, model, field_list):
1183 result = {'resource':model, 'name':name, 'export_fields': []}
1184 for field in field_list:
1185 result['export_fields'].append((0, 0, {'name': field}))
1186 return req.session.model("ir.exports").create(result, req.session.eval_context(req.context))
1188 @openerpweb.jsonrequest
1189 def exist_export_lists(self, req, model):
1190 export_model = req.session.model("ir.exports")
1191 return export_model.read(export_model.search([('resource', '=', model)]), ['name'])
1193 @openerpweb.jsonrequest
1194 def delete_export(self, req, export_id):
1195 req.session.model("ir.exports").unlink(export_id, req.session.eval_context(req.context))
1198 @openerpweb.jsonrequest
1199 def namelist(self,req, model, export_id):
1201 result = self.get_data(req, model, req.session.eval_context(req.context))
1202 ir_export_obj = req.session.model("ir.exports")
1203 ir_export_line_obj = req.session.model("ir.exports.line")
1205 field = ir_export_obj.read(export_id)
1206 fields = ir_export_line_obj.read(field['export_fields'])
1209 [name_list.update({field['name']: result.get(field['name'])}) for field in fields]
1212 def get_data(self, req, model, context=None):
1214 context = context or {}
1216 proxy = req.session.model(model)
1217 fields = self.fields_get(req, model)
1219 f1 = proxy.fields_view_get(False, 'tree', context)['fields']
1220 f2 = proxy.fields_view_get(False, 'form', context)['fields']
1224 fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1227 _fields = {'id': 'ID' , '.id': 'Database ID' }
1228 def model_populate(fields, prefix_node='', prefix=None, prefix_value='', level=2):
1229 fields_order = fields.keys()
1230 fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1232 for field in fields_order:
1233 fields_data[prefix_node+field] = fields[field]
1235 fields_data[prefix_node + field]['string'] = '%s%s' % (prefix_value, fields_data[prefix_node + field]['string'])
1236 st_name = fields[field]['string'] or field
1237 _fields[prefix_node+field] = st_name
1238 if fields[field].get('relation', False) and level>0:
1239 fields2 = self.fields_get(req, fields[field]['relation'])
1240 fields2.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1241 model_populate(fields2, prefix_node+field+'/', None, st_name+'/', level-1)
1242 model_populate(fields)
1246 @openerpweb.jsonrequest
1247 def export_data(self, req, model, fields, ids, domain, import_compat=False, export_format="csv", context=None):
1248 context = req.session.eval_context(req.context)
1249 modle_obj = req.session.model(model)
1250 ids = ids or modle_obj.search(domain, context=context)
1252 field = fields.keys()
1253 result = modle_obj.export_data(ids, field , context).get('datas',[])
1255 if not import_compat:
1256 field = [val.strip() for val in fields.values()]
1258 if export_format == 'xls':
1259 return export_xls(field, result)
1261 return export_csv(field, result)