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