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
62 import openerp.pooler as pooler
64 # get_encodings, ustr and exception_to_unicode were originally from tools.misc.
65 # There are moved to loglevels until we refactor tools.
66 from openerp.loglevels import get_encodings, ustr, exception_to_unicode
68 _logger = logging.getLogger('tools')
70 # List of etree._Element subclasses that we choose to ignore when parsing XML.
71 # We include the *Base ones just in case, currently they seem to be subclasses of the _* ones.
72 SKIPPED_ELEMENT_TYPES = (etree._Comment, etree._ProcessingInstruction, etree.CommentBase, etree.PIBase)
74 # initialize a database with base/base.sql
76 import openerp.addons as addons
77 f = addons.get_module_resource('base', 'base.sql')
78 base_sql_file = file_open(f)
80 cr.execute(base_sql_file.read())
85 for i in addons.get_modules():
86 mod_path = addons.get_module_path(i)
90 info = addons.load_information_from_description_file(i)
94 categs = info.get('category', 'Uncategorized').split('/')
98 cr.execute('SELECT id \
99 FROM ir_module_category \
100 WHERE name=%s AND parent_id=%s', (categs[0], p_id))
102 cr.execute('SELECT id \
103 FROM ir_module_category \
104 WHERE name=%s AND parent_id IS NULL', (categs[0],))
107 cr.execute('INSERT INTO ir_module_category \
109 VALUES (%s, %s) RETURNING id', (categs[0], p_id))
110 c_id = cr.fetchone()[0]
116 active = info.get('active', False)
117 installable = info.get('installable', True)
122 state = 'uninstalled'
124 state = 'uninstallable'
125 cr.execute('INSERT INTO ir_module_module \
126 (author, website, name, shortdesc, description, \
127 category_id, state, certificate, web, license) \
128 VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id', (
129 info.get('author', ''),
130 info.get('website', ''), i, info.get('name', False),
131 info.get('description', ''), p_id, state, info.get('certificate') or None,
132 info.get('web') or False,
133 info.get('license') or 'AGPL-3'))
134 id = cr.fetchone()[0]
135 cr.execute('INSERT INTO ir_model_data \
136 (name,model,module, res_id, noupdate) VALUES (%s,%s,%s,%s,%s)', (
137 'module_meta_information', 'ir.module.module', i, id, True))
138 dependencies = info.get('depends', [])
139 for d in dependencies:
140 cr.execute('INSERT INTO ir_module_module_dependency \
141 (module_id,name) VALUES (%s, %s)', (id, d))
144 def find_in_path(name):
150 def find_pg_tool(name):
152 if config['pg_path'] and config['pg_path'] != 'None':
153 path = config['pg_path']
155 return which(name, path=path)
159 def exec_pg_command(name, *args):
160 prog = find_pg_tool(name)
162 raise Exception('Couldn\'t find %s' % name)
163 args2 = (prog,) + args
165 return subprocess.call(args2)
167 def exec_pg_command_pipe(name, *args):
168 prog = find_pg_tool(name)
170 raise Exception('Couldn\'t find %s' % name)
171 # on win32, passing close_fds=True is not compatible
172 # with redirecting std[in/err/out]
173 pop = subprocess.Popen((prog,) + args, bufsize= -1,
174 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
175 close_fds=(os.name=="posix"))
176 return (pop.stdin, pop.stdout)
178 def exec_command_pipe(name, *args):
179 prog = find_in_path(name)
181 raise Exception('Couldn\'t find %s' % name)
182 # on win32, passing close_fds=True is not compatible
183 # with redirecting std[in/err/out]
184 pop = subprocess.Popen((prog,) + args, bufsize= -1,
185 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
186 close_fds=(os.name=="posix"))
187 return (pop.stdin, pop.stdout)
189 #----------------------------------------------------------
191 #----------------------------------------------------------
192 #file_path_root = os.getcwd()
193 #file_path_addons = os.path.join(file_path_root, 'addons')
195 def file_open(name, mode="r", subdir='addons', pathinfo=False):
196 """Open a file from the OpenERP root, using a subdir folder.
198 >>> file_open('hr/report/timesheer.xsl')
199 >>> file_open('addons/hr/report/timesheet.xsl')
200 >>> file_open('../../base/report/rml_template.xsl', subdir='addons/hr/report', pathinfo=True)
202 @param name: name of the file
203 @param mode: file open mode
204 @param subdir: subdirectory
205 @param pathinfo: if True returns tupple (fileobject, filepath)
207 @return: fileobject if pathinfo is False else (fileobject, filepath)
209 import openerp.addons as addons
210 adps = addons.ad_paths
211 rtp = os.path.normcase(os.path.abspath(config['root_path']))
213 if name.replace(os.path.sep, '/').startswith('addons/'):
217 # First try to locate in addons_path
220 if subdir2.replace(os.path.sep, '/').startswith('addons/'):
221 subdir2 = subdir2[7:]
223 subdir2 = (subdir2 != 'addons' or None) and subdir2
228 fn = os.path.join(adp, subdir2, name)
230 fn = os.path.join(adp, name)
231 fn = os.path.normpath(fn)
232 fo = file_open(fn, mode=mode, subdir=None, pathinfo=pathinfo)
240 name = os.path.join(rtp, subdir, name)
242 name = os.path.join(rtp, name)
244 name = os.path.normpath(name)
246 # Check for a zipfile in the path
251 head, tail = os.path.split(head)
255 zipname = os.path.join(tail, zipname)
258 if zipfile.is_zipfile(head+'.zip'):
259 from cStringIO import StringIO
260 zfile = zipfile.ZipFile(head+'.zip')
263 fo.write(zfile.read(os.path.join(
264 os.path.basename(head), zipname).replace(
271 name2 = os.path.normpath(os.path.join(head + '.zip', zipname))
273 for i in (name2, name):
274 if i and os.path.isfile(i):
279 if os.path.splitext(name)[1] == '.rml':
280 raise IOError, 'Report %s doesn\'t exist or deleted : ' %str(name)
281 raise IOError, 'File not found : %s' % name
284 #----------------------------------------------------------
286 #----------------------------------------------------------
288 """Flatten a list of elements into a uniqu list
289 Author: Christophe Simonis (christophe@tinyerp.com)
298 >>> flatten( [[], [[]]] )
300 >>> flatten( [[['a','b'], 'c'], 'd', ['e', [], 'f']] )
301 ['a', 'b', 'c', 'd', 'e', 'f']
302 >>> t = (1,2,(3,), [4, 5, [6, [7], (8, 9), ([10, 11, (12, 13)]), [14, [], (15,)], []]])
304 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
308 return hasattr(x, "__iter__")
313 map(r.append, flatten(e))
318 def reverse_enumerate(l):
319 """Like enumerate but in the other sens
320 >>> a = ['a', 'b', 'c']
321 >>> it = reverse_enumerate(a)
329 Traceback (most recent call last):
330 File "<stdin>", line 1, in <module>
333 return izip(xrange(len(l)-1, -1, -1), reversed(l))
335 #----------------------------------------------------------
337 #----------------------------------------------------------
338 email_re = re.compile(r"""
339 ([a-zA-Z][\w\.-]*[a-zA-Z0-9] # username part
341 [a-zA-Z0-9][\w\.-]* # domain must start with a letter ... Ged> why do we include a 0-9 then?
346 res_re = re.compile(r"\[([0-9]+)\]", re.UNICODE)
347 command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE)
348 reference_re = re.compile("<.*-openobject-(\\d+)@(.*)>", re.UNICODE)
350 def html2plaintext(html, body_id=None, encoding='utf-8'):
351 ## (c) Fry-IT, www.fry-it.com, 2007
352 ## <peter@fry-it.com>
353 ## download here: http://www.peterbe.com/plog/html2plaintext
356 """ from an HTML text, convert the HTML to plain text.
357 If @body_id is provided then this is the tag where the
358 body (not necessarily <body>) starts.
363 from lxml.etree import tostring
365 from lxml.html.soupparser import fromstring
368 _logger.debug('tools.misc.html2plaintext: cannot use BeautifulSoup, fallback to lxml.etree.HTMLParser')
369 from lxml.etree import fromstring, HTMLParser
370 kwargs = dict(parser=HTMLParser())
372 tree = fromstring(html, **kwargs)
374 if body_id is not None:
375 source = tree.xpath('//*[@id=%s]'%(body_id,))
377 source = tree.xpath('//body')
383 for link in tree.findall('.//a'):
384 url = link.get('href')
388 link.text = '%s [%s]' % (link.text, i)
389 url_index.append(url)
391 html = ustr(tostring(tree, encoding=encoding))
393 html = html.replace('<strong>','*').replace('</strong>','*')
394 html = html.replace('<b>','*').replace('</b>','*')
395 html = html.replace('<h3>','*').replace('</h3>','*')
396 html = html.replace('<h2>','**').replace('</h2>','**')
397 html = html.replace('<h1>','**').replace('</h1>','**')
398 html = html.replace('<em>','/').replace('</em>','/')
399 html = html.replace('<tr>', '\n')
400 html = html.replace('</p>', '\n')
401 html = re.sub('<br\s*/?>', '\n', html)
402 html = re.sub('<.*?>', ' ', html)
403 html = html.replace(' ' * 2, ' ')
406 html = '\n'.join([x.strip() for x in html.splitlines()])
407 html = html.replace('\n' * 2, '\n')
409 for i, url in enumerate(url_index):
412 html += ustr('[%s] %s\n') % (i+1, url)
416 def generate_tracking_message_id(openobject_id):
417 """Returns a string that can be used in the Message-ID RFC822 header field so we
418 can track the replies related to a given object thanks to the "In-Reply-To" or
419 "References" fields that Mail User Agents will set.
421 return "<%s-openobject-%s@%s>" % (time.time(), openobject_id, socket.gethostname())
424 def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
425 attach=None, message_id=None, references=None, openobject_id=False, debug=False, subtype='plain', x_headers=None, priority='3',
426 smtp_server=None, smtp_port=None, ssl=False, smtp_user=None, smtp_password=None, cr=None, uid=None):
432 `email_from`: A string used to fill the `From` header, if falsy,
433 config['email_from'] is used instead. Also used for
434 the `Reply-To` header if `reply_to` is not provided
436 `email_to`: a sequence of addresses to send the mail to.
439 # If not cr, get cr from current thread database
441 db_name = getattr(threading.currentThread(), 'dbname', None)
443 cr = pooler.get_db_only(db_name).cursor()
445 raise Exception("No database cursor found!")
447 # if not uid, take uid as a root
448 #TOFIX: uid should taken from current thread
452 if not (email_from or config['email_from']):
453 raise ValueError("Sending an email requires either providing a sender "
454 "address or having configured one")
456 if not email_from: email_from = config.get('email_from', False)
458 email_from = ustr(email_from).encode('utf-8')
460 mail_server_pool = pooler.get_pool(cr.dbname).get('ir.mail_server')
462 msg = mail_server_pool.pack_message(cr, uid, subject, body, email_cc, email_bcc, reply_to,
463 attach, message_id, references, openobject_id, debug, subtype, x_headers, priority)
468 res = mail_server_pool.send_email(cr, uid, email_from, flatten([email_to, email_cc, email_bcc]), msg, ssl=ssl, debug=debug,
469 smtp_server=smtp_server, smtp_port=smtp_port, smtp_user=smtp_user, smtp_password=smtp_password)
477 #----------------------------------------------------------
479 #----------------------------------------------------------
480 # text must be latin-1 encoded
481 def sms_send(user, password, api_id, text, to):
483 url = "http://api.urlsms.com/SendSMS.aspx"
484 #url = "http://196.7.150.220/http/sendmsg"
485 params = urllib.urlencode({'UserID': user, 'Password': password, 'SenderID': api_id, 'MsgText': text, 'RecipientMobileNo':to})
486 urllib.urlopen(url+"?"+params)
487 # FIXME: Use the logger if there is an error
490 #---------------------------------------------------------
491 # Class that stores an updateable string (used in wizards)
492 #---------------------------------------------------------
493 class UpdateableStr(local):
495 def __init__(self, string=''):
499 return str(self.string)
502 return str(self.string)
504 def __nonzero__(self):
505 return bool(self.string)
508 class UpdateableDict(local):
509 '''Stores an updateable dict to use in wizards'''
511 def __init__(self, dict=None):
517 return str(self.dict)
520 return str(self.dict)
523 return self.dict.clear()
526 return self.dict.keys()
528 def __setitem__(self, i, y):
529 self.dict.__setitem__(i, y)
531 def __getitem__(self, i):
532 return self.dict.__getitem__(i)
535 return self.dict.copy()
538 return self.dict.iteritems()
541 return self.dict.iterkeys()
543 def itervalues(self):
544 return self.dict.itervalues()
546 def pop(self, k, d=None):
547 return self.dict.pop(k, d)
550 return self.dict.popitem()
552 def setdefault(self, k, d=None):
553 return self.dict.setdefault(k, d)
555 def update(self, E, **F):
556 return self.dict.update(E, F)
559 return self.dict.values()
561 def get(self, k, d=None):
562 return self.dict.get(k, d)
564 def has_key(self, k):
565 return self.dict.has_key(k)
568 return self.dict.items()
570 def __cmp__(self, y):
571 return self.dict.__cmp__(y)
573 def __contains__(self, k):
574 return self.dict.__contains__(k)
576 def __delitem__(self, y):
577 return self.dict.__delitem__(y)
580 return self.dict.__eq__(y)
583 return self.dict.__ge__(y)
586 return self.dict.__gt__(y)
589 return self.dict.__hash__()
592 return self.dict.__iter__()
595 return self.dict.__le__(y)
598 return self.dict.__len__()
601 return self.dict.__lt__(y)
604 return self.dict.__ne__(y)
607 # Don't use ! Use res.currency.round()
608 class currency(float):
610 def __init__(self, value, accuracy=2, rounding=None):
612 rounding=10**-accuracy
613 self.rounding=rounding
614 self.accuracy=accuracy
616 def __new__(cls, value, accuracy=2, rounding=None):
617 return float.__new__(cls, round(value, accuracy))
620 # display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
621 # return str(display_value)
633 Use it as a decorator of the function you plan to cache
634 Timeout: 0 = no timeout, otherwise in seconds
639 def __init__(self, timeout=None, skiparg=2, multi=None, size=8192):
640 assert skiparg >= 2 # at least self and cr
642 self.timeout = config['cache_timeout']
644 self.timeout = timeout
645 self.skiparg = skiparg
647 self.lasttime = time.time()
648 self.cache = LRU(size) # TODO take size from config
650 cache.__caches.append(self)
653 def _generate_keys(self, dbname, kwargs2):
655 Generate keys depending of the arguments and the self.mutli value
660 pairs.sort(key=lambda (k,v): k)
661 for i, (k, v) in enumerate(pairs):
662 if isinstance(v, dict):
663 pairs[i] = (k, to_tuple(v))
664 if isinstance(v, (list, set)):
665 pairs[i] = (k, tuple(v))
666 elif not is_hashable(v):
667 pairs[i] = (k, repr(v))
671 key = (('dbname', dbname),) + to_tuple(kwargs2)
674 multis = kwargs2[self.multi][:]
676 kwargs2[self.multi] = (id,)
677 key = (('dbname', dbname),) + to_tuple(kwargs2)
680 def _unify_args(self, *args, **kwargs):
681 # Update named arguments with positional argument values (without self and cr)
682 kwargs2 = self.fun_default_values.copy()
683 kwargs2.update(kwargs)
684 kwargs2.update(dict(zip(self.fun_arg_names, args[self.skiparg-2:])))
687 def clear(self, dbname, *args, **kwargs):
688 """clear the cache for database dbname
689 if *args and **kwargs are both empty, clear all the keys related to this database
691 if not args and not kwargs:
692 keys_to_del = [key for key in self.cache.keys() if key[0][1] == dbname]
694 kwargs2 = self._unify_args(*args, **kwargs)
695 keys_to_del = [key for key, _ in self._generate_keys(dbname, kwargs2) if key in self.cache.keys()]
697 for key in keys_to_del:
701 def clean_caches_for_db(cls, dbname):
702 for c in cls.__caches:
705 def __call__(self, fn):
706 if self.fun is not None:
707 raise Exception("Can not use a cache instance on more than one function")
710 argspec = inspect.getargspec(fn)
711 self.fun_arg_names = argspec[0][self.skiparg:]
712 self.fun_default_values = {}
714 self.fun_default_values = dict(zip(self.fun_arg_names[-len(argspec[3]):], argspec[3]))
716 def cached_result(self2, cr, *args, **kwargs):
717 if time.time()-int(self.timeout) > self.lasttime:
718 self.lasttime = time.time()
719 t = time.time()-int(self.timeout)
720 old_keys = [key for key in self.cache.keys() if self.cache[key][1] < t]
724 kwargs2 = self._unify_args(*args, **kwargs)
728 for key, id in self._generate_keys(cr.dbname, kwargs2):
729 if key in self.cache:
730 result[id] = self.cache[key][0]
736 kwargs2[self.multi] = notincache.keys()
738 result2 = fn(self2, cr, *args[:self.skiparg-2], **kwargs2)
740 key = notincache[None]
741 self.cache[key] = (result2, time.time())
742 result[None] = result2
746 self.cache[key] = (result2[id], time.time())
747 result.update(result2)
753 cached_result.clear_cache = self.clear
757 return s.replace('&','&').replace('<','<').replace('>','>')
759 # to be compatible with python 2.4
761 if not hasattr(__builtin__, 'all'):
763 for element in iterable:
768 __builtin__.all = all
771 if not hasattr(__builtin__, 'any'):
773 for element in iterable:
778 __builtin__.any = any
781 def get_iso_codes(lang):
782 if lang.find('_') != -1:
783 if lang.split('_')[0] == lang.split('_')[1].lower():
784 lang = lang.split('_')[0]
788 # The codes below are those from Launchpad's Rosetta, with the exception
789 # of some trivial codes where the Launchpad code is xx and we have xx_XX.
791 'ab_RU': u'Abkhazian / аҧсуа',
792 'ar_AR': u'Arabic / الْعَرَبيّة',
793 'bg_BG': u'Bulgarian / български език',
794 'bs_BS': u'Bosnian / bosanski jezik',
795 'ca_ES': u'Catalan / Català',
796 'cs_CZ': u'Czech / Čeština',
797 'da_DK': u'Danish / Dansk',
798 'de_DE': u'German / Deutsch',
799 'el_GR': u'Greek / Ελληνικά',
800 'en_CA': u'English (CA)',
801 'en_GB': u'English (UK)',
802 'en_US': u'English (US)',
803 'es_AR': u'Spanish (AR) / Español (AR)',
804 'es_BO': u'Spanish (BO) / Español (BO)',
805 'es_CL': u'Spanish (CL) / Español (CL)',
806 'es_CO': u'Spanish (CO) / Español (CO)',
807 'es_CR': u'Spanish (CR) / Español (CR)',
808 'es_DO': u'Spanish (DO) / Español (DO)',
809 'es_EC': u'Spanish (EC) / Español (EC)',
810 'es_ES': u'Spanish / Español',
811 'es_GT': u'Spanish (GT) / Español (GT)',
812 'es_HN': u'Spanish (HN) / Español (HN)',
813 'es_MX': u'Spanish (MX) / Español (MX)',
814 'es_NI': u'Spanish (NI) / Español (NI)',
815 'es_PA': u'Spanish (PA) / Español (PA)',
816 'es_PE': u'Spanish (PE) / Español (PE)',
817 'es_PR': u'Spanish (PR) / Español (PR)',
818 'es_PY': u'Spanish (PY) / Español (PY)',
819 'es_SV': u'Spanish (SV) / Español (SV)',
820 'es_UY': u'Spanish (UY) / Español (UY)',
821 'es_VE': u'Spanish (VE) / Español (VE)',
822 'et_EE': u'Estonian / Eesti keel',
823 'fa_IR': u'Persian / فارس',
824 'fi_FI': u'Finnish / Suomi',
825 'fr_BE': u'French (BE) / Français (BE)',
826 'fr_CH': u'French (CH) / Français (CH)',
827 'fr_FR': u'French / Français',
828 'gl_ES': u'Galician / Galego',
829 'gu_IN': u'Gujarati / ગુજરાતી',
830 'he_IL': u'Hebrew / עִבְרִי',
831 'hi_IN': u'Hindi / हिंदी',
832 'hr_HR': u'Croatian / hrvatski jezik',
833 'hu_HU': u'Hungarian / Magyar',
834 'id_ID': u'Indonesian / Bahasa Indonesia',
835 'it_IT': u'Italian / Italiano',
836 'iu_CA': u'Inuktitut / ᐃᓄᒃᑎᑐᑦ',
837 'ja_JP': u'Japanese / 日本語',
838 'ko_KP': u'Korean (KP) / 한국어 (KP)',
839 'ko_KR': u'Korean (KR) / 한국어 (KR)',
840 'lt_LT': u'Lithuanian / Lietuvių kalba',
841 'lv_LV': u'Latvian / latviešu valoda',
842 'ml_IN': u'Malayalam / മലയാളം',
843 'mn_MN': u'Mongolian / монгол',
844 'nb_NO': u'Norwegian Bokmål / Norsk bokmål',
845 'nl_NL': u'Dutch / Nederlands',
846 'nl_BE': u'Flemish (BE) / Vlaams (BE)',
847 'oc_FR': u'Occitan (FR, post 1500) / Occitan',
848 'pl_PL': u'Polish / Język polski',
849 'pt_BR': u'Portugese (BR) / Português (BR)',
850 'pt_PT': u'Portugese / Português',
851 'ro_RO': u'Romanian / română',
852 'ru_RU': u'Russian / русский язык',
853 'si_LK': u'Sinhalese / සිංහල',
854 'sl_SI': u'Slovenian / slovenščina',
855 'sk_SK': u'Slovak / Slovenský jazyk',
856 'sq_AL': u'Albanian / Shqip',
857 'sr_RS': u'Serbian (Cyrillic) / српски',
858 'sr@latin': u'Serbian (Latin) / srpski',
859 'sv_SE': u'Swedish / svenska',
860 'te_IN': u'Telugu / తెలుగు',
861 'tr_TR': u'Turkish / Türkçe',
862 'vi_VN': u'Vietnamese / Tiếng Việt',
863 'uk_UA': u'Ukrainian / українська',
864 'ur_PK': u'Urdu / اردو',
865 'zh_CN': u'Chinese (CN) / 简体中文',
866 'zh_HK': u'Chinese (HK)',
867 'zh_TW': u'Chinese (TW) / 正體字',
868 'th_TH': u'Thai / ภาษาไทย',
869 'tlh_TLH': u'Klingon',
873 def scan_languages():
874 # Now it will take all languages from get languages function without filter it with base module languages
875 lang_dict = get_languages()
876 ret = [(lang, lang_dict.get(lang, lang)) for lang in list(lang_dict)]
877 ret.sort(key=lambda k:k[1])
881 def get_user_companies(cr, user):
882 def _get_company_children(cr, ids):
885 cr.execute('SELECT id FROM res_company WHERE parent_id IN %s', (tuple(ids),))
886 res = [x[0] for x in cr.fetchall()]
887 res.extend(_get_company_children(cr, res))
889 cr.execute('SELECT company_id FROM res_users WHERE id=%s', (user,))
890 user_comp = cr.fetchone()[0]
893 return [user_comp] + _get_company_children(cr, [user_comp])
897 Input number : account or invoice number
898 Output return: the same number completed with the recursive mod10
901 codec=[0,9,4,6,8,2,7,1,3,5]
907 report = codec[ (int(digit) + report) % 10 ]
908 return result + str((10 - report) % 10)
913 Return the size in a human readable format
917 units = ('bytes', 'Kb', 'Mb', 'Gb')
918 if isinstance(sz,basestring):
921 while s >= 1024 and i < len(units)-1:
924 return "%0.2f %s" % (s, units[i])
927 from func import wraps
930 def wrapper(*args, **kwargs):
931 from pprint import pformat
933 vector = ['Call -> function: %r' % f]
934 for i, arg in enumerate(args):
935 vector.append(' arg %02d: %s' % (i, pformat(arg)))
936 for key, value in kwargs.items():
937 vector.append(' kwarg %10s: %s' % (key, pformat(value)))
940 res = f(*args, **kwargs)
942 vector.append(' result: %s' % pformat(res))
943 vector.append(' time delta: %s' % (time.time() - timeb4))
944 loglevels.Logger().notifyChannel('logged', loglevels.LOG_DEBUG, '\n'.join(vector))
949 class profile(object):
950 def __init__(self, fname=None):
953 def __call__(self, f):
954 from func import wraps
957 def wrapper(*args, **kwargs):
958 class profile_wrapper(object):
962 self.result = f(*args, **kwargs)
963 pw = profile_wrapper()
965 fname = self.fname or ("%s.cprof" % (f.func_name,))
966 cProfile.runctx('pw()', globals(), locals(), filename=fname)
973 This method allow you to debug your code without print
975 >>> def func_foo(bar)
983 This will output on the logger:
985 [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:baz = 42
986 [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:qnx = (42, 42)
988 To view the DEBUG lines in the logger you must start the server with the option
992 warnings.warn("The tools.debug() method is deprecated, please use logging.",
993 DeprecationWarning, stacklevel=2)
994 from inspect import stack
995 from pprint import pformat
997 param = re.split("debug *\((.+)\)", st[4][0].strip())[1].strip()
998 while param.count(')') > param.count('('): param = param[:param.rfind(')')]
1001 what = "%s = %s" % (param, what)
1002 logging.getLogger(st[3]).debug(what)
1005 __icons_list = ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
1006 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
1007 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
1008 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
1009 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
1010 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
1011 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
1012 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
1013 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
1014 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
1015 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
1016 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
1017 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
1018 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
1019 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
1020 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
1021 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
1022 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
1023 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
1024 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
1025 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
1026 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
1027 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
1028 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
1029 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
1030 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
1031 'terp-check','terp-go-month','terp-go-year','terp-go-today','terp-document-new','terp-camera_test',
1032 'terp-emblem-important','terp-gtk-media-pause','terp-gtk-stop','terp-gnome-cpu-frequency-applet+',
1033 'terp-dialog-close','terp-gtk-jump-to-rtl','terp-gtk-jump-to-ltr','terp-accessories-archiver',
1034 'terp-stock_align_left_24','terp-stock_effects-object-colorize','terp-go-home','terp-gtk-go-back-rtl',
1035 'terp-gtk-go-back-ltr','terp-personal','terp-personal-','terp-personal+','terp-accessories-archiver-minus',
1036 'terp-accessories-archiver+','terp-stock_symbol-selection','terp-call-start','terp-dolar',
1037 'terp-face-plain','terp-folder-blue','terp-folder-green','terp-folder-orange','terp-folder-yellow',
1038 'terp-gdu-smart-failing','terp-go-week','terp-gtk-select-all','terp-locked','terp-mail-forward',
1039 'terp-mail-message-new','terp-mail-replied','terp-rating-rated','terp-stage','terp-stock_format-scientific',
1040 'terp-dolar_ok!','terp-idea','terp-stock_format-default','terp-mail-','terp-mail_delete'
1043 def icons(*a, **kw):
1045 return [(x, x) for x in __icons_list ]
1047 def extract_zip_file(zip_file, outdirectory):
1048 zf = zipfile.ZipFile(zip_file, 'r')
1050 for path in zf.namelist():
1051 tgt = os.path.join(out, path)
1052 tgtdir = os.path.dirname(tgt)
1053 if not os.path.exists(tgtdir):
1056 if not tgt.endswith(os.sep):
1057 fp = open(tgt, 'wb')
1058 fp.write(zf.read(path))
1062 def detect_ip_addr():
1063 """Try a very crude method to figure out a valid external
1064 IP or hostname for the current machine. Don't rely on this
1065 for binding to an interface, but it could be used as basis
1066 for constructing a remote URL to the server.
1068 def _detect_ip_addr():
1069 from array import array
1070 from struct import pack, unpack
1079 if not fcntl: # not UNIX:
1080 host = socket.gethostname()
1081 ip_addr = socket.gethostbyname(host)
1083 # get all interfaces:
1085 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1086 names = array('B', '\0' * nbytes)
1087 #print 'names: ', names
1088 outbytes = unpack('iL', fcntl.ioctl( s.fileno(), 0x8912, pack('iL', nbytes, names.buffer_info()[0])))[0]
1089 namestr = names.tostring()
1091 # try 64 bit kernel:
1092 for i in range(0, outbytes, 40):
1093 name = namestr[i:i+16].split('\0', 1)[0]
1095 ip_addr = socket.inet_ntoa(namestr[i+20:i+24])
1098 # try 32 bit kernel:
1100 ifaces = filter(None, [namestr[i:i+32].split('\0', 1)[0] for i in range(0, outbytes, 32)])
1102 for ifname in [iface for iface in ifaces if iface != 'lo']:
1103 ip_addr = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, pack('256s', ifname[:15]))[20:24])
1106 return ip_addr or 'localhost'
1109 ip_addr = _detect_ip_addr()
1111 ip_addr = 'localhost'
1114 # RATIONALE BEHIND TIMESTAMP CALCULATIONS AND TIMEZONE MANAGEMENT:
1115 # The server side never does any timestamp calculation, always
1116 # sends them in a naive (timezone agnostic) format supposed to be
1117 # expressed within the server timezone, and expects the clients to
1118 # provide timestamps in the server timezone as well.
1119 # It stores all timestamps in the database in naive format as well,
1120 # which also expresses the time in the server timezone.
1121 # For this reason the server makes its timezone name available via the
1122 # common/timezone_get() rpc method, which clients need to read
1123 # to know the appropriate time offset to use when reading/writing
1125 def get_win32_timezone():
1126 """Attempt to return the "standard name" of the current timezone on a win32 system.
1127 @return: the standard name of the current win32 timezone, or False if it cannot be found.
1130 if (sys.platform == "win32"):
1133 hklm = _winreg.ConnectRegistry(None,_winreg.HKEY_LOCAL_MACHINE)
1134 current_tz_key = _winreg.OpenKey(hklm, r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", 0,_winreg.KEY_ALL_ACCESS)
1135 res = str(_winreg.QueryValueEx(current_tz_key,"StandardName")[0]) # [0] is value, [1] is type code
1136 _winreg.CloseKey(current_tz_key)
1137 _winreg.CloseKey(hklm)
1142 def detect_server_timezone():
1143 """Attempt to detect the timezone to use on the server side.
1144 Defaults to UTC if no working timezone can be found.
1145 @return: the timezone identifier as expected by pytz.timezone.
1150 loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_WARNING,
1151 "Python pytz module is not available. Timezone will be set to UTC by default.")
1154 # Option 1: the configuration option (did not exist before, so no backwards compatibility issue)
1155 # 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
1156 # Option 3: the environment variable TZ
1157 sources = [ (config['timezone'], 'OpenERP configuration'),
1158 (time.tzname[0], 'time.tzname'),
1159 (os.environ.get('TZ',False),'TZ environment variable'), ]
1160 # Option 4: OS-specific: /etc/timezone on Unix
1161 if (os.path.exists("/etc/timezone")):
1164 f = open("/etc/timezone")
1165 tz_value = f.read(128).strip()
1170 sources.append((tz_value,"/etc/timezone file"))
1171 # Option 5: timezone info from registry on Win32
1172 if (sys.platform == "win32"):
1173 # Timezone info is stored in windows registry.
1174 # However this is not likely to work very well as the standard name
1175 # of timezones in windows is rarely something that is known to pytz.
1176 # But that's ok, it is always possible to use a config option to set
1178 sources.append((get_win32_timezone(),"Windows Registry"))
1180 for (value,source) in sources:
1183 tz = pytz.timezone(value)
1184 loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_INFO,
1185 "Using timezone %s obtained from %s." % (tz.zone,source))
1187 except pytz.UnknownTimeZoneError:
1188 loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_WARNING,
1189 "The timezone specified in %s (%s) is invalid, ignoring it." % (source,value))
1191 loglevels.Logger().notifyChannel("detect_server_timezone", loglevels.LOG_WARNING,
1192 "No valid timezone could be detected, using default UTC timezone. You can specify it explicitly with option 'timezone' in the server configuration.")
1195 def get_server_timezone():
1196 # timezone detection is safe in multithread, so lazy init is ok here
1197 if (not config['timezone']):
1198 config['timezone'] = detect_server_timezone()
1199 return config['timezone']
1202 DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"
1203 DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S"
1204 DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % (
1205 DEFAULT_SERVER_DATE_FORMAT,
1206 DEFAULT_SERVER_TIME_FORMAT)
1208 # Python's strftime supports only the format directives
1209 # that are available on the platform's libc, so in order to
1210 # be cross-platform we map to the directives required by
1211 # the C standard (1989 version), always available on platforms
1212 # with a C standard implementation.
1213 DATETIME_FORMATS_MAP = {
1215 '%D': '%m/%d/%Y', # modified %y->%Y
1217 '%E': '', # special modifier
1219 '%g': '%Y', # modified %y->%Y
1225 '%O': '', # special modifier
1228 '%r': '%I:%M:%S %p',
1229 '%s': '', #num of seconds since epoch
1234 '%y': '%Y', # Even if %y works, it's ambiguous, so we should use %Y
1235 '%+': '%Y-%m-%d %H:%M:%S',
1237 # %Z is a special case that causes 2 problems at least:
1238 # - the timezone names we use (in res_user.context_tz) come
1239 # from pytz, but not all these names are recognized by
1240 # strptime(), so we cannot convert in both directions
1241 # when such a timezone is selected and %Z is in the format
1242 # - %Z is replaced by an empty string in strftime() when
1243 # there is not tzinfo in a datetime value (e.g when the user
1244 # did not pick a context_tz). The resulting string does not
1245 # parse back if the format requires %Z.
1246 # As a consequence, we strip it completely from format strings.
1247 # The user can always have a look at the context_tz in
1248 # preferences to check the timezone.
1253 def server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_name,
1254 tz_offset=True, ignore_unparsable_time=True):
1256 Convert a source timestamp string into a destination timestamp string, attempting to apply the
1257 correct offset if both the server and local timezone are recognized, or no
1258 offset at all if they aren't or if tz_offset is false (i.e. assuming they are both in the same TZ).
1260 WARNING: This method is here to allow formatting dates correctly for inclusion in strings where
1261 the client would not be able to format/offset it correctly. DO NOT use it for returning
1262 date fields directly, these are supposed to be handled by the client!!
1264 @param src_tstamp_str: the str value containing the timestamp in the server timezone.
1265 @param src_format: the format to use when parsing the server timestamp.
1266 @param dst_format: the format to use when formatting the resulting timestamp for the local/client timezone.
1267 @param dst_tz_name: name of the destination timezone (such as the 'tz' value of the client context)
1268 @param ignore_unparsable_time: if True, return False if src_tstamp_str cannot be parsed
1269 using src_format or formatted using dst_format.
1271 @return: local/client formatted timestamp, expressed in the local/client timezone if possible
1272 and if tz_offset is true, or src_tstamp_str if timezone offset could not be determined.
1274 if not src_tstamp_str:
1277 res = src_tstamp_str
1278 if src_format and dst_format:
1279 # find out server timezone
1280 server_tz = get_server_timezone()
1282 # dt_value needs to be a datetime.datetime object (so no time.struct_time or mx.DateTime.DateTime here!)
1283 dt_value = datetime.strptime(src_tstamp_str, src_format)
1284 if tz_offset and dst_tz_name:
1287 src_tz = pytz.timezone(server_tz)
1288 dst_tz = pytz.timezone(dst_tz_name)
1289 src_dt = src_tz.localize(dt_value, is_dst=True)
1290 dt_value = src_dt.astimezone(dst_tz)
1293 res = dt_value.strftime(dst_format)
1295 # Normal ways to end up here are if strptime or strftime failed
1296 if not ignore_unparsable_time:
1301 def split_every(n, iterable, piece_maker=tuple):
1302 """Splits an iterable into length-n pieces. The last piece will be shorter
1303 if ``n`` does not evenly divide the iterable length.
1304 @param ``piece_maker``: function to build the pieces
1305 from the slices (tuple,list,...)
1307 iterator = iter(iterable)
1308 piece = piece_maker(islice(iterator, n))
1311 piece = piece_maker(islice(iterator, n))
1313 if __name__ == '__main__':
1317 class upload_data_thread(threading.Thread):
1318 def __init__(self, email, data, type):
1319 self.args = [('email',email),('type',type),('data',data)]
1320 super(upload_data_thread,self).__init__()
1324 args = urllib.urlencode(self.args)
1325 fp = urllib.urlopen('http://www.openerp.com/scripts/survey.php', args)
1331 def upload_data(email, data, type='SURVEY'):
1332 a = upload_data_thread(email, data, type)
1337 # port of python 2.6's attrgetter with support for dotted notation
1338 def resolve_attr(obj, attr):
1339 for name in attr.split("."):
1340 obj = getattr(obj, name)
1343 def attrgetter(*items):
1347 return resolve_attr(obj, attr)
1350 return tuple(resolve_attr(obj, attr) for attr in items)
1354 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: