[IMP] orm: Use a list of tuples instead of three collections.
[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
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 def server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_name,
826         tz_offset=True, ignore_unparsable_time=True):
827     """
828     Convert a source timestamp string into a destination timestamp string, attempting to apply the
829     correct offset if both the server and local timezone are recognized, or no
830     offset at all if they aren't or if tz_offset is false (i.e. assuming they are both in the same TZ).
831
832     WARNING: This method is here to allow formatting dates correctly for inclusion in strings where
833              the client would not be able to format/offset it correctly. DO NOT use it for returning
834              date fields directly, these are supposed to be handled by the client!!
835
836     @param src_tstamp_str: the str value containing the timestamp in the server timezone.
837     @param src_format: the format to use when parsing the server timestamp.
838     @param dst_format: the format to use when formatting the resulting timestamp for the local/client timezone.
839     @param dst_tz_name: name of the destination timezone (such as the 'tz' value of the client context)
840     @param ignore_unparsable_time: if True, return False if src_tstamp_str cannot be parsed
841                                    using src_format or formatted using dst_format.
842
843     @return local/client formatted timestamp, expressed in the local/client timezone if possible
844             and if tz_offset is true, or src_tstamp_str if timezone offset could not be determined.
845     """
846     if not src_tstamp_str:
847         return False
848
849     res = src_tstamp_str
850     if src_format and dst_format:
851         # find out server timezone
852         server_tz = get_server_timezone()
853         try:
854             # dt_value needs to be a datetime.datetime object (so no time.struct_time or mx.DateTime.DateTime here!)
855             dt_value = datetime.strptime(src_tstamp_str, src_format)
856             if tz_offset and dst_tz_name:
857                 try:
858                     import pytz
859                     src_tz = pytz.timezone(server_tz)
860                     dst_tz = pytz.timezone(dst_tz_name)
861                     src_dt = src_tz.localize(dt_value, is_dst=True)
862                     dt_value = src_dt.astimezone(dst_tz)
863                 except Exception:
864                     pass
865             res = dt_value.strftime(dst_format)
866         except Exception:
867             # Normal ways to end up here are if strptime or strftime failed
868             if not ignore_unparsable_time:
869                 return False
870     return res
871
872
873 def split_every(n, iterable, piece_maker=tuple):
874     """Splits an iterable into length-n pieces. The last piece will be shorter
875        if ``n`` does not evenly divide the iterable length.
876        @param ``piece_maker``: function to build the pieces
877        from the slices (tuple,list,...)
878     """
879     iterator = iter(iterable)
880     piece = piece_maker(islice(iterator, n))
881     while piece:
882         yield piece
883         piece = piece_maker(islice(iterator, n))
884
885 if __name__ == '__main__':
886     import doctest
887     doctest.testmod()
888
889 class upload_data_thread(threading.Thread):
890     def __init__(self, email, data, type):
891         self.args = [('email',email),('type',type),('data',data)]
892         super(upload_data_thread,self).__init__()
893     def run(self):
894         try:
895             import urllib
896             args = urllib.urlencode(self.args)
897             fp = urllib.urlopen('http://www.openerp.com/scripts/survey.php', args)
898             fp.read()
899             fp.close()
900         except Exception:
901             pass
902
903 def upload_data(email, data, type='SURVEY'):
904     a = upload_data_thread(email, data, type)
905     a.start()
906     return True
907
908 def get_and_group_by_field(cr, uid, obj, ids, field, context=None):
909     """ Read the values of ``field´´ for the given ``ids´´ and group ids by value.
910
911        :param string field: name of the field we want to read and group by
912        :return: mapping of field values to the list of ids that have it
913        :rtype: dict
914     """
915     res = {}
916     for record in obj.read(cr, uid, ids, [field], context=context):
917         key = record[field]
918         res.setdefault(key[0] if isinstance(key, tuple) else key, []).append(record['id'])
919     return res
920
921 def get_and_group_by_company(cr, uid, obj, ids, context=None):
922     return get_and_group_by_field(cr, uid, obj, ids, field='company_id', context=context)
923
924 # port of python 2.6's attrgetter with support for dotted notation
925 def resolve_attr(obj, attr):
926     for name in attr.split("."):
927         obj = getattr(obj, name)
928     return obj
929
930 def attrgetter(*items):
931     if len(items) == 1:
932         attr = items[0]
933         def g(obj):
934             return resolve_attr(obj, attr)
935     else:
936         def g(obj):
937             return tuple(resolve_attr(obj, attr) for attr in items)
938     return g
939
940 class unquote(str):
941     """A subclass of str that implements repr() without enclosing quotation marks
942        or escaping, keeping the original string untouched. The name come from Lisp's unquote.
943        One of the uses for this is to preserve or insert bare variable names within dicts during eval()
944        of a dict's repr(). Use with care.
945
946        Some examples (notice that there are never quotes surrounding
947        the ``active_id`` name:
948
949        >>> unquote('active_id')
950        active_id
951        >>> d = {'test': unquote('active_id')}
952        >>> d
953        {'test': active_id}
954        >>> print d
955        {'test': active_id}
956     """
957     def __repr__(self):
958         return self
959
960 class UnquoteEvalContext(defaultdict):
961     """Defaultdict-based evaluation context that returns 
962        an ``unquote`` string for any missing name used during
963        the evaluation.
964        Mostly useful for evaluating OpenERP domains/contexts that
965        may refer to names that are unknown at the time of eval,
966        so that when the context/domain is converted back to a string,
967        the original names are preserved.
968
969        **Warning**: using an ``UnquoteEvalContext`` as context for ``eval()`` or
970        ``safe_eval()`` will shadow the builtins, which may cause other
971        failures, depending on what is evaluated.
972
973        Example (notice that ``section_id`` is preserved in the final
974        result) :
975
976        >>> context_str = "{'default_user_id': uid, 'default_section_id': section_id}"
977        >>> eval(context_str, UnquoteEvalContext(uid=1))
978        {'default_user_id': 1, 'default_section_id': section_id}
979
980        """
981     def __init__(self, *args, **kwargs):
982         super(UnquoteEvalContext, self).__init__(None, *args, **kwargs)
983
984     def __missing__(self, key):
985         return unquote(key)
986
987
988 class mute_logger(object):
989     """Temporary suppress the logging.
990     Can be used as context manager or decorator.
991
992         @mute_logger('openerp.plic.ploc')
993         def do_stuff():
994             blahblah()
995
996         with mute_logger('openerp.foo.bar'):
997             do_suff()
998
999     """
1000     def __init__(self, *loggers):
1001         self.loggers = loggers
1002
1003     def filter(self, record):
1004         return 0
1005
1006     def __enter__(self):
1007         for logger in self.loggers:
1008             logging.getLogger(logger).addFilter(self)
1009
1010     def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
1011         for logger in self.loggers:
1012             logging.getLogger(logger).removeFilter(self)
1013
1014     def __call__(self, func):
1015         @wraps(func)
1016         def deco(*args, **kwargs):
1017             with self:
1018                 return func(*args, **kwargs)
1019         return deco
1020
1021 _ph = object()
1022 class CountingStream(object):
1023     """ Stream wrapper counting the number of element it has yielded. Similar
1024     role to ``enumerate``, but for use when the iteration process of the stream
1025     isn't fully under caller control (the stream can be iterated from multiple
1026     points including within a library)
1027
1028     ``start`` allows overriding the starting index (the index before the first
1029     item is returned).
1030
1031     On each iteration (call to :meth:`~.next`), increases its :attr:`~.index`
1032     by one.
1033
1034     .. attribute:: index
1035
1036         ``int``, index of the last yielded element in the stream. If the stream
1037         has ended, will give an index 1-past the stream
1038     """
1039     def __init__(self, stream, start=-1):
1040         self.stream = iter(stream)
1041         self.index = start
1042         self.stopped = False
1043     def __iter__(self):
1044         return self
1045     def next(self):
1046         if self.stopped: raise StopIteration()
1047         self.index += 1
1048         val = next(self.stream, _ph)
1049         if val is _ph:
1050             self.stopped = True
1051             raise StopIteration()
1052         return val
1053
1054 def stripped_sys_argv(*strip_args):
1055     """Return sys.argv with some arguments stripped, suitable for reexecution or subprocesses"""
1056     strip_args = sorted(set(strip_args) | set(['-s', '--save', '-d', '--database', '-u', '--update', '-i', '--init']))
1057     assert all(config.parser.has_option(s) for s in strip_args)
1058     takes_value = dict((s, config.parser.get_option(s).takes_value()) for s in strip_args)
1059
1060     longs, shorts = list(tuple(y) for _, y in groupby(strip_args, lambda x: x.startswith('--')))
1061     longs_eq = tuple(l + '=' for l in longs if takes_value[l])
1062
1063     args = sys.argv[:]
1064
1065     def strip(args, i):
1066         return args[i].startswith(shorts) \
1067             or args[i].startswith(longs_eq) or (args[i] in longs) \
1068             or (i >= 1 and (args[i - 1] in strip_args) and takes_value[args[i - 1]])
1069
1070     return [x for i, x in enumerate(args) if not strip(args, i)]
1071
1072
1073 def dumpstacks(sig, frame):
1074     """ Signal handler: dump a stack trace for each existing thread."""
1075     code = []
1076
1077     def extract_stack(stack):
1078         for filename, lineno, name, line in traceback.extract_stack(stack):
1079             yield 'File: "%s", line %d, in %s' % (filename, lineno, name)
1080             if line:
1081                 yield "  %s" % (line.strip(),)
1082
1083     # code from http://stackoverflow.com/questions/132058/getting-stack-trace-from-a-running-python-application#answer-2569696
1084     # modified for python 2.5 compatibility
1085     threads_info = dict([(th.ident, {'name': th.name, 'uid': getattr(th, 'uid', 'n/a')})
1086                         for th in threading.enumerate()])
1087     for threadId, stack in sys._current_frames().items():
1088         thread_info = threads_info.get(threadId)
1089         code.append("\n# Thread: %s (id:%s) (uid:%s)" %
1090                     (thread_info and thread_info['name'] or 'n/a',
1091                      threadId,
1092                      thread_info and thread_info['uid'] or 'n/a'))
1093         for line in extract_stack(stack):
1094             code.append(line)
1095
1096     if openerp.evented:
1097         # code from http://stackoverflow.com/questions/12510648/in-gevent-how-can-i-dump-stack-traces-of-all-running-greenlets
1098         import gc
1099         from greenlet import greenlet
1100         for ob in gc.get_objects():
1101             if not isinstance(ob, greenlet) or not ob:
1102                 continue
1103             code.append("\n# Greenlet: %r" % (ob,))
1104             for line in extract_stack(ob.gr_frame):
1105                 code.append(line)
1106
1107     _logger.info("\n".join(code))
1108
1109
1110 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: