diff --git a/app/Http/Controllers/Api/AssetsController.php b/app/Http/Controllers/Api/AssetsController.php index 0ae5b91b83a0..f98ea31b54b1 100644 --- a/app/Http/Controllers/Api/AssetsController.php +++ b/app/Http/Controllers/Api/AssetsController.php @@ -143,6 +143,9 @@ public function index(Request $request, $action = null, $upcoming_status = null) 'supplier' ); // it might be tempting to add 'assetlog' here, but don't. It blows up update-heavy users. + if ($request->input('include_child_assets') === 'true') { + $assets->with(['assignedAssets.assignedTo']); + } if ($filter_non_deprecable_assets) { $non_deprecable_models = AssetModel::select('id')->whereNotNull('depreciation_id')->get(); @@ -432,6 +435,16 @@ public function index(Request $request, $action = null, $upcoming_status = null) $total = $assets->count(); $assets = $assets->skip($offset)->take($limit)->get(); + if ($request->input('include_child_assets') === 'true') { + // Each asset might have a collection of assignedAssets on it. + // We need to move those up to the top level so the transformer runs on them. + foreach ($assets as $asset) { + if ($asset->assignedAssets->count()) { + $assets = $assets->merge($asset->assignedAssets); + } + } + } + /** * Include additional associated relationships diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php index bbff6ba4f77e..c6eac34805b9 100755 --- a/app/Http/Controllers/ViewAssetsController.php +++ b/app/Http/Controllers/ViewAssetsController.php @@ -41,6 +41,14 @@ public function getIndex() : View | RedirectResponse 'licenses', )->find(auth()->id()); + if (Setting::getSettings()->show_assigned_assets) { + $user->load([ + 'assets.assignedAssets', + 'assets.assignedAssets.model', + 'assets.assignedAssets.model.fieldset.fields', + ]); + } + $field_array = array(); // Loop through all the custom fields that are applied to any model the user has assigned @@ -58,6 +66,20 @@ public function getIndex() : View | RedirectResponse } } + foreach ($asset->assignedAssets as $assignedAsset) { + // Make sure the model has a custom fieldset before trying to loop through the associated fields + if ($assignedAsset->model->fieldset) { + + foreach ($assignedAsset->model->fieldset->fields as $field) { + // check and make sure they're allowed to see the value of the custom field + if ($field->display_in_user_view == '1') { + $field_array[$field->db_column] = $field->name; + } + + } + } + } + } // Since some models may re-use the same fieldsets/fields, let's make the array unique so we don't repeat columns diff --git a/app/Notifications/CurrentInventory.php b/app/Notifications/CurrentInventory.php index ae64b188cdbc..b2db9ed156f4 100644 --- a/app/Notifications/CurrentInventory.php +++ b/app/Notifications/CurrentInventory.php @@ -2,6 +2,7 @@ namespace App\Notifications; +use App\Models\Setting; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; @@ -38,12 +39,20 @@ public function via() */ public function toMail() { + $this->user->load([ + 'assets.assignedAssets', + 'accessories', + 'licenses', + 'consumables', + ]); + $message = (new MailMessage)->markdown('notifications.markdown.user-inventory', [ 'assets' => $this->user->assets, 'accessories' => $this->user->accessories, 'licenses' => $this->user->licenses, 'consumables' => $this->user->consumables, + 'show_assigned_assets' => Setting::getSettings()->show_assigned_assets, ]) ->subject(trans('mail.inventory_report')); diff --git a/database/factories/AssetFactory.php b/database/factories/AssetFactory.php index ca85c2358442..7396ce550b69 100644 --- a/database/factories/AssetFactory.php +++ b/database/factories/AssetFactory.php @@ -312,12 +312,11 @@ public function assignedToLocation(Location $location = null) }); } - public function assignedToAsset() + public function assignedToAsset(Asset $asset = null) { - return $this->state(function () { + return $this->state(function () use ($asset) { return [ - 'model_id' => 1, - 'assigned_to' => Asset::factory(), + 'assigned_to' => $asset->id ?? Asset::factory(), 'assigned_type' => Asset::class, ]; }); diff --git a/resources/views/account/view-assets.blade.php b/resources/views/account/view-assets.blade.php index 038aaae1ce10..a9510d5af6d8 100755 --- a/resources/views/account/view-assets.blade.php +++ b/resources/views/account/view-assets.blade.php @@ -455,73 +455,14 @@ class="table table-striped snipe-table" - @php - $counter = 1 - @endphp @foreach ($user->assets as $asset) - - {{ $counter }} - - @if (($asset->image) && ($asset->image!='')) - - @elseif (($asset->model) && ($asset->model->image!='')) - - @endif - - - @if (($asset->model) && ($asset->model->category)) - {{ $asset->model->category->name }} - @endif - - - {{ $asset->asset_tag }} - - - {{ $asset->name }} - - - {{ $asset->model->name }} - - - {{ $asset->model->model_number }} - - - {{ $asset->serial }} - - - {{ ($asset->defaultLoc) ? $asset->defaultLoc->name : '' }} - - - {{ ($asset->location) ? $asset->location->name : '' }} - - @can('self.view_purchase_cost') - - {!! Helper::formatCurrencyOutput($asset->purchase_cost) !!} - - @endcan - - - {{ ($asset->asset_eol_date != '') ? Helper::getFormattedDateObject($asset->asset_eol_date, 'date', false) : null }} - - - - {{ Helper::getFormattedDateObject($asset->last_audit_date, 'datetime', false) }} - - - {{ Helper::getFormattedDateObject($asset->next_audit_date, 'date', false) }} - - - @foreach ($field_array as $db_column => $field_value) - - {{ $asset->{$db_column} }} - - @endforeach - - + - @php - $counter++ - @endphp + @if ($settings->show_assigned_assets && $asset->assignedAssets->count()) + @foreach($asset->assignedAssets as $assignedAsset) + + @endforeach + @endif @endforeach diff --git a/resources/views/blade/asset-table-row.blade.php b/resources/views/blade/asset-table-row.blade.php new file mode 100644 index 000000000000..0703f9fe2004 --- /dev/null +++ b/resources/views/blade/asset-table-row.blade.php @@ -0,0 +1,68 @@ +@props([ + 'asset', + 'field_array', + 'counter', +]) + +{{-- + This component was extracted for the account.view_assets view. Not guaranteed to work in other views. +--}} + + + {{ $counter }} + + @if (($asset->image) && ($asset->image!='')) + + @elseif (($asset->model) && ($asset->model->image!='')) + + @endif + + + @if (($asset->model) && ($asset->model->category)) + {{ $asset->model->category->name }} + @endif + + + {{ $asset->asset_tag }} + + + {{ $asset->name }} + + + {{ $asset->model->name }} + + + {{ $asset->model->model_number }} + + + {{ $asset->serial }} + + + {{ ($asset->defaultLoc) ? $asset->defaultLoc->name : '' }} + + + {{ ($asset->location) ? $asset->location->name : '' }} + + @can('self.view_purchase_cost') + + {!! Helper::formatCurrencyOutput($asset->purchase_cost) !!} + + @endcan + + + {{ ($asset->asset_eol_date != '') ? Helper::getFormattedDateObject($asset->asset_eol_date, 'date', false) : null }} + + + + {{ Helper::getFormattedDateObject($asset->last_audit_date, 'datetime', false) }} + + + {{ Helper::getFormattedDateObject($asset->next_audit_date, 'date', false) }} + + + @foreach ($field_array as $db_column => $field_value) + + {{ $asset->{$db_column} }} + + @endforeach + diff --git a/resources/views/notifications/markdown/user-inventory.blade.php b/resources/views/notifications/markdown/user-inventory.blade.php index 6c062b4a0ac5..72799e29b01e 100644 --- a/resources/views/notifications/markdown/user-inventory.blade.php +++ b/resources/views/notifications/markdown/user-inventory.blade.php @@ -9,11 +9,12 @@ ## {{ $assets->count() }} {{ trans('general.assets') }} - + @foreach($assets as $asset) + @@ -24,6 +25,22 @@ @endif +@if ($show_assigned_assets && $asset->assignedAssets->count()) +@foreach($asset->assignedAssets as $assignedAsset) + + + + + + + @if (($snipeSettings->show_images_in_email =='1') && $assignedAsset->getImageUrl()) + + @endif + +@endforeach +@endif @endforeach
{{ trans('mail.name') }} {{ trans('mail.asset_tag') }}{{ trans('admin/hardware/table.serial') }}{{ trans('general.category') }}
#{{ trans('mail.name') }} {{ trans('mail.asset_tag') }}{{ trans('admin/hardware/table.serial') }}{{ trans('general.category') }}
{{ $loop->iteration }} {{ $asset->present()->name }} {{ $asset->asset_tag }} {{ $asset->serial }}
{{ $loop->parent->iteration }}.{{ $loop->iteration }}{{ $assignedAsset->present()->name }} {{ $assignedAsset->asset_tag }} {{ $assignedAsset->serial }} {{ $assignedAsset->model->category->name }} + Asset +
@endif diff --git a/resources/views/users/view.blade.php b/resources/views/users/view.blade.php index e77ab3bd2392..1a71516d54e1 100755 --- a/resources/views/users/view.blade.php +++ b/resources/views/users/view.blade.php @@ -782,7 +782,11 @@ data-bulk-form-id="#assetsBulkForm" id="userAssetsListingTable" class="table table-striped snipe-table" - data-url="{{ route('api.assets.index',['assigned_to' => e($user->id), 'assigned_type' => 'App\Models\User']) }}" + data-url="{{ route('api.assets.index', [ + 'assigned_to' => e($user->id), + 'assigned_type' => 'App\Models\User', + 'include_child_assets' => $snipeSettings->show_assigned_assets ? 'true' : 'false', + ]) }}" data-export-options='{ "fileName": "export-{{ str_slug($user->present()->fullName()) }}-assets-{{ date('Y-m-d') }}", "ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"] diff --git a/tests/Feature/Assets/Api/AssetIndexTest.php b/tests/Feature/Assets/Api/AssetIndexTest.php index c4a362d28b71..6177283d325c 100644 --- a/tests/Feature/Assets/Api/AssetIndexTest.php +++ b/tests/Feature/Assets/Api/AssetIndexTest.php @@ -171,4 +171,37 @@ public function testAssetApiIndexAdheresToCompanyScoping() ->assertResponseDoesNotContainInRows($assetA, 'asset_tag') ->assertResponseContainsInRows($assetB, 'asset_tag'); } + + public function testCanIncludeChildAssetsForGivenUser() + { + $user = User::factory()->create(); + + $parentAsset = Asset::factory()->assignedToUser($user)->create(['asset_tag' => 'parent-asset-tag']); + $childAsset = Asset::factory()->assignedToAsset($parentAsset)->create(['asset_tag' => 'child-asset-tag']); + Asset::factory()->assignedToAsset($childAsset)->create(['asset_tag' => 'grandchild-asset-tag']); + + $this->actingAsForApi(User::factory()->superuser()->create()) + ->getJson( + route('api.assets.index', [ + 'sort' => 'name', + 'order' => 'asc', + 'offset' => '0', + 'limit' => '20', + 'assigned_to' => $user->id, + 'assigned_type' => 'App\Models\User', + 'include_child_assets' => 'true', + ])) + ->assertOk() + ->assertJsonStructure([ + 'total', + 'rows', + ]) + ->assertJson(function (AssertableJson $json) { + return $json->has('rows', 3) + ->where('rows.0.asset_tag', 'parent-asset-tag') + ->where('rows.1.asset_tag', 'child-asset-tag') + ->where('rows.1.asset_tag', 'grandchild-asset-tag') + ->etc(); + }); + } } diff --git a/tests/Feature/Notifications/Email/CurrentInventoryTest.php b/tests/Feature/Notifications/Email/CurrentInventoryTest.php new file mode 100644 index 000000000000..0730ec8ad419 --- /dev/null +++ b/tests/Feature/Notifications/Email/CurrentInventoryTest.php @@ -0,0 +1,93 @@ +actingAs(User::factory()->create()) + ->post(route('users.email', User::factory()->create()->id)) + ->assertForbidden(); + } + + public function test_handles_attempt_to_send_to_user_without_email_address() + { + $user = User::factory()->create(['email' => null]); + + $this->actingAs(User::factory()->viewUsers()->create()) + ->post(route('users.email', $user->id)) + ->assertSessionHas('error'); + } + + public function test_can_send_users_current_inventory() + { + Notification::fake(); + + $user = User::factory()->create(['email' => 'hi@there.com']); + + $this->actingAs(User::factory()->viewUsers()->create()) + ->post(route('users.email', $user->id)) + ->assertSessionHas('success'); + + Notification::assertCount(1); + Notification::assertSentTo($user, CurrentInventory::class); + } + + public function test_current_inventory_contents() + { + $user = User::factory()->create(); + + Asset::factory()->assignedToUser($user)->create(['asset_tag' => 'complex-asset-tag']); + Accessory::factory()->checkedOutToUser($user)->create(['name' => 'Complex Accessory Name']); + LicenseSeat::factory()->for(License::factory()->state(['name' => 'Complex License Name']))->assignedToUser($user)->create(); + Consumable::factory()->checkedOutToUser($user)->create(['name' => 'Complex Consumable Name']); + + $emailContents = (new CurrentInventory($user))->toMail()->render(); + + $this->assertStringContainsString('complex-asset-tag', $emailContents); + $this->assertStringContainsString('Complex Accessory Name', $emailContents); + $this->assertStringContainsString('Complex License Name', $emailContents); + $this->assertStringContainsString('Complex Consumable Name', $emailContents); + } + + public function test_current_inventory_does_not_include_child_assets_when_disabled() + { + $this->settings->disableShowingAssignedAssets(); + + $user = User::factory()->create(); + + $parentAsset = Asset::factory()->assignedToUser($user)->create(['asset_tag' => 'parent-asset-tag']); + Asset::factory()->assignedToAsset($parentAsset)->create(['asset_tag' => 'child-asset-tag']); + + $emailContents = (new CurrentInventory($user))->toMail()->render(); + + $this->assertStringContainsString('parent-asset-tag', $emailContents); + $this->assertStringNotContainsString('child-asset-tag', $emailContents); + } + + public function test_current_inventory_includes_child_assets_when_enabled() + { + $this->settings->enableShowingAssignedAssets(); + + $user = User::factory()->create(); + + $parentAsset = Asset::factory()->assignedToUser($user)->create(['asset_tag' => 'parent-asset-tag']); + Asset::factory()->assignedToAsset($parentAsset)->create(['asset_tag' => 'child-asset-tag']); + + $emailContents = (new CurrentInventory($user))->toMail()->render(); + + $this->assertStringContainsString('parent-asset-tag', $emailContents); + $this->assertStringContainsString('child-asset-tag', $emailContents); + } +} diff --git a/tests/Feature/Users/Ui/PrintUserInventoryTest.php b/tests/Feature/Users/Ui/PrintUserInventoryTest.php index 4de7c7cddde4..78a5e386c9bc 100644 --- a/tests/Feature/Users/Ui/PrintUserInventoryTest.php +++ b/tests/Feature/Users/Ui/PrintUserInventoryTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature\Users\Ui; +use App\Models\Asset; use App\Models\Company; use App\Models\User; use Tests\TestCase; @@ -38,4 +39,36 @@ public function testCannotPrintUserInventoryFromAnotherCompany() ->get(route('users.print', $user)) ->assertStatus(302); } + + public function testPrintingUserInventoryDoesNotIncludeChildAssetsWhenDisabled() + { + $this->settings->disableShowingAssignedAssets(); + + $actor = User::factory()->viewUsers()->create(); + $user = User::factory()->create(); + + $parentAsset = Asset::factory()->assignedToUser($user)->create(['asset_tag' => 'parent-asset-tag']); + Asset::factory()->assignedToAsset($parentAsset)->create(['asset_tag' => 'child-asset-tag']); + + $this->actingAs($actor) + ->get(route('users.print', $user->id)) + ->assertSeeText('parent-asset-tag') + ->assertDontSeeText('child-asset-tag'); + } + + public function testPrintingUserInventoryIncludesChildAssetsWhenEnabled() + { + $this->settings->enableShowingAssignedAssets(); + + $actor = User::factory()->viewUsers()->create(); + $user = User::factory()->create(); + + $parentAsset = Asset::factory()->assignedToUser($user)->create(['asset_tag' => 'parent-asset-tag']); + Asset::factory()->assignedToAsset($parentAsset)->create(['asset_tag' => 'child-asset-tag']); + + $this->actingAs($actor) + ->get(route('users.print', $user->id)) + ->assertSeeText('parent-asset-tag') + ->assertSeeText('child-asset-tag'); + } } diff --git a/tests/Support/Settings.php b/tests/Support/Settings.php index a4c6e65ccc7a..78c6944a4ce6 100644 --- a/tests/Support/Settings.php +++ b/tests/Support/Settings.php @@ -145,6 +145,21 @@ public function enableBadPasswordLdap(): Settings 'ldap_basedn' => 'CN=Users,DC=ad,DC=example,Dc=com' ]); } + + public function enableShowingAssignedAssets(): Settings + { + return $this->update([ + 'show_assigned_assets' => 1, + ]); + } + + public function disableShowingAssignedAssets(): Settings + { + return $this->update([ + 'show_assigned_assets' => 0, + ]); + } + public function setEula($text = 'Default EULA text') { return $this->update(['default_eula_text' => $text]);