Warenwirtschaft: AP-27 Wareneingang gleiche Charge zusammenführen
- Chargen-Existenz-Check beim Buchen (JSON-Endpoint batch-check): Hinweis „Charge bereits im Haus" + Vorbelegung MHD/Qualität aus dem letzten Eingang - Rohstoffbestand-Detail: Chargen je batch_number summiert (InventoryService::chargeGroupsForIngredient), Einkäufe als Unterzeilen - Produktion: Charge-Dropdown je Charge mit Summen-Rest (batches im Recipe-Payload), Verbrauch wird per expandBatchLines FEFO auf die Einzel-Einkäufe der Charge verteilt; Service-API bleibt je stock_entry - Jeder Einkauf bleibt eigener stock_entry (Preis/Datum/Audit) - Hinweise-Doku + Plan V6.0 §6 fortgeschrieben Tests: StockEntryChargeMergeTest (7 neu); Suite 193 grün, 698 Assertions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e47d848d3d
commit
7a5b5e0f7b
16 changed files with 673 additions and 34 deletions
|
|
@ -53,6 +53,12 @@ class ProductionController extends Controller
|
|||
$payload = $request->validatedPayload();
|
||||
|
||||
try {
|
||||
// AP-27: Chargen-Zeilen (batch_key) FEFO auf die Einzel-Wareneingänge der Charge auflösen.
|
||||
$ingredientLines = $this->productionService->expandBatchLines(
|
||||
$payload['ingredient_lines'],
|
||||
$payload['location_id']
|
||||
);
|
||||
|
||||
$production = $this->productionService->store(
|
||||
[
|
||||
'product_id' => $payload['product_id'],
|
||||
|
|
@ -61,7 +67,7 @@ class ProductionController extends Controller
|
|||
'quantity' => $payload['quantity'],
|
||||
'notes' => $payload['notes'],
|
||||
],
|
||||
$payload['ingredient_lines'],
|
||||
$ingredientLines,
|
||||
(int) $request->user()->id
|
||||
);
|
||||
} catch (ValidationException $e) {
|
||||
|
|
@ -119,6 +125,13 @@ class ProductionController extends Controller
|
|||
$payload = $request->validatedPayload();
|
||||
|
||||
try {
|
||||
// AP-27: Beim Bearbeiten die eigene Produktion aus der Rest-Berechnung ausnehmen.
|
||||
$ingredientLines = $this->productionService->expandBatchLines(
|
||||
$payload['ingredient_lines'],
|
||||
$payload['location_id'],
|
||||
$production->id
|
||||
);
|
||||
|
||||
$production = $this->productionService->updateProduction(
|
||||
$production,
|
||||
[
|
||||
|
|
@ -128,7 +141,7 @@ class ProductionController extends Controller
|
|||
'quantity' => $payload['quantity'],
|
||||
'notes' => $payload['notes'],
|
||||
],
|
||||
$payload['ingredient_lines'],
|
||||
$ingredientLines,
|
||||
(int) $request->user()->id
|
||||
);
|
||||
} catch (ValidationException $e) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ namespace App\Http\Controllers\Admin\Inventory;
|
|||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Ingredient;
|
||||
use App\Models\Location;
|
||||
use App\Models\StockEntry;
|
||||
use App\Models\TaxRate;
|
||||
use App\Repositories\StockEntryRepository;
|
||||
use App\Services\InventoryService;
|
||||
|
|
@ -76,8 +75,9 @@ class RawMaterialStockController extends Controller
|
|||
'products' => fn ($q) => $q->where('active', true),
|
||||
]);
|
||||
|
||||
$charges = $this->inventoryService->chargesWithRemainingForIngredient($ingredient->id);
|
||||
$remaining = round((float) $charges->sum(fn (StockEntry $entry) => (float) $entry->getAttribute('remaining_quantity')), 2);
|
||||
// AP-27: Chargen je batch_number zusammengeführt (jeder Einkauf bleibt eigener stock_entry).
|
||||
$chargeGroups = $this->inventoryService->chargeGroupsForIngredient($ingredient->id);
|
||||
$remaining = round((float) $chargeGroups->sum('remaining'), 2);
|
||||
|
||||
$remainingByLocation = $this->inventoryService->remainingByLocationForIngredient($ingredient->id);
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ class RawMaterialStockController extends Controller
|
|||
return view('admin.inventory.raw-material-stock.show', [
|
||||
'ingredient' => $ingredient,
|
||||
'productStock' => $productStock,
|
||||
'charges' => $charges,
|
||||
'chargeGroups' => $chargeGroups,
|
||||
'remaining' => round($remaining, 2),
|
||||
'remainingByLocation' => $remainingByLocation,
|
||||
'locations' => Location::query()->where('active', true)->orderBy('name')->get(),
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use App\Models\StockEntry;
|
|||
use App\Models\Supplier;
|
||||
use App\Models\TaxRate;
|
||||
use App\Repositories\StockEntryRepository;
|
||||
use App\Services\InventoryService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
|
@ -201,6 +202,51 @@ class StockEntryController extends Controller
|
|||
return redirect()->route('admin.inventory.stock-entries.show', $stockEntry->fresh());
|
||||
}
|
||||
|
||||
/**
|
||||
* AP-27: Existenz-Check beim Buchen des Wareneingangs — ist die Charge
|
||||
* (batch_number + ingredient_id) schon im Haus? Liefert Hinweis-Daten
|
||||
* und Vorbelegungswerte (MHD/Qualität) aus dem letzten Eingang der Charge.
|
||||
*/
|
||||
public function batchCheck(Request $request, InventoryService $inventoryService): JsonResponse
|
||||
{
|
||||
$ingredientId = (int) $request->query('ingredient_id');
|
||||
$batchNumber = trim((string) $request->query('batch_number', ''));
|
||||
$excludeId = (int) $request->query('exclude', 0);
|
||||
|
||||
if ($ingredientId < 1 || $batchNumber === '') {
|
||||
return response()->json(['exists' => false]);
|
||||
}
|
||||
|
||||
$entries = StockEntry::query()
|
||||
->with('location')
|
||||
->where('entry_type', 'ingredient')
|
||||
->where('status', 'received')
|
||||
->where('ingredient_id', $ingredientId)
|
||||
->where('batch_number', $batchNumber)
|
||||
->when($excludeId > 0, fn ($q) => $q->where('id', '!=', $excludeId))
|
||||
->orderByDesc('received_at')
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
|
||||
if ($entries->isEmpty()) {
|
||||
return response()->json(['exists' => false]);
|
||||
}
|
||||
|
||||
$latest = $entries->first();
|
||||
$remaining = $inventoryService->chargesWithRemainingForIngredient($ingredientId)
|
||||
->filter(fn (StockEntry $e) => trim((string) $e->batch_number) === $batchNumber && $e->id !== $excludeId)
|
||||
->sum(fn (StockEntry $e) => (float) $e->getAttribute('remaining_quantity'));
|
||||
|
||||
return response()->json([
|
||||
'exists' => true,
|
||||
'purchases' => $entries->count(),
|
||||
'remaining' => round((float) $remaining, 2),
|
||||
'best_before' => $latest->best_before?->format('d.m.Y'),
|
||||
'location' => $latest->location?->name,
|
||||
'quality_id' => $latest->quality_id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function searchIngredients(Request $request): JsonResponse
|
||||
{
|
||||
$term = trim((string) $request->query('q', ''));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue