Make the same changes in to the email_send like email_send_attach
[odoo/odoo.git] / bin / tools / misc.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 # Copyright (c) 2004-2008 Tiny SPRL (http://tiny.be) All Rights Reserved.
5 #
6 # $Id$
7 #
8 # WARNING: This program as such is intended to be used by professional
9 # programmers who take the whole responsability of assessing all potential
10 # consequences resulting from its eventual inadequacies and bugs
11 # End users who are looking for a ready-to-use solution with commercial
12 # garantees and support are strongly adviced to contract a Free Software
13 # Service Company
14 #
15 # This program is Free Software; you can redistribute it and/or
16 # modify it under the terms of the GNU General Public License
17 # as published by the Free Software Foundation; either version 2
18 # of the License, or (at your option) any later version.
19 #
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23 # GNU General Public License for more details.
24 #
25 # You should have received a copy of the GNU General Public License
26 # along with this program; if not, write to the Free Software
27 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
28 ###############################################################################
29
30 """
31 Miscelleanous tools used by tiny ERP.
32 """
33
34 import os, time, sys
35 import inspect
36
37 import psycopg
38 #import netsvc
39 from config import config
40 #import tools
41
42 import zipfile
43 import release
44 import socket
45
46 if sys.version_info[:2] < (2, 4):
47     from threadinglocal import local
48 else:
49     from threading import local
50
51 from itertools import izip
52
53 # initialize a database with base/base.sql 
54 def init_db(cr):
55     import addons
56     f = addons.get_module_resource('base', 'base.sql')
57     for line in file(f).read().split(';'):
58         if (len(line)>0) and (not line.isspace()):
59             cr.execute(line)
60     cr.commit()
61
62     for i in addons.get_modules():
63         terp_file = addons.get_module_resource(i, '__terp__.py')
64         mod_path = addons.get_module_path(i)
65         info = False
66         if os.path.isfile(terp_file) and not os.path.isfile(mod_path+'.zip'):
67             info = eval(file(terp_file).read())
68         elif zipfile.is_zipfile(mod_path+'.zip'):
69             zfile = zipfile.ZipFile(mod_path+'.zip')
70             i = os.path.splitext(i)[0]
71             info = eval(zfile.read(os.path.join(i, '__terp__.py')))
72         if info:
73             categs = info.get('category', 'Uncategorized').split('/')
74             p_id = None
75             while categs:
76                 if p_id is not None:
77                     cr.execute('select id \
78                             from ir_module_category \
79                             where name=%s and parent_id=%d', (categs[0], p_id))
80                 else:
81                     cr.execute('select id \
82                             from ir_module_category \
83                             where name=%s and parent_id is NULL', (categs[0],))
84                 c_id = cr.fetchone()
85                 if not c_id:
86                     cr.execute('select nextval(\'ir_module_category_id_seq\')')
87                     c_id = cr.fetchone()[0]
88                     cr.execute('insert into ir_module_category \
89                             (id, name, parent_id) \
90                             values (%d, %s, %d)', (c_id, categs[0], p_id))
91                 else:
92                     c_id = c_id[0]
93                 p_id = c_id
94                 categs = categs[1:]
95
96             active = info.get('active', False)
97             installable = info.get('installable', True)
98             if installable:
99                 if active:
100                     state = 'to install'
101                 else:
102                     state = 'uninstalled'
103             else:
104                 state = 'uninstallable'
105             cr.execute('select nextval(\'ir_module_module_id_seq\')')
106             id = cr.fetchone()[0]
107             cr.execute('insert into ir_module_module \
108                     (id, author, latest_version, website, name, shortdesc, description, \
109                         category_id, state) \
110                     values (%d, %s, %s, %s, %s, %s, %s, %d, %s)', (
111                 id, info.get('author', ''),
112                 release.version.rsplit('.', 1)[0] + '.' + info.get('version', ''),
113                 info.get('website', ''), i, info.get('name', False),
114                 info.get('description', ''), p_id, state))
115             dependencies = info.get('depends', [])
116             for d in dependencies:
117                 cr.execute('insert into ir_module_module_dependency \
118                         (module_id,name) values (%s, %s)', (id, d))
119             cr.commit()
120
121 def find_in_path(name):
122     if os.name == "nt":
123         sep = ';'
124     else:
125         sep = ':'
126     path = [dir for dir in os.environ['PATH'].split(sep)
127             if os.path.isdir(dir)]
128     for dir in path:
129         val = os.path.join(dir, name)
130         if os.path.isfile(val) or os.path.islink(val):
131             return val
132     return None
133
134 def find_pg_tool(name):
135     if config['pg_path'] and config['pg_path'] != 'None':
136         return os.path.join(config['pg_path'], name)
137     else:
138         return find_in_path(name)
139
140 def exec_pg_command(name, *args):
141     prog = find_pg_tool(name)
142     if not prog:
143         raise Exception('Couldn\'t find %s' % name)
144     args2 = (os.path.basename(prog),) + args
145     return os.spawnv(os.P_WAIT, prog, args2)
146
147 def exec_pg_command_pipe(name, *args):
148     prog = find_pg_tool(name)
149     if not prog:
150         raise Exception('Couldn\'t find %s' % name)
151     if os.name == "nt":
152         cmd = '"' + prog + '" ' + ' '.join(args)
153     else:
154         cmd = prog + ' ' + ' '.join(args)
155     return os.popen2(cmd, 'b')
156
157 def exec_command_pipe(name, *args):
158     prog = find_in_path(name)
159     if not prog:
160         raise Exception('Couldn\'t find %s' % name)
161     if os.name == "nt":
162         cmd = '"'+prog+'" '+' '.join(args)
163     else:
164         cmd = prog+' '+' '.join(args)
165     return os.popen2(cmd, 'b')
166
167 #----------------------------------------------------------
168 # File paths
169 #----------------------------------------------------------
170 #file_path_root = os.getcwd()
171 #file_path_addons = os.path.join(file_path_root, 'addons')
172
173 def file_open(name, mode="r", subdir='addons', pathinfo=False):
174     """Open a file from the Tiny ERP root, using a subdir folder.
175
176     >>> file_open('hr/report/timesheer.xsl')
177     >>> file_open('addons/hr/report/timesheet.xsl')
178     >>> file_open('../../base/report/rml_template.xsl', subdir='addons/hr/report', pathinfo=True)
179
180     @param name: name of the file
181     @param mode: file open mode
182     @param subdir: subdirectory
183     @param pathinfo: if True returns tupple (fileobject, filepath)
184
185     @return: fileobject if pathinfo is False else (fileobject, filepath)
186     """
187
188     adp = os.path.normcase(os.path.abspath(config['addons_path']))
189     rtp = os.path.normcase(os.path.abspath(config['root_path']))
190
191     if name.replace(os.path.sep, '/').startswith('addons/'):
192         subdir = 'addons'
193         name = name[7:]
194
195     # First try to locate in addons_path
196     if subdir:
197         subdir2 = subdir
198         if subdir2.replace(os.path.sep, '/').startswith('addons/'):
199             subdir2 = subdir2[7:]
200
201         subdir2 = (subdir2 != 'addons' or None) and subdir2
202
203         try:
204             if subdir2:
205                 fn = os.path.join(adp, subdir2, name)
206             else:
207                 fn = os.path.join(adp, name)
208             fn = os.path.normpath(fn)
209             fo = file_open(fn, mode=mode, subdir=None, pathinfo=pathinfo)
210             if pathinfo:
211                 return fo, fn
212             return fo
213         except IOError, e:
214             pass
215
216     if subdir:
217         name = os.path.join(rtp, subdir, name)
218     else:
219         name = os.path.join(rtp, name)
220
221     name = os.path.normpath(name)
222
223     # Check for a zipfile in the path
224     head = name
225     zipname = False
226     name2 = False
227     while True:
228         head, tail = os.path.split(head)
229         if not tail:
230             break
231         if zipname:
232             zipname = os.path.join(tail, zipname)
233         else:
234             zipname = tail
235         if zipfile.is_zipfile(head+'.zip'):
236             import StringIO
237             zfile = zipfile.ZipFile(head+'.zip')
238             try:
239                 fo = StringIO.StringIO(zfile.read(os.path.join(
240                     os.path.basename(head), zipname).replace(
241                         os.sep, '/')))
242
243                 if pathinfo:
244                     return fo, name
245                 return fo
246             except:
247                 name2 = os.path.normpath(os.path.join(head + '.zip', zipname))
248                 pass
249     for i in (name2, name):
250         if i and os.path.isfile(i):
251             fo = file(i, mode)
252             if pathinfo:
253                 return fo, i
254             return fo
255
256     raise IOError, 'File not found : '+str(name)
257
258
259 #----------------------------------------------------------
260 # iterables
261 #----------------------------------------------------------
262 def flatten(list):
263     """Flatten a list of elements into a uniqu list
264     Author: Christophe Simonis (christophe@tinyerp.com)
265     
266     Examples:
267     >>> flatten(['a'])
268     ['a']
269     >>> flatten('b')
270     ['b']
271     >>> flatten( [] )
272     []
273     >>> flatten( [[], [[]]] )
274     []
275     >>> flatten( [[['a','b'], 'c'], 'd', ['e', [], 'f']] )
276     ['a', 'b', 'c', 'd', 'e', 'f']
277     >>> t = (1,2,(3,), [4, 5, [6, [7], (8, 9), ([10, 11, (12, 13)]), [14, [], (15,)], []]])
278     >>> flatten(t)
279     [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
280     """
281     
282     def isiterable(x):
283         return hasattr(x, "__iter__")
284
285     r = []
286     for e in list:
287         if isiterable(e):
288             map(r.append, flatten(e))
289         else:
290             r.append(e)
291     return r
292
293 def reverse_enumerate(l):
294     """Like enumerate but in the other sens
295     >>> a = ['a', 'b', 'c']
296     >>> it = reverse_enumerate(a)
297     >>> it.next()
298     (2, 'c')
299     >>> it.next()
300     (1, 'b')
301     >>> it.next()
302     (0, 'a')
303     >>> it.next()
304     Traceback (most recent call last):
305       File "<stdin>", line 1, in <module>
306     StopIteration
307     """
308     return izip(xrange(len(l)-1, -1, -1), reversed(l))
309
310 #----------------------------------------------------------
311 # Emails
312 #----------------------------------------------------------
313 def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, on_error=False, reply_to=False, tinycrm=False):
314     """Send an email."""
315     if not email_cc:
316         email_cc=[]
317     if not email_bcc:
318         email_bcc=[]
319     import smtplib
320     from email.MIMEText import MIMEText
321     from email.MIMEMultipart import MIMEMultipart
322     from email.Header import Header
323     from email.Utils import formatdate, COMMASPACE
324
325     msg = MIMEText(body or '', _charset='utf-8')
326     msg['Subject'] = Header(subject.decode('utf8'), 'utf-8')
327     msg['From'] = email_from
328     del msg['Reply-To']
329     if reply_to:
330         msg['Reply-To'] = msg['From']+', '+reply_to
331     msg['To'] = COMMASPACE.join(email_to)
332     if email_cc:
333         msg['Cc'] = COMMASPACE.join(email_cc)
334     if email_bcc:
335         msg['Bcc'] = COMMASPACE.join(email_bcc)
336     msg['Date'] = formatdate(localtime=True)
337     if tinycrm:
338         msg['Message-Id'] = '<'+str(time.time())+'-tinycrm-'+str(tinycrm)+'@'+socket.gethostname()+'>'
339     try:
340         s = smtplib.SMTP()
341     
342         if debug:
343             s.debuglevel = 5        
344         if ssl:
345             s.ehlo()
346             s.starttls()
347             s.ehlo()
348       
349         s.connect(config['smtp_server'], config['smtp_port'])
350         if config['smtp_user'] or config['smtp_password']:
351             s.login(config['smtp_user'], config['smtp_password'])
352         s.sendmail(email_from, flatten([email_to, email_cc, email_bcc]), msg.as_string())
353         s.quit()
354     except Exception, e:
355         import logging
356         logging.getLogger().error(str(e))
357     return True
358
359
360 #----------------------------------------------------------
361 # Emails
362 #----------------------------------------------------------
363 def email_send_attach(email_from, email_to, subject, body, email_cc=None, email_bcc=None, on_error=False, reply_to=False, attach=None, tinycrm=False, ssl=False, debug=False):
364     """Send an email."""
365     if not email_cc:
366         email_cc=[]
367     if not email_bcc:
368         email_bcc=[]
369     if not attach:
370         attach=[]
371     import smtplib
372     from email.MIMEText import MIMEText
373     from email.MIMEBase import MIMEBase
374     from email.MIMEMultipart import MIMEMultipart
375     from email.Header import Header
376     from email.Utils import formatdate, COMMASPACE
377     from email import Encoders
378
379     msg = MIMEMultipart()
380     
381     if not ssl:
382         ssl = config['smtp_ssl']
383         
384     msg['Subject'] = Header(subject.decode('utf8'), 'utf-8')
385     msg['From'] = email_from
386     del msg['Reply-To']
387     if reply_to:
388         msg['Reply-To'] = reply_to
389     msg['To'] = COMMASPACE.join(email_to)
390     if email_cc:
391         msg['Cc'] = COMMASPACE.join(email_cc)
392     if email_bcc:
393         msg['Bcc'] = COMMASPACE.join(email_bcc)
394     if tinycrm:
395         msg['Message-Id'] = '<'+str(time.time())+'-tinycrm-'+str(tinycrm)+'@'+socket.gethostname()+'>'
396     msg['Date'] = formatdate(localtime=True)
397     msg.attach( MIMEText(body or '', _charset='utf-8') )
398     for (fname,fcontent) in attach:
399         part = MIMEBase('application', "octet-stream")
400         part.set_payload( fcontent )
401         Encoders.encode_base64(part)
402         part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,))
403         msg.attach(part)
404     try:
405         s = smtplib.SMTP()
406         
407         if debug:
408             s.debuglevel = 5            
409         if ssl:
410             s.ehlo()
411             s.starttls()
412             s.ehlo()
413       
414         s.connect(config['smtp_server'], config['smtp_port'])
415         if config['smtp_user'] or config['smtp_password']:
416             s.login(config['smtp_user'], config['smtp_password'])
417         s.sendmail(email_from, flatten([email_to, email_cc, email_bcc]), msg.as_string())
418         s.quit()
419     except Exception, e:
420         import logging
421         logging.getLogger().error(str(e))
422         return False
423
424     return True
425
426 #----------------------------------------------------------
427 # SMS
428 #----------------------------------------------------------
429 # text must be latin-1 encoded
430 def sms_send(user, password, api_id, text, to):
431     import urllib
432     params = urllib.urlencode({'user': user, 'password': password, 'api_id': api_id, 'text': text, 'to':to})
433     #f = urllib.urlopen("http://api.clickatell.com/http/sendmsg", params)
434     f = urllib.urlopen("http://196.7.150.220/http/sendmsg", params)
435     print f.read()
436     return True
437
438 #---------------------------------------------------------
439 # Class that stores an updateable string (used in wizards)
440 #---------------------------------------------------------
441 class UpdateableStr(local):
442
443     def __init__(self, string=''):
444         self.string = string
445     
446     def __str__(self):
447         return str(self.string)
448
449     def __repr__(self):
450         return str(self.string)
451
452     def __nonzero__(self):
453         return bool(self.string)
454
455
456 class UpdateableDict(local):
457     '''Stores an updateable dict to use in wizards'''
458
459     def __init__(self, dict=None):
460         if dict is None:
461             dict = {}
462         self.dict = dict
463
464     def __str__(self):
465         return str(self.dict)
466
467     def __repr__(self):
468         return str(self.dict)
469
470     def clear(self):
471         return self.dict.clear()
472
473     def keys(self):
474         return self.dict.keys()
475
476     def __setitem__(self, i, y):
477         self.dict.__setitem__(i, y)
478
479     def __getitem__(self, i):
480         return self.dict.__getitem__(i)
481
482     def copy(self):
483         return self.dict.copy()
484
485     def iteritems(self):
486         return self.dict.iteritems()
487
488     def iterkeys(self):
489         return self.dict.iterkeys()
490
491     def itervalues(self):
492         return self.dict.itervalues()
493
494     def pop(self, k, d=None):
495         return self.dict.pop(k, d)
496
497     def popitem(self):
498         return self.dict.popitem()
499
500     def setdefault(self, k, d=None):
501         return self.dict.setdefault(k, d)
502
503     def update(self, E, **F):
504         return self.dict.update(E, F)
505
506     def values(self):
507         return self.dict.values()
508
509     def get(self, k, d=None):
510         return self.dict.get(k, d)
511
512     def has_key(self, k):
513         return self.dict.has_key(k)
514
515     def items(self):
516         return self.dict.items()
517
518     def __cmp__(self, y):
519         return self.dict.__cmp__(y)
520
521     def __contains__(self, k):
522         return self.dict.__contains__(k)
523
524     def __delitem__(self, y):
525         return self.dict.__delitem__(y)
526
527     def __eq__(self, y):
528         return self.dict.__eq__(y)
529
530     def __ge__(self, y):
531         return self.dict.__ge__(y)
532
533     def __getitem__(self, y):
534         return self.dict.__getitem__(y)
535
536     def __gt__(self, y):
537         return self.dict.__gt__(y)
538
539     def __hash__(self):
540         return self.dict.__hash__()
541
542     def __iter__(self):
543         return self.dict.__iter__()
544
545     def __le__(self, y):
546         return self.dict.__le__(y)
547
548     def __len__(self):
549         return self.dict.__len__()
550
551     def __lt__(self, y):
552         return self.dict.__lt__(y)
553
554     def __ne__(self, y):
555         return self.dict.__ne__(y)
556
557
558 # Don't use ! Use res.currency.round()
559 class currency(float):
560
561     def __init__(self, value, accuracy=2, rounding=None):
562         if rounding is None:
563             rounding=10**-accuracy
564         self.rounding=rounding
565         self.accuracy=accuracy
566
567     def __new__(cls, value, accuracy=2, rounding=None):
568         return float.__new__(cls, round(value, accuracy))
569
570     #def __str__(self):
571     #   display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
572     #   return str(display_value)
573
574
575 #
576 # Use it as a decorator of the function you plan to cache
577 # Timeout: 0 = no timeout, otherwise in seconds
578 #
579 class cache(object):
580     def __init__(self, timeout=10000, skiparg=2):
581         self.timeout = timeout
582         self.cache = {}
583
584     def __call__(self, fn):
585         arg_names = inspect.getargspec(fn)[0][2:]
586         def cached_result(self2, cr=None, *args, **kwargs):
587             if cr is None:
588                 self.cache = {}
589                 return True
590
591             # Update named arguments with positional argument values
592             kwargs.update(dict(zip(arg_names, args)))
593             kwargs = kwargs.items()
594             kwargs.sort()
595             
596             # Work out key as a tuple of ('argname', value) pairs
597             key = (('dbname', cr.dbname),) + tuple(kwargs)
598
599             # Check cache and return cached value if possible
600             if key in self.cache:
601                 (value, last_time) = self.cache[key]
602                 mintime = time.time() - self.timeout
603                 if self.timeout <= 0 or mintime <= last_time:
604                     return value
605
606             # Work out new value, cache it and return it
607             # Should copy() this value to avoid futur modf of the cacle ?
608             result = fn(self2,cr,**dict(kwargs))
609
610             self.cache[key] = (result, time.time())
611             return result
612         return cached_result
613
614 def to_xml(s):
615     return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;')
616
617 def get_languages():
618     languages={
619         'zh_CN': 'Chinese (CN)',
620         'zh_TW': 'Chinese (TW)',
621         'cs_CZ': 'Czech',
622         'de_DE': 'Deutsch',
623         'es_AR': 'Español (Argentina)',
624         'es_ES': 'Español (España)',
625         'fr_FR': 'Français',
626         'fr_CH': 'Français (Suisse)',
627         'en_EN': 'English (default)',
628         'hu_HU': 'Hungarian',
629         'it_IT': 'Italiano',
630         'pt_BR': 'Portugese (Brasil)',
631         'pt_PT': 'Portugese (Portugal)',
632         'nl_NL': 'Nederlands',
633         'ro_RO': 'Romanian',
634         'ru_RU': 'Russian',
635         'sv_SE': 'Swedish',
636     }
637     return languages
638
639 def scan_languages():
640     import glob
641     file_list = [os.path.splitext(os.path.basename(f))[0] for f in glob.glob(os.path.join(config['root_path'], 'i18n', '*.csv'))]
642     lang_dict = get_languages()
643     return [(lang, lang_dict.get(lang, lang)) for lang in file_list]
644
645
646 def get_user_companies(cr, user):
647     def _get_company_children(cr, ids):
648         if not ids:
649             return []
650         cr.execute('SELECT id FROM res_company WHERE parent_id = any(array[%s])' %(','.join([str(x) for x in ids]),))
651         res=[x[0] for x in cr.fetchall()]
652         res.extend(_get_company_children(cr, res))
653         return res
654     cr.execute('SELECT comp.id FROM res_company AS comp, res_users AS u WHERE u.id = %d AND comp.id = u.company_id' % (user,))
655     compids=[cr.fetchone()[0]]
656     compids.extend(_get_company_children(cr, compids))
657     return compids
658
659 def mod10r(number):
660     """
661     Input number : account or invoice number
662     Output return: the same number completed with the recursive mod10
663     key
664     """
665     codec=[0,9,4,6,8,2,7,1,3,5]
666     report = 0
667     result=""
668     for digit in number:
669         result += digit
670         if digit.isdigit():
671             report = codec[ (int(digit) + report) % 10 ]
672     return result + str((10 - report) % 10)
673
674
675
676
677 if __name__ == '__main__':
678     import doctest
679     doctest.testmod()
680
681
682
683 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
684