Initial commit: OCA Technical packages (595 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:03 +02:00
commit 2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions

View file

@ -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

View file

@ -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

View file

@ -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"
)

View file

@ -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"
)

View file

@ -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"
)

View file

@ -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"
)

View file

@ -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"
)

View file

@ -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"
)

View file

@ -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"
)

View file

@ -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"
)

View file

@ -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")