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)
282 reference_re = re.compile("<.*-open(?:object|erp)-(\\d+).*@(.*)>", re.UNICODE)
284 def html2plaintext(html, body_id=None, encoding='utf-8'):
285 """ From an HTML text, convert the HTML to plain text.
286 If @param body_id is provided then this is the tag where the
287 body (not necessarily <body>) starts.
289 ## (c) Fry-IT, www.fry-it.com, 2007
290 ## <peter@fry-it.com>
291 ## download here: http://www.peterbe.com/plog/html2plaintext
295 from lxml.etree import tostring
297 from lxml.html.soupparser import fromstring
300 _logger.debug('tools.misc.html2plaintext: cannot use BeautifulSoup, fallback to lxml.etree.HTMLParser')
301 from lxml.etree import fromstring, HTMLParser
302 kwargs = dict(parser=HTMLParser())
304 tree = fromstring(html, **kwargs)
306 if body_id is not None:
307 source = tree.xpath('//*[@id=%s]'%(body_id,))
309 source = tree.xpath('//body')
315 for link in tree.findall('.//a'):
316 url = link.get('href')
320 link.text = '%s [%s]' % (link.text, i)
321 url_index.append(url)
323 html = ustr(tostring(tree, encoding=encoding))
325 html = html.replace('<strong>','*').replace('</strong>','*')
326 html = html.replace('<b>','*').replace('</b>','*')
327 html = html.replace('<h3>','*').replace('</h3>','*')
328 html = html.replace('<h2>','**').replace('</h2>','**')
329 html = html.replace('<h1>','**').replace('</h1>','**')
330 html = html.replace('<em>','/').replace('</em>','/')
331 html = html.replace('<tr>', '\n')
332 html = html.replace('</p>', '\n')
333 html = re.sub('<br\s*/?>', '\n', html)
334 html = re.sub('<.*?>', ' ', html)
335 html = html.replace(' ' * 2, ' ')
338 html = '\n'.join([x.strip() for x in html.splitlines()])
339 html = html.replace('\n' * 2, '\n')
341 for i, url in enumerate(url_index):
344 html += ustr('[%s] %s\n') % (i+1, url)
348 def generate_tracking_message_id(res_id):
349 """Returns a string that can be used in the Message-ID RFC822 header field
351 Used to track the replies related to a given object thanks to the "In-Reply-To"
352 or "References" fields that Mail User Agents will set.
354 return "<%s-openerp-%s@%s>" % (time.time(), res_id, socket.gethostname())
356 def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
357 attachments=None, message_id=None, references=None, openobject_id=False, debug=False, subtype='plain', headers=None,
358 smtp_server=None, smtp_port=None, ssl=False, smtp_user=None, smtp_password=None, cr=None, uid=None):
359 """Low-level function for sending an email (deprecated).
361 :deprecate: since OpenERP 6.1, please use ir.mail_server.send_email() instead.
362 :param email_from: A string used to fill the `From` header, if falsy,
363 config['email_from'] is used instead. Also used for
364 the `Reply-To` header if `reply_to` is not provided
365 :param email_to: a sequence of addresses to send the mail to.
368 # If not cr, get cr from current thread database
370 db_name = getattr(threading.currentThread(), 'dbname', None)
372 cr = pooler.get_db_only(db_name).cursor()
374 raise Exception("No database cursor found, please pass one explicitly")
378 mail_server_pool = pooler.get_pool(cr.dbname).get('ir.mail_server')
380 # Pack Message into MIME Object
381 email_msg = mail_server_pool.build_email(email_from, email_to, subject, body, email_cc, email_bcc, reply_to,
382 attachments, message_id, references, openobject_id, subtype, headers=headers)
384 res = mail_server_pool.send_email(cr, uid or 1, email_msg, mail_server_id=None,
385 smtp_server=smtp_server, smtp_port=smtp_port, smtp_user=smtp_user, smtp_password=smtp_password,
386 smtp_encryption=('ssl' if ssl else None), smtp_debug=debug)
388 _logger.exception("tools.email_send failed to deliver email")
394 #----------------------------------------------------------
396 #----------------------------------------------------------
397 # text must be latin-1 encoded
398 def sms_send(user, password, api_id, text, to):
400 url = "http://api.urlsms.com/SendSMS.aspx"
401 #url = "http://196.7.150.220/http/sendmsg"
402 params = urllib.urlencode({'UserID': user, 'Password': password, 'SenderID': api_id, 'MsgText': text, 'RecipientMobileNo':to})
403 urllib.urlopen(url+"?"+params)
404 # FIXME: Use the logger if there is an error
407 class UpdateableStr(local):
408 """ Class that stores an updateable string (used in wizards)
411 def __init__(self, string=''):
415 return str(self.string)
418 return str(self.string)
420 def __nonzero__(self):
421 return bool(self.string)
424 class UpdateableDict(local):
425 """Stores an updateable dict to use in wizards
428 def __init__(self, dict=None):
434 return str(self.dict)
437 return str(self.dict)
440 return self.dict.clear()
443 return self.dict.keys()
445 def __setitem__(self, i, y):
446 self.dict.__setitem__(i, y)
448 def __getitem__(self, i):
449 return self.dict.__getitem__(i)
452 return self.dict.copy()
455 return self.dict.iteritems()
458 return self.dict.iterkeys()
460 def itervalues(self):
461 return self.dict.itervalues()
463 def pop(self, k, d=None):
464 return self.dict.pop(k, d)
467 return self.dict.popitem()
469 def setdefault(self, k, d=None):
470 return self.dict.setdefault(k, d)
472 def update(self, E, **F):
473 return self.dict.update(E, F)
476 return self.dict.values()
478 def get(self, k, d=None):
479 return self.dict.get(k, d)
481 def has_key(self, k):
482 return self.dict.has_key(k)
485 return self.dict.items()
487 def __cmp__(self, y):
488 return self.dict.__cmp__(y)
490 def __contains__(self, k):
491 return self.dict.__contains__(k)
493 def __delitem__(self, y):
494 return self.dict.__delitem__(y)
497 return self.dict.__eq__(y)
500 return self.dict.__ge__(y)
503 return self.dict.__gt__(y)
506 return self.dict.__hash__()
509 return self.dict.__iter__()
512 return self.dict.__le__(y)
515 return self.dict.__len__()
518 return self.dict.__lt__(y)
521 return self.dict.__ne__(y)
523 class currency(float):
528 Don't use ! Use res.currency.round()
531 def __init__(self, value, accuracy=2, rounding=None):
533 rounding=10**-accuracy
534 self.rounding=rounding
535 self.accuracy=accuracy
537 def __new__(cls, value, accuracy=2, rounding=None):
538 return float.__new__(cls, round(value, accuracy))
541 # display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
542 # return str(display_value)
545 return s.replace('&','&').replace('<','<').replace('>','>')
547 def get_iso_codes(lang):
548 if lang.find('_') != -1:
549 if lang.split('_')[0] == lang.split('_')[1].lower():
550 lang = lang.split('_')[0]
554 # The codes below are those from Launchpad's Rosetta, with the exception
555 # of some trivial codes where the Launchpad code is xx and we have xx_XX.
557 'ab_RU': u'Abkhazian / аҧсуа',
558 'ar_AR': u'Arabic / الْعَرَبيّة',
559 'bg_BG': u'Bulgarian / български език',
560 'bs_BS': u'Bosnian / bosanski jezik',
561 'ca_ES': u'Catalan / Català',
562 'cs_CZ': u'Czech / Čeština',
563 'da_DK': u'Danish / Dansk',
564 'de_DE': u'German / Deutsch',
565 'el_GR': u'Greek / Ελληνικά',
566 'en_CA': u'English (CA)',
567 'en_GB': u'English (UK)',
568 'en_US': u'English (US)',
569 'es_AR': u'Spanish (AR) / Español (AR)',
570 'es_BO': u'Spanish (BO) / Español (BO)',
571 'es_CL': u'Spanish (CL) / Español (CL)',
572 'es_CO': u'Spanish (CO) / Español (CO)',
573 'es_CR': u'Spanish (CR) / Español (CR)',
574 'es_DO': u'Spanish (DO) / Español (DO)',
575 'es_EC': u'Spanish (EC) / Español (EC)',
576 'es_ES': u'Spanish / Español',
577 'es_GT': u'Spanish (GT) / Español (GT)',
578 'es_HN': u'Spanish (HN) / Español (HN)',
579 'es_MX': u'Spanish (MX) / Español (MX)',
580 'es_NI': u'Spanish (NI) / Español (NI)',
581 'es_PA': u'Spanish (PA) / Español (PA)',
582 'es_PE': u'Spanish (PE) / Español (PE)',
583 'es_PR': u'Spanish (PR) / Español (PR)',
584 'es_PY': u'Spanish (PY) / Español (PY)',
585 'es_SV': u'Spanish (SV) / Español (SV)',
586 'es_UY': u'Spanish (UY) / Español (UY)',
587 'es_VE': u'Spanish (VE) / Español (VE)',
588 'et_EE': u'Estonian / Eesti keel',
589 'fa_IR': u'Persian / فارس',
590 'fi_FI': u'Finnish / Suomi',
591 'fr_BE': u'French (BE) / Français (BE)',
592 'fr_CH': u'French (CH) / Français (CH)',
593 'fr_FR': u'French / Français',
594 'gl_ES': u'Galician / Galego',
595 'gu_IN': u'Gujarati / ગુજરાતી',
596 'he_IL': u'Hebrew / עִבְרִי',
597 'hi_IN': u'Hindi / हिंदी',
598 'hr_HR': u'Croatian / hrvatski jezik',
599 'hu_HU': u'Hungarian / Magyar',
600 'id_ID': u'Indonesian / Bahasa Indonesia',
601 'it_IT': u'Italian / Italiano',
602 'iu_CA': u'Inuktitut / ᐃᓄᒃᑎᑐᑦ',
603 'ja_JP': u'Japanese / 日本語',
604 'ko_KP': u'Korean (KP) / 한국어 (KP)',
605 'ko_KR': u'Korean (KR) / 한국어 (KR)',
606 'lt_LT': u'Lithuanian / Lietuvių kalba',
607 'lv_LV': u'Latvian / latviešu valoda',
608 'ml_IN': u'Malayalam / മലയാളം',
609 'mn_MN': u'Mongolian / монгол',
610 'nb_NO': u'Norwegian Bokmål / Norsk bokmål',
611 'nl_NL': u'Dutch / Nederlands',
612 'nl_BE': u'Flemish (BE) / Vlaams (BE)',
613 'oc_FR': u'Occitan (FR, post 1500) / Occitan',
614 'pl_PL': u'Polish / Język polski',
615 'pt_BR': u'Portugese (BR) / Português (BR)',
616 'pt_PT': u'Portugese / Português',
617 'ro_RO': u'Romanian / română',
618 'ru_RU': u'Russian / русский язык',
619 'si_LK': u'Sinhalese / සිංහල',
620 'sl_SI': u'Slovenian / slovenščina',
621 'sk_SK': u'Slovak / Slovenský jazyk',
622 'sq_AL': u'Albanian / Shqip',
623 'sr_RS': u'Serbian (Cyrillic) / српски',
624 'sr@latin': u'Serbian (Latin) / srpski',
625 'sv_SE': u'Swedish / svenska',
626 'te_IN': u'Telugu / తెలుగు',
627 'tr_TR': u'Turkish / Türkçe',
628 'vi_VN': u'Vietnamese / Tiếng Việt',
629 'uk_UA': u'Ukrainian / українська',
630 'ur_PK': u'Urdu / اردو',
631 'zh_CN': u'Chinese (CN) / 简体中文',
632 'zh_HK': u'Chinese (HK)',
633 'zh_TW': u'Chinese (TW) / 正體字',
634 'th_TH': u'Thai / ภาษาไทย',
635 'tlh_TLH': u'Klingon',
639 def scan_languages():
640 # Now it will take all languages from get languages function without filter it with base module languages
641 lang_dict = get_languages()
642 ret = [(lang, lang_dict.get(lang, lang)) for lang in list(lang_dict)]
643 ret.sort(key=lambda k:k[1])
647 def get_user_companies(cr, user):
648 def _get_company_children(cr, ids):
651 cr.execute('SELECT id FROM res_company WHERE parent_id IN %s', (tuple(ids),))
652 res = [x[0] for x in cr.fetchall()]
653 res.extend(_get_company_children(cr, res))
655 cr.execute('SELECT company_id FROM res_users WHERE id=%s', (user,))
656 user_comp = cr.fetchone()[0]
659 return [user_comp] + _get_company_children(cr, [user_comp])
663 Input number : account or invoice number
664 Output return: the same number completed with the recursive mod10
667 codec=[0,9,4,6,8,2,7,1,3,5]
673 report = codec[ (int(digit) + report) % 10 ]
674 return result + str((10 - report) % 10)
679 Return the size in a human readable format
683 units = ('bytes', 'Kb', 'Mb', 'Gb')
684 if isinstance(sz,basestring):
687 while s >= 1024 and i < len(units)-1:
690 return "%0.2f %s" % (s, units[i])
694 def wrapper(*args, **kwargs):
695 from pprint import pformat
697 vector = ['Call -> function: %r' % f]
698 for i, arg in enumerate(args):
699 vector.append(' arg %02d: %s' % (i, pformat(arg)))
700 for key, value in kwargs.items():
701 vector.append(' kwarg %10s: %s' % (key, pformat(value)))
704 res = f(*args, **kwargs)
706 vector.append(' result: %s' % pformat(res))
707 vector.append(' time delta: %s' % (time.time() - timeb4))
708 _logger.debug('\n'.join(vector))
713 class profile(object):
714 def __init__(self, fname=None):
717 def __call__(self, f):
719 def wrapper(*args, **kwargs):
720 class profile_wrapper(object):
724 self.result = f(*args, **kwargs)
725 pw = profile_wrapper()
727 fname = self.fname or ("%s.cprof" % (f.func_name,))
728 cProfile.runctx('pw()', globals(), locals(), filename=fname)
733 __icons_list = ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
734 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
735 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
736 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
737 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
738 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
739 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
740 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
741 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
742 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
743 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
744 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
745 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
746 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
747 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
748 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
749 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
750 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
751 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
752 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
753 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
754 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
755 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
756 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
757 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
758 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
759 'terp-check','terp-go-month','terp-go-year','terp-go-today','terp-document-new','terp-camera_test',
760 'terp-emblem-important','terp-gtk-media-pause','terp-gtk-stop','terp-gnome-cpu-frequency-applet+',
761 'terp-dialog-close','terp-gtk-jump-to-rtl','terp-gtk-jump-to-ltr','terp-accessories-archiver',
762 'terp-stock_align_left_24','terp-stock_effects-object-colorize','terp-go-home','terp-gtk-go-back-rtl',
763 'terp-gtk-go-back-ltr','terp-personal','terp-personal-','terp-personal+','terp-accessories-archiver-minus',
764 'terp-accessories-archiver+','terp-stock_symbol-selection','terp-call-start','terp-dolar',
765 'terp-face-plain','terp-folder-blue','terp-folder-green','terp-folder-orange','terp-folder-yellow',
766 'terp-gdu-smart-failing','terp-go-week','terp-gtk-select-all','terp-locked','terp-mail-forward',
767 'terp-mail-message-new','terp-mail-replied','terp-rating-rated','terp-stage','terp-stock_format-scientific',
768 'terp-dolar_ok!','terp-idea','terp-stock_format-default','terp-mail-','terp-mail_delete'
773 return [(x, x) for x in __icons_list ]
775 def extract_zip_file(zip_file, outdirectory):
776 zf = zipfile.ZipFile(zip_file, 'r')
778 for path in zf.namelist():
779 tgt = os.path.join(out, path)
780 tgtdir = os.path.dirname(tgt)
781 if not os.path.exists(tgtdir):
784 if not tgt.endswith(os.sep):
786 fp.write(zf.read(path))
790 def detect_ip_addr():
791 """Try a very crude method to figure out a valid external
792 IP or hostname for the current machine. Don't rely on this
793 for binding to an interface, but it could be used as basis
794 for constructing a remote URL to the server.
796 def _detect_ip_addr():
797 from array import array
798 from struct import pack, unpack
807 if not fcntl: # not UNIX:
808 host = socket.gethostname()
809 ip_addr = socket.gethostbyname(host)
811 # get all interfaces:
813 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
814 names = array('B', '\0' * nbytes)
815 #print 'names: ', names
816 outbytes = unpack('iL', fcntl.ioctl( s.fileno(), 0x8912, pack('iL', nbytes, names.buffer_info()[0])))[0]
817 namestr = names.tostring()
820 for i in range(0, outbytes, 40):
821 name = namestr[i:i+16].split('\0', 1)[0]
823 ip_addr = socket.inet_ntoa(namestr[i+20:i+24])
828 ifaces = filter(None, [namestr[i:i+32].split('\0', 1)[0] for i in range(0, outbytes, 32)])
830 for ifname in [iface for iface in ifaces if iface != 'lo']:
831 ip_addr = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, pack('256s', ifname[:15]))[20:24])
834 return ip_addr or 'localhost'
837 ip_addr = _detect_ip_addr()
839 ip_addr = 'localhost'
842 # RATIONALE BEHIND TIMESTAMP CALCULATIONS AND TIMEZONE MANAGEMENT:
843 # The server side never does any timestamp calculation, always
844 # sends them in a naive (timezone agnostic) format supposed to be
845 # expressed within the server timezone, and expects the clients to
846 # provide timestamps in the server timezone as well.
847 # It stores all timestamps in the database in naive format as well,
848 # which also expresses the time in the server timezone.
849 # For this reason the server makes its timezone name available via the
850 # common/timezone_get() rpc method, which clients need to read
851 # to know the appropriate time offset to use when reading/writing
853 def get_win32_timezone():
854 """Attempt to return the "standard name" of the current timezone on a win32 system.
855 @return the standard name of the current win32 timezone, or False if it cannot be found.
858 if (sys.platform == "win32"):
861 hklm = _winreg.ConnectRegistry(None,_winreg.HKEY_LOCAL_MACHINE)
862 current_tz_key = _winreg.OpenKey(hklm, r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", 0,_winreg.KEY_ALL_ACCESS)
863 res = str(_winreg.QueryValueEx(current_tz_key,"StandardName")[0]) # [0] is value, [1] is type code
864 _winreg.CloseKey(current_tz_key)
865 _winreg.CloseKey(hklm)
870 def detect_server_timezone():
871 """Attempt to detect the timezone to use on the server side.
872 Defaults to UTC if no working timezone can be found.
873 @return the timezone identifier as expected by pytz.timezone.
878 _logger.warning("Python pytz module is not available. "
879 "Timezone will be set to UTC by default.")
882 # Option 1: the configuration option (did not exist before, so no backwards compatibility issue)
883 # 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
884 # Option 3: the environment variable TZ
885 sources = [ (config['timezone'], 'OpenERP configuration'),
886 (time.tzname[0], 'time.tzname'),
887 (os.environ.get('TZ',False),'TZ environment variable'), ]
888 # Option 4: OS-specific: /etc/timezone on Unix
889 if (os.path.exists("/etc/timezone")):
892 f = open("/etc/timezone")
893 tz_value = f.read(128).strip()
898 sources.append((tz_value,"/etc/timezone file"))
899 # Option 5: timezone info from registry on Win32
900 if (sys.platform == "win32"):
901 # Timezone info is stored in windows registry.
902 # However this is not likely to work very well as the standard name
903 # of timezones in windows is rarely something that is known to pytz.
904 # But that's ok, it is always possible to use a config option to set
906 sources.append((get_win32_timezone(),"Windows Registry"))
908 for (value,source) in sources:
911 tz = pytz.timezone(value)
912 _logger.info("Using timezone %s obtained from %s.", tz.zone, source)
914 except pytz.UnknownTimeZoneError:
915 _logger.warning("The timezone specified in %s (%s) is invalid, ignoring it.", source, value)
917 _logger.warning("No valid timezone could be detected, using default UTC "
918 "timezone. You can specify it explicitly with option 'timezone' in "
919 "the server configuration.")
922 def get_server_timezone():
926 DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"
927 DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S"
928 DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % (
929 DEFAULT_SERVER_DATE_FORMAT,
930 DEFAULT_SERVER_TIME_FORMAT)
932 # Python's strftime supports only the format directives
933 # that are available on the platform's libc, so in order to
934 # be cross-platform we map to the directives required by
935 # the C standard (1989 version), always available on platforms
936 # with a C standard implementation.
937 DATETIME_FORMATS_MAP = {
939 '%D': '%m/%d/%Y', # modified %y->%Y
941 '%E': '', # special modifier
943 '%g': '%Y', # modified %y->%Y
949 '%O': '', # special modifier
953 '%s': '', #num of seconds since epoch
958 '%y': '%Y', # Even if %y works, it's ambiguous, so we should use %Y
959 '%+': '%Y-%m-%d %H:%M:%S',
961 # %Z is a special case that causes 2 problems at least:
962 # - the timezone names we use (in res_user.context_tz) come
963 # from pytz, but not all these names are recognized by
964 # strptime(), so we cannot convert in both directions
965 # when such a timezone is selected and %Z is in the format
966 # - %Z is replaced by an empty string in strftime() when
967 # there is not tzinfo in a datetime value (e.g when the user
968 # did not pick a context_tz). The resulting string does not
969 # parse back if the format requires %Z.
970 # As a consequence, we strip it completely from format strings.
971 # The user can always have a look at the context_tz in
972 # preferences to check the timezone.
977 def server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_name,
978 tz_offset=True, ignore_unparsable_time=True):
980 Convert a source timestamp string into a destination timestamp string, attempting to apply the
981 correct offset if both the server and local timezone are recognized, or no
982 offset at all if they aren't or if tz_offset is false (i.e. assuming they are both in the same TZ).
984 WARNING: This method is here to allow formatting dates correctly for inclusion in strings where
985 the client would not be able to format/offset it correctly. DO NOT use it for returning
986 date fields directly, these are supposed to be handled by the client!!
988 @param src_tstamp_str: the str value containing the timestamp in the server timezone.
989 @param src_format: the format to use when parsing the server timestamp.
990 @param dst_format: the format to use when formatting the resulting timestamp for the local/client timezone.
991 @param dst_tz_name: name of the destination timezone (such as the 'tz' value of the client context)
992 @param ignore_unparsable_time: if True, return False if src_tstamp_str cannot be parsed
993 using src_format or formatted using dst_format.
995 @return local/client formatted timestamp, expressed in the local/client timezone if possible
996 and if tz_offset is true, or src_tstamp_str if timezone offset could not be determined.
998 if not src_tstamp_str:
1001 res = src_tstamp_str
1002 if src_format and dst_format:
1003 # find out server timezone
1004 server_tz = get_server_timezone()
1006 # dt_value needs to be a datetime.datetime object (so no time.struct_time or mx.DateTime.DateTime here!)
1007 dt_value = datetime.strptime(src_tstamp_str, src_format)
1008 if tz_offset and dst_tz_name:
1011 src_tz = pytz.timezone(server_tz)
1012 dst_tz = pytz.timezone(dst_tz_name)
1013 src_dt = src_tz.localize(dt_value, is_dst=True)
1014 dt_value = src_dt.astimezone(dst_tz)
1017 res = dt_value.strftime(dst_format)
1019 # Normal ways to end up here are if strptime or strftime failed
1020 if not ignore_unparsable_time:
1025 def split_every(n, iterable, piece_maker=tuple):
1026 """Splits an iterable into length-n pieces. The last piece will be shorter
1027 if ``n`` does not evenly divide the iterable length.
1028 @param ``piece_maker``: function to build the pieces
1029 from the slices (tuple,list,...)
1031 iterator = iter(iterable)
1032 piece = piece_maker(islice(iterator, n))
1035 piece = piece_maker(islice(iterator, n))
1037 if __name__ == '__main__':
1041 class upload_data_thread(threading.Thread):
1042 def __init__(self, email, data, type):
1043 self.args = [('email',email),('type',type),('data',data)]
1044 super(upload_data_thread,self).__init__()
1048 args = urllib.urlencode(self.args)
1049 fp = urllib.urlopen('http://www.openerp.com/scripts/survey.php', args)
1055 def upload_data(email, data, type='SURVEY'):
1056 a = upload_data_thread(email, data, type)
1060 def get_and_group_by_field(cr, uid, obj, ids, field, context=None):
1061 """ Read the values of ``field´´ for the given ``ids´´ and group ids by value.
1063 :param string field: name of the field we want to read and group by
1064 :return: mapping of field values to the list of ids that have it
1068 for record in obj.read(cr, uid, ids, [field], context=context):
1070 res.setdefault(key[0] if isinstance(key, tuple) else key, []).append(record['id'])
1073 def get_and_group_by_company(cr, uid, obj, ids, context=None):
1074 return get_and_group_by_field(cr, uid, obj, ids, field='company_id', context=context)
1076 # port of python 2.6's attrgetter with support for dotted notation
1077 def resolve_attr(obj, attr):
1078 for name in attr.split("."):
1079 obj = getattr(obj, name)
1082 def attrgetter(*items):
1086 return resolve_attr(obj, attr)
1089 return tuple(resolve_attr(obj, attr) for attr in items)
1093 """A subclass of str that implements repr() without enclosing quotation marks
1094 or escaping, keeping the original string untouched. The name come from Lisp's unquote.
1095 One of the uses for this is to preserve or insert bare variable names within dicts during eval()
1096 of a dict's repr(). Use with care.
1098 Some examples (notice that there are never quotes surrounding
1099 the ``active_id`` name:
1101 >>> unquote('active_id')
1103 >>> d = {'test': unquote('active_id')}
1112 class UnquoteEvalContext(defaultdict):
1113 """Defaultdict-based evaluation context that returns
1114 an ``unquote`` string for any missing name used during
1116 Mostly useful for evaluating OpenERP domains/contexts that
1117 may refer to names that are unknown at the time of eval,
1118 so that when the context/domain is converted back to a string,
1119 the original names are preserved.
1121 **Warning**: using an ``UnquoteEvalContext`` as context for ``eval()`` or
1122 ``safe_eval()`` will shadow the builtins, which may cause other
1123 failures, depending on what is evaluated.
1125 Example (notice that ``section_id`` is preserved in the final
1128 >>> context_str = "{'default_user_id': uid, 'default_section_id': section_id}"
1129 >>> eval(context_str, UnquoteEvalContext(uid=1))
1130 {'default_user_id': 1, 'default_section_id': section_id}
1133 def __init__(self, *args, **kwargs):
1134 super(UnquoteEvalContext, self).__init__(None, *args, **kwargs)
1136 def __missing__(self, key):
1139 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: