diff --git a/app/Actions/Assets/DestroyAssetAction.php b/app/Actions/Assets/DestroyAssetAction.php new file mode 100644 index 000000000000..6ea96abede8d --- /dev/null +++ b/app/Actions/Assets/DestroyAssetAction.php @@ -0,0 +1,40 @@ +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(); + } + +} \ No newline at end of file diff --git a/app/Actions/Assets/StoreAssetAction.php b/app/Actions/Assets/StoreAssetAction.php new file mode 100644 index 000000000000..b9a40546a03a --- /dev/null +++ b/app/Actions/Assets/StoreAssetAction.php @@ -0,0 +1,162 @@ +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); + } +} \ No newline at end of file diff --git a/app/Actions/Assets/UpdateAssetAction.php b/app/Actions/Assets/UpdateAssetAction.php new file mode 100644 index 000000000000..7cde179c5430 --- /dev/null +++ b/app/Actions/Assets/UpdateAssetAction.php @@ -0,0 +1,259 @@ +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); + } +} \ No newline at end of file diff --git a/app/Exceptions/CustomFieldPermissionException.php b/app/Exceptions/CustomFieldPermissionException.php new file mode 100644 index 000000000000..4425d00cb8dd --- /dev/null +++ b/app/Exceptions/CustomFieldPermissionException.php @@ -0,0 +1,10 @@ +with( + ->with([ 'model', 'location', 'assetstatus', @@ -140,7 +144,7 @@ public function index(Request $request, $action = null, $upcoming_status = null) '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 @@ public function selectlist(Request $request): array */ 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,83 +653,19 @@ public function store(StoreAssetRequest $request): JsonResponse */ 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'])); - } - 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', (new AssetsTransformer)->transformAsset($asset), trans('admin/hardware/message.update.encrypted_warning'))); - } else { - return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($asset), trans('admin/hardware/message.update.success'))); - } + 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'))); } - return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200); } @@ -781,30 +676,16 @@ public function update(UpdateAssetRequest $request, Asset $asset): JsonResponse * @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); } @@ -1237,9 +1118,9 @@ public function assignedAccessories(Request $request, Asset $asset) : JsonRespon } /** * 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 */ @@ -1250,7 +1131,7 @@ public function getLabels(Request $request): JsonResponse // 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); } @@ -1273,7 +1154,7 @@ public function getLabels(Request $request): JsonResponse $label = new Label(); - + if (!$label) { throw new \Exception('Label object could not be created'); } diff --git a/app/Http/Controllers/Assets/AssetsController.php b/app/Http/Controllers/Assets/AssetsController.php index 6fd27ed03c2d..e81b6cdf786b 100755 --- a/app/Http/Controllers/Assets/AssetsController.php +++ b/app/Http/Controllers/Assets/AssetsController.php @@ -2,34 +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 Watson\Validating\ValidationException; /** * This class controls all actions related to assets for @@ -96,121 +96,51 @@ public function create(Request $request) : View * @author [A. Gianotto] [] * @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[] = " $asset->id]) . "' style='color: white;'>" . e($asset->asset_tag) . ""; - - } 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[] = " $asset->id])."' style='color: white;'>".e($asset->asset_tag).""; + } 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 @@ -229,13 +159,12 @@ public function store(ImageUploadRequest $request) : RedirectResponse ->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', ['hardware' => $asset->id]), 'id', 'tag' => e($asset->asset_tag)])); } - /** * Returns a view that presents a form to edit an existing asset. * @@ -308,133 +237,50 @@ public function show($assetId = null) : View | RedirectResponse * @since [v1.0] * @author [A. Gianotto] [] */ - public function update(ImageUploadRequest $request, $assetId = null) : RedirectResponse + public function update(UpdateAssetRequest $request, Asset $asset): RedirectResponse { - - // Check if the asset exists - if (! $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($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 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)); - } - - 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, $assetId, '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()); } /** @@ -444,39 +290,16 @@ public function update(ImageUploadRequest $request, $assetId = null) : RedirectR * @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]); - } - - - if ($asset->image) { - try { - Storage::disk('public')->delete('assets'.'/'.$asset->image); - } catch (\Exception $e) { - Log::debug($e); - } + 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()); } - - $asset->delete(); - - return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.delete.success')); } /** @@ -590,7 +413,7 @@ public function getBarCode($assetId = null) file_put_contents($barcode_file, $barcode_obj->getPngData()); return response($barcode_obj->getPngData())->header('Content-type', 'image/png'); - } catch (\Exception $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'); @@ -692,7 +515,7 @@ public function postImportHistory(Request $request) $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 = []; diff --git a/app/Http/Controllers/Assets/BulkAssetsController.php b/app/Http/Controllers/Assets/BulkAssetsController.php index 93f7255c0bf1..c4170a9a3455 100644 --- a/app/Http/Controllers/Assets/BulkAssetsController.php +++ b/app/Http/Controllers/Assets/BulkAssetsController.php @@ -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 Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Database\Eloquent\ModelNotFoundException; +use Watson\Validating\ValidationException; class BulkAssetsController extends Controller { @@ -199,298 +203,59 @@ public function edit(Request $request) : View | RedirectResponse * @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')) { - $updated_status = Statuslabel::find($request->input('status_id')); - - // 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')); } /** diff --git a/app/Http/Requests/Assets/DestroyAssetRequest.php b/app/Http/Requests/Assets/DestroyAssetRequest.php new file mode 100644 index 000000000000..564233ae95be --- /dev/null +++ b/app/Http/Requests/Assets/DestroyAssetRequest.php @@ -0,0 +1,29 @@ +asset); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + return [ + // + ]; + } +} diff --git a/app/Http/Requests/StoreAssetRequest.php b/app/Http/Requests/Assets/StoreAssetRequest.php similarity index 84% rename from app/Http/Requests/StoreAssetRequest.php rename to app/Http/Requests/Assets/StoreAssetRequest.php index fb7469ac88f9..c3db8108a5e2 100644 --- a/app/Http/Requests/StoreAssetRequest.php +++ b/app/Http/Requests/Assets/StoreAssetRequest.php @@ -1,7 +1,8 @@ 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 @@ public function rules(): array // converted to a float via setPurchaseCostAttribute). $modelRules = $this->removeNumericRulesFromPurchaseCost($modelRules); } - return array_merge( $modelRules, ['status_id' => [new AssetCannotBeCheckedOutToNondeployableStatus()]], @@ -100,4 +102,14 @@ private function removeNumericRulesFromPurchaseCost(array $rules): array 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(); + } } diff --git a/app/Http/Requests/UpdateAssetRequest.php b/app/Http/Requests/Assets/UpdateAssetRequest.php similarity index 55% rename from app/Http/Requests/UpdateAssetRequest.php rename to app/Http/Requests/Assets/UpdateAssetRequest.php index 1b379358f951..5c8d524bdee9 100644 --- a/app/Http/Requests/UpdateAssetRequest.php +++ b/app/Http/Requests/Assets/UpdateAssetRequest.php @@ -1,7 +1,8 @@ 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 @@ public function rules() 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; } diff --git a/app/Importer/AssetImporter.php b/app/Importer/AssetImporter.php index 1112a04e3508..ab38c3672212 100644 --- a/app/Importer/AssetImporter.php +++ b/app/Importer/AssetImporter.php @@ -7,6 +7,7 @@ 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 @@ public function createAssetIfNotExists(array $row) // 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'].'"'); } diff --git a/app/Models/Asset.php b/app/Models/Asset.php index ce8b870eb2e0..3f53dda623b4 100644 --- a/app/Models/Asset.php +++ b/app/Models/Asset.php @@ -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'; diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 4b752b736f96..f9c39fa87101 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -107,7 +107,7 @@ public function admin() return User::where('permissions->superuser', '1')->first() ?? User::factory()->firstAdmin(); }, ]; - }); + })->appendPermission(['assets.view.encrypted_custom_fields' => '1']); } public function viewAssets() diff --git a/resources/lang/en-US/admin/hardware/message.php b/resources/lang/en-US/admin/hardware/message.php index 222cbc439e6a..b16b5e4269df 100644 --- a/resources/lang/en-US/admin/hardware/message.php +++ b/resources/lang/en-US/admin/hardware/message.php @@ -16,9 +16,9 @@ 'create' => [ 'error' => 'Asset was not created, please try again. :(', 'success' => 'Asset created successfully. :)', - 'success_linked' => 'Asset with tag :tag was created successfully. Click here to view.', + 'success_linked' => 'Asset with tag :tag was created successfully. Click here to view.', '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' => [ diff --git a/resources/views/hardware/edit.blade.php b/resources/views/hardware/edit.blade.php index efd5d24b9cd0..6e3dd3c54eae 100755 --- a/resources/views/hardware/edit.blade.php +++ b/resources/views/hardware/edit.blade.php @@ -5,7 +5,7 @@ 'topSubmit' => true, 'helpText' => trans('help.assets'), 'helpPosition' => 'right', - 'formAction' => ($item->id) ? route('hardware.update', ['hardware' => $item->id]) : route('hardware.store'), + 'formAction' => ($item->id) ? route('hardware.update', ['asset' => $item->id]) : route('hardware.store'), 'index_route' => 'hardware.index', 'options' => [ 'index' => trans('admin/hardware/form.redirect_to_all', ['type' => 'assets']), diff --git a/resources/views/notifications.blade.php b/resources/views/notifications.blade.php index 7d74e5d04345..67871192d413 100755 --- a/resources/views/notifications.blade.php +++ b/resources/views/notifications.blade.php @@ -136,7 +136,7 @@ {{ trans('general.notification_error') }}: {{ trans('general.notification_bulk_error_hint') }} - @foreach($messages as $key => $message) + @foreach($messages as $key => $message) @for ($x = 0; $x < count($message); $x++)
  • {{ $message[$x] }}
  • diff --git a/routes/api.php b/routes/api.php index b51e139a41d7..16e4fb037cd5 100644 --- a/routes/api.php +++ b/routes/api.php @@ -596,15 +596,16 @@ 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 diff --git a/routes/web/hardware.php b/routes/web/hardware.php index ee888aa1db71..f12bc60169c2 100644 --- a/routes/web/hardware.php +++ b/routes/web/hardware.php @@ -163,6 +163,10 @@ function () { }); +Route::delete('/hardware/{asset}', [AssetsController::class, 'destroy'])->name('hardware.destroy'); + +Route::match(['put', 'patch'], '/hardware/{asset}', [AssetsController::class, 'update'])->name('hardware.update'); + Route::resource('hardware', AssetsController::class, [ @@ -172,6 +176,7 @@ function () { 'show' => 'view', ], ], + 'except' => ['destroy', 'update'], ]); Route::get('ht/{any?}', diff --git a/tests/Feature/Assets/Ui/BulkEditAssetsTest.php b/tests/Feature/Assets/Ui/BulkEditAssetsTest.php index 44e905248208..6708ebb3154c 100644 --- a/tests/Feature/Assets/Ui/BulkEditAssetsTest.php +++ b/tests/Feature/Assets/Ui/BulkEditAssetsTest.php @@ -211,7 +211,7 @@ public function testBulkEditAssetsAcceptsAndUpdatesEncryptedCustomFields() $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 @@ public function testBulkEditAssetsAcceptsAndUpdatesEncryptedCustomFields() }); } - public function testBulkEditAssetsRequiresadminToUpdateEncryptedCustomFields() + public function testBulkEditAssetsRequiresAdminToUpdateEncryptedCustomFields() { $this->markIncompleteIfMySQL('Custom Fields tests do not work on mysql'); $edit_user = User::factory()->editAssets()->create(); diff --git a/tests/Feature/Assets/Ui/DeleteAssetTest.php b/tests/Feature/Assets/Ui/DeleteAssetTest.php new file mode 100644 index 000000000000..948f0d683313 --- /dev/null +++ b/tests/Feature/Assets/Ui/DeleteAssetTest.php @@ -0,0 +1,35 @@ +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); + } + +} \ No newline at end of file diff --git a/tests/Feature/Assets/Ui/EditAssetTest.php b/tests/Feature/Assets/Ui/EditAssetTest.php index 27f00b531321..d085036f2bd0 100644 --- a/tests/Feature/Assets/Ui/EditAssetTest.php +++ b/tests/Feature/Assets/Ui/EditAssetTest.php @@ -68,13 +68,26 @@ public function testAssetEditPostIsRedirectedIfRedirectSelectionIsItem() $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,7 +96,7 @@ public function testNewCheckinIsLoggedIfStatusChangedToUndeployable() $this->actingAs(User::factory()->viewAssets()->editAssets()->create()) ->from(route('hardware.edit', $asset->id)) ->put(route('hardware.update', $asset->id), [ - 'status_id' => $achived_status->id, + 'status_id' => $archived_status->id, 'model_id' => $asset->model_id, 'asset_tags' => $asset->asset_tag, ], @@ -95,7 +108,7 @@ public function testNewCheckinIsLoggedIfStatusChangedToUndeployable() $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; diff --git a/tests/Feature/Assets/Ui/StoreAssetTest.php b/tests/Feature/Assets/Ui/StoreAssetTest.php new file mode 100644 index 000000000000..db1cd1eb09c7 --- /dev/null +++ b/tests/Feature/Assets/Ui/StoreAssetTest.php @@ -0,0 +1,148 @@ +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', ['hardware' => $storedAsset->id])); + + $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(); + } +} \ No newline at end of file diff --git a/tests/Unit/AssetTest.php b/tests/Unit/AssetTest.php index d0f3af623373..bf9d4f6fdc0a 100644 --- a/tests/Unit/AssetTest.php +++ b/tests/Unit/AssetTest.php @@ -7,6 +7,7 @@ use Carbon\Carbon; use Tests\TestCase; use App\Models\Setting; +use Watson\Validating\ValidationException; class AssetTest extends TestCase { @@ -31,7 +32,8 @@ public function testAutoIncrementCollision() $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 @@ public function testWarrantyExpiresAttribute() ] )->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") ]); diff --git a/tests/Unit/DepreciationTest.php b/tests/Unit/DepreciationTest.php index c170ac0e1dfc..507523d581f1 100644 --- a/tests/Unit/DepreciationTest.php +++ b/tests/Unit/DepreciationTest.php @@ -38,8 +38,7 @@ public function testDepreciationAmount() ->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 @@ public function testDepreciationPercentage() ->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([