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, attach=None, tinycrm=False, ssl=False, debug=False, subtype='plain'):
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
317 ssl = config.get('smtp_ssl', False)
325 msg = MIMEText(body or '',_subtype=subtype,_charset='utf-8')
327 msg = MIMEMultipart()
329 msg['Subject'] = Header(subject.decode('utf8'), 'utf-8')
330 msg['From'] = email_from
333 msg['Reply-To'] = msg['From']+', '+reply_to
334 msg['To'] = COMMASPACE.join(email_to)
336 msg['Cc'] = COMMASPACE.join(email_cc)
338 msg['Bcc'] = COMMASPACE.join(email_bcc)
339 msg['Date'] = formatdate(localtime=True)
342 msg['Message-Id'] = "<%s-tinycrm-%s@%s>" % (time.time(), tinycrm, socket.gethostname())
345 msg.attach( MIMEText(body or '', _charset='utf-8', _subtype=subtype) )
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,))
358 s.connect(config['smtp_server'], config['smtp_port'])
364 if config['smtp_user'] or config['smtp_password']:
365 s.login(config['smtp_user'], config['smtp_password'])
367 s.sendmail(email_from,
368 flatten([email_to, email_cc, email_bcc]),
374 logging.getLogger().error(str(e))
378 #----------------------------------------------------------
380 #----------------------------------------------------------
381 # text must be latin-1 encoded
382 def sms_send(user, password, api_id, text, to):
384 url = "http://api.urlsms.com/SendSMS.aspx"
385 #url = "http://196.7.150.220/http/sendmsg"
386 params = urllib.urlencode({'UserID': user, 'Password': password, 'SenderID': api_id, 'MsgText': text, 'RecipientMobileNo':to})
387 f = urllib.urlopen(url+"?"+params)
388 # FIXME: Use the logger if there is an error
391 #---------------------------------------------------------
392 # Class that stores an updateable string (used in wizards)
393 #---------------------------------------------------------
394 class UpdateableStr(local):
396 def __init__(self, string=''):
400 return str(self.string)
403 return str(self.string)
405 def __nonzero__(self):
406 return bool(self.string)
409 class UpdateableDict(local):
410 '''Stores an updateable dict to use in wizards'''
412 def __init__(self, dict=None):
418 return str(self.dict)
421 return str(self.dict)
424 return self.dict.clear()
427 return self.dict.keys()
429 def __setitem__(self, i, y):
430 self.dict.__setitem__(i, y)
432 def __getitem__(self, i):
433 return self.dict.__getitem__(i)
436 return self.dict.copy()
439 return self.dict.iteritems()
442 return self.dict.iterkeys()
444 def itervalues(self):
445 return self.dict.itervalues()
447 def pop(self, k, d=None):
448 return self.dict.pop(k, d)
451 return self.dict.popitem()
453 def setdefault(self, k, d=None):
454 return self.dict.setdefault(k, d)
456 def update(self, E, **F):
457 return self.dict.update(E, F)
460 return self.dict.values()
462 def get(self, k, d=None):
463 return self.dict.get(k, d)
465 def has_key(self, k):
466 return self.dict.has_key(k)
469 return self.dict.items()
471 def __cmp__(self, y):
472 return self.dict.__cmp__(y)
474 def __contains__(self, k):
475 return self.dict.__contains__(k)
477 def __delitem__(self, y):
478 return self.dict.__delitem__(y)
481 return self.dict.__eq__(y)
484 return self.dict.__ge__(y)
486 def __getitem__(self, y):
487 return self.dict.__getitem__(y)
490 return self.dict.__gt__(y)
493 return self.dict.__hash__()
496 return self.dict.__iter__()
499 return self.dict.__le__(y)
502 return self.dict.__len__()
505 return self.dict.__lt__(y)
508 return self.dict.__ne__(y)
511 # Don't use ! Use res.currency.round()
512 class currency(float):
514 def __init__(self, value, accuracy=2, rounding=None):
516 rounding=10**-accuracy
517 self.rounding=rounding
518 self.accuracy=accuracy
520 def __new__(cls, value, accuracy=2, rounding=None):
521 return float.__new__(cls, round(value, accuracy))
524 # display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
525 # return str(display_value)
537 Use it as a decorator of the function you plan to cache
538 Timeout: 0 = no timeout, otherwise in seconds
543 def __init__(self, timeout=None, skiparg=2, multi=None):
544 assert skiparg >= 2 # at least self and cr
546 self.timeout = config['cache_timeout']
548 self.timeout = timeout
549 self.skiparg = skiparg
551 self.lasttime = time.time()
554 cache.__caches.append(self)
557 def _generate_keys(self, dbname, kwargs2):
559 Generate keys depending of the arguments and the self.mutli value
564 i.sort(key=lambda (x,y): x)
568 key = (('dbname', dbname),) + to_tuple(kwargs2)
571 multis = kwargs2[self.multi][:]
573 kwargs2[self.multi] = (id,)
574 key = (('dbname', dbname),) + to_tuple(kwargs2)
577 def _unify_args(self, *args, **kwargs):
578 # Update named arguments with positional argument values (without self and cr)
579 kwargs2 = self.fun_default_values.copy()
580 kwargs2.update(kwargs)
581 kwargs2.update(dict(zip(self.fun_arg_names, args[self.skiparg-2:])))
583 if isinstance(kwargs2[k], (list, dict, set)):
584 kwargs2[k] = tuple(kwargs2[k])
585 elif not is_hashable(kwargs2[k]):
586 kwargs2[k] = repr(kwargs2[k])
590 def clear(self, dbname, *args, **kwargs):
591 """clear the cache for database dbname
592 if *args and **kwargs are both empty, clear all the keys related to this database
594 if not args and not kwargs:
595 keys_to_del = [key for key in self.cache if key[0][1] == dbname]
597 kwargs2 = self._unify_args(*args, **kwargs)
598 keys_to_del = [key for key, _ in self._generate_keys(dbname, kwargs2) if key in self.cache]
600 for key in keys_to_del:
604 def clean_caches_for_db(cls, dbname):
605 for c in cls.__caches:
608 def __call__(self, fn):
609 if self.fun is not None:
610 raise Exception("Can not use a cache instance on more than one function")
613 argspec = inspect.getargspec(fn)
614 self.fun_arg_names = argspec[0][self.skiparg:]
615 self.fun_default_values = {}
617 self.fun_default_values = dict(zip(self.fun_arg_names[-len(argspec[3]):], argspec[3]))
619 def cached_result(self2, cr, *args, **kwargs):
620 if time.time()-self.timeout > self.lasttime:
621 self.lasttime = time.time()
622 t = time.time()-self.timeout
623 for key in self.cache.keys():
624 if self.cache[key][1]<t:
627 kwargs2 = self._unify_args(*args, **kwargs)
631 for key, id in self._generate_keys(cr.dbname, kwargs2):
632 if key in self.cache:
633 result[id] = self.cache[key][0]
639 kwargs2[self.multi] = notincache.keys()
641 result2 = fn(self2, cr, *args[2:self.skiparg], **kwargs2)
643 key = notincache[None]
644 self.cache[key] = (result2, time.time())
645 result[None] = result2
649 self.cache[key] = (result2[id], time.time())
650 result.update(result2)
656 cached_result.clear_cache = self.clear
660 return s.replace('&','&').replace('<','<').replace('>','>')
663 """This method is similar to the builtin `str` method, except
664 it will return Unicode string.
666 @param value: the value to convert
669 @return: unicode string
672 if isinstance(value, unicode):
675 if hasattr(value, '__unicode__'):
676 return unicode(value)
678 if not isinstance(value, str):
681 try: # first try utf-8
682 return unicode(value, 'utf-8')
686 try: # then extened iso-8858
687 return unicode(value, 'iso-8859-15')
691 # else use default system locale
692 from locale import getlocale
693 return unicode(value, getlocale()[1])
695 def exception_to_unicode(e):
696 if hasattr(e, 'message'):
697 return ustr(e.message)
698 if hasattr(e, 'args'):
699 return "\n".join((ustr(a) for a in e.args))
703 return u"Unknow message"
706 # to be compatible with python 2.4
708 if not hasattr(__builtin__, 'all'):
710 for element in iterable:
715 __builtin__.all = all
718 if not hasattr(__builtin__, 'any'):
720 for element in iterable:
725 __builtin__.any = any
732 'ar_AR': u'Arabic / الْعَرَبيّة',
733 'bg_BG': u'Bulgarian / български',
734 'bs_BS': u'Bosnian / bosanski jezik',
735 'ca_ES': u'Catalan / Català',
736 'cs_CZ': u'Czech / Čeština',
737 'da_DK': u'Danish / Dansk',
738 'de_DE': u'German / Deutsch',
739 'el_EL': u'Greek / Ελληνικά',
740 'en_CA': u'English (CA)',
741 'en_EN': u'English (default)',
742 'en_GB': u'English (UK)',
743 'en_US': u'English (US)',
744 'es_AR': u'Spanish (AR) / Español (AR)',
745 'es_ES': u'Spanish / Español',
746 'et_EE': u'Estonian / Eesti keel',
747 'fr_BE': u'French (BE) / Français (BE)',
748 'fr_CH': u'French (CH) / Français (CH)',
749 'fr_FR': u'French / Français',
750 'hr_HR': u'Croatian / hrvatski jezik',
751 'hu_HU': u'Hungarian / Magyar',
752 'id_ID': u'Indonesian / Bahasa Indonesia',
753 'it_IT': u'Italian / Italiano',
754 'lt_LT': u'Lithuanian / Lietuvių kalba',
755 'nl_NL': u'Dutch / Nederlands',
756 'nl_BE': u'Dutch (Belgium) / Nederlands (Belgïe)',
757 'pl_PL': u'Polish / Język polski',
758 'pt_BR': u'Portugese (BR) / português (BR)',
759 'pt_PT': u'Portugese / português',
760 'ro_RO': u'Romanian / limba română',
761 'ru_RU': u'Russian / русский язык',
762 'sl_SL': u'Slovenian / slovenščina',
763 'sv_SE': u'Swedish / svenska',
764 'tr_TR': u'Turkish / Türkçe',
765 'uk_UK': u'Ukrainian / украї́нська мо́ва',
766 'zh_CN': u'Chinese (CN) / 简体中文' ,
767 'zh_TW': u'Chinese (TW) / 正體字',
771 def scan_languages():
773 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'))]
774 lang_dict = get_languages()
775 ret = [(lang, lang_dict.get(lang, lang)) for lang in file_list]
776 ret.sort(key=lambda k:k[1])
780 def get_user_companies(cr, user):
781 def _get_company_children(cr, ids):
784 cr.execute('SELECT id FROM res_company WHERE parent_id = any(array[%s])' %(','.join([str(x) for x in ids]),))
785 res=[x[0] for x in cr.fetchall()]
786 res.extend(_get_company_children(cr, res))
788 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,))
789 compids=[cr.fetchone()[0]]
790 compids.extend(_get_company_children(cr, compids))
795 Input number : account or invoice number
796 Output return: the same number completed with the recursive mod10
799 codec=[0,9,4,6,8,2,7,1,3,5]
805 report = codec[ (int(digit) + report) % 10 ]
806 return result + str((10 - report) % 10)
811 Return the size in a human readable format
815 units = ('bytes', 'Kb', 'Mb', 'Gb')
816 if isinstance(sz,basestring):
819 while s >= 1024 and i < len(units)-1:
822 return "%0.2f %s" % (s, units[i])
825 from tools.func import wraps
828 def wrapper(*args, **kwargs):
830 from pprint import pformat
832 vector = ['Call -> function: %r' % f]
833 for i, arg in enumerate(args):
834 vector.append(' arg %02d: %s' % (i, pformat(arg)))
835 for key, value in kwargs.items():
836 vector.append(' kwarg %10s: %s' % (key, pformat(value)))
839 res = f(*args, **kwargs)
841 vector.append(' result: %s' % pformat(res))
842 vector.append(' time delta: %s' % (time.time() - timeb4))
843 netsvc.Logger().notifyChannel('logged', netsvc.LOG_DEBUG, '\n'.join(vector))
848 class profile(object):
849 def __init__(self, fname=None):
852 def __call__(self, f):
853 from tools.func import wraps
856 def wrapper(*args, **kwargs):
857 class profile_wrapper(object):
861 self.result = f(*args, **kwargs)
862 pw = profile_wrapper()
864 fname = self.fname or ("%s.cprof" % (f.func_name,))
865 cProfile.runctx('pw()', globals(), locals(), filename=fname)
872 This method allow you to debug your code without print
874 >>> def func_foo(bar)
882 This will output on the logger:
884 [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:baz = 42
885 [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:qnx = (42, 42)
887 To view the DEBUG lines in the logger you must start the server with the option
892 from inspect import stack
894 from pprint import pformat
896 param = re.split("debug *\((.+)\)", st[4][0].strip())[1].strip()
897 while param.count(')') > param.count('('): param = param[:param.rfind(')')]
900 what = "%s = %s" % (param, what)
901 netsvc.Logger().notifyChannel(st[3], netsvc.LOG_DEBUG, what)
904 icons = map(lambda x: (x,x), ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
905 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
906 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
907 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
908 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
909 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
910 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
911 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
912 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
913 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
914 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
915 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
916 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
917 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
918 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
919 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
920 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
921 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
922 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
923 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
924 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
925 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
926 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
927 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
928 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
929 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
932 def extract_zip_file(zip_file, outdirectory):
936 zf = zipfile.ZipFile(zip_file, 'r')
938 for path in zf.namelist():
939 tgt = os.path.join(out, path)
940 tgtdir = os.path.dirname(tgt)
941 if not os.path.exists(tgtdir):
944 if not tgt.endswith(os.sep):
946 fp.write(zf.read(path))
954 if __name__ == '__main__':
960 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: