mirror of
https://github.com/bringout/oca-technical.git
synced 2026-04-18 15:52:03 +02:00
Initial commit: OCA Technical packages (595 packages)
This commit is contained in:
commit
2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions
|
|
@ -0,0 +1,10 @@
|
|||
from . import purge_wizard
|
||||
from . import purge_modules
|
||||
from . import purge_models
|
||||
from . import purge_fields
|
||||
from . import purge_columns
|
||||
from . import purge_tables
|
||||
from . import purge_data
|
||||
from . import purge_menus
|
||||
from . import create_indexes
|
||||
from . import purge_properties
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
# Copyright 2017 Therp BV <http://therp.nl>
|
||||
# Copyright 2021 Camptocamp <https://camptocamp.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
# pylint: disable=consider-merging-classes-inherited
|
||||
from odoo import fields, models
|
||||
|
||||
from ..identifier_adapter import IdentifierAdapter
|
||||
|
||||
|
||||
class CreateIndexesLine(models.TransientModel):
|
||||
_inherit = "cleanup.purge.line"
|
||||
_name = "cleanup.create_indexes.line"
|
||||
_description = "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)
|
||||
|
||||
def purge(self):
|
||||
for field in self.mapped("field_id"):
|
||||
model = self.env[field.model]
|
||||
name = "{}_{}_index".format(model._table, field.name)
|
||||
self.env.cr.execute(
|
||||
"create index %s ON %s (%s)",
|
||||
(
|
||||
IdentifierAdapter(name, quote=False),
|
||||
IdentifierAdapter(model._table),
|
||||
IdentifierAdapter(field.name),
|
||||
),
|
||||
)
|
||||
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",
|
||||
)
|
||||
|
||||
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 = "{}_{}_index".format(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": "{}.{}".format(field.model, field.name),
|
||||
"field_id": field.id,
|
||||
},
|
||||
)
|
||||
)
|
||||
return res
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
# Copyright 2014-2016 Therp BV <http://therp.nl>
|
||||
# Copyright 2021 Camptocamp <https://camptocamp.com>
|
||||
# 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 = "Cleanup Purge Line Column"
|
||||
|
||||
model_id = fields.Many2one("ir.model", "Model", required=True, ondelete="CASCADE")
|
||||
wizard_id = fields.Many2one(
|
||||
"cleanup.purge.wizard.column", "Purge Wizard", readonly=True
|
||||
)
|
||||
|
||||
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", "totp_secret"],
|
||||
"res_partner": ["signup_token"],
|
||||
}
|
||||
|
||||
@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(
|
||||
{
|
||||
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"
|
||||
)
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
# Copyright 2014-2016 Therp BV <http://therp.nl>
|
||||
# Copyright 2021 Camptocamp <https://camptocamp.com>
|
||||
# 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"
|
||||
_description = "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
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
# 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.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG
|
||||
|
||||
from ..identifier_adapter import IdentifierAdapter
|
||||
|
||||
|
||||
class CleanupPurgeLineField(models.TransientModel):
|
||||
_inherit = "cleanup.purge.line"
|
||||
_name = "cleanup.purge.line.field"
|
||||
_description = "Purge fields"
|
||||
|
||||
wizard_id = fields.Many2one(
|
||||
"cleanup.purge.wizard.field", "Purge Wizard", readonly=True
|
||||
)
|
||||
field_id = fields.Many2one(
|
||||
comodel_name="ir.model.fields",
|
||||
string="Field",
|
||||
)
|
||||
model_id = fields.Many2one(
|
||||
comodel_name="ir.model",
|
||||
related="field_id.model_id",
|
||||
string="Model",
|
||||
store=True,
|
||||
)
|
||||
model_name = fields.Char(
|
||||
related="model_id.model",
|
||||
string="Model Technical Name",
|
||||
store=True,
|
||||
)
|
||||
|
||||
def purge(self):
|
||||
"""
|
||||
Unlink fields upon manual confirmation.
|
||||
"""
|
||||
context_flags = {
|
||||
MODULE_UNINSTALL_FLAG: True,
|
||||
"purge": True,
|
||||
}
|
||||
if self:
|
||||
objs = self
|
||||
else:
|
||||
objs = self.env["cleanup.purge.line.action"].browse(
|
||||
self._context.get("active_ids")
|
||||
)
|
||||
to_unlink = objs.filtered(lambda x: not x.purged and x.field_id)
|
||||
self.logger.info("Purging field entries:")
|
||||
for rec in to_unlink:
|
||||
self.logger.info(" - %s.%s", rec.model_name, rec.field_id.name)
|
||||
field_id = rec.with_context(**context_flags).field_id
|
||||
model = self.env[rec.model_name]
|
||||
table_name = model._table
|
||||
column_name = field_id.name
|
||||
force_drop = False
|
||||
# FIX: on unlink, odoo will not DROP the SQL column even if exists if the
|
||||
# store attribute is set to False.
|
||||
if not field_id.store and model._auto:
|
||||
force_drop = True
|
||||
# Odoo will internally drop the SQL column
|
||||
field_id.unlink()
|
||||
if force_drop:
|
||||
self._drop_column(table_name, column_name)
|
||||
rec.purged = True
|
||||
return True
|
||||
|
||||
def _drop_column(self, table, column):
|
||||
# Use code from `purge_columns.py::purge()`
|
||||
# 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",
|
||||
(table, column),
|
||||
)
|
||||
if not self.env.cr.fetchone()[0]:
|
||||
return
|
||||
self.logger.info("Dropping column %s from table %s", column, table)
|
||||
self.env.cr.execute(
|
||||
"ALTER TABLE %s DROP COLUMN %s",
|
||||
(IdentifierAdapter(table), IdentifierAdapter(column)),
|
||||
)
|
||||
# we need this commit because the ORM will deadlock if
|
||||
# we still have a pending transaction
|
||||
self.env.cr.commit() # pylint: disable=invalid-commit
|
||||
|
||||
|
||||
class CleanupPurgeWizardField(models.TransientModel):
|
||||
_inherit = "cleanup.purge.wizard"
|
||||
_name = "cleanup.purge.wizard.field"
|
||||
_description = "Purge fields"
|
||||
|
||||
@api.model
|
||||
def find(self):
|
||||
"""
|
||||
Search for fields not technically mapped to a model.
|
||||
"""
|
||||
res = []
|
||||
ignored_fields = models.MAGIC_COLUMNS + [
|
||||
"display_name",
|
||||
models.BaseModel.CONCURRENCY_CHECK_FIELD,
|
||||
]
|
||||
domain = [("state", "=", "base")]
|
||||
for field_id in self.env["ir.model.fields"].search(domain):
|
||||
if field_id.name in ignored_fields:
|
||||
continue
|
||||
model = self.env[field_id.model_id.model]
|
||||
if field_id.name not in model._fields.keys():
|
||||
res.append(
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": field_id.name,
|
||||
"field_id": field_id.id,
|
||||
},
|
||||
)
|
||||
)
|
||||
if not res:
|
||||
raise UserError(_("No orphaned fields found"))
|
||||
return res
|
||||
|
||||
purge_line_ids = fields.One2many(
|
||||
"cleanup.purge.line.field", "wizard_id", "Fields to purge"
|
||||
)
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
# Copyright 2014-2016 Therp BV <http://therp.nl>
|
||||
# Copyright 2021 Camptocamp <https://camptocamp.com>
|
||||
# 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"
|
||||
_description = "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")
|
||||
|
||||
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:
|
||||
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"
|
||||
)
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
# Copyright 2014-2016 Therp BV <http://therp.nl>
|
||||
# Copyright 2021 Camptocamp <https://camptocamp.com>
|
||||
# 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 UserError
|
||||
|
||||
from odoo.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IrModel(models.Model):
|
||||
_inherit = "ir.model"
|
||||
|
||||
def _drop_table(self):
|
||||
"""this function crashes for undefined models"""
|
||||
self = self.filtered(lambda x: x.model in self.env)
|
||||
return super()._drop_table()
|
||||
|
||||
@api.depends()
|
||||
def _inherited_models(self):
|
||||
"""this function crashes for undefined models"""
|
||||
self = self.filtered(lambda x: x.model in self.env)
|
||||
return super()._inherited_models()
|
||||
|
||||
|
||||
class IrModelFields(models.Model):
|
||||
_inherit = "ir.model.fields"
|
||||
|
||||
def _prepare_update(self):
|
||||
"""this function crashes for undefined models"""
|
||||
self = self.filtered(lambda x: x.model in self.env)
|
||||
return super()._prepare_update()
|
||||
|
||||
|
||||
class CleanupPurgeLineModel(models.TransientModel):
|
||||
_inherit = "cleanup.purge.line"
|
||||
_name = "cleanup.purge.line.model"
|
||||
_description = "Cleanup Purge Line Model"
|
||||
|
||||
wizard_id = fields.Many2one(
|
||||
"cleanup.purge.wizard.model", "Purge Wizard", readonly=True
|
||||
)
|
||||
|
||||
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()
|
||||
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:
|
||||
_logger.error("")
|
||||
except AttributeError:
|
||||
_logger.error("")
|
||||
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"
|
||||
)
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
# Copyright 2014-2016 Therp BV <http://therp.nl>
|
||||
# Copyright 2021 Camptocamp <https://camptocamp.com>
|
||||
# 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()._module_data_uninstall(modules_to_remove)
|
||||
|
||||
|
||||
class CleanupPurgeLineModule(models.TransientModel):
|
||||
_inherit = "cleanup.purge.line"
|
||||
_name = "cleanup.purge.line.module"
|
||||
_description = "Cleanup Purge Line Module"
|
||||
|
||||
wizard_id = fields.Many2one(
|
||||
"cleanup.purge.wizard.module", "Purge Wizard", readonly=True
|
||||
)
|
||||
|
||||
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))
|
||||
installed = modules.filtered(lambda x: x.state in ("installed", "to upgrade"))
|
||||
to_remove = modules - installed
|
||||
to_remove += to_remove.downstream_dependencies()
|
||||
to_remove.write({"state": "to remove"})
|
||||
installed.button_immediate_uninstall()
|
||||
with self.env.registry.cursor() as new_cr:
|
||||
self.env(cr=new_cr)["ir.module.module"].browse(modules.ids).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 = []
|
||||
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
|
||||
res.append((0, 0, {"name": module.name}))
|
||||
|
||||
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"
|
||||
)
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
# Copyright 2017 Therp BV <http://therp.nl>
|
||||
# Copyright 2021 Camptocamp <https://camptocamp.com>
|
||||
# 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
|
||||
|
||||
REASON_DUPLICATE = "REASON_DUPLICATE"
|
||||
REASON_DEFAULT = "REASON_DEFAULT"
|
||||
REASON_DEFAULT_FALSE = "REASON_DEFAULT_FALSE"
|
||||
REASON_UNKNOWN_MODEL = "REASON_UNKNOWN_MODEL"
|
||||
|
||||
|
||||
class CleanupPurgeLineProperty(models.TransientModel):
|
||||
_inherit = "cleanup.purge.line"
|
||||
_name = "cleanup.purge.line.property"
|
||||
_description = "Cleanup Purge Line Property"
|
||||
|
||||
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"),
|
||||
]
|
||||
)
|
||||
|
||||
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": "{}@{}: {}".format(prop.name, prop.res_id, value),
|
||||
"property_id": prop.id,
|
||||
"reason": REASON_UNKNOWN_MODEL,
|
||||
}
|
||||
)
|
||||
continue
|
||||
if not value:
|
||||
result.append(
|
||||
{
|
||||
"name": "{}@{}: {}".format(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": "{}@{}: {}".format(
|
||||
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": "{}@{}: {}".format(
|
||||
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"
|
||||
)
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
# Copyright 2014-2016 Therp BV <http://therp.nl>
|
||||
# Copyright 2021 Camptocamp <https://camptocamp.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
# pylint: disable=consider-merging-classes-inherited
|
||||
from psycopg2.extensions import AsIs
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
from ..identifier_adapter import IdentifierAdapter
|
||||
|
||||
_TABLE_TYPE_SELECTION = [
|
||||
("base", "SQL Table"),
|
||||
("view", "SQL View"),
|
||||
]
|
||||
|
||||
|
||||
class CleanupPurgeLineTable(models.TransientModel):
|
||||
_inherit = "cleanup.purge.line"
|
||||
_name = "cleanup.purge.line.table"
|
||||
_description = "Cleanup Purge Line Table"
|
||||
|
||||
wizard_id = fields.Many2one(
|
||||
"cleanup.purge.wizard.table", "Purge Wizard", readonly=True
|
||||
)
|
||||
table_type = fields.Selection(selection=_TABLE_TYPE_SELECTION)
|
||||
|
||||
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]),
|
||||
),
|
||||
)
|
||||
|
||||
if line.table_type == "base":
|
||||
_sql_type = "TABLE"
|
||||
elif line.table_type == "view":
|
||||
_sql_type = "VIEW"
|
||||
self.logger.info("Dropping %s %s", _sql_type, line.name)
|
||||
self.env.cr.execute(
|
||||
"DROP %s %s", (AsIs(_sql_type), 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 and views that cannot be instantiated.
|
||||
"""
|
||||
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, table_type FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_type in ('BASE TABLE', 'VIEW')
|
||||
AND table_name NOT IN %s""",
|
||||
(tuple(known_tables),),
|
||||
)
|
||||
|
||||
res = [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{"name": row[0], "table_type": "view" if row[1] == "VIEW" else "base"},
|
||||
)
|
||||
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"
|
||||
)
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
# Copyright 2014-2016 Therp BV <http://therp.nl>
|
||||
# Copyright 2021 Camptocamp <https://camptocamp.com>
|
||||
# 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(readonly=True)
|
||||
purged = fields.Boolean(readonly=True)
|
||||
wizard_id = fields.Many2one("cleanup.purge.wizard")
|
||||
|
||||
logger = logging.getLogger("odoo.addons.database_cleanup")
|
||||
|
||||
def purge(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@api.model_create_multi
|
||||
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().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().default_get(fields_list)
|
||||
if "purge_line_ids" in fields_list:
|
||||
res["purge_line_ids"] = self.find()
|
||||
return res
|
||||
|
||||
def find(self):
|
||||
raise NotImplementedError
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
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)],
|
||||
}
|
||||
|
||||
def name_get(self):
|
||||
return [(this.id, self._description) for this in self]
|
||||
|
||||
@api.model_create_multi
|
||||
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().create(values)
|
||||
|
||||
purge_line_ids = fields.One2many("cleanup.purge.line", "wizard_id")
|
||||
Loading…
Add table
Add a link
Reference in a new issue