odoo/odoo · error · UserError
You cannot remove parts of a restricted audit trail. Archive
Error message
You cannot remove parts of a restricted audit trail. Archive the record instead.
What it means
mail.message's audit-log guard (_except_audit_log, @api.ondelete and invoked from write) raises UserError when a message belongs to a restricted accounting audit trail (account_audit_log_restricted) and the related move was not posted before restriction. Restricted audit trails are legally immutable in several localizations, so neither deletion nor edits to body/type/links are allowed. An internal bypass exists via context key 'bypass_audit' matching a secret token, used by Odoo's own rollback flows.
Source
Thrown at addons/account/models/mail_message.py:188
query = self.env[model]._search(value)
else:
query = value
res_id_domain = [('res_id', 'in' if operator in ('any', 'any!') else 'not in', query)]
elif operator in ('in', 'not in'):
res_id_domain = [('res_id', operator, value)]
else:
return NotImplemented
return [('model', '=', model)] + res_id_domain
@api.ondelete(at_uninstall=False)
def _except_audit_log(self):
if self.env.context.get('bypass_audit') is bypass_token:
return
for message in self:
if message.account_audit_log_move_id and not message.account_audit_log_move_id.posted_before:
continue
if message.account_audit_log_restricted:
raise UserError(self.env._("You cannot remove parts of a restricted audit trail. Archive the record instead."))
def write(self, vals):
# We allow any whitespace modifications in the subject
normalized_subject = ' '.join(vals['subject'].split()) if vals.get('subject') else None
if (
vals.keys() & {'res_id', 'res_model', 'message_type', 'subtype_id'}
or ('subject' in vals and any(' '.join(s.subject.split()) != normalized_subject for s in self if s.subject))
or ('body' in vals and any(self.mapped('body')))
):
self._except_audit_log()
return super().write(vals)
View on GitHub (pinned to 1e661df964)
Solutions
- Do not delete/modify those messages; archive or exclude messages where account_audit_log_restricted is set.
- Filter before deletion: msgs.filtered(lambda m: not m.account_audit_log_restricted).
- Only if you are implementing an official unpost/rollback flow, pass the internal bypass_audit context token ( Odoo core only).
Example fix
# before messages.unlink() # UserError on restricted audit trail # after messages.filtered(lambda m: not m.account_audit_log_restricted).unlink()
Defensive patterns
Strategy: validation
Validate before calling
deletable = messages.filtered(
lambda m: not m.account_audit_log_restricted) Prevention
- Data-retention jobs must skip messages flagged account_audit_log_restricted.
- Never write to body/res_model/message_type of messages on posted/hashed moves.
When it happens
Trigger: unlink() on mail.message records tied to hashed/restricted moves, or write() touching res_id/res_model/message_type/subtype_id, body of non-empty messages, or changed subjects; e.g. cleanup of chatter, GDPR deletion scripts, or auto-vacuum hooks.
Common situations: Log/data-minimization jobs trying to purge old chatter on closed fiscal years; tests deleting messages on posted entries; modules writing to mail.message fields that trigger the guard.
Related errors
- This document is protected by a hash. Therefore, you cannot
- Can't disable restricted audit trail: forced by localization
- You cannot remove/deactivate the accounts "%s" which are set
- You cannot remove/deactivate the accounts "%s" which are set
- You cannot merge accounts.
AI-assisted analysis of odoo/odoo@1e661df964 (2026-08-15).
Data as JSON: /api/errors/27c75799b0fa3e82.
Report an issue: GitHub.