3a9ba89c0021d7d3b56b533d69cba8babc78ee8f
[odoo/odoo.git] / openerp / service / web_services.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import base64
23 import locale
24 import logging
25 import os
26 import platform
27 import security
28 import sys
29 import thread
30 import threading
31 import time
32 import traceback
33 from cStringIO import StringIO
34 from openerp.tools.translate import _
35 import openerp.netsvc as netsvc
36 import openerp.pooler as pooler
37 import openerp.release as release
38 import openerp.sql_db as sql_db
39 import openerp.tools as tools
40 import openerp.modules
41 import openerp.exceptions
42 from openerp.service import http_server
43
44 #.apidoc title: Exported Service methods
45 #.apidoc module-mods: member-order: bysource
46
47 """ This python module defines the RPC methods available to remote clients.
48
49     Each 'Export Service' is a group of 'methods', which in turn are RPC
50     procedures to be called. Each method has its own arguments footprint.
51 """
52
53 _logger = logging.getLogger(__name__)
54
55 RPC_VERSION_1 = {'server_version': '6.1', 'protocol_version': 1}
56
57 # This should be moved to openerp.modules.db, along side initialize().
58 def _initialize_db(serv, id, db_name, demo, lang, user_password):
59     cr = None
60     try:
61         serv.actions[id]['progress'] = 0
62         cr = sql_db.db_connect(db_name).cursor()
63         openerp.modules.db.initialize(cr) # TODO this should be removed as it is done by pooler.restart_pool.
64         tools.config['lang'] = lang
65         cr.commit()
66         cr.close()
67
68         pool = pooler.restart_pool(db_name, demo, serv.actions[id],
69                                    update_module=True)[1]
70
71         cr = sql_db.db_connect(db_name).cursor()
72
73         if lang:
74             modobj = pool.get('ir.module.module')
75             mids = modobj.search(cr, 1, [('state', '=', 'installed')])
76             modobj.update_translations(cr, 1, mids, lang)
77
78         cr.execute('UPDATE res_users SET password=%s, context_lang=%s, active=True WHERE login=%s', (
79             user_password, lang, 'admin'))
80         cr.execute('SELECT login, password, name ' \
81                    '  FROM res_users ' \
82                    ' ORDER BY login')
83         serv.actions[id].update(users=cr.dictfetchall(), clean=True)
84         cr.commit()
85         cr.close()
86     except Exception, e:
87         serv.actions[id].update(clean=False, exception=e)
88         _logger.exception('CREATE DATABASE failed:')
89         serv.actions[id]['traceback'] = traceback.format_exc()
90         if cr:
91             cr.close()
92
93 class db(netsvc.ExportService):
94     def __init__(self, name="db"):
95         netsvc.ExportService.__init__(self, name)
96         self.actions = {}
97         self.id = 0
98         self.id_protect = threading.Semaphore()
99
100         self._pg_psw_env_var_is_set = False # on win32, pg_dump need the PGPASSWORD env var
101
102     def dispatch(self, method, params):
103         if method in [ 'create', 'get_progress', 'drop', 'dump',
104             'restore', 'rename',
105             'change_admin_password', 'migrate_databases',
106             'create_database' ]:
107             passwd = params[0]
108             params = params[1:]
109             security.check_super(passwd)
110         elif method in [ 'db_exist', 'list', 'list_lang', 'server_version' ]:
111             # params = params
112             # No security check for these methods
113             pass
114         else:
115             raise KeyError("Method not found: %s" % method)
116         fn = getattr(self, 'exp_'+method)
117         return fn(*params)
118
119     def _create_empty_database(self, name):
120         db = sql_db.db_connect('template1')
121         cr = db.cursor()
122         chosen_template = tools.config['db_template']
123         try:
124             cr.autocommit(True) # avoid transaction block
125             cr.execute("""CREATE DATABASE "%s" ENCODING 'unicode' TEMPLATE "%s" """ % (name, chosen_template))
126         finally:
127             cr.close()
128
129     def exp_create(self, db_name, demo, lang, user_password='admin'):
130         self.id_protect.acquire()
131         self.id += 1
132         id = self.id
133         self.id_protect.release()
134
135         self.actions[id] = {'clean': False}
136
137         self._create_empty_database(db_name)
138
139         _logger.info('CREATE DATABASE %s', db_name.lower())
140         create_thread = threading.Thread(target=_initialize_db,
141                 args=(self, id, db_name, demo, lang, user_password))
142         create_thread.start()
143         self.actions[id]['thread'] = create_thread
144         return id
145
146     def exp_create_database(self, db_name, demo, lang, user_password='admin'):
147         """ Similar to exp_create but blocking."""
148         self.id_protect.acquire()
149         self.id += 1
150         id = self.id
151         self.id_protect.release()
152
153         self.actions[id] = {'clean': False}
154
155         _logger.info('CREATE DATABASE %s', db_name.lower())
156         self._create_empty_database(db_name)
157         _initialize_db(self, id, db_name, demo, lang, user_password)
158         return True
159
160     def exp_get_progress(self, id):
161         if self.actions[id]['thread'].isAlive():
162 #           return openerp.modules.init_progress[db_name]
163             return (min(self.actions[id].get('progress', 0),0.95), [])
164         else:
165             clean = self.actions[id]['clean']
166             if clean:
167                 users = self.actions[id]['users']
168                 self.actions.pop(id)
169                 return (1.0, users)
170             else:
171                 e = self.actions[id]['exception'] # TODO this seems wrong: actions[id]['traceback'] is set, but not 'exception'.
172                 self.actions.pop(id)
173                 raise Exception, e
174
175     def exp_drop(self, db_name):
176         openerp.modules.registry.RegistryManager.delete(db_name)
177         sql_db.close_db(db_name)
178
179         db = sql_db.db_connect('template1')
180         cr = db.cursor()
181         cr.autocommit(True) # avoid transaction block
182         try:
183             try:
184                 cr.execute('DROP DATABASE "%s"' % db_name)
185             except Exception, e:
186                 _logger.error('DROP DB: %s failed:\n%s', db_name, e)
187                 raise Exception("Couldn't drop database %s: %s" % (db_name, e))
188             else:
189                 _logger.info('DROP DB: %s', db_name)
190         finally:
191             cr.close()
192         return True
193
194
195     def _set_pg_psw_env_var(self):
196         # see http://www.postgresql.org/docs/8.4/static/libpq-envars.html
197         # FIXME: This is not thread-safe, and should never be enabled for
198         # SaaS (giving SaaS users the super-admin password is not a good idea
199         # anyway)
200         if tools.config['db_password'] and not os.environ.get('PGPASSWORD', ''):
201             os.environ['PGPASSWORD'] = tools.config['db_password']
202             self._pg_psw_env_var_is_set = True
203
204     def _unset_pg_psw_env_var(self):
205         if self._pg_psw_env_var_is_set:
206             os.environ['PGPASSWORD'] = ''
207
208     def exp_dump(self, db_name):
209         try:
210             self._set_pg_psw_env_var()
211             cmd = ['pg_dump', '--format=c', '--no-owner']
212             if tools.config['db_user']:
213                 cmd.append('--username=' + tools.config['db_user'])
214             if tools.config['db_host']:
215                 cmd.append('--host=' + tools.config['db_host'])
216             if tools.config['db_port']:
217                 cmd.append('--port=' + str(tools.config['db_port']))
218             cmd.append(db_name)
219     
220             stdin, stdout = tools.exec_pg_command_pipe(*tuple(cmd))
221             stdin.close()
222             data = stdout.read()
223             res = stdout.close()
224     
225             if not data or res:
226                 _logger.error(
227                         'DUMP DB: %s failed! Please verify the configuration of the database password on the server. '\
228                         'It should be provided as a -w <PASSWD> command-line option, or as `db_password` in the '\
229                         'server configuration file.\n %s'  % (db_name, data))
230                 raise Exception, "Couldn't dump database"
231             _logger.info('DUMP DB successful: %s', db_name)
232     
233             return base64.encodestring(data)
234         finally:
235             self._unset_pg_psw_env_var()
236
237     def exp_restore(self, db_name, data):
238         try:
239             self._set_pg_psw_env_var()
240
241             if self.exp_db_exist(db_name):
242                 _logger.warning('RESTORE DB: %s already exists' % (db_name,))
243                 raise Exception, "Database already exists"
244
245             self._create_empty_database(db_name)
246
247             cmd = ['pg_restore', '--no-owner']
248             if tools.config['db_user']:
249                 cmd.append('--username=' + tools.config['db_user'])
250             if tools.config['db_host']:
251                 cmd.append('--host=' + tools.config['db_host'])
252             if tools.config['db_port']:
253                 cmd.append('--port=' + str(tools.config['db_port']))
254             cmd.append('--dbname=' + db_name)
255             args2 = tuple(cmd)
256
257             buf=base64.decodestring(data)
258             if os.name == "nt":
259                 tmpfile = (os.environ['TMP'] or 'C:\\') + os.tmpnam()
260                 file(tmpfile, 'wb').write(buf)
261                 args2=list(args2)
262                 args2.append(' ' + tmpfile)
263                 args2=tuple(args2)
264             stdin, stdout = tools.exec_pg_command_pipe(*args2)
265             if not os.name == "nt":
266                 stdin.write(base64.decodestring(data))
267             stdin.close()
268             res = stdout.close()
269             if res:
270                 raise Exception, "Couldn't restore database"
271             _logger.info('RESTORE DB: %s' % (db_name))
272
273             return True
274         finally:
275             self._unset_pg_psw_env_var()
276
277     def exp_rename(self, old_name, new_name):
278         openerp.modules.registry.RegistryManager.delete(old_name)
279         sql_db.close_db(old_name)
280
281         db = sql_db.db_connect('template1')
282         cr = db.cursor()
283         cr.autocommit(True) # avoid transaction block
284         try:
285             try:
286                 cr.execute('ALTER DATABASE "%s" RENAME TO "%s"' % (old_name, new_name))
287             except Exception, e:
288                 _logger.error('RENAME DB: %s -> %s failed:\n%s', old_name, new_name, e)
289                 raise Exception("Couldn't rename database %s to %s: %s" % (old_name, new_name, e))
290             else:
291                 fs = os.path.join(tools.config['root_path'], 'filestore')
292                 if os.path.exists(os.path.join(fs, old_name)):
293                     os.rename(os.path.join(fs, old_name), os.path.join(fs, new_name))
294
295                 _logger.info('RENAME DB: %s -> %s', old_name, new_name)
296         finally:
297             cr.close()
298         return True
299
300     def exp_db_exist(self, db_name):
301         ## Not True: in fact, check if connection to database is possible. The database may exists
302         return bool(sql_db.db_connect(db_name))
303
304     def exp_list(self, document=False):
305         if not tools.config['list_db'] and not document:
306             raise openerp.exceptions.AccessDenied()
307         chosen_template = tools.config['db_template']
308         templates_list = tuple(set(['template0', 'template1', 'postgres', chosen_template]))
309         db = sql_db.db_connect('template1')
310         cr = db.cursor()
311         try:
312             try:
313                 db_user = tools.config["db_user"]
314                 if not db_user and os.name == 'posix':
315                     import pwd
316                     db_user = pwd.getpwuid(os.getuid())[0]
317                 if not db_user:
318                     cr.execute("select usename from pg_user where usesysid=(select datdba from pg_database where datname=%s)", (tools.config["db_name"],))
319                     res = cr.fetchone()
320                     db_user = res and str(res[0])
321                 if db_user:
322                     cr.execute("select datname from pg_database where datdba=(select usesysid from pg_user where usename=%s) and datname not in %s order by datname", (db_user, templates_list))
323                 else:
324                     cr.execute("select datname from pg_database where datname not in %s order by datname", (templates_list,))
325                 res = [str(name) for (name,) in cr.fetchall()]
326             except Exception:
327                 res = []
328         finally:
329             cr.close()
330         res.sort()
331         return res
332
333     def exp_change_admin_password(self, new_password):
334         tools.config['admin_passwd'] = new_password
335         tools.config.save()
336         return True
337
338     def exp_list_lang(self):
339         return tools.scan_languages()
340
341     def exp_server_version(self):
342         """ Return the version of the server
343             Used by the client to verify the compatibility with its own version
344         """
345         return release.version
346
347     def exp_migrate_databases(self,databases):
348
349         from openerp.osv.orm import except_orm
350         from openerp.osv.osv import except_osv
351
352         for db in databases:
353             try:
354                 _logger.info('migrate database %s', db)
355                 tools.config['update']['base'] = True
356                 pooler.restart_pool(db, force_demo=False, update_module=True)
357             except except_orm, inst:
358                 netsvc.abort_response(1, inst.name, 'warning', inst.value)
359             except except_osv, inst:
360                 netsvc.abort_response(1, inst.name, 'warning', inst.value)
361             except Exception:
362                 _logger.exception('Exception in migrate_databases:')
363                 raise
364         return True
365
366 class common(netsvc.ExportService):
367
368     def __init__(self,name="common"):
369         netsvc.ExportService.__init__(self,name)
370
371     def dispatch(self, method, params):
372         if method in ['login', 'about', 'timezone_get', 'get_server_environment',
373                       'login_message','get_stats', 'check_connectivity',
374                       'list_http_services', 'version', 'authenticate']:
375             pass
376         elif method in ['get_available_updates', 'get_migration_scripts', 'set_loglevel', 'get_os_time', 'get_sqlcount']:
377             passwd = params[0]
378             params = params[1:]
379             security.check_super(passwd)
380         else:
381             raise Exception("Method not found: %s" % method)
382
383         fn = getattr(self, 'exp_'+method)
384         return fn(*params)
385
386     def exp_login(self, db, login, password):
387         # TODO: legacy indirection through 'security', should use directly
388         # the res.users model
389         res = security.login(db, login, password)
390         msg = res and 'successful login' or 'bad login or password'
391         _logger.info("%s from '%s' using database '%s'", msg, login, db.lower())
392         return res or False
393
394     def exp_authenticate(self, db, login, password, user_agent_env):
395         res_users = pooler.get_pool(db).get('res.users')
396         return res_users.authenticate(db, login, password, user_agent_env)
397
398     def exp_version(self):
399         return RPC_VERSION_1
400
401     def exp_about(self, extended=False):
402         """Return information about the OpenERP Server.
403
404         @param extended: if True then return version info
405         @return string if extended is False else tuple
406         """
407
408         info = _('''
409
410 OpenERP is an ERP+CRM program for small and medium businesses.
411
412 The whole source code is distributed under the terms of the
413 GNU Public Licence.
414
415 (c) 2003-TODAY, Fabien Pinckaers - Tiny sprl''')
416
417         if extended:
418             return info, release.version
419         return info
420
421     def exp_timezone_get(self, db, login, password):
422         return tools.misc.get_server_timezone()
423
424     def exp_get_available_updates(self, contract_id, contract_password):
425         import openerp.tools.maintenance as tm
426         try:
427             rc = tm.remote_contract(contract_id, contract_password)
428             if not rc.id:
429                 raise tm.RemoteContractException('This contract does not exist or is not active')
430
431             return rc.get_available_updates(rc.id, openerp.modules.get_modules_with_version())
432
433         except tm.RemoteContractException, e:
434             netsvc.abort_response(1, 'Migration Error', 'warning', str(e))
435
436
437     def exp_get_migration_scripts(self, contract_id, contract_password):
438         import openerp.tools.maintenance as tm
439         try:
440             rc = tm.remote_contract(contract_id, contract_password)
441             if not rc.id:
442                 raise tm.RemoteContractException('This contract does not exist or is not active')
443             if rc.status != 'full':
444                 raise tm.RemoteContractException('Can not get updates for a partial contract')
445
446             _logger.info('starting migration with contract %s', rc.name)
447
448             zips = rc.retrieve_updates(rc.id, openerp.modules.get_modules_with_version())
449
450             from shutil import rmtree, copytree, copy
451
452             backup_directory = os.path.join(tools.config['root_path'], 'backup', time.strftime('%Y-%m-%d-%H-%M'))
453             if zips and not os.path.isdir(backup_directory):
454                 _logger.info('create a new backup directory to \
455                                 store the old modules: %s', backup_directory)
456                 os.makedirs(backup_directory)
457
458             for module in zips:
459                 _logger.info('upgrade module %s', module)
460                 mp = openerp.modules.get_module_path(module)
461                 if mp:
462                     if os.path.isdir(mp):
463                         copytree(mp, os.path.join(backup_directory, module))
464                         if os.path.islink(mp):
465                             os.unlink(mp)
466                         else:
467                             rmtree(mp)
468                     else:
469                         copy(mp + 'zip', backup_directory)
470                         os.unlink(mp + '.zip')
471
472                 try:
473                     try:
474                         base64_decoded = base64.decodestring(zips[module])
475                     except Exception:
476                         _logger.error('unable to read the module %s', module)
477                         raise
478
479                     zip_contents = StringIO(base64_decoded)
480                     zip_contents.seek(0)
481                     try:
482                         try:
483                             tools.extract_zip_file(zip_contents, tools.config['addons_path'] )
484                         except Exception:
485                             _logger.error('unable to extract the module %s', module)
486                             rmtree(module)
487                             raise
488                     finally:
489                         zip_contents.close()
490                 except Exception:
491                     _logger.error('restore the previous version of the module %s', module)
492                     nmp = os.path.join(backup_directory, module)
493                     if os.path.isdir(nmp):
494                         copytree(nmp, tools.config['addons_path'])
495                     else:
496                         copy(nmp+'.zip', tools.config['addons_path'])
497                     raise
498
499             return True
500         except tm.RemoteContractException, e:
501             netsvc.abort_response(1, 'Migration Error', 'warning', str(e))
502         except Exception, e:
503             _logger.exception('Exception in get_migration_script:')
504             raise
505
506     def exp_get_server_environment(self):
507         os_lang = '.'.join( [x for x in locale.getdefaultlocale() if x] )
508         if not os_lang:
509             os_lang = 'NOT SET'
510         environment = '\nEnvironment Information : \n' \
511                      'System : %s\n' \
512                      'OS Name : %s\n' \
513                      %(platform.platform(), platform.os.name)
514         if os.name == 'posix':
515           if platform.system() == 'Linux':
516              lsbinfo = os.popen('lsb_release -a').read()
517              environment += '%s'%(lsbinfo)
518           else:
519              environment += 'Your System is not lsb compliant\n'
520         environment += 'Operating System Release : %s\n' \
521                     'Operating System Version : %s\n' \
522                     'Operating System Architecture : %s\n' \
523                     'Operating System Locale : %s\n'\
524                     'Python Version : %s\n'\
525                     'OpenERP-Server Version : %s'\
526                     %(platform.release(), platform.version(), platform.architecture()[0],
527                       os_lang, platform.python_version(),release.version)
528         return environment
529
530     def exp_login_message(self):
531         return tools.config.get('login_message', False)
532
533     def exp_set_loglevel(self, loglevel, logger=None):
534         # TODO Previously, the level was set on the now deprecated
535         # `openerp.netsvc.Logger` class.
536         return True
537
538     def exp_get_stats(self):
539         res = "OpenERP server: %d threads\n" % threading.active_count()
540         res += netsvc.Server.allStats()
541         return res
542
543     def exp_list_http_services(self):
544         return http_server.list_http_services()
545
546     def exp_check_connectivity(self):
547         return bool(sql_db.db_connect('template1'))
548
549     def exp_get_os_time(self):
550         return os.times()
551
552     def exp_get_sqlcount(self):
553         if not logging.getLogger('openerp.sql_db').isEnabledFor(logging.DEBUG):
554             _logger.warning("Counters of SQL will not be reliable unless logger openerp.sql_db is set to level DEBUG or higer.")
555         return sql_db.sql_counter
556
557
558 class objects_proxy(netsvc.ExportService):
559     def __init__(self, name="object"):
560         netsvc.ExportService.__init__(self,name)
561
562     def dispatch(self, method, params):
563         (db, uid, passwd ) = params[0:3]
564         params = params[3:]
565         if method == 'obj_list':
566             raise NameError("obj_list has been discontinued via RPC as of 6.0, please query ir.model directly!")
567         if method not in ['execute', 'execute_kw', 'exec_workflow']:
568             raise NameError("Method not available %s" % method)
569         security.check(db,uid,passwd)
570         assert openerp.osv.osv.service, "The object_proxy class must be started with start_object_proxy."
571         fn = getattr(openerp.osv.osv.service, method)
572         res = fn(db, uid, *params)
573         return res
574
575
576 #
577 # Wizard ID: 1
578 #    - None = end of wizard
579 #
580 # Wizard Type: 'form'
581 #    - form
582 #    - print
583 #
584 # Wizard datas: {}
585 # TODO: change local request to OSE request/reply pattern
586 #
587 class wizard(netsvc.ExportService):
588     def __init__(self, name='wizard'):
589         netsvc.ExportService.__init__(self,name)
590         self.id = 0
591         self.wiz_datas = {}
592         self.wiz_name = {}
593         self.wiz_uid = {}
594
595     def dispatch(self, method, params):
596         (db, uid, passwd ) = params[0:3]
597         params = params[3:]
598         if method not in ['execute','create']:
599             raise KeyError("Method not supported %s" % method)
600         security.check(db,uid,passwd)
601         fn = getattr(self, 'exp_'+method)
602         res = fn(db, uid, *params)
603         return res
604
605     def _execute(self, db, uid, wiz_id, datas, action, context):
606         self.wiz_datas[wiz_id].update(datas)
607         wiz = netsvc.LocalService('wizard.'+self.wiz_name[wiz_id])
608         return wiz.execute(db, uid, self.wiz_datas[wiz_id], action, context)
609
610     def exp_create(self, db, uid, wiz_name, datas=None):
611         if not datas:
612             datas={}
613 #FIXME: this is not thread-safe
614         self.id += 1
615         self.wiz_datas[self.id] = {}
616         self.wiz_name[self.id] = wiz_name
617         self.wiz_uid[self.id] = uid
618         return self.id
619
620     def exp_execute(self, db, uid, wiz_id, datas, action='init', context=None):
621         if not context:
622             context={}
623
624         if wiz_id in self.wiz_uid:
625             if self.wiz_uid[wiz_id] == uid:
626                 return self._execute(db, uid, wiz_id, datas, action, context)
627             else:
628                 raise openerp.exceptions.AccessDenied()
629         else:
630             raise openerp.exceptions.Warning('Wizard not found.')
631
632 #
633 # TODO: set a maximum report number per user to avoid DOS attacks
634 #
635 # Report state:
636 #     False -> True
637 #
638
639 class report_spool(netsvc.ExportService):
640     def __init__(self, name='report'):
641         netsvc.ExportService.__init__(self, name)
642         self._reports = {}
643         self.id = 0
644         self.id_protect = threading.Semaphore()
645
646     def dispatch(self, method, params):
647         (db, uid, passwd ) = params[0:3]
648         params = params[3:]
649         if method not in ['report', 'report_get', 'render_report']:
650             raise KeyError("Method not supported %s" % method)
651         security.check(db,uid,passwd)
652         fn = getattr(self, 'exp_' + method)
653         res = fn(db, uid, *params)
654         return res
655
656     def exp_render_report(self, db, uid, object, ids, datas=None, context=None):
657         if not datas:
658             datas={}
659         if not context:
660             context={}
661
662         self.id_protect.acquire()
663         self.id += 1
664         id = self.id
665         self.id_protect.release()
666
667         self._reports[id] = {'uid': uid, 'result': False, 'state': False, 'exception': None}
668
669         cr = pooler.get_db(db).cursor()
670         try:
671             obj = netsvc.LocalService('report.'+object)
672             (result, format) = obj.create(cr, uid, ids, datas, context)
673             if not result:
674                 tb = sys.exc_info()
675                 self._reports[id]['exception'] = openerp.exceptions.DeferredException('RML is not available at specified location or not enough data to print!', tb)
676             self._reports[id]['result'] = result
677             self._reports[id]['format'] = format
678             self._reports[id]['state'] = True
679         except Exception, exception:
680
681             _logger.exception('Exception: %s\n', str(exception))
682             if hasattr(exception, 'name') and hasattr(exception, 'value'):
683                 self._reports[id]['exception'] = openerp.exceptions.DeferredException(tools.ustr(exception.name), tools.ustr(exception.value))
684             else:
685                 tb = sys.exc_info()
686                 self._reports[id]['exception'] = openerp.exceptions.DeferredException(tools.exception_to_unicode(exception), tb)
687             self._reports[id]['state'] = True
688         cr.commit()
689         cr.close()
690
691         return self._check_report(id)
692
693     def exp_report(self, db, uid, object, ids, datas=None, context=None):
694         if not datas:
695             datas={}
696         if not context:
697             context={}
698
699         self.id_protect.acquire()
700         self.id += 1
701         id = self.id
702         self.id_protect.release()
703
704         self._reports[id] = {'uid': uid, 'result': False, 'state': False, 'exception': None}
705
706         def go(id, uid, ids, datas, context):
707             cr = pooler.get_db(db).cursor()
708             try:
709                 obj = netsvc.LocalService('report.'+object)
710                 (result, format) = obj.create(cr, uid, ids, datas, context)
711                 if not result:
712                     tb = sys.exc_info()
713                     self._reports[id]['exception'] = openerp.exceptions.DeferredException('RML is not available at specified location or not enough data to print!', tb)
714                 self._reports[id]['result'] = result
715                 self._reports[id]['format'] = format
716                 self._reports[id]['state'] = True
717             except Exception, exception:
718                 _logger.exception('Exception: %s\n', str(exception))
719                 if hasattr(exception, 'name') and hasattr(exception, 'value'):
720                     self._reports[id]['exception'] = openerp.exceptions.DeferredException(tools.ustr(exception.name), tools.ustr(exception.value))
721                 else:
722                     tb = sys.exc_info()
723                     self._reports[id]['exception'] = openerp.exceptions.DeferredException(tools.exception_to_unicode(exception), tb)
724                 self._reports[id]['state'] = True
725             cr.commit()
726             cr.close()
727             return True
728
729         thread.start_new_thread(go, (id, uid, ids, datas, context))
730         return id
731
732     def _check_report(self, report_id):
733         result = self._reports[report_id]
734         exc = result['exception']
735         if exc:
736             netsvc.abort_response(exc, exc.message, 'warning', exc.traceback)
737         res = {'state': result['state']}
738         if res['state']:
739             if tools.config['reportgz']:
740                 import zlib
741                 res2 = zlib.compress(result['result'])
742                 res['code'] = 'zlib'
743             else:
744                 #CHECKME: why is this needed???
745                 if isinstance(result['result'], unicode):
746                     res2 = result['result'].encode('latin1', 'replace')
747                 else:
748                     res2 = result['result']
749             if res2:
750                 res['result'] = base64.encodestring(res2)
751             res['format'] = result['format']
752             del self._reports[report_id]
753         return res
754
755     def exp_report_get(self, db, uid, report_id):
756         if report_id in self._reports:
757             if self._reports[report_id]['uid'] == uid:
758                 return self._check_report(report_id)
759             else:
760                 raise Exception, 'AccessDenied'
761         else:
762             raise Exception, 'ReportNotFound'
763
764
765 def start_web_services():
766     db()
767     common()
768     objects_proxy()
769     wizard()
770     report_spool()
771
772
773 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
774