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, addon_name, lang):
163 f_name = os.path.join(openerpweb.path_addons, addon_name, "po", lang + ".po")
164 with open(f_name) as t_file:
167 transl = {"messages":[]}
171 transl["messages"].append({'id': x.id, 'string': x.string})
176 class Database(openerpweb.Controller):
177 _cp_path = "/base/database"
179 @openerpweb.jsonrequest
180 def get_list(self, req):
181 proxy = req.session.proxy("db")
183 h = req.httprequest.headers['Host'].split(':')[0]
185 r = cherrypy.config['openerp.dbfilter'].replace('%h', h).replace('%d', d)
186 dbs = [i for i in dbs if re.match(r, i)]
187 return {"db_list": dbs}
189 @openerpweb.jsonrequest
190 def progress(self, req, password, id):
191 return req.session.proxy('db').get_progress(password, id)
193 @openerpweb.jsonrequest
194 def create(self, req, fields):
196 params = dict(map(operator.itemgetter('name', 'value'), fields))
198 params['super_admin_pwd'],
200 bool(params.get('demo_data')),
202 params['create_admin_pwd']
206 return req.session.proxy("db").create(*create_attrs)
207 except xmlrpclib.Fault, e:
208 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
209 return {'error': e.faultCode, 'title': 'Create Database'}
210 return {'error': 'Could not create database !', 'title': 'Create Database'}
212 @openerpweb.jsonrequest
213 def drop(self, req, fields):
214 password, db = operator.itemgetter(
215 'drop_pwd', 'drop_db')(
216 dict(map(operator.itemgetter('name', 'value'), fields)))
219 return req.session.proxy("db").drop(password, db)
220 except xmlrpclib.Fault, e:
221 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
222 return {'error': e.faultCode, 'title': 'Drop Database'}
223 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
225 @openerpweb.httprequest
226 def backup(self, req, backup_db, backup_pwd, token):
228 db_dump = base64.decodestring(
229 req.session.proxy("db").dump(backup_pwd, backup_db))
230 cherrypy.response.headers['Content-Type'] = "application/octet-stream; charset=binary"
231 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename="' + backup_db + '.dump"'
232 cherrypy.response.cookie['fileToken'] = token
233 cherrypy.response.cookie['fileToken']['path'] = '/'
235 except xmlrpclib.Fault, e:
236 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
237 return 'Backup Database|' + e.faultCode
238 return 'Backup Database|Could not generate database backup'
240 @openerpweb.httprequest
241 def restore(self, req, db_file, restore_pwd, new_db):
243 data = base64.encodestring(db_file.file.read())
244 req.session.proxy("db").restore(restore_pwd, new_db, data)
246 except xmlrpclib.Fault, e:
247 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
248 raise cherrypy.HTTPError(403)
250 raise cherrypy.HTTPError()
252 @openerpweb.jsonrequest
253 def change_password(self, req, fields):
254 old_password, new_password = operator.itemgetter(
255 'old_pwd', 'new_pwd')(
256 dict(map(operator.itemgetter('name', 'value'), fields)))
258 return req.session.proxy("db").change_admin_password(old_password, new_password)
259 except xmlrpclib.Fault, e:
260 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
261 return {'error': e.faultCode, 'title': 'Change Password'}
262 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
264 class Session(openerpweb.Controller):
265 _cp_path = "/base/session"
267 @openerpweb.jsonrequest
268 def login(self, req, db, login, password):
269 req.session.login(db, login, password)
272 "session_id": req.session_id,
273 "uid": req.session._uid,
276 @openerpweb.jsonrequest
277 def sc_list(self, req):
278 return req.session.model('ir.ui.view_sc').get_sc(req.session._uid, "ir.ui.menu",
279 req.session.eval_context(req.context))
281 @openerpweb.jsonrequest
282 def get_lang_list(self, req):
285 'lang_list': (req.session.proxy("db").list_lang() or []),
289 return {"error": e, "title": "Languages"}
291 @openerpweb.jsonrequest
292 def modules(self, req):
293 # TODO query server for installed web modules
295 for name, manifest in openerpweb.addons_manifest.items():
296 if name != 'base' and manifest.get('active', True):
300 @openerpweb.jsonrequest
301 def eval_domain_and_context(self, req, contexts, domains,
303 """ Evaluates sequences of domains and contexts, composing them into
304 a single context, domain or group_by sequence.
306 :param list contexts: list of contexts to merge together. Contexts are
307 evaluated in sequence, all previous contexts
308 are part of their own evaluation context
309 (starting at the session context).
310 :param list domains: list of domains to merge together. Domains are
311 evaluated in sequence and appended to one another
312 (implicit AND), their evaluation domain is the
313 result of merging all contexts.
314 :param list group_by_seq: list of domains (which may be in a different
315 order than the ``contexts`` parameter),
316 evaluated in sequence, their ``'group_by'``
317 key is extracted if they have one.
322 the global context created by merging all of
326 the concatenation of all domains
329 a list of fields to group by, potentially empty (in which case
330 no group by should be performed)
332 context, domain = eval_context_and_domain(req.session,
333 openerpweb.nonliterals.CompoundContext(*(contexts or [])),
334 openerpweb.nonliterals.CompoundDomain(*(domains or [])))
336 group_by_sequence = []
337 for candidate in (group_by_seq or []):
338 ctx = req.session.eval_context(candidate, context)
339 group_by = ctx.get('group_by')
342 elif isinstance(group_by, basestring):
343 group_by_sequence.append(group_by)
345 group_by_sequence.extend(group_by)
350 'group_by': group_by_sequence
353 @openerpweb.jsonrequest
354 def save_session_action(self, req, the_action):
356 This method store an action object in the session object and returns an integer
357 identifying that action. The method get_session_action() can be used to get
360 :param the_action: The action to save in the session.
361 :type the_action: anything
362 :return: A key identifying the saved action.
365 saved_actions = cherrypy.session.get('saved_actions')
366 if not saved_actions:
367 saved_actions = {"next":0, "actions":{}}
368 cherrypy.session['saved_actions'] = saved_actions
369 # we don't allow more than 10 stored actions
370 if len(saved_actions["actions"]) >= 10:
371 del saved_actions["actions"][min(saved_actions["actions"].keys())]
372 key = saved_actions["next"]
373 saved_actions["actions"][key] = the_action
374 saved_actions["next"] = key + 1
377 @openerpweb.jsonrequest
378 def get_session_action(self, req, key):
380 Gets back a previously saved action. This method can return None if the action
381 was saved since too much time (this case should be handled in a smart way).
383 :param key: The key given by save_session_action()
385 :return: The saved action or None.
388 saved_actions = cherrypy.session.get('saved_actions')
389 if not saved_actions:
391 return saved_actions["actions"].get(key)
393 @openerpweb.jsonrequest
394 def check(self, req):
395 req.session.assert_valid()
398 def eval_context_and_domain(session, context, domain=None):
399 e_context = session.eval_context(context)
400 # should we give the evaluated context as an evaluation context to the domain?
401 e_domain = session.eval_domain(domain or [])
403 return e_context, e_domain
405 def load_actions_from_ir_values(req, key, key2, models, meta, context):
406 Values = req.session.model('ir.values')
407 actions = Values.get(key, key2, models, meta, context)
409 return [(id, name, clean_action(action, req.session, context=context))
410 for id, name, action in actions]
412 def clean_action(action, session, context=None):
413 action.setdefault('flags', {})
414 if action['type'] != 'ir.actions.act_window':
416 # values come from the server, we can just eval them
417 if isinstance(action.get('context'), basestring):
418 action['context'] = eval(
420 session.evaluation_context(context=context)) or {}
422 if isinstance(action.get('domain'), basestring):
423 action['domain'] = eval(
425 session.evaluation_context(
426 action.get('context', {}))) or []
428 return fix_view_modes(action)
430 def generate_views(action):
432 While the server generates a sequence called "views" computing dependencies
433 between a bunch of stuff for views coming directly from the database
434 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
435 to return custom view dictionaries generated on the fly.
437 In that case, there is no ``views`` key available on the action.
439 Since the web client relies on ``action['views']``, generate it here from
440 ``view_mode`` and ``view_id``.
442 Currently handles two different cases:
444 * no view_id, multiple view_mode
445 * single view_id, single view_mode
447 :param dict action: action descriptor dictionary to generate a views key for
449 view_id = action.get('view_id', False)
450 if isinstance(view_id, (list, tuple)):
453 # providing at least one view mode is a requirement, not an option
454 view_modes = action['view_mode'].split(',')
456 if len(view_modes) > 1:
458 raise ValueError('Non-db action dictionaries should provide '
459 'either multiple view modes or a single view '
460 'mode and an optional view id.\n\n Got view '
461 'modes %r and view id %r for action %r' % (
462 view_modes, view_id, action))
463 action['views'] = [(False, mode) for mode in view_modes]
465 action['views'] = [(view_id, view_modes[0])]
467 def fix_view_modes(action):
468 """ For historical reasons, OpenERP has weird dealings in relation to
469 view_mode and the view_type attribute (on window actions):
471 * one of the view modes is ``tree``, which stands for both list views
473 * the choice is made by checking ``view_type``, which is either
474 ``form`` for a list view or ``tree`` for an actual tree view
476 This methods simply folds the view_type into view_mode by adding a
477 new view mode ``list`` which is the result of the ``tree`` view_mode
478 in conjunction with the ``form`` view_type.
480 TODO: this should go into the doc, some kind of "peculiarities" section
482 :param dict action: an action descriptor
483 :returns: nothing, the action is modified in place
485 if 'views' not in action:
486 generate_views(action)
488 if action.pop('view_type') != 'form':
492 [id, mode if mode != 'tree' else 'list']
493 for id, mode in action['views']
498 class Menu(openerpweb.Controller):
499 _cp_path = "/base/menu"
501 @openerpweb.jsonrequest
503 return {'data': self.do_load(req)}
505 def do_load(self, req):
506 """ Loads all menu items (all applications and their sub-menus).
508 :param req: A request object, with an OpenERP session attribute
509 :type req: < session -> OpenERPSession >
510 :return: the menu root
511 :rtype: dict('children': menu_nodes)
513 Menus = req.session.model('ir.ui.menu')
514 # menus are loaded fully unlike a regular tree view, cause there are
515 # less than 512 items
516 context = req.session.eval_context(req.context)
517 menu_ids = Menus.search([], 0, False, False, context)
518 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
519 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
520 menu_items.append(menu_root)
522 # make a tree using parent_id
523 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
524 for menu_item in menu_items:
525 if menu_item['parent_id']:
526 parent = menu_item['parent_id'][0]
529 if parent in menu_items_map:
530 menu_items_map[parent].setdefault(
531 'children', []).append(menu_item)
533 # sort by sequence a tree using parent_id
534 for menu_item in menu_items:
535 menu_item.setdefault('children', []).sort(
536 key=lambda x:x["sequence"])
540 @openerpweb.jsonrequest
541 def action(self, req, menu_id):
542 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
543 [('ir.ui.menu', menu_id)], False,
544 req.session.eval_context(req.context))
545 return {"action": actions}
547 class DataSet(openerpweb.Controller):
548 _cp_path = "/base/dataset"
550 @openerpweb.jsonrequest
551 def fields(self, req, model):
552 return {'fields': req.session.model(model).fields_get(False,
553 req.session.eval_context(req.context))}
555 @openerpweb.jsonrequest
556 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, sort=None):
557 return self.do_search_read(request, model, fields, offset, limit, domain, sort)
558 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None
560 """ Performs a search() followed by a read() (if needed) using the
561 provided search criteria
563 :param request: a JSON-RPC request object
564 :type request: openerpweb.JsonRequest
565 :param str model: the name of the model to search on
566 :param fields: a list of the fields to return in the result records
568 :param int offset: from which index should the results start being returned
569 :param int limit: the maximum number of records to return
570 :param list domain: the search domain for the query
571 :param list sort: sorting directives
572 :returns: A structure (dict) with two keys: ids (all the ids matching
573 the (domain, context) pair) and records (paginated records
574 matching fields selection set)
577 Model = request.session.model(model)
579 context, domain = eval_context_and_domain(
580 request.session, request.context, domain)
582 ids = Model.search(domain, 0, False, sort or False, context)
583 # need to fill the dataset with all ids for the (domain, context) pair,
584 # so search un-paginated and paginate manually before reading
585 paginated_ids = ids[offset:(offset + limit if limit else None)]
586 if fields and fields == ['id']:
587 # shortcut read if we only want the ids
590 'records': map(lambda id: {'id': id}, paginated_ids)
593 records = Model.read(paginated_ids, fields or False, context)
594 records.sort(key=lambda obj: ids.index(obj['id']))
601 @openerpweb.jsonrequest
602 def read(self, request, model, ids, fields=False):
603 return self.do_search_read(request, model, ids, fields)
605 @openerpweb.jsonrequest
606 def get(self, request, model, ids, fields=False):
607 return self.do_get(request, model, ids, fields)
609 def do_get(self, request, model, ids, fields=False):
610 """ Fetches and returns the records of the model ``model`` whose ids
613 The results are in the same order as the inputs, but elements may be
614 missing (if there is no record left for the id)
616 :param request: the JSON-RPC2 request object
617 :type request: openerpweb.JsonRequest
618 :param model: the model to read from
620 :param ids: a list of identifiers
622 :param fields: a list of fields to fetch, ``False`` or empty to fetch
623 all fields in the model
624 :type fields: list | False
625 :returns: a list of records, in the same order as the list of ids
628 Model = request.session.model(model)
629 records = Model.read(ids, fields, request.session.eval_context(request.context))
631 record_map = dict((record['id'], record) for record in records)
633 return [record_map[id] for id in ids if record_map.get(id)]
635 @openerpweb.jsonrequest
636 def load(self, req, model, id, fields):
637 m = req.session.model(model)
639 r = m.read([id], False, req.session.eval_context(req.context))
642 return {'value': value}
644 @openerpweb.jsonrequest
645 def create(self, req, model, data):
646 m = req.session.model(model)
647 r = m.create(data, req.session.eval_context(req.context))
650 @openerpweb.jsonrequest
651 def save(self, req, model, id, data):
652 m = req.session.model(model)
653 r = m.write([id], data, req.session.eval_context(req.context))
656 @openerpweb.jsonrequest
657 def unlink(self, request, model, ids=()):
658 Model = request.session.model(model)
659 return Model.unlink(ids, request.session.eval_context(request.context))
661 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
662 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
663 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
664 c, d = eval_context_and_domain(req.session, context, domain)
665 if domain_id and len(args) - 1 >= domain_id:
667 if context_id and len(args) - 1 >= context_id:
670 return getattr(req.session.model(model), method)(*args)
672 @openerpweb.jsonrequest
673 def call(self, req, model, method, args, domain_id=None, context_id=None):
674 return self.call_common(req, model, method, args, domain_id, context_id)
676 @openerpweb.jsonrequest
677 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
678 action = self.call_common(req, model, method, args, domain_id, context_id)
679 if isinstance(action, dict) and action.get('type') != '':
680 return {'result': clean_action(action, req.session)}
681 return {'result': False}
683 @openerpweb.jsonrequest
684 def exec_workflow(self, req, model, id, signal):
685 r = req.session.exec_workflow(model, id, signal)
688 @openerpweb.jsonrequest
689 def default_get(self, req, model, fields):
690 Model = req.session.model(model)
691 return Model.default_get(fields, req.session.eval_context(req.context))
693 @openerpweb.jsonrequest
694 def name_search(self, req, model, search_str, domain=[], context={}):
695 m = req.session.model(model)
696 r = m.name_search(search_str+'%', domain, '=ilike', context)
699 class DataGroup(openerpweb.Controller):
700 _cp_path = "/base/group"
701 @openerpweb.jsonrequest
702 def read(self, request, model, fields, group_by_fields, domain=None, sort=None):
703 Model = request.session.model(model)
704 context, domain = eval_context_and_domain(request.session, request.context, domain)
706 return Model.read_group(
707 domain or [], fields, group_by_fields, 0, False,
708 dict(context, group_by=group_by_fields), sort or False)
710 class View(openerpweb.Controller):
711 _cp_path = "/base/view"
713 def fields_view_get(self, request, model, view_id, view_type,
714 transform=True, toolbar=False, submenu=False):
715 Model = request.session.model(model)
716 context = request.session.eval_context(request.context)
717 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
718 # todo fme?: check that we should pass the evaluated context here
719 self.process_view(request.session, fvg, context, transform)
722 def process_view(self, session, fvg, context, transform):
723 # depending on how it feels, xmlrpclib.ServerProxy can translate
724 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
725 # enjoy unicode strings which can not be trivially converted to
726 # strings, and it blows up during parsing.
728 # So ensure we fix this retardation by converting view xml back to
730 if isinstance(fvg['arch'], unicode):
731 arch = fvg['arch'].encode('utf-8')
736 evaluation_context = session.evaluation_context(context or {})
737 xml = self.transform_view(arch, session, evaluation_context)
739 xml = ElementTree.fromstring(arch)
740 fvg['arch'] = Xml2Json.convert_element(xml)
742 for field in fvg['fields'].itervalues():
743 if field.get('views'):
744 for view in field["views"].itervalues():
745 self.process_view(session, view, None, transform)
746 if field.get('domain'):
747 field["domain"] = self.parse_domain(field["domain"], session)
748 if field.get('context'):
749 field["context"] = self.parse_context(field["context"], session)
751 @openerpweb.jsonrequest
752 def add_custom(self, request, view_id, arch):
753 CustomView = request.session.model('ir.ui.view.custom')
755 'user_id': request.session._uid,
758 }, request.session.eval_context(request.context))
759 return {'result': True}
761 @openerpweb.jsonrequest
762 def undo_custom(self, request, view_id, reset=False):
763 CustomView = request.session.model('ir.ui.view.custom')
764 context = request.session.eval_context(request.context)
765 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
766 0, False, False, context)
769 CustomView.unlink(vcustom, context)
771 CustomView.unlink([vcustom[0]], context)
772 return {'result': True}
773 return {'result': False}
775 def transform_view(self, view_string, session, context=None):
776 # transform nodes on the fly via iterparse, instead of
777 # doing it statically on the parsing result
778 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
780 for event, elem in parser:
784 self.parse_domains_and_contexts(elem, session)
787 def parse_domain(self, domain, session):
788 """ Parses an arbitrary string containing a domain, transforms it
789 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
791 :param domain: the domain to parse, if the domain is not a string it is assumed to
792 be a literal domain and is returned as-is
793 :param session: Current OpenERP session
794 :type session: openerpweb.openerpweb.OpenERPSession
796 if not isinstance(domain, (str, unicode)):
799 return openerpweb.ast.literal_eval(domain)
802 return openerpweb.nonliterals.Domain(session, domain)
804 def parse_context(self, context, session):
805 """ Parses an arbitrary string containing a context, transforms it
806 to either a literal context or a :class:`openerpweb.nonliterals.Context`
808 :param context: the context to parse, if the context is not a string it is assumed to
809 be a literal domain and is returned as-is
810 :param session: Current OpenERP session
811 :type session: openerpweb.openerpweb.OpenERPSession
813 if not isinstance(context, (str, unicode)):
816 return openerpweb.ast.literal_eval(context)
818 return openerpweb.nonliterals.Context(session, context)
820 def parse_domains_and_contexts(self, elem, session):
821 """ Converts domains and contexts from the view into Python objects,
822 either literals if they can be parsed by literal_eval or a special
823 placeholder object if the domain or context refers to free variables.
825 :param elem: the current node being parsed
826 :type param: xml.etree.ElementTree.Element
827 :param session: OpenERP session object, used to store and retrieve
829 :type session: openerpweb.openerpweb.OpenERPSession
831 for el in ['domain', 'filter_domain']:
832 domain = elem.get(el, '').strip()
834 elem.set(el, self.parse_domain(domain, session))
835 for el in ['context', 'default_get']:
836 context_string = elem.get(el, '').strip()
838 elem.set(el, self.parse_context(context_string, session))
840 class FormView(View):
841 _cp_path = "/base/formview"
843 @openerpweb.jsonrequest
844 def load(self, req, model, view_id, toolbar=False):
845 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
846 return {'fields_view': fields_view}
848 class ListView(View):
849 _cp_path = "/base/listview"
851 @openerpweb.jsonrequest
852 def load(self, req, model, view_id, toolbar=False):
853 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
854 return {'fields_view': fields_view}
856 def process_colors(self, view, row, context):
857 colors = view['arch']['attrs'].get('colors')
864 for pair in colors.split(';')
865 if eval(pair.split(':')[1], dict(context, **row))
870 elif len(color) == 1:
874 class SearchView(View):
875 _cp_path = "/base/searchview"
877 @openerpweb.jsonrequest
878 def load(self, req, model, view_id):
879 fields_view = self.fields_view_get(req, model, view_id, 'search')
880 return {'fields_view': fields_view}
882 @openerpweb.jsonrequest
883 def fields_get(self, req, model):
884 Model = req.session.model(model)
885 fields = Model.fields_get(False, req.session.eval_context(req.context))
886 for field in fields.values():
887 # shouldn't convert the views too?
888 if field.get('domain'):
889 field["domain"] = self.parse_domain(field["domain"], req.session)
890 if field.get('context'):
891 field["context"] = self.parse_domain(field["context"], req.session)
892 return {'fields': fields}
894 @openerpweb.jsonrequest
895 def get_filters(self, req, model):
896 Model = req.session.model("ir.filters")
897 filters = Model.get_filters(model)
898 for filter in filters:
899 filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session))
900 filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session))
903 @openerpweb.jsonrequest
904 def save_filter(self, req, model, name, context_to_save, domain):
905 Model = req.session.model("ir.filters")
906 ctx = openerpweb.nonliterals.CompoundContext(context_to_save)
907 ctx.session = req.session
909 domain = openerpweb.nonliterals.CompoundDomain(domain)
910 domain.session = req.session
911 domain = domain.evaluate()
912 uid = req.session._uid
913 context = req.session.eval_context(req.context)
914 to_return = Model.create_or_replace({"context": ctx,
922 class Binary(openerpweb.Controller):
923 _cp_path = "/base/binary"
925 @openerpweb.httprequest
926 def image(self, request, session_id, model, id, field, **kw):
927 cherrypy.response.headers['Content-Type'] = 'image/png'
928 Model = request.session.model(model)
929 context = request.session.eval_context(request.context)
932 res = Model.default_get([field], context).get(field, '')
934 res = Model.read([int(id)], [field], context)[0].get(field, '')
935 return base64.decodestring(res)
936 except: # TODO: what's the exception here?
937 return self.placeholder()
938 def placeholder(self):
939 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
941 @openerpweb.httprequest
942 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
943 Model = request.session.model(model)
944 context = request.session.eval_context(request.context)
945 res = Model.read([int(id)], [field, fieldname], context)[0]
946 filecontent = res.get(field, '')
948 raise cherrypy.NotFound
950 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
951 filename = '%s_%s' % (model.replace('.', '_'), id)
953 filename = res.get(fieldname, '') or filename
954 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
955 return base64.decodestring(filecontent)
957 @openerpweb.httprequest
958 def upload(self, request, session_id, callback, ufile=None):
959 cherrypy.response.timeout = 500
961 for key, val in cherrypy.request.headers.iteritems():
962 headers[key.lower()] = val
963 size = int(headers.get('content-length', 0))
964 # TODO: might be useful to have a configuration flag for max-length file uploads
966 out = """<script language="javascript" type="text/javascript">
967 var win = window.top.window,
969 if (typeof(callback) === 'function') {
970 callback.apply(this, %s);
972 win.jQuery('#oe_notification', win.document).notify('create', {
973 title: "Ajax File Upload",
974 text: "Could not find callback"
978 data = ufile.file.read()
979 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
981 args = [False, e.message]
982 return out % (simplejson.dumps(callback), simplejson.dumps(args))
984 @openerpweb.httprequest
985 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
986 cherrypy.response.timeout = 500
987 context = request.session.eval_context(request.context)
988 Model = request.session.model('ir.attachment')
990 out = """<script language="javascript" type="text/javascript">
991 var win = window.top.window,
993 if (typeof(callback) === 'function') {
994 callback.call(this, %s);
997 attachment_id = Model.create({
998 'name': ufile.filename,
999 'datas': base64.encodestring(ufile.file.read()),
1004 'filename': ufile.filename,
1007 except Exception, e:
1008 args = { 'error': e.message }
1009 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1011 class Action(openerpweb.Controller):
1012 _cp_path = "/base/action"
1014 @openerpweb.jsonrequest
1015 def load(self, req, action_id):
1016 Actions = req.session.model('ir.actions.actions')
1018 context = req.session.eval_context(req.context)
1019 action_type = Actions.read([action_id], ['type'], context)
1021 action = req.session.model(action_type[0]['type']).read([action_id], False,
1024 value = clean_action(action[0], req.session)
1025 return {'result': value}
1027 @openerpweb.jsonrequest
1028 def run(self, req, action_id):
1029 return clean_action(req.session.model('ir.actions.server').run(
1030 [action_id], req.session.eval_context(req.context)), req.session)
1032 class TreeView(View):
1033 _cp_path = "/base/treeview"
1035 @openerpweb.jsonrequest
1036 def load(self, req, model, view_id, toolbar=False):
1037 return self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
1039 @openerpweb.jsonrequest
1040 def action(self, req, model, id):
1041 return load_actions_from_ir_values(
1042 req,'action', 'tree_but_open',[(model, id)],
1043 False, req.session.eval_context(req.context))
1045 def export_csv(fields, result):
1047 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1049 writer.writerow(fields)
1054 if isinstance(d, basestring):
1055 d = d.replace('\n',' ').replace('\t',' ')
1057 d = d.encode('utf-8')
1060 if d is False: d = None
1062 writer.writerow(row)
1069 def export_xls(fieldnames, table):
1073 common.error(_('Import Error.'), _('Please install xlwt library to export to MS Excel.'))
1075 workbook = xlwt.Workbook()
1076 worksheet = workbook.add_sheet('Sheet 1')
1078 for i, fieldname in enumerate(fieldnames):
1079 worksheet.write(0, i, str(fieldname))
1080 worksheet.col(i).width = 8000 # around 220 pixels
1082 style = xlwt.easyxf('align: wrap yes')
1084 for row_index, row in enumerate(table):
1085 for cell_index, cell_value in enumerate(row):
1086 cell_value = str(cell_value)
1087 cell_value = re.sub("\r", " ", cell_value)
1088 worksheet.write(row_index + 1, cell_index, cell_value, style)
1096 #return data.decode('ISO-8859-1')
1097 return unicode(data, 'utf-8', 'replace')
1100 _cp_path = "/base/export"
1102 def fields_get(self, req, model):
1103 Model = req.session.model(model)
1104 fields = Model.fields_get(False, req.session.eval_context(req.context))
1107 @openerpweb.jsonrequest
1108 def get_fields(self, req, model, prefix='', name= '', field_parent=None, params={}):
1109 import_compat = params.get("import_compat", False)
1111 fields = self.fields_get(req, model)
1112 field_parent_type = params.get("parent_field_type",False)
1114 if import_compat and field_parent_type and field_parent_type == "many2one":
1117 fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1119 fields_order = fields.keys()
1120 fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1122 for index, field in enumerate(fields_order):
1123 value = fields[field]
1125 if import_compat and value.get('readonly', False):
1127 for sl in value.get('states', {}).values():
1129 ok = ok or (s==['readonly',False])
1132 id = prefix + (prefix and '/'or '') + field
1133 nm = name + (name and '/' or '') + value['string']
1134 record.update(id=id, string= nm, action='javascript: void(0)',
1135 target=None, icon=None, children=[], field_type=value.get('type',False), required=value.get('required', False))
1136 records.append(record)
1138 if len(nm.split('/')) < 3 and value.get('relation', False):
1140 ref = value.pop('relation')
1141 cfields = self.fields_get(req, ref)
1142 if (value['type'] == 'many2many'):
1143 record['children'] = []
1144 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1146 elif value['type'] == 'many2one':
1147 record['children'] = [id + '/id', id + '/.id']
1148 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1151 cfields_order = cfields.keys()
1152 cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1154 for j, fld in enumerate(cfields_order):
1155 cid = id + '/' + fld
1156 cid = cid.replace(' ', '_')
1157 children.append(cid)
1158 record['children'] = children or []
1159 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1161 ref = value.pop('relation')
1162 cfields = self.fields_get(req, ref)
1163 cfields_order = cfields.keys()
1164 cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1166 for j, fld in enumerate(cfields_order):
1167 cid = id + '/' + fld
1168 cid = cid.replace(' ', '_')
1169 children.append(cid)
1170 record['children'] = children or []
1171 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1176 @openerpweb.jsonrequest
1177 def save_export_lists(self, req, name, model, field_list):
1178 result = {'resource':model, 'name':name, 'export_fields': []}
1179 for field in field_list:
1180 result['export_fields'].append((0, 0, {'name': field}))
1181 return req.session.model("ir.exports").create(result, req.session.eval_context(req.context))
1183 @openerpweb.jsonrequest
1184 def exist_export_lists(self, req, model):
1185 export_model = req.session.model("ir.exports")
1186 return export_model.read(export_model.search([('resource', '=', model)]), ['name'])
1188 @openerpweb.jsonrequest
1189 def delete_export(self, req, export_id):
1190 req.session.model("ir.exports").unlink(export_id, req.session.eval_context(req.context))
1193 @openerpweb.jsonrequest
1194 def namelist(self,req, model, export_id):
1196 result = self.get_data(req, model, req.session.eval_context(req.context))
1197 ir_export_obj = req.session.model("ir.exports")
1198 ir_export_line_obj = req.session.model("ir.exports.line")
1200 field = ir_export_obj.read(export_id)
1201 fields = ir_export_line_obj.read(field['export_fields'])
1204 [name_list.update({field['name']: result.get(field['name'])}) for field in fields]
1207 def get_data(self, req, model, context=None):
1209 context = context or {}
1211 proxy = req.session.model(model)
1212 fields = self.fields_get(req, model)
1214 f1 = proxy.fields_view_get(False, 'tree', context)['fields']
1215 f2 = proxy.fields_view_get(False, 'form', context)['fields']
1219 fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1222 _fields = {'id': 'ID' , '.id': 'Database ID' }
1223 def model_populate(fields, prefix_node='', prefix=None, prefix_value='', level=2):
1224 fields_order = fields.keys()
1225 fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1227 for field in fields_order:
1228 fields_data[prefix_node+field] = fields[field]
1230 fields_data[prefix_node + field]['string'] = '%s%s' % (prefix_value, fields_data[prefix_node + field]['string'])
1231 st_name = fields[field]['string'] or field
1232 _fields[prefix_node+field] = st_name
1233 if fields[field].get('relation', False) and level>0:
1234 fields2 = self.fields_get(req, fields[field]['relation'])
1235 fields2.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1236 model_populate(fields2, prefix_node+field+'/', None, st_name+'/', level-1)
1237 model_populate(fields)
1241 @openerpweb.jsonrequest
1242 def export_data(self, req, model, fields, ids, domain, import_compat=False, export_format="csv", context=None):
1243 context = req.session.eval_context(req.context)
1244 modle_obj = req.session.model(model)
1245 ids = ids or modle_obj.search(domain, context=context)
1247 field = fields.keys()
1248 result = modle_obj.export_data(ids, field , context).get('datas',[])
1250 if not import_compat:
1251 field = [val.strip() for val in fields.values()]
1253 if export_format == 'xls':
1254 return export_xls(field, result)
1256 return export_csv(field, result)