Introduce spice_stock_movements ledger for traceable stock changes
Deploy / deploy (push) Successful in 16s

Every delivery and batch now writes a signed movement entry instead of
directly incrementing/decrementing spice.quantity. The balance is always
the sum of all movements. Existing data is backfilled in the migration.
Rohstoff edit page shows the full movement history (Lagerbewegungen tab).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Malek TellissiandClaude Sonnet 4.6 committed 2026-09-12 22:34:10 +02:00
1 parent 7545aed494
commit fb19e9a579
9 files changed
+179 -43

No files matched your search

+29 -25
View File
@@ -38,58 +38,62 @@ class Batch extends Model
protected static function booted(): void
{
// Auto-calculate quantity_g before saving
static::saving(function (Batch $batch) {
if ($batch->units_produced && $batch->size_id) {
$gramsPerUnit = $batch->size?->packing?->size ?? 0;
$batch->quantity_g = $batch->units_produced * $gramsPerUnit;
$batch->quantity_g = $batch->units_produced * ($batch->size?->packing?->size ?? 0);
}
});
static::created(function (Batch $batch) {
$batch->deductSpices();
$batch->writeSpiceMovements();
});
static::updated(function (Batch $batch) {
if ($batch->wasChanged(['recipe_id', 'size_id', 'units_produced'])) {
$batch->restoreSpices(
$batch->getOriginal('recipe_id'),
$batch->getOriginal('quantity_g'),
);
$batch->deductSpices();
$batch->deleteSpiceMovements();
$batch->writeSpiceMovements();
}
});
static::deleting(function (Batch $batch) {
$batch->restoreSpices($batch->recipe_id, $batch->quantity_g);
$batch->deleteSpiceMovements();
});
}
private function deductSpices(): void
private function writeSpiceMovements(): void
{
if (! $this->quantity_g || ! $this->recipe_id) {
return;
}
$recipe = Recipe::with('spices')->find($this->recipe_id);
foreach ($recipe->spices as $spice) {
$grams = $this->quantity_g * $spice->pivot->amount_in_percent / 100;
$spice->decrement('quantity', $grams);
}
}
private function restoreSpices(int $recipeId, float $quantityG): void
{
if (! $quantityG || ! $recipeId) {
if (! $recipe) {
return;
}
$recipe = Recipe::with('spices')->find($recipeId);
foreach ($recipe->spices as $spice) {
$grams = $quantityG * $spice->pivot->amount_in_percent / 100;
$spice->increment('quantity', $grams);
$grams = $this->quantity_g * $spice->pivot->amount_in_percent / 100;
SpiceStockMovement::create([
'spice_id' => $spice->id,
'amount' => -$grams,
'source_type' => 'batch',
'source_id' => $this->id,
'description' => 'Charge ' . $this->lot_number . ' (' . $spice->pivot->amount_in_percent . '%)',
]);
$spice->recalculateQuantity();
}
}
private function deleteSpiceMovements(): void
{
$spiceIds = SpiceStockMovement::where('source_type', 'batch')
->where('source_id', $this->id)
->pluck('spice_id');
SpiceStockMovement::where('source_type', 'batch')
->where('source_id', $this->id)
->delete();
Spice::whereIn('id', $spiceIds)->get()->each->recalculateQuantity();
}
}
+10
View File
@@ -14,4 +14,14 @@ class Spice extends Model
->withPivot('amount_in_percent')
->withTimestamps();
}
public function stockMovements()
{
return $this->hasMany(SpiceStockMovement::class);
}
public function recalculateQuantity(): void
{
$this->update(['quantity' => $this->stockMovements()->sum('amount')]);
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SpiceStockMovement extends Model
{
protected $fillable = ['spice_id', 'amount', 'source_type', 'source_id', 'description'];
public function spice()
{
return $this->belongsTo(Spice::class);
}
}
+21 -1
View File
@@ -18,7 +18,6 @@ class SpiceSupplier extends Model
'supplied_at',
];
// Beziehungen
public function spice()
{
return $this->belongsTo(Spice::class);
@@ -28,4 +27,25 @@ class SpiceSupplier extends Model
{
return $this->belongsTo(Supplier::class);
}
protected static function booted(): void
{
static::created(function (SpiceSupplier $ss) {
SpiceStockMovement::create([
'spice_id' => $ss->spice_id,
'amount' => $ss->amount,
'source_type' => 'delivery',
'source_id' => $ss->id,
'description' => 'Lieferung' . ($ss->supplier ? ' von ' . $ss->supplier->name : ''),
]);
$ss->spice->recalculateQuantity();
});
static::deleted(function (SpiceSupplier $ss) {
SpiceStockMovement::where('source_type', 'delivery')
->where('source_id', $ss->id)
->delete();
$ss->spice->recalculateQuantity();
});
}
}