[FIX] in form views, replace class 'oe_highlight_on_dirty' by 'oe_highlight'
[odoo/odoo.git] / openerp / addons / base / module / module.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2012 OpenERP S.A. (<http://openerp.com>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21 import imp
22 import logging
23 import re
24 import urllib
25 import zipimport
26 import base64
27
28 from openerp import modules, pooler, release, tools, addons
29 from openerp.tools.parse_version import parse_version
30 from openerp.tools.translate import _
31 from openerp.osv import fields, osv, orm
32
33 _logger = logging.getLogger(__name__)
34
35 ACTION_DICT = {
36     'view_type': 'form',
37     'view_mode': 'form',
38     'res_model': 'base.module.upgrade',
39     'target': 'new',
40     'type': 'ir.actions.act_window',
41     'nodestroy':True,
42 }
43
44 class module_category(osv.osv):
45     _name = "ir.module.category"
46     _description = "Application"
47
48     def _module_nbr(self,cr,uid, ids, prop, unknow_none, context):
49         cr.execute('SELECT category_id, COUNT(*) \
50                       FROM ir_module_module \
51                      WHERE category_id IN %(ids)s \
52                         OR category_id IN (SELECT id \
53                                              FROM ir_module_category \
54                                             WHERE parent_id IN %(ids)s) \
55                      GROUP BY category_id', {'ids': tuple(ids)}
56                     )
57         result = dict(cr.fetchall())
58         for id in ids:
59             cr.execute('select id from ir_module_category where parent_id=%s', (id,))
60             result[id] = sum([result.get(c, 0) for (c,) in cr.fetchall()],
61                              result.get(id, 0))
62         return result
63
64     _columns = {
65         'name': fields.char("Name", size=128, required=True, translate=True, select=True),
66         'parent_id': fields.many2one('ir.module.category', 'Parent Application', select=True),
67         'child_ids': fields.one2many('ir.module.category', 'parent_id', 'Child Applications'),
68         'module_nr': fields.function(_module_nbr, string='Number of Modules', type='integer'),
69         'module_ids' : fields.one2many('ir.module.module', 'category_id', 'Modules'),
70         'description' : fields.text("Description", translate=True),
71         'sequence' : fields.integer('Sequence'),
72         'visible' : fields.boolean('Visible'),
73         'xml_id': fields.function(osv.osv.get_external_id, type='char', size=128, string="External ID"),
74     }
75     _order = 'name'
76
77     _defaults = {
78         'visible' : 1,
79     }
80
81 class module(osv.osv):
82     _name = "ir.module.module"
83     _description = "Module"
84
85     @classmethod
86     def get_module_info(cls, name):
87         info = {}
88         try:
89             info = modules.load_information_from_description_file(name)
90             info['version'] = release.major_version + '.' + info['version']
91         except Exception:
92             _logger.debug('Error when trying to fetch informations for '
93                           'module %s', name, exc_info=True)
94         return info
95
96     def _get_latest_version(self, cr, uid, ids, field_name=None, arg=None, context=None):
97         res = dict.fromkeys(ids, '')
98         for m in self.browse(cr, uid, ids):
99             res[m.id] = self.get_module_info(m.name).get('version', '')
100         return res
101
102     def _get_views(self, cr, uid, ids, field_name=None, arg=None, context=None):
103         res = {}
104         model_data_obj = self.pool.get('ir.model.data')
105         view_obj = self.pool.get('ir.ui.view')
106         report_obj = self.pool.get('ir.actions.report.xml')
107         menu_obj = self.pool.get('ir.ui.menu')
108
109         dmodels = []
110         if field_name is None or 'views_by_module' in field_name:
111             dmodels.append('ir.ui.view')
112         if field_name is None or 'reports_by_module' in field_name:
113             dmodels.append('ir.actions.report.xml')
114         if field_name is None or 'menus_by_module' in field_name:
115             dmodels.append('ir.ui.menu')
116         assert dmodels, "no models for %s" % field_name
117
118         for module_rec in self.browse(cr, uid, ids, context=context):
119             res[module_rec.id] = {
120                 'menus_by_module': [],
121                 'reports_by_module': [],
122                 'views_by_module': []
123             }
124
125             # Skip uninstalled modules below, no data to find anyway.
126             if module_rec.state not in ('installed', 'to upgrade', 'to remove'):
127                 continue
128
129             # then, search and group ir.model.data records
130             imd_models = dict( [(m,[]) for m in dmodels])
131             imd_ids = model_data_obj.search(cr,uid,[('module','=', module_rec.name),
132                 ('model','in',tuple(dmodels))])
133
134             for imd_res in model_data_obj.read(cr, uid, imd_ids, ['model', 'res_id'], context=context):
135                 imd_models[imd_res['model']].append(imd_res['res_id'])
136
137             # For each one of the models, get the names of these ids.
138             # We use try except, because views or menus may not exist.
139             try:
140                 res_mod_dic = res[module_rec.id]
141                 for v in view_obj.browse(cr, uid, imd_models.get('ir.ui.view', []), context=context):
142                     aa = v.inherit_id and '* INHERIT ' or ''
143                     res_mod_dic['views_by_module'].append(aa + v.name + '('+v.type+')')
144
145                 for rx in report_obj.browse(cr, uid, imd_models.get('ir.actions.report.xml', []), context=context):
146                     res_mod_dic['reports_by_module'].append(rx.name)
147
148                 for um in menu_obj.browse(cr, uid, imd_models.get('ir.ui.menu', []), context=context):
149                     res_mod_dic['menus_by_module'].append(um.complete_name)
150             except KeyError, e:
151                 _logger.warning(
152                       'Data not found for items of %s', module_rec.name)
153             except AttributeError, e:
154                 _logger.warning(
155                       'Data not found for items of %s %s', module_rec.name, str(e))
156             except Exception, e:
157                 _logger.warning('Unknown error while fetching data of %s',
158                       module_rec.name, exc_info=True)
159         for key, _ in res.iteritems():
160             for k, v in res[key].iteritems():
161                 res[key][k] = "\n".join(sorted(v))
162         return res
163
164     def _get_icon_image(self, cr, uid, ids, field_name=None, arg=None, context=None):
165         res = dict.fromkeys(ids, '')
166         for module in self.browse(cr, uid, ids, context=context):
167             path = addons.get_module_resource(module.name, 'static', 'src', 'img', 'icon.png')
168             if path:
169                 image_file = tools.file_open(path, 'rb')
170                 try:
171                     res[module.id] = image_file.read().encode('base64')
172                 finally:
173                     image_file.close()
174         return res
175
176     _columns = {
177         'name': fields.char("Technical Name", size=128, readonly=True, required=True, select=True),
178         'category_id': fields.many2one('ir.module.category', 'Category', readonly=True, select=True),
179         'shortdesc': fields.char('Module Name', size=256, readonly=True, translate=True),
180         'description': fields.text("Description", readonly=True, translate=True),
181         'author': fields.char("Author", size=128, readonly=True),
182         'maintainer': fields.char('Maintainer', size=128, readonly=True),
183         'contributors': fields.text('Contributors', readonly=True),
184         'website': fields.char("Website", size=256, readonly=True),
185
186         # attention: Incorrect field names !!
187         #   installed_version refer the latest version (the one on disk)
188         #   latest_version refer the installed version (the one in database)
189         #   published_version refer the version available on the repository
190         'installed_version': fields.function(_get_latest_version,
191             string='Latest Version', type='char'),
192         'latest_version': fields.char('Installed Version', size=64, readonly=True),
193         'published_version': fields.char('Published Version', size=64, readonly=True),
194
195         'url': fields.char('URL', size=128, readonly=True),
196         'sequence': fields.integer('Sequence'),
197         'dependencies_id': fields.one2many('ir.module.module.dependency',
198             'module_id', 'Dependencies', readonly=True),
199         'auto_install': fields.boolean('Automatic Installation',
200             help='An auto-installable module is automatically installed by the '
201             'system when all its dependencies are satisfied. '
202             'If the module has no dependency, it is always installed.'),
203         'state': fields.selection([
204             ('uninstallable','Not Installable'),
205             ('uninstalled','Not Installed'),
206             ('installed','Installed'),
207             ('to upgrade','To be upgraded'),
208             ('to remove','To be removed'),
209             ('to install','To be installed')
210         ], string='State', readonly=True, select=True),
211         'demo': fields.boolean('Demo Data', readonly=True),
212         'license': fields.selection([
213                 ('GPL-2', 'GPL Version 2'),
214                 ('GPL-2 or any later version', 'GPL-2 or later version'),
215                 ('GPL-3', 'GPL Version 3'),
216                 ('GPL-3 or any later version', 'GPL-3 or later version'),
217                 ('AGPL-3', 'Affero GPL-3'),
218                 ('Other OSI approved licence', 'Other OSI Approved Licence'),
219                 ('Other proprietary', 'Other Proprietary')
220             ], string='License', readonly=True),
221         'menus_by_module': fields.function(_get_views, string='Menus', type='text', multi="meta", store=True),
222         'reports_by_module': fields.function(_get_views, string='Reports', type='text', multi="meta", store=True),
223         'views_by_module': fields.function(_get_views, string='Views', type='text', multi="meta", store=True),
224         'certificate' : fields.char('Quality Certificate', size=64, readonly=True),
225         'application': fields.boolean('Application', readonly=True),
226         'icon': fields.char('Icon URL', size=128),
227         'icon_image': fields.function(_get_icon_image, string='Icon', type="binary"),
228     }
229
230     _defaults = {
231         'state': 'uninstalled',
232         'sequence': 100,
233         'demo': False,
234         'license': 'AGPL-3',
235     }
236     _order = 'sequence,name'
237
238     def _name_uniq_msg(self, cr, uid, ids, context=None):
239         return _('The name of the module must be unique !')
240     def _certificate_uniq_msg(self, cr, uid, ids, context=None):
241         return _('The certificate ID of the module must be unique !')
242
243     _sql_constraints = [
244         ('name_uniq', 'UNIQUE (name)',_name_uniq_msg ),
245         ('certificate_uniq', 'UNIQUE (certificate)',_certificate_uniq_msg )
246     ]
247
248     def unlink(self, cr, uid, ids, context=None):
249         if not ids:
250             return True
251         if isinstance(ids, (int, long)):
252             ids = [ids]
253         mod_names = []
254         for mod in self.read(cr, uid, ids, ['state','name'], context):
255             if mod['state'] in ('installed', 'to upgrade', 'to remove', 'to install'):
256                 raise orm.except_orm(_('Error'),
257                         _('You try to remove a module that is installed or will be installed'))
258             mod_names.append(mod['name'])
259         #Removing the entry from ir_model_data
260         #ids_meta = self.pool.get('ir.model.data').search(cr, uid, [('name', '=', 'module_meta_information'), ('module', 'in', mod_names)])
261
262         #if ids_meta:
263         #    self.pool.get('ir.model.data').unlink(cr, uid, ids_meta, context)
264
265         return super(module, self).unlink(cr, uid, ids, context=context)
266
267     @staticmethod
268     def _check_external_dependencies(terp):
269         depends = terp.get('external_dependencies')
270         if not depends:
271             return
272         for pydep in depends.get('python', []):
273             parts = pydep.split('.')
274             parts.reverse()
275             path = None
276             while parts:
277                 part = parts.pop()
278                 try:
279                     _, path, _ = imp.find_module(part, path and [path] or None)
280                 except ImportError:
281                     raise ImportError('No module named %s' % (pydep,))
282
283         for binary in depends.get('bin', []):
284             if tools.find_in_path(binary) is None:
285                 raise Exception('Unable to find %r in path' % (binary,))
286
287     @classmethod
288     def check_external_dependencies(cls, module_name, newstate='to install'):
289         terp = cls.get_module_info(module_name)
290         try:
291             cls._check_external_dependencies(terp)
292         except Exception, e:
293             if newstate == 'to install':
294                 msg = _('Unable to install module "%s" because an external dependency is not met: %s')
295             elif newstate == 'to upgrade':
296                 msg = _('Unable to upgrade module "%s" because an external dependency is not met: %s')
297             else:
298                 msg = _('Unable to process module "%s" because an external dependency is not met: %s')
299             raise orm.except_orm(_('Error'), msg % (module_name, e.args[0]))
300
301     def state_update(self, cr, uid, ids, newstate, states_to_update, context=None, level=100):
302         if level<1:
303             raise orm.except_orm(_('Error'), _('Recursion error in modules dependencies !'))
304         demo = False
305         for module in self.browse(cr, uid, ids, context=context):
306             mdemo = False
307             for dep in module.dependencies_id:
308                 if dep.state == 'unknown':
309                     raise orm.except_orm(_('Error'), _("You try to install module '%s' that depends on module '%s'.\nBut the latter module is not available in your system.") % (module.name, dep.name,))
310                 ids2 = self.search(cr, uid, [('name','=',dep.name)])
311                 if dep.state != newstate:
312                     mdemo = self.state_update(cr, uid, ids2, newstate, states_to_update, context, level-1,) or mdemo
313                 else:
314                     od = self.browse(cr, uid, ids2)[0]
315                     mdemo = od.demo or mdemo
316
317             self.check_external_dependencies(module.name, newstate)
318             if not module.dependencies_id:
319                 mdemo = module.demo
320             if module.state in states_to_update:
321                 self.write(cr, uid, [module.id], {'state': newstate, 'demo':mdemo})
322             demo = demo or mdemo
323         return demo
324
325     def button_install(self, cr, uid, ids, context=None):
326
327         # Mark the given modules to be installed.
328         self.state_update(cr, uid, ids, 'to install', ['uninstalled'], context)
329
330         # Mark (recursively) the newly satisfied modules to also be installed:
331
332         # Select all auto-installable (but not yet installed) modules.
333         domain = [('state', '=', 'uninstalled'), ('auto_install', '=', True),]
334         uninstalled_ids = self.search(cr, uid, domain, context=context)
335         uninstalled_modules = self.browse(cr, uid, uninstalled_ids, context=context)
336
337         # Keep those with all their dependencies satisfied.
338         def all_depencies_satisfied(m):
339             return all(x.state in ('to install', 'installed', 'to upgrade') for x in m.dependencies_id)
340         to_install_modules = filter(all_depencies_satisfied, uninstalled_modules)
341         to_install_ids = map(lambda m: m.id, to_install_modules)
342
343         # Mark them to be installed.
344         if to_install_ids:
345             self.button_install(cr, uid, to_install_ids, context=context)
346         return dict(ACTION_DICT, name=_('Install'))
347
348     def button_immediate_install(self, cr, uid, ids, context=None):
349         """ Installs the selected module(s) immediately and fully,
350         returns the next res.config action to execute
351
352         :param ids: identifiers of the modules to install
353         :returns: next res.config item to execute
354         :rtype: dict[str, object]
355         """
356         self.button_install(cr, uid, ids, context=context)
357         cr.commit()
358         _, pool = pooler.restart_pool(cr.dbname, update_module=True)
359
360         config = pool.get('res.config').next(cr, uid, [], context=context) or {}
361         if config.get('type') not in ('ir.actions.reload', 'ir.actions.act_window_close'):
362             return config
363
364         # reload the client
365         menu_ids = self.root_menus(cr,uid,ids,context)
366         return {
367             'type': 'ir.actions.client',
368             'tag': 'reload',
369             'params': {'menu_id': menu_ids and menu_ids[0] or False},
370         }
371
372     def button_install_cancel(self, cr, uid, ids, context=None):
373         self.write(cr, uid, ids, {'state': 'uninstalled', 'demo':False})
374         return True
375
376     def module_uninstall(self, cr, uid, ids, context=None):
377         """Perform the various steps required to uninstall a module completely
378         including the deletion of all database structures created by the module:
379         tables, columns, constraints, etc."""
380         ir_model_data = self.pool.get('ir.model.data')
381         ir_model_constraint = self.pool.get('ir.model.constraint')
382         modules_to_remove = [m.name for m in self.browse(cr, uid, ids, context)]
383         constraint_ids = ir_model_constraint.search(cr, uid, [('module', 'in', modules_to_remove)])
384         ir_model_constraint._module_data_uninstall(cr, uid, constraint_ids, context)
385         ir_model_data._module_data_uninstall(cr, uid, modules_to_remove, context)
386         self.write(cr, uid, ids, {'state': 'uninstalled'})
387         return True
388
389     def downstream_dependencies(self, cr, uid, ids, known_dep_ids=None,
390                                 exclude_states=['uninstalled','uninstallable','to remove'],
391                                 context=None):
392         """Return the ids of all modules that directly or indirectly depend
393         on the given module `ids`, and that satisfy the `exclude_states`
394         filter"""
395         if not ids: return []
396         known_dep_ids = set(known_dep_ids or [])
397         cr.execute('''SELECT DISTINCT m.id
398                         FROM
399                             ir_module_module_dependency d
400                         JOIN
401                             ir_module_module m ON (d.module_id=m.id)
402                         WHERE
403                             d.name IN (SELECT name from ir_module_module where id in %s) AND
404                             m.state NOT IN %s AND
405                             m.id NOT IN %s ''',
406                    (tuple(ids),tuple(exclude_states), tuple(known_dep_ids or ids)))
407         new_dep_ids = set([m[0] for m in cr.fetchall()])
408         missing_mod_ids = new_dep_ids - known_dep_ids
409         known_dep_ids |= new_dep_ids
410         if missing_mod_ids:
411             known_dep_ids |= set(self.downstream_dependencies(cr, uid, list(missing_mod_ids),
412                                                           known_dep_ids, exclude_states,context))
413         return list(known_dep_ids)
414
415     def button_uninstall(self, cr, uid, ids, context=None):
416         if any(m.name == 'base' for m in self.browse(cr, uid, ids)):
417             raise orm.except_orm(_('Error'), _("The `base` module cannot be uninstalled"))
418         dep_ids = self.downstream_dependencies(cr, uid, ids, context=context)
419         self.write(cr, uid, ids + dep_ids, {'state': 'to remove'})
420         return dict(ACTION_DICT, name=_('Uninstall'))
421
422     def button_uninstall_cancel(self, cr, uid, ids, context=None):
423         self.write(cr, uid, ids, {'state': 'installed'})
424         return True
425
426     def button_upgrade(self, cr, uid, ids, context=None):
427         depobj = self.pool.get('ir.module.module.dependency')
428         todo = self.browse(cr, uid, ids, context=context)
429         self.update_list(cr, uid)
430
431         i = 0
432         while i<len(todo):
433             mod = todo[i]
434             i += 1
435             if mod.state not in ('installed','to upgrade'):
436                 raise orm.except_orm(_('Error'),
437                         _("Can not upgrade module '%s'. It is not installed.") % (mod.name,))
438             self.check_external_dependencies(mod.name, 'to upgrade')
439             iids = depobj.search(cr, uid, [('name', '=', mod.name)], context=context)
440             for dep in depobj.browse(cr, uid, iids, context=context):
441                 if dep.module_id.state=='installed' and dep.module_id not in todo:
442                     todo.append(dep.module_id)
443
444         ids = map(lambda x: x.id, todo)
445         self.write(cr, uid, ids, {'state':'to upgrade'}, context=context)
446
447         to_install = []
448         for mod in todo:
449             for dep in mod.dependencies_id:
450                 if dep.state == 'unknown':
451                     raise orm.except_orm(_('Error'), _('You try to upgrade a module that depends on the module: %s.\nBut this module is not available in your system.') % (dep.name,))
452                 if dep.state == 'uninstalled':
453                     ids2 = self.search(cr, uid, [('name','=',dep.name)])
454                     to_install.extend(ids2)
455
456         self.button_install(cr, uid, to_install, context=context)
457         return dict(ACTION_DICT, name=_('Upgrade'))
458
459     def button_upgrade_cancel(self, cr, uid, ids, context=None):
460         self.write(cr, uid, ids, {'state': 'installed'})
461         return True
462
463     def button_update_translations(self, cr, uid, ids, context=None):
464         self.update_translations(cr, uid, ids)
465         return True
466
467     @staticmethod
468     def get_values_from_terp(terp):
469         return {
470             'description': terp.get('description', ''),
471             'shortdesc': terp.get('name', ''),
472             'author': terp.get('author', 'Unknown'),
473             'maintainer': terp.get('maintainer', False),
474             'contributors': ', '.join(terp.get('contributors', [])) or False,
475             'website': terp.get('website', ''),
476             'license': terp.get('license', 'AGPL-3'),
477             'certificate': terp.get('certificate') or False,
478             'sequence': terp.get('sequence', 100),
479             'application': terp.get('application', False),
480             'auto_install': terp.get('auto_install', False),
481             'icon': terp.get('icon', False),
482         }
483
484     # update the list of available packages
485     def update_list(self, cr, uid, context=None):
486         res = [0, 0] # [update, add]
487
488         known_mods = self.browse(cr, uid, self.search(cr, uid, []))
489         known_mods_names = dict([(m.name, m) for m in known_mods])
490
491         # iterate through detected modules and update/create them in db
492         for mod_name in modules.get_modules():
493             mod = known_mods_names.get(mod_name)
494             terp = self.get_module_info(mod_name)
495             values = self.get_values_from_terp(terp)
496
497             if mod:
498                 updated_values = {}
499                 for key in values:
500                     old = getattr(mod, key)
501                     updated = isinstance(values[key], basestring) and tools.ustr(values[key]) or values[key]
502                     if not old == updated:
503                         updated_values[key] = values[key]
504                 if terp.get('installable', True) and mod.state == 'uninstallable':
505                     updated_values['state'] = 'uninstalled'
506                 if parse_version(terp.get('version', '')) > parse_version(mod.latest_version or ''):
507                     res[0] += 1
508                 if updated_values:
509                     self.write(cr, uid, mod.id, updated_values)
510             else:
511                 mod_path = modules.get_module_path(mod_name)
512                 if not mod_path:
513                     continue
514                 if not terp or not terp.get('installable', True):
515                     continue
516                 id = self.create(cr, uid, dict(name=mod_name, state='uninstalled', **values))
517                 mod = self.browse(cr, uid, id)
518                 res[1] += 1
519
520             self._update_dependencies(cr, uid, mod, terp.get('depends', []))
521             self._update_category(cr, uid, mod, terp.get('category', 'Uncategorized'))
522
523         return res
524
525     def download(self, cr, uid, ids, download=True, context=None):
526         res = []
527         for mod in self.browse(cr, uid, ids, context=context):
528             if not mod.url:
529                 continue
530             match = re.search('-([a-zA-Z0-9\._-]+)(\.zip)', mod.url, re.I)
531             version = '0'
532             if match:
533                 version = match.group(1)
534             if parse_version(mod.installed_version or '0') >= parse_version(version):
535                 continue
536             res.append(mod.url)
537             if not download:
538                 continue
539             zip_content = urllib.urlopen(mod.url).read()
540             fname = modules.get_module_path(str(mod.name)+'.zip', downloaded=True)
541             try:
542                 with open(fname, 'wb') as fp:
543                     fp.write(zip_content)
544             except Exception:
545                 _logger.exception('Error when trying to create module '
546                                   'file %s', fname)
547                 raise orm.except_orm(_('Error'), _('Can not create the module file:\n %s') % (fname,))
548             terp = self.get_module_info(mod.name)
549             self.write(cr, uid, mod.id, self.get_values_from_terp(terp))
550             cr.execute('DELETE FROM ir_module_module_dependency ' \
551                     'WHERE module_id = %s', (mod.id,))
552             self._update_dependencies(cr, uid, mod, terp.get('depends',
553                 []))
554             self._update_category(cr, uid, mod, terp.get('category',
555                 'Uncategorized'))
556             # Import module
557             zimp = zipimport.zipimporter(fname)
558             zimp.load_module(mod.name)
559         return res
560
561     def _update_dependencies(self, cr, uid, mod_browse, depends=None):
562         if depends is None:
563             depends = []
564         existing = set(x.name for x in mod_browse.dependencies_id)
565         needed = set(depends)
566         for dep in (needed - existing):
567             cr.execute('INSERT INTO ir_module_module_dependency (module_id, name) values (%s, %s)', (mod_browse.id, dep))
568         for dep in (existing - needed):
569             cr.execute('DELETE FROM ir_module_module_dependency WHERE module_id = %s and name = %s', (mod_browse.id, dep))
570
571     def _update_category(self, cr, uid, mod_browse, category='Uncategorized'):
572         current_category = mod_browse.category_id
573         current_category_path = []
574         while current_category:
575             current_category_path.insert(0, current_category.name)
576             current_category = current_category.parent_id
577
578         categs = category.split('/')
579         if categs != current_category_path:
580             p_id = None
581             while categs:
582                 if p_id is not None:
583                     cr.execute('SELECT id FROM ir_module_category WHERE name=%s AND parent_id=%s', (categs[0], p_id))
584                 else:
585                     cr.execute('SELECT id FROM ir_module_category WHERE name=%s AND parent_id is NULL', (categs[0],))
586                 c_id = cr.fetchone()
587                 if not c_id:
588                     cr.execute('INSERT INTO ir_module_category (name, parent_id) VALUES (%s, %s) RETURNING id', (categs[0], p_id))
589                     c_id = cr.fetchone()[0]
590                 else:
591                     c_id = c_id[0]
592                 p_id = c_id
593                 categs = categs[1:]
594             self.write(cr, uid, [mod_browse.id], {'category_id': p_id})
595
596     def update_translations(self, cr, uid, ids, filter_lang=None, context=None):
597         if context is None:
598             context = {}
599         if not filter_lang:
600             pool = pooler.get_pool(cr.dbname)
601             lang_obj = pool.get('res.lang')
602             lang_ids = lang_obj.search(cr, uid, [('translatable', '=', True)])
603             filter_lang = [lang.code for lang in lang_obj.browse(cr, uid, lang_ids)]
604         elif not isinstance(filter_lang, (list, tuple)):
605             filter_lang = [filter_lang]
606
607         for mod in self.browse(cr, uid, ids):
608             if mod.state != 'installed':
609                 continue
610             modpath = modules.get_module_path(mod.name)
611             if not modpath:
612                 # unable to find the module. we skip
613                 continue
614             for lang in filter_lang:
615                 iso_lang = tools.get_iso_codes(lang)
616                 f = modules.get_module_resource(mod.name, 'i18n', iso_lang + '.po')
617                 context2 = context and context.copy() or {}
618                 if f and '_' in iso_lang:
619                     iso_lang2 = iso_lang.split('_')[0]
620                     f2 = modules.get_module_resource(mod.name, 'i18n', iso_lang2 + '.po')
621                     if f2:
622                         _logger.info('module %s: loading base translation file %s for language %s', mod.name, iso_lang2, lang)
623                         tools.trans_load(cr, f2, lang, verbose=False, context=context)
624                         context2['overwrite'] = True
625                 # Implementation notice: we must first search for the full name of
626                 # the language derivative, like "en_UK", and then the generic,
627                 # like "en".
628                 if (not f) and '_' in iso_lang:
629                     iso_lang = iso_lang.split('_')[0]
630                     f = modules.get_module_resource(mod.name, 'i18n', iso_lang + '.po')
631                 if f:
632                     _logger.info('module %s: loading translation file (%s) for language %s', mod.name, iso_lang, lang)
633                     tools.trans_load(cr, f, lang, verbose=False, context=context2)
634                 elif iso_lang != 'en':
635                     _logger.warning('module %s: no translation for language %s', mod.name, iso_lang)
636
637     def check(self, cr, uid, ids, context=None):
638         for mod in self.browse(cr, uid, ids, context=context):
639             if not mod.description:
640                 _logger.warning('module %s: description is empty !', mod.name)
641
642             if not mod.certificate or not mod.certificate.isdigit():
643                 _logger.info('module %s: no quality certificate', mod.name)
644             else:
645                 val = long(mod.certificate[2:]) % 97 == 29
646                 if not val:
647                     _logger.critical('module %s: invalid quality certificate: %s', mod.name, mod.certificate)
648                     raise osv.except_osv(_('Error'), _('Module %s: Invalid Quality Certificate') % (mod.name,))
649
650     def root_menus(self, cr, uid, ids, context=None):
651         """ Return root menu ids the menus created by the modules whose ids are
652         provided.
653
654         :param list[int] ids: modules to get menus from
655         """
656         values = self.read(cr, uid, ids, ['name'], context=context)
657         module_names = [i['name'] for i in values]
658
659         ids = self.pool.get('ir.model.data').search(cr, uid, [ ('model', '=', 'ir.ui.menu'), ('module', 'in', module_names) ], context=context)
660         values = self.pool.get('ir.model.data').read(cr, uid, ids, ['res_id'], context=context)
661         all_menu_ids = [i['res_id'] for i in values]
662
663         root_menu_ids = []
664         for menu in self.pool.get('ir.ui.menu').browse(cr, uid, all_menu_ids, context=context):
665             while menu.parent_id:
666                 menu = menu.parent_id
667             if not menu.id in root_menu_ids:
668                 root_menu_ids.append((menu.sequence,menu.id))
669         root_menu_ids.sort()
670         root_menu_ids = [i[1] for i in root_menu_ids]
671         return root_menu_ids
672
673 class module_dependency(osv.osv):
674     _name = "ir.module.module.dependency"
675     _description = "Module dependency"
676
677     def _state(self, cr, uid, ids, name, args, context=None):
678         result = {}
679         mod_obj = self.pool.get('ir.module.module')
680         for md in self.browse(cr, uid, ids):
681             ids = mod_obj.search(cr, uid, [('name', '=', md.name)])
682             if ids:
683                 result[md.id] = mod_obj.read(cr, uid, [ids[0]], ['state'])[0]['state']
684             else:
685                 result[md.id] = 'unknown'
686         return result
687
688     _columns = {
689         # The dependency name
690         'name': fields.char('Name',  size=128, select=True),
691
692         # The module that depends on it
693         'module_id': fields.many2one('ir.module.module', 'Module', select=True, ondelete='cascade'),
694
695         'state': fields.function(_state, type='selection', selection=[
696             ('uninstallable','Uninstallable'),
697             ('uninstalled','Not Installed'),
698             ('installed','Installed'),
699             ('to upgrade','To be upgraded'),
700             ('to remove','To be removed'),
701             ('to install','To be installed'),
702             ('unknown', 'Unknown'),
703             ], string='State', readonly=True, select=True),
704     }
705
706 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: