1 # -*- coding: utf-8 -*-
2 ##############################################################################
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>).
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.
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.
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/>.
21 ##############################################################################
24 Miscelleanous tools used by OpenERP.
39 from datetime import datetime
40 from email.MIMEText import MIMEText
41 from email.MIMEBase import MIMEBase
42 from email.MIMEMultipart import MIMEMultipart
43 from email.Header import Header
44 from email.Utils import formatdate, COMMASPACE
45 from email import Encoders
46 from itertools import islice, izip
47 from lxml import etree
48 from which import which
49 if sys.version_info[:2] < (2, 4):
50 from threadinglocal import local
52 from threading import local
54 from html2text import html2text
58 import openerp.loglevels as loglevels
59 from config import config
62 # get_encodings, ustr and exception_to_unicode were originally from tools.misc.
63 # There are moved to loglevels until we refactor tools.
64 from openerp.loglevels import get_encodings, ustr, exception_to_unicode
66 _logger = logging.getLogger('tools')
68 # List of etree._Element subclasses that we choose to ignore when parsing XML.
69 # We include the *Base ones just in case, currently they seem to be subclasses of the _* ones.
70 SKIPPED_ELEMENT_TYPES = (etree._Comment, etree._ProcessingInstruction, etree.CommentBase, etree.PIBase)
72 def find_in_path(name):
78 def find_pg_tool(name):
80 if config['pg_path'] and config['pg_path'] != 'None':
81 path = config['pg_path']
83 return which(name, path=path)
87 def exec_pg_command(name, *args):
88 prog = find_pg_tool(name)
90 raise Exception('Couldn\'t find %s' % name)
91 args2 = (prog,) + args
93 return subprocess.call(args2)
95 def exec_pg_command_pipe(name, *args):
96 prog = find_pg_tool(name)
98 raise Exception('Couldn\'t find %s' % name)
99 # on win32, passing close_fds=True is not compatible
100 # with redirecting std[in/err/out]
101 pop = subprocess.Popen((prog,) + args, bufsize= -1,
102 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
103 close_fds=(os.name=="posix"))
104 return (pop.stdin, pop.stdout)
106 def exec_command_pipe(name, *args):
107 prog = find_in_path(name)
109 raise Exception('Couldn\'t find %s' % name)
110 # on win32, passing close_fds=True is not compatible
111 # with redirecting std[in/err/out]
112 pop = subprocess.Popen((prog,) + args, bufsize= -1,
113 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
114 close_fds=(os.name=="posix"))
115 return (pop.stdin, pop.stdout)
117 #----------------------------------------------------------
119 #----------------------------------------------------------
120 #file_path_root = os.getcwd()
121 #file_path_addons = os.path.join(file_path_root, 'addons')
123 def file_open(name, mode="r", subdir='addons', pathinfo=False):
124 """Open a file from the OpenERP root, using a subdir folder.
126 >>> file_open('hr/report/timesheer.xsl')
127 >>> file_open('addons/hr/report/timesheet.xsl')
128 >>> file_open('../../base/report/rml_template.xsl', subdir='addons/hr/report', pathinfo=True)
130 @param name: name of the file
131 @param mode: file open mode
132 @param subdir: subdirectory
133 @param pathinfo: if True returns tupple (fileobject, filepath)
135 @return: fileobject if pathinfo is False else (fileobject, filepath)
137 import openerp.modules as addons
138 adps = addons.module.ad_paths
139 rtp = os.path.normcase(os.path.abspath(config['root_path']))
141 if name.replace(os.path.sep, '/').startswith('addons/'):
145 # First try to locate in addons_path
148 if subdir2.replace(os.path.sep, '/').startswith('addons/'):
149 subdir2 = subdir2[7:]
151 subdir2 = (subdir2 != 'addons' or None) and subdir2
156 fn = os.path.join(adp, subdir2, name)
158 fn = os.path.join(adp, name)
159 fn = os.path.normpath(fn)
160 fo = file_open(fn, mode=mode, subdir=None, pathinfo=pathinfo)
168 name = os.path.join(rtp, subdir, name)
170 name = os.path.join(rtp, name)
172 name = os.path.normpath(name)
174 # Check for a zipfile in the path
179 head, tail = os.path.split(head)
183 zipname = os.path.join(tail, zipname)
186 if zipfile.is_zipfile(head+'.zip'):
187 from cStringIO import StringIO
188 zfile = zipfile.ZipFile(head+'.zip')
191 fo.write(zfile.read(os.path.join(
192 os.path.basename(head), zipname).replace(
199 name2 = os.path.normpath(os.path.join(head + '.zip', zipname))
201 for i in (name2, name):
202 if i and os.path.isfile(i):
207 if os.path.splitext(name)[1] == '.rml':
208 raise IOError, 'Report %s doesn\'t exist or deleted : ' %str(name)
209 raise IOError, 'File not found : %s' % name
212 #----------------------------------------------------------
214 #----------------------------------------------------------
216 """Flatten a list of elements into a uniqu list
217 Author: Christophe Simonis (christophe@tinyerp.com)
226 >>> flatten( [[], [[]]] )
228 >>> flatten( [[['a','b'], 'c'], 'd', ['e', [], 'f']] )
229 ['a', 'b', 'c', 'd', 'e', 'f']
230 >>> t = (1,2,(3,), [4, 5, [6, [7], (8, 9), ([10, 11, (12, 13)]), [14, [], (15,)], []]])
232 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
236 return hasattr(x, "__iter__")
241 map(r.append, flatten(e))
246 def reverse_enumerate(l):
247 """Like enumerate but in the other sens
248 >>> a = ['a', 'b', 'c']
249 >>> it = reverse_enumerate(a)
257 Traceback (most recent call last):
258 File "<stdin>", line 1, in <module>
261 return izip(xrange(len(l)-1, -1, -1), reversed(l))
263 #----------------------------------------------------------
265 #----------------------------------------------------------
266 email_re = re.compile(r"""
267 ([a-zA-Z][\w\.-]*[a-zA-Z0-9] # username part
269 [a-zA-Z0-9][\w\.-]* # domain must start with a letter ... Ged> why do we include a 0-9 then?
274 res_re = re.compile(r"\[([0-9]+)\]", re.UNICODE)
275 command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE)
276 reference_re = re.compile("<.*-openobject-(\\d+)@(.*)>", re.UNICODE)
286 def html2plaintext(html, body_id=None, encoding='utf-8'):
287 ## (c) Fry-IT, www.fry-it.com, 2007
288 ## <peter@fry-it.com>
289 ## download here: http://www.peterbe.com/plog/html2plaintext
292 """ from an HTML text, convert the HTML to plain text.
293 If @body_id is provided then this is the tag where the
294 body (not necessarily <body>) starts.
299 from lxml.etree import tostring
301 from lxml.html.soupparser import fromstring
304 _logger.debug('tools.misc.html2plaintext: cannot use BeautifulSoup, fallback to lxml.etree.HTMLParser')
305 from lxml.etree import fromstring, HTMLParser
306 kwargs = dict(parser=HTMLParser())
308 tree = fromstring(html, **kwargs)
310 if body_id is not None:
311 source = tree.xpath('//*[@id=%s]'%(body_id,))
313 source = tree.xpath('//body')
319 for link in tree.findall('.//a'):
320 url = link.get('href')
324 link.text = '%s [%s]' % (link.text, i)
325 url_index.append(url)
327 html = ustr(tostring(tree, encoding=encoding))
329 html = html.replace('<strong>','*').replace('</strong>','*')
330 html = html.replace('<b>','*').replace('</b>','*')
331 html = html.replace('<h3>','*').replace('</h3>','*')
332 html = html.replace('<h2>','**').replace('</h2>','**')
333 html = html.replace('<h1>','**').replace('</h1>','**')
334 html = html.replace('<em>','/').replace('</em>','/')
335 html = html.replace('<tr>', '\n')
336 html = html.replace('</p>', '\n')
337 html = re.sub('<br\s*/?>', '\n', html)
338 html = re.sub('<.*?>', ' ', html)
339 html = html.replace(' ' * 2, ' ')
342 html = '\n'.join([x.strip() for x in html.splitlines()])
343 html = html.replace('\n' * 2, '\n')
345 for i, url in enumerate(url_index):
348 html += ustr('[%s] %s\n') % (i+1, url)
352 def generate_tracking_message_id(openobject_id):
353 """Returns a string that can be used in the Message-ID RFC822 header field so we
354 can track the replies related to a given object thanks to the "In-Reply-To" or
355 "References" fields that Mail User Agents will set.
357 return "<%s-openobject-%s@%s>" % (time.time(), openobject_id, socket.gethostname())
359 def _email_send(smtp_from, smtp_to_list, message, openobject_id=None, ssl=False, debug=False):
360 """Low-level method to send directly a Message through the configured smtp server.
361 :param smtp_from: RFC-822 envelope FROM (not displayed to recipient)
362 :param smtp_to_list: RFC-822 envelope RCPT_TOs (not displayed to recipient)
363 :param message: an email.message.Message to send
364 :param debug: True if messages should be output to stderr before being sent,
365 and smtplib.SMTP put into debug mode.
366 :return: True if the mail was delivered successfully to the smtp,
367 else False (+ exception logged)
369 class WriteToLogger(object):
371 self.logger = loglevels.Logger()
374 self.logger.notifyChannel('email_send', loglevels.LOG_DEBUG, s)
377 message['Message-Id'] = generate_tracking_message_id(openobject_id)
380 smtp_server = config['smtp_server']
382 if smtp_server.startswith('maildir:/'):
383 from mailbox import Maildir
384 maildir_path = smtp_server[8:]
385 mdir = Maildir(maildir_path,factory=None, create = True)
386 mdir.add(message.as_string(True))
389 oldstderr = smtplib.stderr
390 if not ssl: ssl = config.get('smtp_ssl', False)
393 # in case of debug, the messages are printed to stderr.
395 smtplib.stderr = WriteToLogger()
397 s.set_debuglevel(int(bool(debug))) # 0 or 1
398 s.connect(smtp_server, config['smtp_port'])
404 if config['smtp_user'] or config['smtp_password']:
405 s.login(config['smtp_user'], config['smtp_password'])
407 s.sendmail(smtp_from, smtp_to_list, message.as_string())
412 smtplib.stderr = oldstderr
414 # ignored, just a consequence of the previous exception
418 _logger.error('could not deliver email', exc_info=True)
424 def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
425 attach=None, openobject_id=False, ssl=False, debug=False, subtype='plain', x_headers=None, priority='3'):
431 `email_from`: A string used to fill the `From` header, if falsy,
432 config['email_from'] is used instead. Also used for
433 the `Reply-To` header if `reply_to` is not provided
435 `email_to`: a sequence of addresses to send the mail to.
437 if x_headers is None:
441 if not (email_from or config['email_from']):
442 raise ValueError("Sending an email requires either providing a sender "
443 "address or having configured one")
445 if not email_from: email_from = config.get('email_from', False)
446 email_from = ustr(email_from).encode('utf-8')
448 if not email_cc: email_cc = []
449 if not email_bcc: email_bcc = []
450 if not body: body = u''
452 email_body = ustr(body).encode('utf-8')
453 email_text = MIMEText(email_body or '',_subtype=subtype,_charset='utf-8')
455 msg = MIMEMultipart()
457 msg['Subject'] = Header(ustr(subject), 'utf-8')
458 msg['From'] = email_from
461 msg['Reply-To'] = reply_to
463 msg['Reply-To'] = msg['From']
464 msg['To'] = COMMASPACE.join(email_to)
466 msg['Cc'] = COMMASPACE.join(email_cc)
468 msg['Bcc'] = COMMASPACE.join(email_bcc)
469 msg['Date'] = formatdate(localtime=True)
471 msg['X-Priority'] = priorities.get(priority, '3 (Normal)')
473 # Add dynamic X Header
474 for key, value in x_headers.iteritems():
475 msg['%s' % key] = str(value)
477 if html2text and subtype == 'html':
478 text = html2text(email_body.decode('utf-8')).encode('utf-8')
479 alternative_part = MIMEMultipart(_subtype="alternative")
480 alternative_part.attach(MIMEText(text, _charset='utf-8', _subtype='plain'))
481 alternative_part.attach(email_text)
482 msg.attach(alternative_part)
484 msg.attach(email_text)
487 for (fname,fcontent) in attach:
488 part = MIMEBase('application', "octet-stream")
489 part.set_payload( fcontent )
490 Encoders.encode_base64(part)
491 part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,))
494 return _email_send(email_from, flatten([email_to, email_cc, email_bcc]), msg, openobject_id=openobject_id, ssl=ssl, debug=debug)
496 #----------------------------------------------------------
498 #----------------------------------------------------------
499 # text must be latin-1 encoded
500 def sms_send(user, password, api_id, text, to):
502 url = "http://api.urlsms.com/SendSMS.aspx"
503 #url = "http://196.7.150.220/http/sendmsg"
504 params = urllib.urlencode({'UserID': user, 'Password': password, 'SenderID': api_id, 'MsgText': text, 'RecipientMobileNo':to})
505 urllib.urlopen(url+"?"+params)
506 # FIXME: Use the logger if there is an error
509 #---------------------------------------------------------
510 # Class that stores an updateable string (used in wizards)
511 #---------------------------------------------------------
512 class UpdateableStr(local):
514 def __init__(self, string=''):
518 return str(self.string)
521 return str(self.string)
523 def __nonzero__(self):
524 return bool(self.string)
527 class UpdateableDict(local):
528 '''Stores an updateable dict to use in wizards'''
530 def __init__(self, dict=None):
536 return str(self.dict)
539 return str(self.dict)
542 return self.dict.clear()
545 return self.dict.keys()
547 def __setitem__(self, i, y):
548 self.dict.__setitem__(i, y)
550 def __getitem__(self, i):
551 return self.dict.__getitem__(i)
554 return self.dict.copy()
557 return self.dict.iteritems()
560 return self.dict.iterkeys()
562 def itervalues(self):
563 return self.dict.itervalues()
565 def pop(self, k, d=None):
566 return self.dict.pop(k, d)
569 return self.dict.popitem()
571 def setdefault(self, k, d=None):
572 return self.dict.setdefault(k, d)
574 def update(self, E, **F):
575 return self.dict.update(E, F)
578 return self.dict.values()
580 def get(self, k, d=None):
581 return self.dict.get(k, d)
583 def has_key(self, k):
584 return self.dict.has_key(k)
587 return self.dict.items()
589 def __cmp__(self, y):
590 return self.dict.__cmp__(y)
592 def __contains__(self, k):
593 return self.dict.__contains__(k)
595 def __delitem__(self, y):
596 return self.dict.__delitem__(y)
599 return self.dict.__eq__(y)
602 return self.dict.__ge__(y)
605 return self.dict.__gt__(y)
608 return self.dict.__hash__()
611 return self.dict.__iter__()
614 return self.dict.__le__(y)
617 return self.dict.__len__()
620 return self.dict.__lt__(y)
623 return self.dict.__ne__(y)
626 # Don't use ! Use res.currency.round()
627 class currency(float):
629 def __init__(self, value, accuracy=2, rounding=None):
631 rounding=10**-accuracy
632 self.rounding=rounding
633 self.accuracy=accuracy
635 def __new__(cls, value, accuracy=2, rounding=None):
636 return float.__new__(cls, round(value, accuracy))
639 # display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
640 # return str(display_value)
650 class dummy_cache(object):
651 """ Cache decorator replacement to actually do no caching.
653 This can be useful to benchmark and/or track memory leak.
657 def __init__(self, timeout=None, skiparg=2, multi=None, size=8192):
660 def clear(self, dbname, *args, **kwargs):
664 def clean_caches_for_db(cls, dbname):
667 def __call__(self, fn):
668 fn.clear_cache = self.clear
671 class real_cache(object):
673 Use it as a decorator of the function you plan to cache
674 Timeout: 0 = no timeout, otherwise in seconds
679 def __init__(self, timeout=None, skiparg=2, multi=None, size=8192):
680 assert skiparg >= 2 # at least self and cr
682 self.timeout = config['cache_timeout']
684 self.timeout = timeout
685 self.skiparg = skiparg
687 self.lasttime = time.time()
688 self.cache = LRU(size) # TODO take size from config
690 cache.__caches.append(self)
693 def _generate_keys(self, dbname, kwargs2):
695 Generate keys depending of the arguments and the self.mutli value
700 pairs.sort(key=lambda (k,v): k)
701 for i, (k, v) in enumerate(pairs):
702 if isinstance(v, dict):
703 pairs[i] = (k, to_tuple(v))
704 if isinstance(v, (list, set)):
705 pairs[i] = (k, tuple(v))
706 elif not is_hashable(v):
707 pairs[i] = (k, repr(v))
711 key = (('dbname', dbname),) + to_tuple(kwargs2)
714 multis = kwargs2[self.multi][:]
716 kwargs2[self.multi] = (id,)
717 key = (('dbname', dbname),) + to_tuple(kwargs2)
720 def _unify_args(self, *args, **kwargs):
721 # Update named arguments with positional argument values (without self and cr)
722 kwargs2 = self.fun_default_values.copy()
723 kwargs2.update(kwargs)
724 kwargs2.update(dict(zip(self.fun_arg_names, args[self.skiparg-2:])))
727 def clear(self, dbname, *args, **kwargs):
728 """clear the cache for database dbname
729 if *args and **kwargs are both empty, clear all the keys related to this database
731 if not args and not kwargs:
732 keys_to_del = [key for key in self.cache.keys() if key[0][1] == dbname]
734 kwargs2 = self._unify_args(*args, **kwargs)
735 keys_to_del = [key for key, _ in self._generate_keys(dbname, kwargs2) if key in self.cache.keys()]
737 for key in keys_to_del:
741 def clean_caches_for_db(cls, dbname):
742 for c in cls.__caches:
745 def __call__(self, fn):
746 if self.fun is not None:
747 raise Exception("Can not use a cache instance on more than one function")
750 argspec = inspect.getargspec(fn)
751 self.fun_arg_names = argspec[0][self.skiparg:]
752 self.fun_default_values = {}
754 self.fun_default_values = dict(zip(self.fun_arg_names[-len(argspec[3]):], argspec[3]))
756 def cached_result(self2, cr, *args, **kwargs):
757 if time.time()-int(self.timeout) > self.lasttime:
758 self.lasttime = time.time()
759 t = time.time()-int(self.timeout)
760 old_keys = [key for key in self.cache.keys() if self.cache[key][1] < t]
764 kwargs2 = self._unify_args(*args, **kwargs)
768 for key, id in self._generate_keys(cr.dbname, kwargs2):
769 if key in self.cache:
770 result[id] = self.cache[key][0]
776 kwargs2[self.multi] = notincache.keys()
778 result2 = fn(self2, cr, *args[:self.skiparg-2], **kwargs2)
780 key = notincache[None]
781 self.cache[key] = (result2, time.time())
782 result[None] = result2
786 self.cache[key] = (result2[id], time.time())
787 result.update(result2)
793 cached_result.clear_cache = self.clear
796 # TODO make it an option
800 return s.replace('&','&').replace('<','<').replace('>','>')
802 # to be compatible with python 2.4
804 if not hasattr(__builtin__, 'all'):
806 for element in iterable:
811 __builtin__.all = all
814 if not hasattr(__builtin__, 'any'):
816 for element in iterable:
821 __builtin__.any = any
824 def get_iso_codes(lang):
825 if lang.find('_') != -1:
826 if lang.split('_')[0] == lang.split('_')[1].lower():
827 lang = lang.split('_')[0]
831 # The codes below are those from Launchpad's Rosetta, with the exception
832 # of some trivial codes where the Launchpad code is xx and we have xx_XX.
834 'ab_RU': u'Abkhazian / аҧсуа',
835 'ar_AR': u'Arabic / الْعَرَبيّة',
836 'bg_BG': u'Bulgarian / български език',
837 'bs_BS': u'Bosnian / bosanski jezik',
838 'ca_ES': u'Catalan / Català',
839 'cs_CZ': u'Czech / Čeština',
840 'da_DK': u'Danish / Dansk',
841 'de_DE': u'German / Deutsch',
842 'el_GR': u'Greek / Ελληνικά',
843 'en_CA': u'English (CA)',
844 'en_GB': u'English (UK)',
845 'en_US': u'English (US)',
846 'es_AR': u'Spanish (AR) / Español (AR)',
847 'es_BO': u'Spanish (BO) / Español (BO)',
848 'es_CL': u'Spanish (CL) / Español (CL)',
849 'es_CO': u'Spanish (CO) / Español (CO)',
850 'es_CR': u'Spanish (CR) / Español (CR)',
851 'es_DO': u'Spanish (DO) / Español (DO)',
852 'es_EC': u'Spanish (EC) / Español (EC)',
853 'es_ES': u'Spanish / Español',
854 'es_GT': u'Spanish (GT) / Español (GT)',
855 'es_HN': u'Spanish (HN) / Español (HN)',
856 'es_MX': u'Spanish (MX) / Español (MX)',
857 'es_NI': u'Spanish (NI) / Español (NI)',
858 'es_PA': u'Spanish (PA) / Español (PA)',
859 'es_PE': u'Spanish (PE) / Español (PE)',
860 'es_PR': u'Spanish (PR) / Español (PR)',
861 'es_PY': u'Spanish (PY) / Español (PY)',
862 'es_SV': u'Spanish (SV) / Español (SV)',
863 'es_UY': u'Spanish (UY) / Español (UY)',
864 'es_VE': u'Spanish (VE) / Español (VE)',
865 'et_EE': u'Estonian / Eesti keel',
866 'fa_IR': u'Persian / فارس',
867 'fi_FI': u'Finnish / Suomi',
868 'fr_BE': u'French (BE) / Français (BE)',
869 'fr_CH': u'French (CH) / Français (CH)',
870 'fr_FR': u'French / Français',
871 'gl_ES': u'Galician / Galego',
872 'gu_IN': u'Gujarati / ગુજરાતી',
873 'he_IL': u'Hebrew / עִבְרִי',
874 'hi_IN': u'Hindi / हिंदी',
875 'hr_HR': u'Croatian / hrvatski jezik',
876 'hu_HU': u'Hungarian / Magyar',
877 'id_ID': u'Indonesian / Bahasa Indonesia',
878 'it_IT': u'Italian / Italiano',
879 'iu_CA': u'Inuktitut / ᐃᓄᒃᑎᑐᑦ',
880 'ja_JP': u'Japanese / 日本語',
881 'ko_KP': u'Korean (KP) / 한국어 (KP)',
882 'ko_KR': u'Korean (KR) / 한국어 (KR)',
883 'lt_LT': u'Lithuanian / Lietuvių kalba',
884 'lv_LV': u'Latvian / latviešu valoda',
885 'ml_IN': u'Malayalam / മലയാളം',
886 'mn_MN': u'Mongolian / монгол',
887 'nb_NO': u'Norwegian Bokmål / Norsk bokmål',
888 'nl_NL': u'Dutch / Nederlands',
889 'nl_BE': u'Flemish (BE) / Vlaams (BE)',
890 'oc_FR': u'Occitan (FR, post 1500) / Occitan',
891 'pl_PL': u'Polish / Język polski',
892 'pt_BR': u'Portugese (BR) / Português (BR)',
893 'pt_PT': u'Portugese / Português',
894 'ro_RO': u'Romanian / română',
895 'ru_RU': u'Russian / русский язык',
896 'si_LK': u'Sinhalese / සිංහල',
897 'sl_SI': u'Slovenian / slovenščina',
898 'sk_SK': u'Slovak / Slovenský jazyk',
899 'sq_AL': u'Albanian / Shqip',
900 'sr_RS': u'Serbian (Cyrillic) / српски',
901 'sr@latin': u'Serbian (Latin) / srpski',
902 'sv_SE': u'Swedish / svenska',
903 'te_IN': u'Telugu / తెలుగు',
904 'tr_TR': u'Turkish / Türkçe',
905 'vi_VN': u'Vietnamese / Tiếng Việt',
906 'uk_UA': u'Ukrainian / українська',
907 'ur_PK': u'Urdu / اردو',
908 'zh_CN': u'Chinese (CN) / 简体中文',
909 'zh_HK': u'Chinese (HK)',
910 'zh_TW': u'Chinese (TW) / 正體字',
911 'th_TH': u'Thai / ภาษาไทย',
912 'tlh_TLH': u'Klingon',
916 def scan_languages():
917 # Now it will take all languages from get languages function without filter it with base module languages
918 lang_dict = get_languages()
919 ret = [(lang, lang_dict.get(lang, lang)) for lang in list(lang_dict)]
920 ret.sort(key=lambda k:k[1])
924 def get_user_companies(cr, user):
925 def _get_company_children(cr, ids):
928 cr.execute('SELECT id FROM res_company WHERE parent_id IN %s', (tuple(ids),))
929 res = [x[0] for x in cr.fetchall()]
930 res.extend(_get_company_children(cr, res))
932 cr.execute('SELECT company_id FROM res_users WHERE id=%s', (user,))
933 user_comp = cr.fetchone()[0]
936 return [user_comp] + _get_company_children(cr, [user_comp])
940 Input number : account or invoice number
941 Output return: the same number completed with the recursive mod10
944 codec=[0,9,4,6,8,2,7,1,3,5]
950 report = codec[ (int(digit) + report) % 10 ]
951 return result + str((10 - report) % 10)
956 Return the size in a human readable format
960 units = ('bytes', 'Kb', 'Mb', 'Gb')
961 if isinstance(sz,basestring):
964 while s >= 1024 and i < len(units)-1:
967 return "%0.2f %s" % (s, units[i])
970 from func import wraps
973 def wrapper(*args, **kwargs):
974 from pprint import pformat
976 vector = ['Call -> function: %r' % f]
977 for i, arg in enumerate(args):
978 vector.append(' arg %02d: %s' % (i, pformat(arg)))
979 for key, value in kwargs.items():
980 vector.append(' kwarg %10s: %s' % (key, pformat(value)))
983 res = f(*args, **kwargs)
985 vector.append(' result: %s' % pformat(res))
986 vector.append(' time delta: %s' % (time.time() - timeb4))
987 loglevels.Logger().notifyChannel('logged', loglevels.LOG_DEBUG, '\n'.join(vector))
992 class profile(object):
993 def __init__(self, fname=None):
996 def __call__(self, f):
997 from func import wraps
1000 def wrapper(*args, **kwargs):
1001 class profile_wrapper(object):
1005 self.result = f(*args, **kwargs)
1006 pw = profile_wrapper()
1008 fname = self.fname or ("%s.cprof" % (f.func_name,))
1009 cProfile.runctx('pw()', globals(), locals(), filename=fname)
1016 This method allow you to debug your code without print
1018 >>> def func_foo(bar)
1021 ... qnx = (baz, bar)
1026 This will output on the logger:
1028 [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:baz = 42
1029 [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:qnx = (42, 42)
1031 To view the DEBUG lines in the logger you must start the server with the option
1035 warnings.warn("The tools.debug() method is deprecated, please use logging.",
1036 DeprecationWarning, stacklevel=2)
1037 from inspect import stack
1038 from pprint import pformat
1040 param = re.split("debug *\((.+)\)", st[4][0].strip())[1].strip()
1041 while param.count(')') > param.count('('): param = param[:param.rfind(')')]
1042 what = pformat(what)
1044 what = "%s = %s" % (param, what)
1045 logging.getLogger(st[3]).debug(what)
1048 __icons_list = ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
1049 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
1050 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
1051 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
1052 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
1053 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
1054 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
1055 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
1056 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
1057 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
1058 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
1059 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
1060 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
1061 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
1062 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
1063 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
1064 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
1065 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
1066 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
1067 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
1068 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
1069 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
1070 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
1071 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
1072 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
1073 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
1074 'terp-check','terp-go-month','terp-go-year','terp-go-today','terp-document-new','terp-camera_test',
1075 'terp-emblem-important','terp-gtk-media-pause','terp-gtk-stop','terp-gnome-cpu-frequency-applet+',
1076 'terp-dialog-close','terp-gtk-jump-to-rtl','terp-gtk-jump-to-ltr','terp-accessories-archiver',
1077 'terp-stock_align_left_24','terp-stock_effects-object-colorize','terp-go-home','terp-gtk-go-back-rtl',
1078 'terp-gtk-go-back-ltr','terp-personal','terp-personal-','terp-personal+','terp-accessories-archiver-minus',
1079 'terp-accessories-archiver+','terp-stock_symbol-selection','terp-call-start','terp-dolar',
1080 'terp-face-plain','terp-folder-blue','terp-folder-green','terp-folder-orange','terp-folder-yellow',
1081 'terp-gdu-smart-failing','terp-go-week','terp-gtk-select-all','terp-locked','terp-mail-forward',
1082 'terp-mail-message-new','terp-mail-replied','terp-rating-rated','terp-stage','terp-stock_format-scientific',
1083 'terp-dolar_ok!','terp-idea','terp-stock_format-default','terp-mail-','terp-mail_delete'
1086 def icons(*a, **kw):
1088 return [(x, x) for x in __icons_list ]
1090 def extract_zip_file(zip_file, outdirectory):
1091 zf = zipfile.ZipFile(zip_file, 'r')
1093 for path in zf.namelist():
1094 tgt = os.path.join(out, path)
1095 tgtdir = os.path.dirname(tgt)
1096 if not os.path.exists(tgtdir):
1099 if not tgt.endswith(os.sep):
1100 fp = open(tgt, 'wb')
1101 fp.write(zf.read(path))
1105 def detect_ip_addr():
1106 """Try a very crude method to figure out a valid external
1107 IP or hostname for the current machine. Don't rely on this
1108 for binding to an interface, but it could be used as basis
1109 for constructing a remote URL to the server.
1111 def _detect_ip_addr():
1112 from array import array
1113 from struct import pack, unpack
1122 if not fcntl: # not UNIX:
1123 host = socket.gethostname()
1124 ip_addr = socket.gethostbyname(host)
1126 # get all interfaces:
1128 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1129 names = array('B', '\0' * nbytes)
1130 #print 'names: ', names
1131 outbytes = unpack('iL', fcntl.ioctl( s.fileno(), 0x8912, pack('iL', nbytes, names.buffer_info()[0])))[0]
1132 namestr = names.tostring()
1134 # try 64 bit kernel:
1135 for i in range(0, outbytes, 40):
1136 name = namestr[i:i+16].split('\0', 1)[0]
1138 ip_addr = socket.inet_ntoa(namestr[i+20:i+24])
1141 # try 32 bit kernel:
1143 ifaces = filter(None, [namestr[i:i+32].split('\0', 1)[0] for i in range(0, outbytes, 32)])
1145 for ifname in [iface for iface in ifaces if iface != 'lo']:
1146 ip_addr = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, pack('256s', ifname[:15]))[20:24])
1149 return ip_addr or 'localhost'
1152 ip_addr = _detect_ip_addr()
1154 ip_addr = 'localhost'
1157 # RATIONALE BEHIND TIMESTAMP CALCULATIONS AND TIMEZONE MANAGEMENT:
1158 # The server side never does any timestamp calculation, always
1159 # sends them in a naive (timezone agnostic) format supposed to be
1160 # expressed within the server timezone, and expects the clients to
1161 # provide timestamps in the server timezone as well.
1162 # It stores all timestamps in the database in naive format as well,
1163 # which also expresses the time in the server timezone.
1164 # For this reason the server makes its timezone name available via the
1165 # common/timezone_get() rpc method, which clients need to read
1166 # to know the appropriate time offset to use when reading/writing
1168 def get_win32_timezone():
1169 """Attempt to return the "standard name" of the current timezone on a win32 system.
1170 @return: the standard name of the current win32 timezone, or False if it cannot be found.
1173 if (sys.platform == "win32"):
1176 hklm = _winreg.ConnectRegistry(None,_winreg.HKEY_LOCAL_MACHINE)
1177 current_tz_key = _winreg.OpenKey(hklm, r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", 0,_winreg.KEY_ALL_ACCESS)
1178 res = str(_winreg.QueryValueEx(current_tz_key,"StandardName")[0]) # [0] is value, [1] is type code
1179 _winreg.CloseKey(current_tz_key)
1180 _winreg.CloseKey(hklm)
1185 def detect_server_timezone():
1186 """Attempt to detect the timezone to use on the server side.
1187 Defaults to UTC if no working timezone can be found.
1188 @return: the timezone identifier as expected by pytz.timezone.
1193 loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_WARNING,
1194 "Python pytz module is not available. Timezone will be set to UTC by default.")
1197 # Option 1: the configuration option (did not exist before, so no backwards compatibility issue)
1198 # Option 2: to be backwards compatible with 5.0 or earlier, the value from time.tzname[0], but only if it is known to pytz
1199 # Option 3: the environment variable TZ
1200 sources = [ (config['timezone'], 'OpenERP configuration'),
1201 (time.tzname[0], 'time.tzname'),
1202 (os.environ.get('TZ',False),'TZ environment variable'), ]
1203 # Option 4: OS-specific: /etc/timezone on Unix
1204 if (os.path.exists("/etc/timezone")):
1207 f = open("/etc/timezone")
1208 tz_value = f.read(128).strip()
1213 sources.append((tz_value,"/etc/timezone file"))
1214 # Option 5: timezone info from registry on Win32
1215 if (sys.platform == "win32"):
1216 # Timezone info is stored in windows registry.
1217 # However this is not likely to work very well as the standard name
1218 # of timezones in windows is rarely something that is known to pytz.
1219 # But that's ok, it is always possible to use a config option to set
1221 sources.append((get_win32_timezone(),"Windows Registry"))
1223 for (value,source) in sources:
1226 tz = pytz.timezone(value)
1227 loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_INFO,
1228 "Using timezone %s obtained from %s." % (tz.zone,source))
1230 except pytz.UnknownTimeZoneError:
1231 loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_WARNING,
1232 "The timezone specified in %s (%s) is invalid, ignoring it." % (source,value))
1234 loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_WARNING,
1235 "No valid timezone could be detected, using default UTC timezone. You can specify it explicitly with option 'timezone' in the server configuration.")
1238 def get_server_timezone():
1239 # timezone detection is safe in multithread, so lazy init is ok here
1240 if (not config['timezone']):
1241 config['timezone'] = detect_server_timezone()
1242 return config['timezone']
1245 DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"
1246 DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S"
1247 DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % (
1248 DEFAULT_SERVER_DATE_FORMAT,
1249 DEFAULT_SERVER_TIME_FORMAT)
1251 # Python's strftime supports only the format directives
1252 # that are available on the platform's libc, so in order to
1253 # be cross-platform we map to the directives required by
1254 # the C standard (1989 version), always available on platforms
1255 # with a C standard implementation.
1256 DATETIME_FORMATS_MAP = {
1258 '%D': '%m/%d/%Y', # modified %y->%Y
1260 '%E': '', # special modifier
1262 '%g': '%Y', # modified %y->%Y
1268 '%O': '', # special modifier
1271 '%r': '%I:%M:%S %p',
1272 '%s': '', #num of seconds since epoch
1277 '%y': '%Y', # Even if %y works, it's ambiguous, so we should use %Y
1278 '%+': '%Y-%m-%d %H:%M:%S',
1280 # %Z is a special case that causes 2 problems at least:
1281 # - the timezone names we use (in res_user.context_tz) come
1282 # from pytz, but not all these names are recognized by
1283 # strptime(), so we cannot convert in both directions
1284 # when such a timezone is selected and %Z is in the format
1285 # - %Z is replaced by an empty string in strftime() when
1286 # there is not tzinfo in a datetime value (e.g when the user
1287 # did not pick a context_tz). The resulting string does not
1288 # parse back if the format requires %Z.
1289 # As a consequence, we strip it completely from format strings.
1290 # The user can always have a look at the context_tz in
1291 # preferences to check the timezone.
1296 def server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_name,
1297 tz_offset=True, ignore_unparsable_time=True):
1299 Convert a source timestamp string into a destination timestamp string, attempting to apply the
1300 correct offset if both the server and local timezone are recognized, or no
1301 offset at all if they aren't or if tz_offset is false (i.e. assuming they are both in the same TZ).
1303 WARNING: This method is here to allow formatting dates correctly for inclusion in strings where
1304 the client would not be able to format/offset it correctly. DO NOT use it for returning
1305 date fields directly, these are supposed to be handled by the client!!
1307 @param src_tstamp_str: the str value containing the timestamp in the server timezone.
1308 @param src_format: the format to use when parsing the server timestamp.
1309 @param dst_format: the format to use when formatting the resulting timestamp for the local/client timezone.
1310 @param dst_tz_name: name of the destination timezone (such as the 'tz' value of the client context)
1311 @param ignore_unparsable_time: if True, return False if src_tstamp_str cannot be parsed
1312 using src_format or formatted using dst_format.
1314 @return: local/client formatted timestamp, expressed in the local/client timezone if possible
1315 and if tz_offset is true, or src_tstamp_str if timezone offset could not be determined.
1317 if not src_tstamp_str:
1320 res = src_tstamp_str
1321 if src_format and dst_format:
1322 # find out server timezone
1323 server_tz = get_server_timezone()
1325 # dt_value needs to be a datetime.datetime object (so no time.struct_time or mx.DateTime.DateTime here!)
1326 dt_value = datetime.strptime(src_tstamp_str, src_format)
1327 if tz_offset and dst_tz_name:
1330 src_tz = pytz.timezone(server_tz)
1331 dst_tz = pytz.timezone(dst_tz_name)
1332 src_dt = src_tz.localize(dt_value, is_dst=True)
1333 dt_value = src_dt.astimezone(dst_tz)
1336 res = dt_value.strftime(dst_format)
1338 # Normal ways to end up here are if strptime or strftime failed
1339 if not ignore_unparsable_time:
1344 def split_every(n, iterable, piece_maker=tuple):
1345 """Splits an iterable into length-n pieces. The last piece will be shorter
1346 if ``n`` does not evenly divide the iterable length.
1347 @param ``piece_maker``: function to build the pieces
1348 from the slices (tuple,list,...)
1350 iterator = iter(iterable)
1351 piece = piece_maker(islice(iterator, n))
1354 piece = piece_maker(islice(iterator, n))
1356 if __name__ == '__main__':
1360 class upload_data_thread(threading.Thread):
1361 def __init__(self, email, data, type):
1362 self.args = [('email',email),('type',type),('data',data)]
1363 super(upload_data_thread,self).__init__()
1367 args = urllib.urlencode(self.args)
1368 fp = urllib.urlopen('http://www.openerp.com/scripts/survey.php', args)
1374 def upload_data(email, data, type='SURVEY'):
1375 a = upload_data_thread(email, data, type)
1380 # port of python 2.6's attrgetter with support for dotted notation
1381 def resolve_attr(obj, attr):
1382 for name in attr.split("."):
1383 obj = getattr(obj, name)
1386 def attrgetter(*items):
1390 return resolve_attr(obj, attr)
1393 return tuple(resolve_attr(obj, attr) for attr in items)
1397 """A subclass of str that implements repr() without enclosing quotation marks
1398 or escaping, keeping the original string untouched. The name come from Lisp's unquote.
1399 One of the uses for this is to preserve or insert bare variable names within dicts during eval()
1400 of a dict's repr(). Use with care.
1402 >>> unquote('active_id')
1404 >>> repr(unquote('active_id'))
1406 >>> d = {'test': unquote('active_id')}
1410 "{'test': active_id}"
1415 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: