[FIX] Made timezone detect more robust and added a config option to override it.
[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:
419         ssl = config.get('smtp_ssl', False)
420
421     if not email_from and not config['email_from']:
422         raise Exception("No Email sender by default, see config file")
423
424     if not email_from:
425         email_from = config.get('email_from', False)
426
427     if not email_cc:
428         email_cc = []
429     if not email_bcc:
430         email_bcc = []
431
432     if not attach:
433         try:
434             msg = MIMEText(body.encode('utf8') or '',_subtype=subtype,_charset='utf-8')
435         except:
436             msg = MIMEText(body or '',_subtype=subtype,_charset='utf-8')
437     else:
438         msg = MIMEMultipart()
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     if priority:
459         msg['X-Priority'] = priorities.get(priority, '3 (Normal)')
460
461     # Add dynamic X Header
462     for key, value in x_headers.items():
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         try:
470             msg.attach(MIMEText(body.encode('utf8') or '',_subtype=subtype,_charset='utf-8'))
471         except:
472             msg.attach(MIMEText(body or '', _charset='utf-8', _subtype=subtype) )
473         for (fname,fcontent) in attach:
474             part = MIMEBase('application', "octet-stream")
475             part.set_payload( fcontent )
476             Encoders.encode_base64(part)
477             part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,))
478             msg.attach(part)
479
480     class WriteToLogger(object):
481         def __init__(self):
482             self.logger = netsvc.Logger()
483
484         def write(self, s):
485             self.logger.notifyChannel('email_send', netsvc.LOG_DEBUG, s)
486
487     smtp_server = config['smtp_server']
488     if smtp_server.startswith('maildir:/'):
489         from mailbox import Maildir
490         maildir_path = smtp_server[8:]
491         try:
492             mdir = Maildir(maildir_path,factory=None, create = True)
493             mdir.add(msg.as_string(True))
494             return True
495         except Exception,e:
496             netsvc.Logger().notifyChannel('email_send (maildir)', netsvc.LOG_ERROR, e)
497             return False    
498     
499     try:
500         oldstderr = smtplib.stderr
501         s = smtplib.SMTP()
502         try:
503             # in case of debug, the messages are printed to stderr.
504             if debug:
505                 smtplib.stderr = WriteToLogger()
506
507             s.set_debuglevel(int(bool(debug)))  # 0 or 1            
508             s.connect(smtp_server, config['smtp_port'])
509             if ssl:
510                 s.ehlo()
511                 s.starttls()
512                 s.ehlo()
513
514             if config['smtp_user'] or config['smtp_password']:
515                 s.login(config['smtp_user'], config['smtp_password'])            
516             s.sendmail(email_from,
517                        flatten([email_to, email_cc, email_bcc]),
518                        msg.as_string()
519                       )
520         finally:
521             s.quit()
522             if debug:
523                 smtplib.stderr = oldstderr
524
525     except Exception, e:
526         netsvc.Logger().notifyChannel('email_send', netsvc.LOG_ERROR, e)
527         return False
528
529     return True
530
531 #----------------------------------------------------------
532 # SMS
533 #----------------------------------------------------------
534 # text must be latin-1 encoded
535 def sms_send(user, password, api_id, text, to):
536     import urllib
537     url = "http://api.urlsms.com/SendSMS.aspx"
538     #url = "http://196.7.150.220/http/sendmsg"
539     params = urllib.urlencode({'UserID': user, 'Password': password, 'SenderID': api_id, 'MsgText': text, 'RecipientMobileNo':to})
540     f = urllib.urlopen(url+"?"+params)
541     # FIXME: Use the logger if there is an error
542     return True
543
544 #---------------------------------------------------------
545 # Class that stores an updateable string (used in wizards)
546 #---------------------------------------------------------
547 class UpdateableStr(local):
548
549     def __init__(self, string=''):
550         self.string = string
551
552     def __str__(self):
553         return str(self.string)
554
555     def __repr__(self):
556         return str(self.string)
557
558     def __nonzero__(self):
559         return bool(self.string)
560
561
562 class UpdateableDict(local):
563     '''Stores an updateable dict to use in wizards'''
564
565     def __init__(self, dict=None):
566         if dict is None:
567             dict = {}
568         self.dict = dict
569
570     def __str__(self):
571         return str(self.dict)
572
573     def __repr__(self):
574         return str(self.dict)
575
576     def clear(self):
577         return self.dict.clear()
578
579     def keys(self):
580         return self.dict.keys()
581
582     def __setitem__(self, i, y):
583         self.dict.__setitem__(i, y)
584
585     def __getitem__(self, i):
586         return self.dict.__getitem__(i)
587
588     def copy(self):
589         return self.dict.copy()
590
591     def iteritems(self):
592         return self.dict.iteritems()
593
594     def iterkeys(self):
595         return self.dict.iterkeys()
596
597     def itervalues(self):
598         return self.dict.itervalues()
599
600     def pop(self, k, d=None):
601         return self.dict.pop(k, d)
602
603     def popitem(self):
604         return self.dict.popitem()
605
606     def setdefault(self, k, d=None):
607         return self.dict.setdefault(k, d)
608
609     def update(self, E, **F):
610         return self.dict.update(E, F)
611
612     def values(self):
613         return self.dict.values()
614
615     def get(self, k, d=None):
616         return self.dict.get(k, d)
617
618     def has_key(self, k):
619         return self.dict.has_key(k)
620
621     def items(self):
622         return self.dict.items()
623
624     def __cmp__(self, y):
625         return self.dict.__cmp__(y)
626
627     def __contains__(self, k):
628         return self.dict.__contains__(k)
629
630     def __delitem__(self, y):
631         return self.dict.__delitem__(y)
632
633     def __eq__(self, y):
634         return self.dict.__eq__(y)
635
636     def __ge__(self, y):
637         return self.dict.__ge__(y)
638
639     def __getitem__(self, y):
640         return self.dict.__getitem__(y)
641
642     def __gt__(self, y):
643         return self.dict.__gt__(y)
644
645     def __hash__(self):
646         return self.dict.__hash__()
647
648     def __iter__(self):
649         return self.dict.__iter__()
650
651     def __le__(self, y):
652         return self.dict.__le__(y)
653
654     def __len__(self):
655         return self.dict.__len__()
656
657     def __lt__(self, y):
658         return self.dict.__lt__(y)
659
660     def __ne__(self, y):
661         return self.dict.__ne__(y)
662
663
664 # Don't use ! Use res.currency.round()
665 class currency(float):
666
667     def __init__(self, value, accuracy=2, rounding=None):
668         if rounding is None:
669             rounding=10**-accuracy
670         self.rounding=rounding
671         self.accuracy=accuracy
672
673     def __new__(cls, value, accuracy=2, rounding=None):
674         return float.__new__(cls, round(value, accuracy))
675
676     #def __str__(self):
677     #   display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
678     #   return str(display_value)
679
680
681 def is_hashable(h):
682     try:
683         hash(h)
684         return True
685     except TypeError:
686         return False
687
688 class cache(object):
689     """
690     Use it as a decorator of the function you plan to cache
691     Timeout: 0 = no timeout, otherwise in seconds
692     """
693
694     __caches = []
695
696     def __init__(self, timeout=None, skiparg=2, multi=None):
697         assert skiparg >= 2 # at least self and cr
698         if timeout is None:
699             self.timeout = config['cache_timeout']
700         else:
701             self.timeout = timeout
702         self.skiparg = skiparg
703         self.multi = multi
704         self.lasttime = time.time()
705         self.cache = {}
706         self.fun = None
707         cache.__caches.append(self)
708
709
710     def _generate_keys(self, dbname, kwargs2):
711         """
712         Generate keys depending of the arguments and the self.mutli value
713         """
714
715         def to_tuple(d):
716             pairs = d.items()
717             pairs.sort(key=lambda (k,v): k)
718             for i, (k, v) in enumerate(pairs):
719                 if isinstance(v, dict):
720                     pairs[i] = (k, to_tuple(v))
721                 if isinstance(v, (list, set)):
722                     pairs[i] = (k, tuple(v))
723                 elif not is_hashable(v):
724                     pairs[i] = (k, repr(v))
725             return tuple(pairs)
726
727         if not self.multi:
728             key = (('dbname', dbname),) + to_tuple(kwargs2)
729             yield key, None
730         else:
731             multis = kwargs2[self.multi][:]
732             for id in multis:
733                 kwargs2[self.multi] = (id,)
734                 key = (('dbname', dbname),) + to_tuple(kwargs2)
735                 yield key, id
736
737     def _unify_args(self, *args, **kwargs):
738         # Update named arguments with positional argument values (without self and cr)
739         kwargs2 = self.fun_default_values.copy()
740         kwargs2.update(kwargs)
741         kwargs2.update(dict(zip(self.fun_arg_names, args[self.skiparg-2:])))
742         return kwargs2
743
744     def clear(self, dbname, *args, **kwargs):
745         """clear the cache for database dbname
746             if *args and **kwargs are both empty, clear all the keys related to this database
747         """
748         if not args and not kwargs:
749             keys_to_del = [key for key in self.cache if key[0][1] == dbname]
750         else:
751             kwargs2 = self._unify_args(*args, **kwargs)
752             keys_to_del = [key for key, _ in self._generate_keys(dbname, kwargs2) if key in self.cache]
753
754         for key in keys_to_del:
755             del self.cache[key]
756
757     @classmethod
758     def clean_caches_for_db(cls, dbname):
759         for c in cls.__caches:
760             c.clear(dbname)
761
762     def __call__(self, fn):
763         if self.fun is not None:
764             raise Exception("Can not use a cache instance on more than one function")
765         self.fun = fn
766
767         argspec = inspect.getargspec(fn)
768         self.fun_arg_names = argspec[0][self.skiparg:]
769         self.fun_default_values = {}
770         if argspec[3]:
771             self.fun_default_values = dict(zip(self.fun_arg_names[-len(argspec[3]):], argspec[3]))
772
773         def cached_result(self2, cr, *args, **kwargs):
774             if time.time()-int(self.timeout) > self.lasttime:
775                 self.lasttime = time.time()
776                 t = time.time()-int(self.timeout)
777                 old_keys = [key for key in self.cache if self.cache[key][1] < t]
778                 for key in old_keys:
779                     del self.cache[key]
780
781             kwargs2 = self._unify_args(*args, **kwargs)
782
783             result = {}
784             notincache = {}
785             for key, id in self._generate_keys(cr.dbname, kwargs2):
786                 if key in self.cache:
787                     result[id] = self.cache[key][0]
788                 else:
789                     notincache[id] = key
790
791             if notincache:
792                 if self.multi:
793                     kwargs2[self.multi] = notincache.keys()
794
795                 result2 = fn(self2, cr, *args[:self.skiparg-2], **kwargs2)
796                 if not self.multi:
797                     key = notincache[None]
798                     self.cache[key] = (result2, time.time())
799                     result[None] = result2
800                 else:
801                     for id in result2:
802                         key = notincache[id]
803                         self.cache[key] = (result2[id], time.time())
804                     result.update(result2)
805
806             if not self.multi:
807                 return result[None]
808             return result
809
810         cached_result.clear_cache = self.clear
811         return cached_result
812
813 def to_xml(s):
814     return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;')
815
816 def ustr(value):
817     """This method is similar to the builtin `str` method, except
818     it will return Unicode string.
819
820     @param value: the value to convert
821
822     @rtype: unicode
823     @return: unicode string
824     """
825
826     if isinstance(value, unicode):
827         return value
828
829     if hasattr(value, '__unicode__'):
830         return unicode(value)
831
832     if not isinstance(value, str):
833         value = str(value)
834
835     try: # first try utf-8
836         return unicode(value, 'utf-8')
837     except:
838         pass
839
840     try: # then extened iso-8858
841         return unicode(value, 'iso-8859-15')
842     except:
843         pass
844
845     # else use default system locale
846     from locale import getlocale
847     return unicode(value, getlocale()[1])
848
849 def exception_to_unicode(e):
850     if (sys.version_info[:2] < (2,6)) and hasattr(e, 'message'):
851         return ustr(e.message)
852     if hasattr(e, 'args'):
853         return "\n".join((ustr(a) for a in e.args))
854     try:
855         return ustr(e)
856     except:
857         return u"Unknown message"
858
859
860 # to be compatible with python 2.4
861 import __builtin__
862 if not hasattr(__builtin__, 'all'):
863     def all(iterable):
864         for element in iterable:
865             if not element:
866                 return False
867         return True
868
869     __builtin__.all = all
870     del all
871
872 if not hasattr(__builtin__, 'any'):
873     def any(iterable):
874         for element in iterable:
875             if element:
876                 return True
877         return False
878
879     __builtin__.any = any
880     del any
881
882 get_iso = {'ca_ES':'ca',
883 'cs_CZ': 'cs',
884 'et_EE': 'et',
885 'sv_SE': 'sv',
886 'sq_AL': 'sq',
887 'uk_UA': 'uk',
888 'vi_VN': 'vi',
889 'af_ZA': 'af',
890 'be_BY': 'be',
891 'ja_JP': 'ja',
892 'ko_KR': 'ko'
893 }
894
895 def get_iso_codes(lang):
896     if lang in get_iso:
897         lang = get_iso[lang]
898     elif lang.find('_') != -1:
899         if lang.split('_')[0] == lang.split('_')[1].lower():
900             lang = lang.split('_')[0]
901     return lang
902
903 def get_languages():
904     languages={
905         'ar_AR': u'Arabic / الْعَرَبيّة',
906         'bg_BG': u'Bulgarian / български',
907         'bs_BS': u'Bosnian / bosanski jezik',
908         'ca_ES': u'Catalan / Català',
909         'cs_CZ': u'Czech / Čeština',
910         'da_DK': u'Danish / Dansk',
911         'de_DE': u'German / Deutsch',
912         'el_GR': u'Greek / Ελληνικά',
913         'en_CA': u'English (CA)',
914         'en_GB': u'English (UK)',
915         'en_US': u'English (US)',
916         'es_AR': u'Spanish (AR) / Español (AR)',
917         'es_ES': u'Spanish / Español',
918         'et_EE': u'Estonian / Eesti keel',
919         'fi_FI': u'Finland / Suomi',
920         'fr_BE': u'French (BE) / Français (BE)',
921         'fr_CH': u'French (CH) / Français (CH)',
922         'fr_FR': u'French / Français',
923         'hr_HR': u'Croatian / hrvatski jezik',
924         'hu_HU': u'Hungarian / Magyar',
925         'id_ID': u'Indonesian / Bahasa Indonesia',
926         'it_IT': u'Italian / Italiano',
927         'lt_LT': u'Lithuanian / Lietuvių kalba',
928         'nl_NL': u'Dutch / Nederlands',
929         'nl_BE': u'Dutch (Belgium) / Nederlands (Belgïe)',
930         'pl_PL': u'Polish / Język polski',
931         'pt_BR': u'Portugese (BR) / português (BR)',
932         'pt_PT': u'Portugese / português',
933         'ro_RO': u'Romanian / limba română',
934         'ru_RU': u'Russian / русский язык',
935         'sl_SL': u'Slovenian / slovenščina',
936         'sq_AL': u'Albanian / Shqipëri',
937         'sv_SE': u'Swedish / svenska',
938         'tr_TR': u'Turkish / Türkçe',
939         'vi_VN': u'Vietnam / Cộng hòa xã hội chủ nghĩa Việt Nam',
940         'uk_UA': u'Ukrainian / украї́нська мо́ва',
941         'zh_CN': u'Chinese (CN) / 简体中文',
942         'zh_TW': u'Chinese (TW) / 正體字',
943         'th_TH': u'Thai / ภาษาไทย',
944         'tlh_TLH': u'Klingon',
945     }
946     return languages
947
948 def scan_languages():
949 #    import glob
950 #    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'))]
951 #    ret = [(lang, lang_dict.get(lang, lang)) for lang in file_list]
952     # Now it will take all languages from get languages function without filter it with base module languages
953     lang_dict = get_languages()
954     ret = [(lang, lang_dict.get(lang, lang)) for lang in list(lang_dict)]
955     ret.sort(key=lambda k:k[1])
956     return ret
957
958
959 def get_user_companies(cr, user):
960     def _get_company_children(cr, ids):
961         if not ids:
962             return []
963         cr.execute('SELECT id FROM res_company WHERE parent_id = ANY (%s)', (ids,))
964         res=[x[0] for x in cr.fetchall()]
965         res.extend(_get_company_children(cr, res))
966         return res
967     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,))
968     compids=[cr.fetchone()[0]]
969     compids.extend(_get_company_children(cr, compids))
970     return compids
971
972 def mod10r(number):
973     """
974     Input number : account or invoice number
975     Output return: the same number completed with the recursive mod10
976     key
977     """
978     codec=[0,9,4,6,8,2,7,1,3,5]
979     report = 0
980     result=""
981     for digit in number:
982         result += digit
983         if digit.isdigit():
984             report = codec[ (int(digit) + report) % 10 ]
985     return result + str((10 - report) % 10)
986
987
988 def human_size(sz):
989     """
990     Return the size in a human readable format
991     """
992     if not sz:
993         return False
994     units = ('bytes', 'Kb', 'Mb', 'Gb')
995     if isinstance(sz,basestring):
996         sz=len(sz)
997     s, i = float(sz), 0
998     while s >= 1024 and i < len(units)-1:
999         s = s / 1024
1000         i = i + 1
1001     return "%0.2f %s" % (s, units[i])
1002
1003 def logged(f):
1004     from tools.func import wraps
1005
1006     @wraps(f)
1007     def wrapper(*args, **kwargs):
1008         import netsvc
1009         from pprint import pformat
1010
1011         vector = ['Call -> function: %r' % f]
1012         for i, arg in enumerate(args):
1013             vector.append('  arg %02d: %s' % (i, pformat(arg)))
1014         for key, value in kwargs.items():
1015             vector.append('  kwarg %10s: %s' % (key, pformat(value)))
1016
1017         timeb4 = time.time()
1018         res = f(*args, **kwargs)
1019
1020         vector.append('  result: %s' % pformat(res))
1021         vector.append('  time delta: %s' % (time.time() - timeb4))
1022         netsvc.Logger().notifyChannel('logged', netsvc.LOG_DEBUG, '\n'.join(vector))
1023         return res
1024
1025     return wrapper
1026
1027 class profile(object):
1028     def __init__(self, fname=None):
1029         self.fname = fname
1030
1031     def __call__(self, f):
1032         from tools.func import wraps
1033
1034         @wraps(f)
1035         def wrapper(*args, **kwargs):
1036             class profile_wrapper(object):
1037                 def __init__(self):
1038                     self.result = None
1039                 def __call__(self):
1040                     self.result = f(*args, **kwargs)
1041             pw = profile_wrapper()
1042             import cProfile
1043             fname = self.fname or ("%s.cprof" % (f.func_name,))
1044             cProfile.runctx('pw()', globals(), locals(), filename=fname)
1045             return pw.result
1046
1047         return wrapper
1048
1049 def debug(what):
1050     """
1051         This method allow you to debug your code without print
1052         Example:
1053         >>> def func_foo(bar)
1054         ...     baz = bar
1055         ...     debug(baz)
1056         ...     qnx = (baz, bar)
1057         ...     debug(qnx)
1058         ...
1059         >>> func_foo(42)
1060
1061         This will output on the logger:
1062
1063             [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:baz = 42
1064             [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:qnx = (42, 42)
1065
1066         To view the DEBUG lines in the logger you must start the server with the option
1067             --log-level=debug
1068
1069     """
1070     import netsvc
1071     from inspect import stack
1072     import re
1073     from pprint import pformat
1074     st = stack()[1]
1075     param = re.split("debug *\((.+)\)", st[4][0].strip())[1].strip()
1076     while param.count(')') > param.count('('): param = param[:param.rfind(')')]
1077     what = pformat(what)
1078     if param != what:
1079         what = "%s = %s" % (param, what)
1080     netsvc.Logger().notifyChannel(st[3], netsvc.LOG_DEBUG, what)
1081
1082
1083 icons = map(lambda x: (x,x), ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
1084 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
1085 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
1086 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
1087 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
1088 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
1089 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
1090 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
1091 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
1092 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
1093 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
1094 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
1095 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
1096 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
1097 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
1098 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
1099 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
1100 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
1101 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
1102 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
1103 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
1104 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
1105 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
1106 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
1107 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
1108 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
1109 ])
1110
1111 def extract_zip_file(zip_file, outdirectory):
1112     import zipfile
1113     import os
1114
1115     zf = zipfile.ZipFile(zip_file, 'r')
1116     out = outdirectory
1117     for path in zf.namelist():
1118         tgt = os.path.join(out, path)
1119         tgtdir = os.path.dirname(tgt)
1120         if not os.path.exists(tgtdir):
1121             os.makedirs(tgtdir)
1122
1123         if not tgt.endswith(os.sep):
1124             fp = open(tgt, 'wb')
1125             fp.write(zf.read(path))
1126             fp.close()
1127     zf.close()
1128
1129 def detect_ip_addr():
1130     def _detect_ip_addr():
1131         from array import array
1132         import socket
1133         from struct import pack, unpack
1134
1135         try:
1136             import fcntl
1137         except ImportError:
1138             fcntl = None
1139
1140         ip_addr = None
1141
1142         if not fcntl: # not UNIX:
1143             host = socket.gethostname()
1144             ip_addr = socket.gethostbyname(host)
1145         else: # UNIX:
1146             # get all interfaces:
1147             nbytes = 128 * 32
1148             s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1149             names = array('B', '\0' * nbytes)
1150             #print 'names: ', names
1151             outbytes = unpack('iL', fcntl.ioctl( s.fileno(), 0x8912, pack('iL', nbytes, names.buffer_info()[0])))[0]
1152             namestr = names.tostring()
1153
1154             # try 64 bit kernel:
1155             for i in range(0, outbytes, 40):
1156                 name = namestr[i:i+16].split('\0', 1)[0]
1157                 if name != 'lo':
1158                     ip_addr = socket.inet_ntoa(namestr[i+20:i+24])
1159                     break
1160
1161             # try 32 bit kernel:
1162             if ip_addr is None:
1163                 ifaces = filter(None, [namestr[i:i+32].split('\0', 1)[0] for i in range(0, outbytes, 32)])
1164
1165                 for ifname in [iface for iface in ifaces if iface != 'lo']:
1166                     ip_addr = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, pack('256s', ifname[:15]))[20:24])
1167                     break
1168
1169         return ip_addr or 'localhost'
1170
1171     try:
1172         ip_addr = _detect_ip_addr()
1173     except:
1174         ip_addr = 'localhost'
1175     return ip_addr
1176
1177 # RATIONALE BEHIND TIMESTAMP CALCULATIONS AND TIMEZONE MANAGEMENT:
1178 #  The server side never does any timestamp calculation, always
1179 #  sends them in a naive (timezone agnostic) format supposed to be
1180 #  expressed within the server timezone, and expects the clients to 
1181 #  provide timestamps in the server timezone as well.
1182 #  It stores all timestamps in the database in naive format as well, 
1183 #  which also expresses the time in the server timezone.
1184 #  For this reason the server makes its timezone name available via the
1185 #  common/timezone_get() rpc method, which clients need to read
1186 #  to know the appropriate time offset to use when reading/writing
1187 #  times.
1188 def get_win32_timezone():
1189     """Attempt to return the "standard name" of the current timezone on a win32 system.
1190        @return: the standard name of the current win32 timezone, or False if it cannot be found.
1191     """
1192     res = False
1193     if (sys.platform == "win32"):
1194         try:
1195             import _winreg
1196             hklm = _winreg.ConnectRegistry(None,_winreg.HKEY_LOCAL_MACHINE)
1197             current_tz_key = _winreg.OpenKey(hklm, r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", 0,_winreg.KEY_ALL_ACCESS)
1198             res = str(_winreg.QueryValueEx(current_tz_key,"StandardName")[0])  # [0] is value, [1] is type code
1199             _winreg.CloseKey(current_tz_key)
1200             _winreg.CloseKey(hklm)
1201         except:
1202             pass
1203     return res
1204
1205 def detect_server_timezone():
1206     """Attempt to detect the timezone to use on the server side.
1207        Defaults to UTC if no working timezone can be found.
1208        @return: the timezone identifier as expected by pytz.timezone.
1209     """
1210     import time
1211     import netsvc
1212     try:
1213         import pytz
1214     except:
1215         netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_WARNING,
1216             "Python pytz module is not available. Timezone will be set to UTC by default.")
1217         return 'UTC'
1218
1219     # Option 1: the configuration option (did not exist before, so no backwards compatibility issue)
1220     # 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
1221     # Option 3: the environment variable TZ
1222     sources = [ (config['timezone'], 'OpenERP configuration'),
1223                 (time.tzname[0], 'time.tzname'),
1224                 (os.environ.get('TZ',False),'TZ environment variable'), ]
1225     # Option 4: OS-specific: /etc/timezone on Unix
1226     if (os.path.exists("/etc/timezone")):
1227         tz_value = False
1228         try:
1229             f = open("/etc/timezone")
1230             tz_value = f.read(128).strip()
1231         except:
1232             pass
1233         finally:
1234             f.close()
1235         sources.append((tz_value,"/etc/timezone file"))
1236     # Option 5: timezone info from registry on Win32
1237     if (sys.platform == "win32"):
1238         # Timezone info is stored in windows registry.
1239         # However this is not likely to work very well as the standard name
1240         # of timezones in windows is rarely something that is known to pytz.
1241         # But that's ok, it is always possible to use a config option to set
1242         # it explicitly.
1243         sources.append((get_win32_tz(),"Windows Registry"))
1244
1245     for (value,source) in sources:
1246         if value:
1247             try:
1248                 tz = pytz.timezone(value)
1249                 netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_INFO,
1250                     "Using timezone %s obtained from %s." % (tz.zone,source))
1251                 return value
1252             except pytz.UnknownTimeZoneError:
1253                 netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_WARNING,
1254                     "The timezone specified in %s (%s) is invalid, ignoring it." % (source,value))
1255
1256     netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_WARNING,
1257         "No valid timezone could be detected, using default UTC timezone. You can specify it explicitly with option 'timezone' in the server configuration.")
1258     return 'UTC'
1259
1260
1261 if __name__ == '__main__':
1262     import doctest
1263     doctest.testmod()
1264
1265
1266 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1267