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