command line server management with -i and -u options
[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 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 import tarfile
23 import re
24 import urllib
25 import os
26 import tools
27 from osv import fields, osv, orm
28 import zipfile
29 import release
30 import zipimport
31
32 import wizard
33 import addons
34 import pooler
35 import netsvc
36
37 from tools.parse_version import parse_version
38 from tools.translate import _
39
40 class module_category(osv.osv):
41     _name = "ir.module.category"
42     _description = "Module Category"
43
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())
47         for id in ids:
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))
51         return result
52
53     _columns = {
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')
58     }
59     _order = 'name'
60 module_category()
61
62 class module(osv.osv):
63     _name = "ir.module.module"
64     _description = "Module"
65
66     def get_module_info(self, name):
67         info = {}
68         try:
69             info = addons.load_information_from_description_file(name)
70             if 'version' in info:
71                 info['version'] = release.major_version + '.' + info['version']
72         except:
73             pass
74         return info
75
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', '')
80         return res
81
82     def _get_views(self, cr, uid, ids, field_name=None, arg=None, context={}):
83         res = {}
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)
89         mnames = {}
90         for m in mlist:
91             mnames[m.name] = m.id
92             res[m.id] = {
93                 'menus_by_module':'',
94                 'reports_by_module':'',
95                 'views_by_module': ''
96             }
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
101             try:
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'
111             except KeyError, e:
112                 pass
113         return res
114
115     _columns = {
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),
122
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),
131
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([
145                 ('GPL-2', 'GPL-2'),
146                 ('GPL-2 or any later version', 'GPL-2 or later version'),
147                 ('GPL-3', 'GPL-3'),
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),
155     }
156
157     _defaults = {
158         'state': lambda *a: 'uninstalled',
159         'demo': lambda *a: False,
160         'license': lambda *a: 'GPL-2',
161     }
162     _order = 'name'
163
164     _sql_constraints = [
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 !')
167     ]
168
169     def unlink(self, cr, uid, ids, context=None):
170         if not ids:
171             return True
172         if isinstance(ids, (int, long)):
173             ids = [ids]
174         mod_names = []
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)])
182
183         if ids_meta:
184             self.pool.get('ir.model.data').unlink(cr, uid, ids_meta, context)
185
186         return super(module, self).unlink(cr, uid, ids, context=context)
187
188     def state_update(self, cr, uid, ids, newstate, states_to_update, context={}, level=100):
189         if level<1:
190             raise orm.except_orm(_('Error'), _('Recursion error in modules dependencies !'))
191         demo = False
192         for module in self.browse(cr, uid, ids):
193             mdemo = False
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
200                 else:
201                     od = self.browse(cr, uid, ids2)[0]
202                     mdemo = od.demo or mdemo
203             if not module.dependencies_id:
204                 mdemo = module.demo
205             if module.state in states_to_update:
206                 self.write(cr, uid, [module.id], {'state': newstate, 'demo':mdemo})
207             demo = demo or mdemo
208         return demo
209
210     def button_install(self, cr, uid, ids, context={}):
211         return self.state_update(cr, uid, ids, 'to install', ['uninstalled'], context)
212
213     def button_install_cancel(self, cr, uid, ids, context={}):
214         self.write(cr, uid, ids, {'state': 'uninstalled', 'demo':False})
215         return True
216
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
220                 from
221                     ir_module_module_dependency d
222                 join
223                     ir_module_module m on (d.module_id=m.id)
224                 where
225                     d.name=%s and
226                     m.state not in ('uninstalled','uninstallable','to remove')''', (module.name,))
227             res = cr.fetchall()
228             if res:
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'})
231         return True
232
233     def button_uninstall_cancel(self, cr, uid, ids, context={}):
234         self.write(cr, uid, ids, {'state': 'installed'})
235         return True
236
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)
241
242         i = 0
243         while i<len(todo):
244             mod = todo[i]
245             i += 1
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)
253
254         ids = map(lambda x: x.id, todo)
255         self.write(cr, uid, ids, {'state':'to upgrade'}, context=context)
256
257         to_install = []
258         for mod in todo:
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)
265
266         self.button_install(cr, uid, to_install, context=context)
267         return True
268
269     def button_upgrade_cancel(self, cr, uid, ids, context={}):
270         self.write(cr, uid, ids, {'state': 'installed'})
271         return True
272     def button_update_translations(self, cr, uid, ids, context=None):
273         self.update_translations(cr, uid, ids)
274         return True
275
276     # update the list of available packages
277     def update_list(self, cr, uid, context={}):
278         res = [0, 0] # [update, add]
279
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)])
283             if ids:
284                 id = ids[0]
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': ''})
291                     res[0] += 1
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,
299                     })
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'))
303                 continue
304             mod_path = addons.get_module_path(mod_name)
305             if mod_path:
306                 terp = self.get_module_info(mod_name)
307                 if not terp or not terp.get('installable', True):
308                     continue
309
310                 id = self.create(cr, uid, {
311                     'name': mod_name,
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,
319                 })
320                 res[1] += 1
321                 self._update_dependencies(cr, uid, id, terp.get('depends', []))
322                 self._update_category(cr, uid, id, terp.get('category', 'Uncategorized'))
323
324         return res
325
326     def download(self, cr, uid, ids, download=True, context=None):
327         res = []
328         for mod in self.browse(cr, uid, ids, context=context):
329             if not mod.url:
330                 continue
331             match = re.search('-([a-zA-Z0-9\._-]+)(\.zip)', mod.url, re.I)
332             version = '0'
333             if match:
334                 version = match.group(1)
335             if parse_version(mod.installed_version or '0') >= parse_version(version):
336                 continue
337             res.append(mod.url)
338             if not download:
339                 continue
340             zipfile = urllib.urlopen(mod.url).read()
341             fname = addons.get_module_path(str(mod.name)+'.zip', downloaded=True)
342             try:
343                 fp = file(fname, 'wb')
344                 fp.write(zipfile)
345                 fp.close()
346             except Exception, e:
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,
356                 })
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',
360                 []))
361             self._update_category(cr, uid, mod.id, terp.get('category',
362                 'Uncategorized'))
363             # Import module
364             zimp = zipimport.zipimporter(fname)
365             zimp.load_module(mod.name)
366         return res
367
368     def _update_dependencies(self, cr, uid, id, depends=[]):
369         for d in depends:
370             cr.execute('INSERT INTO ir_module_module_dependency (module_id, name) values (%s, %s)', (id, d))
371
372     def _update_category(self, cr, uid, id, category='Uncategorized'):
373         categs = category.split('/')
374         p_id = None
375         while categs:
376             if p_id is not None:
377                 cr.execute('select id from ir_module_category where name=%s and parent_id=%s', (categs[0], p_id))
378             else:
379                 cr.execute('select id from ir_module_category where name=%s and parent_id is NULL', (categs[0],))
380             c_id = cr.fetchone()
381             if not c_id:
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))
385             else:
386                 c_id = c_id[0]
387             p_id = c_id
388             categs = categs[1:]
389         self.write(cr, uid, [id], {'category_id': p_id})
390
391     def update_translations(self, cr, uid, ids, filter_lang=None):
392         logger = netsvc.Logger()
393         if not filter_lang:
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]
400
401         for mod in self.browse(cr, uid, ids):
402             if mod.state != 'installed':
403                 continue
404             modpath = addons.get_module_path(mod.name)
405             if not modpath:
406                 # unable to find the module. we skip
407                 continue
408             for lang in filter_lang:
409                 if len(lang) > 5:
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)
419
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,))
425
426             if not mod.certificate or not mod.certificate.isdigit():
427                 logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: no quality certificate' % (mod.name,))
428             else:
429                 val = long(mod.certificate[2:]) % 97 == 29
430                 if not val:
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,))
433
434
435     def create(self, cr, uid, data, context={}):
436         id = super(module, self).create(cr, uid, data, context)
437         if data.get('name'):
438             self.pool.get('ir.model.data').create(cr, uid, {
439                 'name': 'module_meta_information',
440                 'model': 'ir.module.module',
441                 'res_id': id,
442                 'module': data['name'],
443                 'noupdate': True,
444             })
445         return id
446 module()
447
448 class module_dependency(osv.osv):
449     _name = "ir.module.module.dependency"
450     _description = "Module dependency"
451
452     def _state(self, cr, uid, ids, name, args, context={}):
453         result = {}
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)])
457             if ids:
458                 result[md.id] = mod_obj.read(cr, uid, [ids[0]], ['state'])[0]['state']
459             else:
460                 result[md.id] = 'unknown'
461         return result
462
463     _columns = {
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),
475     }
476 module_dependency()
477 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
478