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 ##############################################################################
23 #.apidoc title: Utilities: tools.misc
26 Miscelleanous tools used by OpenERP.
41 from collections import defaultdict
42 from datetime import datetime
43 from email.MIMEText import MIMEText
44 from email.MIMEBase import MIMEBase
45 from email.MIMEMultipart import MIMEMultipart
46 from email.Header import Header
47 from email.Utils import formatdate, COMMASPACE
48 from email import Encoders
49 from itertools import islice, izip
50 from lxml import etree
51 from which import which
52 if sys.version_info[:2] < (2, 4):
53 from threadinglocal import local
55 from threading import local
57 from html2text import html2text
61 import openerp.loglevels as loglevels
62 from config import config
65 # get_encodings, ustr and exception_to_unicode were originally from tools.misc.
66 # There are moved to loglevels until we refactor tools.
67 from openerp.loglevels import get_encodings, ustr, exception_to_unicode
69 _logger = logging.getLogger('tools')
71 # List of etree._Element subclasses that we choose to ignore when parsing XML.
72 # We include the *Base ones just in case, currently they seem to be subclasses of the _* ones.
73 SKIPPED_ELEMENT_TYPES = (etree._Comment, etree._ProcessingInstruction, etree.CommentBase, etree.PIBase)
75 def find_in_path(name):
81 def find_pg_tool(name):
83 if config['pg_path'] and config['pg_path'] != 'None':
84 path = config['pg_path']
86 return which(name, path=path)
90 def exec_pg_command(name, *args):
91 prog = find_pg_tool(name)
93 raise Exception('Couldn\'t find %s' % name)
94 args2 = (prog,) + args
96 return subprocess.call(args2)
98 def exec_pg_command_pipe(name, *args):
99 prog = find_pg_tool(name)
101 raise Exception('Couldn\'t find %s' % name)
102 # on win32, passing close_fds=True is not compatible
103 # with redirecting std[in/err/out]
104 pop = subprocess.Popen((prog,) + args, bufsize= -1,
105 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
106 close_fds=(os.name=="posix"))
107 return (pop.stdin, pop.stdout)
109 def exec_command_pipe(name, *args):
110 prog = find_in_path(name)
112 raise Exception('Couldn\'t find %s' % name)
113 # on win32, passing close_fds=True is not compatible
114 # with redirecting std[in/err/out]
115 pop = subprocess.Popen((prog,) + args, bufsize= -1,
116 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
117 close_fds=(os.name=="posix"))
118 return (pop.stdin, pop.stdout)
120 #----------------------------------------------------------
122 #----------------------------------------------------------
123 #file_path_root = os.getcwd()
124 #file_path_addons = os.path.join(file_path_root, 'addons')
126 def file_open(name, mode="r", subdir='addons', pathinfo=False):
127 """Open a file from the OpenERP root, using a subdir folder.
131 >>> file_open('hr/report/timesheer.xsl')
132 >>> file_open('addons/hr/report/timesheet.xsl')
133 >>> file_open('../../base/report/rml_template.xsl', subdir='addons/hr/report', pathinfo=True)
135 @param name name of the file
136 @param mode file open mode
137 @param subdir subdirectory
138 @param pathinfo if True returns tupple (fileobject, filepath)
140 @return fileobject if pathinfo is False else (fileobject, filepath)
142 import openerp.modules as addons
143 adps = addons.module.ad_paths
144 rtp = os.path.normcase(os.path.abspath(config['root_path']))
146 if name.replace(os.path.sep, '/').startswith('addons/'):
150 # First try to locate in addons_path
153 if subdir2.replace(os.path.sep, '/').startswith('addons/'):
154 subdir2 = subdir2[7:]
156 subdir2 = (subdir2 != 'addons' or None) and subdir2
161 fn = os.path.join(adp, subdir2, name)
163 fn = os.path.join(adp, name)
164 fn = os.path.normpath(fn)
165 fo = file_open(fn, mode=mode, subdir=None, pathinfo=pathinfo)
173 name = os.path.join(rtp, subdir, name)
175 name = os.path.join(rtp, name)
177 name = os.path.normpath(name)
179 # Check for a zipfile in the path
184 head, tail = os.path.split(head)
188 zipname = os.path.join(tail, zipname)
191 if zipfile.is_zipfile(head+'.zip'):
192 from cStringIO import StringIO
193 zfile = zipfile.ZipFile(head+'.zip')
196 fo.write(zfile.read(os.path.join(
197 os.path.basename(head), zipname).replace(
204 name2 = os.path.normpath(os.path.join(head + '.zip', zipname))
206 for i in (name2, name):
207 if i and os.path.isfile(i):
212 if os.path.splitext(name)[1] == '.rml':
213 raise IOError, 'Report %s doesn\'t exist or deleted : ' %str(name)
214 raise IOError, 'File not found : %s' % name
217 #----------------------------------------------------------
219 #----------------------------------------------------------
221 """Flatten a list of elements into a uniqu list
222 Author: Christophe Simonis (christophe@tinyerp.com)
231 >>> flatten( [[], [[]]] )
233 >>> flatten( [[['a','b'], 'c'], 'd', ['e', [], 'f']] )
234 ['a', 'b', 'c', 'd', 'e', 'f']
235 >>> t = (1,2,(3,), [4, 5, [6, [7], (8, 9), ([10, 11, (12, 13)]), [14, [], (15,)], []]])
237 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
241 return hasattr(x, "__iter__")
246 map(r.append, flatten(e))
251 def reverse_enumerate(l):
252 """Like enumerate but in the other sens
255 >>> a = ['a', 'b', 'c']
256 >>> it = reverse_enumerate(a)
264 Traceback (most recent call last):
265 File "<stdin>", line 1, in <module>
268 return izip(xrange(len(l)-1, -1, -1), reversed(l))
270 #----------------------------------------------------------
272 #----------------------------------------------------------
273 email_re = re.compile(r"""
274 ([a-zA-Z][\w\.-]*[a-zA-Z0-9] # username part
276 [a-zA-Z0-9][\w\.-]* # domain must start with a letter ... Ged> why do we include a 0-9 then?
281 res_re = re.compile(r"\[([0-9]+)\]", re.UNICODE)
282 command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE)
283 reference_re = re.compile("<.*-openobject-(\\d+)@(.*)>", re.UNICODE)
293 def html2plaintext(html, body_id=None, encoding='utf-8'):
294 """ From an HTML text, convert the HTML to plain text.
295 If @param body_id is provided then this is the tag where the
296 body (not necessarily <body>) starts.
298 ## (c) Fry-IT, www.fry-it.com, 2007
299 ## <peter@fry-it.com>
300 ## download here: http://www.peterbe.com/plog/html2plaintext
304 from lxml.etree import tostring
306 from lxml.html.soupparser import fromstring
309 _logger.debug('tools.misc.html2plaintext: cannot use BeautifulSoup, fallback to lxml.etree.HTMLParser')
310 from lxml.etree import fromstring, HTMLParser
311 kwargs = dict(parser=HTMLParser())
313 tree = fromstring(html, **kwargs)
315 if body_id is not None:
316 source = tree.xpath('//*[@id=%s]'%(body_id,))
318 source = tree.xpath('//body')
324 for link in tree.findall('.//a'):
325 url = link.get('href')
329 link.text = '%s [%s]' % (link.text, i)
330 url_index.append(url)
332 html = ustr(tostring(tree, encoding=encoding))
334 html = html.replace('<strong>','*').replace('</strong>','*')
335 html = html.replace('<b>','*').replace('</b>','*')
336 html = html.replace('<h3>','*').replace('</h3>','*')
337 html = html.replace('<h2>','**').replace('</h2>','**')
338 html = html.replace('<h1>','**').replace('</h1>','**')
339 html = html.replace('<em>','/').replace('</em>','/')
340 html = html.replace('<tr>', '\n')
341 html = html.replace('</p>', '\n')
342 html = re.sub('<br\s*/?>', '\n', html)
343 html = re.sub('<.*?>', ' ', html)
344 html = html.replace(' ' * 2, ' ')
347 html = '\n'.join([x.strip() for x in html.splitlines()])
348 html = html.replace('\n' * 2, '\n')
350 for i, url in enumerate(url_index):
353 html += ustr('[%s] %s\n') % (i+1, url)
357 def generate_tracking_message_id(openobject_id):
358 """Returns a string that can be used in the Message-ID RFC822 header field
360 Used to track the replies related to a given object thanks to the "In-Reply-To"
361 or "References" fields that Mail User Agents will set.
363 return "<%s-openobject-%s@%s>" % (time.time(), openobject_id, socket.gethostname())
365 def _email_send(smtp_from, smtp_to_list, message, openobject_id=None, ssl=False, debug=False):
366 """ Low-level method to send directly a Message through the configured smtp server.
368 :param smtp_from: RFC-822 envelope FROM (not displayed to recipient)
369 :param smtp_to_list: RFC-822 envelope RCPT_TOs (not displayed to recipient)
370 :param message: an email.message.Message to send
371 :param debug: True if messages should be output to stderr before being sent,
372 and smtplib.SMTP put into debug mode.
373 :return: True if the mail was delivered successfully to the smtp,
374 else False (+ exception logged)
376 class WriteToLogger(object):
378 self.logger = loglevels.Logger()
381 self.logger.notifyChannel('email_send', loglevels.LOG_DEBUG, s)
384 message['Message-Id'] = generate_tracking_message_id(openobject_id)
387 smtp_server = config['smtp_server']
389 if smtp_server.startswith('maildir:/'):
390 from mailbox import Maildir
391 maildir_path = smtp_server[8:]
392 mdir = Maildir(maildir_path,factory=None, create = True)
393 mdir.add(message.as_string(True))
396 oldstderr = smtplib.stderr
397 if not ssl: ssl = config.get('smtp_ssl', False)
400 # in case of debug, the messages are printed to stderr.
402 smtplib.stderr = WriteToLogger()
404 s.set_debuglevel(int(bool(debug))) # 0 or 1
405 s.connect(smtp_server, config['smtp_port'])
411 if config['smtp_user'] or config['smtp_password']:
412 s.login(config['smtp_user'], config['smtp_password'])
414 s.sendmail(smtp_from, smtp_to_list, message.as_string())
419 smtplib.stderr = oldstderr
421 # ignored, just a consequence of the previous exception
425 _logger.error('could not deliver email', exc_info=True)
431 def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
432 attach=None, openobject_id=False, ssl=False, debug=False, subtype='plain', x_headers=None, priority='3'):
436 @param email_from A string used to fill the `From` header, if falsy,
437 config['email_from'] is used instead. Also used for
438 the `Reply-To` header if `reply_to` is not provided
440 @param email_to a sequence of addresses to send the mail to.
442 if x_headers is None:
447 if not (email_from or config['email_from']):
448 raise ValueError("Sending an email requires either providing a sender "
449 "address or having configured one")
451 if not email_from: email_from = config.get('email_from', False)
452 email_from = ustr(email_from).encode('utf-8')
454 if not email_cc: email_cc = []
455 if not email_bcc: email_bcc = []
456 if not body: body = u''
458 email_body = ustr(body).encode('utf-8')
459 email_text = MIMEText(email_body or '',_subtype=subtype,_charset='utf-8')
461 msg = MIMEMultipart()
463 msg['Subject'] = Header(ustr(subject), 'utf-8')
464 msg['From'] = email_from
467 msg['Reply-To'] = reply_to
469 msg['Reply-To'] = msg['From']
470 msg['To'] = COMMASPACE.join(email_to)
472 msg['Cc'] = COMMASPACE.join(email_cc)
474 msg['Bcc'] = COMMASPACE.join(email_bcc)
475 msg['Date'] = formatdate(localtime=True)
477 msg['X-Priority'] = priorities.get(priority, '3 (Normal)')
479 # Add dynamic X Header
480 for key, value in x_headers.iteritems():
481 msg['%s' % key] = str(value)
483 if html2text and subtype == 'html':
484 text = html2text(email_body.decode('utf-8')).encode('utf-8')
485 alternative_part = MIMEMultipart(_subtype="alternative")
486 alternative_part.attach(MIMEText(text, _charset='utf-8', _subtype='plain'))
487 alternative_part.attach(email_text)
488 msg.attach(alternative_part)
490 msg.attach(email_text)
493 for (fname,fcontent) in attach:
494 part = MIMEBase('application', "octet-stream")
495 part.set_payload( fcontent )
496 Encoders.encode_base64(part)
497 part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,))
500 return _email_send(email_from, flatten([email_to, email_cc, email_bcc]), msg, openobject_id=openobject_id, ssl=ssl, debug=debug)
502 #----------------------------------------------------------
504 #----------------------------------------------------------
505 # text must be latin-1 encoded
506 def sms_send(user, password, api_id, text, to):
508 url = "http://api.urlsms.com/SendSMS.aspx"
509 #url = "http://196.7.150.220/http/sendmsg"
510 params = urllib.urlencode({'UserID': user, 'Password': password, 'SenderID': api_id, 'MsgText': text, 'RecipientMobileNo':to})
511 urllib.urlopen(url+"?"+params)
512 # FIXME: Use the logger if there is an error
515 class UpdateableStr(local):
516 """ Class that stores an updateable string (used in wizards)
519 def __init__(self, string=''):
523 return str(self.string)
526 return str(self.string)
528 def __nonzero__(self):
529 return bool(self.string)
532 class UpdateableDict(local):
533 """Stores an updateable dict to use in wizards
536 def __init__(self, dict=None):
542 return str(self.dict)
545 return str(self.dict)
548 return self.dict.clear()
551 return self.dict.keys()
553 def __setitem__(self, i, y):
554 self.dict.__setitem__(i, y)
556 def __getitem__(self, i):
557 return self.dict.__getitem__(i)
560 return self.dict.copy()
563 return self.dict.iteritems()
566 return self.dict.iterkeys()
568 def itervalues(self):
569 return self.dict.itervalues()
571 def pop(self, k, d=None):
572 return self.dict.pop(k, d)
575 return self.dict.popitem()
577 def setdefault(self, k, d=None):
578 return self.dict.setdefault(k, d)
580 def update(self, E, **F):
581 return self.dict.update(E, F)
584 return self.dict.values()
586 def get(self, k, d=None):
587 return self.dict.get(k, d)
589 def has_key(self, k):
590 return self.dict.has_key(k)
593 return self.dict.items()
595 def __cmp__(self, y):
596 return self.dict.__cmp__(y)
598 def __contains__(self, k):
599 return self.dict.__contains__(k)
601 def __delitem__(self, y):
602 return self.dict.__delitem__(y)
605 return self.dict.__eq__(y)
608 return self.dict.__ge__(y)
611 return self.dict.__gt__(y)
614 return self.dict.__hash__()
617 return self.dict.__iter__()
620 return self.dict.__le__(y)
623 return self.dict.__len__()
626 return self.dict.__lt__(y)
629 return self.dict.__ne__(y)
632 class currency(float):
637 Don't use ! Use res.currency.round()
640 def __init__(self, value, accuracy=2, rounding=None):
642 rounding=10**-accuracy
643 self.rounding=rounding
644 self.accuracy=accuracy
646 def __new__(cls, value, accuracy=2, rounding=None):
647 return float.__new__(cls, round(value, accuracy))
650 # display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
651 # return str(display_value)
661 class dummy_cache(object):
662 """ Cache decorator replacement to actually do no caching.
664 This can be useful to benchmark and/or track memory leak.
668 def __init__(self, timeout=None, skiparg=2, multi=None, size=8192):
671 def clear(self, dbname, *args, **kwargs):
675 def clean_caches_for_db(cls, dbname):
678 def __call__(self, fn):
679 fn.clear_cache = self.clear
682 class real_cache(object):
684 Use it as a decorator of the function you plan to cache
685 Timeout: 0 = no timeout, otherwise in seconds
690 def __init__(self, timeout=None, skiparg=2, multi=None, size=8192):
691 assert skiparg >= 2 # at least self and cr
693 self.timeout = config['cache_timeout']
695 self.timeout = timeout
696 self.skiparg = skiparg
698 self.lasttime = time.time()
699 self.cache = LRU(size) # TODO take size from config
701 cache.__caches.append(self)
704 def _generate_keys(self, dbname, kwargs2):
706 Generate keys depending of the arguments and the self.mutli value
711 pairs.sort(key=lambda (k,v): k)
712 for i, (k, v) in enumerate(pairs):
713 if isinstance(v, dict):
714 pairs[i] = (k, to_tuple(v))
715 if isinstance(v, (list, set)):
716 pairs[i] = (k, tuple(v))
717 elif not is_hashable(v):
718 pairs[i] = (k, repr(v))
722 key = (('dbname', dbname),) + to_tuple(kwargs2)
725 multis = kwargs2[self.multi][:]
727 kwargs2[self.multi] = (id,)
728 key = (('dbname', dbname),) + to_tuple(kwargs2)
731 def _unify_args(self, *args, **kwargs):
732 # Update named arguments with positional argument values (without self and cr)
733 kwargs2 = self.fun_default_values.copy()
734 kwargs2.update(kwargs)
735 kwargs2.update(dict(zip(self.fun_arg_names, args[self.skiparg-2:])))
738 def clear(self, dbname, *args, **kwargs):
739 """clear the cache for database dbname
740 if *args and **kwargs are both empty, clear all the keys related to this database
742 if not args and not kwargs:
743 keys_to_del = [key for key in self.cache.keys() if key[0][1] == dbname]
745 kwargs2 = self._unify_args(*args, **kwargs)
746 keys_to_del = [key for key, _ in self._generate_keys(dbname, kwargs2) if key in self.cache.keys()]
748 for key in keys_to_del:
752 def clean_caches_for_db(cls, dbname):
753 for c in cls.__caches:
756 def __call__(self, fn):
757 if self.fun is not None:
758 raise Exception("Can not use a cache instance on more than one function")
761 argspec = inspect.getargspec(fn)
762 self.fun_arg_names = argspec[0][self.skiparg:]
763 self.fun_default_values = {}
765 self.fun_default_values = dict(zip(self.fun_arg_names[-len(argspec[3]):], argspec[3]))
767 def cached_result(self2, cr, *args, **kwargs):
768 if time.time()-int(self.timeout) > self.lasttime:
769 self.lasttime = time.time()
770 t = time.time()-int(self.timeout)
771 old_keys = [key for key in self.cache.keys() if self.cache[key][1] < t]
775 kwargs2 = self._unify_args(*args, **kwargs)
779 for key, id in self._generate_keys(cr.dbname, kwargs2):
780 if key in self.cache:
781 result[id] = self.cache[key][0]
787 kwargs2[self.multi] = notincache.keys()
789 result2 = fn(self2, cr, *args[:self.skiparg-2], **kwargs2)
791 key = notincache[None]
792 self.cache[key] = (result2, time.time())
793 result[None] = result2
797 self.cache[key] = (result2[id], time.time())
798 result.update(result2)
804 cached_result.clear_cache = self.clear
807 # TODO make it an option
811 return s.replace('&','&').replace('<','<').replace('>','>')
813 # to be compatible with python 2.4
815 if not hasattr(__builtin__, 'all'):
817 for element in iterable:
822 __builtin__.all = all
825 if not hasattr(__builtin__, 'any'):
827 for element in iterable:
832 __builtin__.any = any
835 def get_iso_codes(lang):
836 if lang.find('_') != -1:
837 if lang.split('_')[0] == lang.split('_')[1].lower():
838 lang = lang.split('_')[0]
842 # The codes below are those from Launchpad's Rosetta, with the exception
843 # of some trivial codes where the Launchpad code is xx and we have xx_XX.
845 'ab_RU': u'Abkhazian / аҧсуа',
846 'ar_AR': u'Arabic / الْعَرَبيّة',
847 'bg_BG': u'Bulgarian / български език',
848 'bs_BS': u'Bosnian / bosanski jezik',
849 'ca_ES': u'Catalan / Català',
850 'cs_CZ': u'Czech / Čeština',
851 'da_DK': u'Danish / Dansk',
852 'de_DE': u'German / Deutsch',
853 'el_GR': u'Greek / Ελληνικά',
854 'en_CA': u'English (CA)',
855 'en_GB': u'English (UK)',
856 'en_US': u'English (US)',
857 'es_AR': u'Spanish (AR) / Español (AR)',
858 'es_BO': u'Spanish (BO) / Español (BO)',
859 'es_CL': u'Spanish (CL) / Español (CL)',
860 'es_CO': u'Spanish (CO) / Español (CO)',
861 'es_CR': u'Spanish (CR) / Español (CR)',
862 'es_DO': u'Spanish (DO) / Español (DO)',
863 'es_EC': u'Spanish (EC) / Español (EC)',
864 'es_ES': u'Spanish / Español',
865 'es_GT': u'Spanish (GT) / Español (GT)',
866 'es_HN': u'Spanish (HN) / Español (HN)',
867 'es_MX': u'Spanish (MX) / Español (MX)',
868 'es_NI': u'Spanish (NI) / Español (NI)',
869 'es_PA': u'Spanish (PA) / Español (PA)',
870 'es_PE': u'Spanish (PE) / Español (PE)',
871 'es_PR': u'Spanish (PR) / Español (PR)',
872 'es_PY': u'Spanish (PY) / Español (PY)',
873 'es_SV': u'Spanish (SV) / Español (SV)',
874 'es_UY': u'Spanish (UY) / Español (UY)',
875 'es_VE': u'Spanish (VE) / Español (VE)',
876 'et_EE': u'Estonian / Eesti keel',
877 'fa_IR': u'Persian / فارس',
878 'fi_FI': u'Finnish / Suomi',
879 'fr_BE': u'French (BE) / Français (BE)',
880 'fr_CH': u'French (CH) / Français (CH)',
881 'fr_FR': u'French / Français',
882 'gl_ES': u'Galician / Galego',
883 'gu_IN': u'Gujarati / ગુજરાતી',
884 'he_IL': u'Hebrew / עִבְרִי',
885 'hi_IN': u'Hindi / हिंदी',
886 'hr_HR': u'Croatian / hrvatski jezik',
887 'hu_HU': u'Hungarian / Magyar',
888 'id_ID': u'Indonesian / Bahasa Indonesia',
889 'it_IT': u'Italian / Italiano',
890 'iu_CA': u'Inuktitut / ᐃᓄᒃᑎᑐᑦ',
891 'ja_JP': u'Japanese / 日本語',
892 'ko_KP': u'Korean (KP) / 한국어 (KP)',
893 'ko_KR': u'Korean (KR) / 한국어 (KR)',
894 'lt_LT': u'Lithuanian / Lietuvių kalba',
895 'lv_LV': u'Latvian / latviešu valoda',
896 'ml_IN': u'Malayalam / മലയാളം',
897 'mn_MN': u'Mongolian / монгол',
898 'nb_NO': u'Norwegian Bokmål / Norsk bokmål',
899 'nl_NL': u'Dutch / Nederlands',
900 'nl_BE': u'Flemish (BE) / Vlaams (BE)',
901 'oc_FR': u'Occitan (FR, post 1500) / Occitan',
902 'pl_PL': u'Polish / Język polski',
903 'pt_BR': u'Portugese (BR) / Português (BR)',
904 'pt_PT': u'Portugese / Português',
905 'ro_RO': u'Romanian / română',
906 'ru_RU': u'Russian / русский язык',
907 'si_LK': u'Sinhalese / සිංහල',
908 'sl_SI': u'Slovenian / slovenščina',
909 'sk_SK': u'Slovak / Slovenský jazyk',
910 'sq_AL': u'Albanian / Shqip',
911 'sr_RS': u'Serbian (Cyrillic) / српски',
912 'sr@latin': u'Serbian (Latin) / srpski',
913 'sv_SE': u'Swedish / svenska',
914 'te_IN': u'Telugu / తెలుగు',
915 'tr_TR': u'Turkish / Türkçe',
916 'vi_VN': u'Vietnamese / Tiếng Việt',
917 'uk_UA': u'Ukrainian / українська',
918 'ur_PK': u'Urdu / اردو',
919 'zh_CN': u'Chinese (CN) / 简体中文',
920 'zh_HK': u'Chinese (HK)',
921 'zh_TW': u'Chinese (TW) / 正體字',
922 'th_TH': u'Thai / ภาษาไทย',
923 'tlh_TLH': u'Klingon',
927 def scan_languages():
928 # Now it will take all languages from get languages function without filter it with base module languages
929 lang_dict = get_languages()
930 ret = [(lang, lang_dict.get(lang, lang)) for lang in list(lang_dict)]
931 ret.sort(key=lambda k:k[1])
935 def get_user_companies(cr, user):
936 def _get_company_children(cr, ids):
939 cr.execute('SELECT id FROM res_company WHERE parent_id IN %s', (tuple(ids),))
940 res = [x[0] for x in cr.fetchall()]
941 res.extend(_get_company_children(cr, res))
943 cr.execute('SELECT company_id FROM res_users WHERE id=%s', (user,))
944 user_comp = cr.fetchone()[0]
947 return [user_comp] + _get_company_children(cr, [user_comp])
951 Input number : account or invoice number
952 Output return: the same number completed with the recursive mod10
955 codec=[0,9,4,6,8,2,7,1,3,5]
961 report = codec[ (int(digit) + report) % 10 ]
962 return result + str((10 - report) % 10)
967 Return the size in a human readable format
971 units = ('bytes', 'Kb', 'Mb', 'Gb')
972 if isinstance(sz,basestring):
975 while s >= 1024 and i < len(units)-1:
978 return "%0.2f %s" % (s, units[i])
981 from func import wraps
984 def wrapper(*args, **kwargs):
985 from pprint import pformat
987 vector = ['Call -> function: %r' % f]
988 for i, arg in enumerate(args):
989 vector.append(' arg %02d: %s' % (i, pformat(arg)))
990 for key, value in kwargs.items():
991 vector.append(' kwarg %10s: %s' % (key, pformat(value)))
994 res = f(*args, **kwargs)
996 vector.append(' result: %s' % pformat(res))
997 vector.append(' time delta: %s' % (time.time() - timeb4))
998 loglevels.Logger().notifyChannel('logged', loglevels.LOG_DEBUG, '\n'.join(vector))
1003 class profile(object):
1004 def __init__(self, fname=None):
1007 def __call__(self, f):
1008 from func import wraps
1011 def wrapper(*args, **kwargs):
1012 class profile_wrapper(object):
1016 self.result = f(*args, **kwargs)
1017 pw = profile_wrapper()
1019 fname = self.fname or ("%s.cprof" % (f.func_name,))
1020 cProfile.runctx('pw()', globals(), locals(), filename=fname)
1027 This method allow you to debug your code without print
1029 >>> def func_foo(bar)
1032 ... qnx = (baz, bar)
1037 This will output on the logger:
1039 [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:baz = 42
1040 [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:qnx = (42, 42)
1042 To view the DEBUG lines in the logger you must start the server with the option
1046 warnings.warn("The tools.debug() method is deprecated, please use logging.",
1047 DeprecationWarning, stacklevel=2)
1048 from inspect import stack
1049 from pprint import pformat
1051 param = re.split("debug *\((.+)\)", st[4][0].strip())[1].strip()
1052 while param.count(')') > param.count('('): param = param[:param.rfind(')')]
1053 what = pformat(what)
1055 what = "%s = %s" % (param, what)
1056 logging.getLogger(st[3]).debug(what)
1059 __icons_list = ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
1060 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
1061 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
1062 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
1063 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
1064 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
1065 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
1066 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
1067 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
1068 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
1069 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
1070 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
1071 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
1072 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
1073 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
1074 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
1075 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
1076 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
1077 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
1078 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
1079 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
1080 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
1081 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
1082 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
1083 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
1084 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
1085 'terp-check','terp-go-month','terp-go-year','terp-go-today','terp-document-new','terp-camera_test',
1086 'terp-emblem-important','terp-gtk-media-pause','terp-gtk-stop','terp-gnome-cpu-frequency-applet+',
1087 'terp-dialog-close','terp-gtk-jump-to-rtl','terp-gtk-jump-to-ltr','terp-accessories-archiver',
1088 'terp-stock_align_left_24','terp-stock_effects-object-colorize','terp-go-home','terp-gtk-go-back-rtl',
1089 'terp-gtk-go-back-ltr','terp-personal','terp-personal-','terp-personal+','terp-accessories-archiver-minus',
1090 'terp-accessories-archiver+','terp-stock_symbol-selection','terp-call-start','terp-dolar',
1091 'terp-face-plain','terp-folder-blue','terp-folder-green','terp-folder-orange','terp-folder-yellow',
1092 'terp-gdu-smart-failing','terp-go-week','terp-gtk-select-all','terp-locked','terp-mail-forward',
1093 'terp-mail-message-new','terp-mail-replied','terp-rating-rated','terp-stage','terp-stock_format-scientific',
1094 'terp-dolar_ok!','terp-idea','terp-stock_format-default','terp-mail-','terp-mail_delete'
1097 def icons(*a, **kw):
1099 return [(x, x) for x in __icons_list ]
1101 def extract_zip_file(zip_file, outdirectory):
1102 zf = zipfile.ZipFile(zip_file, 'r')
1104 for path in zf.namelist():
1105 tgt = os.path.join(out, path)
1106 tgtdir = os.path.dirname(tgt)
1107 if not os.path.exists(tgtdir):
1110 if not tgt.endswith(os.sep):
1111 fp = open(tgt, 'wb')
1112 fp.write(zf.read(path))
1116 def detect_ip_addr():
1117 """Try a very crude method to figure out a valid external
1118 IP or hostname for the current machine. Don't rely on this
1119 for binding to an interface, but it could be used as basis
1120 for constructing a remote URL to the server.
1122 def _detect_ip_addr():
1123 from array import array
1124 from struct import pack, unpack
1133 if not fcntl: # not UNIX:
1134 host = socket.gethostname()
1135 ip_addr = socket.gethostbyname(host)
1137 # get all interfaces:
1139 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1140 names = array('B', '\0' * nbytes)
1141 #print 'names: ', names
1142 outbytes = unpack('iL', fcntl.ioctl( s.fileno(), 0x8912, pack('iL', nbytes, names.buffer_info()[0])))[0]
1143 namestr = names.tostring()
1145 # try 64 bit kernel:
1146 for i in range(0, outbytes, 40):
1147 name = namestr[i:i+16].split('\0', 1)[0]
1149 ip_addr = socket.inet_ntoa(namestr[i+20:i+24])
1152 # try 32 bit kernel:
1154 ifaces = filter(None, [namestr[i:i+32].split('\0', 1)[0] for i in range(0, outbytes, 32)])
1156 for ifname in [iface for iface in ifaces if iface != 'lo']:
1157 ip_addr = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, pack('256s', ifname[:15]))[20:24])
1160 return ip_addr or 'localhost'
1163 ip_addr = _detect_ip_addr()
1165 ip_addr = 'localhost'
1168 # RATIONALE BEHIND TIMESTAMP CALCULATIONS AND TIMEZONE MANAGEMENT:
1169 # The server side never does any timestamp calculation, always
1170 # sends them in a naive (timezone agnostic) format supposed to be
1171 # expressed within the server timezone, and expects the clients to
1172 # provide timestamps in the server timezone as well.
1173 # It stores all timestamps in the database in naive format as well,
1174 # which also expresses the time in the server timezone.
1175 # For this reason the server makes its timezone name available via the
1176 # common/timezone_get() rpc method, which clients need to read
1177 # to know the appropriate time offset to use when reading/writing
1179 def get_win32_timezone():
1180 """Attempt to return the "standard name" of the current timezone on a win32 system.
1181 @return the standard name of the current win32 timezone, or False if it cannot be found.
1184 if (sys.platform == "win32"):
1187 hklm = _winreg.ConnectRegistry(None,_winreg.HKEY_LOCAL_MACHINE)
1188 current_tz_key = _winreg.OpenKey(hklm, r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", 0,_winreg.KEY_ALL_ACCESS)
1189 res = str(_winreg.QueryValueEx(current_tz_key,"StandardName")[0]) # [0] is value, [1] is type code
1190 _winreg.CloseKey(current_tz_key)
1191 _winreg.CloseKey(hklm)
1196 def detect_server_timezone():
1197 """Attempt to detect the timezone to use on the server side.
1198 Defaults to UTC if no working timezone can be found.
1199 @return the timezone identifier as expected by pytz.timezone.
1204 loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_WARNING,
1205 "Python pytz module is not available. Timezone will be set to UTC by default.")
1208 # Option 1: the configuration option (did not exist before, so no backwards compatibility issue)
1209 # Option 2: to be backwards compatible with 5.0 or earlier, the value from time.tzname[0], but only if it is known to pytz
1210 # Option 3: the environment variable TZ
1211 sources = [ (config['timezone'], 'OpenERP configuration'),
1212 (time.tzname[0], 'time.tzname'),
1213 (os.environ.get('TZ',False),'TZ environment variable'), ]
1214 # Option 4: OS-specific: /etc/timezone on Unix
1215 if (os.path.exists("/etc/timezone")):
1218 f = open("/etc/timezone")
1219 tz_value = f.read(128).strip()
1224 sources.append((tz_value,"/etc/timezone file"))
1225 # Option 5: timezone info from registry on Win32
1226 if (sys.platform == "win32"):
1227 # Timezone info is stored in windows registry.
1228 # However this is not likely to work very well as the standard name
1229 # of timezones in windows is rarely something that is known to pytz.
1230 # But that's ok, it is always possible to use a config option to set
1232 sources.append((get_win32_timezone(),"Windows Registry"))
1234 for (value,source) in sources:
1237 tz = pytz.timezone(value)
1238 loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_INFO,
1239 "Using timezone %s obtained from %s." % (tz.zone,source))
1241 except pytz.UnknownTimeZoneError:
1242 loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_WARNING,
1243 "The timezone specified in %s (%s) is invalid, ignoring it." % (source,value))
1245 loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_WARNING,
1246 "No valid timezone could be detected, using default UTC timezone. You can specify it explicitly with option 'timezone' in the server configuration.")
1249 def get_server_timezone():
1250 # timezone detection is safe in multithread, so lazy init is ok here
1251 if (not config['timezone']):
1252 config['timezone'] = detect_server_timezone()
1253 return config['timezone']
1256 DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"
1257 DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S"
1258 DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % (
1259 DEFAULT_SERVER_DATE_FORMAT,
1260 DEFAULT_SERVER_TIME_FORMAT)
1262 # Python's strftime supports only the format directives
1263 # that are available on the platform's libc, so in order to
1264 # be cross-platform we map to the directives required by
1265 # the C standard (1989 version), always available on platforms
1266 # with a C standard implementation.
1267 DATETIME_FORMATS_MAP = {
1269 '%D': '%m/%d/%Y', # modified %y->%Y
1271 '%E': '', # special modifier
1273 '%g': '%Y', # modified %y->%Y
1279 '%O': '', # special modifier
1282 '%r': '%I:%M:%S %p',
1283 '%s': '', #num of seconds since epoch
1288 '%y': '%Y', # Even if %y works, it's ambiguous, so we should use %Y
1289 '%+': '%Y-%m-%d %H:%M:%S',
1291 # %Z is a special case that causes 2 problems at least:
1292 # - the timezone names we use (in res_user.context_tz) come
1293 # from pytz, but not all these names are recognized by
1294 # strptime(), so we cannot convert in both directions
1295 # when such a timezone is selected and %Z is in the format
1296 # - %Z is replaced by an empty string in strftime() when
1297 # there is not tzinfo in a datetime value (e.g when the user
1298 # did not pick a context_tz). The resulting string does not
1299 # parse back if the format requires %Z.
1300 # As a consequence, we strip it completely from format strings.
1301 # The user can always have a look at the context_tz in
1302 # preferences to check the timezone.
1307 def server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_name,
1308 tz_offset=True, ignore_unparsable_time=True):
1310 Convert a source timestamp string into a destination timestamp string, attempting to apply the
1311 correct offset if both the server and local timezone are recognized, or no
1312 offset at all if they aren't or if tz_offset is false (i.e. assuming they are both in the same TZ).
1314 WARNING: This method is here to allow formatting dates correctly for inclusion in strings where
1315 the client would not be able to format/offset it correctly. DO NOT use it for returning
1316 date fields directly, these are supposed to be handled by the client!!
1318 @param src_tstamp_str: the str value containing the timestamp in the server timezone.
1319 @param src_format: the format to use when parsing the server timestamp.
1320 @param dst_format: the format to use when formatting the resulting timestamp for the local/client timezone.
1321 @param dst_tz_name: name of the destination timezone (such as the 'tz' value of the client context)
1322 @param ignore_unparsable_time: if True, return False if src_tstamp_str cannot be parsed
1323 using src_format or formatted using dst_format.
1325 @return local/client formatted timestamp, expressed in the local/client timezone if possible
1326 and if tz_offset is true, or src_tstamp_str if timezone offset could not be determined.
1328 if not src_tstamp_str:
1331 res = src_tstamp_str
1332 if src_format and dst_format:
1333 # find out server timezone
1334 server_tz = get_server_timezone()
1336 # dt_value needs to be a datetime.datetime object (so no time.struct_time or mx.DateTime.DateTime here!)
1337 dt_value = datetime.strptime(src_tstamp_str, src_format)
1338 if tz_offset and dst_tz_name:
1341 src_tz = pytz.timezone(server_tz)
1342 dst_tz = pytz.timezone(dst_tz_name)
1343 src_dt = src_tz.localize(dt_value, is_dst=True)
1344 dt_value = src_dt.astimezone(dst_tz)
1347 res = dt_value.strftime(dst_format)
1349 # Normal ways to end up here are if strptime or strftime failed
1350 if not ignore_unparsable_time:
1355 def split_every(n, iterable, piece_maker=tuple):
1356 """Splits an iterable into length-n pieces. The last piece will be shorter
1357 if ``n`` does not evenly divide the iterable length.
1358 @param ``piece_maker``: function to build the pieces
1359 from the slices (tuple,list,...)
1361 iterator = iter(iterable)
1362 piece = piece_maker(islice(iterator, n))
1365 piece = piece_maker(islice(iterator, n))
1367 if __name__ == '__main__':
1371 class upload_data_thread(threading.Thread):
1372 def __init__(self, email, data, type):
1373 self.args = [('email',email),('type',type),('data',data)]
1374 super(upload_data_thread,self).__init__()
1378 args = urllib.urlencode(self.args)
1379 fp = urllib.urlopen('http://www.openerp.com/scripts/survey.php', args)
1385 def upload_data(email, data, type='SURVEY'):
1386 a = upload_data_thread(email, data, type)
1391 # port of python 2.6's attrgetter with support for dotted notation
1392 def resolve_attr(obj, attr):
1393 for name in attr.split("."):
1394 obj = getattr(obj, name)
1397 def attrgetter(*items):
1401 return resolve_attr(obj, attr)
1404 return tuple(resolve_attr(obj, attr) for attr in items)
1408 """A subclass of str that implements repr() without enclosing quotation marks
1409 or escaping, keeping the original string untouched. The name come from Lisp's unquote.
1410 One of the uses for this is to preserve or insert bare variable names within dicts during eval()
1411 of a dict's repr(). Use with care.
1413 Some examples (notice that there are never quotes surrounding
1414 the ``active_id`` name:
1416 >>> unquote('active_id')
1418 >>> d = {'test': unquote('active_id')}
1427 class UnquoteEvalContext(defaultdict):
1428 """Defaultdict-based evaluation context that returns
1429 an ``unquote`` string for any missing name used during
1431 Mostly useful for evaluating OpenERP domains/contexts that
1432 may refer to names that are unknown at the time of eval,
1433 so that when the context/domain is converted back to a string,
1434 the original names are preserved.
1436 **Warning**: using an ``UnquoteEvalContext`` as context for ``eval()`` or
1437 ``safe_eval()`` will shadow the builtins, which may cause other
1438 failures, depending on what is evaluated.
1440 Example (notice that ``section_id`` is preserved in the final
1443 >>> context_str = "{'default_user_id': uid, 'default_section_id': section_id}"
1444 >>> eval(context_str, UnquoteEvalContext(uid=1))
1445 {'default_user_id': 1, 'default_section_id': section_id}
1448 def __init__(self, *args, **kwargs):
1449 super(UnquoteEvalContext, self).__init__(None, *args, **kwargs)
1451 def __missing__(self, key):
1454 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: