fbdd32e7cb1db49a2e0f9de68f8629b010b2c9bb
[odoo/odoo.git] / openerp / tools / misc.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
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>).
7 #
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.
12 #
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.
17 #
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/>.
20 #
21 ##############################################################################
22
23
24 """
25 Miscellaneous tools used by OpenERP.
26 """
27
28 from functools import wraps
29 import cProfile
30 from contextlib import contextmanager
31 import subprocess
32 import logging
33 import os
34 import socket
35 import sys
36 import threading
37 import time
38 import werkzeug.utils
39 import zipfile
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
46 import traceback
47
48 try:
49     from html2text import html2text
50 except ImportError:
51     html2text = None
52
53 from config import config
54 from cache import *
55 from .parse_version import parse_version 
56
57 import openerp
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
61
62 _logger = logging.getLogger(__name__)
63
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)
67
68 def find_in_path(name):
69     try:
70         return which(name)
71     except IOError:
72         return None
73
74 def find_pg_tool(name):
75     path = None
76     if config['pg_path'] and config['pg_path'] != 'None':
77         path = config['pg_path']
78     try:
79         return which(name, path=path)
80     except IOError:
81         return None
82
83 def exec_pg_command(name, *args):
84     prog = find_pg_tool(name)
85     if not prog:
86         raise Exception('Couldn\'t find %s' % name)
87     args2 = (prog,) + args
88
89     with open(os.devnull) as dn:
90         return subprocess.call(args2, stdout=dn, stderr=subprocess.STDOUT)
91
92 def exec_pg_command_pipe(name, *args):
93     prog = find_pg_tool(name)
94     if not prog:
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
102
103 def exec_command_pipe(name, *args):
104     prog = find_in_path(name)
105     if not prog:
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
113
114 #----------------------------------------------------------
115 # File paths
116 #----------------------------------------------------------
117 #file_path_root = os.getcwd()
118 #file_path_addons = os.path.join(file_path_root, 'addons')
119
120 def file_open(name, mode="r", subdir='addons', pathinfo=False):
121     """Open a file from the OpenERP root, using a subdir folder.
122
123     Example::
124     
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)
128
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)
133
134     @return fileobject if pathinfo is False else (fileobject, filepath)
135     """
136     import openerp.modules as addons
137     adps = addons.module.ad_paths
138     rtp = os.path.normcase(os.path.abspath(config['root_path']))
139
140     basename = name
141
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:]
151                 break
152         else:
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)
156
157     if name.replace(os.sep, '/').startswith('addons/'):
158         subdir = 'addons'
159         name2 = name[7:]
160     elif subdir:
161         name = os.path.join(subdir, name)
162         if name.replace(os.sep, '/').startswith('addons/'):
163             subdir = 'addons'
164             name2 = name[7:]
165         else:
166             name2 = name
167
168     # First, try to locate in addons_path
169     if subdir:
170         for adp in adps:
171             try:
172                 return _fileopen(name2, mode=mode, basedir=adp,
173                                  pathinfo=pathinfo, basename=basename)
174             except IOError:
175                 pass
176
177     # Second, try to locate in root_path
178     return _fileopen(name, mode=mode, basedir=rtp, pathinfo=pathinfo, basename=basename)
179
180
181 def _fileopen(path, mode, basedir, pathinfo, basename=None):
182     name = os.path.normpath(os.path.join(basedir, path))
183
184     if basename is None:
185         basename = name
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)
190         if pathinfo:
191             return fo, name
192         return fo
193
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)
198     zipname = False
199     while os.sep in head:
200         head, tail = os.path.split(head)
201         if not tail:
202             break
203         if zipname:
204             zipname = os.path.join(tail, zipname)
205         else:
206             zipname = tail
207         zpath = os.path.join(basedir, head + '.zip')
208         if zipfile.is_zipfile(zpath):
209             from cStringIO import StringIO
210             zfile = zipfile.ZipFile(zpath)
211             try:
212                 fo = StringIO()
213                 fo.write(zfile.read(os.path.join(
214                     os.path.basename(head), zipname).replace(
215                         os.sep, '/')))
216                 fo.seek(0)
217                 if pathinfo:
218                     return fo, name
219                 return fo
220             except Exception:
221                 pass
222     # Not found
223     if name.endswith('.rml'):
224         raise IOError('Report %r doesn\'t exist or deleted' % basename)
225     raise IOError('File not found: %s' % basename)
226
227
228 #----------------------------------------------------------
229 # iterables
230 #----------------------------------------------------------
231 def flatten(list):
232     """Flatten a list of elements into a uniqu list
233     Author: Christophe Simonis (christophe@tinyerp.com)
234
235     Examples::
236     >>> flatten(['a'])
237     ['a']
238     >>> flatten('b')
239     ['b']
240     >>> flatten( [] )
241     []
242     >>> flatten( [[], [[]]] )
243     []
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,)], []]])
247     >>> flatten(t)
248     [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
249     """
250
251     def isiterable(x):
252         return hasattr(x, "__iter__")
253
254     r = []
255     for e in list:
256         if isiterable(e):
257             map(r.append, flatten(e))
258         else:
259             r.append(e)
260     return r
261
262 def reverse_enumerate(l):
263     """Like enumerate but in the other sens
264     
265     Usage::
266     >>> a = ['a', 'b', 'c']
267     >>> it = reverse_enumerate(a)
268     >>> it.next()
269     (2, 'c')
270     >>> it.next()
271     (1, 'b')
272     >>> it.next()
273     (0, 'a')
274     >>> it.next()
275     Traceback (most recent call last):
276       File "<stdin>", line 1, in <module>
277     StopIteration
278     """
279     return izip(xrange(len(l)-1, -1, -1), reversed(l))
280
281
282 class UpdateableStr(local):
283     """ Class that stores an updateable string (used in wizards)
284     """
285
286     def __init__(self, string=''):
287         self.string = string
288
289     def __str__(self):
290         return str(self.string)
291
292     def __repr__(self):
293         return str(self.string)
294
295     def __nonzero__(self):
296         return bool(self.string)
297
298
299 class UpdateableDict(local):
300     """Stores an updateable dict to use in wizards
301     """
302
303     def __init__(self, dict=None):
304         if dict is None:
305             dict = {}
306         self.dict = dict
307
308     def __str__(self):
309         return str(self.dict)
310
311     def __repr__(self):
312         return str(self.dict)
313
314     def clear(self):
315         return self.dict.clear()
316
317     def keys(self):
318         return self.dict.keys()
319
320     def __setitem__(self, i, y):
321         self.dict.__setitem__(i, y)
322
323     def __getitem__(self, i):
324         return self.dict.__getitem__(i)
325
326     def copy(self):
327         return self.dict.copy()
328
329     def iteritems(self):
330         return self.dict.iteritems()
331
332     def iterkeys(self):
333         return self.dict.iterkeys()
334
335     def itervalues(self):
336         return self.dict.itervalues()
337
338     def pop(self, k, d=None):
339         return self.dict.pop(k, d)
340
341     def popitem(self):
342         return self.dict.popitem()
343
344     def setdefault(self, k, d=None):
345         return self.dict.setdefault(k, d)
346
347     def update(self, E, **F):
348         return self.dict.update(E, F)
349
350     def values(self):
351         return self.dict.values()
352
353     def get(self, k, d=None):
354         return self.dict.get(k, d)
355
356     def has_key(self, k):
357         return self.dict.has_key(k)
358
359     def items(self):
360         return self.dict.items()
361
362     def __cmp__(self, y):
363         return self.dict.__cmp__(y)
364
365     def __contains__(self, k):
366         return self.dict.__contains__(k)
367
368     def __delitem__(self, y):
369         return self.dict.__delitem__(y)
370
371     def __eq__(self, y):
372         return self.dict.__eq__(y)
373
374     def __ge__(self, y):
375         return self.dict.__ge__(y)
376
377     def __gt__(self, y):
378         return self.dict.__gt__(y)
379
380     def __hash__(self):
381         return self.dict.__hash__()
382
383     def __iter__(self):
384         return self.dict.__iter__()
385
386     def __le__(self, y):
387         return self.dict.__le__(y)
388
389     def __len__(self):
390         return self.dict.__len__()
391
392     def __lt__(self, y):
393         return self.dict.__lt__(y)
394
395     def __ne__(self, y):
396         return self.dict.__ne__(y)
397
398 class currency(float):
399     """ Deprecate
400     
401     .. warning::
402     
403     Don't use ! Use res.currency.round()
404     """
405
406     def __init__(self, value, accuracy=2, rounding=None):
407         if rounding is None:
408             rounding=10**-accuracy
409         self.rounding=rounding
410         self.accuracy=accuracy
411
412     def __new__(cls, value, accuracy=2, rounding=None):
413         return float.__new__(cls, round(value, accuracy))
414
415     #def __str__(self):
416     #   display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
417     #   return str(display_value)
418
419 def to_xml(s):
420     return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;')
421
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]
426     return lang
427
428 ALL_LANGUAGES = {
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',
512     }
513
514 def scan_languages():
515     """ Returns all languages supported by OpenERP for translation
516
517     :returns: a list of (lang_code, lang_name) pairs
518     :rtype: [(str, unicode)]
519     """
520     return sorted(ALL_LANGUAGES.iteritems(), key=lambda k: k[1])
521
522 def get_user_companies(cr, user):
523     def _get_company_children(cr, ids):
524         if not ids:
525             return []
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))
529         return res
530     cr.execute('SELECT company_id FROM res_users WHERE id=%s', (user,))
531     user_comp = cr.fetchone()[0]
532     if not user_comp:
533         return []
534     return [user_comp] + _get_company_children(cr, [user_comp])
535
536 def mod10r(number):
537     """
538     Input number : account or invoice number
539     Output return: the same number completed with the recursive mod10
540     key
541     """
542     codec=[0,9,4,6,8,2,7,1,3,5]
543     report = 0
544     result=""
545     for digit in number:
546         result += digit
547         if digit.isdigit():
548             report = codec[ (int(digit) + report) % 10 ]
549     return result + str((10 - report) % 10)
550
551
552 def human_size(sz):
553     """
554     Return the size in a human readable format
555     """
556     if not sz:
557         return False
558     units = ('bytes', 'Kb', 'Mb', 'Gb')
559     if isinstance(sz,basestring):
560         sz=len(sz)
561     s, i = float(sz), 0
562     while s >= 1024 and i < len(units)-1:
563         s /= 1024
564         i += 1
565     return "%0.2f %s" % (s, units[i])
566
567 def logged(f):
568     @wraps(f)
569     def wrapper(*args, **kwargs):
570         from pprint import pformat
571
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)))
577
578         timeb4 = time.time()
579         res = f(*args, **kwargs)
580
581         vector.append('  result: %s' % pformat(res))
582         vector.append('  time delta: %s' % (time.time() - timeb4))
583         _logger.debug('\n'.join(vector))
584         return res
585
586     return wrapper
587
588 class profile(object):
589     def __init__(self, fname=None):
590         self.fname = fname
591
592     def __call__(self, f):
593         @wraps(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,)))
598             return result
599
600         return wrapper
601
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'
638 ]
639
640 def icons(*a, **kw):
641     global __icons_list
642     return [(x, x) for x in __icons_list ]
643
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.
649     """
650     def _detect_ip_addr():
651         from array import array
652         from struct import pack, unpack
653
654         try:
655             import fcntl
656         except ImportError:
657             fcntl = None
658
659         ip_addr = None
660
661         if not fcntl: # not UNIX:
662             host = socket.gethostname()
663             ip_addr = socket.gethostbyname(host)
664         else: # UNIX:
665             # get all interfaces:
666             nbytes = 128 * 32
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()
672
673             # try 64 bit kernel:
674             for i in range(0, outbytes, 40):
675                 name = namestr[i:i+16].split('\0', 1)[0]
676                 if name != 'lo':
677                     ip_addr = socket.inet_ntoa(namestr[i+20:i+24])
678                     break
679
680             # try 32 bit kernel:
681             if ip_addr is None:
682                 ifaces = filter(None, [namestr[i:i+32].split('\0', 1)[0] for i in range(0, outbytes, 32)])
683
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])
686                     break
687
688         return ip_addr or 'localhost'
689
690     try:
691         ip_addr = _detect_ip_addr()
692     except Exception:
693         ip_addr = 'localhost'
694     return ip_addr
695
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
706 #  times.
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.
710     """
711     res = False
712     if sys.platform == "win32":
713         try:
714             import _winreg
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)
720         except Exception:
721             pass
722     return res
723
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.
728     """
729     try:
730         import pytz
731     except Exception:
732         _logger.warning("Python pytz module is not available. "
733             "Timezone will be set to UTC by default.")
734         return 'UTC'
735
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"):
744         tz_value = False
745         try:
746             f = open("/etc/timezone")
747             tz_value = f.read(128).strip()
748         except Exception:
749             pass
750         finally:
751             f.close()
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
759         # it explicitly.
760         sources.append((get_win32_timezone(),"Windows Registry"))
761
762     for (value,source) in sources:
763         if value:
764             try:
765                 tz = pytz.timezone(value)
766                 _logger.info("Using timezone %s obtained from %s.", tz.zone, source)
767                 return value
768             except pytz.UnknownTimeZoneError:
769                 _logger.warning("The timezone specified in %s (%s) is invalid, ignoring it.", source, value)
770
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.")
774     return 'UTC'
775
776 def get_server_timezone():
777     return "UTC"
778
779
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)
785
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 = {
792         '%C': '', # century
793         '%D': '%m/%d/%Y', # modified %y->%Y
794         '%e': '%d',
795         '%E': '', # special modifier
796         '%F': '%Y-%m-%d',
797         '%g': '%Y', # modified %y->%Y
798         '%G': '%Y',
799         '%h': '%b',
800         '%k': '%H',
801         '%l': '%I',
802         '%n': '\n',
803         '%O': '', # special modifier
804         '%P': '%p',
805         '%R': '%H:%M',
806         '%r': '%I:%M:%S %p',
807         '%s': '', #num of seconds since epoch
808         '%T': '%H:%M:%S',
809         '%t': ' ', # tab
810         '%u': ' %w',
811         '%V': '%W',
812         '%y': '%Y', # Even if %y works, it's ambiguous, so we should use %Y
813         '%+': '%Y-%m-%d %H:%M:%S',
814
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.
827         '%z': '',
828         '%Z': '',
829 }
830
831 POSIX_TO_LDML = {
832     'a': 'E',
833     'A': 'EEEE',
834     'b': 'MMM',
835     'B': 'MMMM',
836     #'c': '',
837     'd': 'dd',
838     'H': 'HH',
839     'I': 'hh',
840     'j': 'DDD',
841     'm': 'MM',
842     'M': 'mm',
843     'p': 'a',
844     'S': 'ss',
845     'U': 'w',
846     'w': 'e',
847     'W': 'w',
848     'y': 'yy',
849     'Y': 'yyyy',
850     # see comments above, and babel's format_datetime assumes an UTC timezone
851     # for naive datetime objects
852     #'z': 'Z',
853     #'Z': 'z',
854 }
855
856 def posix_to_ldml(fmt, locale):
857     """ Converts a posix/strftime pattern into an LDML date format pattern.
858
859     :param fmt: non-extended C89/C90 strftime pattern
860     :param locale: babel locale used for locale-specific conversions (e.g. %x and %X)
861     :return: unicode
862     """
863     buf = []
864     pc = False
865     quoted = []
866
867     for c in fmt:
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 "''")
871             continue
872         if quoted:
873             buf.append("'")
874             buf.append(''.join(quoted))
875             buf.append("'")
876             quoted = []
877
878         if pc:
879             if c == '%': # escaped percent
880                 buf.append('%')
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])
887             pc = False
888         elif c == '%':
889             pc = True
890         else:
891             buf.append(c)
892
893     # flush anything remaining in quoted buffer
894     if quoted:
895         buf.append("'")
896         buf.append(''.join(quoted))
897         buf.append("'")
898
899     return ''.join(buf)
900
901 def server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_name,
902         tz_offset=True, ignore_unparsable_time=True):
903     """
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).
907
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!!
911
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.
918
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.
921     """
922     if not src_tstamp_str:
923         return False
924
925     res = src_tstamp_str
926     if src_format and dst_format:
927         # find out server timezone
928         server_tz = get_server_timezone()
929         try:
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:
933                 try:
934                     import pytz
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)
939                 except Exception:
940                     pass
941             res = dt_value.strftime(dst_format)
942         except Exception:
943             # Normal ways to end up here are if strptime or strftime failed
944             if not ignore_unparsable_time:
945                 return False
946     return res
947
948
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,...)
954     """
955     iterator = iter(iterable)
956     piece = piece_maker(islice(iterator, n))
957     while piece:
958         yield piece
959         piece = piece_maker(islice(iterator, n))
960
961 if __name__ == '__main__':
962     import doctest
963     doctest.testmod()
964
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__()
969     def run(self):
970         try:
971             import urllib
972             args = urllib.urlencode(self.args)
973             fp = urllib.urlopen('http://www.openerp.com/scripts/survey.php', args)
974             fp.read()
975             fp.close()
976         except Exception:
977             pass
978
979 def upload_data(email, data, type='SURVEY'):
980     a = upload_data_thread(email, data, type)
981     a.start()
982     return True
983
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.
986
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
989        :rtype: dict
990     """
991     res = {}
992     for record in obj.read(cr, uid, ids, [field], context=context):
993         key = record[field]
994         res.setdefault(key[0] if isinstance(key, tuple) else key, []).append(record['id'])
995     return res
996
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)
999
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)
1004     return obj
1005
1006 def attrgetter(*items):
1007     if len(items) == 1:
1008         attr = items[0]
1009         def g(obj):
1010             return resolve_attr(obj, attr)
1011     else:
1012         def g(obj):
1013             return tuple(resolve_attr(obj, attr) for attr in items)
1014     return g
1015
1016 class unquote(str):
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.
1021
1022        Some examples (notice that there are never quotes surrounding
1023        the ``active_id`` name:
1024
1025        >>> unquote('active_id')
1026        active_id
1027        >>> d = {'test': unquote('active_id')}
1028        >>> d
1029        {'test': active_id}
1030        >>> print d
1031        {'test': active_id}
1032     """
1033     def __repr__(self):
1034         return self
1035
1036 class UnquoteEvalContext(defaultdict):
1037     """Defaultdict-based evaluation context that returns 
1038        an ``unquote`` string for any missing name used during
1039        the evaluation.
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.
1044
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.
1048
1049        Example (notice that ``section_id`` is preserved in the final
1050        result) :
1051
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}
1055
1056        """
1057     def __init__(self, *args, **kwargs):
1058         super(UnquoteEvalContext, self).__init__(None, *args, **kwargs)
1059
1060     def __missing__(self, key):
1061         return unquote(key)
1062
1063
1064 class mute_logger(object):
1065     """Temporary suppress the logging.
1066     Can be used as context manager or decorator.
1067
1068         @mute_logger('openerp.plic.ploc')
1069         def do_stuff():
1070             blahblah()
1071
1072         with mute_logger('openerp.foo.bar'):
1073             do_suff()
1074
1075     """
1076     def __init__(self, *loggers):
1077         self.loggers = loggers
1078
1079     def filter(self, record):
1080         return 0
1081
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)
1087
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)
1091
1092     def __call__(self, func):
1093         @wraps(func)
1094         def deco(*args, **kwargs):
1095             with self:
1096                 return func(*args, **kwargs)
1097         return deco
1098
1099 _ph = object()
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)
1105
1106     ``start`` allows overriding the starting index (the index before the first
1107     item is returned).
1108
1109     On each iteration (call to :meth:`~.next`), increases its :attr:`~.index`
1110     by one.
1111
1112     .. attribute:: index
1113
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
1116     """
1117     def __init__(self, stream, start=-1):
1118         self.stream = iter(stream)
1119         self.index = start
1120         self.stopped = False
1121     def __iter__(self):
1122         return self
1123     def next(self):
1124         if self.stopped: raise StopIteration()
1125         self.index += 1
1126         val = next(self.stream, _ph)
1127         if val is _ph:
1128             self.stopped = True
1129             raise StopIteration()
1130         return val
1131
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)
1137
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])
1140
1141     args = sys.argv[:]
1142
1143     def strip(args, i):
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]])
1147
1148     return [x for i, x in enumerate(args) if not strip(args, i)]
1149
1150
1151 class ConstantMapping(Mapping):
1152     """
1153     An immutable mapping returning the provided value for every single key.
1154
1155     Useful for default value to methods
1156     """
1157     __slots__ = ['_value']
1158     def __init__(self, val):
1159         self._value = val
1160
1161     def __len__(self):
1162         """
1163         defaultdict updates its length for each individually requested key, is
1164         that really useful?
1165         """
1166         return 0
1167
1168     def __iter__(self):
1169         """
1170         same as len, defaultdict udpates its iterable keyset with each key
1171         requested, is there a point for this?
1172         """
1173         return iter([])
1174
1175     def __getitem__(self, item):
1176         return self._value
1177
1178
1179 def dumpstacks(sig=None, frame=None):
1180     """ Signal handler: dump a stack trace for each existing thread."""
1181     code = []
1182
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)
1186             if line:
1187                 yield "  %s" % (line.strip(),)
1188
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',
1197                      threadId,
1198                      thread_info and thread_info['uid'] or 'n/a'))
1199         for line in extract_stack(stack):
1200             code.append(line)
1201
1202     if openerp.evented:
1203         # code from http://stackoverflow.com/questions/12510648/in-gevent-how-can-i-dump-stack-traces-of-all-running-greenlets
1204         import gc
1205         from greenlet import greenlet
1206         for ob in gc.get_objects():
1207             if not isinstance(ob, greenlet) or not ob:
1208                 continue
1209             code.append("\n# Greenlet: %r" % (ob,))
1210             for line in extract_stack(ob.gr_frame):
1211                 code.append(line)
1212
1213     _logger.info("\n".join(code))
1214
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")
1221     def clear(self):
1222         raise NotImplementedError("'clear' not supported on frozendict")
1223     def pop(self, key, default=None):
1224         raise NotImplementedError("'pop' not supported on frozendict")
1225     def popitem(self):
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")
1231
1232 @contextmanager
1233 def ignore(*exc):
1234     try:
1235         yield
1236     except exc:
1237         pass
1238
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)
1243 else:
1244     def html_escape(text):
1245         return werkzeug.utils.escape(text)
1246
1247 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: