1 # -*- coding: utf-8 -*-
2 ##############################################################################
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>).
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.
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.
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/>.
21 ##############################################################################
34 from tools.parse_version import parse_version
35 from tools.translate import _
37 from osv import fields, osv, orm
39 class module_category(osv.osv):
40 _name = "ir.module.category"
41 _description = "Module Category"
43 def _module_nbr(self,cr,uid, ids, prop, unknow_none, context):
44 cr.execute('SELECT category_id, COUNT(*) \
45 FROM ir_module_module \
46 WHERE category_id IN %(ids)s \
47 OR category_id IN (SELECT id \
48 FROM ir_module_category \
49 WHERE parent_id IN %(ids)s) \
50 GROUP BY category_id', {'ids': tuple(ids)}
52 result = dict(cr.fetchall())
54 cr.execute('select id from ir_module_category where parent_id=%s', (id,))
55 result[id] = sum([result.get(c, 0) for (c,) in cr.fetchall()],
60 'name': fields.char("Name", size=128, required=True, select=True),
61 'parent_id': fields.many2one('ir.module.category', 'Parent Category', select=True),
62 'child_ids': fields.one2many('ir.module.category', 'parent_id', 'Child Categories'),
63 'module_nr': fields.function(_module_nbr, method=True, string='Number of Modules', type='integer')
68 class module(osv.osv):
69 _name = "ir.module.module"
70 _description = "Module"
71 __logger = logging.getLogger('base.' + _name)
74 def get_module_info(cls, name):
77 info = addons.load_information_from_description_file(name)
79 info['version'] = release.major_version + '.' + info['version']
81 cls.__logger.debug('Error when trying to fetch informations for '
82 'module %s', name, exc_info=True)
85 def _get_latest_version(self, cr, uid, ids, field_name=None, arg=None, context=None):
86 res = dict.fromkeys(ids, '')
87 for m in self.browse(cr, uid, ids):
88 res[m.id] = self.get_module_info(m.name).get('version', '')
91 def _get_views(self, cr, uid, ids, field_name=None, arg=None, context=None):
93 model_data_obj = self.pool.get('ir.model.data')
94 view_obj = self.pool.get('ir.ui.view')
95 report_obj = self.pool.get('ir.actions.report.xml')
96 menu_obj = self.pool.get('ir.ui.menu')
97 mlist = self.browse(cr, uid, ids, context=context)
100 # skip uninstalled modules below,
101 # no data to find anyway
102 if m.state in ('installed', 'to upgrade', 'to remove'):
103 mnames[m.name] = m.id
105 'menus_by_module':[],
106 'reports_by_module':[],
107 'views_by_module': []
113 view_id = model_data_obj.search(cr,uid,[('module','in', mnames.keys()),
114 ('model','in',('ir.ui.view','ir.actions.report.xml','ir.ui.menu'))])
115 for data_id in model_data_obj.browse(cr,uid,view_id,context):
116 # We use try except, because views or menus may not exist
119 res_mod_dic = res[mnames[data_id.module]]
120 if key=='ir.ui.view':
121 v = view_obj.browse(cr,uid,data_id.res_id)
122 aa = v.inherit_id and '* INHERIT ' or ''
123 res_mod_dic['views_by_module'].append(aa + v.name + '('+v.type+')')
124 elif key=='ir.actions.report.xml':
125 res_mod_dic['reports_by_module'].append(report_obj.browse(cr,uid,data_id.res_id).name)
126 elif key=='ir.ui.menu':
127 res_mod_dic['menus_by_module'].append(menu_obj.browse(cr,uid,data_id.res_id).complete_name)
129 self.__logger.warning(
130 'Data not found for reference %s[%s:%s.%s]', data_id.model,
131 data_id.res_id, data_id.model, data_id.name, exc_info=True)
134 self.__logger.warning('Unknown error while browsing %s[%s]',
135 data_id.model, data_id.res_id, exc_info=True)
137 for key, value in res.iteritems():
138 for k, v in res[key].iteritems() :
139 res[key][k] = "\n".join(sorted(v))
143 'name': fields.char("Name", size=128, readonly=True, required=True, select=True),
144 'category_id': fields.many2one('ir.module.category', 'Category', readonly=True, select=True),
145 'shortdesc': fields.char('Short Description', size=256, readonly=True, translate=True),
146 'description': fields.text("Description", readonly=True, translate=True),
147 'author': fields.char("Author", size=128, readonly=True),
148 'maintainer': fields.char('Maintainer', size=128, readonly=True),
149 'contributors': fields.text('Contributors', readonly=True),
150 'website': fields.char("Website", size=256, readonly=True),
152 # attention: Incorrect field names !!
153 # installed_version refer the latest version (the one on disk)
154 # latest_version refer the installed version (the one in database)
155 # published_version refer the version available on the repository
156 'installed_version': fields.function(_get_latest_version, method=True,
157 string='Latest version', type='char'),
158 'latest_version': fields.char('Installed version', size=64, readonly=True),
159 'published_version': fields.char('Published Version', size=64, readonly=True),
161 'url': fields.char('URL', size=128, readonly=True),
162 'dependencies_id': fields.one2many('ir.module.module.dependency',
163 'module_id', 'Dependencies', readonly=True),
164 'state': fields.selection([
165 ('uninstallable','Not Installable'),
166 ('uninstalled','Not Installed'),
167 ('installed','Installed'),
168 ('to upgrade','To be upgraded'),
169 ('to remove','To be removed'),
170 ('to install','To be installed')
171 ], string='State', readonly=True, select=True),
172 'demo': fields.boolean('Demo data'),
173 'license': fields.selection([
174 ('GPL-2', 'GPL Version 2'),
175 ('GPL-2 or any later version', 'GPL-2 or later version'),
176 ('GPL-3', 'GPL Version 3'),
177 ('GPL-3 or any later version', 'GPL-3 or later version'),
178 ('AGPL-3', 'Affero GPL-3'),
179 ('Other OSI approved licence', 'Other OSI Approved Licence'),
180 ('Other proprietary', 'Other Proprietary')
181 ], string='License', readonly=True),
182 'menus_by_module': fields.function(_get_views, method=True, string='Menus', type='text', multi="meta", store=True),
183 'reports_by_module': fields.function(_get_views, method=True, string='Reports', type='text', multi="meta", store=True),
184 'views_by_module': fields.function(_get_views, method=True, string='Views', type='text', multi="meta", store=True),
185 'certificate' : fields.char('Quality Certificate', size=64, readonly=True),
186 'web': fields.boolean('Has a web component', readonly=True),
190 'state': 'uninstalled',
197 def _name_uniq_msg(self, cr, uid, ids, context=None):
198 return _('The name of the module must be unique !')
199 def _certificate_uniq_msg(self, cr, uid, ids, context=None):
200 return _('The certificate ID of the module must be unique !')
203 ('name_uniq', 'UNIQUE (name)',_name_uniq_msg ),
204 ('certificate_uniq', 'UNIQUE (certificate)',_certificate_uniq_msg )
207 def unlink(self, cr, uid, ids, context=None):
210 if isinstance(ids, (int, long)):
213 for mod in self.read(cr, uid, ids, ['state','name'], context):
214 if mod['state'] in ('installed', 'to upgrade', 'to remove', 'to install'):
215 raise orm.except_orm(_('Error'),
216 _('You try to remove a module that is installed or will be installed'))
217 mod_names.append(mod['name'])
218 #Removing the entry from ir_model_data
219 ids_meta = self.pool.get('ir.model.data').search(cr, uid, [('name', '=', 'module_meta_information'), ('module', 'in', mod_names)])
222 self.pool.get('ir.model.data').unlink(cr, uid, ids_meta, context)
224 return super(module, self).unlink(cr, uid, ids, context=context)
227 def _check_external_dependencies(terp):
228 depends = terp.get('external_dependencies')
231 for pydep in depends.get('python', []):
232 parts = pydep.split('.')
238 f, path, descr = imp.find_module(part, path and [path] or None)
240 raise ImportError('No module named %s' % (pydep,))
242 for binary in depends.get('bin', []):
243 if tools.find_in_path(binary) is None:
244 raise Exception('Unable to find %r in path' % (binary,))
247 def check_external_dependencies(cls, module_name, newstate='to install'):
248 terp = cls.get_module_info(module_name)
250 cls._check_external_dependencies(terp)
252 if newstate == 'to install':
253 msg = _('Unable to install module "%s" because an external dependency is not met: %s')
254 elif newstate == 'to upgrade':
255 msg = _('Unable to upgrade module "%s" because an external dependency is not met: %s')
257 msg = _('Unable to process module "%s" because an external dependency is not met: %s')
258 raise orm.except_orm(_('Error'), msg % (module_name, e.args[0]))
260 def state_update(self, cr, uid, ids, newstate, states_to_update, context=None, level=100):
262 raise orm.except_orm(_('Error'), _('Recursion error in modules dependencies !'))
264 for module in self.browse(cr, uid, ids):
266 for dep in module.dependencies_id:
267 if dep.state == 'unknown':
268 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,))
269 ids2 = self.search(cr, uid, [('name','=',dep.name)])
270 if dep.state != newstate:
271 mdemo = self.state_update(cr, uid, ids2, newstate, states_to_update, context, level-1,) or mdemo
273 od = self.browse(cr, uid, ids2)[0]
274 mdemo = od.demo or mdemo
276 self.check_external_dependencies(module.name, newstate)
277 if not module.dependencies_id:
279 if module.state in states_to_update:
280 self.write(cr, uid, [module.id], {'state': newstate, 'demo':mdemo})
284 def button_install(self, cr, uid, ids, context=None):
285 return self.state_update(cr, uid, ids, 'to install', ['uninstalled'], context)
287 def button_install_cancel(self, cr, uid, ids, context=None):
288 self.write(cr, uid, ids, {'state': 'uninstalled', 'demo':False})
291 def button_uninstall(self, cr, uid, ids, context=None):
292 for module in self.browse(cr, uid, ids):
293 cr.execute('''select m.state,m.name
295 ir_module_module_dependency d
297 ir_module_module m on (d.module_id=m.id)
300 m.state not in ('uninstalled','uninstallable','to remove')''', (module.name,))
303 raise orm.except_orm(_('Error'), _('Some installed modules depend on the module you plan to Uninstall :\n %s') % '\n'.join(map(lambda x: '\t%s: %s' % (x[0], x[1]), res)))
304 self.write(cr, uid, ids, {'state': 'to remove'})
307 def button_uninstall_cancel(self, cr, uid, ids, context=None):
308 self.write(cr, uid, ids, {'state': 'installed'})
311 def button_upgrade(self, cr, uid, ids, context=None):
312 depobj = self.pool.get('ir.module.module.dependency')
313 todo = self.browse(cr, uid, ids, context=context)
314 self.update_list(cr, uid)
320 if mod.state not in ('installed','to upgrade'):
321 raise orm.except_orm(_('Error'),
322 _("Can not upgrade module '%s'. It is not installed.") % (mod.name,))
323 self.check_external_dependencies(mod.name, 'to upgrade')
324 iids = depobj.search(cr, uid, [('name', '=', mod.name)], context=context)
325 for dep in depobj.browse(cr, uid, iids, context=context):
326 if dep.module_id.state=='installed' and dep.module_id not in todo:
327 todo.append(dep.module_id)
329 ids = map(lambda x: x.id, todo)
330 self.write(cr, uid, ids, {'state':'to upgrade'}, context=context)
334 for dep in mod.dependencies_id:
335 if dep.state == 'unknown':
336 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,))
337 if dep.state == 'uninstalled':
338 ids2 = self.search(cr, uid, [('name','=',dep.name)])
339 to_install.extend(ids2)
341 self.button_install(cr, uid, to_install, context=context)
344 def button_upgrade_cancel(self, cr, uid, ids, context=None):
345 self.write(cr, uid, ids, {'state': 'installed'})
347 def button_update_translations(self, cr, uid, ids, context=None):
348 self.update_translations(cr, uid, ids)
352 def get_values_from_terp(terp):
354 'description': terp.get('description', ''),
355 'shortdesc': terp.get('name', ''),
356 'author': terp.get('author', 'Unknown'),
357 'maintainer': terp.get('maintainer', False),
358 'contributors': ', '.join(terp.get('contributors', [])) or False,
359 'website': terp.get('website', ''),
360 'license': terp.get('license', 'AGPL-3'),
361 'certificate': terp.get('certificate') or False,
362 'web': terp.get('web') or False,
365 # update the list of available packages
366 def update_list(self, cr, uid, context={}):
367 res = [0, 0] # [update, add]
369 known_mods = self.browse(cr, uid, self.search(cr, uid, []))
370 known_mods_names = dict([(m.name, m) for m in known_mods])
372 # iterate through detected modules and update/create them in db
373 for mod_name in addons.get_modules():
374 mod = known_mods_names.get(mod_name)
375 terp = self.get_module_info(mod_name)
376 values = self.get_values_from_terp(terp)
381 old = getattr(mod, key)
382 updated = isinstance(values[key], basestring) and tools.ustr(values[key]) or values[key]
383 if not old == updated:
384 updated_values[key] = values[key]
385 if terp.get('installable', True) and mod.state == 'uninstallable':
386 updated_values['state'] = 'uninstalled'
387 if parse_version(terp.get('version', '')) > parse_version(mod.latest_version or ''):
390 self.write(cr, uid, mod.id, updated_values)
392 mod_path = addons.get_module_path(mod_name)
395 if not terp or not terp.get('installable', True):
397 id = self.create(cr, uid, dict(name=mod_name, state='uninstalled', **values))
398 mod = self.browse(cr, uid, id)
401 self._update_dependencies(cr, uid, mod, terp.get('depends', []))
402 self._update_category(cr, uid, mod, terp.get('category', 'Uncategorized'))
406 def download(self, cr, uid, ids, download=True, context=None):
408 for mod in self.browse(cr, uid, ids, context=context):
411 match = re.search('-([a-zA-Z0-9\._-]+)(\.zip)', mod.url, re.I)
414 version = match.group(1)
415 if parse_version(mod.installed_version or '0') >= parse_version(version):
420 zipfile = urllib.urlopen(mod.url).read()
421 fname = addons.get_module_path(str(mod.name)+'.zip', downloaded=True)
423 fp = file(fname, 'wb')
427 self.__logger.exception('Error when trying to create module '
429 raise orm.except_orm(_('Error'), _('Can not create the module file:\n %s') % (fname,))
430 terp = self.get_module_info(mod.name)
431 self.write(cr, uid, mod.id, self.get_values_from_terp(terp))
432 cr.execute('DELETE FROM ir_module_module_dependency ' \
433 'WHERE module_id = %s', (mod.id,))
434 self._update_dependencies(cr, uid, mod, terp.get('depends',
436 self._update_category(cr, uid, mod, terp.get('category',
439 zimp = zipimport.zipimporter(fname)
440 zimp.load_module(mod.name)
443 def _update_dependencies(self, cr, uid, mod_browse, depends=None):
446 existing = set(x.name for x in mod_browse.dependencies_id)
447 needed = set(depends)
448 for dep in (needed - existing):
449 cr.execute('INSERT INTO ir_module_module_dependency (module_id, name) values (%s, %s)', (mod_browse.id, dep))
450 for dep in (existing - needed):
451 cr.execute('DELETE FROM ir_module_module_dependency WHERE module_id = %s and name = %s', (mod_browse.id, dep))
453 def _update_category(self, cr, uid, mod_browse, category='Uncategorized'):
454 current_category = mod_browse.category_id
455 current_category_path = []
456 while current_category:
457 current_category_path.insert(0, current_category.name)
458 current_category = current_category.parent_id
460 categs = category.split('/')
461 if categs != current_category_path:
465 cr.execute('SELECT id FROM ir_module_category WHERE name=%s AND parent_id=%s', (categs[0], p_id))
467 cr.execute('SELECT id FROM ir_module_category WHERE name=%s AND parent_id is NULL', (categs[0],))
470 cr.execute('INSERT INTO ir_module_category (name, parent_id) VALUES (%s, %s) RETURNING id', (categs[0], p_id))
471 c_id = cr.fetchone()[0]
476 self.write(cr, uid, [mod_browse.id], {'category_id': p_id})
478 def update_translations(self, cr, uid, ids, filter_lang=None, context=None):
479 logger = logging.getLogger('i18n')
481 pool = pooler.get_pool(cr.dbname)
482 lang_obj = pool.get('res.lang')
483 lang_ids = lang_obj.search(cr, uid, [('translatable', '=', True)])
484 filter_lang = [lang.code for lang in lang_obj.browse(cr, uid, lang_ids)]
485 elif not isinstance(filter_lang, (list, tuple)):
486 filter_lang = [filter_lang]
488 for mod in self.browse(cr, uid, ids):
489 if mod.state != 'installed':
491 modpath = addons.get_module_path(mod.name)
493 # unable to find the module. we skip
495 for lang in filter_lang:
496 iso_lang = tools.get_iso_codes(lang)
497 f = addons.get_module_resource(mod.name, 'i18n', iso_lang + '.po')
498 # Implementation notice: we must first search for the full name of
499 # the language derivative, like "en_UK", and then the generic,
501 if (not f) and '_' in iso_lang:
502 f = addons.get_module_resource(mod.name, 'i18n', iso_lang.split('_')[0] + '.po')
503 iso_lang = iso_lang.split('_')[0]
505 logger.info('module %s: loading translation file for language %s', mod.name, iso_lang)
506 tools.trans_load(cr.dbname, f, lang, verbose=False, context=context)
507 elif iso_lang != 'en':
508 logger.warning('module %s: no translation for language %s', mod.name, iso_lang)
510 def check(self, cr, uid, ids, context=None):
511 logger = logging.getLogger('init')
512 for mod in self.browse(cr, uid, ids, context=context):
513 if not mod.description:
514 logger.warn('module %s: description is empty !', mod.name)
516 if not mod.certificate or not mod.certificate.isdigit():
517 logger.info('module %s: no quality certificate', mod.name)
519 val = long(mod.certificate[2:]) % 97 == 29
521 logger.critical('module %s: invalid quality certificate: %s', mod.name, mod.certificate)
522 raise osv.except_osv(_('Error'), _('Module %s: Invalid Quality Certificate') % (mod.name,))
524 def list_web(self, cr, uid, context=None):
525 """ list_web(cr, uid, context) -> [module_name]
526 Lists all the currently installed modules with a web component.
528 Returns a list of addon names.
532 for module in self.browse(cr, uid,
535 ('state', 'in', ['installed','to upgrade','to remove'])],
538 def _web_dependencies(self, cr, uid, module, context=None):
539 for dependency in module.dependencies_id:
540 (parent,) = self.browse(cr, uid, self.search(cr, uid,
541 [('name', '=', dependency.name)], context=context),
546 self._web_dependencies(
547 cr, uid, parent, context=context)
548 def get_web(self, cr, uid, names, context=None):
549 """ get_web(cr, uid, [module_name], context) -> [{name, depends, content}]
551 Returns the web content of all the named addons.
553 The toplevel directory of the zipped content is called 'web',
554 its final naming has to be managed by the client
556 modules = self.browse(cr, uid,
557 self.search(cr, uid, [('name', 'in', names)], context=context),
559 if not modules: return []
560 self.__logger.info('Sending web content of modules %s '
561 'to web client', names)
563 {'name': module.name,
564 'depends': list(self._web_dependencies(
565 cr, uid, module, context=context)),
566 'content': addons.zip_directory(
567 addons.get_module_resource(module.name, 'web'))}
568 for module in modules
573 class module_dependency(osv.osv):
574 _name = "ir.module.module.dependency"
575 _description = "Module dependency"
577 def _state(self, cr, uid, ids, name, args, context=None):
579 mod_obj = self.pool.get('ir.module.module')
580 for md in self.browse(cr, uid, ids):
581 ids = mod_obj.search(cr, uid, [('name', '=', md.name)])
583 result[md.id] = mod_obj.read(cr, uid, [ids[0]], ['state'])[0]['state']
585 result[md.id] = 'unknown'
589 'name': fields.char('Name', size=128, select=True),
590 'module_id': fields.many2one('ir.module.module', 'Module', select=True, ondelete='cascade'),
591 'state': fields.function(_state, method=True, type='selection', selection=[
592 ('uninstallable','Uninstallable'),
593 ('uninstalled','Not Installed'),
594 ('installed','Installed'),
595 ('to upgrade','To be upgraded'),
596 ('to remove','To be removed'),
597 ('to install','To be installed'),
598 ('unknown', 'Unknown'),
599 ], string='State', readonly=True, select=True),