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 ##############################################################################
33 import openerp.modules as addons
38 from tools.parse_version import parse_version
39 from tools.translate import _
41 from osv import fields, osv, orm
47 'res_model': 'base.module.upgrade',
49 'type': 'ir.actions.act_window',
53 class module_category(osv.osv):
54 _name = "ir.module.category"
55 _description = "Module Category"
57 def _module_nbr(self,cr,uid, ids, prop, unknow_none, context):
58 cr.execute('SELECT category_id, COUNT(*) \
59 FROM ir_module_module \
60 WHERE category_id IN %(ids)s \
61 OR category_id IN (SELECT id \
62 FROM ir_module_category \
63 WHERE parent_id IN %(ids)s) \
64 GROUP BY category_id', {'ids': tuple(ids)}
66 result = dict(cr.fetchall())
68 cr.execute('select id from ir_module_category where parent_id=%s', (id,))
69 result[id] = sum([result.get(c, 0) for (c,) in cr.fetchall()],
74 'name': fields.char("Name", size=128, required=True, select=True),
75 'parent_id': fields.many2one('ir.module.category', 'Parent Category', select=True),
76 'child_ids': fields.one2many('ir.module.category', 'parent_id', 'Child Categories'),
77 'module_nr': fields.function(_module_nbr, method=True, string='Number of Modules', type='integer')
82 class module(osv.osv):
83 _name = "ir.module.module"
84 _description = "Module"
85 __logger = logging.getLogger('base.' + _name)
88 def get_module_info(cls, name):
91 info = addons.load_information_from_description_file(name)
92 info['version'] = release.major_version + '.' + info['version']
94 cls.__logger.debug('Error when trying to fetch informations for '
95 'module %s', name, exc_info=True)
98 def _get_latest_version(self, cr, uid, ids, field_name=None, arg=None, context=None):
99 res = dict.fromkeys(ids, '')
100 for m in self.browse(cr, uid, ids):
101 res[m.id] = self.get_module_info(m.name).get('version', '')
104 def _get_views(self, cr, uid, ids, field_name=None, arg=None, context=None):
106 model_data_obj = self.pool.get('ir.model.data')
107 view_obj = self.pool.get('ir.ui.view')
108 report_obj = self.pool.get('ir.actions.report.xml')
109 menu_obj = self.pool.get('ir.ui.menu')
110 mlist = self.browse(cr, uid, ids, context=context)
113 # skip uninstalled modules below,
114 # no data to find anyway
115 if m.state in ('installed', 'to upgrade', 'to remove'):
116 mnames[m.name] = m.id
118 'menus_by_module':[],
119 'reports_by_module':[],
120 'views_by_module': []
126 view_id = model_data_obj.search(cr,uid,[('module','in', mnames.keys()),
127 ('model','in',('ir.ui.view','ir.actions.report.xml','ir.ui.menu'))])
128 for data_id in model_data_obj.browse(cr,uid,view_id,context):
129 # We use try except, because views or menus may not exist
132 res_mod_dic = res[mnames[data_id.module]]
133 if key=='ir.ui.view':
134 v = view_obj.browse(cr,uid,data_id.res_id)
135 aa = v.inherit_id and '* INHERIT ' or ''
136 res_mod_dic['views_by_module'].append(aa + v.name + '('+v.type+')')
137 elif key=='ir.actions.report.xml':
138 res_mod_dic['reports_by_module'].append(report_obj.browse(cr,uid,data_id.res_id).name)
139 elif key=='ir.ui.menu':
140 res_mod_dic['menus_by_module'].append(menu_obj.browse(cr,uid,data_id.res_id).complete_name)
142 self.__logger.warning(
143 'Data not found for reference %s[%s:%s.%s]', data_id.model,
144 data_id.res_id, data_id.model, data_id.name, exc_info=True)
147 self.__logger.warning('Unknown error while browsing %s[%s]',
148 data_id.model, data_id.res_id, exc_info=True)
150 for key, value in res.iteritems():
151 for k, v in res[key].iteritems() :
152 res[key][k] = "\n".join(sorted(v))
156 'name': fields.char("Name", size=128, readonly=True, required=True, select=True),
157 'category_id': fields.many2one('ir.module.category', 'Category', readonly=True, select=True),
158 'shortdesc': fields.char('Short Description', size=256, readonly=True, translate=True),
159 'description': fields.text("Description", readonly=True, translate=True),
160 'author': fields.char("Author", size=128, readonly=True),
161 'maintainer': fields.char('Maintainer', size=128, readonly=True),
162 'contributors': fields.text('Contributors', readonly=True),
163 'website': fields.char("Website", size=256, readonly=True),
165 # attention: Incorrect field names !!
166 # installed_version refer the latest version (the one on disk)
167 # latest_version refer the installed version (the one in database)
168 # published_version refer the version available on the repository
169 'installed_version': fields.function(_get_latest_version, method=True,
170 string='Latest version', type='char'),
171 'latest_version': fields.char('Installed version', size=64, readonly=True),
172 'published_version': fields.char('Published Version', size=64, readonly=True),
174 'url': fields.char('URL', size=128, readonly=True),
175 'dependencies_id': fields.one2many('ir.module.module.dependency',
176 'module_id', 'Dependencies', readonly=True),
177 'state': fields.selection([
178 ('uninstallable','Not Installable'),
179 ('uninstalled','Not Installed'),
180 ('installed','Installed'),
181 ('to upgrade','To be upgraded'),
182 ('to remove','To be removed'),
183 ('to install','To be installed')
184 ], string='State', readonly=True, select=True),
185 'demo': fields.boolean('Demo data'),
186 'license': fields.selection([
187 ('GPL-2', 'GPL Version 2'),
188 ('GPL-2 or any later version', 'GPL-2 or later version'),
189 ('GPL-3', 'GPL Version 3'),
190 ('GPL-3 or any later version', 'GPL-3 or later version'),
191 ('AGPL-3', 'Affero GPL-3'),
192 ('Other OSI approved licence', 'Other OSI Approved Licence'),
193 ('Other proprietary', 'Other Proprietary')
194 ], string='License', readonly=True),
195 'menus_by_module': fields.function(_get_views, method=True, string='Menus', type='text', multi="meta", store=True),
196 'reports_by_module': fields.function(_get_views, method=True, string='Reports', type='text', multi="meta", store=True),
197 'views_by_module': fields.function(_get_views, method=True, string='Views', type='text', multi="meta", store=True),
198 'certificate' : fields.char('Quality Certificate', size=64, readonly=True),
199 'web': fields.boolean('Has a web component', readonly=True),
203 'state': 'uninstalled',
210 def _name_uniq_msg(self, cr, uid, ids, context=None):
211 return _('The name of the module must be unique !')
212 def _certificate_uniq_msg(self, cr, uid, ids, context=None):
213 return _('The certificate ID of the module must be unique !')
216 ('name_uniq', 'UNIQUE (name)',_name_uniq_msg ),
217 ('certificate_uniq', 'UNIQUE (certificate)',_certificate_uniq_msg )
220 def unlink(self, cr, uid, ids, context=None):
223 if isinstance(ids, (int, long)):
226 for mod in self.read(cr, uid, ids, ['state','name'], context):
227 if mod['state'] in ('installed', 'to upgrade', 'to remove', 'to install'):
228 raise orm.except_orm(_('Error'),
229 _('You try to remove a module that is installed or will be installed'))
230 mod_names.append(mod['name'])
231 #Removing the entry from ir_model_data
232 ids_meta = self.pool.get('ir.model.data').search(cr, uid, [('name', '=', 'module_meta_information'), ('module', 'in', mod_names)])
235 self.pool.get('ir.model.data').unlink(cr, uid, ids_meta, context)
237 return super(module, self).unlink(cr, uid, ids, context=context)
240 def _check_external_dependencies(terp):
241 depends = terp.get('external_dependencies')
244 for pydep in depends.get('python', []):
245 parts = pydep.split('.')
251 f, path, descr = imp.find_module(part, path and [path] or None)
253 raise ImportError('No module named %s' % (pydep,))
255 for binary in depends.get('bin', []):
256 if tools.find_in_path(binary) is None:
257 raise Exception('Unable to find %r in path' % (binary,))
260 def check_external_dependencies(cls, module_name, newstate='to install'):
261 terp = cls.get_module_info(module_name)
263 cls._check_external_dependencies(terp)
265 if newstate == 'to install':
266 msg = _('Unable to install module "%s" because an external dependency is not met: %s')
267 elif newstate == 'to upgrade':
268 msg = _('Unable to upgrade module "%s" because an external dependency is not met: %s')
270 msg = _('Unable to process module "%s" because an external dependency is not met: %s')
271 raise orm.except_orm(_('Error'), msg % (module_name, e.args[0]))
273 def state_update(self, cr, uid, ids, newstate, states_to_update, context=None, level=100):
275 raise orm.except_orm(_('Error'), _('Recursion error in modules dependencies !'))
277 for module in self.browse(cr, uid, ids):
279 for dep in module.dependencies_id:
280 if dep.state == 'unknown':
281 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,))
282 ids2 = self.search(cr, uid, [('name','=',dep.name)])
283 if dep.state != newstate:
284 mdemo = self.state_update(cr, uid, ids2, newstate, states_to_update, context, level-1,) or mdemo
286 od = self.browse(cr, uid, ids2)[0]
287 mdemo = od.demo or mdemo
289 self.check_external_dependencies(module.name, newstate)
290 if not module.dependencies_id:
292 if module.state in states_to_update:
293 self.write(cr, uid, [module.id], {'state': newstate, 'demo':mdemo})
297 def button_install(self, cr, uid, ids, context=None):
298 self.state_update(cr, uid, ids, 'to install', ['uninstalled'], context)
299 return dict(ACTION_DICT, name=_('Install'))
302 def button_install_cancel(self, cr, uid, ids, context=None):
303 self.write(cr, uid, ids, {'state': 'uninstalled', 'demo':False})
306 def button_uninstall(self, cr, uid, ids, context=None):
307 for module in self.browse(cr, uid, ids):
308 cr.execute('''select m.state,m.name
310 ir_module_module_dependency d
312 ir_module_module m on (d.module_id=m.id)
315 m.state not in ('uninstalled','uninstallable','to remove')''', (module.name,))
318 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)))
319 self.write(cr, uid, ids, {'state': 'to remove'})
320 return dict(ACTION_DICT, name=_('Uninstall'))
322 def button_uninstall_cancel(self, cr, uid, ids, context=None):
323 self.write(cr, uid, ids, {'state': 'installed'})
326 def button_upgrade(self, cr, uid, ids, context=None):
327 depobj = self.pool.get('ir.module.module.dependency')
328 todo = self.browse(cr, uid, ids, context=context)
329 self.update_list(cr, uid)
335 if mod.state not in ('installed','to upgrade'):
336 raise orm.except_orm(_('Error'),
337 _("Can not upgrade module '%s'. It is not installed.") % (mod.name,))
338 self.check_external_dependencies(mod.name, 'to upgrade')
339 iids = depobj.search(cr, uid, [('name', '=', mod.name)], context=context)
340 for dep in depobj.browse(cr, uid, iids, context=context):
341 if dep.module_id.state=='installed' and dep.module_id not in todo:
342 todo.append(dep.module_id)
344 ids = map(lambda x: x.id, todo)
345 self.write(cr, uid, ids, {'state':'to upgrade'}, context=context)
349 for dep in mod.dependencies_id:
350 if dep.state == 'unknown':
351 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,))
352 if dep.state == 'uninstalled':
353 ids2 = self.search(cr, uid, [('name','=',dep.name)])
354 to_install.extend(ids2)
356 self.button_install(cr, uid, to_install, context=context)
357 return dict(ACTION_DICT, name=_('Upgrade'))
359 def button_upgrade_cancel(self, cr, uid, ids, context=None):
360 self.write(cr, uid, ids, {'state': 'installed'})
363 def button_update_translations(self, cr, uid, ids, context=None):
364 self.update_translations(cr, uid, ids)
368 def get_values_from_terp(terp):
370 'description': terp.get('description', ''),
371 'shortdesc': terp.get('name', ''),
372 'author': terp.get('author', 'Unknown'),
373 'maintainer': terp.get('maintainer', False),
374 'contributors': ', '.join(terp.get('contributors', [])) or False,
375 'website': terp.get('website', ''),
376 'license': terp.get('license', 'AGPL-3'),
377 'certificate': terp.get('certificate') or False,
378 'web': terp.get('web') or False,
381 # update the list of available packages
382 def update_list(self, cr, uid, context={}):
383 res = [0, 0] # [update, add]
385 known_mods = self.browse(cr, uid, self.search(cr, uid, []))
386 known_mods_names = dict([(m.name, m) for m in known_mods])
388 # iterate through detected modules and update/create them in db
389 for mod_name in addons.get_modules():
390 mod = known_mods_names.get(mod_name)
391 terp = self.get_module_info(mod_name)
392 values = self.get_values_from_terp(terp)
397 old = getattr(mod, key)
398 updated = isinstance(values[key], basestring) and tools.ustr(values[key]) or values[key]
399 if not old == updated:
400 updated_values[key] = values[key]
401 if terp.get('installable', True) and mod.state == 'uninstallable':
402 updated_values['state'] = 'uninstalled'
403 if parse_version(terp.get('version', '')) > parse_version(mod.latest_version or ''):
406 self.write(cr, uid, mod.id, updated_values)
408 mod_path = addons.get_module_path(mod_name)
411 if not terp or not terp.get('installable', True):
413 id = self.create(cr, uid, dict(name=mod_name, state='uninstalled', **values))
414 mod = self.browse(cr, uid, id)
417 self._update_dependencies(cr, uid, mod, terp.get('depends', []))
418 self._update_category(cr, uid, mod, terp.get('category', 'Uncategorized'))
422 def download(self, cr, uid, ids, download=True, context=None):
424 for mod in self.browse(cr, uid, ids, context=context):
427 match = re.search('-([a-zA-Z0-9\._-]+)(\.zip)', mod.url, re.I)
430 version = match.group(1)
431 if parse_version(mod.installed_version or '0') >= parse_version(version):
436 zipfile = urllib.urlopen(mod.url).read()
437 fname = addons.get_module_path(str(mod.name)+'.zip', downloaded=True)
439 fp = file(fname, 'wb')
443 self.__logger.exception('Error when trying to create module '
445 raise orm.except_orm(_('Error'), _('Can not create the module file:\n %s') % (fname,))
446 terp = self.get_module_info(mod.name)
447 self.write(cr, uid, mod.id, self.get_values_from_terp(terp))
448 cr.execute('DELETE FROM ir_module_module_dependency ' \
449 'WHERE module_id = %s', (mod.id,))
450 self._update_dependencies(cr, uid, mod, terp.get('depends',
452 self._update_category(cr, uid, mod, terp.get('category',
455 zimp = zipimport.zipimporter(fname)
456 zimp.load_module(mod.name)
459 def _update_dependencies(self, cr, uid, mod_browse, depends=None):
462 existing = set(x.name for x in mod_browse.dependencies_id)
463 needed = set(depends)
464 for dep in (needed - existing):
465 cr.execute('INSERT INTO ir_module_module_dependency (module_id, name) values (%s, %s)', (mod_browse.id, dep))
466 for dep in (existing - needed):
467 cr.execute('DELETE FROM ir_module_module_dependency WHERE module_id = %s and name = %s', (mod_browse.id, dep))
469 def _update_category(self, cr, uid, mod_browse, category='Uncategorized'):
470 current_category = mod_browse.category_id
471 current_category_path = []
472 while current_category:
473 current_category_path.insert(0, current_category.name)
474 current_category = current_category.parent_id
476 categs = category.split('/')
477 if categs != current_category_path:
481 cr.execute('SELECT id FROM ir_module_category WHERE name=%s AND parent_id=%s', (categs[0], p_id))
483 cr.execute('SELECT id FROM ir_module_category WHERE name=%s AND parent_id is NULL', (categs[0],))
486 cr.execute('INSERT INTO ir_module_category (name, parent_id) VALUES (%s, %s) RETURNING id', (categs[0], p_id))
487 c_id = cr.fetchone()[0]
492 self.write(cr, uid, [mod_browse.id], {'category_id': p_id})
494 def update_translations(self, cr, uid, ids, filter_lang=None, context=None):
497 logger = logging.getLogger('i18n')
499 pool = pooler.get_pool(cr.dbname)
500 lang_obj = pool.get('res.lang')
501 lang_ids = lang_obj.search(cr, uid, [('translatable', '=', True)])
502 filter_lang = [lang.code for lang in lang_obj.browse(cr, uid, lang_ids)]
503 elif not isinstance(filter_lang, (list, tuple)):
504 filter_lang = [filter_lang]
506 for mod in self.browse(cr, uid, ids):
507 if mod.state != 'installed':
509 modpath = addons.get_module_path(mod.name)
511 # unable to find the module. we skip
513 for lang in filter_lang:
514 iso_lang = tools.get_iso_codes(lang)
515 f = addons.get_module_resource(mod.name, 'i18n', iso_lang + '.po')
516 context2 = context and context.copy() or {}
517 if f and '_' in iso_lang:
518 iso_lang2 = iso_lang.split('_')[0]
519 f2 = addons.get_module_resource(mod.name, 'i18n', iso_lang2 + '.po')
521 logger.info('module %s: loading base translation file %s for language %s', mod.name, iso_lang2, lang)
522 tools.trans_load(cr, f2, lang, verbose=False, context=context)
523 context2['overwrite'] = True
524 # Implementation notice: we must first search for the full name of
525 # the language derivative, like "en_UK", and then the generic,
527 if (not f) and '_' in iso_lang:
528 iso_lang = iso_lang.split('_')[0]
529 f = addons.get_module_resource(mod.name, 'i18n', iso_lang + '.po')
531 logger.info('module %s: loading translation file (%s) for language %s', mod.name, iso_lang, lang)
532 tools.trans_load(cr, f, lang, verbose=False, context=context2)
533 elif iso_lang != 'en':
534 logger.warning('module %s: no translation for language %s', mod.name, iso_lang)
535 tools.trans_update_res_ids(cr)
537 def check(self, cr, uid, ids, context=None):
538 logger = logging.getLogger('init')
539 for mod in self.browse(cr, uid, ids, context=context):
540 if not mod.description:
541 logger.warn('module %s: description is empty !', mod.name)
543 if not mod.certificate or not mod.certificate.isdigit():
544 logger.info('module %s: no quality certificate', mod.name)
546 val = long(mod.certificate[2:]) % 97 == 29
548 logger.critical('module %s: invalid quality certificate: %s', mod.name, mod.certificate)
549 raise osv.except_osv(_('Error'), _('Module %s: Invalid Quality Certificate') % (mod.name,))
551 def list_web(self, cr, uid, context=None):
552 """ list_web(cr, uid, context) -> [(module_name, module_version)]
553 Lists all the currently installed modules with a web component.
555 Returns a list of a tuple of addon names and addon versions.
558 (module['name'], module['installed_version'])
559 for module in self.browse(cr, uid,
562 ('state', 'in', ['installed','to upgrade','to remove'])],
565 def _web_dependencies(self, cr, uid, module, context=None):
566 for dependency in module.dependencies_id:
567 (parent,) = self.browse(cr, uid, self.search(cr, uid,
568 [('name', '=', dependency.name)], context=context),
573 self._web_dependencies(
574 cr, uid, parent, context=context)
576 def _translations_subdir(self, module):
577 """ Returns the path to the subdirectory holding translations for the
578 module files, or None if it can't find one
580 :param module: a module object
581 :type module: browse(ir.module.module)
583 subdir = addons.get_module_resource(module.name, 'po')
584 if subdir: return subdir
585 # old naming convention
586 subdir = addons.get_module_resource(module.name, 'i18n')
587 if subdir: return subdir
590 def _add_translations(self, module, web_data):
591 """ Adds translation data to a zipped web module
593 :param module: a module descriptor
594 :type module: browse(ir.module.module)
595 :param web_data: zipped data of a web module
596 :type web_data: bytes
598 # cStringIO.StringIO is either read or write, not r/w
599 web_zip = StringIO.StringIO(web_data)
600 web_archive = zipfile.ZipFile(web_zip, 'a')
602 # get the contents of the i18n or po folder and move them to the
603 # po/messages subdirectory of the web module.
604 # The POT file will be incorrectly named, but that should not
605 # matter since the web client is not going to use it, only the PO
607 translations_file = cStringIO.StringIO(
608 addons.zip_directory(self._translations_subdir(module), False))
609 translations_archive = zipfile.ZipFile(translations_file)
611 for path in translations_archive.namelist():
612 web_path = os.path.join(
613 'web', 'po', 'messages', os.path.basename(path))
614 web_archive.writestr(
616 translations_archive.read(path))
618 translations_archive.close()
619 translations_file.close()
623 return web_zip.getvalue()
627 def get_web(self, cr, uid, names, context=None):
628 """ get_web(cr, uid, [module_name], context) -> [{name, depends, content}]
630 Returns the web content of all the named addons.
632 The toplevel directory of the zipped content is called 'web',
633 its final naming has to be managed by the client
635 modules = self.browse(cr, uid,
636 self.search(cr, uid, [('name', 'in', names)], context=context),
638 if not modules: return []
639 self.__logger.info('Sending web content of modules %s '
640 'to web client', names)
643 for module in modules:
644 web_data = addons.zip_directory(
645 addons.get_module_resource(module.name, 'web'), False)
646 if self._translations_subdir(module):
647 web_data = self._add_translations(module, web_data)
648 modules_data.append({
650 'version': module.installed_version,
651 'depends': list(self._web_dependencies(
652 cr, uid, module, context=context)),
653 'content': base64.encodestring(web_data)
659 class module_dependency(osv.osv):
660 _name = "ir.module.module.dependency"
661 _description = "Module dependency"
663 def _state(self, cr, uid, ids, name, args, context=None):
665 mod_obj = self.pool.get('ir.module.module')
666 for md in self.browse(cr, uid, ids):
667 ids = mod_obj.search(cr, uid, [('name', '=', md.name)])
669 result[md.id] = mod_obj.read(cr, uid, [ids[0]], ['state'])[0]['state']
671 result[md.id] = 'unknown'
675 'name': fields.char('Name', size=128, select=True),
676 'module_id': fields.many2one('ir.module.module', 'Module', select=True, ondelete='cascade'),
677 'state': fields.function(_state, method=True, type='selection', selection=[
678 ('uninstallable','Uninstallable'),
679 ('uninstalled','Not Installed'),
680 ('installed','Installed'),
681 ('to upgrade','To be upgraded'),
682 ('to remove','To be removed'),
683 ('to install','To be installed'),
684 ('unknown', 'Unknown'),
685 ], string='State', readonly=True, select=True),