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 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
390 #---------------------------------------------------------
391 # Class that stores an updateable string (used in wizards)
392 #---------------------------------------------------------
393 class UpdateableStr(local):
395 def __init__(self, string=''):
399 return str(self.string)
402 return str(self.string)
404 def __nonzero__(self):
405 return bool(self.string)
408 class UpdateableDict(local):
409 '''Stores an updateable dict to use in wizards'''
411 def __init__(self, dict=None):
417 return str(self.dict)
420 return str(self.dict)
423 return self.dict.clear()
426 return self.dict.keys()
428 def __setitem__(self, i, y):
429 self.dict.__setitem__(i, y)
431 def __getitem__(self, i):
432 return self.dict.__getitem__(i)
435 return self.dict.copy()
438 return self.dict.iteritems()
441 return self.dict.iterkeys()
443 def itervalues(self):
444 return self.dict.itervalues()
446 def pop(self, k, d=None):
447 return self.dict.pop(k, d)
450 return self.dict.popitem()
452 def setdefault(self, k, d=None):
453 return self.dict.setdefault(k, d)
455 def update(self, E, **F):
456 return self.dict.update(E, F)
459 return self.dict.values()
461 def get(self, k, d=None):
462 return self.dict.get(k, d)
464 def has_key(self, k):
465 return self.dict.has_key(k)
468 return self.dict.items()
470 def __cmp__(self, y):
471 return self.dict.__cmp__(y)
473 def __contains__(self, k):
474 return self.dict.__contains__(k)
476 def __delitem__(self, y):
477 return self.dict.__delitem__(y)
480 return self.dict.__eq__(y)
483 return self.dict.__ge__(y)
485 def __getitem__(self, y):
486 return self.dict.__getitem__(y)
489 return self.dict.__gt__(y)
492 return self.dict.__hash__()
495 return self.dict.__iter__()
498 return self.dict.__le__(y)
501 return self.dict.__len__()
504 return self.dict.__lt__(y)
507 return self.dict.__ne__(y)
510 # Don't use ! Use res.currency.round()
511 class currency(float):
513 def __init__(self, value, accuracy=2, rounding=None):
515 rounding=10**-accuracy
516 self.rounding=rounding
517 self.accuracy=accuracy
519 def __new__(cls, value, accuracy=2, rounding=None):
520 return float.__new__(cls, round(value, accuracy))
523 # display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
524 # return str(display_value)
536 Use it as a decorator of the function you plan to cache
537 Timeout: 0 = no timeout, otherwise in seconds
542 def __init__(self, timeout=None, skiparg=2, multi=None):
543 assert skiparg >= 2 # at least self and cr
545 self.timeout = config['cache_timeout']
547 self.timeout = timeout
548 self.skiparg = skiparg
550 self.lasttime = time.time()
553 cache.__caches.append(self)
556 def _generate_keys(self, dbname, kwargs2):
558 Generate keys depending of the arguments and the self.mutli value
563 i.sort(key=lambda (x,y): x)
567 key = (('dbname', dbname),) + to_tuple(kwargs2)
570 multis = kwargs2[self.multi][:]
572 kwargs2[self.multi] = (id,)
573 key = (('dbname', dbname),) + to_tuple(kwargs2)
576 def _unify_args(self, *args, **kwargs):
577 # Update named arguments with positional argument values (without self and cr)
578 kwargs2 = self.fun_default_values.copy()
579 kwargs2.update(kwargs)
580 kwargs2.update(dict(zip(self.fun_arg_names, args[self.skiparg-2:])))
582 if isinstance(kwargs2[k], (list, dict, set)):
583 kwargs2[k] = tuple(kwargs2[k])
584 elif not is_hashable(kwargs2[k]):
585 kwargs2[k] = repr(kwargs2[k])
589 def clear(self, dbname, *args, **kwargs):
590 """clear the cache for database dbname
591 if *args and **kwargs are both empty, clear all the keys related to this database
593 if not args and not kwargs:
594 keys_to_del = [key for key in self.cache if key[0][1] == dbname]
596 kwargs2 = self._unify_args(*args, **kwargs)
597 keys_to_del = [key for key, _ in self._generate_keys(dbname, kwargs2) if key in self.cache]
599 for key in keys_to_del:
603 def clean_caches_for_db(cls, dbname):
604 for c in cls.__caches:
607 def __call__(self, fn):
608 if self.fun is not None:
609 raise Exception("Can not use a cache instance on more than one function")
612 argspec = inspect.getargspec(fn)
613 self.fun_arg_names = argspec[0][self.skiparg:]
614 self.fun_default_values = {}
616 self.fun_default_values = dict(zip(self.fun_arg_names[-len(argspec[3]):], argspec[3]))
618 def cached_result(self2, cr, *args, **kwargs):
619 if time.time()-self.timeout > self.lasttime:
620 self.lasttime = time.time()
621 t = time.time()-self.timeout
622 for key in self.cache.keys():
623 if self.cache[key][1]<t:
626 kwargs2 = self._unify_args(*args, **kwargs)
630 for key, id in self._generate_keys(cr.dbname, kwargs2):
631 if key in self.cache:
632 result[id] = self.cache[key][0]
638 kwargs2[self.multi] = notincache.keys()
640 result2 = fn(self2, cr, *args[2:self.skiparg], **kwargs2)
642 key = notincache[None]
643 self.cache[key] = (result2, time.time())
644 result[None] = result2
648 self.cache[key] = (result2[id], time.time())
649 result.update(result2)
655 cached_result.clear_cache = self.clear
659 return s.replace('&','&').replace('<','<').replace('>','>')
662 """This method is similar to the builtin `str` method, except
663 it will return Unicode string.
665 @param value: the value to convert
668 @return: unicode string
671 if isinstance(value, unicode):
674 if hasattr(value, '__unicode__'):
675 return unicode(value)
677 if not isinstance(value, str):
680 try: # first try utf-8
681 return unicode(value, 'utf-8')
685 try: # then extened iso-8858
686 return unicode(value, 'iso-8859-15')
690 # else use default system locale
691 from locale import getlocale
692 return unicode(value, getlocale()[1])
694 def exception_to_unicode(e):
695 if hasattr(e, 'message'):
696 return ustr(e.message)
697 if hasattr(e, 'args'):
698 return "\n".join((ustr(a) for a in e.args))
702 return u"Unknow message"
705 # to be compatible with python 2.4
707 if not hasattr(__builtin__, 'all'):
709 for element in iterable:
714 __builtin__.all = all
717 if not hasattr(__builtin__, 'any'):
719 for element in iterable:
724 __builtin__.any = any
731 'ar_AR': u'Arabic / الْعَرَبيّة',
732 'bg_BG': u'Bulgarian / български',
733 'bs_BS': u'Bosnian / bosanski jezik',
734 'ca_ES': u'Catalan / Català',
735 'cs_CZ': u'Czech / Čeština',
736 'da_DK': u'Danish / Dansk',
737 'de_DE': u'German / Deutsch',
738 'el_GR': u'Greek / Ελληνικά',
739 'en_CA': u'English (CA)',
740 'en_EN': u'English (default)',
741 'en_GB': u'English (UK)',
742 'en_US': u'English (US)',
743 'es_AR': u'Spanish (AR) / Español (AR)',
744 'es_ES': u'Spanish / Español',
745 'et_EE': u'Estonian / Eesti keel',
746 'fr_BE': u'French (BE) / Français (BE)',
747 'fr_CH': u'French (CH) / Français (CH)',
748 'fr_FR': u'French / Français',
749 'hr_HR': u'Croatian / hrvatski jezik',
750 'hu_HU': u'Hungarian / Magyar',
751 'id_ID': u'Indonesian / Bahasa Indonesia',
752 'it_IT': u'Italian / Italiano',
753 'lt_LT': u'Lithuanian / Lietuvių kalba',
754 'nl_NL': u'Dutch / Nederlands',
755 'nl_BE': u'Dutch (Belgium) / Nederlands (Belgïe)',
756 'pl_PL': u'Polish / Język polski',
757 'pt_BR': u'Portugese (BR) / português (BR)',
758 'pt_PT': u'Portugese / português',
759 'ro_RO': u'Romanian / limba română',
760 'ru_RU': u'Russian / русский язык',
761 'sl_SL': u'Slovenian / slovenščina',
762 'sv_SE': u'Swedish / svenska',
763 'tr_TR': u'Turkish / Türkçe',
764 'uk_UK': u'Ukrainian / украї́нська мо́ва',
765 'zh_CN': u'Chinese (CN) / 简体中文' ,
766 'zh_TW': u'Chinese (TW) / 正體字',
770 def scan_languages():
772 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'))]
773 lang_dict = get_languages()
774 ret = [(lang, lang_dict.get(lang, lang)) for lang in file_list]
775 ret.sort(key=lambda k:k[1])
779 def get_user_companies(cr, user):
780 def _get_company_children(cr, ids):
783 cr.execute('SELECT id FROM res_company WHERE parent_id = any(array[%s])' %(','.join([str(x) for x in ids]),))
784 res=[x[0] for x in cr.fetchall()]
785 res.extend(_get_company_children(cr, res))
787 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,))
788 compids=[cr.fetchone()[0]]
789 compids.extend(_get_company_children(cr, compids))
794 Input number : account or invoice number
795 Output return: the same number completed with the recursive mod10
798 codec=[0,9,4,6,8,2,7,1,3,5]
804 report = codec[ (int(digit) + report) % 10 ]
805 return result + str((10 - report) % 10)
810 Return the size in a human readable format
814 units = ('bytes', 'Kb', 'Mb', 'Gb')
815 if isinstance(sz,basestring):
818 while s >= 1024 and i < len(units)-1:
821 return "%0.2f %s" % (s, units[i])
824 from tools.func import wraps
827 def wrapper(*args, **kwargs):
829 from pprint import pformat
831 vector = ['Call -> function: %r' % f]
832 for i, arg in enumerate(args):
833 vector.append(' arg %02d: %s' % (i, pformat(arg)))
834 for key, value in kwargs.items():
835 vector.append(' kwarg %10s: %s' % (key, pformat(value)))
838 res = f(*args, **kwargs)
840 vector.append(' result: %s' % pformat(res))
841 vector.append(' time delta: %s' % (time.time() - timeb4))
842 netsvc.Logger().notifyChannel('logged', netsvc.LOG_DEBUG, '\n'.join(vector))
847 class profile(object):
848 def __init__(self, fname=None):
851 def __call__(self, f):
852 from tools.func import wraps
855 def wrapper(*args, **kwargs):
856 class profile_wrapper(object):
860 self.result = f(*args, **kwargs)
861 pw = profile_wrapper()
863 fname = self.fname or ("%s.cprof" % (f.func_name,))
864 cProfile.runctx('pw()', globals(), locals(), filename=fname)
871 This method allow you to debug your code without print
873 >>> def func_foo(bar)
881 This will output on the logger:
883 [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:baz = 42
884 [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:qnx = (42, 42)
886 To view the DEBUG lines in the logger you must start the server with the option
891 from inspect import stack
893 from pprint import pformat
895 param = re.split("debug *\((.+)\)", st[4][0].strip())[1].strip()
896 while param.count(')') > param.count('('): param = param[:param.rfind(')')]
899 what = "%s = %s" % (param, what)
900 netsvc.Logger().notifyChannel(st[3], netsvc.LOG_DEBUG, what)
903 icons = map(lambda x: (x,x), ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
904 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
905 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
906 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
907 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
908 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
909 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
910 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
911 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
912 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
913 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
914 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
915 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
916 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
917 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
918 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
919 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
920 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
921 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
922 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
923 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
924 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
925 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
926 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
927 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
928 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
931 def extract_zip_file(zip_file, outdirectory):
935 zf = zipfile.ZipFile(zip_file, 'r')
937 for path in zf.namelist():
938 tgt = os.path.join(out, path)
939 tgtdir = os.path.dirname(tgt)
940 if not os.path.exists(tgtdir):
943 if not tgt.endswith(os.sep):
945 fp.write(zf.read(path))
953 if __name__ == '__main__':
959 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: