[IMP] openerp python module.
[odoo/odoo.git] / openerp / 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 openerp.loglevels as loglevels
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 openerp.addons as 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 openerp.addons as 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 = loglevels.Logger()
438
439         def write(self, s):
440             self.logger.notifyChannel('email_send', loglevels.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     if email_bcc:
534         msg['Bcc'] = COMMASPACE.join(email_bcc)
535     msg['Date'] = formatdate(localtime=True)
536
537     msg['X-Priority'] = priorities.get(priority, '3 (Normal)')
538
539     # Add dynamic X Header
540     for key, value in x_headers.iteritems():
541         msg['%s' % key] = str(value)
542
543     if html2text and subtype == 'html':
544         text = html2text(email_body.decode('utf-8')).encode('utf-8')
545         alternative_part = MIMEMultipart(_subtype="alternative")
546         alternative_part.attach(MIMEText(text, _charset='utf-8', _subtype='plain'))
547         alternative_part.attach(email_text)
548         msg.attach(alternative_part)
549     else:
550         msg.attach(email_text)
551
552     if attach:
553         for (fname,fcontent) in attach:
554             part = MIMEBase('application', "octet-stream")
555             part.set_payload( fcontent )
556             Encoders.encode_base64(part)
557             part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,))
558             msg.attach(part)
559
560     return _email_send(email_from, flatten([email_to, email_cc, email_bcc]), msg, openobject_id=openobject_id, ssl=ssl, debug=debug)
561
562 #----------------------------------------------------------
563 # SMS
564 #----------------------------------------------------------
565 # text must be latin-1 encoded
566 def sms_send(user, password, api_id, text, to):
567     import urllib
568     url = "http://api.urlsms.com/SendSMS.aspx"
569     #url = "http://196.7.150.220/http/sendmsg"
570     params = urllib.urlencode({'UserID': user, 'Password': password, 'SenderID': api_id, 'MsgText': text, 'RecipientMobileNo':to})
571     urllib.urlopen(url+"?"+params)
572     # FIXME: Use the logger if there is an error
573     return True
574
575 #---------------------------------------------------------
576 # Class that stores an updateable string (used in wizards)
577 #---------------------------------------------------------
578 class UpdateableStr(local):
579
580     def __init__(self, string=''):
581         self.string = string
582
583     def __str__(self):
584         return str(self.string)
585
586     def __repr__(self):
587         return str(self.string)
588
589     def __nonzero__(self):
590         return bool(self.string)
591
592
593 class UpdateableDict(local):
594     '''Stores an updateable dict to use in wizards'''
595
596     def __init__(self, dict=None):
597         if dict is None:
598             dict = {}
599         self.dict = dict
600
601     def __str__(self):
602         return str(self.dict)
603
604     def __repr__(self):
605         return str(self.dict)
606
607     def clear(self):
608         return self.dict.clear()
609
610     def keys(self):
611         return self.dict.keys()
612
613     def __setitem__(self, i, y):
614         self.dict.__setitem__(i, y)
615
616     def __getitem__(self, i):
617         return self.dict.__getitem__(i)
618
619     def copy(self):
620         return self.dict.copy()
621
622     def iteritems(self):
623         return self.dict.iteritems()
624
625     def iterkeys(self):
626         return self.dict.iterkeys()
627
628     def itervalues(self):
629         return self.dict.itervalues()
630
631     def pop(self, k, d=None):
632         return self.dict.pop(k, d)
633
634     def popitem(self):
635         return self.dict.popitem()
636
637     def setdefault(self, k, d=None):
638         return self.dict.setdefault(k, d)
639
640     def update(self, E, **F):
641         return self.dict.update(E, F)
642
643     def values(self):
644         return self.dict.values()
645
646     def get(self, k, d=None):
647         return self.dict.get(k, d)
648
649     def has_key(self, k):
650         return self.dict.has_key(k)
651
652     def items(self):
653         return self.dict.items()
654
655     def __cmp__(self, y):
656         return self.dict.__cmp__(y)
657
658     def __contains__(self, k):
659         return self.dict.__contains__(k)
660
661     def __delitem__(self, y):
662         return self.dict.__delitem__(y)
663
664     def __eq__(self, y):
665         return self.dict.__eq__(y)
666
667     def __ge__(self, y):
668         return self.dict.__ge__(y)
669
670     def __gt__(self, y):
671         return self.dict.__gt__(y)
672
673     def __hash__(self):
674         return self.dict.__hash__()
675
676     def __iter__(self):
677         return self.dict.__iter__()
678
679     def __le__(self, y):
680         return self.dict.__le__(y)
681
682     def __len__(self):
683         return self.dict.__len__()
684
685     def __lt__(self, y):
686         return self.dict.__lt__(y)
687
688     def __ne__(self, y):
689         return self.dict.__ne__(y)
690
691
692 # Don't use ! Use res.currency.round()
693 class currency(float):
694
695     def __init__(self, value, accuracy=2, rounding=None):
696         if rounding is None:
697             rounding=10**-accuracy
698         self.rounding=rounding
699         self.accuracy=accuracy
700
701     def __new__(cls, value, accuracy=2, rounding=None):
702         return float.__new__(cls, round(value, accuracy))
703
704     #def __str__(self):
705     #   display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
706     #   return str(display_value)
707
708
709 def is_hashable(h):
710     try:
711         hash(h)
712         return True
713     except TypeError:
714         return False
715
716 class cache(object):
717     """
718     Use it as a decorator of the function you plan to cache
719     Timeout: 0 = no timeout, otherwise in seconds
720     """
721
722     __caches = []
723
724     def __init__(self, timeout=None, skiparg=2, multi=None, size=8192):
725         assert skiparg >= 2 # at least self and cr
726         if timeout is None:
727             self.timeout = config['cache_timeout']
728         else:
729             self.timeout = timeout
730         self.skiparg = skiparg
731         self.multi = multi
732         self.lasttime = time.time()
733         self.cache = LRU(size)      # TODO take size from config
734         self.fun = None
735         cache.__caches.append(self)
736
737
738     def _generate_keys(self, dbname, kwargs2):
739         """
740         Generate keys depending of the arguments and the self.mutli value
741         """
742
743         def to_tuple(d):
744             pairs = d.items()
745             pairs.sort(key=lambda (k,v): k)
746             for i, (k, v) in enumerate(pairs):
747                 if isinstance(v, dict):
748                     pairs[i] = (k, to_tuple(v))
749                 if isinstance(v, (list, set)):
750                     pairs[i] = (k, tuple(v))
751                 elif not is_hashable(v):
752                     pairs[i] = (k, repr(v))
753             return tuple(pairs)
754
755         if not self.multi:
756             key = (('dbname', dbname),) + to_tuple(kwargs2)
757             yield key, None
758         else:
759             multis = kwargs2[self.multi][:]
760             for id in multis:
761                 kwargs2[self.multi] = (id,)
762                 key = (('dbname', dbname),) + to_tuple(kwargs2)
763                 yield key, id
764
765     def _unify_args(self, *args, **kwargs):
766         # Update named arguments with positional argument values (without self and cr)
767         kwargs2 = self.fun_default_values.copy()
768         kwargs2.update(kwargs)
769         kwargs2.update(dict(zip(self.fun_arg_names, args[self.skiparg-2:])))
770         return kwargs2
771
772     def clear(self, dbname, *args, **kwargs):
773         """clear the cache for database dbname
774             if *args and **kwargs are both empty, clear all the keys related to this database
775         """
776         if not args and not kwargs:
777             keys_to_del = [key for key in self.cache.keys() if key[0][1] == dbname]
778         else:
779             kwargs2 = self._unify_args(*args, **kwargs)
780             keys_to_del = [key for key, _ in self._generate_keys(dbname, kwargs2) if key in self.cache.keys()]
781
782         for key in keys_to_del:
783             self.cache.pop(key)
784
785     @classmethod
786     def clean_caches_for_db(cls, dbname):
787         for c in cls.__caches:
788             c.clear(dbname)
789
790     def __call__(self, fn):
791         if self.fun is not None:
792             raise Exception("Can not use a cache instance on more than one function")
793         self.fun = fn
794
795         argspec = inspect.getargspec(fn)
796         self.fun_arg_names = argspec[0][self.skiparg:]
797         self.fun_default_values = {}
798         if argspec[3]:
799             self.fun_default_values = dict(zip(self.fun_arg_names[-len(argspec[3]):], argspec[3]))
800
801         def cached_result(self2, cr, *args, **kwargs):
802             if time.time()-int(self.timeout) > self.lasttime:
803                 self.lasttime = time.time()
804                 t = time.time()-int(self.timeout)
805                 old_keys = [key for key in self.cache.keys() if self.cache[key][1] < t]
806                 for key in old_keys:
807                     self.cache.pop(key)
808
809             kwargs2 = self._unify_args(*args, **kwargs)
810
811             result = {}
812             notincache = {}
813             for key, id in self._generate_keys(cr.dbname, kwargs2):
814                 if key in self.cache:
815                     result[id] = self.cache[key][0]
816                 else:
817                     notincache[id] = key
818
819             if notincache:
820                 if self.multi:
821                     kwargs2[self.multi] = notincache.keys()
822
823                 result2 = fn(self2, cr, *args[:self.skiparg-2], **kwargs2)
824                 if not self.multi:
825                     key = notincache[None]
826                     self.cache[key] = (result2, time.time())
827                     result[None] = result2
828                 else:
829                     for id in result2:
830                         key = notincache[id]
831                         self.cache[key] = (result2[id], time.time())
832                     result.update(result2)
833
834             if not self.multi:
835                 return result[None]
836             return result
837
838         cached_result.clear_cache = self.clear
839         return cached_result
840
841 def to_xml(s):
842     return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;')
843
844 def get_encodings(hint_encoding='utf-8'):
845     fallbacks = {
846         'latin1': 'latin9',
847         'iso-8859-1': 'iso8859-15',
848         'cp1252': '1252',
849     }
850     if hint_encoding:
851         yield hint_encoding
852         if hint_encoding.lower() in fallbacks:
853             yield fallbacks[hint_encoding.lower()]
854
855     # some defaults (also taking care of pure ASCII)
856     for charset in ['utf8','latin1']:
857         if not (hint_encoding) or (charset.lower() != hint_encoding.lower()):
858             yield charset
859
860     from locale import getpreferredencoding
861     prefenc = getpreferredencoding()
862     if prefenc and prefenc.lower() != 'utf-8':
863         yield prefenc
864         prefenc = fallbacks.get(prefenc.lower())
865         if prefenc:
866             yield prefenc
867
868
869 def ustr(value, hint_encoding='utf-8'):
870     """This method is similar to the builtin `str` method, except
871        it will return unicode() string.
872
873     @param value: the value to convert
874     @param hint_encoding: an optional encoding that was detected
875                           upstream and should be tried first to
876                           decode ``value``.
877
878     @rtype: unicode
879     @return: unicode string
880     """
881     if isinstance(value, Exception):
882         return exception_to_unicode(value)
883
884     if isinstance(value, unicode):
885         return value
886
887     if not isinstance(value, basestring):
888         try:
889             return unicode(value)
890         except Exception:
891             raise UnicodeError('unable to convert %r' % (value,))
892
893     for ln in get_encodings(hint_encoding):
894         try:
895             return unicode(value, ln)
896         except Exception:
897             pass
898     raise UnicodeError('unable to convert %r' % (value,))
899
900
901 def exception_to_unicode(e):
902     if (sys.version_info[:2] < (2,6)) and hasattr(e, 'message'):
903         return ustr(e.message)
904     if hasattr(e, 'args'):
905         return "\n".join((ustr(a) for a in e.args))
906     try:
907         return ustr(e)
908     except Exception:
909         return u"Unknown message"
910
911
912 # to be compatible with python 2.4
913 import __builtin__
914 if not hasattr(__builtin__, 'all'):
915     def all(iterable):
916         for element in iterable:
917             if not element:
918                 return False
919         return True
920
921     __builtin__.all = all
922     del all
923
924 if not hasattr(__builtin__, 'any'):
925     def any(iterable):
926         for element in iterable:
927             if element:
928                 return True
929         return False
930
931     __builtin__.any = any
932     del any
933
934 def get_iso_codes(lang):
935     if lang.find('_') != -1:
936         if lang.split('_')[0] == lang.split('_')[1].lower():
937             lang = lang.split('_')[0]
938     return lang
939
940 def get_languages():
941     # The codes below are those from Launchpad's Rosetta, with the exception
942     # of some trivial codes where the Launchpad code is xx and we have xx_XX.
943     languages={
944         'ab_RU': u'Abkhazian / аҧсуа',
945         'ar_AR': u'Arabic / الْعَرَبيّة',
946         'bg_BG': u'Bulgarian / български език',
947         'bs_BS': u'Bosnian / bosanski jezik',
948         'ca_ES': u'Catalan / Català',
949         'cs_CZ': u'Czech / Čeština',
950         'da_DK': u'Danish / Dansk',
951         'de_DE': u'German / Deutsch',
952         'el_GR': u'Greek / Ελληνικά',
953         'en_CA': u'English (CA)',
954         'en_GB': u'English (UK)',
955         'en_US': u'English (US)',
956         'es_AR': u'Spanish (AR) / Español (AR)',
957         'es_BO': u'Spanish (BO) / Español (BO)',
958         'es_CL': u'Spanish (CL) / Español (CL)',
959         'es_CO': u'Spanish (CO) / Español (CO)',
960         'es_CR': u'Spanish (CR) / Español (CR)',
961         'es_DO': u'Spanish (DO) / Español (DO)',
962         'es_EC': u'Spanish (EC) / Español (EC)',
963         'es_ES': u'Spanish / Español',
964         'es_GT': u'Spanish (GT) / Español (GT)',
965         'es_HN': u'Spanish (HN) / Español (HN)',
966         'es_MX': u'Spanish (MX) / Español (MX)',
967         'es_NI': u'Spanish (NI) / Español (NI)',
968         'es_PA': u'Spanish (PA) / Español (PA)',
969         'es_PE': u'Spanish (PE) / Español (PE)',
970         'es_PR': u'Spanish (PR) / Español (PR)',
971         'es_PY': u'Spanish (PY) / Español (PY)',
972         'es_SV': u'Spanish (SV) / Español (SV)',
973         'es_UY': u'Spanish (UY) / Español (UY)',
974         'es_VE': u'Spanish (VE) / Español (VE)',
975         'et_EE': u'Estonian / Eesti keel',
976         'fa_IR': u'Persian / فارس',
977         'fi_FI': u'Finnish / Suomi',
978         'fr_BE': u'French (BE) / Français (BE)',
979         'fr_CH': u'French (CH) / Français (CH)',
980         'fr_FR': u'French / Français',
981         'gl_ES': u'Galician / Galego',
982         'gu_IN': u'Gujarati / ગુજરાતી',
983         'he_IL': u'Hebrew / עִבְרִי',
984         'hi_IN': u'Hindi / हिंदी',
985         'hr_HR': u'Croatian / hrvatski jezik',
986         'hu_HU': u'Hungarian / Magyar',
987         'id_ID': u'Indonesian / Bahasa Indonesia',
988         'it_IT': u'Italian / Italiano',
989         'iu_CA': u'Inuktitut / ᐃᓄᒃᑎᑐᑦ',
990         'ja_JP': u'Japanese / 日本語',
991         'ko_KP': u'Korean (KP) / 한국어 (KP)',
992         'ko_KR': u'Korean (KR) / 한국어 (KR)',
993         'lt_LT': u'Lithuanian / Lietuvių kalba',
994         'lv_LV': u'Latvian / latviešu valoda',
995         'ml_IN': u'Malayalam / മലയാളം',
996         'mn_MN': u'Mongolian / монгол',
997         'nb_NO': u'Norwegian Bokmål / Norsk bokmål',
998         'nl_NL': u'Dutch / Nederlands',
999         'nl_BE': u'Flemish (BE) / Vlaams (BE)',
1000         'oc_FR': u'Occitan (FR, post 1500) / Occitan',
1001         'pl_PL': u'Polish / Język polski',
1002         'pt_BR': u'Portugese (BR) / Português (BR)',
1003         'pt_PT': u'Portugese / Português',
1004         'ro_RO': u'Romanian / română',
1005         'ru_RU': u'Russian / русский язык',
1006         'si_LK': u'Sinhalese / සිංහල',
1007         'sl_SI': u'Slovenian / slovenščina',
1008         'sk_SK': u'Slovak / Slovenský jazyk',
1009         'sq_AL': u'Albanian / Shqip',
1010         'sr_RS': u'Serbian (Cyrillic) / српски',
1011         'sr@latin': u'Serbian (Latin) / srpski',
1012         'sv_SE': u'Swedish / svenska',
1013         'te_IN': u'Telugu / తెలుగు',
1014         'tr_TR': u'Turkish / Türkçe',
1015         'vi_VN': u'Vietnamese / Tiếng Việt',
1016         'uk_UA': u'Ukrainian / українська',
1017         'ur_PK': u'Urdu / اردو',
1018         'zh_CN': u'Chinese (CN) / 简体中文',
1019         'zh_HK': u'Chinese (HK)',
1020         'zh_TW': u'Chinese (TW) / 正體字',
1021         'th_TH': u'Thai / ภาษาไทย',
1022         'tlh_TLH': u'Klingon',
1023     }
1024     return languages
1025
1026 def scan_languages():
1027     # Now it will take all languages from get languages function without filter it with base module languages
1028     lang_dict = get_languages()
1029     ret = [(lang, lang_dict.get(lang, lang)) for lang in list(lang_dict)]
1030     ret.sort(key=lambda k:k[1])
1031     return ret
1032
1033
1034 def get_user_companies(cr, user):
1035     def _get_company_children(cr, ids):
1036         if not ids:
1037             return []
1038         cr.execute('SELECT id FROM res_company WHERE parent_id IN %s', (tuple(ids),))
1039         res = [x[0] for x in cr.fetchall()]
1040         res.extend(_get_company_children(cr, res))
1041         return res
1042     cr.execute('SELECT company_id FROM res_users WHERE id=%s', (user,))
1043     user_comp = cr.fetchone()[0]
1044     if not user_comp:
1045         return []
1046     return [user_comp] + _get_company_children(cr, [user_comp])
1047
1048 def mod10r(number):
1049     """
1050     Input number : account or invoice number
1051     Output return: the same number completed with the recursive mod10
1052     key
1053     """
1054     codec=[0,9,4,6,8,2,7,1,3,5]
1055     report = 0
1056     result=""
1057     for digit in number:
1058         result += digit
1059         if digit.isdigit():
1060             report = codec[ (int(digit) + report) % 10 ]
1061     return result + str((10 - report) % 10)
1062
1063
1064 def human_size(sz):
1065     """
1066     Return the size in a human readable format
1067     """
1068     if not sz:
1069         return False
1070     units = ('bytes', 'Kb', 'Mb', 'Gb')
1071     if isinstance(sz,basestring):
1072         sz=len(sz)
1073     s, i = float(sz), 0
1074     while s >= 1024 and i < len(units)-1:
1075         s = s / 1024
1076         i = i + 1
1077     return "%0.2f %s" % (s, units[i])
1078
1079 def logged(f):
1080     from func import wraps
1081
1082     @wraps(f)
1083     def wrapper(*args, **kwargs):
1084         from pprint import pformat
1085
1086         vector = ['Call -> function: %r' % f]
1087         for i, arg in enumerate(args):
1088             vector.append('  arg %02d: %s' % (i, pformat(arg)))
1089         for key, value in kwargs.items():
1090             vector.append('  kwarg %10s: %s' % (key, pformat(value)))
1091
1092         timeb4 = time.time()
1093         res = f(*args, **kwargs)
1094
1095         vector.append('  result: %s' % pformat(res))
1096         vector.append('  time delta: %s' % (time.time() - timeb4))
1097         loglevels.Logger().notifyChannel('logged', loglevels.LOG_DEBUG, '\n'.join(vector))
1098         return res
1099
1100     return wrapper
1101
1102 class profile(object):
1103     def __init__(self, fname=None):
1104         self.fname = fname
1105
1106     def __call__(self, f):
1107         from func import wraps
1108
1109         @wraps(f)
1110         def wrapper(*args, **kwargs):
1111             class profile_wrapper(object):
1112                 def __init__(self):
1113                     self.result = None
1114                 def __call__(self):
1115                     self.result = f(*args, **kwargs)
1116             pw = profile_wrapper()
1117             import cProfile
1118             fname = self.fname or ("%s.cprof" % (f.func_name,))
1119             cProfile.runctx('pw()', globals(), locals(), filename=fname)
1120             return pw.result
1121
1122         return wrapper
1123
1124 def debug(what):
1125     """
1126         This method allow you to debug your code without print
1127         Example:
1128         >>> def func_foo(bar)
1129         ...     baz = bar
1130         ...     debug(baz)
1131         ...     qnx = (baz, bar)
1132         ...     debug(qnx)
1133         ...
1134         >>> func_foo(42)
1135
1136         This will output on the logger:
1137
1138             [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:baz = 42
1139             [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:qnx = (42, 42)
1140
1141         To view the DEBUG lines in the logger you must start the server with the option
1142             --log-level=debug
1143
1144     """
1145     warnings.warn("The tools.debug() method is deprecated, please use logging.",
1146                       DeprecationWarning, stacklevel=2)
1147     from inspect import stack
1148     from pprint import pformat
1149     st = stack()[1]
1150     param = re.split("debug *\((.+)\)", st[4][0].strip())[1].strip()
1151     while param.count(')') > param.count('('): param = param[:param.rfind(')')]
1152     what = pformat(what)
1153     if param != what:
1154         what = "%s = %s" % (param, what)
1155     logging.getLogger(st[3]).debug(what)
1156
1157
1158 __icons_list = ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
1159 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
1160 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
1161 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
1162 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
1163 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
1164 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
1165 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
1166 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
1167 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
1168 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
1169 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
1170 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
1171 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
1172 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
1173 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
1174 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
1175 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
1176 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
1177 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
1178 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
1179 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
1180 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
1181 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
1182 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
1183 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
1184 'terp-check','terp-go-month','terp-go-year','terp-go-today','terp-document-new','terp-camera_test',
1185 'terp-emblem-important','terp-gtk-media-pause','terp-gtk-stop','terp-gnome-cpu-frequency-applet+',
1186 'terp-dialog-close','terp-gtk-jump-to-rtl','terp-gtk-jump-to-ltr','terp-accessories-archiver',
1187 'terp-stock_align_left_24','terp-stock_effects-object-colorize','terp-go-home','terp-gtk-go-back-rtl',
1188 'terp-gtk-go-back-ltr','terp-personal','terp-personal-','terp-personal+','terp-accessories-archiver-minus',
1189 'terp-accessories-archiver+','terp-stock_symbol-selection','terp-call-start','terp-dolar',
1190 'terp-face-plain','terp-folder-blue','terp-folder-green','terp-folder-orange','terp-folder-yellow',
1191 'terp-gdu-smart-failing','terp-go-week','terp-gtk-select-all','terp-locked','terp-mail-forward',
1192 'terp-mail-message-new','terp-mail-replied','terp-rating-rated','terp-stage','terp-stock_format-scientific',
1193 'terp-dolar_ok!','terp-idea','terp-stock_format-default','terp-mail-','terp-mail_delete'
1194 ]
1195
1196 def icons(*a, **kw):
1197     global __icons_list
1198     return [(x, x) for x in __icons_list ]
1199
1200 def extract_zip_file(zip_file, outdirectory):
1201     zf = zipfile.ZipFile(zip_file, 'r')
1202     out = outdirectory
1203     for path in zf.namelist():
1204         tgt = os.path.join(out, path)
1205         tgtdir = os.path.dirname(tgt)
1206         if not os.path.exists(tgtdir):
1207             os.makedirs(tgtdir)
1208
1209         if not tgt.endswith(os.sep):
1210             fp = open(tgt, 'wb')
1211             fp.write(zf.read(path))
1212             fp.close()
1213     zf.close()
1214
1215 def detect_ip_addr():
1216     """Try a very crude method to figure out a valid external
1217        IP or hostname for the current machine. Don't rely on this
1218        for binding to an interface, but it could be used as basis
1219        for constructing a remote URL to the server.
1220     """
1221     def _detect_ip_addr():
1222         from array import array
1223         from struct import pack, unpack
1224
1225         try:
1226             import fcntl
1227         except ImportError:
1228             fcntl = None
1229
1230         ip_addr = None
1231
1232         if not fcntl: # not UNIX:
1233             host = socket.gethostname()
1234             ip_addr = socket.gethostbyname(host)
1235         else: # UNIX:
1236             # get all interfaces:
1237             nbytes = 128 * 32
1238             s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1239             names = array('B', '\0' * nbytes)
1240             #print 'names: ', names
1241             outbytes = unpack('iL', fcntl.ioctl( s.fileno(), 0x8912, pack('iL', nbytes, names.buffer_info()[0])))[0]
1242             namestr = names.tostring()
1243
1244             # try 64 bit kernel:
1245             for i in range(0, outbytes, 40):
1246                 name = namestr[i:i+16].split('\0', 1)[0]
1247                 if name != 'lo':
1248                     ip_addr = socket.inet_ntoa(namestr[i+20:i+24])
1249                     break
1250
1251             # try 32 bit kernel:
1252             if ip_addr is None:
1253                 ifaces = filter(None, [namestr[i:i+32].split('\0', 1)[0] for i in range(0, outbytes, 32)])
1254
1255                 for ifname in [iface for iface in ifaces if iface != 'lo']:
1256                     ip_addr = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, pack('256s', ifname[:15]))[20:24])
1257                     break
1258
1259         return ip_addr or 'localhost'
1260
1261     try:
1262         ip_addr = _detect_ip_addr()
1263     except Exception:
1264         ip_addr = 'localhost'
1265     return ip_addr
1266
1267 # RATIONALE BEHIND TIMESTAMP CALCULATIONS AND TIMEZONE MANAGEMENT:
1268 #  The server side never does any timestamp calculation, always
1269 #  sends them in a naive (timezone agnostic) format supposed to be
1270 #  expressed within the server timezone, and expects the clients to
1271 #  provide timestamps in the server timezone as well.
1272 #  It stores all timestamps in the database in naive format as well,
1273 #  which also expresses the time in the server timezone.
1274 #  For this reason the server makes its timezone name available via the
1275 #  common/timezone_get() rpc method, which clients need to read
1276 #  to know the appropriate time offset to use when reading/writing
1277 #  times.
1278 def get_win32_timezone():
1279     """Attempt to return the "standard name" of the current timezone on a win32 system.
1280        @return: the standard name of the current win32 timezone, or False if it cannot be found.
1281     """
1282     res = False
1283     if (sys.platform == "win32"):
1284         try:
1285             import _winreg
1286             hklm = _winreg.ConnectRegistry(None,_winreg.HKEY_LOCAL_MACHINE)
1287             current_tz_key = _winreg.OpenKey(hklm, r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", 0,_winreg.KEY_ALL_ACCESS)
1288             res = str(_winreg.QueryValueEx(current_tz_key,"StandardName")[0])  # [0] is value, [1] is type code
1289             _winreg.CloseKey(current_tz_key)
1290             _winreg.CloseKey(hklm)
1291         except Exception:
1292             pass
1293     return res
1294
1295 def detect_server_timezone():
1296     """Attempt to detect the timezone to use on the server side.
1297        Defaults to UTC if no working timezone can be found.
1298        @return: the timezone identifier as expected by pytz.timezone.
1299     """
1300     try:
1301         import pytz
1302     except Exception:
1303         loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_WARNING,
1304             "Python pytz module is not available. Timezone will be set to UTC by default.")
1305         return 'UTC'
1306
1307     # Option 1: the configuration option (did not exist before, so no backwards compatibility issue)
1308     # 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
1309     # Option 3: the environment variable TZ
1310     sources = [ (config['timezone'], 'OpenERP configuration'),
1311                 (time.tzname[0], 'time.tzname'),
1312                 (os.environ.get('TZ',False),'TZ environment variable'), ]
1313     # Option 4: OS-specific: /etc/timezone on Unix
1314     if (os.path.exists("/etc/timezone")):
1315         tz_value = False
1316         try:
1317             f = open("/etc/timezone")
1318             tz_value = f.read(128).strip()
1319         except Exception:
1320             pass
1321         finally:
1322             f.close()
1323         sources.append((tz_value,"/etc/timezone file"))
1324     # Option 5: timezone info from registry on Win32
1325     if (sys.platform == "win32"):
1326         # Timezone info is stored in windows registry.
1327         # However this is not likely to work very well as the standard name
1328         # of timezones in windows is rarely something that is known to pytz.
1329         # But that's ok, it is always possible to use a config option to set
1330         # it explicitly.
1331         sources.append((get_win32_timezone(),"Windows Registry"))
1332
1333     for (value,source) in sources:
1334         if value:
1335             try:
1336                 tz = pytz.timezone(value)
1337                 loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_INFO,
1338                     "Using timezone %s obtained from %s." % (tz.zone,source))
1339                 return value
1340             except pytz.UnknownTimeZoneError:
1341                 loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_WARNING,
1342                     "The timezone specified in %s (%s) is invalid, ignoring it." % (source,value))
1343
1344     loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_WARNING,
1345         "No valid timezone could be detected, using default UTC timezone. You can specify it explicitly with option 'timezone' in the server configuration.")
1346     return 'UTC'
1347
1348 def get_server_timezone():
1349     # timezone detection is safe in multithread, so lazy init is ok here
1350     if (not config['timezone']):
1351         config['timezone'] = detect_server_timezone()
1352     return config['timezone']
1353
1354
1355 DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"
1356 DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S"
1357 DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % (
1358     DEFAULT_SERVER_DATE_FORMAT,
1359     DEFAULT_SERVER_TIME_FORMAT)
1360
1361 # Python's strftime supports only the format directives
1362 # that are available on the platform's libc, so in order to
1363 # be cross-platform we map to the directives required by
1364 # the C standard (1989 version), always available on platforms
1365 # with a C standard implementation.
1366 DATETIME_FORMATS_MAP = {
1367         '%C': '', # century
1368         '%D': '%m/%d/%Y', # modified %y->%Y
1369         '%e': '%d',
1370         '%E': '', # special modifier
1371         '%F': '%Y-%m-%d',
1372         '%g': '%Y', # modified %y->%Y
1373         '%G': '%Y',
1374         '%h': '%b',
1375         '%k': '%H',
1376         '%l': '%I',
1377         '%n': '\n',
1378         '%O': '', # special modifier
1379         '%P': '%p',
1380         '%R': '%H:%M',
1381         '%r': '%I:%M:%S %p',
1382         '%s': '', #num of seconds since epoch
1383         '%T': '%H:%M:%S',
1384         '%t': ' ', # tab
1385         '%u': ' %w',
1386         '%V': '%W',
1387         '%y': '%Y', # Even if %y works, it's ambiguous, so we should use %Y
1388         '%+': '%Y-%m-%d %H:%M:%S',
1389
1390         # %Z is a special case that causes 2 problems at least:
1391         #  - the timezone names we use (in res_user.context_tz) come
1392         #    from pytz, but not all these names are recognized by
1393         #    strptime(), so we cannot convert in both directions
1394         #    when such a timezone is selected and %Z is in the format
1395         #  - %Z is replaced by an empty string in strftime() when
1396         #    there is not tzinfo in a datetime value (e.g when the user
1397         #    did not pick a context_tz). The resulting string does not
1398         #    parse back if the format requires %Z.
1399         # As a consequence, we strip it completely from format strings.
1400         # The user can always have a look at the context_tz in
1401         # preferences to check the timezone.
1402         '%z': '',
1403         '%Z': '',
1404 }
1405
1406 def server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_name,
1407         tz_offset=True, ignore_unparsable_time=True):
1408     """
1409     Convert a source timestamp string into a destination timestamp string, attempting to apply the
1410     correct offset if both the server and local timezone are recognized, or no
1411     offset at all if they aren't or if tz_offset is false (i.e. assuming they are both in the same TZ).
1412
1413     WARNING: This method is here to allow formatting dates correctly for inclusion in strings where
1414              the client would not be able to format/offset it correctly. DO NOT use it for returning
1415              date fields directly, these are supposed to be handled by the client!!
1416
1417     @param src_tstamp_str: the str value containing the timestamp in the server timezone.
1418     @param src_format: the format to use when parsing the server timestamp.
1419     @param dst_format: the format to use when formatting the resulting timestamp for the local/client timezone.
1420     @param dst_tz_name: name of the destination timezone (such as the 'tz' value of the client context)
1421     @param ignore_unparsable_time: if True, return False if src_tstamp_str cannot be parsed
1422                                    using src_format or formatted using dst_format.
1423
1424     @return: local/client formatted timestamp, expressed in the local/client timezone if possible
1425             and if tz_offset is true, or src_tstamp_str if timezone offset could not be determined.
1426     """
1427     if not src_tstamp_str:
1428         return False
1429
1430     res = src_tstamp_str
1431     if src_format and dst_format:
1432         # find out server timezone
1433         server_tz = get_server_timezone()
1434         try:
1435             # dt_value needs to be a datetime.datetime object (so no time.struct_time or mx.DateTime.DateTime here!)
1436             dt_value = datetime.strptime(src_tstamp_str, src_format)
1437             if tz_offset and dst_tz_name:
1438                 try:
1439                     import pytz
1440                     src_tz = pytz.timezone(server_tz)
1441                     dst_tz = pytz.timezone(dst_tz_name)
1442                     src_dt = src_tz.localize(dt_value, is_dst=True)
1443                     dt_value = src_dt.astimezone(dst_tz)
1444                 except Exception:
1445                     pass
1446             res = dt_value.strftime(dst_format)
1447         except Exception:
1448             # Normal ways to end up here are if strptime or strftime failed
1449             if not ignore_unparsable_time:
1450                 return False
1451     return res
1452
1453
1454 def split_every(n, iterable, piece_maker=tuple):
1455     """Splits an iterable into length-n pieces. The last piece will be shorter
1456        if ``n`` does not evenly divide the iterable length.
1457        @param ``piece_maker``: function to build the pieces
1458        from the slices (tuple,list,...)
1459     """
1460     iterator = iter(iterable)
1461     piece = piece_maker(islice(iterator, n))
1462     while piece:
1463         yield piece
1464         piece = piece_maker(islice(iterator, n))
1465
1466 if __name__ == '__main__':
1467     import doctest
1468     doctest.testmod()
1469
1470 class upload_data_thread(threading.Thread):
1471     def __init__(self, email, data, type):
1472         self.args = [('email',email),('type',type),('data',data)]
1473         super(upload_data_thread,self).__init__()
1474     def run(self):
1475         try:
1476             import urllib
1477             args = urllib.urlencode(self.args)
1478             fp = urllib.urlopen('http://www.openerp.com/scripts/survey.php', args)
1479             fp.read()
1480             fp.close()
1481         except Exception:
1482             pass
1483
1484 def upload_data(email, data, type='SURVEY'):
1485     a = upload_data_thread(email, data, type)
1486     a.start()
1487     return True
1488
1489
1490 # port of python 2.6's attrgetter with support for dotted notation
1491 def resolve_attr(obj, attr):
1492     for name in attr.split("."):
1493         obj = getattr(obj, name)
1494     return obj
1495
1496 def attrgetter(*items):
1497     if len(items) == 1:
1498         attr = items[0]
1499         def g(obj):
1500             return resolve_attr(obj, attr)
1501     else:
1502         def g(obj):
1503             return tuple(resolve_attr(obj, attr) for attr in items)
1504     return g
1505
1506
1507 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1508