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