1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
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.
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.
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/>.
21 ##############################################################################
24 Miscelleanous tools used by OpenERP.
30 from config import config
36 if sys.version_info[:2] < (2, 4):
37 from threadinglocal import local
39 from threading import local
41 from itertools import izip
43 # initialize a database with base/base.sql
46 f = addons.get_module_resource('base', 'base.sql')
47 for line in file_open(f).read().split(';'):
48 if (len(line)>0) and (not line.isspace()):
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)
58 if os.path.isfile(terp_file) or os.path.isfile(mod_path+'.zip'):
59 info = eval(file_open(terp_file).read())
61 categs = info.get('category', 'Uncategorized').split('/')
65 cr.execute('select id \
66 from ir_module_category \
67 where name=%s and parent_id=%s', (categs[0], p_id))
69 cr.execute('select id \
70 from ir_module_category \
71 where name=%s and parent_id is NULL', (categs[0],))
74 cr.execute('select nextval(\'ir_module_category_id_seq\')')
75 c_id = cr.fetchone()[0]
76 cr.execute('insert into ir_module_category \
77 (id, name, parent_id) \
78 values (%s, %s, %s)', (c_id, categs[0], p_id))
84 active = info.get('active', False)
85 installable = info.get('installable', True)
92 state = 'uninstallable'
93 cr.execute('select nextval(\'ir_module_module_id_seq\')')
95 cr.execute('insert into ir_module_module \
96 (id, author, website, name, shortdesc, description, \
97 category_id, state, certificate) \
98 values (%s, %s, %s, %s, %s, %s, %s, %s, %s)', (
99 id, info.get('author', ''),
100 info.get('website', ''), i, info.get('name', False),
101 info.get('description', ''), p_id, state, info.get('certificate')))
102 cr.execute('insert into ir_model_data \
103 (name,model,module, res_id) values (%s,%s,%s,%s)', (
104 'module_meta_information', 'ir.module.module', i, id))
105 dependencies = info.get('depends', [])
106 for d in dependencies:
107 cr.execute('insert into ir_module_module_dependency \
108 (module_id,name) values (%s, %s)', (id, d))
111 def find_in_path(name):
116 path = [dir for dir in os.environ['PATH'].split(sep)
117 if os.path.isdir(dir)]
119 val = os.path.join(dir, name)
120 if os.path.isfile(val) or os.path.islink(val):
124 def find_pg_tool(name):
125 if config['pg_path'] and config['pg_path'] != 'None':
126 return os.path.join(config['pg_path'], name)
128 return find_in_path(name)
130 def exec_pg_command(name, *args):
131 prog = find_pg_tool(name)
133 raise Exception('Couldn\'t find %s' % name)
134 args2 = (os.path.basename(prog),) + args
135 return os.spawnv(os.P_WAIT, prog, args2)
137 def exec_pg_command_pipe(name, *args):
138 prog = find_pg_tool(name)
140 raise Exception('Couldn\'t find %s' % name)
142 cmd = '"' + prog + '" ' + ' '.join(args)
144 cmd = prog + ' ' + ' '.join(args)
145 return os.popen2(cmd, 'b')
147 def exec_command_pipe(name, *args):
148 prog = find_in_path(name)
150 raise Exception('Couldn\'t find %s' % name)
152 cmd = '"'+prog+'" '+' '.join(args)
154 cmd = prog+' '+' '.join(args)
155 return os.popen2(cmd, 'b')
157 #----------------------------------------------------------
159 #----------------------------------------------------------
160 #file_path_root = os.getcwd()
161 #file_path_addons = os.path.join(file_path_root, 'addons')
163 def file_open(name, mode="r", subdir='addons', pathinfo=False):
164 """Open a file from the OpenERP root, using a subdir folder.
166 >>> file_open('hr/report/timesheer.xsl')
167 >>> file_open('addons/hr/report/timesheet.xsl')
168 >>> file_open('../../base/report/rml_template.xsl', subdir='addons/hr/report', pathinfo=True)
170 @param name: name of the file
171 @param mode: file open mode
172 @param subdir: subdirectory
173 @param pathinfo: if True returns tupple (fileobject, filepath)
175 @return: fileobject if pathinfo is False else (fileobject, filepath)
178 adp = os.path.normcase(os.path.abspath(config['addons_path']))
179 rtp = os.path.normcase(os.path.abspath(config['root_path']))
181 if name.replace(os.path.sep, '/').startswith('addons/'):
185 # First try to locate in addons_path
188 if subdir2.replace(os.path.sep, '/').startswith('addons/'):
189 subdir2 = subdir2[7:]
191 subdir2 = (subdir2 != 'addons' or None) and subdir2
195 fn = os.path.join(adp, subdir2, name)
197 fn = os.path.join(adp, name)
198 fn = os.path.normpath(fn)
199 fo = file_open(fn, mode=mode, subdir=None, pathinfo=pathinfo)
207 name = os.path.join(rtp, subdir, name)
209 name = os.path.join(rtp, name)
211 name = os.path.normpath(name)
213 # Check for a zipfile in the path
218 head, tail = os.path.split(head)
222 zipname = os.path.join(tail, zipname)
225 if zipfile.is_zipfile(head+'.zip'):
226 from cStringIO import StringIO
227 zfile = zipfile.ZipFile(head+'.zip')
230 fo.write(zfile.read(os.path.join(
231 os.path.basename(head), zipname).replace(
238 name2 = os.path.normpath(os.path.join(head + '.zip', zipname))
240 for i in (name2, name):
241 if i and os.path.isfile(i):
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)
251 #----------------------------------------------------------
253 #----------------------------------------------------------
255 """Flatten a list of elements into a uniqu list
256 Author: Christophe Simonis (christophe@tinyerp.com)
265 >>> flatten( [[], [[]]] )
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,)], []]])
271 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
275 return hasattr(x, "__iter__")
280 map(r.append, flatten(e))
285 def reverse_enumerate(l):
286 """Like enumerate but in the other sens
287 >>> a = ['a', 'b', 'c']
288 >>> it = reverse_enumerate(a)
296 Traceback (most recent call last):
297 File "<stdin>", line 1, in <module>
300 return izip(xrange(len(l)-1, -1, -1), reversed(l))
302 #----------------------------------------------------------
304 #----------------------------------------------------------
305 def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
306 attach=None, tinycrm=False, ssl=False, debug=False, subtype='plain', x_headers=None):
309 from email.MIMEText import MIMEText
310 from email.MIMEBase import MIMEBase
311 from email.MIMEMultipart import MIMEMultipart
312 from email.Header import Header
313 from email.Utils import formatdate, COMMASPACE
314 from email.Utils import formatdate, COMMASPACE
315 from email import Encoders
317 if x_headers is None:
321 ssl = config.get('smtp_ssl', False)
323 if not email_from and not config['email_from']:
324 raise Exception("No Email sender by default, see config file")
332 msg = MIMEText(body or '',_subtype=subtype,_charset='utf-8')
334 msg = MIMEMultipart()
336 msg['Subject'] = Header(ustr(subject), 'utf-8')
337 msg['From'] = email_from
340 msg['Reply-To'] = reply_to
342 msg['Reply-To'] = msg['From']
343 msg['To'] = COMMASPACE.join(email_to)
345 msg['Cc'] = COMMASPACE.join(email_cc)
347 msg['Bcc'] = COMMASPACE.join(email_bcc)
348 msg['Date'] = formatdate(localtime=True)
350 # Add OpenERP Server information
351 msg['X-Generated-By'] = 'OpenERP (http://www.openerp.com)'
352 msg['X-OpenERP-Server-Host'] = socket.gethostname()
353 msg['X-OpenERP-Server-Version'] = release.version
355 # Add dynamic X Header
356 for key, value in x_headers.items():
357 msg['X-OpenERP-%s' % key] = str(value)
360 msg['Message-Id'] = "<%s-tinycrm-%s@%s>" % (time.time(), tinycrm, socket.gethostname())
363 msg.attach( MIMEText(body or '', _charset='utf-8', _subtype=subtype) )
365 for (fname,fcontent) in attach:
366 part = MIMEBase('application', "octet-stream")
367 part.set_payload( fcontent )
368 Encoders.encode_base64(part)
369 part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,))
376 s.connect(config['smtp_server'], config['smtp_port'])
382 if config['smtp_user'] or config['smtp_password']:
383 s.login(config['smtp_user'], config['smtp_password'])
385 s.sendmail(email_from,
386 flatten([email_to, email_cc, email_bcc]),
392 netsvc.Logger().notifyChannel('email_send', netsvc.LOG_ERROR, e)
396 #----------------------------------------------------------
398 #----------------------------------------------------------
399 # text must be latin-1 encoded
400 def sms_send(user, password, api_id, text, to):
402 url = "http://api.urlsms.com/SendSMS.aspx"
403 #url = "http://196.7.150.220/http/sendmsg"
404 params = urllib.urlencode({'UserID': user, 'Password': password, 'SenderID': api_id, 'MsgText': text, 'RecipientMobileNo':to})
405 f = urllib.urlopen(url+"?"+params)
406 # FIXME: Use the logger if there is an error
409 #---------------------------------------------------------
410 # Class that stores an updateable string (used in wizards)
411 #---------------------------------------------------------
412 class UpdateableStr(local):
414 def __init__(self, string=''):
418 return str(self.string)
421 return str(self.string)
423 def __nonzero__(self):
424 return bool(self.string)
427 class UpdateableDict(local):
428 '''Stores an updateable dict to use in wizards'''
430 def __init__(self, dict=None):
436 return str(self.dict)
439 return str(self.dict)
442 return self.dict.clear()
445 return self.dict.keys()
447 def __setitem__(self, i, y):
448 self.dict.__setitem__(i, y)
450 def __getitem__(self, i):
451 return self.dict.__getitem__(i)
454 return self.dict.copy()
457 return self.dict.iteritems()
460 return self.dict.iterkeys()
462 def itervalues(self):
463 return self.dict.itervalues()
465 def pop(self, k, d=None):
466 return self.dict.pop(k, d)
469 return self.dict.popitem()
471 def setdefault(self, k, d=None):
472 return self.dict.setdefault(k, d)
474 def update(self, E, **F):
475 return self.dict.update(E, F)
478 return self.dict.values()
480 def get(self, k, d=None):
481 return self.dict.get(k, d)
483 def has_key(self, k):
484 return self.dict.has_key(k)
487 return self.dict.items()
489 def __cmp__(self, y):
490 return self.dict.__cmp__(y)
492 def __contains__(self, k):
493 return self.dict.__contains__(k)
495 def __delitem__(self, y):
496 return self.dict.__delitem__(y)
499 return self.dict.__eq__(y)
502 return self.dict.__ge__(y)
504 def __getitem__(self, y):
505 return self.dict.__getitem__(y)
508 return self.dict.__gt__(y)
511 return self.dict.__hash__()
514 return self.dict.__iter__()
517 return self.dict.__le__(y)
520 return self.dict.__len__()
523 return self.dict.__lt__(y)
526 return self.dict.__ne__(y)
529 # Don't use ! Use res.currency.round()
530 class currency(float):
532 def __init__(self, value, accuracy=2, rounding=None):
534 rounding=10**-accuracy
535 self.rounding=rounding
536 self.accuracy=accuracy
538 def __new__(cls, value, accuracy=2, rounding=None):
539 return float.__new__(cls, round(value, accuracy))
542 # display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
543 # return str(display_value)
555 Use it as a decorator of the function you plan to cache
556 Timeout: 0 = no timeout, otherwise in seconds
561 def __init__(self, timeout=None, skiparg=2, multi=None):
562 assert skiparg >= 2 # at least self and cr
564 self.timeout = config['cache_timeout']
566 self.timeout = timeout
567 self.skiparg = skiparg
569 self.lasttime = time.time()
572 cache.__caches.append(self)
575 def _generate_keys(self, dbname, kwargs2):
577 Generate keys depending of the arguments and the self.mutli value
582 i.sort(key=lambda (x,y): x)
586 key = (('dbname', dbname),) + to_tuple(kwargs2)
589 multis = kwargs2[self.multi][:]
591 kwargs2[self.multi] = (id,)
592 key = (('dbname', dbname),) + to_tuple(kwargs2)
595 def _unify_args(self, *args, **kwargs):
596 # Update named arguments with positional argument values (without self and cr)
597 kwargs2 = self.fun_default_values.copy()
598 kwargs2.update(kwargs)
599 kwargs2.update(dict(zip(self.fun_arg_names, args[self.skiparg-2:])))
601 if isinstance(kwargs2[k], (list, dict, set)):
602 kwargs2[k] = tuple(kwargs2[k])
603 elif not is_hashable(kwargs2[k]):
604 kwargs2[k] = repr(kwargs2[k])
608 def clear(self, dbname, *args, **kwargs):
609 """clear the cache for database dbname
610 if *args and **kwargs are both empty, clear all the keys related to this database
612 if not args and not kwargs:
613 keys_to_del = [key for key in self.cache if key[0][1] == dbname]
615 kwargs2 = self._unify_args(*args, **kwargs)
616 keys_to_del = [key for key, _ in self._generate_keys(dbname, kwargs2) if key in self.cache]
618 for key in keys_to_del:
622 def clean_caches_for_db(cls, dbname):
623 for c in cls.__caches:
626 def __call__(self, fn):
627 if self.fun is not None:
628 raise Exception("Can not use a cache instance on more than one function")
631 argspec = inspect.getargspec(fn)
632 self.fun_arg_names = argspec[0][self.skiparg:]
633 self.fun_default_values = {}
635 self.fun_default_values = dict(zip(self.fun_arg_names[-len(argspec[3]):], argspec[3]))
637 def cached_result(self2, cr, *args, **kwargs):
638 if time.time()-self.timeout > self.lasttime:
639 self.lasttime = time.time()
640 t = time.time()-self.timeout
641 for key in self.cache.keys():
642 if self.cache[key][1]<t:
645 kwargs2 = self._unify_args(*args, **kwargs)
649 for key, id in self._generate_keys(cr.dbname, kwargs2):
650 if key in self.cache:
651 result[id] = self.cache[key][0]
657 kwargs2[self.multi] = notincache.keys()
659 result2 = fn(self2, cr, *args[:self.skiparg-2], **kwargs2)
661 key = notincache[None]
662 self.cache[key] = (result2, time.time())
663 result[None] = result2
667 self.cache[key] = (result2[id], time.time())
668 result.update(result2)
674 cached_result.clear_cache = self.clear
678 return s.replace('&','&').replace('<','<').replace('>','>')
681 """This method is similar to the builtin `str` method, except
682 it will return Unicode string.
684 @param value: the value to convert
687 @return: unicode string
690 if isinstance(value, unicode):
693 if hasattr(value, '__unicode__'):
694 return unicode(value)
696 if not isinstance(value, str):
699 try: # first try utf-8
700 return unicode(value, 'utf-8')
704 try: # then extened iso-8858
705 return unicode(value, 'iso-8859-15')
709 # else use default system locale
710 from locale import getlocale
711 return unicode(value, getlocale()[1])
713 def exception_to_unicode(e):
714 if hasattr(e, 'message'):
715 return ustr(e.message)
716 if hasattr(e, 'args'):
717 return "\n".join((ustr(a) for a in e.args))
721 return u"Unknow message"
724 # to be compatible with python 2.4
726 if not hasattr(__builtin__, 'all'):
728 for element in iterable:
733 __builtin__.all = all
736 if not hasattr(__builtin__, 'any'):
738 for element in iterable:
743 __builtin__.any = any
750 'ar_AR': u'Arabic / الْعَرَبيّة',
751 'bg_BG': u'Bulgarian / български',
752 'bs_BS': u'Bosnian / bosanski jezik',
753 'ca_ES': u'Catalan / Català',
754 'cs_CZ': u'Czech / Čeština',
755 'da_DK': u'Danish / Dansk',
756 'de_DE': u'German / Deutsch',
757 'el_EL': u'Greek / Ελληνικά',
758 'en_CA': u'English (CA)',
759 'en_GB': u'English (UK)',
760 'en_US': u'English (US)',
761 'es_AR': u'Spanish (AR) / Español (AR)',
762 'es_ES': u'Spanish / Español',
763 'et_EE': u'Estonian / Eesti keel',
764 'fr_BE': u'French (BE) / Français (BE)',
765 'fr_CH': u'French (CH) / Français (CH)',
766 'fr_FR': u'French / Français',
767 'hr_HR': u'Croatian / hrvatski jezik',
768 'hu_HU': u'Hungarian / Magyar',
769 'id_ID': u'Indonesian / Bahasa Indonesia',
770 'it_IT': u'Italian / Italiano',
771 'lt_LT': u'Lithuanian / Lietuvių kalba',
772 'nl_NL': u'Dutch / Nederlands',
773 'nl_BE': u'Dutch (Belgium) / Nederlands (Belgïe)',
774 'pl_PL': u'Polish / Język polski',
775 'pt_BR': u'Portugese (BR) / português (BR)',
776 'pt_PT': u'Portugese / português',
777 'ro_RO': u'Romanian / limba română',
778 'ru_RU': u'Russian / русский язык',
779 'sl_SL': u'Slovenian / slovenščina',
780 'sv_SE': u'Swedish / svenska',
781 'tr_TR': u'Turkish / Türkçe',
782 'uk_UA': u'Ukrainian / украї́нська мо́ва',
783 'zh_CN': u'Chinese (CN) / 简体中文' ,
784 'zh_TW': u'Chinese (TW) / 正體字',
788 def scan_languages():
790 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'))]
791 lang_dict = get_languages()
792 ret = [(lang, lang_dict.get(lang, lang)) for lang in file_list]
793 ret.sort(key=lambda k:k[1])
797 def get_user_companies(cr, user):
798 def _get_company_children(cr, ids):
801 cr.execute('SELECT id FROM res_company WHERE parent_id = any(array[%s])' %(','.join([str(x) for x in ids]),))
802 res=[x[0] for x in cr.fetchall()]
803 res.extend(_get_company_children(cr, res))
805 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,))
806 compids=[cr.fetchone()[0]]
807 compids.extend(_get_company_children(cr, compids))
812 Input number : account or invoice number
813 Output return: the same number completed with the recursive mod10
816 codec=[0,9,4,6,8,2,7,1,3,5]
822 report = codec[ (int(digit) + report) % 10 ]
823 return result + str((10 - report) % 10)
828 Return the size in a human readable format
832 units = ('bytes', 'Kb', 'Mb', 'Gb')
833 if isinstance(sz,basestring):
836 while s >= 1024 and i < len(units)-1:
839 return "%0.2f %s" % (s, units[i])
842 from tools.func import wraps
845 def wrapper(*args, **kwargs):
847 from pprint import pformat
849 vector = ['Call -> function: %r' % f]
850 for i, arg in enumerate(args):
851 vector.append(' arg %02d: %s' % (i, pformat(arg)))
852 for key, value in kwargs.items():
853 vector.append(' kwarg %10s: %s' % (key, pformat(value)))
856 res = f(*args, **kwargs)
858 vector.append(' result: %s' % pformat(res))
859 vector.append(' time delta: %s' % (time.time() - timeb4))
860 netsvc.Logger().notifyChannel('logged', netsvc.LOG_DEBUG, '\n'.join(vector))
865 class profile(object):
866 def __init__(self, fname=None):
869 def __call__(self, f):
870 from tools.func import wraps
873 def wrapper(*args, **kwargs):
874 class profile_wrapper(object):
878 self.result = f(*args, **kwargs)
879 pw = profile_wrapper()
881 fname = self.fname or ("%s.cprof" % (f.func_name,))
882 cProfile.runctx('pw()', globals(), locals(), filename=fname)
889 This method allow you to debug your code without print
891 >>> def func_foo(bar)
899 This will output on the logger:
901 [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:baz = 42
902 [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:qnx = (42, 42)
904 To view the DEBUG lines in the logger you must start the server with the option
909 from inspect import stack
911 from pprint import pformat
913 param = re.split("debug *\((.+)\)", st[4][0].strip())[1].strip()
914 while param.count(')') > param.count('('): param = param[:param.rfind(')')]
917 what = "%s = %s" % (param, what)
918 netsvc.Logger().notifyChannel(st[3], netsvc.LOG_DEBUG, what)
921 icons = map(lambda x: (x,x), ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
922 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
923 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
924 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
925 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
926 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
927 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
928 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
929 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
930 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
931 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
932 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
933 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
934 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
935 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
936 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
937 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
938 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
939 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
940 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
941 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
942 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
943 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
944 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
945 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
946 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
949 def extract_zip_file(zip_file, outdirectory):
953 zf = zipfile.ZipFile(zip_file, 'r')
955 for path in zf.namelist():
956 tgt = os.path.join(out, path)
957 tgtdir = os.path.dirname(tgt)
958 if not os.path.exists(tgtdir):
961 if not tgt.endswith(os.sep):
963 fp.write(zf.read(path))
971 if __name__ == '__main__':
977 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: