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))
181 create_attrs = operator.itemgetter(
182 'super_admin_pwd', 'db_name', 'demo_data', 'db_lang', 'create_admin_pwd')(
186 return req.session.proxy("db").create(*create_attrs)
187 except xmlrpclib.Fault, e:
188 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
189 return {'error': e.faultCode, 'title': 'Create Database'}
190 return {'error': 'Could not create database !', 'title': 'Create Database'}
192 @openerpweb.jsonrequest
193 def drop(self, req, fields):
194 password, db = operator.itemgetter(
195 'drop_pwd', 'drop_db')(
196 dict(map(operator.itemgetter('name', 'value'), fields)))
199 return req.session.proxy("db").drop(password, db)
200 except xmlrpclib.Fault, e:
201 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
202 return {'error': e.faultCode, 'title': 'Drop Database'}
203 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
205 @openerpweb.httprequest
206 def backup(self, req, backup_db, backup_pwd, token):
208 db_dump = base64.decodestring(
209 req.session.proxy("db").dump(backup_pwd, backup_db))
210 cherrypy.response.headers['Content-Type'] = "application/octet-stream; charset=binary"
211 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename="' + backup_db + '.dump"'
212 cherrypy.response.cookie['fileToken'] = token
213 cherrypy.response.cookie['fileToken']['path'] = '/'
215 except xmlrpclib.Fault, e:
216 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
217 return 'Backup Database|' + e.faultCode
218 return 'Backup Database|Could not generate database backup'
220 @openerpweb.httprequest
221 def restore(self, req, db_file, restore_pwd, new_db):
223 data = base64.encodestring(db_file.file.read())
224 req.session.proxy("db").restore(restore_pwd, new_db, data)
226 except xmlrpclib.Fault, e:
227 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
228 raise cherrypy.HTTPError(403)
230 raise cherrypy.HTTPError()
232 @openerpweb.jsonrequest
233 def change_password(self, req, fields):
234 old_password, new_password = operator.itemgetter(
235 'old_pwd', 'new_pwd')(
236 dict(map(operator.itemgetter('name', 'value'), fields)))
238 return req.session.proxy("db").change_admin_password(old_password, new_password)
239 except xmlrpclib.Fault, e:
240 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
241 return {'error': e.faultCode, 'title': 'Change Password'}
242 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
244 class Session(openerpweb.Controller):
245 _cp_path = "/base/session"
247 @openerpweb.jsonrequest
248 def login(self, req, db, login, password):
249 req.session.login(db, login, password)
252 "session_id": req.session_id,
253 "uid": req.session._uid,
256 @openerpweb.jsonrequest
257 def sc_list(self, req):
258 return req.session.model('ir.ui.view_sc').get_sc(req.session._uid, "ir.ui.menu",
259 req.session.eval_context(req.context))
261 @openerpweb.jsonrequest
262 def get_lang_list(self, req):
265 'lang_list': (req.session.proxy("db").list_lang() or []),
269 return {"error": e, "title": "Languages"}
271 @openerpweb.jsonrequest
272 def modules(self, req):
273 # TODO query server for installed web modules
275 for name, manifest in openerpweb.addons_manifest.items():
276 if name != 'base' and manifest.get('active', True):
280 @openerpweb.jsonrequest
281 def eval_domain_and_context(self, req, contexts, domains,
283 """ Evaluates sequences of domains and contexts, composing them into
284 a single context, domain or group_by sequence.
286 :param list contexts: list of contexts to merge together. Contexts are
287 evaluated in sequence, all previous contexts
288 are part of their own evaluation context
289 (starting at the session context).
290 :param list domains: list of domains to merge together. Domains are
291 evaluated in sequence and appended to one another
292 (implicit AND), their evaluation domain is the
293 result of merging all contexts.
294 :param list group_by_seq: list of domains (which may be in a different
295 order than the ``contexts`` parameter),
296 evaluated in sequence, their ``'group_by'``
297 key is extracted if they have one.
302 the global context created by merging all of
306 the concatenation of all domains
309 a list of fields to group by, potentially empty (in which case
310 no group by should be performed)
312 context, domain = eval_context_and_domain(req.session,
313 openerpweb.nonliterals.CompoundContext(*(contexts or [])),
314 openerpweb.nonliterals.CompoundDomain(*(domains or [])))
316 group_by_sequence = []
317 for candidate in (group_by_seq or []):
318 ctx = req.session.eval_context(candidate, context)
319 group_by = ctx.get('group_by')
322 elif isinstance(group_by, basestring):
323 group_by_sequence.append(group_by)
325 group_by_sequence.extend(group_by)
330 'group_by': group_by_sequence
333 @openerpweb.jsonrequest
334 def save_session_action(self, req, the_action):
336 This method store an action object in the session object and returns an integer
337 identifying that action. The method get_session_action() can be used to get
340 :param the_action: The action to save in the session.
341 :type the_action: anything
342 :return: A key identifying the saved action.
345 saved_actions = cherrypy.session.get('saved_actions')
346 if not saved_actions:
347 saved_actions = {"next":0, "actions":{}}
348 cherrypy.session['saved_actions'] = saved_actions
349 # we don't allow more than 10 stored actions
350 if len(saved_actions["actions"]) >= 10:
351 del saved_actions["actions"][min(saved_actions["actions"].keys())]
352 key = saved_actions["next"]
353 saved_actions["actions"][key] = the_action
354 saved_actions["next"] = key + 1
357 @openerpweb.jsonrequest
358 def get_session_action(self, req, key):
360 Gets back a previously saved action. This method can return None if the action
361 was saved since too much time (this case should be handled in a smart way).
363 :param key: The key given by save_session_action()
365 :return: The saved action or None.
368 saved_actions = cherrypy.session.get('saved_actions')
369 if not saved_actions:
371 return saved_actions["actions"].get(key)
373 @openerpweb.jsonrequest
374 def check(self, req):
375 req.session.assert_valid()
378 def eval_context_and_domain(session, context, domain=None):
379 e_context = session.eval_context(context)
380 # should we give the evaluated context as an evaluation context to the domain?
381 e_domain = session.eval_domain(domain or [])
383 return e_context, e_domain
385 def load_actions_from_ir_values(req, key, key2, models, meta, context):
386 Values = req.session.model('ir.values')
387 actions = Values.get(key, key2, models, meta, context)
389 return [(id, name, clean_action(action, req.session))
390 for id, name, action in actions]
392 def clean_action(action, session):
393 action.setdefault('flags', {})
394 if action['type'] != 'ir.actions.act_window':
396 # values come from the server, we can just eval them
397 if isinstance(action.get('context'), basestring):
398 action['context'] = eval(
400 session.evaluation_context()) or {}
402 if isinstance(action.get('domain'), basestring):
403 action['domain'] = eval(
405 session.evaluation_context(
406 action.get('context', {}))) or []
408 return fix_view_modes(action)
410 def generate_views(action):
412 While the server generates a sequence called "views" computing dependencies
413 between a bunch of stuff for views coming directly from the database
414 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
415 to return custom view dictionaries generated on the fly.
417 In that case, there is no ``views`` key available on the action.
419 Since the web client relies on ``action['views']``, generate it here from
420 ``view_mode`` and ``view_id``.
422 Currently handles two different cases:
424 * no view_id, multiple view_mode
425 * single view_id, single view_mode
427 :param dict action: action descriptor dictionary to generate a views key for
429 view_id = action.get('view_id', False)
430 if isinstance(view_id, (list, tuple)):
433 # providing at least one view mode is a requirement, not an option
434 view_modes = action['view_mode'].split(',')
436 if len(view_modes) > 1:
438 raise ValueError('Non-db action dictionaries should provide '
439 'either multiple view modes or a single view '
440 'mode and an optional view id.\n\n Got view '
441 'modes %r and view id %r for action %r' % (
442 view_modes, view_id, action))
443 action['views'] = [(False, mode) for mode in view_modes]
445 action['views'] = [(view_id, view_modes[0])]
447 def fix_view_modes(action):
448 """ For historical reasons, OpenERP has weird dealings in relation to
449 view_mode and the view_type attribute (on window actions):
451 * one of the view modes is ``tree``, which stands for both list views
453 * the choice is made by checking ``view_type``, which is either
454 ``form`` for a list view or ``tree`` for an actual tree view
456 This methods simply folds the view_type into view_mode by adding a
457 new view mode ``list`` which is the result of the ``tree`` view_mode
458 in conjunction with the ``form`` view_type.
460 TODO: this should go into the doc, some kind of "peculiarities" section
462 :param dict action: an action descriptor
463 :returns: nothing, the action is modified in place
465 if 'views' not in action:
466 generate_views(action)
468 if action.pop('view_type') != 'form':
472 [id, mode if mode != 'tree' else 'list']
473 for id, mode in action['views']
478 class Menu(openerpweb.Controller):
479 _cp_path = "/base/menu"
481 @openerpweb.jsonrequest
483 return {'data': self.do_load(req)}
485 def do_load(self, req):
486 """ Loads all menu items (all applications and their sub-menus).
488 :param req: A request object, with an OpenERP session attribute
489 :type req: < session -> OpenERPSession >
490 :return: the menu root
491 :rtype: dict('children': menu_nodes)
493 Menus = req.session.model('ir.ui.menu')
494 # menus are loaded fully unlike a regular tree view, cause there are
495 # less than 512 items
496 context = req.session.eval_context(req.context)
497 menu_ids = Menus.search([], 0, False, False, context)
498 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
499 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
500 menu_items.append(menu_root)
502 # make a tree using parent_id
503 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
504 for menu_item in menu_items:
505 if menu_item['parent_id']:
506 parent = menu_item['parent_id'][0]
509 if parent in menu_items_map:
510 menu_items_map[parent].setdefault(
511 'children', []).append(menu_item)
513 # sort by sequence a tree using parent_id
514 for menu_item in menu_items:
515 menu_item.setdefault('children', []).sort(
516 key=lambda x:x["sequence"])
520 @openerpweb.jsonrequest
521 def action(self, req, menu_id):
522 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
523 [('ir.ui.menu', menu_id)], False,
524 req.session.eval_context(req.context))
525 return {"action": actions}
527 class DataSet(openerpweb.Controller):
528 _cp_path = "/base/dataset"
530 @openerpweb.jsonrequest
531 def fields(self, req, model):
532 return {'fields': req.session.model(model).fields_get(False,
533 req.session.eval_context(req.context))}
535 @openerpweb.jsonrequest
536 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, sort=None):
537 return self.do_search_read(request, model, fields, offset, limit, domain, sort)
538 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None
540 """ Performs a search() followed by a read() (if needed) using the
541 provided search criteria
543 :param request: a JSON-RPC request object
544 :type request: openerpweb.JsonRequest
545 :param str model: the name of the model to search on
546 :param fields: a list of the fields to return in the result records
548 :param int offset: from which index should the results start being returned
549 :param int limit: the maximum number of records to return
550 :param list domain: the search domain for the query
551 :param list sort: sorting directives
552 :returns: A structure (dict) with two keys: ids (all the ids matching
553 the (domain, context) pair) and records (paginated records
554 matching fields selection set)
557 Model = request.session.model(model)
558 context, domain = eval_context_and_domain(
559 request.session, request.context, domain)
561 ids = Model.search(domain, 0, False, sort or False, context)
562 # need to fill the dataset with all ids for the (domain, context) pair,
563 # so search un-paginated and paginate manually before reading
564 paginated_ids = ids[offset:(offset + limit if limit else None)]
565 if fields and fields == ['id']:
566 # shortcut read if we only want the ids
569 'records': map(lambda id: {'id': id}, paginated_ids)
572 records = Model.read(paginated_ids, fields or False, context)
573 records.sort(key=lambda obj: ids.index(obj['id']))
580 @openerpweb.jsonrequest
581 def get(self, request, model, ids, fields=False):
582 return self.do_get(request, model, ids, fields)
583 def do_get(self, request, model, ids, fields=False):
584 """ Fetches and returns the records of the model ``model`` whose ids
587 The results are in the same order as the inputs, but elements may be
588 missing (if there is no record left for the id)
590 :param request: the JSON-RPC2 request object
591 :type request: openerpweb.JsonRequest
592 :param model: the model to read from
594 :param ids: a list of identifiers
596 :param fields: a list of fields to fetch, ``False`` or empty to fetch
597 all fields in the model
598 :type fields: list | False
599 :returns: a list of records, in the same order as the list of ids
602 Model = request.session.model(model)
603 records = Model.read(ids, fields, request.session.eval_context(request.context))
605 record_map = dict((record['id'], record) for record in records)
607 return [record_map[id] for id in ids if record_map.get(id)]
609 @openerpweb.jsonrequest
610 def load(self, req, model, id, fields):
611 m = req.session.model(model)
613 r = m.read([id], False, req.session.eval_context(req.context))
616 return {'value': value}
618 @openerpweb.jsonrequest
619 def create(self, req, model, data):
620 m = req.session.model(model)
621 r = m.create(data, req.session.eval_context(req.context))
624 @openerpweb.jsonrequest
625 def save(self, req, model, id, data):
626 m = req.session.model(model)
627 r = m.write([id], data, req.session.eval_context(req.context))
630 @openerpweb.jsonrequest
631 def unlink(self, request, model, ids=()):
632 Model = request.session.model(model)
633 return Model.unlink(ids, request.session.eval_context(request.context))
635 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
636 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
637 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
638 c, d = eval_context_and_domain(req.session, context, domain)
639 if domain_id and len(args) - 1 >= domain_id:
641 if context_id and len(args) - 1 >= context_id:
644 return getattr(req.session.model(model), method)(*args)
646 @openerpweb.jsonrequest
647 def call(self, req, model, method, args, domain_id=None, context_id=None):
648 return self.call_common(req, model, method, args, domain_id, context_id)
650 @openerpweb.jsonrequest
651 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
652 action = self.call_common(req, model, method, args, domain_id, context_id)
653 if isinstance(action, dict) and action.get('type') != '':
654 return {'result': clean_action(action, req.session)}
655 return {'result': False}
657 @openerpweb.jsonrequest
658 def exec_workflow(self, req, model, id, signal):
659 r = req.session.exec_workflow(model, id, signal)
662 @openerpweb.jsonrequest
663 def default_get(self, req, model, fields):
664 Model = req.session.model(model)
665 return Model.default_get(fields, req.session.eval_context(req.context))
667 class DataGroup(openerpweb.Controller):
668 _cp_path = "/base/group"
669 @openerpweb.jsonrequest
670 def read(self, request, model, fields, group_by_fields, domain=None, sort=None):
671 Model = request.session.model(model)
672 context, domain = eval_context_and_domain(request.session, request.context, domain)
674 return Model.read_group(
675 domain or [], fields, group_by_fields, 0, False,
676 dict(context, group_by=group_by_fields), sort or False)
678 class View(openerpweb.Controller):
679 _cp_path = "/base/view"
681 def fields_view_get(self, request, model, view_id, view_type,
682 transform=True, toolbar=False, submenu=False):
683 Model = request.session.model(model)
684 context = request.session.eval_context(request.context)
685 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
686 # todo fme?: check that we should pass the evaluated context here
687 self.process_view(request.session, fvg, context, transform)
690 def process_view(self, session, fvg, context, transform):
691 # depending on how it feels, xmlrpclib.ServerProxy can translate
692 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
693 # enjoy unicode strings which can not be trivially converted to
694 # strings, and it blows up during parsing.
696 # So ensure we fix this retardation by converting view xml back to
698 if isinstance(fvg['arch'], unicode):
699 arch = fvg['arch'].encode('utf-8')
704 evaluation_context = session.evaluation_context(context or {})
705 xml = self.transform_view(arch, session, evaluation_context)
707 xml = ElementTree.fromstring(arch)
708 fvg['arch'] = Xml2Json.convert_element(xml)
710 for field in fvg['fields'].itervalues():
711 if field.get('views'):
712 for view in field["views"].itervalues():
713 self.process_view(session, view, None, transform)
714 if field.get('domain'):
715 field["domain"] = self.parse_domain(field["domain"], session)
716 if field.get('context'):
717 field["context"] = self.parse_context(field["context"], session)
719 @openerpweb.jsonrequest
720 def add_custom(self, request, view_id, arch):
721 CustomView = request.session.model('ir.ui.view.custom')
723 'user_id': request.session._uid,
726 }, request.session.eval_context(request.context))
727 return {'result': True}
729 @openerpweb.jsonrequest
730 def undo_custom(self, request, view_id, reset=False):
731 CustomView = request.session.model('ir.ui.view.custom')
732 context = request.session.eval_context(request.context)
733 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
734 0, False, False, context)
737 CustomView.unlink(vcustom, context)
739 CustomView.unlink([vcustom[0]], context)
740 return {'result': True}
741 return {'result': False}
743 def transform_view(self, view_string, session, context=None):
744 # transform nodes on the fly via iterparse, instead of
745 # doing it statically on the parsing result
746 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
748 for event, elem in parser:
752 self.parse_domains_and_contexts(elem, session)
755 def parse_domain(self, domain, session):
756 """ Parses an arbitrary string containing a domain, transforms it
757 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
759 :param domain: the domain to parse, if the domain is not a string it is assumed to
760 be a literal domain and is returned as-is
761 :param session: Current OpenERP session
762 :type session: openerpweb.openerpweb.OpenERPSession
764 if not isinstance(domain, (str, unicode)):
767 return openerpweb.ast.literal_eval(domain)
770 return openerpweb.nonliterals.Domain(session, domain)
772 def parse_context(self, context, session):
773 """ Parses an arbitrary string containing a context, transforms it
774 to either a literal context or a :class:`openerpweb.nonliterals.Context`
776 :param context: the context to parse, if the context is not a string it is assumed to
777 be a literal domain and is returned as-is
778 :param session: Current OpenERP session
779 :type session: openerpweb.openerpweb.OpenERPSession
781 if not isinstance(context, (str, unicode)):
784 return openerpweb.ast.literal_eval(context)
786 return openerpweb.nonliterals.Context(session, context)
788 def parse_domains_and_contexts(self, elem, session):
789 """ Converts domains and contexts from the view into Python objects,
790 either literals if they can be parsed by literal_eval or a special
791 placeholder object if the domain or context refers to free variables.
793 :param elem: the current node being parsed
794 :type param: xml.etree.ElementTree.Element
795 :param session: OpenERP session object, used to store and retrieve
797 :type session: openerpweb.openerpweb.OpenERPSession
799 for el in ['domain', 'filter_domain']:
800 domain = elem.get(el, '').strip()
802 elem.set(el, self.parse_domain(domain, session))
803 for el in ['context', 'default_get']:
804 context_string = elem.get(el, '').strip()
806 elem.set(el, self.parse_context(context_string, session))
808 class FormView(View):
809 _cp_path = "/base/formview"
811 @openerpweb.jsonrequest
812 def load(self, req, model, view_id, toolbar=False):
813 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
814 return {'fields_view': fields_view}
816 class ListView(View):
817 _cp_path = "/base/listview"
819 @openerpweb.jsonrequest
820 def load(self, req, model, view_id, toolbar=False):
821 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
822 return {'fields_view': fields_view}
824 def process_colors(self, view, row, context):
825 colors = view['arch']['attrs'].get('colors')
832 for pair in colors.split(';')
833 if eval(pair.split(':')[1], dict(context, **row))
838 elif len(color) == 1:
842 class SearchView(View):
843 _cp_path = "/base/searchview"
845 @openerpweb.jsonrequest
846 def load(self, req, model, view_id):
847 fields_view = self.fields_view_get(req, model, view_id, 'search')
848 return {'fields_view': fields_view}
850 @openerpweb.jsonrequest
851 def fields_get(self, req, model):
852 Model = req.session.model(model)
853 fields = Model.fields_get(False, req.session.eval_context(req.context))
854 for field in fields.values():
855 # shouldn't convert the views too?
856 if field.get('domain'):
857 field["domain"] = self.parse_domain(field["domain"], req.session)
858 if field.get('context'):
859 field["context"] = self.parse_domain(field["context"], req.session)
860 return {'fields': fields}
862 @openerpweb.jsonrequest
863 def get_filters(self, req, model):
864 Model = req.session.model("ir.filters")
865 filters = Model.get_filters(model)
866 for filter in filters:
867 filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session))
868 filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session))
871 @openerpweb.jsonrequest
872 def save_filter(self, req, model, name, context_to_save, domain):
873 Model = req.session.model("ir.filters")
874 ctx = openerpweb.nonliterals.CompoundContext(context_to_save)
875 ctx.session = req.session
877 domain = openerpweb.nonliterals.CompoundDomain(domain)
878 domain.session = req.session
879 domain = domain.evaluate()
880 uid = req.session._uid
881 context = req.session.eval_context(req.context)
882 to_return = Model.create_or_replace({"context": ctx,
890 class Binary(openerpweb.Controller):
891 _cp_path = "/base/binary"
893 @openerpweb.httprequest
894 def image(self, request, session_id, model, id, field, **kw):
895 cherrypy.response.headers['Content-Type'] = 'image/png'
896 Model = request.session.model(model)
897 context = request.session.eval_context(request.context)
900 res = Model.default_get([field], context).get(field, '')
902 res = Model.read([int(id)], [field], context)[0].get(field, '')
903 return base64.decodestring(res)
904 except: # TODO: what's the exception here?
905 return self.placeholder()
906 def placeholder(self):
907 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
909 @openerpweb.httprequest
910 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
911 Model = request.session.model(model)
912 context = request.session.eval_context(request.context)
913 res = Model.read([int(id)], [field, fieldname], context)[0]
914 filecontent = res.get(field, '')
916 raise cherrypy.NotFound
918 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
919 filename = '%s_%s' % (model.replace('.', '_'), id)
921 filename = res.get(fieldname, '') or filename
922 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
923 return base64.decodestring(filecontent)
925 @openerpweb.httprequest
926 def upload(self, request, session_id, callback, ufile=None):
927 cherrypy.response.timeout = 500
929 for key, val in cherrypy.request.headers.iteritems():
930 headers[key.lower()] = val
931 size = int(headers.get('content-length', 0))
932 # TODO: might be useful to have a configuration flag for max-length file uploads
934 out = """<script language="javascript" type="text/javascript">
935 var win = window.top.window,
937 if (typeof(callback) === 'function') {
938 callback.apply(this, %s);
940 win.jQuery('#oe_notification', win.document).notify('create', {
941 title: "Ajax File Upload",
942 text: "Could not find callback"
946 data = ufile.file.read()
947 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
949 args = [False, e.message]
950 return out % (simplejson.dumps(callback), simplejson.dumps(args))
952 @openerpweb.httprequest
953 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
954 cherrypy.response.timeout = 500
955 context = request.session.eval_context(request.context)
956 Model = request.session.model('ir.attachment')
958 out = """<script language="javascript" type="text/javascript">
959 var win = window.top.window,
961 if (typeof(callback) === 'function') {
962 callback.call(this, %s);
965 attachment_id = Model.create({
966 'name': ufile.filename,
967 'datas': base64.encodestring(ufile.file.read()),
972 'filename': ufile.filename,
976 args = { 'error': e.message }
977 return out % (simplejson.dumps(callback), simplejson.dumps(args))
979 class Action(openerpweb.Controller):
980 _cp_path = "/base/action"
982 @openerpweb.jsonrequest
983 def load(self, req, action_id):
984 Actions = req.session.model('ir.actions.actions')
986 context = req.session.eval_context(req.context)
987 action_type = Actions.read([action_id], ['type'], context)
989 action = req.session.model(action_type[0]['type']).read([action_id], False,
992 value = clean_action(action[0], req.session)
993 return {'result': value}
995 @openerpweb.jsonrequest
996 def run(self, req, action_id):
997 return clean_action(req.session.model('ir.actions.server').run(
998 [action_id], req.session.eval_context(req.context)), req.session)
1000 def export_csv(fields, result):
1002 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1004 writer.writerow(fields)
1009 if isinstance(d, basestring):
1010 d = d.replace('\n',' ').replace('\t',' ')
1012 d = d.encode('utf-8')
1015 if d is False: d = None
1017 writer.writerow(row)
1024 def export_xls(fieldnames, table):
1028 common.error(_('Import Error.'), _('Please install xlwt library to export to MS Excel.'))
1030 workbook = xlwt.Workbook()
1031 worksheet = workbook.add_sheet('Sheet 1')
1033 for i, fieldname in enumerate(fieldnames):
1034 worksheet.write(0, i, str(fieldname))
1035 worksheet.col(i).width = 8000 # around 220 pixels
1037 style = xlwt.easyxf('align: wrap yes')
1039 for row_index, row in enumerate(table):
1040 for cell_index, cell_value in enumerate(row):
1041 cell_value = str(cell_value)
1042 cell_value = re.sub("\r", " ", cell_value)
1043 worksheet.write(row_index + 1, cell_index, cell_value, style)
1051 #return data.decode('ISO-8859-1')
1052 return unicode(data, 'utf-8', 'replace')
1055 _cp_path = "/base/export"
1057 def fields_get(self, req, model):
1058 Model = req.session.model(model)
1059 fields = Model.fields_get(False, req.session.eval_context(req.context))
1062 @openerpweb.jsonrequest
1063 def get_fields(self, req, model, prefix='', name= '', field_parent=None, params={}):
1064 import_compat = params.get("import_compat", False)
1066 fields = self.fields_get(req, model)
1067 field_parent_type = params.get("parent_field_type",False)
1069 if import_compat and field_parent_type and field_parent_type == "many2one":
1072 fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1074 fields_order = fields.keys()
1075 fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1077 for index, field in enumerate(fields_order):
1078 value = fields[field]
1080 if import_compat and value.get('readonly', False):
1082 for sl in value.get('states', {}).values():
1084 ok = ok or (s==['readonly',False])
1087 id = prefix + (prefix and '/'or '') + field
1088 nm = name + (name and '/' or '') + value['string']
1089 record.update(id=id, string= nm, action='javascript: void(0)',
1090 target=None, icon=None, children=[], field_type=value.get('type',False), required=value.get('required', False))
1091 records.append(record)
1093 if len(nm.split('/')) < 3 and value.get('relation', False):
1095 ref = value.pop('relation')
1096 cfields = self.fields_get(req, ref)
1097 if (value['type'] == 'many2many'):
1098 record['children'] = []
1099 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1101 elif value['type'] == 'many2one':
1102 record['children'] = [id + '/id', id + '/.id']
1103 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1106 cfields_order = cfields.keys()
1107 cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1109 for j, fld in enumerate(cfields_order):
1110 cid = id + '/' + fld
1111 cid = cid.replace(' ', '_')
1112 children.append(cid)
1113 record['children'] = children or []
1114 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1116 ref = value.pop('relation')
1117 cfields = self.fields_get(req, ref)
1118 cfields_order = cfields.keys()
1119 cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1121 for j, fld in enumerate(cfields_order):
1122 cid = id + '/' + fld
1123 cid = cid.replace(' ', '_')
1124 children.append(cid)
1125 record['children'] = children or []
1126 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1131 @openerpweb.jsonrequest
1132 def save_export_lists(self, req, name, model, field_list):
1133 result = {'resource':model, 'name':name, 'export_fields': []}
1134 for field in field_list:
1135 result['export_fields'].append((0, 0, {'name': field}))
1136 return req.session.model("ir.exports").create(result, req.session.eval_context(req.context))
1138 @openerpweb.jsonrequest
1139 def exist_export_lists(self, req, model):
1140 export_model = req.session.model("ir.exports")
1141 return export_model.read(export_model.search([('resource', '=', model)]), ['name'])
1143 @openerpweb.jsonrequest
1144 def delete_export(self, req, export_id):
1145 req.session.model("ir.exports").unlink(export_id, req.session.eval_context(req.context))
1148 @openerpweb.jsonrequest
1149 def namelist(self,req, model, export_id):
1151 result = self.get_data(req, model, req.session.eval_context(req.context))
1152 ir_export_obj = req.session.model("ir.exports")
1153 ir_export_line_obj = req.session.model("ir.exports.line")
1155 field = ir_export_obj.read(export_id)
1156 fields = ir_export_line_obj.read(field['export_fields'])
1159 [name_list.update({field['name']: result.get(field['name'])}) for field in fields]
1162 def get_data(self, req, model, context=None):
1164 context = context or {}
1166 proxy = req.session.model(model)
1167 fields = self.fields_get(req, model)
1169 f1 = proxy.fields_view_get(False, 'tree', context)['fields']
1170 f2 = proxy.fields_view_get(False, 'form', context)['fields']
1174 fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1177 _fields = {'id': 'ID' , '.id': 'Database ID' }
1178 def model_populate(fields, prefix_node='', prefix=None, prefix_value='', level=2):
1179 fields_order = fields.keys()
1180 fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1182 for field in fields_order:
1183 fields_data[prefix_node+field] = fields[field]
1185 fields_data[prefix_node + field]['string'] = '%s%s' % (prefix_value, fields_data[prefix_node + field]['string'])
1186 st_name = fields[field]['string'] or field
1187 _fields[prefix_node+field] = st_name
1188 if fields[field].get('relation', False) and level>0:
1189 fields2 = self.fields_get(req, fields[field]['relation'])
1190 fields2.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1191 model_populate(fields2, prefix_node+field+'/', None, st_name+'/', level-1)
1192 model_populate(fields)
1196 @openerpweb.jsonrequest
1197 def export_data(self, req, model, fields, ids, domain, import_compat=False, export_format="csv", context=None):
1198 context = req.session.eval_context(req.context)
1199 modle_obj = req.session.model(model)
1200 ids = ids or modle_obj.search(domain, context=context)
1202 field = fields.keys()
1203 result = modle_obj.export_data(ids, field , context).get('datas',[])
1205 if not import_compat:
1206 field = [val.strip() for val in fields.values()]
1208 if export_format == 'xls':
1209 return export_xls(field, result)
1211 return export_csv(field, result)