[REF] refactor the debug decorator 'logged'
[odoo/odoo.git] / bin / tools / misc.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2008 Tiny SPRL (<http://tiny.be>). All Rights Reserved
6 #    $Id$
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU General Public License as published by
10 #    the Free Software Foundation, either version 3 of the License, or
11 #    (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 General Public License for more details.
17 #
18 #    You should have received a copy of the GNU 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
36 if sys.version_info[:2] < (2, 4):
37     from threadinglocal import local
38 else:
39     from threading import local
40
41 from itertools import izip
42
43 # initialize a database with base/base.sql
44 def init_db(cr):
45     import addons
46     f = addons.get_module_resource('base', 'base.sql')
47     for line in file(f).read().split(';'):
48         if (len(line)>0) and (not line.isspace()):
49             cr.execute(line)
50     cr.commit()
51
52     for i in addons.get_modules():
53         terp_file = addons.get_module_resource(i, '__terp__.py')
54         mod_path = addons.get_module_path(i)
55         if not mod_path:
56             continue
57         info = False
58         if os.path.isfile(terp_file) and not os.path.isfile(mod_path+'.zip'):
59             info = eval(file(terp_file).read())
60         elif zipfile.is_zipfile(mod_path+'.zip'):
61             zfile = zipfile.ZipFile(mod_path+'.zip')
62             i = os.path.splitext(i)[0]
63             info = eval(zfile.read(os.path.join(i, '__terp__.py')))
64         if info:
65             categs = info.get('category', 'Uncategorized').split('/')
66             p_id = None
67             while categs:
68                 if p_id is not None:
69                     cr.execute('select id \
70                             from ir_module_category \
71                             where name=%s and parent_id=%s', (categs[0], p_id))
72                 else:
73                     cr.execute('select id \
74                             from ir_module_category \
75                             where name=%s and parent_id is NULL', (categs[0],))
76                 c_id = cr.fetchone()
77                 if not c_id:
78                     cr.execute('select nextval(\'ir_module_category_id_seq\')')
79                     c_id = cr.fetchone()[0]
80                     cr.execute('insert into ir_module_category \
81                             (id, name, parent_id) \
82                             values (%s, %s, %s)', (c_id, categs[0], p_id))
83                 else:
84                     c_id = c_id[0]
85                 p_id = c_id
86                 categs = categs[1:]
87
88             active = info.get('active', False)
89             installable = info.get('installable', True)
90             if installable:
91                 if active:
92                     state = 'to install'
93                 else:
94                     state = 'uninstalled'
95             else:
96                 state = 'uninstallable'
97             cr.execute('select nextval(\'ir_module_module_id_seq\')')
98             id = cr.fetchone()[0]
99             cr.execute('insert into ir_module_module \
100                     (id, author, website, name, shortdesc, description, \
101                         category_id, state) \
102                     values (%s, %s, %s, %s, %s, %s, %s, %s)', (
103                 id, info.get('author', ''),
104                 info.get('website', ''), i, info.get('name', False),
105                 info.get('description', ''), p_id, state))
106             dependencies = info.get('depends', [])
107             for d in dependencies:
108                 cr.execute('insert into ir_module_module_dependency \
109                         (module_id,name) values (%s, %s)', (id, d))
110             cr.commit()
111
112 def find_in_path(name):
113     if os.name == "nt":
114         sep = ';'
115     else:
116         sep = ':'
117     path = [dir for dir in os.environ['PATH'].split(sep)
118             if os.path.isdir(dir)]
119     for dir in path:
120         val = os.path.join(dir, name)
121         if os.path.isfile(val) or os.path.islink(val):
122             return val
123     return None
124
125 def find_pg_tool(name):
126     if config['pg_path'] and config['pg_path'] != 'None':
127         return os.path.join(config['pg_path'], name)
128     else:
129         return find_in_path(name)
130
131 def exec_pg_command(name, *args):
132     prog = find_pg_tool(name)
133     if not prog:
134         raise Exception('Couldn\'t find %s' % name)
135     args2 = (os.path.basename(prog),) + args
136     return os.spawnv(os.P_WAIT, prog, args2)
137
138 def exec_pg_command_pipe(name, *args):
139     prog = find_pg_tool(name)
140     if not prog:
141         raise Exception('Couldn\'t find %s' % name)
142     if os.name == "nt":
143         cmd = '"' + prog + '" ' + ' '.join(args)
144     else:
145         cmd = prog + ' ' + ' '.join(args)
146     return os.popen2(cmd, 'b')
147
148 def exec_command_pipe(name, *args):
149     prog = find_in_path(name)
150     if not prog:
151         raise Exception('Couldn\'t find %s' % name)
152     if os.name == "nt":
153         cmd = '"'+prog+'" '+' '.join(args)
154     else:
155         cmd = prog+' '+' '.join(args)
156     return os.popen2(cmd, 'b')
157
158 #----------------------------------------------------------
159 # File paths
160 #----------------------------------------------------------
161 #file_path_root = os.getcwd()
162 #file_path_addons = os.path.join(file_path_root, 'addons')
163
164 def file_open(name, mode="r", subdir='addons', pathinfo=False):
165     """Open a file from the OpenERP root, using a subdir folder.
166
167     >>> file_open('hr/report/timesheer.xsl')
168     >>> file_open('addons/hr/report/timesheet.xsl')
169     >>> file_open('../../base/report/rml_template.xsl', subdir='addons/hr/report', pathinfo=True)
170
171     @param name: name of the file
172     @param mode: file open mode
173     @param subdir: subdirectory
174     @param pathinfo: if True returns tupple (fileobject, filepath)
175
176     @return: fileobject if pathinfo is False else (fileobject, filepath)
177     """
178
179     adp = os.path.normcase(os.path.abspath(config['addons_path']))
180     rtp = os.path.normcase(os.path.abspath(config['root_path']))
181
182     if name.replace(os.path.sep, '/').startswith('addons/'):
183         subdir = 'addons'
184         name = name[7:]
185
186     # First try to locate in addons_path
187     if subdir:
188         subdir2 = subdir
189         if subdir2.replace(os.path.sep, '/').startswith('addons/'):
190             subdir2 = subdir2[7:]
191
192         subdir2 = (subdir2 != 'addons' or None) and subdir2
193
194         try:
195             if subdir2:
196                 fn = os.path.join(adp, subdir2, name)
197             else:
198                 fn = os.path.join(adp, name)
199             fn = os.path.normpath(fn)
200             fo = file_open(fn, mode=mode, subdir=None, pathinfo=pathinfo)
201             if pathinfo:
202                 return fo, fn
203             return fo
204         except IOError, e:
205             pass
206
207     if subdir:
208         name = os.path.join(rtp, subdir, name)
209     else:
210         name = os.path.join(rtp, name)
211
212     name = os.path.normpath(name)
213
214     # Check for a zipfile in the path
215     head = name
216     zipname = False
217     name2 = False
218     while True:
219         head, tail = os.path.split(head)
220         if not tail:
221             break
222         if zipname:
223             zipname = os.path.join(tail, zipname)
224         else:
225             zipname = tail
226         if zipfile.is_zipfile(head+'.zip'):
227             import StringIO
228             zfile = zipfile.ZipFile(head+'.zip')
229             try:
230                 fo = StringIO.StringIO(zfile.read(os.path.join(
231                     os.path.basename(head), zipname).replace(
232                         os.sep, '/')))
233
234                 if pathinfo:
235                     return fo, name
236                 return fo
237             except:
238                 name2 = os.path.normpath(os.path.join(head + '.zip', zipname))
239                 pass
240     for i in (name2, name):
241         if i and os.path.isfile(i):
242             fo = file(i, mode)
243             if pathinfo:
244                 return fo, i
245             return fo
246     if os.path.splitext(name)[1] == '.rml':
247         raise IOError, 'Report %s doesn\'t exist or deleted : ' %str(name)
248     raise IOError, 'File not found : '+str(name)
249
250
251 #----------------------------------------------------------
252 # iterables
253 #----------------------------------------------------------
254 def flatten(list):
255     """Flatten a list of elements into a uniqu list
256     Author: Christophe Simonis (christophe@tinyerp.com)
257
258     Examples:
259     >>> flatten(['a'])
260     ['a']
261     >>> flatten('b')
262     ['b']
263     >>> flatten( [] )
264     []
265     >>> flatten( [[], [[]]] )
266     []
267     >>> flatten( [[['a','b'], 'c'], 'd', ['e', [], 'f']] )
268     ['a', 'b', 'c', 'd', 'e', 'f']
269     >>> t = (1,2,(3,), [4, 5, [6, [7], (8, 9), ([10, 11, (12, 13)]), [14, [], (15,)], []]])
270     >>> flatten(t)
271     [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
272     """
273
274     def isiterable(x):
275         return hasattr(x, "__iter__")
276
277     r = []
278     for e in list:
279         if isiterable(e):
280             map(r.append, flatten(e))
281         else:
282             r.append(e)
283     return r
284
285 def reverse_enumerate(l):
286     """Like enumerate but in the other sens
287     >>> a = ['a', 'b', 'c']
288     >>> it = reverse_enumerate(a)
289     >>> it.next()
290     (2, 'c')
291     >>> it.next()
292     (1, 'b')
293     >>> it.next()
294     (0, 'a')
295     >>> it.next()
296     Traceback (most recent call last):
297       File "<stdin>", line 1, in <module>
298     StopIteration
299     """
300     return izip(xrange(len(l)-1, -1, -1), reversed(l))
301
302 #----------------------------------------------------------
303 # Emails
304 #----------------------------------------------------------
305 def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False, attach=None, tinycrm=False, ssl=False, debug=False,subtype='plain'):
306     """Send an email."""
307     import smtplib
308     from email.MIMEText import MIMEText
309     from email.MIMEBase import MIMEBase
310     from email.MIMEMultipart import MIMEMultipart
311     from email.Header import Header
312     from email.Utils import formatdate, COMMASPACE
313     from email.Utils import formatdate, COMMASPACE
314     from email import Encoders
315
316     if not ssl:
317         ssl = config.get('smtp_ssl', False)
318
319     if not email_cc:
320         email_cc = []
321     if not email_bcc:
322         email_bcc = []
323
324     if not attach:
325         msg = MIMEText(body or '',_subtype=subtype,_charset='utf-8')
326     else:
327         msg = MIMEMultipart()
328
329     msg['Subject'] = Header(subject.decode('utf8'), 'utf-8')
330     msg['From'] = email_from
331     del msg['Reply-To']
332     if reply_to:
333         msg['Reply-To'] = msg['From']+', '+reply_to
334     msg['To'] = COMMASPACE.join(email_to)
335     if email_cc:
336         msg['Cc'] = COMMASPACE.join(email_cc)
337     if email_bcc:
338         msg['Bcc'] = COMMASPACE.join(email_bcc)
339     msg['Date'] = formatdate(localtime=True)
340
341     if tinycrm:
342         msg['Message-Id'] = "<%s-tinycrm-%s@%s>" % (time.time(), tinycrm, socket.gethostname())
343
344     if attach:
345         msg.attach( MIMEText(body or '', _charset='utf-8', _subtype=subtype) )
346
347         for (fname,fcontent) in attach:
348             part = MIMEBase('application', "octet-stream")
349             part.set_payload( fcontent )
350             Encoders.encode_base64(part)
351             part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,))
352             msg.attach(part)
353     try:
354         s = smtplib.SMTP()
355
356         if debug:
357             s.debuglevel = 5
358         s.connect(config['smtp_server'], config['smtp_port'])
359         if ssl:
360             s.ehlo()
361             s.starttls()
362             s.ehlo()
363
364         if config['smtp_user'] or config['smtp_password']:
365             s.login(config['smtp_user'], config['smtp_password'])
366
367         s.sendmail(email_from, 
368                    flatten([email_to, email_cc, email_bcc]), 
369                    msg.as_string()
370                   )
371         s.quit()
372     except Exception, e:
373         import logging
374         logging.getLogger().error(str(e))
375         return False
376     return True
377
378 #----------------------------------------------------------
379 # SMS
380 #----------------------------------------------------------
381 # text must be latin-1 encoded
382 def sms_send(user, password, api_id, text, to):
383     import urllib
384     params = urllib.urlencode({'user': user, 'password': password, 'api_id': api_id, 'text': text, 'to':to})
385     #f = urllib.urlopen("http://api.clickatell.com/http/sendmsg", params)
386     f = urllib.urlopen("http://196.7.150.220/http/sendmsg", params)
387     # FIXME: Use the logger if there is an error
388     print f.read()
389     return True
390
391 #---------------------------------------------------------
392 # Class that stores an updateable string (used in wizards)
393 #---------------------------------------------------------
394 class UpdateableStr(local):
395
396     def __init__(self, string=''):
397         self.string = string
398
399     def __str__(self):
400         return str(self.string)
401
402     def __repr__(self):
403         return str(self.string)
404
405     def __nonzero__(self):
406         return bool(self.string)
407
408
409 class UpdateableDict(local):
410     '''Stores an updateable dict to use in wizards'''
411
412     def __init__(self, dict=None):
413         if dict is None:
414             dict = {}
415         self.dict = dict
416
417     def __str__(self):
418         return str(self.dict)
419
420     def __repr__(self):
421         return str(self.dict)
422
423     def clear(self):
424         return self.dict.clear()
425
426     def keys(self):
427         return self.dict.keys()
428
429     def __setitem__(self, i, y):
430         self.dict.__setitem__(i, y)
431
432     def __getitem__(self, i):
433         return self.dict.__getitem__(i)
434
435     def copy(self):
436         return self.dict.copy()
437
438     def iteritems(self):
439         return self.dict.iteritems()
440
441     def iterkeys(self):
442         return self.dict.iterkeys()
443
444     def itervalues(self):
445         return self.dict.itervalues()
446
447     def pop(self, k, d=None):
448         return self.dict.pop(k, d)
449
450     def popitem(self):
451         return self.dict.popitem()
452
453     def setdefault(self, k, d=None):
454         return self.dict.setdefault(k, d)
455
456     def update(self, E, **F):
457         return self.dict.update(E, F)
458
459     def values(self):
460         return self.dict.values()
461
462     def get(self, k, d=None):
463         return self.dict.get(k, d)
464
465     def has_key(self, k):
466         return self.dict.has_key(k)
467
468     def items(self):
469         return self.dict.items()
470
471     def __cmp__(self, y):
472         return self.dict.__cmp__(y)
473
474     def __contains__(self, k):
475         return self.dict.__contains__(k)
476
477     def __delitem__(self, y):
478         return self.dict.__delitem__(y)
479
480     def __eq__(self, y):
481         return self.dict.__eq__(y)
482
483     def __ge__(self, y):
484         return self.dict.__ge__(y)
485
486     def __getitem__(self, y):
487         return self.dict.__getitem__(y)
488
489     def __gt__(self, y):
490         return self.dict.__gt__(y)
491
492     def __hash__(self):
493         return self.dict.__hash__()
494
495     def __iter__(self):
496         return self.dict.__iter__()
497
498     def __le__(self, y):
499         return self.dict.__le__(y)
500
501     def __len__(self):
502         return self.dict.__len__()
503
504     def __lt__(self, y):
505         return self.dict.__lt__(y)
506
507     def __ne__(self, y):
508         return self.dict.__ne__(y)
509
510
511 # Don't use ! Use res.currency.round()
512 class currency(float):
513
514     def __init__(self, value, accuracy=2, rounding=None):
515         if rounding is None:
516             rounding=10**-accuracy
517         self.rounding=rounding
518         self.accuracy=accuracy
519
520     def __new__(cls, value, accuracy=2, rounding=None):
521         return float.__new__(cls, round(value, accuracy))
522
523     #def __str__(self):
524     #   display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
525     #   return str(display_value)
526
527
528 def is_hashable(h):
529     try:
530         hash(h)
531         return True
532     except TypeError:
533         return False
534
535 #
536 # Use it as a decorator of the function you plan to cache
537 # Timeout: 0 = no timeout, otherwise in seconds
538 #
539 class cache(object):
540     def __init__(self, timeout=10000, skiparg=2):
541         self.timeout = timeout
542         self.cache = {}
543
544     def __call__(self, fn):
545         arg_names = inspect.getargspec(fn)[0][2:]
546         def cached_result(self2, cr=None, *args, **kwargs):
547             if cr is None:
548                 self.cache = {}
549                 return True
550
551             # Update named arguments with positional argument values
552             kwargs.update(dict(zip(arg_names, args)))
553             for k in kwargs:
554                 if isinstance(kwargs[k], (list, dict, set)):
555                     kwargs[k] = tuple(kwargs[k])
556                 elif not is_hashable(kwargs[k]):
557                     kwargs[k] = repr(kwargs[k])
558             kwargs = kwargs.items()
559             kwargs.sort()
560
561             # Work out key as a tuple of ('argname', value) pairs
562             key = (('dbname', cr.dbname),) + tuple(kwargs)
563
564             # Check cache and return cached value if possible
565             if key in self.cache:
566                 (value, last_time) = self.cache[key]
567                 mintime = time.time() - self.timeout
568                 if self.timeout <= 0 or mintime <= last_time:
569                     return value
570
571             # Work out new value, cache it and return it
572             # FIXME Should copy() this value to avoid futur modifications of the cache ?
573             # FIXME What about exceptions ?
574             result = fn(self2,cr,**dict(kwargs))
575
576             self.cache[key] = (result, time.time())
577             return result
578         return cached_result
579
580 def to_xml(s):
581     return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;')
582
583 def ustr(value):
584     """This method is similar to the builtin `str` method, except
585     it will return Unicode string.
586
587     @param value: the value to convert
588
589     @rtype: unicode
590     @return: unicode string
591     """
592
593     if isinstance(value, unicode):
594         return value
595
596     if hasattr(value, '__unicode__'):
597         return unicode(value)
598
599     if not isinstance(value, str):
600         value = str(value)
601
602     return unicode(value, 'utf-8')
603
604
605 def get_languages():
606     languages={
607         'bg_BG': u'Bulgarian / български',
608         'ca_ES': u'Catalan / Català',
609         'cs_CZ': u'Czech / Čeština',
610         'de_DE': u'German / Deutsch',
611         'en_CA': u'English (CA)',
612         'en_EN': u'English (default)',
613         'en_GB': u'English (UK)',
614         'en_US': u'English (US)',
615         'es_AR': u'Spanish (AR) / Español (AR)',
616         'es_ES': u'Spanish / Español',
617         'et_ET': u'Estonian / Eesti keel',
618         'fr_BE': u'French (BE) / Français (BE)',
619         'fr_CH': u'French (CH) / Français (CH)',
620         'fr_FR': u'French / Français',
621         'hr_HR': u'Croatian / hrvatski jezik',
622         'hu_HU': u'Hungarian / Magyar',
623         'it_IT': u'Italian / Italiano',
624         'lt_LT': u'Lithuanian / Lietuvių kalba',
625         'nl_NL': u'Dutch / Nederlands',
626         'pt_BR': u'Portugese (BR) / português (BR)',
627         'pt_PT': u'Portugese / português',
628         'ro_RO': u'Romanian / limba română',
629         'ru_RU': u'Russian / русский язык',
630         'sl_SL': u'Slovenian / slovenščina',
631         'sv_SE': u'Swedish / svenska',
632         'uk_UK': u'Ukrainian / украї́нська мо́ва',
633         'zh_CN': u'Chinese (CN) / 简体中文' ,
634         'zh_TW': u'Chinese (TW) / 正體字',
635     }
636     return languages
637
638 def scan_languages():
639     import glob
640     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'))]
641     lang_dict = get_languages()
642     ret = [(lang, lang_dict.get(lang, lang)) for lang in file_list]
643     ret.sort(key=lambda k:k[1])
644     return ret
645
646
647 def get_user_companies(cr, user):
648     def _get_company_children(cr, ids):
649         if not ids:
650             return []
651         cr.execute('SELECT id FROM res_company WHERE parent_id = any(array[%s])' %(','.join([str(x) for x in ids]),))
652         res=[x[0] for x in cr.fetchall()]
653         res.extend(_get_company_children(cr, res))
654         return res
655     cr.execute('SELECT comp.id FROM res_company AS comp, res_users AS u WHERE u.id = %s AND comp.id = u.company_id' % (user,))
656     compids=[cr.fetchone()[0]]
657     compids.extend(_get_company_children(cr, compids))
658     return compids
659
660 def mod10r(number):
661     """
662     Input number : account or invoice number
663     Output return: the same number completed with the recursive mod10
664     key
665     """
666     codec=[0,9,4,6,8,2,7,1,3,5]
667     report = 0
668     result=""
669     for digit in number:
670         result += digit
671         if digit.isdigit():
672             report = codec[ (int(digit) + report) % 10 ]
673     return result + str((10 - report) % 10)
674
675
676 def human_size(sz):
677     """
678     Return the size in a human readable format
679     """
680     if not sz:
681         return False
682     units = ('bytes', 'Kb', 'Mb', 'Gb')
683     if isinstance(sz,basestring):
684         sz=len(sz)
685     s, i = float(sz), 0
686     while s >= 1024 and i < len(units)-1:
687         s = s / 1024
688         i = i + 1
689     return "%0.2f %s" % (s, units[i])
690
691 def logged(f):
692     from functools import wraps
693     
694     @wraps(f)
695     def wrapper(*args, **kwargs):
696         import netsvc
697         from pprint import pformat
698
699         vector = ['Call -> function: %r' % f]
700         for i, arg in enumerate(args):
701             vector.append('  arg %02d: %s' % (i, pformat(arg)))
702         for key, value in kwargs.items():
703             vector.append('  kwarg %10s: %s' % (key, pformat(value)))
704
705         timeb4 = time.time()
706         res = f(*args, **kwargs)
707         
708         vector.append('  result: %s' % pformat(res))
709         vector.append('  time delta: %s' % (time.time() - timeb4))
710         #netsvc.Logger().notifyChannel('logged', netsvc.LOG_DEBUG, '\n'.join(vector))
711         return res
712
713     return wrapper
714
715
716 icons = map(lambda x: (x,x), ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
717 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
718 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
719 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
720 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
721 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
722 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
723 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
724 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
725 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
726 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
727 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
728 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
729 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
730 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
731 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
732 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
733 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
734 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
735 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
736 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
737 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
738 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
739 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
740 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
741 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
742 ])
743
744
745
746 if __name__ == '__main__':
747     import doctest
748     doctest.testmod()
749
750
751
752 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
753