Warenwirtschaft: N-01/N-03/N-05 — Entwurfsmenge ohne Charge, Snapshot-Signatur je Produkt, Alt-Ausschuss in Chargenverfügbarkeit
- N-01: Ist-Menge ohne gewählte Charge bleibt im Entwurf erhalten (Filter in Request und Service, formBatchKey liefert '' für chargenlose Zeilen). - N-03: outputSignature summiert Stückzahlen je Produkt — ein Chargensplit ersetzt den historischen Verpackungs-Snapshot nicht mehr. - N-05: Ausschuss ohne Chargenbezug (Altbestand) wird für die Verfügbarkeit je Standort FEFO auf die Chargen angerechnet (applyUnboundDisposals, lesend); Chargen-Ausschussprüfung nutzt dieselbe Verfügbarkeit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
520d4e2deb
commit
313c6277ce
8 changed files with 138 additions and 23 deletions
|
|
@ -343,12 +343,16 @@ class InventoryService
|
|||
|
||||
$consumed = $this->productionService->consumedByStockEntry($entries->pluck('id')->all());
|
||||
|
||||
return $entries->map(function (StockEntry $entry) use ($consumed) {
|
||||
$entries = $entries->map(function (StockEntry $entry) use ($consumed) {
|
||||
$received = $entry->received_quantity !== null ? (float) $entry->received_quantity : 0.0;
|
||||
$entry->setAttribute('remaining_quantity', round($received - ($consumed[(int) $entry->id] ?? 0.0), 2));
|
||||
|
||||
return $entry;
|
||||
})->filter(fn (StockEntry $entry) => (float) $entry->getAttribute('remaining_quantity') > 0.0)
|
||||
});
|
||||
|
||||
// N-05: Alt-Ausschuss ohne Chargenbezug je Standort FEFO anrechnen (nur Anzeige/Verfügbarkeit).
|
||||
return $this->productionService->applyUnboundDisposals($entries, $ingredientId)
|
||||
->filter(fn (StockEntry $entry) => (float) $entry->getAttribute('remaining_quantity') > 0.0)
|
||||
->values();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -350,13 +350,15 @@ class ProductionService
|
|||
*/
|
||||
private function outputSignature(Production $production): array
|
||||
{
|
||||
$signature = $production->outputs()
|
||||
->get(['product_id', 'quantity'])
|
||||
->map(fn ($o) => (int) $o->product_id.':'.(int) $o->quantity)
|
||||
->all();
|
||||
sort($signature);
|
||||
// N-03 (Nachprüfung 08.09.2026): Stückzahlen je Produkt summieren — ein reiner
|
||||
// Chargensplit (6 + 4 statt 10) ist keine Mengenänderung.
|
||||
$totals = [];
|
||||
foreach ($production->outputs()->get(['product_id', 'quantity']) as $o) {
|
||||
$totals[(int) $o->product_id] = ($totals[(int) $o->product_id] ?? 0) + (int) $o->quantity;
|
||||
}
|
||||
ksort($totals);
|
||||
|
||||
return $signature;
|
||||
return array_map(fn ($pid, $qty) => $pid.':'.$qty, array_keys($totals), $totals);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -537,17 +539,65 @@ class ProductionService
|
|||
|
||||
$consumed = $this->consumedByStockEntry($entries->pluck('id')->all(), $excludeProductionId);
|
||||
|
||||
return $entries
|
||||
->map(function (StockEntry $entry) use ($consumed) {
|
||||
$received = $entry->received_quantity !== null ? (float) $entry->received_quantity : 0.0;
|
||||
$entry->setAttribute('remaining_quantity', round($received - ($consumed[(int) $entry->id] ?? 0.0), 2));
|
||||
$entries = $entries->map(function (StockEntry $entry) use ($consumed) {
|
||||
$received = $entry->received_quantity !== null ? (float) $entry->received_quantity : 0.0;
|
||||
$entry->setAttribute('remaining_quantity', round($received - ($consumed[(int) $entry->id] ?? 0.0), 2));
|
||||
|
||||
return $entry;
|
||||
})
|
||||
return $entry;
|
||||
});
|
||||
|
||||
return $this->applyUnboundDisposals($entries, $ingredientId)
|
||||
->filter(fn (StockEntry $entry) => (float) $entry->getAttribute('remaining_quantity') > 0.0)
|
||||
->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* N-05 (Nachprüfung 08.09.2026): Ausschuss **ohne** Chargenbezug (Altbestand vor F-09)
|
||||
* mindert Gesamt- und Standortbestand, aber keine einzelne Charge. Für die
|
||||
* Verfügbarkeit wird er hier je Standort FEFO auf die Chargen angerechnet — dieselbe
|
||||
* Regel, nach der neuer ungebundener Ausschuss gebucht wird. Es wird nichts
|
||||
* geschrieben; die Altbuchungen bleiben unverändert.
|
||||
*
|
||||
* @param Collection<int, StockEntry> $entries FEFO-sortiert, mit Attribut remaining_quantity
|
||||
* @return Collection<int, StockEntry>
|
||||
*/
|
||||
public function applyUnboundDisposals(Collection $entries, int $ingredientId): Collection
|
||||
{
|
||||
if ($entries->isEmpty()) {
|
||||
return $entries;
|
||||
}
|
||||
|
||||
$unbound = StockDisposal::query()
|
||||
->where('disposal_type', 'ingredient')
|
||||
->where('ingredient_id', $ingredientId)
|
||||
->whereNull('stock_entry_id')
|
||||
->whereIn('location_id', $entries->pluck('location_id')->unique()->all())
|
||||
->selectRaw('location_id, SUM(quantity) as qty')
|
||||
->groupBy('location_id')
|
||||
->pluck('qty', 'location_id');
|
||||
|
||||
foreach ($unbound as $locationId => $qty) {
|
||||
$needed = round((float) $qty, 2);
|
||||
foreach ($entries as $entry) {
|
||||
if ($needed <= 0) {
|
||||
break;
|
||||
}
|
||||
if ((int) $entry->location_id !== (int) $locationId) {
|
||||
continue;
|
||||
}
|
||||
$remaining = (float) $entry->getAttribute('remaining_quantity');
|
||||
if ($remaining <= 0) {
|
||||
continue;
|
||||
}
|
||||
$take = round(min($remaining, $needed), 2);
|
||||
$entry->setAttribute('remaining_quantity', round($remaining - $take, 2));
|
||||
$needed = round($needed - $take, 2);
|
||||
}
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
public function stockEntryLabel(StockEntry $entry): string
|
||||
{
|
||||
$parts = [];
|
||||
|
|
@ -788,7 +838,10 @@ class ProductionService
|
|||
{
|
||||
return array_values(array_filter($ingredientLines, function (array $line): bool {
|
||||
return (int) ($line['ingredient_id'] ?? 0) > 0
|
||||
&& ((int) ($line['stock_entry_id'] ?? 0) > 0 || trim((string) ($line['batch_key'] ?? '')) !== '');
|
||||
&& ((int) ($line['stock_entry_id'] ?? 0) > 0
|
||||
|| trim((string) ($line['batch_key'] ?? '')) !== ''
|
||||
// N-01: Ist-Menge ohne Charge ebenfalls sichern.
|
||||
|| $this->parseQuantity($line['quantity_used'] ?? null) > 0);
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue