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-2014 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 ##############################################################################
25 Miscellaneous tools used by OpenERP.
28 from functools import wraps
30 from contextlib import contextmanager
40 from collections import defaultdict, Mapping
41 from datetime import datetime
42 from itertools import islice, izip, groupby
43 from lxml import etree
44 from which import which
45 from threading import local
49 from html2text import html2text
53 from config import config
55 from .parse_version import parse_version
58 # get_encodings, ustr and exception_to_unicode were originally from tools.misc.
59 # There are moved to loglevels until we refactor tools.
60 from openerp.loglevels import get_encodings, ustr, exception_to_unicode # noqa
62 _logger = logging.getLogger(__name__)
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 def find_in_path(name):
74 def find_pg_tool(name):
76 if config['pg_path'] and config['pg_path'] != 'None':
77 path = config['pg_path']
79 return which(name, path=path)
83 def exec_pg_command(name, *args):
84 prog = find_pg_tool(name)
86 raise Exception('Couldn\'t find %s' % name)
87 args2 = (prog,) + args
89 with open(os.devnull) as dn:
90 return subprocess.call(args2, stdout=dn, stderr=subprocess.STDOUT)
92 def exec_pg_command_pipe(name, *args):
93 prog = find_pg_tool(name)
95 raise Exception('Couldn\'t find %s' % name)
96 # on win32, passing close_fds=True is not compatible
97 # with redirecting std[in/err/out]
98 pop = subprocess.Popen((prog,) + args, bufsize= -1,
99 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
100 close_fds=(os.name=="posix"))
101 return pop.stdin, pop.stdout
103 def exec_command_pipe(name, *args):
104 prog = find_in_path(name)
106 raise Exception('Couldn\'t find %s' % name)
107 # on win32, passing close_fds=True is not compatible
108 # with redirecting std[in/err/out]
109 pop = subprocess.Popen((prog,) + args, bufsize= -1,
110 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
111 close_fds=(os.name=="posix"))
112 return pop.stdin, pop.stdout
114 #----------------------------------------------------------
116 #----------------------------------------------------------
117 #file_path_root = os.getcwd()
118 #file_path_addons = os.path.join(file_path_root, 'addons')
120 def file_open(name, mode="r", subdir='addons', pathinfo=False):
121 """Open a file from the OpenERP root, using a subdir folder.
125 >>> file_open('hr/report/timesheer.xsl')
126 >>> file_open('addons/hr/report/timesheet.xsl')
127 >>> file_open('../../base/report/rml_template.xsl', subdir='addons/hr/report', pathinfo=True)
129 @param name name of the file
130 @param mode file open mode
131 @param subdir subdirectory
132 @param pathinfo if True returns tuple (fileobject, filepath)
134 @return fileobject if pathinfo is False else (fileobject, filepath)
136 import openerp.modules as addons
137 adps = addons.module.ad_paths
138 rtp = os.path.normcase(os.path.abspath(config['root_path']))
142 if os.path.isabs(name):
143 # It is an absolute path
144 # Is it below 'addons_path' or 'root_path'?
145 name = os.path.normcase(os.path.normpath(name))
146 for root in adps + [rtp]:
147 root = os.path.normcase(os.path.normpath(root)) + os.sep
148 if name.startswith(root):
149 base = root.rstrip(os.sep)
150 name = name[len(base) + 1:]
153 # It is outside the OpenERP root: skip zipfile lookup.
154 base, name = os.path.split(name)
155 return _fileopen(name, mode=mode, basedir=base, pathinfo=pathinfo, basename=basename)
157 if name.replace(os.sep, '/').startswith('addons/'):
161 name = os.path.join(subdir, name)
162 if name.replace(os.sep, '/').startswith('addons/'):
168 # First, try to locate in addons_path
172 return _fileopen(name2, mode=mode, basedir=adp,
173 pathinfo=pathinfo, basename=basename)
177 # Second, try to locate in root_path
178 return _fileopen(name, mode=mode, basedir=rtp, pathinfo=pathinfo, basename=basename)
181 def _fileopen(path, mode, basedir, pathinfo, basename=None):
182 name = os.path.normpath(os.path.join(basedir, path))
186 # Give higher priority to module directories, which is
187 # a more common case than zipped modules.
188 if os.path.isfile(name):
189 fo = open(name, mode)
194 # Support for loading modules in zipped form.
195 # This will not work for zipped modules that are sitting
196 # outside of known addons paths.
197 head = os.path.normpath(path)
199 while os.sep in head:
200 head, tail = os.path.split(head)
204 zipname = os.path.join(tail, zipname)
207 zpath = os.path.join(basedir, head + '.zip')
208 if zipfile.is_zipfile(zpath):
209 from cStringIO import StringIO
210 zfile = zipfile.ZipFile(zpath)
213 fo.write(zfile.read(os.path.join(
214 os.path.basename(head), zipname).replace(
223 if name.endswith('.rml'):
224 raise IOError('Report %r doesn\'t exist or deleted' % basename)
225 raise IOError('File not found: %s' % basename)
228 #----------------------------------------------------------
230 #----------------------------------------------------------
232 """Flatten a list of elements into a uniqu list
233 Author: Christophe Simonis (christophe@tinyerp.com)
242 >>> flatten( [[], [[]]] )
244 >>> flatten( [[['a','b'], 'c'], 'd', ['e', [], 'f']] )
245 ['a', 'b', 'c', 'd', 'e', 'f']
246 >>> t = (1,2,(3,), [4, 5, [6, [7], (8, 9), ([10, 11, (12, 13)]), [14, [], (15,)], []]])
248 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
252 return hasattr(x, "__iter__")
257 map(r.append, flatten(e))
262 def reverse_enumerate(l):
263 """Like enumerate but in the other sens
266 >>> a = ['a', 'b', 'c']
267 >>> it = reverse_enumerate(a)
275 Traceback (most recent call last):
276 File "<stdin>", line 1, in <module>
279 return izip(xrange(len(l)-1, -1, -1), reversed(l))
282 class UpdateableStr(local):
283 """ Class that stores an updateable string (used in wizards)
286 def __init__(self, string=''):
290 return str(self.string)
293 return str(self.string)
295 def __nonzero__(self):
296 return bool(self.string)
299 class UpdateableDict(local):
300 """Stores an updateable dict to use in wizards
303 def __init__(self, dict=None):
309 return str(self.dict)
312 return str(self.dict)
315 return self.dict.clear()
318 return self.dict.keys()
320 def __setitem__(self, i, y):
321 self.dict.__setitem__(i, y)
323 def __getitem__(self, i):
324 return self.dict.__getitem__(i)
327 return self.dict.copy()
330 return self.dict.iteritems()
333 return self.dict.iterkeys()
335 def itervalues(self):
336 return self.dict.itervalues()
338 def pop(self, k, d=None):
339 return self.dict.pop(k, d)
342 return self.dict.popitem()
344 def setdefault(self, k, d=None):
345 return self.dict.setdefault(k, d)
347 def update(self, E, **F):
348 return self.dict.update(E, F)
351 return self.dict.values()
353 def get(self, k, d=None):
354 return self.dict.get(k, d)
356 def has_key(self, k):
357 return self.dict.has_key(k)
360 return self.dict.items()
362 def __cmp__(self, y):
363 return self.dict.__cmp__(y)
365 def __contains__(self, k):
366 return self.dict.__contains__(k)
368 def __delitem__(self, y):
369 return self.dict.__delitem__(y)
372 return self.dict.__eq__(y)
375 return self.dict.__ge__(y)
378 return self.dict.__gt__(y)
381 return self.dict.__hash__()
384 return self.dict.__iter__()
387 return self.dict.__le__(y)
390 return self.dict.__len__()
393 return self.dict.__lt__(y)
396 return self.dict.__ne__(y)
398 class currency(float):
403 Don't use ! Use res.currency.round()
406 def __init__(self, value, accuracy=2, rounding=None):
408 rounding=10**-accuracy
409 self.rounding=rounding
410 self.accuracy=accuracy
412 def __new__(cls, value, accuracy=2, rounding=None):
413 return float.__new__(cls, round(value, accuracy))
416 # display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
417 # return str(display_value)
420 return s.replace('&','&').replace('<','<').replace('>','>')
422 def get_iso_codes(lang):
423 if lang.find('_') != -1:
424 if lang.split('_')[0] == lang.split('_')[1].lower():
425 lang = lang.split('_')[0]
429 'ab_RU': u'Abkhazian / аҧсуа',
430 'am_ET': u'Amharic / አምሃርኛ',
431 'ar_SY': u'Arabic / الْعَرَبيّة',
432 'bg_BG': u'Bulgarian / български език',
433 'bs_BS': u'Bosnian / bosanski jezik',
434 'ca_ES': u'Catalan / Català',
435 'cs_CZ': u'Czech / Čeština',
436 'da_DK': u'Danish / Dansk',
437 'de_DE': u'German / Deutsch',
438 'el_GR': u'Greek / Ελληνικά',
439 'en_CA': u'English (CA)',
440 'en_GB': u'English (UK)',
441 'en_US': u'English (US)',
442 'es_AR': u'Spanish (AR) / Español (AR)',
443 'es_BO': u'Spanish (BO) / Español (BO)',
444 'es_CL': u'Spanish (CL) / Español (CL)',
445 'es_CO': u'Spanish (CO) / Español (CO)',
446 'es_CR': u'Spanish (CR) / Español (CR)',
447 'es_DO': u'Spanish (DO) / Español (DO)',
448 'es_EC': u'Spanish (EC) / Español (EC)',
449 'es_ES': u'Spanish / Español',
450 'es_GT': u'Spanish (GT) / Español (GT)',
451 'es_HN': u'Spanish (HN) / Español (HN)',
452 'es_MX': u'Spanish (MX) / Español (MX)',
453 'es_NI': u'Spanish (NI) / Español (NI)',
454 'es_PA': u'Spanish (PA) / Español (PA)',
455 'es_PE': u'Spanish (PE) / Español (PE)',
456 'es_PR': u'Spanish (PR) / Español (PR)',
457 'es_PY': u'Spanish (PY) / Español (PY)',
458 'es_SV': u'Spanish (SV) / Español (SV)',
459 'es_UY': u'Spanish (UY) / Español (UY)',
460 'es_VE': u'Spanish (VE) / Español (VE)',
461 'et_EE': u'Estonian / Eesti keel',
462 'fa_IR': u'Persian / فارس',
463 'fi_FI': u'Finnish / Suomi',
464 'fr_BE': u'French (BE) / Français (BE)',
465 'fr_CA': u'French (CA) / Français (CA)',
466 'fr_CH': u'French (CH) / Français (CH)',
467 'fr_FR': u'French / Français',
468 'gl_ES': u'Galician / Galego',
469 'gu_IN': u'Gujarati / ગુજરાતી',
470 'he_IL': u'Hebrew / עִבְרִי',
471 'hi_IN': u'Hindi / हिंदी',
472 'hr_HR': u'Croatian / hrvatski jezik',
473 'hu_HU': u'Hungarian / Magyar',
474 'id_ID': u'Indonesian / Bahasa Indonesia',
475 'it_IT': u'Italian / Italiano',
476 'iu_CA': u'Inuktitut / ᐃᓄᒃᑎᑐᑦ',
477 'ja_JP': u'Japanese / 日本語',
478 'ko_KP': u'Korean (KP) / 한국어 (KP)',
479 'ko_KR': u'Korean (KR) / 한국어 (KR)',
480 'lo_LA': u'Lao / ພາສາລາວ',
481 'lt_LT': u'Lithuanian / Lietuvių kalba',
482 'lv_LV': u'Latvian / latviešu valoda',
483 'mk_MK': u'Macedonian / македонски јазик',
484 'ml_IN': u'Malayalam / മലയാളം',
485 'mn_MN': u'Mongolian / монгол',
486 'nb_NO': u'Norwegian Bokmål / Norsk bokmål',
487 'nl_NL': u'Dutch / Nederlands',
488 'nl_BE': u'Flemish (BE) / Vlaams (BE)',
489 'oc_FR': u'Occitan (FR, post 1500) / Occitan',
490 'pl_PL': u'Polish / Język polski',
491 'pt_BR': u'Portuguese (BR) / Português (BR)',
492 'pt_PT': u'Portuguese / Português',
493 'ro_RO': u'Romanian / română',
494 'ru_RU': u'Russian / русский язык',
495 'si_LK': u'Sinhalese / සිංහල',
496 'sl_SI': u'Slovenian / slovenščina',
497 'sk_SK': u'Slovak / Slovenský jazyk',
498 'sq_AL': u'Albanian / Shqip',
499 'sr_RS': u'Serbian (Cyrillic) / српски',
500 'sr@latin': u'Serbian (Latin) / srpski',
501 'sv_SE': u'Swedish / svenska',
502 'te_IN': u'Telugu / తెలుగు',
503 'tr_TR': u'Turkish / Türkçe',
504 'vi_VN': u'Vietnamese / Tiếng Việt',
505 'uk_UA': u'Ukrainian / українська',
506 'ur_PK': u'Urdu / اردو',
507 'zh_CN': u'Chinese (CN) / 简体中文',
508 'zh_HK': u'Chinese (HK)',
509 'zh_TW': u'Chinese (TW) / 正體字',
510 'th_TH': u'Thai / ภาษาไทย',
511 'tlh_TLH': u'Klingon',
514 def scan_languages():
515 """ Returns all languages supported by OpenERP for translation
517 :returns: a list of (lang_code, lang_name) pairs
518 :rtype: [(str, unicode)]
520 return sorted(ALL_LANGUAGES.iteritems(), key=lambda k: k[1])
522 def get_user_companies(cr, user):
523 def _get_company_children(cr, ids):
526 cr.execute('SELECT id FROM res_company WHERE parent_id IN %s', (tuple(ids),))
527 res = [x[0] for x in cr.fetchall()]
528 res.extend(_get_company_children(cr, res))
530 cr.execute('SELECT company_id FROM res_users WHERE id=%s', (user,))
531 user_comp = cr.fetchone()[0]
534 return [user_comp] + _get_company_children(cr, [user_comp])
538 Input number : account or invoice number
539 Output return: the same number completed with the recursive mod10
542 codec=[0,9,4,6,8,2,7,1,3,5]
548 report = codec[ (int(digit) + report) % 10 ]
549 return result + str((10 - report) % 10)
554 Return the size in a human readable format
558 units = ('bytes', 'Kb', 'Mb', 'Gb')
559 if isinstance(sz,basestring):
562 while s >= 1024 and i < len(units)-1:
565 return "%0.2f %s" % (s, units[i])
569 def wrapper(*args, **kwargs):
570 from pprint import pformat
572 vector = ['Call -> function: %r' % f]
573 for i, arg in enumerate(args):
574 vector.append(' arg %02d: %s' % (i, pformat(arg)))
575 for key, value in kwargs.items():
576 vector.append(' kwarg %10s: %s' % (key, pformat(value)))
579 res = f(*args, **kwargs)
581 vector.append(' result: %s' % pformat(res))
582 vector.append(' time delta: %s' % (time.time() - timeb4))
583 _logger.debug('\n'.join(vector))
588 class profile(object):
589 def __init__(self, fname=None):
592 def __call__(self, f):
594 def wrapper(*args, **kwargs):
595 profile = cProfile.Profile()
596 result = profile.runcall(f, *args, **kwargs)
597 profile.dump_stats(self.fname or ("%s.cprof" % (f.func_name,)))
602 __icons_list = ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
603 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
604 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
605 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
606 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
607 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
608 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
609 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
610 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
611 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
612 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
613 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
614 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
615 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
616 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
617 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
618 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
619 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
620 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
621 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
622 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
623 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
624 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
625 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
626 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
627 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
628 'terp-check','terp-go-month','terp-go-year','terp-go-today','terp-document-new','terp-camera_test',
629 'terp-emblem-important','terp-gtk-media-pause','terp-gtk-stop','terp-gnome-cpu-frequency-applet+',
630 'terp-dialog-close','terp-gtk-jump-to-rtl','terp-gtk-jump-to-ltr','terp-accessories-archiver',
631 'terp-stock_align_left_24','terp-stock_effects-object-colorize','terp-go-home','terp-gtk-go-back-rtl',
632 'terp-gtk-go-back-ltr','terp-personal','terp-personal-','terp-personal+','terp-accessories-archiver-minus',
633 'terp-accessories-archiver+','terp-stock_symbol-selection','terp-call-start','terp-dolar',
634 'terp-face-plain','terp-folder-blue','terp-folder-green','terp-folder-orange','terp-folder-yellow',
635 'terp-gdu-smart-failing','terp-go-week','terp-gtk-select-all','terp-locked','terp-mail-forward',
636 'terp-mail-message-new','terp-mail-replied','terp-rating-rated','terp-stage','terp-stock_format-scientific',
637 'terp-dolar_ok!','terp-idea','terp-stock_format-default','terp-mail-','terp-mail_delete'
642 return [(x, x) for x in __icons_list ]
644 def detect_ip_addr():
645 """Try a very crude method to figure out a valid external
646 IP or hostname for the current machine. Don't rely on this
647 for binding to an interface, but it could be used as basis
648 for constructing a remote URL to the server.
650 def _detect_ip_addr():
651 from array import array
652 from struct import pack, unpack
661 if not fcntl: # not UNIX:
662 host = socket.gethostname()
663 ip_addr = socket.gethostbyname(host)
665 # get all interfaces:
667 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
668 names = array('B', '\0' * nbytes)
669 #print 'names: ', names
670 outbytes = unpack('iL', fcntl.ioctl( s.fileno(), 0x8912, pack('iL', nbytes, names.buffer_info()[0])))[0]
671 namestr = names.tostring()
674 for i in range(0, outbytes, 40):
675 name = namestr[i:i+16].split('\0', 1)[0]
677 ip_addr = socket.inet_ntoa(namestr[i+20:i+24])
682 ifaces = filter(None, [namestr[i:i+32].split('\0', 1)[0] for i in range(0, outbytes, 32)])
684 for ifname in [iface for iface in ifaces if iface != 'lo']:
685 ip_addr = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, pack('256s', ifname[:15]))[20:24])
688 return ip_addr or 'localhost'
691 ip_addr = _detect_ip_addr()
693 ip_addr = 'localhost'
696 # RATIONALE BEHIND TIMESTAMP CALCULATIONS AND TIMEZONE MANAGEMENT:
697 # The server side never does any timestamp calculation, always
698 # sends them in a naive (timezone agnostic) format supposed to be
699 # expressed within the server timezone, and expects the clients to
700 # provide timestamps in the server timezone as well.
701 # It stores all timestamps in the database in naive format as well,
702 # which also expresses the time in the server timezone.
703 # For this reason the server makes its timezone name available via the
704 # common/timezone_get() rpc method, which clients need to read
705 # to know the appropriate time offset to use when reading/writing
707 def get_win32_timezone():
708 """Attempt to return the "standard name" of the current timezone on a win32 system.
709 @return the standard name of the current win32 timezone, or False if it cannot be found.
712 if sys.platform == "win32":
715 hklm = _winreg.ConnectRegistry(None,_winreg.HKEY_LOCAL_MACHINE)
716 current_tz_key = _winreg.OpenKey(hklm, r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", 0,_winreg.KEY_ALL_ACCESS)
717 res = str(_winreg.QueryValueEx(current_tz_key,"StandardName")[0]) # [0] is value, [1] is type code
718 _winreg.CloseKey(current_tz_key)
719 _winreg.CloseKey(hklm)
724 def detect_server_timezone():
725 """Attempt to detect the timezone to use on the server side.
726 Defaults to UTC if no working timezone can be found.
727 @return the timezone identifier as expected by pytz.timezone.
732 _logger.warning("Python pytz module is not available. "
733 "Timezone will be set to UTC by default.")
736 # Option 1: the configuration option (did not exist before, so no backwards compatibility issue)
737 # 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
738 # Option 3: the environment variable TZ
739 sources = [ (config['timezone'], 'OpenERP configuration'),
740 (time.tzname[0], 'time.tzname'),
741 (os.environ.get('TZ',False),'TZ environment variable'), ]
742 # Option 4: OS-specific: /etc/timezone on Unix
743 if os.path.exists("/etc/timezone"):
746 f = open("/etc/timezone")
747 tz_value = f.read(128).strip()
752 sources.append((tz_value,"/etc/timezone file"))
753 # Option 5: timezone info from registry on Win32
754 if sys.platform == "win32":
755 # Timezone info is stored in windows registry.
756 # However this is not likely to work very well as the standard name
757 # of timezones in windows is rarely something that is known to pytz.
758 # But that's ok, it is always possible to use a config option to set
760 sources.append((get_win32_timezone(),"Windows Registry"))
762 for (value,source) in sources:
765 tz = pytz.timezone(value)
766 _logger.info("Using timezone %s obtained from %s.", tz.zone, source)
768 except pytz.UnknownTimeZoneError:
769 _logger.warning("The timezone specified in %s (%s) is invalid, ignoring it.", source, value)
771 _logger.warning("No valid timezone could be detected, using default UTC "
772 "timezone. You can specify it explicitly with option 'timezone' in "
773 "the server configuration.")
776 def get_server_timezone():
780 DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"
781 DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S"
782 DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % (
783 DEFAULT_SERVER_DATE_FORMAT,
784 DEFAULT_SERVER_TIME_FORMAT)
786 # Python's strftime supports only the format directives
787 # that are available on the platform's libc, so in order to
788 # be cross-platform we map to the directives required by
789 # the C standard (1989 version), always available on platforms
790 # with a C standard implementation.
791 DATETIME_FORMATS_MAP = {
793 '%D': '%m/%d/%Y', # modified %y->%Y
795 '%E': '', # special modifier
797 '%g': '%Y', # modified %y->%Y
803 '%O': '', # special modifier
807 '%s': '', #num of seconds since epoch
812 '%y': '%Y', # Even if %y works, it's ambiguous, so we should use %Y
813 '%+': '%Y-%m-%d %H:%M:%S',
815 # %Z is a special case that causes 2 problems at least:
816 # - the timezone names we use (in res_user.context_tz) come
817 # from pytz, but not all these names are recognized by
818 # strptime(), so we cannot convert in both directions
819 # when such a timezone is selected and %Z is in the format
820 # - %Z is replaced by an empty string in strftime() when
821 # there is not tzinfo in a datetime value (e.g when the user
822 # did not pick a context_tz). The resulting string does not
823 # parse back if the format requires %Z.
824 # As a consequence, we strip it completely from format strings.
825 # The user can always have a look at the context_tz in
826 # preferences to check the timezone.
850 # see comments above, and babel's format_datetime assumes an UTC timezone
851 # for naive datetime objects
856 def posix_to_ldml(fmt, locale):
857 """ Converts a posix/strftime pattern into an LDML date format pattern.
859 :param fmt: non-extended C89/C90 strftime pattern
860 :param locale: babel locale used for locale-specific conversions (e.g. %x and %X)
868 # LDML date format patterns uses letters, so letters must be quoted
869 if not pc and c.isalpha():
870 quoted.append(c if c != "'" else "''")
874 buf.append(''.join(quoted))
879 if c == '%': # escaped percent
881 elif c == 'x': # date format, short seems to match
882 buf.append(locale.date_formats['short'].pattern)
883 elif c == 'X': # time format, seems to include seconds. short does not
884 buf.append(locale.time_formats['medium'].pattern)
885 else: # look up format char in static mapping
886 buf.append(POSIX_TO_LDML[c])
893 # flush anything remaining in quoted buffer
896 buf.append(''.join(quoted))
901 def server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_name,
902 tz_offset=True, ignore_unparsable_time=True):
904 Convert a source timestamp string into a destination timestamp string, attempting to apply the
905 correct offset if both the server and local timezone are recognized, or no
906 offset at all if they aren't or if tz_offset is false (i.e. assuming they are both in the same TZ).
908 WARNING: This method is here to allow formatting dates correctly for inclusion in strings where
909 the client would not be able to format/offset it correctly. DO NOT use it for returning
910 date fields directly, these are supposed to be handled by the client!!
912 @param src_tstamp_str: the str value containing the timestamp in the server timezone.
913 @param src_format: the format to use when parsing the server timestamp.
914 @param dst_format: the format to use when formatting the resulting timestamp for the local/client timezone.
915 @param dst_tz_name: name of the destination timezone (such as the 'tz' value of the client context)
916 @param ignore_unparsable_time: if True, return False if src_tstamp_str cannot be parsed
917 using src_format or formatted using dst_format.
919 @return local/client formatted timestamp, expressed in the local/client timezone if possible
920 and if tz_offset is true, or src_tstamp_str if timezone offset could not be determined.
922 if not src_tstamp_str:
926 if src_format and dst_format:
927 # find out server timezone
928 server_tz = get_server_timezone()
930 # dt_value needs to be a datetime.datetime object (so no time.struct_time or mx.DateTime.DateTime here!)
931 dt_value = datetime.strptime(src_tstamp_str, src_format)
932 if tz_offset and dst_tz_name:
935 src_tz = pytz.timezone(server_tz)
936 dst_tz = pytz.timezone(dst_tz_name)
937 src_dt = src_tz.localize(dt_value, is_dst=True)
938 dt_value = src_dt.astimezone(dst_tz)
941 res = dt_value.strftime(dst_format)
943 # Normal ways to end up here are if strptime or strftime failed
944 if not ignore_unparsable_time:
949 def split_every(n, iterable, piece_maker=tuple):
950 """Splits an iterable into length-n pieces. The last piece will be shorter
951 if ``n`` does not evenly divide the iterable length.
952 @param ``piece_maker``: function to build the pieces
953 from the slices (tuple,list,...)
955 iterator = iter(iterable)
956 piece = piece_maker(islice(iterator, n))
959 piece = piece_maker(islice(iterator, n))
961 if __name__ == '__main__':
965 class upload_data_thread(threading.Thread):
966 def __init__(self, email, data, type):
967 self.args = [('email',email),('type',type),('data',data)]
968 super(upload_data_thread,self).__init__()
972 args = urllib.urlencode(self.args)
973 fp = urllib.urlopen('http://www.openerp.com/scripts/survey.php', args)
979 def upload_data(email, data, type='SURVEY'):
980 a = upload_data_thread(email, data, type)
984 def get_and_group_by_field(cr, uid, obj, ids, field, context=None):
985 """ Read the values of ``field´´ for the given ``ids´´ and group ids by value.
987 :param string field: name of the field we want to read and group by
988 :return: mapping of field values to the list of ids that have it
992 for record in obj.read(cr, uid, ids, [field], context=context):
994 res.setdefault(key[0] if isinstance(key, tuple) else key, []).append(record['id'])
997 def get_and_group_by_company(cr, uid, obj, ids, context=None):
998 return get_and_group_by_field(cr, uid, obj, ids, field='company_id', context=context)
1000 # port of python 2.6's attrgetter with support for dotted notation
1001 def resolve_attr(obj, attr):
1002 for name in attr.split("."):
1003 obj = getattr(obj, name)
1006 def attrgetter(*items):
1010 return resolve_attr(obj, attr)
1013 return tuple(resolve_attr(obj, attr) for attr in items)
1017 """A subclass of str that implements repr() without enclosing quotation marks
1018 or escaping, keeping the original string untouched. The name come from Lisp's unquote.
1019 One of the uses for this is to preserve or insert bare variable names within dicts during eval()
1020 of a dict's repr(). Use with care.
1022 Some examples (notice that there are never quotes surrounding
1023 the ``active_id`` name:
1025 >>> unquote('active_id')
1027 >>> d = {'test': unquote('active_id')}
1036 class UnquoteEvalContext(defaultdict):
1037 """Defaultdict-based evaluation context that returns
1038 an ``unquote`` string for any missing name used during
1040 Mostly useful for evaluating OpenERP domains/contexts that
1041 may refer to names that are unknown at the time of eval,
1042 so that when the context/domain is converted back to a string,
1043 the original names are preserved.
1045 **Warning**: using an ``UnquoteEvalContext`` as context for ``eval()`` or
1046 ``safe_eval()`` will shadow the builtins, which may cause other
1047 failures, depending on what is evaluated.
1049 Example (notice that ``section_id`` is preserved in the final
1052 >>> context_str = "{'default_user_id': uid, 'default_section_id': section_id}"
1053 >>> eval(context_str, UnquoteEvalContext(uid=1))
1054 {'default_user_id': 1, 'default_section_id': section_id}
1057 def __init__(self, *args, **kwargs):
1058 super(UnquoteEvalContext, self).__init__(None, *args, **kwargs)
1060 def __missing__(self, key):
1064 class mute_logger(object):
1065 """Temporary suppress the logging.
1066 Can be used as context manager or decorator.
1068 @mute_logger('openerp.plic.ploc')
1072 with mute_logger('openerp.foo.bar'):
1076 def __init__(self, *loggers):
1077 self.loggers = loggers
1079 def filter(self, record):
1082 def __enter__(self):
1083 for logger in self.loggers:
1084 assert isinstance(logger, basestring),\
1085 "A logger name must be a string, got %s" % type(logger)
1086 logging.getLogger(logger).addFilter(self)
1088 def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
1089 for logger in self.loggers:
1090 logging.getLogger(logger).removeFilter(self)
1092 def __call__(self, func):
1094 def deco(*args, **kwargs):
1096 return func(*args, **kwargs)
1100 class CountingStream(object):
1101 """ Stream wrapper counting the number of element it has yielded. Similar
1102 role to ``enumerate``, but for use when the iteration process of the stream
1103 isn't fully under caller control (the stream can be iterated from multiple
1104 points including within a library)
1106 ``start`` allows overriding the starting index (the index before the first
1109 On each iteration (call to :meth:`~.next`), increases its :attr:`~.index`
1112 .. attribute:: index
1114 ``int``, index of the last yielded element in the stream. If the stream
1115 has ended, will give an index 1-past the stream
1117 def __init__(self, stream, start=-1):
1118 self.stream = iter(stream)
1120 self.stopped = False
1124 if self.stopped: raise StopIteration()
1126 val = next(self.stream, _ph)
1129 raise StopIteration()
1132 def stripped_sys_argv(*strip_args):
1133 """Return sys.argv with some arguments stripped, suitable for reexecution or subprocesses"""
1134 strip_args = sorted(set(strip_args) | set(['-s', '--save', '-d', '--database', '-u', '--update', '-i', '--init']))
1135 assert all(config.parser.has_option(s) for s in strip_args)
1136 takes_value = dict((s, config.parser.get_option(s).takes_value()) for s in strip_args)
1138 longs, shorts = list(tuple(y) for _, y in groupby(strip_args, lambda x: x.startswith('--')))
1139 longs_eq = tuple(l + '=' for l in longs if takes_value[l])
1144 return args[i].startswith(shorts) \
1145 or args[i].startswith(longs_eq) or (args[i] in longs) \
1146 or (i >= 1 and (args[i - 1] in strip_args) and takes_value[args[i - 1]])
1148 return [x for i, x in enumerate(args) if not strip(args, i)]
1151 class ConstantMapping(Mapping):
1153 An immutable mapping returning the provided value for every single key.
1155 Useful for default value to methods
1157 __slots__ = ['_value']
1158 def __init__(self, val):
1163 defaultdict updates its length for each individually requested key, is
1170 same as len, defaultdict udpates its iterable keyset with each key
1171 requested, is there a point for this?
1175 def __getitem__(self, item):
1179 def dumpstacks(sig=None, frame=None):
1180 """ Signal handler: dump a stack trace for each existing thread."""
1183 def extract_stack(stack):
1184 for filename, lineno, name, line in traceback.extract_stack(stack):
1185 yield 'File: "%s", line %d, in %s' % (filename, lineno, name)
1187 yield " %s" % (line.strip(),)
1189 # code from http://stackoverflow.com/questions/132058/getting-stack-trace-from-a-running-python-application#answer-2569696
1190 # modified for python 2.5 compatibility
1191 threads_info = dict([(th.ident, {'name': th.name, 'uid': getattr(th, 'uid', 'n/a')})
1192 for th in threading.enumerate()])
1193 for threadId, stack in sys._current_frames().items():
1194 thread_info = threads_info.get(threadId)
1195 code.append("\n# Thread: %s (id:%s) (uid:%s)" %
1196 (thread_info and thread_info['name'] or 'n/a',
1198 thread_info and thread_info['uid'] or 'n/a'))
1199 for line in extract_stack(stack):
1203 # code from http://stackoverflow.com/questions/12510648/in-gevent-how-can-i-dump-stack-traces-of-all-running-greenlets
1205 from greenlet import greenlet
1206 for ob in gc.get_objects():
1207 if not isinstance(ob, greenlet) or not ob:
1209 code.append("\n# Greenlet: %r" % (ob,))
1210 for line in extract_stack(ob.gr_frame):
1213 _logger.info("\n".join(code))
1215 class frozendict(dict):
1216 """ An implementation of an immutable dictionary. """
1217 def __delitem__(self, key):
1218 raise NotImplementedError("'__delitem__' not supported on frozendict")
1219 def __setitem__(self, key, val):
1220 raise NotImplementedError("'__setitem__' not supported on frozendict")
1222 raise NotImplementedError("'clear' not supported on frozendict")
1223 def pop(self, key, default=None):
1224 raise NotImplementedError("'pop' not supported on frozendict")
1226 raise NotImplementedError("'popitem' not supported on frozendict")
1227 def setdefault(self, key, default=None):
1228 raise NotImplementedError("'setdefault' not supported on frozendict")
1229 def update(self, *args, **kwargs):
1230 raise NotImplementedError("'update' not supported on frozendict")
1239 # Avoid DeprecationWarning while still remaining compatible with werkzeug pre-0.9
1240 if parse_version(getattr(werkzeug, '__version__', '0.0')) < parse_version('0.9.0'):
1241 def html_escape(text):
1242 return werkzeug.utils.escape(text, quote=True)
1244 def html_escape(text):
1245 return werkzeug.utils.escape(text)
1247 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: