diff --git a/app/Http/Controllers/Components/ComponentCheckoutController.php b/app/Http/Controllers/Components/ComponentCheckoutController.php index 5547b7170..e9db70811 100644 --- a/app/Http/Controllers/Components/ComponentCheckoutController.php +++ b/app/Http/Controllers/Components/ComponentCheckoutController.php @@ -8,6 +8,7 @@ use App\Helpers\Helper; use App\Http\Controllers\Controller; use App\Models\Asset; use App\Models\Component; +use App\Models\Setting; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Input; @@ -97,6 +98,10 @@ class ComponentCheckoutController extends Controller // Check if the asset exists $asset = Asset::find($request->input('asset_id')); + if ((Setting::getSettings()->full_multiple_companies_support) && $component->company_id !== $asset->company_id) { + return redirect()->route('components.checkout.show', $componentId)->with('error', trans('general.error_user_company')); + } + // Update the component data $component->asset_id = $request->input('asset_id'); $component->assets()->attach($component->id, [ diff --git a/app/Models/Component.php b/app/Models/Component.php index 671b0101c..536e06d0a 100644 --- a/app/Models/Component.php +++ b/app/Models/Component.php @@ -205,7 +205,11 @@ class Component extends SnipeModel public function numCheckedOut() { $checkedout = 0; - foreach ($this->assets as $checkout) { + + // In case there are elements checked out to assets that belong to a different company + // than this asset and full multiple company support is on we'll remove the global scope, + // so they are included in the count. + foreach ($this->assets()->withoutGlobalScope(new CompanyableScope)->get() as $checkout) { $checkedout += $checkout->pivot->assigned_qty; } diff --git a/tests/Feature/Checkouts/Ui/ComponentsCheckoutTest.php b/tests/Feature/Checkouts/Ui/ComponentsCheckoutTest.php index 5c34c0dc9..e2e91823c 100644 --- a/tests/Feature/Checkouts/Ui/ComponentsCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/ComponentsCheckoutTest.php @@ -2,9 +2,12 @@ namespace Tests\Feature\Checkouts\Ui; +use App\Events\CheckoutableCheckedOut; use App\Models\Asset; +use App\Models\Company; use App\Models\Component; use App\Models\User; +use Illuminate\Support\Facades\Event; use Tests\TestCase; class ComponentsCheckoutTest extends TestCase @@ -18,6 +21,27 @@ class ComponentsCheckoutTest extends TestCase ->assertForbidden(); } + public function test_cannot_checkout_across_companies_when_full_company_support_enabled() + { + Event::fake([CheckoutableCheckedOut::class]); + + $this->settings->enableMultipleFullCompanySupport(); + + [$assetCompany, $componentCompany] = Company::factory()->count(2)->create(); + + $asset = Asset::factory()->for($assetCompany)->create(); + $component = Component::factory()->for($componentCompany)->create(); + + $this->actingAs(User::factory()->superuser()->create()) + ->post(route('components.checkout.store', $component), [ + 'asset_id' => $asset->id, + 'assigned_qty' => '1', + 'redirect_option' => 'index', + ]); + + Event::assertNotDispatched(CheckoutableCheckedOut::class); + } + public function testComponentCheckoutPagePostIsRedirectedIfRedirectSelectionIsIndex() { $component = Component::factory()->create(); @@ -63,6 +87,4 @@ class ComponentsCheckoutTest extends TestCase ->assertStatus(302) ->assertRedirect(route('hardware.show', ['hardware' => $asset])); } - - } diff --git a/tests/Unit/ComponentTest.php b/tests/Unit/ComponentTest.php index df8f64771..6abbffd82 100644 --- a/tests/Unit/ComponentTest.php +++ b/tests/Unit/ComponentTest.php @@ -1,10 +1,12 @@ assertInstanceOf(Category::class, $component->category); $this->assertEquals('component', $component->category->category_type); } + + public function test_num_checked_out_takes_does_not_scope_by_company() + { + $this->settings->enableMultipleFullCompanySupport(); + + [$companyA, $companyB] = Company::factory()->count(2)->create(); + + $componentForCompanyA = Component::factory()->for($companyA)->create(['qty' => 5]); + $assetForCompanyB = Asset::factory()->for($companyB)->create(); + + // Ideally, we shouldn't have a component attached to an + // asset from a different company but alas... + $componentForCompanyA->assets()->attach($componentForCompanyA->id, [ + 'component_id' => $componentForCompanyA->id, + 'assigned_qty' => 4, + 'asset_id' => $assetForCompanyB->id, + ]); + + $this->actingAs(User::factory()->superuser()->create()); + $this->assertEquals(4, $componentForCompanyA->fresh()->numCheckedOut()); + + $this->actingAs(User::factory()->admin()->create()); + $this->assertEquals(4, $componentForCompanyA->fresh()->numCheckedOut()); + + $this->actingAs(User::factory()->for($companyA)->create()); + $this->assertEquals(4, $componentForCompanyA->fresh()->numCheckedOut()); + } + + public function test_num_remaining_takes_company_scoping_into_account() + { + $this->settings->enableMultipleFullCompanySupport(); + + [$companyA, $companyB] = Company::factory()->count(2)->create(); + + $componentForCompanyA = Component::factory()->for($companyA)->create(['qty' => 5]); + $assetForCompanyB = Asset::factory()->for($companyB)->create(); + + // Ideally, we shouldn't have a component attached to an + // asset from a different company but alas... + $componentForCompanyA->assets()->attach($componentForCompanyA->id, [ + 'component_id' => $componentForCompanyA->id, + 'assigned_qty' => 4, + 'asset_id' => $assetForCompanyB->id, + ]); + + $this->actingAs(User::factory()->superuser()->create()); + $this->assertEquals(1, $componentForCompanyA->fresh()->numRemaining()); + + $this->actingAs(User::factory()->admin()->create()); + $this->assertEquals(1, $componentForCompanyA->fresh()->numRemaining()); + + $this->actingAs(User::factory()->for($companyA)->create()); + $this->assertEquals(1, $componentForCompanyA->fresh()->numRemaining()); + } }