product_library adds some fields to product and some utilities addons from OCA
This commit is contained in:
parent
9145e0cabe
commit
6c8876b991
334 changed files with 92878 additions and 0 deletions
9
database_cleanup/models/__init__.py
Normal file
9
database_cleanup/models/__init__.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from . import purge_wizard
|
||||
from . import purge_modules
|
||||
from . import purge_models
|
||||
from . import purge_columns
|
||||
from . import purge_tables
|
||||
from . import purge_data
|
||||
from . import purge_menus
|
||||
from . import create_indexes
|
||||
from . import purge_properties
|
82
database_cleanup/models/create_indexes.py
Normal file
82
database_cleanup/models/create_indexes.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
# Copyright 2017 Therp BV <http://therp.nl>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
# pylint: disable=consider-merging-classes-inherited
|
||||
from ..identifier_adapter import IdentifierAdapter
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class CreateIndexesLine(models.TransientModel):
|
||||
_inherit = 'cleanup.purge.line'
|
||||
_name = 'cleanup.create_indexes.line'
|
||||
|
||||
purged = fields.Boolean('Created')
|
||||
wizard_id = fields.Many2one('cleanup.create_indexes.wizard')
|
||||
field_id = fields.Many2one('ir.model.fields', required=True)
|
||||
|
||||
@api.multi
|
||||
def purge(self):
|
||||
tables = set()
|
||||
for field in self.mapped('field_id'):
|
||||
model = self.env[field.model]
|
||||
name = '%s_%s_index' % (model._table, field.name)
|
||||
self.env.cr.execute(
|
||||
'create index %s ON %s (%s)',
|
||||
(
|
||||
IdentifierAdapter(name, quote=False),
|
||||
IdentifierAdapter(model._table),
|
||||
IdentifierAdapter(field.name),
|
||||
),
|
||||
)
|
||||
tables.add(model._table)
|
||||
for table in tables:
|
||||
self.env.cr.execute(
|
||||
'analyze %s', (IdentifierAdapter(model._table),)
|
||||
)
|
||||
self.write({
|
||||
'purged': True,
|
||||
})
|
||||
|
||||
|
||||
class CreateIndexesWizard(models.TransientModel):
|
||||
_inherit = 'cleanup.purge.wizard'
|
||||
_name = 'cleanup.create_indexes.wizard'
|
||||
_description = 'Create indexes'
|
||||
|
||||
purge_line_ids = fields.One2many(
|
||||
'cleanup.create_indexes.line', 'wizard_id',
|
||||
)
|
||||
|
||||
@api.multi
|
||||
def find(self):
|
||||
res = list()
|
||||
for field in self.env['ir.model.fields'].search([
|
||||
('index', '=', True),
|
||||
]):
|
||||
if field.model not in self.env.registry:
|
||||
continue
|
||||
model = self.env[field.model]
|
||||
name = '%s_%s_index' % (model._table, field.name)
|
||||
self.env.cr.execute(
|
||||
'select indexname from pg_indexes '
|
||||
'where indexname=%s and tablename=%s',
|
||||
(name, model._table)
|
||||
)
|
||||
if self.env.cr.rowcount:
|
||||
continue
|
||||
|
||||
self.env.cr.execute(
|
||||
'select a.attname '
|
||||
'from pg_attribute a '
|
||||
'join pg_class c on a.attrelid=c.oid '
|
||||
'join pg_tables t on t.tablename=c.relname '
|
||||
'where attname=%s and c.relname=%s',
|
||||
(field.name, model._table,)
|
||||
)
|
||||
if not self.env.cr.rowcount:
|
||||
continue
|
||||
|
||||
res.append((0, 0, {
|
||||
'name': '%s.%s' % (field.model, field.name),
|
||||
'field_id': field.id,
|
||||
}))
|
||||
return res
|
132
database_cleanup/models/purge_columns.py
Normal file
132
database_cleanup/models/purge_columns.py
Normal file
|
@ -0,0 +1,132 @@
|
|||
# Copyright 2014-2016 Therp BV <http://therp.nl>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
# pylint: disable=consider-merging-classes-inherited
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from ..identifier_adapter import IdentifierAdapter
|
||||
|
||||
|
||||
class CleanupPurgeLineColumn(models.TransientModel):
|
||||
_inherit = 'cleanup.purge.line'
|
||||
_name = 'cleanup.purge.line.column'
|
||||
_description = 'Purge Column Wizard Lines'
|
||||
|
||||
model_id = fields.Many2one('ir.model', 'Model', required=True,
|
||||
ondelete='CASCADE')
|
||||
wizard_id = fields.Many2one(
|
||||
'cleanup.purge.wizard.column', 'Purge Wizard', readonly=True)
|
||||
|
||||
@api.multi
|
||||
def purge(self):
|
||||
"""
|
||||
Unlink columns upon manual confirmation.
|
||||
"""
|
||||
if self:
|
||||
objs = self
|
||||
else:
|
||||
objs = self.env['cleanup.purge.line.column']\
|
||||
.browse(self._context.get('active_ids'))
|
||||
for line in objs:
|
||||
if line.purged:
|
||||
continue
|
||||
model_pool = self.env[line.model_id.model]
|
||||
# Check whether the column actually still exists.
|
||||
# Inheritance such as stock.picking.in from stock.picking
|
||||
# can lead to double attempts at removal
|
||||
self.env.cr.execute(
|
||||
'SELECT count(attname) FROM pg_attribute '
|
||||
'WHERE attrelid = '
|
||||
'( SELECT oid FROM pg_class WHERE relname = %s ) '
|
||||
'AND attname = %s',
|
||||
(model_pool._table, line.name))
|
||||
if not self.env.cr.fetchone()[0]:
|
||||
continue
|
||||
|
||||
self.logger.info(
|
||||
'Dropping column %s from table %s',
|
||||
line.name, model_pool._table)
|
||||
self.env.cr.execute(
|
||||
'ALTER TABLE %s DROP COLUMN %s',
|
||||
(
|
||||
IdentifierAdapter(model_pool._table),
|
||||
IdentifierAdapter(line.name)
|
||||
))
|
||||
line.write({'purged': True})
|
||||
# we need this commit because the ORM will deadlock if
|
||||
# we still have a pending transaction
|
||||
self.env.cr.commit() # pylint: disable=invalid-commit
|
||||
return True
|
||||
|
||||
|
||||
class CleanupPurgeWizardColumn(models.TransientModel):
|
||||
_inherit = 'cleanup.purge.wizard'
|
||||
_name = 'cleanup.purge.wizard.column'
|
||||
_description = 'Purge columns'
|
||||
|
||||
# List of known columns in use without corresponding fields
|
||||
# Format: {table: [fields]}
|
||||
blacklist = {
|
||||
'wkf_instance': ['uid'], # lp:1277899
|
||||
'res_users': ['password', 'password_crypt'],
|
||||
}
|
||||
|
||||
@api.model
|
||||
def get_orphaned_columns(self, model_pools):
|
||||
"""
|
||||
From openobject-server/openerp/osv/orm.py
|
||||
Iterate on the database columns to identify columns
|
||||
of fields which have been removed
|
||||
"""
|
||||
columns = list(set([
|
||||
column.name
|
||||
for model_pool in model_pools
|
||||
for column in model_pool._fields.values()
|
||||
if not (column.compute is not None and not column.store)
|
||||
]))
|
||||
columns += models.MAGIC_COLUMNS
|
||||
columns += self.blacklist.get(model_pools[0]._table, [])
|
||||
|
||||
self.env.cr.execute(
|
||||
"SELECT a.attname FROM pg_class c, pg_attribute a "
|
||||
"WHERE c.relname=%s AND c.oid=a.attrelid AND a.attisdropped=False "
|
||||
"AND pg_catalog.format_type(a.atttypid, a.atttypmod) "
|
||||
"NOT IN ('cid', 'tid', 'oid', 'xid') "
|
||||
"AND a.attname NOT IN %s",
|
||||
(model_pools[0]._table, tuple(columns)))
|
||||
return [column for column, in self.env.cr.fetchall()]
|
||||
|
||||
@api.model
|
||||
def find(self):
|
||||
"""
|
||||
Search for columns that are not in the corresponding model.
|
||||
|
||||
Group models by table to prevent false positives for columns
|
||||
that are only in some of the models sharing the same table.
|
||||
Example of this is 'sale_id' not being a field of stock.picking.in
|
||||
"""
|
||||
res = []
|
||||
|
||||
# mapping of tables to tuples (model id, [pool1, pool2, ...])
|
||||
table2model = {}
|
||||
|
||||
for model in self.env['ir.model'].search([]):
|
||||
if model.model not in self.env:
|
||||
continue
|
||||
model_pool = self.env[model.model]
|
||||
if not model_pool._auto:
|
||||
continue
|
||||
table2model.setdefault(
|
||||
model_pool._table, (model.id, [])
|
||||
)[1].append(model_pool)
|
||||
|
||||
for table, model_spec in table2model.items():
|
||||
for column in self.get_orphaned_columns(model_spec[1]):
|
||||
res.append((0, 0, {
|
||||
'name': column,
|
||||
'model_id': model_spec[0]}))
|
||||
if not res:
|
||||
raise UserError(_('No orphaned columns found'))
|
||||
return res
|
||||
|
||||
purge_line_ids = fields.One2many(
|
||||
'cleanup.purge.line.column', 'wizard_id', 'Columns to purge')
|
72
database_cleanup/models/purge_data.py
Normal file
72
database_cleanup/models/purge_data.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
# Copyright 2014-2016 Therp BV <http://therp.nl>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from ..identifier_adapter import IdentifierAdapter
|
||||
|
||||
|
||||
class CleanupPurgeLineData(models.TransientModel):
|
||||
_inherit = 'cleanup.purge.line'
|
||||
_name = 'cleanup.purge.line.data'
|
||||
|
||||
data_id = fields.Many2one('ir.model.data', 'Data entry')
|
||||
wizard_id = fields.Many2one(
|
||||
'cleanup.purge.wizard.data', 'Purge Wizard', readonly=True)
|
||||
|
||||
@api.multi
|
||||
def purge(self):
|
||||
"""Unlink data entries upon manual confirmation."""
|
||||
if self:
|
||||
objs = self
|
||||
else:
|
||||
objs = self.env['cleanup.purge.line.data']\
|
||||
.browse(self._context.get('active_ids'))
|
||||
to_unlink = objs.filtered(lambda x: not x.purged and x.data_id)
|
||||
self.logger.info('Purging data entries: %s', to_unlink.mapped('name'))
|
||||
to_unlink.mapped('data_id').unlink()
|
||||
return to_unlink.write({'purged': True})
|
||||
|
||||
|
||||
class CleanupPurgeWizardData(models.TransientModel):
|
||||
_inherit = 'cleanup.purge.wizard'
|
||||
_name = 'cleanup.purge.wizard.data'
|
||||
_description = 'Purge data'
|
||||
|
||||
@api.model
|
||||
def find(self):
|
||||
"""Collect all rows from ir_model_data that refer
|
||||
to a nonexisting model, or to a nonexisting
|
||||
row in the model's table."""
|
||||
res = []
|
||||
data_ids = []
|
||||
unknown_models = []
|
||||
self.env.cr.execute("""SELECT DISTINCT(model) FROM ir_model_data""")
|
||||
for model, in self.env.cr.fetchall():
|
||||
if not model:
|
||||
continue
|
||||
if model not in self.env:
|
||||
unknown_models.append(model)
|
||||
continue
|
||||
self.env.cr.execute(
|
||||
"""
|
||||
SELECT id FROM ir_model_data
|
||||
WHERE model = %s
|
||||
AND res_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT id FROM %s WHERE id=ir_model_data.res_id)
|
||||
""", (model, IdentifierAdapter(self.env[model]._table)))
|
||||
data_ids.extend(data_row for data_row, in self.env.cr.fetchall())
|
||||
data_ids += self.env['ir.model.data'].search([
|
||||
('model', 'in', unknown_models),
|
||||
]).ids
|
||||
for data in self.env['ir.model.data'].browse(data_ids):
|
||||
res.append((0, 0, {
|
||||
'data_id': data.id,
|
||||
'name': "%s.%s, object of type %s" % (
|
||||
data.module, data.name, data.model)}))
|
||||
if not res:
|
||||
raise UserError(_('No orphaned data entries found'))
|
||||
return res
|
||||
|
||||
purge_line_ids = fields.One2many(
|
||||
'cleanup.purge.line.data', 'wizard_id', 'Data to purge')
|
58
database_cleanup/models/purge_menus.py
Normal file
58
database_cleanup/models/purge_menus.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
# Copyright 2014-2016 Therp BV <http://therp.nl>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
# pylint: disable=consider-merging-classes-inherited
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class CleanupPurgeLineMenu(models.TransientModel):
|
||||
_inherit = 'cleanup.purge.line'
|
||||
_name = 'cleanup.purge.line.menu'
|
||||
|
||||
wizard_id = fields.Many2one(
|
||||
'cleanup.purge.wizard.menu', 'Purge Wizard', readonly=True)
|
||||
menu_id = fields.Many2one('ir.ui.menu', 'Menu entry')
|
||||
|
||||
@api.multi
|
||||
def purge(self):
|
||||
"""Unlink menu entries upon manual confirmation."""
|
||||
if self:
|
||||
objs = self
|
||||
else:
|
||||
objs = self.env['cleanup.purge.line.menu']\
|
||||
.browse(self._context.get('active_ids'))
|
||||
to_unlink = objs.filtered(lambda x: not x.purged and x.menu_id)
|
||||
self.logger.info('Purging menu entries: %s', to_unlink.mapped('name'))
|
||||
to_unlink.mapped('menu_id').unlink()
|
||||
return to_unlink.write({'purged': True})
|
||||
|
||||
|
||||
class CleanupPurgeWizardMenu(models.TransientModel):
|
||||
_inherit = 'cleanup.purge.wizard'
|
||||
_name = 'cleanup.purge.wizard.menu'
|
||||
_description = 'Purge menus'
|
||||
|
||||
@api.model
|
||||
def find(self):
|
||||
"""
|
||||
Search for models that cannot be instantiated.
|
||||
"""
|
||||
res = []
|
||||
for menu in self.env['ir.ui.menu'].with_context(active_test=False)\
|
||||
.search([('action', '!=', False)]):
|
||||
if menu.action.type != 'ir.actions.act_window':
|
||||
continue
|
||||
if (menu.action.res_model and menu.action.res_model not in
|
||||
self.env) or \
|
||||
(menu.action.src_model and menu.action.src_model not in
|
||||
self.env):
|
||||
res.append((0, 0, {
|
||||
'name': menu.complete_name,
|
||||
'menu_id': menu.id,
|
||||
}))
|
||||
if not res:
|
||||
raise UserError(_('No dangling menu entries found'))
|
||||
return res
|
||||
|
||||
purge_line_ids = fields.One2many(
|
||||
'cleanup.purge.line.menu', 'wizard_id', 'Menus to purge')
|
124
database_cleanup/models/purge_models.py
Normal file
124
database_cleanup/models/purge_models.py
Normal file
|
@ -0,0 +1,124 @@
|
|||
# Copyright 2014-2016 Therp BV <http://therp.nl>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
# pylint: disable=consider-merging-classes-inherited
|
||||
from odoo import _, api, models, fields
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG
|
||||
|
||||
|
||||
class IrModel(models.Model):
|
||||
_inherit = 'ir.model'
|
||||
|
||||
def _drop_table(self):
|
||||
"""this function crashes for undefined models"""
|
||||
existing_model_ids = self.filtered(lambda x: x.model in self.env)
|
||||
return super(IrModel, existing_model_ids)._drop_table()
|
||||
|
||||
@api.depends()
|
||||
def _inherited_models(self):
|
||||
"""this function crashes for undefined models"""
|
||||
existing_model_ids = self.filtered(lambda x: x.model in self.env)
|
||||
super(IrModel, existing_model_ids)._inherited_models()
|
||||
|
||||
|
||||
class IrModelFields(models.Model):
|
||||
_inherit = 'ir.model.fields'
|
||||
|
||||
@api.multi
|
||||
def _prepare_update(self):
|
||||
"""this function crashes for undefined models"""
|
||||
existing = self.filtered(lambda x: x.model in self.env)
|
||||
return super(IrModelFields, existing)._prepare_update()
|
||||
|
||||
|
||||
class CleanupPurgeLineModel(models.TransientModel):
|
||||
_inherit = 'cleanup.purge.line'
|
||||
_name = 'cleanup.purge.line.model'
|
||||
_description = 'Purge models'
|
||||
|
||||
wizard_id = fields.Many2one(
|
||||
'cleanup.purge.wizard.model', 'Purge Wizard', readonly=True)
|
||||
|
||||
@api.multi
|
||||
def purge(self):
|
||||
"""
|
||||
Unlink models upon manual confirmation.
|
||||
"""
|
||||
context_flags = {
|
||||
MODULE_UNINSTALL_FLAG: True,
|
||||
'purge': True,
|
||||
}
|
||||
|
||||
if self:
|
||||
objs = self
|
||||
else:
|
||||
objs = self.env['cleanup.purge.line.model']\
|
||||
.browse(self._context.get('active_ids'))
|
||||
for line in objs:
|
||||
self.env.cr.execute(
|
||||
"SELECT id, model from ir_model WHERE model = %s",
|
||||
(line.name,))
|
||||
row = self.env.cr.fetchone()
|
||||
if not row:
|
||||
continue
|
||||
self.logger.info('Purging model %s', row[1])
|
||||
attachments = self.env['ir.attachment'].search([
|
||||
('res_model', '=', line.name)
|
||||
])
|
||||
if attachments:
|
||||
self.env.cr.execute(
|
||||
"UPDATE ir_attachment SET res_model = NULL "
|
||||
"WHERE id in %s",
|
||||
(tuple(attachments.ids), ))
|
||||
self.env['ir.model.constraint'].search([
|
||||
('model', '=', line.name),
|
||||
]).unlink()
|
||||
cronjobs = self.env['ir.cron'].with_context(
|
||||
active_test=False
|
||||
).search([
|
||||
('model_id.model', '=', line.name),
|
||||
])
|
||||
if cronjobs:
|
||||
cronjobs.unlink()
|
||||
relations = self.env['ir.model.fields'].search([
|
||||
('relation', '=', row[1]),
|
||||
]).with_context(**context_flags)
|
||||
for relation in relations:
|
||||
try:
|
||||
# Fails if the model on the target side
|
||||
# cannot be instantiated
|
||||
relation.unlink()
|
||||
except KeyError:
|
||||
pass
|
||||
except AttributeError:
|
||||
pass
|
||||
self.env['ir.model.relation'].search([
|
||||
('model', '=', line.name)
|
||||
]).with_context(**context_flags).unlink()
|
||||
self.env['ir.model'].browse([row[0]])\
|
||||
.with_context(**context_flags).unlink()
|
||||
line.write({'purged': True})
|
||||
return True
|
||||
|
||||
|
||||
class CleanupPurgeWizardModel(models.TransientModel):
|
||||
_inherit = 'cleanup.purge.wizard'
|
||||
_name = 'cleanup.purge.wizard.model'
|
||||
_description = 'Purge models'
|
||||
|
||||
@api.model
|
||||
def find(self):
|
||||
"""
|
||||
Search for models that cannot be instantiated.
|
||||
"""
|
||||
res = []
|
||||
self.env.cr.execute("SELECT model from ir_model")
|
||||
for model, in self.env.cr.fetchall():
|
||||
if model not in self.env:
|
||||
res.append((0, 0, {'name': model}))
|
||||
if not res:
|
||||
raise UserError(_('No orphaned models found'))
|
||||
return res
|
||||
|
||||
purge_line_ids = fields.One2many(
|
||||
'cleanup.purge.line.model', 'wizard_id', 'Models to purge')
|
99
database_cleanup/models/purge_modules.py
Normal file
99
database_cleanup/models/purge_modules.py
Normal file
|
@ -0,0 +1,99 @@
|
|||
# Copyright 2014-2016 Therp BV <http://therp.nl>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
# pylint: disable=consider-merging-classes-inherited
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.modules.module import get_module_path
|
||||
from odoo.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG
|
||||
|
||||
|
||||
class IrModelData(models.Model):
|
||||
_inherit = 'ir.model.data'
|
||||
|
||||
@api.model
|
||||
def _module_data_uninstall(self, modules_to_remove):
|
||||
"""this function crashes for xmlids on undefined models or fields
|
||||
referring to undefined models"""
|
||||
for this in self.search([('module', 'in', modules_to_remove)]):
|
||||
if this.model == 'ir.model.fields':
|
||||
field = self.env[this.model].with_context(
|
||||
**{MODULE_UNINSTALL_FLAG: True}).browse(this.res_id)
|
||||
if not field.exists() or field.model not in self.env:
|
||||
this.unlink()
|
||||
continue
|
||||
if this.model not in self.env:
|
||||
this.unlink()
|
||||
return super(IrModelData, self)._module_data_uninstall(
|
||||
modules_to_remove)
|
||||
|
||||
|
||||
class CleanupPurgeLineModule(models.TransientModel):
|
||||
_inherit = 'cleanup.purge.line'
|
||||
_name = 'cleanup.purge.line.module'
|
||||
|
||||
wizard_id = fields.Many2one(
|
||||
'cleanup.purge.wizard.module', 'Purge Wizard', readonly=True)
|
||||
|
||||
@api.multi
|
||||
def purge(self):
|
||||
"""
|
||||
Uninstall modules upon manual confirmation, then reload
|
||||
the database.
|
||||
"""
|
||||
module_names = self.filtered(lambda x: not x.purged).mapped('name')
|
||||
modules = self.env['ir.module.module'].search([
|
||||
('name', 'in', module_names)
|
||||
])
|
||||
if not modules:
|
||||
return True
|
||||
self.logger.info('Purging modules %s', ', '.join(module_names))
|
||||
modules.filtered(
|
||||
lambda x: x.state == 'to install'
|
||||
).write({'state': 'uninstalled'})
|
||||
modules.filtered(
|
||||
lambda x: x.state in ('to upgrade', 'to remove')
|
||||
).write({'state': 'installed'})
|
||||
modules.filtered(
|
||||
lambda x: x.state == 'installed' and x.name != 'base'
|
||||
).button_immediate_uninstall()
|
||||
modules.refresh()
|
||||
modules.filtered(
|
||||
lambda x: x.state not in (
|
||||
'installed', 'to upgrade', 'to remove', 'to install')
|
||||
).unlink()
|
||||
return self.write({'purged': True})
|
||||
|
||||
|
||||
class CleanupPurgeWizardModule(models.TransientModel):
|
||||
_inherit = 'cleanup.purge.wizard'
|
||||
_name = 'cleanup.purge.wizard.module'
|
||||
_description = 'Purge modules'
|
||||
|
||||
@api.model
|
||||
def find(self):
|
||||
res = []
|
||||
purge_lines = self.env['cleanup.purge.line.module']
|
||||
IrModule = self.env['ir.module.module']
|
||||
for module in IrModule.search(
|
||||
[
|
||||
('to_buy', '=', False),
|
||||
('name', '!=', 'studio_customization')
|
||||
]
|
||||
):
|
||||
if get_module_path(module.name, display_warning=False):
|
||||
continue
|
||||
if module.state == 'uninstalled':
|
||||
purge_lines += self.env['cleanup.purge.line.module'].create({
|
||||
'name': module.name,
|
||||
})
|
||||
continue
|
||||
res.append((0, 0, {'name': module.name}))
|
||||
|
||||
purge_lines.purge()
|
||||
|
||||
if not res:
|
||||
raise UserError(_('No modules found to purge'))
|
||||
return res
|
||||
|
||||
purge_line_ids = fields.One2many(
|
||||
'cleanup.purge.line.module', 'wizard_id', 'Modules to purge')
|
136
database_cleanup/models/purge_properties.py
Normal file
136
database_cleanup/models/purge_properties.py
Normal file
|
@ -0,0 +1,136 @@
|
|||
# Copyright 2017 Therp BV <http://therp.nl>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
# pylint: disable=consider-merging-classes-inherited
|
||||
from odoo import api, models, fields
|
||||
REASON_DUPLICATE = 1
|
||||
REASON_DEFAULT = 2
|
||||
REASON_DEFAULT_FALSE = 3
|
||||
REASON_UNKNOWN_MODEL = 4
|
||||
|
||||
|
||||
class CleanupPurgeLineProperty(models.TransientModel):
|
||||
_inherit = 'cleanup.purge.line'
|
||||
_name = 'cleanup.purge.line.property'
|
||||
_description = 'Purge properties'
|
||||
|
||||
wizard_id = fields.Many2one(
|
||||
'cleanup.purge.wizard.property', 'Purge Wizard', readonly=True)
|
||||
property_id = fields.Many2one('ir.property')
|
||||
reason = fields.Selection([
|
||||
(REASON_DUPLICATE, 'Duplicated property'),
|
||||
(REASON_DEFAULT, 'Same value as default'),
|
||||
(REASON_DEFAULT_FALSE, 'Empty default property'),
|
||||
(REASON_UNKNOWN_MODEL, 'Unknown model'),
|
||||
])
|
||||
|
||||
@api.multi
|
||||
def purge(self):
|
||||
"""Delete properties"""
|
||||
self.write({'purged': True})
|
||||
return self.mapped('property_id').unlink()
|
||||
|
||||
|
||||
class CleanupPurgeWizardProperty(models.TransientModel):
|
||||
_inherit = 'cleanup.purge.wizard'
|
||||
_name = 'cleanup.purge.wizard.property'
|
||||
_description = 'Purge properties'
|
||||
|
||||
@api.model
|
||||
def find(self):
|
||||
"""
|
||||
Search property records which are duplicated or the same as the default
|
||||
"""
|
||||
result = []
|
||||
default_properties = self.env['ir.property'].search([
|
||||
('res_id', '=', False),
|
||||
])
|
||||
handled_field_ids = []
|
||||
for prop in default_properties:
|
||||
value = None
|
||||
try:
|
||||
value = prop.get_by_record()
|
||||
except KeyError:
|
||||
result.append({
|
||||
'name': '%s@%s: %s' % (
|
||||
prop.name, prop.res_id, value,
|
||||
),
|
||||
'property_id': prop.id,
|
||||
'reason': REASON_UNKNOWN_MODEL,
|
||||
})
|
||||
continue
|
||||
if not value:
|
||||
result.append({
|
||||
'name': '%s@%s: %s' % (
|
||||
prop.name, prop.res_id, value,
|
||||
),
|
||||
'property_id': prop.id,
|
||||
'reason': REASON_DEFAULT_FALSE,
|
||||
})
|
||||
continue
|
||||
if prop.fields_id.id in handled_field_ids:
|
||||
continue
|
||||
domain = [
|
||||
('id', '!=', prop.id),
|
||||
('fields_id', '=', prop.fields_id.id),
|
||||
# =? explicitly tests for None or False, not falsyness
|
||||
('value_float', '=?', prop.value_float or False),
|
||||
('value_integer', '=?', prop.value_integer or False),
|
||||
('value_text', '=?', prop.value_text or False),
|
||||
('value_binary', '=?', prop.value_binary or False),
|
||||
('value_reference', '=?', prop.value_reference or False),
|
||||
('value_datetime', '=?', prop.value_datetime or False),
|
||||
]
|
||||
if prop.company_id:
|
||||
domain.append(('company_id', '=', prop.company_id.id))
|
||||
else:
|
||||
domain.extend([
|
||||
'|',
|
||||
('company_id', '=', False),
|
||||
(
|
||||
'company_id', 'in', self.env['res.company'].search([
|
||||
(
|
||||
'id', 'not in', default_properties.filtered(
|
||||
lambda x: x.company_id and
|
||||
x.fields_id == prop.fields_id
|
||||
).ids,
|
||||
)
|
||||
]).ids
|
||||
),
|
||||
])
|
||||
|
||||
for redundant_property in self.env['ir.property'].search(domain):
|
||||
result.append({
|
||||
'name': '%s@%s: %s' % (
|
||||
prop.name, redundant_property.res_id,
|
||||
prop.get_by_record()
|
||||
),
|
||||
'property_id': redundant_property.id,
|
||||
'reason': REASON_DEFAULT,
|
||||
})
|
||||
handled_field_ids.append(prop.fields_id.id)
|
||||
self.env.cr.execute(
|
||||
'''
|
||||
with grouped_properties(ids, cnt) as (
|
||||
select array_agg(id), count(*)
|
||||
from ir_property group by res_id, company_id, fields_id
|
||||
)
|
||||
select ids from grouped_properties where cnt > 1
|
||||
'''
|
||||
)
|
||||
for ids, in self.env.cr.fetchall():
|
||||
# odoo uses the first property found by search
|
||||
for prop in self.env['ir.property'].search([
|
||||
('id', 'in', ids)
|
||||
])[1:]:
|
||||
result.append({
|
||||
'name': '%s@%s: %s' % (
|
||||
prop.name, prop.res_id, prop.get_by_record()
|
||||
),
|
||||
'property_id': prop.id,
|
||||
'reason': REASON_DUPLICATE,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
purge_line_ids = fields.One2many(
|
||||
'cleanup.purge.line.property', 'wizard_id', 'Properties to purge')
|
111
database_cleanup/models/purge_tables.py
Normal file
111
database_cleanup/models/purge_tables.py
Normal file
|
@ -0,0 +1,111 @@
|
|||
# Copyright 2014-2016 Therp BV <http://therp.nl>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
# pylint: disable=consider-merging-classes-inherited
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from ..identifier_adapter import IdentifierAdapter
|
||||
|
||||
|
||||
class CleanupPurgeLineTable(models.TransientModel):
|
||||
_inherit = 'cleanup.purge.line'
|
||||
_name = 'cleanup.purge.line.table'
|
||||
_description = 'Purge tables wizard lines'
|
||||
|
||||
wizard_id = fields.Many2one(
|
||||
'cleanup.purge.wizard.table', 'Purge Wizard', readonly=True)
|
||||
|
||||
@api.multi
|
||||
def purge(self):
|
||||
"""
|
||||
Unlink tables upon manual confirmation.
|
||||
"""
|
||||
if self:
|
||||
objs = self
|
||||
else:
|
||||
objs = self.env['cleanup.purge.line.table']\
|
||||
.browse(self._context.get('active_ids'))
|
||||
tables = objs.mapped('name')
|
||||
for line in objs:
|
||||
if line.purged:
|
||||
continue
|
||||
|
||||
# Retrieve constraints on the tables to be dropped
|
||||
# This query is referenced in numerous places
|
||||
# on the Internet but credits probably go to Tom Lane
|
||||
# in this post http://www.postgresql.org/\
|
||||
# message-id/22895.1226088573@sss.pgh.pa.us
|
||||
# Only using the constraint name and the source table,
|
||||
# but I'm leaving the rest in for easier debugging
|
||||
self.env.cr.execute(
|
||||
"""
|
||||
SELECT conname, confrelid::regclass, af.attname AS fcol,
|
||||
conrelid::regclass, a.attname AS col
|
||||
FROM pg_attribute af, pg_attribute a,
|
||||
(SELECT conname, conrelid, confrelid,conkey[i] AS conkey,
|
||||
confkey[i] AS confkey
|
||||
FROM (select conname, conrelid, confrelid, conkey,
|
||||
confkey, generate_series(1,array_upper(conkey,1)) AS i
|
||||
FROM pg_constraint WHERE contype = 'f') ss) ss2
|
||||
WHERE af.attnum = confkey AND af.attrelid = confrelid AND
|
||||
a.attnum = conkey AND a.attrelid = conrelid
|
||||
AND confrelid::regclass = '%s'::regclass;
|
||||
""", (IdentifierAdapter(line.name, quote=False),))
|
||||
|
||||
for constraint in self.env.cr.fetchall():
|
||||
if constraint[3] in tables:
|
||||
self.logger.info(
|
||||
'Dropping constraint %s on table %s (to be dropped)',
|
||||
constraint[0], constraint[3])
|
||||
self.env.cr.execute(
|
||||
"ALTER TABLE %s DROP CONSTRAINT %s",
|
||||
(
|
||||
IdentifierAdapter(constraint[3]),
|
||||
IdentifierAdapter(constraint[0])
|
||||
))
|
||||
|
||||
self.logger.info(
|
||||
'Dropping table %s', line.name)
|
||||
self.env.cr.execute(
|
||||
"DROP TABLE %s", (IdentifierAdapter(line.name),))
|
||||
line.write({'purged': True})
|
||||
return True
|
||||
|
||||
|
||||
class CleanupPurgeWizardTable(models.TransientModel):
|
||||
_inherit = 'cleanup.purge.wizard'
|
||||
_name = 'cleanup.purge.wizard.table'
|
||||
_description = 'Purge tables'
|
||||
|
||||
@api.model
|
||||
def find(self):
|
||||
"""
|
||||
Search for tables that cannot be instantiated.
|
||||
Ignore views for now.
|
||||
"""
|
||||
known_tables = []
|
||||
for model in self.env['ir.model'].search([]):
|
||||
if model.model not in self.env:
|
||||
continue
|
||||
model_pool = self.env[model.model]
|
||||
known_tables.append(model_pool._table)
|
||||
known_tables += [
|
||||
column.relation
|
||||
for column in model_pool._fields.values()
|
||||
if column.type == 'many2many' and
|
||||
(column.compute is None or column.store)
|
||||
and column.relation
|
||||
]
|
||||
|
||||
self.env.cr.execute(
|
||||
"""
|
||||
SELECT table_name FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
|
||||
AND table_name NOT IN %s""", (tuple(known_tables),))
|
||||
|
||||
res = [(0, 0, {'name': row[0]}) for row in self.env.cr.fetchall()]
|
||||
if not res:
|
||||
raise UserError(_('No orphaned tables found'))
|
||||
return res
|
||||
|
||||
purge_line_ids = fields.One2many(
|
||||
'cleanup.purge.line.table', 'wizard_id', 'Tables to purge')
|
96
database_cleanup/models/purge_wizard.py
Normal file
96
database_cleanup/models/purge_wizard.py
Normal file
|
@ -0,0 +1,96 @@
|
|||
# Copyright 2014-2016 Therp BV <http://therp.nl>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
# pylint: disable=consider-merging-classes-inherited
|
||||
|
||||
import logging
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import AccessDenied
|
||||
|
||||
|
||||
class CleanupPurgeLine(models.AbstractModel):
|
||||
""" Abstract base class for the purge wizard lines """
|
||||
_name = 'cleanup.purge.line'
|
||||
_order = 'name'
|
||||
_description = 'Purge Column Abstract Wizard'
|
||||
|
||||
name = fields.Char('Name', readonly=True)
|
||||
purged = fields.Boolean('Purged', readonly=True)
|
||||
wizard_id = fields.Many2one('cleanup.purge.wizard')
|
||||
|
||||
logger = logging.getLogger('odoo.addons.database_cleanup')
|
||||
|
||||
@api.multi
|
||||
def purge(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@api.model
|
||||
def create(self, values):
|
||||
# make sure the user trying this is actually supposed to do it
|
||||
if self.env.ref(
|
||||
'base.group_erp_manager') not in self.env.user.groups_id:
|
||||
raise AccessDenied
|
||||
return super(CleanupPurgeLine, self).create(values)
|
||||
|
||||
|
||||
class PurgeWizard(models.AbstractModel):
|
||||
""" Abstract base class for the purge wizards """
|
||||
_name = 'cleanup.purge.wizard'
|
||||
_description = 'Purge stuff'
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
res = super(PurgeWizard, self).default_get(fields_list)
|
||||
if 'purge_line_ids' in fields_list:
|
||||
res['purge_line_ids'] = self.find()
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def find(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@api.multi
|
||||
def purge_all(self):
|
||||
self.mapped('purge_line_ids').purge()
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def get_wizard_action(self):
|
||||
wizard = self.create({})
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': wizard.display_name,
|
||||
'views': [(False, 'form')],
|
||||
'res_model': self._name,
|
||||
'res_id': wizard.id,
|
||||
'flags': {
|
||||
'action_buttons': False,
|
||||
'sidebar': False,
|
||||
},
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def select_lines(self):
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Select lines to purge'),
|
||||
'views': [(False, 'tree'), (False, 'form')],
|
||||
'res_model': self._fields['purge_line_ids'].comodel_name,
|
||||
'domain': [('wizard_id', 'in', self.ids)],
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def name_get(self):
|
||||
return [
|
||||
(this.id, self._description)
|
||||
for this in self
|
||||
]
|
||||
|
||||
@api.model
|
||||
def create(self, values):
|
||||
# make sure the user trying this is actually supposed to do it
|
||||
if self.env.ref(
|
||||
'base.group_erp_manager') not in self.env.user.groups_id:
|
||||
raise AccessDenied
|
||||
return super(PurgeWizard, self).create(values)
|
||||
|
||||
purge_line_ids = fields.One2many('cleanup.purge.line', 'wizard_id')
|
Loading…
Add table
Add a link
Reference in a new issue