[FIX] Partially revert xmo@openerp.com-20130812074509-yopeb4pxtsads4d9
[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
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         'lt_LT': u'Lithuanian / Lietuvių kalba',
474         'lv_LV': u'Latvian / latviešu valoda',
475         'ml_IN': u'Malayalam / മലയാളം',
476         'mn_MN': u'Mongolian / монгол',
477         'nb_NO': u'Norwegian Bokmål / Norsk bokmål',
478         'nl_NL': u'Dutch / Nederlands',
479         'nl_BE': u'Flemish (BE) / Vlaams (BE)',
480         'oc_FR': u'Occitan (FR, post 1500) / Occitan',
481         'pl_PL': u'Polish / Język polski',
482         'pt_BR': u'Portuguese (BR) / Português (BR)',
483         'pt_PT': u'Portuguese / Português',
484         'ro_RO': u'Romanian / română',
485         'ru_RU': u'Russian / русский язык',
486         'si_LK': u'Sinhalese / සිංහල',
487         'sl_SI': u'Slovenian / slovenščina',
488         'sk_SK': u'Slovak / Slovenský jazyk',
489         'sq_AL': u'Albanian / Shqip',
490         'sr_RS': u'Serbian (Cyrillic) / српски',
491         'sr@latin': u'Serbian (Latin) / srpski',
492         'sv_SE': u'Swedish / svenska',
493         'te_IN': u'Telugu / తెలుగు',
494         'tr_TR': u'Turkish / Türkçe',
495         'vi_VN': u'Vietnamese / Tiếng Việt',
496         'uk_UA': u'Ukrainian / українська',
497         'ur_PK': u'Urdu / اردو',
498         'zh_CN': u'Chinese (CN) / 简体中文',
499         'zh_HK': u'Chinese (HK)',
500         'zh_TW': u'Chinese (TW) / 正體字',
501         'th_TH': u'Thai / ภาษาไทย',
502         'tlh_TLH': u'Klingon',
503     }
504
505 def scan_languages():
506     """ Returns all languages supported by OpenERP for translation
507
508     :returns: a list of (lang_code, lang_name) pairs
509     :rtype: [(str, unicode)]
510     """
511     return sorted(ALL_LANGUAGES.iteritems(), key=lambda k: k[1])
512
513 def get_user_companies(cr, user):
514     def _get_company_children(cr, ids):
515         if not ids:
516             return []
517         cr.execute('SELECT id FROM res_company WHERE parent_id IN %s', (tuple(ids),))
518         res = [x[0] for x in cr.fetchall()]
519         res.extend(_get_company_children(cr, res))
520         return res
521     cr.execute('SELECT company_id FROM res_users WHERE id=%s', (user,))
522     user_comp = cr.fetchone()[0]
523     if not user_comp:
524         return []
525     return [user_comp] + _get_company_children(cr, [user_comp])
526
527 def mod10r(number):
528     """
529     Input number : account or invoice number
530     Output return: the same number completed with the recursive mod10
531     key
532     """
533     codec=[0,9,4,6,8,2,7,1,3,5]
534     report = 0
535     result=""
536     for digit in number:
537         result += digit
538         if digit.isdigit():
539             report = codec[ (int(digit) + report) % 10 ]
540     return result + str((10 - report) % 10)
541
542
543 def human_size(sz):
544     """
545     Return the size in a human readable format
546     """
547     if not sz:
548         return False
549     units = ('bytes', 'Kb', 'Mb', 'Gb')
550     if isinstance(sz,basestring):
551         sz=len(sz)
552     s, i = float(sz), 0
553     while s >= 1024 and i < len(units)-1:
554         s /= 1024
555         i += 1
556     return "%0.2f %s" % (s, units[i])
557
558 def logged(f):
559     @wraps(f)
560     def wrapper(*args, **kwargs):
561         from pprint import pformat
562
563         vector = ['Call -> function: %r' % f]
564         for i, arg in enumerate(args):
565             vector.append('  arg %02d: %s' % (i, pformat(arg)))
566         for key, value in kwargs.items():
567             vector.append('  kwarg %10s: %s' % (key, pformat(value)))
568
569         timeb4 = time.time()
570         res = f(*args, **kwargs)
571
572         vector.append('  result: %s' % pformat(res))
573         vector.append('  time delta: %s' % (time.time() - timeb4))
574         _logger.debug('\n'.join(vector))
575         return res
576
577     return wrapper
578
579 class profile(object):
580     def __init__(self, fname=None):
581         self.fname = fname
582
583     def __call__(self, f):
584         @wraps(f)
585         def wrapper(*args, **kwargs):
586             profile = cProfile.Profile()
587             result = profile.runcall(f, *args, **kwargs)
588             profile.dump_stats(self.fname or ("%s.cprof" % (f.func_name,)))
589             return result
590
591         return wrapper
592
593 __icons_list = ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
594 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
595 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
596 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
597 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
598 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
599 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
600 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
601 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
602 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
603 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
604 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
605 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
606 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
607 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
608 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
609 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
610 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
611 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
612 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
613 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
614 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
615 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
616 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
617 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
618 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
619 'terp-check','terp-go-month','terp-go-year','terp-go-today','terp-document-new','terp-camera_test',
620 'terp-emblem-important','terp-gtk-media-pause','terp-gtk-stop','terp-gnome-cpu-frequency-applet+',
621 'terp-dialog-close','terp-gtk-jump-to-rtl','terp-gtk-jump-to-ltr','terp-accessories-archiver',
622 'terp-stock_align_left_24','terp-stock_effects-object-colorize','terp-go-home','terp-gtk-go-back-rtl',
623 'terp-gtk-go-back-ltr','terp-personal','terp-personal-','terp-personal+','terp-accessories-archiver-minus',
624 'terp-accessories-archiver+','terp-stock_symbol-selection','terp-call-start','terp-dolar',
625 'terp-face-plain','terp-folder-blue','terp-folder-green','terp-folder-orange','terp-folder-yellow',
626 'terp-gdu-smart-failing','terp-go-week','terp-gtk-select-all','terp-locked','terp-mail-forward',
627 'terp-mail-message-new','terp-mail-replied','terp-rating-rated','terp-stage','terp-stock_format-scientific',
628 'terp-dolar_ok!','terp-idea','terp-stock_format-default','terp-mail-','terp-mail_delete'
629 ]
630
631 def icons(*a, **kw):
632     global __icons_list
633     return [(x, x) for x in __icons_list ]
634
635 def detect_ip_addr():
636     """Try a very crude method to figure out a valid external
637        IP or hostname for the current machine. Don't rely on this
638        for binding to an interface, but it could be used as basis
639        for constructing a remote URL to the server.
640     """
641     def _detect_ip_addr():
642         from array import array
643         from struct import pack, unpack
644
645         try:
646             import fcntl
647         except ImportError:
648             fcntl = None
649
650         ip_addr = None
651
652         if not fcntl: # not UNIX:
653             host = socket.gethostname()
654             ip_addr = socket.gethostbyname(host)
655         else: # UNIX:
656             # get all interfaces:
657             nbytes = 128 * 32
658             s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
659             names = array('B', '\0' * nbytes)
660             #print 'names: ', names
661             outbytes = unpack('iL', fcntl.ioctl( s.fileno(), 0x8912, pack('iL', nbytes, names.buffer_info()[0])))[0]
662             namestr = names.tostring()
663
664             # try 64 bit kernel:
665             for i in range(0, outbytes, 40):
666                 name = namestr[i:i+16].split('\0', 1)[0]
667                 if name != 'lo':
668                     ip_addr = socket.inet_ntoa(namestr[i+20:i+24])
669                     break
670
671             # try 32 bit kernel:
672             if ip_addr is None:
673                 ifaces = filter(None, [namestr[i:i+32].split('\0', 1)[0] for i in range(0, outbytes, 32)])
674
675                 for ifname in [iface for iface in ifaces if iface != 'lo']:
676                     ip_addr = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, pack('256s', ifname[:15]))[20:24])
677                     break
678
679         return ip_addr or 'localhost'
680
681     try:
682         ip_addr = _detect_ip_addr()
683     except Exception:
684         ip_addr = 'localhost'
685     return ip_addr
686
687 # RATIONALE BEHIND TIMESTAMP CALCULATIONS AND TIMEZONE MANAGEMENT:
688 #  The server side never does any timestamp calculation, always
689 #  sends them in a naive (timezone agnostic) format supposed to be
690 #  expressed within the server timezone, and expects the clients to
691 #  provide timestamps in the server timezone as well.
692 #  It stores all timestamps in the database in naive format as well,
693 #  which also expresses the time in the server timezone.
694 #  For this reason the server makes its timezone name available via the
695 #  common/timezone_get() rpc method, which clients need to read
696 #  to know the appropriate time offset to use when reading/writing
697 #  times.
698 def get_win32_timezone():
699     """Attempt to return the "standard name" of the current timezone on a win32 system.
700        @return the standard name of the current win32 timezone, or False if it cannot be found.
701     """
702     res = False
703     if sys.platform == "win32":
704         try:
705             import _winreg
706             hklm = _winreg.ConnectRegistry(None,_winreg.HKEY_LOCAL_MACHINE)
707             current_tz_key = _winreg.OpenKey(hklm, r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", 0,_winreg.KEY_ALL_ACCESS)
708             res = str(_winreg.QueryValueEx(current_tz_key,"StandardName")[0])  # [0] is value, [1] is type code
709             _winreg.CloseKey(current_tz_key)
710             _winreg.CloseKey(hklm)
711         except Exception:
712             pass
713     return res
714
715 def detect_server_timezone():
716     """Attempt to detect the timezone to use on the server side.
717        Defaults to UTC if no working timezone can be found.
718        @return the timezone identifier as expected by pytz.timezone.
719     """
720     try:
721         import pytz
722     except Exception:
723         _logger.warning("Python pytz module is not available. "
724             "Timezone will be set to UTC by default.")
725         return 'UTC'
726
727     # Option 1: the configuration option (did not exist before, so no backwards compatibility issue)
728     # 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
729     # Option 3: the environment variable TZ
730     sources = [ (config['timezone'], 'OpenERP configuration'),
731                 (time.tzname[0], 'time.tzname'),
732                 (os.environ.get('TZ',False),'TZ environment variable'), ]
733     # Option 4: OS-specific: /etc/timezone on Unix
734     if os.path.exists("/etc/timezone"):
735         tz_value = False
736         try:
737             f = open("/etc/timezone")
738             tz_value = f.read(128).strip()
739         except Exception:
740             pass
741         finally:
742             f.close()
743         sources.append((tz_value,"/etc/timezone file"))
744     # Option 5: timezone info from registry on Win32
745     if sys.platform == "win32":
746         # Timezone info is stored in windows registry.
747         # However this is not likely to work very well as the standard name
748         # of timezones in windows is rarely something that is known to pytz.
749         # But that's ok, it is always possible to use a config option to set
750         # it explicitly.
751         sources.append((get_win32_timezone(),"Windows Registry"))
752
753     for (value,source) in sources:
754         if value:
755             try:
756                 tz = pytz.timezone(value)
757                 _logger.info("Using timezone %s obtained from %s.", tz.zone, source)
758                 return value
759             except pytz.UnknownTimeZoneError:
760                 _logger.warning("The timezone specified in %s (%s) is invalid, ignoring it.", source, value)
761
762     _logger.warning("No valid timezone could be detected, using default UTC "
763         "timezone. You can specify it explicitly with option 'timezone' in "
764         "the server configuration.")
765     return 'UTC'
766
767 def get_server_timezone():
768     return "UTC"
769
770
771 DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"
772 DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S"
773 DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % (
774     DEFAULT_SERVER_DATE_FORMAT,
775     DEFAULT_SERVER_TIME_FORMAT)
776
777 # Python's strftime supports only the format directives
778 # that are available on the platform's libc, so in order to
779 # be cross-platform we map to the directives required by
780 # the C standard (1989 version), always available on platforms
781 # with a C standard implementation.
782 DATETIME_FORMATS_MAP = {
783         '%C': '', # century
784         '%D': '%m/%d/%Y', # modified %y->%Y
785         '%e': '%d',
786         '%E': '', # special modifier
787         '%F': '%Y-%m-%d',
788         '%g': '%Y', # modified %y->%Y
789         '%G': '%Y',
790         '%h': '%b',
791         '%k': '%H',
792         '%l': '%I',
793         '%n': '\n',
794         '%O': '', # special modifier
795         '%P': '%p',
796         '%R': '%H:%M',
797         '%r': '%I:%M:%S %p',
798         '%s': '', #num of seconds since epoch
799         '%T': '%H:%M:%S',
800         '%t': ' ', # tab
801         '%u': ' %w',
802         '%V': '%W',
803         '%y': '%Y', # Even if %y works, it's ambiguous, so we should use %Y
804         '%+': '%Y-%m-%d %H:%M:%S',
805
806         # %Z is a special case that causes 2 problems at least:
807         #  - the timezone names we use (in res_user.context_tz) come
808         #    from pytz, but not all these names are recognized by
809         #    strptime(), so we cannot convert in both directions
810         #    when such a timezone is selected and %Z is in the format
811         #  - %Z is replaced by an empty string in strftime() when
812         #    there is not tzinfo in a datetime value (e.g when the user
813         #    did not pick a context_tz). The resulting string does not
814         #    parse back if the format requires %Z.
815         # As a consequence, we strip it completely from format strings.
816         # The user can always have a look at the context_tz in
817         # preferences to check the timezone.
818         '%z': '',
819         '%Z': '',
820 }
821
822 def server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_name,
823         tz_offset=True, ignore_unparsable_time=True):
824     """
825     Convert a source timestamp string into a destination timestamp string, attempting to apply the
826     correct offset if both the server and local timezone are recognized, or no
827     offset at all if they aren't or if tz_offset is false (i.e. assuming they are both in the same TZ).
828
829     WARNING: This method is here to allow formatting dates correctly for inclusion in strings where
830              the client would not be able to format/offset it correctly. DO NOT use it for returning
831              date fields directly, these are supposed to be handled by the client!!
832
833     @param src_tstamp_str: the str value containing the timestamp in the server timezone.
834     @param src_format: the format to use when parsing the server timestamp.
835     @param dst_format: the format to use when formatting the resulting timestamp for the local/client timezone.
836     @param dst_tz_name: name of the destination timezone (such as the 'tz' value of the client context)
837     @param ignore_unparsable_time: if True, return False if src_tstamp_str cannot be parsed
838                                    using src_format or formatted using dst_format.
839
840     @return local/client formatted timestamp, expressed in the local/client timezone if possible
841             and if tz_offset is true, or src_tstamp_str if timezone offset could not be determined.
842     """
843     if not src_tstamp_str:
844         return False
845
846     res = src_tstamp_str
847     if src_format and dst_format:
848         # find out server timezone
849         server_tz = get_server_timezone()
850         try:
851             # dt_value needs to be a datetime.datetime object (so no time.struct_time or mx.DateTime.DateTime here!)
852             dt_value = datetime.strptime(src_tstamp_str, src_format)
853             if tz_offset and dst_tz_name:
854                 try:
855                     import pytz
856                     src_tz = pytz.timezone(server_tz)
857                     dst_tz = pytz.timezone(dst_tz_name)
858                     src_dt = src_tz.localize(dt_value, is_dst=True)
859                     dt_value = src_dt.astimezone(dst_tz)
860                 except Exception:
861                     pass
862             res = dt_value.strftime(dst_format)
863         except Exception:
864             # Normal ways to end up here are if strptime or strftime failed
865             if not ignore_unparsable_time:
866                 return False
867     return res
868
869
870 def split_every(n, iterable, piece_maker=tuple):
871     """Splits an iterable into length-n pieces. The last piece will be shorter
872        if ``n`` does not evenly divide the iterable length.
873        @param ``piece_maker``: function to build the pieces
874        from the slices (tuple,list,...)
875     """
876     iterator = iter(iterable)
877     piece = piece_maker(islice(iterator, n))
878     while piece:
879         yield piece
880         piece = piece_maker(islice(iterator, n))
881
882 if __name__ == '__main__':
883     import doctest
884     doctest.testmod()
885
886 class upload_data_thread(threading.Thread):
887     def __init__(self, email, data, type):
888         self.args = [('email',email),('type',type),('data',data)]
889         super(upload_data_thread,self).__init__()
890     def run(self):
891         try:
892             import urllib
893             args = urllib.urlencode(self.args)
894             fp = urllib.urlopen('http://www.openerp.com/scripts/survey.php', args)
895             fp.read()
896             fp.close()
897         except Exception:
898             pass
899
900 def upload_data(email, data, type='SURVEY'):
901     a = upload_data_thread(email, data, type)
902     a.start()
903     return True
904
905 def get_and_group_by_field(cr, uid, obj, ids, field, context=None):
906     """ Read the values of ``field´´ for the given ``ids´´ and group ids by value.
907
908        :param string field: name of the field we want to read and group by
909        :return: mapping of field values to the list of ids that have it
910        :rtype: dict
911     """
912     res = {}
913     for record in obj.read(cr, uid, ids, [field], context=context):
914         key = record[field]
915         res.setdefault(key[0] if isinstance(key, tuple) else key, []).append(record['id'])
916     return res
917
918 def get_and_group_by_company(cr, uid, obj, ids, context=None):
919     return get_and_group_by_field(cr, uid, obj, ids, field='company_id', context=context)
920
921 # port of python 2.6's attrgetter with support for dotted notation
922 def resolve_attr(obj, attr):
923     for name in attr.split("."):
924         obj = getattr(obj, name)
925     return obj
926
927 def attrgetter(*items):
928     if len(items) == 1:
929         attr = items[0]
930         def g(obj):
931             return resolve_attr(obj, attr)
932     else:
933         def g(obj):
934             return tuple(resolve_attr(obj, attr) for attr in items)
935     return g
936
937 class unquote(str):
938     """A subclass of str that implements repr() without enclosing quotation marks
939        or escaping, keeping the original string untouched. The name come from Lisp's unquote.
940        One of the uses for this is to preserve or insert bare variable names within dicts during eval()
941        of a dict's repr(). Use with care.
942
943        Some examples (notice that there are never quotes surrounding
944        the ``active_id`` name:
945
946        >>> unquote('active_id')
947        active_id
948        >>> d = {'test': unquote('active_id')}
949        >>> d
950        {'test': active_id}
951        >>> print d
952        {'test': active_id}
953     """
954     def __repr__(self):
955         return self
956
957 class UnquoteEvalContext(defaultdict):
958     """Defaultdict-based evaluation context that returns 
959        an ``unquote`` string for any missing name used during
960        the evaluation.
961        Mostly useful for evaluating OpenERP domains/contexts that
962        may refer to names that are unknown at the time of eval,
963        so that when the context/domain is converted back to a string,
964        the original names are preserved.
965
966        **Warning**: using an ``UnquoteEvalContext`` as context for ``eval()`` or
967        ``safe_eval()`` will shadow the builtins, which may cause other
968        failures, depending on what is evaluated.
969
970        Example (notice that ``section_id`` is preserved in the final
971        result) :
972
973        >>> context_str = "{'default_user_id': uid, 'default_section_id': section_id}"
974        >>> eval(context_str, UnquoteEvalContext(uid=1))
975        {'default_user_id': 1, 'default_section_id': section_id}
976
977        """
978     def __init__(self, *args, **kwargs):
979         super(UnquoteEvalContext, self).__init__(None, *args, **kwargs)
980
981     def __missing__(self, key):
982         return unquote(key)
983
984
985 class mute_logger(object):
986     """Temporary suppress the logging.
987     Can be used as context manager or decorator.
988
989         @mute_logger('openerp.plic.ploc')
990         def do_stuff():
991             blahblah()
992
993         with mute_logger('openerp.foo.bar'):
994             do_suff()
995
996     """
997     def __init__(self, *loggers):
998         self.loggers = loggers
999
1000     def filter(self, record):
1001         return 0
1002
1003     def __enter__(self):
1004         for logger in self.loggers:
1005             logging.getLogger(logger).addFilter(self)
1006
1007     def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
1008         for logger in self.loggers:
1009             logging.getLogger(logger).removeFilter(self)
1010
1011     def __call__(self, func):
1012         @wraps(func)
1013         def deco(*args, **kwargs):
1014             with self:
1015                 return func(*args, **kwargs)
1016         return deco
1017
1018 _ph = object()
1019 class CountingStream(object):
1020     """ Stream wrapper counting the number of element it has yielded. Similar
1021     role to ``enumerate``, but for use when the iteration process of the stream
1022     isn't fully under caller control (the stream can be iterated from multiple
1023     points including within a library)
1024
1025     ``start`` allows overriding the starting index (the index before the first
1026     item is returned).
1027
1028     On each iteration (call to :meth:`~.next`), increases its :attr:`~.index`
1029     by one.
1030
1031     .. attribute:: index
1032
1033         ``int``, index of the last yielded element in the stream. If the stream
1034         has ended, will give an index 1-past the stream
1035     """
1036     def __init__(self, stream, start=-1):
1037         self.stream = iter(stream)
1038         self.index = start
1039         self.stopped = False
1040     def __iter__(self):
1041         return self
1042     def next(self):
1043         if self.stopped: raise StopIteration()
1044         self.index += 1
1045         val = next(self.stream, _ph)
1046         if val is _ph:
1047             self.stopped = True
1048             raise StopIteration()
1049         return val
1050
1051 def stripped_sys_argv(*strip_args):
1052     """Return sys.argv with some arguments stripped, suitable for reexecution or subprocesses"""
1053     strip_args = sorted(set(strip_args) | set(['-s', '--save', '-d', '--database', '-u', '--update', '-i', '--init']))
1054     assert all(config.parser.has_option(s) for s in strip_args)
1055     takes_value = dict((s, config.parser.get_option(s).takes_value()) for s in strip_args)
1056
1057     longs, shorts = list(tuple(y) for _, y in groupby(strip_args, lambda x: x.startswith('--')))
1058     longs_eq = tuple(l + '=' for l in longs if takes_value[l])
1059
1060     args = sys.argv[:]
1061
1062     def strip(args, i):
1063         return args[i].startswith(shorts) \
1064             or args[i].startswith(longs_eq) or (args[i] in longs) \
1065             or (i >= 1 and (args[i - 1] in strip_args) and takes_value[args[i - 1]])
1066
1067     return [x for i, x in enumerate(args) if not strip(args, i)]
1068
1069 class ConstantMapping(Mapping):
1070     """
1071     An immutable mapping returning the provided value for every single key.
1072
1073     Useful for default value to methods
1074     """
1075     __slots__ = ['_value']
1076     def __init__(self, val):
1077         self._value = val
1078
1079     def __len__(self):
1080         """
1081         defaultdict updates its length for each individually requested key, is
1082         that really useful?
1083         """
1084         return 0
1085
1086     def __iter__(self):
1087         """
1088         same as len, defaultdict udpates its iterable keyset with each key
1089         requested, is there a point for this?
1090         """
1091         return iter([])
1092
1093     def __getitem__(self, item):
1094         return self._value
1095
1096
1097 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: