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