From 2796b1103e89dc1aa6445540e84f1ed45fa4a21f Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Tue, 8 Sep 2026 10:59:48 +0000 Subject: [PATCH] =?UTF-8?q?Warenwirtschaft:=20F-08=20=E2=80=94=20F=C3=BCll?= =?UTF-8?q?gewicht=20mit=20Nachkommastellen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fill_weight decimal(10,3) + float-Cast; deutsche Eingabe normalisiert, positiv validiert, Fehler im Formular sichtbar. Tests für F-07/F-08. Co-Authored-By: Claude Fable 5.1 --- app/Http/Controllers/ProductController.php | 9 +++ app/Models/Product.php | 2 +- ...ll_weight_to_decimal_on_products_table.php | 26 +++++++ resources/views/admin/product/form.blade.php | 5 +- tests/Feature/ProductFillWeightTest.php | 74 +++++++++++++++++++ 5 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 database/migrations/2026_09_08_120000_change_fill_weight_to_decimal_on_products_table.php create mode 100644 tests/Feature/ProductFillWeightTest.php 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 @@
- {{ Form::text('fill_weight', $product->fill_weight, ['placeholder' => __('leer = Menge = Gramm (1 g/ml)'), 'class' => 'form-control', 'id' => 'fill_weight']) }} + {{ Form::text('fill_weight', $product->fill_weight !== null ? rtrim(rtrim(number_format((float) $product->fill_weight, 3, ',', '.'), '0'), ',') : null, ['placeholder' => __('leer = Menge = Gramm (1 g/ml)'), 'class' => 'form-control'.($errors->has('fill_weight') ? ' is-invalid' : ''), 'id' => 'fill_weight', 'inputmode' => 'decimal']) }} + @error('fill_weight') +
{{ $message }}
+ @enderror {{ __('Basis für die Rezeptur-Mengenberechnung (Prozent × Füllgewicht).') }}
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'); +});