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