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