This commit is contained in:
Spencer Long 2025-03-05 17:19:34 +00:00 committed by GitHub
commit e2680bd0e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 979 additions and 771 deletions

View file

@ -0,0 +1,40 @@
<?php
namespace App\Actions\Assets;
use App\Events\CheckoutableCheckedIn;
use App\Models\Asset;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Exception;
class DestroyAssetAction
{
public static function run(Asset $asset)
{
if ($asset->assignedTo) {
$target = $asset->assignedTo;
$checkin_at = date('Y-m-d H:i:s');
$originalValues = $asset->getRawOriginal();
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), 'Checkin on delete', $checkin_at, $originalValues));
DB::table('assets')
->where('id', $asset->id)
->update(['assigned_to' => null]);
}
if ($asset->image) {
try {
Storage::disk('public')->delete('assets'.'/'.$asset->image);
} catch (Exception $e) {
Log::debug($e);
}
}
$asset->delete();
}
}

View file

@ -0,0 +1,162 @@
<?php
namespace App\Actions\Assets;
use App\Exceptions\CheckoutNotAllowed;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Company;
use App\Models\Location;
use App\Models\Setting;
use App\Models\SnipeModel;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Gate;
class StoreAssetAction
{
/**
* @throws CheckoutNotAllowed
*/
public static function run(
$model_id,
$status_id,//
ImageUploadRequest $request, //temp for handleImages - i'd like to see that moved to a helper or something - or maybe just invoked at the extended request level so that it doesn't need to be done in the action?
$name = null,
$serial = null,
$company_id = null,
$asset_tag = null,
$order_number = null,
$notes = null,
$warranty_months = null,
$purchase_cost = null,
$asset_eol_date = null,
$purchase_date = null,
$assigned_to = null,
$supplier_id = null,
$requestable = null,
$rtd_location_id = null,
$location_id = null,
$byod = 0,
$assigned_user = null,
$assigned_asset = null,
$assigned_location = null,
$last_audit_date = null,
$next_audit_date = null,
): Asset|bool
{
$settings = Setting::getSettings();
// initial setting up of asset
$asset = new Asset();
$asset->model()->associate(AssetModel::find($model_id));
$asset->name = $name;
$asset->serial = $serial;
$asset->asset_tag = $asset_tag;
$asset->company_id = Company::getIdForCurrentUser($company_id);
$asset->model_id = $model_id;
$asset->order_number = $order_number;
$asset->notes = $notes;
$asset->created_by = auth()->id();
$asset->status_id = $status_id;
$asset->warranty_months = $warranty_months;
$asset->purchase_cost = $purchase_cost;
$asset->purchase_date = $purchase_date;
$asset->asset_eol_date = $asset_eol_date;
$asset->assigned_to = $assigned_to;
$asset->supplier_id = $supplier_id;
$asset->requestable = $requestable;
$asset->rtd_location_id = $rtd_location_id;
$asset->byod = $byod;
$asset->last_audit_date = $last_audit_date;
$asset->next_audit_date = $next_audit_date;
$asset->location_id = $location_id;
// set up next audit date
if (!empty($settings->audit_interval) && is_null($next_audit_date)) {
$asset->next_audit_date = Carbon::now()->addMonths($settings->audit_interval)->toDateString();
}
// Set location_id to rtd_location_id ONLY if the asset isn't being checked out
if (!$assigned_user && !$assigned_asset && !$assigned_location) {
$asset->location_id = $rtd_location_id;
}
$asset = self::handleImages($request, $asset);
$model = AssetModel::find($model_id);
self::handleCustomFields($model, $request, $asset);
$asset->save();
if (request('assigned_user')) {
$target = User::find(request('assigned_user'));
// the api doesn't have these location-y bits - good reason?
$location = $target->location_id;
} elseif (request('assigned_asset')) {
$target = Asset::find(request('assigned_asset'));
$location = $target->location_id;
} elseif (request('assigned_location')) {
$target = Location::find(request('assigned_location'));
$location = $target->id;
}
if (isset($target)) {
self::handleCheckout($target, $asset, $request, $location);
}
//this was in api and not gui
if ($asset->image) {
$asset->image = $asset->getImageUrl();
}
return $asset;
}
/**
* @param $model
* @param ImageUploadRequest $request
* @param Asset|\App\Models\SnipeModel $asset
* @return void
*/
private static function handleCustomFields($model, ImageUploadRequest $request, $asset): void
{
if (($model) && ($model instanceof AssetModel) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) {
if ($field->field_encrypted == '1') {
if (Gate::allows('assets.view.encrypted_custom_fields')) {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
} else {
$asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column));
}
}
} else {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = implode(', ', $request->input($field->db_column));
} else {
$asset->{$field->db_column} = $request->input($field->db_column);
}
}
}
}
}
private static function handleImages($request, $asset)
{
//api
if ($request->has('image_source')) {
$request->offsetSet('image', $request->offsetGet('image_source'));
}
if ($request->has('image')) {
$asset = $request->handleImages($asset);
}
return $asset;
}
private static function handleCheckout($target, $asset, $request, $location): void
{
$asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), $request->input('expected_checkin', null), 'Checked out on asset creation', $request->get('name'), $location);
}
}

View file

