Add an option "Tax calculation rounding method" that can be set to "Round per line...
[odoo/odoo.git] / openerp / modules / module.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #    Copyright (C) 2010-2011 OpenERP s.a. (<http://openerp.com>).
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU Affero General Public License as
10 #    published by the Free Software Foundation, either version 3 of the
11 #    License, or (at your option) any later version.
12 #
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 Affero General Public License for more details.
17 #
18 #    You should have received a copy of the GNU Affero General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 import imp
24 import itertools
25 import os
26 from os.path import join as opj
27 import sys
28 import types
29 import zipimport
30
31 import openerp
32
33 import openerp.osv as osv
34 import openerp.tools as tools
35 import openerp.tools.osutil as osutil
36 from openerp.tools.safe_eval import safe_eval as eval
37 from openerp.tools.translate import _
38
39 import openerp.netsvc as netsvc
40
41 import zipfile
42 import openerp.release as release
43
44 import re
45 import base64
46 from zipfile import PyZipFile, ZIP_DEFLATED
47 from cStringIO import StringIO
48
49 import logging
50
51 import openerp.modules.db
52 import openerp.modules.graph
53
54 _logger = logging.getLogger(__name__)
55
56 _ad = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons') # default addons path (base)
57 ad_paths = []
58
59 # Modules already loaded
60 loaded = []
61
62 _logger = logging.getLogger(__name__)
63
64 class AddonsImportHook(object):
65     """
66     Import hook to load OpenERP addons from multiple paths.
67
68     OpenERP implements its own import-hook to load its addons. OpenERP
69     addons are Python modules. Originally, they were each living in their
70     own top-level namespace, e.g. the sale module, or the hr module. For
71     backward compatibility, `import <module>` is still supported. Now they
72     are living in `openerp.addons`. The good way to import such modules is
73     thus `import openerp.addons.module`.
74
75     For backward compatibility, loading an addons puts it in `sys.modules`
76     under both the legacy (short) name, and the new (longer) name. This
77     ensures that
78         import hr
79         import openerp.addons.hr
80     loads the hr addons only once.
81
82     When an OpenERP addons name clashes with some other installed Python
83     module (for instance this is the case of the `resource` addons),
84     obtaining the OpenERP addons is only possible with the long name. The
85     short name will give the expected Python module.
86
87     Instead of relying on some addons path, an alternative approach would be
88     to use pkg_resources entry points from already installed Python libraries
89     (and install our addons as such). Even when implemented, we would still
90     have to support the addons path approach for backward compatibility.
91     """
92
93     def find_module(self, module_name, package_path):
94         module_parts = module_name.split('.')
95         if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
96             return self # We act as a loader too.
97
98         # TODO list of loadable modules can be cached instead of always
99         # calling get_module_path().
100         if len(module_parts) == 1 and \
101             get_module_path(module_parts[0],
102                 display_warning=False):
103             try:
104                 # Check if the bare module name clashes with another module.
105                 f, path, descr = imp.find_module(module_parts[0])
106                 _logger.warning("""
107 Ambiguous import: the OpenERP module `%s` is shadowed by another
108 module (available at %s).
109 To import it, use `import openerp.addons.<module>.`.""" % (module_name, path))
110                 return
111             except ImportError, e:
112                 # Using `import <module_name>` instead of
113                 # `import openerp.addons.<module_name>` is ugly but not harmful
114                 # and kept for backward compatibility.
115                 return self # We act as a loader too.
116
117     def load_module(self, module_name):
118
119         module_parts = module_name.split('.')
120         if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
121             module_part = module_parts[2]
122             if module_name in sys.modules:
123                 return sys.modules[module_name]
124
125         if len(module_parts) == 1:
126             module_part = module_parts[0]
127             if module_part in sys.modules:
128                 return sys.modules[module_part]
129
130         try:
131             # Check if the bare module name shadows another module.
132             f, path, descr = imp.find_module(module_part)
133             is_shadowing = True
134         except ImportError, e:
135             # Using `import <module_name>` instead of
136             # `import openerp.addons.<module_name>` is ugly but not harmful
137             # and kept for backward compatibility.
138             is_shadowing = False
139
140         # Note: we don't support circular import.
141         f, path, descr = imp.find_module(module_part, ad_paths)
142         mod = imp.load_module('openerp.addons.' + module_part, f, path, descr)
143         if not is_shadowing:
144             sys.modules[module_part] = mod
145             for k in sys.modules.keys():
146                 if k.startswith('openerp.addons.' + module_part):
147                     sys.modules[k[len('openerp.addons.'):]] = sys.modules[k]
148         sys.modules['openerp.addons.' + module_part] = mod
149         return mod
150
151 def initialize_sys_path():
152     """
153     Setup an import-hook to be able to import OpenERP addons from the different
154     addons paths.
155
156     This ensures something like ``import crm`` (or even
157     ``import openerp.addons.crm``) works even if the addons are not in the
158     PYTHONPATH.
159     """
160     global ad_paths
161     if ad_paths:
162         return
163
164     ad_paths = map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(','))
165     ad_paths.append(_ad) # for get_module_path
166     sys.meta_path.append(AddonsImportHook())
167
168 def get_module_path(module, downloaded=False, display_warning=True):
169     """Return the path of the given module.
170
171     Search the addons paths and return the first path where the given
172     module is found. If downloaded is True, return the default addons
173     path if nothing else is found.
174
175     """
176     initialize_sys_path()
177     for adp in ad_paths:
178         if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
179             return opj(adp, module)
180
181     if downloaded:
182         return opj(_ad, module)
183     if display_warning:
184         _logger.warning('module %s: module not found', module)
185     return False
186
187
188 def get_module_filetree(module, dir='.'):
189     path = get_module_path(module)
190     if not path:
191         return False
192
193     dir = os.path.normpath(dir)
194     if dir == '.':
195         dir = ''
196     if dir.startswith('..') or (dir and dir[0] == '/'):
197         raise Exception('Cannot access file outside the module')
198
199     if not os.path.isdir(path):
200         # zipmodule
201         zip = zipfile.ZipFile(path + ".zip")
202         files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
203     else:
204         files = osutil.listdir(path, True)
205
206     tree = {}
207     for f in files:
208         if not f.startswith(dir):
209             continue
210
211         if dir:
212             f = f[len(dir)+int(not dir.endswith('/')):]
213         lst = f.split(os.sep)
214         current = tree
215         while len(lst) != 1:
216             current = current.setdefault(lst.pop(0), {})
217         current[lst.pop(0)] = None
218
219     return tree
220
221 def zip_directory(directory, b64enc=True, src=True):
222     """Compress a directory
223
224     @param directory: The directory to compress
225     @param base64enc: if True the function will encode the zip file with base64
226     @param src: Integrate the source files
227
228     @return: a string containing the zip file
229     """
230
231     RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
232
233     def _zippy(archive, path, src=True):
234         path = os.path.abspath(path)
235         base = os.path.basename(path)
236         for f in osutil.listdir(path, True):
237             bf = os.path.basename(f)
238             if not RE_exclude.search(bf) and (src or bf in ('__openerp__.py', '__terp__.py') or not bf.endswith('.py')):
239                 archive.write(os.path.join(path, f), os.path.join(base, f))
240
241     archname = StringIO()
242     archive = PyZipFile(archname, "w", ZIP_DEFLATED)
243
244     # for Python 2.5, ZipFile.write() still expects 8-bit strings (2.6 converts to utf-8)
245     directory = tools.ustr(directory).encode('utf-8')
246
247     archive.writepy(directory)
248     _zippy(archive, directory, src=src)
249     archive.close()
250     archive_data = archname.getvalue()
251     archname.close()
252
253     if b64enc:
254         return base64.encodestring(archive_data)
255
256     return archive_data
257
258 def get_module_as_zip(modulename, b64enc=True, src=True):
259     """Generate a module as zip file with the source or not and can do a base64 encoding
260
261     @param modulename: The module name
262     @param b64enc: if True the function will encode the zip file with base64
263     @param src: Integrate the source files
264
265     @return: a stream to store in a file-like object
266     """
267
268     ap = get_module_path(str(modulename))
269     if not ap:
270         raise Exception('Unable to find path for module %s' % modulename)
271
272     ap = ap.encode('utf8')
273     if os.path.isfile(ap + '.zip'):
274         val = file(ap + '.zip', 'rb').read()
275         if b64enc:
276             val = base64.encodestring(val)
277     else:
278         val = zip_directory(ap, b64enc, src)
279
280     return val
281
282
283 def get_module_resource(module, *args):
284     """Return the full path of a resource of the given module.
285
286     @param module: the module
287     @param args: the resource path components
288
289     @return: absolute path to the resource
290
291     TODO name it get_resource_path
292     TODO make it available inside on osv object (self.get_resource_path)
293     """
294     a = get_module_path(module)
295     if not a: return False
296     resource_path = opj(a, *args)
297     if zipfile.is_zipfile( a +'.zip') :
298         zip = zipfile.ZipFile( a + ".zip")
299         files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
300         resource_path = '/'.join(args)
301         if resource_path in files:
302             return opj(a, resource_path)
303     elif os.path.exists(resource_path):
304         return resource_path
305     return False
306
307 def get_module_icon(module):
308     iconpath = ['static', 'src', 'img', 'icon.png']
309     if get_module_resource(module, *iconpath):
310         return ('/' + module + '/') + '/'.join(iconpath)
311     return '/base/'  + '/'.join(iconpath)
312
313 def load_information_from_description_file(module):
314     """
315     :param module: The name of the module (sale, purchase, ...)
316     """
317
318     terp_file = get_module_resource(module, '__openerp__.py')
319     if not terp_file:
320         terp_file = get_module_resource(module, '__terp__.py')
321     mod_path = get_module_path(module)
322     if terp_file:
323         info = {}
324         if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
325             # default values for descriptor
326             info = {
327                 'application': False,
328                 'author': '',
329                 'auto_install': False,
330                 'category': 'Uncategorized',
331                 'certificate': None,
332                 'depends': [],
333                 'description': '',
334                 'icon': get_module_icon(module),
335                 'installable': True,
336                 'auto_install': False,
337                 'license': 'AGPL-3',
338                 'name': False,
339                 'post_load': None,
340                 'version': '0.0.0',
341                 'web': False,
342                 'website': '',
343                 'sequence': 100,
344             }
345             info.update(itertools.izip(
346                 'depends data demo test init_xml update_xml demo_xml'.split(),
347                 iter(list, None)))
348
349             f = tools.file_open(terp_file)
350             try:
351                 info.update(eval(f.read()))
352             finally:
353                 f.close()
354
355             if 'active' in info:
356                 # 'active' has been renamed 'auto_install'
357                 info['auto_install'] = info['active']
358
359             return info
360
361     #TODO: refactor the logger in this file to follow the logging guidelines
362     #      for 6.0
363     _logger.debug('module %s: no descriptor file'
364         ' found: __openerp__.py or __terp__.py (deprecated)', module)
365     return {}
366
367
368 def init_module_models(cr, module_name, obj_list):
369     """ Initialize a list of models.
370
371     Call _auto_init and init on each model to create or update the
372     database tables supporting the models.
373
374     TODO better explanation of _auto_init and init.
375
376     """
377     _logger.info('module %s: creating or updating database tables', module_name)
378     todo = []
379     for obj in obj_list:
380         result = obj._auto_init(cr, {'module': module_name})
381         if result:
382             todo += result
383         if hasattr(obj, 'init'):
384             obj.init(cr)
385         cr.commit()
386     for obj in obj_list:
387         obj._auto_end(cr, {'module': module_name})
388         cr.commit()
389     todo.sort()
390     for t in todo:
391         t[1](cr, *t[2])
392     cr.commit()
393
394 def load_openerp_module(module_name):
395     """ Load an OpenERP module, if not already loaded.
396
397     This loads the module and register all of its models, thanks to either
398     the MetaModel metaclass, or the explicit instantiation of the model.
399     This is also used to load server-wide module (i.e. it is also used
400     when there is no model to register).
401     """
402     global loaded
403     if module_name in loaded:
404         return
405
406     initialize_sys_path()
407     try:
408         mod_path = get_module_path(module_name)
409         zip_mod_path = mod_path + '.zip'
410         if not os.path.isfile(zip_mod_path):
411             __import__('openerp.addons.' + module_name)
412         else:
413             zimp = zipimport.zipimporter(zip_mod_path)
414             zimp.load_module(module_name)
415
416         # Call the module's post-load hook. This can done before any model or
417         # data has been initialized. This is ok as the post-load hook is for
418         # server-wide (instead of registry-specific) functionalities.
419         info = load_information_from_description_file(module_name)
420         if info['post_load']:
421             getattr(sys.modules['openerp.addons.' + module_name], info['post_load'])()
422
423     except Exception, e:
424         mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
425         msg = "Couldn't load %smodule %s" % (mt, module_name)
426         _logger.critical(msg)
427         _logger.critical(e)
428         raise
429     else:
430         loaded.append(module_name)
431
432 def get_modules():
433     """Returns the list of module names
434     """
435     def listdir(dir):
436         def clean(name):
437             name = os.path.basename(name)
438             if name[-4:] == '.zip':
439                 name = name[:-4]
440             return name
441
442         def is_really_module(name):
443             name = opj(dir, name)
444             return os.path.isdir(name) or zipfile.is_zipfile(name)
445         return map(clean, filter(is_really_module, os.listdir(dir)))
446
447     plist = []
448     initialize_sys_path()
449     for ad in ad_paths:
450         plist.extend(listdir(ad))
451     return list(set(plist))
452
453
454 def get_modules_with_version():
455     modules = get_modules()
456     res = {}
457     for module in modules:
458         try:
459             info = load_information_from_description_file(module)
460             res[module] = "%s.%s" % (release.major_version, info['version'])
461         except Exception, e:
462             continue
463     return res
464
465 def get_test_modules(module, submodule, explode):
466     """
467     Return a list of submodules containing tests.
468     `submodule` can be:
469       - None
470       - the name of a submodule
471       - '__fast_suite__'
472       - '__sanity_checks__'
473     """
474     # Turn command-line module, submodule into importable names.
475     if module is None:
476         pass
477     elif module == 'openerp':
478         module = 'openerp.tests'
479     else:
480         module = 'openerp.addons.' + module + '.tests'
481
482     # Try to import the module
483     try:
484         __import__(module)
485     except Exception, e:
486         if explode:
487             print 'Can not `import %s`.' % module
488             import logging
489             logging.exception('')
490             sys.exit(1)
491         else:
492             if str(e) == 'No module named tests':
493                 # It seems the module has no `tests` sub-module, no problem.
494                 pass
495             else:
496                 _logger.exception('Can not `import %s`.', module)
497             return []
498
499     # Discover available test sub-modules.
500     m = sys.modules[module]
501     submodule_names =  sorted([x for x in dir(m) \
502         if x.startswith('test_') and \
503         isinstance(getattr(m, x), types.ModuleType)])
504     submodules = [getattr(m, x) for x in submodule_names]
505
506     def show_submodules_and_exit():
507         if submodule_names:
508             print 'Available submodules are:'
509             for x in submodule_names:
510                 print ' ', x
511         sys.exit(1)
512
513     if submodule is None:
514         # Use auto-discovered sub-modules.
515         ms = submodules
516     elif submodule == '__fast_suite__':
517         # Obtain the explicit test sub-modules list.
518         ms = getattr(sys.modules[module], 'fast_suite', None)
519         # `suite` was used before the 6.1 release instead of `fast_suite`.
520         ms = ms if ms else getattr(sys.modules[module], 'suite', None)
521         if ms is None:
522             if explode:
523                 print 'The module `%s` has no defined test suite.' % (module,)
524                 show_submodules_and_exit()
525             else:
526                 ms = []
527     elif submodule == '__sanity_checks__':
528         ms = getattr(sys.modules[module], 'checks', None)
529         if ms is None:
530             if explode:
531                 print 'The module `%s` has no defined sanity checks.' % (module,)
532                 show_submodules_and_exit()
533             else:
534                 ms = []
535     else:
536         # Pick the command-line-specified test sub-module.
537         m = getattr(sys.modules[module], submodule, None)
538         ms = [m]
539
540         if m is None:
541             if explode:
542                 print 'The module `%s` has no submodule named `%s`.' % \
543                     (module, submodule)
544                 show_submodules_and_exit()
545             else:
546                 ms = []
547
548     return ms
549
550 def run_unit_tests(module_name):
551     """
552     Return True or False if some tests were found and succeeded or failed.
553     Return None if no test was found.
554     """
555     import unittest2
556     ms = get_test_modules(module_name, '__fast_suite__', explode=False)
557     # TODO: No need to try again if the above call failed because of e.g. a syntax error.
558     ms.extend(get_test_modules(module_name, '__sanity_checks__', explode=False))
559     suite = unittest2.TestSuite()
560     for m in ms:
561         suite.addTests(unittest2.TestLoader().loadTestsFromModule(m))
562     if ms:
563         _logger.info('module %s: executing %s `fast_suite` and/or `checks` sub-modules', module_name, len(ms))
564         # Use a custom stream object to log the test executions.
565         class MyStream(object):
566             def __init__(self):
567                 self.r = re.compile(r'^-*$|^ *... *$|^ok$')
568             def flush(self):
569                 pass
570             def write(self, s):
571                 if self.r.match(s):
572                     return
573                 first = True
574                 for c in s.split('\n'):
575                     if not first:
576                         c = '` ' + c
577                     first = False
578                     _logger.log(logging.TEST, c)
579         result = unittest2.TextTestRunner(verbosity=2, stream=MyStream()).run(suite)
580         if result.wasSuccessful():
581             return True
582         else:
583             _logger.error('module %s: at least one error occurred in a test', module_name)
584             return False
585
586 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: