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
272
tests/Feature/ProductSetTest.php
Normal file
272
tests/Feature/ProductSetTest.php
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Country;
|
||||
use App\Models\Ingredient;
|
||||
use App\Models\Location;
|
||||
use App\Models\PackagingItem;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductIngredient;
|
||||
use App\Repositories\ProductRepository;
|
||||
use App\Services\ProductionService;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function setMakeUser(int $adminLevel = 1): User
|
||||
{
|
||||
$user = User::query()->create([
|
||||
'email' => uniqid('set_', true).'@test.example',
|
||||
'password' => bcrypt('password'),
|
||||
]);
|
||||
$user->forceFill([
|
||||
'admin' => $adminLevel,
|
||||
'confirmed' => true,
|
||||
'active' => true,
|
||||
'wizard' => 100,
|
||||
'blocked' => false,
|
||||
])->save();
|
||||
|
||||
return $user->fresh();
|
||||
}
|
||||
|
||||
function setMakeProduct(string $name, bool $active = true): Product
|
||||
{
|
||||
return Product::query()->create([
|
||||
'name' => $name,
|
||||
'title' => $name,
|
||||
'active' => $active,
|
||||
]);
|
||||
}
|
||||
|
||||
test('repository speichert ein set mit bestandteilen und mengen', function () {
|
||||
$set = setMakeProduct('SET-Bundle');
|
||||
$a = setMakeProduct('SET-Einzel-A');
|
||||
$b = setMakeProduct('SET-Einzel-B');
|
||||
|
||||
$repo = new ProductRepository($set);
|
||||
$repo->update([
|
||||
'id' => (string) $set->id,
|
||||
'name' => $set->name,
|
||||
'title' => $set->title,
|
||||
'active' => '1',
|
||||
'is_set' => '1',
|
||||
'set_component_id' => [(string) $a->id, (string) $b->id],
|
||||
'set_quantity' => ['2', '3'],
|
||||
]);
|
||||
|
||||
$set->refresh();
|
||||
|
||||
expect($set->is_set)->toBeTrue();
|
||||
expect($set->setItems)->toHaveCount(2);
|
||||
expect((int) $set->setItems->firstWhere('id', $a->id)->pivot->quantity)->toBe(2);
|
||||
expect((int) $set->setItems->firstWhere('id', $b->id)->pivot->quantity)->toBe(3);
|
||||
});
|
||||
|
||||
test('set leert rezeptur und verpackung', function () {
|
||||
$ing = Ingredient::query()->create([
|
||||
'name' => 'SET-Roh', 'trans_name' => '', 'inci' => 'SR', 'trans_inci' => '',
|
||||
'effect' => '', 'trans_effect' => '', 'active' => true, 'pos' => 0,
|
||||
]);
|
||||
$packaging = PackagingItem::factory()->create();
|
||||
$component = setMakeProduct('SET-Komp');
|
||||
|
||||
$product = setMakeProduct('SET-Wandelbar');
|
||||
ProductIngredient::query()->create([
|
||||
'product_id' => $product->id, 'ingredient_id' => $ing->id,
|
||||
'pos' => 0, 'gram' => 50, 'factor' => 1.1, 'recipe_type' => 'manufacturer',
|
||||
]);
|
||||
$product->packagings()->attach($packaging->id, ['quantity' => 1, 'pos' => 0]);
|
||||
|
||||
$repo = new ProductRepository($product);
|
||||
$repo->update([
|
||||
'id' => (string) $product->id,
|
||||
'name' => $product->name,
|
||||
'title' => $product->title,
|
||||
'active' => '1',
|
||||
'is_set' => '1',
|
||||
'set_component_id' => [(string) $component->id],
|
||||
'set_quantity' => ['1'],
|
||||
]);
|
||||
|
||||
$product->refresh();
|
||||
|
||||
expect($product->is_set)->toBeTrue();
|
||||
expect($product->manufacturer_ingredients)->toHaveCount(0);
|
||||
expect($product->packagings)->toHaveCount(0);
|
||||
expect($product->setItems)->toHaveCount(1);
|
||||
});
|
||||
|
||||
test('hauptprodukt-zuordnung wird gespeichert und bei set genullt', function () {
|
||||
$main = setMakeProduct('SET-Haupt');
|
||||
$variant = setMakeProduct('SET-Variante');
|
||||
|
||||
$repo = new ProductRepository($variant);
|
||||
$repo->update([
|
||||
'id' => (string) $variant->id,
|
||||
'name' => $variant->name,
|
||||
'title' => $variant->title,
|
||||
'active' => '1',
|
||||
'main_product_id' => (string) $main->id,
|
||||
'main_product_quantity' => '50',
|
||||
]);
|
||||
|
||||
$variant->refresh();
|
||||
expect($variant->main_product_id)->toBe($main->id);
|
||||
expect($variant->main_product_quantity)->toBe(50);
|
||||
|
||||
// Wird das Produkt zum Set, ist es keine Variante mehr.
|
||||
$repo->update([
|
||||
'id' => (string) $variant->id,
|
||||
'name' => $variant->name,
|
||||
'title' => $variant->title,
|
||||
'active' => '1',
|
||||
'is_set' => '1',
|
||||
'main_product_id' => (string) $main->id,
|
||||
'set_component_id' => [(string) $main->id],
|
||||
'set_quantity' => ['1'],
|
||||
]);
|
||||
|
||||
$variant->refresh();
|
||||
expect($variant->main_product_id)->toBeNull();
|
||||
});
|
||||
|
||||
test('scopes trennen einzelprodukte, sets und hauptprodukte', function () {
|
||||
$single = setMakeProduct('SCOPE-Einzel');
|
||||
$set = setMakeProduct('SCOPE-Set');
|
||||
$set->update(['is_set' => true]);
|
||||
$variant = setMakeProduct('SCOPE-Variante');
|
||||
$variant->update(['main_product_id' => $single->id]);
|
||||
|
||||
expect(Product::query()->singleProducts()->pluck('id')->all())
|
||||
->toContain($single->id, $variant->id)
|
||||
->not->toContain($set->id);
|
||||
|
||||
expect(Product::query()->sets()->pluck('id')->all())
|
||||
->toContain($set->id)
|
||||
->not->toContain($single->id);
|
||||
|
||||
expect(Product::query()->mainProducts()->pluck('id')->all())
|
||||
->toContain($single->id, $set->id)
|
||||
->not->toContain($variant->id);
|
||||
});
|
||||
|
||||
test('set kann nicht produziert werden', function () {
|
||||
$location = Location::factory()->create();
|
||||
$set = setMakeProduct('SET-NichtProduzierbar');
|
||||
$set->update(['is_set' => true]);
|
||||
|
||||
$service = app(ProductionService::class);
|
||||
|
||||
expect(fn () => $service->store(
|
||||
[
|
||||
'product_id' => $set->id,
|
||||
'location_id' => $location->id,
|
||||
'produced_at' => '2026-03-01',
|
||||
'quantity' => 1,
|
||||
'notes' => null,
|
||||
],
|
||||
[],
|
||||
setMakeUser()->id
|
||||
))->toThrow(ValidationException::class);
|
||||
});
|
||||
|
||||
test('produktions-formular zeigt keine sets', function () {
|
||||
p51EnsureCountryForSet();
|
||||
Location::factory()->create(['name' => 'Köln']);
|
||||
$user = setMakeUser(1);
|
||||
|
||||
setMakeProduct('SET-FORM-EINZEL');
|
||||
$set = setMakeProduct('SET-FORM-SET');
|
||||
$set->update(['is_set' => true]);
|
||||
|
||||
$this->actingAs($user, 'user');
|
||||
$response = $this->get(route('admin.inventory.productions.create'));
|
||||
|
||||
$response->assertSuccessful();
|
||||
$response->assertSee('SET-FORM-EINZEL');
|
||||
$response->assertDontSee('SET-FORM-SET');
|
||||
});
|
||||
|
||||
test('set ohne bestandteile wird abgelehnt', function () {
|
||||
$user = setMakeUser(1);
|
||||
$this->actingAs($user, 'user');
|
||||
|
||||
$response = $this->post(route('admin_product_store'), [
|
||||
'id' => 'new',
|
||||
'name' => 'SET-LEER',
|
||||
'is_set' => '1',
|
||||
]);
|
||||
|
||||
$response->assertSuccessful();
|
||||
$response->assertSee('mindestens ein Einzelprodukt', false);
|
||||
expect(Product::query()->where('name', 'SET-LEER')->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('set mit set-bestandteil wird abgelehnt', function () {
|
||||
$user = setMakeUser(1);
|
||||
$this->actingAs($user, 'user');
|
||||
|
||||
$innerSet = setMakeProduct('SET-INNEN');
|
||||
$innerSet->update(['is_set' => true]);
|
||||
|
||||
$response = $this->post(route('admin_product_store'), [
|
||||
'id' => 'new',
|
||||
'name' => 'SET-AUSSEN',
|
||||
'is_set' => '1',
|
||||
'set_component_id' => [(string) $innerSet->id],
|
||||
'set_quantity' => ['1'],
|
||||
]);
|
||||
|
||||
$response->assertSuccessful();
|
||||
$response->assertSee('dürfen selbst keine Sets sein', false);
|
||||
});
|
||||
|
||||
test('gueltiges set wird per http gespeichert', function () {
|
||||
$user = setMakeUser(1);
|
||||
$this->actingAs($user, 'user');
|
||||
|
||||
$component = setMakeProduct('SET-HTTP-KOMP');
|
||||
|
||||
$response = $this->post(route('admin_product_store'), [
|
||||
'id' => 'new',
|
||||
'name' => 'SET-HTTP',
|
||||
'is_set' => '1',
|
||||
'set_component_id' => [(string) $component->id],
|
||||
'set_quantity' => ['4'],
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
|
||||
$set = Product::query()->where('name', 'SET-HTTP')->firstOrFail();
|
||||
expect($set->is_set)->toBeTrue();
|
||||
expect($set->setItems)->toHaveCount(1);
|
||||
expect((int) $set->setItems->first()->pivot->quantity)->toBe(4);
|
||||
});
|
||||
|
||||
test('produkt-kopie uebernimmt set-bestandteile', function () {
|
||||
$component = setMakeProduct('COPY-KOMP');
|
||||
$set = setMakeProduct('COPY-SET');
|
||||
$set->update(['is_set' => true]);
|
||||
$set->setItems()->attach($component->id, ['quantity' => 5, 'pos' => 0]);
|
||||
|
||||
$repo = new ProductRepository(new Product);
|
||||
$copy = $repo->copy($set->fresh());
|
||||
|
||||
expect($copy->is_set)->toBeTrue();
|
||||
expect($copy->setItems()->count())->toBe(1);
|
||||
expect((int) $copy->setItems()->first()->pivot->quantity)->toBe(5);
|
||||
});
|
||||
|
||||
function p51EnsureCountryForSet(): Country
|
||||
{
|
||||
return Country::query()->firstOrCreate(
|
||||
['code' => 'DE'],
|
||||
[
|
||||
'phone' => '00', 'en' => 'Germany', 'de' => 'Deutschland', 'es' => 'Germany',
|
||||
'fr' => 'Germany', 'it' => 'Germany', 'ru' => 'Germany', 'active' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue