1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
27 from osv import fields, osv, orm
37 from tools.parse_version import parse_version
38 from tools.translate import _
40 class module_category(osv.osv):
41 _name = "ir.module.category"
42 _description = "Module Category"
44 def _module_nbr(self,cr,uid, ids, prop, unknow_none,context):
45 cr.execute('select category_id,count(*) from ir_module_module where category_id in ('+','.join(map(str, ids))+') or category_id in (select id from ir_module_category where parent_id in ('+','.join(map(str, ids))+')) group by category_id')
46 result = dict(cr.fetchall())
48 cr.execute('select id from ir_module_category where parent_id=%s', (id,))
49 childs = [c for c, in cr.fetchall()]
50 result[id] = reduce(lambda x,y:x+y, [result.get(c, 0) for c in childs], result.get(id, 0))
54 'name': fields.char("Name", size=128, required=True),
55 'parent_id': fields.many2one('ir.module.category', 'Parent Category', select=True),
56 'child_ids': fields.one2many('ir.module.category', 'parent_id', 'Child Categories'),
57 'module_nr': fields.function(_module_nbr, method=True, string='Number of Modules', type='integer')
62 class module(osv.osv):
63 _name = "ir.module.module"
64 _description = "Module"
66 def get_module_info(self, name):
69 info = addons.load_information_from_description_file(name)
71 info['version'] = release.major_version + '.' + info['version']
76 def _get_latest_version(self, cr, uid, ids, field_name=None, arg=None, context={}):
77 res = dict.fromkeys(ids, '')
78 for m in self.browse(cr, uid, ids):
79 res[m.id] = self.get_module_info(m.name).get('version', '')
82 def _get_views(self, cr, uid, ids, field_name=None, arg=None, context={}):
84 model_data_obj = self.pool.get('ir.model.data')
85 view_obj = self.pool.get('ir.ui.view')
86 report_obj = self.pool.get('ir.actions.report.xml')
87 menu_obj = self.pool.get('ir.ui.menu')
88 mlist = self.browse(cr, uid, ids, context=context)
94 'reports_by_module':'',
97 view_id = model_data_obj.search(cr,uid,[('module','in', mnames.keys()),
98 ('model','in',('ir.ui.view','ir.actions.report.xml','ir.ui.menu'))])
99 for data_id in model_data_obj.browse(cr,uid,view_id,context):
100 # We use try except, because views or menus may not exist
102 key = data_id['model']
103 if key=='ir.ui.view':
104 v = view_obj.browse(cr,uid,data_id.res_id)
105 aa = v.inherit_id and '* INHERIT ' or ''
106 res[mnames[data_id.module]]['views_by_module'] += aa + v.name + ' ('+v.type+')\n'
107 elif key=='ir.actions.report.xml':
108 res[mnames[data_id.module]]['reports_by_module'] += report_obj.browse(cr,uid,data_id.res_id).name + '\n'
109 elif key=='ir.ui.menu':
110 res[mnames[data_id.module]]['menus_by_module'] += menu_obj.browse(cr,uid,data_id.res_id).complete_name + '\n'
116 'name': fields.char("Name", size=128, readonly=True, required=True),
117 'category_id': fields.many2one('ir.module.category', 'Category', readonly=True),
118 'shortdesc': fields.char('Short Description', size=256, readonly=True, translate=True),
119 'description': fields.text("Description", readonly=True, translate=True),
120 'author': fields.char("Author", size=128, readonly=True),
121 'website': fields.char("Website", size=256, readonly=True),
123 # attention: Incorrect field names !!
124 # installed_version refer the latest version (the one on disk)
125 # latest_version refer the installed version (the one in database)
126 # published_version refer the version available on the repository
127 'installed_version': fields.function(_get_latest_version, method=True,
128 string='Latest version', type='char'),
129 'latest_version': fields.char('Installed version', size=64, readonly=True),
130 'published_version': fields.char('Published Version', size=64, readonly=True),
132 'url': fields.char('URL', size=128, readonly=True),
133 'dependencies_id': fields.one2many('ir.module.module.dependency',
134 'module_id', 'Dependencies', readonly=True),
135 'state': fields.selection([
136 ('uninstallable','Not Installable'),
137 ('uninstalled','Not Installed'),
138 ('installed','Installed'),
139 ('to upgrade','To be upgraded'),
140 ('to remove','To be removed'),
141 ('to install','To be installed')
142 ], string='State', readonly=True),
143 'demo': fields.boolean('Demo data'),
144 'license': fields.selection([
146 ('GPL-2 or any later version', 'GPL-2 or later version'),
148 ('GPL-3 or any later version', 'GPL-3 or later version'),
149 ('Other proprietary', 'Other proprietary')
150 ], string='License', readonly=True),
151 'menus_by_module': fields.function(_get_views, method=True, string='Menus', type='text', multi="meta", store=True),
152 'reports_by_module': fields.function(_get_views, method=True, string='Reports', type='text', multi="meta", store=True),
153 'views_by_module': fields.function(_get_views, method=True, string='Views', type='text', multi="meta", store=True),
154 'certificate' : fields.char('Quality Certificate', size=64, readonly=True),
158 'state': lambda *a: 'uninstalled',
159 'demo': lambda *a: False,
160 'license': lambda *a: 'GPL-2',
165 ('name_uniq', 'unique (name)', 'The name of the module must be unique !'),
166 ('certificate_uniq', 'unique (certificate)', 'The certificate ID of the module must be unique !')
169 def unlink(self, cr, uid, ids, context=None):
172 if isinstance(ids, (int, long)):
175 for mod in self.read(cr, uid, ids, ['state','name'], context):
176 if mod['state'] in ('installed', 'to upgrade', 'to remove', 'to install'):
177 raise orm.except_orm(_('Error'),
178 _('You try to remove a module that is installed or will be installed'))
179 mod_names.append(mod['name'])
180 #Removing the entry from ir_model_data
181 ids_meta = self.pool.get('ir.model.data').search(cr, uid, [('name', '=', 'module_meta_information'), ('module', 'in', mod_names)])
184 self.pool.get('ir.model.data').unlink(cr, uid, ids_meta, context)
186 return super(module, self).unlink(cr, uid, ids, context=context)
188 def state_update(self, cr, uid, ids, newstate, states_to_update, context={}, level=100):
190 raise orm.except_orm(_('Error'), _('Recursion error in modules dependencies !'))
192 for module in self.browse(cr, uid, ids):
194 for dep in module.dependencies_id:
195 if dep.state == 'unknown':
196 raise orm.except_orm(_('Error'), _("You try to install the module '%s' that depends on the module:'%s'.\nBut this module is not available in your system.") % (module.name, dep.name,))
197 ids2 = self.search(cr, uid, [('name','=',dep.name)])
198 if dep.state != newstate:
199 mdemo = self.state_update(cr, uid, ids2, newstate, states_to_update, context, level-1,) or mdemo
201 od = self.browse(cr, uid, ids2)[0]
202 mdemo = od.demo or mdemo
203 if not module.dependencies_id:
205 if module.state in states_to_update:
206 self.write(cr, uid, [module.id], {'state': newstate, 'demo':mdemo})
210 def button_install(self, cr, uid, ids, context={}):
211 return self.state_update(cr, uid, ids, 'to install', ['uninstalled'], context)
213 def button_install_cancel(self, cr, uid, ids, context={}):
214 self.write(cr, uid, ids, {'state': 'uninstalled', 'demo':False})
217 def button_uninstall(self, cr, uid, ids, context={}):
218 for module in self.browse(cr, uid, ids):
219 cr.execute('''select m.state,m.name
221 ir_module_module_dependency d
223 ir_module_module m on (d.module_id=m.id)
226 m.state not in ('uninstalled','uninstallable','to remove')''', (module.name,))
229 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)))
230 self.write(cr, uid, ids, {'state': 'to remove'})
233 def button_uninstall_cancel(self, cr, uid, ids, context={}):
234 self.write(cr, uid, ids, {'state': 'installed'})
237 def button_upgrade(self, cr, uid, ids, context=None):
238 depobj = self.pool.get('ir.module.module.dependency')
239 todo = self.browse(cr, uid, ids, context=context)
240 self.update_list(cr, uid)
246 if mod.state not in ('installed','to upgrade'):
247 raise orm.except_orm(_('Error'),
248 _("Can not upgrade module '%s'. It is not installed.") % (mod.name,))
249 iids = depobj.search(cr, uid, [('name', '=', mod.name)], context=context)
250 for dep in depobj.browse(cr, uid, iids, context=context):
251 if dep.module_id.state=='installed' and dep.module_id not in todo:
252 todo.append(dep.module_id)
254 ids = map(lambda x: x.id, todo)
255 self.write(cr, uid, ids, {'state':'to upgrade'}, context=context)
259 for dep in mod.dependencies_id:
260 if dep.state == 'unknown':
261 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,))
262 if dep.state == 'uninstalled':
263 ids2 = self.search(cr, uid, [('name','=',dep.name)])
264 to_install.extend(ids2)
266 self.button_install(cr, uid, to_install, context=context)
269 def button_upgrade_cancel(self, cr, uid, ids, context={}):
270 self.write(cr, uid, ids, {'state': 'installed'})
272 def button_update_translations(self, cr, uid, ids, context=None):
273 self.update_translations(cr, uid, ids)
276 # update the list of available packages
277 def update_list(self, cr, uid, context={}):
278 res = [0, 0] # [update, add]
280 # iterate through installed modules and mark them as being so
281 for mod_name in addons.get_modules():
282 ids = self.search(cr, uid, [('name','=',mod_name)])
285 mod = self.browse(cr, uid, id)
286 terp = self.get_module_info(mod_name)
287 if terp.get('installable', True) and mod.state == 'uninstallable':
288 self.write(cr, uid, id, {'state': 'uninstalled'})
289 if parse_version(terp.get('version', '')) > parse_version(mod.latest_version or ''):
290 self.write(cr, uid, id, { 'url': ''})
292 self.write(cr, uid, id, {
293 'description': terp.get('description', ''),
294 'shortdesc': terp.get('name', ''),
295 'author': terp.get('author', 'Unknown'),
296 'website': terp.get('website', ''),
297 'license': terp.get('license', 'GPL-2'),
298 'certificate': terp.get('certificate') or None,
300 cr.execute('DELETE FROM ir_module_module_dependency WHERE module_id = %s', (id,))
301 self._update_dependencies(cr, uid, ids[0], terp.get('depends', []))
302 self._update_category(cr, uid, ids[0], terp.get('category', 'Uncategorized'))
304 mod_path = addons.get_module_path(mod_name)
306 terp = self.get_module_info(mod_name)
307 if not terp or not terp.get('installable', True):
310 id = self.create(cr, uid, {
312 'state': 'uninstalled',
313 'description': terp.get('description', ''),
314 'shortdesc': terp.get('name', ''),
315 'author': terp.get('author', 'Unknown'),
316 'website': terp.get('website', ''),
317 'license': terp.get('license', 'GPL-2'),
318 'certificate': terp.get('certificate') or None,
321 self._update_dependencies(cr, uid, id, terp.get('depends', []))
322 self._update_category(cr, uid, id, terp.get('category', 'Uncategorized'))
326 def download(self, cr, uid, ids, download=True, context=None):
328 for mod in self.browse(cr, uid, ids, context=context):
331 match = re.search('-([a-zA-Z0-9\._-]+)(\.zip)', mod.url, re.I)
334 version = match.group(1)
335 if parse_version(mod.installed_version or '0') >= parse_version(version):
340 zipfile = urllib.urlopen(mod.url).read()
341 fname = addons.get_module_path(str(mod.name)+'.zip', downloaded=True)
343 fp = file(fname, 'wb')
347 raise orm.except_orm(_('Error'), _('Can not create the module file:\n %s') % (fname,))
348 terp = self.get_module_info(mod.name)
349 self.write(cr, uid, mod.id, {
350 'description': terp.get('description', ''),
351 'shortdesc': terp.get('name', ''),
352 'author': terp.get('author', 'Unknown'),
353 'website': terp.get('website', ''),
354 'license': terp.get('license', 'GPL-2'),
355 'certificate': terp.get('certificate') or None,
357 cr.execute('DELETE FROM ir_module_module_dependency ' \
358 'WHERE module_id = %s', (mod.id,))
359 self._update_dependencies(cr, uid, mod.id, terp.get('depends',
361 self._update_category(cr, uid, mod.id, terp.get('category',
364 zimp = zipimport.zipimporter(fname)
365 zimp.load_module(mod.name)
368 def _update_dependencies(self, cr, uid, id, depends=[]):
370 cr.execute('INSERT INTO ir_module_module_dependency (module_id, name) values (%s, %s)', (id, d))
372 def _update_category(self, cr, uid, id, category='Uncategorized'):
373 categs = category.split('/')
377 cr.execute('select id from ir_module_category where name=%s and parent_id=%s', (categs[0], p_id))
379 cr.execute('select id from ir_module_category where name=%s and parent_id is NULL', (categs[0],))
382 cr.execute('select nextval(\'ir_module_category_id_seq\')')
383 c_id = cr.fetchone()[0]
384 cr.execute('insert into ir_module_category (id, name, parent_id) values (%s, %s, %s)', (c_id, categs[0], p_id))
389 self.write(cr, uid, [id], {'category_id': p_id})
391 def update_translations(self, cr, uid, ids, filter_lang=None):
392 logger = netsvc.Logger()
394 pool = pooler.get_pool(cr.dbname)
395 lang_obj = pool.get('res.lang')
396 lang_ids = lang_obj.search(cr, uid, [('translatable', '=', True)])
397 filter_lang = [lang.code for lang in lang_obj.browse(cr, uid, lang_ids)]
398 elif not isinstance(filter_lang, (list, tuple)):
399 filter_lang = [filter_lang]
401 for mod in self.browse(cr, uid, ids):
402 if mod.state != 'installed':
404 modpath = addons.get_module_path(mod.name)
406 # unable to find the module. we skip
408 for lang in filter_lang:
410 raise osv.except_osv(_('Error'), _('You Can Not Load Translation For language Due To Invalid Language/Country Code'))
411 iso_lang = tools.get_iso_codes(lang)
412 f = os.path.join(modpath, 'i18n', iso_lang + '.po')
413 if not os.path.exists(f) and iso_lang.find('_') != -1:
414 f = os.path.join(modpath, 'i18n', iso_lang.split('_')[0] + '.po')
415 iso_lang = iso_lang.split('_')[0]
416 if os.path.exists(f):
417 logger.notifyChannel("i18n", netsvc.LOG_INFO, 'module %s: loading translation file for language %s' % (mod.name, iso_lang))
418 tools.trans_load(cr.dbname, f, lang, verbose=False)
420 def check(self, cr, uid, ids, context=None):
421 logger = netsvc.Logger()
422 for mod in self.browse(cr, uid, ids, context=context):
423 if not mod.description:
424 logger.notifyChannel("init", netsvc.LOG_WARNING, 'module %s: description is empty !' % (mod.name,))
426 if not mod.certificate or not mod.certificate.isdigit():
427 logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: no quality certificate' % (mod.name,))
429 val = long(mod.certificate[2:]) % 97 == 29
431 logger.notifyChannel('init', netsvc.LOG_CRITICAL, 'module %s: invalid quality certificate: %s' % (mod.name, mod.certificate))
432 raise osv.except_osv(_('Error'), _('Module %s: Invalid Quality Certificate') % (mod.name,))
435 def create(self, cr, uid, data, context={}):
436 id = super(module, self).create(cr, uid, data, context)
438 self.pool.get('ir.model.data').create(cr, uid, {
439 'name': 'module_meta_information',
440 'model': 'ir.module.module',
442 'module': data['name'],
448 class module_dependency(osv.osv):
449 _name = "ir.module.module.dependency"
450 _description = "Module dependency"
452 def _state(self, cr, uid, ids, name, args, context={}):
454 mod_obj = self.pool.get('ir.module.module')
455 for md in self.browse(cr, uid, ids):
456 ids = mod_obj.search(cr, uid, [('name', '=', md.name)])
458 result[md.id] = mod_obj.read(cr, uid, [ids[0]], ['state'])[0]['state']
460 result[md.id] = 'unknown'
464 'name': fields.char('Name', size=128),
465 'module_id': fields.many2one('ir.module.module', 'Module', select=True, ondelete='cascade'),
466 'state': fields.function(_state, method=True, type='selection', selection=[
467 ('uninstallable','Uninstallable'),
468 ('uninstalled','Not Installed'),
469 ('installed','Installed'),
470 ('to upgrade','To be upgraded'),
471 ('to remove','To be removed'),
472 ('to install','To be installed'),
473 ('unknown', 'Unknown'),
474 ], string='State', readonly=True),
477 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: