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