diff --git a/muk_autovacuum/__manifest__.py b/muk_autovacuum/__manifest__.py index c6a180e..74e5c4f 100644 --- a/muk_autovacuum/__manifest__.py +++ b/muk_autovacuum/__manifest__.py @@ -20,7 +20,7 @@ { "name": "MuK Autovacuum", "summary": """Configure automatic garbage collection""", - "version": "11.0.2.0.0", + "version": "11.0.2.1.0", "category": "Extra Tools", "license": "AGPL-3", "website": "https://www.mukit.at", diff --git a/muk_autovacuum/doc/changelog.rst b/muk_autovacuum/doc/changelog.rst index 243277c..b0e796f 100644 --- a/muk_autovacuum/doc/changelog.rst +++ b/muk_autovacuum/doc/changelog.rst @@ -1,6 +1,11 @@ `2.0.0` ------- +- Added Python Expressions + +`2.0.0` +------- + - Migrated to Python 3 `1.1.0` diff --git a/muk_autovacuum/models/ir_autovacuum.py b/muk_autovacuum/models/ir_autovacuum.py index 297713f..ecda219 100644 --- a/muk_autovacuum/models/ir_autovacuum.py +++ b/muk_autovacuum/models/ir_autovacuum.py @@ -42,48 +42,54 @@ class AutoVacuum(models.AbstractModel): _inherit = 'ir.autovacuum' + @api.model + def _eval_context(self): + return { + 'datetime': datetime, + 'dateutil': dateutil, + 'time': time, + 'uid': self.env.uid, + 'user': self.env.user} + @api.model def power_on(self, *args, **kwargs): res = super(AutoVacuum, self).power_on(*args, **kwargs) rules = self.env['muk_autovacuum.rules'].sudo().search([], order='sequence asc') for rule in rules: - model = self.env[rule.model.model].sudo() - records = self.env[rule.model.model] - if rule.state == 'time': - computed_time = datetime.datetime.utcnow() - _types[rule.time_type](rule.time) - domain = [(rule.time_field.name, '<', computed_time.strftime(DEFAULT_SERVER_DATETIME_FORMAT))] - if rule.protect_starred and "starred" in rule.model.field_id.mapped("name"): - domain.append(('starred', '=', False)) - if rule.only_inactive and "active" in rule.model.field_id.mapped("name"): - domain.append(('active', '=', False)) - _logger.info(_("GC domain: %s"), domain) - records = model.with_context({'active_test': False}).search(domain) - elif rule.state == 'size': - size = rule.size if rule.size_type == 'fixed' else rule.size_parameter_value - count = model.with_context({'active_test': False}).search([], count=True) - if count > size: - limit = count - size - _logger.info(_("GC domain: [] order: %s limit: %s"), rule.size_order, limit) - records = model.with_context({'active_test': False}).search([], order=rule.size_order, limit=limit) - elif rule.state == 'domain': - _logger.info(_("GC domain: %s"), rule.domain) - context = { - 'datetime': datetime, - 'dateutil': dateutil, - 'time': time, - 'uid': self.env.uid, - 'user': self.env.user} - domain = safe_eval(rule.domain, context) - records = model.with_context({'active_test': False}).search(domain) - if rule.only_attachments: - attachments = self.env['ir.attachment'].sudo().search([ - ('res_model', '=', rule.model.model), - ('res_id', 'in', records.mapped('id'))]) - count = len(attachments) - attachments.unlink() - _logger.info(_("GC'd %s attachments from %s entries"), count, rule.model.model) - else: - count = len(records) - records.unlink() - _logger.info(_("GC'd %s %s records"), count, rule.model.model) + if rule.state in ['time', 'size', 'domain']: + model = self.env[rule.model.model].sudo() + records = self.env[rule.model.model] + if rule.state == 'time': + computed_time = datetime.datetime.utcnow() - _types[rule.time_type](rule.time) + domain = [(rule.time_field.name, '<', computed_time.strftime(DEFAULT_SERVER_DATETIME_FORMAT))] + if rule.protect_starred and "starred" in rule.model.field_id.mapped("name"): + domain.append(('starred', '=', False)) + if rule.only_inactive and "active" in rule.model.field_id.mapped("name"): + domain.append(('active', '=', False)) + _logger.info(_("GC domain: %s"), domain) + records = model.with_context({'active_test': False}).search(domain) + elif rule.state == 'size': + size = rule.size if rule.size_type == 'fixed' else rule.size_parameter_value + count = model.with_context({'active_test': False}).search([], count=True) + if count > size: + limit = count - size + _logger.info(_("GC domain: [] order: %s limit: %s"), rule.size_order, limit) + records = model.with_context({'active_test': False}).search([], order=rule.size_order, limit=limit) + elif rule.state == 'domain': + _logger.info(_("GC domain: %s"), rule.domain) + domain = safe_eval(rule.domain, self._eval_context()) + records = model.with_context({'active_test': False}).search(domain) + if rule.only_attachments: + attachments = self.env['ir.attachment'].sudo().search([ + ('res_model', '=', rule.model.model), + ('res_id', 'in', records.mapped('id'))]) + count = len(attachments) + attachments.unlink() + _logger.info(_("GC'd %s attachments from %s entries"), count, rule.model.model) + else: + count = len(records) + records.unlink() + _logger.info(_("GC'd %s %s records"), count, rule.model.model) + elif rule.state == 'code': + safe_eval(rule.code.strip(), rule._get_eval_context(), mode="exec") return res \ No newline at end of file diff --git a/muk_autovacuum/models/rules.py b/muk_autovacuum/models/rules.py index 2a45cca..b1f93e1 100644 --- a/muk_autovacuum/models/rules.py +++ b/muk_autovacuum/models/rules.py @@ -17,11 +17,19 @@ # ################################################################################### +import time import logging +import datetime +import dateutil + +from pytz import timezone from odoo import _ from odoo import models, api, fields -from odoo.exceptions import ValidationError +from odoo.exceptions import ValidationError, Warning +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT +from odoo.tools.safe_eval import safe_eval, test_python_expr _logger = logging.getLogger(__name__) @@ -58,7 +66,8 @@ class AutoVacuumRules(models.Model): selection=[ ('time', 'Time Based'), ('size', 'Size Based'), - ('domain', 'Domain Based')], + ('domain', 'Domain Based'), + ('code', 'Code Based')], string='Rule Type', default='time', required=True) @@ -89,7 +98,8 @@ class AutoVacuumRules(models.Model): states={ 'time': [('required', True)], 'size': [('invisible', True)], - 'domain': [('invisible', True)]}) + 'domain': [('invisible', True)], + 'code': [('invisible', True)]}) time_type = fields.Selection( selection=[ @@ -104,7 +114,8 @@ class AutoVacuumRules(models.Model): states={ 'time': [('required', True)], 'size': [('invisible', True)], - 'domain': [('invisible', True)]}) + 'domain': [('invisible', True)], + 'code': [('invisible', True)]}) time = fields.Integer( string='Time', @@ -112,7 +123,8 @@ class AutoVacuumRules(models.Model): states={ 'time': [('required', True)], 'size': [('invisible', True)], - 'domain': [('invisible', True)]}, + 'domain': [('invisible', True)], + 'code': [('invisible', True)]}, help="Delete older data than x.") size_type = fields.Selection( @@ -124,7 +136,8 @@ class AutoVacuumRules(models.Model): states={ 'time': [('invisible', True)], 'size': [('required', True)], - 'domain': [('invisible', True)]}) + 'domain': [('invisible', True)], + 'code': [('invisible', True)]}) size_parameter = fields.Many2one( comodel_name='ir.config_parameter', @@ -133,7 +146,8 @@ class AutoVacuumRules(models.Model): states={ 'time': [('invisible', True)], 'size': [('required', True)], - 'domain': [('invisible', True)]}) + 'domain': [('invisible', True)], + 'code': [('invisible', True)]}) size_parameter_value = fields.Integer( compute='_compute_size_parameter_value', @@ -141,7 +155,8 @@ class AutoVacuumRules(models.Model): states={ 'time': [('invisible', True)], 'size': [('readonly', True)], - 'domain': [('invisible', True)]}, + 'domain': [('invisible', True)], + 'code': [('invisible', True)]}, help="Delete records with am index greater than x.") size_order = fields.Char( @@ -150,7 +165,8 @@ class AutoVacuumRules(models.Model): states={ 'time': [('invisible', True)], 'size': [('required', True)], - 'domain': [('invisible', True)]}, + 'domain': [('invisible', True)], + 'code': [('invisible', True)]}, help="Order by which the index is defined.") size = fields.Integer( @@ -159,24 +175,36 @@ class AutoVacuumRules(models.Model): states={ 'time': [('invisible', True)], 'size': [('required', True)], - 'domain': [('invisible', True)]}, + 'domain': [('invisible', True)], + 'code': [('invisible', True)]}, help="Delete records with am index greater than x.") domain = fields.Char( - string='Before Update Domain', + string='Domain', states={ 'time': [('invisible', True)], 'size': [('invisible', True)], - 'domain': [('required', True)]}, + 'domain': [('required', True)], + 'code': [('invisible', True)]}, help="Delete all records which match the domain.") + code = fields.Text( + string='Code', + states={ + 'time': [('invisible', True)], + 'size': [('invisible', True)], + 'domain': [('invisible', True)] , + 'code': [('required', True)]}, + help="Code which will be executed during the clean up.") + protect_starred = fields.Boolean( string='Protect Starred', default=True, states={ 'time': [('invisible', False)], 'size': [('invisible', True)], - 'domain': [('invisible', True)]}, + 'domain': [('invisible', True)], + 'code': [('invisible', True)]}, help="Do not delete starred records.") only_inactive = fields.Boolean( @@ -185,14 +213,40 @@ class AutoVacuumRules(models.Model): states={ 'time': [('invisible', False)], 'size': [('invisible', True)], - 'domain': [('invisible', True)]}, + 'domain': [('invisible', True)], + 'code': [('invisible', True)]}, help="Only delete archived records.") only_attachments = fields.Boolean( string='Only Attachments', default=False, + states={ + 'time': [('invisible', False)], + 'size': [('invisible', False)], + 'domain': [('invisible', False)], + 'code': [('invisible', True)]}, help="Only delete record attachments.") + #---------------------------------------------------------- + # Functions + #---------------------------------------------------------- + + def _get_eval_context(self): + return { + 'env': self.env, + 'model': self.env[self.model_name], + 'uid': self.env.user.id, + 'user': self.env.user, + 'time': time, + 'datetime': datetime, + 'dateutil': dateutil, + 'timezone': timezone, + 'date_format': DEFAULT_SERVER_DATE_FORMAT, + 'datetime_format': DEFAULT_SERVER_DATETIME_FORMAT, + 'Warning': Warning, + 'logger': logging.getLogger("%s (%s)" % (__name__, self.name)), + } + #---------------------------------------------------------- # View #---------------------------------------------------------- @@ -225,15 +279,23 @@ class AutoVacuumRules(models.Model): # Create, Update, Delete #---------------------------------------------------------- + @api.constrains('code') + def _check_code(self): + for record in self.sudo().filtered('code'): + message = test_python_expr(expr=record.code.strip(), mode="exec") + if message: + raise ValidationError(message) + @api.constrains( - 'state', 'model', 'domain', + 'state', 'model', 'domain', 'code', 'time_field', 'time_type', 'time', 'size_type', 'size_parameter', 'size_order', 'size') - def validate(self): + def _validate(self): validators = { 'time': lambda rec: rec.time_field and rec.time_type and rec.time, 'size': lambda rec: rec.size_order and (rec.size_parameter or rec.size), 'domain': lambda rec: rec.domain, + 'code': lambda rec: rec.code, } for record in self: if not validators[record.state](record): diff --git a/muk_autovacuum/views/rules.xml b/muk_autovacuum/views/rules.xml index 161c181..fbba6d1 100644 --- a/muk_autovacuum/views/rules.xml +++ b/muk_autovacuum/views/rules.xml @@ -94,7 +94,7 @@ - @@ -111,10 +111,32 @@ - + + + + + +
+

Help with Python expressions

+

Various fields may use Python code or Python expressions. The following variables can be used:

+
    +
  • uid, user: User on which the rule is triggered
  • +
  • env: Odoo Environment on which the rule is triggered
  • +
  • model: Odoo Model of the record on which the rule is triggered
  • +
  • time, datetime, dateutil, timezone: useful Python libraries
  • +
  • date_format, datetime_format: server date and time formats
  • +
  • logger.info(message): Python logging framework
  • +
  • Warning: Warning Exception to use with raise
  • +
+
+
+