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