{"record":{"id":"27c75799b0fa3e82","repo":"odoo/odoo","slug":"you-cannot-remove-parts-of-a-restricted-audit-trai","errorCode":null,"errorMessage":"You cannot remove parts of a restricted audit trail. Archive the record instead.","messagePattern":"You cannot remove parts of a restricted audit trail\\. Archive the record instead\\.","errorType":"exception","errorClass":"UserError","httpStatus":null,"severity":"error","filePath":"addons/account/models/mail_message.py","lineNumber":188,"sourceCode":"                query = self.env[model]._search(value)\n            else:\n                query = value\n            res_id_domain = [('res_id', 'in' if operator in ('any', 'any!') else 'not in', query)]\n        elif operator in ('in', 'not in'):\n            res_id_domain = [('res_id', operator, value)]\n        else:\n            return NotImplemented\n        return [('model', '=', model)] + res_id_domain\n\n    @api.ondelete(at_uninstall=False)\n    def _except_audit_log(self):\n        if self.env.context.get('bypass_audit') is bypass_token:\n            return\n        for message in self:\n            if message.account_audit_log_move_id and not message.account_audit_log_move_id.posted_before:\n                continue\n            if message.account_audit_log_restricted:\n                raise UserError(self.env._(\"You cannot remove parts of a restricted audit trail. Archive the record instead.\"))\n\n    def write(self, vals):\n        # We allow any whitespace modifications in the subject\n        normalized_subject = ' '.join(vals['subject'].split()) if vals.get('subject') else None\n        if (\n            vals.keys() & {'res_id', 'res_model', 'message_type', 'subtype_id'}\n            or ('subject' in vals and any(' '.join(s.subject.split()) != normalized_subject for s in self if s.subject))\n            or ('body' in vals and any(self.mapped('body')))\n        ):\n            self._except_audit_log()\n        return super().write(vals)\n","sourceCodeStart":170,"sourceCodeEnd":200,"githubUrl":"https://github.com/odoo/odoo/blob/1e661df964b1b264c9cef3ab28430d4785be3fda/addons/account/models/mail_message.py#L170-L200","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"# before\nmessages.unlink()  # UserError on restricted audit trail\n\n# after\nmessages.filtered(lambda m: not m.account_audit_log_restricted).unlink()","handlingStrategy":"validation","validationCode":"deletable = messages.filtered(\n    lambda m: not m.account_audit_log_restricted)","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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."],"tags":["odoo","accounting","audit-trail","mail","legal-compliance"],"backgroundTag":null,"analyzedSha":"1e661df964b1b264c9cef3ab28430d4785be3fda","analyzedAt":"2026-08-15T05:22:16.142Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}