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