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