1bdf2a7aebb63ccba95f6ca443d32b24802ce0d0
[odoo/odoo.git] / bin / addons / base / module / module.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
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>).
7 #
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.
12 #
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.
17 #
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/>.
20 #
21 ##############################################################################
22 import imp
23 import logging
24 import os
25 import re
26 import urllib
27 import zipimport
28
29 import addons
30 import pooler
31 import release
32 import tools
33
34 from tools.parse_version import parse_version
35 from tools.translate import _
36
37 from osv import fields, osv, orm
38
39 class module_category(osv.osv):
40     _name = "ir.module.category"
41     _description = "Module Category"
42
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)}
51                     )
52         result = dict(cr.fetchall())
53         for id in ids:
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()],
56                              result.get(id, 0))
57         return result
58
59     _columns = {
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')
64     }
65     _order = 'name'
66 module_category()
67
68 class module(osv.osv):
69     _name = "ir.module.module"
70     _description = "Module"
71     __logger = logging.getLogger('base.' + _name)
72
73     @classmethod
74     def get_module_info(cls, name):
75         info = {}
76         try:
77             info = addons.load_information_from_description_file(name)
78             if 'version' in info:
79                 info['version'] = release.major_version + '.' + info['version']
80         except Exception:
81             cls.__logger.debug('Error when trying to fetch informations for '
82                                 'module %s', name, exc_info=True)
83         return info
84
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', '')
89         return res
90
91     def _get_views(self, cr, uid, ids, field_name=None, arg=None, context=None):
92         res = {}
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)
98         mnames = {}
99         for m in mlist:
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
104             res[m.id] = {
105                 'menus_by_module':[],
106                 'reports_by_module':[],
107                 'views_by_module': []
108             }
109
110         if not mnames:
111             return res
112
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
117             try:
118                 key = data_id.model
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)
128             except KeyError, e:
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)
132                 pass
133             except Exception, e:
134                 self.__logger.warning('Unknown error while browsing %s[%s]',
135                             data_id.model, data_id.res_id, exc_info=True)
136                 pass
137         for key, value in res.iteritems():
138             for k, v in res[key].iteritems() :
139                 res[key][k] = "\n".join(sorted(v))
140         return res
141
142     _columns = {
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),
151
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),
160
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),
187     }
188
189     _defaults = {
190         'state': 'uninstalled',
191         'demo': False,
192         'license': 'AGPL-3',
193         'web': False,
194     }
195     _order = 'name'
196
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 !')
201
202     _sql_constraints = [
203         ('name_uniq', 'UNIQUE (name)',_name_uniq_msg ),
204         ('certificate_uniq', 'UNIQUE (certificate)',_certificate_uniq_msg )
205     ]
206
207     def unlink(self, cr, uid, ids, context=None):
208         if not ids:
209             return True
210         if isinstance(ids, (int, long)):
211             ids = [ids]
212         mod_names = []
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)])
220
221         if ids_meta:
222             self.pool.get('ir.model.data').unlink(cr, uid, ids_meta, context)
223
224         return super(module, self).unlink(cr, uid, ids, context=context)
225
226     @staticmethod
227     def _check_external_dependencies(terp):
228         depends = terp.get('external_dependencies')
229         if not depends:
230             return
231         for pydep in depends.get('python', []):
232             parts = pydep.split('.')
233             parts.reverse()
234             path = None
235             while parts:
236                 part = parts.pop()
237                 try:
238                     f, path, descr = imp.find_module(part, path and [path] or None)
239                 except ImportError:
240                     raise ImportError('No module named %s' % (pydep,))
241
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,))
245
246     @classmethod
247     def check_external_dependencies(cls, module_name, newstate='to install'):
248         terp = cls.get_module_info(module_name)
249         try:
250             cls._check_external_dependencies(terp)
251         except Exception, e:
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')
256             else:
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]))
259
260     def state_update(self, cr, uid, ids, newstate, states_to_update, context=None, level=100):
261         if level<1:
262             raise orm.except_orm(_('Error'), _('Recursion error in modules dependencies !'))
263         demo = False
264         for module in self.browse(cr, uid, ids):
265             mdemo = False
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
272                 else:
273                     od = self.browse(cr, uid, ids2)[0]
274                     mdemo = od.demo or mdemo
275
276             self.check_external_dependencies(module.name, newstate)
277             if not module.dependencies_id:
278                 mdemo = module.demo
279             if module.state in states_to_update:
280                 self.write(cr, uid, [module.id], {'state': newstate, 'demo':mdemo})
281             demo = demo or mdemo
282         return demo
283
284     def button_install(self, cr, uid, ids, context=None):
285         return self.state_update(cr, uid, ids, 'to install', ['uninstalled'], context)
286
287     def button_install_cancel(self, cr, uid, ids, context=None):
288         self.write(cr, uid, ids, {'state': 'uninstalled', 'demo':False})
289         return True
290
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
294                 from
295                     ir_module_module_dependency d
296                 join
297                     ir_module_module m on (d.module_id=m.id)
298                 where
299                     d.name=%s and
300                     m.state not in ('uninstalled','uninstallable','to remove')''', (module.name,))
301             res = cr.fetchall()
302             if res:
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'})
305         return True
306
307     def button_uninstall_cancel(self, cr, uid, ids, context=None):
308         self.write(cr, uid, ids, {'state': 'installed'})
309         return True
310
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)
315
316         i = 0
317         while i<len(todo):
318             mod = todo[i]
319             i += 1
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)
328
329         ids = map(lambda x: x.id, todo)
330         self.write(cr, uid, ids, {'state':'to upgrade'}, context=context)
331
332         to_install = []
333         for mod in todo:
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)
340
341         self.button_install(cr, uid, to_install, context=context)
342         return True
343
344     def button_upgrade_cancel(self, cr, uid, ids, context=None):
345         self.write(cr, uid, ids, {'state': 'installed'})
346         return True
347     def button_update_translations(self, cr, uid, ids, context=None):
348         self.update_translations(cr, uid, ids)
349         return True
350
351     @staticmethod
352     def get_values_from_terp(terp):
353         return {
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,
363         }
364
365     # update the list of available packages
366     def update_list(self, cr, uid, context={}):
367         res = [0, 0] # [update, add]
368
369         known_mods = self.browse(cr, uid, self.search(cr, uid, []))
370         known_mods_names = dict([(m.name, m) for m in known_mods])
371
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)
377
378             if mod:
379                 updated_values = {}
380                 for key in values:
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 ''):
388                     res[0] += 1
389                 if updated_values:
390                     self.write(cr, uid, mod.id, updated_values)
391             else:
392                 mod_path = addons.get_module_path(mod_name)
393                 if not mod_path:
394                     continue
395                 if not terp or not terp.get('installable', True):
396                     continue
397                 id = self.create(cr, uid, dict(name=mod_name, state='uninstalled', **values))
398                 mod = self.browse(cr, uid, id)
399                 res[1] += 1
400
401             self._update_dependencies(cr, uid, mod, terp.get('depends', []))
402             self._update_category(cr, uid, mod, terp.get('category', 'Uncategorized'))
403
404         return res
405
406     def download(self, cr, uid, ids, download=True, context=None):
407         res = []
408         for mod in self.browse(cr, uid, ids, context=context):
409             if not mod.url:
410                 continue
411             match = re.search('-([a-zA-Z0-9\._-]+)(\.zip)', mod.url, re.I)
412             version = '0'
413             if match:
414                 version = match.group(1)
415             if parse_version(mod.installed_version or '0') >= parse_version(version):
416                 continue
417             res.append(mod.url)
418             if not download:
419                 continue
420             zipfile = urllib.urlopen(mod.url).read()
421             fname = addons.get_module_path(str(mod.name)+'.zip', downloaded=True)
422             try:
423                 fp = file(fname, 'wb')
424                 fp.write(zipfile)
425                 fp.close()
426             except Exception:
427                 self.__logger.exception('Error when trying to create module '
428                                         'file %s', fname)
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',
435                 []))
436             self._update_category(cr, uid, mod, terp.get('category',
437                 'Uncategorized'))
438             # Import module
439             zimp = zipimport.zipimporter(fname)
440             zimp.load_module(mod.name)
441         return res
442
443     def _update_dependencies(self, cr, uid, mod_browse, depends=None):
444         if depends is None:
445             depends = []
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))
452
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
459
460         categs = category.split('/')
461         if categs != current_category_path:
462             p_id = None
463             while categs:
464                 if p_id is not None:
465                     cr.execute('SELECT id FROM ir_module_category WHERE name=%s AND parent_id=%s', (categs[0], p_id))
466                 else:
467                     cr.execute('SELECT id FROM ir_module_category WHERE name=%s AND parent_id is NULL', (categs[0],))
468                 c_id = cr.fetchone()
469                 if not c_id:
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]
472                 else:
473                     c_id = c_id[0]
474                 p_id = c_id
475                 categs = categs[1:]
476             self.write(cr, uid, [mod_browse.id], {'category_id': p_id})
477
478     def update_translations(self, cr, uid, ids, filter_lang=None, context=None):
479         logger = logging.getLogger('i18n')
480         if not filter_lang:
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]
487
488         for mod in self.browse(cr, uid, ids):
489             if mod.state != 'installed':
490                 continue
491             modpath = addons.get_module_path(mod.name)
492             if not modpath:
493                 # unable to find the module. we skip
494                 continue
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,
500                 # like "en".
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]
504                 if f:
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)
509
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)
515
516             if not mod.certificate or not mod.certificate.isdigit():
517                 logger.info('module %s: no quality certificate', mod.name)
518             else:
519                 val = long(mod.certificate[2:]) % 97 == 29
520                 if not val:
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,))
523
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.
527
528         Returns a list of addon names.
529         """
530         return [
531             module['name']
532             for module in self.browse(cr, uid,
533                 self.search(cr, uid,
534                     [('web', '=', True),
535                      ('state', 'in', ['installed','to upgrade','to remove'])],
536                     context=context),
537                 context=context)]
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),
542                                  context=context)
543             if parent.web:
544                 yield parent.name
545             else:
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}]
550
551         Returns the web content of all the named addons.
552
553         The toplevel directory of the zipped content is called 'web',
554         its final naming has to be managed by the client
555         """
556         modules = self.browse(cr, uid,
557             self.search(cr, uid, [('name', 'in', names)], context=context),
558                               context=context)
559         if not modules: return []
560         self.__logger.info('Sending web content of modules %s '
561                            'to web client', names)
562         return [
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
569         ]
570
571 module()
572
573 class module_dependency(osv.osv):
574     _name = "ir.module.module.dependency"
575     _description = "Module dependency"
576
577     def _state(self, cr, uid, ids, name, args, context=None):
578         result = {}
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)])
582             if ids:
583                 result[md.id] = mod_obj.read(cr, uid, [ids[0]], ['state'])[0]['state']
584             else:
585                 result[md.id] = 'unknown'
586         return result
587
588     _columns = {
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),
600     }
601 module_dependency()