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