Warenwirtschaft: AP-09 bis AP-13 (Produktbestand, Set-Produkte, Ausschuss, Konzepte)
- AP-09 Produktbestand inkl. Bewegungshistorie (product_stock_movements, ProductStockService) - AP-10 Rohstoffbestand-Ansicht je Lager (RawMaterialStockController) - AP-11 Bestandsschwellen / Out-of-Stock-Handling fuer Produkte und Shop - AP-12 Ausgang/Ausschuss (stock_disposals, StockDisposalController, InventoryService) - Set-Produkte (product_set_items) inkl. Aufloesung - Produktentwicklung & Hinweise-Verwaltung (Notices) - AP-13 Entwicklungskonzept Shop-Bestandsabzug im Plan dokumentiert - Feature-Tests fuer neue Module + aktualisierter Entwicklungsplan Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
78679e0c55
commit
3ee2d756e9
63 changed files with 5968 additions and 901 deletions
|
|
@ -5,8 +5,10 @@ namespace App\Models;
|
|||
use App\Services\Type;
|
||||
use App\Services\Util;
|
||||
use Cviebrock\EloquentSluggable\Sluggable;
|
||||
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\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
|
@ -200,6 +202,14 @@ class Product extends Model
|
|||
'max_buy' => 'bool',
|
||||
'max_buy_num' => 'int',
|
||||
'whitelabel' => 'bool',
|
||||
'no_recipe_required' => 'bool',
|
||||
'is_set' => 'bool',
|
||||
'main_product_id' => 'int',
|
||||
'main_product_quantity' => 'int',
|
||||
'out_of_stock_until' => 'date',
|
||||
'out_of_stock_indefinite' => 'bool',
|
||||
'min_product_stock' => 'int',
|
||||
'critical_product_stock' => 'int',
|
||||
];
|
||||
|
||||
use Sluggable;
|
||||
|
|
@ -247,6 +257,14 @@ class Product extends Model
|
|||
'max_buy_num',
|
||||
'shelf_life_type',
|
||||
'shelf_life_months',
|
||||
'no_recipe_required',
|
||||
'is_set',
|
||||
'main_product_id',
|
||||
'main_product_quantity',
|
||||
'out_of_stock_until',
|
||||
'out_of_stock_indefinite',
|
||||
'min_product_stock',
|
||||
'critical_product_stock',
|
||||
|
||||
];
|
||||
|
||||
|
|
@ -369,6 +387,94 @@ class Product extends Model
|
|||
return $this->hasMany(Production::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Produktbestands-Bewegungen (Eingang/Ausgang).
|
||||
*
|
||||
* @return HasMany<ProductStockMovement, $this>
|
||||
*/
|
||||
public function stockMovements(): HasMany
|
||||
{
|
||||
return $this->hasMany(ProductStockMovement::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set-Bestandteile (Einzelprodukte) dieses Sets mit Menge und Reihenfolge.
|
||||
*
|
||||
* @return BelongsToMany<Product, $this>
|
||||
*/
|
||||
public function setItems(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Product::class, 'product_set_items', 'set_product_id', 'component_product_id')
|
||||
->withPivot('quantity', 'pos')
|
||||
->withTimestamps()
|
||||
->orderByPivot('pos');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets, in denen dieses Produkt als Bestandteil enthalten ist.
|
||||
*
|
||||
* @return BelongsToMany<Product, $this>
|
||||
*/
|
||||
public function partOfSets(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Product::class, 'product_set_items', 'component_product_id', 'set_product_id')
|
||||
->withPivot('quantity', 'pos')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Übergeordnetes Hauptprodukt (z. B. „50 × 15 ml").
|
||||
*
|
||||
* @return BelongsTo<Product, $this>
|
||||
*/
|
||||
public function mainProduct(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Product::class, 'main_product_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Untergeordnete Varianten, die auf dieses Produkt als Hauptprodukt zeigen.
|
||||
*
|
||||
* @return HasMany<Product, $this>
|
||||
*/
|
||||
public function variants(): HasMany
|
||||
{
|
||||
return $this->hasMany(Product::class, 'main_product_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Nur Einzelprodukte (keine Sets).
|
||||
*
|
||||
* @param Builder<Product> $query
|
||||
* @return Builder<Product>
|
||||
*/
|
||||
public function scopeSingleProducts(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_set', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets.
|
||||
*
|
||||
* @param Builder<Product> $query
|
||||
* @return Builder<Product>
|
||||
*/
|
||||
public function scopeSets(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_set', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Haupt-/Einzelprodukte, die keinem übergeordneten Hauptprodukt zugeordnet sind.
|
||||
*
|
||||
* @param Builder<Product> $query
|
||||
* @return Builder<Product>
|
||||
*/
|
||||
public function scopeMainProducts(Builder $query): Builder
|
||||
{
|
||||
return $query->whereNull('main_product_id');
|
||||
}
|
||||
|
||||
public function getShortCopy($clean = false, $len = false)
|
||||
{
|
||||
$ret = $this->short_copy ? $this->short_copy : $this->description;
|
||||
|
|
@ -437,6 +543,53 @@ class Product extends Model
|
|||
return isset($this->attributes['price']) ? Util::formatNumber($this->attributes['price']) : '';
|
||||
}
|
||||
|
||||
public function isOutOfStock(): bool
|
||||
{
|
||||
if ($this->out_of_stock_indefinite) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->out_of_stock_until !== null && $this->out_of_stock_until->endOfDay()->isFuture();
|
||||
}
|
||||
|
||||
public function outOfStockRemainingDays(): ?int
|
||||
{
|
||||
if ($this->out_of_stock_indefinite || $this->out_of_stock_until === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! $this->out_of_stock_until->endOfDay()->isFuture()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$diffSeconds = $this->out_of_stock_until->copy()->startOfDay()->getTimestamp() - now()->startOfDay()->getTimestamp();
|
||||
|
||||
return (int) max(0, (int) round($diffSeconds / 86400));
|
||||
}
|
||||
|
||||
public function outOfStockNotice(): ?string
|
||||
{
|
||||
if ($this->out_of_stock_indefinite) {
|
||||
return __('Zur Zeit nicht vorrätig');
|
||||
}
|
||||
|
||||
$days = $this->outOfStockRemainingDays();
|
||||
|
||||
if ($days === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($days <= 0) {
|
||||
return __('In Kürze wieder verfügbar');
|
||||
}
|
||||
|
||||
if ($days === 1) {
|
||||
return __('In ca. 1 Tag wieder da!');
|
||||
}
|
||||
|
||||
return __('In ca. :days Tagen wieder da!', ['days' => $days]);
|
||||
}
|
||||
|
||||
public function getFormattedPriceEk()
|
||||
{
|
||||
return isset($this->attributes['price_ek']) ? Util::formatNumber($this->attributes['price_ek']) : '';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue