[REF] separate/expose some code.
[odoo/odoo.git] / openerp / modules / __init__.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 """ Modules (also called addons) management.
24
25 """
26
27 import os, sys, imp
28 from os.path import join as opj
29 import itertools
30 import zipimport
31
32 import openerp
33
34 import openerp.osv as osv
35 import openerp.tools as tools
36 import openerp.tools.osutil as osutil
37 from openerp.tools.safe_eval import safe_eval as eval
38 import openerp.pooler as pooler
39 from openerp.tools.translate import _
40
41 import openerp.netsvc as netsvc
42
43 import zipfile
44 import openerp.release as release
45
46 import re
47 import base64
48 from zipfile import PyZipFile, ZIP_DEFLATED
49 from cStringIO import StringIO
50
51 import logging
52
53 logger = netsvc.Logger()
54
55 _ad = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons') # default addons path (base)
56 ad_paths = []
57
58 # Modules already loaded
59 loaded = []
60
61 def initialize_sys_path():
62     global ad_paths
63
64     if ad_paths:
65         return
66
67     ad_paths = map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(','))
68
69     sys.path.insert(1, _ad)
70
71     ad_cnt=1
72     for adp in ad_paths:
73         if adp != _ad:
74             sys.path.insert(ad_cnt, adp)
75             ad_cnt+=1
76
77     ad_paths.append(_ad)    # for get_module_path
78
79 def open_openerp_namespace():
80     # See comment for open_openerp_namespace.
81     if openerp.conf.deprecation.open_openerp_namespace:
82         for k, v in list(sys.modules.items()):
83             if k.startswith('openerp.') and sys.modules.get(k[8:]) is None:
84                 sys.modules[k[8:]] = v
85
86 class Graph(dict):
87     """ Modules dependency graph.
88
89     The graph is a mapping from module name to Nodes.
90
91     """
92
93     def addNode(self, name, deps):
94         max_depth, father = 0, None
95         for n in [Node(x, self) for x in deps]:
96             if n.depth >= max_depth:
97                 father = n
98                 max_depth = n.depth
99         if father:
100             father.addChild(name)
101         else:
102             Node(name, self)
103
104     def update_from_db(self, cr):
105         if not len(self):
106             return
107         # update the graph with values from the database (if exist)
108         ## First, we set the default values for each package in graph
109         additional_data = dict.fromkeys(self.keys(), {'id': 0, 'state': 'uninstalled', 'dbdemo': False, 'installed_version': None})
110         ## Then we get the values from the database
111         cr.execute('SELECT name, id, state, demo AS dbdemo, latest_version AS installed_version'
112                    '  FROM ir_module_module'
113                    ' WHERE name IN %s',(tuple(additional_data),)
114                    )
115
116         ## and we update the default values with values from the database
117         additional_data.update(dict([(x.pop('name'), x) for x in cr.dictfetchall()]))
118
119         for package in self.values():
120             for k, v in additional_data[package.name].items():
121                 setattr(package, k, v)
122
123     def __iter__(self):
124         level = 0
125         done = set(self.keys())
126         while done:
127             level_modules = [(name, module) for name, module in self.items() if module.depth==level]
128             for name, module in level_modules:
129                 done.remove(name)
130                 yield module
131             level += 1
132
133 class Singleton(object):
134     def __new__(cls, name, graph):
135         if name in graph:
136             inst = graph[name]
137         else:
138             inst = object.__new__(cls)
139             inst.name = name
140             graph[name] = inst
141         return inst
142
143
144 class Node(Singleton):
145     """ One module in the modules dependency graph.
146
147     Node acts as a per-module singleton.
148
149     """
150
151     def __init__(self, name, graph):
152         self.graph = graph
153         if not hasattr(self, 'children'):
154             self.children = []
155         if not hasattr(self, 'depth'):
156             self.depth = 0
157
158     def addChild(self, name):
159         node = Node(name, self.graph)
160         node.depth = self.depth + 1
161         if node not in self.children:
162             self.children.append(node)
163         for attr in ('init', 'update', 'demo'):
164             if hasattr(self, attr):
165                 setattr(node, attr, True)
166         self.children.sort(lambda x, y: cmp(x.name, y.name))
167
168     def __setattr__(self, name, value):
169         super(Singleton, self).__setattr__(name, value)
170         if name in ('init', 'update', 'demo'):
171             tools.config[name][self.name] = 1
172             for child in self.children:
173                 setattr(child, name, value)
174         if name == 'depth':
175             for child in self.children:
176                 setattr(child, name, value + 1)
177
178     def __iter__(self):
179         return itertools.chain(iter(self.children), *map(iter, self.children))
180
181     def __str__(self):
182         return self._pprint()
183
184     def _pprint(self, depth=0):
185         s = '%s\n' % self.name
186         for c in self.children:
187             s += '%s`-> %s' % ('   ' * depth, c._pprint(depth+1))
188         return s
189
190
191 def get_module_path(module, downloaded=False):
192     """Return the path of the given module.
193
194     Search the addons paths and return the first path where the given
195     module is found. If downloaded is True, return the default addons
196     path if nothing else is found.
197
198     """
199     initialize_sys_path()
200     for adp in ad_paths:
201         if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
202             return opj(adp, module)
203
204     if downloaded:
205         return opj(_ad, module)
206     logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: module not found' % (module,))
207     return False
208
209
210 def get_module_filetree(module, dir='.'):
211     path = get_module_path(module)
212     if not path:
213         return False
214
215     dir = os.path.normpath(dir)
216     if dir == '.':
217         dir = ''
218     if dir.startswith('..') or (dir and dir[0] == '/'):
219         raise Exception('Cannot access file outside the module')
220
221     if not os.path.isdir(path):
222         # zipmodule
223         zip = zipfile.ZipFile(path + ".zip")
224         files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
225     else:
226         files = osutil.listdir(path, True)
227
228     tree = {}
229     for f in files:
230         if not f.startswith(dir):
231             continue
232
233         if dir:
234             f = f[len(dir)+int(not dir.endswith('/')):]
235         lst = f.split(os.sep)
236         current = tree
237         while len(lst) != 1:
238             current = current.setdefault(lst.pop(0), {})
239         current[lst.pop(0)] = None
240
241     return tree
242
243 def zip_directory(directory, b64enc=True, src=True):
244     """Compress a directory
245
246     @param directory: The directory to compress
247     @param base64enc: if True the function will encode the zip file with base64
248     @param src: Integrate the source files
249
250     @return: a string containing the zip file
251     """
252
253     RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
254
255     def _zippy(archive, path, src=True):
256         path = os.path.abspath(path)
257         base = os.path.basename(path)
258         for f in osutil.listdir(path, True):
259             bf = os.path.basename(f)
260             if not RE_exclude.search(bf) and (src or bf in ('__openerp__.py', '__terp__.py') or not bf.endswith('.py')):
261                 archive.write(os.path.join(path, f), os.path.join(base, f))
262
263     archname = StringIO()
264     archive = PyZipFile(archname, "w", ZIP_DEFLATED)
265
266     # for Python 2.5, ZipFile.write() still expects 8-bit strings (2.6 converts to utf-8)
267     directory = tools.ustr(directory).encode('utf-8')
268
269     archive.writepy(directory)
270     _zippy(archive, directory, src=src)
271     archive.close()
272     archive_data = archname.getvalue()
273     archname.close()
274
275     if b64enc:
276         return base64.encodestring(archive_data)
277
278     return archive_data
279
280 def get_module_as_zip(modulename, b64enc=True, src=True):
281     """Generate a module as zip file with the source or not and can do a base64 encoding
282
283     @param modulename: The module name
284     @param b64enc: if True the function will encode the zip file with base64
285     @param src: Integrate the source files
286
287     @return: a stream to store in a file-like object
288     """
289
290     ap = get_module_path(str(modulename))
291     if not ap:
292         raise Exception('Unable to find path for module %s' % modulename)
293
294     ap = ap.encode('utf8')
295     if os.path.isfile(ap + '.zip'):
296         val = file(ap + '.zip', 'rb').read()
297         if b64enc:
298             val = base64.encodestring(val)
299     else:
300         val = zip_directory(ap, b64enc, src)
301
302     return val
303
304
305 def get_module_resource(module, *args):
306     """Return the full path of a resource of the given module.
307
308     @param module: the module
309     @param args: the resource path components
310
311     @return: absolute path to the resource
312
313     TODO name it get_resource_path
314     TODO make it available inside on osv object (self.get_resource_path)
315     """
316     a = get_module_path(module)
317     if not a: return False
318     resource_path = opj(a, *args)
319     if zipfile.is_zipfile( a +'.zip') :
320         zip = zipfile.ZipFile( a + ".zip")
321         files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
322         resource_path = '/'.join(args)
323         if resource_path in files:
324             return opj(a, resource_path)
325     elif os.path.exists(resource_path):
326         return resource_path
327     return False
328
329 def get_modules():
330     """Returns the list of module names
331     """
332     def listdir(dir):
333         def clean(name):
334             name = os.path.basename(name)
335             if name[-4:] == '.zip':
336                 name = name[:-4]
337             return name
338
339         def is_really_module(name):
340             name = opj(dir, name)
341             return os.path.isdir(name) or zipfile.is_zipfile(name)
342         return map(clean, filter(is_really_module, os.listdir(dir)))
343
344     plist = []
345     initialize_sys_path()
346     for ad in ad_paths:
347         plist.extend(listdir(ad))
348     return list(set(plist))
349
350 def load_information_from_description_file(module):
351     """
352     :param module: The name of the module (sale, purchase, ...)
353     """
354
355     for filename in ['__openerp__.py', '__terp__.py']:
356         description_file = get_module_resource(module, filename)
357         if description_file :
358             desc_f = tools.file_open(description_file)
359             try:
360                 return eval(desc_f.read())
361             finally:
362                 desc_f.close()
363
364     #TODO: refactor the logger in this file to follow the logging guidelines
365     #      for 6.0
366     logging.getLogger('addons').debug('The module %s does not contain a description file:'\
367                                       '__openerp__.py or __terp__.py (deprecated)', module)
368     return {}
369
370 def get_modules_with_version():
371     modules = get_modules()
372     res = {}
373     for module in modules:
374         try:
375             info = load_information_from_description_file(module)
376             res[module] = "%s.%s" % (release.major_version, info['version'])
377         except Exception, e:
378             continue
379     return res
380
381 def create_graph(cr, module_list, force=None):
382     graph = Graph()
383     upgrade_graph(graph, cr, module_list, force)
384     return graph
385
386 def upgrade_graph(graph, cr, module_list, force=None):
387     if force is None:
388         force = []
389     packages = []
390     len_graph = len(graph)
391     for module in module_list:
392         mod_path = get_module_path(module)
393         terp_file = get_module_resource(module, '__openerp__.py')
394         if not terp_file:
395             terp_file = get_module_resource(module, '__terp__.py')
396         if not mod_path or not terp_file:
397             logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: not found, skipped' % (module))
398             continue
399
400         if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
401             terp_f = tools.file_open(terp_file)
402             try:
403                 info = eval(terp_f.read())
404             except Exception:
405                 logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: eval file %s' % (module, terp_file))
406                 raise
407             finally:
408                 terp_f.close()
409             if info.get('installable', True):
410                 packages.append((module, info.get('depends', []), info))
411             else:
412                 logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: not installable, skipped' % (module))
413
414     dependencies = dict([(p, deps) for p, deps, data in packages])
415     current, later = set([p for p, dep, data in packages]), set()
416
417     while packages and current > later:
418         package, deps, data = packages[0]
419
420         # if all dependencies of 'package' are already in the graph, add 'package' in the graph
421         if reduce(lambda x, y: x and y in graph, deps, True):
422             if not package in current:
423                 packages.pop(0)
424                 continue
425             later.clear()
426             current.remove(package)
427             graph.addNode(package, deps)
428             node = Node(package, graph)
429             node.data = data
430             for kind in ('init', 'demo', 'update'):
431                 if package in tools.config[kind] or 'all' in tools.config[kind] or kind in force:
432                     setattr(node, kind, True)
433         else:
434             later.add(package)
435             packages.append((package, deps, data))
436         packages.pop(0)
437
438     graph.update_from_db(cr)
439
440     for package in later:
441         unmet_deps = filter(lambda p: p not in graph, dependencies[package])
442         logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: Unmet dependencies: %s' % (package, ', '.join(unmet_deps)))
443
444     result = len(graph) - len_graph
445     if result != len(module_list):
446         logger.notifyChannel('init', netsvc.LOG_WARNING, 'Not all modules have loaded.')
447     return result
448
449
450 def init_module_objects(cr, module_name, obj_list):
451     logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: creating or updating database tables' % module_name)
452     todo = []
453     for obj in obj_list:
454         try:
455             result = obj._auto_init(cr, {'module': module_name})
456         except Exception, e:
457             raise
458         if result:
459             todo += result
460         if hasattr(obj, 'init'):
461             obj.init(cr)
462         cr.commit()
463     todo.sort()
464     for t in todo:
465         t[1](cr, *t[2])
466     cr.commit()
467
468
469 def load_module(module_name):
470     """ Load a Python module found on the addons paths."""
471     fm = imp.find_module(module_name, ad_paths)
472     try:
473         imp.load_module(module_name, *fm)
474     finally:
475         if fm[0]:
476             fm[0].close()
477
478
479 def register_class(m):
480     """
481     Register module named m, if not already registered
482     """
483
484     def log(e):
485         mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
486         msg = "Couldn't load %smodule %s" % (mt, m)
487         logger.notifyChannel('init', netsvc.LOG_CRITICAL, msg)
488         logger.notifyChannel('init', netsvc.LOG_CRITICAL, e)
489
490     global loaded
491     if m in loaded:
492         return
493     logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: registering objects' % m)
494     mod_path = get_module_path(m)
495
496     initialize_sys_path()
497     try:
498         zip_mod_path = mod_path + '.zip'
499         if not os.path.isfile(zip_mod_path):
500             load_module(m)
501         else:
502             zimp = zipimport.zipimporter(zip_mod_path)
503             zimp.load_module(m)
504     except Exception, e:
505         log(e)
506         raise
507     else:
508         loaded.append(m)
509
510
511 class MigrationManager(object):
512     """
513         This class manage the migration of modules
514         Migrations files must be python files containing a "migrate(cr, installed_version)" function.
515         Theses files must respect a directory tree structure: A 'migrations' folder which containt a
516         folder by version. Version can be 'module' version or 'server.module' version (in this case,
517         the files will only be processed by this version of the server). Python file names must start
518         by 'pre' or 'post' and will be executed, respectively, before and after the module initialisation
519         Example:
520
521             <moduledir>
522             `-- migrations
523                 |-- 1.0
524                 |   |-- pre-update_table_x.py
525                 |   |-- pre-update_table_y.py
526                 |   |-- post-clean-data.py
527                 |   `-- README.txt              # not processed
528                 |-- 5.0.1.1                     # files in this folder will be executed only on a 5.0 server
529                 |   |-- pre-delete_table_z.py
530                 |   `-- post-clean-data.py
531                 `-- foo.py                      # not processed
532
533         This similar structure is generated by the maintenance module with the migrations files get by
534         the maintenance contract
535
536     """
537     def __init__(self, cr, graph):
538         self.cr = cr
539         self.graph = graph
540         self.migrations = {}
541         self._get_files()
542
543     def _get_files(self):
544
545         """
546         import addons.base.maintenance.utils as maintenance_utils
547         maintenance_utils.update_migrations_files(self.cr)
548         #"""
549
550         for pkg in self.graph:
551             self.migrations[pkg.name] = {}
552             if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
553                 continue
554
555             self.migrations[pkg.name]['module'] = get_module_filetree(pkg.name, 'migrations') or {}
556             self.migrations[pkg.name]['maintenance'] = get_module_filetree('base', 'maintenance/migrations/' + pkg.name) or {}
557
558     def migrate_module(self, pkg, stage):
559         assert stage in ('pre', 'post')
560         stageformat = {'pre': '[>%s]',
561                        'post': '[%s>]',
562                       }
563
564         if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
565             return
566
567         def convert_version(version):
568             if version.startswith(release.major_version) and version != release.major_version:
569                 return version  # the version number already containt the server version
570             return "%s.%s" % (release.major_version, version)
571
572         def _get_migration_versions(pkg):
573             def __get_dir(tree):
574                 return [d for d in tree if tree[d] is not None]
575
576             versions = list(set(
577                 __get_dir(self.migrations[pkg.name]['module']) +
578                 __get_dir(self.migrations[pkg.name]['maintenance'])
579             ))
580             versions.sort(key=lambda k: parse_version(convert_version(k)))
581             return versions
582
583         def _get_migration_files(pkg, version, stage):
584             """ return a list of tuple (module, file)
585             """
586             m = self.migrations[pkg.name]
587             lst = []
588
589             mapping = {'module': opj(pkg.name, 'migrations'),
590                        'maintenance': opj('base', 'maintenance', 'migrations', pkg.name),
591                       }
592
593             for x in mapping.keys():
594                 if version in m[x]:
595                     for f in m[x][version]:
596                         if m[x][version][f] is not None:
597                             continue
598                         if not f.startswith(stage + '-'):
599                             continue
600                         lst.append(opj(mapping[x], version, f))
601             lst.sort()
602             return lst
603
604         def mergedict(a, b):
605             a = a.copy()
606             a.update(b)
607             return a
608
609         from openerp.tools.parse_version import parse_version
610
611         parsed_installed_version = parse_version(pkg.installed_version or '')
612         current_version = parse_version(convert_version(pkg.data.get('version', '0')))
613
614         versions = _get_migration_versions(pkg)
615
616         for version in versions:
617             if parsed_installed_version < parse_version(convert_version(version)) <= current_version:
618
619                 strfmt = {'addon': pkg.name,
620                           'stage': stage,
621                           'version': stageformat[stage] % version,
622                           }
623
624                 for pyfile in _get_migration_files(pkg, version, stage):
625                     name, ext = os.path.splitext(os.path.basename(pyfile))
626                     if ext.lower() != '.py':
627                         continue
628                     mod = fp = fp2 = None
629                     try:
630                         fp = tools.file_open(pyfile)
631
632                         # imp.load_source need a real file object, so we create
633                         # one from the file-like object we get from file_open
634                         fp2 = os.tmpfile()
635                         fp2.write(fp.read())
636                         fp2.seek(0)
637                         try:
638                             mod = imp.load_source(name, pyfile, fp2)
639                             logger.notifyChannel('migration', netsvc.LOG_INFO, 'module %(addon)s: Running migration %(version)s %(name)s' % mergedict({'name': mod.__name__}, strfmt))
640                             mod.migrate(self.cr, pkg.installed_version)
641                         except ImportError:
642                             logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Unable to load %(stage)s-migration file %(file)s' % mergedict({'file': pyfile}, strfmt))
643                             raise
644                         except AttributeError:
645                             logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Each %(stage)s-migration file must have a "migrate(cr, installed_version)" function' % strfmt)
646                         except:
647                             raise
648                     finally:
649                         if fp:
650                             fp.close()
651                         if fp2:
652                             fp2.close()
653                         if mod:
654                             del mod
655
656 log = logging.getLogger('init')
657
658 def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=None, **kwargs):
659     """Migrates+Updates or Installs all module nodes from ``graph``
660        :param graph: graph of module nodes to load
661        :param status: status dictionary for keeping track of progress
662        :param perform_checks: whether module descriptors should be checked for validity (prints warnings
663                               for same cases, and even raise osv_except if certificate is invalid)
664        :param skip_modules: optional list of module names (packages) which have previously been loaded and can be skipped
665        :return: list of modules that were installed or updated
666     """
667     def process_sql_file(cr, fp):
668         queries = fp.read().split(';')
669         for query in queries:
670             new_query = ' '.join(query.split())
671             if new_query:
672                 cr.execute(new_query)
673
674     def load_init_update_xml(cr, m, idref, mode, kind):
675         for filename in package.data.get('%s_xml' % kind, []):
676             logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, filename))
677             _, ext = os.path.splitext(filename)
678             fp = tools.file_open(opj(m, filename))
679             try:
680                 if ext == '.csv':
681                     noupdate = (kind == 'init')
682                     tools.convert_csv_import(cr, m, os.path.basename(filename), fp.read(), idref, mode=mode, noupdate=noupdate)
683                 elif ext == '.sql':
684                     process_sql_file(cr, fp)
685                 elif ext == '.yml':
686                     tools.convert_yaml_import(cr, m, fp, idref, mode=mode, **kwargs)
687                 else:
688                     tools.convert_xml_import(cr, m, fp, idref, mode=mode, **kwargs)
689             finally:
690                 fp.close()
691
692     def load_demo_xml(cr, m, idref, mode):
693         for xml in package.data.get('demo_xml', []):
694             name, ext = os.path.splitext(xml)
695             logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, xml))
696             fp = tools.file_open(opj(m, xml))
697             try:
698                 if ext == '.csv':
699                     tools.convert_csv_import(cr, m, os.path.basename(xml), fp.read(), idref, mode=mode, noupdate=True)
700                 elif ext == '.yml':
701                     tools.convert_yaml_import(cr, m, fp, idref, mode=mode, noupdate=True, **kwargs)
702                 else:
703                     tools.convert_xml_import(cr, m, fp, idref, mode=mode, noupdate=True, **kwargs)
704             finally:
705                 fp.close()
706
707     def load_data(cr, module_name, id_map, mode):
708         _load_data(cr, module_name, id_map, mode, 'data')
709
710     def load_demo(cr, module_name, id_map, mode):
711         _load_data(cr, module_name, id_map, mode, 'demo')
712
713     def load_test(cr, module_name, id_map, mode):
714         cr.commit()
715         if not tools.config.options['test_disable']:
716             try:
717                 _load_data(cr, module_name, id_map, mode, 'test')
718             except Exception, e:
719                 logging.getLogger('test').exception('Tests failed to execute in module %s', module_name)
720             finally:
721                 if tools.config.options['test_commit']:
722                     cr.commit()
723                 else:
724                     cr.rollback()
725
726     def _load_data(cr, module_name, id_map, mode, kind):
727         for filename in package.data.get(kind, []):
728             noupdate = (kind == 'demo')
729             _, ext = os.path.splitext(filename)
730             log.info("module %s: loading %s", module_name, filename)
731             pathname = os.path.join(module_name, filename)
732             file = tools.file_open(pathname)
733             try:
734                 if ext == '.sql':
735                     process_sql_file(cr, file)
736                 elif ext == '.csv':
737                     noupdate = (kind == 'init')
738                     tools.convert_csv_import(cr, module_name, pathname, file.read(), id_map, mode, noupdate)
739                 elif ext == '.yml':
740                     tools.convert_yaml_import(cr, module_name, file, id_map, mode, noupdate)
741                 else:
742                     tools.convert_xml_import(cr, module_name, file, id_map, mode, noupdate)
743             finally:
744                 file.close()
745
746     # **kwargs is passed directly to convert_xml_import
747     if not status:
748         status = {}
749
750     status = status.copy()
751     processed_modules = []
752     statusi = 0
753     pool = pooler.get_pool(cr.dbname)
754     migrations = MigrationManager(cr, graph)
755     modobj = None
756     logger.notifyChannel('init', netsvc.LOG_DEBUG, 'loading %d packages..' % len(graph))
757
758     for package in graph:
759         if skip_modules and package.name in skip_modules:
760             continue
761         logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading objects' % package.name)
762         migrations.migrate_module(package, 'pre')
763         register_class(package.name)
764         modules = pool.instanciate(package.name, cr)
765         if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
766             init_module_objects(cr, package.name, modules)
767         cr.commit()
768
769     for package in graph:
770         status['progress'] = (float(statusi)+0.1) / len(graph)
771         m = package.name
772         mid = package.id
773
774         if skip_modules and m in skip_modules:
775             continue
776
777         if modobj is None:
778             modobj = pool.get('ir.module.module')
779
780         if modobj and perform_checks:
781             modobj.check(cr, 1, [mid])
782
783         idref = {}
784         status['progress'] = (float(statusi)+0.4) / len(graph)
785
786         mode = 'update'
787         if hasattr(package, 'init') or package.state == 'to install':
788             mode = 'init'
789
790         if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
791             for kind in ('init', 'update'):
792                 if package.state=='to upgrade':
793                     # upgrading the module information
794                     modobj.write(cr, 1, [mid], modobj.get_values_from_terp(package.data))
795                 load_init_update_xml(cr, m, idref, mode, kind)
796             load_data(cr, m, idref, mode)
797             if hasattr(package, 'demo') or (package.dbdemo and package.state != 'installed'):
798                 status['progress'] = (float(statusi)+0.75) / len(graph)
799                 load_demo_xml(cr, m, idref, mode)
800                 load_demo(cr, m, idref, mode)
801                 cr.execute('update ir_module_module set demo=%s where id=%s', (True, mid))
802
803                 # launch tests only in demo mode, as most tests will depend
804                 # on demo data. Other tests can be added into the regular
805                 # 'data' section, but should probably not alter the data,
806                 # as there is no rollback.
807                 load_test(cr, m, idref, mode)
808
809             processed_modules.append(package.name)
810
811             migrations.migrate_module(package, 'post')
812
813             if modobj:
814                 ver = release.major_version + '.' + package.data.get('version', '1.0')
815                 # Set new modules and dependencies
816                 modobj.write(cr, 1, [mid], {'state': 'installed', 'latest_version': ver})
817                 cr.commit()
818                 # Update translations for all installed languages
819                 modobj.update_translations(cr, 1, [mid], None, {'overwrite': tools.config['overwrite_existing_translations']})
820                 cr.commit()
821
822             package.state = 'installed'
823             for kind in ('init', 'demo', 'update'):
824                 if hasattr(package, kind):
825                     delattr(package, kind)
826
827         statusi += 1
828
829     cr.commit()
830
831     return processed_modules
832
833 def _check_module_names(cr, module_names):
834     mod_names = set(module_names)
835     if 'base' in mod_names:
836         # ignore dummy 'all' module
837         if 'all' in mod_names:
838             mod_names.remove('all')
839     if mod_names:
840         cr.execute("SELECT count(id) AS count FROM ir_module_module WHERE name in %s", (tuple(mod_names),))
841         if cr.dictfetchone()['count'] != len(mod_names):
842             # find out what module name(s) are incorrect:
843             cr.execute("SELECT name FROM ir_module_module")
844             incorrect_names = mod_names.difference([x['name'] for x in cr.dictfetchall()])
845             logging.getLogger('init').warning('invalid module names, ignored: %s', ", ".join(incorrect_names))
846
847 def load_modules(db, force_demo=False, status=None, update_module=False):
848
849     initialize_sys_path()
850
851     open_openerp_namespace()
852
853     if not status:
854         status = {}
855     cr = db.cursor()
856     if cr:
857         cr.execute("SELECT relname FROM pg_class WHERE relkind='r' AND relname='ir_module_module'")
858         if len(cr.fetchall())==0:
859             logger.notifyChannel("init", netsvc.LOG_INFO, "init db")
860             tools.init_db(cr)
861             tools.config["init"]["all"] = 1
862             tools.config['update']['all'] = 1
863             if not tools.config['without_demo']:
864                 tools.config["demo"]['all'] = 1
865     force = []
866     if force_demo:
867         force.append('demo')
868
869     # This is a brand new pool, just created in pooler.get_db_and_pool()
870     pool = pooler.get_pool(cr.dbname)
871
872     try:
873         processed_modules = []
874         report = tools.assertion_report()
875         # NOTE: Try to also load the modules that have been marked as uninstallable previously...
876         STATES_TO_LOAD = ['installed', 'to upgrade', 'uninstallable']
877         if 'base' in tools.config['update'] or 'all' in tools.config['update']:
878             cr.execute("update ir_module_module set state=%s where name=%s and state=%s", ('to upgrade', 'base', 'installed'))
879
880         # STEP 1: LOAD BASE (must be done before module dependencies can be computed for later steps) 
881         graph = create_graph(cr, ['base'], force)
882         if not graph:
883             logger.notifyChannel('init', netsvc.LOG_CRITICAL, 'module base cannot be loaded! (hint: verify addons-path)')
884             raise osv.osv.except_osv(_('Could not load base module'), _('module base cannot be loaded! (hint: verify addons-path)'))
885         processed_modules.extend(load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report))
886
887         if tools.config['load_language']:
888             for lang in tools.config['load_language'].split(','):
889                 tools.load_language(cr, lang)
890
891         # STEP 2: Mark other modules to be loaded/updated
892         if update_module:
893             modobj = pool.get('ir.module.module')
894             logger.notifyChannel('init', netsvc.LOG_INFO, 'updating modules list')
895             if ('base' in tools.config['init']) or ('base' in tools.config['update']):
896                 modobj.update_list(cr, 1)
897
898             _check_module_names(cr, itertools.chain(tools.config['init'].keys(), tools.config['update'].keys()))
899
900             mods = [k for k in tools.config['init'] if tools.config['init'][k]]
901             if mods:
902                 ids = modobj.search(cr, 1, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)])
903                 if ids:
904                     modobj.button_install(cr, 1, ids)
905
906             mods = [k for k in tools.config['update'] if tools.config['update'][k]]
907             if mods:
908                 ids = modobj.search(cr, 1, ['&', ('state', '=', 'installed'), ('name', 'in', mods)])
909                 if ids:
910                     modobj.button_upgrade(cr, 1, ids)
911
912             cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base'))
913
914             STATES_TO_LOAD += ['to install']
915
916
917         # STEP 3: Load marked modules (skipping base which was done in STEP 1)
918         loop_guardrail = 0
919         while True:
920             loop_guardrail += 1
921             if loop_guardrail > 100:
922                 raise ValueError('Possible recursive module tree detected, aborting.')
923             cr.execute("SELECT name from ir_module_module WHERE state IN %s" ,(tuple(STATES_TO_LOAD),))
924
925             module_list = [name for (name,) in cr.fetchall() if name not in graph]
926             if not module_list:
927                 break
928
929             new_modules_in_graph = upgrade_graph(graph, cr, module_list, force)
930             if new_modules_in_graph == 0:
931                 # nothing to load
932                 break
933
934             logger.notifyChannel('init', netsvc.LOG_DEBUG, 'Updating graph with %d more modules' % (len(module_list)))
935             processed_modules.extend(load_module_graph(cr, graph, status, report=report, skip_modules=processed_modules))
936
937         # load custom models
938         cr.execute('select model from ir_model where state=%s', ('manual',))
939         for model in cr.dictfetchall():
940             pool.get('ir.model').instanciate(cr, 1, model['model'], {})
941
942         # STEP 4: Finish and cleanup
943         if processed_modules:
944             cr.execute("""select model,name from ir_model where id NOT IN (select distinct model_id from ir_model_access)""")
945             for (model, name) in cr.fetchall():
946                 model_obj = pool.get(model)
947                 if model_obj and not isinstance(model_obj, osv.osv.osv_memory):
948                     logger.notifyChannel('init', netsvc.LOG_WARNING, 'object %s (%s) has no access rules!' % (model, name))
949
950             # Temporary warning while we remove access rights on osv_memory objects, as they have
951             # been replaced by owner-only access rights
952             cr.execute("""select distinct mod.model, mod.name from ir_model_access acc, ir_model mod where acc.model_id = mod.id""")
953             for (model, name) in cr.fetchall():
954                 model_obj = pool.get(model)
955                 if isinstance(model_obj, osv.osv.osv_memory):
956                     logger.notifyChannel('init', netsvc.LOG_WARNING, 'In-memory object %s (%s) should not have explicit access rules!' % (model, name))
957
958             cr.execute("SELECT model from ir_model")
959             for (model,) in cr.fetchall():
960                 obj = pool.get(model)
961                 if obj:
962                     obj._check_removed_columns(cr, log=True)
963                 else:
964                     logger.notifyChannel('init', netsvc.LOG_WARNING, "Model %s is referenced but not present in the orm pool!" % model)
965
966             # Cleanup orphan records
967             pool.get('ir.model.data')._process_end(cr, 1, processed_modules)
968
969         if report.get_report():
970             logger.notifyChannel('init', netsvc.LOG_INFO, report)
971
972         for kind in ('init', 'demo', 'update'):
973             tools.config[kind] = {}
974
975         cr.commit()
976         if update_module:
977             cr.execute("select id,name from ir_module_module where state=%s", ('to remove',))
978             for mod_id, mod_name in cr.fetchall():
979                 cr.execute('select model,res_id from ir_model_data where noupdate=%s and module=%s order by id desc', (False, mod_name,))
980                 for rmod, rid in cr.fetchall():
981                     uid = 1
982                     rmod_module= pool.get(rmod)
983                     if rmod_module:
984                         rmod_module.unlink(cr, uid, [rid])
985                     else:
986                         logger.notifyChannel('init', netsvc.LOG_ERROR, 'Could not locate %s to remove res=%d' % (rmod,rid))
987                 cr.execute('delete from ir_model_data where noupdate=%s and module=%s', (False, mod_name,))
988                 cr.commit()
989             #
990             # TODO: remove menu without actions of children
991             #
992             while True:
993                 cr.execute('''delete from
994                         ir_ui_menu
995                     where
996                         (id not IN (select parent_id from ir_ui_menu where parent_id is not null))
997                     and
998                         (id not IN (select res_id from ir_values where model='ir.ui.menu'))
999                     and
1000                         (id not IN (select res_id from ir_model_data where model='ir.ui.menu'))''')
1001                 cr.commit()
1002                 if not cr.rowcount:
1003                     break
1004                 else:
1005                     logger.notifyChannel('init', netsvc.LOG_INFO, 'removed %d unused menus' % (cr.rowcount,))
1006
1007             cr.execute("update ir_module_module set state=%s where state=%s", ('uninstalled', 'to remove',))
1008             cr.commit()
1009     finally:
1010         cr.close()
1011
1012
1013 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: