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, [])
70 for pattern in globlist:
71 for path in glob.glob(os.path.join(openerpweb.path_addons, addon, pattern)):
72 files.append(path[len(openerpweb.path_addons):])
75 def concat_files(file_list):
76 """ Concatenate file content
77 return (concat,timestamp)
78 concat: concatenation of file content
79 timestamp: max(os.path.getmtime of file_list)
84 fname = os.path.join(openerpweb.path_addons, i[1:])
85 ftime = os.path.getmtime(fname)
86 if ftime > files_timestamp:
87 files_timestamp = ftime
88 files_content.append(open(fname).read())
89 files_concat = "".join(files_content)
90 return (files_concat,files_timestamp)
92 home_template = textwrap.dedent("""<!DOCTYPE html>
93 <html style="height: 100%%">
95 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
96 <title>OpenERP</title>
97 <link rel="shortcut icon" href="/base/static/src/img/favicon.ico" type="image/x-icon"/>
100 <link rel="stylesheet" href="/base/static/src/css/base-ie7.css" type="text/css"/>
103 <script type="text/javascript">
105 QWeb = new QWeb2.Engine();
106 var c = new openerp.init();
107 var wc = new c.base.WebClient("oe");
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 req.httpresponse.headers['Content-Type'] = 'text/css'
129 files = manifest_glob(mods.split(','), 'css')
130 content,timestamp = concat_files(files)
131 # TODO request set the Date of last modif and Etag
134 @openerpweb.httprequest
135 def js(self, req, mods='base'):
136 req.httpresponse.headers['Content-Type'] = 'application/javascript'
137 files = manifest_glob(mods.split(','), 'js')
138 content,timestamp = concat_files(files)
139 # TODO request set the Date of last modif and Etag
142 @openerpweb.httprequest
143 def home(self, req, s_action=None):
146 jslist = ['/base/webclient/js']
148 jslist = manifest_glob(['base'], 'js')
149 js = "\n ".join(['<script type="text/javascript" src="%s"></script>'%i for i in jslist])
152 csslist = ['/base/webclient/css']
154 csslist = manifest_glob(['base'], 'css')
155 css = "\n ".join(['<link rel="stylesheet" href="%s">'%i for i in csslist])
156 r = home_template % {
162 @openerpweb.jsonrequest
163 def translations(self, req, mods, lang):
164 lang_model = req.session.model('res.lang')
165 ids = lang_model.search([("code", "=", lang)])
167 lang_obj = lang_model.read(ids[0], ["direction", "date_format", "time_format",
168 "grouping", "decimal_point", "thousands_sep"])
172 if lang.count("_") > 0:
176 langs = lang.split(separator)
177 langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
180 for addon_name in mods:
181 transl = {"messages":[]}
182 transs[addon_name] = transl
184 f_name = os.path.join(openerpweb.path_addons, addon_name, "po", l + ".po")
185 if not os.path.exists(f_name):
188 with open(f_name) as t_file:
193 if x.id and x.string:
194 transl["messages"].append({'id': x.id, 'string': x.string})
195 return {"modules": transs,
196 "lang_parameters": lang_obj}
198 class Database(openerpweb.Controller):
199 _cp_path = "/base/database"
201 @openerpweb.jsonrequest
202 def get_list(self, req):
203 proxy = req.session.proxy("db")
205 h = req.httprequest.headers['Host'].split(':')[0]
207 r = cherrypy.config['openerp.dbfilter'].replace('%h', h).replace('%d', d)
208 dbs = [i for i in dbs if re.match(r, i)]
209 return {"db_list": dbs}
211 @openerpweb.jsonrequest
212 def progress(self, req, password, id):
213 return req.session.proxy('db').get_progress(password, id)
215 @openerpweb.jsonrequest
216 def create(self, req, fields):
218 params = dict(map(operator.itemgetter('name', 'value'), fields))
220 params['super_admin_pwd'],
222 bool(params.get('demo_data')),
224 params['create_admin_pwd']
228 return req.session.proxy("db").create(*create_attrs)
229 except xmlrpclib.Fault, e:
230 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
231 return {'error': e.faultCode, 'title': 'Create Database'}
232 return {'error': 'Could not create database !', 'title': 'Create Database'}
234 @openerpweb.jsonrequest
235 def drop(self, req, fields):
236 password, db = operator.itemgetter(
237 'drop_pwd', 'drop_db')(
238 dict(map(operator.itemgetter('name', 'value'), fields)))
241 return req.session.proxy("db").drop(password, db)
242 except xmlrpclib.Fault, e:
243 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
244 return {'error': e.faultCode, 'title': 'Drop Database'}
245 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
247 @openerpweb.httprequest
248 def backup(self, req, backup_db, backup_pwd, token):
250 db_dump = base64.decodestring(
251 req.session.proxy("db").dump(backup_pwd, backup_db))
252 req.httpresponse.headers['Content-Type'] = "application/octet-stream; charset=binary"
253 req.httpresponse.headers['Content-Disposition'] = 'attachment; filename="' + backup_db + '.dump"'
254 req.httpresponse.cookie['fileToken'] = token
255 req.httpresponse.cookie['fileToken']['path'] = '/'
257 except xmlrpclib.Fault, e:
258 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
259 return 'Backup Database|' + e.faultCode
260 return 'Backup Database|Could not generate database backup'
262 @openerpweb.httprequest
263 def restore(self, req, db_file, restore_pwd, new_db):
265 data = base64.encodestring(db_file.file.read())
266 req.session.proxy("db").restore(restore_pwd, new_db, data)
268 except xmlrpclib.Fault, e:
269 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
270 raise Exception("AccessDenied")
272 @openerpweb.jsonrequest
273 def change_password(self, req, fields):
274 old_password, new_password = operator.itemgetter(
275 'old_pwd', 'new_pwd')(
276 dict(map(operator.itemgetter('name', 'value'), fields)))
278 return req.session.proxy("db").change_admin_password(old_password, new_password)
279 except xmlrpclib.Fault, e:
280 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
281 return {'error': e.faultCode, 'title': 'Change Password'}
282 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
284 class Session(openerpweb.Controller):
285 _cp_path = "/base/session"
287 @openerpweb.jsonrequest
288 def login(self, req, db, login, password):
289 req.session.login(db, login, password)
290 ctx = req.session.get_context()
293 "session_id": req.session_id,
294 "uid": req.session._uid,
298 @openerpweb.jsonrequest
299 def sc_list(self, req):
300 return req.session.model('ir.ui.view_sc').get_sc(
301 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
303 @openerpweb.jsonrequest
304 def get_lang_list(self, req):
307 'lang_list': (req.session.proxy("db").list_lang() or []),
311 return {"error": e, "title": "Languages"}
313 @openerpweb.jsonrequest
314 def modules(self, req):
315 # TODO query server for installed web modules
317 for name, manifest in openerpweb.addons_manifest.items():
318 if name != 'base' and manifest.get('active', True):
322 @openerpweb.jsonrequest
323 def eval_domain_and_context(self, req, contexts, domains,
325 """ Evaluates sequences of domains and contexts, composing them into
326 a single context, domain or group_by sequence.
328 :param list contexts: list of contexts to merge together. Contexts are
329 evaluated in sequence, all previous contexts
330 are part of their own evaluation context
331 (starting at the session context).
332 :param list domains: list of domains to merge together. Domains are
333 evaluated in sequence and appended to one another
334 (implicit AND), their evaluation domain is the
335 result of merging all contexts.
336 :param list group_by_seq: list of domains (which may be in a different
337 order than the ``contexts`` parameter),
338 evaluated in sequence, their ``'group_by'``
339 key is extracted if they have one.
344 the global context created by merging all of
348 the concatenation of all domains
351 a list of fields to group by, potentially empty (in which case
352 no group by should be performed)
354 context, domain = eval_context_and_domain(req.session,
355 openerpweb.nonliterals.CompoundContext(*(contexts or [])),
356 openerpweb.nonliterals.CompoundDomain(*(domains or [])))
358 group_by_sequence = []
359 for candidate in (group_by_seq or []):
360 ctx = req.session.eval_context(candidate, context)
361 group_by = ctx.get('group_by')
364 elif isinstance(group_by, basestring):
365 group_by_sequence.append(group_by)
367 group_by_sequence.extend(group_by)
372 'group_by': group_by_sequence
375 @openerpweb.jsonrequest
376 def save_session_action(self, req, the_action):
378 This method store an action object in the session object and returns an integer
379 identifying that action. The method get_session_action() can be used to get
382 :param the_action: The action to save in the session.
383 :type the_action: anything
384 :return: A key identifying the saved action.
387 saved_actions = req.httpsession.get('saved_actions')
388 if not saved_actions:
389 saved_actions = {"next":0, "actions":{}}
390 req.httpsession['saved_actions'] = saved_actions
391 # we don't allow more than 10 stored actions
392 if len(saved_actions["actions"]) >= 10:
393 del saved_actions["actions"][min(saved_actions["actions"].keys())]
394 key = saved_actions["next"]
395 saved_actions["actions"][key] = the_action
396 saved_actions["next"] = key + 1
399 @openerpweb.jsonrequest
400 def get_session_action(self, req, key):
402 Gets back a previously saved action. This method can return None if the action
403 was saved since too much time (this case should be handled in a smart way).
405 :param key: The key given by save_session_action()
407 :return: The saved action or None.
410 saved_actions = req.httpsession.get('saved_actions')
411 if not saved_actions:
413 return saved_actions["actions"].get(key)
415 @openerpweb.jsonrequest
416 def check(self, req):
417 req.session.assert_valid()
420 def eval_context_and_domain(session, context, domain=None):
421 e_context = session.eval_context(context)
422 # should we give the evaluated context as an evaluation context to the domain?
423 e_domain = session.eval_domain(domain or [])
425 return e_context, e_domain
427 def load_actions_from_ir_values(req, key, key2, models, meta):
428 context = req.session.eval_context(req.context)
429 Values = req.session.model('ir.values')
430 actions = Values.get(key, key2, models, meta, context)
432 return [(id, name, clean_action(req, action))
433 for id, name, action in actions]
435 def clean_action(req, action):
436 context = req.session.eval_context(req.context)
437 eval_ctx = req.session.evaluation_context(context)
438 action.setdefault('flags', {})
440 # values come from the server, we can just eval them
441 if isinstance(action.get('context'), basestring):
442 action['context'] = eval( action['context'], eval_ctx ) or {}
444 if isinstance(action.get('domain'), basestring):
445 action['domain'] = eval( action['domain'], eval_ctx ) or []
447 return fix_view_modes(action)
449 # I think generate_views,fix_view_modes should go into js ActionManager
450 def generate_views(action):
452 While the server generates a sequence called "views" computing dependencies
453 between a bunch of stuff for views coming directly from the database
454 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
455 to return custom view dictionaries generated on the fly.
457 In that case, there is no ``views`` key available on the action.
459 Since the web client relies on ``action['views']``, generate it here from
460 ``view_mode`` and ``view_id``.
462 Currently handles two different cases:
464 * no view_id, multiple view_mode
465 * single view_id, single view_mode
467 :param dict action: action descriptor dictionary to generate a views key for
469 view_id = action.get('view_id', False)
470 if isinstance(view_id, (list, tuple)):
473 # providing at least one view mode is a requirement, not an option
474 view_modes = action['view_mode'].split(',')
476 if len(view_modes) > 1:
478 raise ValueError('Non-db action dictionaries should provide '
479 'either multiple view modes or a single view '
480 'mode and an optional view id.\n\n Got view '
481 'modes %r and view id %r for action %r' % (
482 view_modes, view_id, action))
483 action['views'] = [(False, mode) for mode in view_modes]
485 action['views'] = [(view_id, view_modes[0])]
487 def fix_view_modes(action):
488 """ For historical reasons, OpenERP has weird dealings in relation to
489 view_mode and the view_type attribute (on window actions):
491 * one of the view modes is ``tree``, which stands for both list views
493 * the choice is made by checking ``view_type``, which is either
494 ``form`` for a list view or ``tree`` for an actual tree view
496 This methods simply folds the view_type into view_mode by adding a
497 new view mode ``list`` which is the result of the ``tree`` view_mode
498 in conjunction with the ``form`` view_type.
500 TODO: this should go into the doc, some kind of "peculiarities" section
502 :param dict action: an action descriptor
503 :returns: nothing, the action is modified in place
505 if 'views' not in action:
506 generate_views(action)
508 if action.pop('view_type') != 'form':
512 [id, mode if mode != 'tree' else 'list']
513 for id, mode in action['views']
518 class Menu(openerpweb.Controller):
519 _cp_path = "/base/menu"
521 @openerpweb.jsonrequest
523 return {'data': self.do_load(req)}
525 def do_load(self, req):
526 """ Loads all menu items (all applications and their sub-menus).
528 :param req: A request object, with an OpenERP session attribute
529 :type req: < session -> OpenERPSession >
530 :return: the menu root
531 :rtype: dict('children': menu_nodes)
533 Menus = req.session.model('ir.ui.menu')
534 # menus are loaded fully unlike a regular tree view, cause there are
535 # less than 512 items
536 context = req.session.eval_context(req.context)
537 menu_ids = Menus.search([], 0, False, False, context)
538 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
539 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
540 menu_items.append(menu_root)
542 # make a tree using parent_id
543 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
544 for menu_item in menu_items:
545 if menu_item['parent_id']:
546 parent = menu_item['parent_id'][0]
549 if parent in menu_items_map:
550 menu_items_map[parent].setdefault(
551 'children', []).append(menu_item)
553 # sort by sequence a tree using parent_id
554 for menu_item in menu_items:
555 menu_item.setdefault('children', []).sort(
556 key=lambda x:x["sequence"])
560 @openerpweb.jsonrequest
561 def action(self, req, menu_id):
562 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
563 [('ir.ui.menu', menu_id)], False)
564 return {"action": actions}
566 class DataSet(openerpweb.Controller):
567 _cp_path = "/base/dataset"
569 @openerpweb.jsonrequest
570 def fields(self, req, model):
571 return {'fields': req.session.model(model).fields_get(False,
572 req.session.eval_context(req.context))}
574 @openerpweb.jsonrequest
575 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
576 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
577 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
579 """ Performs a search() followed by a read() (if needed) using the
580 provided search criteria
582 :param req: a JSON-RPC request object
583 :type req: openerpweb.JsonRequest
584 :param str model: the name of the model to search on
585 :param fields: a list of the fields to return in the result records
587 :param int offset: from which index should the results start being returned
588 :param int limit: the maximum number of records to return
589 :param list domain: the search domain for the query
590 :param list sort: sorting directives
591 :returns: A structure (dict) with two keys: ids (all the ids matching
592 the (domain, context) pair) and records (paginated records
593 matching fields selection set)
596 Model = req.session.model(model)
598 context, domain = eval_context_and_domain(
599 req.session, req.context, domain)
601 ids = Model.search(domain, 0, False, sort or False, context)
602 # need to fill the dataset with all ids for the (domain, context) pair,
603 # so search un-paginated and paginate manually before reading
604 paginated_ids = ids[offset:(offset + limit if limit else None)]
605 if fields and fields == ['id']:
606 # shortcut read if we only want the ids
609 'records': map(lambda id: {'id': id}, paginated_ids)
612 records = Model.read(paginated_ids, fields or False, context)
613 records.sort(key=lambda obj: ids.index(obj['id']))
620 @openerpweb.jsonrequest
621 def read(self, req, model, ids, fields=False):
622 return self.do_search_read(req, model, ids, fields)
624 @openerpweb.jsonrequest
625 def get(self, req, model, ids, fields=False):
626 return self.do_get(req, model, ids, fields)
628 def do_get(self, req, model, ids, fields=False):
629 """ Fetches and returns the records of the model ``model`` whose ids
632 The results are in the same order as the inputs, but elements may be
633 missing (if there is no record left for the id)
635 :param req: the JSON-RPC2 request object
636 :type req: openerpweb.JsonRequest
637 :param model: the model to read from
639 :param ids: a list of identifiers
641 :param fields: a list of fields to fetch, ``False`` or empty to fetch
642 all fields in the model
643 :type fields: list | False
644 :returns: a list of records, in the same order as the list of ids
647 Model = req.session.model(model)
648 records = Model.read(ids, fields, req.session.eval_context(req.context))
650 record_map = dict((record['id'], record) for record in records)
652 return [record_map[id] for id in ids if record_map.get(id)]
654 @openerpweb.jsonrequest
655 def load(self, req, model, id, fields):
656 m = req.session.model(model)
658 r = m.read([id], False, req.session.eval_context(req.context))
661 return {'value': value}
663 @openerpweb.jsonrequest
664 def create(self, req, model, data):
665 m = req.session.model(model)
666 r = m.create(data, req.session.eval_context(req.context))
669 @openerpweb.jsonrequest
670 def save(self, req, model, id, data):
671 m = req.session.model(model)
672 r = m.write([id], data, req.session.eval_context(req.context))
675 @openerpweb.jsonrequest
676 def unlink(self, req, model, ids=()):
677 Model = req.session.model(model)
678 return Model.unlink(ids, req.session.eval_context(req.context))
680 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
681 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
682 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
683 c, d = eval_context_and_domain(req.session, context, domain)
684 if domain_id and len(args) - 1 >= domain_id:
686 if context_id and len(args) - 1 >= context_id:
689 return getattr(req.session.model(model), method)(*args)
691 @openerpweb.jsonrequest
692 def call(self, req, model, method, args, domain_id=None, context_id=None):
693 return self.call_common(req, model, method, args, domain_id, context_id)
695 @openerpweb.jsonrequest
696 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
697 action = self.call_common(req, model, method, args, domain_id, context_id)
698 if isinstance(action, dict) and action.get('type') != '':
699 return {'result': clean_action(req, action)}
700 return {'result': False}
702 @openerpweb.jsonrequest
703 def exec_workflow(self, req, model, id, signal):
704 r = req.session.exec_workflow(model, id, signal)
707 @openerpweb.jsonrequest
708 def default_get(self, req, model, fields):
709 Model = req.session.model(model)
710 return Model.default_get(fields, req.session.eval_context(req.context))
712 @openerpweb.jsonrequest
713 def name_search(self, req, model, search_str, domain=[], context={}):
714 m = req.session.model(model)
715 r = m.name_search(search_str+'%', domain, '=ilike', context)
718 class DataGroup(openerpweb.Controller):
719 _cp_path = "/base/group"
720 @openerpweb.jsonrequest
721 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
722 Model = req.session.model(model)
723 context, domain = eval_context_and_domain(req.session, req.context, domain)
725 return Model.read_group(
726 domain or [], fields, group_by_fields, 0, False,
727 dict(context, group_by=group_by_fields), sort or False)
729 class View(openerpweb.Controller):
730 _cp_path = "/base/view"
732 def fields_view_get(self, req, model, view_id, view_type,
733 transform=True, toolbar=False, submenu=False):
734 Model = req.session.model(model)
735 context = req.session.eval_context(req.context)
736 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
737 # todo fme?: check that we should pass the evaluated context here
738 self.process_view(req.session, fvg, context, transform)
741 def process_view(self, session, fvg, context, transform):
742 # depending on how it feels, xmlrpclib.ServerProxy can translate
743 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
744 # enjoy unicode strings which can not be trivially converted to
745 # strings, and it blows up during parsing.
747 # So ensure we fix this retardation by converting view xml back to
749 if isinstance(fvg['arch'], unicode):
750 arch = fvg['arch'].encode('utf-8')
755 evaluation_context = session.evaluation_context(context or {})
756 xml = self.transform_view(arch, session, evaluation_context)
758 xml = ElementTree.fromstring(arch)
759 fvg['arch'] = Xml2Json.convert_element(xml)
761 for field in fvg['fields'].itervalues():
762 if field.get('views'):
763 for view in field["views"].itervalues():
764 self.process_view(session, view, None, transform)
765 if field.get('domain'):
766 field["domain"] = self.parse_domain(field["domain"], session)
767 if field.get('context'):
768 field["context"] = self.parse_context(field["context"], session)
770 @openerpweb.jsonrequest
771 def add_custom(self, req, view_id, arch):
772 CustomView = req.session.model('ir.ui.view.custom')
774 'user_id': req.session._uid,
777 }, req.session.eval_context(req.context))
778 return {'result': True}
780 @openerpweb.jsonrequest
781 def undo_custom(self, req, view_id, reset=False):
782 CustomView = req.session.model('ir.ui.view.custom')
783 context = req.session.eval_context(req.context)
784 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
785 0, False, False, context)
788 CustomView.unlink(vcustom, context)
790 CustomView.unlink([vcustom[0]], context)
791 return {'result': True}
792 return {'result': False}
794 def transform_view(self, view_string, session, context=None):
795 # transform nodes on the fly via iterparse, instead of
796 # doing it statically on the parsing result
797 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
799 for event, elem in parser:
803 self.parse_domains_and_contexts(elem, session)
806 def parse_domain(self, domain, session):
807 """ Parses an arbitrary string containing a domain, transforms it
808 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
810 :param domain: the domain to parse, if the domain is not a string it
811 is assumed to be a literal domain and is returned as-is
812 :param session: Current OpenERP session
813 :type session: openerpweb.openerpweb.OpenERPSession
815 if not isinstance(domain, (str, unicode)):
818 return openerpweb.ast.literal_eval(domain)
821 return openerpweb.nonliterals.Domain(session, domain)
823 def parse_context(self, context, session):
824 """ Parses an arbitrary string containing a context, transforms it
825 to either a literal context or a :class:`openerpweb.nonliterals.Context`
827 :param context: the context to parse, if the context is not a string it
828 is assumed to be a literal domain and is returned as-is
829 :param session: Current OpenERP session
830 :type session: openerpweb.openerpweb.OpenERPSession
832 if not isinstance(context, (str, unicode)):
835 return openerpweb.ast.literal_eval(context)
837 return openerpweb.nonliterals.Context(session, context)
839 def parse_domains_and_contexts(self, elem, session):
840 """ Converts domains and contexts from the view into Python objects,
841 either literals if they can be parsed by literal_eval or a special
842 placeholder object if the domain or context refers to free variables.
844 :param elem: the current node being parsed
845 :type param: xml.etree.ElementTree.Element
846 :param session: OpenERP session object, used to store and retrieve
848 :type session: openerpweb.openerpweb.OpenERPSession
850 for el in ['domain', 'filter_domain']:
851 domain = elem.get(el, '').strip()
853 elem.set(el, self.parse_domain(domain, session))
854 for el in ['context', 'default_get']:
855 context_string = elem.get(el, '').strip()
857 elem.set(el, self.parse_context(context_string, session))
859 class FormView(View):
860 _cp_path = "/base/formview"
862 @openerpweb.jsonrequest
863 def load(self, req, model, view_id, toolbar=False):
864 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
865 return {'fields_view': fields_view}
867 class ListView(View):
868 _cp_path = "/base/listview"
870 @openerpweb.jsonrequest
871 def load(self, req, model, view_id, toolbar=False):
872 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
873 return {'fields_view': fields_view}
875 def process_colors(self, view, row, context):
876 colors = view['arch']['attrs'].get('colors')
883 for pair in colors.split(';')
884 if eval(pair.split(':')[1], dict(context, **row))
889 elif len(color) == 1:
893 class SearchView(View):
894 _cp_path = "/base/searchview"
896 @openerpweb.jsonrequest
897 def load(self, req, model, view_id):
898 fields_view = self.fields_view_get(req, model, view_id, 'search')
899 return {'fields_view': fields_view}
901 @openerpweb.jsonrequest
902 def fields_get(self, req, model):
903 Model = req.session.model(model)
904 fields = Model.fields_get(False, req.session.eval_context(req.context))
905 for field in fields.values():
906 # shouldn't convert the views too?
907 if field.get('domain'):
908 field["domain"] = self.parse_domain(field["domain"], req.session)
909 if field.get('context'):
910 field["context"] = self.parse_domain(field["context"], req.session)
911 return {'fields': fields}
913 @openerpweb.jsonrequest
914 def get_filters(self, req, model):
915 Model = req.session.model("ir.filters")
916 filters = Model.get_filters(model)
917 for filter in filters:
918 filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session))
919 filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session))
922 @openerpweb.jsonrequest
923 def save_filter(self, req, model, name, context_to_save, domain):
924 Model = req.session.model("ir.filters")
925 ctx = openerpweb.nonliterals.CompoundContext(context_to_save)
926 ctx.session = req.session
928 domain = openerpweb.nonliterals.CompoundDomain(domain)
929 domain.session = req.session
930 domain = domain.evaluate()
931 uid = req.session._uid
932 context = req.session.eval_context(req.context)
933 to_return = Model.create_or_replace({"context": ctx,
941 class Binary(openerpweb.Controller):
942 _cp_path = "/base/binary"
944 @openerpweb.httprequest
945 def image(self, req, model, id, field, **kw):
946 req.httpresponse.headers['Content-Type'] = 'image/png'
947 Model = req.session.model(model)
948 context = req.session.eval_context(req.context)
951 res = Model.default_get([field], context).get(field, '')
953 res = Model.read([int(id)], [field], context)[0].get(field, '')
954 return base64.decodestring(res)
955 except: # TODO: what's the exception here?
956 return self.placeholder()
957 def placeholder(self):
958 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
960 @openerpweb.httprequest
961 def saveas(self, req, model, id, field, fieldname, **kw):
962 Model = req.session.model(model)
963 context = req.session.eval_context(req.context)
964 res = Model.read([int(id)], [field, fieldname], context)[0]
965 filecontent = res.get(field, '')
967 raise cherrypy.NotFound
969 req.httpresponse.headers['Content-Type'] = 'application/octet-stream'
970 filename = '%s_%s' % (model.replace('.', '_'), id)
972 filename = res.get(fieldname, '') or filename
973 req.httpresponse.headers['Content-Disposition'] = 'attachment; filename=' + filename
974 return base64.decodestring(filecontent)
976 @openerpweb.httprequest
977 def upload(self, req, callback, ufile=None):
978 cherrypy.response.timeout = 500
980 for key, val in req.httprequest.headers.iteritems():
981 headers[key.lower()] = val
982 size = int(headers.get('content-length', 0))
983 # TODO: might be useful to have a configuration flag for max-length file uploads
985 out = """<script language="javascript" type="text/javascript">
986 var win = window.top.window,
988 if (typeof(callback) === 'function') {
989 callback.apply(this, %s);
991 win.jQuery('#oe_notification', win.document).notify('create', {
992 title: "Ajax File Upload",
993 text: "Could not find callback"
997 data = ufile.file.read()
998 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
1000 args = [False, e.message]
1001 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1003 @openerpweb.httprequest
1004 def upload_attachment(self, req, callback, model, id, ufile=None):
1005 cherrypy.response.timeout = 500
1006 context = req.session.eval_context(req.context)
1007 Model = req.session.model('ir.attachment')
1009 out = """<script language="javascript" type="text/javascript">
1010 var win = window.top.window,
1012 if (typeof(callback) === 'function') {
1013 callback.call(this, %s);
1016 attachment_id = Model.create({
1017 'name': ufile.filename,
1018 'datas': base64.encodestring(ufile.file.read()),
1023 'filename': ufile.filename,
1026 except Exception, e:
1027 args = { 'error': e.message }
1028 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1030 class Action(openerpweb.Controller):
1031 _cp_path = "/base/action"
1033 @openerpweb.jsonrequest
1034 def load(self, req, action_id):
1035 Actions = req.session.model('ir.actions.actions')
1037 context = req.session.eval_context(req.context)
1038 action_type = Actions.read([action_id], ['type'], context)
1040 action = req.session.model(action_type[0]['type']).read([action_id], False,
1043 value = clean_action(req, action[0])
1044 return {'result': value}
1046 @openerpweb.jsonrequest
1047 def run(self, req, action_id):
1048 return clean_action(req, req.session.model('ir.actions.server').run(
1049 [action_id], req.session.eval_context(req.context)))
1051 class TreeView(View):
1052 _cp_path = "/base/treeview"
1054 @openerpweb.jsonrequest
1055 def load(self, req, model, view_id, toolbar=False):
1056 return self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
1058 @openerpweb.jsonrequest
1059 def action(self, req, model, id):
1060 return load_actions_from_ir_values(
1061 req,'action', 'tree_but_open',[(model, id)],
1064 def export_csv(fields, result):
1066 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1068 writer.writerow(fields)
1073 if isinstance(d, basestring):
1074 d = d.replace('\n',' ').replace('\t',' ')
1076 d = d.encode('utf-8')
1079 if d is False: d = None
1081 writer.writerow(row)
1088 def export_xls(fieldnames, table):
1092 common.error(_('Import Error.'), _('Please install xlwt library to export to MS Excel.'))
1094 workbook = xlwt.Workbook()
1095 worksheet = workbook.add_sheet('Sheet 1')
1097 for i, fieldname in enumerate(fieldnames):
1098 worksheet.write(0, i, str(fieldname))
1099 worksheet.col(i).width = 8000 # around 220 pixels
1101 style = xlwt.easyxf('align: wrap yes')
1103 for row_index, row in enumerate(table):
1104 for cell_index, cell_value in enumerate(row):
1105 cell_value = str(cell_value)
1106 cell_value = re.sub("\r", " ", cell_value)
1107 worksheet.write(row_index + 1, cell_index, cell_value, style)
1115 #return data.decode('ISO-8859-1')
1116 return unicode(data, 'utf-8', 'replace')
1119 _cp_path = "/base/export"
1121 def fields_get(self, req, model):
1122 Model = req.session.model(model)
1123 fields = Model.fields_get(False, req.session.eval_context(req.context))
1126 @openerpweb.jsonrequest
1127 def get_fields(self, req, model, prefix='', name= '', field_parent=None, params={}):
1128 import_compat = params.get("import_compat", False)
1130 fields = self.fields_get(req, model)
1131 field_parent_type = params.get("parent_field_type",False)
1133 if import_compat and field_parent_type and field_parent_type == "many2one":
1136 fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1138 fields_order = fields.keys()
1139 fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1141 for index, field in enumerate(fields_order):
1142 value = fields[field]
1144 if import_compat and value.get('readonly', False):
1146 for sl in value.get('states', {}).values():
1148 ok = ok or (s==['readonly',False])
1151 id = prefix + (prefix and '/'or '') + field
1152 nm = name + (name and '/' or '') + value['string']
1153 record.update(id=id, string= nm, action='javascript: void(0)',
1154 target=None, icon=None, children=[], field_type=value.get('type',False), required=value.get('required', False))
1155 records.append(record)
1157 if len(nm.split('/')) < 3 and value.get('relation', False):
1159 ref = value.pop('relation')
1160 cfields = self.fields_get(req, ref)
1161 if (value['type'] == 'many2many'):
1162 record['children'] = []
1163 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1165 elif value['type'] == 'many2one':
1166 record['children'] = [id + '/id', id + '/.id']
1167 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1170 cfields_order = cfields.keys()
1171 cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1173 for j, fld in enumerate(cfields_order):
1174 cid = id + '/' + fld
1175 cid = cid.replace(' ', '_')
1176 children.append(cid)
1177 record['children'] = children or []
1178 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1180 ref = value.pop('relation')
1181 cfields = self.fields_get(req, ref)
1182 cfields_order = cfields.keys()
1183 cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1185 for j, fld in enumerate(cfields_order):
1186 cid = id + '/' + fld
1187 cid = cid.replace(' ', '_')
1188 children.append(cid)
1189 record['children'] = children or []
1190 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1195 @openerpweb.jsonrequest
1196 def save_export_lists(self, req, name, model, field_list):
1197 result = {'resource':model, 'name':name, 'export_fields': []}
1198 for field in field_list:
1199 result['export_fields'].append((0, 0, {'name': field}))
1200 return req.session.model("ir.exports").create(result, req.session.eval_context(req.context))
1202 @openerpweb.jsonrequest
1203 def exist_export_lists(self, req, model):
1204 export_model = req.session.model("ir.exports")
1205 return export_model.read(export_model.search([('resource', '=', model)]), ['name'])
1207 @openerpweb.jsonrequest
1208 def delete_export(self, req, export_id):
1209 req.session.model("ir.exports").unlink(export_id, req.session.eval_context(req.context))
1212 @openerpweb.jsonrequest
1213 def namelist(self,req, model, export_id):
1215 result = self.get_data(req, model, req.session.eval_context(req.context))
1216 ir_export_obj = req.session.model("ir.exports")
1217 ir_export_line_obj = req.session.model("ir.exports.line")
1219 field = ir_export_obj.read(export_id)
1220 fields = ir_export_line_obj.read(field['export_fields'])
1223 [name_list.update({field['name']: result.get(field['name'])}) for field in fields]
1226 def get_data(self, req, model, context=None):
1228 context = context or {}
1230 proxy = req.session.model(model)
1231 fields = self.fields_get(req, model)
1233 f1 = proxy.fields_view_get(False, 'tree', context)['fields']
1234 f2 = proxy.fields_view_get(False, 'form', context)['fields']
1238 fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1241 _fields = {'id': 'ID' , '.id': 'Database ID' }
1242 def model_populate(fields, prefix_node='', prefix=None, prefix_value='', level=2):
1243 fields_order = fields.keys()
1244 fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1246 for field in fields_order:
1247 fields_data[prefix_node+field] = fields[field]
1249 fields_data[prefix_node + field]['string'] = '%s%s' % (prefix_value, fields_data[prefix_node + field]['string'])
1250 st_name = fields[field]['string'] or field
1251 _fields[prefix_node+field] = st_name
1252 if fields[field].get('relation', False) and level>0:
1253 fields2 = self.fields_get(req, fields[field]['relation'])
1254 fields2.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1255 model_populate(fields2, prefix_node+field+'/', None, st_name+'/', level-1)
1256 model_populate(fields)
1260 @openerpweb.jsonrequest
1261 def export_data(self, req, model, fields, ids, domain, import_compat=False, export_format="csv", context=None):
1262 context = req.session.eval_context(req.context)
1263 modle_obj = req.session.model(model)
1265 ids = ids or modle_obj.search(domain, context=context)
1267 field = fields.keys()
1268 result = modle_obj.export_data(ids, field , context).get('datas',[])
1270 if not import_compat:
1271 field = [val.strip() for val in fields.values()]
1273 if export_format == 'xls':
1274 return export_xls(field, result)
1276 return export_csv(field, result)
1279 _cp_path = "/base/import"
1281 def fields_get(self, req, model):
1282 Model = req.session.model(model)
1283 fields = Model.fields_get(False, req.session.eval_context(req.context))
1286 @openerpweb.httprequest
1287 def detect_data(self, req, **params):
1293 fields = dict(req.session.model(params.get('model')).fields_get(False, req.session.eval_context(req.context)))
1294 fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1296 def model_populate(fields, prefix_node='', prefix=None, prefix_value='', level=2):
1302 fields_order = fields.keys()
1303 fields_order.sort(lambda x,y: str_comp(fields[x].get('string', ''), fields[y].get('string', '')))
1304 for field in fields_order:
1305 if (fields[field].get('type','') not in ('reference',))\
1306 and (not fields[field].get('readonly')\
1307 or not dict(fields[field].get('states', {}).get(
1308 'draft', [('readonly', True)])).get('readonly',True)):
1310 st_name = prefix_value+fields[field]['string'] or field
1311 _fields[prefix_node+field] = st_name
1312 _fields_invert[st_name] = prefix_node+field
1314 if fields[field].get('type','')=='one2many' and level>0:
1315 fields2 = self.fields_get(req, fields[field]['relation'])
1316 model_populate(fields2, prefix_node+field+'/', None, st_name+'/', level-1)
1318 if fields[field].get('relation',False) and level>0:
1319 model_populate({'/id': {'type': 'char', 'string': 'ID'}, '.id': {'type': 'char', 'string': 'Database ID'}},
1320 prefix_node+field, None, st_name+'/', level-1)
1321 fields.update({'id':{'string':'ID'},'.id':{'string':'Database ID'}})
1322 model_populate(fields)
1323 all_fields = fields.keys()
1325 data = csv.reader(params.get('csvfile').file, quotechar=str(params.get('csvdel')), delimiter=str(params.get('csvsep')))
1327 error={'message': 'error opening .CSV file. Input Error.'}
1328 return simplejson.dumps({'error':error})
1335 for i, row in enumerate(data):
1340 for line in records:
1342 word = str(word.decode(params.get('csvcode')))
1344 fields.append((word, _fields[word]))
1345 elif word in _fields_invert.keys():
1346 fields.append((_fields_invert[word], word))
1348 fields.append((word, word))
1349 # error = {'message':("You cannot import the field '%s', because we cannot auto-detect it" % (word,))}
1352 error = {'message':('Error processing the first line of the file. Field "%s" is unknown') % (word,)}
1355 params.get('csvfile').file.seek(0)
1356 error=dict(error, preview=params.get('csvfile').file.read(200))
1357 return simplejson.dumps({'error':error})
1359 return simplejson.dumps({'records':records[1:],'fields':fields,'all_fields':all_fields})
1361 @openerpweb.httprequest
1362 def import_data(self, req, **params):
1365 context = req.session.eval_context(req.context)
1366 modle_obj = req.session.model(params.get('model'))
1368 content = params.get('csvfile').file.read()
1369 input=StringIO.StringIO(content)
1373 if not (params.get('csvdel') and len(params.get('csvdel')) == 1):
1374 error={'message': "The CSV delimiter must be a single character"}
1375 return simplejson.dumps({'error':error})
1378 for j, line in enumerate(csv.reader(input, quotechar=str(params.get('csvdel')), delimiter=str(params.get('csvsep')))):
1379 # If the line contains no data, we should skip it.
1386 except csv.Error, e:
1387 error={'message': str(e),'title': 'File Format Error'}
1388 return simplejson.dumps({'error':error})
1393 if not isinstance(fields, list):
1398 datas.append(map(lambda x:x.decode(params.get('csvcode')).encode('utf-8'), line))
1400 datas.append(map(lambda x:x.decode('latin').encode('utf-8'), line))
1402 # If the file contains nothing,
1404 error = {'message': 'The file is empty !', 'title': 'Importation !'}
1405 return simplejson.dumps({'error':error})
1407 #Inverting the header into column names
1409 res = modle_obj.import_data(fields, datas, 'init', '', False, ctx)
1410 except xmlrpclib.Fault, e:
1411 error = {"message":e.faultCode}
1412 return simplejson.dumps({'error':error})
1415 success={'message':'Imported %d objects' % (res[0],)}
1416 return simplejson.dumps({'success':success})
1419 for key,val in res[1].items():
1420 d+= ('%s: %s' % (str(key),str(val)))
1421 msg = 'Error trying to import this record:%s. ErrorMessage:%s %s' % (d,res[2],res[3])
1422 error = {'message':str(msg), 'title':'ImportationError'}
1424 return simplejson.dumps({'error':error})