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
|
|
@ -9,6 +9,7 @@ use App\Models\Product;
|
|||
use App\Models\ProductImage;
|
||||
use App\Models\ProductIngredient;
|
||||
use App\Repositories\ProductRepository;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Request;
|
||||
use Validator;
|
||||
|
||||
|
|
@ -47,7 +48,7 @@ class ProductController extends Controller
|
|||
$model->active = true;
|
||||
} else {
|
||||
$model = Product::findOrFail($id);
|
||||
$model->load(['packagings.packagingMaterial']);
|
||||
$model->load(['packagings.packagingMaterial', 'setItems']);
|
||||
}
|
||||
|
||||
$country_for_prices = Country::where('own_eur', '=', true)->orWhere('currency', '=', true)->get();
|
||||
|
|
@ -56,11 +57,27 @@ class ProductController extends Controller
|
|||
'country_for_prices' => $country_for_prices,
|
||||
'ingredient_catalog' => Ingredient::query()->where('active', true)->with('materialQuality')->orderBy('name')->get(['id', 'name', 'inci', 'effect', 'default_factor', 'material_quality_id']),
|
||||
'packaging_catalog' => PackagingItem::query()->where('active', true)->with('packagingMaterial')->orderBy('name')->get(),
|
||||
'set_product_catalog' => $this->setProductCatalog($model),
|
||||
];
|
||||
|
||||
return view('admin.product.edit', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auswählbare Set-Bestandteile: aktive Einzelprodukte (keine Sets, nicht das Produkt selbst).
|
||||
*
|
||||
* @return Collection<int, Product>
|
||||
*/
|
||||
protected function setProductCatalog(Product $model)
|
||||
{
|
||||
return Product::query()
|
||||
->where('active', true)
|
||||
->singleProducts()
|
||||
->when($model->id, fn ($q) => $q->where('id', '!=', $model->id))
|
||||
->orderBy('name')
|
||||
->get(['id', 'name', 'number']);
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
|
||||
|
|
@ -87,15 +104,18 @@ class ProductController extends Controller
|
|||
$model = new Product;
|
||||
} else {
|
||||
$model = Product::findOrFail($data['id']);
|
||||
$model->load(['packagings.packagingMaterial']);
|
||||
$model->load(['packagings.packagingMaterial', 'setItems']);
|
||||
}
|
||||
$country_for_prices = Country::where('own_eur', '=', true)->orWhere('currency', '=', true)->get();
|
||||
|
||||
$this->validateSetItems($validator, Request::all(), $model);
|
||||
|
||||
$data = [
|
||||
'product' => $model,
|
||||
'country_for_prices' => $country_for_prices,
|
||||
'ingredient_catalog' => Ingredient::query()->where('active', true)->with('materialQuality')->orderBy('name')->get(['id', 'name', 'inci', 'effect', 'default_factor', 'material_quality_id']),
|
||||
'packaging_catalog' => PackagingItem::query()->where('active', true)->with('packagingMaterial')->orderBy('name')->get(),
|
||||
'set_product_catalog' => $this->setProductCatalog($model),
|
||||
];
|
||||
|
||||
if ($validator->fails()) {
|
||||
|
|
@ -110,6 +130,43 @@ class ProductController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set-Validierung: Set braucht mindestens ein Einzelprodukt als Bestandteil
|
||||
* (keine Sets, nicht das Produkt selbst).
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
protected function validateSetItems($validator, array $data, Product $model): void
|
||||
{
|
||||
if (! isset($data['is_set'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$validator->after(function ($validator) use ($data, $model) {
|
||||
$componentIds = collect($data['set_component_id'] ?? [])
|
||||
->map(fn ($id) => (int) $id)
|
||||
->filter(fn (int $id) => $id > 0 && $id !== (int) $model->id)
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
if ($componentIds->isEmpty()) {
|
||||
$validator->errors()->add('set_component_id', __('Ein Set benötigt mindestens ein Einzelprodukt als Bestandteil.'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$components = Product::query()->whereIn('id', $componentIds)->get(['id', 'is_set']);
|
||||
|
||||
if ($components->count() !== $componentIds->count()) {
|
||||
$validator->errors()->add('set_component_id', __('Mindestens ein gewählter Bestandteil existiert nicht.'));
|
||||
}
|
||||
|
||||
if ($components->where('is_set', true)->isNotEmpty()) {
|
||||
$validator->errors()->add('set_component_id', __('Set-Bestandteile dürfen selbst keine Sets sein.'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function copy($id)
|
||||
{
|
||||
$model = Product::findOrFail($id);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue