diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php
index bd0a444..16d702a 100755
--- a/app/Http/Controllers/ProductController.php
+++ b/app/Http/Controllers/ProductController.php
@@ -100,8 +100,17 @@ class ProductController extends Controller
}
}
+ // F-08: Füllgewicht deutsch normalisieren („39,5“) und positiv validieren.
+ if (Request::has('fill_weight')) {
+ $fillRaw = trim((string) Request::input('fill_weight'));
+ $fillNormalized = $fillRaw === '' ? null : reFormatWawiNumber($fillRaw);
+ // Unbrauchbare Eingabe („abc“) nicht zu leer normalisieren, sonst greift nullable.
+ Request::merge(['fill_weight' => $fillNormalized === '' || $fillNormalized === false ? $fillRaw : $fillNormalized]);
+ }
+
$rules = [
'name' => 'required',
+ 'fill_weight' => ['nullable', 'numeric', 'gt:0'],
];
/*if(isset($data['number']) && $data['number'] != ""){
$rules['number'] = 'int';
diff --git a/app/Models/Product.php b/app/Models/Product.php
index 8539976..39e7365 100644
--- a/app/Models/Product.php
+++ b/app/Models/Product.php
@@ -194,7 +194,7 @@ class Product extends Model
'show_on' => 'array',
'action' => 'array',
'wp_number' => 'int',
- 'fill_weight' => 'int',
+ 'fill_weight' => 'float',
'production_relevant' => 'bool',
'single_commission' => 'bool',
'amount_commission' => 'bool',
diff --git a/database/migrations/2026_09_08_120000_change_fill_weight_to_decimal_on_products_table.php b/database/migrations/2026_09_08_120000_change_fill_weight_to_decimal_on_products_table.php
new file mode 100644
index 0000000..a9406d8
--- /dev/null
+++ b/database/migrations/2026_09_08_120000_change_fill_weight_to_decimal_on_products_table.php
@@ -0,0 +1,26 @@
+decimal('fill_weight', 10, 3)->nullable()->change();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('products', function (Blueprint $table) {
+ $table->integer('fill_weight')->nullable()->change();
+ });
+ }
+};
diff --git a/resources/views/admin/product/form.blade.php b/resources/views/admin/product/form.blade.php
index 4c7d30b..4edf9ef 100755
--- a/resources/views/admin/product/form.blade.php
+++ b/resources/views/admin/product/form.blade.php
@@ -532,7 +532,10 @@
diff --git a/tests/Feature/ProductFillWeightTest.php b/tests/Feature/ProductFillWeightTest.php
new file mode 100644
index 0000000..af09031
--- /dev/null
+++ b/tests/Feature/ProductFillWeightTest.php
@@ -0,0 +1,74 @@
+create(['email' => uniqid('fw_', true).'@test.example', 'password' => bcrypt('password')]);
+ $user->forceFill(['admin' => 7, 'confirmed' => true, 'active' => true, 'wizard' => 100, 'blocked' => false])->save();
+
+ return $user->fresh();
+}
+
+test('F-08: füllgewicht 39,5 wird mit nachkommastellen gespeichert und skaliert die rezeptur exakt', function () {
+ $this->actingAs(fwUser(), 'user');
+
+ $this->post(route('admin_product_store'), ['id' => 'new', 'name' => 'FW-Dezimal', 'fill_weight' => '39,5'])
+ ->assertRedirect()->assertSessionHasNoErrors();
+
+ $product = Product::query()->where('name', 'FW-Dezimal')->firstOrFail();
+ expect($product->fill_weight)->toBe(39.5)
+ ->and(app(ProductionService::class)->recipeGramScale($product))->toBe(0.395);
+
+ // Punkt mit 1–2 Nachkommastellen gilt als Dezimaltrenner (WaWi-Konvention), leer = null.
+ $this->post(route('admin_product_store'), ['id' => $product->id, 'name' => 'FW-Dezimal', 'fill_weight' => '4.5'])->assertSessionHasNoErrors();
+ expect($product->fresh()->fill_weight)->toBe(4.5);
+
+ $this->post(route('admin_product_store'), ['id' => $product->id, 'name' => 'FW-Dezimal', 'fill_weight' => ''])->assertSessionHasNoErrors();
+ expect($product->fresh()->fill_weight)->toBeNull();
+});
+
+test('F-08: ungültiges oder negatives füllgewicht wird abgelehnt', function () {
+ $this->actingAs(fwUser(), 'user');
+ $product = Product::query()->create(['name' => 'FW-Invalid', 'title' => 'x', 'active' => true, 'fill_weight' => 10]);
+
+ // Der Produkt-Controller rendert bei Fehlern das Formular direkt (kein Redirect).
+ foreach (['abc', '-5', '0'] as $bad) {
+ $this->post(route('admin_product_store'), ['id' => $product->id, 'name' => 'FW-Invalid', 'fill_weight' => $bad])
+ ->assertOk()
+ ->assertSee('is-invalid', false);
+ }
+ expect($product->fresh()->fill_weight)->toBe(10.0);
+});
+
+test('F-07: produktionsformular liefert die verpackungs-stückliste je ergebnisprodukt für die vorschau', function () {
+ $this->actingAs(fwUser(), 'user');
+ $supplier = Supplier::factory()->create();
+ $material = PackagingMaterial::factory()->create();
+ $pk = PackagingItem::factory()->create(['packaging_material_id' => $material->id, 'supplier_id' => $supplier->id, 'name' => 'F07-Tiegel', 'category' => 'packaging']);
+ $small = Product::query()->create(['name' => 'F07-5ml', 'title' => 'x', 'active' => true, 'no_recipe_required' => true, 'production_relevant' => false]);
+ $small->packagings()->sync([$pk->id => ['quantity' => 2, 'pos' => 0]]);
+ Location::factory()->create();
+
+ $html = $this->get(route('admin.inventory.productions.create'))->assertOk()->getContent();
+ $start = strpos($html, "data-packaging-bom='") + strlen("data-packaging-bom='");
+ $bom = json_decode(substr($html, $start, strpos($html, "'", $start) - $start), true);
+
+ expect($bom)->toHaveKey((string) $small->id)
+ ->and($bom[$small->id][0]['name'])->toBe('F07-Tiegel')
+ ->and($bom[$small->id][0]['per_unit'])->toEqual(2)
+ ->and($html)->toContain('renderPackagingPreview');
+});