Merge remote-tracking branch 'origin/develop'

This commit is contained in:
snipe 2023-09-05 16:08:17 +01:00
commit 521fcd45b0
16 changed files with 240 additions and 6 deletions

View file

@ -905,6 +905,7 @@ class AssetsController extends Controller
$asset->expected_checkin = null;
$asset->last_checkout = null;
$asset->last_checkin = now();
$asset->assigned_to = null;
$asset->assignedTo()->disassociate($asset);
$asset->accepted = null;

View file

@ -68,6 +68,7 @@ class AssetCheckinController extends Controller
$asset->expected_checkin = null;
$asset->last_checkout = null;
$asset->last_checkin = now();
$asset->assigned_to = null;
$asset->assignedTo()->disassociate($asset);
$asset->assigned_type = null;

View file

@ -545,6 +545,10 @@ class ReportsController extends Controller
$header[] = trans('admin/hardware/table.checkout_date');
}
if ($request->filled('checkin_date')) {
$header[] = trans('admin/hardware/table.last_checkin_date');
}
if ($request->filled('expected_checkin')) {
$header[] = trans('admin/hardware/form.expected_checkin');
}
@ -651,6 +655,14 @@ class ReportsController extends Controller
$assets->whereBetween('assets.last_checkout', [$checkout_start, $checkout_end]);
}
if (($request->filled('checkin_date_start'))) {
$assets->whereBetween('last_checkin', [
Carbon::parse($request->input('checkin_date_start'))->startOfDay(),
// use today's date is `checkin_date_end` is not provided
Carbon::parse($request->input('checkin_date_end', now()))->endOfDay(),
]);
}
if (($request->filled('expected_checkin_start')) && ($request->filled('expected_checkin_end'))) {
$assets->whereBetween('assets.expected_checkin', [$request->input('expected_checkin_start'), $request->input('expected_checkin_end')]);
}
@ -835,6 +847,12 @@ class ReportsController extends Controller
$row[] = ($asset->last_checkout) ? $asset->last_checkout : '';
}
if ($request->filled('checkin_date')) {
$row[] = ($asset->last_checkin)
? Carbon::parse($asset->last_checkin)->format('Y-m-d')
: '';
}
if ($request->filled('expected_checkin')) {
$row[] = ($asset->expected_checkin) ? $asset->expected_checkin : '';
}

View file

