aureuserp/aureuserp · error · Exception

You cannot cancel a stock move that has been set to 'Done'.

Error message

You cannot cancel a stock move that has been set to 'Done'. Create a return in order to reverse the moves which took place.

What it means

MoveCanceller::cancel() refuses collections containing a move whose state is DONE and is_scraped is false — done moves already settled their quants, so cancelling would falsify stock. Scraped done moves are exempt (they may be reopened); the correct reversal for a done move is a return operation.

Source

Thrown at plugins/webkul/inventories/src/Services/MoveCanceller.php:15

<?php

namespace Webkul\Inventory\Services;

use Illuminate\Support\Collection;
use Webkul\Inventory\Enums\MoveState;
use Webkul\Inventory\Enums\ProcureMethod;
use Webkul\Inventory\Models\Move;

class MoveCanceller
{
    public function cancel(Collection $moves): bool
    {
        if ($moves->some(fn (Move $move) => $move->state === MoveState::DONE && ! $move->is_scraped)) {
            throw new \Exception(__('inventories::system.inventory-manager.cancel-move.already-done'));
        }

        $cancellable = $moves->filter(
            fn (Move $move) => $move->state !== MoveState::CANCELED
                && ! ($move->state === MoveState::DONE && $move->is_scraped)
        );

        $cancellable->each->update(['is_picked' => false]);

        app(MoveReserver::class)->release($cancellable);

        $cancellable->each->update(['state' => MoveState::CANCELED]);

        foreach ($cancellable as $move) {
            $this->propagate($move);
        }

        $cancellable->each(function (Move $move) {

View on GitHub (pinned to bd7cbeeb0c)

Solutions

  1. Create a return for the validated transfer to reverse the done moves
  2. Filter the collection to non-done moves (and scraped-done moves) before calling cancel
  3. Guard UI actions: hide/disable Cancel once every move is done

Example fix

// before
app(MoveCanceller::class)->cancel($operation->moves); // throws if any done

// after
$cancellable = $operation->moves->filter(
    fn ($m) => $m->state !== MoveState::DONE || $m->is_scraped
);
app(MoveCanceller::class)->cancel($cancellable);
// done non-scrap moves are reversed via a return instead
Defensive patterns

Strategy: type-guard

Validate before calling

$cancellable = $moves->filter(
    fn ($move) => $move->state !== MoveState::DONE || $move->is_scraped
);
if ($cancellable->count() !== $moves->count()) {
    // done non-scrap moves present: plan a return for those
}
app(MoveCanceller::class)->cancel($cancellable);

Type guard

function isMoveCancellable(\Webkul\Inventory\Models\Move $move): bool
{
    return $move->state !== MoveState::CANCELED
        && $move->state !== MoveState::DONE
        || ($move->state === MoveState::DONE && $move->is_scraped);
}

Try / catch

try {
    app(MoveCanceller::class)->cancel($moves);
} catch (\Exception $e) {
    if (str_contains($e->getMessage(), 'already-done')) {
        // split the set: cancel open moves, create a return for done ones
    }
}

Prevention

When it happens

Trigger: Calling app(MoveCanceller::class)->cancel($moves) (or the transfer cancel action) where any move in the collection has state DONE and is_scraped = false.

Common situations: User clicks Cancel on a validated transfer; automation cancels all moves of an operation after validation already ran; cancel jobs racing with the validate action.

Related errors


AI-assisted analysis of aureuserp/aureuserp@bd7cbeeb0c (2026-08-21). Data as JSON: /api/errors/5e6953ce82a085b2. Report an issue: GitHub.