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