@ -161,6 +161,7 @@ class ActionlogsTransformer
{ $location = Location::withTrashed()->get();
$supplier = Supplier::withTrashed()->get();
$model = AssetModel::withTrashed()->get();
$company = Company::withTrashed()->get();
if(array_key_exists('rtd_location_id',$clean_meta)) {
@ -177,17 +178,24 @@ class ActionlogsTransformer
}
if(array_key_exists('model_id', $clean_meta)) {
$clean_meta['model_id']['old'] = "[id: ".$clean_meta['model_id']['old']."] ".$model->find($clean_meta['model_id']['old'])->name;
$clean_meta['model_id']['new'] = "[id: ".$clean_meta['model_id']['new']."] ".$model->find($clean_meta['model_id']['new'])->name; /* model is required at asset creation */
$oldModel = $model->find($clean_meta['model_id']['old']);
$oldModelName = $oldModel->name ?? trans('admin/models/message.deleted');
$newModel = $model->find($clean_meta['model_id']['new']);
$newModelName = $newModel->name ?? trans('admin/models/message.deleted');
$clean_meta['model_id']['old'] = "[id: ".$clean_meta['model_id']['old']."] ".$oldModelName;
$clean_meta['model_id']['new'] = "[id: ".$clean_meta['model_id']['new']."] ".$newModelName; /** model is required at asset creation */
$clean_meta['Model'] = $clean_meta['model_id'];
unset($clean_meta['model_id']);
}
if(array_key_exists('company_id', $clean_meta)) {
$oldCompany = Company::find($clean_meta['company_id']['old']);
$oldCompany = $company->find($clean_meta['company_id']['old']);
$oldCompanyName = $oldCompany->name ?? trans('admin/companies/message.deleted');
$newCompany = Company::find($clean_meta['company_id']['new']);
$newCompany = $company->find($clean_meta['company_id']['new']);
$newCompanyName = $newCompany->name ?? trans('admin/companies/message.deleted');
$clean_meta['company_id']['old'] = $clean_meta['company_id']['old'] ? "[id: ".$clean_meta['company_id']['old']."] ". $oldCompanyName : trans('general.unassigned');
@ -196,8 +204,15 @@ class ActionlogsTransformer
unset($clean_meta['company_id']);
}
if(array_key_exists('supplier_id', $clean_meta)) {
$clean_meta['supplier_id']['old'] = $clean_meta['supplier_id']['old'] ? "[id: ".$clean_meta['supplier_id']['old']."] ".$supplier->find($clean_meta['supplier_id']['old'])->name : trans('general.unassigned');
$clean_meta['supplier_id']['new'] = $clean_meta['supplier_id']['new'] ? "[id: ".$clean_meta['supplier_id']['new']."] ".$supplier->find($clean_meta['supplier_id']['new'])->name : trans('general.unassigned');
$oldSupplier = $supplier->find($clean_meta['supplier_id']['old']);
$oldSupplierName = $oldSupplier->name ?? trans('admin/suppliers/message.deleted');
$newSupplier = $supplier->find($clean_meta['supplier_id']['new']);
$newSupplierName = $newSupplier->name ?? trans('admin/suppliers/message.deleted');
$clean_meta['supplier_id']['old'] = $clean_meta['supplier_id']['old'] ? "[id: ".$clean_meta['supplier_id']['old']."] ". $oldSupplierName : trans('general.unassigned');
$clean_meta['supplier_id']['new'] = $clean_meta['supplier_id']['new'] ? "[id: ".$clean_meta['supplier_id']['new']."] ". $newSupplierName : trans('general.unassigned');
$clean_meta['Supplier'] = $clean_meta['supplier_id'];
unset($clean_meta['supplier_id']);
}

View file

@ -73,6 +73,7 @@ class Asset extends Depreciable
protected $casts = [
'purchase_date' => 'date',
'last_checkout' => 'datetime',
'last_checkin' => 'datetime',
'expected_checkin' => 'date',
'last_audit_date' => 'datetime',
'next_audit_date' => 'date',

View file

@ -750,4 +750,26 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
{
return $this->locale;
}
public function getUserTotalCost(){
$asset_cost= 0;
$license_cost= 0;
$accessory_cost= 0;
foreach ($this->assets as $asset){
$asset_cost += $asset->purchase_cost;
$this->asset_cost = $asset_cost;
}
foreach ($this->licenses as $license){
$license_cost += $license->purchase_cost;
$this->license_cost = $license_cost;
}
foreach ($this->accessories as $accessory){
$accessory_cost += $accessory->purchase_cost;
$this->accessory_cost = $accessory_cost;
}
$this->total_user_cost = ($asset_cost + $accessory_cost + $license_cost);
return $this;
}
}

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddLastCheckinToAssets extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('assets', function (Blueprint $table) {
$table->dateTime('last_checkin')->after('last_checkout')->nullable();
});
DB::statement(
"UPDATE " . DB::getTablePrefix() . "assets SET last_checkin=(SELECT MAX(" . DB::getTablePrefix() . "action_logs.action_date) FROM " . DB::getTablePrefix() . "action_logs WHERE item_type='App\\\Models\\\Asset' AND " . DB::getTablePrefix() . "action_logs.item_id=" . DB::getTablePrefix() . "assets.id AND " . DB::getTablePrefix() . "action_logs.action_type='checkin from')"
);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('assets', function (Blueprint $table) {
$table->dropColumn('last_checkin');
});
}
}

View file

@ -14,6 +14,7 @@ return [
'dl_csv' => 'Download CSV',
'eol' => 'EOL',
'id' => 'ID',
'last_checkin_date' => 'Last Checkin Date',
'location' => 'Location',
'purchase_cost' => 'Cost',
'purchase_date' => 'Purchased',

View file

@ -2,6 +2,7 @@
return array(
'deleted' => 'Deleted asset model',
'does_not_exist' => 'Model does not exist.',
'no_association' => 'WARNING! The asset model for this item is invalid or missing!',
'no_association_fix' => 'This will break things in weird and horrible ways. Edit this asset now to assign it a model.',

View file

@ -2,6 +2,7 @@
return array(
'deleted' => 'Deleted supplier',
'does_not_exist' => 'Supplier does not exist.',

View file

@ -29,6 +29,7 @@ return array(
'show_deleted' => 'Show Deleted Users',
'title' => 'Title',
'to_restore_them' => 'to restore them.',
'total_assets_cost' => "Total Assets Cost",
'updateuser' => 'Update User',
'username' => 'Username',
'user_deleted_text' => 'This user has been marked as deleted.',

View file

@ -141,6 +141,11 @@
{{ trans('admin/hardware/table.checkout_date') }}
</label>
<label class="form-control">
{{ Form::checkbox('checkin_date', '1', '1') }}
{{ trans('admin/hardware/table.last_checkin_date') }}
</label>
<label class="form-control">
{{ Form::checkbox('expected_checkin', '1', '1') }}
{{ trans('admin/hardware/form.expected_checkin') }}
@ -289,6 +294,16 @@
</div>
</div>
<!-- Last Checkin Date -->
<div class="form-group checkin-range">
<label for="checkin_date" class="col-md-3 control-label">{{ trans('admin/hardware/table.last_checkin_date') }}</label>
<div class="input-daterange input-group col-md-6" id="datepicker">
<input type="text" class="form-control" name="checkin_date_start" aria-label="checkin_date_start">
<span class="input-group-addon">{{ strtolower(trans('general.to')) }}</span>
<input type="text" class="form-control" name="checkin_date_end" aria-label="checkin_date_end">
</div>
</div>
<!-- Expected Checkin Date -->
<div class="form-group expected_checkin-range">
<label for="expected_checkin_start" class="col-md-3 control-label">{{ trans('admin/hardware/form.expected_checkin') }}</label>
@ -381,6 +396,13 @@
format: 'yyyy-mm-dd'
});
$('.checkin-range .input-daterange').datepicker({
clearBtn: true,
todayHighlight: true,
endDate: '0d',
format: 'yyyy-mm-dd'
});
$('.expected_checkin-range .input-daterange').datepicker({
clearBtn: true,
todayHighlight: true,

View file

@ -638,6 +638,27 @@
</div>
@endif
<div class="row">
<div class="col-md-3">
{{ trans('admin/users/table.total_assets_cost') }}
</div>
<div class="col-md-9">
{{Helper::formatCurrencyOutput($user->getUserTotalCost()->total_user_cost)}}
<a id="optional_info" class="text-primary">
<i class="fa fa-caret-right fa-2x" id="optional_info_icon"></i>
<strong>{{ trans('admin/hardware/form.optional_infos') }}</strong>
</a>
</div>
<div id="optional_details" class="col-md-12" style="display:none">
<div class="col-md-3" style="border-top:none;"></div>
<div class="col-md-9" style="border-top:none;">
{{trans('general.assets').': '. Helper::formatCurrencyOutput($user->getUserTotalCost()->asset_cost)}}<br>
{{trans('general.licenses').': '. Helper::formatCurrencyOutput($user->getUserTotalCost()->license_cost)}}<br>
{{trans('general.accessories').': '.Helper::formatCurrencyOutput($user->getUserTotalCost()->accessory_cost)}}<br>
</div>
</div>
</div>
</div> <!--/end striped container-->
</div> <!-- end col-md-9 -->
@ -1109,6 +1130,12 @@ $(function () {
}
});
$("#optional_info").on("click",function(){
$('#optional_details').fadeToggle(100);
$('#optional_info_icon').toggleClass('fa-caret-right fa-caret-down');
var optional_info_open = $('#optional_info_icon').hasClass('fa-caret-down');
document.cookie = "optional_info_open="+optional_info_open+'; path=/';
});
});
</script>

View file

@ -0,0 +1,30 @@
<?php
namespace Tests\Feature\Api\Assets;
use App\Models\Asset;
use App\Models\User;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class AssetCheckinTest extends TestCase
{
use InteractsWithSettings;
public function testLastCheckInFieldIsSetOnCheckin()
{
$admin = User::factory()->superuser()->create();
$asset = Asset::factory()->create(['last_checkin' => null]);
$asset->checkOut(User::factory()->create(), $admin, now());
$this->actingAsForApi($admin)
->postJson(route('api.asset.checkin', $asset))
->assertOk();
$this->assertNotNull(
$asset->fresh()->last_checkin,
'last_checkin field should be set on checkin'
);
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace Tests\Feature\Assets;
use App\Models\Asset;
use App\Models\User;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class AssetCheckinTest extends TestCase
{
use InteractsWithSettings;
public function testLastCheckInFieldIsSetOnCheckin()
{
$admin = User::factory()->superuser()->create();
$asset = Asset::factory()->create(['last_checkin' => null]);
$asset->checkOut(User::factory()->create(), $admin, now());
$this->actingAs($admin)
->post(route('hardware.checkin.store', [
'assetId' => $asset->id,
]))
->assertRedirect();
$this->assertNotNull(
$asset->fresh()->last_checkin,
'last_checkin field should be set on checkin'
);
}
}

View file

@ -107,4 +107,29 @@ class CustomReportTest extends TestCase
->assertDontSeeTextInStreamedResponse('Asset A')
->assertSeeTextInStreamedResponse('Asset B');
}
public function testCanLimitAssetsByLastCheckIn()
{
Asset::factory()->create(['name' => 'Asset A', 'last_checkin' => '2023-08-01']);
Asset::factory()->create(['name' => 'Asset B', 'last_checkin' => '2023-08-02']);
Asset::factory()->create(['name' => 'Asset C', 'last_checkin' => '2023-08-03']);
Asset::factory()->create(['name' => 'Asset D', 'last_checkin' => '2023-08-04']);
Asset::factory()->create(['name' => 'Asset E', 'last_checkin' => '2023-08-05']);
$this->actingAs(User::factory()->canViewReports()->create())
->post('reports/custom', [
'asset_name' => '1',
'asset_tag' => '1',
'serial' => '1',
'checkin_date' => '1',
'checkin_date_start' => '2023-08-02',
'checkin_date_end' => '2023-08-04',
])->assertOk()
->assertHeader('content-type', 'text/csv; charset=UTF-8')
->assertDontSeeTextInStreamedResponse('Asset A')
->assertSeeTextInStreamedResponse('Asset B')
->assertSeeTextInStreamedResponse('Asset C')
->assertSeeTextInStreamedResponse('Asset D')
->assertDontSeeTextInStreamedResponse('Asset E');
}
}