1 # -*- coding: utf-8 -*-
12 from xml.etree import ElementTree
13 from cStringIO import StringIO
19 import openerpweb.nonliterals
22 # Should move to openerpweb.Xml2Json
25 # Simple and straightforward XML-to-JSON converter in Python
28 # URL: http://code.google.com/p/xml2json-direct/
30 def convert_to_json(s):
31 return simplejson.dumps(
32 Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
35 def convert_to_structure(s):
36 root = ElementTree.fromstring(s)
37 return Xml2Json.convert_element(root)
40 def convert_element(el, skip_whitespaces=True):
43 ns, name = el.tag.rsplit("}", 1)
45 res["namespace"] = ns[1:]
49 for k, v in el.items():
52 if el.text and (not skip_whitespaces or el.text.strip() != ''):
55 kids.append(Xml2Json.convert_element(kid))
56 if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''):
58 res["children"] = kids
61 #----------------------------------------------------------
62 # OpenERP Web base Controllers
63 #----------------------------------------------------------
65 def manifest_glob(addons, key):
68 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)
81 root = openerpweb.path_root
85 fname = os.path.join(root, i)
86 ftime = os.path.getmtime(fname)
87 if ftime > files_timestamp:
88 files_timestamp = ftime
89 files_content = open(fname).read()
90 files_concat = "".join(files_content)
93 home_template = textwrap.dedent("""<!DOCTYPE html>
94 <html style="height: 100%%">
96 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
97 <title>OpenERP</title>
99 <script type="text/javascript">
101 QWeb = new QWeb2.Engine();
102 openerp.init().base.webclient("oe");
105 <link rel="shortcut icon" href="/base/static/src/img/favicon.ico" type="image/x-icon"/>
108 <link rel="stylesheet" href="/base/static/src/css/base-ie7.css" type="text/css"/>
111 <body id="oe" class="openerp"></body>
114 class WebClient(openerpweb.Controller):
115 _cp_path = "/base/webclient"
117 @openerpweb.jsonrequest
118 def csslist(self, req, mods='base'):
119 return manifest_glob(mods.split(','), 'css')
121 @openerpweb.jsonrequest
122 def jslist(self, req, mods='base'):
123 return manifest_glob(mods.split(','), 'js')
125 @openerpweb.httprequest
126 def css(self, req, mods='base'):
127 cherrypy.response.headers['Content-Type'] = 'text/css'
128 files = manifest_glob(mods.split(','), 'css')
129 concat = concat_files(files)[0]
130 # TODO request set the Date of last modif and Etag
133 @openerpweb.httprequest
134 def js(self, req, mods='base'):
135 cherrypy.response.headers['Content-Type'] = 'application/javascript'
136 files = manifest_glob(mods.split(','), 'js')
137 concat = concat_files(files)[0]
138 # TODO request set the Date of last modif and Etag
141 @openerpweb.httprequest
142 def home(self, req, s_action=None):
144 jslist = ['/base/webclient/js']
146 jslist = manifest_glob(['base'], 'js')
147 js = "\n ".join(['<script type="text/javascript" src="%s"></script>'%i for i in jslist])
150 csslist = ['/base/webclient/css']
152 csslist = manifest_glob(['base'], 'css')
153 css = "\n ".join(['<link rel="stylesheet" href="%s">'%i for i in csslist])
154 r = home_template % {
160 class Database(openerpweb.Controller):
161 _cp_path = "/base/database"
163 @openerpweb.jsonrequest
164 def get_list(self, req):
165 proxy = req.session.proxy("db")
167 h = req.httprequest.headers['Host'].split(':')[0]
169 r = cherrypy.config['openerp.dbfilter'].replace('%h', h).replace('%d', d)
170 dbs = [i for i in dbs if re.match(r, i)]
171 return {"db_list": dbs}
173 @openerpweb.jsonrequest
174 def progress(self, req, password, id):
175 return req.session.proxy('db').get_progress(password, id)
177 @openerpweb.jsonrequest
178 def create(self, req, fields):
180 params = dict(map(operator.itemgetter('name', 'value'), fields))
182 params['super_admin_pwd'],
184 bool(params.get('demo_data')),
186 params['create_admin_pwd']
190 return req.session.proxy("db").create(*create_attrs)
191 except xmlrpclib.Fault, e:
192 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
193 return {'error': e.faultCode, 'title': 'Create Database'}
194 return {'error': 'Could not create database !', 'title': 'Create Database'}
196 @openerpweb.jsonrequest
197 def drop(self, req, fields):
198 password, db = operator.itemgetter(
199 'drop_pwd', 'drop_db')(
200 dict(map(operator.itemgetter('name', 'value'), fields)))
203 return req.session.proxy("db").drop(password, db)
204 except xmlrpclib.Fault, e:
205 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
206 return {'error': e.faultCode, 'title': 'Drop Database'}
207 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
209 @openerpweb.httprequest
210 def backup(self, req, backup_db, backup_pwd, token):
212 db_dump = base64.decodestring(
213 req.session.proxy("db").dump(backup_pwd, backup_db))
214 cherrypy.response.headers['Content-Type'] = "application/octet-stream; charset=binary"
215 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename="' + backup_db + '.dump"'
216 cherrypy.response.cookie['fileToken'] = token
217 cherrypy.response.cookie['fileToken']['path'] = '/'
219 except xmlrpclib.Fault, e:
220 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
221 return 'Backup Database|' + e.faultCode
222 return 'Backup Database|Could not generate database backup'
224 @openerpweb.httprequest
225 def restore(self, req, db_file, restore_pwd, new_db):
227 data = base64.encodestring(db_file.file.read())
228 req.session.proxy("db").restore(restore_pwd, new_db, data)
230 except xmlrpclib.Fault, e:
231 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
232 raise cherrypy.HTTPError(403)
234 raise cherrypy.HTTPError()
236 @openerpweb.jsonrequest
237 def change_password(self, req, fields):
238 old_password, new_password = operator.itemgetter(
239 'old_pwd', 'new_pwd')(
240 dict(map(operator.itemgetter('name', 'value'), fields)))
242 return req.session.proxy("db").change_admin_password(old_password, new_password)
243 except xmlrpclib.Fault, e:
244 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
245 return {'error': e.faultCode, 'title': 'Change Password'}
246 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
248 class Session(openerpweb.Controller):
249 _cp_path = "/base/session"
251 @openerpweb.jsonrequest
252 def login(self, req, db, login, password):
253 req.session.login(db, login, password)
256 "session_id": req.session_id,
257 "uid": req.session._uid,
260 @openerpweb.jsonrequest
261 def sc_list(self, req):
262 return req.session.model('ir.ui.view_sc').get_sc(req.session._uid, "ir.ui.menu",
263 req.session.eval_context(req.context))
265 @openerpweb.jsonrequest
266 def get_lang_list(self, req):
269 'lang_list': (req.session.proxy("db").list_lang() or []),
273 return {"error": e, "title": "Languages"}
275 @openerpweb.jsonrequest
276 def modules(self, req):
277 # TODO query server for installed web modules
279 for name, manifest in openerpweb.addons_manifest.items():
280 if name != 'base' and manifest.get('active', True):
284 @openerpweb.jsonrequest
285 def eval_domain_and_context(self, req, contexts, domains,
287 """ Evaluates sequences of domains and contexts, composing them into
288 a single context, domain or group_by sequence.
290 :param list contexts: list of contexts to merge together. Contexts are
291 evaluated in sequence, all previous contexts
292 are part of their own evaluation context
293 (starting at the session context).
294 :param list domains: list of domains to merge together. Domains are
295 evaluated in sequence and appended to one another
296 (implicit AND), their evaluation domain is the
297 result of merging all contexts.
298 :param list group_by_seq: list of domains (which may be in a different
299 order than the ``contexts`` parameter),
300 evaluated in sequence, their ``'group_by'``
301 key is extracted if they have one.
306 the global context created by merging all of
310 the concatenation of all domains
313 a list of fields to group by, potentially empty (in which case
314 no group by should be performed)
316 context, domain = eval_context_and_domain(req.session,
317 openerpweb.nonliterals.CompoundContext(*(contexts or [])),
318 openerpweb.nonliterals.CompoundDomain(*(domains or [])))
320 group_by_sequence = []
321 for candidate in (group_by_seq or []):
322 ctx = req.session.eval_context(candidate, context)
323 group_by = ctx.get('group_by')
326 elif isinstance(group_by, basestring):
327 group_by_sequence.append(group_by)
329 group_by_sequence.extend(group_by)
334 'group_by': group_by_sequence
337 @openerpweb.jsonrequest
338 def save_session_action(self, req, the_action):
340 This method store an action object in the session object and returns an integer
341 identifying that action. The method get_session_action() can be used to get
344 :param the_action: The action to save in the session.
345 :type the_action: anything
346 :return: A key identifying the saved action.
349 saved_actions = cherrypy.session.get('saved_actions')
350 if not saved_actions:
351 saved_actions = {"next":0, "actions":{}}
352 cherrypy.session['saved_actions'] = saved_actions
353 # we don't allow more than 10 stored actions
354 if len(saved_actions["actions"]) >= 10:
355 del saved_actions["actions"][min(saved_actions["actions"].keys())]
356 key = saved_actions["next"]
357 saved_actions["actions"][key] = the_action
358 saved_actions["next"] = key + 1
361 @openerpweb.jsonrequest
362 def get_session_action(self, req, key):
364 Gets back a previously saved action. This method can return None if the action
365 was saved since too much time (this case should be handled in a smart way).
367 :param key: The key given by save_session_action()
369 :return: The saved action or None.
372 saved_actions = cherrypy.session.get('saved_actions')
373 if not saved_actions:
375 return saved_actions["actions"].get(key)
377 @openerpweb.jsonrequest
378 def check(self, req):
379 req.session.assert_valid()
382 def eval_context_and_domain(session, context, domain=None):
383 e_context = session.eval_context(context)
384 # should we give the evaluated context as an evaluation context to the domain?
385 e_domain = session.eval_domain(domain or [])
387 return e_context, e_domain
389 def load_actions_from_ir_values(req, key, key2, models, meta, context):
390 Values = req.session.model('ir.values')
391 actions = Values.get(key, key2, models, meta, context)
393 return [(id, name, clean_action(action, req.session, context=context))
394 for id, name, action in actions]
396 def clean_action(action, session, context=None):
397 action.setdefault('flags', {})
398 if action['type'] != 'ir.actions.act_window':
400 # values come from the server, we can just eval them
401 if isinstance(action.get('context'), basestring):
402 action['context'] = eval(
404 session.evaluation_context(context=context)) or {}
406 if isinstance(action.get('domain'), basestring):
407 action['domain'] = eval(
409 session.evaluation_context(
410 action.get('context', {}))) or []
412 return fix_view_modes(action)
414 def generate_views(action):
416 While the server generates a sequence called "views" computing dependencies
417 between a bunch of stuff for views coming directly from the database
418 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
419 to return custom view dictionaries generated on the fly.
421 In that case, there is no ``views`` key available on the action.
423 Since the web client relies on ``action['views']``, generate it here from
424 ``view_mode`` and ``view_id``.
426 Currently handles two different cases:
428 * no view_id, multiple view_mode
429 * single view_id, single view_mode
431 :param dict action: action descriptor dictionary to generate a views key for
433 view_id = action.get('view_id', False)
434 if isinstance(view_id, (list, tuple)):
437 # providing at least one view mode is a requirement, not an option
438 view_modes = action['view_mode'].split(',')
440 if len(view_modes) > 1:
442 raise ValueError('Non-db action dictionaries should provide '
443 'either multiple view modes or a single view '
444 'mode and an optional view id.\n\n Got view '
445 'modes %r and view id %r for action %r' % (
446 view_modes, view_id, action))
447 action['views'] = [(False, mode) for mode in view_modes]
449 action['views'] = [(view_id, view_modes[0])]
451 def fix_view_modes(action):
452 """ For historical reasons, OpenERP has weird dealings in relation to
453 view_mode and the view_type attribute (on window actions):
455 * one of the view modes is ``tree``, which stands for both list views
457 * the choice is made by checking ``view_type``, which is either
458 ``form`` for a list view or ``tree`` for an actual tree view
460 This methods simply folds the view_type into view_mode by adding a
461 new view mode ``list`` which is the result of the ``tree`` view_mode
462 in conjunction with the ``form`` view_type.
464 TODO: this should go into the doc, some kind of "peculiarities" section
466 :param dict action: an action descriptor
467 :returns: nothing, the action is modified in place
469 if 'views' not in action:
470 generate_views(action)
472 if action.pop('view_type') != 'form':
476 [id, mode if mode != 'tree' else 'list']
477 for id, mode in action['views']
482 class Menu(openerpweb.Controller):
483 _cp_path = "/base/menu"
485 @openerpweb.jsonrequest
487 return {'data': self.do_load(req)}
489 def do_load(self, req):
490 """ Loads all menu items (all applications and their sub-menus).
492 :param req: A request object, with an OpenERP session attribute
493 :type req: < session -> OpenERPSession >
494 :return: the menu root
495 :rtype: dict('children': menu_nodes)
497 Menus = req.session.model('ir.ui.menu')
498 # menus are loaded fully unlike a regular tree view, cause there are
499 # less than 512 items
500 context = req.session.eval_context(req.context)
501 menu_ids = Menus.search([], 0, False, False, context)
502 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
503 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
504 menu_items.append(menu_root)
506 # make a tree using parent_id
507 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
508 for menu_item in menu_items:
509 if menu_item['parent_id']:
510 parent = menu_item['parent_id'][0]
513 if parent in menu_items_map:
514 menu_items_map[parent].setdefault(
515 'children', []).append(menu_item)
517 # sort by sequence a tree using parent_id
518 for menu_item in menu_items:
519 menu_item.setdefault('children', []).sort(
520 key=lambda x:x["sequence"])
524 @openerpweb.jsonrequest
525 def action(self, req, menu_id):
526 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
527 [('ir.ui.menu', menu_id)], False,
528 req.session.eval_context(req.context))
529 return {"action": actions}
531 class DataSet(openerpweb.Controller):
532 _cp_path = "/base/dataset"
534 @openerpweb.jsonrequest
535 def fields(self, req, model):
536 return {'fields': req.session.model(model).fields_get(False,
537 req.session.eval_context(req.context))}
539 @openerpweb.jsonrequest
540 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, sort=None):
541 return self.do_search_read(request, model, fields, offset, limit, domain, sort)
542 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None
544 """ Performs a search() followed by a read() (if needed) using the
545 provided search criteria
547 :param request: a JSON-RPC request object
548 :type request: openerpweb.JsonRequest
549 :param str model: the name of the model to search on
550 :param fields: a list of the fields to return in the result records
552 :param int offset: from which index should the results start being returned
553 :param int limit: the maximum number of records to return
554 :param list domain: the search domain for the query
555 :param list sort: sorting directives
556 :returns: A structure (dict) with two keys: ids (all the ids matching
557 the (domain, context) pair) and records (paginated records
558 matching fields selection set)
561 Model = request.session.model(model)
563 context, domain = eval_context_and_domain(
564 request.session, request.context, domain)
566 ids = Model.search(domain, 0, False, sort or False, context)
567 # need to fill the dataset with all ids for the (domain, context) pair,
568 # so search un-paginated and paginate manually before reading
569 paginated_ids = ids[offset:(offset + limit if limit else None)]
570 if fields and fields == ['id']:
571 # shortcut read if we only want the ids
574 'records': map(lambda id: {'id': id}, paginated_ids)
577 records = Model.read(paginated_ids, fields or False, context)
578 records.sort(key=lambda obj: ids.index(obj['id']))
585 @openerpweb.jsonrequest
586 def read(self, request, model, ids, fields=False):
587 return self.do_search_read(request, model, ids, fields)
589 @openerpweb.jsonrequest
590 def get(self, request, model, ids, fields=False):
591 return self.do_get(request, model, ids, fields)
593 def do_get(self, request, model, ids, fields=False):
594 """ Fetches and returns the records of the model ``model`` whose ids
597 The results are in the same order as the inputs, but elements may be
598 missing (if there is no record left for the id)
600 :param request: the JSON-RPC2 request object
601 :type request: openerpweb.JsonRequest
602 :param model: the model to read from
604 :param ids: a list of identifiers
606 :param fields: a list of fields to fetch, ``False`` or empty to fetch
607 all fields in the model
608 :type fields: list | False
609 :returns: a list of records, in the same order as the list of ids
612 Model = request.session.model(model)
613 records = Model.read(ids, fields, request.session.eval_context(request.context))
615 record_map = dict((record['id'], record) for record in records)
617 return [record_map[id] for id in ids if record_map.get(id)]
619 @openerpweb.jsonrequest
620 def load(self, req, model, id, fields):
621 m = req.session.model(model)
623 r = m.read([id], False, req.session.eval_context(req.context))
626 return {'value': value}
628 @openerpweb.jsonrequest
629 def create(self, req, model, data):
630 m = req.session.model(model)
631 r = m.create(data, req.session.eval_context(req.context))
634 @openerpweb.jsonrequest
635 def save(self, req, model, id, data):
636 m = req.session.model(model)
637 r = m.write([id], data, req.session.eval_context(req.context))
640 @openerpweb.jsonrequest
641 def unlink(self, request, model, ids=()):
642 Model = request.session.model(model)
643 return Model.unlink(ids, request.session.eval_context(request.context))
645 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
646 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
647 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
648 c, d = eval_context_and_domain(req.session, context, domain)
649 if domain_id and len(args) - 1 >= domain_id:
651 if context_id and len(args) - 1 >= context_id:
654 return getattr(req.session.model(model), method)(*args)
656 @openerpweb.jsonrequest
657 def call(self, req, model, method, args, domain_id=None, context_id=None):
658 return self.call_common(req, model, method, args, domain_id, context_id)
660 @openerpweb.jsonrequest
661 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
662 action = self.call_common(req, model, method, args, domain_id, context_id)
663 if isinstance(action, dict) and action.get('type') != '':
664 return {'result': clean_action(action, req.session)}
665 return {'result': False}
667 @openerpweb.jsonrequest
668 def exec_workflow(self, req, model, id, signal):
669 r = req.session.exec_workflow(model, id, signal)
672 @openerpweb.jsonrequest
673 def default_get(self, req, model, fields):
674 Model = req.session.model(model)
675 return Model.default_get(fields, req.session.eval_context(req.context))
677 @openerpweb.jsonrequest
678 def name_search(self, req, model, search_str, domain=[], context={}):
679 m = req.session.model(model)
680 r = m.name_search(search_str+'%', domain, '=ilike', context)
683 class DataGroup(openerpweb.Controller):
684 _cp_path = "/base/group"
685 @openerpweb.jsonrequest
686 def read(self, request, model, fields, group_by_fields, domain=None, sort=None):
687 Model = request.session.model(model)
688 context, domain = eval_context_and_domain(request.session, request.context, domain)
690 return Model.read_group(
691 domain or [], fields, group_by_fields, 0, False,
692 dict(context, group_by=group_by_fields), sort or False)
694 class View(openerpweb.Controller):
695 _cp_path = "/base/view"
697 def fields_view_get(self, request, model, view_id, view_type,
698 transform=True, toolbar=False, submenu=False):
699 Model = request.session.model(model)
700 context = request.session.eval_context(request.context)
701 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
702 # todo fme?: check that we should pass the evaluated context here
703 self.process_view(request.session, fvg, context, transform)
706 def process_view(self, session, fvg, context, transform):
707 # depending on how it feels, xmlrpclib.ServerProxy can translate
708 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
709 # enjoy unicode strings which can not be trivially converted to
710 # strings, and it blows up during parsing.
712 # So ensure we fix this retardation by converting view xml back to
714 if isinstance(fvg['arch'], unicode):
715 arch = fvg['arch'].encode('utf-8')
720 evaluation_context = session.evaluation_context(context or {})
721 xml = self.transform_view(arch, session, evaluation_context)
723 xml = ElementTree.fromstring(arch)
724 fvg['arch'] = Xml2Json.convert_element(xml)
726 for field in fvg['fields'].itervalues():
727 if field.get('views'):
728 for view in field["views"].itervalues():
729 self.process_view(session, view, None, transform)
730 if field.get('domain'):
731 field["domain"] = self.parse_domain(field["domain"], session)
732 if field.get('context'):
733 field["context"] = self.parse_context(field["context"], session)
735 @openerpweb.jsonrequest
736 def add_custom(self, request, view_id, arch):
737 CustomView = request.session.model('ir.ui.view.custom')
739 'user_id': request.session._uid,
742 }, request.session.eval_context(request.context))
743 return {'result': True}
745 @openerpweb.jsonrequest
746 def undo_custom(self, request, view_id, reset=False):
747 CustomView = request.session.model('ir.ui.view.custom')
748 context = request.session.eval_context(request.context)
749 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
750 0, False, False, context)
753 CustomView.unlink(vcustom, context)
755 CustomView.unlink([vcustom[0]], context)
756 return {'result': True}
757 return {'result': False}
759 def transform_view(self, view_string, session, context=None):
760 # transform nodes on the fly via iterparse, instead of
761 # doing it statically on the parsing result
762 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
764 for event, elem in parser:
768 self.parse_domains_and_contexts(elem, session)
771 def parse_domain(self, domain, session):
772 """ Parses an arbitrary string containing a domain, transforms it
773 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
775 :param domain: the domain to parse, if the domain is not a string it
776 is assumed to be a literal domain and is returned as-is
777 :param session: Current OpenERP session
778 :type session: openerpweb.openerpweb.OpenERPSession
780 if not isinstance(domain, (str, unicode)):
783 return openerpweb.ast.literal_eval(domain)
786 return openerpweb.nonliterals.Domain(session, domain)
788 def parse_context(self, context, session):
789 """ Parses an arbitrary string containing a context, transforms it
790 to either a literal context or a :class:`openerpweb.nonliterals.Context`
792 :param context: the context to parse, if the context is not a string it
793 is assumed to be a literal domain and is returned as-is
794 :param session: Current OpenERP session
795 :type session: openerpweb.openerpweb.OpenERPSession
797 if not isinstance(context, (str, unicode)):
800 return openerpweb.ast.literal_eval(context)
802 return openerpweb.nonliterals.Context(session, context)
804 def parse_domains_and_contexts(self, elem, session):
805 """ Converts domains and contexts from the view into Python objects,
806 either literals if they can be parsed by literal_eval or a special
807 placeholder object if the domain or context refers to free variables.
809 :param elem: the current node being parsed
810 :type param: xml.etree.ElementTree.Element
811 :param session: OpenERP session object, used to store and retrieve
813 :type session: openerpweb.openerpweb.OpenERPSession
815 for el in ['domain', 'filter_domain']:
816 domain = elem.get(el, '').strip()
818 elem.set(el, self.parse_domain(domain, session))
819 for el in ['context', 'default_get']:
820 context_string = elem.get(el, '').strip()
822 elem.set(el, self.parse_context(context_string, session))
824 class FormView(View):
825 _cp_path = "/base/formview"
827 @openerpweb.jsonrequest
828 def load(self, req, model, view_id, toolbar=False):
829 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
830 return {'fields_view': fields_view}
832 class ListView(View):
833 _cp_path = "/base/listview"
835 @openerpweb.jsonrequest
836 def load(self, req, model, view_id, toolbar=False):
837 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
838 return {'fields_view': fields_view}
840 def process_colors(self, view, row, context):
841 colors = view['arch']['attrs'].get('colors')
848 for pair in colors.split(';')
849 if eval(pair.split(':')[1], dict(context, **row))
854 elif len(color) == 1:
858 class SearchView(View):
859 _cp_path = "/base/searchview"
861 @openerpweb.jsonrequest
862 def load(self, req, model, view_id):
863 fields_view = self.fields_view_get(req, model, view_id, 'search')
864 return {'fields_view': fields_view}
866 @openerpweb.jsonrequest
867 def fields_get(self, req, model):
868 Model = req.session.model(model)
869 fields = Model.fields_get(False, req.session.eval_context(req.context))
870 for field in fields.values():
871 # shouldn't convert the views too?
872 if field.get('domain'):
873 field["domain"] = self.parse_domain(field["domain"], req.session)
874 if field.get('context'):
875 field["context"] = self.parse_domain(field["context"], req.session)
876 return {'fields': fields}
878 @openerpweb.jsonrequest
879 def get_filters(self, req, model):
880 Model = req.session.model("ir.filters")
881 filters = Model.get_filters(model)
882 for filter in filters:
883 filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session))
884 filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session))
887 @openerpweb.jsonrequest
888 def save_filter(self, req, model, name, context_to_save, domain):
889 Model = req.session.model("ir.filters")
890 ctx = openerpweb.nonliterals.CompoundContext(context_to_save)
891 ctx.session = req.session
893 domain = openerpweb.nonliterals.CompoundDomain(domain)
894 domain.session = req.session
895 domain = domain.evaluate()
896 uid = req.session._uid
897 context = req.session.eval_context(req.context)
898 to_return = Model.create_or_replace({"context": ctx,
906 class Binary(openerpweb.Controller):
907 _cp_path = "/base/binary"
909 @openerpweb.httprequest
910 def image(self, request, session_id, model, id, field, **kw):
911 cherrypy.response.headers['Content-Type'] = 'image/png'
912 Model = request.session.model(model)
913 context = request.session.eval_context(request.context)
916 res = Model.default_get([field], context).get(field, '')
918 res = Model.read([int(id)], [field], context)[0].get(field, '')
919 return base64.decodestring(res)
920 except: # TODO: what's the exception here?
921 return self.placeholder()
922 def placeholder(self):
923 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
925 @openerpweb.httprequest
926 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
927 Model = request.session.model(model)
928 context = request.session.eval_context(request.context)
929 res = Model.read([int(id)], [field, fieldname], context)[0]
930 filecontent = res.get(field, '')
932 raise cherrypy.NotFound
934 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
935 filename = '%s_%s' % (model.replace('.', '_'), id)
937 filename = res.get(fieldname, '') or filename
938 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
939 return base64.decodestring(filecontent)
941 @openerpweb.httprequest
942 def upload(self, request, session_id, callback, ufile=None):
943 cherrypy.response.timeout = 500
945 for key, val in cherrypy.request.headers.iteritems():
946 headers[key.lower()] = val
947 size = int(headers.get('content-length', 0))
948 # TODO: might be useful to have a configuration flag for max-length file uploads
950 out = """<script language="javascript" type="text/javascript">
951 var win = window.top.window,
953 if (typeof(callback) === 'function') {
954 callback.apply(this, %s);
956 win.jQuery('#oe_notification', win.document).notify('create', {
957 title: "Ajax File Upload",
958 text: "Could not find callback"
962 data = ufile.file.read()
963 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
965 args = [False, e.message]
966 return out % (simplejson.dumps(callback), simplejson.dumps(args))
968 @openerpweb.httprequest
969 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
970 cherrypy.response.timeout = 500
971 context = request.session.eval_context(request.context)
972 Model = request.session.model('ir.attachment')
974 out = """<script language="javascript" type="text/javascript">
975 var win = window.top.window,
977 if (typeof(callback) === 'function') {
978 callback.call(this, %s);
981 attachment_id = Model.create({
982 'name': ufile.filename,
983 'datas': base64.encodestring(ufile.file.read()),
988 'filename': ufile.filename,
992 args = { 'error': e.message }
993 return out % (simplejson.dumps(callback), simplejson.dumps(args))
995 class Action(openerpweb.Controller):
996 _cp_path = "/base/action"
998 @openerpweb.jsonrequest
999 def load(self, req, action_id):
1000 Actions = req.session.model('ir.actions.actions')
1002 context = req.session.eval_context(req.context)
1003 action_type = Actions.read([action_id], ['type'], context)
1005 action = req.session.model(action_type[0]['type']).read([action_id], False,
1008 value = clean_action(action[0], req.session)
1009 return {'result': value}
1011 @openerpweb.jsonrequest
1012 def run(self, req, action_id):
1013 return clean_action(req.session.model('ir.actions.server').run(
1014 [action_id], req.session.eval_context(req.context)), req.session)
1016 class TreeView(View):
1017 _cp_path = "/base/treeview"
1019 @openerpweb.jsonrequest
1020 def load(self, req, model, view_id, toolbar=False):
1021 return self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
1023 @openerpweb.jsonrequest
1024 def action(self, req, model, id):
1025 return load_actions_from_ir_values(
1026 req,'action', 'tree_but_open',[(model, id)],
1027 False, req.session.eval_context(req.context))
1029 def export_csv(fields, result):
1031 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1033 writer.writerow(fields)
1038 if isinstance(d, basestring):
1039 d = d.replace('\n',' ').replace('\t',' ')
1041 d = d.encode('utf-8')
1044 if d is False: d = None
1046 writer.writerow(row)
1053 def export_xls(fieldnames, table):
1057 common.error(_('Import Error.'), _('Please install xlwt library to export to MS Excel.'))
1059 workbook = xlwt.Workbook()
1060 worksheet = workbook.add_sheet('Sheet 1')
1062 for i, fieldname in enumerate(fieldnames):
1063 worksheet.write(0, i, str(fieldname))
1064 worksheet.col(i).width = 8000 # around 220 pixels
1066 style = xlwt.easyxf('align: wrap yes')
1068 for row_index, row in enumerate(table):
1069 for cell_index, cell_value in enumerate(row):
1070 cell_value = str(cell_value)
1071 cell_value = re.sub("\r", " ", cell_value)
1072 worksheet.write(row_index + 1, cell_index, cell_value, style)
1080 #return data.decode('ISO-8859-1')
1081 return unicode(data, 'utf-8', 'replace')
1084 _cp_path = "/base/export"
1086 def fields_get(self, req, model):
1087 Model = req.session.model(model)
1088 fields = Model.fields_get(False, req.session.eval_context(req.context))
1091 @openerpweb.jsonrequest
1092 def get_fields(self, req, model, prefix='', name= '', field_parent=None, params={}):
1093 import_compat = params.get("import_compat", False)
1095 fields = self.fields_get(req, model)
1096 field_parent_type = params.get("parent_field_type",False)
1098 if import_compat and field_parent_type and field_parent_type == "many2one":
1101 fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1103 fields_order = fields.keys()
1104 fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1106 for index, field in enumerate(fields_order):
1107 value = fields[field]
1109 if import_compat and value.get('readonly', False):
1111 for sl in value.get('states', {}).values():
1113 ok = ok or (s==['readonly',False])
1116 id = prefix + (prefix and '/'or '') + field
1117 nm = name + (name and '/' or '') + value['string']
1118 record.update(id=id, string= nm, action='javascript: void(0)',
1119 target=None, icon=None, children=[], field_type=value.get('type',False), required=value.get('required', False))
1120 records.append(record)
1122 if len(nm.split('/')) < 3 and value.get('relation', False):
1124 ref = value.pop('relation')
1125 cfields = self.fields_get(req, ref)
1126 if (value['type'] == 'many2many'):
1127 record['children'] = []
1128 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1130 elif value['type'] == 'many2one':
1131 record['children'] = [id + '/id', id + '/.id']
1132 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1135 cfields_order = cfields.keys()
1136 cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1138 for j, fld in enumerate(cfields_order):
1139 cid = id + '/' + fld
1140 cid = cid.replace(' ', '_')
1141 children.append(cid)
1142 record['children'] = children or []
1143 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1145 ref = value.pop('relation')
1146 cfields = self.fields_get(req, ref)
1147 cfields_order = cfields.keys()
1148 cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1150 for j, fld in enumerate(cfields_order):
1151 cid = id + '/' + fld
1152 cid = cid.replace(' ', '_')
1153 children.append(cid)
1154 record['children'] = children or []
1155 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1160 @openerpweb.jsonrequest
1161 def save_export_lists(self, req, name, model, field_list):
1162 result = {'resource':model, 'name':name, 'export_fields': []}
1163 for field in field_list:
1164 result['export_fields'].append((0, 0, {'name': field}))
1165 return req.session.model("ir.exports").create(result, req.session.eval_context(req.context))
1167 @openerpweb.jsonrequest
1168 def exist_export_lists(self, req, model):
1169 export_model = req.session.model("ir.exports")
1170 return export_model.read(export_model.search([('resource', '=', model)]), ['name'])
1172 @openerpweb.jsonrequest
1173 def delete_export(self, req, export_id):
1174 req.session.model("ir.exports").unlink(export_id, req.session.eval_context(req.context))
1177 @openerpweb.jsonrequest
1178 def namelist(self,req, model, export_id):
1180 result = self.get_data(req, model, req.session.eval_context(req.context))
1181 ir_export_obj = req.session.model("ir.exports")
1182 ir_export_line_obj = req.session.model("ir.exports.line")
1184 field = ir_export_obj.read(export_id)
1185 fields = ir_export_line_obj.read(field['export_fields'])
1188 [name_list.update({field['name']: result.get(field['name'])}) for field in fields]
1191 def get_data(self, req, model, context=None):
1193 context = context or {}
1195 proxy = req.session.model(model)
1196 fields = self.fields_get(req, model)
1198 f1 = proxy.fields_view_get(False, 'tree', context)['fields']
1199 f2 = proxy.fields_view_get(False, 'form', context)['fields']
1203 fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1206 _fields = {'id': 'ID' , '.id': 'Database ID' }
1207 def model_populate(fields, prefix_node='', prefix=None, prefix_value='', level=2):
1208 fields_order = fields.keys()
1209 fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1211 for field in fields_order:
1212 fields_data[prefix_node+field] = fields[field]
1214 fields_data[prefix_node + field]['string'] = '%s%s' % (prefix_value, fields_data[prefix_node + field]['string'])
1215 st_name = fields[field]['string'] or field
1216 _fields[prefix_node+field] = st_name
1217 if fields[field].get('relation', False) and level>0:
1218 fields2 = self.fields_get(req, fields[field]['relation'])
1219 fields2.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1220 model_populate(fields2, prefix_node+field+'/', None, st_name+'/', level-1)
1221 model_populate(fields)
1225 @openerpweb.jsonrequest
1226 def export_data(self, req, model, fields, ids, domain, import_compat=False, export_format="csv", context=None):
1227 context = req.session.eval_context(req.context)
1228 modle_obj = req.session.model(model)
1229 ids = ids or modle_obj.search(domain, context=context)
1231 field = fields.keys()
1232 result = modle_obj.export_data(ids, field , context).get('datas',[])
1234 if not import_compat:
1235 field = [val.strip() for val in fields.values()]
1237 if export_format == 'xls':
1238 return export_xls(field, result)
1240 return export_csv(field, result)