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