Warenwirtschaft: AP-21 INCI-Erweiterungen

- Lagerort-Stammdaten Raum/Regal/Fach (storage_rooms/shelves/slots) mit CRUD
  auf Einstellungen -> Allgemein (SettingCrudController-Kinder, ein gemeinsamer
  StoreStorageSegmentRequest, generische Partials setting-list-card/-segment-form)
- Ingredient: alt_name (Handelsname), Lagerort-FKs + storageLabel(),
  scopeUsedInActiveRecipes()
- INCI-Formular: Lieferanten-Repeater mit Shop-URL je Lieferant (leer = Mail),
  Alternativname, drei Lagerort-Dropdowns
- INCI-Liste: Filter "nur aktive (in Rezeptur aktiver Produkte)" + Spalte
  Alternativer Name
- Rohstoffbestand-Detail: Bestellweg pro INCI-Lieferant (Pivot-URL statt
  order_method-Fallback), Kopf zeigt Alternativname + Lagerort
- Hinweise-Doku fortgeschrieben inkl. Chargen-Format-Entscheid ttmmjj (AP-24)

Tests: StorageSettingsTest, IngredientActiveRecipeFilterTest,
IngredientOrderFieldsTest (Repeater-Format), RawMaterialStockTest erweitert
(volle Suite 170 gruen, 613 Assertions)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-06 11:36:28 +00:00
parent 8c90d95c69
commit d05df498d3
34 changed files with 1091 additions and 45 deletions

View file

@ -7,6 +7,7 @@
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;
@ -63,6 +64,7 @@ class Ingredient extends Model
protected $fillable = [
'name',
'trans_name',
'alt_name',
'inci',
'trans_inci',
'effect',
@ -75,6 +77,9 @@ class Ingredient extends Model
'tax_rate_id',
'delivery_time',
'delivery_time_days',
'storage_room_id',
'storage_shelf_id',
'storage_slot_id',
];
/**
@ -115,4 +120,57 @@ class Ingredient extends Model
{
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));
});
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace App\Models;
use Database\Factories\StorageRoomFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class StorageRoom extends StorageSegment
{
/** @use HasFactory<StorageRoomFactory> */
use HasFactory;
protected $table = 'storage_rooms';
}

View file

@ -0,0 +1,38 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
/**
* Gemeinsame Basis der Lagerort-Bausteine Raum/Regal/Fach (AP-21).
*/
abstract class StorageSegment extends Model
{
protected $fillable = [
'label',
'active',
'pos',
];
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'active' => 'boolean',
'pos' => 'integer',
];
}
/**
* @param Builder<static> $query
* @return Builder<static>
*/
public function scopeActive(Builder $query): Builder
{
return $query->where('active', true);
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace App\Models;
use Database\Factories\StorageShelfFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class StorageShelf extends StorageSegment
{
/** @use HasFactory<StorageShelfFactory> */
use HasFactory;
protected $table = 'storage_shelves';
}

View file

@ -0,0 +1,14 @@
<?php
namespace App\Models;
use Database\Factories\StorageSlotFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class StorageSlot extends StorageSegment
{
/** @use HasFactory<StorageSlotFactory> */
use HasFactory;
protected $table = 'storage_slots';
}