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') }}
- {{ 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') }} | |
@foreach($assets as $asset)
+ {{ $loop->iteration }} |
{{ $asset->present()->name }} |
{{ $asset->asset_tag }} |
{{ $asset->serial }} |
@@ -24,6 +25,22 @@
@endif
+@if ($show_assigned_assets && $asset->assignedAssets->count())
+@foreach($asset->assignedAssets as $assignedAsset)
+
+ {{ $loop->parent->iteration }}.{{ $loop->iteration }} |
+ {{ $assignedAsset->present()->name }} |
+ {{ $assignedAsset->asset_tag }} |
+ {{ $assignedAsset->serial }} |
+ {{ $assignedAsset->model->category->name }} |
+ @if (($snipeSettings->show_images_in_email =='1') && $assignedAsset->getImageUrl())
+
+
+ |
+ @endif
+
+@endforeach
+@endif
@endforeach
@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]);