odoo/odoo · error · UserError

Only posted/cancelled journal entries can be reset to draft.

Error message

Only posted/cancelled journal entries can be reset to draft.

What it means

UserError raised at the start of button_draft (Reset to Draft): only moves in 'cancel' or 'posted' state may be reset to draft. Selecting anything else (typically already-draft entries, or the selection containing one) aborts the batch before _check_draftable runs.

Source

Thrown at addons/account/models/account_move.py:6238

        :param partial_id: The id of an existing partial reconciled with the current invoice.
        '''
        self.ensure_one()
        partial = self.env['account.partial.reconcile'].browse(partial_id)
        return partial.unlink()

    def button_set_checked(self):
        self.set_moves_checked()

    def check_selected_moves(self):
        self.env['account.move'].browse(self.env.context.get('active_ids', [])).set_moves_checked()

    def set_moves_checked(self, is_checked=True):
        for move in self.filtered(lambda m: m.state == 'posted'):
            move.checked = is_checked

    def button_draft(self):
        if any(move.state not in ('cancel', 'posted') for move in self):
            raise UserError(_("Only posted/cancelled journal entries can be reset to draft."))
        if any(move.need_cancel_request for move in self):
            raise UserError(_("You can't reset to draft those journal entries. You need to request a cancellation instead."))

        self._check_draftable()
        # We delete next auto_post move if draft
        self._unlink_next_draft_auto_post_moves()
        # We remove all the analytics entries for this journal
        self.line_ids.analytic_line_ids.with_context(skip_analytic_sync=True).unlink()
        self.state = 'draft'
        self.sending_data = False

        self._detach_attachments()
        return True

    def _get_fields_to_detach(self):
        """"
        Returns a list of field names to detach on resetting an invoice to draft. Can be overridden by other modules to
        add more fields.

View on GitHub (pinned to 1e661df964)

Solutions

  1. Restrict the selection to posted or cancelled entries only, then reset.
  2. In code, pre-filter: moves.filtered(lambda m: m.state in ('posted', 'cancel')).button_draft().
  3. Make list-view buttons conditional on state via dynamic domains.

Example fix

# before
moves.button_draft()  # a draft entry in the set

# after
moves.filtered(lambda m: m.state in ('posted', 'cancel')).button_draft()
Defensive patterns

Strategy: validation

Validate before calling

resettable = moves.filtered(lambda m: m.state in ('posted', 'cancel'))
if resettable:
    resettable.button_draft()

Prevention

When it happens

Trigger: Calling button_draft() on a recordset where any move.state is not in ('cancel', 'posted') — most often a draft move selected together with posted ones in a list view bulk action.

Common situations: Bulk Reset to Draft over a mixed selection; users double-clicking Reset to Draft (second click sees state 'draft'); automation calling button_draft on all moves of a partner including fresh drafts.

Related errors


AI-assisted analysis of odoo/odoo@1e661df964 (2026-08-15). Data as JSON: /api/errors/0bb4f13189f769c6. Report an issue: GitHub.