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 class Database(openerpweb.Controller):
60 _cp_path = "/base/database"
62 @openerpweb.jsonrequest
63 def get_databases_list(self, req):
64 proxy = req.session.proxy("db")
66 h = req.httprequest.headers['Host'].split(':')[0]
68 r = cherrypy.config['openerp.dbfilter'].replace('%h',h).replace('%d',d)
70 dbs = [i for i in dbs if re.match(r,i)]
71 return {"db_list": dbs}
73 class Session(openerpweb.Controller):
74 _cp_path = "/base/session"
76 def manifest_glob(self, addons, key):
79 globlist = openerpweb.addons_manifest.get(addon, {}).get(key, [])
82 resource_path[len(openerpweb.path_addons):]
83 for pattern in globlist
84 for resource_path in glob.glob(os.path.join(
85 openerpweb.path_addons, addon, pattern))
89 def concat_files(self, file_list):
90 """ Concatenate file content
91 return (concat,timestamp)
92 concat: concatenation of file content
93 timestamp: max(os.path.getmtime of file_list)
95 root = openerpweb.path_root
99 fname = os.path.join(root, i)
100 ftime = os.path.getmtime(fname)
101 if ftime > files_timestamp:
102 files_timestamp = ftime
103 files_content = open(fname).read()
104 files_concat = "".join(files_content)
107 @openerpweb.jsonrequest
108 def login(self, req, db, login, password):
109 req.session.login(db, login, password)
112 "session_id": req.session_id,
113 "uid": req.session._uid,
116 @openerpweb.jsonrequest
117 def sc_list(self, req):
118 return req.session.model('ir.ui.view_sc').get_sc(req.session._uid, "ir.ui.menu",
119 req.session.eval_context(req.context))
121 @openerpweb.jsonrequest
122 def modules(self, req):
123 return {"modules": [name
124 for name, manifest in openerpweb.addons_manifest.iteritems()
125 if manifest.get('active', True)]}
127 @openerpweb.jsonrequest
128 def csslist(self, req, mods='base'):
129 return {'files': self.manifest_glob(mods.split(','), 'css')}
131 @openerpweb.jsonrequest
132 def jslist(self, req, mods='base'):
133 return {'files': self.manifest_glob(mods.split(','), 'js')}
135 def css(self, req, mods='base'):
136 files = self.manifest_glob(mods.split(','), 'css')
137 concat = self.concat_files(files)[0]
138 # TODO request set the Date of last modif and Etag
142 def js(self, req, mods='base'):
143 files = self.manifest_glob(mods.split(','), 'js')
144 concat = self.concat_files(files)[0]
145 # TODO request set the Date of last modif and Etag
149 @openerpweb.jsonrequest
150 def eval_domain_and_context(self, req, contexts, domains,
152 """ Evaluates sequences of domains and contexts, composing them into
153 a single context, domain or group_by sequence.
155 :param list contexts: list of contexts to merge together. Contexts are
156 evaluated in sequence, all previous contexts
157 are part of their own evaluation context
158 (starting at the session context).
159 :param list domains: list of domains to merge together. Domains are
160 evaluated in sequence and appended to one another
161 (implicit AND), their evaluation domain is the
162 result of merging all contexts.
163 :param list group_by_seq: list of domains (which may be in a different
164 order than the ``contexts`` parameter),
165 evaluated in sequence, their ``'group_by'``
166 key is extracted if they have one.
171 the global context created by merging all of
175 the concatenation of all domains
178 a list of fields to group by, potentially empty (in which case
179 no group by should be performed)
181 context, domain = eval_context_and_domain(req.session,
182 openerpweb.nonliterals.CompoundContext(*(contexts or [])),
183 openerpweb.nonliterals.CompoundDomain(*(domains or [])))
185 group_by_sequence = []
186 for candidate in (group_by_seq or []):
187 ctx = req.session.eval_context(candidate, context)
188 group_by = ctx.get('group_by')
191 elif isinstance(group_by, basestring):
192 group_by_sequence.append(group_by)
194 group_by_sequence.extend(group_by)
199 'group_by': group_by_sequence
202 @openerpweb.jsonrequest
203 def save_session_action(self, req, the_action):
205 This method store an action object in the session object and returns an integer
206 identifying that action. The method get_session_action() can be used to get
209 :param the_action: The action to save in the session.
210 :type the_action: anything
211 :return: A key identifying the saved action.
214 saved_actions = cherrypy.session.get('saved_actions')
215 if not saved_actions:
216 saved_actions = {"next":0, "actions":{}}
217 cherrypy.session['saved_actions'] = saved_actions
218 # we don't allow more than 10 stored actions
219 if len(saved_actions["actions"]) >= 10:
220 del saved_actions["actions"][min(saved_actions["actions"].keys())]
221 key = saved_actions["next"]
222 saved_actions["actions"][key] = the_action
223 saved_actions["next"] = key + 1
226 @openerpweb.jsonrequest
227 def get_session_action(self, req, key):
229 Gets back a previously saved action. This method can return None if the action
230 was saved since too much time (this case should be handled in a smart way).
232 :param key: The key given by save_session_action()
234 :return: The saved action or None.
237 saved_actions = cherrypy.session.get('saved_actions')
238 if not saved_actions:
240 return saved_actions["actions"].get(key)
242 def eval_context_and_domain(session, context, domain=None):
243 e_context = session.eval_context(context)
244 # should we give the evaluated context as an evaluation context to the domain?
245 e_domain = session.eval_domain(domain or [])
247 return e_context, e_domain
249 def load_actions_from_ir_values(req, key, key2, models, meta, context):
250 Values = req.session.model('ir.values')
251 actions = Values.get(key, key2, models, meta, context)
253 return [(id, name, clean_action(action, req.session))
254 for id, name, action in actions]
256 def clean_action(action, session):
257 if action['type'] != 'ir.actions.act_window':
259 # values come from the server, we can just eval them
260 if isinstance(action.get('context', None), basestring):
261 action['context'] = eval(
263 session.evaluation_context()) or {}
265 if isinstance(action.get('domain', None), basestring):
266 action['domain'] = eval(
268 session.evaluation_context(
269 action.get('context', {}))) or []
270 if 'flags' not in action:
271 # Set empty flags dictionary for web client.
272 action['flags'] = dict()
273 return fix_view_modes(action)
275 def generate_views(action):
277 While the server generates a sequence called "views" computing dependencies
278 between a bunch of stuff for views coming directly from the database
279 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
280 to return custom view dictionaries generated on the fly.
282 In that case, there is no ``views`` key available on the action.
284 Since the web client relies on ``action['views']``, generate it here from
285 ``view_mode`` and ``view_id``.
287 Currently handles two different cases:
289 * no view_id, multiple view_mode
290 * single view_id, single view_mode
292 :param dict action: action descriptor dictionary to generate a views key for
294 view_id = action.get('view_id', False)
295 if isinstance(view_id, (list, tuple)):
298 # providing at least one view mode is a requirement, not an option
299 view_modes = action['view_mode'].split(',')
301 if len(view_modes) > 1:
303 raise ValueError('Non-db action dictionaries should provide '
304 'either multiple view modes or a single view '
305 'mode and an optional view id.\n\n Got view '
306 'modes %r and view id %r for action %r' % (
307 view_modes, view_id, action))
308 action['views'] = [(False, mode) for mode in view_modes]
310 action['views'] = [(view_id, view_modes[0])]
312 def fix_view_modes(action):
313 """ For historical reasons, OpenERP has weird dealings in relation to
314 view_mode and the view_type attribute (on window actions):
316 * one of the view modes is ``tree``, which stands for both list views
318 * the choice is made by checking ``view_type``, which is either
319 ``form`` for a list view or ``tree`` for an actual tree view
321 This methods simply folds the view_type into view_mode by adding a
322 new view mode ``list`` which is the result of the ``tree`` view_mode
323 in conjunction with the ``form`` view_type.
325 TODO: this should go into the doc, some kind of "peculiarities" section
327 :param dict action: an action descriptor
328 :returns: nothing, the action is modified in place
330 if 'views' not in action:
331 generate_views(action)
333 if action.pop('view_type') != 'form':
337 [id, mode if mode != 'tree' else 'list']
338 for id, mode in action['views']
343 class Menu(openerpweb.Controller):
344 _cp_path = "/base/menu"
346 @openerpweb.jsonrequest
348 return {'data': self.do_load(req)}
350 def do_load(self, req):
351 """ Loads all menu items (all applications and their sub-menus).
353 :param req: A request object, with an OpenERP session attribute
354 :type req: < session -> OpenERPSession >
355 :return: the menu root
356 :rtype: dict('children': menu_nodes)
358 Menus = req.session.model('ir.ui.menu')
359 # menus are loaded fully unlike a regular tree view, cause there are
360 # less than 512 items
361 context = req.session.eval_context(req.context)
362 menu_ids = Menus.search([], 0, False, False, context)
363 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
364 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
365 menu_items.append(menu_root)
367 # make a tree using parent_id
368 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
369 for menu_item in menu_items:
370 if menu_item['parent_id']:
371 parent = menu_item['parent_id'][0]
374 if parent in menu_items_map:
375 menu_items_map[parent].setdefault(
376 'children', []).append(menu_item)
378 # sort by sequence a tree using parent_id
379 for menu_item in menu_items:
380 menu_item.setdefault('children', []).sort(
381 key=lambda x:x["sequence"])
385 @openerpweb.jsonrequest
386 def action(self, req, menu_id):
387 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
388 [('ir.ui.menu', menu_id)], False,
389 req.session.eval_context(req.context))
390 return {"action": actions}
392 class DataSet(openerpweb.Controller):
393 _cp_path = "/base/dataset"
395 @openerpweb.jsonrequest
396 def fields(self, req, model):
397 return {'fields': req.session.model(model).fields_get(False,
398 req.session.eval_context(req.context))}
400 @openerpweb.jsonrequest
401 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, sort=None):
402 return self.do_search_read(request, model, fields, offset, limit, domain, sort)
403 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None
405 """ Performs a search() followed by a read() (if needed) using the
406 provided search criteria
408 :param request: a JSON-RPC request object
409 :type request: openerpweb.JsonRequest
410 :param str model: the name of the model to search on
411 :param fields: a list of the fields to return in the result records
413 :param int offset: from which index should the results start being returned
414 :param int limit: the maximum number of records to return
415 :param list domain: the search domain for the query
416 :param list sort: sorting directives
417 :returns: A structure (dict) with two keys: ids (all the ids matching
418 the (domain, context) pair) and records (paginated records
419 matching fields selection set)
422 Model = request.session.model(model)
423 context, domain = eval_context_and_domain(
424 request.session, request.context, domain)
426 ids = Model.search(domain, 0, False, sort or False, context)
427 # need to fill the dataset with all ids for the (domain, context) pair,
428 # so search un-paginated and paginate manually before reading
429 paginated_ids = ids[offset:(offset + limit if limit else None)]
430 if fields and fields == ['id']:
431 # shortcut read if we only want the ids
434 'records': map(lambda id: {'id': id}, paginated_ids)
437 records = Model.read(paginated_ids, fields or False, context)
438 records.sort(key=lambda obj: ids.index(obj['id']))
445 @openerpweb.jsonrequest
446 def get(self, request, model, ids, fields=False):
447 return self.do_get(request, model, ids, fields)
448 def do_get(self, request, model, ids, fields=False):
449 """ Fetches and returns the records of the model ``model`` whose ids
452 The results are in the same order as the inputs, but elements may be
453 missing (if there is no record left for the id)
455 :param request: the JSON-RPC2 request object
456 :type request: openerpweb.JsonRequest
457 :param model: the model to read from
459 :param ids: a list of identifiers
461 :param fields: a list of fields to fetch, ``False`` or empty to fetch
462 all fields in the model
463 :type fields: list | False
464 :returns: a list of records, in the same order as the list of ids
467 Model = request.session.model(model)
468 records = Model.read(ids, fields, request.session.eval_context(request.context))
470 record_map = dict((record['id'], record) for record in records)
472 return [record_map[id] for id in ids if record_map.get(id)]
474 @openerpweb.jsonrequest
475 def load(self, req, model, id, fields):
476 m = req.session.model(model)
478 r = m.read([id], False, req.session.eval_context(req.context))
481 return {'value': value}
483 @openerpweb.jsonrequest
484 def create(self, req, model, data):
485 m = req.session.model(model)
486 r = m.create(data, req.session.eval_context(req.context))
489 @openerpweb.jsonrequest
490 def save(self, req, model, id, data):
491 m = req.session.model(model)
492 r = m.write([id], data, req.session.eval_context(req.context))
495 @openerpweb.jsonrequest
496 def unlink(self, request, model, ids=()):
497 Model = request.session.model(model)
498 return Model.unlink(ids, request.session.eval_context(request.context))
500 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
501 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
502 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
503 c, d = eval_context_and_domain(req.session, context, domain)
504 if domain_id and len(args) - 1 >= domain_id:
506 if context_id and len(args) - 1 >= context_id:
509 return getattr(req.session.model(model), method)(*args)
511 @openerpweb.jsonrequest
512 def call(self, req, model, method, args, domain_id=None, context_id=None):
513 return self.call_common(req, model, method, args, domain_id, context_id)
515 @openerpweb.jsonrequest
516 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
517 action = self.call_common(req, model, method, args, domain_id, context_id)
518 if isinstance(action, dict) and action.get('type') != '':
519 return {'result': clean_action(action, req.session)}
520 return {'result': False}
522 @openerpweb.jsonrequest
523 def exec_workflow(self, req, model, id, signal):
524 r = req.session.exec_workflow(model, id, signal)
527 @openerpweb.jsonrequest
528 def default_get(self, req, model, fields):
529 Model = req.session.model(model)
530 return Model.default_get(fields, req.session.eval_context(req.context))
532 class DataGroup(openerpweb.Controller):
533 _cp_path = "/base/group"
534 @openerpweb.jsonrequest
535 def read(self, request, model, fields, group_by_fields, domain=None, sort=None):
536 Model = request.session.model(model)
537 context, domain = eval_context_and_domain(request.session, request.context, domain)
539 return Model.read_group(
540 domain or [], fields, group_by_fields, 0, False,
541 dict(context, group_by=group_by_fields), sort or False)
543 class View(openerpweb.Controller):
544 _cp_path = "/base/view"
546 def fields_view_get(self, request, model, view_id, view_type,
547 transform=True, toolbar=False, submenu=False):
548 Model = request.session.model(model)
549 context = request.session.eval_context(request.context)
550 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
551 # todo fme?: check that we should pass the evaluated context here
552 self.process_view(request.session, fvg, context, transform)
555 def process_view(self, session, fvg, context, transform):
556 # depending on how it feels, xmlrpclib.ServerProxy can translate
557 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
558 # enjoy unicode strings which can not be trivially converted to
559 # strings, and it blows up during parsing.
561 # So ensure we fix this retardation by converting view xml back to
563 if isinstance(fvg['arch'], unicode):
564 arch = fvg['arch'].encode('utf-8')
569 evaluation_context = session.evaluation_context(context or {})
570 xml = self.transform_view(arch, session, evaluation_context)
572 xml = ElementTree.fromstring(arch)
573 fvg['arch'] = Xml2Json.convert_element(xml)
575 for field in fvg['fields'].itervalues():
576 if field.get('views'):
577 for view in field["views"].itervalues():
578 self.process_view(session, view, None, transform)
579 if field.get('domain'):
580 field["domain"] = self.parse_domain(field["domain"], session)
581 if field.get('context'):
582 field["context"] = self.parse_context(field["context"], session)
584 @openerpweb.jsonrequest
585 def add_custom(self, request, view_id, arch):
586 CustomView = request.session.model('ir.ui.view.custom')
588 'user_id': request.session._uid,
591 }, request.session.eval_context(request.context))
592 return {'result': True}
594 @openerpweb.jsonrequest
595 def undo_custom(self, request, view_id, reset=False):
596 CustomView = request.session.model('ir.ui.view.custom')
597 context = request.session.eval_context(request.context)
598 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
599 0, False, False, context)
602 CustomView.unlink(vcustom, context)
604 CustomView.unlink([vcustom[0]], context)
605 return {'result': True}
606 return {'result': False}
608 def transform_view(self, view_string, session, context=None):
609 # transform nodes on the fly via iterparse, instead of
610 # doing it statically on the parsing result
611 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
613 for event, elem in parser:
617 self.parse_domains_and_contexts(elem, session)
620 def parse_domain(self, domain, session):
621 """ Parses an arbitrary string containing a domain, transforms it
622 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
624 :param domain: the domain to parse, if the domain is not a string it is assumed to
625 be a literal domain and is returned as-is
626 :param session: Current OpenERP session
627 :type session: openerpweb.openerpweb.OpenERPSession
629 if not isinstance(domain, (str, unicode)):
632 return openerpweb.ast.literal_eval(domain)
635 return openerpweb.nonliterals.Domain(session, domain)
637 def parse_context(self, context, session):
638 """ Parses an arbitrary string containing a context, transforms it
639 to either a literal context or a :class:`openerpweb.nonliterals.Context`
641 :param context: the context to parse, if the context is not a string it is assumed to
642 be a literal domain and is returned as-is
643 :param session: Current OpenERP session
644 :type session: openerpweb.openerpweb.OpenERPSession
646 if not isinstance(context, (str, unicode)):
649 return openerpweb.ast.literal_eval(context)
651 return openerpweb.nonliterals.Context(session, context)
653 def parse_domains_and_contexts(self, elem, session):
654 """ Converts domains and contexts from the view into Python objects,
655 either literals if they can be parsed by literal_eval or a special
656 placeholder object if the domain or context refers to free variables.
658 :param elem: the current node being parsed
659 :type param: xml.etree.ElementTree.Element
660 :param session: OpenERP session object, used to store and retrieve
662 :type session: openerpweb.openerpweb.OpenERPSession
664 for el in ['domain', 'filter_domain']:
665 domain = elem.get(el, '').strip()
667 elem.set(el, self.parse_domain(domain, session))
668 for el in ['context', 'default_get']:
669 context_string = elem.get(el, '').strip()
671 elem.set(el, self.parse_context(context_string, session))
673 class FormView(View):
674 _cp_path = "/base/formview"
676 @openerpweb.jsonrequest
677 def load(self, req, model, view_id, toolbar=False):
678 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
679 return {'fields_view': fields_view}
681 class ListView(View):
682 _cp_path = "/base/listview"
684 @openerpweb.jsonrequest
685 def load(self, req, model, view_id, toolbar=False):
686 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
687 return {'fields_view': fields_view}
689 def process_colors(self, view, row, context):
690 colors = view['arch']['attrs'].get('colors')
697 for pair in colors.split(';')
698 if eval(pair.split(':')[1], dict(context, **row))
703 elif len(color) == 1:
707 class SearchView(View):
708 _cp_path = "/base/searchview"
710 @openerpweb.jsonrequest
711 def load(self, req, model, view_id):
712 fields_view = self.fields_view_get(req, model, view_id, 'search')
713 return {'fields_view': fields_view}
715 @openerpweb.jsonrequest
716 def fields_get(self, req, model):
717 Model = req.session.model(model)
718 fields = Model.fields_get(False, req.session.eval_context(req.context))
719 for field in fields.values():
720 # shouldn't convert the views too?
721 if field.get('domain'):
722 field["domain"] = self.parse_domain(field["domain"], req.session)
723 if field.get('context'):
724 field["context"] = self.parse_domain(field["context"], req.session)
725 return {'fields': fields}
727 class Binary(openerpweb.Controller):
728 _cp_path = "/base/binary"
730 @openerpweb.httprequest
731 def image(self, request, session_id, model, id, field, **kw):
732 cherrypy.response.headers['Content-Type'] = 'image/png'
733 Model = request.session.model(model)
734 context = request.session.eval_context(request.context)
737 res = Model.default_get([field], context).get(field, '')
739 res = Model.read([int(id)], [field], context)[0].get(field, '')
740 return base64.decodestring(res)
741 except: # TODO: what's the exception here?
742 return self.placeholder()
743 def placeholder(self):
744 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
746 @openerpweb.httprequest
747 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
748 Model = request.session.model(model)
749 context = request.session.eval_context(request.context)
750 res = Model.read([int(id)], [field, fieldname], context)[0]
751 filecontent = res.get(field, '')
753 raise cherrypy.NotFound
755 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
756 filename = '%s_%s' % (model.replace('.', '_'), id)
758 filename = res.get(fieldname, '') or filename
759 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
760 return base64.decodestring(filecontent)
762 @openerpweb.httprequest
763 def upload(self, request, session_id, callback, ufile=None):
764 cherrypy.response.timeout = 500
766 for key, val in cherrypy.request.headers.iteritems():
767 headers[key.lower()] = val
768 size = int(headers.get('content-length', 0))
769 # TODO: might be useful to have a configuration flag for max-length file uploads
771 out = """<script language="javascript" type="text/javascript">
772 var win = window.top.window,
774 if (typeof(callback) === 'function') {
775 callback.apply(this, %s);
777 win.jQuery('#oe_notification', win.document).notify('create', {
778 title: "Ajax File Upload",
779 text: "Could not find callback"
783 data = ufile.file.read()
784 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
786 args = [False, e.message]
787 return out % (simplejson.dumps(callback), simplejson.dumps(args))
789 @openerpweb.httprequest
790 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
791 cherrypy.response.timeout = 500
792 context = request.session.eval_context(request.context)
793 Model = request.session.model('ir.attachment')
795 out = """<script language="javascript" type="text/javascript">
796 var win = window.top.window,
798 if (typeof(callback) === 'function') {
799 callback.call(this, %s);
802 attachment_id = Model.create({
803 'name': ufile.filename,
804 'datas': base64.encodestring(ufile.file.read()),
809 'filename': ufile.filename,
813 args = { 'error': e.message }
814 return out % (simplejson.dumps(callback), simplejson.dumps(args))
816 class Action(openerpweb.Controller):
817 _cp_path = "/base/action"
819 @openerpweb.jsonrequest
820 def load(self, req, action_id):
821 Actions = req.session.model('ir.actions.actions')
823 context = req.session.eval_context(req.context)
824 action_type = Actions.read([action_id], ['type'], context)
826 action = req.session.model(action_type[0]['type']).read([action_id], False,
829 value = clean_action(action[0], req.session)
830 return {'result': value}
832 @openerpweb.jsonrequest
833 def run(self, req, action_id):
834 return clean_action(req.session.model('ir.actions.server').run(
835 [action_id], req.session.eval_context(req.context)), req.session)
837 def export_csv(fields, result):
839 fp = StringIO.StringIO()
840 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
842 writer.writerow(fields)
847 if isinstance(d, basestring):
848 d = d.replace('\n',' ').replace('\t',' ')
850 d = d.encode('utf-8')
853 if d is False: d = None
862 def export_xls(fieldnames, table):
867 common.error(_('Import Error.'), _('Please install xlwt library to export to MS Excel.'))
869 workbook = xlwt.Workbook()
870 worksheet = workbook.add_sheet('Sheet 1')
872 for i, fieldname in enumerate(fieldnames):
873 worksheet.write(0, i, str(fieldname))
874 worksheet.col(i).width = 8000 # around 220 pixels
876 style = xlwt.easyxf('align: wrap yes')
878 for row_index, row in enumerate(table):
879 for cell_index, cell_value in enumerate(row):
880 cell_value = str(cell_value)
881 cell_value = re.sub("\r", " ", cell_value)
882 worksheet.write(row_index + 1, cell_index, cell_value, style)
885 fp = StringIO.StringIO()
890 #return data.decode('ISO-8859-1')
891 return unicode(data, 'utf-8', 'replace')
893 def node_attributes(node):
894 attrs = node.attributes
898 # localName can be a unicode string, we're using attribute names as
899 # **kwargs keys and python-level kwargs don't take unicode keys kindly
900 # (they blow up) so we need to ensure all keys are ``str``
901 return dict([(str(attrs.item(i).localName), attrs.item(i).nodeValue)
902 for i in range(attrs.length)])
904 def _fields_get_all(req, model, views, context=None):
909 def parse(root, fields):
910 for node in root.childNodes:
911 if node.nodeName in ('form', 'notebook', 'page', 'group', 'tree', 'hpaned', 'vpaned'):
913 elif node.nodeName=='field':
914 attrs = node_attributes(node)
916 fields[name].update(attrs)
919 def get_view_fields(view):
921 xml.dom.minidom.parseString(view['arch'].encode('utf-8')).documentElement,
924 model_obj = req.session.model(model)
925 tree_view = model_obj.fields_view_get(views.get('tree', False), 'tree', context)
926 form_view = model_obj.fields_view_get(views.get('form', False), 'form', context)
928 fields.update(get_view_fields(tree_view))
929 fields.update(get_view_fields(form_view))
934 _cp_path = "/base/export"
936 def fields_get(self, req, model):
937 Model = req.session.model(model)
938 fields = Model.fields_get(False, req.session.eval_context(req.context))
941 @openerpweb.jsonrequest
942 def get_fields(self, req, model, prefix='', name= '', field_parent=None, params={}):
943 import_compat = params.get("import_compat", False)
944 views_id = params.get("views_id", {})
946 fields = _fields_get_all(req, model, views=views_id, context=req.session.eval_context(req.context))
947 field_parent_type = params.get("parent_field_type",False)
949 if import_compat and field_parent_type and field_parent_type == "many2one":
952 fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
954 fields_order = fields.keys()
955 fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
957 for index, field in enumerate(fields_order):
958 value = fields[field]
960 if import_compat and value.get('readonly', False):
962 for sl in value.get('states', {}).values():
964 ok = ok or (s==['readonly',False])
967 id = prefix + (prefix and '/'or '') + field
968 nm = name + (name and '/' or '') + value['string']
969 record.update(id=id, string= nm, action='javascript: void(0)',
970 target=None, icon=None, children=[], field_type=value.get('type',False), required=value.get('required', False))
971 records.append(record)
973 if len(nm.split('/')) < 3 and value.get('relation', False):
975 ref = value.pop('relation')
976 cfields = self.fields_get(req, ref)
977 if (value['type'] == 'many2many'):
978 record['children'] = []
979 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
981 elif value['type'] == 'many2one':
982 record['children'] = [id + '/id', id + '/.id']
983 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
986 cfields_order = cfields.keys()
987 cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
989 for j, fld in enumerate(cfields_order):
991 cid = cid.replace(' ', '_')
993 record['children'] = children or []
994 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
996 ref = value.pop('relation')
997 cfields = self.fields_get(req, ref)
998 cfields_order = cfields.keys()
999 cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1001 for j, fld in enumerate(cfields_order):
1002 cid = id + '/' + fld
1003 cid = cid.replace(' ', '_')
1004 children.append(cid)
1005 record['children'] = children or []
1006 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1011 @openerpweb.jsonrequest
1012 def save_export_lists(self, req, name, model, field_list):
1013 result = {'resource':model, 'name':name, 'export_fields': []}
1014 for field in field_list:
1015 result['export_fields'].append((0, 0, {'name': field}))
1016 return req.session.model("ir.exports").create(result, req.session.eval_context(req.context))
1018 @openerpweb.jsonrequest
1019 def exist_export_lists(self, req, model):
1020 export_model = req.session.model("ir.exports")
1021 return export_model.read(export_model.search([('resource', '=', model)]), ['name'])
1023 @openerpweb.jsonrequest
1024 def delete_export(self, req, export_id):
1025 req.session.model("ir.exports").unlink(export_id, req.session.eval_context(req.context))
1028 @openerpweb.jsonrequest
1029 def namelist(self,req, model, export_id):
1031 result = self.get_data(req, model, req.session.eval_context(req.context))
1032 ir_export_obj = req.session.model("ir.exports")
1033 ir_export_line_obj = req.session.model("ir.exports.line")
1035 field = ir_export_obj.read(export_id)
1036 fields = ir_export_line_obj.read(field['export_fields'])
1039 [name_list.update({field['name']: result.get(field['name'])}) for field in fields]
1042 def get_data(self, req, model, context=None):
1044 context = context or {}
1046 proxy = req.session.model(model)
1047 fields = self.fields_get(req, model)
1049 f1 = proxy.fields_view_get(False, 'tree', context)['fields']
1050 f2 = proxy.fields_view_get(False, 'form', context)['fields']
1054 fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1057 _fields = {'id': 'ID' , '.id': 'Database ID' }
1058 def model_populate(fields, prefix_node='', prefix=None, prefix_value='', level=2):
1059 fields_order = fields.keys()
1060 fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1062 for field in fields_order:
1063 fields_data[prefix_node+field] = fields[field]
1065 fields_data[prefix_node + field]['string'] = '%s%s' % (prefix_value, fields_data[prefix_node + field]['string'])
1066 st_name = fields[field]['string'] or field
1067 _fields[prefix_node+field] = st_name
1068 if fields[field].get('relation', False) and level>0:
1069 fields2 = self.fields_get(req, fields[field]['relation'])
1070 fields2.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1071 model_populate(fields2, prefix_node+field+'/', None, st_name+'/', level-1)
1072 model_populate(fields)
1076 @openerpweb.jsonrequest
1077 def export_data(self, req, model, fields, ids, domain, import_compat=False, export_format="csv", context=None):
1078 context = req.session.eval_context(req.context)
1079 modle_obj = req.session.model(model)
1080 ids = ids or modle_obj.search(domain, context=context)
1082 field = fields.keys()
1083 result = modle_obj.export_data(ids, field , context).get('datas',[])
1085 if not import_compat:
1086 field = [val.strip() for val in fields.values()]
1088 if export_format == 'xls':
1089 return export_xls(field, result)
1091 return export_csv(field, result)