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, \
98 values (%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))
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):
308 print 'sending', email_from, email_to, subject, body
310 from email.MIMEText import MIMEText
311 from email.MIMEBase import MIMEBase
312 from email.MIMEMultipart import MIMEMultipart
313 from email.Header import Header
314 from email.Utils import formatdate, COMMASPACE
315 from email.Utils import formatdate, COMMASPACE
316 from email import Encoders
319 ssl = config.get('smtp_ssl', False)
321 if not email_from and not config['email_from']:
322 raise Exception("No Email sender by default, see config file")
330 msg = MIMEText(body or '',_subtype=subtype,_charset='utf-8')
332 msg = MIMEMultipart()
334 msg['Subject'] = Header(subject.decode('utf8'), 'utf-8')
335 msg['From'] = email_from
338 msg['Reply-To'] = reply_to
340 msg['Reply-To'] = msg['From']
341 msg['To'] = COMMASPACE.join(email_to)
343 msg['Cc'] = COMMASPACE.join(email_cc)
345 msg['Bcc'] = COMMASPACE.join(email_bcc)
346 msg['Date'] = formatdate(localtime=True)
348 # Add OpenERP Server information
349 msg['X-Generated-By'] = 'OpenERP (http://www.openerp.com)'
350 msg['X-OpenERP-Server-Host'] = socket.gethostname()
351 msg['X-OpenERP-Server-Version'] = release.version
353 # Add dynamic X Header
354 for key, value in x_headers.items():
355 msg['X-OpenERP-%s' % key] = str(value)
358 msg['Message-Id'] = "<%s-tinycrm-%s@%s>" % (time.time(), tinycrm, socket.gethostname())
361 msg.attach( MIMEText(body or '', _charset='utf-8', _subtype=subtype) )
363 for (fname,fcontent) in attach:
364 part = MIMEBase('application', "octet-stream")
365 part.set_payload( fcontent )
366 Encoders.encode_base64(part)
367 part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,))
374 s.connect(config['smtp_server'], config['smtp_port'])
380 if config['smtp_user'] or config['smtp_password']:
381 s.login(config['smtp_user'], config['smtp_password'])
383 s.sendmail(email_from,
384 flatten([email_to, email_cc, email_bcc]),
390 logging.getLogger().error(str(e))
394 #----------------------------------------------------------
396 #----------------------------------------------------------
397 # text must be latin-1 encoded
398 def sms_send(user, password, api_id, text, to):
400 url = "http://api.urlsms.com/SendSMS.aspx"
401 #url = "http://196.7.150.220/http/sendmsg"
402 params = urllib.urlencode({'UserID': user, 'Password': password, 'SenderID': api_id, 'MsgText': text, 'RecipientMobileNo':to})
403 f = urllib.urlopen(url+"?"+params)
404 # FIXME: Use the logger if there is an error
407 #---------------------------------------------------------
408 # Class that stores an updateable string (used in wizards)
409 #---------------------------------------------------------
410 class UpdateableStr(local):
412 def __init__(self, string=''):
416 return str(self.string)
419 return str(self.string)
421 def __nonzero__(self):
422 return bool(self.string)
425 class UpdateableDict(local):
426 '''Stores an updateable dict to use in wizards'''
428 def __init__(self, dict=None):
434 return str(self.dict)
437 return str(self.dict)
440 return self.dict.clear()
443 return self.dict.keys()
445 def __setitem__(self, i, y):
446 self.dict.__setitem__(i, y)
448 def __getitem__(self, i):
449 return self.dict.__getitem__(i)
452 return self.dict.copy()
455 return self.dict.iteritems()
458 return self.dict.iterkeys()
460 def itervalues(self):
461 return self.dict.itervalues()
463 def pop(self, k, d=None):
464 return self.dict.pop(k, d)
467 return self.dict.popitem()
469 def setdefault(self, k, d=None):
470 return self.dict.setdefault(k, d)
472 def update(self, E, **F):
473 return self.dict.update(E, F)
476 return self.dict.values()
478 def get(self, k, d=None):
479 return self.dict.get(k, d)
481 def has_key(self, k):
482 return self.dict.has_key(k)
485 return self.dict.items()
487 def __cmp__(self, y):
488 return self.dict.__cmp__(y)
490 def __contains__(self, k):
491 return self.dict.__contains__(k)
493 def __delitem__(self, y):
494 return self.dict.__delitem__(y)
497 return self.dict.__eq__(y)
500 return self.dict.__ge__(y)
502 def __getitem__(self, y):
503 return self.dict.__getitem__(y)
506 return self.dict.__gt__(y)
509 return self.dict.__hash__()
512 return self.dict.__iter__()
515 return self.dict.__le__(y)
518 return self.dict.__len__()
521 return self.dict.__lt__(y)
524 return self.dict.__ne__(y)
527 # Don't use ! Use res.currency.round()
528 class currency(float):
530 def __init__(self, value, accuracy=2, rounding=None):
532 rounding=10**-accuracy
533 self.rounding=rounding
534 self.accuracy=accuracy
536 def __new__(cls, value, accuracy=2, rounding=None):
537 return float.__new__(cls, round(value, accuracy))
540 # display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
541 # return str(display_value)
553 Use it as a decorator of the function you plan to cache
554 Timeout: 0 = no timeout, otherwise in seconds
559 def __init__(self, timeout=None, skiparg=2, multi=None):
560 assert skiparg >= 2 # at least self and cr
562 self.timeout = config['cache_timeout']
564 self.timeout = timeout
565 self.skiparg = skiparg
567 self.lasttime = time.time()
570 cache.__caches.append(self)
573 def _generate_keys(self, dbname, kwargs2):
575 Generate keys depending of the arguments and the self.mutli value
580 i.sort(key=lambda (x,y): x)
584 key = (('dbname', dbname),) + to_tuple(kwargs2)
587 multis = kwargs2[self.multi][:]
589 kwargs2[self.multi] = (id,)
590 key = (('dbname', dbname),) + to_tuple(kwargs2)
593 def _unify_args(self, *args, **kwargs):
594 # Update named arguments with positional argument values (without self and cr)
595 kwargs2 = self.fun_default_values.copy()
596 kwargs2.update(kwargs)
597 kwargs2.update(dict(zip(self.fun_arg_names, args[self.skiparg-2:])))
599 if isinstance(kwargs2[k], (list, dict, set)):
600 kwargs2[k] = tuple(kwargs2[k])
601 elif not is_hashable(kwargs2[k]):
602 kwargs2[k] = repr(kwargs2[k])
606 def clear(self, dbname, *args, **kwargs):
607 """clear the cache for database dbname
608 if *args and **kwargs are both empty, clear all the keys related to this database
610 if not args and not kwargs:
611 keys_to_del = [key for key in self.cache if key[0][1] == dbname]
613 kwargs2 = self._unify_args(*args, **kwargs)
614 keys_to_del = [key for key, _ in self._generate_keys(dbname, kwargs2) if key in self.cache]
616 for key in keys_to_del:
620 def clean_caches_for_db(cls, dbname):
621 for c in cls.__caches:
624 def __call__(self, fn):
625 if self.fun is not None:
626 raise Exception("Can not use a cache instance on more than one function")
629 argspec = inspect.getargspec(fn)
630 self.fun_arg_names = argspec[0][self.skiparg:]
631 self.fun_default_values = {}
633 self.fun_default_values = dict(zip(self.fun_arg_names[-len(argspec[3]):], argspec[3]))
635 def cached_result(self2, cr, *args, **kwargs):
636 if time.time()-self.timeout > self.lasttime:
637 self.lasttime = time.time()
638 t = time.time()-self.timeout
639 for key in self.cache.keys():
640 if self.cache[key][1]<t:
643 kwargs2 = self._unify_args(*args, **kwargs)
647 for key, id in self._generate_keys(cr.dbname, kwargs2):
648 if key in self.cache:
649 result[id] = self.cache[key][0]
655 kwargs2[self.multi] = notincache.keys()
657 result2 = fn(self2, cr, *args[2:self.skiparg], **kwargs2)
659 key = notincache[None]
660 self.cache[key] = (result2, time.time())
661 result[None] = result2
665 self.cache[key] = (result2[id], time.time())
666 result.update(result2)
672 cached_result.clear_cache = self.clear
676 return s.replace('&','&').replace('<','<').replace('>','>')
679 """This method is similar to the builtin `str` method, except
680 it will return Unicode string.
682 @param value: the value to convert
685 @return: unicode string
688 if isinstance(value, unicode):
691 if hasattr(value, '__unicode__'):
692 return unicode(value)
694 if not isinstance(value, str):
697 try: # first try utf-8
698 return unicode(value, 'utf-8')
702 try: # then extened iso-8858
703 return unicode(value, 'iso-8859-15')
707 # else use default system locale
708 from locale import getlocale
709 return unicode(value, getlocale()[1])
711 def exception_to_unicode(e):
712 if hasattr(e, 'message'):
713 return ustr(e.message)
714 if hasattr(e, 'args'):
715 return "\n".join((ustr(a) for a in e.args))
719 return u"Unknow message"
722 # to be compatible with python 2.4
724 if not hasattr(__builtin__, 'all'):
726 for element in iterable:
731 __builtin__.all = all
734 if not hasattr(__builtin__, 'any'):
736 for element in iterable:
741 __builtin__.any = any
748 'ar_AR': u'Arabic / الْعَرَبيّة',
749 'bg_BG': u'Bulgarian / български',
750 'bs_BS': u'Bosnian / bosanski jezik',
751 'ca_ES': u'Catalan / Català',
752 'cs_CZ': u'Czech / Čeština',
753 'da_DK': u'Danish / Dansk',
754 'de_DE': u'German / Deutsch',
755 'el_EL': u'Greek / Ελληνικά',
756 'en_CA': u'English (CA)',
757 'en_EN': u'English (default)',
758 'en_GB': u'English (UK)',
759 'en_US': u'English (US)',
760 'es_AR': u'Spanish (AR) / Español (AR)',
761 'es_ES': u'Spanish / Español',
762 'et_EE': u'Estonian / Eesti keel',
763 'fr_BE': u'French (BE) / Français (BE)',
764 'fr_CH': u'French (CH) / Français (CH)',
765 'fr_FR': u'French / Français',
766 'hr_HR': u'Croatian / hrvatski jezik',
767 'hu_HU': u'Hungarian / Magyar',
768 'id_ID': u'Indonesian / Bahasa Indonesia',
769 'it_IT': u'Italian / Italiano',
770 'lt_LT': u'Lithuanian / Lietuvių kalba',
771 'nl_NL': u'Dutch / Nederlands',
772 'nl_BE': u'Dutch (Belgium) / Nederlands (Belgïe)',
773 'pl_PL': u'Polish / Język polski',
774 'pt_BR': u'Portugese (BR) / português (BR)',
775 'pt_PT': u'Portugese / português',
776 'ro_RO': u'Romanian / limba română',
777 'ru_RU': u'Russian / русский язык',
778 'sl_SL': u'Slovenian / slovenščina',
779 'sv_SE': u'Swedish / svenska',
780 'tr_TR': u'Turkish / Türkçe',
781 'uk_UK': u'Ukrainian / украї́нська мо́ва',
782 'zh_CN': u'Chinese (CN) / 简体中文' ,
783 'zh_TW': u'Chinese (TW) / 正體字',
787 def scan_languages():
789 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'))]
790 lang_dict = get_languages()
791 ret = [(lang, lang_dict.get(lang, lang)) for lang in file_list]
792 ret.sort(key=lambda k:k[1])
796 def get_user_companies(cr, user):
797 def _get_company_children(cr, ids):
800 cr.execute('SELECT id FROM res_company WHERE parent_id = any(array[%s])' %(','.join([str(x) for x in ids]),))
801 res=[x[0] for x in cr.fetchall()]
802 res.extend(_get_company_children(cr, res))
804 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,))
805 compids=[cr.fetchone()[0]]
806 compids.extend(_get_company_children(cr, compids))
811 Input number : account or invoice number
812 Output return: the same number completed with the recursive mod10
815 codec=[0,9,4,6,8,2,7,1,3,5]
821 report = codec[ (int(digit) + report) % 10 ]
822 return result + str((10 - report) % 10)
827 Return the size in a human readable format
831 units = ('bytes', 'Kb', 'Mb', 'Gb')
832 if isinstance(sz,basestring):
835 while s >= 1024 and i < len(units)-1:
838 return "%0.2f %s" % (s, units[i])
841 from tools.func import wraps
844 def wrapper(*args, **kwargs):
846 from pprint import pformat
848 vector = ['Call -> function: %r' % f]
849 for i, arg in enumerate(args):
850 vector.append(' arg %02d: %s' % (i, pformat(arg)))
851 for key, value in kwargs.items():
852 vector.append(' kwarg %10s: %s' % (key, pformat(value)))
855 res = f(*args, **kwargs)
857 vector.append(' result: %s' % pformat(res))
858 vector.append(' time delta: %s' % (time.time() - timeb4))
859 netsvc.Logger().notifyChannel('logged', netsvc.LOG_DEBUG, '\n'.join(vector))
864 class profile(object):
865 def __init__(self, fname=None):
868 def __call__(self, f):
869 from tools.func import wraps
872 def wrapper(*args, **kwargs):
873 class profile_wrapper(object):
877 self.result = f(*args, **kwargs)
878 pw = profile_wrapper()
880 fname = self.fname or ("%s.cprof" % (f.func_name,))
881 cProfile.runctx('pw()', globals(), locals(), filename=fname)
888 This method allow you to debug your code without print
890 >>> def func_foo(bar)
898 This will output on the logger:
900 [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:baz = 42
901 [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:qnx = (42, 42)
903 To view the DEBUG lines in the logger you must start the server with the option
908 from inspect import stack
910 from pprint import pformat
912 param = re.split("debug *\((.+)\)", st[4][0].strip())[1].strip()
913 while param.count(')') > param.count('('): param = param[:param.rfind(')')]
916 what = "%s = %s" % (param, what)
917 netsvc.Logger().notifyChannel(st[3], netsvc.LOG_DEBUG, what)
920 icons = map(lambda x: (x,x), ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
921 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
922 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
923 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
924 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
925 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
926 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
927 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
928 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
929 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
930 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
931 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
932 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
933 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
934 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
935 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
936 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
937 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
938 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
939 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
940 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
941 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
942 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
943 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
944 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
945 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
948 def extract_zip_file(zip_file, outdirectory):
952 zf = zipfile.ZipFile(zip_file, 'r')
954 for path in zf.namelist():
955 tgt = os.path.join(out, path)
956 tgtdir = os.path.dirname(tgt)
957 if not os.path.exists(tgtdir):
960 if not tgt.endswith(os.sep):
962 fp.write(zf.read(path))
970 if __name__ == '__main__':
976 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: