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