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))
410 for id, name, action in actions]
412 def clean_action(action, session):
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()) 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)
578 context, domain = eval_context_and_domain(
579 request.session, request.context, domain)
581 ids = Model.search(domain, 0, False, sort or False, context)
582 # need to fill the dataset with all ids for the (domain, context) pair,
583 # so search un-paginated and paginate manually before reading
584 paginated_ids = ids[offset:(offset + limit if limit else None)]
585 if fields and fields == ['id']:
586 # shortcut read if we only want the ids
589 'records': map(lambda id: {'id': id}, paginated_ids)
592 records = Model.read(paginated_ids, fields or False, context)
593 records.sort(key=lambda obj: ids.index(obj['id']))
600 @openerpweb.jsonrequest
601 def get(self, request, model, ids, fields=False):
602 return self.do_get(request, model, ids, fields)
603 def do_get(self, request, model, ids, fields=False):
604 """ Fetches and returns the records of the model ``model`` whose ids
607 The results are in the same order as the inputs, but elements may be
608 missing (if there is no record left for the id)
610 :param request: the JSON-RPC2 request object
611 :type request: openerpweb.JsonRequest
612 :param model: the model to read from
614 :param ids: a list of identifiers
616 :param fields: a list of fields to fetch, ``False`` or empty to fetch
617 all fields in the model
618 :type fields: list | False
619 :returns: a list of records, in the same order as the list of ids
622 Model = request.session.model(model)
623 records = Model.read(ids, fields, request.session.eval_context(request.context))
625 record_map = dict((record['id'], record) for record in records)
627 return [record_map[id] for id in ids if record_map.get(id)]
629 @openerpweb.jsonrequest
630 def load(self, req, model, id, fields):
631 m = req.session.model(model)
633 r = m.read([id], False, req.session.eval_context(req.context))
636 return {'value': value}
638 @openerpweb.jsonrequest
639 def create(self, req, model, data):
640 m = req.session.model(model)
641 r = m.create(data, req.session.eval_context(req.context))
644 @openerpweb.jsonrequest
645 def save(self, req, model, id, data):
646 m = req.session.model(model)
647 r = m.write([id], data, req.session.eval_context(req.context))
650 @openerpweb.jsonrequest
651 def unlink(self, request, model, ids=()):
652 Model = request.session.model(model)
653 return Model.unlink(ids, request.session.eval_context(request.context))
655 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
656 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
657 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
658 c, d = eval_context_and_domain(req.session, context, domain)
659 if domain_id and len(args) - 1 >= domain_id:
661 if context_id and len(args) - 1 >= context_id:
664 return getattr(req.session.model(model), method)(*args)
666 @openerpweb.jsonrequest
667 def call(self, req, model, method, args, domain_id=None, context_id=None):
668 return self.call_common(req, model, method, args, domain_id, context_id)
670 @openerpweb.jsonrequest
671 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
672 action = self.call_common(req, model, method, args, domain_id, context_id)
673 if isinstance(action, dict) and action.get('type') != '':
674 return {'result': clean_action(action, req.session)}
675 return {'result': False}
677 @openerpweb.jsonrequest
678 def exec_workflow(self, req, model, id, signal):
679 r = req.session.exec_workflow(model, id, signal)
682 @openerpweb.jsonrequest
683 def default_get(self, req, model, fields):
684 Model = req.session.model(model)
685 return Model.default_get(fields, req.session.eval_context(req.context))
687 class DataGroup(openerpweb.Controller):
688 _cp_path = "/base/group"
689 @openerpweb.jsonrequest
690 def read(self, request, model, fields, group_by_fields, domain=None, sort=None):
691 Model = request.session.model(model)
692 context, domain = eval_context_and_domain(request.session, request.context, domain)
694 return Model.read_group(
695 domain or [], fields, group_by_fields, 0, False,
696 dict(context, group_by=group_by_fields), sort or False)
698 class View(openerpweb.Controller):
699 _cp_path = "/base/view"
701 def fields_view_get(self, request, model, view_id, view_type,
702 transform=True, toolbar=False, submenu=False):
703 Model = request.session.model(model)
704 context = request.session.eval_context(request.context)
705 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
706 # todo fme?: check that we should pass the evaluated context here
707 self.process_view(request.session, fvg, context, transform)
710 def process_view(self, session, fvg, context, transform):
711 # depending on how it feels, xmlrpclib.ServerProxy can translate
712 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
713 # enjoy unicode strings which can not be trivially converted to
714 # strings, and it blows up during parsing.
716 # So ensure we fix this retardation by converting view xml back to
718 if isinstance(fvg['arch'], unicode):
719 arch = fvg['arch'].encode('utf-8')
724 evaluation_context = session.evaluation_context(context or {})
725 xml = self.transform_view(arch, session, evaluation_context)
727 xml = ElementTree.fromstring(arch)
728 fvg['arch'] = Xml2Json.convert_element(xml)
730 for field in fvg['fields'].itervalues():
731 if field.get('views'):
732 for view in field["views"].itervalues():
733 self.process_view(session, view, None, transform)
734 if field.get('domain'):
735 field["domain"] = self.parse_domain(field["domain"], session)
736 if field.get('context'):
737 field["context"] = self.parse_context(field["context"], session)
739 @openerpweb.jsonrequest
740 def add_custom(self, request, view_id, arch):
741 CustomView = request.session.model('ir.ui.view.custom')
743 'user_id': request.session._uid,
746 }, request.session.eval_context(request.context))
747 return {'result': True}
749 @openerpweb.jsonrequest
750 def undo_custom(self, request, view_id, reset=False):
751 CustomView = request.session.model('ir.ui.view.custom')
752 context = request.session.eval_context(request.context)
753 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
754 0, False, False, context)
757 CustomView.unlink(vcustom, context)
759 CustomView.unlink([vcustom[0]], context)
760 return {'result': True}
761 return {'result': False}
763 def transform_view(self, view_string, session, context=None):
764 # transform nodes on the fly via iterparse, instead of
765 # doing it statically on the parsing result
766 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
768 for event, elem in parser:
772 self.parse_domains_and_contexts(elem, session)
775 def parse_domain(self, domain, session):
776 """ Parses an arbitrary string containing a domain, transforms it
777 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
779 :param domain: the domain to parse, if the domain is not a string it is assumed to
780 be a literal domain and is returned as-is
781 :param session: Current OpenERP session
782 :type session: openerpweb.openerpweb.OpenERPSession
784 if not isinstance(domain, (str, unicode)):
787 return openerpweb.ast.literal_eval(domain)
790 return openerpweb.nonliterals.Domain(session, domain)
792 def parse_context(self, context, session):
793 """ Parses an arbitrary string containing a context, transforms it
794 to either a literal context or a :class:`openerpweb.nonliterals.Context`
796 :param context: the context to parse, if the context is not a string it is assumed to
797 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(context, (str, unicode)):
804 return openerpweb.ast.literal_eval(context)
806 return openerpweb.nonliterals.Context(session, context)
808 def parse_domains_and_contexts(self, elem, session):
809 """ Converts domains and contexts from the view into Python objects,
810 either literals if they can be parsed by literal_eval or a special
811 placeholder object if the domain or context refers to free variables.
813 :param elem: the current node being parsed
814 :type param: xml.etree.ElementTree.Element
815 :param session: OpenERP session object, used to store and retrieve
817 :type session: openerpweb.openerpweb.OpenERPSession
819 for el in ['domain', 'filter_domain']:
820 domain = elem.get(el, '').strip()
822 elem.set(el, self.parse_domain(domain, session))
823 for el in ['context', 'default_get']:
824 context_string = elem.get(el, '').strip()
826 elem.set(el, self.parse_context(context_string, session))
828 class FormView(View):
829 _cp_path = "/base/formview"
831 @openerpweb.jsonrequest
832 def load(self, req, model, view_id, toolbar=False):
833 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
834 return {'fields_view': fields_view}
836 class ListView(View):
837 _cp_path = "/base/listview"
839 @openerpweb.jsonrequest
840 def load(self, req, model, view_id, toolbar=False):
841 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
842 return {'fields_view': fields_view}
844 def process_colors(self, view, row, context):
845 colors = view['arch']['attrs'].get('colors')
852 for pair in colors.split(';')
853 if eval(pair.split(':')[1], dict(context, **row))
858 elif len(color) == 1:
862 class SearchView(View):
863 _cp_path = "/base/searchview"
865 @openerpweb.jsonrequest
866 def load(self, req, model, view_id):
867 fields_view = self.fields_view_get(req, model, view_id, 'search')
868 return {'fields_view': fields_view}
870 @openerpweb.jsonrequest
871 def fields_get(self, req, model):
872 Model = req.session.model(model)
873 fields = Model.fields_get(False, req.session.eval_context(req.context))
874 for field in fields.values():
875 # shouldn't convert the views too?
876 if field.get('domain'):
877 field["domain"] = self.parse_domain(field["domain"], req.session)
878 if field.get('context'):
879 field["context"] = self.parse_domain(field["context"], req.session)
880 return {'fields': fields}
882 @openerpweb.jsonrequest
883 def get_filters(self, req, model):
884 Model = req.session.model("ir.filters")
885 filters = Model.get_filters(model)
886 for filter in filters:
887 filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session))
888 filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session))
891 @openerpweb.jsonrequest
892 def save_filter(self, req, model, name, context_to_save, domain):
893 Model = req.session.model("ir.filters")
894 ctx = openerpweb.nonliterals.CompoundContext(context_to_save)
895 ctx.session = req.session
897 domain = openerpweb.nonliterals.CompoundDomain(domain)
898 domain.session = req.session
899 domain = domain.evaluate()
900 uid = req.session._uid
901 context = req.session.eval_context(req.context)
902 to_return = Model.create_or_replace({"context": ctx,
910 class Binary(openerpweb.Controller):
911 _cp_path = "/base/binary"
913 @openerpweb.httprequest
914 def image(self, request, session_id, model, id, field, **kw):
915 cherrypy.response.headers['Content-Type'] = 'image/png'
916 Model = request.session.model(model)
917 context = request.session.eval_context(request.context)
920 res = Model.default_get([field], context).get(field, '')
922 res = Model.read([int(id)], [field], context)[0].get(field, '')
923 return base64.decodestring(res)
924 except: # TODO: what's the exception here?
925 return self.placeholder()
926 def placeholder(self):
927 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
929 @openerpweb.httprequest
930 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
931 Model = request.session.model(model)
932 context = request.session.eval_context(request.context)
933 res = Model.read([int(id)], [field, fieldname], context)[0]
934 filecontent = res.get(field, '')
936 raise cherrypy.NotFound
938 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
939 filename = '%s_%s' % (model.replace('.', '_'), id)
941 filename = res.get(fieldname, '') or filename
942 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
943 return base64.decodestring(filecontent)
945 @openerpweb.httprequest
946 def upload(self, request, session_id, callback, ufile=None):
947 cherrypy.response.timeout = 500
949 for key, val in cherrypy.request.headers.iteritems():
950 headers[key.lower()] = val
951 size = int(headers.get('content-length', 0))
952 # TODO: might be useful to have a configuration flag for max-length file uploads
954 out = """<script language="javascript" type="text/javascript">
955 var win = window.top.window,
957 if (typeof(callback) === 'function') {
958 callback.apply(this, %s);
960 win.jQuery('#oe_notification', win.document).notify('create', {
961 title: "Ajax File Upload",
962 text: "Could not find callback"
966 data = ufile.file.read()
967 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
969 args = [False, e.message]
970 return out % (simplejson.dumps(callback), simplejson.dumps(args))
972 @openerpweb.httprequest
973 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
974 cherrypy.response.timeout = 500
975 context = request.session.eval_context(request.context)
976 Model = request.session.model('ir.attachment')
978 out = """<script language="javascript" type="text/javascript">
979 var win = window.top.window,
981 if (typeof(callback) === 'function') {
982 callback.call(this, %s);
985 attachment_id = Model.create({
986 'name': ufile.filename,
987 'datas': base64.encodestring(ufile.file.read()),
992 'filename': ufile.filename,
996 args = { 'error': e.message }
997 return out % (simplejson.dumps(callback), simplejson.dumps(args))
999 class Action(openerpweb.Controller):
1000 _cp_path = "/base/action"
1002 @openerpweb.jsonrequest
1003 def load(self, req, action_id):
1004 Actions = req.session.model('ir.actions.actions')
1006 context = req.session.eval_context(req.context)
1007 action_type = Actions.read([action_id], ['type'], context)
1009 action = req.session.model(action_type[0]['type']).read([action_id], False,
1012 value = clean_action(action[0], req.session)
1013 return {'result': value}
1015 @openerpweb.jsonrequest
1016 def run(self, req, action_id):
1017 return clean_action(req.session.model('ir.actions.server').run(
1018 [action_id], req.session.eval_context(req.context)), req.session)
1020 def export_csv(fields, result):
1022 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1024 writer.writerow(fields)
1029 if isinstance(d, basestring):
1030 d = d.replace('\n',' ').replace('\t',' ')
1032 d = d.encode('utf-8')
1035 if d is False: d = None
1037 writer.writerow(row)
1044 def export_xls(fieldnames, table):
1048 common.error(_('Import Error.'), _('Please install xlwt library to export to MS Excel.'))
1050 workbook = xlwt.Workbook()
1051 worksheet = workbook.add_sheet('Sheet 1')
1053 for i, fieldname in enumerate(fieldnames):
1054 worksheet.write(0, i, str(fieldname))
1055 worksheet.col(i).width = 8000 # around 220 pixels
1057 style = xlwt.easyxf('align: wrap yes')
1059 for row_index, row in enumerate(table):
1060 for cell_index, cell_value in enumerate(row):
1061 cell_value = str(cell_value)
1062 cell_value = re.sub("\r", " ", cell_value)
1063 worksheet.write(row_index + 1, cell_index, cell_value, style)
1071 #return data.decode('ISO-8859-1')
1072 return unicode(data, 'utf-8', 'replace')
1075 _cp_path = "/base/export"
1077 def fields_get(self, req, model):
1078 Model = req.session.model(model)
1079 fields = Model.fields_get(False, req.session.eval_context(req.context))
1082 @openerpweb.jsonrequest
1083 def get_fields(self, req, model, prefix='', name= '', field_parent=None, params={}):
1084 import_compat = params.get("import_compat", False)
1086 fields = self.fields_get(req, model)
1087 field_parent_type = params.get("parent_field_type",False)
1089 if import_compat and field_parent_type and field_parent_type == "many2one":
1092 fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1094 fields_order = fields.keys()
1095 fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1097 for index, field in enumerate(fields_order):
1098 value = fields[field]
1100 if import_compat and value.get('readonly', False):
1102 for sl in value.get('states', {}).values():
1104 ok = ok or (s==['readonly',False])
1107 id = prefix + (prefix and '/'or '') + field
1108 nm = name + (name and '/' or '') + value['string']
1109 record.update(id=id, string= nm, action='javascript: void(0)',
1110 target=None, icon=None, children=[], field_type=value.get('type',False), required=value.get('required', False))
1111 records.append(record)
1113 if len(nm.split('/')) < 3 and value.get('relation', False):
1115 ref = value.pop('relation')
1116 cfields = self.fields_get(req, ref)
1117 if (value['type'] == 'many2many'):
1118 record['children'] = []
1119 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1121 elif value['type'] == 'many2one':
1122 record['children'] = [id + '/id', id + '/.id']
1123 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1126 cfields_order = cfields.keys()
1127 cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1129 for j, fld in enumerate(cfields_order):
1130 cid = id + '/' + fld
1131 cid = cid.replace(' ', '_')
1132 children.append(cid)
1133 record['children'] = children or []
1134 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1136 ref = value.pop('relation')
1137 cfields = self.fields_get(req, ref)
1138 cfields_order = cfields.keys()
1139 cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1141 for j, fld in enumerate(cfields_order):
1142 cid = id + '/' + fld
1143 cid = cid.replace(' ', '_')
1144 children.append(cid)
1145 record['children'] = children or []
1146 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1151 @openerpweb.jsonrequest
1152 def save_export_lists(self, req, name, model, field_list):
1153 result = {'resource':model, 'name':name, 'export_fields': []}
1154 for field in field_list:
1155 result['export_fields'].append((0, 0, {'name': field}))
1156 return req.session.model("ir.exports").create(result, req.session.eval_context(req.context))
1158 @openerpweb.jsonrequest
1159 def exist_export_lists(self, req, model):
1160 export_model = req.session.model("ir.exports")
1161 return export_model.read(export_model.search([('resource', '=', model)]), ['name'])
1163 @openerpweb.jsonrequest
1164 def delete_export(self, req, export_id):
1165 req.session.model("ir.exports").unlink(export_id, req.session.eval_context(req.context))
1168 @openerpweb.jsonrequest
1169 def namelist(self,req, model, export_id):
1171 result = self.get_data(req, model, req.session.eval_context(req.context))
1172 ir_export_obj = req.session.model("ir.exports")
1173 ir_export_line_obj = req.session.model("ir.exports.line")
1175 field = ir_export_obj.read(export_id)
1176 fields = ir_export_line_obj.read(field['export_fields'])
1179 [name_list.update({field['name']: result.get(field['name'])}) for field in fields]
1182 def get_data(self, req, model, context=None):
1184 context = context or {}
1186 proxy = req.session.model(model)
1187 fields = self.fields_get(req, model)
1189 f1 = proxy.fields_view_get(False, 'tree', context)['fields']
1190 f2 = proxy.fields_view_get(False, 'form', context)['fields']
1194 fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1197 _fields = {'id': 'ID' , '.id': 'Database ID' }
1198 def model_populate(fields, prefix_node='', prefix=None, prefix_value='', level=2):
1199 fields_order = fields.keys()
1200 fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1202 for field in fields_order:
1203 fields_data[prefix_node+field] = fields[field]
1205 fields_data[prefix_node + field]['string'] = '%s%s' % (prefix_value, fields_data[prefix_node + field]['string'])
1206 st_name = fields[field]['string'] or field
1207 _fields[prefix_node+field] = st_name
1208 if fields[field].get('relation', False) and level>0:
1209 fields2 = self.fields_get(req, fields[field]['relation'])
1210 fields2.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1211 model_populate(fields2, prefix_node+field+'/', None, st_name+'/', level-1)
1212 model_populate(fields)
1216 @openerpweb.jsonrequest
1217 def export_data(self, req, model, fields, ids, domain, import_compat=False, export_format="csv", context=None):
1218 context = req.session.eval_context(req.context)
1219 modle_obj = req.session.model(model)
1220 ids = ids or modle_obj.search(domain, context=context)
1222 field = fields.keys()
1223 result = modle_obj.export_data(ids, field , context).get('datas',[])
1225 if not import_compat:
1226 field = [val.strip() for val in fields.values()]
1228 if export_format == 'xls':
1229 return export_xls(field, result)
1231 return export_csv(field, result)