@ -0,0 +1,259 @@
<?php
namespace App\Actions\Assets;
use App\Events\CheckoutableCheckedIn;
use App\Exceptions\CheckoutNotAllowed;
use App\Exceptions\CustomFieldPermissionException;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Company;
use App\Models\Location;
use App\Models\SnipeModel;
use App\Models\Statuslabel;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Log;
use Watson\Validating\ValidationException;
class UpdateAssetAction
{
/**
* @throws ValidationException
* @throws CustomFieldPermissionException
* @throws CheckoutNotAllowed
*/
public static function run(
Asset $asset,
ImageUploadRequest $request, //very much would like this to go away
$status_id = null,
$warranty_months = null,
$purchase_cost = null,
$purchase_date = null,
$last_audit_date = null,
$next_audit_date = null,
$asset_eol_date = null,
$supplier_id = null,
$expected_checkin = null,
$requestable = false,
$location_id = null,
$rtd_location_id = null,
$assigned_location = null,
$assigned_asset = null,
$assigned_user = null,
$byod = false,
$image_delete = false,
$serial = null,
$name = null,
$company_id = null,
$model_id = null,
$order_number = null,
$asset_tag = null,
$notes = null,
$isBulk = false,
): SnipeModel
{
$asset->status_id = $status_id ?? $asset->status_id;
$asset->warranty_months = $warranty_months ?? $asset->warranty_months;
$asset->purchase_cost = $purchase_cost ?? $asset->purchase_cost;
if ($request->input('null_purchase_date') === '1') {
$asset->purchase_date = null;
if (!($asset->eol_explicit)) {
$asset->asset_eol_date = null;
}
} else {
$asset->purchase_date = $purchase_date ?? $asset->purchase_date?->format('Y-m-d');
}
if ($request->input('null_next_audit_date') == '1') {
$asset->next_audit_date = null;
} else {
$asset->next_audit_date = $next_audit_date ?? $asset->next_audit_date;
}
$asset->last_audit_date = $last_audit_date ?? $asset->last_audit_date;
if ($purchase_date && !$asset_eol_date && ($asset->model->eol > 0)) {
$asset->purchase_date = $purchase_date ?? $asset->purchase_date?->format('Y-m-d');
$asset->asset_eol_date = Carbon::parse($purchase_date)->addMonths($asset->model->eol)->format('Y-m-d');
$asset->eol_explicit = false;
} elseif ($asset_eol_date) {
$asset->asset_eol_date = $asset_eol_date ?? null;
$months = Carbon::parse($asset->asset_eol_date)->diffInMonths($asset->purchase_date);
if ($asset->model->eol) {
if ($months != $asset->model->eol > 0) {
$asset->eol_explicit = true;
} else {
$asset->eol_explicit = false;
}
} else {
$asset->eol_explicit = true;
}
} elseif (!$asset_eol_date && (($asset->model->eol) == 0)) {
$asset->asset_eol_date = null;
$asset->eol_explicit = false;
}
$asset->supplier_id = $supplier_id;
if ($request->input('null_expected_checkin_date') == '1') {
$asset->expected_checkin = null;
} else {
$asset->expected_checkin = $expected_checkin ?? $asset->expected_checkin;
}
$asset->requestable = $requestable;
$asset->location_id = $location_id;
$asset->rtd_location_id = $rtd_location_id ?? $asset->rtd_location_id;
if ($request->has('model_id')) {
$asset->model()->associate(AssetModel::find($request->validated('model_id')));
}
if ($request->has('company_id')) {
$asset->company_id = Company::getIdForCurrentUser($request->validated('company_id'));
}
if ($request->has('rtd_location_id') && !$request->has('location_id')) {
$asset->location_id = $request->validated('rtd_location_id');
}
if ($request->input('last_audit_date')) {
$asset->last_audit_date = Carbon::parse($request->input('last_audit_date'))->startOfDay()->format('Y-m-d H:i:s');
}
$asset->byod = $byod;
$status = Statuslabel::find($status_id);
// This is a non-deployable status label - we should check the asset back in.
if (($status && $status->getStatuslabelType() != 'deployable') && ($target = $asset->assignedTo)) {
$originalValues = $asset->getRawOriginal();
$asset->assigned_to = null;
$asset->assigned_type = null;
$asset->accepted = null;
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), 'Checkin on asset update', date('Y-m-d H:i:s'), $originalValues));
// reset this to null so checkout logic doesn't happen below
$target = null;
}
//this is causing an issue while setting location_id - this came from the gui but doesn't seem to work as expected in the api -
//throwing on !expectsJson for now until we can work out how to handle this better
if ($asset->assigned_to == '' && !$request->expectsJson()) {
$asset->location_id = $rtd_location_id;
}
if ($image_delete) {
try {
unlink(public_path().'/uploads/assets/'.$asset->image);
$asset->image = '';
} catch (\Exception $e) {
Log::info($e);
}
}
$asset->serial = $serial;
if ($request->filled('null_name')) {
$asset->name = null;
} else {
$asset->name = $name ?? $asset->name;
}
$asset->company_id = Company::getIdForCurrentUser($company_id);
$asset->model_id = $model_id ?? $asset->model_id;
$asset->order_number = $order_number ?? $asset->order_number;
$asset->asset_tag = $asset_tag ?? $asset->asset_tag;
$asset->notes = $notes;
$asset = $request->handleImages($asset);
self::handleCustomFields($request, $asset);
if ($isBulk) {
self::bulkLocationUpdate($asset, $request);
}
$asset->save();
// check out stuff
$location = Location::find($asset->location_id);
if (!is_null($assigned_user) && ($target = User::find($assigned_user))) {
$location = $target->location_id;
} elseif (!is_null($assigned_asset) && ($target = Asset::find($assigned_asset))) {
$location = $target->location_id;
Asset::where('assigned_type', \App\Models\Asset::class)->where('assigned_to', $asset->id)
->update(['location_id' => $target->location_id]);
} elseif (!is_null($assigned_location) && ($target = Location::find($assigned_location))) {
$location = $target->id;
}
if (isset($target)) {
self::handleCheckout($asset, $target, $request, $location);
}
if ($asset->image) {
$asset->image = $asset->getImageUrl();
}
return $asset;
}
private static function bulkLocationUpdate($asset, $request): void
{
/**
* We're changing the location ID - figure out which location we should apply
* this change to:
*
* 0 - RTD location only
* 1 - location ID and RTD location ID
* 2 - location ID only
*
* Note: this is kinda dumb and we should just use human-readable values IMHO. - snipe
*/
if ($request->filled('rtd_location_id')) {
if (($request->filled('update_real_loc')) && (($request->input('update_real_loc')) == '0')) {
$asset->rtd_location_id = $request->input('rtd_location_id');
}
if (($request->filled('update_real_loc')) && (($request->input('update_real_loc')) == '1')) {
$asset->location_id = $request->input('rtd_location_id');
$asset->rtd_location_id = $request->input('rtd_location_id');
}
if (($request->filled('update_real_loc')) && (($request->input('update_real_loc')) == '2')) {
$asset->location_id = $request->input('rtd_location_id');
}
}
}
private static function handleCustomFields($request, $asset): void
{
$model = $asset->model;
if (($model) && (isset($model->fieldset))) {
foreach ($model->fieldset->fields as $field) {
$field_val = $request->input($field->db_column, null);
if ($request->has($field->db_column)) {
if ($field->element == 'checkbox') {
if (is_array($field_val)) {
$field_val = implode(',', $field_val);
}
}
if ($field->field_encrypted == '1') {
if (Gate::allows('assets.view.encrypted_custom_fields')) {
$field_val = Crypt::encrypt($field_val);
} else {
throw new CustomFieldPermissionException();
}
}
$asset->{$field->db_column} = $field_val;
}
}
}
}
private static function handleCheckout($asset, $target, $request, $location): void
{
$asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), '', 'Checked out on asset update', e($request->get('name')), $location);
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class CustomFieldPermissionException extends Exception
{
//
}

View file

@ -2,17 +2,21 @@
namespace App\Http\Controllers\Api;
use App\Actions\Assets\DestroyAssetAction;
use App\Actions\Assets\StoreAssetAction;
use App\Actions\Assets\UpdateAssetAction;
use App\Events\CheckoutableCheckedIn;
use App\Http\Requests\StoreAssetRequest;
use App\Http\Requests\UpdateAssetRequest;
use App\Exceptions\CustomFieldPermissionException;
use App\Exceptions\CheckoutNotAllowed;
use App\Http\Requests\Assets\StoreAssetRequest;
use App\Http\Requests\Assets\UpdateAssetRequest;
use App\Http\Traits\MigratesLegacyAssetLocations;
use App\Models\AccessoryCheckout;
use App\Models\CheckoutAcceptance;
use App\Models\LicenseSeat;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Gate;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\AssetCheckoutRequest;
@ -21,7 +25,6 @@ use App\Http\Transformers\LicensesTransformer;
use App\Http\Transformers\SelectlistTransformer;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Company;
use App\Models\CustomField;
use App\Models\License;
use App\Models\Location;
@ -32,6 +35,7 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Route;
use Watson\Validating\ValidationException;
use App\View\Label;
use Illuminate\Support\Facades\Storage;
@ -127,7 +131,7 @@ class AssetsController extends Controller
}
$assets = Asset::select('assets.*')
->with(
->with([
'model',
'location',
'assetstatus',
@ -140,7 +144,7 @@ class AssetsController extends Controller
'model.manufacturer',
'model.fieldset',
'supplier'
); // it might be tempting to add 'assetlog' here, but don't. It blows up update-heavy users.
]); //it might be tempting to add 'assetlog' here, but don't. It blows up update-heavy users.
if ($filter_non_deprecable_assets) {
@ -602,87 +606,42 @@ class AssetsController extends Controller
*/
public function store(StoreAssetRequest $request): JsonResponse
{
$asset = new Asset();
$asset->model()->associate(AssetModel::find((int) $request->get('model_id')));
$asset->fill($request->validated());
$asset->created_by = auth()->id();
/**
* this is here just legacy reasons. Api\AssetController
* used image_source once to allow encoded image uploads.
*/
if ($request->has('image_source')) {
$request->offsetSet('image', $request->offsetGet('image_source'));
}
$asset = $request->handleImages($asset);
// Update custom fields in the database.
$model = AssetModel::find($request->input('model_id'));
// Check that it's an object and not a collection
// (Sometimes people send arrays here and they shouldn't
if (($model) && ($model instanceof AssetModel) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) {
// Set the field value based on what was sent in the request
$field_val = $request->input($field->db_column, null);
// If input value is null, use custom field's default value
if ($field_val == null) {
Log::debug('Field value for ' . $field->db_column . ' is null');
$field_val = $field->defaultValue($request->get('model_id'));
Log::debug('Use the default fieldset value of ' . $field->defaultValue($request->get('model_id')));
}
// if the field is set to encrypted, make sure we encrypt the value
if ($field->field_encrypted == '1') {
Log::debug('This model field is encrypted in this fieldset.');
if (Gate::allows('assets.view.encrypted_custom_fields')) {
// If input value is null, use custom field's default value
if (($field_val == null) && ($request->has('model_id') != '')) {
$field_val = Crypt::encrypt($field->defaultValue($request->get('model_id')));
} else {
$field_val = Crypt::encrypt($request->input($field->db_column));
}
}
}
if ($field->element == 'checkbox') {
if (is_array($field_val)) {
$field_val = implode(',', $field_val);
}
}
$asset->{$field->db_column} = $field_val;
}
}
if ($asset->save()) {
if ($request->get('assigned_user')) {
$target = User::find(request('assigned_user'));
} elseif ($request->get('assigned_asset')) {
$target = Asset::find(request('assigned_asset'));
} elseif ($request->get('assigned_location')) {
$target = Location::find(request('assigned_location'));
}
if (isset($target)) {
$asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), '', 'Checked out on asset creation', e($request->get('name')));
}
if ($asset->image) {
$asset->image = $asset->getImageUrl();
}
try {
$asset = StoreAssetAction::run(
model_id: $request->validated('model_id'),
status_id: $request->validated('status_id'),
request: $request, // this is for handleImages and custom fields
name: $request->validated('name'),
serial: $request->validated('serial'),
company_id: $request->validated('company_id'),
asset_tag: $request->validated('asset_tag'),
order_number: $request->validated('order_number'),
notes: $request->validated('notes'),
warranty_months: $request->validated('warranty_months'),
purchase_cost: $request->validated('purchase_cost'),
asset_eol_date: $request->validated('asset_eol_date'),
purchase_date: $request->validated('purchase_date'),
assigned_to: $request->validated('assigned_to'),
supplier_id: $request->validated('supplier_id'),
requestable: $request->validated('requestable'),
rtd_location_id: $request->validated('rtd_location_id'),
location_id: $request->validated('location_id'),
byod: $request->validated('byod'),
assigned_user: $request->validated('assigned_user'),
assigned_asset: $request->validated('assigned_asset'),
assigned_location: $request->validated('assigned_location'),
last_audit_date: $request->validated('last_audit_date'),
);
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.create.success')));
// not sure why we're not using this yet, but i know there's a reason
return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($asset), trans('admin/hardware/message.create.success')));
} catch (CheckoutNotAllowed $e) {
return response()->json(Helper::formatStandardApiResponse('error', null, $e->getMessage()), 200);
} catch (Exception $e) {
report($e);
return response()->json(Helper::formatStandardApiResponse('error', null, $e->getMessage()));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200);
}
@ -694,87 +653,19 @@ class AssetsController extends Controller
*/
public function update(UpdateAssetRequest $request, Asset $asset): JsonResponse
{
$asset->fill($request->validated());
if ($request->has('model_id')) {
$asset->model()->associate(AssetModel::find($request->validated()['model_id']));
try {
$updatedAsset = UpdateAssetAction::run($asset, $request, ...$request->validated());
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.success')));
} catch (CheckoutNotAllowed $e) {
return response()->json(Helper::formatStandardApiResponse('error', null, $e->getMessage()), 200);
} catch (ValidationException $e) {
return response()->json(Helper::formatStandardApiResponse('error', null, $e->getErrors()), 200);
} catch (CustomFieldPermissionException $e) {
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.encrypted_warning')));
} catch (Exception $e) {
report($e);
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.something_went_wrong')));
}
if ($request->has('company_id')) {
$asset->company_id = Company::getIdForCurrentUser($request->validated()['company_id']);
}
if ($request->has('rtd_location_id') && !$request->has('location_id')) {
$asset->location_id = $request->validated()['rtd_location_id'];
}
if ($request->input('last_audit_date')) {
$asset->last_audit_date = Carbon::parse($request->input('last_audit_date'))->startOfDay()->format('Y-m-d H:i:s');
}
/**
* this is here just legacy reasons. Api\AssetController
* used image_source once to allow encoded image uploads.
*/
if ($request->has('image_source')) {
$request->offsetSet('image', $request->offsetGet('image_source'));
}
$asset = $request->handleImages($asset);
$model = $asset->model;
// Update custom fields
$problems_updating_encrypted_custom_fields = false;
if (($model) && (isset($model->fieldset))) {
foreach ($model->fieldset->fields as $field) {
$field_val = $request->input($field->db_column, null);
if ($request->has($field->db_column)) {
if ($field->element == 'checkbox') {
if (is_array($field_val)) {
$field_val = implode(',', $field_val);
}
}
if ($field->field_encrypted == '1') {
if (Gate::allows('assets.view.encrypted_custom_fields')) {
$field_val = Crypt::encrypt($field_val);
} else {
$problems_updating_encrypted_custom_fields = true;
continue;
}
}
$asset->{$field->db_column} = $field_val;
}
}
}
if ($asset->save()) {
if (($request->filled('assigned_user')) && ($target = User::find($request->get('assigned_user')))) {
$location = $target->location_id;
} elseif (($request->filled('assigned_asset')) && ($target = Asset::find($request->get('assigned_asset')))) {
$location = $target->location_id;
Asset::where('assigned_type', \App\Models\Asset::class)->where('assigned_to', $asset->id)
->update(['location_id' => $target->location_id]);
} elseif (($request->filled('assigned_location')) && ($target = Location::find($request->get('assigned_location')))) {
$location = $target->id;
}
if (isset($target)) {
$asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), '', 'Checked out on asset update', e($request->get('name')), $location);
}
if ($asset->image) {
$asset->image = $asset->getImageUrl();
}
if ($problems_updating_encrypted_custom_fields) {
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.encrypted_warning')));
// Below is the *correct* return since it uses the transformer, but we have to use the old, flat return for now until we can update Jamf2Snipe and Kanji2Snipe
// return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($asset), trans('admin/hardware/message.update.encrypted_warning')));
} else {
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.success')));
// Below is the *correct* return since it uses the transformer, but we have to use the old, flat return for now until we can update Jamf2Snipe and Kanji2Snipe
/// return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($asset), trans('admin/hardware/message.update.success')));
}
}
return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200);
}
@ -785,30 +676,16 @@ class AssetsController extends Controller
* @param int $assetId
* @since [v4.0]
*/
public function destroy($id): JsonResponse
public function destroy(Asset $asset): JsonResponse
{
$this->authorize('delete', Asset::class);
if ($asset = Asset::find($id)) {
$this->authorize('delete', $asset);
if ($asset->assignedTo) {
$target = $asset->assignedTo;
$checkin_at = date('Y-m-d H:i:s');
$originalValues = $asset->getRawOriginal();
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), 'Checkin on delete', $checkin_at, $originalValues));
DB::table('assets')
->where('id', $asset->id)
->update(['assigned_to' => null]);
}
$asset->delete();
$this->authorize('delete', $asset);
try {
DestroyAssetAction::run($asset);
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/hardware/message.delete.success')));
} catch (Exception $e) {
report($e);
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.something_went_wrong')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200);
}
@ -1246,9 +1123,9 @@ class AssetsController extends Controller
/**
* Generate asset labels by tag
*
*
* @author [Nebelkreis] [https://github.com/NebelKreis]
*
*
* @param Request $request Contains asset_tags array of asset tags to generate labels for
* @return JsonResponse Returns base64 encoded PDF on success, error message on failure
*/
@ -1259,7 +1136,7 @@ class AssetsController extends Controller
// Validate that asset tags were provided in the request
if (!$request->filled('asset_tags')) {
return response()->json(Helper::formatStandardApiResponse('error', null,
return response()->json(Helper::formatStandardApiResponse('error', null,
trans('admin/hardware/message.no_assets_selected')), 400);
}
@ -1282,7 +1159,7 @@ class AssetsController extends Controller
$label = new Label();
if (!$label) {
throw new \Exception('Label object could not be created');
}

View file

@ -2,35 +2,34 @@
namespace App\Http\Controllers\Assets;
use App\Events\CheckoutableCheckedIn;
use App\Actions\Assets\DestroyAssetAction;
use App\Actions\Assets\StoreAssetAction;
use App\Actions\Assets\UpdateAssetAction;
use App\Exceptions\CheckoutNotAllowed;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\ImageUploadRequest;
use App\Http\Requests\Assets\UpdateAssetRequest;
use App\Http\Requests\Assets\StoreAssetRequest;
use App\Models\Actionlog;
use App\Http\Requests\UploadFileRequest;
use Exception;
use Illuminate\Support\Facades\Log;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\CheckoutRequest;
use App\Models\Company;
use App\Models\Location;
use App\Models\Setting;
use App\Models\Statuslabel;
use App\Models\User;
use App\View\Label;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use League\Csv\Reader;
use Illuminate\Http\Response;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use TypeError;
use Watson\Validating\ValidationException;
/**
* This class controls all actions related to assets for
@ -97,121 +96,51 @@ class AssetsController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
*/
public function store(ImageUploadRequest $request) : RedirectResponse
public function store(StoreAssetRequest $request): RedirectResponse
{
$this->authorize(Asset::class);
// There are a lot more rules to add here but prevents
// errors around `asset_tags` not being present below.
$this->validate($request, ['asset_tags' => ['required', 'array']]);
// Handle asset tags - there could be one, or potentially many.
// This is only necessary on create, not update, since bulk editing is handled
// differently
$asset_tags = $request->input('asset_tags');
$settings = Setting::getSettings();
$successes = [];
$failures = [];
$errors = [];
$asset_tags = $request->input('asset_tags');
$serials = $request->input('serials');
$asset = null;
for ($a = 1; $a <= count($asset_tags); $a++) {
$asset = new Asset();
$asset->model()->associate(AssetModel::find($request->input('model_id')));
$asset->name = $request->input('name');
// Check for a corresponding serial
if (($serials) && (array_key_exists($a, $serials))) {
$asset->serial = $serials[$a];
}
if (($asset_tags) && (array_key_exists($a, $asset_tags))) {
$asset->asset_tag = $asset_tags[$a];
}
$asset->company_id = Company::getIdForCurrentUser($request->input('company_id'));
$asset->model_id = $request->input('model_id');
$asset->order_number = $request->input('order_number');
$asset->notes = $request->input('notes');
$asset->created_by = auth()->id();
$asset->status_id = request('status_id');
$asset->warranty_months = request('warranty_months', null);
$asset->purchase_cost = request('purchase_cost');
$asset->purchase_date = request('purchase_date', null);
$asset->asset_eol_date = request('asset_eol_date', null);
$asset->assigned_to = request('assigned_to', null);
$asset->supplier_id = request('supplier_id', null);
$asset->requestable = request('requestable', 0);
$asset->rtd_location_id = request('rtd_location_id', null);
$asset->byod = request('byod', 0);
if (! empty($settings->audit_interval)) {
$asset->next_audit_date = Carbon::now()->addMonths($settings->audit_interval)->toDateString();
}
// Set location_id to rtd_location_id ONLY if the asset isn't being checked out
if (!request('assigned_user') && !request('assigned_asset') && !request('assigned_location')) {
$asset->location_id = $request->input('rtd_location_id', null);
}
// Create the image (if one was chosen.)
if ($request->has('image')) {
$asset = $request->handleImages($asset);
}
// Update custom fields in the database.
// Validation for these fields is handled through the AssetRequest form request
$model = AssetModel::find($request->get('model_id'));
if (($model) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) {
if ($field->field_encrypted == '1') {
if (Gate::allows('assets.view.encrypted_custom_fields')) {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
} else {
$asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column));
}
}
} else {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = implode(', ', $request->input($field->db_column));
} else {
$asset->{$field->db_column} = $request->input($field->db_column);
}
}
}
}
// Validate the asset before saving
if ($asset->isValid() && $asset->save()) {
if (request('assigned_user')) {
$target = User::find(request('assigned_user'));
$location = $target->location_id;
} elseif (request('assigned_asset')) {
$target = Asset::find(request('assigned_asset'));
$location = $target->location_id;
} elseif (request('assigned_location')) {
$target = Location::find(request('assigned_location'));
$location = $target->id;
}
if (isset($target)) {
$asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), $request->input('expected_checkin', null), 'Checked out on asset creation', $request->get('name'), $location);
}
$successes[] = "<a href='" . route('hardware.show', $asset) . "' style='color: white;'>" . e($asset->asset_tag) . "</a>";
} else {
$failures[] = join(",", $asset->getErrors()->all());
foreach ($asset_tags as $key => $asset_tag) {
try {
$asset = StoreAssetAction::run(
model_id: $request->validated('model_id'),
status_id: $request->validated('status_id'),
request: $request,
name: $request->validated('name'),
serial: $request->has('serials') ? $serials[$key] : null,
company_id: $request->validated('company_id'),
asset_tag: $asset_tag,
order_number: $request->validated('order_number'),
notes: $request->validated('notes'),
warranty_months: $request->validated('warranty_months'),
purchase_cost: $request->validated('purchase_cost'),
asset_eol_date: $request->validated('asset_eol_date'),
purchase_date: $request->validated('purchase_date'),
assigned_to: $request->validated('assigned_to'),
supplier_id: $request->validated('supplier_id'),
requestable: $request->validated('requestable'),
rtd_location_id: $request->validated('rtd_location_id'),
location_id: $request->validated('location_id'),
byod: $request->validated('byod'),
assigned_user: $request->validated('assigned_user'),
assigned_asset: $request->validated('assigned_asset'),
assigned_location: $request->validated('assigned_location'),
last_audit_date: $request->validated('last_audit_date'),
next_audit_date: $request->validated('next_audit_date'),
);
$successes[] = "<a href='".route('hardware.show', ['hardware' => $asset->id])."' style='color: white;'>".e($asset->asset_tag)."</a>";
} catch (ValidationException|CheckoutNotAllowed $e) {
$errors[] = $e->getMessage();
} catch (Exception $e) {
report($e);
}
}
$failures[] = join(",", $errors);
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
if ($successes) {
if ($failures) {
//some succeeded, some failed
@ -230,13 +159,12 @@ class AssetsController extends Controller
->with('success-unescaped', trans_choice('admin/hardware/message.create.multi_success_linked', $successes, ['links' => join(", ", $successes)]));
}
}
}
return redirect()->back()->withInput()->withErrors($asset->getErrors());
// this shouldn't happen, but php complains if there's no final return
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
->with('success-unescaped', trans('admin/hardware/message.create.success_linked', ['link' => route('hardware.show', $asset->id), 'id', 'tag' => e($asset->asset_tag)]));
}
/**
* Returns a view that presents a form to edit an existing asset.
*
@ -302,127 +230,50 @@ class AssetsController extends Controller
* @since [v1.0]
* @author [A. Gianotto] [<snipe@snipe.net>]
*/
public function update(ImageUploadRequest $request, Asset $asset) : RedirectResponse
public function update(UpdateAssetRequest $request, Asset $asset): RedirectResponse
{
$this->authorize($asset);
$asset->status_id = $request->input('status_id', null);
$asset->warranty_months = $request->input('warranty_months', null);
$asset->purchase_cost = $request->input('purchase_cost', null);
$asset->purchase_date = $request->input('purchase_date', null);
$asset->next_audit_date = $request->input('next_audit_date', null);
if ($request->filled('purchase_date') && !$request->filled('asset_eol_date') && ($asset->model->eol > 0)) {
$asset->purchase_date = $request->input('purchase_date', null);
$asset->asset_eol_date = Carbon::parse($request->input('purchase_date'))->addMonths($asset->model->eol)->format('Y-m-d');
$asset->eol_explicit = false;
} elseif ($request->filled('asset_eol_date')) {
$asset->asset_eol_date = $request->input('asset_eol_date', null);
$months = Carbon::parse($asset->asset_eol_date)->diffInMonths($asset->purchase_date);
if($asset->model->eol) {
if($months != $asset->model->eol > 0) {
$asset->eol_explicit = true;
} else {
$asset->eol_explicit = false;
}
} else {
$asset->eol_explicit = true;
}
} elseif (!$request->filled('asset_eol_date') && (($asset->model->eol) == 0)) {
$asset->asset_eol_date = null;
$asset->eol_explicit = false;
}
$asset->supplier_id = $request->input('supplier_id', null);
$asset->expected_checkin = $request->input('expected_checkin', null);
$asset->requestable = $request->input('requestable', 0);
$asset->rtd_location_id = $request->input('rtd_location_id', null);
$asset->byod = $request->input('byod', 0);
$status = Statuslabel::find($request->input('status_id'));
// This is an archived or undeployable - we should check the asset back in.
// Pending is allowed here
if (($status) && (($status->getStatuslabelType() != 'pending') && ($status->getStatuslabelType() != 'deployable')) && ($target = $asset->assignedTo)) {
$originalValues = $asset->getRawOriginal();
$asset->assigned_to = null;
$asset->assigned_type = null;
$asset->accepted = null;
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), 'Checkin on asset update with '.$status->getStatuslabelType().' status', date('Y-m-d H:i:s'), $originalValues));
}
if ($asset->assigned_to == '') {
$asset->location_id = $request->input('rtd_location_id', null);
}
if ($request->filled('image_delete')) {
try {
unlink(public_path().'/uploads/assets/'.$asset->image);
$asset->image = '';
} catch (\Exception $e) {
Log::info($e);
try {
$serial = $request->input('serials');
$asset_tag = $request->input('asset_tags');
if (is_array($request->input('serials'))) {
$serial = $request->input('serials')[1];
}
}
// Update the asset data
$serial = $request->input('serials');
$asset->serial = $request->input('serials');
if (is_array($request->input('serials'))) {
$asset->serial = $serial[1];
}
$asset->name = $request->input('name');
$asset->company_id = Company::getIdForCurrentUser($request->input('company_id'));
$asset->model_id = $request->input('model_id');
$asset->order_number = $request->input('order_number');
$asset_tags = $request->input('asset_tags');
$asset->asset_tag = $request->input('asset_tags');
if (is_array($request->input('asset_tags'))) {
$asset->asset_tag = $asset_tags[1];
}
$asset->notes = $request->input('notes');
$asset = $request->handleImages($asset);
// Update custom fields in the database.
// Validation for these fields is handlded through the AssetRequest form request
// FIXME: No idea why this is returning a Builder error on db_column_name.
// Need to investigate and fix. Using static method for now.
$model = AssetModel::find($request->get('model_id'));
if (($model) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) {
if ($field->field_encrypted == '1') {
if (Gate::allows('assets.view.encrypted_custom_fields')) {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
} else {
$asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column));
}
}
} else {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = implode(', ', $request->input($field->db_column));
} else {
$asset->{$field->db_column} = $request->input($field->db_column);
}
}
if (is_array($request->input('asset_tags'))) {
$asset_tag = $request->input('asset_tags')[1];
}
}
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
if ($asset->save()) {
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
$updatedAsset = UpdateAssetAction::run(
asset: $asset,
request: $request,
status_id: $request->validated('status_id'),
warranty_months: $request->validated('warranty_months'),
purchase_cost: $request->validated('purchase_cost'),
purchase_date: $request->validated('purchase_date'),
next_audit_date: $request->validated('next_audit_date'),
asset_eol_date: $request->validated('asset_eol_date'),
supplier_id: $request->validated('supplier_id'),
expected_checkin: $request->validated('expected_checkin'),
requestable: $request->validated('requestable'),
rtd_location_id: $request->validated('rtd_location_id'),
byod: $request->validated('byod'),
image_delete: $request->validated('image_delete'),
serial: $serial, // this needs to be set up in request somehow
name: $request->validated('name'),
company_id: $request->validated('company_id'),
model_id: $request->validated('model_id'),
order_number: $request->validated('order_number'),
asset_tag: $asset_tag, // same as serials
notes: $request->validated('notes'),
);
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
return redirect()->to(Helper::getRedirectOption($request, $updatedAsset->id, 'Assets'))
->with('success', trans('admin/hardware/message.update.success'));
} catch (ValidationException $e) {
return redirect()->back()->withInput()->withErrors($e->getErrors());
} catch (Exception $e) {
report($e);
return redirect()->back()->with('error', trans('admin/hardware/message.update.error'));
}
return redirect()->back()->withInput()->withErrors($asset->getErrors());
}
/**
@ -432,39 +283,16 @@ class AssetsController extends Controller
* @param int $assetId
* @since [v1.0]
*/
public function destroy(Request $request, $assetId) : RedirectResponse
public function destroy(Asset $asset): RedirectResponse
{
// Check if the asset exists
if (is_null($asset = Asset::find($assetId))) {
// Redirect to the asset management page with error
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
$this->authorize('delete', $asset);
if ($asset->assignedTo) {
$target = $asset->assignedTo;
$checkin_at = date('Y-m-d H:i:s');
$originalValues = $asset->getRawOriginal();
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), 'Checkin on delete', $checkin_at, $originalValues));
DB::table('assets')
->where('id', $asset->id)
->update(['assigned_to' => null]);
try {
DestroyAssetAction::run($asset);
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.delete.success'));
} catch (Exception $e) {
report($e);
return redirect()->back()->withInput()->withErrors($e->getMessage());
}
if ($asset->image) {
try {
Storage::disk('public')->delete('assets'.'/'.$asset->image);
} catch (\Exception $e) {
Log::debug($e);
}
}
$asset->delete();
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.delete.success'));
}
/**
@ -578,7 +406,7 @@ class AssetsController extends Controller
file_put_contents($barcode_file, $barcode_obj->getPngData());
return response($barcode_obj->getPngData())->header('Content-type', 'image/png');
} catch (\Exception|TypeError $e) {
} catch (Exception $e) {
Log::debug('The barcode format is invalid.');
return response(file_get_contents(public_path('uploads/barcodes/invalid_barcode.gif')))->header('Content-type', 'image/gif');
@ -680,7 +508,7 @@ class AssetsController extends Controller
$isCheckinHeaderExplicit = in_array('checkin date', (array_map('strtolower', $header)));
try {
$results = $csv->getRecords();
} catch (\Exception $e) {
} catch (Exception $e) {
return back()->with('error', trans('general.error_in_import_file', ['error' => $e->getMessage()]));
}
$item = [];

View file

@ -2,9 +2,12 @@
namespace App\Http\Controllers\Assets;
use App\Actions\Assets\UpdateAssetAction;
use App\Exceptions\CustomFieldPermissionException;
use App\Helpers\Helper;
use App\Http\Controllers\CheckInOutRequest;
use App\Http\Controllers\Controller;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Statuslabel;
@ -21,6 +24,7 @@ use App\Models\CustomField;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Watson\Validating\ValidationException;
class BulkAssetsController extends Controller
{
@ -199,302 +203,59 @@ class BulkAssetsController extends Controller
* @internal param array $assets
* @since [v2.0]
*/
public function update(Request $request) : RedirectResponse
public function update(ImageUploadRequest $request): RedirectResponse
{
// this should be in request, but request weird, need to think it through a little
$this->authorize('update', Asset::class);
$has_errors = 0;
$error_array = array();
// Get the back url from the session and then destroy the session
$bulk_back_url = route('hardware.index');
$custom_field_problem = false;
// is this necessary?
if (!$request->filled('ids') || count($request->input('ids')) == 0) {
return redirect($bulk_back_url)->with('error', trans('admin/hardware/message.update.no_assets_selected'));
}
if ($request->session()->has('bulk_back_url')) {
$bulk_back_url = $request->session()->pull('bulk_back_url');
}
$custom_field_columns = CustomField::all()->pluck('db_column')->toArray();
if (! $request->filled('ids') || count($request->input('ids')) == 0) {
return redirect($bulk_back_url)->with('error', trans('admin/hardware/message.update.no_assets_selected'));
}
// find and update assets
$assets = Asset::whereIn('id', $request->input('ids'))->get();
/**
* If ANY of these are filled, prepare to update the values on the assets.
*
* Additional checks will be needed for some of them to make sure the values
* make sense (for example, changing the status ID to something incompatible with
* its checkout status.
*/
if (($request->filled('name'))
|| ($request->filled('purchase_date'))
|| ($request->filled('expected_checkin'))
|| ($request->filled('purchase_cost'))
|| ($request->filled('supplier_id'))
|| ($request->filled('order_number'))
|| ($request->filled('warranty_months'))
|| ($request->filled('rtd_location_id'))
|| ($request->filled('requestable'))
|| ($request->filled('company_id'))
|| ($request->filled('status_id'))
|| ($request->filled('model_id'))
|| ($request->filled('next_audit_date'))
|| ($request->filled('asset_eol_date'))
|| ($request->filled('null_name'))
|| ($request->filled('null_purchase_date'))
|| ($request->filled('null_expected_checkin_date'))
|| ($request->filled('null_next_audit_date'))
|| ($request->filled('null_asset_eol_date'))
|| ($request->anyFilled($custom_field_columns))
) {
// Let's loop through those assets and build an update array
foreach ($assets as $asset) {
$this->update_array = [];
/**
* Leave out model_id and status here because we do math on that later. We have to do some
* extra validation and checks on those two.
*
* It's tempting to make these match the request check above, but some of these values require
* extra work to make sure the data makes sense.
*/
$this->conditionallyAddItem('name')
->conditionallyAddItem('purchase_date')
->conditionallyAddItem('expected_checkin')
->conditionallyAddItem('order_number')
->conditionallyAddItem('requestable')
->conditionallyAddItem('supplier_id')
->conditionallyAddItem('warranty_months')
->conditionallyAddItem('next_audit_date')
->conditionallyAddItem('asset_eol_date');
foreach ($custom_field_columns as $key => $custom_field_column) {
$this->conditionallyAddItem($custom_field_column);
}
if (!($asset->eol_explicit)) {
if ($request->filled('model_id')) {
$model = AssetModel::find($request->input('model_id'));
if ($model->eol > 0) {
if ($request->filled('purchase_date')) {
$this->update_array['asset_eol_date'] = Carbon::parse($request->input('purchase_date'))->addMonths($model->eol)->format('Y-m-d');
} else {
$this->update_array['asset_eol_date'] = Carbon::parse($asset->purchase_date)->addMonths($model->eol)->format('Y-m-d');
}
} else {
$this->update_array['asset_eol_date'] = null;
}
} elseif (($request->filled('purchase_date')) && ($asset->model->eol > 0)) {
$this->update_array['asset_eol_date'] = Carbon::parse($request->input('purchase_date'))->addMonths($asset->model->eol)->format('Y-m-d');
}
}
/**
* Blank out fields that were requested to be blanked out via checkbox
*/
if ($request->input('null_name')=='1') {
$this->update_array['name'] = null;
}
if ($request->input('null_purchase_date')=='1') {
$this->update_array['purchase_date'] = null;
if (!($asset->eol_explicit)) {
$this->update_array['asset_eol_date'] = null;
}
}
if ($request->input('null_expected_checkin_date')=='1') {
$this->update_array['expected_checkin'] = null;
}
if ($request->input('null_next_audit_date')=='1') {
$this->update_array['next_audit_date'] = null;
}
if ($request->input('null_asset_eol_date')=='1') {
$this->update_array['asset_eol_date'] = null;
// If they are nulling the EOL date to allow it to calculate, set eol explicit to 0
if ($request->input('calc_eol')=='1') {
$this->update_array['eol_explicit'] = 0;
}
}
if ($request->filled('purchase_cost')) {
$this->update_array['purchase_cost'] = $request->input('purchase_cost');
}
if ($request->filled('company_id')) {
$this->update_array['company_id'] = $request->input('company_id');
if ($request->input('company_id') == 'clear') {
$this->update_array['company_id'] = null;
}
}
/**
* We're trying to change the model ID - we need to do some extra checks here to make sure
* the custom field values work for the custom fieldset rules around this asset. Uniqueness
* and requiredness across the fieldset is particularly important, since those are
* fieldset-specific attributes.
*/
if ($request->filled('model_id')) {
$this->update_array['model_id'] = AssetModel::find($request->input('model_id'))->id;
}
/**
* We're trying to change the status ID - we need to do some extra checks here to
* make sure the status label type is one that makes sense for the state of the asset,
* for example, we shouldn't be able to make an asset archived if it's currently assigned
* to someone/something.
*/
if ($request->filled('status_id')) {
try {
$updated_status = Statuslabel::findOrFail($request->input('status_id'));
} catch (ModelNotFoundException $e) {
return redirect($bulk_back_url)->with('error', trans('admin/statuslabels/message.does_not_exist'));
}
// We cannot assign a non-deployable status type if the asset is already assigned.
// This could probably be added to a form request.
// If the asset isn't assigned, we don't care what the status is.
// Otherwise we need to make sure the status type is still a deployable one.
if (
($asset->assigned_to == '')
|| ($updated_status->deployable == '1') && ($asset->assetstatus?->deployable == '1')
) {
$this->update_array['status_id'] = $updated_status->id;
}
}
/**
* We're changing the location ID - figure out which location we should apply
* this change to:
*
* 0 - RTD location only
* 1 - location ID and RTD location ID
* 2 - location ID only
*
* Note: this is kinda dumb and we should just use human-readable values IMHO. - snipe
*/
if ($request->filled('rtd_location_id')) {
if (($request->filled('update_real_loc')) && (($request->input('update_real_loc')) == '0')) {
$this->update_array['rtd_location_id'] = $request->input('rtd_location_id');
}
if (($request->filled('update_real_loc')) && (($request->input('update_real_loc')) == '1')) {
$this->update_array['location_id'] = $request->input('rtd_location_id');
$this->update_array['rtd_location_id'] = $request->input('rtd_location_id');
}
if (($request->filled('update_real_loc')) && (($request->input('update_real_loc')) == '2')) {
$this->update_array['location_id'] = $request->input('rtd_location_id');
}
}
/**
* ------------------------------------------------------------------------------
* ANYTHING that happens past this foreach
* WILL NOT BE logged in the edit log_meta data
* ------------------------------------------------------------------------------
*/
$changed = [];
foreach ($this->update_array as $key => $value) {
if ($this->update_array[$key] != $asset->{$key}) {
$changed[$key]['old'] = $asset->{$key};
$changed[$key]['new'] = $this->update_array[$key];
}
}
/**
* Start all the custom fields shenanigans
*/
// Does the model have a fieldset?
if ($asset->model->fieldset) {
foreach ($asset->model->fieldset->fields as $field) {
if ((array_key_exists($field->db_column, $this->update_array)) && ($field->field_encrypted == '1')) {
if (Gate::allows('admin')) {
$decrypted_old = Helper::gracefulDecrypt($field, $asset->{$field->db_column});
/*
* Check if the decrypted existing value is different from one we just submitted
* and if not, pull it out of the object since it shouldn't really be updating at all.
* If we don't do this, it will try to re-encrypt it, and the same value encrypted two
* different times will have different values, so it will *look* like it was updated
* but it wasn't.
*/
if ($decrypted_old != $this->update_array[$field->db_column]) {
$asset->{$field->db_column} = Crypt::encrypt($this->update_array[$field->db_column]);
} else {
/*
* Remove the encrypted custom field from the update_array, since nothing changed
*/
unset($this->update_array[$field->db_column]);
unset($asset->{$field->db_column});
}
/*
* These custom fields aren't encrypted, just carry on as usual
*/
}
} else {
if ((array_key_exists($field->db_column, $this->update_array)) && ($asset->{$field->db_column} != $this->update_array[$field->db_column])) {
// Check if this is an array, and if so, flatten it
if (is_array($this->update_array[$field->db_column])) {
$asset->{$field->db_column} = implode(', ', $this->update_array[$field->db_column]);
} else {
$asset->{$field->db_column} = $this->update_array[$field->db_column];
}
}
}
} // endforeach
}
// Check if it passes validation, and then try to save
if (!$asset->update($this->update_array)) {
// Build the error array
foreach ($asset->getErrors()->toArray() as $key => $message) {
for ($x = 0; $x < count($message); $x++) {
$error_array[$key][] = trans('general.asset') . ' ' . $asset->id . ': ' . $message[$x];
$has_errors++;
}
}
} // end if saved
} // end asset foreach
if ($has_errors > 0) {
return redirect($bulk_back_url)->with('bulk_asset_errors', $error_array);
$errors = [];
foreach ($assets as $key => $asset) {
try {
$updatedAsset = UpdateAssetAction::run(
asset: $asset,
request: $request,
status_id: $request->input('status_id'),
warranty_months: $request->input('warranty_months'),
purchase_cost: $request->input('purchase_cost'),
purchase_date: $request->filled('null_purchase_date') ? null : $request->input('purchase_date'),
next_audit_date: $request->filled('null_next_audit_date') ? null : $request->input('next_audit_date'),
supplier_id: $request->input('supplier_id'),
expected_checkin: $request->filled('null_expected_checkin_date') ? null : $request->input('expected_checkin'),
requestable: $request->input('requestable'),
rtd_location_id: $request->input('rtd_location_id'),
name: $request->filled('null_name') ? null : $request->input('name'),
company_id: $request->input('company_id'),
model_id: $request->input('model_id'),
order_number: $request->input('order_number'),
isBulk: true,
);
} catch (ValidationException $e) {
$errors = $e->validator->errors()->toArray();
} catch (CustomFieldPermissionException $e) {
$custom_field_problem = true;
} catch (\Exception $e) {
report($e);
$errors[$key] = [trans('general.something_went_wrong')];
}
return redirect($bulk_back_url)->with('success', trans('admin/hardware/message.update.success'));
}
// no values given, nothing to update
return redirect($bulk_back_url)->with('warning', trans('admin/hardware/message.update.nothing_updated'));
if (!empty($errors)) {
return redirect($bulk_back_url)->with('bulk_asset_errors', $errors);
}
if ($custom_field_problem) {
return redirect($bulk_back_url)->with('error', trans('admin/hardware/message.update.encrypted_warning'));
}
return redirect($bulk_back_url)->with('success', trans('bulk.update.success'));
}
/**

View file

@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\Assets;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
class DestroyAssetRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return Gate::allows('delete', $this->asset);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
//
];
}
}

View file

@ -1,7 +1,8 @@
<?php
namespace App\Http\Requests;
namespace App\Http\Requests\Assets;
use App\Http\Requests\ImageUploadRequest;
use App\Http\Requests\Traits\MayContainCustomFields;
use App\Models\Asset;
use App\Models\Company;
@ -36,8 +37,10 @@ class StoreAssetRequest extends ImageUploadRequest
$this->parseLastAuditDate();
$asset_tag = $this->parseAssetTag();
$this->merge([
'asset_tag' => $this->asset_tag ?? Asset::autoincrement_asset(),
'asset_tag' => $asset_tag,
'company_id' => $idForCurrentUser,
'assigned_to' => $assigned_to ?? null,
]);
@ -60,7 +63,6 @@ class StoreAssetRequest extends ImageUploadRequest
// converted to a float via setPurchaseCostAttribute).
$modelRules = $this->removeNumericRulesFromPurchaseCost($modelRules);
}
return array_merge(
$modelRules,
['status_id' => [new AssetCannotBeCheckedOutToNondeployableStatus()]],
@ -100,4 +102,14 @@ class StoreAssetRequest extends ImageUploadRequest
return $rules;
}
private function parseAssetTag(): mixed
{
// this is for a gui request to make the request pass validation
// this just checks the first asset tag from the gui, watson should pick up if any of the rest of them fail
if ($this->has('asset_tags') && !$this->expectsJson()) {
return $this->input('asset_tags')[1];
}
return $this->asset_tag ?? Asset::autoincrement_asset();
}
}

View file

@ -1,7 +1,8 @@
<?php
namespace App\Http\Requests;
namespace App\Http\Requests\Assets;
use App\Http\Requests\ImageUploadRequest;
use App\Http\Requests\Traits\MayContainCustomFields;
use App\Models\Asset;
use App\Models\Setting;
@ -28,11 +29,21 @@ class UpdateAssetRequest extends ImageUploadRequest
*/
public function rules()
{
$modelRules = (new Asset)->getRules();
if ((Setting::getSettings()->digit_separator === '1.234,56' || '1,234.56') && is_string($this->input('purchase_cost'))) {
// If purchase_cost was submitted as a string with a comma separator
// then we need to ignore the normal numeric rules.
// Since the original rules still live on the model they will be run
// right before saving (and after purchase_cost has been
// converted to a float via setPurchaseCostAttribute).
$modelRules = $this->removeNumericRulesFromPurchaseCost($modelRules);
}
$rules = array_merge(
parent::rules(),
(new Asset)->getRules(),
$modelRules,
// this is to overwrite rulesets that include required, and rewrite unique_undeleted
[
'image_delete' => ['bool'],
'model_id' => ['integer', 'exists:models,id,deleted_at,NULL', 'not_array'],
'status_id' => ['integer', 'exists:status_labels,id'],
'asset_tag' => [
@ -47,6 +58,21 @@ class UpdateAssetRequest extends ImageUploadRequest
if (Setting::getSettings()->digit_separator === '1.234,56' && is_string($this->input('purchase_cost'))) {
$rules['purchase_cost'] = ['nullable', 'string'];
}
return $rules;
}
private function removeNumericRulesFromPurchaseCost(array $rules): array
{
$purchaseCost = $rules['purchase_cost'];
// If rule is in "|" format then turn it into an array
if (is_string($purchaseCost)) {
$purchaseCost = explode('|', $purchaseCost);
}
$rules['purchase_cost'] = array_filter($purchaseCost, function ($rule) {
return $rule !== 'numeric' && $rule !== 'gte:0';
});
return $rules;
}

View file

@ -7,6 +7,7 @@ use App\Models\Statuslabel;
use App\Models\User;
use App\Events\CheckoutableCheckedIn;
use Illuminate\Support\Facades\Crypt;
use Watson\Validating\ValidationException;
class AssetImporter extends ItemImporter
{
@ -172,26 +173,30 @@ class AssetImporter extends ItemImporter
// This sets an attribute on the Loggable trait for the action log
$asset->setImported(true);
if ($asset->save()) {
try {
$asset->save();
$this->log('Asset '.$this->item['name'].' with serial number '.$this->item['serial'].' was created');
// If we have a target to checkout to, lets do so.
//-- created_by is a property of the abstract class Importer, which this class inherits from and it's set by
//-- the class that needs to use it (command importer or GUI importer inside the project).
if (isset($target) && ($target !== false)) {
if (!is_null($asset->assigned_to)){
if (!is_null($asset->assigned_to)) {
if ($asset->assigned_to != $target->id) {
event(new CheckoutableCheckedIn($asset, User::find($asset->assigned_to), auth()->user(), 'Checkin from CSV Importer', $checkin_date));
}
}
$asset->fresh()->checkOut($target, $this->created_by, $checkout_date, null, 'Checkout from CSV Importer', $asset->name);
$asset->fresh()->checkOut($target, $this->created_by, $checkout_date, null, 'Checkout from CSV Importer', $asset->name);
}
return;
} catch (ValidationException $e) {
$this->logError($asset, 'Asset "'.$this->item['name'].'"');
} catch (\Exception $e) {
report($e);
$this->logError($asset, trans('general.something_went_wrong'));
}
$this->logError($asset, 'Asset "'.$this->item['name'].'"');
}

View file

@ -35,6 +35,8 @@ class Asset extends Depreciable
use CompanyableTrait;
use HasFactory, Loggable, Requestable, Presentable, SoftDeletes, ValidatingTrait, UniqueUndeletedTrait;
protected $throwValidationExceptions = true;
public const LOCATION = 'location';
public const ASSET = 'asset';
public const USER = 'user';

View file

@ -107,7 +107,7 @@ class UserFactory extends Factory
return User::where('permissions->superuser', '1')->first() ?? User::factory()->firstAdmin();
},
];
});
})->appendPermission(['assets.view.encrypted_custom_fields' => '1']);
}
public function viewAssets()

