[REF] modules.__init__:
[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_models(cr, module_name, obj_list):
451     """ Initialize a list of models.
452
453     Call _auto_init and init on each model to create or update the
454     database tables supporting the models.
455
456     TODO better explanation of _auto_init and init.
457
458     """
459
460     logger.notifyChannel('init', netsvc.LOG_INFO,
461         'module %s: creating or updating database tables' % module_name)
462     # TODO _auto_init doesn't seem to return anything
463     # so this todo list would be useless.
464     todo = []
465     for obj in obj_list:
466         try:
467             # TODO the module in the context doesn't seem usefull:
468             # it is available (at least) in the class' _module attribute.
469             # (So module_name would be useless too.)
470             result = obj._auto_init(cr, {'module': module_name})
471         except Exception, e:
472             raise
473         if result:
474             todo += result
475         if hasattr(obj, 'init'):
476             obj.init(cr)
477         cr.commit()
478     todo.sort()
479     for t in todo:
480         t[1](cr, *t[2])
481     cr.commit()
482
483
484 def load_module(module_name):
485     """ Load a Python module found on the addons paths."""
486     fm = imp.find_module(module_name, ad_paths)
487     try:
488         imp.load_module(module_name, *fm)
489     finally:
490         if fm[0]:
491             fm[0].close()
492
493
494 def register_class(m):
495     """
496     Register module named m, if not already registered
497     """
498
499     def log(e):
500         mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
501         msg = "Couldn't load %smodule %s" % (mt, m)
502         logger.notifyChannel('init', netsvc.LOG_CRITICAL, msg)
503         logger.notifyChannel('init', netsvc.LOG_CRITICAL, e)
504
505     global loaded
506     if m in loaded:
507         return
508     logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: registering objects' % m)
509     mod_path = get_module_path(m)
510
511     initialize_sys_path()
512     try:
513         zip_mod_path = mod_path + '.zip'
514         if not os.path.isfile(zip_mod_path):
515             load_module(m)
516         else:
517             zimp = zipimport.zipimporter(zip_mod_path)
518             zimp.load_module(m)
519     except Exception, e:
520         log(e)
521         raise
522     else:
523         loaded.append(m)
524
525
526 class MigrationManager(object):
527     """
528         This class manage the migration of modules
529         Migrations files must be python files containing a "migrate(cr, installed_version)" function.
530         Theses files must respect a directory tree structure: A 'migrations' folder which containt a
531         folder by version. Version can be 'module' version or 'server.module' version (in this case,
532         the files will only be processed by this version of the server). Python file names must start
533         by 'pre' or 'post' and will be executed, respectively, before and after the module initialisation
534         Example:
535
536             <moduledir>
537             `-- migrations
538                 |-- 1.0
539                 |   |-- pre-update_table_x.py
540                 |   |-- pre-update_table_y.py
541                 |   |-- post-clean-data.py
542                 |   `-- README.txt              # not processed
543                 |-- 5.0.1.1                     # files in this folder will be executed only on a 5.0 server
544                 |   |-- pre-delete_table_z.py
545                 |   `-- post-clean-data.py
546                 `-- foo.py                      # not processed
547
548         This similar structure is generated by the maintenance module with the migrations files get by
549         the maintenance contract
550
551     """
552     def __init__(self, cr, graph):
553         self.cr = cr
554         self.graph = graph
555         self.migrations = {}
556         self._get_files()
557
558     def _get_files(self):
559
560         """
561         import addons.base.maintenance.utils as maintenance_utils
562         maintenance_utils.update_migrations_files(self.cr)
563         #"""
564
565         for pkg in self.graph:
566             self.migrations[pkg.name] = {}
567             if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
568                 continue
569
570             self.migrations[pkg.name]['module'] = get_module_filetree(pkg.name, 'migrations') or {}
571             self.migrations[pkg.name]['maintenance'] = get_module_filetree('base', 'maintenance/migrations/' + pkg.name) or {}
572
573     def migrate_module(self, pkg, stage):
574         assert stage in ('pre', 'post')
575         stageformat = {'pre': '[>%s]',
576                        'post': '[%s>]',
577                       }
578
579         if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
580             return
581
582         def convert_version(version):
583             if version.startswith(release.major_version) and version != release.major_version:
584                 return version  # the version number already containt the server version
585             return "%s.%s" % (release.major_version, version)
586
587         def _get_migration_versions(pkg):
588             def __get_dir(tree):
589                 return [d for d in tree if tree[d] is not None]
590
591             versions = list(set(
592                 __get_dir(self.migrations[pkg.name]['module']) +
593                 __get_dir(self.migrations[pkg.name]['maintenance'])
594             ))
595             versions.sort(key=lambda k: parse_version(convert_version(k)))
596             return versions
597
598         def _get_migration_files(pkg, version, stage):
599             """ return a list of tuple (module, file)
600             """
601             m = self.migrations[pkg.name]
602             lst = []
603
604             mapping = {'module': opj(pkg.name, 'migrations'),
605                        'maintenance': opj('base', 'maintenance', 'migrations', pkg.name),
606                       }
607
608             for x in mapping.keys():
609                 if version in m[x]:
610                     for f in m[x][version]:
611                         if m[x][version][f] is not None:
612                             continue
613                         if not f.startswith(stage + '-'):
614                             continue
615                         lst.append(opj(mapping[x], version, f))
616             lst.sort()
617             return lst
618
619         def mergedict(a, b):
620             a = a.copy()
621             a.update(b)
622             return a
623
624         from openerp.tools.parse_version import parse_version
625
626         parsed_installed_version = parse_version(pkg.installed_version or '')
627         current_version = parse_version(convert_version(pkg.data.get('version', '0')))
628
629         versions = _get_migration_versions(pkg)
630
631         for version in versions:
632             if parsed_installed_version < parse_version(convert_version(version)) <= current_version:
633
634                 strfmt = {'addon': pkg.name,
635                           'stage': stage,
636                           'version': stageformat[stage] % version,
637                           }
638
639                 for pyfile in _get_migration_files(pkg, version, stage):
640                     name, ext = os.path.splitext(os.path.basename(pyfile))
641                     if ext.lower() != '.py':
642                         continue
643                     mod = fp = fp2 = None
644                     try:
645                         fp = tools.file_open(pyfile)
646
647                         # imp.load_source need a real file object, so we create
648                         # one from the file-like object we get from file_open
649                         fp2 = os.tmpfile()
650                         fp2.write(fp.read())
651                         fp2.seek(0)
652                         try:
653                             mod = imp.load_source(name, pyfile, fp2)
654                             logger.notifyChannel('migration', netsvc.LOG_INFO, 'module %(addon)s: Running migration %(version)s %(name)s' % mergedict({'name': mod.__name__}, strfmt))
655                             mod.migrate(self.cr, pkg.installed_version)
656                         except ImportError:
657                             logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Unable to load %(stage)s-migration file %(file)s' % mergedict({'file': pyfile}, strfmt))
658                             raise
659                         except AttributeError:
660                             logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Each %(stage)s-migration file must have a "migrate(cr, installed_version)" function' % strfmt)
661                         except:
662                             raise
663                     finally:
664                         if fp:
665                             fp.close()
666                         if fp2:
667                             fp2.close()
668                         if mod:
669                             del mod
670
671
672 def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=None, report=None):
673     """Migrates+Updates or Installs all module nodes from ``graph``
674        :param graph: graph of module nodes to load
675        :param status: status dictionary for keeping track of progress
676        :param perform_checks: whether module descriptors should be checked for validity (prints warnings
677                               for same cases, and even raise osv_except if certificate is invalid)
678        :param skip_modules: optional list of module names (packages) which have previously been loaded and can be skipped
679        :return: list of modules that were installed or updated
680     """
681     def process_sql_file(cr, fp):
682         queries = fp.read().split(';')
683         for query in queries:
684             new_query = ' '.join(query.split())
685             if new_query:
686                 cr.execute(new_query)
687
688     def load_init_xml(cr, m, idref, mode):
689         _load_xml(cr, m, idref, mode, 'init')
690
691     def load_update_xml(cr, m, idref, mode):
692         _load_xml(cr, m, idref, mode, 'update')
693
694     def load_demo_xml(cr, m, idref, mode):
695         _load_xml(cr, m, idref, mode, 'demo')
696
697     def load_data(cr, module_name, id_map, mode):
698         _load_data(cr, module_name, id_map, mode, 'data')
699
700     def load_demo(cr, module_name, id_map, mode):
701         _load_data(cr, module_name, id_map, mode, 'demo')
702
703     def load_test(cr, module_name, id_map, mode):
704         cr.commit()
705         if not tools.config.options['test_disable']:
706             try:
707                 _load_data(cr, module_name, id_map, mode, 'test')
708             except Exception, e:
709                 logging.getLogger('test').exception('Tests failed to execute in module %s', module_name)
710             finally:
711                 if tools.config.options['test_commit']:
712                     cr.commit()
713                 else:
714                     cr.rollback()
715
716     def _load_xml(cr, m, idref, mode, kind):
717         for filename in package.data.get('%s_xml' % kind, []):
718             logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, filename))
719             _, ext = os.path.splitext(filename)
720             fp = tools.file_open(opj(m, filename))
721             noupdate = False
722             if kind == 'demo':
723                 noupdate = True
724             try:
725                 if ext == '.csv':
726                     if kind == 'init':
727                         noupdate = True
728                     # i.e. noupdate == False when kind == 'update'
729                     tools.convert_csv_import(cr, m, os.path.basename(filename), fp.read(), idref, mode=mode, noupdate=noupdate)
730                 elif kind != 'demo' and ext == '.sql':
731                     process_sql_file(cr, fp)
732                 elif ext == '.yml':
733                     tools.convert_yaml_import(cr, m, fp, idref, mode=mode, noupdate=noupdate)
734                 else:
735                     tools.convert_xml_import(cr, m, fp, idref, mode=mode, noupdate=noupdate, report=report)
736             finally:
737                 fp.close()
738
739     def _load_data(cr, module_name, id_map, mode, kind):
740         for filename in package.data.get(kind, []):
741             noupdate = (kind == 'demo')
742             _, ext = os.path.splitext(filename)
743             log = logging.getLogger('init')
744             log.info("module %s: loading %s", module_name, filename)
745             pathname = os.path.join(module_name, filename)
746             file = tools.file_open(pathname)
747             try:
748                 if ext == '.sql':
749                     process_sql_file(cr, file)
750                 elif ext == '.csv':
751                     noupdate = (kind == 'init')
752                     tools.convert_csv_import(cr, module_name, pathname, file.read(), id_map, mode, noupdate)
753                 elif ext == '.yml':
754                     tools.convert_yaml_import(cr, module_name, file, id_map, mode, noupdate)
755                 else:
756                     tools.convert_xml_import(cr, module_name, file, id_map, mode, noupdate)
757             finally:
758                 file.close()
759
760     # **kwargs is passed directly to convert_xml_import
761     if not status:
762         status = {}
763
764     status = status.copy()
765     processed_modules = []
766     statusi = 0
767     pool = pooler.get_pool(cr.dbname)
768     migrations = MigrationManager(cr, graph)
769     modobj = None
770     logger.notifyChannel('init', netsvc.LOG_DEBUG, 'loading %d packages..' % len(graph))
771
772     for package in graph:
773         if skip_modules and package.name in skip_modules:
774             continue
775         logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading objects' % package.name)
776         migrations.migrate_module(package, 'pre')
777         register_class(package.name)
778         models = pool.instanciate(package.name, cr)
779         if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
780             init_module_models(cr, package.name, models)
781         cr.commit()
782
783     for package in graph:
784         status['progress'] = (float(statusi)+0.1) / len(graph)
785         m = package.name
786         mid = package.id
787
788         if skip_modules and m in skip_modules:
789             continue
790
791         if modobj is None:
792             modobj = pool.get('ir.module.module')
793
794         if modobj and perform_checks:
795             modobj.check(cr, 1, [mid])
796
797         idref = {}
798         status['progress'] = (float(statusi)+0.4) / len(graph)
799
800         mode = 'update'
801         if hasattr(package, 'init') or package.state == 'to install':
802             mode = 'init'
803
804         if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
805             if package.state=='to upgrade':
806                 # upgrading the module information
807                 modobj.write(cr, 1, [mid], modobj.get_values_from_terp(package.data))
808             load_init_xml(cr, m, idref, mode)
809             load_update_xml(cr, m, idref, mode)
810             load_data(cr, m, idref, mode)
811             if hasattr(package, 'demo') or (package.dbdemo and package.state != 'installed'):
812                 status['progress'] = (float(statusi)+0.75) / len(graph)
813                 load_demo_xml(cr, m, idref, mode)
814                 load_demo(cr, m, idref, mode)
815                 cr.execute('update ir_module_module set demo=%s where id=%s', (True, mid))
816
817                 # launch tests only in demo mode, as most tests will depend
818                 # on demo data. Other tests can be added into the regular
819                 # 'data' section, but should probably not alter the data,
820                 # as there is no rollback.
821                 load_test(cr, m, idref, mode)
822
823             processed_modules.append(package.name)
824
825             migrations.migrate_module(package, 'post')
826
827             if modobj:
828                 ver = release.major_version + '.' + package.data.get('version', '1.0')
829                 # Set new modules and dependencies
830                 modobj.write(cr, 1, [mid], {'state': 'installed', 'latest_version': ver})
831                 cr.commit()
832                 # Update translations for all installed languages
833                 modobj.update_translations(cr, 1, [mid], None, {'overwrite': tools.config['overwrite_existing_translations']})
834                 cr.commit()
835
836             package.state = 'installed'
837             for kind in ('init', 'demo', 'update'):
838                 if hasattr(package, kind):
839                     delattr(package, kind)
840
841         statusi += 1
842
843     cr.commit()
844
845     return processed_modules
846
847 def _check_module_names(cr, module_names):
848     mod_names = set(module_names)
849     if 'base' in mod_names:
850         # ignore dummy 'all' module
851         if 'all' in mod_names:
852             mod_names.remove('all')
853     if mod_names:
854         cr.execute("SELECT count(id) AS count FROM ir_module_module WHERE name in %s", (tuple(mod_names),))
855         if cr.dictfetchone()['count'] != len(mod_names):
856             # find out what module name(s) are incorrect:
857             cr.execute("SELECT name FROM ir_module_module")
858             incorrect_names = mod_names.difference([x['name'] for x in cr.dictfetchall()])
859             logging.getLogger('init').warning('invalid module names, ignored: %s', ", ".join(incorrect_names))
860
861 def load_modules(db, force_demo=False, status=None, update_module=False):
862
863     initialize_sys_path()
864
865     open_openerp_namespace()
866
867     if not status:
868         status = {}
869     cr = db.cursor()
870     if cr:
871         cr.execute("SELECT relname FROM pg_class WHERE relkind='r' AND relname='ir_module_module'")
872         if len(cr.fetchall())==0:
873             logger.notifyChannel("init", netsvc.LOG_INFO, "init db")
874             tools.init_db(cr)
875             tools.config["init"]["all"] = 1
876             tools.config['update']['all'] = 1
877             if not tools.config['without_demo']:
878                 tools.config["demo"]['all'] = 1
879     force = []
880     if force_demo:
881         force.append('demo')
882
883     # This is a brand new pool, just created in pooler.get_db_and_pool()
884     pool = pooler.get_pool(cr.dbname)
885
886     try:
887         processed_modules = []
888         report = tools.assertion_report()
889         # NOTE: Try to also load the modules that have been marked as uninstallable previously...
890         STATES_TO_LOAD = ['installed', 'to upgrade', 'uninstallable']
891         if 'base' in tools.config['update'] or 'all' in tools.config['update']:
892             cr.execute("update ir_module_module set state=%s where name=%s and state=%s", ('to upgrade', 'base', 'installed'))
893
894         # STEP 1: LOAD BASE (must be done before module dependencies can be computed for later steps) 
895         graph = create_graph(cr, ['base'], force)
896         if not graph:
897             logger.notifyChannel('init', netsvc.LOG_CRITICAL, 'module base cannot be loaded! (hint: verify addons-path)')
898             raise osv.osv.except_osv(_('Could not load base module'), _('module base cannot be loaded! (hint: verify addons-path)'))
899         processed_modules.extend(load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report))
900
901         if tools.config['load_language']:
902             for lang in tools.config['load_language'].split(','):
903                 tools.load_language(cr, lang)
904
905         # STEP 2: Mark other modules to be loaded/updated
906         if update_module:
907             modobj = pool.get('ir.module.module')
908             logger.notifyChannel('init', netsvc.LOG_INFO, 'updating modules list')
909             if ('base' in tools.config['init']) or ('base' in tools.config['update']):
910                 modobj.update_list(cr, 1)
911
912             _check_module_names(cr, itertools.chain(tools.config['init'].keys(), tools.config['update'].keys()))
913
914             mods = [k for k in tools.config['init'] if tools.config['init'][k]]
915             if mods:
916                 ids = modobj.search(cr, 1, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)])
917                 if ids:
918                     modobj.button_install(cr, 1, ids)
919
920             mods = [k for k in tools.config['update'] if tools.config['update'][k]]
921             if mods:
922                 ids = modobj.search(cr, 1, ['&', ('state', '=', 'installed'), ('name', 'in', mods)])
923                 if ids:
924                     modobj.button_upgrade(cr, 1, ids)
925
926             cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base'))
927
928             STATES_TO_LOAD += ['to install']
929
930
931         # STEP 3: Load marked modules (skipping base which was done in STEP 1)
932         loop_guardrail = 0
933         while True:
934             loop_guardrail += 1
935             if loop_guardrail > 100:
936                 raise ValueError('Possible recursive module tree detected, aborting.')
937             cr.execute("SELECT name from ir_module_module WHERE state IN %s" ,(tuple(STATES_TO_LOAD),))
938
939             module_list = [name for (name,) in cr.fetchall() if name not in graph]
940             if not module_list:
941                 break
942
943             new_modules_in_graph = upgrade_graph(graph, cr, module_list, force)
944             if new_modules_in_graph == 0:
945                 # nothing to load
946                 break
947
948             logger.notifyChannel('init', netsvc.LOG_DEBUG, 'Updating graph with %d more modules' % (len(module_list)))
949             processed_modules.extend(load_module_graph(cr, graph, status, report=report, skip_modules=processed_modules))
950
951         # load custom models
952         cr.execute('select model from ir_model where state=%s', ('manual',))
953         for model in cr.dictfetchall():
954             pool.get('ir.model').instanciate(cr, 1, model['model'], {})
955
956         # STEP 4: Finish and cleanup
957         if processed_modules:
958             cr.execute("""select model,name from ir_model where id NOT IN (select distinct model_id from ir_model_access)""")
959             for (model, name) in cr.fetchall():
960                 model_obj = pool.get(model)
961                 if model_obj and not isinstance(model_obj, osv.osv.osv_memory):
962                     logger.notifyChannel('init', netsvc.LOG_WARNING, 'object %s (%s) has no access rules!' % (model, name))
963
964             # Temporary warning while we remove access rights on osv_memory objects, as they have
965             # been replaced by owner-only access rights
966             cr.execute("""select distinct mod.model, mod.name from ir_model_access acc, ir_model mod where acc.model_id = mod.id""")
967             for (model, name) in cr.fetchall():
968                 model_obj = pool.get(model)
969                 if isinstance(model_obj, osv.osv.osv_memory):
970                     logger.notifyChannel('init', netsvc.LOG_WARNING, 'In-memory object %s (%s) should not have explicit access rules!' % (model, name))
971
972             cr.execute("SELECT model from ir_model")
973             for (model,) in cr.fetchall():
974                 obj = pool.get(model)
975                 if obj:
976                     obj._check_removed_columns(cr, log=True)
977                 else:
978                     logger.notifyChannel('init', netsvc.LOG_WARNING, "Model %s is referenced but not present in the orm pool!" % model)
979
980             # Cleanup orphan records
981             pool.get('ir.model.data')._process_end(cr, 1, processed_modules)
982
983         if report.get_report():
984             logger.notifyChannel('init', netsvc.LOG_INFO, report)
985
986         for kind in ('init', 'demo', 'update'):
987             tools.config[kind] = {}
988
989         cr.commit()
990         if update_module:
991             cr.execute("select id,name from ir_module_module where state=%s", ('to remove',))
992             for mod_id, mod_name in cr.fetchall():
993                 cr.execute('select model,res_id from ir_model_data where noupdate=%s and module=%s order by id desc', (False, mod_name,))
994                 for rmod, rid in cr.fetchall():
995                     uid = 1
996                     rmod_module= pool.get(rmod)
997                     if rmod_module:
998                         rmod_module.unlink(cr, uid, [rid])
999                     else:
1000                         logger.notifyChannel('init', netsvc.LOG_ERROR, 'Could not locate %s to remove res=%d' % (rmod,rid))
1001                 cr.execute('delete from ir_model_data where noupdate=%s and module=%s', (False, mod_name,))
1002                 cr.commit()
1003             #
1004             # TODO: remove menu without actions of children
1005             #
1006             while True:
1007                 cr.execute('''delete from
1008                         ir_ui_menu
1009                     where
1010                         (id not IN (select parent_id from ir_ui_menu where parent_id is not null))
1011                     and
1012                         (id not IN (select res_id from ir_values where model='ir.ui.menu'))
1013                     and
1014                         (id not IN (select res_id from ir_model_data where model='ir.ui.menu'))''')
1015                 cr.commit()
1016                 if not cr.rowcount:
1017                     break
1018                 else:
1019                     logger.notifyChannel('init', netsvc.LOG_INFO, 'removed %d unused menus' % (cr.rowcount,))
1020
1021             cr.execute("update ir_module_module set state=%s where state=%s", ('uninstalled', 'to remove',))
1022             cr.commit()
1023     finally:
1024         cr.close()
1025
1026
1027 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: