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