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