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