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 ##############################################################################
38 from tools.parse_version import parse_version
39 from tools.translate import _
41 from osv import fields, osv, orm
43 class module_category(osv.osv):
44 _name = "ir.module.category"
45 _description = "Module Category"
47 def _module_nbr(self,cr,uid, ids, prop, unknow_none, context):
48 cr.execute('SELECT category_id, COUNT(*) \
49 FROM ir_module_module \
50 WHERE category_id IN %(ids)s \
51 OR category_id IN (SELECT id \
52 FROM ir_module_category \
53 WHERE parent_id IN %(ids)s) \
54 GROUP BY category_id', {'ids': tuple(ids)}
56 result = dict(cr.fetchall())
58 cr.execute('select id from ir_module_category where parent_id=%s', (id,))
59 result[id] = sum([result.get(c, 0) for (c,) in cr.fetchall()],
64 'name': fields.char("Name", size=128, required=True, select=True),
65 'parent_id': fields.many2one('ir.module.category', 'Parent Category', select=True),
66 'child_ids': fields.one2many('ir.module.category', 'parent_id', 'Child Categories'),
67 'module_nr': fields.function(_module_nbr, method=True, string='Number of Modules', type='integer')
72 class module(osv.osv):
73 _name = "ir.module.module"
74 _description = "Module"
75 __logger = logging.getLogger('base.' + _name)
78 def get_module_info(cls, name):
81 info = addons.load_information_from_description_file(name)
83 info['version'] = release.major_version + '.' + info['version']
85 cls.__logger.debug('Error when trying to fetch informations for '
86 'module %s', name, exc_info=True)
89 def _get_latest_version(self, cr, uid, ids, field_name=None, arg=None, context=None):
90 res = dict.fromkeys(ids, '')
91 for m in self.browse(cr, uid, ids):
92 res[m.id] = self.get_module_info(m.name).get('version', '')
95 def _get_views(self, cr, uid, ids, field_name=None, arg=None, context=None):
97 model_data_obj = self.pool.get('ir.model.data')
98 view_obj = self.pool.get('ir.ui.view')
99 report_obj = self.pool.get('ir.actions.report.xml')
100 menu_obj = self.pool.get('ir.ui.menu')
101 mlist = self.browse(cr, uid, ids, context=context)
104 # skip uninstalled modules below,
105 # no data to find anyway
106 if m.state in ('installed', 'to upgrade', 'to remove'):
107 mnames[m.name] = m.id
109 'menus_by_module':[],
110 'reports_by_module':[],
111 'views_by_module': []
117 view_id = model_data_obj.search(cr,uid,[('module','in', mnames.keys()),
118 ('model','in',('ir.ui.view','ir.actions.report.xml','ir.ui.menu'))])
119 for data_id in model_data_obj.browse(cr,uid,view_id,context):
120 # We use try except, because views or menus may not exist
123 res_mod_dic = res[mnames[data_id.module]]
124 if key=='ir.ui.view':
125 v = view_obj.browse(cr,uid,data_id.res_id)
126 aa = v.inherit_id and '* INHERIT ' or ''
127 res_mod_dic['views_by_module'].append(aa + v.name + '('+v.type+')')
128 elif key=='ir.actions.report.xml':
129 res_mod_dic['reports_by_module'].append(report_obj.browse(cr,uid,data_id.res_id).name)
130 elif key=='ir.ui.menu':
131 res_mod_dic['menus_by_module'].append(menu_obj.browse(cr,uid,data_id.res_id).complete_name)
133 self.__logger.warning(
134 'Data not found for reference %s[%s:%s.%s]', data_id.model,
135 data_id.res_id, data_id.model, data_id.name, exc_info=True)
138 self.__logger.warning('Unknown error while browsing %s[%s]',
139 data_id.model, data_id.res_id, exc_info=True)
141 for key, value in res.iteritems():
142 for k, v in res[key].iteritems() :
143 res[key][k] = "\n".join(sorted(v))
147 'name': fields.char("Name", size=128, readonly=True, required=True, select=True),
148 'category_id': fields.many2one('ir.module.category', 'Category', readonly=True, select=True),
149 'shortdesc': fields.char('Short Description', size=256, readonly=True, translate=True),
150 'description': fields.text("Description", readonly=True, translate=True),
151 'author': fields.char("Author", size=128, readonly=True),
152 'maintainer': fields.char('Maintainer', size=128, readonly=True),
153 'contributors': fields.text('Contributors', readonly=True),
154 'website': fields.char("Website", size=256, readonly=True),
156 # attention: Incorrect field names !!
157 # installed_version refer the latest version (the one on disk)
158 # latest_version refer the installed version (the one in database)
159 # published_version refer the version available on the repository
160 'installed_version': fields.function(_get_latest_version, method=True,
161 string='Latest version', type='char'),
162 'latest_version': fields.char('Installed version', size=64, readonly=True),
163 'published_version': fields.char('Published Version', size=64, readonly=True),
165 'url': fields.char('URL', size=128, readonly=True),
166 'dependencies_id': fields.one2many('ir.module.module.dependency',
167 'module_id', 'Dependencies', readonly=True),
168 'state': fields.selection([
169 ('uninstallable','Not Installable'),
170 ('uninstalled','Not Installed'),
171 ('installed','Installed'),
172 ('to upgrade','To be upgraded'),
173 ('to remove','To be removed'),
174 ('to install','To be installed')
175 ], string='State', readonly=True, select=True),
176 'demo': fields.boolean('Demo data'),
177 'license': fields.selection([
178 ('GPL-2', 'GPL Version 2'),
179 ('GPL-2 or any later version', 'GPL-2 or later version'),
180 ('GPL-3', 'GPL Version 3'),
181 ('GPL-3 or any later version', 'GPL-3 or later version'),
182 ('AGPL-3', 'Affero GPL-3'),
183 ('Other OSI approved licence', 'Other OSI Approved Licence'),
184 ('Other proprietary', 'Other Proprietary')
185 ], string='License', readonly=True),
186 'menus_by_module': fields.function(_get_views, method=True, string='Menus', type='text', multi="meta", store=True),
187 'reports_by_module': fields.function(_get_views, method=True, string='Reports', type='text', multi="meta", store=True),
188 'views_by_module': fields.function(_get_views, method=True, string='Views', type='text', multi="meta", store=True),
189 'certificate' : fields.char('Quality Certificate', size=64, readonly=True),
190 'web': fields.boolean('Has a web component', readonly=True),
194 'state': 'uninstalled',
201 def _name_uniq_msg(self, cr, uid, ids, context=None):
202 return _('The name of the module must be unique !')
203 def _certificate_uniq_msg(self, cr, uid, ids, context=None):
204 return _('The certificate ID of the module must be unique !')
207 ('name_uniq', 'UNIQUE (name)',_name_uniq_msg ),
208 ('certificate_uniq', 'UNIQUE (certificate)',_certificate_uniq_msg )
211 def unlink(self, cr, uid, ids, context=None):
214 if isinstance(ids, (int, long)):
217 for mod in self.read(cr, uid, ids, ['state','name'], context):
218 if mod['state'] in ('installed', 'to upgrade', 'to remove', 'to install'):
219 raise orm.except_orm(_('Error'),
220 _('You try to remove a module that is installed or will be installed'))
221 mod_names.append(mod['name'])
222 #Removing the entry from ir_model_data
223 ids_meta = self.pool.get('ir.model.data').search(cr, uid, [('name', '=', 'module_meta_information'), ('module', 'in', mod_names)])
226 self.pool.get('ir.model.data').unlink(cr, uid, ids_meta, context)
228 return super(module, self).unlink(cr, uid, ids, context=context)
231 def _check_external_dependencies(terp):
232 depends = terp.get('external_dependencies')
235 for pydep in depends.get('python', []):
236 parts = pydep.split('.')
242 f, path, descr = imp.find_module(part, path and [path] or None)
244 raise ImportError('No module named %s' % (pydep,))
246 for binary in depends.get('bin', []):
247 if tools.find_in_path(binary) is None:
248 raise Exception('Unable to find %r in path' % (binary,))
251 def check_external_dependencies(cls, module_name, newstate='to install'):
252 terp = cls.get_module_info(module_name)
254 cls._check_external_dependencies(terp)
256 if newstate == 'to install':
257 msg = _('Unable to install module "%s" because an external dependency is not met: %s')
258 elif newstate == 'to upgrade':
259 msg = _('Unable to upgrade module "%s" because an external dependency is not met: %s')
261 msg = _('Unable to process module "%s" because an external dependency is not met: %s')
262 raise orm.except_orm(_('Error'), msg % (module_name, e.args[0]))
264 def state_update(self, cr, uid, ids, newstate, states_to_update, context=None, level=100, res=None):
268 raise orm.except_orm(_('Error'), _('Recursion error in modules dependencies !'))
270 for module in self.browse(cr, uid, ids):
272 for dep in module.dependencies_id:
273 if dep.name not in res:
275 if dep.state == 'unknown':
276 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,))
277 ids2 = self.search(cr, uid, [('name','=',dep.name)])
278 if dep.state != newstate:
279 mdemo = self.state_update(cr, uid, ids2, newstate, states_to_update, context, level-1, res=res) or mdemo
281 od = self.browse(cr, uid, ids2)[0]
282 mdemo = od.demo or mdemo
284 self.check_external_dependencies(module.name, newstate)
285 if not module.dependencies_id:
287 if module.state in states_to_update:
288 self.write(cr, uid, [module.id], {'state': newstate, 'demo':mdemo})
292 def button_install(self, cr, uid, ids, context=None):
293 return self.state_update(cr, uid, ids, 'to install', ['uninstalled'], context)
295 def button_install_cancel(self, cr, uid, ids, context=None):
296 self.write(cr, uid, ids, {'state': 'uninstalled', 'demo':False})
299 def button_uninstall(self, cr, uid, ids, context=None):
300 for module in self.browse(cr, uid, ids):
301 cr.execute('''select m.state,m.name
303 ir_module_module_dependency d
305 ir_module_module m on (d.module_id=m.id)
308 m.state not in ('uninstalled','uninstallable','to remove')''', (module.name,))
311 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)))
312 self.write(cr, uid, ids, {'state': 'to remove'})
315 def button_uninstall_cancel(self, cr, uid, ids, context=None):
316 self.write(cr, uid, ids, {'state': 'installed'})
319 def button_upgrade(self, cr, uid, ids, context=None):
320 depobj = self.pool.get('ir.module.module.dependency')
321 todo = self.browse(cr, uid, ids, context=context)
322 self.update_list(cr, uid)
328 if mod.state not in ('installed','to upgrade'):
329 raise orm.except_orm(_('Error'),
330 _("Can not upgrade module '%s'. It is not installed.") % (mod.name,))
331 self.check_external_dependencies(mod.name, 'to upgrade')
332 iids = depobj.search(cr, uid, [('name', '=', mod.name)], context=context)
333 for dep in depobj.browse(cr, uid, iids, context=context):
334 if dep.module_id.state=='installed' and dep.module_id not in todo:
335 todo.append(dep.module_id)
337 ids = map(lambda x: x.id, todo)
338 self.write(cr, uid, ids, {'state':'to upgrade'}, context=context)
342 for dep in mod.dependencies_id:
343 if dep.state == 'unknown':
344 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,))
345 if dep.state == 'uninstalled':
346 ids2 = self.search(cr, uid, [('name','=',dep.name)])
347 to_install.extend(ids2)
349 self.button_install(cr, uid, to_install, context=context)
352 def button_upgrade_cancel(self, cr, uid, ids, context=None):
353 self.write(cr, uid, ids, {'state': 'installed'})
355 def button_update_translations(self, cr, uid, ids, context=None):
356 self.update_translations(cr, uid, ids)
360 def get_values_from_terp(terp):
362 'description': terp.get('description', ''),
363 'shortdesc': terp.get('name', ''),
364 'author': terp.get('author', 'Unknown'),
365 'maintainer': terp.get('maintainer', False),
366 'contributors': ', '.join(terp.get('contributors', [])) or False,
367 'website': terp.get('website', ''),
368 'license': terp.get('license', 'AGPL-3'),
369 'certificate': terp.get('certificate') or False,
370 'web': terp.get('web') or False,
373 # update the list of available packages
374 def update_list(self, cr, uid, context={}):
375 res = [0, 0] # [update, add]
377 known_mods = self.browse(cr, uid, self.search(cr, uid, []))
378 known_mods_names = dict([(m.name, m) for m in known_mods])
380 # iterate through detected modules and update/create them in db
381 for mod_name in addons.get_modules():
382 mod = known_mods_names.get(mod_name)
383 terp = self.get_module_info(mod_name)
384 values = self.get_values_from_terp(terp)
389 old = getattr(mod, key)
390 updated = isinstance(values[key], basestring) and tools.ustr(values[key]) or values[key]
391 if not old == updated:
392 updated_values[key] = values[key]
393 if terp.get('installable', True) and mod.state == 'uninstallable':
394 updated_values['state'] = 'uninstalled'
395 if parse_version(terp.get('version', '')) > parse_version(mod.latest_version or ''):
398 self.write(cr, uid, mod.id, updated_values)
400 mod_path = addons.get_module_path(mod_name)
403 if not terp or not terp.get('installable', True):
405 id = self.create(cr, uid, dict(name=mod_name, state='uninstalled', **values))
406 mod = self.browse(cr, uid, id)
409 self._update_dependencies(cr, uid, mod, terp.get('depends', []))
410 self._update_category(cr, uid, mod, terp.get('category', 'Uncategorized'))
414 def download(self, cr, uid, ids, download=True, context=None):
416 for mod in self.browse(cr, uid, ids, context=context):
419 match = re.search('-([a-zA-Z0-9\._-]+)(\.zip)', mod.url, re.I)
422 version = match.group(1)
423 if parse_version(mod.installed_version or '0') >= parse_version(version):
428 zipfile = urllib.urlopen(mod.url).read()
429 fname = addons.get_module_path(str(mod.name)+'.zip', downloaded=True)
431 fp = file(fname, 'wb')
435 self.__logger.exception('Error when trying to create module '
437 raise orm.except_orm(_('Error'), _('Can not create the module file:\n %s') % (fname,))
438 terp = self.get_module_info(mod.name)
439 self.write(cr, uid, mod.id, self.get_values_from_terp(terp))
440 cr.execute('DELETE FROM ir_module_module_dependency ' \
441 'WHERE module_id = %s', (mod.id,))
442 self._update_dependencies(cr, uid, mod, terp.get('depends',
444 self._update_category(cr, uid, mod, terp.get('category',
447 zimp = zipimport.zipimporter(fname)
448 zimp.load_module(mod.name)
451 def _update_dependencies(self, cr, uid, mod_browse, depends=None):
454 existing = set(x.name for x in mod_browse.dependencies_id)
455 needed = set(depends)
456 for dep in (needed - existing):
457 cr.execute('INSERT INTO ir_module_module_dependency (module_id, name) values (%s, %s)', (mod_browse.id, dep))
458 for dep in (existing - needed):
459 cr.execute('DELETE FROM ir_module_module_dependency WHERE module_id = %s and name = %s', (mod_browse.id, dep))
461 def _update_category(self, cr, uid, mod_browse, category='Uncategorized'):
462 current_category = mod_browse.category_id
463 current_category_path = []
464 while current_category:
465 current_category_path.insert(0, current_category.name)
466 current_category = current_category.parent_id
468 categs = category.split('/')
469 if categs != current_category_path:
473 cr.execute('SELECT id FROM ir_module_category WHERE name=%s AND parent_id=%s', (categs[0], p_id))
475 cr.execute('SELECT id FROM ir_module_category WHERE name=%s AND parent_id is NULL', (categs[0],))
478 cr.execute('INSERT INTO ir_module_category (name, parent_id) VALUES (%s, %s) RETURNING id', (categs[0], p_id))
479 c_id = cr.fetchone()[0]
484 self.write(cr, uid, [mod_browse.id], {'category_id': p_id})
486 def update_translations(self, cr, uid, ids, filter_lang=None, context={}):
487 logger = logging.getLogger('i18n')
489 pool = pooler.get_pool(cr.dbname)
490 lang_obj = pool.get('res.lang')
491 lang_ids = lang_obj.search(cr, uid, [('translatable', '=', True)])
492 filter_lang = [lang.code for lang in lang_obj.browse(cr, uid, lang_ids)]
493 elif not isinstance(filter_lang, (list, tuple)):
494 filter_lang = [filter_lang]
496 for mod in self.browse(cr, uid, ids):
497 if mod.state != 'installed':
499 modpath = addons.get_module_path(mod.name)
501 # unable to find the module. we skip
503 for lang in filter_lang:
504 iso_lang = tools.get_iso_codes(lang)
505 f = addons.get_module_resource(mod.name, 'i18n', iso_lang + '.po')
506 context2 = context and context.copy() or {}
507 if f and '_' in iso_lang:
508 iso_lang2 = iso_lang.split('_')[0]
509 f2 = addons.get_module_resource(mod.name, 'i18n', iso_lang2 + '.po')
511 logger.info('module %s: loading base translation file %s for language %s', mod.name, iso_lang2, lang)
512 tools.trans_load(cr, f2, lang, verbose=False, context=context)
513 context2['overwrite'] = True
514 # Implementation notice: we must first search for the full name of
515 # the language derivative, like "en_UK", and then the generic,
517 if (not f) and '_' in iso_lang:
518 iso_lang = iso_lang.split('_')[0]
519 f = addons.get_module_resource(mod.name, 'i18n', iso_lang + '.po')
521 logger.info('module %s: loading translation file (%s) for language %s', mod.name, iso_lang, lang)
522 tools.trans_load(cr, f, lang, verbose=False, context=context2)
523 elif iso_lang != 'en':
524 logger.warning('module %s: no translation for language %s', mod.name, iso_lang)
525 tools.trans_update_res_ids(cr)
527 def check(self, cr, uid, ids, context=None):
528 logger = logging.getLogger('init')
529 for mod in self.browse(cr, uid, ids, context=context):
530 if not mod.description:
531 logger.warn('module %s: description is empty !', mod.name)
533 if not mod.certificate or not mod.certificate.isdigit():
534 logger.info('module %s: no quality certificate', mod.name)
536 val = long(mod.certificate[2:]) % 97 == 29
538 logger.critical('module %s: invalid quality certificate: %s', mod.name, mod.certificate)
539 raise osv.except_osv(_('Error'), _('Module %s: Invalid Quality Certificate') % (mod.name,))
541 def list_web(self, cr, uid, context=None):
542 """ list_web(cr, uid, context) -> [(module_name, module_version)]
543 Lists all the currently installed modules with a web component.
545 Returns a list of a tuple of addon names and addon versions.
548 (module['name'], module['installed_version'])
549 for module in self.browse(cr, uid,
552 ('state', 'in', ['installed','to upgrade','to remove'])],
555 def _web_dependencies(self, cr, uid, module, context=None):
556 for dependency in module.dependencies_id:
557 (parent,) = self.browse(cr, uid, self.search(cr, uid,
558 [('name', '=', dependency.name)], context=context),
563 self._web_dependencies(
564 cr, uid, parent, context=context)
566 def _translations_subdir(self, module):
567 """ Returns the path to the subdirectory holding translations for the
568 module files, or None if it can't find one
570 :param module: a module object
571 :type module: browse(ir.module.module)
573 subdir = addons.get_module_resource(module.name, 'po')
574 if subdir: return subdir
575 # old naming convention
576 subdir = addons.get_module_resource(module.name, 'i18n')
577 if subdir: return subdir
580 def _add_translations(self, module, web_data):
581 """ Adds translation data to a zipped web module
583 :param module: a module descriptor
584 :type module: browse(ir.module.module)
585 :param web_data: zipped data of a web module
586 :type web_data: bytes
588 # cStringIO.StringIO is either read or write, not r/w
589 web_zip = StringIO.StringIO(web_data)
590 web_archive = zipfile.ZipFile(web_zip, 'a')
592 # get the contents of the i18n or po folder and move them to the
593 # po/messages subdirectory of the web module.
594 # The POT file will be incorrectly named, but that should not
595 # matter since the web client is not going to use it, only the PO
597 translations_file = cStringIO.StringIO(
598 addons.zip_directory(self._translations_subdir(module), False))
599 translations_archive = zipfile.ZipFile(translations_file)
601 for path in translations_archive.namelist():
602 web_path = os.path.join(
603 'web', 'po', 'messages', os.path.basename(path))
604 web_archive.writestr(
606 translations_archive.read(path))
608 translations_archive.close()
609 translations_file.close()
613 return web_zip.getvalue()
617 def get_web(self, cr, uid, names, context=None):
618 """ get_web(cr, uid, [module_name], context) -> [{name, depends, content}]
620 Returns the web content of all the named addons.
622 The toplevel directory of the zipped content is called 'web',
623 its final naming has to be managed by the client
625 modules = self.browse(cr, uid,
626 self.search(cr, uid, [('name', 'in', names)], context=context),
628 if not modules: return []
629 self.__logger.info('Sending web content of modules %s '
630 'to web client', names)
633 for module in modules:
634 web_data = addons.zip_directory(
635 addons.get_module_resource(module.name, 'web'), False)
636 if self._translations_subdir(module):
637 web_data = self._add_translations(module, web_data)
638 modules_data.append({
640 'version': module.installed_version,
641 'depends': list(self._web_dependencies(
642 cr, uid, module, context=context)),
643 'content': base64.encodestring(web_data)
649 class module_dependency(osv.osv):
650 _name = "ir.module.module.dependency"
651 _description = "Module dependency"
653 def _state(self, cr, uid, ids, name, args, context=None):
655 mod_obj = self.pool.get('ir.module.module')
656 for md in self.browse(cr, uid, ids):
657 ids = mod_obj.search(cr, uid, [('name', '=', md.name)])
659 result[md.id] = mod_obj.read(cr, uid, [ids[0]], ['state'])[0]['state']
661 result[md.id] = 'unknown'
665 'name': fields.char('Name', size=128, select=True),
666 'module_id': fields.many2one('ir.module.module', 'Module', select=True, ondelete='cascade'),
667 'state': fields.function(_state, method=True, type='selection', selection=[
668 ('uninstallable','Uninstallable'),
669 ('uninstalled','Not Installed'),
670 ('installed','Installed'),
671 ('to upgrade','To be upgraded'),
672 ('to remove','To be removed'),
673 ('to install','To be installed'),
674 ('unknown', 'Unknown'),
675 ], string='State', readonly=True, select=True),