[REVERT] r3591: causing problem to install some modules
[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 #    Copyright (C) 2010 OpenERP s.a. (<http://openerp.com>).
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU Affero General Public License as
10 #    published by the Free Software Foundation, either version 3 of the
11 #    License, or (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU Affero General Public License for more details.
17 #
18 #    You should have received a copy of the GNU Affero General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 """
24 Miscelleanous tools used by OpenERP.
25 """
26
27 import inspect
28 import subprocess
29 import logging
30 import os
31 import re
32 import smtplib
33 import socket
34 import sys
35 import threading
36 import time
37 import warnings
38 import zipfile
39 from datetime import datetime
40 from email.MIMEText import MIMEText
41 from email.MIMEBase import MIMEBase
42 from email.MIMEMultipart import MIMEMultipart
43 from email.Header import Header
44 from email.Utils import formatdate, COMMASPACE
45 from email import Encoders
46 from itertools import islice, izip
47 from lxml import etree
48 from which import which
49 if sys.version_info[:2] < (2, 4):
50     from threadinglocal import local
51 else:
52     from threading import local
53 try:
54     from html2text import html2text
55 except ImportError:
56     html2text = None
57
58 import netsvc
59 from config import config
60 from lru import LRU
61
62 _logger = logging.getLogger('tools')
63
64 # List of etree._Element subclasses that we choose to ignore when parsing XML.
65 # We include the *Base ones just in case, currently they seem to be subclasses of the _* ones.
66 SKIPPED_ELEMENT_TYPES = (etree._Comment, etree._ProcessingInstruction, etree.CommentBase, etree.PIBase)
67
68 # initialize a database with base/base.sql
69 def init_db(cr):
70     import addons
71     f = addons.get_module_resource('base', 'base.sql')
72     base_sql_file = file_open(f)
73     try:
74         cr.execute(base_sql_file.read())
75         cr.commit()
76     finally:
77         base_sql_file.close()
78
79     for i in addons.get_modules():
80         mod_path = addons.get_module_path(i)
81         if not mod_path:
82             continue
83
84         info = addons.load_information_from_description_file(i)
85
86         if not info:
87             continue
88         categs = info.get('category', 'Uncategorized').split('/')
89         p_id = None
90         while categs:
91             if p_id is not None:
92                 cr.execute('SELECT id \
93                            FROM ir_module_category \
94                            WHERE name=%s AND parent_id=%s', (categs[0], p_id))
95             else:
96                 cr.execute('SELECT id \
97                            FROM ir_module_category \
98                            WHERE name=%s AND parent_id IS NULL', (categs[0],))
99             c_id = cr.fetchone()
100             if not c_id:
101                 cr.execute('INSERT INTO ir_module_category \
102                         (name, parent_id) \
103                         VALUES (%s, %s) RETURNING id', (categs[0], p_id))
104                 c_id = cr.fetchone()[0]
105             else:
106                 c_id = c_id[0]
107             p_id = c_id
108             categs = categs[1:]
109
110         active = info.get('active', False)
111         installable = info.get('installable', True)
112         if installable:
113             if active:
114                 state = 'to install'
115             else:
116                 state = 'uninstalled'
117         else:
118             state = 'uninstallable'
119         cr.execute('INSERT INTO ir_module_module \
120                 (author, website, name, shortdesc, description, \
121                     category_id, state, certificate, web, license) \
122                 VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id', (
123             info.get('author', ''),
124             info.get('website', ''), i, info.get('name', False),
125             info.get('description', ''), p_id, state, info.get('certificate') or None,
126             info.get('web') or False,
127             info.get('license') or 'AGPL-3'))
128         id = cr.fetchone()[0]
129         cr.execute('INSERT INTO ir_model_data \
130             (name,model,module, res_id, noupdate) VALUES (%s,%s,%s,%s,%s)', (
131                 'module_meta_information', 'ir.module.module', i, id, True))
132         dependencies = info.get('depends', [])
133         for d in dependencies:
134             cr.execute('INSERT INTO ir_module_module_dependency \
135                     (module_id,name) VALUES (%s, %s)', (id, d))
136         cr.commit()
137
138 def find_in_path(name):
139     try:
140         return which(name)
141     except IOError:
142         return None
143
144 def find_pg_tool(name):
145     path = None
146     if config['pg_path'] and config['pg_path'] != 'None':
147         path = config['pg_path']
148     try:
149         return which(name, path=path)
150     except IOError:
151         return None
152
153 def exec_pg_command(name, *args):
154     prog = find_pg_tool(name)
155     if not prog:
156         raise Exception('Couldn\'t find %s' % name)
157     args2 = (prog,) + args
158     
159     return subprocess.call(args2)
160
161 def exec_pg_command_pipe(name, *args):
162     prog = find_pg_tool(name)
163     if not prog:
164         raise Exception('Couldn\'t find %s' % name)
165     # on win32, passing close_fds=True is not compatible
166     # with redirecting std[in/err/out]
167     pop = subprocess.Popen((prog,) + args, bufsize= -1,
168           stdin=subprocess.PIPE, stdout=subprocess.PIPE,
169           close_fds=(os.name=="posix"))
170     return (pop.stdin, pop.stdout)
171
172 def exec_command_pipe(name, *args):
173     prog = find_in_path(name)
174     if not prog:
175         raise Exception('Couldn\'t find %s' % name)
176     # on win32, passing close_fds=True is not compatible
177     # with redirecting std[in/err/out]
178     pop = subprocess.Popen((prog,) + args, bufsize= -1,
179           stdin=subprocess.PIPE, stdout=subprocess.PIPE,
180           close_fds=(os.name=="posix"))
181     return (pop.stdin, pop.stdout)
182
183 #----------------------------------------------------------
184 # File paths
185 #----------------------------------------------------------
186 #file_path_root = os.getcwd()
187 #file_path_addons = os.path.join(file_path_root, 'addons')
188
189 def file_open(name, mode="r", subdir='addons', pathinfo=False):
190     """Open a file from the OpenERP root, using a subdir folder.
191
192     >>> file_open('hr/report/timesheer.xsl')
193     >>> file_open('addons/hr/report/timesheet.xsl')
194     >>> file_open('../../base/report/rml_template.xsl', subdir='addons/hr/report', pathinfo=True)
195
196     @param name: name of the file
197     @param mode: file open mode
198     @param subdir: subdirectory
199     @param pathinfo: if True returns tupple (fileobject, filepath)
200
201     @return: fileobject if pathinfo is False else (fileobject, filepath)
202     """
203     import addons
204     adps = addons.ad_paths
205     rtp = os.path.normcase(os.path.abspath(config['root_path']))
206
207     if name.replace(os.path.sep, '/').startswith('addons/'):
208         subdir = 'addons'
209         name = name[7:]
210
211     # First try to locate in addons_path
212     if subdir:
213         subdir2 = subdir
214         if subdir2.replace(os.path.sep, '/').startswith('addons/'):
215             subdir2 = subdir2[7:]
216
217         subdir2 = (subdir2 != 'addons' or None) and subdir2
218
219         for adp in adps:
220             try:
221                 if subdir2:
222                     fn = os.path.join(adp, subdir2, name)
223                 else:
224                     fn = os.path.join(adp, name)
225                 fn = os.path.normpath(fn)
226                 fo = file_open(fn, mode=mode, subdir=None, pathinfo=pathinfo)
227                 if pathinfo:
228                     return fo, fn
229                 return fo
230             except IOError:
231                 pass
232
233     if subdir:
234         name = os.path.join(rtp, subdir, name)
235     else:
236         name = os.path.join(rtp, name)
237
238     name = os.path.normpath(name)
239
240     # Check for a zipfile in the path
241     head = name
242     zipname = False
243     name2 = False
244     while True:
245         head, tail = os.path.split(head)
246         if not tail:
247             break
248         if zipname:
249             zipname = os.path.join(tail, zipname)
250         else:
251             zipname = tail
252         if zipfile.is_zipfile(head+'.zip'):
253             from cStringIO import StringIO
254             zfile = zipfile.ZipFile(head+'.zip')
255             try:
256                 fo = StringIO()
257                 fo.write(zfile.read(os.path.join(
258                     os.path.basename(head), zipname).replace(
259                         os.sep, '/')))
260                 fo.seek(0)
261                 if pathinfo:
262                     return fo, name
263                 return fo
264             except Exception:
265                 name2 = os.path.normpath(os.path.join(head + '.zip', zipname))
266                 pass
267     for i in (name2, name):
268         if i and os.path.isfile(i):
269             fo = file(i, mode)
270             if pathinfo:
271                 return fo, i
272             return fo
273     if os.path.splitext(name)[1] == '.rml':
274         raise IOError, 'Report %s doesn\'t exist or deleted : ' %str(name)
275     raise IOError, 'File not found : %s' % name
276
277
278 #----------------------------------------------------------
279 # iterables
280 #----------------------------------------------------------
281 def flatten(list):
282     """Flatten a list of elements into a uniqu list
283     Author: Christophe Simonis (christophe@tinyerp.com)
284
285     Examples:
286     >>> flatten(['a'])
287     ['a']
288     >>> flatten('b')
289     ['b']
290     >>> flatten( [] )
291     []
292     >>> flatten( [[], [[]]] )
293     []
294     >>> flatten( [[['a','b'], 'c'], 'd', ['e', [], 'f']] )
295     ['a', 'b', 'c', 'd', 'e', 'f']
296     >>> t = (1,2,(3,), [4, 5, [6, [7], (8, 9), ([10, 11, (12, 13)]), [14, [], (15,)], []]])
297     >>> flatten(t)
298     [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
299     """
300
301     def isiterable(x):
302         return hasattr(x, "__iter__")
303
304     r = []
305     for e in list:
306         if isiterable(e):
307             map(r.append, flatten(e))
308         else:
309             r.append(e)
310     return r
311
312 def reverse_enumerate(l):
313     """Like enumerate but in the other sens
314     >>> a = ['a', 'b', 'c']
315     >>> it = reverse_enumerate(a)
316     >>> it.next()
317     (2, 'c')
318     >>> it.next()
319     (1, 'b')
320     >>> it.next()
321     (0, 'a')
322     >>> it.next()
323     Traceback (most recent call last):
324       File "<stdin>", line 1, in <module>
325     StopIteration
326     """
327     return izip(xrange(len(l)-1, -1, -1), reversed(l))
328
329 #----------------------------------------------------------
330 # Emails
331 #----------------------------------------------------------
332 email_re = re.compile(r"""
333     ([a-zA-Z][\w\.-]*[a-zA-Z0-9]     # username part
334     @                                # mandatory @ sign
335     [a-zA-Z0-9][\w\.-]*              # domain must start with a letter ... Ged> why do we include a 0-9 then?
336      \.
337      [a-z]{2,3}                      # TLD
338     )
339     """, re.VERBOSE)
340 res_re = re.compile(r"\[([0-9]+)\]", re.UNICODE)
341 command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE)
342 reference_re = re.compile("<.*-openobject-(\\d+)@(.*)>", re.UNICODE)
343
344 priorities = {
345         '1': '1 (Highest)',
346         '2': '2 (High)',
347         '3': '3 (Normal)',
348         '4': '4 (Low)',
349         '5': '5 (Lowest)',
350     }
351
352 def html2plaintext(html, body_id=None, encoding='utf-8'):
353     ## (c) Fry-IT, www.fry-it.com, 2007
354     ## <peter@fry-it.com>
355     ## download here: http://www.peterbe.com/plog/html2plaintext
356
357
358     """ from an HTML text, convert the HTML to plain text.
359     If @body_id is provided then this is the tag where the
360     body (not necessarily <body>) starts.
361     """
362
363     html = ustr(html)
364
365     from lxml.etree import tostring
366     try:
367         from lxml.html.soupparser import fromstring
368         kwargs = {}
369     except ImportError:
370         _logger.debug('tools.misc.html2plaintext: cannot use BeautifulSoup, fallback to lxml.etree.HTMLParser')
371         from lxml.etree import fromstring, HTMLParser
372         kwargs = dict(parser=HTMLParser())
373
374     tree = fromstring(html, **kwargs)
375
376     if body_id is not None:
377         source = tree.xpath('//*[@id=%s]'%(body_id,))
378     else:
379         source = tree.xpath('//body')
380     if len(source):
381         tree = source[0]
382
383     url_index = []
384     i = 0
385     for link in tree.findall('.//a'):
386         url = link.get('href')
387         if url:
388             i += 1
389             link.tag = 'span'
390             link.text = '%s [%s]' % (link.text, i)
391             url_index.append(url)
392
393     html = ustr(tostring(tree, encoding=encoding))
394
395     html = html.replace('<strong>','*').replace('</strong>','*')
396     html = html.replace('<b>','*').replace('</b>','*')
397     html = html.replace('<h3>','*').replace('</h3>','*')
398     html = html.replace('<h2>','**').replace('</h2>','**')
399     html = html.replace('<h1>','**').replace('</h1>','**')
400     html = html.replace('<em>','/').replace('</em>','/')
401     html = html.replace('<tr>', '\n')
402     html = html.replace('</p>', '\n')
403     html = re.sub('<br\s*/?>', '\n', html)
404     html = re.sub('<.*?>', ' ', html)
405     html = html.replace(' ' * 2, ' ')
406
407     # strip all lines
408     html = '\n'.join([x.strip() for x in html.splitlines()])
409     html = html.replace('\n' * 2, '\n')
410
411     for i, url in enumerate(url_index):
412         if i == 0:
413             html += '\n\n'
414         html += ustr('[%s] %s\n') % (i+1, url)
415
416     return html
417
418 def generate_tracking_message_id(openobject_id):
419     """Returns a string that can be used in the Message-ID RFC822 header field so we
420        can track the replies related to a given object thanks to the "In-Reply-To" or
421        "References" fields that Mail User Agents will set.
422     """
423     return "<%s-openobject-%s@%s>" % (time.time(), openobject_id, socket.gethostname())
424
425 def _email_send(smtp_from, smtp_to_list, message, openobject_id=None, ssl=False, debug=False):
426     """Low-level method to send directly a Message through the configured smtp server.
427         :param smtp_from: RFC-822 envelope FROM (not displayed to recipient)
428         :param smtp_to_list: RFC-822 envelope RCPT_TOs (not displayed to recipient)
429         :param message: an email.message.Message to send
430         :param debug: True if messages should be output to stderr before being sent,
431                       and smtplib.SMTP put into debug mode.
432         :return: True if the mail was delivered successfully to the smtp,
433                  else False (+ exception logged)
434     """
435     class WriteToLogger(object):
436         def __init__(self):
437             self.logger = netsvc.Logger()
438
439         def write(self, s):
440             self.logger.notifyChannel('email_send', netsvc.LOG_DEBUG, s)
441
442     if openobject_id:
443         message['Message-Id'] = generate_tracking_message_id(openobject_id)
444
445     try:
446         smtp_server = config['smtp_server']
447
448         if smtp_server.startswith('maildir:/'):
449             from mailbox import Maildir
450             maildir_path = smtp_server[8:]
451             mdir = Maildir(maildir_path,factory=None, create = True)
452             mdir.add(message.as_string(True))
453             return True
454
455         oldstderr = smtplib.stderr
456         if not ssl: ssl = config.get('smtp_ssl', False)
457         s = smtplib.SMTP()
458         try:
459             # in case of debug, the messages are printed to stderr.
460             if debug:
461                 smtplib.stderr = WriteToLogger()
462
463             s.set_debuglevel(int(bool(debug)))  # 0 or 1
464             s.connect(smtp_server, config['smtp_port'])
465             if ssl:
466                 s.ehlo()
467                 s.starttls()
468                 s.ehlo()
469
470             if config['smtp_user'] or config['smtp_password']:
471                 s.login(config['smtp_user'], config['smtp_password'])
472
473             s.sendmail(smtp_from, smtp_to_list, message.as_string())
474         finally:
475             try:
476                 s.quit()
477                 if debug:
478                     smtplib.stderr = oldstderr
479             except Exception:
480                 # ignored, just a consequence of the previous exception
481                 pass
482
483     except Exception:
484         _logger.error('could not deliver email', exc_info=True)
485         return False
486
487     return True
488
489
490 def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
491                attach=None, openobject_id=False, ssl=False, debug=False, subtype='plain', x_headers=None, priority='3'):
492
493     """Send an email.
494
495     Arguments:
496
497     `email_from`: A string used to fill the `From` header, if falsy,
498                   config['email_from'] is used instead.  Also used for
499                   the `Reply-To` header if `reply_to` is not provided
500
501     `email_to`: a sequence of addresses to send the mail to.
502     """
503     if x_headers is None:
504         x_headers = {}
505
506
507     if not (email_from or config['email_from']):
508         raise ValueError("Sending an email requires either providing a sender "
509                          "address or having configured one")
510
511     if not email_from: email_from = config.get('email_from', False)
512     email_from = ustr(email_from).encode('utf-8')
513
514     if not email_cc: email_cc = []
515     if not email_bcc: email_bcc = []
516     if not body: body = u''
517
518     email_body = ustr(body).encode('utf-8')
519     email_text = MIMEText(email_body or '',_subtype=subtype,_charset='utf-8')
520
521     msg = MIMEMultipart()
522
523     msg['Subject'] = Header(ustr(subject), 'utf-8')
524     msg['From'] = email_from
525     del msg['Reply-To']
526     if reply_to:
527         msg['Reply-To'] = reply_to
528     else:
529         msg['Reply-To'] = msg['From']
530     msg['To'] = COMMASPACE.join(email_to)
531     if email_cc:
532         msg['Cc'] = COMMASPACE.join(email_cc)
533     msg['Date'] = formatdate(localtime=True)
534
535     msg['X-Priority'] = priorities.get(priority, '3 (Normal)')
536
537     # Add dynamic X Header
538     for key, value in x_headers.iteritems():
539         msg['%s' % key] = str(value)
540
541     if html2text and subtype == 'html':
542         text = html2text(email_body.decode('utf-8')).encode('utf-8')
543         alternative_part = MIMEMultipart(_subtype="alternative")
544         alternative_part.attach(MIMEText(text, _charset='utf-8', _subtype='plain'))
545         alternative_part.attach(email_text)
546         msg.attach(alternative_part)
547     else:
548         msg.attach(email_text)
549
550     if attach:
551         for (fname,fcontent) in attach:
552             part = MIMEBase('application', "octet-stream")
553             part.set_payload( fcontent )
554             Encoders.encode_base64(part)
555             part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,))
556             msg.attach(part)
557
558     return _email_send(email_from, flatten([email_to, email_cc, email_bcc]), msg, openobject_id=openobject_id, ssl=ssl, debug=debug)
559
560 #----------------------------------------------------------
561 # SMS
562 #----------------------------------------------------------
563 # text must be latin-1 encoded
564 def sms_send(user, password, api_id, text, to):
565     import urllib
566     url = "http://api.urlsms.com/SendSMS.aspx"
567     #url = "http://196.7.150.220/http/sendmsg"
568     params = urllib.urlencode({'UserID': user, 'Password': password, 'SenderID': api_id, 'MsgText': text, 'RecipientMobileNo':to})
569     urllib.urlopen(url+"?"+params)
570     # FIXME: Use the logger if there is an error
571     return True
572
573 #---------------------------------------------------------
574 # Class that stores an updateable string (used in wizards)
575 #---------------------------------------------------------
576 class UpdateableStr(local):
577
578     def __init__(self, string=''):
579         self.string = string
580
581     def __str__(self):
582         return str(self.string)
583
584     def __repr__(self):
585         return str(self.string)
586
587     def __nonzero__(self):
588         return bool(self.string)
589
590
591 class UpdateableDict(local):
592     '''Stores an updateable dict to use in wizards'''
593
594     def __init__(self, dict=None):
595         if dict is None:
596             dict = {}
597         self.dict = dict
598
599     def __str__(self):
600         return str(self.dict)
601
602     def __repr__(self):
603         return str(self.dict)
604
605     def clear(self):
606         return self.dict.clear()
607
608     def keys(self):
609         return self.dict.keys()
610
611     def __setitem__(self, i, y):
612         self.dict.__setitem__(i, y)
613
614     def __getitem__(self, i):
615         return self.dict.__getitem__(i)
616
617     def copy(self):
618         return self.dict.copy()
619
620     def iteritems(self):
621         return self.dict.iteritems()
622
623     def iterkeys(self):
624         return self.dict.iterkeys()
625
626     def itervalues(self):
627         return self.dict.itervalues()
628
629     def pop(self, k, d=None):
630         return self.dict.pop(k, d)
631
632     def popitem(self):
633         return self.dict.popitem()
634
635     def setdefault(self, k, d=None):
636         return self.dict.setdefault(k, d)
637
638     def update(self, E, **F):
639         return self.dict.update(E, F)
640
641     def values(self):
642         return self.dict.values()
643
644     def get(self, k, d=None):
645         return self.dict.get(k, d)
646
647     def has_key(self, k):
648         return self.dict.has_key(k)
649
650     def items(self):
651         return self.dict.items()
652
653     def __cmp__(self, y):
654         return self.dict.__cmp__(y)
655
656     def __contains__(self, k):
657         return self.dict.__contains__(k)
658
659     def __delitem__(self, y):
660         return self.dict.__delitem__(y)
661
662     def __eq__(self, y):
663         return self.dict.__eq__(y)
664
665     def __ge__(self, y):
666         return self.dict.__ge__(y)
667
668     def __gt__(self, y):
669         return self.dict.__gt__(y)
670
671     def __hash__(self):
672         return self.dict.__hash__()
673
674     def __iter__(self):
675         return self.dict.__iter__()
676
677     def __le__(self, y):
678         return self.dict.__le__(y)
679
680     def __len__(self):
681         return self.dict.__len__()
682
683     def __lt__(self, y):
684         return self.dict.__lt__(y)
685
686     def __ne__(self, y):
687         return self.dict.__ne__(y)
688
689
690 # Don't use ! Use res.currency.round()
691 class currency(float):
692
693     def __init__(self, value, accuracy=2, rounding=None):
694         if rounding is None:
695             rounding=10**-accuracy
696         self.rounding=rounding
697         self.accuracy=accuracy
698
699     def __new__(cls, value, accuracy=2, rounding=None):
700         return float.__new__(cls, round(value, accuracy))
701
702     #def __str__(self):
703     #   display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
704     #   return str(display_value)
705
706
707 def is_hashable(h):
708     try:
709         hash(h)
710         return True
711     except TypeError:
712         return False
713
714 class cache(object):
715     """
716     Use it as a decorator of the function you plan to cache
717     Timeout: 0 = no timeout, otherwise in seconds
718     """
719
720     __caches = []
721
722     def __init__(self, timeout=None, skiparg=2, multi=None, size=8192):
723         assert skiparg >= 2 # at least self and cr
724         if timeout is None:
725             self.timeout = config['cache_timeout']
726         else:
727             self.timeout = timeout
728         self.skiparg = skiparg
729         self.multi = multi
730         self.lasttime = time.time()
731         self.cache = LRU(size)      # TODO take size from config
732         self.fun = None
733         cache.__caches.append(self)
734
735
736     def _generate_keys(self, dbname, kwargs2):
737         """
738         Generate keys depending of the arguments and the self.mutli value
739         """
740
741         def to_tuple(d):
742             pairs = d.items()
743             pairs.sort(key=lambda (k,v): k)
744             for i, (k, v) in enumerate(pairs):
745                 if isinstance(v, dict):
746                     pairs[i] = (k, to_tuple(v))
747                 if isinstance(v, (list, set)):
748                     pairs[i] = (k, tuple(v))
749                 elif not is_hashable(v):
750                     pairs[i] = (k, repr(v))
751             return tuple(pairs)
752
753         if not self.multi:
754             key = (('dbname', dbname),) + to_tuple(kwargs2)
755             yield key, None
756         else:
757             multis = kwargs2[self.multi][:]
758             for id in multis:
759                 kwargs2[self.multi] = (id,)
760                 key = (('dbname', dbname),) + to_tuple(kwargs2)
761                 yield key, id
762
763     def _unify_args(self, *args, **kwargs):
764         # Update named arguments with positional argument values (without self and cr)
765         kwargs2 = self.fun_default_values.copy()
766         kwargs2.update(kwargs)
767         kwargs2.update(dict(zip(self.fun_arg_names, args[self.skiparg-2:])))
768         return kwargs2
769
770     def clear(self, dbname, *args, **kwargs):
771         """clear the cache for database dbname
772             if *args and **kwargs are both empty, clear all the keys related to this database
773         """
774         if not args and not kwargs:
775             keys_to_del = [key for key in self.cache.keys() if key[0][1] == dbname]
776         else:
777             kwargs2 = self._unify_args(*args, **kwargs)
778             keys_to_del = [key for key, _ in self._generate_keys(dbname, kwargs2) if key in self.cache.keys()]
779
780         for key in keys_to_del:
781             self.cache.pop(key)
782
783     @classmethod
784     def clean_caches_for_db(cls, dbname):
785         for c in cls.__caches:
786             c.clear(dbname)
787
788     def __call__(self, fn):
789         if self.fun is not None:
790             raise Exception("Can not use a cache instance on more than one function")
791         self.fun = fn
792
793         argspec = inspect.getargspec(fn)
794         self.fun_arg_names = argspec[0][self.skiparg:]
795         self.fun_default_values = {}
796         if argspec[3]:
797             self.fun_default_values = dict(zip(self.fun_arg_names[-len(argspec[3]):], argspec[3]))
798
799         def cached_result(self2, cr, *args, **kwargs):
800             if time.time()-int(self.timeout) > self.lasttime:
801                 self.lasttime = time.time()
802                 t = time.time()-int(self.timeout)
803                 old_keys = [key for key in self.cache.keys() if self.cache[key][1] < t]
804                 for key in old_keys:
805                     self.cache.pop(key)
806
807             kwargs2 = self._unify_args(*args, **kwargs)
808
809             result = {}
810             notincache = {}
811             for key, id in self._generate_keys(cr.dbname, kwargs2):
812                 if key in self.cache:
813                     result[id] = self.cache[key][0]
814                 else:
815                     notincache[id] = key
816
817             if notincache:
818                 if self.multi:
819                     kwargs2[self.multi] = notincache.keys()
820
821                 result2 = fn(self2, cr, *args[:self.skiparg-2], **kwargs2)
822                 if not self.multi:
823                     key = notincache[None]
824                     self.cache[key] = (result2, time.time())
825                     result[None] = result2
826                 else:
827                     for id in result2:
828                         key = notincache[id]
829                         self.cache[key] = (result2[id], time.time())
830                     result.update(result2)
831
832             if not self.multi:
833                 return result[None]
834             return result
835
836         cached_result.clear_cache = self.clear
837         return cached_result
838
839 def to_xml(s):
840     return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;')
841
842 def get_encodings(hint_encoding='utf-8'):
843     fallbacks = {
844         'latin1': 'latin9',
845         'iso-8859-1': 'iso8859-15',
846         'cp1252': '1252',
847     }
848     if hint_encoding:
849         yield hint_encoding
850         if hint_encoding.lower() in fallbacks:
851             yield fallbacks[hint_encoding.lower()]
852
853     # some defaults (also taking care of pure ASCII)
854     for charset in ['utf8','latin1']:
855         if not (hint_encoding) or (charset.lower() != hint_encoding.lower()):
856             yield charset
857
858     from locale import getpreferredencoding
859     prefenc = getpreferredencoding()
860     if prefenc and prefenc.lower() != 'utf-8':
861         yield prefenc
862         prefenc = fallbacks.get(prefenc.lower())
863         if prefenc:
864             yield prefenc
865
866
867 def ustr(value, hint_encoding='utf-8'):
868     """This method is similar to the builtin `str` method, except
869        it will return unicode() string.
870
871     @param value: the value to convert
872     @param hint_encoding: an optional encoding that was detected
873                           upstream and should be tried first to
874                           decode ``value``.
875
876     @rtype: unicode
877     @return: unicode string
878     """
879     if isinstance(value, Exception):
880         return exception_to_unicode(value)
881
882     if isinstance(value, unicode):
883         return value
884
885     if not isinstance(value, basestring):
886         try:
887             return unicode(value)
888         except Exception:
889             raise UnicodeError('unable to convert %r' % (value,))
890
891     for ln in get_encodings(hint_encoding):
892         try:
893             return unicode(value, ln)
894         except Exception:
895             pass
896     raise UnicodeError('unable to convert %r' % (value,))
897
898
899 def exception_to_unicode(e):
900     if (sys.version_info[:2] < (2,6)) and hasattr(e, 'message'):
901         return ustr(e.message)
902     if hasattr(e, 'args'):
903         return "\n".join((ustr(a) for a in e.args))
904     try:
905         return ustr(e)
906     except Exception:
907         return u"Unknown message"
908
909
910 # to be compatible with python 2.4
911 import __builtin__
912 if not hasattr(__builtin__, 'all'):
913     def all(iterable):
914         for element in iterable:
915             if not element:
916                 return False
917         return True
918
919     __builtin__.all = all
920     del all
921
922 if not hasattr(__builtin__, 'any'):
923     def any(iterable):
924         for element in iterable:
925             if element:
926                 return True
927         return False
928
929     __builtin__.any = any
930     del any
931
932 def get_iso_codes(lang):
933     if lang.find('_') != -1:
934         if lang.split('_')[0] == lang.split('_')[1].lower():
935             lang = lang.split('_')[0]
936     return lang
937
938 def get_languages():
939     # The codes below are those from Launchpad's Rosetta, with the exception
940     # of some trivial codes where the Launchpad code is xx and we have xx_XX.
941     languages={
942         'ab_RU': u'Abkhazian / аҧсуа',
943         'ar_AR': u'Arabic / الْعَرَبيّة',
944         'bg_BG': u'Bulgarian / български език',
945         'bs_BS': u'Bosnian / bosanski jezik',
946         'ca_ES': u'Catalan / Català',
947         'cs_CZ': u'Czech / Čeština',
948         'da_DK': u'Danish / Dansk',
949         'de_DE': u'German / Deutsch',
950         'el_GR': u'Greek / Ελληνικά',
951         'en_CA': u'English (CA)',
952         'en_GB': u'English (UK)',
953         'en_US': u'English (US)',
954         'es_AR': u'Spanish (AR) / Español (AR)',
955         'es_BO': u'Spanish (BO) / Español (BO)',
956         'es_CL': u'Spanish (CL) / Español (CL)',
957         'es_CO': u'Spanish (CO) / Español (CO)',
958         'es_CR': u'Spanish (CR) / Español (CR)',
959         'es_DO': u'Spanish (DO) / Español (DO)',
960         'es_EC': u'Spanish (EC) / Español (EC)',
961         'es_ES': u'Spanish / Español',
962         'es_GT': u'Spanish (GT) / Español (GT)',
963         'es_HN': u'Spanish (HN) / Español (HN)',
964         'es_MX': u'Spanish (MX) / Español (MX)',
965         'es_NI': u'Spanish (NI) / Español (NI)',
966         'es_PA': u'Spanish (PA) / Español (PA)',
967         'es_PE': u'Spanish (PE) / Español (PE)',
968         'es_PR': u'Spanish (PR) / Español (PR)',
969         'es_PY': u'Spanish (PY) / Español (PY)',
970         'es_SV': u'Spanish (SV) / Español (SV)',
971         'es_UY': u'Spanish (UY) / Español (UY)',
972         'es_VE': u'Spanish (VE) / Español (VE)',
973         'et_EE': u'Estonian / Eesti keel',
974         'fa_IR': u'Persian / فارس',
975         'fi_FI': u'Finnish / Suomi',
976         'fr_BE': u'French (BE) / Français (BE)',
977         'fr_CH': u'French (CH) / Français (CH)',
978         'fr_FR': u'French / Français',
979         'gl_ES': u'Galician / Galego',
980         'gu_IN': u'Gujarati / ગુજરાતી',
981         'he_IL': u'Hebrew / עִבְרִי',
982         'hi_IN': u'Hindi / हिंदी',
983         'hr_HR': u'Croatian / hrvatski jezik',
984         'hu_HU': u'Hungarian / Magyar',
985         'id_ID': u'Indonesian / Bahasa Indonesia',
986         'it_IT': u'Italian / Italiano',
987         'iu_CA': u'Inuktitut / ᐃᓄᒃᑎᑐᑦ',
988         'ja_JP': u'Japanese / 日本語',
989         'ko_KP': u'Korean (KP) / 한국어 (KP)',
990         'ko_KR': u'Korean (KR) / 한국어 (KR)',
991         'lt_LT': u'Lithuanian / Lietuvių kalba',
992         'lv_LV': u'Latvian / latviešu valoda',
993         'ml_IN': u'Malayalam / മലയാളം',
994         'mn_MN': u'Mongolian / монгол',
995         'nb_NO': u'Norwegian Bokmål / Norsk bokmål',
996         'nl_NL': u'Dutch / Nederlands',
997         'nl_BE': u'Flemish (BE) / Vlaams (BE)',
998         'oc_FR': u'Occitan (FR, post 1500) / Occitan',
999         'pl_PL': u'Polish / Język polski',
1000         'pt_BR': u'Portugese (BR) / Português (BR)',
1001         'pt_PT': u'Portugese / Português',
1002         'ro_RO': u'Romanian / română',
1003         'ru_RU': u'Russian / русский язык',
1004         'si_LK': u'Sinhalese / සිංහල',
1005         'sl_SI': u'Slovenian / slovenščina',
1006         'sk_SK': u'Slovak / Slovenský jazyk',
1007         'sq_AL': u'Albanian / Shqip',
1008         'sr_RS': u'Serbian (Cyrillic) / српски',
1009         'sr@latin': u'Serbian (Latin) / srpski',
1010         'sv_SE': u'Swedish / svenska',
1011         'te_IN': u'Telugu / తెలుగు',
1012         'tr_TR': u'Turkish / Türkçe',
1013         'vi_VN': u'Vietnamese / Tiếng Việt',
1014         'uk_UA': u'Ukrainian / українська',
1015         'ur_PK': u'Urdu / اردو',
1016         'zh_CN': u'Chinese (CN) / 简体中文',
1017         'zh_HK': u'Chinese (HK)',
1018         'zh_TW': u'Chinese (TW) / 正體字',
1019         'th_TH': u'Thai / ภาษาไทย',
1020         'tlh_TLH': u'Klingon',
1021     }
1022     return languages
1023
1024 def scan_languages():
1025     # Now it will take all languages from get languages function without filter it with base module languages
1026     lang_dict = get_languages()
1027     ret = [(lang, lang_dict.get(lang, lang)) for lang in list(lang_dict)]
1028     ret.sort(key=lambda k:k[1])
1029     return ret
1030
1031
1032 def get_user_companies(cr, user):
1033     def _get_company_children(cr, ids):
1034         if not ids:
1035             return []
1036         cr.execute('SELECT id FROM res_company WHERE parent_id IN %s', (tuple(ids),))
1037         res = [x[0] for x in cr.fetchall()]
1038         res.extend(_get_company_children(cr, res))
1039         return res
1040     cr.execute('SELECT company_id FROM res_users WHERE id=%s', (user,))
1041     user_comp = cr.fetchone()[0]
1042     if not user_comp:
1043         return []
1044     return [user_comp] + _get_company_children(cr, [user_comp])
1045
1046 def mod10r(number):
1047     """
1048     Input number : account or invoice number
1049     Output return: the same number completed with the recursive mod10
1050     key
1051     """
1052     codec=[0,9,4,6,8,2,7,1,3,5]
1053     report = 0
1054     result=""
1055     for digit in number:
1056         result += digit
1057         if digit.isdigit():
1058             report = codec[ (int(digit) + report) % 10 ]
1059     return result + str((10 - report) % 10)
1060
1061
1062 def human_size(sz):
1063     """
1064     Return the size in a human readable format
1065     """
1066     if not sz:
1067         return False
1068     units = ('bytes', 'Kb', 'Mb', 'Gb')
1069     if isinstance(sz,basestring):
1070         sz=len(sz)
1071     s, i = float(sz), 0
1072     while s >= 1024 and i < len(units)-1:
1073         s = s / 1024
1074         i = i + 1
1075     return "%0.2f %s" % (s, units[i])
1076
1077 def logged(f):
1078     from tools.func import wraps
1079
1080     @wraps(f)
1081     def wrapper(*args, **kwargs):
1082         from pprint import pformat
1083
1084         vector = ['Call -> function: %r' % f]
1085         for i, arg in enumerate(args):
1086             vector.append('  arg %02d: %s' % (i, pformat(arg)))
1087         for key, value in kwargs.items():
1088             vector.append('  kwarg %10s: %s' % (key, pformat(value)))
1089
1090         timeb4 = time.time()
1091         res = f(*args, **kwargs)
1092
1093         vector.append('  result: %s' % pformat(res))
1094         vector.append('  time delta: %s' % (time.time() - timeb4))
1095         netsvc.Logger().notifyChannel('logged', netsvc.LOG_DEBUG, '\n'.join(vector))
1096         return res
1097
1098     return wrapper
1099
1100 class profile(object):
1101     def __init__(self, fname=None):
1102         self.fname = fname
1103
1104     def __call__(self, f):
1105         from tools.func import wraps
1106
1107         @wraps(f)
1108         def wrapper(*args, **kwargs):
1109             class profile_wrapper(object):
1110                 def __init__(self):
1111                     self.result = None
1112                 def __call__(self):
1113                     self.result = f(*args, **kwargs)
1114             pw = profile_wrapper()
1115             import cProfile
1116             fname = self.fname or ("%s.cprof" % (f.func_name,))
1117             cProfile.runctx('pw()', globals(), locals(), filename=fname)
1118             return pw.result
1119
1120         return wrapper
1121
1122 def debug(what):
1123     """
1124         This method allow you to debug your code without print
1125         Example:
1126         >>> def func_foo(bar)
1127         ...     baz = bar
1128         ...     debug(baz)
1129         ...     qnx = (baz, bar)
1130         ...     debug(qnx)
1131         ...
1132         >>> func_foo(42)
1133
1134         This will output on the logger:
1135
1136             [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:baz = 42
1137             [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:qnx = (42, 42)
1138
1139         To view the DEBUG lines in the logger you must start the server with the option
1140             --log-level=debug
1141
1142     """
1143     warnings.warn("The tools.debug() method is deprecated, please use logging.",
1144                       DeprecationWarning, stacklevel=2)
1145     from inspect import stack
1146     from pprint import pformat
1147     st = stack()[1]
1148     param = re.split("debug *\((.+)\)", st[4][0].strip())[1].strip()
1149     while param.count(')') > param.count('('): param = param[:param.rfind(')')]
1150     what = pformat(what)
1151     if param != what:
1152         what = "%s = %s" % (param, what)
1153     logging.getLogger(st[3]).debug(what)
1154
1155
1156 __icons_list = ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
1157 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
1158 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
1159 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
1160 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
1161 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
1162 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
1163 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
1164 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
1165 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
1166 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
1167 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
1168 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
1169 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
1170 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
1171 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
1172 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
1173 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
1174 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
1175 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
1176 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
1177 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
1178 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
1179 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
1180 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
1181 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
1182 'terp-check','terp-go-month','terp-go-year','terp-go-today','terp-document-new','terp-camera_test',
1183 'terp-emblem-important','terp-gtk-media-pause','terp-gtk-stop','terp-gnome-cpu-frequency-applet+',
1184 'terp-dialog-close','terp-gtk-jump-to-rtl','terp-gtk-jump-to-ltr','terp-accessories-archiver',
1185 'terp-stock_align_left_24','terp-stock_effects-object-colorize','terp-go-home','terp-gtk-go-back-rtl',
1186 'terp-gtk-go-back-ltr','terp-personal','terp-personal-','terp-personal+','terp-accessories-archiver-minus',
1187 'terp-accessories-archiver+','terp-stock_symbol-selection','terp-call-start','terp-dolar',
1188 'terp-face-plain','terp-folder-blue','terp-folder-green','terp-folder-orange','terp-folder-yellow',
1189 'terp-gdu-smart-failing','terp-go-week','terp-gtk-select-all','terp-locked','terp-mail-forward',
1190 'terp-mail-message-new','terp-mail-replied','terp-rating-rated','terp-stage','terp-stock_format-scientific',
1191 'terp-dolar_ok!','terp-idea','terp-stock_format-default','terp-mail-','terp-mail_delete'
1192 ]
1193
1194 def icons(*a, **kw):
1195     global __icons_list
1196     return [(x, x) for x in __icons_list ]
1197
1198 def extract_zip_file(zip_file, outdirectory):
1199     zf = zipfile.ZipFile(zip_file, 'r')
1200     out = outdirectory
1201     for path in zf.namelist():
1202         tgt = os.path.join(out, path)
1203         tgtdir = os.path.dirname(tgt)
1204         if not os.path.exists(tgtdir):
1205             os.makedirs(tgtdir)
1206
1207         if not tgt.endswith(os.sep):
1208             fp = open(tgt, 'wb')
1209             fp.write(zf.read(path))
1210             fp.close()
1211     zf.close()
1212
1213 def detect_ip_addr():
1214     """Try a very crude method to figure out a valid external
1215        IP or hostname for the current machine. Don't rely on this
1216        for binding to an interface, but it could be used as basis
1217        for constructing a remote URL to the server.
1218     """
1219     def _detect_ip_addr():
1220         from array import array
1221         from struct import pack, unpack
1222
1223         try:
1224             import fcntl
1225         except ImportError:
1226             fcntl = None
1227
1228         ip_addr = None
1229
1230         if not fcntl: # not UNIX:
1231             host = socket.gethostname()
1232             ip_addr = socket.gethostbyname(host)
1233         else: # UNIX:
1234             # get all interfaces:
1235             nbytes = 128 * 32
1236             s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1237             names = array('B', '\0' * nbytes)
1238             #print 'names: ', names
1239             outbytes = unpack('iL', fcntl.ioctl( s.fileno(), 0x8912, pack('iL', nbytes, names.buffer_info()[0])))[0]
1240             namestr = names.tostring()
1241
1242             # try 64 bit kernel:
1243             for i in range(0, outbytes, 40):
1244                 name = namestr[i:i+16].split('\0', 1)[0]
1245                 if name != 'lo':
1246                     ip_addr = socket.inet_ntoa(namestr[i+20:i+24])
1247                     break
1248
1249             # try 32 bit kernel:
1250             if ip_addr is None:
1251                 ifaces = filter(None, [namestr[i:i+32].split('\0', 1)[0] for i in range(0, outbytes, 32)])
1252
1253                 for ifname in [iface for iface in ifaces if iface != 'lo']:
1254                     ip_addr = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, pack('256s', ifname[:15]))[20:24])
1255                     break
1256
1257         return ip_addr or 'localhost'
1258
1259     try:
1260         ip_addr = _detect_ip_addr()
1261     except Exception:
1262         ip_addr = 'localhost'
1263     return ip_addr
1264
1265 # RATIONALE BEHIND TIMESTAMP CALCULATIONS AND TIMEZONE MANAGEMENT:
1266 #  The server side never does any timestamp calculation, always
1267 #  sends them in a naive (timezone agnostic) format supposed to be
1268 #  expressed within the server timezone, and expects the clients to
1269 #  provide timestamps in the server timezone as well.
1270 #  It stores all timestamps in the database in naive format as well,
1271 #  which also expresses the time in the server timezone.
1272 #  For this reason the server makes its timezone name available via the
1273 #  common/timezone_get() rpc method, which clients need to read
1274 #  to know the appropriate time offset to use when reading/writing
1275 #  times.
1276 def get_win32_timezone():
1277     """Attempt to return the "standard name" of the current timezone on a win32 system.
1278        @return: the standard name of the current win32 timezone, or False if it cannot be found.
1279     """
1280     res = False
1281     if (sys.platform == "win32"):
1282         try:
1283             import _winreg
1284             hklm = _winreg.ConnectRegistry(None,_winreg.HKEY_LOCAL_MACHINE)
1285             current_tz_key = _winreg.OpenKey(hklm, r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", 0,_winreg.KEY_ALL_ACCESS)
1286             res = str(_winreg.QueryValueEx(current_tz_key,"StandardName")[0])  # [0] is value, [1] is type code
1287             _winreg.CloseKey(current_tz_key)
1288             _winreg.CloseKey(hklm)
1289         except Exception:
1290             pass
1291     return res
1292
1293 def detect_server_timezone():
1294     """Attempt to detect the timezone to use on the server side.
1295        Defaults to UTC if no working timezone can be found.
1296        @return: the timezone identifier as expected by pytz.timezone.
1297     """
1298     try:
1299         import pytz
1300     except Exception:
1301         netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_WARNING,
1302             "Python pytz module is not available. Timezone will be set to UTC by default.")
1303         return 'UTC'
1304
1305     # Option 1: the configuration option (did not exist before, so no backwards compatibility issue)
1306     # 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
1307     # Option 3: the environment variable TZ
1308     sources = [ (config['timezone'], 'OpenERP configuration'),
1309                 (time.tzname[0], 'time.tzname'),
1310                 (os.environ.get('TZ',False),'TZ environment variable'), ]
1311     # Option 4: OS-specific: /etc/timezone on Unix
1312     if (os.path.exists("/etc/timezone")):
1313         tz_value = False
1314         try:
1315             f = open("/etc/timezone")
1316             tz_value = f.read(128).strip()
1317         except Exception:
1318             pass
1319         finally:
1320             f.close()
1321         sources.append((tz_value,"/etc/timezone file"))
1322     # Option 5: timezone info from registry on Win32
1323     if (sys.platform == "win32"):
1324         # Timezone info is stored in windows registry.
1325         # However this is not likely to work very well as the standard name
1326         # of timezones in windows is rarely something that is known to pytz.
1327         # But that's ok, it is always possible to use a config option to set
1328         # it explicitly.
1329         sources.append((get_win32_timezone(),"Windows Registry"))
1330
1331     for (value,source) in sources:
1332         if value:
1333             try:
1334                 tz = pytz.timezone(value)
1335                 netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_INFO,
1336                     "Using timezone %s obtained from %s." % (tz.zone,source))
1337                 return value
1338             except pytz.UnknownTimeZoneError:
1339                 netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_WARNING,
1340                     "The timezone specified in %s (%s) is invalid, ignoring it." % (source,value))
1341
1342     netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_WARNING,
1343         "No valid timezone could be detected, using default UTC timezone. You can specify it explicitly with option 'timezone' in the server configuration.")
1344     return 'UTC'
1345
1346 def get_server_timezone():
1347     # timezone detection is safe in multithread, so lazy init is ok here
1348     if (not config['timezone']):
1349         config['timezone'] = detect_server_timezone()
1350     return config['timezone']
1351
1352
1353 DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"
1354 DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S"
1355 DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % (
1356     DEFAULT_SERVER_DATE_FORMAT,
1357     DEFAULT_SERVER_TIME_FORMAT)
1358
1359 # Python's strftime supports only the format directives
1360 # that are available on the platform's libc, so in order to
1361 # be cross-platform we map to the directives required by
1362 # the C standard (1989 version), always available on platforms
1363 # with a C standard implementation.
1364 DATETIME_FORMATS_MAP = {
1365         '%C': '', # century
1366         '%D': '%m/%d/%Y', # modified %y->%Y
1367         '%e': '%d',
1368         '%E': '', # special modifier
1369         '%F': '%Y-%m-%d',
1370         '%g': '%Y', # modified %y->%Y
1371         '%G': '%Y',
1372         '%h': '%b',
1373         '%k': '%H',
1374         '%l': '%I',
1375         '%n': '\n',
1376         '%O': '', # special modifier
1377         '%P': '%p',
1378         '%R': '%H:%M',
1379         '%r': '%I:%M:%S %p',
1380         '%s': '', #num of seconds since epoch
1381         '%T': '%H:%M:%S',
1382         '%t': ' ', # tab
1383         '%u': ' %w',
1384         '%V': '%W',
1385         '%y': '%Y', # Even if %y works, it's ambiguous, so we should use %Y
1386         '%+': '%Y-%m-%d %H:%M:%S',
1387
1388         # %Z is a special case that causes 2 problems at least:
1389         #  - the timezone names we use (in res_user.context_tz) come
1390         #    from pytz, but not all these names are recognized by
1391         #    strptime(), so we cannot convert in both directions
1392         #    when such a timezone is selected and %Z is in the format
1393         #  - %Z is replaced by an empty string in strftime() when
1394         #    there is not tzinfo in a datetime value (e.g when the user
1395         #    did not pick a context_tz). The resulting string does not
1396         #    parse back if the format requires %Z.
1397         # As a consequence, we strip it completely from format strings.
1398         # The user can always have a look at the context_tz in
1399         # preferences to check the timezone.
1400         '%z': '',
1401         '%Z': '',
1402 }
1403
1404 def server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_name,
1405         tz_offset=True, ignore_unparsable_time=True):
1406     """
1407     Convert a source timestamp string into a destination timestamp string, attempting to apply the
1408     correct offset if both the server and local timezone are recognized, or no
1409     offset at all if they aren't or if tz_offset is false (i.e. assuming they are both in the same TZ).
1410
1411     WARNING: This method is here to allow formatting dates correctly for inclusion in strings where
1412              the client would not be able to format/offset it correctly. DO NOT use it for returning
1413              date fields directly, these are supposed to be handled by the client!!
1414
1415     @param src_tstamp_str: the str value containing the timestamp in the server timezone.
1416     @param src_format: the format to use when parsing the server timestamp.
1417     @param dst_format: the format to use when formatting the resulting timestamp for the local/client timezone.
1418     @param dst_tz_name: name of the destination timezone (such as the 'tz' value of the client context)
1419     @param ignore_unparsable_time: if True, return False if src_tstamp_str cannot be parsed
1420                                    using src_format or formatted using dst_format.
1421
1422     @return: local/client formatted timestamp, expressed in the local/client timezone if possible
1423             and if tz_offset is true, or src_tstamp_str if timezone offset could not be determined.
1424     """
1425     if not src_tstamp_str:
1426         return False
1427
1428     res = src_tstamp_str
1429     if src_format and dst_format:
1430         # find out server timezone
1431         server_tz = get_server_timezone()
1432         try:
1433             # dt_value needs to be a datetime.datetime object (so no time.struct_time or mx.DateTime.DateTime here!)
1434             dt_value = datetime.strptime(src_tstamp_str, src_format)
1435             if tz_offset and dst_tz_name:
1436                 try:
1437                     import pytz
1438                     src_tz = pytz.timezone(server_tz)
1439                     dst_tz = pytz.timezone(dst_tz_name)
1440                     src_dt = src_tz.localize(dt_value, is_dst=True)
1441                     dt_value = src_dt.astimezone(dst_tz)
1442                 except Exception:
1443                     pass
1444             res = dt_value.strftime(dst_format)
1445         except Exception:
1446             # Normal ways to end up here are if strptime or strftime failed
1447             if not ignore_unparsable_time:
1448                 return False
1449     return res
1450
1451
1452 def split_every(n, iterable, piece_maker=tuple):
1453     """Splits an iterable into length-n pieces. The last piece will be shorter
1454        if ``n`` does not evenly divide the iterable length.
1455        @param ``piece_maker``: function to build the pieces
1456        from the slices (tuple,list,...)
1457     """
1458     iterator = iter(iterable)
1459     piece = piece_maker(islice(iterator, n))
1460     while piece:
1461         yield piece
1462         piece = piece_maker(islice(iterator, n))
1463
1464 if __name__ == '__main__':
1465     import doctest
1466     doctest.testmod()
1467
1468 class upload_data_thread(threading.Thread):
1469     def __init__(self, email, data, type):
1470         self.args = [('email',email),('type',type),('data',data)]
1471         super(upload_data_thread,self).__init__()
1472     def run(self):
1473         try:
1474             import urllib
1475             args = urllib.urlencode(self.args)
1476             fp = urllib.urlopen('http://www.openerp.com/scripts/survey.php', args)
1477             fp.read()
1478             fp.close()
1479         except Exception:
1480             pass
1481
1482 def upload_data(email, data, type='SURVEY'):
1483     a = upload_data_thread(email, data, type)
1484     a.start()
1485     return True
1486
1487
1488 # port of python 2.6's attrgetter with support for dotted notation
1489 def resolve_attr(obj, attr):
1490     for name in attr.split("."):
1491         obj = getattr(obj, name)
1492     return obj
1493
1494 def attrgetter(*items):
1495     if len(items) == 1:
1496         attr = items[0]
1497         def g(obj):
1498             return resolve_attr(obj, attr)
1499     else:
1500         def g(obj):
1501             return tuple(resolve_attr(obj, attr) for attr in items)
1502     return g
1503
1504
1505 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1506