diff --git a/module_uninstall_check/README.rst b/module_uninstall_check/README.rst new file mode 100644 index 000000000..8cb99425e --- /dev/null +++ b/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 +`_. 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. + diff --git a/module_uninstall_check/__init__.py b/module_uninstall_check/__init__.py new file mode 100644 index 000000000..943cefd96 --- /dev/null +++ b/module_uninstall_check/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import wizards diff --git a/module_uninstall_check/__openerp__.py b/module_uninstall_check/__openerp__.py new file mode 100644 index 000000000..9712c0575 --- /dev/null +++ b/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', + ], +} diff --git a/module_uninstall_check/demo/res_groups.xml b/module_uninstall_check/demo/res_groups.xml new file mode 100644 index 000000000..6b7ad574d --- /dev/null +++ b/module_uninstall_check/demo/res_groups.xml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/module_uninstall_check/i18n/fr.po b/module_uninstall_check/i18n/fr.po new file mode 100644 index 000000000..242d155de --- /dev/null +++ b/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" + diff --git a/module_uninstall_check/static/description/module_form.png b/module_uninstall_check/static/description/module_form.png new file mode 100644 index 000000000..ff3cf7d14 Binary files /dev/null and b/module_uninstall_check/static/description/module_form.png differ diff --git a/module_uninstall_check/static/description/sale_margin_uninstallation.png b/module_uninstall_check/static/description/sale_margin_uninstallation.png new file mode 100644 index 000000000..99cf3e9c6 Binary files /dev/null and b/module_uninstall_check/static/description/sale_margin_uninstallation.png differ diff --git a/module_uninstall_check/static/description/sale_uninstallation.png b/module_uninstall_check/static/description/sale_uninstallation.png new file mode 100644 index 000000000..adf969390 Binary files /dev/null and b/module_uninstall_check/static/description/sale_uninstallation.png differ diff --git a/module_uninstall_check/views/ir_module_module.xml b/module_uninstall_check/views/ir_module_module.xml new file mode 100644 index 000000000..3ad1bb55d --- /dev/null +++ b/module_uninstall_check/views/ir_module_module.xml @@ -0,0 +1,22 @@ + + + + + + + ir.module.module + + + + + + + diff --git a/module_uninstall_check/wizards/__init__.py b/module_uninstall_check/wizards/__init__.py new file mode 100644 index 000000000..476e5cfe6 --- /dev/null +++ b/module_uninstall_check/wizards/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import wizard_module_uninstall +from . import wizard_module_uninstall_line diff --git a/module_uninstall_check/wizards/action.xml b/module_uninstall_check/wizards/action.xml new file mode 100644 index 000000000..d7c2a5bcd --- /dev/null +++ b/module_uninstall_check/wizards/action.xml @@ -0,0 +1,18 @@ + + + + + + + Uninstallation Impact + wizard.module.uninstall + form + form + new + + + diff --git a/module_uninstall_check/wizards/wizard_module_uninstall.py b/module_uninstall_check/wizards/wizard_module_uninstall.py new file mode 100644 index 000000000..a84e9eee4 --- /dev/null +++ b/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 diff --git a/module_uninstall_check/wizards/wizard_module_uninstall.xml b/module_uninstall_check/wizards/wizard_module_uninstall.xml new file mode 100644 index 000000000..f6ab685c1 --- /dev/null +++ b/module_uninstall_check/wizards/wizard_module_uninstall.xml @@ -0,0 +1,49 @@ + + + + + + + wizard.module.uninstall + +
+

The following models and fields will be dropped if uninstallation of the selected module is done

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
diff --git a/module_uninstall_check/wizards/wizard_module_uninstall_line.py b/module_uninstall_check/wizards/wizard_module_uninstall_line.py new file mode 100644 index 000000000..b34759448 --- /dev/null +++ b/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