fix tools
[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 OpenERP.
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 OpenERP 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 def oswalksymlinks(top, topdown=True, onerror=None):
260     """
261     same as os.walk but follow symlinks
262     attention: all symlinks are walked before all normals directories
263     """
264     for dirpath, dirnames, filenames in os.walk(top, topdown, onerror):
265         if topdown:
266             yield dirpath, dirnames, filenames
267
268         symlinks = filter(lambda dirname: os.path.islink(os.path.join(dirpath, dirname)), dirnames)
269         for s in symlinks:
270             for x in oswalksymlinks(os.path.join(dirpath, s), topdown, onerror):
271                 yield x
272                 
273         if not topdown:
274             yield dirpath, dirnames, filenames
275
276 #----------------------------------------------------------
277 # iterables
278 #----------------------------------------------------------
279 def flatten(list):
280     """Flatten a list of elements into a uniqu list
281     Author: Christophe Simonis (christophe@tinyerp.com)
282     
283     Examples:
284     >>> flatten(['a'])
285     ['a']
286     >>> flatten('b')
287     ['b']
288     >>> flatten( [] )
289     []
290     >>> flatten( [[], [[]]] )
291     []
292     >>> flatten( [[['a','b'], 'c'], 'd', ['e', [], 'f']] )
293     ['a', 'b', 'c', 'd', 'e', 'f']
294     >>> t = (1,2,(3,), [4, 5, [6, [7], (8, 9), ([10, 11, (12, 13)]), [14, [], (15,)], []]])
295     >>> flatten(t)
296     [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
297     """
298     
299     def isiterable(x):
300         return hasattr(x, "__iter__")
301
302     r = []
303     for e in list:
304         if isiterable(e):
305             map(r.append, flatten(e))
306         else:
307             r.append(e)
308     return r
309
310 def reverse_enumerate(l):
311     """Like enumerate but in the other sens
312     >>> a = ['a', 'b', 'c']
313     >>> it = reverse_enumerate(a)
314     >>> it.next()
315     (2, 'c')
316     >>> it.next()
317     (1, 'b')
318     >>> it.next()
319     (0, 'a')
320     >>> it.next()
321     Traceback (most recent call last):
322       File "<stdin>", line 1, in <module>
323     StopIteration
324     """
325     return izip(xrange(len(l)-1, -1, -1), reversed(l))
326
327 #----------------------------------------------------------
328 # Emails
329 #----------------------------------------------------------
330 def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, on_error=False, reply_to=False, tinycrm=False):
331     """Send an email."""
332     if not email_cc:
333         email_cc=[]
334     if not email_bcc:
335         email_bcc=[]
336     import smtplib
337     from email.MIMEText import MIMEText
338     from email.MIMEMultipart import MIMEMultipart
339     from email.Header import Header
340     from email.Utils import formatdate, COMMASPACE
341
342     msg = MIMEText(body or '', _charset='utf-8')
343     msg['Subject'] = Header(subject.decode('utf8'), 'utf-8')
344     msg['From'] = email_from
345     del msg['Reply-To']
346     if reply_to:
347         msg['Reply-To'] = msg['From']+', '+reply_to
348     msg['To'] = COMMASPACE.join(email_to)
349     if email_cc:
350         msg['Cc'] = COMMASPACE.join(email_cc)
351     if email_bcc:
352         msg['Bcc'] = COMMASPACE.join(email_bcc)
353     msg['Date'] = formatdate(localtime=True)
354     if tinycrm:
355         msg['Message-Id'] = '<'+str(time.time())+'-tinycrm-'+str(tinycrm)+'@'+socket.gethostname()+'>'
356     try:
357         s = smtplib.SMTP()
358     
359         if debug:
360             s.debuglevel = 5        
361         if ssl:
362             s.ehlo()
363             s.starttls()
364             s.ehlo()
365       
366         s.connect(config['smtp_server'], config['smtp_port'])
367         if config['smtp_user'] or config['smtp_password']:
368             s.login(config['smtp_user'], config['smtp_password'])
369         s.sendmail(email_from, flatten([email_to, email_cc, email_bcc]), msg.as_string())
370         s.quit()
371     except Exception, e:
372         import logging
373         logging.getLogger().error(str(e))
374     return True
375
376
377 #----------------------------------------------------------
378 # Emails
379 #----------------------------------------------------------
380 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):
381     """Send an email."""
382     if not email_cc:
383         email_cc=[]
384     if not email_bcc:
385         email_bcc=[]
386     if not attach:
387         attach=[]
388     import smtplib
389     from email.MIMEText import MIMEText
390     from email.MIMEBase import MIMEBase
391     from email.MIMEMultipart import MIMEMultipart
392     from email.Header import Header
393     from email.Utils import formatdate, COMMASPACE
394     from email import Encoders
395
396     msg = MIMEMultipart()
397     
398     if not ssl:
399         ssl = config['smtp_ssl']
400         
401     msg['Subject'] = Header(subject.decode('utf8'), 'utf-8')
402     msg['From'] = email_from
403     del msg['Reply-To']
404     if reply_to:
405         msg['Reply-To'] = reply_to
406     msg['To'] = COMMASPACE.join(email_to)
407     if email_cc:
408         msg['Cc'] = COMMASPACE.join(email_cc)
409     if email_bcc:
410         msg['Bcc'] = COMMASPACE.join(email_bcc)
411     if tinycrm:
412         msg['Message-Id'] = '<'+str(time.time())+'-tinycrm-'+str(tinycrm)+'@'+socket.gethostname()+'>'
413     msg['Date'] = formatdate(localtime=True)
414     msg.attach( MIMEText(body or '', _charset='utf-8', _subtype="html"))
415     for (fname,fcontent) in attach:
416         part = MIMEBase('application', "octet-stream")
417         part.set_payload( fcontent )
418         Encoders.encode_base64(part)
419         part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,))
420         msg.attach(part)
421     try:
422         s = smtplib.SMTP()
423         
424         if debug:
425             s.debuglevel = 5            
426         if ssl:
427             s.ehlo()
428             s.starttls()
429             s.ehlo()
430       
431         s.connect(config['smtp_server'], config['smtp_port'])
432         if config['smtp_user'] or config['smtp_password']:
433             s.login(config['smtp_user'], config['smtp_password'])
434         s.sendmail(email_from, flatten([email_to, email_cc, email_bcc]), msg.as_string())
435         s.quit()
436     except Exception, e:
437         import logging
438         logging.getLogger().error(str(e))
439         return False
440
441     return True
442
443 #----------------------------------------------------------
444 # SMS
445 #----------------------------------------------------------
446 # text must be latin-1 encoded
447 def sms_send(user, password, api_id, text, to):
448     import urllib
449     params = urllib.urlencode({'user': user, 'password': password, 'api_id': api_id, 'text': text, 'to':to})
450     #f = urllib.urlopen("http://api.clickatell.com/http/sendmsg", params)
451     f = urllib.urlopen("http://196.7.150.220/http/sendmsg", params)
452     print f.read()
453     return True
454
455 #---------------------------------------------------------
456 # Class that stores an updateable string (used in wizards)
457 #---------------------------------------------------------
458 class UpdateableStr(local):
459
460     def __init__(self, string=''):
461         self.string = string
462     
463     def __str__(self):
464         return str(self.string)
465
466     def __repr__(self):
467         return str(self.string)
468
469     def __nonzero__(self):
470         return bool(self.string)
471
472
473 class UpdateableDict(local):
474     '''Stores an updateable dict to use in wizards'''
475
476     def __init__(self, dict=None):
477         if dict is None:
478             dict = {}
479         self.dict = dict
480
481     def __str__(self):
482         return str(self.dict)
483
484     def __repr__(self):
485         return str(self.dict)
486
487     def clear(self):
488         return self.dict.clear()
489
490     def keys(self):
491         return self.dict.keys()
492
493     def __setitem__(self, i, y):
494         self.dict.__setitem__(i, y)
495
496     def __getitem__(self, i):
497         return self.dict.__getitem__(i)
498
499     def copy(self):
500         return self.dict.copy()
501
502     def iteritems(self):
503         return self.dict.iteritems()
504
505     def iterkeys(self):
506         return self.dict.iterkeys()
507
508     def itervalues(self):
509         return self.dict.itervalues()
510
511     def pop(self, k, d=None):
512         return self.dict.pop(k, d)
513
514     def popitem(self):
515         return self.dict.popitem()
516
517     def setdefault(self, k, d=None):
518         return self.dict.setdefault(k, d)
519
520     def update(self, E, **F):
521         return self.dict.update(E, F)
522
523     def values(self):
524         return self.dict.values()
525
526     def get(self, k, d=None):
527         return self.dict.get(k, d)
528
529     def has_key(self, k):
530         return self.dict.has_key(k)
531
532     def items(self):
533         return self.dict.items()
534
535     def __cmp__(self, y):
536         return self.dict.__cmp__(y)
537
538     def __contains__(self, k):
539         return self.dict.__contains__(k)
540
541     def __delitem__(self, y):
542         return self.dict.__delitem__(y)
543
544     def __eq__(self, y):
545         return self.dict.__eq__(y)
546
547     def __ge__(self, y):
548         return self.dict.__ge__(y)
549
550     def __getitem__(self, y):
551         return self.dict.__getitem__(y)
552
553     def __gt__(self, y):
554         return self.dict.__gt__(y)
555
556     def __hash__(self):
557         return self.dict.__hash__()
558
559     def __iter__(self):
560         return self.dict.__iter__()
561
562     def __le__(self, y):
563         return self.dict.__le__(y)
564
565     def __len__(self):
566         return self.dict.__len__()
567
568     def __lt__(self, y):
569         return self.dict.__lt__(y)
570
571     def __ne__(self, y):
572         return self.dict.__ne__(y)
573
574
575 # Don't use ! Use res.currency.round()
576 class currency(float):
577
578     def __init__(self, value, accuracy=2, rounding=None):
579         if rounding is None:
580             rounding=10**-accuracy
581         self.rounding=rounding
582         self.accuracy=accuracy
583
584     def __new__(cls, value, accuracy=2, rounding=None):
585         return float.__new__(cls, round(value, accuracy))
586
587     #def __str__(self):
588     #   display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
589     #   return str(display_value)
590
591
592 #
593 # Use it as a decorator of the function you plan to cache
594 # Timeout: 0 = no timeout, otherwise in seconds
595 #
596 class cache(object):
597     def __init__(self, timeout=10000, skiparg=2):
598         self.timeout = timeout
599         self.cache = {}
600
601     def __call__(self, fn):
602         arg_names = inspect.getargspec(fn)[0][2:]
603         def cached_result(self2, cr=None, *args, **kwargs):
604             if cr is None:
605                 self.cache = {}
606                 return True
607
608             # Update named arguments with positional argument values
609             kwargs.update(dict(zip(arg_names, args)))
610             kwargs = kwargs.items()
611             kwargs.sort()
612             
613             # Work out key as a tuple of ('argname', value) pairs
614             key = (('dbname', cr.dbname),) + tuple(kwargs)
615
616             # Check cache and return cached value if possible
617             if key in self.cache:
618                 (value, last_time) = self.cache[key]
619                 mintime = time.time() - self.timeout
620                 if self.timeout <= 0 or mintime <= last_time:
621                     return value
622
623             # Work out new value, cache it and return it
624             # Should copy() this value to avoid futur modf of the cacle ?
625             result = fn(self2,cr,**dict(kwargs))
626
627             self.cache[key] = (result, time.time())
628             return result
629         return cached_result
630
631 def to_xml(s):
632     return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;')
633
634 def get_languages():
635     languages={
636         'zh_CN': 'Chinese (CN)',
637         'zh_TW': 'Chinese (TW)',
638         'cs_CZ': 'Czech',
639         'de_DE': 'Deutsch',
640         'es_AR': 'Español (Argentina)',
641         'es_ES': 'Español (España)',
642         'fr_FR': 'Français',
643         'fr_CH': 'Français (Suisse)',
644         'en_EN': 'English (default)',
645         'hu_HU': 'Hungarian',
646         'it_IT': 'Italiano',
647         'pt_BR': 'Portugese (Brasil)',
648         'pt_PT': 'Portugese (Portugal)',
649         'nl_NL': 'Nederlands',
650         'ro_RO': 'Romanian',
651         'ru_RU': 'Russian',
652         'sv_SE': 'Swedish',
653     }
654     return languages
655
656 def scan_languages():
657     import glob
658     file_list = [os.path.splitext(os.path.basename(f))[0] for f in glob.glob(os.path.join(config['addons_path'], 'base', 'i18n', '*.po'))]
659     lang_dict = get_languages()
660     return [(lang, lang_dict.get(lang, lang)) for lang in file_list]
661
662
663 def get_user_companies(cr, user):
664     def _get_company_children(cr, ids):
665         if not ids:
666             return []
667         cr.execute('SELECT id FROM res_company WHERE parent_id = any(array[%s])' %(','.join([str(x) for x in ids]),))
668         res=[x[0] for x in cr.fetchall()]
669         res.extend(_get_company_children(cr, res))
670         return res
671     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,))
672     compids=[cr.fetchone()[0]]
673     compids.extend(_get_company_children(cr, compids))
674     return compids
675
676 def mod10r(number):
677     """
678     Input number : account or invoice number
679     Output return: the same number completed with the recursive mod10
680     key
681     """
682     codec=[0,9,4,6,8,2,7,1,3,5]
683     report = 0
684     result=""
685     for digit in number:
686         result += digit
687         if digit.isdigit():
688             report = codec[ (int(digit) + report) % 10 ]
689     return result + str((10 - report) % 10)
690
691
692 def human_size(sz):
693     """
694     Return the size in a human readable format
695     """
696     if not sz:
697         return False
698     units = ('bytes', 'Kb', 'Mb', 'Gb')
699     s, i = float(sz), 0
700     while s >= 1024 and i < len(units)-1:
701         s = s / 1024
702         i = i + 1
703     return "%0.2f %s" % (s, units[i])
704
705 def logged(when):
706     def log(f, res, *args, **kwargs):
707         vector = ['Call -> function: %s' % f]
708         for i, arg in enumerate(args):
709             vector.append( '  arg %02d: %r' % ( i, arg ) )
710         for key, value in kwargs.items():
711             vector.append( '  kwarg %10s: %r' % ( key, value ) )
712         vector.append( '  result: %r' % res )
713         print "\n".join(vector)
714
715     def pre_logged(f):
716         def wrapper(*args, **kwargs):
717             res = f(*args, **kwargs)
718             log(f, res, *args, **kwargs)
719             return res
720         return wrapper
721
722     def post_logged(f):
723         def wrapper(*args, **kwargs):
724             now = time.time()
725             res = None
726             try:
727                 res = f(*args, **kwargs)
728                 return res
729             finally:
730                 log(f, res, *args, **kwargs)
731                 print "  time delta: %s" % (time.time() - now)
732         return wrapper
733
734     try:
735         return { "pre" : pre_logged, "post" : post_logged}[when]
736     except KeyError, e:
737         raise ValueError(e), "must to be 'pre' or 'post'"
738
739 icons = map(lambda x: (x,x), ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
740 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
741 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
742 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
743 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
744 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
745 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
746 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
747 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
748 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
749 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
750 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
751 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
752 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
753 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
754 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
755 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
756 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
757 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
758 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
759 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
760 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
761 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
762 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
763 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
764 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
765 ])
766
767
768
769 if __name__ == '__main__':
770     import doctest
771     doctest.testmod()
772
773
774
775 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
776