Browse Source

[ADD] module module_uninstall_check (#816)

* [ADD] module module_uninstall_check

* [ADD] database size information + [IMP] various. cortesy @jcdrubay

* [IMP] make module working if model are present but code is not present

* [FIX] pylint and readme rile

* [FIX] flake8

* [FIX] pylint
pull/1364/head
Sylvain LE GAL 7 years ago
committed by Dave Lasley
parent
commit
c2258f30dc
  1. 103
      module_uninstall_check/README.rst
  2. 2
      module_uninstall_check/__init__.py
  3. 23
      module_uninstall_check/__openerp__.py
  4. 15
      module_uninstall_check/demo/res_groups.xml
  5. 188
      module_uninstall_check/i18n/fr.po
  6. BIN
      module_uninstall_check/static/description/module_form.png
  7. BIN
      module_uninstall_check/static/description/sale_margin_uninstallation.png
  8. BIN
      module_uninstall_check/static/description/sale_uninstallation.png
  9. 22
      module_uninstall_check/views/ir_module_module.xml
  10. 3
      module_uninstall_check/wizards/__init__.py
  11. 18
      module_uninstall_check/wizards/action.xml
  12. 117
      module_uninstall_check/wizards/wizard_module_uninstall.py
  13. 49
      module_uninstall_check/wizards/wizard_module_uninstall.xml
  14. 122
      module_uninstall_check/wizards/wizard_module_uninstall_line.py

103
module_uninstall_check/README.rst

@ -0,0 +1,103 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
============================
Module Uninstallation Checks
============================
This module extends the functionality of base module, to improve modules
uninstallation process.
It provides an extra view, on module form to display which models (SQL tables)
and which fields (SQL columns) will be dropped, if the selected module is
uninstalled.
Technical Note
==============
This module uses postgreSQL native column like reltuples in pg_class that
provides approximative rows quantity. To have a precise value, please
run first the following code:
.. code-block:: sql
REINDEX DATABASE my_database_name;
Usage
=====
To use this module, you need to:
#. Go to Settings / Modules / Local Modules
#. Select an installed module
#. Click on the button 'Uninstallation Impact'
.. figure:: /module_uninstall_check/static/description/module_form.png
:width: 800 px
* Sample, selecting sale_margin module
.. figure:: /module_uninstall_check/static/description/sale_margin_uninstallation.png
:width: 800 px
* Sample, selecting sale_stock module, when sale_margin is installed
.. figure:: /module_uninstall_check/static/description/sale_uninstallation.png
:width: 800 px
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/149/8.0
Known issues / Roadmap
======================
* In some cases, we want to uninstall a module, but prevent some data deletion.
This can happen if we want to keep some datas after uninstallation or if the
data moved into another module after a refactoring.
This module could implement such feature, adding extra feature on wizard lines,
deleting or renaming xml ids.
* For the time being, wizard displays size used for models in database. It
could be interesting to know the space released by the deletion of a column.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues
<https://github.com/OCA/server-tools/issues>`_. In case of trouble, please
check there if your issue has already been reported. If you spotted it first,
help us smash it by providing detailed and welcomed feedback.
Credits
=======
Contributors
------------
* Sylvain LE GAL (https://twitter.com/legalsylvain)
Funders
-------
The development of this module has been financially supported by:
* GRAP, Groupement Régional Alimentaire de Proximité (http://www.grap.coop)
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
To contribute to this module, please visit https://odoo-community.org.

2
module_uninstall_check/__init__.py

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import wizards

23
module_uninstall_check/__openerp__.py

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Module Uninstall Check",
"summary": "Add Extra Checks before uninstallation of modules",
"version": "8.0.1.0.0",
"category": "Base",
"website": "https://odoo-community.org/",
"author": "GRAP, Odoo Community Association (OCA)",
"license": "AGPL-3",
"installable": True,
"depends": [
'base',
],
"data": [
'wizards/wizard_module_uninstall.xml',
'wizards/action.xml',
'views/ir_module_module.xml',
],
"demo": [
'demo/res_groups.xml',
],
}

15
module_uninstall_check/demo/res_groups.xml

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2015 - Today: GRAP (http://www.grap.coop)
@author Sylvain LE GAL (https://twitter.com/legalsylvain)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<openerp><data>
<record id="base.group_no_one" model="res.groups">
<field name="users" eval="[
(4, ref('base.user_root'))]" />
</record>
</data></openerp>

188
module_uninstall_check/i18n/fr.po

@ -0,0 +1,188 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * module_uninstall_check
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 8.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-04-20 20:14+0000\n"
"PO-Revision-Date: 2017-04-20 20:14+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: module_uninstall_check
#: view:wizard.module.uninstall:module_uninstall_check.view_wizard_module_uninstall_form
msgid "Cancel"
msgstr "Cancel"
#. module: module_uninstall_check
#: help:wizard.module.uninstall,module_id:0
msgid "Choose a module. The wizard will display all the models and fields linked to that module, that will be dropped, if selected module is uninstalled.\n"
" Note : Only Non Transient items will be displayed"
msgstr "Choisissez un module. L'assistant va afficher tous les modèles et tous les champs associés à ce module, et qui seront supprimés, si le module sélectionné est désinstallé.\n"
" Note: Seulement les éléments persistants en base de données seront affichés"
#. module: module_uninstall_check
#: field:wizard.module.uninstall,create_uid:0
#: field:wizard.module.uninstall.line,create_uid:0
msgid "Created by"
msgstr "Créé par"
#. module: module_uninstall_check
#: field:wizard.module.uninstall,create_date:0
#: field:wizard.module.uninstall.line,create_date:0
msgid "Created on"
msgstr "Créé le"
#. module: module_uninstall_check
#: field:wizard.module.uninstall,display_name:0
#: field:wizard.module.uninstall.line,display_name:0
msgid "Display Name"
msgstr "Nom affiché"
#. module: module_uninstall_check
#: selection:wizard.module.uninstall.line,type:0
msgid "Field"
msgstr "Champ"
#. module: module_uninstall_check
#: field:wizard.module.uninstall.line,field_model_name:0
msgid "Field Model Name"
msgstr "Nom du modèle du champ"
#. module: module_uninstall_check
#: field:wizard.module.uninstall.line,field_name:0
msgid "Field Name"
msgstr "Nom du champ"
#. module: module_uninstall_check
#: field:wizard.module.uninstall.line,field_ttype:0
msgid "Field Type"
msgstr "Type du champ"
#. module: module_uninstall_check
#: field:wizard.module.uninstall.line,field_id:0
msgid "Field id"
msgstr "Field id"
#. module: module_uninstall_check
#: field:wizard.module.uninstall,field_line_ids:0
msgid "Field line ids"
msgstr "Field line ids"
#. module: module_uninstall_check
#: field:wizard.module.uninstall,id:0
#: field:wizard.module.uninstall.line,id:0
msgid "ID"
msgstr "ID"
#. module: module_uninstall_check
#: field:wizard.module.uninstall,module_ids:0
msgid "Impacted modules"
msgstr "Modules impactés"
#. module: module_uninstall_check
#: field:wizard.module.uninstall,module_qty:0
msgid "Impacted modules Quantity"
msgstr "Nombre de modules impactés"
#. module: module_uninstall_check
#: field:wizard.module.uninstall,__last_update:0
#: field:wizard.module.uninstall.line,__last_update:0
msgid "Last Modified on"
msgstr "Dernière modification le"
#. module: module_uninstall_check
#: field:wizard.module.uninstall,write_uid:0
#: field:wizard.module.uninstall.line,write_uid:0
msgid "Last Updated by"
msgstr "Dernière mise à jour par"
#. module: module_uninstall_check
#: field:wizard.module.uninstall,write_date:0
#: field:wizard.module.uninstall.line,write_date:0
msgid "Last Updated on"
msgstr "Dernière mise à jour le"
#. module: module_uninstall_check
#: selection:wizard.module.uninstall.line,type:0
msgid "Model"
msgstr "Modèle"
#. module: module_uninstall_check
#: field:wizard.module.uninstall.line,model_name:0
msgid "Model Name"
msgstr "Nom du modèle"
#. module: module_uninstall_check
#: field:wizard.module.uninstall.line,model_id:0
msgid "Model id"
msgstr "Model id"
#. module: module_uninstall_check
#: field:wizard.module.uninstall,model_line_ids:0
msgid "Model line ids"
msgstr "Model line ids"
#. module: module_uninstall_check
#: field:wizard.module.uninstall,module_id:0
msgid "Module"
msgstr "Module"
#. module: module_uninstall_check
#: field:wizard.module.uninstall,module_name:0
msgid "Module Name"
msgstr "Nom du module"
#. module: module_uninstall_check
#: help:wizard.module.uninstall,module_ids:0
msgid "Modules list that will be uninstalled by dependency"
msgstr "Liste des modules qui seront désinstallés par dépendance"
#. module: module_uninstall_check
#: view:wizard.module.uninstall:module_uninstall_check.view_wizard_module_uninstall_form
msgid "Related Fields"
msgstr "Champs associés"
#. module: module_uninstall_check
#: view:wizard.module.uninstall:module_uninstall_check.view_wizard_module_uninstall_form
msgid "Related Models"
msgstr "Modèle associés"
#. module: module_uninstall_check
#: field:wizard.module.uninstall.line,model_row_qty:0
msgid "Row Quantity"
msgstr "Nombre de lignes"
#. module: module_uninstall_check
#: view:wizard.module.uninstall:module_uninstall_check.view_wizard_module_uninstall_form
msgid "System Update"
msgstr "System Update"
#. module: module_uninstall_check
#: view:wizard.module.uninstall:module_uninstall_check.view_wizard_module_uninstall_form
msgid "The following models and fields will be dropped if uninstallation of the selected module is done"
msgstr "Les modèles et les champs suivants seront supprimés si la désinstallation du module sélectionné est réalisé"
#. module: module_uninstall_check
#: field:wizard.module.uninstall.line,type:0
msgid "Type"
msgstr "Type"
#. module: module_uninstall_check
#: model:ir.actions.act_window,name:module_uninstall_check.action_wizard_module_uninstall
#: view:ir.module.module:module_uninstall_check.view_ir_module_module_form
msgid "Uninstallation Impact"
msgstr "Impact de la désinstallation"
#. module: module_uninstall_check
#: field:wizard.module.uninstall.line,wizard_id:0
msgid "Wizard id"
msgstr "Wizard id"

BIN
module_uninstall_check/static/description/module_form.png

After

Width: 760  |  Height: 228  |  Size: 29 KiB

BIN
module_uninstall_check/static/description/sale_margin_uninstallation.png

After

Width: 888  |  Height: 548  |  Size: 33 KiB

BIN
module_uninstall_check/static/description/sale_uninstallation.png

After

Width: 706  |  Height: 719  |  Size: 64 KiB

22
module_uninstall_check/views/ir_module_module.xml

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2017 - Today: GRAP (http://www.grap.coop)
@author Sylvain LE GAL (https://twitter.com/legalsylvain)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<openerp><data>
<record id="ir_module_module_view_form" model="ir.ui.view">
<field name="model">ir.module.module</field>
<field name="inherit_id" ref="base.module_form"/>
<field name="arch" type="xml">
<button name="button_install_cancel" position="after">
<button name="%(wizard_module_uninstall_action)d"
type="action" string="Uninstallation Impact"
attrs="{'invisible': [('state', 'in', ('uninstalled', 'uninstallable'))]}"/>
</button>
</field>
</record>
</data></openerp>

3
module_uninstall_check/wizards/__init__.py

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import wizard_module_uninstall
from . import wizard_module_uninstall_line

18
module_uninstall_check/wizards/action.xml

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2016-Today GRAP (http://www.grap.coop)
@author: Sylvain LE GAL (https://twitter.com/legalsylvain)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<openerp><data>
<record id="wizard_module_uninstall_action" model="ir.actions.act_window">
<field name="name">Uninstallation Impact</field>
<field name="res_model">wizard.module.uninstall</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</data></openerp>

117
module_uninstall_check/wizards/wizard_module_uninstall.py

@ -0,0 +1,117 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2017 - Today: GRAP (http://www.grap.coop)
# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import api, fields, models
class WizardModuleUninstall(models.TransientModel):
_name = 'wizard.module.uninstall'
def _default_module_id(self):
return self._context.get('active_id', False)
module_id = fields.Many2one(
string='Module', comodel_name='ir.module.module', required=True,
domain=[('state', 'not in', ['uninstalled', 'uninstallable'])],
default=_default_module_id,
help="Choose a module. The wizard will display all the models"
" and fields linked to that module, that will be dropped,"
" if selected module is uninstalled.\n"
" Note : Only Non Transient items will be displayed")
module_ids = fields.Many2many(
string='Impacted modules', compute='_compute_module_ids',
multi='module_ids',
comodel_name='ir.module.module', readonly=True,
help="Modules list that will be uninstalled by dependency")
module_qty = fields.Integer(
string='Impacted modules Quantity', compute='_compute_module_ids',
multi='module_ids', readonly=True)
module_name = fields.Char(
string='Module Name', related='module_id.name', readonly=True)
model_line_ids = fields.One2many(
comodel_name='wizard.module.uninstall.line', readonly=True,
inverse_name='wizard_id', domain=[('line_type', '=', 'model')])
field_line_ids = fields.One2many(
comodel_name='wizard.module.uninstall.line', readonly=True,
inverse_name='wizard_id', domain=[('line_type', '=', 'field')])
# Compute Section
@api.multi
@api.depends('module_id')
def _compute_module_ids(self):
for wizard in self:
if wizard.module_id:
res = wizard.module_id.downstream_dependencies()
wizard.module_ids = res
wizard.module_qty = len(res)
else:
wizard.module_ids = False
wizard.module_qty = 0
# OnChange Section
@api.multi
@api.onchange('module_id')
def onchange_module_id(self):
model_data_obj = self.env['ir.model.data']
model_obj = self.env['ir.model']
field_obj = self.env['ir.model.fields']
for wizard in self:
wizard.model_line_ids = False
wizard.field_line_ids = False
for wizard in self:
model_ids = []
module_names = wizard.module_ids.mapped('name')\
+ [wizard.module_id.name]
# Get Models
models_data = []
all_model_ids = model_data_obj.search([
('module', 'in', module_names),
('model', '=', 'ir.model')]).mapped('res_id')
all_model_ids = list(set(all_model_ids))
for model in model_obj.browse(all_model_ids).filtered(
lambda x: not x.osv_memory):
# Filter models that are not associated to other modules,
# and that will be removed, if the selected module is
# uninstalled
other_declarations = model_data_obj.search([
('module', 'not in', module_names),
('model', '=', 'ir.model'),
('res_id', '=', model.id)])
if not len(other_declarations):
models_data.append((0, 0, {
'line_type': 'model',
'model_id': model.id,
}))
model_ids.append(model.id)
wizard.model_line_ids = models_data
# Get Fields
fields_data = []
all_field_ids = model_data_obj.search([
('module', 'in', module_names),
('model', '=', 'ir.model.fields')]).mapped('res_id')
for field in field_obj.search([
('id', 'in', all_field_ids),
('model_id', 'not in', model_ids),
('ttype', 'not in', ['one2many'])],
order='model_id, name'):
other_declarations = model_data_obj.search([
('module', 'not in', module_names),
('model', '=', 'ir.model.field'),
('res_id', '=', model.id)])
if not len(other_declarations)\
and not field.model_id.osv_memory:
fields_data.append((0, 0, {
'line_type': 'field',
'field_id': field.id,
}))
wizard.field_line_ids = fields_data

49
module_uninstall_check/wizards/wizard_module_uninstall.xml

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2017 - Today: GRAP (http://www.grap.coop)
@author Sylvain LE GAL (https://twitter.com/legalsylvain)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<openerp><data>
<record id="wizard_module_uninstall_view_form" model="ir.ui.view">
<field name="model">wizard.module.uninstall</field>
<field name="arch" type="xml">
<form string="System Update">
<p>The following models and fields will be dropped if uninstallation of the selected module is done</p>
<field name="module_qty" invisible="1"/>
<group col="4">
<field name="module_id"/>
<field name="module_name" />
<field name="module_ids" widget="many2many_tags" attrs="{'invisible': [('module_qty', '=', 0)]}" colspan="4"/>
</group>
<separator string="Related Models"/>
<field name="model_line_ids" notitle="1" colspan="4" attrs="{'invisible': [('model_line_ids', '=', False)]}">
<tree colors="gray: db_size==0; brown:db_type=='v'">
<field name="db_type" invisible="1"/>
<field name="model_id" />
<field name="model_name" />
<field name="model_row_qty" />
<field name="table_size" sum="Total"/>
<field name="index_size" sum="Total"/>
<field name="db_size" sum="Total"/>
</tree>
</field>
<separator string="Related Fields"/>
<field name="field_line_ids" notitle="1" colspan="4" attrs="{'invisible': [('field_line_ids', '=', False)]}">
<tree>
<field name="field_id" />
<field name="field_model_name" />
<field name="field_name" />
<field name="field_ttype" />
</tree>
</field>
<footer>
<button special="cancel" string="Cancel"/>
</footer>
</form>
</field>
</record>
</data></openerp>

122
module_uninstall_check/wizards/wizard_module_uninstall_line.py

@ -0,0 +1,122 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2017 - Today: GRAP (http://www.grap.coop)
# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging
from openerp import api, fields, models
_logger = logging.getLogger(__name__)
class WizardModuleUninstallLine(models.TransientModel):
_name = 'wizard.module.uninstall.line'
LINE_TYPE_SELECTION = [
('model', 'Model'),
('field', 'Field'),
]
DB_TYPE_SELECTION = [
('', 'Unknown Type'),
('r', 'Ordinary Table'),
('i', 'Index'),
('S', 'Sequence'),
('v', 'view'),
('c', 'Composite Type'),
('t', 'TOAST table'),
]
wizard_id = fields.Many2one(
comodel_name='wizard.module.uninstall', required=True)
line_type = fields.Selection(
selection=LINE_TYPE_SELECTION, required=True)
model_id = fields.Many2one(comodel_name='ir.model', readonly=True)
model_name = fields.Char(
string='Model Name', related='model_id.model', readonly=True)
table_size = fields.Integer(
string='Table Size (KB)', compute='_compute_database',
multi='database', store=True)
index_size = fields.Integer(
string='Indexes Size (KB)', compute='_compute_database',
multi='database', store=True)
db_type = fields.Selection(
selection=DB_TYPE_SELECTION, compute='_compute_database',
multi='database', store=True)
db_size = fields.Integer(
string='Total DB Size (KB)', compute='_compute_database',
multi='database', store=True)
model_row_qty = fields.Integer(
string='Rows Quantity', compute='_compute_database', multi='database',
store=True,
help="The approximate value of the number of records in the database,"
" based on the PostgreSQL column 'reltuples'.\n You should reindex"
" your database, to have a more precise value\n\n"
" 'REINDEX database your_database_name;'")
field_id = fields.Many2one(comodel_name='ir.model.fields', readonly=True)
field_name = fields.Char(
string='Field Name', related='field_id.name', readonly=True)
field_ttype = fields.Selection(
string='Field Type', related='field_id.ttype', readonly=True)
field_model_name = fields.Char(
string='Field Model Name', related='field_id.model_id.model',
readonly=True)
@api.multi
@api.depends('model_id', 'line_type')
def _compute_database(self):
table_names = []
for line in self.filtered(lambda x: x.model_id):
model_obj = self.env.registry.get(line.model_id.model, False)
if model_obj:
table_names.append(model_obj._table)
else:
# Try to guess table name, replacing "." by "_"
table_names.append(line.model_id.model.replace('.', '_'))
# Get Relation Informations
req = (
"SELECT"
" table_name,"
" row_qty,"
" db_type,"
" pg_table_size(table_name::regclass::oid) AS table_size,"
" pg_indexes_size(table_name::regclass::oid) AS index_size,"
" pg_total_relation_size(table_name::regclass::oid) AS db_size"
" FROM ("
" SELECT"
" relname AS table_name,"
" reltuples AS row_qty,"
" relkind as db_type"
" FROM pg_class"
" WHERE relname IN %s) AS tmp;"
)
self.env.cr.execute(req, (tuple(table_names),))
res = self.env.cr.fetchall()
table_res = {x[0]: (x[1], x[2], x[3], x[4], x[5]) for x in res}
for line in self:
model_obj = self.env.registry.get(line.model_id.model, False)
if model_obj:
table_name = model_obj._table
else:
# Try to guess table name, replacing "." by "_"
table_name = line.model_id.model.replace('.', '_')
res = table_res.get(table_name, (0, '', 0, 0, 0))
line.model_row_qty = res[0]
line.db_type = res[1]
line.table_size = res[2] / 1024
line.index_size = res[3] / 1024
line.db_size = res[4] / 1024
Loading…
Cancel
Save