1 # -*- coding: utf-8 -*-
2 import base64, glob, os, re
3 from xml.etree import ElementTree
4 from cStringIO import StringIO
10 import openerpweb.nonliterals
14 import xml.dom.minidom
16 # Should move to openerpweb.Xml2Json
19 # Simple and straightforward XML-to-JSON converter in Python
22 # URL: http://code.google.com/p/xml2json-direct/
24 def convert_to_json(s):
25 return simplejson.dumps(
26 Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
29 def convert_to_structure(s):
30 root = ElementTree.fromstring(s)
31 return Xml2Json.convert_element(root)
34 def convert_element(el, skip_whitespaces=True):
37 ns, name = el.tag.rsplit("}", 1)
39 res["namespace"] = ns[1:]
43 for k, v in el.items():
46 if el.text and (not skip_whitespaces or el.text.strip() != ''):
49 kids.append(Xml2Json.convert_element(kid))
50 if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''):
52 res["children"] = kids
55 #----------------------------------------------------------
56 # OpenERP Web base Controllers
57 #----------------------------------------------------------
59 def manifest_glob(addons, key):
62 globlist = openerpweb.addons_manifest.get(addon, {}).get(key, [])
64 for pattern in globlist:
65 for path in glob.glob(os.path.join(openerpweb.path_addons, addon, pattern)):
66 files.append(path[len(openerpweb.path_addons):])
69 def concat_files(file_list):
70 """ Concatenate file content
71 return (concat,timestamp)
72 concat: concatenation of file content
73 timestamp: max(os.path.getmtime of file_list)
75 root = openerpweb.path_root
79 fname = os.path.join(root, i)
80 ftime = os.path.getmtime(fname)
81 if ftime > files_timestamp:
82 files_timestamp = ftime
83 files_content = open(fname).read()
84 files_concat = "".join(files_content)
87 class WebClient(openerpweb.Controller):
88 _cp_path = "/base/webclient"
90 @openerpweb.jsonrequest
91 def csslist(self, req, mods='base'):
92 return manifest_glob(mods.split(','), 'css')
94 @openerpweb.jsonrequest
95 def jslist(self, req, mods='base'):
96 return manifest_glob(mods.split(','), 'js')
98 @openerpweb.httprequest
99 def css(self, req, mods='base'):
100 cherrypy.response.headers['Content-Type'] = 'text/css'
101 files = manifest_glob(mods.split(','), 'css')
102 concat = concat_files(files)[0]
103 # TODO request set the Date of last modif and Etag
106 @openerpweb.httprequest
107 def js(self, req, mods='base'):
108 cherrypy.response.headers['Content-Type'] = 'application/javascript'
109 files = manifest_glob(mods.split(','), 'js')
110 concat = concat_files(files)[0]
111 # TODO request set the Date of last modif and Etag
114 @openerpweb.httprequest
116 template ="""<!DOCTYPE html>
117 <html style="height: 100%%">
119 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
120 <title>OpenERP</title>
122 <script type="text/javascript">
124 QWeb = new QWeb2.Engine();
125 openerp.init().base.webclient("oe");
128 <link rel="shortcut icon" href="/base/static/src/img/favicon.ico" type="image/x-icon"/>
131 <link rel="stylesheet" href="/base/static/src/css/base-ie7.css" type="text/css"/>
134 <body id="oe" class="openerp"></body>
136 """.replace('\n'+' '*8,'\n')
139 jslist = ['/base/webclient/js']
141 jslist = manifest_glob(['base'], 'js')
142 js = "\n ".join(['<script type="text/javascript" src="%s"></script>'%i for i in jslist])
145 csslist = ['/base/webclient/css']
147 csslist = manifest_glob(['base'], 'css')
148 css = "\n ".join(['<link rel="stylesheet" href="%s">'%i for i in csslist])
149 r = template % (js, css)
152 class Database(openerpweb.Controller):
153 _cp_path = "/base/database"
155 @openerpweb.jsonrequest
156 def get_databases_list(self, req):
157 proxy = req.session.proxy("db")
159 h = req.httprequest.headers['Host'].split(':')[0]
161 r = cherrypy.config['openerp.dbfilter'].replace('%h',h).replace('%d',d)
163 dbs = [i for i in dbs if re.match(r,i)]
164 return {"db_list": dbs}
166 class Session(openerpweb.Controller):
167 _cp_path = "/base/session"
169 @openerpweb.jsonrequest
170 def login(self, req, db, login, password):
171 req.session.login(db, login, password)
174 "session_id": req.session_id,
175 "uid": req.session._uid,
178 @openerpweb.jsonrequest
179 def sc_list(self, req):
180 return req.session.model('ir.ui.view_sc').get_sc(req.session._uid, "ir.ui.menu",
181 req.session.eval_context(req.context))
183 @openerpweb.jsonrequest
184 def modules(self, req):
185 # TODO query server for installed web modules
187 for name, manifest in openerpweb.addons_manifest.items():
188 if name != 'base' and manifest.get('active', True):
192 @openerpweb.jsonrequest
193 def eval_domain_and_context(self, req, contexts, domains,
195 """ Evaluates sequences of domains and contexts, composing them into
196 a single context, domain or group_by sequence.
198 :param list contexts: list of contexts to merge together. Contexts are
199 evaluated in sequence, all previous contexts
200 are part of their own evaluation context
201 (starting at the session context).
202 :param list domains: list of domains to merge together. Domains are
203 evaluated in sequence and appended to one another
204 (implicit AND), their evaluation domain is the
205 result of merging all contexts.
206 :param list group_by_seq: list of domains (which may be in a different
207 order than the ``contexts`` parameter),
208 evaluated in sequence, their ``'group_by'``
209 key is extracted if they have one.
214 the global context created by merging all of
218 the concatenation of all domains
221 a list of fields to group by, potentially empty (in which case
222 no group by should be performed)
224 context, domain = eval_context_and_domain(req.session,
225 openerpweb.nonliterals.CompoundContext(*(contexts or [])),
226 openerpweb.nonliterals.CompoundDomain(*(domains or [])))
228 group_by_sequence = []
229 for candidate in (group_by_seq or []):
230 ctx = req.session.eval_context(candidate, context)
231 group_by = ctx.get('group_by')
234 elif isinstance(group_by, basestring):
235 group_by_sequence.append(group_by)
237 group_by_sequence.extend(group_by)
242 'group_by': group_by_sequence
245 @openerpweb.jsonrequest
246 def save_session_action(self, req, the_action):
248 This method store an action object in the session object and returns an integer
249 identifying that action. The method get_session_action() can be used to get
252 :param the_action: The action to save in the session.
253 :type the_action: anything
254 :return: A key identifying the saved action.
257 saved_actions = cherrypy.session.get('saved_actions')
258 if not saved_actions:
259 saved_actions = {"next":0, "actions":{}}
260 cherrypy.session['saved_actions'] = saved_actions
261 # we don't allow more than 10 stored actions
262 if len(saved_actions["actions"]) >= 10:
263 del saved_actions["actions"][min(saved_actions["actions"].keys())]
264 key = saved_actions["next"]
265 saved_actions["actions"][key] = the_action
266 saved_actions["next"] = key + 1
269 @openerpweb.jsonrequest
270 def get_session_action(self, req, key):
272 Gets back a previously saved action. This method can return None if the action
273 was saved since too much time (this case should be handled in a smart way).
275 :param key: The key given by save_session_action()
277 :return: The saved action or None.
280 saved_actions = cherrypy.session.get('saved_actions')
281 if not saved_actions:
283 return saved_actions["actions"].get(key)
285 def eval_context_and_domain(session, context, domain=None):
286 e_context = session.eval_context(context)
287 # should we give the evaluated context as an evaluation context to the domain?
288 e_domain = session.eval_domain(domain or [])
290 return e_context, e_domain
292 def load_actions_from_ir_values(req, key, key2, models, meta, context):
293 Values = req.session.model('ir.values')
294 actions = Values.get(key, key2, models, meta, context)
296 return [(id, name, clean_action(action, req.session))
297 for id, name, action in actions]
299 def clean_action(action, session):
300 if action['type'] != 'ir.actions.act_window':
302 # values come from the server, we can just eval them
303 if isinstance(action.get('context', None), basestring):
304 action['context'] = eval(
306 session.evaluation_context()) or {}
308 if isinstance(action.get('domain', None), basestring):
309 action['domain'] = eval(
311 session.evaluation_context(
312 action.get('context', {}))) or []
313 if 'flags' not in action:
314 # Set empty flags dictionary for web client.
315 action['flags'] = dict()
316 return fix_view_modes(action)
318 def generate_views(action):
320 While the server generates a sequence called "views" computing dependencies
321 between a bunch of stuff for views coming directly from the database
322 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
323 to return custom view dictionaries generated on the fly.
325 In that case, there is no ``views`` key available on the action.
327 Since the web client relies on ``action['views']``, generate it here from
328 ``view_mode`` and ``view_id``.
330 Currently handles two different cases:
332 * no view_id, multiple view_mode
333 * single view_id, single view_mode
335 :param dict action: action descriptor dictionary to generate a views key for
337 view_id = action.get('view_id', False)
338 if isinstance(view_id, (list, tuple)):
341 # providing at least one view mode is a requirement, not an option
342 view_modes = action['view_mode'].split(',')
344 if len(view_modes) > 1:
346 raise ValueError('Non-db action dictionaries should provide '
347 'either multiple view modes or a single view '
348 'mode and an optional view id.\n\n Got view '
349 'modes %r and view id %r for action %r' % (
350 view_modes, view_id, action))
351 action['views'] = [(False, mode) for mode in view_modes]
353 action['views'] = [(view_id, view_modes[0])]
355 def fix_view_modes(action):
356 """ For historical reasons, OpenERP has weird dealings in relation to
357 view_mode and the view_type attribute (on window actions):
359 * one of the view modes is ``tree``, which stands for both list views
361 * the choice is made by checking ``view_type``, which is either
362 ``form`` for a list view or ``tree`` for an actual tree view
364 This methods simply folds the view_type into view_mode by adding a
365 new view mode ``list`` which is the result of the ``tree`` view_mode
366 in conjunction with the ``form`` view_type.
368 TODO: this should go into the doc, some kind of "peculiarities" section
370 :param dict action: an action descriptor
371 :returns: nothing, the action is modified in place
373 if 'views' not in action:
374 generate_views(action)
376 if action.pop('view_type') != 'form':
380 [id, mode if mode != 'tree' else 'list']
381 for id, mode in action['views']
386 class Menu(openerpweb.Controller):
387 _cp_path = "/base/menu"
389 @openerpweb.jsonrequest
391 return {'data': self.do_load(req)}
393 def do_load(self, req):
394 """ Loads all menu items (all applications and their sub-menus).
396 :param req: A request object, with an OpenERP session attribute
397 :type req: < session -> OpenERPSession >
398 :return: the menu root
399 :rtype: dict('children': menu_nodes)
401 Menus = req.session.model('ir.ui.menu')
402 # menus are loaded fully unlike a regular tree view, cause there are
403 # less than 512 items
404 context = req.session.eval_context(req.context)
405 menu_ids = Menus.search([], 0, False, False, context)
406 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
407 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
408 menu_items.append(menu_root)
410 # make a tree using parent_id
411 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
412 for menu_item in menu_items:
413 if menu_item['parent_id']:
414 parent = menu_item['parent_id'][0]
417 if parent in menu_items_map:
418 menu_items_map[parent].setdefault(
419 'children', []).append(menu_item)
421 # sort by sequence a tree using parent_id
422 for menu_item in menu_items:
423 menu_item.setdefault('children', []).sort(
424 key=lambda x:x["sequence"])
428 @openerpweb.jsonrequest
429 def action(self, req, menu_id):
430 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
431 [('ir.ui.menu', menu_id)], False,
432 req.session.eval_context(req.context))
433 return {"action": actions}
435 class DataSet(openerpweb.Controller):
436 _cp_path = "/base/dataset"
438 @openerpweb.jsonrequest
439 def fields(self, req, model):
440 return {'fields': req.session.model(model).fields_get(False,
441 req.session.eval_context(req.context))}
443 @openerpweb.jsonrequest
444 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, sort=None):
445 return self.do_search_read(request, model, fields, offset, limit, domain, sort)
446 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None
448 """ Performs a search() followed by a read() (if needed) using the
449 provided search criteria
451 :param request: a JSON-RPC request object
452 :type request: openerpweb.JsonRequest
453 :param str model: the name of the model to search on
454 :param fields: a list of the fields to return in the result records
456 :param int offset: from which index should the results start being returned
457 :param int limit: the maximum number of records to return
458 :param list domain: the search domain for the query
459 :param list sort: sorting directives
460 :returns: A structure (dict) with two keys: ids (all the ids matching
461 the (domain, context) pair) and records (paginated records
462 matching fields selection set)
465 Model = request.session.model(model)
466 context, domain = eval_context_and_domain(
467 request.session, request.context, domain)
469 ids = Model.search(domain, 0, False, sort or False, context)
470 # need to fill the dataset with all ids for the (domain, context) pair,
471 # so search un-paginated and paginate manually before reading
472 paginated_ids = ids[offset:(offset + limit if limit else None)]
473 if fields and fields == ['id']:
474 # shortcut read if we only want the ids
477 'records': map(lambda id: {'id': id}, paginated_ids)
480 records = Model.read(paginated_ids, fields or False, context)
481 records.sort(key=lambda obj: ids.index(obj['id']))
488 @openerpweb.jsonrequest
489 def get(self, request, model, ids, fields=False):
490 return self.do_get(request, model, ids, fields)
491 def do_get(self, request, model, ids, fields=False):
492 """ Fetches and returns the records of the model ``model`` whose ids
495 The results are in the same order as the inputs, but elements may be
496 missing (if there is no record left for the id)
498 :param request: the JSON-RPC2 request object
499 :type request: openerpweb.JsonRequest
500 :param model: the model to read from
502 :param ids: a list of identifiers
504 :param fields: a list of fields to fetch, ``False`` or empty to fetch
505 all fields in the model
506 :type fields: list | False
507 :returns: a list of records, in the same order as the list of ids
510 Model = request.session.model(model)
511 records = Model.read(ids, fields, request.session.eval_context(request.context))
513 record_map = dict((record['id'], record) for record in records)
515 return [record_map[id] for id in ids if record_map.get(id)]
517 @openerpweb.jsonrequest
518 def load(self, req, model, id, fields):
519 m = req.session.model(model)
521 r = m.read([id], False, req.session.eval_context(req.context))
524 return {'value': value}
526 @openerpweb.jsonrequest
527 def create(self, req, model, data):
528 m = req.session.model(model)
529 r = m.create(data, req.session.eval_context(req.context))
532 @openerpweb.jsonrequest
533 def save(self, req, model, id, data):
534 m = req.session.model(model)
535 r = m.write([id], data, req.session.eval_context(req.context))
538 @openerpweb.jsonrequest
539 def unlink(self, request, model, ids=()):
540 Model = request.session.model(model)
541 return Model.unlink(ids, request.session.eval_context(request.context))
543 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
544 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
545 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
546 c, d = eval_context_and_domain(req.session, context, domain)
547 if domain_id and len(args) - 1 >= domain_id:
549 if context_id and len(args) - 1 >= context_id:
552 return getattr(req.session.model(model), method)(*args)
554 @openerpweb.jsonrequest
555 def call(self, req, model, method, args, domain_id=None, context_id=None):
556 return self.call_common(req, model, method, args, domain_id, context_id)
558 @openerpweb.jsonrequest
559 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
560 action = self.call_common(req, model, method, args, domain_id, context_id)
561 if isinstance(action, dict) and action.get('type') != '':
562 return {'result': clean_action(action, req.session)}
563 return {'result': False}
565 @openerpweb.jsonrequest
566 def exec_workflow(self, req, model, id, signal):
567 r = req.session.exec_workflow(model, id, signal)
570 @openerpweb.jsonrequest
571 def default_get(self, req, model, fields):
572 Model = req.session.model(model)
573 return Model.default_get(fields, req.session.eval_context(req.context))
575 class DataGroup(openerpweb.Controller):
576 _cp_path = "/base/group"
577 @openerpweb.jsonrequest
578 def read(self, request, model, fields, group_by_fields, domain=None, sort=None):
579 Model = request.session.model(model)
580 context, domain = eval_context_and_domain(request.session, request.context, domain)
582 return Model.read_group(
583 domain or [], fields, group_by_fields, 0, False,
584 dict(context, group_by=group_by_fields), sort or False)
586 class View(openerpweb.Controller):
587 _cp_path = "/base/view"
589 def fields_view_get(self, request, model, view_id, view_type,
590 transform=True, toolbar=False, submenu=False):
591 Model = request.session.model(model)
592 context = request.session.eval_context(request.context)
593 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
594 # todo fme?: check that we should pass the evaluated context here
595 self.process_view(request.session, fvg, context, transform)
598 def process_view(self, session, fvg, context, transform):
599 # depending on how it feels, xmlrpclib.ServerProxy can translate
600 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
601 # enjoy unicode strings which can not be trivially converted to
602 # strings, and it blows up during parsing.
604 # So ensure we fix this retardation by converting view xml back to
606 if isinstance(fvg['arch'], unicode):
607 arch = fvg['arch'].encode('utf-8')
612 evaluation_context = session.evaluation_context(context or {})
613 xml = self.transform_view(arch, session, evaluation_context)
615 xml = ElementTree.fromstring(arch)
616 fvg['arch'] = Xml2Json.convert_element(xml)
618 for field in fvg['fields'].itervalues():
619 if field.get('views'):
620 for view in field["views"].itervalues():
621 self.process_view(session, view, None, transform)
622 if field.get('domain'):
623 field["domain"] = self.parse_domain(field["domain"], session)
624 if field.get('context'):
625 field["context"] = self.parse_context(field["context"], session)
627 @openerpweb.jsonrequest
628 def add_custom(self, request, view_id, arch):
629 CustomView = request.session.model('ir.ui.view.custom')
631 'user_id': request.session._uid,
634 }, request.session.eval_context(request.context))
635 return {'result': True}
637 @openerpweb.jsonrequest
638 def undo_custom(self, request, view_id, reset=False):
639 CustomView = request.session.model('ir.ui.view.custom')
640 context = request.session.eval_context(request.context)
641 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
642 0, False, False, context)
645 CustomView.unlink(vcustom, context)
647 CustomView.unlink([vcustom[0]], context)
648 return {'result': True}
649 return {'result': False}
651 def transform_view(self, view_string, session, context=None):
652 # transform nodes on the fly via iterparse, instead of
653 # doing it statically on the parsing result
654 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
656 for event, elem in parser:
660 self.parse_domains_and_contexts(elem, session)
663 def parse_domain(self, domain, session):
664 """ Parses an arbitrary string containing a domain, transforms it
665 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
667 :param domain: the domain to parse, if the domain is not a string it is assumed to
668 be a literal domain and is returned as-is
669 :param session: Current OpenERP session
670 :type session: openerpweb.openerpweb.OpenERPSession
672 if not isinstance(domain, (str, unicode)):
675 return openerpweb.ast.literal_eval(domain)
678 return openerpweb.nonliterals.Domain(session, domain)
680 def parse_context(self, context, session):
681 """ Parses an arbitrary string containing a context, transforms it
682 to either a literal context or a :class:`openerpweb.nonliterals.Context`
684 :param context: the context to parse, if the context is not a string it is assumed to
685 be a literal domain and is returned as-is
686 :param session: Current OpenERP session
687 :type session: openerpweb.openerpweb.OpenERPSession
689 if not isinstance(context, (str, unicode)):
692 return openerpweb.ast.literal_eval(context)
694 return openerpweb.nonliterals.Context(session, context)
696 def parse_domains_and_contexts(self, elem, session):
697 """ Converts domains and contexts from the view into Python objects,
698 either literals if they can be parsed by literal_eval or a special
699 placeholder object if the domain or context refers to free variables.
701 :param elem: the current node being parsed
702 :type param: xml.etree.ElementTree.Element
703 :param session: OpenERP session object, used to store and retrieve
705 :type session: openerpweb.openerpweb.OpenERPSession
707 for el in ['domain', 'filter_domain']:
708 domain = elem.get(el, '').strip()
710 elem.set(el, self.parse_domain(domain, session))
711 for el in ['context', 'default_get']:
712 context_string = elem.get(el, '').strip()
714 elem.set(el, self.parse_context(context_string, session))
716 class FormView(View):
717 _cp_path = "/base/formview"
719 @openerpweb.jsonrequest
720 def load(self, req, model, view_id, toolbar=False):
721 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
722 return {'fields_view': fields_view}
724 class ListView(View):
725 _cp_path = "/base/listview"
727 @openerpweb.jsonrequest
728 def load(self, req, model, view_id, toolbar=False):
729 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
730 return {'fields_view': fields_view}
732 def process_colors(self, view, row, context):
733 colors = view['arch']['attrs'].get('colors')
740 for pair in colors.split(';')
741 if eval(pair.split(':')[1], dict(context, **row))
746 elif len(color) == 1:
750 class SearchView(View):
751 _cp_path = "/base/searchview"
753 @openerpweb.jsonrequest
754 def load(self, req, model, view_id):
755 fields_view = self.fields_view_get(req, model, view_id, 'search')
756 return {'fields_view': fields_view}
758 @openerpweb.jsonrequest
759 def fields_get(self, req, model):
760 Model = req.session.model(model)
761 fields = Model.fields_get(False, req.session.eval_context(req.context))
762 for field in fields.values():
763 # shouldn't convert the views too?
764 if field.get('domain'):
765 field["domain"] = self.parse_domain(field["domain"], req.session)
766 if field.get('context'):
767 field["context"] = self.parse_domain(field["context"], req.session)
768 return {'fields': fields}
770 @openerpweb.jsonrequest
771 def get_filters(self, req, model):
772 Model = req.session.model("ir.filters")
773 filters = Model.get_filters(model)
774 for filter in filters:
775 filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session))
776 filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session))
779 @openerpweb.jsonrequest
780 def save_filter(self, req, model, name, context_to_save, domain):
781 Model = req.session.model("ir.filters")
782 ctx = openerpweb.nonliterals.CompoundContext(context_to_save)
783 ctx.session = req.session
785 domain = openerpweb.nonliterals.CompoundDomain(domain)
786 domain.session = req.session
787 domain = domain.evaluate()
788 uid = req.session._uid
789 context = req.session.eval_context(req.context)
790 to_return = Model.create_or_replace({"context": ctx,
798 class Binary(openerpweb.Controller):
799 _cp_path = "/base/binary"
801 @openerpweb.httprequest
802 def image(self, request, session_id, model, id, field, **kw):
803 cherrypy.response.headers['Content-Type'] = 'image/png'
804 Model = request.session.model(model)
805 context = request.session.eval_context(request.context)
808 res = Model.default_get([field], context).get(field, '')
810 res = Model.read([int(id)], [field], context)[0].get(field, '')
811 return base64.decodestring(res)
812 except: # TODO: what's the exception here?
813 return self.placeholder()
814 def placeholder(self):
815 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
817 @openerpweb.httprequest
818 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
819 Model = request.session.model(model)
820 context = request.session.eval_context(request.context)
821 res = Model.read([int(id)], [field, fieldname], context)[0]
822 filecontent = res.get(field, '')
824 raise cherrypy.NotFound
826 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
827 filename = '%s_%s' % (model.replace('.', '_'), id)
829 filename = res.get(fieldname, '') or filename
830 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
831 return base64.decodestring(filecontent)
833 @openerpweb.httprequest
834 def upload(self, request, session_id, callback, ufile=None):
835 cherrypy.response.timeout = 500
837 for key, val in cherrypy.request.headers.iteritems():
838 headers[key.lower()] = val
839 size = int(headers.get('content-length', 0))
840 # TODO: might be useful to have a configuration flag for max-length file uploads
842 out = """<script language="javascript" type="text/javascript">
843 var win = window.top.window,
845 if (typeof(callback) === 'function') {
846 callback.apply(this, %s);
848 win.jQuery('#oe_notification', win.document).notify('create', {
849 title: "Ajax File Upload",
850 text: "Could not find callback"
854 data = ufile.file.read()
855 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
857 args = [False, e.message]
858 return out % (simplejson.dumps(callback), simplejson.dumps(args))
860 @openerpweb.httprequest
861 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
862 cherrypy.response.timeout = 500
863 context = request.session.eval_context(request.context)
864 Model = request.session.model('ir.attachment')
866 out = """<script language="javascript" type="text/javascript">
867 var win = window.top.window,
869 if (typeof(callback) === 'function') {
870 callback.call(this, %s);
873 attachment_id = Model.create({
874 'name': ufile.filename,
875 'datas': base64.encodestring(ufile.file.read()),
880 'filename': ufile.filename,
884 args = { 'error': e.message }
885 return out % (simplejson.dumps(callback), simplejson.dumps(args))
887 class Action(openerpweb.Controller):
888 _cp_path = "/base/action"
890 @openerpweb.jsonrequest
891 def load(self, req, action_id):
892 Actions = req.session.model('ir.actions.actions')
894 context = req.session.eval_context(req.context)
895 action_type = Actions.read([action_id], ['type'], context)
897 action = req.session.model(action_type[0]['type']).read([action_id], False,
900 value = clean_action(action[0], req.session)
901 return {'result': value}
903 @openerpweb.jsonrequest
904 def run(self, req, action_id):
905 return clean_action(req.session.model('ir.actions.server').run(
906 [action_id], req.session.eval_context(req.context)), req.session)
908 def export_csv(fields, result):
910 fp = StringIO.StringIO()
911 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
913 writer.writerow(fields)
918 if isinstance(d, basestring):
919 d = d.replace('\n',' ').replace('\t',' ')
921 d = d.encode('utf-8')
924 if d is False: d = None
933 def export_xls(fieldnames, table):
938 common.error(_('Import Error.'), _('Please install xlwt library to export to MS Excel.'))
940 workbook = xlwt.Workbook()
941 worksheet = workbook.add_sheet('Sheet 1')
943 for i, fieldname in enumerate(fieldnames):
944 worksheet.write(0, i, str(fieldname))
945 worksheet.col(i).width = 8000 # around 220 pixels
947 style = xlwt.easyxf('align: wrap yes')
949 for row_index, row in enumerate(table):
950 for cell_index, cell_value in enumerate(row):
951 cell_value = str(cell_value)
952 cell_value = re.sub("\r", " ", cell_value)
953 worksheet.write(row_index + 1, cell_index, cell_value, style)
956 fp = StringIO.StringIO()
961 #return data.decode('ISO-8859-1')
962 return unicode(data, 'utf-8', 'replace')
964 def node_attributes(node):
965 attrs = node.attributes
969 # localName can be a unicode string, we're using attribute names as
970 # **kwargs keys and python-level kwargs don't take unicode keys kindly
971 # (they blow up) so we need to ensure all keys are ``str``
972 return dict([(str(attrs.item(i).localName), attrs.item(i).nodeValue)
973 for i in range(attrs.length)])
975 def _fields_get_all(req, model, views, context=None):
980 def parse(root, fields):
981 for node in root.childNodes:
982 if node.nodeName in ('form', 'notebook', 'page', 'group', 'tree', 'hpaned', 'vpaned'):
984 elif node.nodeName=='field':
985 attrs = node_attributes(node)
987 fields[name].update(attrs)
990 def get_view_fields(view):
992 xml.dom.minidom.parseString(view['arch'].encode('utf-8')).documentElement,
995 model_obj = req.session.model(model)
996 tree_view = model_obj.fields_view_get(views.get('tree', False), 'tree', context)
997 form_view = model_obj.fields_view_get(views.get('form', False), 'form', context)
999 fields.update(get_view_fields(tree_view))
1000 fields.update(get_view_fields(form_view))
1005 _cp_path = "/base/export"
1007 def fields_get(self, req, model):
1008 Model = req.session.model(model)
1009 fields = Model.fields_get(False, req.session.eval_context(req.context))
1012 @openerpweb.jsonrequest
1013 def get_fields(self, req, model, prefix='', name= '', field_parent=None, params={}):
1014 import_compat = params.get("import_compat", False)
1015 views_id = params.get("views_id", {})
1017 fields = _fields_get_all(req, model, views=views_id, context=req.session.eval_context(req.context))
1018 field_parent_type = params.get("parent_field_type",False)
1020 if import_compat and field_parent_type and field_parent_type == "many2one":
1023 fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1025 fields_order = fields.keys()
1026 fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1028 for index, field in enumerate(fields_order):
1029 value = fields[field]
1031 if import_compat and value.get('readonly', False):
1033 for sl in value.get('states', {}).values():
1035 ok = ok or (s==['readonly',False])
1038 id = prefix + (prefix and '/'or '') + field
1039 nm = name + (name and '/' or '') + value['string']
1040 record.update(id=id, string= nm, action='javascript: void(0)',
1041 target=None, icon=None, children=[], field_type=value.get('type',False), required=value.get('required', False))
1042 records.append(record)
1044 if len(nm.split('/')) < 3 and value.get('relation', False):
1046 ref = value.pop('relation')
1047 cfields = self.fields_get(req, ref)
1048 if (value['type'] == 'many2many'):
1049 record['children'] = []
1050 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1052 elif value['type'] == 'many2one':
1053 record['children'] = [id + '/id', id + '/.id']
1054 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1057 cfields_order = cfields.keys()
1058 cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1060 for j, fld in enumerate(cfields_order):
1061 cid = id + '/' + fld
1062 cid = cid.replace(' ', '_')
1063 children.append(cid)
1064 record['children'] = children or []
1065 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1067 ref = value.pop('relation')
1068 cfields = self.fields_get(req, ref)
1069 cfields_order = cfields.keys()
1070 cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1072 for j, fld in enumerate(cfields_order):
1073 cid = id + '/' + fld
1074 cid = cid.replace(' ', '_')
1075 children.append(cid)
1076 record['children'] = children or []
1077 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1082 @openerpweb.jsonrequest
1083 def save_export_lists(self, req, name, model, field_list):
1084 result = {'resource':model, 'name':name, 'export_fields': []}
1085 for field in field_list:
1086 result['export_fields'].append((0, 0, {'name': field}))
1087 return req.session.model("ir.exports").create(result, req.session.eval_context(req.context))
1089 @openerpweb.jsonrequest
1090 def exist_export_lists(self, req, model):
1091 export_model = req.session.model("ir.exports")
1092 return export_model.read(export_model.search([('resource', '=', model)]), ['name'])
1094 @openerpweb.jsonrequest
1095 def delete_export(self, req, export_id):
1096 req.session.model("ir.exports").unlink(export_id, req.session.eval_context(req.context))
1099 @openerpweb.jsonrequest
1100 def namelist(self,req, model, export_id):
1102 result = self.get_data(req, model, req.session.eval_context(req.context))
1103 ir_export_obj = req.session.model("ir.exports")
1104 ir_export_line_obj = req.session.model("ir.exports.line")
1106 field = ir_export_obj.read(export_id)
1107 fields = ir_export_line_obj.read(field['export_fields'])
1110 [name_list.update({field['name']: result.get(field['name'])}) for field in fields]
1113 def get_data(self, req, model, context=None):
1115 context = context or {}
1117 proxy = req.session.model(model)
1118 fields = self.fields_get(req, model)
1120 f1 = proxy.fields_view_get(False, 'tree', context)['fields']
1121 f2 = proxy.fields_view_get(False, 'form', context)['fields']
1125 fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1128 _fields = {'id': 'ID' , '.id': 'Database ID' }
1129 def model_populate(fields, prefix_node='', prefix=None, prefix_value='', level=2):
1130 fields_order = fields.keys()
1131 fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1133 for field in fields_order:
1134 fields_data[prefix_node+field] = fields[field]
1136 fields_data[prefix_node + field]['string'] = '%s%s' % (prefix_value, fields_data[prefix_node + field]['string'])
1137 st_name = fields[field]['string'] or field
1138 _fields[prefix_node+field] = st_name
1139 if fields[field].get('relation', False) and level>0:
1140 fields2 = self.fields_get(req, fields[field]['relation'])
1141 fields2.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1142 model_populate(fields2, prefix_node+field+'/', None, st_name+'/', level-1)
1143 model_populate(fields)
1147 @openerpweb.jsonrequest
1148 def export_data(self, req, model, fields, ids, domain, import_compat=False, export_format="csv", context=None):
1149 context = req.session.eval_context(req.context)
1150 modle_obj = req.session.model(model)
1151 ids = ids or modle_obj.search(domain, context=context)
1153 field = fields.keys()
1154 result = modle_obj.export_data(ids, field , context).get('datas',[])
1156 if not import_compat:
1157 field = [val.strip() for val in fields.values()]
1159 if export_format == 'xls':
1160 return export_xls(field, result)
1162 return export_csv(field, result)