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