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.
32 from config import config
39 if sys.version_info[:2] < (2, 4):
40 from threadinglocal import local
42 from threading import local
44 from itertools import izip
46 # initialize a database with base/base.sql
49 f = addons.get_module_resource('base', 'base.sql')
50 for line in file(f).read().split(';'):
51 if (len(line)>0) and (not line.isspace()):
55 for i in addons.get_modules():
56 terp_file = addons.get_module_resource(i, '__terp__.py')
57 mod_path = addons.get_module_path(i)
61 if os.path.isfile(terp_file) and not os.path.isfile(mod_path+'.zip'):
62 info = eval(file(terp_file).read())
63 elif zipfile.is_zipfile(mod_path+'.zip'):
64 zfile = zipfile.ZipFile(mod_path+'.zip')
65 i = os.path.splitext(i)[0]
66 info = eval(zfile.read(os.path.join(i, '__terp__.py')))
68 categs = info.get('category', 'Uncategorized').split('/')
72 cr.execute('select id \
73 from ir_module_category \
74 where name=%s and parent_id=%d', (categs[0], p_id))
76 cr.execute('select id \
77 from ir_module_category \
78 where name=%s and parent_id is NULL', (categs[0],))
81 cr.execute('select nextval(\'ir_module_category_id_seq\')')
82 c_id = cr.fetchone()[0]
83 cr.execute('insert into ir_module_category \
84 (id, name, parent_id) \
85 values (%d, %s, %d)', (c_id, categs[0], p_id))
91 active = info.get('active', False)
92 installable = info.get('installable', True)
99 state = 'uninstallable'
100 cr.execute('select nextval(\'ir_module_module_id_seq\')')
101 id = cr.fetchone()[0]
102 cr.execute('insert into ir_module_module \
103 (id, author, latest_version, website, name, shortdesc, description, \
104 category_id, state) \
105 values (%d, %s, %s, %s, %s, %s, %s, %d, %s)', (
106 id, info.get('author', ''),
107 release.version.rsplit('.', 1)[0] + '.' + info.get('version', ''),
108 info.get('website', ''), i, info.get('name', False),
109 info.get('description', ''), p_id, state))
110 dependencies = info.get('depends', [])
111 for d in dependencies:
112 cr.execute('insert into ir_module_module_dependency \
113 (module_id,name) values (%s, %s)', (id, d))
116 def find_in_path(name):
121 path = [dir for dir in os.environ['PATH'].split(sep)
122 if os.path.isdir(dir)]
124 val = os.path.join(dir, name)
125 if os.path.isfile(val) or os.path.islink(val):
129 def find_pg_tool(name):
130 if config['pg_path'] and config['pg_path'] != 'None':
131 return os.path.join(config['pg_path'], name)
133 return find_in_path(name)
135 def exec_pg_command(name, *args):
136 prog = find_pg_tool(name)
138 raise Exception('Couldn\'t find %s' % name)
139 args2 = (os.path.basename(prog),) + args
140 return os.spawnv(os.P_WAIT, prog, args2)
142 def exec_pg_command_pipe(name, *args):
143 prog = find_pg_tool(name)
145 raise Exception('Couldn\'t find %s' % name)
147 cmd = '"' + prog + '" ' + ' '.join(args)
149 cmd = prog + ' ' + ' '.join(args)
150 return os.popen2(cmd, 'b')
152 def exec_command_pipe(name, *args):
153 prog = find_in_path(name)
155 raise Exception('Couldn\'t find %s' % name)
157 cmd = '"'+prog+'" '+' '.join(args)
159 cmd = prog+' '+' '.join(args)
160 return os.popen2(cmd, 'b')
162 #----------------------------------------------------------
164 #----------------------------------------------------------
165 #file_path_root = os.getcwd()
166 #file_path_addons = os.path.join(file_path_root, 'addons')
168 def file_open(name, mode="r", subdir='addons', pathinfo=False):
169 """Open a file from the OpenERP root, using a subdir folder.
171 >>> file_open('hr/report/timesheer.xsl')
172 >>> file_open('addons/hr/report/timesheet.xsl')
173 >>> file_open('../../base/report/rml_template.xsl', subdir='addons/hr/report', pathinfo=True)
175 @param name: name of the file
176 @param mode: file open mode
177 @param subdir: subdirectory
178 @param pathinfo: if True returns tupple (fileobject, filepath)
180 @return: fileobject if pathinfo is False else (fileobject, filepath)
183 adp = os.path.normcase(os.path.abspath(config['addons_path']))
184 rtp = os.path.normcase(os.path.abspath(config['root_path']))
186 if name.replace(os.path.sep, '/').startswith('addons/'):
190 # First try to locate in addons_path
193 if subdir2.replace(os.path.sep, '/').startswith('addons/'):
194 subdir2 = subdir2[7:]
196 subdir2 = (subdir2 != 'addons' or None) and subdir2
200 fn = os.path.join(adp, subdir2, name)
202 fn = os.path.join(adp, name)
203 fn = os.path.normpath(fn)
204 fo = file_open(fn, mode=mode, subdir=None, pathinfo=pathinfo)
212 name = os.path.join(rtp, subdir, name)
214 name = os.path.join(rtp, name)
216 name = os.path.normpath(name)
218 # Check for a zipfile in the path
223 head, tail = os.path.split(head)
227 zipname = os.path.join(tail, zipname)
230 if zipfile.is_zipfile(head+'.zip'):
232 zfile = zipfile.ZipFile(head+'.zip')
234 fo = StringIO.StringIO(zfile.read(os.path.join(
235 os.path.basename(head), zipname).replace(
242 name2 = os.path.normpath(os.path.join(head + '.zip', zipname))
244 for i in (name2, name):
245 if i and os.path.isfile(i):
250 if os.path.splitext(name)[1] == '.rml':
251 raise IOError, 'Report %s doesn\'t exist or deleted : ' %str(name)
252 raise IOError, 'File not found : '+str(name)
255 def oswalksymlinks(top, topdown=True, onerror=None):
257 same as os.walk but follow symlinks
258 attention: all symlinks are walked before all normals directories
260 for dirpath, dirnames, filenames in os.walk(top, topdown, onerror):
262 yield dirpath, dirnames, filenames
264 symlinks = filter(lambda dirname: os.path.islink(os.path.join(dirpath, dirname)), dirnames)
266 for x in oswalksymlinks(os.path.join(dirpath, s), topdown, onerror):
270 yield dirpath, dirnames, filenames
272 #----------------------------------------------------------
274 #----------------------------------------------------------
276 """Flatten a list of elements into a uniqu list
277 Author: Christophe Simonis (christophe@tinyerp.com)
286 >>> flatten( [[], [[]]] )
288 >>> flatten( [[['a','b'], 'c'], 'd', ['e', [], 'f']] )
289 ['a', 'b', 'c', 'd', 'e', 'f']
290 >>> t = (1,2,(3,), [4, 5, [6, [7], (8, 9), ([10, 11, (12, 13)]), [14, [], (15,)], []]])
292 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
296 return hasattr(x, "__iter__")
301 map(r.append, flatten(e))
306 def reverse_enumerate(l):
307 """Like enumerate but in the other sens
308 >>> a = ['a', 'b', 'c']
309 >>> it = reverse_enumerate(a)
317 Traceback (most recent call last):
318 File "<stdin>", line 1, in <module>
321 return izip(xrange(len(l)-1, -1, -1), reversed(l))
323 #----------------------------------------------------------
325 #----------------------------------------------------------
326 def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, on_error=False, reply_to=False, tinycrm=False, ssl=False, debug=False,subtype='plain'):
333 from email.MIMEText import MIMEText
334 from email.MIMEMultipart import MIMEMultipart
335 from email.Header import Header
336 from email.Utils import formatdate, COMMASPACE
338 msg = MIMEText(body or '',_subtype=subtype,_charset='utf-8')
339 msg['Subject'] = Header(subject.decode('utf8'), 'utf-8')
340 msg['From'] = email_from
343 msg['Reply-To'] = msg['From']+', '+reply_to
344 msg['To'] = COMMASPACE.join(email_to)
346 msg['Cc'] = COMMASPACE.join(email_cc)
348 msg['Bcc'] = COMMASPACE.join(email_bcc)
349 msg['Date'] = formatdate(localtime=True)
351 msg['Message-Id'] = '<'+str(time.time())+'-tinycrm-'+str(tinycrm)+'@'+socket.gethostname()+'>'
357 s.connect(config['smtp_server'], config['smtp_port'])
363 if config['smtp_user'] or config['smtp_password']:
364 s.login(config['smtp_user'], config['smtp_password'])
365 s.sendmail(email_from, flatten([email_to, email_cc, email_bcc]), msg.as_string())
369 logging.getLogger().error(str(e))
373 #----------------------------------------------------------
375 #----------------------------------------------------------
376 def email_send_attach(email_from, email_to, subject, body, email_cc=None, email_bcc=None, on_error=False, reply_to=False, attach=None, tinycrm=False, ssl=False, debug=False):
385 from email.MIMEText import MIMEText
386 from email.MIMEBase import MIMEBase
387 from email.MIMEMultipart import MIMEMultipart
388 from email.Header import Header
389 from email.Utils import formatdate, COMMASPACE
390 from email import Encoders
392 msg = MIMEMultipart()
395 ssl = config.get('smtp_ssl', False)
397 msg['Subject'] = Header(subject.decode('utf8'), 'utf-8')
398 msg['From'] = email_from
401 msg['Reply-To'] = reply_to
402 msg['To'] = COMMASPACE.join(email_to)
404 msg['Cc'] = COMMASPACE.join(email_cc)
406 msg['Bcc'] = COMMASPACE.join(email_bcc)
408 msg['Message-Id'] = '<'+str(time.time())+'-tinycrm-'+str(tinycrm)+'@'+socket.gethostname()+'>'
409 msg['Date'] = formatdate(localtime=True)
410 msg.attach( MIMEText(body or '', _charset='utf-8', _subtype="html"))
411 for (fname,fcontent) in attach:
412 part = MIMEBase('application', "octet-stream")
413 part.set_payload( fcontent )
414 Encoders.encode_base64(part)
415 part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,))
422 s.connect(config['smtp_server'], config['smtp_port'])
428 if config['smtp_user'] or config['smtp_password']:
429 s.login(config['smtp_user'], config['smtp_password'])
430 s.sendmail(email_from, flatten([email_to, email_cc, email_bcc]), msg.as_string())
434 logging.getLogger().error(str(e))
439 #----------------------------------------------------------
441 #----------------------------------------------------------
442 # text must be latin-1 encoded
443 def sms_send(user, password, api_id, text, to):
445 params = urllib.urlencode({'user': user, 'password': password, 'api_id': api_id, 'text': text, 'to':to})
446 #f = urllib.urlopen("http://api.clickatell.com/http/sendmsg", params)
447 f = urllib.urlopen("http://196.7.150.220/http/sendmsg", params)
451 #---------------------------------------------------------
452 # Class that stores an updateable string (used in wizards)
453 #---------------------------------------------------------
454 class UpdateableStr(local):
456 def __init__(self, string=''):
460 return str(self.string)
463 return str(self.string)
465 def __nonzero__(self):
466 return bool(self.string)
469 class UpdateableDict(local):
470 '''Stores an updateable dict to use in wizards'''
472 def __init__(self, dict=None):
478 return str(self.dict)
481 return str(self.dict)
484 return self.dict.clear()
487 return self.dict.keys()
489 def __setitem__(self, i, y):
490 self.dict.__setitem__(i, y)
492 def __getitem__(self, i):
493 return self.dict.__getitem__(i)
496 return self.dict.copy()
499 return self.dict.iteritems()
502 return self.dict.iterkeys()
504 def itervalues(self):
505 return self.dict.itervalues()
507 def pop(self, k, d=None):
508 return self.dict.pop(k, d)
511 return self.dict.popitem()
513 def setdefault(self, k, d=None):
514 return self.dict.setdefault(k, d)
516 def update(self, E, **F):
517 return self.dict.update(E, F)
520 return self.dict.values()
522 def get(self, k, d=None):
523 return self.dict.get(k, d)
525 def has_key(self, k):
526 return self.dict.has_key(k)
529 return self.dict.items()
531 def __cmp__(self, y):
532 return self.dict.__cmp__(y)
534 def __contains__(self, k):
535 return self.dict.__contains__(k)
537 def __delitem__(self, y):
538 return self.dict.__delitem__(y)
541 return self.dict.__eq__(y)
544 return self.dict.__ge__(y)
546 def __getitem__(self, y):
547 return self.dict.__getitem__(y)
550 return self.dict.__gt__(y)
553 return self.dict.__hash__()
556 return self.dict.__iter__()
559 return self.dict.__le__(y)
562 return self.dict.__len__()
565 return self.dict.__lt__(y)
568 return self.dict.__ne__(y)
571 # Don't use ! Use res.currency.round()
572 class currency(float):
574 def __init__(self, value, accuracy=2, rounding=None):
576 rounding=10**-accuracy
577 self.rounding=rounding
578 self.accuracy=accuracy
580 def __new__(cls, value, accuracy=2, rounding=None):
581 return float.__new__(cls, round(value, accuracy))
584 # display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
585 # return str(display_value)
596 # Use it as a decorator of the function you plan to cache
597 # Timeout: 0 = no timeout, otherwise in seconds
600 def __init__(self, timeout=10000, skiparg=2):
601 self.timeout = timeout
604 def __call__(self, fn):
605 arg_names = inspect.getargspec(fn)[0][2:]
606 def cached_result(self2, cr=None, *args, **kwargs):
611 # Update named arguments with positional argument values
612 kwargs.update(dict(zip(arg_names, args)))
614 if isinstance(kwargs[k], (list, dict, set)):
615 kwargs[k] = tuple(kwargs[k])
616 elif not is_hashable(kwargs[k]):
617 kwargs[k] = repr(kwargs[k])
618 kwargs = kwargs.items()
621 # Work out key as a tuple of ('argname', value) pairs
622 key = (('dbname', cr.dbname),) + tuple(kwargs)
624 # Check cache and return cached value if possible
625 if key in self.cache:
626 (value, last_time) = self.cache[key]
627 mintime = time.time() - self.timeout
628 if self.timeout <= 0 or mintime <= last_time:
631 # Work out new value, cache it and return it
632 # FIXME Should copy() this value to avoid futur modifications of the cache ?
633 # FIXME What about exceptions ?
634 result = fn(self2,cr,**dict(kwargs))
636 self.cache[key] = (result, time.time())
641 return s.replace('&','&').replace('<','<').replace('>','>')
645 'bg_BG': u'Bulgarian / български',
646 'ca_ES': u'Catalan / Català',
647 'cs_CZ': u'Czech / Čeština',
648 'de_DE': u'German / Deutsch',
649 'en_CA': u'English (Canada)',
650 'en_EN': u'English (default)',
651 'en_GB': u'English (United Kingdom)',
652 'en_US': u'English (Unites States)',
653 'es_AR': u'Spanish (Argentina) / Español (República Argentina)',
654 'es_ES': u'Spanish / Español',
655 'et_ET': u'Estonian / Eesti keel',
656 'fr_BE': u'French (Belgium) / Français (Belgique)',
657 'fr_CH': u'French (Switzerland) / Français (Suisse)',
658 'fr_FR': u'French / Français',
659 'hr_HR': u'Croatian / hrvatski jezik',
660 'hu_HU': u'Hungarian / Magyar',
661 'it_IT': u'Italian / Italiano',
662 'lt_LT': u'Lithuanian / Lietuvių kalba',
663 'nl_NL': u'Dutch / Nederlands',
664 'pt_BR': u'Portugese (Federative Republic of Brazil) / português (República Federativa do Brasil)',
665 'pt_PT': u'Portugese / português',
666 'ro_RO': u'Romanian / limba română',
667 'ru_RU': u'Russian / русский язык',
668 'sl_SL': u'Slovenian / slovenščina',
669 'sv_SE': u'Swedish / svenska',
670 'uk_UK': u'Ukrainian / украї́нська мо́ва',
671 'zh_CN': u'Chinese (Simplified) / 简体中文' ,
672 'zh_TW': u'Chinese (Traditional) / 正體字',
676 def scan_languages():
678 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'))]
679 lang_dict = get_languages()
680 ret = [(lang, lang_dict.get(lang, lang)) for lang in file_list]
681 ret.sort(key=lambda k:k[1])
685 def get_user_companies(cr, user):
686 def _get_company_children(cr, ids):
689 cr.execute('SELECT id FROM res_company WHERE parent_id = any(array[%s])' %(','.join([str(x) for x in ids]),))
690 res=[x[0] for x in cr.fetchall()]
691 res.extend(_get_company_children(cr, res))
693 cr.execute('SELECT comp.id FROM res_company AS comp, res_users AS u WHERE u.id = %d AND comp.id = u.company_id' % (user,))
694 compids=[cr.fetchone()[0]]
695 compids.extend(_get_company_children(cr, compids))
700 Input number : account or invoice number
701 Output return: the same number completed with the recursive mod10
704 codec=[0,9,4,6,8,2,7,1,3,5]
710 report = codec[ (int(digit) + report) % 10 ]
711 return result + str((10 - report) % 10)
716 Return the size in a human readable format
720 units = ('bytes', 'Kb', 'Mb', 'Gb')
721 if isinstance(sz,basestring):
724 while s >= 1024 and i < len(units)-1:
727 return "%0.2f %s" % (s, units[i])
730 def log(f, res, *args, **kwargs):
731 vector = ['Call -> function: %s' % f]
732 for i, arg in enumerate(args):
733 vector.append( ' arg %02d: %r' % ( i, arg ) )
734 for key, value in kwargs.items():
735 vector.append( ' kwarg %10s: %r' % ( key, value ) )
736 vector.append( ' result: %r' % res )
737 print "\n".join(vector)
740 def wrapper(*args, **kwargs):
741 res = f(*args, **kwargs)
742 log(f, res, *args, **kwargs)
747 def wrapper(*args, **kwargs):
751 res = f(*args, **kwargs)
754 log(f, res, *args, **kwargs)
755 print " time delta: %s" % (time.time() - now)
759 return { "pre" : pre_logged, "post" : post_logged}[when]
761 raise ValueError(e), "must to be 'pre' or 'post'"
763 icons = map(lambda x: (x,x), ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
764 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
765 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
766 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
767 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
768 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
769 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
770 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
771 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
772 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
773 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
774 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
775 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
776 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
777 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
778 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
779 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
780 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
781 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
782 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
783 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
784 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
785 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
786 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
787 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
788 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
793 if __name__ == '__main__':
799 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: