[MERGE] Forward-port of latest saas-2 bugfixes, up to rev.5012 revid:mat@openerp...
[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-2013 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 import subprocess
31 import logging
32 import os
33 import socket
34 import sys
35 import threading
36 import time
37 import zipfile
38 from collections import defaultdict, Mapping
39 from datetime import datetime
40 from itertools import islice, izip, groupby
41 from lxml import etree
42 from which import which
43 from threading import local
44 import traceback
45
46 try:
47     from html2text import html2text
48 except ImportError:
49     html2text = None
50
51 from config import config
52 from cache import *
53
54 import openerp
55 # get_encodings, ustr and exception_to_unicode were originally from tools.misc.
56 # There are moved to loglevels until we refactor tools.
57 from openerp.loglevels import get_encodings, ustr, exception_to_unicode     # noqa
58
59 _logger = logging.getLogger(__name__)
60
61 # List of etree._Element subclasses that we choose to ignore when parsing XML.
62 # We include the *Base ones just in case, currently they seem to be subclasses of the _* ones.
63 SKIPPED_ELEMENT_TYPES = (etree._Comment, etree._ProcessingInstruction, etree.CommentBase, etree.PIBase)
64
65 def find_in_path(name):
66     try:
67         return which(name)
68     except IOError:
69         return None
70
71 def find_pg_tool(name):
72     path = None
73     if config['pg_path'] and config['pg_path'] != 'None':
74         path = config['pg_path']
75     try:
76         return which(name, path=path)
77     except IOError:
78         return None
79
80 def exec_pg_command(name, *args):
81     prog = find_pg_tool(name)
82     if not prog:
83         raise Exception('Couldn\'t find %s' % name)
84     args2 = (prog,) + args
85
86     return subprocess.call(args2)
87
88 def exec_pg_command_pipe(name, *args):
89     prog = find_pg_tool(name)
90     if not prog:
91         raise Exception('Couldn\'t find %s' % name)
92     # on win32, passing close_fds=True is not compatible
93     # with redirecting std[in/err/out]
94     pop = subprocess.Popen((prog,) + args, bufsize= -1,
95           stdin=subprocess.PIPE, stdout=subprocess.PIPE,
96           close_fds=(os.name=="posix"))
97     return pop.stdin, pop.stdout
98
99 def exec_command_pipe(name, *args):
100     prog = find_in_path(name)
101     if not prog:
102         raise Exception('Couldn\'t find %s' % name)
103     # on win32, passing close_fds=True is not compatible
104     # with redirecting std[in/err/out]
105     pop = subprocess.Popen((prog,) + args, bufsize= -1,
106           stdin=subprocess.PIPE, stdout=subprocess.PIPE,
107           close_fds=(os.name=="posix"))
108     return pop.stdin, pop.stdout
109
110 #----------------------------------------------------------
111 # File paths
112 #----------------------------------------------------------
113 #file_path_root = os.getcwd()
114 #file_path_addons = os.path.join(file_path_root, 'addons')
115
116 def file_open(name, mode="r", subdir='addons', pathinfo=False):
117     """Open a file from the OpenERP root, using a subdir folder.
118
119     Example::
120     
121     >>> file_open('hr/report/timesheer.xsl')
122     >>> file_open('addons/hr/report/timesheet.xsl')
123     >>> file_open('../../base/report/rml_template.xsl', subdir='addons/hr/report', pathinfo=True)
124
125     @param name name of the file
126     @param mode file open mode
127     @param subdir subdirectory
128     @param pathinfo if True returns tuple (fileobject, filepath)
129
130     @return fileobject if pathinfo is False else (fileobject, filepath)
131     """
132     import openerp.modules as addons
133     adps = addons.module.ad_paths
134     rtp = os.path.normcase(os.path.abspath(config['root_path']))
135
136     basename = name
137
138     if os.path.isabs(name):
139         # It is an absolute path
140         # Is it below 'addons_path' or 'root_path'?
141         name = os.path.normcase(os.path.normpath(name))
142         for root in adps + [rtp]:
143             root = os.path.normcase(os.path.normpath(root)) + os.sep
144             if name.startswith(root):
145                 base = root.rstrip(os.sep)
146                 name = name[len(base) + 1:]
147                 break
148         else:
149             # It is outside the OpenERP root: skip zipfile lookup.
150             base, name = os.path.split(name)
151         return _fileopen(name, mode=mode, basedir=base, pathinfo=pathinfo, basename=basename)
152
153     if name.replace(os.sep, '/').startswith('addons/'):
154         subdir = 'addons'
155         name2 = name[7:]
156     elif subdir:
157         name = os.path.join(subdir, name)
158         if name.replace(os.sep, '/').startswith('addons/'):
159             subdir = 'addons'
160             name2 = name[7:]
161         else:
162             name2 = name
163
164     # First, try to locate in addons_path
165     if subdir:
166         for adp in adps:
167             try:
168                 return _fileopen(name2, mode=mode, basedir=adp,
169                                  pathinfo=pathinfo, basename=basename)
170             except IOError:
171                 pass
172
173     # Second, try to locate in root_path
174     return _fileopen(name, mode=mode, basedir=rtp, pathinfo=pathinfo, basename=basename)
175
176
177 def _fileopen(path, mode, basedir, pathinfo, basename=None):
178     name = os.path.normpath(os.path.join(basedir, path))
179
180     if basename is None:
181         basename = name
182     # Give higher priority to module directories, which is
183     # a more common case than zipped modules.
184     if os.path.isfile(name):
185         fo = open(name, mode)
186         if pathinfo:
187             return fo, name
188         return fo
189
190     # Support for loading modules in zipped form.
191     # This will not work for zipped modules that are sitting
192     # outside of known addons paths.
193     head = os.path.normpath(path)
194     zipname = False
195     while os.sep in head:
196         head, tail = os.path.split(head)
197         if not tail:
198             break
199         if zipname:
200             zipname = os.path.join(tail, zipname)
201         else:
202             zipname = tail
203         zpath = os.path.join(basedir, head + '.zip')
204         if zipfile.is_zipfile(zpath):
205             from cStringIO import StringIO
206             zfile = zipfile.ZipFile(zpath)
207             try:
208                 fo = StringIO()
209                 fo.write(zfile.read(os.path.join(
210                     os.path.basename(head), zipname).replace(
211                         os.sep, '/')))
212                 fo.seek(0)
213                 if pathinfo:
214                     return fo, name
215                 return fo
216             except Exception:
217                 pass
218     # Not found
219     if name.endswith('.rml'):
220         raise IOError('Report %r doesn\'t exist or deleted' % basename)
221     raise IOError('File not found: %s' % basename)
222
223
224 #----------------------------------------------------------
225 # iterables
226 #----------------------------------------------------------
227 def flatten(list):
228     """Flatten a list of elements into a uniqu list
229     Author: Christophe Simonis (christophe@tinyerp.com)
230
231     Examples::
232     >>> flatten(['a'])
233     ['a']
234     >>> flatten('b')
235     ['b']
236     >>> flatten( [] )
237     []
238     >>> flatten( [[], [[]]] )
239     []
240     >>> flatten( [[['a','b'], 'c'], 'd', ['e', [], 'f']] )
241     ['a', 'b', 'c', 'd', 'e', 'f']
242     >>> t = (1,2,(3,), [4, 5, [6, [7], (8, 9), ([10, 11, (12, 13)]), [14, [], (15,)], []]])
243     >>> flatten(t)
244     [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
245     """
246
247     def isiterable(x):
248         return hasattr(x, "__iter__")
249
250     r = []
251     for e in list:
252         if isiterable(e):
253             map(r.append, flatten(e))
254         else:
255             r.append(e)
256     return r
257
258 def reverse_enumerate(l):
259     """Like enumerate but in the other sens
260     
261     Usage::
262     >>> a = ['a', 'b', 'c']
263     >>> it = reverse_enumerate(a)
264     >>> it.next()
265     (2, 'c')
266     >>> it.next()
267     (1, 'b')
268     >>> it.next()
269     (0, 'a')
270     >>> it.next()
271     Traceback (most recent call last):
272       File "<stdin>", line 1, in <module>
273     StopIteration
274     """
275     return izip(xrange(len(l)-1, -1, -1), reversed(l))
276
277
278 class UpdateableStr(local):
279     """ Class that stores an updateable string (used in wizards)
280     """
281
282     def __init__(self, string=''):
283         self.string = string
284
285     def __str__(self):
286         return str(self.string)
287
288     def __repr__(self):
289         return str(self.string)
290
291     def __nonzero__(self):
292         return bool(self.string)
293
294
295 class UpdateableDict(local):
296     """Stores an updateable dict to use in wizards
297     """
298
299     def __init__(self, dict=None):
300         if dict is None:
301             dict = {}
302         self.dict = dict
303
304     def __str__(self):
305         return str(self.dict)
306
307     def __repr__(self):
308         return str(self.dict)
309
310     def clear(self):
311         return self.dict.clear()
312
313     def keys(self):
314         return self.dict.keys()
315
316     def __setitem__(self, i, y):
317         self.dict.__setitem__(i, y)
318
319     def __getitem__(self, i):
320         return self.dict.__getitem__(i)
321
322     def copy(self):
323         return self.dict.copy()
324
325     def iteritems(self):
326         return self.dict.iteritems()
327
328     def iterkeys(self):
329         return self.dict.iterkeys()
330
331     def itervalues(self):
332         return self.dict.itervalues()
333
334     def pop(self, k, d=None):
335         return self.dict.pop(k, d)
336
337     def popitem(self):
338         return self.dict.popitem()
339
340     def setdefault(self, k, d=None):
341         return self.dict.setdefault(k, d)
342
343     def update(self, E, **F):
344         return self.dict.update(E, F)
345
346     def values(self):
347         return self.dict.values()
348
349     def get(self, k, d=None):
350         return self.dict.get(k, d)
351
352     def has_key(self, k):
353         return self.dict.has_key(k)
354
355     def items(self):
356         return self.dict.items()
357
358     def __cmp__(self, y):
359         return self.dict.__cmp__(y)
360
361     def __contains__(self, k):
362         return self.dict.__contains__(k)
363
364     def __delitem__(self, y):
365         return self.dict.__delitem__(y)
366
367     def __eq__(self, y):
368         return self.dict.__eq__(y)
369
370     def __ge__(self, y):
371         return self.dict.__ge__(y)
372
373     def __gt__(self, y):
374         return self.dict.__gt__(y)
375
376     def __hash__(self):
377         return self.dict.__hash__()
378
379     def __iter__(self):
380         return self.dict.__iter__()
381
382     def __le__(self, y):
383         return self.dict.__le__(y)
384
385     def __len__(self):
386         return self.dict.__len__()
387
388     def __lt__(self, y):
389         return self.dict.__lt__(y)
390
391     def __ne__(self, y):
392         return self.dict.__ne__(y)
393
394 class currency(float):
395     """ Deprecate
396     
397     .. warning::
398     
399     Don't use ! Use res.currency.round()
400     """
401
402     def __init__(self, value, accuracy=2, rounding=None):
403         if rounding is None:
404             rounding=10**-accuracy
405         self.rounding=rounding
406         self.accuracy=accuracy
407
408     def __new__(cls, value, accuracy=2, rounding=None):
409         return float.__new__(cls, round(value, accuracy))
410
411     #def __str__(self):
412     #   display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
413     #   return str(display_value)
414
415 def to_xml(s):
416     return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;')
417
418 def get_iso_codes(lang):
419     if lang.find('_') != -1:
420         if lang.split('_')[0] == lang.split('_')[1].lower():
421             lang = lang.split('_')[0]
422     return lang
423
424 ALL_LANGUAGES = {
425         'ab_RU': u'Abkhazian / аҧсуа',
426         'am_ET': u'Amharic / አምሃርኛ',
427         'ar_SY': u'Arabic / الْعَرَبيّة',
428         'bg_BG': u'Bulgarian / български език',
429         'bs_BS': u'Bosnian / bosanski jezik',
430         'ca_ES': u'Catalan / Català',
431         'cs_CZ': u'Czech / Čeština',
432         'da_DK': u'Danish / Dansk',
433         'de_DE': u'German / Deutsch',
434         'el_GR': u'Greek / Ελληνικά',
435         'en_CA': u'English (CA)',
436         'en_GB': u'English (UK)',
437         'en_US': u'English (US)',
438         'es_AR': u'Spanish (AR) / Español (AR)',
439         'es_BO': u'Spanish (BO) / Español (BO)',
440         'es_CL': u'Spanish (CL) / Español (CL)',
441         'es_CO': u'Spanish (CO) / Español (CO)',
442         'es_CR': u'Spanish (CR) / Español (CR)',
443         'es_DO': u'Spanish (DO) / Español (DO)',
444         'es_EC': u'Spanish (EC) / Español (EC)',
445         'es_ES': u'Spanish / Español',
446         'es_GT': u'Spanish (GT) / Español (GT)',
447         'es_HN': u'Spanish (HN) / Español (HN)',
448         'es_MX': u'Spanish (MX) / Español (MX)',
449         'es_NI': u'Spanish (NI) / Español (NI)',
450         'es_PA': u'Spanish (PA) / Español (PA)',
451         'es_PE': u'Spanish (PE) / Español (PE)',
452         'es_PR': u'Spanish (PR) / Español (PR)',
453         'es_PY': u'Spanish (PY) / Español (PY)',
454         'es_SV': u'Spanish (SV) / Español (SV)',
455         'es_UY': u'Spanish (UY) / Español (UY)',
456         'es_VE': u'Spanish (VE) / Español (VE)',
457         'et_EE': u'Estonian / Eesti keel',
458         'fa_IR': u'Persian / فارس',
459         'fi_FI': u'Finnish / Suomi',
460         'fr_BE': u'French (BE) / Français (BE)',
461         'fr_CH': u'French (CH) / Français (CH)',
462         'fr_FR': u'French / Français',
463         'gl_ES': u'Galician / Galego',
464         'gu_IN': u'Gujarati / ગુજરાતી',
465         'he_IL': u'Hebrew / עִבְרִי',
466         'hi_IN': u'Hindi / हिंदी',
467         'hr_HR': u'Croatian / hrvatski jezik',
468         'hu_HU': u'Hungarian / Magyar',
469         'id_ID': u'Indonesian / Bahasa Indonesia',
470         'it_IT': u'Italian / Italiano',
471         'iu_CA': u'Inuktitut / ᐃᓄᒃᑎᑐᑦ',
472         'ja_JP': u'Japanese / 日本語',
473         'ko_KP': u'Korean (KP) / 한국어 (KP)',
474         'ko_KR': u'Korean (KR) / 한국어 (KR)',
475         'lo_LA': u'Lao / ພາສາລາວ',
476         'lt_LT': u'Lithuanian / Lietuvių kalba',
477         'lv_LV': u'Latvian / latviešu valoda',
478         'ml_IN': u'Malayalam / മലയാളം',
479         'mn_MN': u'Mongolian / монгол',
480         'nb_NO': u'Norwegian Bokmål / Norsk bokmål',
481         'nl_NL': u'Dutch / Nederlands',
482         'nl_BE': u'Flemish (BE) / Vlaams (BE)',
483         'oc_FR': u'Occitan (FR, post 1500) / Occitan',
484         'pl_PL': u'Polish / Język polski',
485         'pt_BR': u'Portuguese (BR) / Português (BR)',
486         'pt_PT': u'Portuguese / Português',
487         'ro_RO': u'Romanian / română',
488         'ru_RU': u'Russian / русский язык',
489         'si_LK': u'Sinhalese / සිංහල',
490         'sl_SI': u'Slovenian / slovenščina',
491         'sk_SK': u'Slovak / Slovenský jazyk',
492         'sq_AL': u'Albanian / Shqip',
493         'sr_RS': u'Serbian (Cyrillic) / српски',
494         'sr@latin': u'Serbian (Latin) / srpski',
495         'sv_SE': u'Swedish / svenska',
496         'te_IN': u'Telugu / తెలుగు',
497         'tr_TR': u'Turkish / Türkçe',
498         'vi_VN': u'Vietnamese / Tiếng Việt',
499         'uk_UA': u'Ukrainian / українська',
500         'ur_PK': u'Urdu / اردو',
501         'zh_CN': u'Chinese (CN) / 简体中文',
502         'zh_HK': u'Chinese (HK)',
503         'zh_TW': u'Chinese (TW) / 正體字',
504         'th_TH': u'Thai / ภาษาไทย',
505         'tlh_TLH': u'Klingon',
506     }
507
508 def scan_languages():
509     """ Returns all languages supported by OpenERP for translation
510
511     :returns: a list of (lang_code, lang_name) pairs
512     :rtype: [(str, unicode)]
513     """
514     return sorted(ALL_LANGUAGES.iteritems(), key=lambda k: k[1])
515
516 def get_user_companies(cr, user):
517     def _get_company_children(cr, ids):
518         if not ids:
519             return []
520         cr.execute('SELECT id FROM res_company WHERE parent_id IN %s', (tuple(ids),))
521         res = [x[0] for x in cr.fetchall()]
522         res.extend(_get_company_children(cr, res))
523         return res
524     cr.execute('SELECT company_id FROM res_users WHERE id=%s', (user,))
525     user_comp = cr.fetchone()[0]
526     if not user_comp:
527         return []
528     return [user_comp] + _get_company_children(cr, [user_comp])
529
530 def mod10r(number):
531     """
532     Input number : account or invoice number
533     Output return: the same number completed with the recursive mod10
534     key
535     """
536     codec=[0,9,4,6,8,2,7,1,3,5]
537     report = 0
538     result=""
539     for digit in number:
540         result += digit
541         if digit.isdigit():
542             report = codec[ (int(digit) + report) % 10 ]
543     return result + str((10 - report) % 10)
544
545
546 def human_size(sz):
547     """
548     Return the size in a human readable format
549     """
550     if not sz:
551         return False
552     units = ('bytes', 'Kb', 'Mb', 'Gb')
553     if isinstance(sz,basestring):
554         sz=len(sz)
555     s, i = float(sz), 0
556     while s >= 1024 and i < len(units)-1:
557         s /= 1024
558         i += 1
559     return "%0.2f %s" % (s, units[i])
560
561 def logged(f):
562     @wraps(f)
563     def wrapper(*args, **kwargs):
564         from pprint import pformat
565
566         vector = ['Call -> function: %r' % f]
567         for i, arg in enumerate(args):
568             vector.append('  arg %02d: %s' % (i, pformat(arg)))
569         for key, value in kwargs.items():
570             vector.append('  kwarg %10s: %s' % (key, pformat(value)))
571
572         timeb4 = time.time()
573         res = f(*args, **kwargs)
574
575         vector.append('  result: %s' % pformat(res))
576         vector.append('  time delta: %s' % (time.time() - timeb4))
577         _logger.debug('\n'.join(vector))
578         return res
579
580     return wrapper
581
582 class profile(object):
583     def __init__(self, fname=None):
584         self.fname = fname
585
586     def __call__(self, f):
587         @wraps(f)
588         def wrapper(*args, **kwargs):
589             profile = cProfile.Profile()
590             result = profile.runcall(f, *args, **kwargs)
591             profile.dump_stats(self.fname or ("%s.cprof" % (f.func_name,)))
592             return result
593
594         return wrapper
595
596 __icons_list = ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
597 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
598 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
599 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
600 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
601 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
602 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
603 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
604 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
605 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
606 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
607 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
608 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
609 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
610 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
611 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
612 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
613 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
614 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
615 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
616 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
617 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
618 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
619 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
620 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
621 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
622 'terp-check','terp-go-month','terp-go-year','terp-go-today','terp-document-new','terp-camera_test',
623 'terp-emblem-important','terp-gtk-media-pause','terp-gtk-stop','terp-gnome-cpu-frequency-applet+',
624 'terp-dialog-close','terp-gtk-jump-to-rtl','terp-gtk-jump-to-ltr','terp-accessories-archiver',
625 'terp-stock_align_left_24','terp-stock_effects-object-colorize','terp-go-home','terp-gtk-go-back-rtl',
626 'terp-gtk-go-back-ltr','terp-personal','terp-personal-','terp-personal+','terp-accessories-archiver-minus',
627 'terp-accessories-archiver+','terp-stock_symbol-selection','terp-call-start','terp-dolar',
628 'terp-face-plain','terp-folder-blue','terp-folder-green','terp-folder-orange','terp-folder-yellow',
629 'terp-gdu-smart-failing','terp-go-week','terp-gtk-select-all','terp-locked','terp-mail-forward',
630 'terp-mail-message-new','terp-mail-replied','terp-rating-rated','terp-stage','terp-stock_format-scientific',
631 'terp-dolar_ok!','terp-idea','terp-stock_format-default','terp-mail-','terp-mail_delete'
632 ]
633
634 def icons(*a, **kw):
635     global __icons_list
636     return [(x, x) for x in __icons_list ]
637
638 def detect_ip_addr():
639     """Try a very crude method to figure out a valid external
640        IP or hostname for the current machine. Don't rely on this
641        for binding to an interface, but it could be used as basis
642        for constructing a remote URL to the server.
643     """
644     def _detect_ip_addr():
645         from array import array
646         from struct import pack, unpack
647
648         try:
649             import fcntl
650         except ImportError:
651             fcntl = None
652
653         ip_addr = None
654
655         if not fcntl: # not UNIX:
656             host = socket.gethostname()
657             ip_addr = socket.gethostbyname(host)
658         else: # UNIX:
659             # get all interfaces:
660             nbytes = 128 * 32
661             s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
662             names = array('B', '\0' * nbytes)
663             #print 'names: ', names
664             outbytes = unpack('iL', fcntl.ioctl( s.fileno(), 0x8912, pack('iL', nbytes, names.buffer_info()[0])))[0]
665             namestr = names.tostring()
666
667             # try 64 bit kernel:
668             for i in range(0, outbytes, 40):
669                 name = namestr[i:i+16].split('\0', 1)[0]
670                 if name != 'lo':
671                     ip_addr = socket.inet_ntoa(namestr[i+20:i+24])
672                     break
673
674             # try 32 bit kernel:
675             if ip_addr is None:
676                 ifaces = filter(None, [namestr[i:i+32].split('\0', 1)[0] for i in range(0, outbytes, 32)])
677
678                 for ifname in [iface for iface in ifaces if iface != 'lo']:
679                     ip_addr = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, pack('256s', ifname[:15]))[20:24])
680                     break
681
682         return ip_addr or 'localhost'
683
684     try:
685         ip_addr = _detect_ip_addr()
686     except Exception:
687         ip_addr = 'localhost'
688     return ip_addr
689
690 # RATIONALE BEHIND TIMESTAMP CALCULATIONS AND TIMEZONE MANAGEMENT:
691 #  The server side never does any timestamp calculation, always
692 #  sends them in a naive (timezone agnostic) format supposed to be
693 #  expressed within the server timezone, and expects the clients to
694 #  provide timestamps in the server timezone as well.
695 #  It stores all timestamps in the database in naive format as well,
696 #  which also expresses the time in the server timezone.
697 #  For this reason the server makes its timezone name available via the
698 #  common/timezone_get() rpc method, which clients need to read
699 #  to know the appropriate time offset to use when reading/writing
700 #  times.
701 def get_win32_timezone():
702     """Attempt to return the "standard name" of the current timezone on a win32 system.
703        @return the standard name of the current win32 timezone, or False if it cannot be found.
704     """
705     res = False
706     if sys.platform == "win32":
707         try:
708             import _winreg
709             hklm = _winreg.ConnectRegistry(None,_winreg.HKEY_LOCAL_MACHINE)
710             current_tz_key = _winreg.OpenKey(hklm, r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", 0,_winreg.KEY_ALL_ACCESS)
711             res = str(_winreg.QueryValueEx(current_tz_key,"StandardName")[0])  # [0] is value, [1] is type code
712             _winreg.CloseKey(current_tz_key)
713             _winreg.CloseKey(hklm)
714         except Exception:
715             pass
716     return res
717
718 def detect_server_timezone():
719     """Attempt to detect the timezone to use on the server side.
720        Defaults to UTC if no working timezone can be found.
721        @return the timezone identifier as expected by pytz.timezone.
722     """
723     try:
724         import pytz
725     except Exception:
726         _logger.warning("Python pytz module is not available. "
727             "Timezone will be set to UTC by default.")
728         return 'UTC'
729
730     # Option 1: the configuration option (did not exist before, so no backwards compatibility issue)
731     # 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
732     # Option 3: the environment variable TZ
733     sources = [ (config['timezone'], 'OpenERP configuration'),
734                 (time.tzname[0], 'time.tzname'),
735                 (os.environ.get('TZ',False),'TZ environment variable'), ]
736     # Option 4: OS-specific: /etc/timezone on Unix
737     if os.path.exists("/etc/timezone"):
738         tz_value = False
739         try:
740             f = open("/etc/timezone")
741             tz_value = f.read(128).strip()
742         except Exception:
743             pass
744         finally:
745             f.close()
746         sources.append((tz_value,"/etc/timezone file"))
747     # Option 5: timezone info from registry on Win32
748     if sys.platform == "win32":
749         # Timezone info is stored in windows registry.
750         # However this is not likely to work very well as the standard name
751         # of timezones in windows is rarely something that is known to pytz.
752         # But that's ok, it is always possible to use a config option to set
753         # it explicitly.
754         sources.append((get_win32_timezone(),"Windows Registry"))
755
756     for (value,source) in sources:
757         if value:
758             try:
759                 tz = pytz.timezone(value)
760                 _logger.info("Using timezone %s obtained from %s.", tz.zone, source)
761                 return value
762             except pytz.UnknownTimeZoneError:
763                 _logger.warning("The timezone specified in %s (%s) is invalid, ignoring it.", source, value)
764
765     _logger.warning("No valid timezone could be detected, using default UTC "
766         "timezone. You can specify it explicitly with option 'timezone' in "
767         "the server configuration.")
768     return 'UTC'
769
770 def get_server_timezone():
771     return "UTC"
772
773
774 DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"
775 DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S"
776 DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % (
777     DEFAULT_SERVER_DATE_FORMAT,
778     DEFAULT_SERVER_TIME_FORMAT)
779
780 # Python's strftime supports only the format directives
781 # that are available on the platform's libc, so in order to
782 # be cross-platform we map to the directives required by
783 # the C standard (1989 version), always available on platforms
784 # with a C standard implementation.
785 DATETIME_FORMATS_MAP = {
786         '%C': '', # century
787         '%D': '%m/%d/%Y', # modified %y->%Y
788         '%e': '%d',
789         '%E': '', # special modifier
790         '%F': '%Y-%m-%d',
791         '%g': '%Y', # modified %y->%Y
792         '%G': '%Y',
793         '%h': '%b',
794         '%k': '%H',
795         '%l': '%I',
796         '%n': '\n',
797         '%O': '', # special modifier
798         '%P': '%p',
799         '%R': '%H:%M',
800         '%r': '%I:%M:%S %p',
801         '%s': '', #num of seconds since epoch
802         '%T': '%H:%M:%S',
803         '%t': ' ', # tab
804         '%u': ' %w',
805         '%V': '%W',
806         '%y': '%Y', # Even if %y works, it's ambiguous, so we should use %Y
807         '%+': '%Y-%m-%d %H:%M:%S',
808
809         # %Z is a special case that causes 2 problems at least:
810         #  - the timezone names we use (in res_user.context_tz) come
811         #    from pytz, but not all these names are recognized by
812         #    strptime(), so we cannot convert in both directions
813         #    when such a timezone is selected and %Z is in the format
814         #  - %Z is replaced by an empty string in strftime() when
815         #    there is not tzinfo in a datetime value (e.g when the user
816         #    did not pick a context_tz). The resulting string does not
817         #    parse back if the format requires %Z.
818         # As a consequence, we strip it completely from format strings.
819         # The user can always have a look at the context_tz in
820         # preferences to check the timezone.
821         '%z': '',
822         '%Z': '',
823 }
824
825 POSIX_TO_LDML = {
826     'a': 'E',
827     'A': 'EEEE',
828     'b': 'MMM',
829     'B': 'MMMM',
830     #'c': '',
831     'd': 'dd',
832     'H': 'HH',
833     'I': 'hh',
834     'j': 'DDD',
835     'm': 'MM',
836     'M': 'mm',
837     'p': 'a',
838     'S': 'ss',
839     'U': 'w',
840     'w': 'e',
841     'W': 'w',
842     'y': 'yy',
843     'Y': 'yyyy',
844     # see comments above, and babel's format_datetime assumes an UTC timezone
845     # for naive datetime objects
846     #'z': 'Z',
847     #'Z': 'z',
848 }
849
850 def posix_to_ldml(fmt, locale):
851     """ Converts a posix/strftime pattern into an LDML date format pattern.
852
853     :param fmt: non-extended C89/C90 strftime pattern
854     :param locale: babel locale used for locale-specific conversions (e.g. %x and %X)
855     :return: unicode
856     """
857     buf = []
858     pc = False
859     quoted = []
860
861     for c in fmt:
862         # LDML date format patterns uses letters, so letters must be quoted
863         if not pc and c.isalpha():
864             quoted.append(c if c != "'" else "''")
865             continue
866         if quoted:
867             buf.append("'")
868             buf.append(''.join(quoted))
869             buf.append("'")
870             quoted = []
871
872         if pc:
873             if c == '%': # escaped percent
874                 buf.append('%')
875             elif c == 'x': # date format, short seems to match
876                 buf.append(locale.date_formats['short'].pattern)
877             elif c == 'X': # time format, seems to include seconds. short does not
878                 buf.append(locale.time_formats['medium'].pattern)
879             else: # look up format char in static mapping
880                 buf.append(POSIX_TO_LDML[c])
881             pc = False
882         elif c == '%':
883             pc = True
884         else:
885             buf.append(c)
886
887     # flush anything remaining in quoted buffer
888     if quoted:
889         buf.append("'")
890         buf.append(''.join(quoted))
891         buf.append("'")
892
893     return ''.join(buf)
894
895 def server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_name,
896         tz_offset=True, ignore_unparsable_time=True):
897     """
898     Convert a source timestamp string into a destination timestamp string, attempting to apply the
899     correct offset if both the server and local timezone are recognized, or no
900     offset at all if they aren't or if tz_offset is false (i.e. assuming they are both in the same TZ).
901
902     WARNING: This method is here to allow formatting dates correctly for inclusion in strings where
903              the client would not be able to format/offset it correctly. DO NOT use it for returning
904              date fields directly, these are supposed to be handled by the client!!
905
906     @param src_tstamp_str: the str value containing the timestamp in the server timezone.
907     @param src_format: the format to use when parsing the server timestamp.
908     @param dst_format: the format to use when formatting the resulting timestamp for the local/client timezone.
909     @param dst_tz_name: name of the destination timezone (such as the 'tz' value of the client context)
910     @param ignore_unparsable_time: if True, return False if src_tstamp_str cannot be parsed
911                                    using src_format or formatted using dst_format.
912
913     @return local/client formatted timestamp, expressed in the local/client timezone if possible
914             and if tz_offset is true, or src_tstamp_str if timezone offset could not be determined.
915     """
916     if not src_tstamp_str:
917         return False
918
919     res = src_tstamp_str
920     if src_format and dst_format:
921         # find out server timezone
922         server_tz = get_server_timezone()
923         try:
924             # dt_value needs to be a datetime.datetime object (so no time.struct_time or mx.DateTime.DateTime here!)
925             dt_value = datetime.strptime(src_tstamp_str, src_format)
926             if tz_offset and dst_tz_name:
927                 try:
928                     import pytz
929                     src_tz = pytz.timezone(server_tz)
930                     dst_tz = pytz.timezone(dst_tz_name)
931                     src_dt = src_tz.localize(dt_value, is_dst=True)
932                     dt_value = src_dt.astimezone(dst_tz)
933                 except Exception:
934                     pass
935             res = dt_value.strftime(dst_format)
936         except Exception:
937             # Normal ways to end up here are if strptime or strftime failed
938             if not ignore_unparsable_time:
939                 return False
940     return res
941
942
943 def split_every(n, iterable, piece_maker=tuple):
944     """Splits an iterable into length-n pieces. The last piece will be shorter
945        if ``n`` does not evenly divide the iterable length.
946        @param ``piece_maker``: function to build the pieces
947        from the slices (tuple,list,...)
948     """
949     iterator = iter(iterable)
950     piece = piece_maker(islice(iterator, n))
951     while piece:
952         yield piece
953         piece = piece_maker(islice(iterator, n))
954
955 if __name__ == '__main__':
956     import doctest
957     doctest.testmod()
958
959 class upload_data_thread(threading.Thread):
960     def __init__(self, email, data, type):
961         self.args = [('email',email),('type',type),('data',data)]
962         super(upload_data_thread,self).__init__()
963     def run(self):
964         try:
965             import urllib
966             args = urllib.urlencode(self.args)
967             fp = urllib.urlopen('http://www.openerp.com/scripts/survey.php', args)
968             fp.read()
969             fp.close()
970         except Exception:
971             pass
972
973 def upload_data(email, data, type='SURVEY'):
974     a = upload_data_thread(email, data, type)
975     a.start()
976     return True
977
978 def get_and_group_by_field(cr, uid, obj, ids, field, context=None):
979     """ Read the values of ``field´´ for the given ``ids´´ and group ids by value.
980
981        :param string field: name of the field we want to read and group by
982        :return: mapping of field values to the list of ids that have it
983        :rtype: dict
984     """
985     res = {}
986     for record in obj.read(cr, uid, ids, [field], context=context):
987         key = record[field]
988         res.setdefault(key[0] if isinstance(key, tuple) else key, []).append(record['id'])
989     return res
990
991 def get_and_group_by_company(cr, uid, obj, ids, context=None):
992     return get_and_group_by_field(cr, uid, obj, ids, field='company_id', context=context)
993
994 # port of python 2.6's attrgetter with support for dotted notation
995 def resolve_attr(obj, attr):
996     for name in attr.split("."):
997         obj = getattr(obj, name)
998     return obj
999
1000 def attrgetter(*items):
1001     if len(items) == 1:
1002         attr = items[0]
1003         def g(obj):
1004             return resolve_attr(obj, attr)
1005     else:
1006         def g(obj):
1007             return tuple(resolve_attr(obj, attr) for attr in items)
1008     return g
1009
1010 class unquote(str):
1011     """A subclass of str that implements repr() without enclosing quotation marks
1012        or escaping, keeping the original string untouched. The name come from Lisp's unquote.
1013        One of the uses for this is to preserve or insert bare variable names within dicts during eval()
1014        of a dict's repr(). Use with care.
1015
1016        Some examples (notice that there are never quotes surrounding
1017        the ``active_id`` name:
1018
1019        >>> unquote('active_id')
1020        active_id
1021        >>> d = {'test': unquote('active_id')}
1022        >>> d
1023        {'test': active_id}
1024        >>> print d
1025        {'test': active_id}
1026     """
1027     def __repr__(self):
1028         return self
1029
1030 class UnquoteEvalContext(defaultdict):
1031     """Defaultdict-based evaluation context that returns 
1032        an ``unquote`` string for any missing name used during
1033        the evaluation.
1034        Mostly useful for evaluating OpenERP domains/contexts that
1035        may refer to names that are unknown at the time of eval,
1036        so that when the context/domain is converted back to a string,
1037        the original names are preserved.
1038
1039        **Warning**: using an ``UnquoteEvalContext`` as context for ``eval()`` or
1040        ``safe_eval()`` will shadow the builtins, which may cause other
1041        failures, depending on what is evaluated.
1042
1043        Example (notice that ``section_id`` is preserved in the final
1044        result) :
1045
1046        >>> context_str = "{'default_user_id': uid, 'default_section_id': section_id}"
1047        >>> eval(context_str, UnquoteEvalContext(uid=1))
1048        {'default_user_id': 1, 'default_section_id': section_id}
1049
1050        """
1051     def __init__(self, *args, **kwargs):
1052         super(UnquoteEvalContext, self).__init__(None, *args, **kwargs)
1053
1054     def __missing__(self, key):
1055         return unquote(key)
1056
1057
1058 class mute_logger(object):
1059     """Temporary suppress the logging.
1060     Can be used as context manager or decorator.
1061
1062         @mute_logger('openerp.plic.ploc')
1063         def do_stuff():
1064             blahblah()
1065
1066         with mute_logger('openerp.foo.bar'):
1067             do_suff()
1068
1069     """
1070     def __init__(self, *loggers):
1071         self.loggers = loggers
1072
1073     def filter(self, record):
1074         return 0
1075
1076     def __enter__(self):
1077         for logger in self.loggers:
1078             assert isinstance(logger, basestring),\
1079                 "A logger name must be a string, got %s" % type(logger)
1080             logging.getLogger(logger).addFilter(self)
1081
1082     def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
1083         for logger in self.loggers:
1084             logging.getLogger(logger).removeFilter(self)
1085
1086     def __call__(self, func):
1087         @wraps(func)
1088         def deco(*args, **kwargs):
1089             with self:
1090                 return func(*args, **kwargs)
1091         return deco
1092
1093 _ph = object()
1094 class CountingStream(object):
1095     """ Stream wrapper counting the number of element it has yielded. Similar
1096     role to ``enumerate``, but for use when the iteration process of the stream
1097     isn't fully under caller control (the stream can be iterated from multiple
1098     points including within a library)
1099
1100     ``start`` allows overriding the starting index (the index before the first
1101     item is returned).
1102
1103     On each iteration (call to :meth:`~.next`), increases its :attr:`~.index`
1104     by one.
1105
1106     .. attribute:: index
1107
1108         ``int``, index of the last yielded element in the stream. If the stream
1109         has ended, will give an index 1-past the stream
1110     """
1111     def __init__(self, stream, start=-1):
1112         self.stream = iter(stream)
1113         self.index = start
1114         self.stopped = False
1115     def __iter__(self):
1116         return self
1117     def next(self):
1118         if self.stopped: raise StopIteration()
1119         self.index += 1
1120         val = next(self.stream, _ph)
1121         if val is _ph:
1122             self.stopped = True
1123             raise StopIteration()
1124         return val
1125
1126 def stripped_sys_argv(*strip_args):
1127     """Return sys.argv with some arguments stripped, suitable for reexecution or subprocesses"""
1128     strip_args = sorted(set(strip_args) | set(['-s', '--save', '-d', '--database', '-u', '--update', '-i', '--init']))
1129     assert all(config.parser.has_option(s) for s in strip_args)
1130     takes_value = dict((s, config.parser.get_option(s).takes_value()) for s in strip_args)
1131
1132     longs, shorts = list(tuple(y) for _, y in groupby(strip_args, lambda x: x.startswith('--')))
1133     longs_eq = tuple(l + '=' for l in longs if takes_value[l])
1134
1135     args = sys.argv[:]
1136
1137     def strip(args, i):
1138         return args[i].startswith(shorts) \
1139             or args[i].startswith(longs_eq) or (args[i] in longs) \
1140             or (i >= 1 and (args[i - 1] in strip_args) and takes_value[args[i - 1]])
1141
1142     return [x for i, x in enumerate(args) if not strip(args, i)]
1143
1144
1145 class ConstantMapping(Mapping):
1146     """
1147     An immutable mapping returning the provided value for every single key.
1148
1149     Useful for default value to methods
1150     """
1151     __slots__ = ['_value']
1152     def __init__(self, val):
1153         self._value = val
1154
1155     def __len__(self):
1156         """
1157         defaultdict updates its length for each individually requested key, is
1158         that really useful?
1159         """
1160         return 0
1161
1162     def __iter__(self):
1163         """
1164         same as len, defaultdict udpates its iterable keyset with each key
1165         requested, is there a point for this?
1166         """
1167         return iter([])
1168
1169     def __getitem__(self, item):
1170         return self._value
1171
1172
1173 def dumpstacks(sig, frame):
1174     """ Signal handler: dump a stack trace for each existing thread."""
1175     code = []
1176
1177     def extract_stack(stack):
1178         for filename, lineno, name, line in traceback.extract_stack(stack):
1179             yield 'File: "%s", line %d, in %s' % (filename, lineno, name)
1180             if line:
1181                 yield "  %s" % (line.strip(),)
1182
1183     # code from http://stackoverflow.com/questions/132058/getting-stack-trace-from-a-running-python-application#answer-2569696
1184     # modified for python 2.5 compatibility
1185     threads_info = dict([(th.ident, {'name': th.name, 'uid': getattr(th, 'uid', 'n/a')})
1186                         for th in threading.enumerate()])
1187     for threadId, stack in sys._current_frames().items():
1188         thread_info = threads_info.get(threadId)
1189         code.append("\n# Thread: %s (id:%s) (uid:%s)" %
1190                     (thread_info and thread_info['name'] or 'n/a',
1191                      threadId,
1192                      thread_info and thread_info['uid'] or 'n/a'))
1193         for line in extract_stack(stack):
1194             code.append(line)
1195
1196     if openerp.evented:
1197         # code from http://stackoverflow.com/questions/12510648/in-gevent-how-can-i-dump-stack-traces-of-all-running-greenlets
1198         import gc
1199         from greenlet import greenlet
1200         for ob in gc.get_objects():
1201             if not isinstance(ob, greenlet) or not ob:
1202                 continue
1203             code.append("\n# Greenlet: %r" % (ob,))
1204             for line in extract_stack(ob.gr_frame):
1205                 code.append(line)
1206
1207     _logger.info("\n".join(code))
1208
1209
1210
1211 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: