Kundenentscheid 26.08.: Rohstoffe werden in Gramm geführt, dürfen aber in ml erfasst werden; über die Dichte ergeben sich die Gramm (100 ml Propolistinktur × 0,79 = 79 g). - Neues Feld ingredients.density (g/ml, optional) inkl. Feld im INCI-Formular. - Wareneingang: Einheiten-Umschalter g/ml am Mengenfeld (nur wenn Dichte hinterlegt; Live-Hinweis mit Gramm-Umrechnung, Dichte kommt aus der Rohstoff-Suche mit). Erfassung in ml wird über die Dichte in Gramm umgerechnet und kanonisch als Gramm gespeichert — InventoryService und alle Aggregationen bleiben unverändert. purchase_unit + density_used (Snapshot) protokollieren die ml-Erfassung für Anzeige/Bearbeitung; Detailseite zeigt „790 g (= 1.000 ml)". Wareneingang-Buchung rechnet gelieferte ml ebenfalls um. - Serverseitige Validierung: ml nur mit hinterlegter Dichte. Migrationen add_density_* auf Dev-/Live-DB. 4 Tests, Suite 318 grün. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
183 lines
5.5 KiB
PHP
183 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',
|
|
'density' => 'decimal:4',
|
|
'delivery_time_days' => 'integer',
|
|
];
|
|
|
|
protected $fillable = [
|
|
'name',
|
|
'trans_name',
|
|
'alt_name',
|
|
'inci',
|
|
'trans_inci',
|
|
'effect',
|
|
'trans_effect',
|
|
'active',
|
|
'pos',
|
|
'default_factor',
|
|
'min_stock_alert',
|
|
'density',
|
|
'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));
|
|
});
|
|
}
|
|
}
|