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:
Kevin Adametz 2026-06-03 11:04:22 +00:00
parent 78679e0c55
commit 3ee2d756e9
63 changed files with 5968 additions and 901 deletions

View file

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('products', function (Blueprint $table) {
$table->boolean('no_recipe_required')->default(false)->after('shelf_life_months');
});
}
public function down(): void
{
Schema::table('products', function (Blueprint $table) {
$table->dropColumn('no_recipe_required');
});
}
};

View file

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('products', function (Blueprint $table) {
$table->boolean('is_set')->default(false)->after('no_recipe_required');
$table->unsignedInteger('main_product_id')->nullable()->after('is_set');
$table->unsignedInteger('main_product_quantity')->nullable()->after('main_product_id');
$table->foreign('main_product_id')->references('id')->on('products')->nullOnDelete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('products', function (Blueprint $table) {
$table->dropForeign(['main_product_id']);
$table->dropColumn(['is_set', 'main_product_id', 'main_product_quantity']);
});
}
};

View file

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('product_set_items', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('set_product_id');
$table->unsignedInteger('component_product_id');
$table->unsignedInteger('quantity')->default(1);
$table->unsignedSmallInteger('pos')->default(0);
$table->timestamps();
$table->foreign('set_product_id')->references('id')->on('products')->cascadeOnDelete();
$table->foreign('component_product_id')->references('id')->on('products')->cascadeOnDelete();
$table->unique(['set_product_id', 'component_product_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('product_set_items');
}
};

View file

@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('products', function (Blueprint $table) {
$table->date('out_of_stock_until')->nullable()->after('main_product_quantity');
$table->boolean('out_of_stock_indefinite')->default(false)->after('out_of_stock_until');
});
}
public function down(): void
{
Schema::table('products', function (Blueprint $table) {
$table->dropColumn(['out_of_stock_until', 'out_of_stock_indefinite']);
});
}
};

View file

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('products', function (Blueprint $table) {
$table->unsignedInteger('min_product_stock')->nullable()->after('out_of_stock_indefinite');
$table->unsignedInteger('critical_product_stock')->nullable()->after('min_product_stock');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('products', function (Blueprint $table) {
$table->dropColumn(['min_product_stock', 'critical_product_stock']);
});
}
};

View file

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('product_stock_movements', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('product_id');
$table->enum('direction', ['in', 'out']);
$table->unsignedInteger('quantity');
$table->string('reason', 100)->nullable();
$table->string('source', 30)->default('manual');
$table->string('note', 255)->nullable();
$table->unsignedInteger('user_id')->nullable();
$table->nullableMorphs('reference');
$table->timestamps();
$table->foreign('product_id')->references('id')->on('products')->cascadeOnDelete();
$table->foreign('user_id')->references('id')->on('users')->nullOnDelete();
$table->index(['product_id', 'created_at']);
$table->index('source');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('product_stock_movements');
}
};

View file

@ -0,0 +1,47 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('stock_disposals', function (Blueprint $table) {
$table->id();
$table->enum('disposal_type', ['ingredient', 'packaging']);
$table->unsignedInteger('ingredient_id')->nullable();
$table->unsignedBigInteger('packaging_item_id')->nullable();
$table->unsignedBigInteger('stock_entry_id')->nullable();
$table->unsignedBigInteger('location_id');
$table->decimal('quantity', 12, 2);
$table->string('unit', 20)->default('gram');
$table->string('reason', 100);
$table->string('note', 255)->nullable();
$table->unsignedInteger('user_id')->nullable();
$table->date('disposed_at');
$table->timestamps();
$table->foreign('ingredient_id')->references('id')->on('ingredients')->nullOnDelete();
$table->foreign('packaging_item_id')->references('id')->on('packaging_items')->nullOnDelete();
$table->foreign('stock_entry_id')->references('id')->on('stock_entries')->nullOnDelete();
$table->foreign('location_id')->references('id')->on('locations')->cascadeOnDelete();
$table->foreign('user_id')->references('id')->on('users')->nullOnDelete();
$table->index(['disposal_type', 'ingredient_id']);
$table->index(['disposal_type', 'packaging_item_id']);
$table->index('disposed_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('stock_disposals');
}
};