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 _logger = logging.getLogger('tools')
64 # List of etree._Element subclasses that we choose to ignore when parsing XML.
65 # We include the *Base ones just in case, currently they seem to be subclasses of the _* ones.
66 SKIPPED_ELEMENT_TYPES = (etree._Comment, etree._ProcessingInstruction, etree.CommentBase, etree.PIBase)
68 # initialize a database with base/base.sql
70 import openerp.addons as addons
71 f = addons.get_module_resource('base', 'base.sql')
72 base_sql_file = file_open(f)
74 cr.execute(base_sql_file.read())
79 for i in addons.get_modules():
80 mod_path = addons.get_module_path(i)
84 info = addons.load_information_from_description_file(i)
88 categs = info.get('category', 'Uncategorized').split('/')
92 cr.execute('SELECT id \
93 FROM ir_module_category \
94 WHERE name=%s AND parent_id=%s', (categs[0], p_id))
96 cr.execute('SELECT id \
97 FROM ir_module_category \
98 WHERE name=%s AND parent_id IS NULL', (categs[0],))
101 cr.execute('INSERT INTO ir_module_category \
103 VALUES (%s, %s) RETURNING id', (categs[0], p_id))
104 c_id = cr.fetchone()[0]
110 active = info.get('active', False)
111 installable = info.get('installable', True)
116 state = 'uninstalled'
118 state = 'uninstallable'
119 cr.execute('INSERT INTO ir_module_module \
120 (author, website, name, shortdesc, description, \
121 category_id, state, certificate, web, license) \
122 VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id', (
123 info.get('author', ''),
124 info.get('website', ''), i, info.get('name', False),
125 info.get('description', ''), p_id, state, info.get('certificate') or None,
126 info.get('web') or False,
127 info.get('license') or 'AGPL-3'))
128 id = cr.fetchone()[0]
129 cr.execute('INSERT INTO ir_model_data \
130 (name,model,module, res_id, noupdate) VALUES (%s,%s,%s,%s,%s)', (
131 'module_meta_information', 'ir.module.module', i, id, True))
132 dependencies = info.get('depends', [])
133 for d in dependencies:
134 cr.execute('INSERT INTO ir_module_module_dependency \
135 (module_id,name) VALUES (%s, %s)', (id, d))
138 def find_in_path(name):
144 def find_pg_tool(name):
146 if config['pg_path'] and config['pg_path'] != 'None':
147 path = config['pg_path']
149 return which(name, path=path)
153 def exec_pg_command(name, *args):
154 prog = find_pg_tool(name)
156 raise Exception('Couldn\'t find %s' % name)
157 args2 = (prog,) + args
159 return subprocess.call(args2)
161 def exec_pg_command_pipe(name, *args):
162 prog = find_pg_tool(name)
164 raise Exception('Couldn\'t find %s' % name)
165 # on win32, passing close_fds=True is not compatible
166 # with redirecting std[in/err/out]
167 pop = subprocess.Popen((prog,) + args, bufsize= -1,
168 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
169 close_fds=(os.name=="posix"))
170 return (pop.stdin, pop.stdout)
172 def exec_command_pipe(name, *args):
173 prog = find_in_path(name)
175 raise Exception('Couldn\'t find %s' % name)
176 # on win32, passing close_fds=True is not compatible
177 # with redirecting std[in/err/out]
178 pop = subprocess.Popen((prog,) + args, bufsize= -1,
179 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
180 close_fds=(os.name=="posix"))
181 return (pop.stdin, pop.stdout)
183 #----------------------------------------------------------
185 #----------------------------------------------------------
186 #file_path_root = os.getcwd()
187 #file_path_addons = os.path.join(file_path_root, 'addons')
189 def file_open(name, mode="r", subdir='addons', pathinfo=False):
190 """Open a file from the OpenERP root, using a subdir folder.
192 >>> file_open('hr/report/timesheer.xsl')
193 >>> file_open('addons/hr/report/timesheet.xsl')
194 >>> file_open('../../base/report/rml_template.xsl', subdir='addons/hr/report', pathinfo=True)
196 @param name: name of the file
197 @param mode: file open mode
198 @param subdir: subdirectory
199 @param pathinfo: if True returns tupple (fileobject, filepath)
201 @return: fileobject if pathinfo is False else (fileobject, filepath)
203 import openerp.addons as addons
204 adps = addons.ad_paths
205 rtp = os.path.normcase(os.path.abspath(config['root_path']))
207 if name.replace(os.path.sep, '/').startswith('addons/'):
211 # First try to locate in addons_path
214 if subdir2.replace(os.path.sep, '/').startswith('addons/'):
215 subdir2 = subdir2[7:]
217 subdir2 = (subdir2 != 'addons' or None) and subdir2
222 fn = os.path.join(adp, subdir2, name)
224 fn = os.path.join(adp, name)
225 fn = os.path.normpath(fn)
226 fo = file_open(fn, mode=mode, subdir=None, pathinfo=pathinfo)
234 name = os.path.join(rtp, subdir, name)
236 name = os.path.join(rtp, name)
238 name = os.path.normpath(name)
240 # Check for a zipfile in the path
245 head, tail = os.path.split(head)
249 zipname = os.path.join(tail, zipname)
252 if zipfile.is_zipfile(head+'.zip'):
253 from cStringIO import StringIO
254 zfile = zipfile.ZipFile(head+'.zip')
257 fo.write(zfile.read(os.path.join(
258 os.path.basename(head), zipname).replace(
265 name2 = os.path.normpath(os.path.join(head + '.zip', zipname))
267 for i in (name2, name):
268 if i and os.path.isfile(i):
273 if os.path.splitext(name)[1] == '.rml':
274 raise IOError, 'Report %s doesn\'t exist or deleted : ' %str(name)
275 raise IOError, 'File not found : %s' % name
278 #----------------------------------------------------------
280 #----------------------------------------------------------
282 """Flatten a list of elements into a uniqu list
283 Author: Christophe Simonis (christophe@tinyerp.com)
292 >>> flatten( [[], [[]]] )
294 >>> flatten( [[['a','b'], 'c'], 'd', ['e', [], 'f']] )
295 ['a', 'b', 'c', 'd', 'e', 'f']
296 >>> t = (1,2,(3,), [4, 5, [6, [7], (8, 9), ([10, 11, (12, 13)]), [14, [], (15,)], []]])
298 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
302 return hasattr(x, "__iter__")
307 map(r.append, flatten(e))
312 def reverse_enumerate(l):
313 """Like enumerate but in the other sens
314 >>> a = ['a', 'b', 'c']
315 >>> it = reverse_enumerate(a)
323 Traceback (most recent call last):
324 File "<stdin>", line 1, in <module>
327 return izip(xrange(len(l)-1, -1, -1), reversed(l))
329 #----------------------------------------------------------
331 #----------------------------------------------------------
332 email_re = re.compile(r"""
333 ([a-zA-Z][\w\.-]*[a-zA-Z0-9] # username part
335 [a-zA-Z0-9][\w\.-]* # domain must start with a letter ... Ged> why do we include a 0-9 then?
340 res_re = re.compile(r"\[([0-9]+)\]", re.UNICODE)
341 command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE)
342 reference_re = re.compile("<.*-openobject-(\\d+)@(.*)>", re.UNICODE)
352 def html2plaintext(html, body_id=None, encoding='utf-8'):
353 ## (c) Fry-IT, www.fry-it.com, 2007
354 ## <peter@fry-it.com>
355 ## download here: http://www.peterbe.com/plog/html2plaintext
358 """ from an HTML text, convert the HTML to plain text.
359 If @body_id is provided then this is the tag where the
360 body (not necessarily <body>) starts.
365 from lxml.etree import tostring
367 from lxml.html.soupparser import fromstring
370 _logger.debug('tools.misc.html2plaintext: cannot use BeautifulSoup, fallback to lxml.etree.HTMLParser')
371 from lxml.etree import fromstring, HTMLParser
372 kwargs = dict(parser=HTMLParser())
374 tree = fromstring(html, **kwargs)
376 if body_id is not None:
377 source = tree.xpath('//*[@id=%s]'%(body_id,))
379 source = tree.xpath('//body')
385 for link in tree.findall('.//a'):
386 url = link.get('href')
390 link.text = '%s [%s]' % (link.text, i)
391 url_index.append(url)
393 html = ustr(tostring(tree, encoding=encoding))
395 html = html.replace('<strong>','*').replace('</strong>','*')
396 html = html.replace('<b>','*').replace('</b>','*')
397 html = html.replace('<h3>','*').replace('</h3>','*')
398 html = html.replace('<h2>','**').replace('</h2>','**')
399 html = html.replace('<h1>','**').replace('</h1>','**')
400 html = html.replace('<em>','/').replace('</em>','/')
401 html = html.replace('<tr>', '\n')
402 html = html.replace('</p>', '\n')
403 html = re.sub('<br\s*/?>', '\n', html)
404 html = re.sub('<.*?>', ' ', html)
405 html = html.replace(' ' * 2, ' ')
408 html = '\n'.join([x.strip() for x in html.splitlines()])
409 html = html.replace('\n' * 2, '\n')
411 for i, url in enumerate(url_index):
414 html += ustr('[%s] %s\n') % (i+1, url)
418 def generate_tracking_message_id(openobject_id):
419 """Returns a string that can be used in the Message-ID RFC822 header field so we
420 can track the replies related to a given object thanks to the "In-Reply-To" or
421 "References" fields that Mail User Agents will set.
423 return "<%s-openobject-%s@%s>" % (time.time(), openobject_id, socket.gethostname())
425 def _email_send(smtp_from, smtp_to_list, message, openobject_id=None, ssl=False, debug=False):
426 """Low-level method to send directly a Message through the configured smtp server.
427 :param smtp_from: RFC-822 envelope FROM (not displayed to recipient)
428 :param smtp_to_list: RFC-822 envelope RCPT_TOs (not displayed to recipient)
429 :param message: an email.message.Message to send
430 :param debug: True if messages should be output to stderr before being sent,
431 and smtplib.SMTP put into debug mode.
432 :return: True if the mail was delivered successfully to the smtp,
433 else False (+ exception logged)
435 class WriteToLogger(object):
437 self.logger = loglevels.Logger()
440 self.logger.notifyChannel('email_send', loglevels.LOG_DEBUG, s)
443 message['Message-Id'] = generate_tracking_message_id(openobject_id)
446 smtp_server = config['smtp_server']
448 if smtp_server.startswith('maildir:/'):
449 from mailbox import Maildir
450 maildir_path = smtp_server[8:]
451 mdir = Maildir(maildir_path,factory=None, create = True)
452 mdir.add(message.as_string(True))
455 oldstderr = smtplib.stderr
456 if not ssl: ssl = config.get('smtp_ssl', False)
459 # in case of debug, the messages are printed to stderr.
461 smtplib.stderr = WriteToLogger()
463 s.set_debuglevel(int(bool(debug))) # 0 or 1
464 s.connect(smtp_server, config['smtp_port'])
470 if config['smtp_user'] or config['smtp_password']:
471 s.login(config['smtp_user'], config['smtp_password'])
473 s.sendmail(smtp_from, smtp_to_list, message.as_string())
478 smtplib.stderr = oldstderr
480 # ignored, just a consequence of the previous exception
484 _logger.error('could not deliver email', exc_info=True)
490 def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
491 attach=None, openobject_id=False, ssl=False, debug=False, subtype='plain', x_headers=None, priority='3'):
497 `email_from`: A string used to fill the `From` header, if falsy,
498 config['email_from'] is used instead. Also used for
499 the `Reply-To` header if `reply_to` is not provided
501 `email_to`: a sequence of addresses to send the mail to.
503 if x_headers is None:
507 if not (email_from or config['email_from']):
508 raise ValueError("Sending an email requires either providing a sender "
509 "address or having configured one")
511 if not email_from: email_from = config.get('email_from', False)
512 email_from = ustr(email_from).encode('utf-8')
514 if not email_cc: email_cc = []
515 if not email_bcc: email_bcc = []
516 if not body: body = u''
518 email_body = ustr(body).encode('utf-8')
519 email_text = MIMEText(email_body or '',_subtype=subtype,_charset='utf-8')
521 msg = MIMEMultipart()
523 msg['Subject'] = Header(ustr(subject), 'utf-8')
524 msg['From'] = email_from
527 msg['Reply-To'] = reply_to
529 msg['Reply-To'] = msg['From']
530 msg['To'] = COMMASPACE.join(email_to)
532 msg['Cc'] = COMMASPACE.join(email_cc)
534 msg['Bcc'] = COMMASPACE.join(email_bcc)
535 msg['Date'] = formatdate(localtime=True)
537 msg['X-Priority'] = priorities.get(priority, '3 (Normal)')
539 # Add dynamic X Header
540 for key, value in x_headers.iteritems():
541 msg['%s' % key] = str(value)
543 if html2text and subtype == 'html':
544 text = html2text(email_body.decode('utf-8')).encode('utf-8')
545 alternative_part = MIMEMultipart(_subtype="alternative")
546 alternative_part.attach(MIMEText(text, _charset='utf-8', _subtype='plain'))
547 alternative_part.attach(email_text)
548 msg.attach(alternative_part)
550 msg.attach(email_text)
553 for (fname,fcontent) in attach:
554 part = MIMEBase('application', "octet-stream")
555 part.set_payload( fcontent )
556 Encoders.encode_base64(part)
557 part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,))
560 return _email_send(email_from, flatten([email_to, email_cc, email_bcc]), msg, openobject_id=openobject_id, ssl=ssl, debug=debug)
562 #----------------------------------------------------------
564 #----------------------------------------------------------
565 # text must be latin-1 encoded
566 def sms_send(user, password, api_id, text, to):
568 url = "http://api.urlsms.com/SendSMS.aspx"
569 #url = "http://196.7.150.220/http/sendmsg"
570 params = urllib.urlencode({'UserID': user, 'Password': password, 'SenderID': api_id, 'MsgText': text, 'RecipientMobileNo':to})
571 urllib.urlopen(url+"?"+params)
572 # FIXME: Use the logger if there is an error
575 #---------------------------------------------------------
576 # Class that stores an updateable string (used in wizards)
577 #---------------------------------------------------------
578 class UpdateableStr(local):
580 def __init__(self, string=''):
584 return str(self.string)
587 return str(self.string)
589 def __nonzero__(self):
590 return bool(self.string)
593 class UpdateableDict(local):
594 '''Stores an updateable dict to use in wizards'''
596 def __init__(self, dict=None):
602 return str(self.dict)
605 return str(self.dict)
608 return self.dict.clear()
611 return self.dict.keys()
613 def __setitem__(self, i, y):
614 self.dict.__setitem__(i, y)
616 def __getitem__(self, i):
617 return self.dict.__getitem__(i)
620 return self.dict.copy()
623 return self.dict.iteritems()
626 return self.dict.iterkeys()
628 def itervalues(self):
629 return self.dict.itervalues()
631 def pop(self, k, d=None):
632 return self.dict.pop(k, d)
635 return self.dict.popitem()
637 def setdefault(self, k, d=None):
638 return self.dict.setdefault(k, d)
640 def update(self, E, **F):
641 return self.dict.update(E, F)
644 return self.dict.values()
646 def get(self, k, d=None):
647 return self.dict.get(k, d)
649 def has_key(self, k):
650 return self.dict.has_key(k)
653 return self.dict.items()
655 def __cmp__(self, y):
656 return self.dict.__cmp__(y)
658 def __contains__(self, k):
659 return self.dict.__contains__(k)
661 def __delitem__(self, y):
662 return self.dict.__delitem__(y)
665 return self.dict.__eq__(y)
668 return self.dict.__ge__(y)
671 return self.dict.__gt__(y)
674 return self.dict.__hash__()
677 return self.dict.__iter__()
680 return self.dict.__le__(y)
683 return self.dict.__len__()
686 return self.dict.__lt__(y)
689 return self.dict.__ne__(y)
692 # Don't use ! Use res.currency.round()
693 class currency(float):
695 def __init__(self, value, accuracy=2, rounding=None):
697 rounding=10**-accuracy
698 self.rounding=rounding
699 self.accuracy=accuracy
701 def __new__(cls, value, accuracy=2, rounding=None):
702 return float.__new__(cls, round(value, accuracy))
705 # display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
706 # return str(display_value)
718 Use it as a decorator of the function you plan to cache
719 Timeout: 0 = no timeout, otherwise in seconds
724 def __init__(self, timeout=None, skiparg=2, multi=None, size=8192):
725 assert skiparg >= 2 # at least self and cr
727 self.timeout = config['cache_timeout']
729 self.timeout = timeout
730 self.skiparg = skiparg
732 self.lasttime = time.time()
733 self.cache = LRU(size) # TODO take size from config
735 cache.__caches.append(self)
738 def _generate_keys(self, dbname, kwargs2):
740 Generate keys depending of the arguments and the self.mutli value
745 pairs.sort(key=lambda (k,v): k)
746 for i, (k, v) in enumerate(pairs):
747 if isinstance(v, dict):
748 pairs[i] = (k, to_tuple(v))
749 if isinstance(v, (list, set)):
750 pairs[i] = (k, tuple(v))
751 elif not is_hashable(v):
752 pairs[i] = (k, repr(v))
756 key = (('dbname', dbname),) + to_tuple(kwargs2)
759 multis = kwargs2[self.multi][:]
761 kwargs2[self.multi] = (id,)
762 key = (('dbname', dbname),) + to_tuple(kwargs2)
765 def _unify_args(self, *args, **kwargs):
766 # Update named arguments with positional argument values (without self and cr)
767 kwargs2 = self.fun_default_values.copy()
768 kwargs2.update(kwargs)
769 kwargs2.update(dict(zip(self.fun_arg_names, args[self.skiparg-2:])))
772 def clear(self, dbname, *args, **kwargs):
773 """clear the cache for database dbname
774 if *args and **kwargs are both empty, clear all the keys related to this database
776 if not args and not kwargs:
777 keys_to_del = [key for key in self.cache.keys() if key[0][1] == dbname]
779 kwargs2 = self._unify_args(*args, **kwargs)
780 keys_to_del = [key for key, _ in self._generate_keys(dbname, kwargs2) if key in self.cache.keys()]
782 for key in keys_to_del:
786 def clean_caches_for_db(cls, dbname):
787 for c in cls.__caches:
790 def __call__(self, fn):
791 if self.fun is not None:
792 raise Exception("Can not use a cache instance on more than one function")
795 argspec = inspect.getargspec(fn)
796 self.fun_arg_names = argspec[0][self.skiparg:]
797 self.fun_default_values = {}
799 self.fun_default_values = dict(zip(self.fun_arg_names[-len(argspec[3]):], argspec[3]))
801 def cached_result(self2, cr, *args, **kwargs):
802 if time.time()-int(self.timeout) > self.lasttime:
803 self.lasttime = time.time()
804 t = time.time()-int(self.timeout)
805 old_keys = [key for key in self.cache.keys() if self.cache[key][1] < t]
809 kwargs2 = self._unify_args(*args, **kwargs)
813 for key, id in self._generate_keys(cr.dbname, kwargs2):
814 if key in self.cache:
815 result[id] = self.cache[key][0]
821 kwargs2[self.multi] = notincache.keys()
823 result2 = fn(self2, cr, *args[:self.skiparg-2], **kwargs2)
825 key = notincache[None]
826 self.cache[key] = (result2, time.time())
827 result[None] = result2
831 self.cache[key] = (result2[id], time.time())
832 result.update(result2)
838 cached_result.clear_cache = self.clear
842 return s.replace('&','&').replace('<','<').replace('>','>')
844 def get_encodings(hint_encoding='utf-8'):
847 'iso-8859-1': 'iso8859-15',
852 if hint_encoding.lower() in fallbacks:
853 yield fallbacks[hint_encoding.lower()]
855 # some defaults (also taking care of pure ASCII)
856 for charset in ['utf8','latin1']:
857 if not (hint_encoding) or (charset.lower() != hint_encoding.lower()):
860 from locale import getpreferredencoding
861 prefenc = getpreferredencoding()
862 if prefenc and prefenc.lower() != 'utf-8':
864 prefenc = fallbacks.get(prefenc.lower())
869 def ustr(value, hint_encoding='utf-8'):
870 """This method is similar to the builtin `str` method, except
871 it will return unicode() string.
873 @param value: the value to convert
874 @param hint_encoding: an optional encoding that was detected
875 upstream and should be tried first to
879 @return: unicode string
881 if isinstance(value, Exception):
882 return exception_to_unicode(value)
884 if isinstance(value, unicode):
887 if not isinstance(value, basestring):
889 return unicode(value)
891 raise UnicodeError('unable to convert %r' % (value,))
893 for ln in get_encodings(hint_encoding):
895 return unicode(value, ln)
898 raise UnicodeError('unable to convert %r' % (value,))
901 def exception_to_unicode(e):
902 if (sys.version_info[:2] < (2,6)) and hasattr(e, 'message'):
903 return ustr(e.message)
904 if hasattr(e, 'args'):
905 return "\n".join((ustr(a) for a in e.args))
909 return u"Unknown message"
912 # to be compatible with python 2.4
914 if not hasattr(__builtin__, 'all'):
916 for element in iterable:
921 __builtin__.all = all
924 if not hasattr(__builtin__, 'any'):
926 for element in iterable:
931 __builtin__.any = any
934 def get_iso_codes(lang):
935 if lang.find('_') != -1:
936 if lang.split('_')[0] == lang.split('_')[1].lower():
937 lang = lang.split('_')[0]
941 # The codes below are those from Launchpad's Rosetta, with the exception
942 # of some trivial codes where the Launchpad code is xx and we have xx_XX.
944 'ab_RU': u'Abkhazian / аҧсуа',
945 'ar_AR': u'Arabic / الْعَرَبيّة',
946 'bg_BG': u'Bulgarian / български език',
947 'bs_BS': u'Bosnian / bosanski jezik',
948 'ca_ES': u'Catalan / Català',
949 'cs_CZ': u'Czech / Čeština',
950 'da_DK': u'Danish / Dansk',
951 'de_DE': u'German / Deutsch',
952 'el_GR': u'Greek / Ελληνικά',
953 'en_CA': u'English (CA)',
954 'en_GB': u'English (UK)',
955 'en_US': u'English (US)',
956 'es_AR': u'Spanish (AR) / Español (AR)',
957 'es_BO': u'Spanish (BO) / Español (BO)',
958 'es_CL': u'Spanish (CL) / Español (CL)',
959 'es_CO': u'Spanish (CO) / Español (CO)',
960 'es_CR': u'Spanish (CR) / Español (CR)',
961 'es_DO': u'Spanish (DO) / Español (DO)',
962 'es_EC': u'Spanish (EC) / Español (EC)',
963 'es_ES': u'Spanish / Español',
964 'es_GT': u'Spanish (GT) / Español (GT)',
965 'es_HN': u'Spanish (HN) / Español (HN)',
966 'es_MX': u'Spanish (MX) / Español (MX)',
967 'es_NI': u'Spanish (NI) / Español (NI)',
968 'es_PA': u'Spanish (PA) / Español (PA)',
969 'es_PE': u'Spanish (PE) / Español (PE)',
970 'es_PR': u'Spanish (PR) / Español (PR)',
971 'es_PY': u'Spanish (PY) / Español (PY)',
972 'es_SV': u'Spanish (SV) / Español (SV)',
973 'es_UY': u'Spanish (UY) / Español (UY)',
974 'es_VE': u'Spanish (VE) / Español (VE)',
975 'et_EE': u'Estonian / Eesti keel',
976 'fa_IR': u'Persian / فارس',
977 'fi_FI': u'Finnish / Suomi',
978 'fr_BE': u'French (BE) / Français (BE)',
979 'fr_CH': u'French (CH) / Français (CH)',
980 'fr_FR': u'French / Français',
981 'gl_ES': u'Galician / Galego',
982 'gu_IN': u'Gujarati / ગુજરાતી',
983 'he_IL': u'Hebrew / עִבְרִי',
984 'hi_IN': u'Hindi / हिंदी',
985 'hr_HR': u'Croatian / hrvatski jezik',
986 'hu_HU': u'Hungarian / Magyar',
987 'id_ID': u'Indonesian / Bahasa Indonesia',
988 'it_IT': u'Italian / Italiano',
989 'iu_CA': u'Inuktitut / ᐃᓄᒃᑎᑐᑦ',
990 'ja_JP': u'Japanese / 日本語',
991 'ko_KP': u'Korean (KP) / 한국어 (KP)',
992 'ko_KR': u'Korean (KR) / 한국어 (KR)',
993 'lt_LT': u'Lithuanian / Lietuvių kalba',
994 'lv_LV': u'Latvian / latviešu valoda',
995 'ml_IN': u'Malayalam / മലയാളം',
996 'mn_MN': u'Mongolian / монгол',
997 'nb_NO': u'Norwegian Bokmål / Norsk bokmål',
998 'nl_NL': u'Dutch / Nederlands',
999 'nl_BE': u'Flemish (BE) / Vlaams (BE)',
1000 'oc_FR': u'Occitan (FR, post 1500) / Occitan',
1001 'pl_PL': u'Polish / Język polski',
1002 'pt_BR': u'Portugese (BR) / Português (BR)',
1003 'pt_PT': u'Portugese / Português',
1004 'ro_RO': u'Romanian / română',
1005 'ru_RU': u'Russian / русский язык',
1006 'si_LK': u'Sinhalese / සිංහල',
1007 'sl_SI': u'Slovenian / slovenščina',
1008 'sk_SK': u'Slovak / Slovenský jazyk',
1009 'sq_AL': u'Albanian / Shqip',
1010 'sr_RS': u'Serbian (Cyrillic) / српски',
1011 'sr@latin': u'Serbian (Latin) / srpski',
1012 'sv_SE': u'Swedish / svenska',
1013 'te_IN': u'Telugu / తెలుగు',
1014 'tr_TR': u'Turkish / Türkçe',
1015 'vi_VN': u'Vietnamese / Tiếng Việt',
1016 'uk_UA': u'Ukrainian / українська',
1017 'ur_PK': u'Urdu / اردو',
1018 'zh_CN': u'Chinese (CN) / 简体中文',
1019 'zh_HK': u'Chinese (HK)',
1020 'zh_TW': u'Chinese (TW) / 正體字',
1021 'th_TH': u'Thai / ภาษาไทย',
1022 'tlh_TLH': u'Klingon',
1026 def scan_languages():
1027 # Now it will take all languages from get languages function without filter it with base module languages
1028 lang_dict = get_languages()
1029 ret = [(lang, lang_dict.get(lang, lang)) for lang in list(lang_dict)]
1030 ret.sort(key=lambda k:k[1])
1034 def get_user_companies(cr, user):
1035 def _get_company_children(cr, ids):
1038 cr.execute('SELECT id FROM res_company WHERE parent_id IN %s', (tuple(ids),))
1039 res = [x[0] for x in cr.fetchall()]
1040 res.extend(_get_company_children(cr, res))
1042 cr.execute('SELECT company_id FROM res_users WHERE id=%s', (user,))
1043 user_comp = cr.fetchone()[0]
1046 return [user_comp] + _get_company_children(cr, [user_comp])
1050 Input number : account or invoice number
1051 Output return: the same number completed with the recursive mod10
1054 codec=[0,9,4,6,8,2,7,1,3,5]
1057 for digit in number:
1060 report = codec[ (int(digit) + report) % 10 ]
1061 return result + str((10 - report) % 10)
1066 Return the size in a human readable format
1070 units = ('bytes', 'Kb', 'Mb', 'Gb')
1071 if isinstance(sz,basestring):
1074 while s >= 1024 and i < len(units)-1:
1077 return "%0.2f %s" % (s, units[i])
1080 from func import wraps
1083 def wrapper(*args, **kwargs):
1084 from pprint import pformat
1086 vector = ['Call -> function: %r' % f]
1087 for i, arg in enumerate(args):
1088 vector.append(' arg %02d: %s' % (i, pformat(arg)))
1089 for key, value in kwargs.items():
1090 vector.append(' kwarg %10s: %s' % (key, pformat(value)))
1092 timeb4 = time.time()
1093 res = f(*args, **kwargs)
1095 vector.append(' result: %s' % pformat(res))
1096 vector.append(' time delta: %s' % (time.time() - timeb4))
1097 loglevels.Logger().notifyChannel('logged', loglevels.LOG_DEBUG, '\n'.join(vector))
1102 class profile(object):
1103 def __init__(self, fname=None):
1106 def __call__(self, f):
1107 from func import wraps
1110 def wrapper(*args, **kwargs):
1111 class profile_wrapper(object):
1115 self.result = f(*args, **kwargs)
1116 pw = profile_wrapper()
1118 fname = self.fname or ("%s.cprof" % (f.func_name,))
1119 cProfile.runctx('pw()', globals(), locals(), filename=fname)
1126 This method allow you to debug your code without print
1128 >>> def func_foo(bar)
1131 ... qnx = (baz, bar)
1136 This will output on the logger:
1138 [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:baz = 42
1139 [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:qnx = (42, 42)
1141 To view the DEBUG lines in the logger you must start the server with the option
1145 warnings.warn("The tools.debug() method is deprecated, please use logging.",
1146 DeprecationWarning, stacklevel=2)
1147 from inspect import stack
1148 from pprint import pformat
1150 param = re.split("debug *\((.+)\)", st[4][0].strip())[1].strip()
1151 while param.count(')') > param.count('('): param = param[:param.rfind(')')]
1152 what = pformat(what)
1154 what = "%s = %s" % (param, what)
1155 logging.getLogger(st[3]).debug(what)
1158 __icons_list = ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
1159 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
1160 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
1161 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
1162 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
1163 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
1164 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
1165 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
1166 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
1167 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
1168 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
1169 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
1170 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
1171 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
1172 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
1173 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
1174 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
1175 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
1176 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
1177 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
1178 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
1179 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
1180 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
1181 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
1182 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
1183 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
1184 'terp-check','terp-go-month','terp-go-year','terp-go-today','terp-document-new','terp-camera_test',
1185 'terp-emblem-important','terp-gtk-media-pause','terp-gtk-stop','terp-gnome-cpu-frequency-applet+',
1186 'terp-dialog-close','terp-gtk-jump-to-rtl','terp-gtk-jump-to-ltr','terp-accessories-archiver',
1187 'terp-stock_align_left_24','terp-stock_effects-object-colorize','terp-go-home','terp-gtk-go-back-rtl',
1188 'terp-gtk-go-back-ltr','terp-personal','terp-personal-','terp-personal+','terp-accessories-archiver-minus',
1189 'terp-accessories-archiver+','terp-stock_symbol-selection','terp-call-start','terp-dolar',
1190 'terp-face-plain','terp-folder-blue','terp-folder-green','terp-folder-orange','terp-folder-yellow',
1191 'terp-gdu-smart-failing','terp-go-week','terp-gtk-select-all','terp-locked','terp-mail-forward',
1192 'terp-mail-message-new','terp-mail-replied','terp-rating-rated','terp-stage','terp-stock_format-scientific',
1193 'terp-dolar_ok!','terp-idea','terp-stock_format-default','terp-mail-','terp-mail_delete'
1196 def icons(*a, **kw):
1198 return [(x, x) for x in __icons_list ]
1200 def extract_zip_file(zip_file, outdirectory):
1201 zf = zipfile.ZipFile(zip_file, 'r')
1203 for path in zf.namelist():
1204 tgt = os.path.join(out, path)
1205 tgtdir = os.path.dirname(tgt)
1206 if not os.path.exists(tgtdir):
1209 if not tgt.endswith(os.sep):
1210 fp = open(tgt, 'wb')
1211 fp.write(zf.read(path))
1215 def detect_ip_addr():
1216 """Try a very crude method to figure out a valid external
1217 IP or hostname for the current machine. Don't rely on this
1218 for binding to an interface, but it could be used as basis
1219 for constructing a remote URL to the server.
1221 def _detect_ip_addr():
1222 from array import array
1223 from struct import pack, unpack
1232 if not fcntl: # not UNIX:
1233 host = socket.gethostname()
1234 ip_addr = socket.gethostbyname(host)
1236 # get all interfaces:
1238 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1239 names = array('B', '\0' * nbytes)
1240 #print 'names: ', names
1241 outbytes = unpack('iL', fcntl.ioctl( s.fileno(), 0x8912, pack('iL', nbytes, names.buffer_info()[0])))[0]
1242 namestr = names.tostring()
1244 # try 64 bit kernel:
1245 for i in range(0, outbytes, 40):
1246 name = namestr[i:i+16].split('\0', 1)[0]
1248 ip_addr = socket.inet_ntoa(namestr[i+20:i+24])
1251 # try 32 bit kernel:
1253 ifaces = filter(None, [namestr[i:i+32].split('\0', 1)[0] for i in range(0, outbytes, 32)])
1255 for ifname in [iface for iface in ifaces if iface != 'lo']:
1256 ip_addr = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, pack('256s', ifname[:15]))[20:24])
1259 return ip_addr or 'localhost'
1262 ip_addr = _detect_ip_addr()
1264 ip_addr = 'localhost'
1267 # RATIONALE BEHIND TIMESTAMP CALCULATIONS AND TIMEZONE MANAGEMENT:
1268 # The server side never does any timestamp calculation, always
1269 # sends them in a naive (timezone agnostic) format supposed to be
1270 # expressed within the server timezone, and expects the clients to
1271 # provide timestamps in the server timezone as well.
1272 # It stores all timestamps in the database in naive format as well,
1273 # which also expresses the time in the server timezone.
1274 # For this reason the server makes its timezone name available via the
1275 # common/timezone_get() rpc method, which clients need to read
1276 # to know the appropriate time offset to use when reading/writing
1278 def get_win32_timezone():
1279 """Attempt to return the "standard name" of the current timezone on a win32 system.
1280 @return: the standard name of the current win32 timezone, or False if it cannot be found.
1283 if (sys.platform == "win32"):
1286 hklm = _winreg.ConnectRegistry(None,_winreg.HKEY_LOCAL_MACHINE)
1287 current_tz_key = _winreg.OpenKey(hklm, r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", 0,_winreg.KEY_ALL_ACCESS)
1288 res = str(_winreg.QueryValueEx(current_tz_key,"StandardName")[0]) # [0] is value, [1] is type code
1289 _winreg.CloseKey(current_tz_key)
1290 _winreg.CloseKey(hklm)
1295 def detect_server_timezone():
1296 """Attempt to detect the timezone to use on the server side.
1297 Defaults to UTC if no working timezone can be found.
1298 @return: the timezone identifier as expected by pytz.timezone.
1303 loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_WARNING,
1304 "Python pytz module is not available. Timezone will be set to UTC by default.")
1307 # Option 1: the configuration option (did not exist before, so no backwards compatibility issue)
1308 # 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
1309 # Option 3: the environment variable TZ
1310 sources = [ (config['timezone'], 'OpenERP configuration'),
1311 (time.tzname[0], 'time.tzname'),
1312 (os.environ.get('TZ',False),'TZ environment variable'), ]
1313 # Option 4: OS-specific: /etc/timezone on Unix
1314 if (os.path.exists("/etc/timezone")):
1317 f = open("/etc/timezone")
1318 tz_value = f.read(128).strip()
1323 sources.append((tz_value,"/etc/timezone file"))
1324 # Option 5: timezone info from registry on Win32
1325 if (sys.platform == "win32"):
1326 # Timezone info is stored in windows registry.
1327 # However this is not likely to work very well as the standard name
1328 # of timezones in windows is rarely something that is known to pytz.
1329 # But that's ok, it is always possible to use a config option to set
1331 sources.append((get_win32_timezone(),"Windows Registry"))
1333 for (value,source) in sources:
1336 tz = pytz.timezone(value)
1337 loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_INFO,
1338 "Using timezone %s obtained from %s." % (tz.zone,source))
1340 except pytz.UnknownTimeZoneError:
1341 loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_WARNING,
1342 "The timezone specified in %s (%s) is invalid, ignoring it." % (source,value))
1344 loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_WARNING,
1345 "No valid timezone could be detected, using default UTC timezone. You can specify it explicitly with option 'timezone' in the server configuration.")
1348 def get_server_timezone():
1349 # timezone detection is safe in multithread, so lazy init is ok here
1350 if (not config['timezone']):
1351 config['timezone'] = detect_server_timezone()
1352 return config['timezone']
1355 DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"
1356 DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S"
1357 DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % (
1358 DEFAULT_SERVER_DATE_FORMAT,
1359 DEFAULT_SERVER_TIME_FORMAT)
1361 # Python's strftime supports only the format directives
1362 # that are available on the platform's libc, so in order to
1363 # be cross-platform we map to the directives required by
1364 # the C standard (1989 version), always available on platforms
1365 # with a C standard implementation.
1366 DATETIME_FORMATS_MAP = {
1368 '%D': '%m/%d/%Y', # modified %y->%Y
1370 '%E': '', # special modifier
1372 '%g': '%Y', # modified %y->%Y
1378 '%O': '', # special modifier
1381 '%r': '%I:%M:%S %p',
1382 '%s': '', #num of seconds since epoch
1387 '%y': '%Y', # Even if %y works, it's ambiguous, so we should use %Y
1388 '%+': '%Y-%m-%d %H:%M:%S',
1390 # %Z is a special case that causes 2 problems at least:
1391 # - the timezone names we use (in res_user.context_tz) come
1392 # from pytz, but not all these names are recognized by
1393 # strptime(), so we cannot convert in both directions
1394 # when such a timezone is selected and %Z is in the format
1395 # - %Z is replaced by an empty string in strftime() when
1396 # there is not tzinfo in a datetime value (e.g when the user
1397 # did not pick a context_tz). The resulting string does not
1398 # parse back if the format requires %Z.
1399 # As a consequence, we strip it completely from format strings.
1400 # The user can always have a look at the context_tz in
1401 # preferences to check the timezone.
1406 def server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_name,
1407 tz_offset=True, ignore_unparsable_time=True):
1409 Convert a source timestamp string into a destination timestamp string, attempting to apply the
1410 correct offset if both the server and local timezone are recognized, or no
1411 offset at all if they aren't or if tz_offset is false (i.e. assuming they are both in the same TZ).
1413 WARNING: This method is here to allow formatting dates correctly for inclusion in strings where
1414 the client would not be able to format/offset it correctly. DO NOT use it for returning
1415 date fields directly, these are supposed to be handled by the client!!
1417 @param src_tstamp_str: the str value containing the timestamp in the server timezone.
1418 @param src_format: the format to use when parsing the server timestamp.
1419 @param dst_format: the format to use when formatting the resulting timestamp for the local/client timezone.
1420 @param dst_tz_name: name of the destination timezone (such as the 'tz' value of the client context)
1421 @param ignore_unparsable_time: if True, return False if src_tstamp_str cannot be parsed
1422 using src_format or formatted using dst_format.
1424 @return: local/client formatted timestamp, expressed in the local/client timezone if possible
1425 and if tz_offset is true, or src_tstamp_str if timezone offset could not be determined.
1427 if not src_tstamp_str:
1430 res = src_tstamp_str
1431 if src_format and dst_format:
1432 # find out server timezone
1433 server_tz = get_server_timezone()
1435 # dt_value needs to be a datetime.datetime object (so no time.struct_time or mx.DateTime.DateTime here!)
1436 dt_value = datetime.strptime(src_tstamp_str, src_format)
1437 if tz_offset and dst_tz_name:
1440 src_tz = pytz.timezone(server_tz)
1441 dst_tz = pytz.timezone(dst_tz_name)
1442 src_dt = src_tz.localize(dt_value, is_dst=True)
1443 dt_value = src_dt.astimezone(dst_tz)
1446 res = dt_value.strftime(dst_format)
1448 # Normal ways to end up here are if strptime or strftime failed
1449 if not ignore_unparsable_time:
1454 def split_every(n, iterable, piece_maker=tuple):
1455 """Splits an iterable into length-n pieces. The last piece will be shorter
1456 if ``n`` does not evenly divide the iterable length.
1457 @param ``piece_maker``: function to build the pieces
1458 from the slices (tuple,list,...)
1460 iterator = iter(iterable)
1461 piece = piece_maker(islice(iterator, n))
1464 piece = piece_maker(islice(iterator, n))
1466 if __name__ == '__main__':
1470 class upload_data_thread(threading.Thread):
1471 def __init__(self, email, data, type):
1472 self.args = [('email',email),('type',type),('data',data)]
1473 super(upload_data_thread,self).__init__()
1477 args = urllib.urlencode(self.args)
1478 fp = urllib.urlopen('http://www.openerp.com/scripts/survey.php', args)
1484 def upload_data(email, data, type='SURVEY'):
1485 a = upload_data_thread(email, data, type)
1490 # port of python 2.6's attrgetter with support for dotted notation
1491 def resolve_attr(obj, attr):
1492 for name in attr.split("."):
1493 obj = getattr(obj, name)
1496 def attrgetter(*items):
1500 return resolve_attr(obj, attr)
1503 return tuple(resolve_attr(obj, attr) for attr in items)
1507 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: