gruene-seele/app/Models/Ingredient.php
Kevin Adametz 35b9076cbc Warenwirtschaft: AP-31 Löschen mit Soft-Delete + Abhängigkeitsprüfung
Anforderung 15.07.2026: Nahezu alle WaWi-Einträge sind jetzt löschbar
(Soft-Delete, deleted_at), mit Abhängigkeitsprüfung und Bestätigungs-
warnung vor jeder Löschung.

- Migration: deleted_at auf 14 WaWi-Tabellen (Stammdaten, ingredients,
  stock_entries, stock_disposals, productions); SoftDeletes-Trait in
  den Models (suppliers/packaging_items hatten es bereits).
- Neuer App\Services\DeletionGuard: zentrale Abhängigkeitsprüfung je
  Entität — referenzierte Einträge sind nicht löschbar, Fehlermeldung
  nennt die Verwendungen (behebt auch Audit M-4: kein 500er/stiller
  Cascade-Verlust mehr beim Löschen verwendeter Stammdaten).
- SettingCrudController::destroy() prüft Blocker; Supplier-,
  PackagingItem-, Ingredient-destroy ebenso (Ingredient: vollständige
  Prüfung inkl. Produktion/Ausschuss, Route GET→DELETE, Audit M-6).
- Wareneingänge: auch gebuchte Eingänge löschbar, sofern kein
  Produktions-Verbrauch/Ausschuss darauf gebucht ist.
- Ausschuss: löschbar (Menge steht dem Bestand wieder zur Verfügung).
- Produktionen: löschbar (admin >= 7); Produktbestand wird per
  Gegenbuchung zurückgeführt (reverseProductionStock, append-only,
  withTrashed), Rohstoffverbrauch zählt nicht mehr (Bestandsqueries
  schließen gelöschte Produktionen aus).
- lastNetPricePerSupplier: DB::query statt Model-Query (Global Scope
  auf Sub-Select-Alias).
- Debug-Log-Reste im PackagingItemController entfernt (Audit N-16).
- Views: Lösch-Buttons mit deutschen Bestätigungswarnungen (Ausschuss,
  Produktionen, gebuchte Wareneingänge, INCI als DELETE-Formular).

Tests: neuer InventoryDeletionTest (10 Tests), StorageSettingsTest an
neue Löschregel angepasst. Volle Suite: 214 grün, 783 Assertions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 12:31:11 +00:00

181 lines
5.5 KiB
PHP

<?php
/**
* Created by Reliese Model.
*/
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* Class Ingredient
*
* @property int $id
* @property string $name
* @property string $trans_name
* @property string $inci
* @property string $trans_inci
* @property string $effect
* @property string $trans_effect
* @property bool $active
* @property int $pos
* @property Carbon $created_at
* @property Carbon $updated_at
* @property Collection|Product[] $products
* @property-read Collection|ProductIngredient[] $product_ingredients
* @property-read int|null $product_ingredients_count
* @property-read int|null $products_count
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Ingredient newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Ingredient newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Ingredient query()
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Ingredient whereActive($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Ingredient whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Ingredient whereEffect($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Ingredient whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Ingredient whereInci($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Ingredient whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Ingredient wherePos($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Ingredient whereTransEffect($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Ingredient whereTransInci($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Ingredient whereTransName($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Ingredient whereUpdatedAt($value)
*
* @mixin \Eloquent
*/
class Ingredient extends Model
{
use SoftDeletes;
protected $table = 'ingredients';
protected $casts = [
'active' => 'bool',
'exclude_from_stock' => 'bool',
'pos' => 'int',
'default_factor' => 'decimal:2',
'min_stock_alert' => 'decimal:2',
'delivery_time_days' => 'integer',
];
protected $fillable = [
'name',
'trans_name',
'alt_name',
'inci',
'trans_inci',
'effect',
'trans_effect',
'active',
'pos',
'default_factor',
'min_stock_alert',
'exclude_from_stock',
'material_quality_id',
'tax_rate_id',
'delivery_time',
'delivery_time_days',
'storage_room_id',
'storage_shelf_id',
'storage_slot_id',
];
/**
* @return BelongsTo<MaterialQuality, $this>
*/
public function materialQuality(): BelongsTo
{
return $this->belongsTo(MaterialQuality::class);
}
/**
* @return BelongsTo<TaxRate, $this>
*/
public function taxRate(): BelongsTo
{
return $this->belongsTo(TaxRate::class);
}
/**
* @return BelongsToMany<Supplier, $this>
*/
public function suppliers(): BelongsToMany
{
return $this->belongsToMany(Supplier::class, 'ingredient_supplier')
->withPivot(['preferred', 'supplier_sku', 'url'])
->withTimestamps();
}
public function products()
{
return $this->belongsToMany(Product::class, 'product_ingredients')
->withPivot('id', 'pos', 'gram', 'factor')
->withTimestamps()
->orderByPivot('pos');
}
public function product_ingredients()
{
return $this->hasMany(ProductIngredient::class, 'ingredient_id');
}
/**
* @return BelongsTo<StorageRoom, $this>
*/
public function storageRoom(): BelongsTo
{
return $this->belongsTo(StorageRoom::class);
}
/**
* @return BelongsTo<StorageShelf, $this>
*/
public function storageShelf(): BelongsTo
{
return $this->belongsTo(StorageShelf::class);
}
/**
* @return BelongsTo<StorageSlot, $this>
*/
public function storageSlot(): BelongsTo
{
return $this->belongsTo(StorageSlot::class);
}
/**
* Lagerort-Anzeige im Schema „Raum 1 | Regal 3 | A" (nur gepflegte Teile).
*/
public function storageLabel(): ?string
{
$parts = array_filter([
$this->storageRoom?->label,
$this->storageShelf?->label,
$this->storageSlot?->label,
]);
return $parts === [] ? null : implode(' | ', $parts);
}
/**
* Nur INCI, die in der Hersteller-Rezeptur mindestens eines aktiven Produkts vorkommen
* (fachlich „aktiv" laut Kunde — nicht zu verwechseln mit `ingredients.active`).
*
* @param Builder<Ingredient> $query
* @return Builder<Ingredient>
*/
public function scopeUsedInActiveRecipes($query)
{
return $query->whereHas('product_ingredients', function ($q) {
$q->where('recipe_type', 'manufacturer')
->whereHas('product', fn ($p) => $p->where('active', true));
});
}
}