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-2012 OpenERP s.a. (<http://openerp.com>).
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU Affero General Public License as
10 # published by the Free Software Foundation, either version 3 of the
11 # License, or (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU Affero General Public License for more details.
18 # You should have received a copy of the GNU Affero General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
23 #.apidoc title: Utilities: tools.misc
26 Miscellaneous tools used by OpenERP.
29 from functools import wraps
41 from collections import defaultdict
42 from datetime import datetime
43 from email.MIMEText import MIMEText
44 from email.MIMEBase import MIMEBase
45 from email.MIMEMultipart import MIMEMultipart
46 from email.Header import Header
47 from email.Utils import formatdate, COMMASPACE
48 from email import Utils
49 from email import Encoders
50 from itertools import islice, izip
51 from lxml import etree
52 from which import which
53 from threading import local
55 from html2text import html2text
59 import openerp.loglevels as loglevels
60 import openerp.pooler as pooler
61 from config import config
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(__name__)
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 def find_in_path(name):
80 def find_pg_tool(name):
82 if config['pg_path'] and config['pg_path'] != 'None':
83 path = config['pg_path']
85 return which(name, path=path)
89 def exec_pg_command(name, *args):
90 prog = find_pg_tool(name)
92 raise Exception('Couldn\'t find %s' % name)
93 args2 = (prog,) + args
95 return subprocess.call(args2)
97 def exec_pg_command_pipe(name, *args):
98 prog = find_pg_tool(name)
100 raise Exception('Couldn\'t find %s' % name)
101 # on win32, passing close_fds=True is not compatible
102 # with redirecting std[in/err/out]
103 pop = subprocess.Popen((prog,) + args, bufsize= -1,
104 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
105 close_fds=(os.name=="posix"))
106 return (pop.stdin, pop.stdout)
108 def exec_command_pipe(name, *args):
109 prog = find_in_path(name)
111 raise Exception('Couldn\'t find %s' % name)
112 # on win32, passing close_fds=True is not compatible
113 # with redirecting std[in/err/out]
114 pop = subprocess.Popen((prog,) + args, bufsize= -1,
115 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
116 close_fds=(os.name=="posix"))
117 return (pop.stdin, pop.stdout)
119 #----------------------------------------------------------
121 #----------------------------------------------------------
122 #file_path_root = os.getcwd()
123 #file_path_addons = os.path.join(file_path_root, 'addons')
125 def file_open(name, mode="r", subdir='addons', pathinfo=False):
126 """Open a file from the OpenERP root, using a subdir folder.
130 >>> file_open('hr/report/timesheer.xsl')
131 >>> file_open('addons/hr/report/timesheet.xsl')
132 >>> file_open('../../base/report/rml_template.xsl', subdir='addons/hr/report', pathinfo=True)
134 @param name name of the file
135 @param mode file open mode
136 @param subdir subdirectory
137 @param pathinfo if True returns tupple (fileobject, filepath)
139 @return fileobject if pathinfo is False else (fileobject, filepath)
141 import openerp.modules as addons
142 adps = addons.module.ad_paths
143 rtp = os.path.normcase(os.path.abspath(config['root_path']))
145 if name.replace(os.path.sep, '/').startswith('addons/'):
149 # First try to locate in addons_path
152 if subdir2.replace(os.path.sep, '/').startswith('addons/'):
153 subdir2 = subdir2[7:]
155 subdir2 = (subdir2 != 'addons' or None) and subdir2
160 fn = os.path.join(adp, subdir2, name)
162 fn = os.path.join(adp, name)
163 fn = os.path.normpath(fn)
164 fo = file_open(fn, mode=mode, subdir=None, pathinfo=pathinfo)
172 name = os.path.join(rtp, subdir, name)
174 name = os.path.join(rtp, name)
176 name = os.path.normpath(name)
178 # Check for a zipfile in the path
183 head, tail = os.path.split(head)
187 zipname = os.path.join(tail, zipname)
190 if zipfile.is_zipfile(head+'.zip'):
191 from cStringIO import StringIO
192 zfile = zipfile.ZipFile(head+'.zip')
195 fo.write(zfile.read(os.path.join(
196 os.path.basename(head), zipname).replace(
203 name2 = os.path.normpath(os.path.join(head + '.zip', zipname))
205 for i in (name2, name):
206 if i and os.path.isfile(i):
211 if os.path.splitext(name)[1] == '.rml':
212 raise IOError, 'Report %s doesn\'t exist or deleted : ' %str(name)
213 raise IOError, 'File not found : %s' % name
216 #----------------------------------------------------------
218 #----------------------------------------------------------
220 """Flatten a list of elements into a uniqu list
221 Author: Christophe Simonis (christophe@tinyerp.com)
230 >>> flatten( [[], [[]]] )
232 >>> flatten( [[['a','b'], 'c'], 'd', ['e', [], 'f']] )
233 ['a', 'b', 'c', 'd', 'e', 'f']
234 >>> t = (1,2,(3,), [4, 5, [6, [7], (8, 9), ([10, 11, (12, 13)]), [14, [], (15,)], []]])
236 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
240 return hasattr(x, "__iter__")
245 map(r.append, flatten(e))
250 def reverse_enumerate(l):
251 """Like enumerate but in the other sens
254 >>> a = ['a', 'b', 'c']
255 >>> it = reverse_enumerate(a)
263 Traceback (most recent call last):
264 File "<stdin>", line 1, in <module>
267 return izip(xrange(len(l)-1, -1, -1), reversed(l))
269 #----------------------------------------------------------
271 #----------------------------------------------------------
272 email_re = re.compile(r"""
273 ([a-zA-Z][\w\.-]*[a-zA-Z0-9] # username part
275 [a-zA-Z0-9][\w\.-]* # domain must start with a letter ... Ged> why do we include a 0-9 then?
280 res_re = re.compile(r"\[([0-9]+)\]", re.UNICODE)
281 command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE)
283 # Updated in 7.0 to match the model name as well
284 # Typical form of references is <timestamp-openerp-record_id-model_name@domain>
285 # group(1) = the record ID ; group(2) = the model (if any) ; group(3) = the domain
286 reference_re = re.compile("<.*-open(?:object|erp)-(\\d+)(?:-([\w.]+))?.*@(.*)>", re.UNICODE)
288 def html2plaintext(html, body_id=None, encoding='utf-8'):
289 """ From an HTML text, convert the HTML to plain text.
290 If @param body_id is provided then this is the tag where the
291 body (not necessarily <body>) starts.
293 ## (c) Fry-IT, www.fry-it.com, 2007
294 ## <peter@fry-it.com>
295 ## download here: http://www.peterbe.com/plog/html2plaintext
299 from lxml.etree import tostring
301 from lxml.html.soupparser import fromstring
304 _logger.debug('tools.misc.html2plaintext: cannot use BeautifulSoup, fallback to lxml.etree.HTMLParser')
305 from lxml.etree import fromstring, HTMLParser
306 kwargs = dict(parser=HTMLParser())
308 tree = fromstring(html, **kwargs)
310 if body_id is not None:
311 source = tree.xpath('//*[@id=%s]'%(body_id,))
313 source = tree.xpath('//body')
319 for link in tree.findall('.//a'):
320 url = link.get('href')
324 link.text = '%s [%s]' % (link.text, i)
325 url_index.append(url)
327 html = ustr(tostring(tree, encoding=encoding))
329 html = html.replace('<strong>','*').replace('</strong>','*')
330 html = html.replace('<b>','*').replace('</b>','*')
331 html = html.replace('<h3>','*').replace('</h3>','*')
332 html = html.replace('<h2>','**').replace('</h2>','**')
333 html = html.replace('<h1>','**').replace('</h1>','**')
334 html = html.replace('<em>','/').replace('</em>','/')
335 html = html.replace('<tr>', '\n')
336 html = html.replace('</p>', '\n')
337 html = re.sub('<br\s*/?>', '\n', html)
338 html = re.sub('<.*?>', ' ', html)
339 html = html.replace(' ' * 2, ' ')
342 html = '\n'.join([x.strip() for x in html.splitlines()])
343 html = html.replace('\n' * 2, '\n')
345 for i, url in enumerate(url_index):
348 html += ustr('[%s] %s\n') % (i+1, url)
352 def generate_tracking_message_id(res_id):
353 """Returns a string that can be used in the Message-ID RFC822 header field
355 Used to track the replies related to a given object thanks to the "In-Reply-To"
356 or "References" fields that Mail User Agents will set.
358 return "<%s-openerp-%s@%s>" % (time.time(), res_id, socket.gethostname())
360 def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
361 attachments=None, message_id=None, references=None, openobject_id=False, debug=False, subtype='plain', headers=None,
362 smtp_server=None, smtp_port=None, ssl=False, smtp_user=None, smtp_password=None, cr=None, uid=None):
363 """Low-level function for sending an email (deprecated).
365 :deprecate: since OpenERP 6.1, please use ir.mail_server.send_email() instead.
366 :param email_from: A string used to fill the `From` header, if falsy,
367 config['email_from'] is used instead. Also used for
368 the `Reply-To` header if `reply_to` is not provided
369 :param email_to: a sequence of addresses to send the mail to.
372 # If not cr, get cr from current thread database
374 db_name = getattr(threading.currentThread(), 'dbname', None)
376 cr = pooler.get_db_only(db_name).cursor()
378 raise Exception("No database cursor found, please pass one explicitly")
382 mail_server_pool = pooler.get_pool(cr.dbname).get('ir.mail_server')
384 # Pack Message into MIME Object
385 email_msg = mail_server_pool.build_email(email_from, email_to, subject, body, email_cc, email_bcc, reply_to,
386 attachments, message_id, references, openobject_id, subtype, headers=headers)
388 res = mail_server_pool.send_email(cr, uid or 1, email_msg, mail_server_id=None,
389 smtp_server=smtp_server, smtp_port=smtp_port, smtp_user=smtp_user, smtp_password=smtp_password,
390 smtp_encryption=('ssl' if ssl else None), smtp_debug=debug)
392 _logger.exception("tools.email_send failed to deliver email")
398 #----------------------------------------------------------
400 #----------------------------------------------------------
401 # text must be latin-1 encoded
402 def sms_send(user, password, api_id, text, to):
404 url = "http://api.urlsms.com/SendSMS.aspx"
405 #url = "http://196.7.150.220/http/sendmsg"
406 params = urllib.urlencode({'UserID': user, 'Password': password, 'SenderID': api_id, 'MsgText': text, 'RecipientMobileNo':to})
407 urllib.urlopen(url+"?"+params)
408 # FIXME: Use the logger if there is an error
411 class UpdateableStr(local):
412 """ Class that stores an updateable string (used in wizards)
415 def __init__(self, string=''):
419 return str(self.string)
422 return str(self.string)
424 def __nonzero__(self):
425 return bool(self.string)
428 class UpdateableDict(local):
429 """Stores an updateable dict to use in wizards
432 def __init__(self, dict=None):
438 return str(self.dict)
441 return str(self.dict)
444 return self.dict.clear()
447 return self.dict.keys()
449 def __setitem__(self, i, y):
450 self.dict.__setitem__(i, y)
452 def __getitem__(self, i):
453 return self.dict.__getitem__(i)
456 return self.dict.copy()
459 return self.dict.iteritems()
462 return self.dict.iterkeys()
464 def itervalues(self):
465 return self.dict.itervalues()
467 def pop(self, k, d=None):
468 return self.dict.pop(k, d)
471 return self.dict.popitem()
473 def setdefault(self, k, d=None):
474 return self.dict.setdefault(k, d)
476 def update(self, E, **F):
477 return self.dict.update(E, F)
480 return self.dict.values()
482 def get(self, k, d=None):
483 return self.dict.get(k, d)
485 def has_key(self, k):
486 return self.dict.has_key(k)
489 return self.dict.items()
491 def __cmp__(self, y):
492 return self.dict.__cmp__(y)
494 def __contains__(self, k):
495 return self.dict.__contains__(k)
497 def __delitem__(self, y):
498 return self.dict.__delitem__(y)
501 return self.dict.__eq__(y)
504 return self.dict.__ge__(y)
507 return self.dict.__gt__(y)
510 return self.dict.__hash__()
513 return self.dict.__iter__()
516 return self.dict.__le__(y)
519 return self.dict.__len__()
522 return self.dict.__lt__(y)
525 return self.dict.__ne__(y)
527 class currency(float):
532 Don't use ! Use res.currency.round()
535 def __init__(self, value, accuracy=2, rounding=None):
537 rounding=10**-accuracy
538 self.rounding=rounding
539 self.accuracy=accuracy
541 def __new__(cls, value, accuracy=2, rounding=None):
542 return float.__new__(cls, round(value, accuracy))
545 # display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
546 # return str(display_value)
549 return s.replace('&','&').replace('<','<').replace('>','>')
551 def get_iso_codes(lang):
552 if lang.find('_') != -1:
553 if lang.split('_')[0] == lang.split('_')[1].lower():
554 lang = lang.split('_')[0]
558 # The codes below are those from Launchpad's Rosetta, with the exception
559 # of some trivial codes where the Launchpad code is xx and we have xx_XX.
561 'ab_RU': u'Abkhazian / аҧсуа',
562 'ar_AR': u'Arabic / الْعَرَبيّة',
563 'bg_BG': u'Bulgarian / български език',
564 'bs_BS': u'Bosnian / bosanski jezik',
565 'ca_ES': u'Catalan / Català',
566 'cs_CZ': u'Czech / Čeština',
567 'da_DK': u'Danish / Dansk',
568 'de_DE': u'German / Deutsch',
569 'el_GR': u'Greek / Ελληνικά',
570 'en_CA': u'English (CA)',
571 'en_GB': u'English (UK)',
572 'en_US': u'English (US)',
573 'es_AR': u'Spanish (AR) / Español (AR)',
574 'es_BO': u'Spanish (BO) / Español (BO)',
575 'es_CL': u'Spanish (CL) / Español (CL)',
576 'es_CO': u'Spanish (CO) / Español (CO)',
577 'es_CR': u'Spanish (CR) / Español (CR)',
578 'es_DO': u'Spanish (DO) / Español (DO)',
579 'es_EC': u'Spanish (EC) / Español (EC)',
580 'es_ES': u'Spanish / Español',
581 'es_GT': u'Spanish (GT) / Español (GT)',
582 'es_HN': u'Spanish (HN) / Español (HN)',
583 'es_MX': u'Spanish (MX) / Español (MX)',
584 'es_NI': u'Spanish (NI) / Español (NI)',
585 'es_PA': u'Spanish (PA) / Español (PA)',
586 'es_PE': u'Spanish (PE) / Español (PE)',
587 'es_PR': u'Spanish (PR) / Español (PR)',
588 'es_PY': u'Spanish (PY) / Español (PY)',
589 'es_SV': u'Spanish (SV) / Español (SV)',
590 'es_UY': u'Spanish (UY) / Español (UY)',
591 'es_VE': u'Spanish (VE) / Español (VE)',
592 'et_EE': u'Estonian / Eesti keel',
593 'fa_IR': u'Persian / فارس',
594 'fi_FI': u'Finnish / Suomi',
595 'fr_BE': u'French (BE) / Français (BE)',
596 'fr_CH': u'French (CH) / Français (CH)',
597 'fr_FR': u'French / Français',
598 'gl_ES': u'Galician / Galego',
599 'gu_IN': u'Gujarati / ગુજરાતી',
600 'he_IL': u'Hebrew / עִבְרִי',
601 'hi_IN': u'Hindi / हिंदी',
602 'hr_HR': u'Croatian / hrvatski jezik',
603 'hu_HU': u'Hungarian / Magyar',
604 'id_ID': u'Indonesian / Bahasa Indonesia',
605 'it_IT': u'Italian / Italiano',
606 'iu_CA': u'Inuktitut / ᐃᓄᒃᑎᑐᑦ',
607 'ja_JP': u'Japanese / 日本語',
608 'ko_KP': u'Korean (KP) / 한국어 (KP)',
609 'ko_KR': u'Korean (KR) / 한국어 (KR)',
610 'lt_LT': u'Lithuanian / Lietuvių kalba',
611 'lv_LV': u'Latvian / latviešu valoda',
612 'ml_IN': u'Malayalam / മലയാളം',
613 'mn_MN': u'Mongolian / монгол',
614 'nb_NO': u'Norwegian Bokmål / Norsk bokmål',
615 'nl_NL': u'Dutch / Nederlands',
616 'nl_BE': u'Flemish (BE) / Vlaams (BE)',
617 'oc_FR': u'Occitan (FR, post 1500) / Occitan',
618 'pl_PL': u'Polish / Język polski',
619 'pt_BR': u'Portugese (BR) / Português (BR)',
620 'pt_PT': u'Portugese / Português',
621 'ro_RO': u'Romanian / română',
622 'ru_RU': u'Russian / русский язык',
623 'si_LK': u'Sinhalese / සිංහල',
624 'sl_SI': u'Slovenian / slovenščina',
625 'sk_SK': u'Slovak / Slovenský jazyk',
626 'sq_AL': u'Albanian / Shqip',
627 'sr_RS': u'Serbian (Cyrillic) / српски',
628 'sr@latin': u'Serbian (Latin) / srpski',
629 'sv_SE': u'Swedish / svenska',
630 'te_IN': u'Telugu / తెలుగు',
631 'tr_TR': u'Turkish / Türkçe',
632 'vi_VN': u'Vietnamese / Tiếng Việt',
633 'uk_UA': u'Ukrainian / українська',
634 'ur_PK': u'Urdu / اردو',
635 'zh_CN': u'Chinese (CN) / 简体中文',
636 'zh_HK': u'Chinese (HK)',
637 'zh_TW': u'Chinese (TW) / 正體字',
638 'th_TH': u'Thai / ภาษาไทย',
639 'tlh_TLH': u'Klingon',
643 def scan_languages():
644 # Now it will take all languages from get languages function without filter it with base module languages
645 lang_dict = get_languages()
646 ret = [(lang, lang_dict.get(lang, lang)) for lang in list(lang_dict)]
647 ret.sort(key=lambda k:k[1])
651 def get_user_companies(cr, user):
652 def _get_company_children(cr, ids):
655 cr.execute('SELECT id FROM res_company WHERE parent_id IN %s', (tuple(ids),))
656 res = [x[0] for x in cr.fetchall()]
657 res.extend(_get_company_children(cr, res))
659 cr.execute('SELECT company_id FROM res_users WHERE id=%s', (user,))
660 user_comp = cr.fetchone()[0]
663 return [user_comp] + _get_company_children(cr, [user_comp])
667 Input number : account or invoice number
668 Output return: the same number completed with the recursive mod10
671 codec=[0,9,4,6,8,2,7,1,3,5]
677 report = codec[ (int(digit) + report) % 10 ]
678 return result + str((10 - report) % 10)
683 Return the size in a human readable format
687 units = ('bytes', 'Kb', 'Mb', 'Gb')
688 if isinstance(sz,basestring):
691 while s >= 1024 and i < len(units)-1:
694 return "%0.2f %s" % (s, units[i])
698 def wrapper(*args, **kwargs):
699 from pprint import pformat
701 vector = ['Call -> function: %r' % f]
702 for i, arg in enumerate(args):
703 vector.append(' arg %02d: %s' % (i, pformat(arg)))
704 for key, value in kwargs.items():
705 vector.append(' kwarg %10s: %s' % (key, pformat(value)))
708 res = f(*args, **kwargs)
710 vector.append(' result: %s' % pformat(res))
711 vector.append(' time delta: %s' % (time.time() - timeb4))
712 _logger.debug('\n'.join(vector))
717 class profile(object):
718 def __init__(self, fname=None):
721 def __call__(self, f):
723 def wrapper(*args, **kwargs):
724 class profile_wrapper(object):
728 self.result = f(*args, **kwargs)
729 pw = profile_wrapper()
731 fname = self.fname or ("%s.cprof" % (f.func_name,))
732 cProfile.runctx('pw()', globals(), locals(), filename=fname)
737 __icons_list = ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
738 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
739 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
740 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
741 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
742 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
743 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
744 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
745 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
746 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
747 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
748 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
749 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
750 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
751 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
752 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
753 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
754 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
755 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
756 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
757 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
758 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
759 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
760 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
761 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
762 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
763 'terp-check','terp-go-month','terp-go-year','terp-go-today','terp-document-new','terp-camera_test',
764 'terp-emblem-important','terp-gtk-media-pause','terp-gtk-stop','terp-gnome-cpu-frequency-applet+',
765 'terp-dialog-close','terp-gtk-jump-to-rtl','terp-gtk-jump-to-ltr','terp-accessories-archiver',
766 'terp-stock_align_left_24','terp-stock_effects-object-colorize','terp-go-home','terp-gtk-go-back-rtl',
767 'terp-gtk-go-back-ltr','terp-personal','terp-personal-','terp-personal+','terp-accessories-archiver-minus',
768 'terp-accessories-archiver+','terp-stock_symbol-selection','terp-call-start','terp-dolar',
769 'terp-face-plain','terp-folder-blue','terp-folder-green','terp-folder-orange','terp-folder-yellow',
770 'terp-gdu-smart-failing','terp-go-week','terp-gtk-select-all','terp-locked','terp-mail-forward',
771 'terp-mail-message-new','terp-mail-replied','terp-rating-rated','terp-stage','terp-stock_format-scientific',
772 'terp-dolar_ok!','terp-idea','terp-stock_format-default','terp-mail-','terp-mail_delete'
777 return [(x, x) for x in __icons_list ]
779 def extract_zip_file(zip_file, outdirectory):
780 zf = zipfile.ZipFile(zip_file, 'r')
782 for path in zf.namelist():
783 tgt = os.path.join(out, path)
784 tgtdir = os.path.dirname(tgt)
785 if not os.path.exists(tgtdir):
788 if not tgt.endswith(os.sep):
790 fp.write(zf.read(path))
794 def detect_ip_addr():
795 """Try a very crude method to figure out a valid external
796 IP or hostname for the current machine. Don't rely on this
797 for binding to an interface, but it could be used as basis
798 for constructing a remote URL to the server.
800 def _detect_ip_addr():
801 from array import array
802 from struct import pack, unpack
811 if not fcntl: # not UNIX:
812 host = socket.gethostname()
813 ip_addr = socket.gethostbyname(host)
815 # get all interfaces:
817 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
818 names = array('B', '\0' * nbytes)
819 #print 'names: ', names
820 outbytes = unpack('iL', fcntl.ioctl( s.fileno(), 0x8912, pack('iL', nbytes, names.buffer_info()[0])))[0]
821 namestr = names.tostring()
824 for i in range(0, outbytes, 40):
825 name = namestr[i:i+16].split('\0', 1)[0]
827 ip_addr = socket.inet_ntoa(namestr[i+20:i+24])
832 ifaces = filter(None, [namestr[i:i+32].split('\0', 1)[0] for i in range(0, outbytes, 32)])
834 for ifname in [iface for iface in ifaces if iface != 'lo']:
835 ip_addr = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, pack('256s', ifname[:15]))[20:24])
838 return ip_addr or 'localhost'
841 ip_addr = _detect_ip_addr()
843 ip_addr = 'localhost'
846 # RATIONALE BEHIND TIMESTAMP CALCULATIONS AND TIMEZONE MANAGEMENT:
847 # The server side never does any timestamp calculation, always
848 # sends them in a naive (timezone agnostic) format supposed to be
849 # expressed within the server timezone, and expects the clients to
850 # provide timestamps in the server timezone as well.
851 # It stores all timestamps in the database in naive format as well,
852 # which also expresses the time in the server timezone.
853 # For this reason the server makes its timezone name available via the
854 # common/timezone_get() rpc method, which clients need to read
855 # to know the appropriate time offset to use when reading/writing
857 def get_win32_timezone():
858 """Attempt to return the "standard name" of the current timezone on a win32 system.
859 @return the standard name of the current win32 timezone, or False if it cannot be found.
862 if (sys.platform == "win32"):
865 hklm = _winreg.ConnectRegistry(None,_winreg.HKEY_LOCAL_MACHINE)
866 current_tz_key = _winreg.OpenKey(hklm, r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", 0,_winreg.KEY_ALL_ACCESS)
867 res = str(_winreg.QueryValueEx(current_tz_key,"StandardName")[0]) # [0] is value, [1] is type code
868 _winreg.CloseKey(current_tz_key)
869 _winreg.CloseKey(hklm)
874 def detect_server_timezone():
875 """Attempt to detect the timezone to use on the server side.
876 Defaults to UTC if no working timezone can be found.
877 @return the timezone identifier as expected by pytz.timezone.
882 _logger.warning("Python pytz module is not available. "
883 "Timezone will be set to UTC by default.")
886 # Option 1: the configuration option (did not exist before, so no backwards compatibility issue)
887 # 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
888 # Option 3: the environment variable TZ
889 sources = [ (config['timezone'], 'OpenERP configuration'),
890 (time.tzname[0], 'time.tzname'),
891 (os.environ.get('TZ',False),'TZ environment variable'), ]
892 # Option 4: OS-specific: /etc/timezone on Unix
893 if (os.path.exists("/etc/timezone")):
896 f = open("/etc/timezone")
897 tz_value = f.read(128).strip()
902 sources.append((tz_value,"/etc/timezone file"))
903 # Option 5: timezone info from registry on Win32
904 if (sys.platform == "win32"):
905 # Timezone info is stored in windows registry.
906 # However this is not likely to work very well as the standard name
907 # of timezones in windows is rarely something that is known to pytz.
908 # But that's ok, it is always possible to use a config option to set
910 sources.append((get_win32_timezone(),"Windows Registry"))
912 for (value,source) in sources:
915 tz = pytz.timezone(value)
916 _logger.info("Using timezone %s obtained from %s.", tz.zone, source)
918 except pytz.UnknownTimeZoneError:
919 _logger.warning("The timezone specified in %s (%s) is invalid, ignoring it.", source, value)
921 _logger.warning("No valid timezone could be detected, using default UTC "
922 "timezone. You can specify it explicitly with option 'timezone' in "
923 "the server configuration.")
926 def get_server_timezone():
930 DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"
931 DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S"
932 DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % (
933 DEFAULT_SERVER_DATE_FORMAT,
934 DEFAULT_SERVER_TIME_FORMAT)
936 # Python's strftime supports only the format directives
937 # that are available on the platform's libc, so in order to
938 # be cross-platform we map to the directives required by
939 # the C standard (1989 version), always available on platforms
940 # with a C standard implementation.
941 DATETIME_FORMATS_MAP = {
943 '%D': '%m/%d/%Y', # modified %y->%Y
945 '%E': '', # special modifier
947 '%g': '%Y', # modified %y->%Y
953 '%O': '', # special modifier
957 '%s': '', #num of seconds since epoch
962 '%y': '%Y', # Even if %y works, it's ambiguous, so we should use %Y
963 '%+': '%Y-%m-%d %H:%M:%S',
965 # %Z is a special case that causes 2 problems at least:
966 # - the timezone names we use (in res_user.context_tz) come
967 # from pytz, but not all these names are recognized by
968 # strptime(), so we cannot convert in both directions
969 # when such a timezone is selected and %Z is in the format
970 # - %Z is replaced by an empty string in strftime() when
971 # there is not tzinfo in a datetime value (e.g when the user
972 # did not pick a context_tz). The resulting string does not
973 # parse back if the format requires %Z.
974 # As a consequence, we strip it completely from format strings.
975 # The user can always have a look at the context_tz in
976 # preferences to check the timezone.
981 def server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_name,
982 tz_offset=True, ignore_unparsable_time=True):
984 Convert a source timestamp string into a destination timestamp string, attempting to apply the
985 correct offset if both the server and local timezone are recognized, or no
986 offset at all if they aren't or if tz_offset is false (i.e. assuming they are both in the same TZ).
988 WARNING: This method is here to allow formatting dates correctly for inclusion in strings where
989 the client would not be able to format/offset it correctly. DO NOT use it for returning
990 date fields directly, these are supposed to be handled by the client!!
992 @param src_tstamp_str: the str value containing the timestamp in the server timezone.
993 @param src_format: the format to use when parsing the server timestamp.
994 @param dst_format: the format to use when formatting the resulting timestamp for the local/client timezone.
995 @param dst_tz_name: name of the destination timezone (such as the 'tz' value of the client context)
996 @param ignore_unparsable_time: if True, return False if src_tstamp_str cannot be parsed
997 using src_format or formatted using dst_format.
999 @return local/client formatted timestamp, expressed in the local/client timezone if possible
1000 and if tz_offset is true, or src_tstamp_str if timezone offset could not be determined.
1002 if not src_tstamp_str:
1005 res = src_tstamp_str
1006 if src_format and dst_format:
1007 # find out server timezone
1008 server_tz = get_server_timezone()
1010 # dt_value needs to be a datetime.datetime object (so no time.struct_time or mx.DateTime.DateTime here!)
1011 dt_value = datetime.strptime(src_tstamp_str, src_format)
1012 if tz_offset and dst_tz_name:
1015 src_tz = pytz.timezone(server_tz)
1016 dst_tz = pytz.timezone(dst_tz_name)
1017 src_dt = src_tz.localize(dt_value, is_dst=True)
1018 dt_value = src_dt.astimezone(dst_tz)
1021 res = dt_value.strftime(dst_format)
1023 # Normal ways to end up here are if strptime or strftime failed
1024 if not ignore_unparsable_time:
1029 def split_every(n, iterable, piece_maker=tuple):
1030 """Splits an iterable into length-n pieces. The last piece will be shorter
1031 if ``n`` does not evenly divide the iterable length.
1032 @param ``piece_maker``: function to build the pieces
1033 from the slices (tuple,list,...)
1035 iterator = iter(iterable)
1036 piece = piece_maker(islice(iterator, n))
1039 piece = piece_maker(islice(iterator, n))
1041 if __name__ == '__main__':
1045 class upload_data_thread(threading.Thread):
1046 def __init__(self, email, data, type):
1047 self.args = [('email',email),('type',type),('data',data)]
1048 super(upload_data_thread,self).__init__()
1052 args = urllib.urlencode(self.args)
1053 fp = urllib.urlopen('http://www.openerp.com/scripts/survey.php', args)
1059 def upload_data(email, data, type='SURVEY'):
1060 a = upload_data_thread(email, data, type)
1064 def get_and_group_by_field(cr, uid, obj, ids, field, context=None):
1065 """ Read the values of ``field´´ for the given ``ids´´ and group ids by value.
1067 :param string field: name of the field we want to read and group by
1068 :return: mapping of field values to the list of ids that have it
1072 for record in obj.read(cr, uid, ids, [field], context=context):
1074 res.setdefault(key[0] if isinstance(key, tuple) else key, []).append(record['id'])
1077 def get_and_group_by_company(cr, uid, obj, ids, context=None):
1078 return get_and_group_by_field(cr, uid, obj, ids, field='company_id', context=context)
1080 # port of python 2.6's attrgetter with support for dotted notation
1081 def resolve_attr(obj, attr):
1082 for name in attr.split("."):
1083 obj = getattr(obj, name)
1086 def attrgetter(*items):
1090 return resolve_attr(obj, attr)
1093 return tuple(resolve_attr(obj, attr) for attr in items)
1097 """A subclass of str that implements repr() without enclosing quotation marks
1098 or escaping, keeping the original string untouched. The name come from Lisp's unquote.
1099 One of the uses for this is to preserve or insert bare variable names within dicts during eval()
1100 of a dict's repr(). Use with care.
1102 Some examples (notice that there are never quotes surrounding
1103 the ``active_id`` name:
1105 >>> unquote('active_id')
1107 >>> d = {'test': unquote('active_id')}
1116 class UnquoteEvalContext(defaultdict):
1117 """Defaultdict-based evaluation context that returns
1118 an ``unquote`` string for any missing name used during
1120 Mostly useful for evaluating OpenERP domains/contexts that
1121 may refer to names that are unknown at the time of eval,
1122 so that when the context/domain is converted back to a string,
1123 the original names are preserved.
1125 **Warning**: using an ``UnquoteEvalContext`` as context for ``eval()`` or
1126 ``safe_eval()`` will shadow the builtins, which may cause other
1127 failures, depending on what is evaluated.
1129 Example (notice that ``section_id`` is preserved in the final
1132 >>> context_str = "{'default_user_id': uid, 'default_section_id': section_id}"
1133 >>> eval(context_str, UnquoteEvalContext(uid=1))
1134 {'default_user_id': 1, 'default_section_id': section_id}
1137 def __init__(self, *args, **kwargs):
1138 super(UnquoteEvalContext, self).__init__(None, *args, **kwargs)
1140 def __missing__(self, key):
1143 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: