[imp] remove redundancies between sending mails with and without attachments
[odoo/odoo.git] / bin / tools / misc.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 """
23 Miscelleanous tools used by OpenERP.
24 """
25
26 import os, time, sys
27 import inspect
28
29 from config import config
30
31 import zipfile
32 import release
33 import socket
34 import re
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_open(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) or os.path.isfile(mod_path+'.zip'):
59             info = eval(file_open(terp_file).read())
60         if info:
61             categs = info.get('category', 'Uncategorized').split('/')
62             p_id = None
63             while categs:
64                 if p_id is not None:
65                     cr.execute('select id \
66                             from ir_module_category \
67                             where name=%s and parent_id=%s', (categs[0], p_id))
68                 else:
69                     cr.execute('select id \
70                             from ir_module_category \
71                             where name=%s and parent_id is NULL', (categs[0],))
72                 c_id = cr.fetchone()
73                 if not c_id:
74                     cr.execute('select nextval(\'ir_module_category_id_seq\')')
75                     c_id = cr.fetchone()[0]
76                     cr.execute('insert into ir_module_category \
77                             (id, name, parent_id) \
78                             values (%s, %s, %s)', (c_id, categs[0], p_id))
79                 else:
80                     c_id = c_id[0]
81                 p_id = c_id
82                 categs = categs[1:]
83
84             active = info.get('active', False)
85             installable = info.get('installable', True)
86             if installable:
87                 if active:
88                     state = 'to install'
89                 else:
90                     state = 'uninstalled'
91             else:
92                 state = 'uninstallable'
93             cr.execute('select nextval(\'ir_module_module_id_seq\')')
94             id = cr.fetchone()[0]
95             cr.execute('insert into ir_module_module \
96                     (id, author, website, name, shortdesc, description, \
97                         category_id, state, certificate) \
98                     values (%s, %s, %s, %s, %s, %s, %s, %s, %s)', (
99                 id, info.get('author', ''),
100                 info.get('website', ''), i, info.get('name', False),
101                 info.get('description', ''), p_id, state, info.get('certificate')))
102             cr.execute('insert into ir_model_data \
103                 (name,model,module, res_id, noupdate) values (%s,%s,%s,%s,%s)', (
104                     'module_meta_information', 'ir.module.module', i, id, True))
105             dependencies = info.get('depends', [])
106             for d in dependencies:
107                 cr.execute('insert into ir_module_module_dependency \
108                         (module_id,name) values (%s, %s)', (id, d))
109             cr.commit()
110
111 def find_in_path(name):
112     if os.name == "nt":
113         sep = ';'
114     else:
115         sep = ':'
116     path = [dir for dir in os.environ['PATH'].split(sep)
117             if os.path.isdir(dir)]
118     for dir in path:
119         val = os.path.join(dir, name)
120         if os.path.isfile(val) or os.path.islink(val):
121             return val
122     return None
123
124 def find_pg_tool(name):
125     if config['pg_path'] and config['pg_path'] != 'None':
126         return os.path.join(config['pg_path'], name)
127     else:
128         return find_in_path(name)
129
130 def exec_pg_command(name, *args):
131     prog = find_pg_tool(name)
132     if not prog:
133         raise Exception('Couldn\'t find %s' % name)
134     args2 = (os.path.basename(prog),) + args
135     return os.spawnv(os.P_WAIT, prog, args2)
136
137 def exec_pg_command_pipe(name, *args):
138     prog = find_pg_tool(name)
139     if not prog:
140         raise Exception('Couldn\'t find %s' % name)
141     if os.name == "nt":
142         cmd = '"' + prog + '" ' + ' '.join(args)
143     else:
144         cmd = prog + ' ' + ' '.join(args)
145     return os.popen2(cmd, 'b')
146
147 def exec_command_pipe(name, *args):
148     prog = find_in_path(name)
149     if not prog:
150         raise Exception('Couldn\'t find %s' % name)
151     if os.name == "nt":
152         cmd = '"'+prog+'" '+' '.join(args)
153     else:
154         cmd = prog+' '+' '.join(args)
155     return os.popen2(cmd, 'b')
156
157 #----------------------------------------------------------
158 # File paths
159 #----------------------------------------------------------
160 #file_path_root = os.getcwd()
161 #file_path_addons = os.path.join(file_path_root, 'addons')
162
163 def file_open(name, mode="r", subdir='addons', pathinfo=False):
164     """Open a file from the OpenERP root, using a subdir folder.
165
166     >>> file_open('hr/report/timesheer.xsl')
167     >>> file_open('addons/hr/report/timesheet.xsl')
168     >>> file_open('../../base/report/rml_template.xsl', subdir='addons/hr/report', pathinfo=True)
169
170     @param name: name of the file
171     @param mode: file open mode
172     @param subdir: subdirectory
173     @param pathinfo: if True returns tupple (fileobject, filepath)
174
175     @return: fileobject if pathinfo is False else (fileobject, filepath)
176     """
177     import addons
178     adps = addons.ad_paths
179     rtp = os.path.normcase(os.path.abspath(config['root_path']))
180
181     if name.replace(os.path.sep, '/').startswith('addons/'):
182         subdir = 'addons'
183         name = name[7:]
184
185     # First try to locate in addons_path
186     if subdir:
187         subdir2 = subdir
188         if subdir2.replace(os.path.sep, '/').startswith('addons/'):
189             subdir2 = subdir2[7:]
190
191         subdir2 = (subdir2 != 'addons' or None) and subdir2
192
193         for adp in adps:
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             from cStringIO import StringIO
228             zfile = zipfile.ZipFile(head+'.zip')
229             try:
230                 fo = StringIO()
231                 fo.write(zfile.read(os.path.join(
232                     os.path.basename(head), zipname).replace(
233                         os.sep, '/')))
234                 fo.seek(0)
235                 if pathinfo:
236                     return fo, name
237                 return fo
238             except:
239                 name2 = os.path.normpath(os.path.join(head + '.zip', zipname))
240                 pass
241     for i in (name2, name):
242         if i and os.path.isfile(i):
243             fo = file(i, mode)
244             if pathinfo:
245                 return fo, i
246             return fo
247     if os.path.splitext(name)[1] == '.rml':
248         raise IOError, 'Report %s doesn\'t exist or deleted : ' %str(name)
249     raise IOError, 'File not found : '+str(name)
250
251
252 #----------------------------------------------------------
253 # iterables
254 #----------------------------------------------------------
255 def flatten(list):
256     """Flatten a list of elements into a uniqu list
257     Author: Christophe Simonis (christophe@tinyerp.com)
258
259     Examples:
260     >>> flatten(['a'])
261     ['a']
262     >>> flatten('b')
263     ['b']
264     >>> flatten( [] )
265     []
266     >>> flatten( [[], [[]]] )
267     []
268     >>> flatten( [[['a','b'], 'c'], 'd', ['e', [], 'f']] )
269     ['a', 'b', 'c', 'd', 'e', 'f']
270     >>> t = (1,2,(3,), [4, 5, [6, [7], (8, 9), ([10, 11, (12, 13)]), [14, [], (15,)], []]])
271     >>> flatten(t)
272     [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
273     """
274
275     def isiterable(x):
276         return hasattr(x, "__iter__")
277
278     r = []
279     for e in list:
280         if isiterable(e):
281             map(r.append, flatten(e))
282         else:
283             r.append(e)
284     return r
285
286 def reverse_enumerate(l):
287     """Like enumerate but in the other sens
288     >>> a = ['a', 'b', 'c']
289     >>> it = reverse_enumerate(a)
290     >>> it.next()
291     (2, 'c')
292     >>> it.next()
293     (1, 'b')
294     >>> it.next()
295     (0, 'a')
296     >>> it.next()
297     Traceback (most recent call last):
298       File "<stdin>", line 1, in <module>
299     StopIteration
300     """
301     return izip(xrange(len(l)-1, -1, -1), reversed(l))
302
303 #----------------------------------------------------------
304 # Emails
305 #----------------------------------------------------------
306 email_re = re.compile(r"""
307     ([a-zA-Z][\w\.-]*[a-zA-Z0-9]     # username part
308     @                                # mandatory @ sign
309     [a-zA-Z0-9][\w\.-]*              # domain must start with a letter ... Ged> why do we include a 0-9 then?
310      \.
311      [a-z]{2,3}                      # TLD
312     )
313     """, re.VERBOSE)
314 res_re = re.compile(r"\[([0-9]+)\]", re.UNICODE)
315 command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE)
316 reference_re = re.compile("<.*-openobject-(\\d+)@(.*)>", re.UNICODE)
317
318 priorities = {
319         '1': '1 (Highest)',
320         '2': '2 (High)',
321         '3': '3 (Normal)',
322         '4': '4 (Low)',
323         '5': '5 (Lowest)',
324     }
325
326 def html2plaintext(html, body_id=None, encoding='utf-8'):
327     ## (c) Fry-IT, www.fry-it.com, 2007
328     ## <peter@fry-it.com>
329     ## download here: http://www.peterbe.com/plog/html2plaintext
330     
331     
332     """ from an HTML text, convert the HTML to plain text.
333     If @body_id is provided then this is the tag where the 
334     body (not necessarily <body>) starts.
335     """
336     try:
337         from BeautifulSoup import BeautifulSoup, SoupStrainer, Comment
338     except:
339         return html
340             
341     urls = []
342     if body_id is not None:
343         strainer = SoupStrainer(id=body_id)
344     else:
345         strainer = SoupStrainer('body')
346     
347     soup = BeautifulSoup(html, parseOnlyThese=strainer, fromEncoding=encoding)
348     for link in soup.findAll('a'):
349         title = link.renderContents()
350         for url in [x[1] for x in link.attrs if x[0]=='href']:
351             urls.append(dict(url=url, tag=str(link), title=title))
352
353     html = soup.__str__()
354             
355     url_index = []
356     i = 0
357     for d in urls:
358         if d['title'] == d['url'] or 'http://'+d['title'] == d['url']:
359             html = html.replace(d['tag'], d['url'])
360         else:
361             i += 1
362             html = html.replace(d['tag'], '%s [%s]' % (d['title'], i))
363             url_index.append(d['url'])
364
365     html = html.replace('<strong>','*').replace('</strong>','*')
366     html = html.replace('<b>','*').replace('</b>','*')
367     html = html.replace('<h3>','*').replace('</h3>','*')
368     html = html.replace('<h2>','**').replace('</h2>','**')
369     html = html.replace('<h1>','**').replace('</h1>','**')
370     html = html.replace('<em>','/').replace('</em>','/')
371     
372
373     # the only line breaks we respect is those of ending tags and 
374     # breaks
375     
376     html = html.replace('\n',' ')
377     html = html.replace('<br>', '\n')
378     html = html.replace('<tr>', '\n')
379     html = html.replace('</p>', '\n\n')
380     html = re.sub('<br\s*/>', '\n', html)
381     html = html.replace(' ' * 2, ' ')
382
383
384     # for all other tags we failed to clean up, just remove then and 
385     # complain about them on the stderr
386     def desperate_fixer(g):
387         #print >>sys.stderr, "failed to clean up %s" % str(g.group())
388         return ' '
389
390     html = re.sub('<.*?>', desperate_fixer, html)
391
392     # lstrip all lines
393     html = '\n'.join([x.lstrip() for x in html.splitlines()])
394
395     for i, url in enumerate(url_index):
396         if i == 0:
397             html += '\n\n'
398         html += '[%s] %s\n' % (i+1, url)
399     return html
400
401 def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
402                attach=None, openobject_id=False, ssl=False, debug=False, subtype='plain', x_headers=None, priority='3'):
403
404     """Send an email."""
405     import smtplib
406     from email.MIMEText import MIMEText
407     from email.MIMEBase import MIMEBase
408     from email.MIMEMultipart import MIMEMultipart
409     from email.Header import Header
410     from email.Utils import formatdate, COMMASPACE
411     from email.Utils import formatdate, COMMASPACE
412     from email import Encoders
413     import netsvc
414
415     if x_headers is None:
416         x_headers = {}
417
418     if not ssl: ssl = config.get('smtp_ssl', False)
419
420     if not (email_from or config['email_from']):
421         raise ValueError("Sending an email requires either providing a sender "
422                          "address or having configured one")
423
424     if not email_from: email_from = config.get('email_from', False)
425
426     if not email_cc: email_cc = []
427     if not email_bcc: email_bcc = []
428     if not body: body = u''
429     try:
430         email_text = MIMEText(body.encode('utf-8'),_subtype=subtype,
431                               _charset='utf-8')
432     except (UnicodeEncodeError, UnicodeDecodeError):
433         email_text = MIMEText(body,_subtype=subtype,_charset='utf-8')
434
435     if attach:
436         msg = MIMEMultipart()
437     else:
438         msg = email_text
439
440     msg['Subject'] = Header(ustr(subject), 'utf-8')
441     msg['From'] = email_from
442     del msg['Reply-To']
443     if reply_to:
444         msg['Reply-To'] = reply_to
445     else:
446         msg['Reply-To'] = msg['From']
447     msg['To'] = COMMASPACE.join(email_to)
448     if email_cc:
449         msg['Cc'] = COMMASPACE.join(email_cc)
450     if email_bcc:
451         msg['Bcc'] = COMMASPACE.join(email_bcc)
452     msg['Date'] = formatdate(localtime=True)
453
454     # Add OpenERP Server information
455     msg['X-Generated-By'] = 'OpenERP (http://www.openerp.com)'
456     msg['X-OpenERP-Server-Host'] = socket.gethostname()
457     msg['X-OpenERP-Server-Version'] = release.version
458
459     msg['X-Priority'] = priorities.get(priority, '3 (Normal)')
460
461     # Add dynamic X Header
462     for key, value in x_headers.iteritems():
463         msg['X-OpenERP-%s' % key] = str(value)
464
465     if openobject_id:
466         msg['Message-Id'] = "<%s-openobject-%s@%s>" % (time.time(), openobject_id, socket.gethostname())
467
468     if attach:
469         msg.attach(email_text)
470         for (fname,fcontent) in attach:
471             part = MIMEBase('application', "octet-stream")
472             part.set_payload( fcontent )
473             Encoders.encode_base64(part)
474             part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,))
475             msg.attach(part)
476
477     class WriteToLogger(object):
478         def __init__(self):
479             self.logger = netsvc.Logger()
480
481         def write(self, s):
482             self.logger.notifyChannel('email_send', netsvc.LOG_DEBUG, s)
483
484     smtp_server = config['smtp_server']
485     if smtp_server.startswith('maildir:/'):
486         from mailbox import Maildir
487         maildir_path = smtp_server[8:]
488         try:
489             mdir = Maildir(maildir_path,factory=None, create = True)
490             mdir.add(msg.as_string(True))
491             return True
492         except Exception,e:
493             netsvc.Logger().notifyChannel('email_send (maildir)', netsvc.LOG_ERROR, e)
494             return False    
495     
496     try:
497         oldstderr = smtplib.stderr
498         s = smtplib.SMTP()
499         try:
500             # in case of debug, the messages are printed to stderr.
501             if debug:
502                 smtplib.stderr = WriteToLogger()
503
504             s.set_debuglevel(int(bool(debug)))  # 0 or 1            
505             s.connect(smtp_server, config['smtp_port'])
506             if ssl:
507                 s.ehlo()
508                 s.starttls()
509                 s.ehlo()
510
511             if config['smtp_user'] or config['smtp_password']:
512                 s.login(config['smtp_user'], config['smtp_password'])            
513             s.sendmail(email_from,
514                        flatten([email_to, email_cc, email_bcc]),
515                        msg.as_string()
516                       )
517         finally:
518             s.quit()
519             if debug:
520                 smtplib.stderr = oldstderr
521
522     except Exception, e:
523         netsvc.Logger().notifyChannel('email_send', netsvc.LOG_ERROR, e)
524         return False
525
526     return True
527
528 #----------------------------------------------------------
529 # SMS
530 #----------------------------------------------------------
531 # text must be latin-1 encoded
532 def sms_send(user, password, api_id, text, to):
533     import urllib
534     url = "http://api.urlsms.com/SendSMS.aspx"
535     #url = "http://196.7.150.220/http/sendmsg"
536     params = urllib.urlencode({'UserID': user, 'Password': password, 'SenderID': api_id, 'MsgText': text, 'RecipientMobileNo':to})
537     f = urllib.urlopen(url+"?"+params)
538     # FIXME: Use the logger if there is an error
539     return True
540
541 #---------------------------------------------------------
542 # Class that stores an updateable string (used in wizards)
543 #---------------------------------------------------------
544 class UpdateableStr(local):
545
546     def __init__(self, string=''):
547         self.string = string
548
549     def __str__(self):
550         return str(self.string)
551
552     def __repr__(self):
553         return str(self.string)
554
555     def __nonzero__(self):
556         return bool(self.string)
557
558
559 class UpdateableDict(local):
560     '''Stores an updateable dict to use in wizards'''
561
562     def __init__(self, dict=None):
563         if dict is None:
564             dict = {}
565         self.dict = dict
566
567     def __str__(self):
568         return str(self.dict)
569
570     def __repr__(self):
571         return str(self.dict)
572
573     def clear(self):
574         return self.dict.clear()
575
576     def keys(self):
577         return self.dict.keys()
578
579     def __setitem__(self, i, y):
580         self.dict.__setitem__(i, y)
581
582     def __getitem__(self, i):
583         return self.dict.__getitem__(i)
584
585     def copy(self):
586         return self.dict.copy()
587
588     def iteritems(self):
589         return self.dict.iteritems()
590
591     def iterkeys(self):
592         return self.dict.iterkeys()
593
594     def itervalues(self):
595         return self.dict.itervalues()
596
597     def pop(self, k, d=None):
598         return self.dict.pop(k, d)
599
600     def popitem(self):
601         return self.dict.popitem()
602
603     def setdefault(self, k, d=None):
604         return self.dict.setdefault(k, d)
605
606     def update(self, E, **F):
607         return self.dict.update(E, F)
608
609     def values(self):
610         return self.dict.values()
611
612     def get(self, k, d=None):
613         return self.dict.get(k, d)
614
615     def has_key(self, k):
616         return self.dict.has_key(k)
617
618     def items(self):
619         return self.dict.items()
620
621     def __cmp__(self, y):
622         return self.dict.__cmp__(y)
623
624     def __contains__(self, k):
625         return self.dict.__contains__(k)
626
627     def __delitem__(self, y):
628         return self.dict.__delitem__(y)
629
630     def __eq__(self, y):
631         return self.dict.__eq__(y)
632
633     def __ge__(self, y):
634         return self.dict.__ge__(y)
635
636     def __gt__(self, y):
637         return self.dict.__gt__(y)
638
639     def __hash__(self):
640         return self.dict.__hash__()
641
642     def __iter__(self):
643         return self.dict.__iter__()
644
645     def __le__(self, y):
646         return self.dict.__le__(y)
647
648     def __len__(self):
649         return self.dict.__len__()
650
651     def __lt__(self, y):
652         return self.dict.__lt__(y)
653
654     def __ne__(self, y):
655         return self.dict.__ne__(y)
656
657
658 # Don't use ! Use res.currency.round()
659 class currency(float):
660
661     def __init__(self, value, accuracy=2, rounding=None):
662         if rounding is None:
663             rounding=10**-accuracy
664         self.rounding=rounding
665         self.accuracy=accuracy
666
667     def __new__(cls, value, accuracy=2, rounding=None):
668         return float.__new__(cls, round(value, accuracy))
669
670     #def __str__(self):
671     #   display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
672     #   return str(display_value)
673
674
675 def is_hashable(h):
676     try:
677         hash(h)
678         return True
679     except TypeError:
680         return False
681
682 class cache(object):
683     """
684     Use it as a decorator of the function you plan to cache
685     Timeout: 0 = no timeout, otherwise in seconds
686     """
687
688     __caches = []
689
690     def __init__(self, timeout=None, skiparg=2, multi=None):
691         assert skiparg >= 2 # at least self and cr
692         if timeout is None:
693             self.timeout = config['cache_timeout']
694         else:
695             self.timeout = timeout
696         self.skiparg = skiparg
697         self.multi = multi
698         self.lasttime = time.time()
699         self.cache = {}
700         self.fun = None
701         cache.__caches.append(self)
702
703
704     def _generate_keys(self, dbname, kwargs2):
705         """
706         Generate keys depending of the arguments and the self.mutli value
707         """
708
709         def to_tuple(d):
710             pairs = d.items()
711             pairs.sort(key=lambda (k,v): k)
712             for i, (k, v) in enumerate(pairs):
713                 if isinstance(v, dict):
714                     pairs[i] = (k, to_tuple(v))
715                 if isinstance(v, (list, set)):
716                     pairs[i] = (k, tuple(v))
717                 elif not is_hashable(v):
718                     pairs[i] = (k, repr(v))
719             return tuple(pairs)
720
721         if not self.multi:
722             key = (('dbname', dbname),) + to_tuple(kwargs2)
723             yield key, None
724         else:
725             multis = kwargs2[self.multi][:]
726             for id in multis:
727                 kwargs2[self.multi] = (id,)
728                 key = (('dbname', dbname),) + to_tuple(kwargs2)
729                 yield key, id
730
731     def _unify_args(self, *args, **kwargs):
732         # Update named arguments with positional argument values (without self and cr)
733         kwargs2 = self.fun_default_values.copy()
734         kwargs2.update(kwargs)
735         kwargs2.update(dict(zip(self.fun_arg_names, args[self.skiparg-2:])))
736         return kwargs2
737
738     def clear(self, dbname, *args, **kwargs):
739         """clear the cache for database dbname
740             if *args and **kwargs are both empty, clear all the keys related to this database
741         """
742         if not args and not kwargs:
743             keys_to_del = [key for key in self.cache if key[0][1] == dbname]
744         else:
745             kwargs2 = self._unify_args(*args, **kwargs)
746             keys_to_del = [key for key, _ in self._generate_keys(dbname, kwargs2) if key in self.cache]
747
748         for key in keys_to_del:
749             del self.cache[key]
750
751     @classmethod
752     def clean_caches_for_db(cls, dbname):
753         for c in cls.__caches:
754             c.clear(dbname)
755
756     def __call__(self, fn):
757         if self.fun is not None:
758             raise Exception("Can not use a cache instance on more than one function")
759         self.fun = fn
760
761         argspec = inspect.getargspec(fn)
762         self.fun_arg_names = argspec[0][self.skiparg:]
763         self.fun_default_values = {}
764         if argspec[3]:
765             self.fun_default_values = dict(zip(self.fun_arg_names[-len(argspec[3]):], argspec[3]))
766
767         def cached_result(self2, cr, *args, **kwargs):
768             if time.time()-int(self.timeout) > self.lasttime:
769                 self.lasttime = time.time()
770                 t = time.time()-int(self.timeout)
771                 old_keys = [key for key in self.cache if self.cache[key][1] < t]
772                 for key in old_keys:
773                     del self.cache[key]
774
775             kwargs2 = self._unify_args(*args, **kwargs)
776
777             result = {}
778             notincache = {}
779             for key, id in self._generate_keys(cr.dbname, kwargs2):
780                 if key in self.cache:
781                     result[id] = self.cache[key][0]
782                 else:
783                     notincache[id] = key
784
785             if notincache:
786                 if self.multi:
787                     kwargs2[self.multi] = notincache.keys()
788
789                 result2 = fn(self2, cr, *args[:self.skiparg-2], **kwargs2)
790                 if not self.multi:
791                     key = notincache[None]
792                     self.cache[key] = (result2, time.time())
793                     result[None] = result2
794                 else:
795                     for id in result2:
796                         key = notincache[id]
797                         self.cache[key] = (result2[id], time.time())
798                     result.update(result2)
799
800             if not self.multi:
801                 return result[None]
802             return result
803
804         cached_result.clear_cache = self.clear
805         return cached_result
806
807 def to_xml(s):
808     return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;')
809
810 def ustr(value):
811     """This method is similar to the builtin `str` method, except
812     it will return Unicode string.
813
814     @param value: the value to convert
815
816     @rtype: unicode
817     @return: unicode string
818     """
819
820     if isinstance(value, unicode):
821         return value
822
823     if hasattr(value, '__unicode__'):
824         return unicode(value)
825
826     if not isinstance(value, str):
827         value = str(value)
828
829     try: # first try utf-8
830         return unicode(value, 'utf-8')
831     except:
832         pass
833
834     try: # then extened iso-8858
835         return unicode(value, 'iso-8859-15')
836     except:
837         pass
838
839     # else use default system locale
840     from locale import getlocale
841     return unicode(value, getlocale()[1])
842
843 def exception_to_unicode(e):
844     if (sys.version_info[:2] < (2,6)) and hasattr(e, 'message'):
845         return ustr(e.message)
846     if hasattr(e, 'args'):
847         return "\n".join((ustr(a) for a in e.args))
848     try:
849         return ustr(e)
850     except:
851         return u"Unknown message"
852
853
854 # to be compatible with python 2.4
855 import __builtin__
856 if not hasattr(__builtin__, 'all'):
857     def all(iterable):
858         for element in iterable:
859             if not element:
860                 return False
861         return True
862
863     __builtin__.all = all
864     del all
865
866 if not hasattr(__builtin__, 'any'):
867     def any(iterable):
868         for element in iterable:
869             if element:
870                 return True
871         return False
872
873     __builtin__.any = any
874     del any
875
876 get_iso = {'ca_ES':'ca',
877 'cs_CZ': 'cs',
878 'et_EE': 'et',
879 'sv_SE': 'sv',
880 'sq_AL': 'sq',
881 'uk_UA': 'uk',
882 'vi_VN': 'vi',
883 'af_ZA': 'af',
884 'be_BY': 'be',
885 'ja_JP': 'ja',
886 'ko_KR': 'ko'
887 }
888
889 def get_iso_codes(lang):
890     if lang in get_iso:
891         lang = get_iso[lang]
892     elif lang.find('_') != -1:
893         if lang.split('_')[0] == lang.split('_')[1].lower():
894             lang = lang.split('_')[0]
895     return lang
896
897 def get_languages():
898     languages={
899         'ar_AR': u'Arabic / الْعَرَبيّة',
900         'bg_BG': u'Bulgarian / български',
901         'bs_BS': u'Bosnian / bosanski jezik',
902         'ca_ES': u'Catalan / Català',
903         'cs_CZ': u'Czech / Čeština',
904         'da_DK': u'Danish / Dansk',
905         'de_DE': u'German / Deutsch',
906         'el_GR': u'Greek / Ελληνικά',
907         'en_CA': u'English (CA)',
908         'en_GB': u'English (UK)',
909         'en_US': u'English (US)',
910         'es_AR': u'Spanish (AR) / Español (AR)',
911         'es_ES': u'Spanish / Español',
912         'et_EE': u'Estonian / Eesti keel',
913         'fi_FI': u'Finland / Suomi',
914         'fr_BE': u'French (BE) / Français (BE)',
915         'fr_CH': u'French (CH) / Français (CH)',
916         'fr_FR': u'French / Français',
917         'hr_HR': u'Croatian / hrvatski jezik',
918         'hu_HU': u'Hungarian / Magyar',
919         'id_ID': u'Indonesian / Bahasa Indonesia',
920         'it_IT': u'Italian / Italiano',
921         'lt_LT': u'Lithuanian / Lietuvių kalba',
922         'nl_NL': u'Dutch / Nederlands',
923         'nl_BE': u'Dutch (Belgium) / Nederlands (Belgïe)',
924         'pl_PL': u'Polish / Język polski',
925         'pt_BR': u'Portugese (BR) / português (BR)',
926         'pt_PT': u'Portugese / português',
927         'ro_RO': u'Romanian / limba română',
928         'ru_RU': u'Russian / русский язык',
929         'sl_SL': u'Slovenian / slovenščina',
930         'sq_AL': u'Albanian / Shqipëri',
931         'sv_SE': u'Swedish / svenska',
932         'tr_TR': u'Turkish / Türkçe',
933         'vi_VN': u'Vietnam / Cộng hòa xã hội chủ nghĩa Việt Nam',
934         'uk_UA': u'Ukrainian / украї́нська мо́ва',
935         'zh_CN': u'Chinese (CN) / 简体中文',
936         'zh_TW': u'Chinese (TW) / 正體字',
937         'th_TH': u'Thai / ภาษาไทย',
938         'tlh_TLH': u'Klingon',
939     }
940     return languages
941
942 def scan_languages():
943 #    import glob
944 #    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'))]
945 #    ret = [(lang, lang_dict.get(lang, lang)) for lang in file_list]
946     # Now it will take all languages from get languages function without filter it with base module languages
947     lang_dict = get_languages()
948     ret = [(lang, lang_dict.get(lang, lang)) for lang in list(lang_dict)]
949     ret.sort(key=lambda k:k[1])
950     return ret
951
952
953 def get_user_companies(cr, user):
954     def _get_company_children(cr, ids):
955         if not ids:
956             return []
957         cr.execute('SELECT id FROM res_company WHERE parent_id = ANY (%s)', (ids,))
958         res=[x[0] for x in cr.fetchall()]
959         res.extend(_get_company_children(cr, res))
960         return res
961     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,))
962     compids=[cr.fetchone()[0]]
963     compids.extend(_get_company_children(cr, compids))
964     return compids
965
966 def mod10r(number):
967     """
968     Input number : account or invoice number
969     Output return: the same number completed with the recursive mod10
970     key
971     """
972     codec=[0,9,4,6,8,2,7,1,3,5]
973     report = 0
974     result=""
975     for digit in number:
976         result += digit
977         if digit.isdigit():
978             report = codec[ (int(digit) + report) % 10 ]
979     return result + str((10 - report) % 10)
980
981
982 def human_size(sz):
983     """
984     Return the size in a human readable format
985     """
986     if not sz:
987         return False
988     units = ('bytes', 'Kb', 'Mb', 'Gb')
989     if isinstance(sz,basestring):
990         sz=len(sz)
991     s, i = float(sz), 0
992     while s >= 1024 and i < len(units)-1:
993         s = s / 1024
994         i = i + 1
995     return "%0.2f %s" % (s, units[i])
996
997 def logged(f):
998     from tools.func import wraps
999
1000     @wraps(f)
1001     def wrapper(*args, **kwargs):
1002         import netsvc
1003         from pprint import pformat
1004
1005         vector = ['Call -> function: %r' % f]
1006         for i, arg in enumerate(args):
1007             vector.append('  arg %02d: %s' % (i, pformat(arg)))
1008         for key, value in kwargs.items():
1009             vector.append('  kwarg %10s: %s' % (key, pformat(value)))
1010
1011         timeb4 = time.time()
1012         res = f(*args, **kwargs)
1013
1014         vector.append('  result: %s' % pformat(res))
1015         vector.append('  time delta: %s' % (time.time() - timeb4))
1016         netsvc.Logger().notifyChannel('logged', netsvc.LOG_DEBUG, '\n'.join(vector))
1017         return res
1018
1019     return wrapper
1020
1021 class profile(object):
1022     def __init__(self, fname=None):
1023         self.fname = fname
1024
1025     def __call__(self, f):
1026         from tools.func import wraps
1027
1028         @wraps(f)
1029         def wrapper(*args, **kwargs):
1030             class profile_wrapper(object):
1031                 def __init__(self):
1032                     self.result = None
1033                 def __call__(self):
1034                     self.result = f(*args, **kwargs)
1035             pw = profile_wrapper()
1036             import cProfile
1037             fname = self.fname or ("%s.cprof" % (f.func_name,))
1038             cProfile.runctx('pw()', globals(), locals(), filename=fname)
1039             return pw.result
1040
1041         return wrapper
1042
1043 def debug(what):
1044     """
1045         This method allow you to debug your code without print
1046         Example:
1047         >>> def func_foo(bar)
1048         ...     baz = bar
1049         ...     debug(baz)
1050         ...     qnx = (baz, bar)
1051         ...     debug(qnx)
1052         ...
1053         >>> func_foo(42)
1054
1055         This will output on the logger:
1056
1057             [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:baz = 42
1058             [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:qnx = (42, 42)
1059
1060         To view the DEBUG lines in the logger you must start the server with the option
1061             --log-level=debug
1062
1063     """
1064     import netsvc
1065     from inspect import stack
1066     import re
1067     from pprint import pformat
1068     st = stack()[1]
1069     param = re.split("debug *\((.+)\)", st[4][0].strip())[1].strip()
1070     while param.count(')') > param.count('('): param = param[:param.rfind(')')]
1071     what = pformat(what)
1072     if param != what:
1073         what = "%s = %s" % (param, what)
1074     netsvc.Logger().notifyChannel(st[3], netsvc.LOG_DEBUG, what)
1075
1076
1077 icons = map(lambda x: (x,x), ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
1078 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
1079 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
1080 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
1081 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
1082 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
1083 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
1084 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
1085 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
1086 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
1087 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
1088 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
1089 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
1090 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
1091 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
1092 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
1093 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
1094 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
1095 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
1096 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
1097 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
1098 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
1099 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
1100 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
1101 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
1102 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
1103 ])
1104
1105 def extract_zip_file(zip_file, outdirectory):
1106     import zipfile
1107     import os
1108
1109     zf = zipfile.ZipFile(zip_file, 'r')
1110     out = outdirectory
1111     for path in zf.namelist():
1112         tgt = os.path.join(out, path)
1113         tgtdir = os.path.dirname(tgt)
1114         if not os.path.exists(tgtdir):
1115             os.makedirs(tgtdir)
1116
1117         if not tgt.endswith(os.sep):
1118             fp = open(tgt, 'wb')
1119             fp.write(zf.read(path))
1120             fp.close()
1121     zf.close()
1122
1123 def detect_ip_addr():
1124     def _detect_ip_addr():
1125         from array import array
1126         import socket
1127         from struct import pack, unpack
1128
1129         try:
1130             import fcntl
1131         except ImportError:
1132             fcntl = None
1133
1134         ip_addr = None
1135
1136         if not fcntl: # not UNIX:
1137             host = socket.gethostname()
1138             ip_addr = socket.gethostbyname(host)
1139         else: # UNIX:
1140             # get all interfaces:
1141             nbytes = 128 * 32
1142             s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1143             names = array('B', '\0' * nbytes)
1144             #print 'names: ', names
1145             outbytes = unpack('iL', fcntl.ioctl( s.fileno(), 0x8912, pack('iL', nbytes, names.buffer_info()[0])))[0]
1146             namestr = names.tostring()
1147
1148             # try 64 bit kernel:
1149             for i in range(0, outbytes, 40):
1150                 name = namestr[i:i+16].split('\0', 1)[0]
1151                 if name != 'lo':
1152                     ip_addr = socket.inet_ntoa(namestr[i+20:i+24])
1153                     break
1154
1155             # try 32 bit kernel:
1156             if ip_addr is None:
1157                 ifaces = filter(None, [namestr[i:i+32].split('\0', 1)[0] for i in range(0, outbytes, 32)])
1158
1159                 for ifname in [iface for iface in ifaces if iface != 'lo']:
1160                     ip_addr = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, pack('256s', ifname[:15]))[20:24])
1161                     break
1162
1163         return ip_addr or 'localhost'
1164
1165     try:
1166         ip_addr = _detect_ip_addr()
1167     except:
1168         ip_addr = 'localhost'
1169     return ip_addr
1170
1171 # RATIONALE BEHIND TIMESTAMP CALCULATIONS AND TIMEZONE MANAGEMENT:
1172 #  The server side never does any timestamp calculation, always
1173 #  sends them in a naive (timezone agnostic) format supposed to be
1174 #  expressed within the server timezone, and expects the clients to 
1175 #  provide timestamps in the server timezone as well.
1176 #  It stores all timestamps in the database in naive format as well, 
1177 #  which also expresses the time in the server timezone.
1178 #  For this reason the server makes its timezone name available via the
1179 #  common/timezone_get() rpc method, which clients need to read
1180 #  to know the appropriate time offset to use when reading/writing
1181 #  times.
1182 def get_win32_timezone():
1183     """Attempt to return the "standard name" of the current timezone on a win32 system.
1184        @return: the standard name of the current win32 timezone, or False if it cannot be found.
1185     """
1186     res = False
1187     if (sys.platform == "win32"):
1188         try:
1189             import _winreg
1190             hklm = _winreg.ConnectRegistry(None,_winreg.HKEY_LOCAL_MACHINE)
1191             current_tz_key = _winreg.OpenKey(hklm, r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", 0,_winreg.KEY_ALL_ACCESS)
1192             res = str(_winreg.QueryValueEx(current_tz_key,"StandardName")[0])  # [0] is value, [1] is type code
1193             _winreg.CloseKey(current_tz_key)
1194             _winreg.CloseKey(hklm)
1195         except:
1196             pass
1197     return res
1198
1199 def detect_server_timezone():
1200     """Attempt to detect the timezone to use on the server side.
1201        Defaults to UTC if no working timezone can be found.
1202        @return: the timezone identifier as expected by pytz.timezone.
1203     """
1204     import time
1205     import netsvc
1206     try:
1207         import pytz
1208     except:
1209         netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_WARNING,
1210             "Python pytz module is not available. Timezone will be set to UTC by default.")
1211         return 'UTC'
1212
1213     # Option 1: the configuration option (did not exist before, so no backwards compatibility issue)
1214     # Option 2: to be backwards compatible with 5.0 or earlier, the value from time.tzname[0], but only if it is known to pytz
1215     # Option 3: the environment variable TZ
1216     sources = [ (config['timezone'], 'OpenERP configuration'),
1217                 (time.tzname[0], 'time.tzname'),
1218                 (os.environ.get('TZ',False),'TZ environment variable'), ]
1219     # Option 4: OS-specific: /etc/timezone on Unix
1220     if (os.path.exists("/etc/timezone")):
1221         tz_value = False
1222         try:
1223             f = open("/etc/timezone")
1224             tz_value = f.read(128).strip()
1225         except:
1226             pass
1227         finally:
1228             f.close()
1229         sources.append((tz_value,"/etc/timezone file"))
1230     # Option 5: timezone info from registry on Win32
1231     if (sys.platform == "win32"):
1232         # Timezone info is stored in windows registry.
1233         # However this is not likely to work very well as the standard name
1234         # of timezones in windows is rarely something that is known to pytz.
1235         # But that's ok, it is always possible to use a config option to set
1236         # it explicitly.
1237         sources.append((get_win32_timezone(),"Windows Registry"))
1238
1239     for (value,source) in sources:
1240         if value:
1241             try:
1242                 tz = pytz.timezone(value)
1243                 netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_INFO,
1244                     "Using timezone %s obtained from %s." % (tz.zone,source))
1245                 return value
1246             except pytz.UnknownTimeZoneError:
1247                 netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_WARNING,
1248                     "The timezone specified in %s (%s) is invalid, ignoring it." % (source,value))
1249
1250     netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_WARNING,
1251         "No valid timezone could be detected, using default UTC timezone. You can specify it explicitly with option 'timezone' in the server configuration.")
1252     return 'UTC'
1253
1254
1255 if __name__ == '__main__':
1256     import doctest
1257     doctest.testmod()
1258
1259
1260 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1261