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),
200 'complexity': fields.selection([('easy','Easy'), ('normal','Normal'), ('expert','Expert')],
201 string='Complexity', readonly=True,
202 help='Level of difficulty of module. Easy: intuitive and easy to use for everyone. Normal: easy to use for business experts. Expert: requires technical skills.'),
206 'state': 'uninstalled',
213 def _name_uniq_msg(self, cr, uid, ids, context=None):
214 return _('The name of the module must be unique !')
215 def _certificate_uniq_msg(self, cr, uid, ids, context=None):
216 return _('The certificate ID of the module must be unique !')
219 ('name_uniq', 'UNIQUE (name)',_name_uniq_msg ),
220 ('certificate_uniq', 'UNIQUE (certificate)',_certificate_uniq_msg )
223 def unlink(self, cr, uid, ids, context=None):
226 if isinstance(ids, (int, long)):
229 for mod in self.read(cr, uid, ids, ['state','name'], context):
230 if mod['state'] in ('installed', 'to upgrade', 'to remove', 'to install'):
231 raise orm.except_orm(_('Error'),
232 _('You try to remove a module that is installed or will be installed'))
233 mod_names.append(mod['name'])
234 #Removing the entry from ir_model_data
235 ids_meta = self.pool.get('ir.model.data').search(cr, uid, [('name', '=', 'module_meta_information'), ('module', 'in', mod_names)])
238 self.pool.get('ir.model.data').unlink(cr, uid, ids_meta, context)
240 return super(module, self).unlink(cr, uid, ids, context=context)
243 def _check_external_dependencies(terp):
244 depends = terp.get('external_dependencies')
247 for pydep in depends.get('python', []):
248 parts = pydep.split('.')
254 f, path, descr = imp.find_module(part, path and [path] or None)
256 raise ImportError('No module named %s' % (pydep,))
258 for binary in depends.get('bin', []):
259 if tools.find_in_path(binary) is None:
260 raise Exception('Unable to find %r in path' % (binary,))
263 def check_external_dependencies(cls, module_name, newstate='to install'):
264 terp = cls.get_module_info(module_name)
266 cls._check_external_dependencies(terp)
268 if newstate == 'to install':
269 msg = _('Unable to install module "%s" because an external dependency is not met: %s')
270 elif newstate == 'to upgrade':
271 msg = _('Unable to upgrade module "%s" because an external dependency is not met: %s')
273 msg = _('Unable to process module "%s" because an external dependency is not met: %s')
274 raise orm.except_orm(_('Error'), msg % (module_name, e.args[0]))
276 def state_update(self, cr, uid, ids, newstate, states_to_update, context=None, level=100):
278 raise orm.except_orm(_('Error'), _('Recursion error in modules dependencies !'))
280 for module in self.browse(cr, uid, ids):
282 for dep in module.dependencies_id:
283 if dep.state == 'unknown':
284 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,))
285 ids2 = self.search(cr, uid, [('name','=',dep.name)])
286 if dep.state != newstate:
287 mdemo = self.state_update(cr, uid, ids2, newstate, states_to_update, context, level-1,) or mdemo
289 od = self.browse(cr, uid, ids2)[0]
290 mdemo = od.demo or mdemo
292 self.check_external_dependencies(module.name, newstate)
293 if not module.dependencies_id:
295 if module.state in states_to_update:
296 self.write(cr, uid, [module.id], {'state': newstate, 'demo':mdemo})
300 def button_install(self, cr, uid, ids, context=None):
301 self.state_update(cr, uid, ids, 'to install', ['uninstalled'], context)
302 return dict(ACTION_DICT, name=_('Install'))
305 def button_install_cancel(self, cr, uid, ids, context=None):
306 self.write(cr, uid, ids, {'state': 'uninstalled', 'demo':False})
309 def button_uninstall(self, cr, uid, ids, context=None):
310 for module in self.browse(cr, uid, ids):
311 cr.execute('''select m.state,m.name
313 ir_module_module_dependency d
315 ir_module_module m on (d.module_id=m.id)
318 m.state not in ('uninstalled','uninstallable','to remove')''', (module.name,))
321 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)))
322 self.write(cr, uid, ids, {'state': 'to remove'})
323 return dict(ACTION_DICT, name=_('Uninstall'))
325 def button_uninstall_cancel(self, cr, uid, ids, context=None):
326 self.write(cr, uid, ids, {'state': 'installed'})
329 def button_upgrade(self, cr, uid, ids, context=None):
330 depobj = self.pool.get('ir.module.module.dependency')
331 todo = self.browse(cr, uid, ids, context=context)
332 self.update_list(cr, uid)
338 if mod.state not in ('installed','to upgrade'):
339 raise orm.except_orm(_('Error'),
340 _("Can not upgrade module '%s'. It is not installed.") % (mod.name,))
341 self.check_external_dependencies(mod.name, 'to upgrade')
342 iids = depobj.search(cr, uid, [('name', '=', mod.name)], context=context)
343 for dep in depobj.browse(cr, uid, iids, context=context):
344 if dep.module_id.state=='installed' and dep.module_id not in todo:
345 todo.append(dep.module_id)
347 ids = map(lambda x: x.id, todo)
348 self.write(cr, uid, ids, {'state':'to upgrade'}, context=context)
352 for dep in mod.dependencies_id:
353 if dep.state == 'unknown':
354 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,))
355 if dep.state == 'uninstalled':
356 ids2 = self.search(cr, uid, [('name','=',dep.name)])
357 to_install.extend(ids2)
359 self.button_install(cr, uid, to_install, context=context)
360 return dict(ACTION_DICT, name=_('Upgrade'))
362 def button_upgrade_cancel(self, cr, uid, ids, context=None):
363 self.write(cr, uid, ids, {'state': 'installed'})
366 def button_update_translations(self, cr, uid, ids, context=None):
367 self.update_translations(cr, uid, ids)
371 def get_values_from_terp(terp):
373 'description': terp.get('description', ''),
374 'shortdesc': terp.get('name', ''),
375 'author': terp.get('author', 'Unknown'),
376 'maintainer': terp.get('maintainer', False),
377 'contributors': ', '.join(terp.get('contributors', [])) or False,
378 'website': terp.get('website', ''),
379 'license': terp.get('license', 'AGPL-3'),
380 'certificate': terp.get('certificate') or False,
381 'web': terp.get('web') or False,
382 'complexity': terp.get('complexity', ''),
385 # update the list of available packages
386 def update_list(self, cr, uid, context={}):
387 res = [0, 0] # [update, add]
389 known_mods = self.browse(cr, uid, self.search(cr, uid, []))
390 known_mods_names = dict([(m.name, m) for m in known_mods])
392 # iterate through detected modules and update/create them in db
393 for mod_name in addons.get_modules():
394 mod = known_mods_names.get(mod_name)
395 terp = self.get_module_info(mod_name)
396 values = self.get_values_from_terp(terp)
401 old = getattr(mod, key)
402 updated = isinstance(values[key], basestring) and tools.ustr(values[key]) or values[key]
403 if not old == updated:
404 updated_values[key] = values[key]
405 if terp.get('installable', True) and mod.state == 'uninstallable':
406 updated_values['state'] = 'uninstalled'
407 if parse_version(terp.get('version', '')) > parse_version(mod.latest_version or ''):
410 self.write(cr, uid, mod.id, updated_values)
412 mod_path = addons.get_module_path(mod_name)
415 if not terp or not terp.get('installable', True):
417 id = self.create(cr, uid, dict(name=mod_name, state='uninstalled', **values))
418 mod = self.browse(cr, uid, id)
421 self._update_dependencies(cr, uid, mod, terp.get('depends', []))
422 self._update_category(cr, uid, mod, terp.get('category', 'Uncategorized'))
426 def download(self, cr, uid, ids, download=True, context=None):
428 for mod in self.browse(cr, uid, ids, context=context):
431 match = re.search('-([a-zA-Z0-9\._-]+)(\.zip)', mod.url, re.I)
434 version = match.group(1)
435 if parse_version(mod.installed_version or '0') >= parse_version(version):
440 zipfile = urllib.urlopen(mod.url).read()
441 fname = addons.get_module_path(str(mod.name)+'.zip', downloaded=True)
443 fp = file(fname, 'wb')
447 self.__logger.exception('Error when trying to create module '
449 raise orm.except_orm(_('Error'), _('Can not create the module file:\n %s') % (fname,))
450 terp = self.get_module_info(mod.name)
451 self.write(cr, uid, mod.id, self.get_values_from_terp(terp))
452 cr.execute('DELETE FROM ir_module_module_dependency ' \
453 'WHERE module_id = %s', (mod.id,))
454 self._update_dependencies(cr, uid, mod, terp.get('depends',
456 self._update_category(cr, uid, mod, terp.get('category',
459 zimp = zipimport.zipimporter(fname)
460 zimp.load_module(mod.name)
463 def _update_dependencies(self, cr, uid, mod_browse, depends=None):
466 existing = set(x.name for x in mod_browse.dependencies_id)
467 needed = set(depends)
468 for dep in (needed - existing):
469 cr.execute('INSERT INTO ir_module_module_dependency (module_id, name) values (%s, %s)', (mod_browse.id, dep))
470 for dep in (existing - needed):
471 cr.execute('DELETE FROM ir_module_module_dependency WHERE module_id = %s and name = %s', (mod_browse.id, dep))
473 def _update_category(self, cr, uid, mod_browse, category='Uncategorized'):
474 current_category = mod_browse.category_id
475 current_category_path = []
476 while current_category:
477 current_category_path.insert(0, current_category.name)
478 current_category = current_category.parent_id
480 categs = category.split('/')
481 if categs != current_category_path:
485 cr.execute('SELECT id FROM ir_module_category WHERE name=%s AND parent_id=%s', (categs[0], p_id))
487 cr.execute('SELECT id FROM ir_module_category WHERE name=%s AND parent_id is NULL', (categs[0],))
490 cr.execute('INSERT INTO ir_module_category (name, parent_id) VALUES (%s, %s) RETURNING id', (categs[0], p_id))
491 c_id = cr.fetchone()[0]
496 self.write(cr, uid, [mod_browse.id], {'category_id': p_id})
498 def update_translations(self, cr, uid, ids, filter_lang=None, context=None):
501 logger = logging.getLogger('i18n')
503 pool = pooler.get_pool(cr.dbname)
504 lang_obj = pool.get('res.lang')
505 lang_ids = lang_obj.search(cr, uid, [('translatable', '=', True)])
506 filter_lang = [lang.code for lang in lang_obj.browse(cr, uid, lang_ids)]
507 elif not isinstance(filter_lang, (list, tuple)):
508 filter_lang = [filter_lang]
510 for mod in self.browse(cr, uid, ids):
511 if mod.state != 'installed':
513 modpath = addons.get_module_path(mod.name)
515 # unable to find the module. we skip
517 for lang in filter_lang:
518 iso_lang = tools.get_iso_codes(lang)
519 f = addons.get_module_resource(mod.name, 'i18n', iso_lang + '.po')
520 context2 = context and context.copy() or {}
521 if f and '_' in iso_lang:
522 iso_lang2 = iso_lang.split('_')[0]
523 f2 = addons.get_module_resource(mod.name, 'i18n', iso_lang2 + '.po')
525 logger.info('module %s: loading base translation file %s for language %s', mod.name, iso_lang2, lang)
526 tools.trans_load(cr, f2, lang, verbose=False, context=context)
527 context2['overwrite'] = True
528 # Implementation notice: we must first search for the full name of
529 # the language derivative, like "en_UK", and then the generic,
531 if (not f) and '_' in iso_lang:
532 iso_lang = iso_lang.split('_')[0]
533 f = addons.get_module_resource(mod.name, 'i18n', iso_lang + '.po')
535 logger.info('module %s: loading translation file (%s) for language %s', mod.name, iso_lang, lang)
536 tools.trans_load(cr, f, lang, verbose=False, context=context2)
537 elif iso_lang != 'en':
538 logger.warning('module %s: no translation for language %s', mod.name, iso_lang)
539 tools.trans_update_res_ids(cr)
541 def check(self, cr, uid, ids, context=None):
542 logger = logging.getLogger('init')
543 for mod in self.browse(cr, uid, ids, context=context):
544 if not mod.description:
545 logger.warn('module %s: description is empty !', mod.name)
547 if not mod.certificate or not mod.certificate.isdigit():
548 logger.info('module %s: no quality certificate', mod.name)
550 val = long(mod.certificate[2:]) % 97 == 29
552 logger.critical('module %s: invalid quality certificate: %s', mod.name, mod.certificate)
553 raise osv.except_osv(_('Error'), _('Module %s: Invalid Quality Certificate') % (mod.name,))
555 def list_web(self, cr, uid, context=None):
556 """ list_web(cr, uid, context) -> [(module_name, module_version)]
557 Lists all the currently installed modules with a web component.
559 Returns a list of a tuple of addon names and addon versions.
562 (module['name'], module['installed_version'])
563 for module in self.browse(cr, uid,
566 ('state', 'in', ['installed','to upgrade','to remove'])],
569 def _web_dependencies(self, cr, uid, module, context=None):
570 for dependency in module.dependencies_id:
571 (parent,) = self.browse(cr, uid, self.search(cr, uid,
572 [('name', '=', dependency.name)], context=context),
577 self._web_dependencies(
578 cr, uid, parent, context=context)
580 def _translations_subdir(self, module):
581 """ Returns the path to the subdirectory holding translations for the
582 module files, or None if it can't find one
584 :param module: a module object
585 :type module: browse(ir.module.module)
587 subdir = addons.get_module_resource(module.name, 'po')
588 if subdir: return subdir
589 # old naming convention
590 subdir = addons.get_module_resource(module.name, 'i18n')
591 if subdir: return subdir
594 def _add_translations(self, module, web_data):
595 """ Adds translation data to a zipped web module
597 :param module: a module descriptor
598 :type module: browse(ir.module.module)
599 :param web_data: zipped data of a web module
600 :type web_data: bytes
602 # cStringIO.StringIO is either read or write, not r/w
603 web_zip = StringIO.StringIO(web_data)
604 web_archive = zipfile.ZipFile(web_zip, 'a')
606 # get the contents of the i18n or po folder and move them to the
607 # po/messages subdirectory of the web module.
608 # The POT file will be incorrectly named, but that should not
609 # matter since the web client is not going to use it, only the PO
611 translations_file = cStringIO.StringIO(
612 addons.zip_directory(self._translations_subdir(module), False))
613 translations_archive = zipfile.ZipFile(translations_file)
615 for path in translations_archive.namelist():
616 web_path = os.path.join(
617 'web', 'po', 'messages', os.path.basename(path))
618 web_archive.writestr(
620 translations_archive.read(path))
622 translations_archive.close()
623 translations_file.close()
627 return web_zip.getvalue()
631 def get_web(self, cr, uid, names, context=None):
632 """ get_web(cr, uid, [module_name], context) -> [{name, depends, content}]
634 Returns the web content of all the named addons.
636 The toplevel directory of the zipped content is called 'web',
637 its final naming has to be managed by the client
639 modules = self.browse(cr, uid,
640 self.search(cr, uid, [('name', 'in', names)], context=context),
642 if not modules: return []
643 self.__logger.info('Sending web content of modules %s '
644 'to web client', names)
647 for module in modules:
648 web_data = addons.zip_directory(
649 addons.get_module_resource(module.name, 'web'), False)
650 if self._translations_subdir(module):
651 web_data = self._add_translations(module, web_data)
652 modules_data.append({
654 'version': module.installed_version,
655 'depends': list(self._web_dependencies(
656 cr, uid, module, context=context)),
657 'content': base64.encodestring(web_data)
663 class module_dependency(osv.osv):
664 _name = "ir.module.module.dependency"
665 _description = "Module dependency"
667 def _state(self, cr, uid, ids, name, args, context=None):
669 mod_obj = self.pool.get('ir.module.module')
670 for md in self.browse(cr, uid, ids):
671 ids = mod_obj.search(cr, uid, [('name', '=', md.name)])
673 result[md.id] = mod_obj.read(cr, uid, [ids[0]], ['state'])[0]['state']
675 result[md.id] = 'unknown'
679 'name': fields.char('Name', size=128, select=True),
680 'module_id': fields.many2one('ir.module.module', 'Module', select=True, ondelete='cascade'),
681 'state': fields.function(_state, method=True, type='selection', selection=[
682 ('uninstallable','Uninstallable'),
683 ('uninstalled','Not Installed'),
684 ('installed','Installed'),
685 ('to upgrade','To be upgraded'),
686 ('to remove','To be removed'),
687 ('to install','To be installed'),
688 ('unknown', 'Unknown'),
689 ], string='State', readonly=True, select=True),