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