Warenwirtschaft: F-09 — Ausschuss ohne Charge standort- und chargenkonsistent
Ohne Charge wird gegen den Restbestand des gewählten Lagerorts geprüft und die Menge FEFO auf die dort verfügbaren Chargen verteilt (je Charge gebucht, in einer Transaktion) — wie der Produktionsverbrauch. Gesamt-, Standort- und Chargenbestand stimmen damit überein. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
2796b1103e
commit
db27f040e6
3 changed files with 107 additions and 5 deletions
|
|
@ -10,9 +10,11 @@ use App\Models\Location;
|
|||
use App\Models\StockDisposal;
|
||||
use App\Models\StockEntry;
|
||||
use App\Services\InventoryService;
|
||||
use App\Services\ProductionService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class StockDisposalController extends Controller
|
||||
|
|
@ -67,18 +69,61 @@ class StockDisposalController extends Controller
|
|||
]);
|
||||
}
|
||||
|
||||
public function store(StoreStockDisposalRequest $request): RedirectResponse
|
||||
public function store(StoreStockDisposalRequest $request, ProductionService $productionService): RedirectResponse
|
||||
{
|
||||
$data = $request->validatedPayload();
|
||||
$data['user_id'] = (int) $request->user()->id;
|
||||
|
||||
StockDisposal::query()->create($data);
|
||||
DB::transaction(function () use ($data, $productionService): void {
|
||||
foreach ($this->splitAcrossBatches($data, $productionService) as $row) {
|
||||
StockDisposal::query()->create($row);
|
||||
}
|
||||
});
|
||||
|
||||
\Session::flash('alert-save', '1');
|
||||
|
||||
return redirect()->route('admin.inventory.stock-disposals.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüfung 08.09.2026 F-09: Rohstoff-Ausschuss ohne bestimmte Charge wird — wie der
|
||||
* Produktionsverbrauch (AP-27, FEFO) — auf die am Lagerort verfügbaren Chargen
|
||||
* verteilt und je Charge gebucht. Sonst sinkt nur der Gesamtbestand, während die
|
||||
* Chargen in Produktion/Ausschuss weiter als voll verfügbar angeboten würden.
|
||||
* Ohne verfügbare Charge (Altbestand ohne Chargenbezug) bleibt die Buchung ungebunden.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function splitAcrossBatches(array $data, ProductionService $productionService): array
|
||||
{
|
||||
if (($data['disposal_type'] ?? null) !== 'ingredient' || ! empty($data['stock_entry_id'])) {
|
||||
return [$data];
|
||||
}
|
||||
|
||||
$entries = $productionService->availableStockEntriesForIngredient((int) $data['ingredient_id'], (int) $data['location_id']);
|
||||
if ($entries->isEmpty()) {
|
||||
return [$data];
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
$needed = round((float) $data['quantity'], 2);
|
||||
$lastIndex = $entries->count() - 1;
|
||||
foreach ($entries as $index => $entry) {
|
||||
$capacity = (float) $entry->getAttribute('remaining_quantity');
|
||||
$take = $index === $lastIndex ? $needed : round(min($needed, $capacity), 2);
|
||||
if ($take > 0) {
|
||||
$rows[] = array_merge($data, ['stock_entry_id' => (int) $entry->id, 'quantity' => $take]);
|
||||
}
|
||||
$needed = round($needed - $take, 2);
|
||||
if ($needed <= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Anforderung 15.07.2026: Ausschuss-Buchungen löschbar (Soft-Delete).
|
||||
* Die Bestandsaggregationen schließen gelöschte Buchungen automatisch
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue