Warenwirtschaft: Bestandsrechnung auf der Rohstoff-Detailseite nachvollziehbar
Neuer Klappbereich „Bestandsrechnung nachvollziehen" zeigt alle gebuchten Eingänge – auch aufgebrauchte mit Rest 0 – mit Produktionsverbrauch, Ausschuss mit Chargenbezug und dem nach FEFO zugerechneten Ausschuss ohne Chargenbezug; Summenzeile bis zum Gesamtbestand identisch zur Kopfzahl. Alt-Ausschuss ohne Chargenbezug wird einzeln mit Grund/Notiz/Bearbeiter gelistet. Anlass: Live-Fall Sonnenblumenöl (doppelte Korrektur vom 28.07.). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
79bec0ff0b
commit
761ea35047
7 changed files with 417 additions and 2 deletions
|
|
@ -365,6 +365,106 @@ class InventoryService
|
|||
->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bestandsrechnung eines Rohstoffs zum Nachvollziehen (17.09.2026): alle gebuchten
|
||||
* Eingänge — auch die mit Rest 0 — mit Produktionsverbrauch, Ausschuss mit Chargenbezug
|
||||
* und anteilig (FEFO) zugerechnetem Ausschuss ohne Chargenbezug. Die Summenzeile
|
||||
* entspricht exakt remainingByIngredient(); Verbrauch/Ausschuss, der keinem gelisteten
|
||||
* Eingang mehr zuzuordnen ist, wird als „sonstige" Position ausgewiesen.
|
||||
*
|
||||
* @return array{
|
||||
* entries: Collection<int, StockEntry>,
|
||||
* unbound_disposals: Collection<int, StockDisposal>,
|
||||
* received_total: float,
|
||||
* production_total: float,
|
||||
* production_other: float,
|
||||
* disposed_bound_total: float,
|
||||
* disposed_unbound_total: float,
|
||||
* disposed_unbound_unallocated: float,
|
||||
* disposed_other: float,
|
||||
* remaining_total: float
|
||||
* }
|
||||
*/
|
||||
public function stockBreakdownForIngredient(int $ingredientId): array
|
||||
{
|
||||
$entries = StockEntry::query()
|
||||
->with(['supplier', 'location'])
|
||||
->where('status', 'received')
|
||||
->where('entry_type', 'ingredient')
|
||||
->where('ingredient_id', $ingredientId)
|
||||
->orderByRaw('best_before is null, best_before asc')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
$ids = $entries->pluck('id')->all();
|
||||
|
||||
$productionByEntry = ProductionIngredient::query()
|
||||
->join('productions', 'productions.id', '=', 'production_ingredients.production_id')
|
||||
->whereNull('productions.deleted_at')
|
||||
->where('productions.status', '!=', Production::STATUS_DRAFT)
|
||||
->whereIn('production_ingredients.stock_entry_id', $ids)
|
||||
->selectRaw('production_ingredients.stock_entry_id as stock_entry_id, SUM(production_ingredients.quantity_used) as used')
|
||||
->groupBy('production_ingredients.stock_entry_id')
|
||||
->pluck('used', 'stock_entry_id');
|
||||
|
||||
$boundByEntry = StockDisposal::query()
|
||||
->where('disposal_type', 'ingredient')
|
||||
->whereIn('stock_entry_id', $ids)
|
||||
->selectRaw('stock_entry_id, SUM(quantity) as used')
|
||||
->groupBy('stock_entry_id')
|
||||
->pluck('used', 'stock_entry_id');
|
||||
|
||||
$entries = $entries->map(function (StockEntry $entry) use ($productionByEntry, $boundByEntry) {
|
||||
$received = $entry->received_quantity !== null ? (float) $entry->received_quantity : 0.0;
|
||||
$production = roundWawiQuantity((float) ($productionByEntry[$entry->id] ?? 0.0));
|
||||
$bound = roundWawiQuantity((float) ($boundByEntry[$entry->id] ?? 0.0));
|
||||
$entry->setAttribute('production_quantity', $production);
|
||||
$entry->setAttribute('bound_disposal_quantity', $bound);
|
||||
$entry->setAttribute('unbound_disposal_quantity', 0.0);
|
||||
$entry->setAttribute('remaining_quantity', roundWawiQuantity($received - $production - $bound));
|
||||
|
||||
return $entry;
|
||||
});
|
||||
|
||||
// Gleiche FEFO-Zurechnung wie in der Chargenansicht — nur so stimmen beide Sichten überein.
|
||||
$entries = $this->productionService->applyUnboundDisposals($entries, $ingredientId);
|
||||
|
||||
$unboundDisposals = StockDisposal::query()
|
||||
->with(['location', 'user'])
|
||||
->where('disposal_type', 'ingredient')
|
||||
->where('ingredient_id', $ingredientId)
|
||||
->whereNull('stock_entry_id')
|
||||
->orderBy('disposed_at')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
$receivedTotal = roundWawiQuantity((float) $entries->sum(fn (StockEntry $e) => (float) ($e->received_quantity ?? 0)));
|
||||
$productionByEntryTotal = roundWawiQuantity((float) $entries->sum(fn (StockEntry $e) => (float) $e->getAttribute('production_quantity')));
|
||||
$productionTotal = roundWawiQuantity((float) ProductionIngredient::query()
|
||||
->join('productions', 'productions.id', '=', 'production_ingredients.production_id')
|
||||
->whereNull('productions.deleted_at')
|
||||
->where('productions.status', '!=', Production::STATUS_DRAFT)
|
||||
->where('production_ingredients.ingredient_id', $ingredientId)
|
||||
->sum('production_ingredients.quantity_used'));
|
||||
$disposedBoundTotal = roundWawiQuantity((float) $entries->sum(fn (StockEntry $e) => (float) $e->getAttribute('bound_disposal_quantity')));
|
||||
$disposedUnboundTotal = roundWawiQuantity((float) $unboundDisposals->sum(fn (StockDisposal $d) => (float) $d->quantity));
|
||||
$disposedUnboundAllocated = roundWawiQuantity((float) $entries->sum(fn (StockEntry $e) => (float) $e->getAttribute('unbound_disposal_quantity')));
|
||||
$disposedTotal = $this->disposedByIngredient([$ingredientId])[$ingredientId] ?? 0.0;
|
||||
|
||||
return [
|
||||
'entries' => $entries,
|
||||
'unbound_disposals' => $unboundDisposals,
|
||||
'received_total' => $receivedTotal,
|
||||
'production_total' => $productionTotal,
|
||||
'production_other' => roundWawiQuantity($productionTotal - $productionByEntryTotal),
|
||||
'disposed_bound_total' => $disposedBoundTotal,
|
||||
'disposed_unbound_total' => $disposedUnboundTotal,
|
||||
'disposed_unbound_unallocated' => roundWawiQuantity($disposedUnboundTotal - $disposedUnboundAllocated),
|
||||
'disposed_other' => roundWawiQuantity($disposedTotal - $disposedBoundTotal - $disposedUnboundTotal),
|
||||
'remaining_total' => roundWawiQuantity($receivedTotal - $productionTotal - $disposedTotal),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* AP-27: Verfügbare Chargen eines Rohstoffs je Chargennummer zusammengeführt.
|
||||
* Jeder Einkauf bleibt ein eigener stock_entry (in `entries` enthalten),
|
||||
|
|
|
|||
|
|
@ -605,6 +605,8 @@ class ProductionService
|
|||
}
|
||||
$take = roundWawiQuantity(min($remaining, $needed));
|
||||
$entry->setAttribute('remaining_quantity', roundWawiQuantity($remaining - $take));
|
||||
// Bestandsrechnung (17.09.2026): anteilig zugerechneten Alt-Ausschuss am Eingang merken.
|
||||
$entry->setAttribute('unbound_disposal_quantity', roundWawiQuantity((float) ($entry->getAttribute('unbound_disposal_quantity') ?? 0.0) + $take));
|
||||
$needed = roundWawiQuantity($needed - $take);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue