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