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