[FIX] Use the right variable for the send email function
[odoo/odoo.git] / bin / 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 """
24 Miscelleanous tools used by OpenERP.
25 """
26
27 import os, time, sys
28 import inspect
29
30 from config import config
31
32 import zipfile
33 import release
34 import socket
35 import logging
36 import re
37 from itertools import islice
38 import threading
39 from which import which
40
41 import smtplib
42 from email.MIMEText import MIMEText
43 from email.MIMEBase import MIMEBase
44 from email.MIMEMultipart import MIMEMultipart
45 from email.Header import Header
46 from email.Utils import formatdate, COMMASPACE
47 from email.Utils import formatdate, COMMASPACE
48 from email import Encoders
49 import netsvc
50
51
52 if sys.version_info[:2] < (2, 4):
53     from threadinglocal import local
54 else:
55     from threading import local
56
57 from itertools import izip
58
59 _logger = logging.getLogger('tools')
60
61 # initialize a database with base/base.sql
62 def init_db(cr):
63     import addons
64     f = addons.get_module_resource('base', 'base.sql')
65     for line in file_open(f).read().split(';'):
66         if (len(line)>0) and (not line.isspace()):
67             cr.execute(line)
68     cr.commit()
69
70     for i in addons.get_modules():
71         mod_path = addons.get_module_path(i)
72         if not mod_path:
73             continue
74
75         info = addons.load_information_from_description_file(i)
76
77         if not info:
78             continue
79         categs = info.get('category', 'Uncategorized').split('/')
80         p_id = None
81         while categs:
82             if p_id is not None:
83                 cr.execute('select id \
84                            from ir_module_category \
85                            where name=%s and parent_id=%s', (categs[0], p_id))
86             else:
87                 cr.execute('select id \
88                            from ir_module_category \
89                            where name=%s and parent_id is NULL', (categs[0],))
90             c_id = cr.fetchone()
91             if not c_id:
92                 cr.execute('select nextval(\'ir_module_category_id_seq\')')
93                 c_id = cr.fetchone()[0]
94                 cr.execute('insert into ir_module_category \
95                         (id, name, parent_id) \
96                         values (%s, %s, %s)', (c_id, categs[0], p_id))
97             else:
98                 c_id = c_id[0]
99             p_id = c_id
100             categs = categs[1:]
101
102         active = info.get('active', False)
103         installable = info.get('installable', True)
104         if installable:
105             if active:
106                 state = 'to install'
107             else:
108                 state = 'uninstalled'
109         else:
110             state = 'uninstallable'
111         cr.execute('select nextval(\'ir_module_module_id_seq\')')
112         id = cr.fetchone()[0]
113         cr.execute('insert into ir_module_module \
114                 (id, author, website, name, shortdesc, description, \
115                     category_id, state, certificate) \
116                 values (%s, %s, %s, %s, %s, %s, %s, %s, %s)', (
117             id, info.get('author', ''),
118             info.get('website', ''), i, info.get('name', False),
119             info.get('description', ''), p_id, state, info.get('certificate') or None))
120         cr.execute('insert into ir_model_data \
121             (name,model,module, res_id, noupdate) values (%s,%s,%s,%s,%s)', (
122                 'module_meta_information', 'ir.module.module', i, id, True))
123         dependencies = info.get('depends', [])
124         for d in dependencies:
125             cr.execute('insert into ir_module_module_dependency \
126                     (module_id,name) values (%s, %s)', (id, d))
127         cr.commit()
128
129 def find_in_path(name):
130     try:
131         return which(name)
132     except IOError:
133         return None
134
135 def find_pg_tool(name):
136     path = None
137     if config['pg_path'] and config['pg_path'] != 'None':
138         path = config['pg_path']
139     try:
140         return which(name, path=path)
141     except IOError:
142         return None
143
144 def exec_pg_command(name, *args):
145     prog = find_pg_tool(name)
146     if not prog:
147         raise Exception('Couldn\'t find %s' % name)
148     args2 = (os.path.basename(prog),) + args
149     return os.spawnv(os.P_WAIT, prog, args2)
150
151 def exec_pg_command_pipe(name, *args):
152     prog = find_pg_tool(name)
153     if not prog:
154         raise Exception('Couldn\'t find %s' % name)
155     if os.name == "nt":
156         cmd = '"' + prog + '" ' + ' '.join(args)
157     else:
158         cmd = prog + ' ' + ' '.join(args)
159     return os.popen2(cmd, 'b')
160
161 def exec_command_pipe(name, *args):
162     prog = find_in_path(name)
163     if not prog:
164         raise Exception('Couldn\'t find %s' % name)
165     if os.name == "nt":
166         cmd = '"'+prog+'" '+' '.join(args)
167     else:
168         cmd = prog+' '+' '.join(args)
169     return os.popen2(cmd, 'b')
170
171 #----------------------------------------------------------
172 # File paths
173 #----------------------------------------------------------
174 #file_path_root = os.getcwd()
175 #file_path_addons = os.path.join(file_path_root, 'addons')
176
177 def file_open(name, mode="r", subdir='addons', pathinfo=False):
178     """Open a file from the OpenERP root, using a subdir folder.
179
180     >>> file_open('hr/report/timesheer.xsl')
181     >>> file_open('addons/hr/report/timesheet.xsl')
182     >>> file_open('../../base/report/rml_template.xsl', subdir='addons/hr/report', pathinfo=True)
183
184     @param name: name of the file
185     @param mode: file open mode
186     @param subdir: subdirectory
187     @param pathinfo: if True returns tupple (fileobject, filepath)
188
189     @return: fileobject if pathinfo is False else (fileobject, filepath)
190     """
191     import addons
192     adps = addons.ad_paths
193     rtp = os.path.normcase(os.path.abspath(config['root_path']))
194
195     if name.replace(os.path.sep, '/').startswith('addons/'):
196         subdir = 'addons'
197         name = name[7:]
198
199     # First try to locate in addons_path
200     if subdir:
201         subdir2 = subdir
202         if subdir2.replace(os.path.sep, '/').startswith('addons/'):
203             subdir2 = subdir2[7:]
204
205         subdir2 = (subdir2 != 'addons' or None) and subdir2
206
207         for adp in adps:
208           try:
209             if subdir2:
210                 fn = os.path.join(adp, subdir2, name)
211             else:
212                 fn = os.path.join(adp, name)
213             fn = os.path.normpath(fn)
214             fo = file_open(fn, mode=mode, subdir=None, pathinfo=pathinfo)
215             if pathinfo:
216                 return fo, fn
217             return fo
218           except IOError, e:
219             pass
220
221     if subdir:
222         name = os.path.join(rtp, subdir, name)
223     else:
224         name = os.path.join(rtp, name)
225
226     name = os.path.normpath(name)
227
228     # Check for a zipfile in the path
229     head = name
230     zipname = False
231     name2 = False
232     while True:
233         head, tail = os.path.split(head)
234         if not tail:
235             break
236         if zipname:
237             zipname = os.path.join(tail, zipname)
238         else:
239             zipname = tail
240         if zipfile.is_zipfile(head+'.zip'):
241             from cStringIO import StringIO
242             zfile = zipfile.ZipFile(head+'.zip')
243             try:
244                 fo = StringIO()
245                 fo.write(zfile.read(os.path.join(
246                     os.path.basename(head), zipname).replace(
247                         os.sep, '/')))
248                 fo.seek(0)
249                 if pathinfo:
250                     return fo, name
251                 return fo
252             except:
253                 name2 = os.path.normpath(os.path.join(head + '.zip', zipname))
254                 pass
255     for i in (name2, name):
256         if i and os.path.isfile(i):
257             fo = file(i, mode)
258             if pathinfo:
259                 return fo, i
260             return fo
261     if os.path.splitext(name)[1] == '.rml':
262         raise IOError, 'Report %s doesn\'t exist or deleted : ' %str(name)
263     raise IOError, 'File not found : '+str(name)
264
265
266 #----------------------------------------------------------
267 # iterables
268 #----------------------------------------------------------
269 def flatten(list):
270     """Flatten a list of elements into a uniqu list
271     Author: Christophe Simonis (christophe@tinyerp.com)
272
273     Examples:
274     >>> flatten(['a'])
275     ['a']
276     >>> flatten('b')
277     ['b']
278     >>> flatten( [] )
279     []
280     >>> flatten( [[], [[]]] )
281     []
282     >>> flatten( [[['a','b'], 'c'], 'd', ['e', [], 'f']] )
283     ['a', 'b', 'c', 'd', 'e', 'f']
284     >>> t = (1,2,(3,), [4, 5, [6, [7], (8, 9), ([10, 11, (12, 13)]), [14, [], (15,)], []]])
285     >>> flatten(t)
286     [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
287     """
288
289     def isiterable(x):
290         return hasattr(x, "__iter__")
291
292     r = []
293     for e in list:
294         if isiterable(e):
295             map(r.append, flatten(e))
296         else:
297             r.append(e)
298     return r
299
300 def reverse_enumerate(l):
301     """Like enumerate but in the other sens
302     >>> a = ['a', 'b', 'c']
303     >>> it = reverse_enumerate(a)
304     >>> it.next()
305     (2, 'c')
306     >>> it.next()
307     (1, 'b')
308     >>> it.next()
309     (0, 'a')
310     >>> it.next()
311     Traceback (most recent call last):
312       File "<stdin>", line 1, in <module>
313     StopIteration
314     """
315     return izip(xrange(len(l)-1, -1, -1), reversed(l))
316
317 #----------------------------------------------------------
318 # Emails
319 #----------------------------------------------------------
320 email_re = re.compile(r"""
321     ([a-zA-Z][\w\.-]*[a-zA-Z0-9]     # username part
322     @                                # mandatory @ sign
323     [a-zA-Z0-9][\w\.-]*              # domain must start with a letter ... Ged> why do we include a 0-9 then?
324      \.
325      [a-z]{2,3}                      # TLD
326     )
327     """, re.VERBOSE)
328 res_re = re.compile(r"\[([0-9]+)\]", re.UNICODE)
329 command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE)
330 reference_re = re.compile("<.*-openobject-(\\d+)@(.*)>", re.UNICODE)
331
332 priorities = {
333         '1': '1 (Highest)',
334         '2': '2 (High)',
335         '3': '3 (Normal)',
336         '4': '4 (Low)',
337         '5': '5 (Lowest)',
338     }
339
340 def html2plaintext(html, body_id=None, encoding='utf-8'):
341     ## (c) Fry-IT, www.fry-it.com, 2007
342     ## <peter@fry-it.com>
343     ## download here: http://www.peterbe.com/plog/html2plaintext
344
345
346     """ from an HTML text, convert the HTML to plain text.
347     If @body_id is provided then this is the tag where the
348     body (not necessarily <body>) starts.
349     """
350
351     html = ustr(html)
352
353     try:
354         from BeautifulSoup import BeautifulSoup, SoupStrainer, Comment
355     except:
356         return html
357
358     urls = []
359     if body_id is not None:
360         strainer = SoupStrainer(id=body_id)
361     else:
362         strainer = SoupStrainer('body')
363
364     soup = BeautifulSoup(html, parseOnlyThese=strainer, fromEncoding=encoding)
365     for link in soup.findAll('a'):
366         title = link.renderContents()
367         for url in [x[1] for x in link.attrs if x[0]=='href']:
368             urls.append(dict(url=ustr(url), tag=ustr(link), title=ustr(title)))
369
370     html = ustr(soup.__str__())
371
372     url_index = []
373     i = 0
374     for d in urls:
375         if d['title'] == d['url'] or 'http://'+d['title'] == d['url']:
376             html = html.replace(d['tag'], d['url'])
377         else:
378             i += 1
379             html = html.replace(d['tag'], '%s [%s]' % (d['title'], i))
380             url_index.append(d['url'])
381
382     html = html.replace('<strong>','*').replace('</strong>','*')
383     html = html.replace('<b>','*').replace('</b>','*')
384     html = html.replace('<h3>','*').replace('</h3>','*')
385     html = html.replace('<h2>','**').replace('</h2>','**')
386     html = html.replace('<h1>','**').replace('</h1>','**')
387     html = html.replace('<em>','/').replace('</em>','/')
388
389
390     html = html.replace('<br>', '\n')
391     html = html.replace('<tr>', '\n')
392     html = html.replace('</p>', '\n\n')
393     html = re.sub('<br\s*/>', '\n', html)
394     html = html.replace(' ' * 2, ' ')
395
396
397     # for all other tags we failed to clean up, just remove then and
398     # complain about them on the stderr
399     def desperate_fixer(g):
400         #print >>sys.stderr, "failed to clean up %s" % str(g.group())
401         return ' '
402
403     html = re.sub('<.*?>', desperate_fixer, html)
404
405     # lstrip all lines
406     html = '\n'.join([x.lstrip() for x in html.splitlines()])
407
408     for i, url in enumerate(url_index):
409         if i == 0:
410             html += '\n\n'
411         html += ustr('[%s] %s\n') % (i+1, url)
412
413     return html
414
415 def _email_send(message, openobject_id=None, debug=False):
416     """Low-level method to send directly a Message through the configured smtp server.
417         :param message: an email.message.Message to send
418         :param debug: True if messages should be output to stderr before being sent,
419                       and smtplib.SMTP put into debug mode.
420         :return: True if the mail was delivered successfully to the smtp,
421                  else False (+ exception logged)
422     """
423     if openobject_id:
424         message['Message-Id'] = "<%s-openobject-%s@%s>" % (time.time(), openobject_id, socket.gethostname())
425
426     try:
427         oldstderr = smtplib.stderr
428         s = smtplib.SMTP()
429         try:
430             # in case of debug, the messages are printed to stderr.
431             if debug:
432                 smtplib.stderr = WriteToLogger()
433
434             s.set_debuglevel(int(bool(debug)))  # 0 or 1
435             s.connect(smtp_server, config['smtp_port'])
436             if ssl:
437                 s.ehlo()
438                 s.starttls()
439                 s.ehlo()
440
441             if config['smtp_user'] or config['smtp_password']:
442                 s.login(config['smtp_user'], config['smtp_password'])
443             s.sendmail(email_from,
444                        flatten([email_to, email_cc, email_bcc]),
445                        message.as_string())
446         finally:
447             s.quit()
448             if debug:
449                 smtplib.stderr = oldstderr
450
451     except Exception, e:
452         _logger.error('could not deliver email', exc_info=True)
453         return False
454
455     return True
456
457
458 def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
459                attach=None, openobject_id=False, ssl=False, debug=False, subtype='plain', x_headers=None, priority='3'):
460
461     """Send an email.
462
463     Arguments:
464
465     `email_from`: A string used to fill the `From` header, if falsy,
466                   config['email_from'] is used instead.  Also used for
467                   the `Reply-To` header if `reply_to` is not provided
468
469     `email_to`: a sequence of addresses to send the mail to.
470     """
471     if x_headers is None:
472         x_headers = {}
473
474     if not ssl: ssl = config.get('smtp_ssl', False)
475
476     if not (email_from or config['email_from']):
477         raise ValueError("Sending an email requires either providing a sender "
478                          "address or having configured one")
479
480     if not email_from: email_from = config.get('email_from', False)
481
482     if not email_cc: email_cc = []
483     if not email_bcc: email_bcc = []
484     if not body: body = u''
485     try: email_body = body.encode('utf-8')
486     except (UnicodeEncodeError, UnicodeDecodeError):
487         email_body = body
488
489     try:
490         email_text = MIMEText(email_body.encode('utf8') or '',_subtype=subtype,_charset='utf-8')
491     except:
492         email_text = MIMEText(email_body or '',_subtype=subtype,_charset='utf-8')
493
494     if attach: msg = MIMEMultipart()
495     else: msg = email_text
496
497     msg['Subject'] = Header(ustr(subject), 'utf-8')
498     msg['From'] = email_from
499     del msg['Reply-To']
500     if reply_to:
501         msg['Reply-To'] = reply_to
502     else:
503         msg['Reply-To'] = msg['From']
504     msg['To'] = COMMASPACE.join(email_to)
505     if email_cc:
506         msg['Cc'] = COMMASPACE.join(email_cc)
507     if email_bcc:
508         msg['Bcc'] = COMMASPACE.join(email_bcc)
509     msg['Date'] = formatdate(localtime=True)
510
511     msg['X-Priority'] = priorities.get(priority, '3 (Normal)')
512
513     # Add dynamic X Header
514     for key, value in x_headers.iteritems():
515         msg['%s' % key] = str(value)
516
517     if attach:
518         msg.attach(email_text)
519         for (fname,fcontent) in attach:
520             part = MIMEBase('application', "octet-stream")
521             part.set_payload( fcontent )
522             Encoders.encode_base64(part)
523             part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,))
524             msg.attach(part)
525
526     class WriteToLogger(object):
527         def __init__(self):
528             self.logger = netsvc.Logger()
529
530         def write(self, s):
531             self.logger.notifyChannel('email_send', netsvc.LOG_DEBUG, s)
532
533     smtp_server = config['smtp_server']
534     if smtp_server.startswith('maildir:/'):
535         from mailbox import Maildir
536         maildir_path = smtp_server[8:]
537         try:
538             mdir = Maildir(maildir_path,factory=None, create = True)
539             mdir.add(msg.as_string(True))
540             return True
541         except Exception,e:
542             netsvc.Logger().notifyChannel('email_send (maildir)', netsvc.LOG_ERROR, e)
543             return False
544
545     return _email_send(msg, openobject_id=openobject_id, debug=debug)
546
547 #----------------------------------------------------------
548 # SMS
549 #----------------------------------------------------------
550 # text must be latin-1 encoded
551 def sms_send(user, password, api_id, text, to):
552     import urllib
553     url = "http://api.urlsms.com/SendSMS.aspx"
554     #url = "http://196.7.150.220/http/sendmsg"
555     params = urllib.urlencode({'UserID': user, 'Password': password, 'SenderID': api_id, 'MsgText': text, 'RecipientMobileNo':to})
556     f = urllib.urlopen(url+"?"+params)
557     # FIXME: Use the logger if there is an error
558     return True
559
560 #---------------------------------------------------------
561 # Class that stores an updateable string (used in wizards)
562 #---------------------------------------------------------
563 class UpdateableStr(local):
564
565     def __init__(self, string=''):
566         self.string = string
567
568     def __str__(self):
569         return str(self.string)
570
571     def __repr__(self):
572         return str(self.string)
573
574     def __nonzero__(self):
575         return bool(self.string)
576
577
578 class UpdateableDict(local):
579     '''Stores an updateable dict to use in wizards'''
580
581     def __init__(self, dict=None):
582         if dict is None:
583             dict = {}
584         self.dict = dict
585
586     def __str__(self):
587         return str(self.dict)
588
589     def __repr__(self):
590         return str(self.dict)
591
592     def clear(self):
593         return self.dict.clear()
594
595     def keys(self):
596         return self.dict.keys()
597
598     def __setitem__(self, i, y):
599         self.dict.__setitem__(i, y)
600
601     def __getitem__(self, i):
602         return self.dict.__getitem__(i)
603
604     def copy(self):
605         return self.dict.copy()
606
607     def iteritems(self):
608         return self.dict.iteritems()
609
610     def iterkeys(self):
611         return self.dict.iterkeys()
612
613     def itervalues(self):
614         return self.dict.itervalues()
615
616     def pop(self, k, d=None):
617         return self.dict.pop(k, d)
618
619     def popitem(self):
620         return self.dict.popitem()
621
622     def setdefault(self, k, d=None):
623         return self.dict.setdefault(k, d)
624
625     def update(self, E, **F):
626         return self.dict.update(E, F)
627
628     def values(self):
629         return self.dict.values()
630
631     def get(self, k, d=None):
632         return self.dict.get(k, d)
633
634     def has_key(self, k):
635         return self.dict.has_key(k)
636
637     def items(self):
638         return self.dict.items()
639
640     def __cmp__(self, y):
641         return self.dict.__cmp__(y)
642
643     def __contains__(self, k):
644         return self.dict.__contains__(k)
645
646     def __delitem__(self, y):
647         return self.dict.__delitem__(y)
648
649     def __eq__(self, y):
650         return self.dict.__eq__(y)
651
652     def __ge__(self, y):
653         return self.dict.__ge__(y)
654
655     def __gt__(self, y):
656         return self.dict.__gt__(y)
657
658     def __hash__(self):
659         return self.dict.__hash__()
660
661     def __iter__(self):
662         return self.dict.__iter__()
663
664     def __le__(self, y):
665         return self.dict.__le__(y)
666
667     def __len__(self):
668         return self.dict.__len__()
669
670     def __lt__(self, y):
671         return self.dict.__lt__(y)
672
673     def __ne__(self, y):
674         return self.dict.__ne__(y)
675
676
677 # Don't use ! Use res.currency.round()
678 class currency(float):
679
680     def __init__(self, value, accuracy=2, rounding=None):
681         if rounding is None:
682             rounding=10**-accuracy
683         self.rounding=rounding
684         self.accuracy=accuracy
685
686     def __new__(cls, value, accuracy=2, rounding=None):
687         return float.__new__(cls, round(value, accuracy))
688
689     #def __str__(self):
690     #   display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
691     #   return str(display_value)
692
693
694 def is_hashable(h):
695     try:
696         hash(h)
697         return True
698     except TypeError:
699         return False
700
701 class cache(object):
702     """
703     Use it as a decorator of the function you plan to cache
704     Timeout: 0 = no timeout, otherwise in seconds
705     """
706
707     __caches = []
708
709     def __init__(self, timeout=None, skiparg=2, multi=None):
710         assert skiparg >= 2 # at least self and cr
711         if timeout is None:
712             self.timeout = config['cache_timeout']
713         else:
714             self.timeout = timeout
715         self.skiparg = skiparg
716         self.multi = multi
717         self.lasttime = time.time()
718         self.cache = {}
719         self.fun = None
720         cache.__caches.append(self)
721
722
723     def _generate_keys(self, dbname, kwargs2):
724         """
725         Generate keys depending of the arguments and the self.mutli value
726         """
727
728         def to_tuple(d):
729             pairs = d.items()
730             pairs.sort(key=lambda (k,v): k)
731             for i, (k, v) in enumerate(pairs):
732                 if isinstance(v, dict):
733                     pairs[i] = (k, to_tuple(v))
734                 if isinstance(v, (list, set)):
735                     pairs[i] = (k, tuple(v))
736                 elif not is_hashable(v):
737                     pairs[i] = (k, repr(v))
738             return tuple(pairs)
739
740         if not self.multi:
741             key = (('dbname', dbname),) + to_tuple(kwargs2)
742             yield key, None
743         else:
744             multis = kwargs2[self.multi][:]
745             for id in multis:
746                 kwargs2[self.multi] = (id,)
747                 key = (('dbname', dbname),) + to_tuple(kwargs2)
748                 yield key, id
749
750     def _unify_args(self, *args, **kwargs):
751         # Update named arguments with positional argument values (without self and cr)
752         kwargs2 = self.fun_default_values.copy()
753         kwargs2.update(kwargs)
754         kwargs2.update(dict(zip(self.fun_arg_names, args[self.skiparg-2:])))
755         return kwargs2
756
757     def clear(self, dbname, *args, **kwargs):
758         """clear the cache for database dbname
759             if *args and **kwargs are both empty, clear all the keys related to this database
760         """
761         if not args and not kwargs:
762             keys_to_del = [key for key in self.cache.keys() if key[0][1] == dbname]
763         else:
764             kwargs2 = self._unify_args(*args, **kwargs)
765             keys_to_del = [key for key, _ in self._generate_keys(dbname, kwargs2) if key in self.cache.keys()]
766
767         for key in keys_to_del:
768             self.cache.pop(key)
769
770     @classmethod
771     def clean_caches_for_db(cls, dbname):
772         for c in cls.__caches:
773             c.clear(dbname)
774
775     def __call__(self, fn):
776         if self.fun is not None:
777             raise Exception("Can not use a cache instance on more than one function")
778         self.fun = fn
779
780         argspec = inspect.getargspec(fn)
781         self.fun_arg_names = argspec[0][self.skiparg:]
782         self.fun_default_values = {}
783         if argspec[3]:
784             self.fun_default_values = dict(zip(self.fun_arg_names[-len(argspec[3]):], argspec[3]))
785
786         def cached_result(self2, cr, *args, **kwargs):
787             if time.time()-int(self.timeout) > self.lasttime:
788                 self.lasttime = time.time()
789                 t = time.time()-int(self.timeout)
790                 old_keys = [key for key in self.cache.keys() if self.cache[key][1] < t]
791                 for key in old_keys:
792                     self.cache.pop(key)
793
794             kwargs2 = self._unify_args(*args, **kwargs)
795
796             result = {}
797             notincache = {}
798             for key, id in self._generate_keys(cr.dbname, kwargs2):
799                 if key in self.cache:
800                     result[id] = self.cache[key][0]
801                 else:
802                     notincache[id] = key
803
804             if notincache:
805                 if self.multi:
806                     kwargs2[self.multi] = notincache.keys()
807
808                 result2 = fn(self2, cr, *args[:self.skiparg-2], **kwargs2)
809                 if not self.multi:
810                     key = notincache[None]
811                     self.cache[key] = (result2, time.time())
812                     result[None] = result2
813                 else:
814                     for id in result2:
815                         key = notincache[id]
816                         self.cache[key] = (result2[id], time.time())
817                     result.update(result2)
818
819             if not self.multi:
820                 return result[None]
821             return result
822
823         cached_result.clear_cache = self.clear
824         return cached_result
825
826 def to_xml(s):
827     return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;')
828
829 def get_encodings():
830     yield 'utf8'
831     from locale import getpreferredencoding
832     prefenc = getpreferredencoding()
833     if prefenc:
834         yield prefenc
835
836         prefenc = {
837             'latin1': 'latin9',
838             'iso-8859-1': 'iso8859-15',
839             'cp1252': '1252',
840         }.get(prefenc.lower())
841         if prefenc:
842             yield prefenc
843
844
845 def ustr(value):
846     """This method is similar to the builtin `str` method, except
847     it will return Unicode string.
848
849     @param value: the value to convert
850
851     @rtype: unicode
852     @return: unicode string
853     """
854     orig = value
855     if isinstance(value, Exception):
856         return exception_to_unicode(value)
857
858     if isinstance(value, unicode):
859         return value
860
861     try:
862         return unicode(value)
863     except:
864         pass
865
866     for ln in get_encodings():
867         try:
868             return unicode(value, ln)
869         except:
870             pass
871     raise UnicodeError('unable de to convert %r' % (orig,))
872
873
874 def exception_to_unicode(e):
875     if (sys.version_info[:2] < (2,6)) and hasattr(e, 'message'):
876         return ustr(e.message)
877     if hasattr(e, 'args'):
878         return "\n".join((ustr(a) for a in e.args))
879     try:
880         return ustr(e)
881     except:
882         return u"Unknown message"
883
884
885 # to be compatible with python 2.4
886 import __builtin__
887 if not hasattr(__builtin__, 'all'):
888     def all(iterable):
889         for element in iterable:
890             if not element:
891                 return False
892         return True
893
894     __builtin__.all = all
895     del all
896
897 if not hasattr(__builtin__, 'any'):
898     def any(iterable):
899         for element in iterable:
900             if element:
901                 return True
902         return False
903
904     __builtin__.any = any
905     del any
906
907 def get_iso_codes(lang):
908     if lang.find('_') != -1:
909         if lang.split('_')[0] == lang.split('_')[1].lower():
910             lang = lang.split('_')[0]
911     return lang
912
913 def get_languages():
914     languages={
915         'ab_RU': u'Abkhazian (RU)',
916         'ar_AR': u'Arabic / الْعَرَبيّة',
917         'bg_BG': u'Bulgarian / български',
918         'bs_BS': u'Bosnian / bosanski jezik',
919         'ca_ES': u'Catalan / Català',
920         'cs_CZ': u'Czech / Čeština',
921         'da_DK': u'Danish / Dansk',
922         'de_DE': u'German / Deutsch',
923         'el_GR': u'Greek / Ελληνικά',
924         'en_CA': u'English (CA)',
925         'en_GB': u'English (UK)',
926         'en_US': u'English (US)',
927         'es_AR': u'Spanish (AR) / Español (AR)',
928         'es_ES': u'Spanish / Español',
929         'et_EE': u'Estonian / Eesti keel',
930         'fa_IR': u'Persian / فارس',
931         'fi_FI': u'Finland / Suomi',
932         'fr_BE': u'French (BE) / Français (BE)',
933         'fr_CH': u'French (CH) / Français (CH)',
934         'fr_FR': u'French / Français',
935         'gl_ES': u'Galician / Galego',
936         'gu_IN': u'Gujarati / India',
937         'hi_IN': u'Hindi / India',
938         'hr_HR': u'Croatian / hrvatski jezik',
939         'hu_HU': u'Hungarian / Magyar',
940         'id_ID': u'Indonesian / Bahasa Indonesia',
941         'it_IT': u'Italian / Italiano',
942         'iu_CA': u'Inuktitut / Canada',
943         'ja_JP': u'Japanese / Japan',
944         'ko_KP': u'Korean / Korea, Democratic Peoples Republic of',
945         'ko_KR': u'Korean / Korea, Republic of',
946         'lt_LT': u'Lithuanian / Lietuvių kalba',
947         'lv_LV': u'Latvian / Latvia',
948         'ml_IN': u'Malayalam / India',
949         'mn_MN': u'Mongolian / Mongolia',
950         'nb_NO': u'Norwegian Bokmål / Norway',
951         'nl_NL': u'Dutch / Nederlands',
952         'nl_BE': u'Dutch (Belgium) / Nederlands (Belgïe)',
953         'oc_FR': u'Occitan (post 1500) / France',
954         'pl_PL': u'Polish / Język polski',
955         'pt_BR': u'Portugese (BR) / português (BR)',
956         'pt_PT': u'Portugese / português',
957         'ro_RO': u'Romanian / limba română',
958         'ru_RU': u'Russian / русский язык',
959         'si_LK': u'Sinhalese / Sri Lanka',
960         'sl_SI': u'Slovenian / slovenščina',
961         'sk_SK': u'Slovak / Slovenský jazyk',
962         'sq_AL': u'Albanian / Shqipëri',
963         'sr_RS': u'Serbian / Serbia',
964         'sv_SE': u'Swedish / svenska',
965         'te_IN': u'Telugu / India',
966         'tr_TR': u'Turkish / Türkçe',
967         'vi_VN': u'Vietnam / Cộng hòa xã hội chủ nghĩa Việt Nam',
968         'uk_UA': u'Ukrainian / украї́нська мо́ва',
969         'ur_PK': u'Urdu / Pakistan',
970         'zh_CN': u'Chinese (CN) / 简体中文',
971         'zh_HK': u'Chinese (HK)',
972         'zh_TW': u'Chinese (TW) / 正體字',
973         'th_TH': u'Thai / ภาษาไทย',
974         'tlh_TLH': u'Klingon',
975     }
976     return languages
977
978 def scan_languages():
979 #    import glob
980 #    file_list = [os.path.splitext(os.path.basename(f))[0] for f in glob.glob(os.path.join(config['root_path'],'addons', 'base', 'i18n', '*.po'))]
981 #    ret = [(lang, lang_dict.get(lang, lang)) for lang in file_list]
982     # Now it will take all languages from get languages function without filter it with base module languages
983     lang_dict = get_languages()
984     ret = [(lang, lang_dict.get(lang, lang)) for lang in list(lang_dict)]
985     ret.sort(key=lambda k:k[1])
986     return ret
987
988
989 def get_user_companies(cr, user):
990     def _get_company_children(cr, ids):
991         if not ids:
992             return []
993         cr.execute('SELECT id FROM res_company WHERE parent_id IN %s', (tuple(ids),))
994         res = [x[0] for x in cr.fetchall()]
995         res.extend(_get_company_children(cr, res))
996         return res
997     cr.execute('SELECT company_id FROM res_users WHERE id=%s', (user,))
998     user_comp = cr.fetchone()[0]
999     if not user_comp:
1000         return []
1001     return [user_comp] + _get_company_children(cr, [user_comp])
1002
1003 def mod10r(number):
1004     """
1005     Input number : account or invoice number
1006     Output return: the same number completed with the recursive mod10
1007     key
1008     """
1009     codec=[0,9,4,6,8,2,7,1,3,5]
1010     report = 0
1011     result=""
1012     for digit in number:
1013         result += digit
1014         if digit.isdigit():
1015             report = codec[ (int(digit) + report) % 10 ]
1016     return result + str((10 - report) % 10)
1017
1018
1019 def human_size(sz):
1020     """
1021     Return the size in a human readable format
1022     """
1023     if not sz:
1024         return False
1025     units = ('bytes', 'Kb', 'Mb', 'Gb')
1026     if isinstance(sz,basestring):
1027         sz=len(sz)
1028     s, i = float(sz), 0
1029     while s >= 1024 and i < len(units)-1:
1030         s = s / 1024
1031         i = i + 1
1032     return "%0.2f %s" % (s, units[i])
1033
1034 def logged(f):
1035     from tools.func import wraps
1036
1037     @wraps(f)
1038     def wrapper(*args, **kwargs):
1039         import netsvc
1040         from pprint import pformat
1041
1042         vector = ['Call -> function: %r' % f]
1043         for i, arg in enumerate(args):
1044             vector.append('  arg %02d: %s' % (i, pformat(arg)))
1045         for key, value in kwargs.items():
1046             vector.append('  kwarg %10s: %s' % (key, pformat(value)))
1047
1048         timeb4 = time.time()
1049         res = f(*args, **kwargs)
1050
1051         vector.append('  result: %s' % pformat(res))
1052         vector.append('  time delta: %s' % (time.time() - timeb4))
1053         netsvc.Logger().notifyChannel('logged', netsvc.LOG_DEBUG, '\n'.join(vector))
1054         return res
1055
1056     return wrapper
1057
1058 class profile(object):
1059     def __init__(self, fname=None):
1060         self.fname = fname
1061
1062     def __call__(self, f):
1063         from tools.func import wraps
1064
1065         @wraps(f)
1066         def wrapper(*args, **kwargs):
1067             class profile_wrapper(object):
1068                 def __init__(self):
1069                     self.result = None
1070                 def __call__(self):
1071                     self.result = f(*args, **kwargs)
1072             pw = profile_wrapper()
1073             import cProfile
1074             fname = self.fname or ("%s.cprof" % (f.func_name,))
1075             cProfile.runctx('pw()', globals(), locals(), filename=fname)
1076             return pw.result
1077
1078         return wrapper
1079
1080 def debug(what):
1081     """
1082         This method allow you to debug your code without print
1083         Example:
1084         >>> def func_foo(bar)
1085         ...     baz = bar
1086         ...     debug(baz)
1087         ...     qnx = (baz, bar)
1088         ...     debug(qnx)
1089         ...
1090         >>> func_foo(42)
1091
1092         This will output on the logger:
1093
1094             [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:baz = 42
1095             [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:qnx = (42, 42)
1096
1097         To view the DEBUG lines in the logger you must start the server with the option
1098             --log-level=debug
1099
1100     """
1101     import netsvc
1102     from inspect import stack
1103     import re
1104     from pprint import pformat
1105     st = stack()[1]
1106     param = re.split("debug *\((.+)\)", st[4][0].strip())[1].strip()
1107     while param.count(')') > param.count('('): param = param[:param.rfind(')')]
1108     what = pformat(what)
1109     if param != what:
1110         what = "%s = %s" % (param, what)
1111     netsvc.Logger().notifyChannel(st[3], netsvc.LOG_DEBUG, what)
1112
1113
1114 icons = map(lambda x: (x,x), ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
1115 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
1116 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
1117 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
1118 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
1119 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
1120 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
1121 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
1122 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
1123 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
1124 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
1125 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
1126 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
1127 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
1128 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
1129 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
1130 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
1131 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
1132 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
1133 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
1134 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
1135 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
1136 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
1137 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
1138 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
1139 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
1140 'terp-check','terp-go-month','terp-go-year','terp-go-today','terp-document-new','terp-camera_test',
1141 'terp-emblem-important','terp-gtk-media-pause','terp-gtk-stop','terp-gnome-cpu-frequency-applet+',
1142 'terp-dialog-close','terp-gtk-jump-to-rtl','terp-gtk-jump-to-ltr','terp-accessories-archiver',
1143 'terp-stock_align_left_24','terp-stock_effects-object-colorize','terp-go-home','terp-gtk-go-back-rtl',
1144 'terp-gtk-go-back-ltr','terp-personal','terp-personal-','terp-personal+','terp-accessories-archiver-minus',
1145 'terp-accessories-archiver+','terp-stock_symbol-selection','terp-call-start','terp-dolar',
1146 'terp-face-plain','terp-folder-blue','terp-folder-green','terp-folder-orange','terp-folder-yellow',
1147 'terp-gdu-smart-failing','terp-go-week','terp-gtk-select-all','terp-locked','terp-mail-forward',
1148 'terp-mail-message-new','terp-mail-replied','terp-rating-rated','terp-stage','terp-stock_format-scientific',
1149 'terp-dolar_ok!','terp-idea','terp-stock_format-default','terp-mail-','terp-mail_delete'
1150 ])
1151
1152 def extract_zip_file(zip_file, outdirectory):
1153     import zipfile
1154     import os
1155
1156     zf = zipfile.ZipFile(zip_file, 'r')
1157     out = outdirectory
1158     for path in zf.namelist():
1159         tgt = os.path.join(out, path)
1160         tgtdir = os.path.dirname(tgt)
1161         if not os.path.exists(tgtdir):
1162             os.makedirs(tgtdir)
1163
1164         if not tgt.endswith(os.sep):
1165             fp = open(tgt, 'wb')
1166             fp.write(zf.read(path))
1167             fp.close()
1168     zf.close()
1169
1170 def detect_ip_addr():
1171     """Try a very crude method to figure out a valid external
1172        IP or hostname for the current machine. Don't rely on this
1173        for binding to an interface, but it could be used as basis
1174        for constructing a remote URL to the server.
1175     """
1176     def _detect_ip_addr():
1177         from array import array
1178         import socket
1179         from struct import pack, unpack
1180
1181         try:
1182             import fcntl
1183         except ImportError:
1184             fcntl = None
1185
1186         ip_addr = None
1187
1188         if not fcntl: # not UNIX:
1189             host = socket.gethostname()
1190             ip_addr = socket.gethostbyname(host)
1191         else: # UNIX:
1192             # get all interfaces:
1193             nbytes = 128 * 32
1194             s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1195             names = array('B', '\0' * nbytes)
1196             #print 'names: ', names
1197             outbytes = unpack('iL', fcntl.ioctl( s.fileno(), 0x8912, pack('iL', nbytes, names.buffer_info()[0])))[0]
1198             namestr = names.tostring()
1199
1200             # try 64 bit kernel:
1201             for i in range(0, outbytes, 40):
1202                 name = namestr[i:i+16].split('\0', 1)[0]
1203                 if name != 'lo':
1204                     ip_addr = socket.inet_ntoa(namestr[i+20:i+24])
1205                     break
1206
1207             # try 32 bit kernel:
1208             if ip_addr is None:
1209                 ifaces = filter(None, [namestr[i:i+32].split('\0', 1)[0] for i in range(0, outbytes, 32)])
1210
1211                 for ifname in [iface for iface in ifaces if iface != 'lo']:
1212                     ip_addr = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, pack('256s', ifname[:15]))[20:24])
1213                     break
1214
1215         return ip_addr or 'localhost'
1216
1217     try:
1218         ip_addr = _detect_ip_addr()
1219     except:
1220         ip_addr = 'localhost'
1221     return ip_addr
1222
1223 # RATIONALE BEHIND TIMESTAMP CALCULATIONS AND TIMEZONE MANAGEMENT:
1224 #  The server side never does any timestamp calculation, always
1225 #  sends them in a naive (timezone agnostic) format supposed to be
1226 #  expressed within the server timezone, and expects the clients to
1227 #  provide timestamps in the server timezone as well.
1228 #  It stores all timestamps in the database in naive format as well,
1229 #  which also expresses the time in the server timezone.
1230 #  For this reason the server makes its timezone name available via the
1231 #  common/timezone_get() rpc method, which clients need to read
1232 #  to know the appropriate time offset to use when reading/writing
1233 #  times.
1234 def get_win32_timezone():
1235     """Attempt to return the "standard name" of the current timezone on a win32 system.
1236        @return: the standard name of the current win32 timezone, or False if it cannot be found.
1237     """
1238     res = False
1239     if (sys.platform == "win32"):
1240         try:
1241             import _winreg
1242             hklm = _winreg.ConnectRegistry(None,_winreg.HKEY_LOCAL_MACHINE)
1243             current_tz_key = _winreg.OpenKey(hklm, r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", 0,_winreg.KEY_ALL_ACCESS)
1244             res = str(_winreg.QueryValueEx(current_tz_key,"StandardName")[0])  # [0] is value, [1] is type code
1245             _winreg.CloseKey(current_tz_key)
1246             _winreg.CloseKey(hklm)
1247         except:
1248             pass
1249     return res
1250
1251 def detect_server_timezone():
1252     """Attempt to detect the timezone to use on the server side.
1253        Defaults to UTC if no working timezone can be found.
1254        @return: the timezone identifier as expected by pytz.timezone.
1255     """
1256     import time
1257     import netsvc
1258     try:
1259         import pytz
1260     except:
1261         netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_WARNING,
1262             "Python pytz module is not available. Timezone will be set to UTC by default.")
1263         return 'UTC'
1264
1265     # Option 1: the configuration option (did not exist before, so no backwards compatibility issue)
1266     # 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
1267     # Option 3: the environment variable TZ
1268     sources = [ (config['timezone'], 'OpenERP configuration'),
1269                 (time.tzname[0], 'time.tzname'),
1270                 (os.environ.get('TZ',False),'TZ environment variable'), ]
1271     # Option 4: OS-specific: /etc/timezone on Unix
1272     if (os.path.exists("/etc/timezone")):
1273         tz_value = False
1274         try:
1275             f = open("/etc/timezone")
1276             tz_value = f.read(128).strip()
1277         except:
1278             pass
1279         finally:
1280             f.close()
1281         sources.append((tz_value,"/etc/timezone file"))
1282     # Option 5: timezone info from registry on Win32
1283     if (sys.platform == "win32"):
1284         # Timezone info is stored in windows registry.
1285         # However this is not likely to work very well as the standard name
1286         # of timezones in windows is rarely something that is known to pytz.
1287         # But that's ok, it is always possible to use a config option to set
1288         # it explicitly.
1289         sources.append((get_win32_timezone(),"Windows Registry"))
1290
1291     for (value,source) in sources:
1292         if value:
1293             try:
1294                 tz = pytz.timezone(value)
1295                 netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_INFO,
1296                     "Using timezone %s obtained from %s." % (tz.zone,source))
1297                 return value
1298             except pytz.UnknownTimeZoneError:
1299                 netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_WARNING,
1300                     "The timezone specified in %s (%s) is invalid, ignoring it." % (source,value))
1301
1302     netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_WARNING,
1303         "No valid timezone could be detected, using default UTC timezone. You can specify it explicitly with option 'timezone' in the server configuration.")
1304     return 'UTC'
1305
1306
1307 def split_every(n, iterable, piece_maker=tuple):
1308     """Splits an iterable into length-n pieces. The last piece will be shorter
1309        if ``n`` does not evenly divide the iterable length.
1310        @param ``piece_maker``: function to build the pieces
1311        from the slices (tuple,list,...)
1312     """
1313     iterator = iter(iterable)
1314     piece = piece_maker(islice(iterator, n))
1315     while piece:
1316         yield piece
1317         piece = piece_maker(islice(iterator, n))
1318
1319 if __name__ == '__main__':
1320     import doctest
1321     doctest.testmod()
1322
1323 class upload_data_thread(threading.Thread):
1324     def __init__(self, email, data, type):
1325         self.args = [('email',email),('type',type),('data',data)]
1326         super(upload_data_thread,self).__init__()
1327     def run(self):
1328         try:
1329             import urllib
1330             args = urllib.urlencode(self.args)
1331             fp = urllib.urlopen('http://www.openerp.com/scripts/survey.php', args)
1332             fp.read()
1333             fp.close()
1334         except:
1335             pass
1336
1337 def upload_data(email, data, type='SURVEY'):
1338     a = upload_data_thread(email, data, type)
1339     a.start()
1340     return True
1341 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1342