1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2008 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(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) 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')))
65 categs = info.get('category', 'Uncategorized').split('/')
69 cr.execute('select id \
70 from ir_module_category \
71 where name=%s and parent_id=%s', (categs[0], p_id))
73 cr.execute('select id \
74 from ir_module_category \
75 where name=%s and parent_id is NULL', (categs[0],))
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))
88 active = info.get('active', False)
89 installable = info.get('installable', True)
96 state = 'uninstallable'
97 cr.execute('select nextval(\'ir_module_module_id_seq\')')
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))
112 def find_in_path(name):
117 path = [dir for dir in os.environ['PATH'].split(sep)
118 if os.path.isdir(dir)]
120 val = os.path.join(dir, name)
121 if os.path.isfile(val) or os.path.islink(val):
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)
129 return find_in_path(name)
131 def exec_pg_command(name, *args):
132 prog = find_pg_tool(name)
134 raise Exception('Couldn\'t find %s' % name)
135 args2 = (os.path.basename(prog),) + args
136 return os.spawnv(os.P_WAIT, prog, args2)
138 def exec_pg_command_pipe(name, *args):
139 prog = find_pg_tool(name)
141 raise Exception('Couldn\'t find %s' % name)
143 cmd = '"' + prog + '" ' + ' '.join(args)
145 cmd = prog + ' ' + ' '.join(args)
146 return os.popen2(cmd, 'b')
148 def exec_command_pipe(name, *args):
149 prog = find_in_path(name)
151 raise Exception('Couldn\'t find %s' % name)
153 cmd = '"'+prog+'" '+' '.join(args)
155 cmd = prog+' '+' '.join(args)
156 return os.popen2(cmd, 'b')
158 #----------------------------------------------------------
160 #----------------------------------------------------------
161 #file_path_root = os.getcwd()
162 #file_path_addons = os.path.join(file_path_root, 'addons')
164 def file_open(name, mode="r", subdir='addons', pathinfo=False):
165 """Open a file from the OpenERP root, using a subdir folder.
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)
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)
176 @return: fileobject if pathinfo is False else (fileobject, filepath)
179 adp = os.path.normcase(os.path.abspath(config['addons_path']))
180 rtp = os.path.normcase(os.path.abspath(config['root_path']))
182 if name.replace(os.path.sep, '/').startswith('addons/'):
186 # First try to locate in addons_path
189 if subdir2.replace(os.path.sep, '/').startswith('addons/'):
190 subdir2 = subdir2[7:]
192 subdir2 = (subdir2 != 'addons' or None) and subdir2
196 fn = os.path.join(adp, subdir2, name)
198 fn = os.path.join(adp, name)
199 fn = os.path.normpath(fn)
200 fo = file_open(fn, mode=mode, subdir=None, pathinfo=pathinfo)
208 name = os.path.join(rtp, subdir, name)
210 name = os.path.join(rtp, name)
212 name = os.path.normpath(name)
214 # Check for a zipfile in the path
219 head, tail = os.path.split(head)
223 zipname = os.path.join(tail, zipname)
226 if zipfile.is_zipfile(head+'.zip'):
228 zfile = zipfile.ZipFile(head+'.zip')
230 fo = StringIO.StringIO(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 clean_cache_for_db(cls, dbname):
557 def get_dbname_from_key(key):
563 for cache in cls.__caches:
564 keys_to_del = [key for key in cache.cache if get_dbname_from_key(key) == dbname]
565 for key in keys_to_del:
568 def __call__(self, fn):
569 arg_names = inspect.getargspec(fn)[0][self.skiparg:]
571 def cached_result(self2, cr=None, *args, **kwargs):
572 if time.time()-self.timeout > self.lasttime:
573 self.lasttime = time.time()
574 t = time.time()-self.timeout
575 for key in self.cache.keys():
576 if self.cache[key][1]<t:
582 if ('clear_keys' in kwargs):
583 if (kwargs['clear_keys'] in self.cache):
584 del self.cache[kwargs['clear_keys']]
587 # Update named arguments with positional argument values (without self and cr)
588 kwargs2 = kwargs.copy()
589 kwargs2.update(dict(zip(arg_names, args[self.skiparg-2:])))
591 if isinstance(kwargs2[k], (list, dict, set)):
592 kwargs2[k] = tuple(kwargs2[k])
593 elif not is_hashable(kwargs2[k]):
594 kwargs2[k] = repr(kwargs2[k])
597 kwargs3 = kwargs2.copy()
600 for id in kwargs3[self.multi]:
601 kwargs2[self.multi] = [id]
602 kwargs4 = kwargs2.items()
605 # Work out key as a tuple of ('argname', value) pairs
606 key = (('dbname', cr.dbname),) + tuple(kwargs4)
607 if key in self.cache:
608 result[id] = self.cache[key][0]
610 notincache.append(id)
615 kwargs2[self.multi] = notincache
616 result2 = fn(self2, cr, *args[2:self.skip], **kwargs3)
618 kwargs2[self.multi] = [id]
619 kwargs4 = kwargs2.items()
621 key = (('dbname', cr.dbname),) + tuple(kwargs4)
622 self.cache[key] = result2[id]
623 result.updat(result2)
626 kwargs2 = kwargs2.items()
629 key = (('dbname', cr.dbname),) + tuple(kwargs2)
630 if key in self.cache:
631 return self.cache[key][0]
633 result = fn(self2, cr, *args, **kwargs)
635 self.cache[key] = (result, time.time())
640 return s.replace('&','&').replace('<','<').replace('>','>')
643 """This method is similar to the builtin `str` method, except
644 it will return Unicode string.
646 @param value: the value to convert
649 @return: unicode string
652 if (value is None) or (value is False):
656 if isinstance(value, unicode):
659 if hasattr(value, '__unicode__'):
660 return unicode(value)
662 if not isinstance(value, str):
665 return unicode(value, 'utf-8')
670 'ar_AR': u'Arabic / الْعَرَبيّة',
671 'bg_BG': u'Bulgarian / български',
672 'ca_ES': u'Catalan / Català',
673 'cs_CZ': u'Czech / Čeština',
674 'de_DE': u'German / Deutsch',
675 'en_CA': u'English (CA)',
676 'en_EN': u'English (default)',
677 'en_GB': u'English (UK)',
678 'en_US': u'English (US)',
679 'es_AR': u'Spanish (AR) / Español (AR)',
680 'es_ES': u'Spanish / Español',
681 'et_ET': u'Estonian / Eesti keel',
682 'fr_BE': u'French (BE) / Français (BE)',
683 'fr_CH': u'French (CH) / Français (CH)',
684 'fr_FR': u'French / Français',
685 'hr_HR': u'Croatian / hrvatski jezik',
686 'hu_HU': u'Hungarian / Magyar',
687 'it_IT': u'Italian / Italiano',
688 'lt_LT': u'Lithuanian / Lietuvių kalba',
689 'nl_NL': u'Dutch / Nederlands',
690 'pl_PL': u'Polish / Język polski',
691 'pt_BR': u'Portugese (BR) / português (BR)',
692 'pt_PT': u'Portugese / português',
693 'ro_RO': u'Romanian / limba română',
694 'ru_RU': u'Russian / русский язык',
695 'sl_SL': u'Slovenian / slovenščina',
696 'sv_SE': u'Swedish / svenska',
697 'tr_TR': u'Turkish / Türkçe',
698 'uk_UK': u'Ukrainian / украї́нська мо́ва',
699 'zh_CN': u'Chinese (CN) / 简体中文' ,
700 'zh_TW': u'Chinese (TW) / 正體字',
704 def scan_languages():
706 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'))]
707 lang_dict = get_languages()
708 ret = [(lang, lang_dict.get(lang, lang)) for lang in file_list]
709 ret.sort(key=lambda k:k[1])
713 def get_user_companies(cr, user):
714 def _get_company_children(cr, ids):
717 cr.execute('SELECT id FROM res_company WHERE parent_id = any(array[%s])' %(','.join([str(x) for x in ids]),))
718 res=[x[0] for x in cr.fetchall()]
719 res.extend(_get_company_children(cr, res))
721 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,))
722 compids=[cr.fetchone()[0]]
723 compids.extend(_get_company_children(cr, compids))
728 Input number : account or invoice number
729 Output return: the same number completed with the recursive mod10
732 codec=[0,9,4,6,8,2,7,1,3,5]
738 report = codec[ (int(digit) + report) % 10 ]
739 return result + str((10 - report) % 10)
744 Return the size in a human readable format
748 units = ('bytes', 'Kb', 'Mb', 'Gb')
749 if isinstance(sz,basestring):
752 while s >= 1024 and i < len(units)-1:
755 return "%0.2f %s" % (s, units[i])
758 from tools.func import wraps
761 def wrapper(*args, **kwargs):
763 from pprint import pformat
765 vector = ['Call -> function: %r' % f]
766 for i, arg in enumerate(args):
767 vector.append(' arg %02d: %s' % (i, pformat(arg)))
768 for key, value in kwargs.items():
769 vector.append(' kwarg %10s: %s' % (key, pformat(value)))
772 res = f(*args, **kwargs)
774 vector.append(' result: %s' % pformat(res))
775 vector.append(' time delta: %s' % (time.time() - timeb4))
776 netsvc.Logger().notifyChannel('logged', netsvc.LOG_DEBUG, '\n'.join(vector))
783 This method allow you to debug your code without print
785 >>> def func_foo(bar)
793 This will output on the logger:
795 [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:baz = 42
796 [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:qnx = (42, 42)
798 To view the DEBUG lines in the logger you must start the server with the option
803 from inspect import stack
805 from pprint import pformat
807 param = re.split("debug *\((.+)\)", st[4][0].strip())[1].strip()
808 while param.count(')') > param.count('('): param = param[:param.rfind(')')]
811 what = "%s = %s" % (param, what)
812 netsvc.Logger().notifyChannel(st[3], netsvc.LOG_DEBUG, what)
815 icons = map(lambda x: (x,x), ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
816 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
817 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
818 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
819 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
820 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
821 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
822 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
823 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
824 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
825 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
826 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
827 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
828 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
829 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
830 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
831 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
832 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
833 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
834 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
835 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
836 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
837 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
838 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
839 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
840 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
845 if __name__ == '__main__':
851 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: