Warenwirtschaft: F-10 — Versandabschluss nur mit Zahlungsfreigabe

Board-Statuswechsel auf „versendet“ prüft serverseitig isReadyToShip()
(Rechnung/API weiterhin erlaubt); Button im Board ohne Freigabe deaktiviert.
Konfektionieren bleibt vor Zahlungseingang möglich.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-08 11:04:07 +00:00
parent db27f040e6
commit f93b16c8f7
3 changed files with 47 additions and 1 deletions

View file

@ -92,6 +92,15 @@ class OrderFulfillmentController extends Controller
return redirect()->route('admin.inventory.orders.index');
}
// Prüfung 08.09.2026 F-10: „versendet“ (Bestandsabzug) nur für versandfertige
// Bestellungen — Zahlung eingegangen bzw. Rechnung/API (Kundenentscheid 16.07.2026,
// isReadyToShip). Konfektionieren bleibt auch vor Zahlungseingang möglich.
if ((int) $request->input('shipped') === 2 && ! $order->isReadyToShip()) {
session()->flash('alert-error', __('Bestellung #:id wartet auf Zahlung und kann noch nicht als versendet gebucht werden.', ['id' => $id]));
return redirect()->route('admin.inventory.orders.index', array_filter(['filter' => $request->input('back_filter')]));
}
app(OrderStatusService::class)->setShipped($id, (int) $request->input('shipped'), auth()->id(), lockStatus: true);
session()->flash('alert-success', (int) $request->input('shipped') === 2

View file

@ -227,7 +227,12 @@
</button>
</form>
@endif
@if (in_array((int) $order->shipped, [0, 1], true))
@if (in_array((int) $order->shipped, [0, 1], true) && ! $readyToShip)
{{-- F-10: ohne Zahlungsfreigabe kein Versandabschluss (Server prüft ebenfalls) --}}
<button type="button" class="btn btn-sm btn-primary" disabled title="{{ __('Wartet auf Zahlung — Versand erst nach Zahlungseingang') }}">
<span class="fas fa-shipping-fast mr-1"></span><span class="of-btn-text">{{ __('Versendet') }}</span>
</button>
@elseif (in_array((int) $order->shipped, [0, 1], true))
<form action="{{ route('admin.inventory.orders.status', $order->id) }}" method="post" class="d-inline"
onsubmit="return confirm(@json(__('Bestellung als versendet buchen? Der Produktbestand wird abgezogen.')));">
@csrf

View file

@ -423,3 +423,35 @@ test('api-bestellung bekommt keine rechnung im system', function () {
expect($order->fresh()->invoice)->toBeNull()
->and(session('alert-error'))->toContain('API-Bestellung');
});
test('F-10: ohne zahlungsfreigabe kein versandabschluss ueber das board, konfektionieren bleibt moeglich', function () {
$product = ofMakeProduct('OF F10');
$order = ofMakeOrder([['product' => $product, 'qty' => 1]], ['txaction' => 'failed', 'paid' => false], 'vor');
expect($order->isReadyToShip())->toBeFalse();
$staff = ofMakeUser(1);
$this->actingAs($staff)
->post(route('admin.inventory.orders.status', $order->id), ['shipped' => 2])
->assertRedirect(route('admin.inventory.orders.index'));
expect((int) $order->fresh()->shipped)->toBe(0)
->and(session('alert-error'))->toContain('wartet auf Zahlung')
->and(ProductStockMovement::query()->where('product_id', $product->id)->count())->toBe(0);
// Board zeigt den Versand-Button nur deaktiviert.
$this->actingAs($staff)->get(route('admin.inventory.orders.index'))
->assertOk()->assertSee('Wartet auf Zahlung — Versand erst nach Zahlungseingang');
$this->actingAs($staff)
->post(route('admin.inventory.orders.status', $order->id), ['shipped' => 1])
->assertRedirect();
expect((int) $order->fresh()->shipped)->toBe(1);
// Nach Zahlungseingang geht der Versand.
$order->forceFill(['txaction' => 'paid', 'paid' => true])->save();
$this->actingAs($staff)
->post(route('admin.inventory.orders.status', $order->id), ['shipped' => 2])
->assertRedirect();
expect((int) $order->fresh()->shipped)->toBe(2)
->and(ProductStockMovement::query()->where('product_id', $product->id)->count())->toBe(1);
});