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
- Create a return for the validated transfer to reverse the done moves
- Filter the collection to non-done moves (and scraped-done moves) before calling cancel
- 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
- Hide/disable Cancel once every move on the transfer is done — offer Return instead
- Always filter done non-scrap moves out of collections passed to cancel()
- Treat validation and cancellation as mutually exclusive actions in UI flows
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
- You cannot split a stock move that has been set to 'Done' or
- You cannot split a draft move. It needs to be confirmed firs
- You can not unreserve a stock move that has been set to 'Don
- Only posted/cancelled journal entries can be reset to draft.
- The quantity done for the product :product doesn't respect t
AI-assisted analysis of aureuserp/aureuserp@bd7cbeeb0c (2026-08-21).
Data as JSON: /api/errors/5e6953ce82a085b2.
Report an issue: GitHub.