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 Utils
46 from email import Encoders
47 from itertools import islice, izip
48 from lxml import etree
49 from which import which
50 if sys.version_info[:2] < (2, 4):
51 from threadinglocal import local
53 from threading import local
55 from html2text import html2text
59 import openerp.loglevels as loglevels
60 from config import config
63 # get_encodings, ustr and exception_to_unicode were originally from tools.misc.
64 # There are moved to loglevels until we refactor tools.
65 from openerp.loglevels import get_encodings, ustr, exception_to_unicode
67 _logger = logging.getLogger('tools')
69 # List of etree._Element subclasses that we choose to ignore when parsing XML.
70 # We include the *Base ones just in case, currently they seem to be subclasses of the _* ones.
71 SKIPPED_ELEMENT_TYPES = (etree._Comment, etree._ProcessingInstruction, etree.CommentBase, etree.PIBase)
73 # initialize a database with base/base.sql
75 import openerp.addons as addons
76 f = addons.get_module_resource('base', 'base.sql')
77 base_sql_file = file_open(f)
79 cr.execute(base_sql_file.read())
84 for i in addons.get_modules():
85 mod_path = addons.get_module_path(i)
89 info = addons.load_information_from_description_file(i)
93 categs = info.get('category', 'Uncategorized').split('/')
97 cr.execute('SELECT id \
98 FROM ir_module_category \
99 WHERE name=%s AND parent_id=%s', (categs[0], p_id))
101 cr.execute('SELECT id \
102 FROM ir_module_category \
103 WHERE name=%s AND parent_id IS NULL', (categs[0],))
106 cr.execute('INSERT INTO ir_module_category \
108 VALUES (%s, %s) RETURNING id', (categs[0], p_id))
109 c_id = cr.fetchone()[0]
115 active = info.get('active', False)
116 installable = info.get('installable', True)
121 state = 'uninstalled'
123 state = 'uninstallable'
124 cr.execute('INSERT INTO ir_module_module \
125 (author, website, name, shortdesc, description, \
126 category_id, state, certificate, web, license) \
127 VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id', (
128 info.get('author', ''),
129 info.get('website', ''), i, info.get('name', False),
130 info.get('description', ''), p_id, state, info.get('certificate') or None,
131 info.get('web') or False,
132 info.get('license') or 'AGPL-3'))
133 id = cr.fetchone()[0]
134 cr.execute('INSERT INTO ir_model_data \
135 (name,model,module, res_id, noupdate) VALUES (%s,%s,%s,%s,%s)', (
136 'module_meta_information', 'ir.module.module', i, id, True))
137 dependencies = info.get('depends', [])
138 for d in dependencies:
139 cr.execute('INSERT INTO ir_module_module_dependency \
140 (module_id,name) VALUES (%s, %s)', (id, d))
143 def find_in_path(name):
149 def find_pg_tool(name):
151 if config['pg_path'] and config['pg_path'] != 'None':
152 path = config['pg_path']
154 return which(name, path=path)
158 def exec_pg_command(name, *args):
159 prog = find_pg_tool(name)
161 raise Exception('Couldn\'t find %s' % name)
162 args2 = (prog,) + args
164 return subprocess.call(args2)
166 def exec_pg_command_pipe(name, *args):
167 prog = find_pg_tool(name)
169 raise Exception('Couldn\'t find %s' % name)
170 # on win32, passing close_fds=True is not compatible
171 # with redirecting std[in/err/out]
172 pop = subprocess.Popen((prog,) + args, bufsize= -1,
173 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
174 close_fds=(os.name=="posix"))
175 return (pop.stdin, pop.stdout)
177 def exec_command_pipe(name, *args):
178 prog = find_in_path(name)
180 raise Exception('Couldn\'t find %s' % name)
181 # on win32, passing close_fds=True is not compatible
182 # with redirecting std[in/err/out]
183 pop = subprocess.Popen((prog,) + args, bufsize= -1,
184 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
185 close_fds=(os.name=="posix"))
186 return (pop.stdin, pop.stdout)
188 #----------------------------------------------------------
190 #----------------------------------------------------------
191 #file_path_root = os.getcwd()
192 #file_path_addons = os.path.join(file_path_root, 'addons')
194 def file_open(name, mode="r", subdir='addons', pathinfo=False):
195 """Open a file from the OpenERP root, using a subdir folder.
197 >>> file_open('hr/report/timesheer.xsl')
198 >>> file_open('addons/hr/report/timesheet.xsl')
199 >>> file_open('../../base/report/rml_template.xsl', subdir='addons/hr/report', pathinfo=True)
201 @param name: name of the file
202 @param mode: file open mode
203 @param subdir: subdirectory
204 @param pathinfo: if True returns tupple (fileobject, filepath)
206 @return: fileobject if pathinfo is False else (fileobject, filepath)
208 import openerp.addons as addons
209 adps = addons.ad_paths
210 rtp = os.path.normcase(os.path.abspath(config['root_path']))
212 if name.replace(os.path.sep, '/').startswith('addons/'):
216 # First try to locate in addons_path
219 if subdir2.replace(os.path.sep, '/').startswith('addons/'):
220 subdir2 = subdir2[7:]
222 subdir2 = (subdir2 != 'addons' or None) and subdir2
227 fn = os.path.join(adp, subdir2, name)
229 fn = os.path.join(adp, name)
230 fn = os.path.normpath(fn)
231 fo = file_open(fn, mode=mode, subdir=None, pathinfo=pathinfo)
239 name = os.path.join(rtp, subdir, name)
241 name = os.path.join(rtp, name)
243 name = os.path.normpath(name)
245 # Check for a zipfile in the path
250 head, tail = os.path.split(head)
254 zipname = os.path.join(tail, zipname)
257 if zipfile.is_zipfile(head+'.zip'):
258 from cStringIO import StringIO
259 zfile = zipfile.ZipFile(head+'.zip')
262 fo.write(zfile.read(os.path.join(
263 os.path.basename(head), zipname).replace(
270 name2 = os.path.normpath(os.path.join(head + '.zip', zipname))
272 for i in (name2, name):
273 if i and os.path.isfile(i):
278 if os.path.splitext(name)[1] == '.rml':
279 raise IOError, 'Report %s doesn\'t exist or deleted : ' %str(name)
280 raise IOError, 'File not found : %s' % name
283 #----------------------------------------------------------
285 #----------------------------------------------------------
287 """Flatten a list of elements into a uniqu list
288 Author: Christophe Simonis (christophe@tinyerp.com)
297 >>> flatten( [[], [[]]] )
299 >>> flatten( [[['a','b'], 'c'], 'd', ['e', [], 'f']] )
300 ['a', 'b', 'c', 'd', 'e', 'f']
301 >>> t = (1,2,(3,), [4, 5, [6, [7], (8, 9), ([10, 11, (12, 13)]), [14, [], (15,)], []]])
303 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
307 return hasattr(x, "__iter__")
312 map(r.append, flatten(e))
317 def reverse_enumerate(l):
318 """Like enumerate but in the other sens
319 >>> a = ['a', 'b', 'c']
320 >>> it = reverse_enumerate(a)
328 Traceback (most recent call last):
329 File "<stdin>", line 1, in <module>
332 return izip(xrange(len(l)-1, -1, -1), reversed(l))
334 #----------------------------------------------------------
336 #----------------------------------------------------------
337 email_re = re.compile(r"""
338 ([a-zA-Z][\w\.-]*[a-zA-Z0-9] # username part
340 [a-zA-Z0-9][\w\.-]* # domain must start with a letter ... Ged> why do we include a 0-9 then?
345 res_re = re.compile(r"\[([0-9]+)\]", re.UNICODE)
346 command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE)
347 reference_re = re.compile("<.*-openobject-(\\d+)@(.*)>", re.UNICODE)
357 def html2plaintext(html, body_id=None, encoding='utf-8'):
358 ## (c) Fry-IT, www.fry-it.com, 2007
359 ## <peter@fry-it.com>
360 ## download here: http://www.peterbe.com/plog/html2plaintext
363 """ from an HTML text, convert the HTML to plain text.
364 If @body_id is provided then this is the tag where the
365 body (not necessarily <body>) starts.
370 from lxml.etree import tostring
372 from lxml.html.soupparser import fromstring
375 _logger.debug('tools.misc.html2plaintext: cannot use BeautifulSoup, fallback to lxml.etree.HTMLParser')
376 from lxml.etree import fromstring, HTMLParser
377 kwargs = dict(parser=HTMLParser())
379 tree = fromstring(html, **kwargs)
381 if body_id is not None:
382 source = tree.xpath('//*[@id=%s]'%(body_id,))
384 source = tree.xpath('//body')
390 for link in tree.findall('.//a'):
391 url = link.get('href')
395 link.text = '%s [%s]' % (link.text, i)
396 url_index.append(url)
398 html = ustr(tostring(tree, encoding=encoding))
400 html = html.replace('<strong>','*').replace('</strong>','*')
401 html = html.replace('<b>','*').replace('</b>','*')
402 html = html.replace('<h3>','*').replace('</h3>','*')
403 html = html.replace('<h2>','**').replace('</h2>','**')
404 html = html.replace('<h1>','**').replace('</h1>','**')
405 html = html.replace('<em>','/').replace('</em>','/')
406 html = html.replace('<tr>', '\n')
407 html = html.replace('</p>', '\n')
408 html = re.sub('<br\s*/?>', '\n', html)
409 html = re.sub('<.*?>', ' ', html)
410 html = html.replace(' ' * 2, ' ')
413 html = '\n'.join([x.strip() for x in html.splitlines()])
414 html = html.replace('\n' * 2, '\n')
416 for i, url in enumerate(url_index):
419 html += ustr('[%s] %s\n') % (i+1, url)
423 def generate_tracking_message_id(openobject_id):
424 """Returns a string that can be used in the Message-ID RFC822 header field so we
425 can track the replies related to a given object thanks to the "In-Reply-To" or
426 "References" fields that Mail User Agents will set.
428 return "<%s-openobject-%s@%s>" % (time.time(), openobject_id, socket.gethostname())
430 def connect_smtp_server(server_host, server_port, user_name=None, user_password=None, ssl=False, tls=False, debug=False):
432 Connect SMTP Server and returned the (SMTP) object
438 smtp_server = smtplib.SMTP_SSL(server_host, server_port)
440 smtp_server = smtplib.SMTP(server_host, server_port)
442 smtp_server.set_debuglevel(int(bool(debug))) # 0 or 1
447 smtp_server.starttls()
450 #smtp_server.connect(server_host, server_port)
452 if smtp_server.has_extn('AUTH') or user_name or user_password:
453 smtp_server.login(user_name, user_password)
456 except Exception, error:
457 _logger.error('Could not connect to smtp server : %s' %(error), exc_info=True)
462 def _email_send(smtp_from, smtp_to_list, message, ssl=False, debug=False,
463 smtp_server=None, smtp_port=None, smtp_user=None, smtp_password=None):
464 """Low-level method to send directly a Message through the configured smtp server.
465 :param smtp_from: RFC-822 envelope FROM (not displayed to recipient)
466 :param smtp_to_list: RFC-822 envelope RCPT_TOs (not displayed to recipient)
467 :param message: an email.message.Message to send
468 :param debug: True if messages should be output to stderr before being sent,
469 and smtplib.SMTP put into debug mode.
470 :return: True if the mail was delivered successfully to the smtp,
471 else False (+ exception logged)
473 class WriteToLogger(object):
475 self.logger = loglevels.Logger()
478 self.logger.notifyChannel('email_send', loglevels.LOG_DEBUG, s)
481 smtp_server = smtp_server or config['smtp_server']
483 if smtp_server.startswith('maildir:/'):
484 from mailbox import Maildir
485 maildir_path = smtp_server[8:]
486 mdir = Maildir(maildir_path,factory=None, create = True)
487 mdir.add(message.as_string(True))
491 oldstderr = smtplib.stderr
492 smtplib.stderr = WriteToLogger()
494 if not ssl: ssl = config.get('smtp_ssl', False)
495 smtp = connect_smtp_server(smtp_server, smtp_port, smtp_user, smtp_password, ssl=ssl, tls=True, debug=debug)
497 smtp.sendmail(smtp_from, smtp_to_list, message.as_string())
499 _logger.error('could not deliver Email(s)', exc_info=True)
505 # ignored, just a consequence of the previous exception
509 smtplib.stderr = oldstderr
511 _logger.error('Error on Send Emails Services', exc_info=True)
517 def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
518 attach=None, message_id=None, references=None, openobject_id=False, debug=False, subtype='plain', x_headers=None, priority='3',
519 smtp_server=None, smtp_port=None, ssl=False, smtp_user=None, smtp_password=None):
525 `email_from`: A string used to fill the `From` header, if falsy,
526 config['email_from'] is used instead. Also used for
527 the `Reply-To` header if `reply_to` is not provided
529 `email_to`: a sequence of addresses to send the mail to.
531 if x_headers is None:
535 if not (email_from or config['email_from']):
536 raise ValueError("Sending an email requires either providing a sender "
537 "address or having configured one")
539 if not email_from: email_from = config.get('email_from', False)
540 email_from = ustr(email_from).encode('utf-8')
542 if not email_cc: email_cc = []
543 if not email_bcc: email_bcc = []
544 if not body: body = u''
546 email_body = ustr(body).encode('utf-8')
547 email_text = MIMEText(email_body or '',_subtype=subtype,_charset='utf-8')
548 msg = MIMEMultipart()
550 if not message_id and openobject_id:
551 message_id = generate_tracking_message_id(openobject_id)
553 message_id = Utils.make_msgid()
555 msg['references'] = references
556 msg['Message-Id'] = message_id
557 msg['Subject'] = Header(ustr(subject), 'utf-8')
558 msg['From'] = email_from
561 msg['Reply-To'] = reply_to
563 msg['Reply-To'] = msg['From']
564 msg['To'] = COMMASPACE.join(email_to)
566 msg['Cc'] = COMMASPACE.join(email_cc)
568 msg['Bcc'] = COMMASPACE.join(email_bcc)
569 msg['Date'] = formatdate(localtime=True)
571 msg['X-Priority'] = priorities.get(priority, '3 (Normal)')
573 # Add dynamic X Header
574 for key, value in x_headers.iteritems():
575 msg['%s' % key] = str(value)
577 if html2text and subtype == 'html':
578 text = html2text(email_body.decode('utf-8')).encode('utf-8')
579 alternative_part = MIMEMultipart(_subtype="alternative")
580 alternative_part.attach(MIMEText(text, _charset='utf-8', _subtype='plain'))
581 alternative_part.attach(email_text)
582 msg.attach(alternative_part)
584 msg.attach(email_text)
587 for (fname,fcontent) in attach:
588 part = MIMEBase('application', "octet-stream")
589 part.set_payload( fcontent )
590 Encoders.encode_base64(part)
591 part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,))
594 res = _email_send(email_from, flatten([email_to, email_cc, email_bcc]), msg, ssl=ssl, debug=debug,
595 smtp_server=smtp_server, smtp_port=smtp_port, smtp_user=smtp_user, smtp_password=smtp_password)
600 #----------------------------------------------------------
602 #----------------------------------------------------------
603 # text must be latin-1 encoded
604 def sms_send(user, password, api_id, text, to):
606 url = "http://api.urlsms.com/SendSMS.aspx"
607 #url = "http://196.7.150.220/http/sendmsg"
608 params = urllib.urlencode({'UserID': user, 'Password': password, 'SenderID': api_id, 'MsgText': text, 'RecipientMobileNo':to})
609 urllib.urlopen(url+"?"+params)
610 # FIXME: Use the logger if there is an error
613 #---------------------------------------------------------
614 # Class that stores an updateable string (used in wizards)
615 #---------------------------------------------------------
616 class UpdateableStr(local):
618 def __init__(self, string=''):
622 return str(self.string)
625 return str(self.string)
627 def __nonzero__(self):
628 return bool(self.string)
631 class UpdateableDict(local):
632 '''Stores an updateable dict to use in wizards'''
634 def __init__(self, dict=None):
640 return str(self.dict)
643 return str(self.dict)
646 return self.dict.clear()
649 return self.dict.keys()
651 def __setitem__(self, i, y):
652 self.dict.__setitem__(i, y)
654 def __getitem__(self, i):
655 return self.dict.__getitem__(i)
658 return self.dict.copy()
661 return self.dict.iteritems()
664 return self.dict.iterkeys()
666 def itervalues(self):
667 return self.dict.itervalues()
669 def pop(self, k, d=None):
670 return self.dict.pop(k, d)
673 return self.dict.popitem()
675 def setdefault(self, k, d=None):
676 return self.dict.setdefault(k, d)
678 def update(self, E, **F):
679 return self.dict.update(E, F)
682 return self.dict.values()
684 def get(self, k, d=None):
685 return self.dict.get(k, d)
687 def has_key(self, k):
688 return self.dict.has_key(k)
691 return self.dict.items()
693 def __cmp__(self, y):
694 return self.dict.__cmp__(y)
696 def __contains__(self, k):
697 return self.dict.__contains__(k)
699 def __delitem__(self, y):
700 return self.dict.__delitem__(y)
703 return self.dict.__eq__(y)
706 return self.dict.__ge__(y)
709 return self.dict.__gt__(y)
712 return self.dict.__hash__()
715 return self.dict.__iter__()
718 return self.dict.__le__(y)
721 return self.dict.__len__()
724 return self.dict.__lt__(y)
727 return self.dict.__ne__(y)
730 # Don't use ! Use res.currency.round()
731 class currency(float):
733 def __init__(self, value, accuracy=2, rounding=None):
735 rounding=10**-accuracy
736 self.rounding=rounding
737 self.accuracy=accuracy
739 def __new__(cls, value, accuracy=2, rounding=None):
740 return float.__new__(cls, round(value, accuracy))
743 # display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
744 # return str(display_value)
756 Use it as a decorator of the function you plan to cache
757 Timeout: 0 = no timeout, otherwise in seconds
762 def __init__(self, timeout=None, skiparg=2, multi=None, size=8192):
763 assert skiparg >= 2 # at least self and cr
765 self.timeout = config['cache_timeout']
767 self.timeout = timeout
768 self.skiparg = skiparg
770 self.lasttime = time.time()
771 self.cache = LRU(size) # TODO take size from config
773 cache.__caches.append(self)
776 def _generate_keys(self, dbname, kwargs2):
778 Generate keys depending of the arguments and the self.mutli value
783 pairs.sort(key=lambda (k,v): k)
784 for i, (k, v) in enumerate(pairs):
785 if isinstance(v, dict):
786 pairs[i] = (k, to_tuple(v))
787 if isinstance(v, (list, set)):
788 pairs[i] = (k, tuple(v))
789 elif not is_hashable(v):
790 pairs[i] = (k, repr(v))
794 key = (('dbname', dbname),) + to_tuple(kwargs2)
797 multis = kwargs2[self.multi][:]
799 kwargs2[self.multi] = (id,)
800 key = (('dbname', dbname),) + to_tuple(kwargs2)
803 def _unify_args(self, *args, **kwargs):
804 # Update named arguments with positional argument values (without self and cr)
805 kwargs2 = self.fun_default_values.copy()
806 kwargs2.update(kwargs)
807 kwargs2.update(dict(zip(self.fun_arg_names, args[self.skiparg-2:])))
810 def clear(self, dbname, *args, **kwargs):
811 """clear the cache for database dbname
812 if *args and **kwargs are both empty, clear all the keys related to this database
814 if not args and not kwargs:
815 keys_to_del = [key for key in self.cache.keys() if key[0][1] == dbname]
817 kwargs2 = self._unify_args(*args, **kwargs)
818 keys_to_del = [key for key, _ in self._generate_keys(dbname, kwargs2) if key in self.cache.keys()]
820 for key in keys_to_del:
824 def clean_caches_for_db(cls, dbname):
825 for c in cls.__caches:
828 def __call__(self, fn):
829 if self.fun is not None:
830 raise Exception("Can not use a cache instance on more than one function")
833 argspec = inspect.getargspec(fn)
834 self.fun_arg_names = argspec[0][self.skiparg:]
835 self.fun_default_values = {}
837 self.fun_default_values = dict(zip(self.fun_arg_names[-len(argspec[3]):], argspec[3]))
839 def cached_result(self2, cr, *args, **kwargs):
840 if time.time()-int(self.timeout) > self.lasttime:
841 self.lasttime = time.time()
842 t = time.time()-int(self.timeout)
843 old_keys = [key for key in self.cache.keys() if self.cache[key][1] < t]
847 kwargs2 = self._unify_args(*args, **kwargs)
851 for key, id in self._generate_keys(cr.dbname, kwargs2):
852 if key in self.cache:
853 result[id] = self.cache[key][0]
859 kwargs2[self.multi] = notincache.keys()
861 result2 = fn(self2, cr, *args[:self.skiparg-2], **kwargs2)
863 key = notincache[None]
864 self.cache[key] = (result2, time.time())
865 result[None] = result2
869 self.cache[key] = (result2[id], time.time())
870 result.update(result2)
876 cached_result.clear_cache = self.clear
880 return s.replace('&','&').replace('<','<').replace('>','>')
882 # to be compatible with python 2.4
884 if not hasattr(__builtin__, 'all'):
886 for element in iterable:
891 __builtin__.all = all
894 if not hasattr(__builtin__, 'any'):
896 for element in iterable:
901 __builtin__.any = any
904 def get_iso_codes(lang):
905 if lang.find('_') != -1:
906 if lang.split('_')[0] == lang.split('_')[1].lower():
907 lang = lang.split('_')[0]
911 # The codes below are those from Launchpad's Rosetta, with the exception
912 # of some trivial codes where the Launchpad code is xx and we have xx_XX.
914 'ab_RU': u'Abkhazian / аҧсуа',
915 'ar_AR': u'Arabic / الْعَرَبيّة',
916 'bg_BG': u'Bulgarian / български език',
917 'bs_BS': u'Bosnian / bosanski jezik',
918 'ca_ES': u'Catalan / Català',
919 'cs_CZ': u'Czech / Čeština',
920 'da_DK': u'Danish / Dansk',
921 'de_DE': u'German / Deutsch',
922 'el_GR': u'Greek / Ελληνικά',
923 'en_CA': u'English (CA)',
924 'en_GB': u'English (UK)',
925 'en_US': u'English (US)',
926 'es_AR': u'Spanish (AR) / Español (AR)',
927 'es_BO': u'Spanish (BO) / Español (BO)',
928 'es_CL': u'Spanish (CL) / Español (CL)',
929 'es_CO': u'Spanish (CO) / Español (CO)',
930 'es_CR': u'Spanish (CR) / Español (CR)',
931 'es_DO': u'Spanish (DO) / Español (DO)',
932 'es_EC': u'Spanish (EC) / Español (EC)',
933 'es_ES': u'Spanish / Español',
934 'es_GT': u'Spanish (GT) / Español (GT)',
935 'es_HN': u'Spanish (HN) / Español (HN)',
936 'es_MX': u'Spanish (MX) / Español (MX)',
937 'es_NI': u'Spanish (NI) / Español (NI)',
938 'es_PA': u'Spanish (PA) / Español (PA)',
939 'es_PE': u'Spanish (PE) / Español (PE)',
940 'es_PR': u'Spanish (PR) / Español (PR)',
941 'es_PY': u'Spanish (PY) / Español (PY)',
942 'es_SV': u'Spanish (SV) / Español (SV)',
943 'es_UY': u'Spanish (UY) / Español (UY)',
944 'es_VE': u'Spanish (VE) / Español (VE)',
945 'et_EE': u'Estonian / Eesti keel',
946 'fa_IR': u'Persian / فارس',
947 'fi_FI': u'Finnish / Suomi',
948 'fr_BE': u'French (BE) / Français (BE)',
949 'fr_CH': u'French (CH) / Français (CH)',
950 'fr_FR': u'French / Français',
951 'gl_ES': u'Galician / Galego',
952 'gu_IN': u'Gujarati / ગુજરાતી',
953 'he_IL': u'Hebrew / עִבְרִי',
954 'hi_IN': u'Hindi / हिंदी',
955 'hr_HR': u'Croatian / hrvatski jezik',
956 'hu_HU': u'Hungarian / Magyar',
957 'id_ID': u'Indonesian / Bahasa Indonesia',
958 'it_IT': u'Italian / Italiano',
959 'iu_CA': u'Inuktitut / ᐃᓄᒃᑎᑐᑦ',
960 'ja_JP': u'Japanese / 日本語',
961 'ko_KP': u'Korean (KP) / 한국어 (KP)',
962 'ko_KR': u'Korean (KR) / 한국어 (KR)',
963 'lt_LT': u'Lithuanian / Lietuvių kalba',
964 'lv_LV': u'Latvian / latviešu valoda',
965 'ml_IN': u'Malayalam / മലയാളം',
966 'mn_MN': u'Mongolian / монгол',
967 'nb_NO': u'Norwegian Bokmål / Norsk bokmål',
968 'nl_NL': u'Dutch / Nederlands',
969 'nl_BE': u'Flemish (BE) / Vlaams (BE)',
970 'oc_FR': u'Occitan (FR, post 1500) / Occitan',
971 'pl_PL': u'Polish / Język polski',
972 'pt_BR': u'Portugese (BR) / Português (BR)',
973 'pt_PT': u'Portugese / Português',
974 'ro_RO': u'Romanian / română',
975 'ru_RU': u'Russian / русский язык',
976 'si_LK': u'Sinhalese / සිංහල',
977 'sl_SI': u'Slovenian / slovenščina',
978 'sk_SK': u'Slovak / Slovenský jazyk',
979 'sq_AL': u'Albanian / Shqip',
980 'sr_RS': u'Serbian (Cyrillic) / српски',
981 'sr@latin': u'Serbian (Latin) / srpski',
982 'sv_SE': u'Swedish / svenska',
983 'te_IN': u'Telugu / తెలుగు',
984 'tr_TR': u'Turkish / Türkçe',
985 'vi_VN': u'Vietnamese / Tiếng Việt',
986 'uk_UA': u'Ukrainian / українська',
987 'ur_PK': u'Urdu / اردو',
988 'zh_CN': u'Chinese (CN) / 简体中文',
989 'zh_HK': u'Chinese (HK)',
990 'zh_TW': u'Chinese (TW) / 正體字',
991 'th_TH': u'Thai / ภาษาไทย',
992 'tlh_TLH': u'Klingon',
996 def scan_languages():
997 # Now it will take all languages from get languages function without filter it with base module languages
998 lang_dict = get_languages()
999 ret = [(lang, lang_dict.get(lang, lang)) for lang in list(lang_dict)]
1000 ret.sort(key=lambda k:k[1])
1004 def get_user_companies(cr, user):
1005 def _get_company_children(cr, ids):
1008 cr.execute('SELECT id FROM res_company WHERE parent_id IN %s', (tuple(ids),))
1009 res = [x[0] for x in cr.fetchall()]
1010 res.extend(_get_company_children(cr, res))
1012 cr.execute('SELECT company_id FROM res_users WHERE id=%s', (user,))
1013 user_comp = cr.fetchone()[0]
1016 return [user_comp] + _get_company_children(cr, [user_comp])
1020 Input number : account or invoice number
1021 Output return: the same number completed with the recursive mod10
1024 codec=[0,9,4,6,8,2,7,1,3,5]
1027 for digit in number:
1030 report = codec[ (int(digit) + report) % 10 ]
1031 return result + str((10 - report) % 10)
1036 Return the size in a human readable format
1040 units = ('bytes', 'Kb', 'Mb', 'Gb')
1041 if isinstance(sz,basestring):
1044 while s >= 1024 and i < len(units)-1:
1047 return "%0.2f %s" % (s, units[i])
1050 from func import wraps
1053 def wrapper(*args, **kwargs):
1054 from pprint import pformat
1056 vector = ['Call -> function: %r' % f]
1057 for i, arg in enumerate(args):
1058 vector.append(' arg %02d: %s' % (i, pformat(arg)))
1059 for key, value in kwargs.items():
1060 vector.append(' kwarg %10s: %s' % (key, pformat(value)))
1062 timeb4 = time.time()
1063 res = f(*args, **kwargs)
1065 vector.append(' result: %s' % pformat(res))
1066 vector.append(' time delta: %s' % (time.time() - timeb4))
1067 loglevels.Logger().notifyChannel('logged', loglevels.LOG_DEBUG, '\n'.join(vector))
1072 class profile(object):
1073 def __init__(self, fname=None):
1076 def __call__(self, f):
1077 from func import wraps
1080 def wrapper(*args, **kwargs):
1081 class profile_wrapper(object):
1085 self.result = f(*args, **kwargs)
1086 pw = profile_wrapper()
1088 fname = self.fname or ("%s.cprof" % (f.func_name,))
1089 cProfile.runctx('pw()', globals(), locals(), filename=fname)
1096 This method allow you to debug your code without print
1098 >>> def func_foo(bar)
1101 ... qnx = (baz, bar)
1106 This will output on the logger:
1108 [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:baz = 42
1109 [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:qnx = (42, 42)
1111 To view the DEBUG lines in the logger you must start the server with the option
1115 warnings.warn("The tools.debug() method is deprecated, please use logging.",
1116 DeprecationWarning, stacklevel=2)
1117 from inspect import stack
1118 from pprint import pformat
1120 param = re.split("debug *\((.+)\)", st[4][0].strip())[1].strip()
1121 while param.count(')') > param.count('('): param = param[:param.rfind(')')]
1122 what = pformat(what)
1124 what = "%s = %s" % (param, what)
1125 logging.getLogger(st[3]).debug(what)
1128 __icons_list = ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
1129 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
1130 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
1131 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
1132 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
1133 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
1134 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
1135 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
1136 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
1137 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
1138 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
1139 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
1140 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
1141 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
1142 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
1143 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
1144 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
1145 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
1146 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
1147 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
1148 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
1149 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
1150 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
1151 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
1152 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
1153 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
1154 'terp-check','terp-go-month','terp-go-year','terp-go-today','terp-document-new','terp-camera_test',
1155 'terp-emblem-important','terp-gtk-media-pause','terp-gtk-stop','terp-gnome-cpu-frequency-applet+',
1156 'terp-dialog-close','terp-gtk-jump-to-rtl','terp-gtk-jump-to-ltr','terp-accessories-archiver',
1157 'terp-stock_align_left_24','terp-stock_effects-object-colorize','terp-go-home','terp-gtk-go-back-rtl',
1158 'terp-gtk-go-back-ltr','terp-personal','terp-personal-','terp-personal+','terp-accessories-archiver-minus',
1159 'terp-accessories-archiver+','terp-stock_symbol-selection','terp-call-start','terp-dolar',
1160 'terp-face-plain','terp-folder-blue','terp-folder-green','terp-folder-orange','terp-folder-yellow',
1161 'terp-gdu-smart-failing','terp-go-week','terp-gtk-select-all','terp-locked','terp-mail-forward',
1162 'terp-mail-message-new','terp-mail-replied','terp-rating-rated','terp-stage','terp-stock_format-scientific',
1163 'terp-dolar_ok!','terp-idea','terp-stock_format-default','terp-mail-','terp-mail_delete'
1166 def icons(*a, **kw):
1168 return [(x, x) for x in __icons_list ]
1170 def extract_zip_file(zip_file, outdirectory):
1171 zf = zipfile.ZipFile(zip_file, 'r')
1173 for path in zf.namelist():
1174 tgt = os.path.join(out, path)
1175 tgtdir = os.path.dirname(tgt)
1176 if not os.path.exists(tgtdir):
1179 if not tgt.endswith(os.sep):
1180 fp = open(tgt, 'wb')
1181 fp.write(zf.read(path))
1185 def detect_ip_addr():
1186 """Try a very crude method to figure out a valid external
1187 IP or hostname for the current machine. Don't rely on this
1188 for binding to an interface, but it could be used as basis
1189 for constructing a remote URL to the server.
1191 def _detect_ip_addr():
1192 from array import array
1193 from struct import pack, unpack
1202 if not fcntl: # not UNIX:
1203 host = socket.gethostname()
1204 ip_addr = socket.gethostbyname(host)
1206 # get all interfaces:
1208 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1209 names = array('B', '\0' * nbytes)
1210 #print 'names: ', names
1211 outbytes = unpack('iL', fcntl.ioctl( s.fileno(), 0x8912, pack('iL', nbytes, names.buffer_info()[0])))[0]
1212 namestr = names.tostring()
1214 # try 64 bit kernel:
1215 for i in range(0, outbytes, 40):
1216 name = namestr[i:i+16].split('\0', 1)[0]
1218 ip_addr = socket.inet_ntoa(namestr[i+20:i+24])
1221 # try 32 bit kernel:
1223 ifaces = filter(None, [namestr[i:i+32].split('\0', 1)[0] for i in range(0, outbytes, 32)])
1225 for ifname in [iface for iface in ifaces if iface != 'lo']:
1226 ip_addr = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, pack('256s', ifname[:15]))[20:24])
1229 return ip_addr or 'localhost'
1232 ip_addr = _detect_ip_addr()
1234 ip_addr = 'localhost'
1237 # RATIONALE BEHIND TIMESTAMP CALCULATIONS AND TIMEZONE MANAGEMENT:
1238 # The server side never does any timestamp calculation, always
1239 # sends them in a naive (timezone agnostic) format supposed to be
1240 # expressed within the server timezone, and expects the clients to
1241 # provide timestamps in the server timezone as well.
1242 # It stores all timestamps in the database in naive format as well,
1243 # which also expresses the time in the server timezone.
1244 # For this reason the server makes its timezone name available via the
1245 # common/timezone_get() rpc method, which clients need to read
1246 # to know the appropriate time offset to use when reading/writing
1248 def get_win32_timezone():
1249 """Attempt to return the "standard name" of the current timezone on a win32 system.
1250 @return: the standard name of the current win32 timezone, or False if it cannot be found.
1253 if (sys.platform == "win32"):
1256 hklm = _winreg.ConnectRegistry(None,_winreg.HKEY_LOCAL_MACHINE)
1257 current_tz_key = _winreg.OpenKey(hklm, r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", 0,_winreg.KEY_ALL_ACCESS)
1258 res = str(_winreg.QueryValueEx(current_tz_key,"StandardName")[0]) # [0] is value, [1] is type code
1259 _winreg.CloseKey(current_tz_key)
1260 _winreg.CloseKey(hklm)
1265 def detect_server_timezone():
1266 """Attempt to detect the timezone to use on the server side.
1267 Defaults to UTC if no working timezone can be found.
1268 @return: the timezone identifier as expected by pytz.timezone.
1273 loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_WARNING,
1274 "Python pytz module is not available. Timezone will be set to UTC by default.")
1277 # Option 1: the configuration option (did not exist before, so no backwards compatibility issue)
1278 # Option 2: to be backwards compatible with 5.0 or earlier, the value from time.tzname[0], but only if it is known to pytz
1279 # Option 3: the environment variable TZ
1280 sources = [ (config['timezone'], 'OpenERP configuration'),
1281 (time.tzname[0], 'time.tzname'),
1282 (os.environ.get('TZ',False),'TZ environment variable'), ]
1283 # Option 4: OS-specific: /etc/timezone on Unix
1284 if (os.path.exists("/etc/timezone")):
1287 f = open("/etc/timezone")
1288 tz_value = f.read(128).strip()
1293 sources.append((tz_value,"/etc/timezone file"))
1294 # Option 5: timezone info from registry on Win32
1295 if (sys.platform == "win32"):
1296 # Timezone info is stored in windows registry.
1297 # However this is not likely to work very well as the standard name
1298 # of timezones in windows is rarely something that is known to pytz.
1299 # But that's ok, it is always possible to use a config option to set
1301 sources.append((get_win32_timezone(),"Windows Registry"))
1303 for (value,source) in sources:
1306 tz = pytz.timezone(value)
1307 loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_INFO,
1308 "Using timezone %s obtained from %s." % (tz.zone,source))
1310 except pytz.UnknownTimeZoneError:
1311 loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_WARNING,
1312 "The timezone specified in %s (%s) is invalid, ignoring it." % (source,value))
1314 loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_WARNING,
1315 "No valid timezone could be detected, using default UTC timezone. You can specify it explicitly with option 'timezone' in the server configuration.")
1318 def get_server_timezone():
1319 # timezone detection is safe in multithread, so lazy init is ok here
1320 if (not config['timezone']):
1321 config['timezone'] = detect_server_timezone()
1322 return config['timezone']
1325 DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"
1326 DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S"
1327 DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % (
1328 DEFAULT_SERVER_DATE_FORMAT,
1329 DEFAULT_SERVER_TIME_FORMAT)
1331 # Python's strftime supports only the format directives
1332 # that are available on the platform's libc, so in order to
1333 # be cross-platform we map to the directives required by
1334 # the C standard (1989 version), always available on platforms
1335 # with a C standard implementation.
1336 DATETIME_FORMATS_MAP = {
1338 '%D': '%m/%d/%Y', # modified %y->%Y
1340 '%E': '', # special modifier
1342 '%g': '%Y', # modified %y->%Y
1348 '%O': '', # special modifier
1351 '%r': '%I:%M:%S %p',
1352 '%s': '', #num of seconds since epoch
1357 '%y': '%Y', # Even if %y works, it's ambiguous, so we should use %Y
1358 '%+': '%Y-%m-%d %H:%M:%S',
1360 # %Z is a special case that causes 2 problems at least:
1361 # - the timezone names we use (in res_user.context_tz) come
1362 # from pytz, but not all these names are recognized by
1363 # strptime(), so we cannot convert in both directions
1364 # when such a timezone is selected and %Z is in the format
1365 # - %Z is replaced by an empty string in strftime() when
1366 # there is not tzinfo in a datetime value (e.g when the user
1367 # did not pick a context_tz). The resulting string does not
1368 # parse back if the format requires %Z.
1369 # As a consequence, we strip it completely from format strings.
1370 # The user can always have a look at the context_tz in
1371 # preferences to check the timezone.
1376 def server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_name,
1377 tz_offset=True, ignore_unparsable_time=True):
1379 Convert a source timestamp string into a destination timestamp string, attempting to apply the
1380 correct offset if both the server and local timezone are recognized, or no
1381 offset at all if they aren't or if tz_offset is false (i.e. assuming they are both in the same TZ).
1383 WARNING: This method is here to allow formatting dates correctly for inclusion in strings where
1384 the client would not be able to format/offset it correctly. DO NOT use it for returning
1385 date fields directly, these are supposed to be handled by the client!!
1387 @param src_tstamp_str: the str value containing the timestamp in the server timezone.
1388 @param src_format: the format to use when parsing the server timestamp.
1389 @param dst_format: the format to use when formatting the resulting timestamp for the local/client timezone.
1390 @param dst_tz_name: name of the destination timezone (such as the 'tz' value of the client context)
1391 @param ignore_unparsable_time: if True, return False if src_tstamp_str cannot be parsed
1392 using src_format or formatted using dst_format.
1394 @return: local/client formatted timestamp, expressed in the local/client timezone if possible
1395 and if tz_offset is true, or src_tstamp_str if timezone offset could not be determined.
1397 if not src_tstamp_str:
1400 res = src_tstamp_str
1401 if src_format and dst_format:
1402 # find out server timezone
1403 server_tz = get_server_timezone()
1405 # dt_value needs to be a datetime.datetime object (so no time.struct_time or mx.DateTime.DateTime here!)
1406 dt_value = datetime.strptime(src_tstamp_str, src_format)
1407 if tz_offset and dst_tz_name:
1410 src_tz = pytz.timezone(server_tz)
1411 dst_tz = pytz.timezone(dst_tz_name)
1412 src_dt = src_tz.localize(dt_value, is_dst=True)
1413 dt_value = src_dt.astimezone(dst_tz)
1416 res = dt_value.strftime(dst_format)
1418 # Normal ways to end up here are if strptime or strftime failed
1419 if not ignore_unparsable_time:
1424 def split_every(n, iterable, piece_maker=tuple):
1425 """Splits an iterable into length-n pieces. The last piece will be shorter
1426 if ``n`` does not evenly divide the iterable length.
1427 @param ``piece_maker``: function to build the pieces
1428 from the slices (tuple,list,...)
1430 iterator = iter(iterable)
1431 piece = piece_maker(islice(iterator, n))
1434 piece = piece_maker(islice(iterator, n))
1436 if __name__ == '__main__':
1440 class upload_data_thread(threading.Thread):
1441 def __init__(self, email, data, type):
1442 self.args = [('email',email),('type',type),('data',data)]
1443 super(upload_data_thread,self).__init__()
1447 args = urllib.urlencode(self.args)
1448 fp = urllib.urlopen('http://www.openerp.com/scripts/survey.php', args)
1454 def upload_data(email, data, type='SURVEY'):
1455 a = upload_data_thread(email, data, type)
1460 # port of python 2.6's attrgetter with support for dotted notation
1461 def resolve_attr(obj, attr):
1462 for name in attr.split("."):
1463 obj = getattr(obj, name)
1466 def attrgetter(*items):
1470 return resolve_attr(obj, attr)
1473 return tuple(resolve_attr(obj, attr) for attr in items)
1477 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: