{"record":{"id":"5e6953ce82a085b2","repo":"aureuserp/aureuserp","slug":"you-cannot-cancel-a-stock-move-that-has-been-set-t","errorCode":null,"errorMessage":"You cannot cancel a stock move that has been set to 'Done'. Create a return in order to reverse the moves which took place.","messagePattern":"You cannot cancel a stock move that has been set to 'Done'\\. Create a return in order to reverse the moves which took place\\.","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"plugins/webkul/inventories/src/Services/MoveCanceller.php","lineNumber":15,"sourceCode":"<?php\n\nnamespace Webkul\\Inventory\\Services;\n\nuse Illuminate\\Support\\Collection;\nuse Webkul\\Inventory\\Enums\\MoveState;\nuse Webkul\\Inventory\\Enums\\ProcureMethod;\nuse Webkul\\Inventory\\Models\\Move;\n\nclass MoveCanceller\n{\n    public function cancel(Collection $moves): bool\n    {\n        if ($moves->some(fn (Move $move) => $move->state === MoveState::DONE && ! $move->is_scraped)) {\n            throw new \\Exception(__('inventories::system.inventory-manager.cancel-move.already-done'));\n        }\n\n        $cancellable = $moves->filter(\n            fn (Move $move) => $move->state !== MoveState::CANCELED\n                && ! ($move->state === MoveState::DONE && $move->is_scraped)\n        );\n\n        $cancellable->each->update(['is_picked' => false]);\n\n        app(MoveReserver::class)->release($cancellable);\n\n        $cancellable->each->update(['state' => MoveState::CANCELED]);\n\n        foreach ($cancellable as $move) {\n            $this->propagate($move);\n        }\n\n        $cancellable->each(function (Move $move) {","sourceCodeStart":1,"sourceCodeEnd":33,"githubUrl":"https://github.com/aureuserp/aureuserp/blob/bd7cbeeb0c620985dfd9b88c56be7f3ea86be6b4/plugins/webkul/inventories/src/Services/MoveCanceller.php#L1-L33","documentation":"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.","triggerScenarios":"Calling app(MoveCanceller::class)->cancel($moves) (or the transfer cancel action) where any move in the collection has state DONE and is_scraped = false.","commonSituations":"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.","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"],"exampleFix":"// before\napp(MoveCanceller::class)->cancel($operation->moves); // throws if any done\n\n// after\n$cancellable = $operation->moves->filter(\n    fn ($m) => $m->state !== MoveState::DONE || $m->is_scraped\n);\napp(MoveCanceller::class)->cancel($cancellable);\n// done non-scrap moves are reversed via a return instead","handlingStrategy":"type-guard","validationCode":"$cancellable = $moves->filter(\n    fn ($move) => $move->state !== MoveState::DONE || $move->is_scraped\n);\nif ($cancellable->count() !== $moves->count()) {\n    // done non-scrap moves present: plan a return for those\n}\napp(MoveCanceller::class)->cancel($cancellable);","typeGuard":"function isMoveCancellable(\\Webkul\\Inventory\\Models\\Move $move): bool\n{\n    return $move->state !== MoveState::CANCELED\n        && $move->state !== MoveState::DONE\n        || ($move->state === MoveState::DONE && $move->is_scraped);\n}","tryCatchPattern":"try {\n    app(MoveCanceller::class)->cancel($moves);\n} catch (\\Exception $e) {\n    if (str_contains($e->getMessage(), 'already-done')) {\n        // split the set: cancel open moves, create a return for done ones\n    }\n}","preventionTips":["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"],"tags":["inventory","move","cancel","state-machine","return"],"backgroundTag":"invalid-state-transition","analyzedSha":"bd7cbeeb0c620985dfd9b88c56be7f3ea86be6b4","analyzedAt":"2026-08-21T06:41:00.338Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}