View file

@ -16,9 +16,9 @@ return [
'create' => [
'error' => 'Asset was not created, please try again. :(',
'success' => 'Asset created successfully. :)',
'success_linked' => 'Asset with tag :tag was created successfully. <strong><a href=":link" style="color: white;">Click here to view</a></strong>.',
'success_linked' => 'Asset with tag :tag was created successfully. <strong><a href=":link" style="color: white;">Click here to view</a></strong>.',
'multi_success_linked' => 'Asset with tag :links was created successfully.|:count assets were created succesfully. :links.',
'partial_failure' => 'An asset was unable to be created. Reason: :failures|:count assets were unable to be created. Reasons: :failures',
'partial_failure' => 'An asset was unable to be created. Reason: :failures|:count assets were unable to be created. Reasons: :failures',
],
'update' => [

View file

@ -136,7 +136,7 @@
<i class="fas fa-exclamation-triangle faa-pulse animated"></i>
<strong>{{ trans('general.notification_error') }}: </strong>
{{ trans('general.notification_bulk_error_hint') }}
@foreach($messages as $key => $message)
@foreach($messages as $key => $message)
@for ($x = 0; $x < count($message); $x++)
<ul>
<li>{{ $message[$x] }}</li>

View file

@ -596,15 +596,16 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
Route::put('/hardware/{asset}', [Api\AssetsController::class, 'update'])->name('api.assets.put-update');
Route::delete('/hardware/{asset}', [Api\AssetsController::class, 'destroy'])->name('api.assets.destroy');
Route::resource('hardware',
Api\AssetsController::class,
['names' => [
'index' => 'api.assets.index',
'show' => 'api.assets.show',
'store' => 'api.assets.store',
'destroy' => 'api.assets.destroy',
],
'except' => ['create', 'edit', 'update'],
'except' => ['create', 'edit', 'update', 'destroy'],
'parameters' => ['asset' => 'asset_id'],
]
); // end assets API routes

View file

@ -211,7 +211,7 @@ class BulkEditAssetsTest extends TestCase
$id_array = $assets->pluck('id')->toArray();
$this->actingAs(User::factory()->admin()->create())->post(route('hardware/bulksave'), [
$this->actingAs(User::factory()->superuser()->create())->post(route('hardware/bulksave'), [
'ids' => $id_array,
$encrypted->db_column => 'New Encrypted Text',
])->assertStatus(302);
@ -221,7 +221,7 @@ class BulkEditAssetsTest extends TestCase
});
}
public function testBulkEditAssetsRequiresadminToUpdateEncryptedCustomFields()
public function testBulkEditAssetsRequiresAdminToUpdateEncryptedCustomFields()
{
$this->markIncompleteIfMySQL('Custom Fields tests do not work on mysql');
$edit_user = User::factory()->editAssets()->create();

View file

@ -0,0 +1,35 @@
<?php
namespace Tests\Feature\Assets\Ui;
use App\Models\Asset;
use App\Models\User;
use Tests\TestCase;
class DeleteAssetTest extends TestCase
{
public function test_asset_can_be_deleted_with_permissions()
{
$user = User::factory()->deleteAssets()->create();
$asset = Asset::factory()->create();
$this->actingAs($user)
->delete(route('hardware.destroy', $asset))
->assertRedirect(route('hardware.index'));
$this->assertSoftDeleted($asset);
}
public function test_asset_cannot_be_deleted_without_permissions()
{
$user = User::factory()->create();
$asset = Asset::factory()->create();
$this->actingAs($user)
->delete(route('hardware.destroy', $asset))
->assertForbidden();
$this->assertModelExists($asset);
}
}

View file

@ -68,13 +68,26 @@ class EditAssetTest extends TestCase
$this->assertDatabaseHas('assets', ['asset_tag' => 'New Asset Tag']);
}
public function test_user_without_permission_is_denied()
{
$user = User::factory()->create();
$asset = Asset::factory()->create();
$this->actingAs($user)->put(route('hardware.update', $asset), [
'name' => 'New name',
'asset_tags' => 'New Asset Tag',
'status_id' => StatusLabel::factory()->create()->id,
'model_id' => AssetModel::factory()->create()->id,
])->assertForbidden();
}
public function testNewCheckinIsLoggedIfStatusChangedToUndeployable()
{
Event::fake([CheckoutableCheckedIn::class]);
$user = User::factory()->create();
$deployable_status = Statuslabel::factory()->rtd()->create();
$achived_status = Statuslabel::factory()->archived()->create();
$archived_status = Statuslabel::factory()->archived()->create();
$asset = Asset::factory()->assignedToUser($user)->create(['status_id' => $deployable_status->id]);
$this->assertTrue($asset->assignedTo->is($user));
@ -83,9 +96,9 @@ class EditAssetTest extends TestCase
$this->actingAs(User::factory()->viewAssets()->editAssets()->create())
->from(route('hardware.edit', $asset))
->put(route('hardware.update', $asset), [
'status_id' => $achived_status->id,
'model_id' => $asset->model_id,
'asset_tags' => $asset->asset_tag,
'status_id' => $archived_status->id,
'model_id' => $asset->model_id,
'asset_tags' => $asset->asset_tag,
],
)
->assertStatus(302);
@ -95,7 +108,7 @@ class EditAssetTest extends TestCase
$asset = Asset::find($asset->id);
$this->assertNull($asset->assigned_to);
$this->assertNull($asset->assigned_type);
$this->assertEquals($achived_status->id, $asset->status_id);
$this->assertEquals($archived_status->id, $asset->status_id);
Event::assertDispatched(function (CheckoutableCheckedIn $event) use ($currentTimestamp) {
return Carbon::parse($event->action_date)->diffInSeconds($currentTimestamp) < 2;

View file

@ -0,0 +1,148 @@
<?php
namespace Tests\Feature\Assets\Ui;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Location;
use App\Models\Statuslabel;
use App\Models\Supplier;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class StoreAssetTest extends TestCase
{
public function test_all_fields_are_saved()
{
Storage::fake('public');
$user = User::factory()->createAssets()->create();
$model = AssetModel::factory()->create();
$status = Statuslabel::factory()->readyToDeploy()->create();
$defaultLocation = Location::factory()->create();
$supplier = Supplier::factory()->create();
$file = UploadedFile::fake()->image("test.jpg", 2000);
$response = $this->actingAs($user)
->post(route('hardware.store'), [
'redirect_option' => 'item',
'name' => 'Test Asset',
'model_id' => $model->id,
'status_id' => $status->id,
// ugh, this is because for some reason asset tags and serials are expected to start at an index of [1], so throwing an empty in for [0]
'asset_tags' => ['', 'TEST-ASSET'],
'serials' => ['', 'TEST-SERIAL'],
'notes' => 'Test Notes',
'rtd_location_id' => $defaultLocation->id,
'requestable' => true,
'image' => $file,
'warranty_months' => 12,
'next_audit_date' => Carbon::now()->addMonths(12)->format('Y-m-d'),
'byod' => true,
'order_number' => 'TEST-ORDER',
'purchase_date' => Carbon::now()->format('Y-m-d'),
'asset_eol_date' => Carbon::now()->addMonths(36)->format('Y-m-d'),
'supplier_id' => $supplier->id,
'purchase_cost' => 1234.56,
])->assertSessionHasNoErrors();
$storedAsset = Asset::where('asset_tag', 'TEST-ASSET')->sole();
$response->assertRedirect(route('hardware.show', $storedAsset));
$this->assertDatabaseHas('assets', [
'id' => $storedAsset->id,
'name' => 'Test Asset',
'model_id' => $model->id,
'status_id' => $status->id,
'asset_tag' => 'TEST-ASSET',
'serial' => 'TEST-SERIAL',
'notes' => 'Test Notes',
'rtd_location_id' => $defaultLocation->id,
'requestable' => 1,
'image' => $storedAsset->image,
'warranty_months' => 12,
'next_audit_date' => Carbon::now()->addMonths(12)->format('Y-m-d'),
'byod' => 1,
'order_number' => 'TEST-ORDER',
'purchase_date' => Carbon::now()->format('Y-m-d'),
'asset_eol_date' => Carbon::now()->addMonths(36)->format('Y-m-d'),
'supplier_id' => $supplier->id,
'purchase_cost' => 1234.56,
]);
}
public function test_multiple_assets_are_stored()
{
$user = User::factory()->createAssets()->create();
$model = AssetModel::factory()->create();
$status = Statuslabel::factory()->readyToDeploy()->create();
$defaultLocation = Location::factory()->create();
$supplier = Supplier::factory()->create();
$file = UploadedFile::fake()->image("test.jpg", 2000);
$this->actingAs($user)->post(route('hardware.store'), [
'redirect_option' => 'index',
'name' => 'Test Assets',
'model_id' => $model->id,
'status_id' => $status->id,
'asset_tags' => ['', 'TEST-ASSET-1', 'TEST-ASSET-2'],
'serials' => ['', 'TEST-SERIAL-1', 'TEST-SERIAL-2'],
'notes' => 'Test Notes',
'rtd_location_id' => $defaultLocation->id,
'requestable' => true,
'image' => $file,
'warranty_months' => 12,
'next_audit_date' => Carbon::now()->addMonths(12)->format('Y-m-d'),
'byod' => true,
'order_number' => 'TEST-ORDER',
'purchase_date' => Carbon::now()->format('Y-m-d'),
'asset_eol_date' => Carbon::now()->addMonths(36)->format('Y-m-d'),
'supplier_id' => $supplier->id,
'purchase_cost' => 1234.56,
])->assertRedirect(route('hardware.index'))->assertSessionHasNoErrors();
$storedAsset = Asset::where('asset_tag', 'TEST-ASSET-1')->sole();
$storedAsset2 = Asset::where('asset_tag', 'TEST-ASSET-2')->sole();
$commonData = [
'name' => 'Test Assets',
'model_id' => $model->id,
'status_id' => $status->id,
'notes' => 'Test Notes',
'rtd_location_id' => $defaultLocation->id,
'requestable' => 1,
'warranty_months' => 12,
'next_audit_date' => Carbon::now()->addMonths(12)->format('Y-m-d'),
'byod' => 1,
'order_number' => 'TEST-ORDER',
'purchase_date' => Carbon::now()->format('Y-m-d'),
'asset_eol_date' => Carbon::now()->addMonths(36)->format('Y-m-d'),
'supplier_id' => $supplier->id,
'purchase_cost' => 1234.56,
];
$this->assertDatabaseHas('assets', array_merge($commonData, ['asset_tag' => 'TEST-ASSET-1', 'serial' => 'TEST-SERIAL-1', 'image' => $storedAsset->image]));
$this->assertDatabaseHas('assets', array_merge($commonData, ['asset_tag' => 'TEST-ASSET-2', 'serial' => 'TEST-SERIAL-2', 'image' => $storedAsset2->image]));
}
public function test_user_without_permission_denied()
{
$user = User::factory()->create();
$model = AssetModel::factory()->create();
$status = Statuslabel::factory()->readyToDeploy()->create();
$this->actingAs($user)->post(route('hardware.store'), [
'redirect_option' => 'index',
'name' => 'Test Assets',
'model_id' => $model->id,
'status_id' => $status->id,
'asset_tags' => ['', 'TEST-ASSET-1'],
'serials' => ['', 'TEST-SERIAL-1'],
])->assertForbidden();
}
}

View file

@ -7,6 +7,7 @@ use App\Models\Category;
use Carbon\Carbon;
use Tests\TestCase;
use App\Models\Setting;
use Watson\Validating\ValidationException;
class AssetTest extends TestCase
{
@ -31,7 +32,8 @@ class AssetTest extends TestCase
$b = Asset::factory()->make(['asset_tag' => Asset::autoincrement_asset() ]);
$this->assertTrue($a->save());
$this->assertFalse($b->save());
$this->expectException(ValidationException::class);
$b->save();
}
public function testAutoIncrementDouble()
@ -181,7 +183,7 @@ class AssetTest extends TestCase
]
)->id,
'warranty_months' => 24,
'purchase_date' => Carbon::createFromDate(2017, 1, 1)->hour(0)->minute(0)->second(0)
'purchase_date' => Carbon::createFromDate(2017, 1, 1)->hour(0)->minute(0)->second(0)->format("Y-m-d")
]);

View file

@ -38,8 +38,7 @@ class DepreciationTest extends TestCase
->laptopMbp()
->create(
[
'category_id' => Category::factory()->assetLaptopCategory()->create(),
'purchase_date' => now()->subDecade(),
'purchase_date' => now()->subDecade()->format("Y-m-d"),
'purchase_cost' => 4000,
]);
$asset->model->update([
@ -62,8 +61,7 @@ class DepreciationTest extends TestCase
->laptopMbp()
->create(
[
'category_id' => Category::factory()->assetLaptopCategory()->create(),
'purchase_date' => now()->subDecade(),
'purchase_date' => now()->subDecade()->format("Y-m-d"),
'purchase_cost' => 4000,
]);
$asset->model->update([