bugfixes
[odoo/odoo.git] / bin / tools / misc.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2008 Tiny SPRL (<http://tiny.be>). All Rights Reserved
6 #    $Id$
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU General Public License as published by
10 #    the Free Software Foundation, either version 3 of the License, or
11 #    (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU General Public License for more details.
17 #
18 #    You should have received a copy of the GNU General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 """
24 Miscelleanous tools used by OpenERP.
25 """
26
27 import os, time, sys
28 import inspect
29
30 from config import config
31
32 import zipfile
33 import release
34 import socket
35
36 if sys.version_info[:2] < (2, 4):
37     from threadinglocal import local
38 else:
39     from threading import local
40
41 from itertools import izip
42
43 # initialize a database with base/base.sql
44 def init_db(cr):
45     import addons
46     f = addons.get_module_resource('base', 'base.sql')
47     for line in file(f).read().split(';'):
48         if (len(line)>0) and (not line.isspace()):
49             cr.execute(line)
50     cr.commit()
51
52     for i in addons.get_modules():
53         terp_file = addons.get_module_resource(i, '__terp__.py')
54         mod_path = addons.get_module_path(i)
55         if not mod_path:
56             continue
57         info = False
58         if os.path.isfile(terp_file) and not os.path.isfile(mod_path+'.zip'):
59             info = eval(file(terp_file).read())
60         elif zipfile.is_zipfile(mod_path+'.zip'):
61             zfile = zipfile.ZipFile(mod_path+'.zip')
62             i = os.path.splitext(i)[0]
63             info = eval(zfile.read(os.path.join(i, '__terp__.py')))
64         if info:
65             categs = info.get('category', 'Uncategorized').split('/')
66             p_id = None
67             while categs:
68                 if p_id is not None:
69                     cr.execute('select id \
70                             from ir_module_category \
71                             where name=%s and parent_id=%s', (categs[0], p_id))
72                 else:
73                     cr.execute('select id \
74                             from ir_module_category \
75                             where name=%s and parent_id is NULL', (categs[0],))
76                 c_id = cr.fetchone()
77                 if not c_id:
78                     cr.execute('select nextval(\'ir_module_category_id_seq\')')
79                     c_id = cr.fetchone()[0]
80                     cr.execute('insert into ir_module_category \
81                             (id, name, parent_id) \
82                             values (%s, %s, %s)', (c_id, categs[0], p_id))
83                 else:
84                     c_id = c_id[0]
85                 p_id = c_id
86                 categs = categs[1:]
87
88             active = info.get('active', False)
89             installable = info.get('installable', True)
90             if installable:
91                 if active:
92                     state = 'to install'
93                 else:
94                     state = 'uninstalled'
95             else:
96                 state = 'uninstallable'
97             cr.execute('select nextval(\'ir_module_module_id_seq\')')
98             id = cr.fetchone()[0]
99             cr.execute('insert into ir_module_module \
100                     (id, author, website, name, shortdesc, description, \
101                         category_id, state) \
102                     values (%s, %s, %s, %s, %s, %s, %s, %s)', (
103                 id, info.get('author', ''),
104                 info.get('website', ''), i, info.get('name', False),
105                 info.get('description', ''), p_id, state))
106             dependencies = info.get('depends', [])
107             for d in dependencies:
108                 cr.execute('insert into ir_module_module_dependency \
109                         (module_id,name) values (%s, %s)', (id, d))
110             cr.commit()
111
112 def find_in_path(name):
113     if os.name == "nt":
114         sep = ';'
115     else:
116         sep = ':'
117     path = [dir for dir in os.environ['PATH'].split(sep)
118             if os.path.isdir(dir)]
119     for dir in path:
120         val = os.path.join(dir, name)
121         if os.path.isfile(val) or os.path.islink(val):
122             return val
123     return None
124
125 def find_pg_tool(name):
126     if config['pg_path'] and config['pg_path'] != 'None':
127         return os.path.join(config['pg_path'], name)
128     else:
129         return find_in_path(name)
130
131 def exec_pg_command(name, *args):
132     prog = find_pg_tool(name)
133     if not prog:
134         raise Exception('Couldn\'t find %s' % name)
135     args2 = (os.path.basename(prog),) + args
136     return os.spawnv(os.P_WAIT, prog, args2)
137
138 def exec_pg_command_pipe(name, *args):
139     prog = find_pg_tool(name)
140     if not prog:
141         raise Exception('Couldn\'t find %s' % name)
142     if os.name == "nt":
143         cmd = '"' + prog + '" ' + ' '.join(args)
144     else:
145         cmd = prog + ' ' + ' '.join(args)
146     return os.popen2(cmd, 'b')
147
148 def exec_command_pipe(name, *args):
149     prog = find_in_path(name)
150     if not prog:
151         raise Exception('Couldn\'t find %s' % name)
152     if os.name == "nt":
153         cmd = '"'+prog+'" '+' '.join(args)
154     else:
155         cmd = prog+' '+' '.join(args)
156     return os.popen2(cmd, 'b')
157
158 #----------------------------------------------------------
159 # File paths
160 #----------------------------------------------------------
161 #file_path_root = os.getcwd()
162 #file_path_addons = os.path.join(file_path_root, 'addons')
163
164 def file_open(name, mode="r", subdir='addons', pathinfo=False):
165     """Open a file from the OpenERP root, using a subdir folder.
166
167     >>> file_open('hr/report/timesheer.xsl')
168     >>> file_open('addons/hr/report/timesheet.xsl')
169     >>> file_open('../../base/report/rml_template.xsl', subdir='addons/hr/report', pathinfo=True)
170
171     @param name: name of the file
172     @param mode: file open mode
173     @param subdir: subdirectory
174     @param pathinfo: if True returns tupple (fileobject, filepath)
175
176     @return: fileobject if pathinfo is False else (fileobject, filepath)
177     """
178
179     adp = os.path.normcase(os.path.abspath(config['addons_path']))
180     rtp = os.path.normcase(os.path.abspath(config['root_path']))
181
182     if name.replace(os.path.sep, '/').startswith('addons/'):
183         subdir = 'addons'
184         name = name[7:]
185
186     # First try to locate in addons_path
187     if subdir:
188         subdir2 = subdir
189         if subdir2.replace(os.path.sep, '/').startswith('addons/'):
190             subdir2 = subdir2[7:]
191
192         subdir2 = (subdir2 != 'addons' or None) and subdir2
193
194         try:
195             if subdir2:
196                 fn = os.path.join(adp, subdir2, name)
197             else:
198                 fn = os.path.join(adp, name)
199             fn = os.path.normpath(fn)
200             fo = file_open(fn, mode=mode, subdir=None, pathinfo=pathinfo)
201             if pathinfo:
202                 return fo, fn
203             return fo
204         except IOError, e:
205             pass
206
207     if subdir:
208         name = os.path.join(rtp, subdir, name)
209     else:
210         name = os.path.join(rtp, name)
211
212     name = os.path.normpath(name)
213
214     # Check for a zipfile in the path
215     head = name
216     zipname = False
217     name2 = False
218     while True:
219         head, tail = os.path.split(head)
220         if not tail:
221             break
222         if zipname:
223             zipname = os.path.join(tail, zipname)
224         else:
225             zipname = tail
226         if zipfile.is_zipfile(head+'.zip'):
227             import StringIO
228             zfile = zipfile.ZipFile(head+'.zip')
229             try:
230                 fo = StringIO.StringIO(zfile.read(os.path.join(
231                     os.path.basename(head), zipname).replace(
232                         os.sep, '/')))
233
234                 if pathinfo:
235                     return fo, name
236                 return fo
237             except:
238                 name2 = os.path.normpath(os.path.join(head + '.zip', zipname))
239                 pass
240     for i in (name2, name):
241         if i and os.path.isfile(i):
242             fo = file(i, mode)
243             if pathinfo:
244                 return fo, i
245             return fo
246     if os.path.splitext(name)[1] == '.rml':
247         raise IOError, 'Report %s doesn\'t exist or deleted : ' %str(name)
248     raise IOError, 'File not found : '+str(name)
249
250
251 #----------------------------------------------------------
252 # iterables
253 #----------------------------------------------------------
254 def flatten(list):
255     """Flatten a list of elements into a uniqu list
256     Author: Christophe Simonis (christophe@tinyerp.com)
257
258     Examples:
259     >>> flatten(['a'])
260     ['a']
261     >>> flatten('b')
262     ['b']
263     >>> flatten( [] )
264     []
265     >>> flatten( [[], [[]]] )
266     []
267     >>> flatten( [[['a','b'], 'c'], 'd', ['e', [], 'f']] )
268     ['a', 'b', 'c', 'd', 'e', 'f']
269     >>> t = (1,2,(3,), [4, 5, [6, [7], (8, 9), ([10, 11, (12, 13)]), [14, [], (15,)], []]])
270     >>> flatten(t)
271     [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
272     """
273
274     def isiterable(x):
275         return hasattr(x, "__iter__")
276
277     r = []
278     for e in list:
279         if isiterable(e):
280             map(r.append, flatten(e))
281         else:
282             r.append(e)
283     return r
284
285 def reverse_enumerate(l):
286     """Like enumerate but in the other sens
287     >>> a = ['a', 'b', 'c']
288     >>> it = reverse_enumerate(a)
289     >>> it.next()
290     (2, 'c')
291     >>> it.next()
292     (1, 'b')
293     >>> it.next()
294     (0, 'a')
295     >>> it.next()
296     Traceback (most recent call last):
297       File "<stdin>", line 1, in <module>
298     StopIteration
299     """
300     return izip(xrange(len(l)-1, -1, -1), reversed(l))
301
302 #----------------------------------------------------------
303 # Emails
304 #----------------------------------------------------------
305 def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False, attach=None, tinycrm=False, ssl=False, debug=False,subtype='plain'):
306     """Send an email."""
307     import smtplib
308     from email.MIMEText import MIMEText
309     from email.MIMEBase import MIMEBase
310     from email.MIMEMultipart import MIMEMultipart
311     from email.Header import Header
312     from email.Utils import formatdate, COMMASPACE
313     from email.Utils import formatdate, COMMASPACE
314     from email import Encoders
315
316     if not ssl:
317         ssl = config.get('smtp_ssl', False)
318
319     if not email_cc:
320         email_cc = []
321     if not email_bcc:
322         email_bcc = []
323
324     if not attach:
325         msg = MIMEText(body or '',_subtype=subtype,_charset='utf-8')
326     else:
327         msg = MIMEMultipart()
328
329     msg['Subject'] = Header(subject.decode('utf8'), 'utf-8')
330     msg['From'] = email_from
331     del msg['Reply-To']
332     if reply_to:
333         msg['Reply-To'] = msg['From']+', '+reply_to
334     msg['To'] = COMMASPACE.join(email_to)
335     if email_cc:
336         msg['Cc'] = COMMASPACE.join(email_cc)
337     if email_bcc:
338         msg['Bcc'] = COMMASPACE.join(email_bcc)
339     msg['Date'] = formatdate(localtime=True)
340
341     if tinycrm:
342         msg['Message-Id'] = "<%s-tinycrm-%s@%s>" % (time.time(), tinycrm, socket.gethostname())
343
344     if attach:
345         msg.attach( MIMEText(body or '', _charset='utf-8', _subtype=subtype) )
346
347         for (fname,fcontent) in attach:
348             part = MIMEBase('application', "octet-stream")
349             part.set_payload( fcontent )
350             Encoders.encode_base64(part)
351             part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,))
352             msg.attach(part)
353     try:
354         s = smtplib.SMTP()
355
356         if debug:
357             s.debuglevel = 5
358         s.connect(config['smtp_server'], config['smtp_port'])
359         if ssl:
360             s.ehlo()
361             s.starttls()
362             s.ehlo()
363
364         if config['smtp_user'] or config['smtp_password']:
365             s.login(config['smtp_user'], config['smtp_password'])
366
367         s.sendmail(email_from, 
368                    flatten([email_to, email_cc, email_bcc]), 
369                    msg.as_string()
370                   )
371         s.quit()
372     except Exception, e:
373         import logging
374         logging.getLogger().error(str(e))
375         return False
376     return True
377
378 #----------------------------------------------------------
379 # SMS
380 #----------------------------------------------------------
381 # text must be latin-1 encoded
382 def sms_send(user, password, api_id, text, to):
383     import urllib
384     params = urllib.urlencode({'user': user, 'password': password, 'api_id': api_id, 'text': text, 'to':to})
385     #f = urllib.urlopen("http://api.clickatell.com/http/sendmsg", params)
386     f = urllib.urlopen("http://196.7.150.220/http/sendmsg", params)
387     # FIXME: Use the logger if there is an error
388     return True
389
390 #---------------------------------------------------------
391 # Class that stores an updateable string (used in wizards)
392 #---------------------------------------------------------
393 class UpdateableStr(local):
394
395     def __init__(self, string=''):
396         self.string = string
397
398     def __str__(self):
399         return str(self.string)
400
401     def __repr__(self):
402         return str(self.string)
403
404     def __nonzero__(self):
405         return bool(self.string)
406
407
408 class UpdateableDict(local):
409     '''Stores an updateable dict to use in wizards'''
410
411     def __init__(self, dict=None):
412         if dict is None:
413             dict = {}
414         self.dict = dict
415
416     def __str__(self):
417         return str(self.dict)
418
419     def __repr__(self):
420         return str(self.dict)
421
422     def clear(self):
423         return self.dict.clear()
424
425     def keys(self):
426         return self.dict.keys()
427
428     def __setitem__(self, i, y):
429         self.dict.__setitem__(i, y)
430
431     def __getitem__(self, i):
432         return self.dict.__getitem__(i)
433
434     def copy(self):
435         return self.dict.copy()
436
437     def iteritems(self):
438         return self.dict.iteritems()
439
440     def iterkeys(self):
441         return self.dict.iterkeys()
442
443     def itervalues(self):
444         return self.dict.itervalues()
445
446     def pop(self, k, d=None):
447         return self.dict.pop(k, d)
448
449     def popitem(self):
450         return self.dict.popitem()
451
452     def setdefault(self, k, d=None):
453         return self.dict.setdefault(k, d)
454
455     def update(self, E, **F):
456         return self.dict.update(E, F)
457
458     def values(self):
459         return self.dict.values()
460
461     def get(self, k, d=None):
462         return self.dict.get(k, d)
463
464     def has_key(self, k):
465         return self.dict.has_key(k)
466
467     def items(self):
468         return self.dict.items()
469
470     def __cmp__(self, y):
471         return self.dict.__cmp__(y)
472
473     def __contains__(self, k):
474         return self.dict.__contains__(k)
475
476     def __delitem__(self, y):
477         return self.dict.__delitem__(y)
478
479     def __eq__(self, y):
480         return self.dict.__eq__(y)
481
482     def __ge__(self, y):
483         return self.dict.__ge__(y)
484
485     def __getitem__(self, y):
486         return self.dict.__getitem__(y)
487
488     def __gt__(self, y):
489         return self.dict.__gt__(y)
490
491     def __hash__(self):
492         return self.dict.__hash__()
493
494     def __iter__(self):
495         return self.dict.__iter__()
496
497     def __le__(self, y):
498         return self.dict.__le__(y)
499
500     def __len__(self):
501         return self.dict.__len__()
502
503     def __lt__(self, y):
504         return self.dict.__lt__(y)
505
506     def __ne__(self, y):
507         return self.dict.__ne__(y)
508
509
510 # Don't use ! Use res.currency.round()
511 class currency(float):
512
513     def __init__(self, value, accuracy=2, rounding=None):
514         if rounding is None:
515             rounding=10**-accuracy
516         self.rounding=rounding
517         self.accuracy=accuracy
518
519     def __new__(cls, value, accuracy=2, rounding=None):
520         return float.__new__(cls, round(value, accuracy))
521
522     #def __str__(self):
523     #   display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
524     #   return str(display_value)
525
526
527 def is_hashable(h):
528     try:
529         hash(h)
530         return True
531     except TypeError:
532         return False
533
534 class cache(object):
535     """
536     Use it as a decorator of the function you plan to cache
537     Timeout: 0 = no timeout, otherwise in seconds
538     """
539     
540     __caches = []
541     
542     def __init__(self, timeout=None, skiparg=2, multi=None):
543         assert skiparg >= 2 # at least self and cr
544         if timeout is None:
545             self.timeout = config['cache_timeout']
546         else:
547             self.timeout = timeout
548         self.skiparg = skiparg
549         self.multi = multi
550         self.lasttime = time.time()
551         self.cache = {}
552         
553         cache.__caches.append(self)
554
555     @classmethod        
556     def clean_cache_for_db(cls, dbname):
557         def get_dbname_from_key(key):
558             for e in key:
559                 if e[0] == 'dbname':
560                     return e[1]
561             return None
562
563         for cache in cls.__caches:
564             keys_to_del = [key for key in cache.cache if get_dbname_from_key(key) == dbname]
565             for key in keys_to_del:
566                 del cache.cache[key]
567
568     def __call__(self, fn):
569         arg_names = inspect.getargspec(fn)[0][self.skiparg:]
570
571         def cached_result(self2, cr=None, *args, **kwargs):
572             if time.time()-self.timeout > self.lasttime:
573                 self.lasttime = time.time()
574                 t = time.time()-self.timeout 
575                 for key in self.cache.keys():
576                     if self.cache[key][1]<t:
577                         del self.cache[key]
578
579             if cr is None:
580                 self.cache = {}
581                 return True
582             if ('clear_keys' in kwargs):
583                 if (kwargs['clear_keys'] in self.cache):
584                     del self.cache[kwargs['clear_keys']]
585                 return True
586
587             # Update named arguments with positional argument values (without self and cr)
588             kwargs2 = kwargs.copy()
589             kwargs2.update(dict(zip(arg_names, args[self.skiparg-2:])))
590             for k in kwargs2:
591                 if isinstance(kwargs2[k], (list, dict, set)):
592                     kwargs2[k] = tuple(kwargs2[k])
593                 elif not is_hashable(kwargs2[k]):
594                     kwargs2[k] = repr(kwargs2[k])
595
596             if self.multi:
597                 kwargs3 = kwargs2.copy()
598                 notincache = []
599                 result = {}
600                 for id in kwargs3[self.multi]:
601                     kwargs2[self.multi] = [id]
602                     kwargs4 = kwargs2.items()
603                     kwargs4.sort()
604
605                     # Work out key as a tuple of ('argname', value) pairs
606                     key = (('dbname', cr.dbname),) + tuple(kwargs4)
607                     if key in self.cache:
608                         result[id] = self.cache[key][0]
609                     else:
610                         notincache.append(id)
611
612
613
614                 if notincache:
615                     kwargs2[self.multi] = notincache
616                     result2 = fn(self2, cr, *args[2:self.skip], **kwargs3)
617                     for id in result2:
618                         kwargs2[self.multi] = [id]
619                         kwargs4 = kwargs2.items()
620                         kwargs4.sort()
621                         key = (('dbname', cr.dbname),) + tuple(kwargs4)
622                         self.cache[key] = result2[id]
623                     result.updat(result2)
624                 return result
625
626             kwargs2 = kwargs2.items()
627             kwargs2.sort()
628
629             key = (('dbname', cr.dbname),) + tuple(kwargs2)
630             if key in self.cache:
631                 return self.cache[key][0]
632
633             result = fn(self2, cr, *args, **kwargs)
634
635             self.cache[key] = (result, time.time())
636             return result
637         return cached_result
638
639 def to_xml(s):
640     return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;')
641
642 def ustr(value):
643     """This method is similar to the builtin `str` method, except
644     it will return Unicode string.
645
646     @param value: the value to convert
647
648     @rtype: unicode
649     @return: unicode string
650     """
651
652     if (value is None) or (value is False):
653         return value
654     if not value:
655         return u''
656     if isinstance(value, unicode):
657         return value
658
659     if hasattr(value, '__unicode__'):
660         return unicode(value)
661
662     if not isinstance(value, str):
663         value = str(value)
664
665     return unicode(value, 'utf-8')
666
667
668 def get_languages():
669     languages={
670         'ar_AR': u'Arabic / الْعَرَبيّة',
671         'bg_BG': u'Bulgarian / български',
672         'ca_ES': u'Catalan / Català',
673         'cs_CZ': u'Czech / Čeština',
674         'de_DE': u'German / Deutsch',
675         'en_CA': u'English (CA)',
676         'en_EN': u'English (default)',
677         'en_GB': u'English (UK)',
678         'en_US': u'English (US)',
679         'es_AR': u'Spanish (AR) / Español (AR)',
680         'es_ES': u'Spanish / Español',
681         'et_ET': u'Estonian / Eesti keel',
682         'fr_BE': u'French (BE) / Français (BE)',
683         'fr_CH': u'French (CH) / Français (CH)',
684         'fr_FR': u'French / Français',
685         'hr_HR': u'Croatian / hrvatski jezik',
686         'hu_HU': u'Hungarian / Magyar',
687         'it_IT': u'Italian / Italiano',
688         'lt_LT': u'Lithuanian / Lietuvių kalba',
689         'nl_NL': u'Dutch / Nederlands',
690         'pl_PL': u'Polish / Język polski',
691         'pt_BR': u'Portugese (BR) / português (BR)',
692         'pt_PT': u'Portugese / português',
693         'ro_RO': u'Romanian / limba română',
694         'ru_RU': u'Russian / русский язык',
695         'sl_SL': u'Slovenian / slovenščina',
696         'sv_SE': u'Swedish / svenska',
697         'tr_TR': u'Turkish / Türkçe',
698         'uk_UK': u'Ukrainian / украї́нська мо́ва',
699         'zh_CN': u'Chinese (CN) / 简体中文' ,
700         'zh_TW': u'Chinese (TW) / 正體字',
701     }
702     return languages
703
704 def scan_languages():
705     import glob
706     file_list = [os.path.splitext(os.path.basename(f))[0] for f in glob.glob(os.path.join(config['root_path'],'addons', 'base', 'i18n', '*.po'))]
707     lang_dict = get_languages()
708     ret = [(lang, lang_dict.get(lang, lang)) for lang in file_list]
709     ret.sort(key=lambda k:k[1])
710     return ret
711
712
713 def get_user_companies(cr, user):
714     def _get_company_children(cr, ids):
715         if not ids:
716             return []
717         cr.execute('SELECT id FROM res_company WHERE parent_id = any(array[%s])' %(','.join([str(x) for x in ids]),))
718         res=[x[0] for x in cr.fetchall()]
719         res.extend(_get_company_children(cr, res))
720         return res
721     cr.execute('SELECT comp.id FROM res_company AS comp, res_users AS u WHERE u.id = %s AND comp.id = u.company_id' % (user,))
722     compids=[cr.fetchone()[0]]
723     compids.extend(_get_company_children(cr, compids))
724     return compids
725
726 def mod10r(number):
727     """
728     Input number : account or invoice number
729     Output return: the same number completed with the recursive mod10
730     key
731     """
732     codec=[0,9,4,6,8,2,7,1,3,5]
733     report = 0
734     result=""
735     for digit in number:
736         result += digit
737         if digit.isdigit():
738             report = codec[ (int(digit) + report) % 10 ]
739     return result + str((10 - report) % 10)
740
741
742 def human_size(sz):
743     """
744     Return the size in a human readable format
745     """
746     if not sz:
747         return False
748     units = ('bytes', 'Kb', 'Mb', 'Gb')
749     if isinstance(sz,basestring):
750         sz=len(sz)
751     s, i = float(sz), 0
752     while s >= 1024 and i < len(units)-1:
753         s = s / 1024
754         i = i + 1
755     return "%0.2f %s" % (s, units[i])
756
757 def logged(f):
758     from tools.func import wraps
759     
760     @wraps(f)
761     def wrapper(*args, **kwargs):
762         import netsvc
763         from pprint import pformat
764
765         vector = ['Call -> function: %r' % f]
766         for i, arg in enumerate(args):
767             vector.append('  arg %02d: %s' % (i, pformat(arg)))
768         for key, value in kwargs.items():
769             vector.append('  kwarg %10s: %s' % (key, pformat(value)))
770
771         timeb4 = time.time()
772         res = f(*args, **kwargs)
773         
774         vector.append('  result: %s' % pformat(res))
775         vector.append('  time delta: %s' % (time.time() - timeb4))
776         netsvc.Logger().notifyChannel('logged', netsvc.LOG_DEBUG, '\n'.join(vector))
777         return res
778
779     return wrapper
780
781 def debug(what):
782     """
783         This method allow you to debug your code without print
784         Example:
785         >>> def func_foo(bar)
786         ...     baz = bar
787         ...     debug(baz)
788         ...     qnx = (baz, bar)
789         ...     debug(qnx)
790         ...
791         >>> func_foo(42)
792
793         This will output on the logger:
794         
795             [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:baz = 42
796             [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:qnx = (42, 42)
797
798         To view the DEBUG lines in the logger you must start the server with the option
799             --log-level=debug
800
801     """
802     import netsvc
803     from inspect import stack
804     import re
805     from pprint import pformat
806     st = stack()[1]
807     param = re.split("debug *\((.+)\)", st[4][0].strip())[1].strip()
808     while param.count(')') > param.count('('): param = param[:param.rfind(')')]
809     what = pformat(what)
810     if param != what:
811         what = "%s = %s" % (param, what)
812     netsvc.Logger().notifyChannel(st[3], netsvc.LOG_DEBUG, what)
813
814
815 icons = map(lambda x: (x,x), ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
816 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
817 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
818 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
819 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
820 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
821 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
822 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
823 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
824 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
825 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
826 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
827 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
828 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
829 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
830 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
831 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
832 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
833 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
834 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
835 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
836 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
837 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
838 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
839 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
840 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
841 ])
842
843
844
845 if __name__ == '__main__':
846     import doctest
847     doctest.testmod()
848
849
850
851 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
852