mirror of
https://github.com/snipe/snipe-it.git
synced 2026-08-18 11:15:42 +00:00
992 lines
41 KiB
PHP
Executable File
992 lines
41 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Http\Controllers\Assets;
|
|
|
|
use App\Events\CheckoutableCheckedIn;
|
|
use App\Helpers\Helper;
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Requests\CreateMultipleAssetRequest;
|
|
use App\Http\Requests\ImageUploadRequest;
|
|
use App\Http\Requests\UploadFileRequest;
|
|
use App\Models\Actionlog;
|
|
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\Observers\AssetObserver;
|
|
use App\View\Label;
|
|
use Carbon\Carbon;
|
|
use Com\Tecnick\Barcode\Barcode;
|
|
use Illuminate\Contracts\View\View;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
use Illuminate\Support\Facades\Crypt;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Gate;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
use TypeError;
|
|
|
|
/**
|
|
* This class controls all actions related to assets for
|
|
* the Snipe-IT Asset Management application.
|
|
*
|
|
* @version v1.0
|
|
*
|
|
* @author [A. Gianotto] [<snipe@snipe.net>]
|
|
*/
|
|
class AssetsController extends Controller
|
|
{
|
|
protected $qrCodeDimensions = ['height' => 3.5, 'width' => 3.5];
|
|
|
|
protected $barCodeDimensions = ['height' => 2, 'width' => 22];
|
|
|
|
public function __construct()
|
|
{
|
|
$this->middleware('auth');
|
|
parent::__construct();
|
|
}
|
|
|
|
/**
|
|
* Returns a view that invokes the ajax tables which actually contains
|
|
* the content for the assets listing, which is generated in getDatatable.
|
|
*
|
|
* @author [A. Gianotto] [<snipe@snipe.net>]
|
|
*
|
|
* @see AssetController::getDatatable() method that generates the JSON response
|
|
* @since [v1.0]
|
|
*/
|
|
public function index(Request $request): View
|
|
{
|
|
$this->authorize('index', Asset::class);
|
|
$companyId = $request->input('company_id');
|
|
$company = is_scalar($companyId) ? Company::find($companyId) : null;
|
|
|
|
return view('hardware/index')->with('company', $company);
|
|
}
|
|
|
|
/**
|
|
* Returns a view that presents a form to create a new asset.
|
|
*
|
|
* @author [A. Gianotto] [<snipe@snipe.net>]
|
|
*
|
|
* @since [v1.0]
|
|
*
|
|
* @internal param int $model_id
|
|
*/
|
|
public function create(Request $request): View
|
|
{
|
|
$this->authorize('create', Asset::class);
|
|
$view = view('hardware/edit')
|
|
->with('statuslabel_list', Helper::statusLabelList())
|
|
->with('item', new Asset)
|
|
->with('statuslabel_types', Helper::statusTypeList());
|
|
|
|
if ($request->filled('model_id')) {
|
|
$selected_model = AssetModel::find($request->input('model_id'));
|
|
$view->with('selected_model', $selected_model);
|
|
}
|
|
|
|
return $view;
|
|
}
|
|
|
|
/**
|
|
* Validate and process new asset form data.
|
|
*
|
|
* @author [A. Gianotto] [<snipe@snipe.net>]
|
|
*
|
|
* @since [v1.0]
|
|
*/
|
|
public function store(CreateMultipleAssetRequest $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');
|
|
$model = AssetModel::find($request->input('model_id'));
|
|
$serial_errors = [];
|
|
$serials = $request->input('serials');
|
|
|
|
$settings = Setting::getSettings();
|
|
|
|
// Validate required serial based on model setting
|
|
for ($a = 1, $aMax = count($asset_tags); $a <= $aMax; $a++) {
|
|
if ($model && $model->require_serial === 1 && empty($serials[$a])) {
|
|
$serial_errors["serials.$a"] = trans('admin/hardware/form.serial_required', ['number' => $a]);
|
|
}
|
|
|
|
}
|
|
|
|
if (! empty($serial_errors)) {
|
|
return redirect()->back()
|
|
->withInput()
|
|
->withErrors($serial_errors);
|
|
}
|
|
|
|
$asset = null;
|
|
$companyId = Company::getIdForCurrentUser($request->input('company_id'));
|
|
$successes = [];
|
|
$failures = [];
|
|
|
|
try {
|
|
DB::beginTransaction();
|
|
for ($a = 1, $aMax = count($asset_tags); $a <= $aMax; $a++) {
|
|
$asset = new Asset;
|
|
|
|
$asset->model()->associate($model);
|
|
$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 = $companyId;
|
|
$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((int) $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);
|
|
}
|
|
|
|
if ($request->has('use_cloned_image')) {
|
|
$cloned_model_img = Asset::select('image')->find($request->input('clone_image_from_id'));
|
|
if ($cloned_model_img) {
|
|
$new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
|
|
$new_image = 'assets/'.$new_image_name;
|
|
Storage::disk('public')->copy('assets/'.$cloned_model_img->image, $new_image);
|
|
$asset->image = $new_image_name;
|
|
}
|
|
|
|
} else {
|
|
$asset = $request->handleImages($asset);
|
|
}
|
|
|
|
// Update custom fields in the database.
|
|
// Validation for these fields is handled through the AssetRequest form request
|
|
|
|
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
|
|
// Note - it can be tempting to instead want to call saveOrFail(), to automatically throw when an object
|
|
// is invalid (and can't save). But this won't work, because Custom Fields _overrides_ the save() method
|
|
// to inject the Custom Field Rules into the $rules property right before invoking the _real_ save.
|
|
// so, instead, we have to catch failures on the 'else' clause and throw there.
|
|
if ($asset->isValid() && $asset->save()) {
|
|
$target = null;
|
|
$location = null;
|
|
|
|
if ($userId = request('assigned_user')) {
|
|
$target = User::find($userId);
|
|
|
|
if (! $target) {
|
|
return redirect()->back()->withInput()->with('error', trans('admin/hardware/message.create.target_not_found.user'));
|
|
}
|
|
$location = $target->location_id;
|
|
|
|
} elseif ($assetId = request('assigned_asset')) {
|
|
$target = Asset::find($assetId);
|
|
|
|
if (! $target) {
|
|
return redirect()->back()->withInput()->with('error', trans('admin/hardware/message.create.target_not_found.asset'));
|
|
}
|
|
$location = $target->location_id;
|
|
|
|
} elseif ($locationId = request('assigned_location')) {
|
|
$target = Location::find($locationId);
|
|
|
|
if (! $target) {
|
|
return redirect()->back()->withInput()->with('error', trans('admin/hardware/message.create.target_not_found.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->input('name'), $location);
|
|
}
|
|
|
|
$successes[] = "<a href='".route('hardware.show', $asset)."' style='color: white;'>".e($asset->asset_tag).'</a>';
|
|
|
|
} else {
|
|
$asset->throwValidationException(); // we have to do this for the reason listed above - can't use saveOrFail()
|
|
$failures[] = implode(',', $asset->getErrors()->all()); // TODO - this can probably go away soon
|
|
}
|
|
}
|
|
} catch (\Throwable $e) {
|
|
\Log::debug('Caught exception in multi-create - rolling back: '.$e->getMessage());
|
|
DB::rollBack();
|
|
throw $e;
|
|
}
|
|
DB::commit();
|
|
|
|
if ($request->input('redirect_option') === 'back') {
|
|
session()->put(['redirect_option' => 'index']);
|
|
} else {
|
|
session()->put(['redirect_option' => $request->input('redirect_option')]);
|
|
}
|
|
|
|
session()->put(['checkout_to_type' => $request->input('checkout_to_type'),
|
|
'other_redirect' => 'model']);
|
|
|
|
if ($successes) {
|
|
if ($failures) {
|
|
// some succeeded, some failed
|
|
return Helper::getRedirectOption($request, $asset->id, 'Assets') // FIXME - not tested
|
|
->with('success-unescaped', trans_choice('admin/hardware/message.create.multi_success_linked', $successes, ['links' => implode(', ', $successes)]))
|
|
->with('warning', trans_choice('admin/hardware/message.create.partial_failure', $failures, ['failures' => implode('; ', $failures)]));
|
|
} else {
|
|
if (count($successes) == 1) {
|
|
// the most common case, keeping it so we don't have to make every use of that translation string be trans_choice'ed
|
|
// and re-translated
|
|
return Helper::getRedirectOption($request, $asset->id, 'Assets')
|
|
->with('success-unescaped', trans('admin/hardware/message.create.success_linked', ['link' => route('hardware.show', $asset), 'id', 'tag' => e($asset->asset_tag)]));
|
|
} else {
|
|
// multi-success
|
|
return Helper::getRedirectOption($request, $asset->id, 'Assets')
|
|
->with('success-unescaped', trans_choice('admin/hardware/message.create.multi_success_linked', $successes, ['links' => implode(', ', $successes)]));
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return redirect()->back()->withInput()->withErrors($asset->getErrors());
|
|
}
|
|
|
|
/**
|
|
* Returns a view that presents a form to edit an existing asset.
|
|
*
|
|
* @author [A. Gianotto] [<snipe@snipe.net>]
|
|
*
|
|
* @since [v1.0]
|
|
*
|
|
* @return View
|
|
*/
|
|
public function edit(Asset $asset): View|RedirectResponse
|
|
{
|
|
$this->authorize($asset);
|
|
if ($safeReferer = Helper::sameOriginUrl(url()->previous())) {
|
|
session()->put('url.intended', $safeReferer);
|
|
}
|
|
|
|
return view('hardware/edit')
|
|
->with('item', $asset)
|
|
->with('statuslabel_list', Helper::statusLabelList())
|
|
->with('statuslabel_types', Helper::statusTypeList());
|
|
}
|
|
|
|
/**
|
|
* Returns a view that presents information about an asset for detail view.
|
|
*
|
|
* @author [A. Gianotto] [<snipe@snipe.net>]
|
|
*
|
|
* @param int $assetId
|
|
*
|
|
* @since [v1.0]
|
|
*
|
|
* @return View
|
|
*/
|
|
public function show(Asset $asset): View|RedirectResponse
|
|
{
|
|
$this->authorize('view', $asset);
|
|
$settings = Setting::getSettings();
|
|
|
|
if (isset($asset)) {
|
|
$audit_log = Actionlog::where('action_type', '=', 'audit')
|
|
->where('item_id', '=', $asset->id)
|
|
->where('item_type', '=', Asset::class)
|
|
->orderBy('created_at', 'DESC')->first();
|
|
|
|
if ($asset->location) {
|
|
$use_currency = $asset->location->currency;
|
|
} else {
|
|
if ($settings->default_currency != '') {
|
|
$use_currency = $settings->default_currency;
|
|
} else {
|
|
$use_currency = trans('general.currency');
|
|
}
|
|
}
|
|
|
|
$qr_code = (object) [
|
|
'display' => $settings->qr_code == '1',
|
|
'url' => route('qr_code/common', ['object_type' => 'hardware', 'id' => $asset->id]),
|
|
];
|
|
|
|
$total_maintenance_cost = $asset->maintenances?->sum('cost');
|
|
$total_asset_cost = ($asset->assignedAssets()?->AssetsForShow()) ? $asset->assignedAssets()?->AssetsForShow()?->sum('purchase_cost') : 0;
|
|
$total_license_cost = ($asset->licenses) ? $asset->licenses->sum('purchase_cost') : 0;
|
|
// accessories.purchase_cost no longer exists; getAccessoryCost()
|
|
// walks lastOrderDefaults() per attached accessory so the total
|
|
// reflects each item's last acquisition (with the parent's
|
|
// default_purchase_cost as fallback).
|
|
$total_accessory_cost = $asset->getAccessoryCost();
|
|
$total_component_cost = ($asset->components) ? $asset->components->sum('calculated_purchase_cost') : 0;
|
|
|
|
$total_cost_for_asset = $asset->purchase_cost + $total_maintenance_cost + $total_asset_cost + $total_license_cost + $total_accessory_cost + $total_component_cost;
|
|
|
|
$audit_custom_field_columns = [];
|
|
if ($asset->model && $asset->model->fieldset) {
|
|
$audit_custom_field_columns = $asset->model->fieldset->fields
|
|
->where('display_audit', '1')
|
|
->map(fn ($field) => [
|
|
'field' => $field->db_column,
|
|
'searchable' => false,
|
|
'sortable' => false,
|
|
'switchable' => true,
|
|
'title' => e($field->name),
|
|
'visible' => true,
|
|
])
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
return view('hardware/view', compact('asset', 'qr_code', 'settings'))
|
|
->with('total_maintenance_cost', $total_maintenance_cost)
|
|
->with('total_asset_cost', $total_asset_cost)
|
|
->with('total_license_cost', $total_license_cost)
|
|
->with('total_accessory_cost', $total_accessory_cost)
|
|
->with('total_component_cost', $total_component_cost)
|
|
->with('total_cost_for_asset', $total_cost_for_asset)
|
|
->with('use_currency', $use_currency)
|
|
->with('audit_log', $audit_log)
|
|
->with('audit_custom_field_columns', $audit_custom_field_columns);
|
|
}
|
|
|
|
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
|
|
}
|
|
|
|
/**
|
|
* Validate and process asset edit form.
|
|
*
|
|
* @param int $assetId
|
|
*
|
|
* @since [v1.0]
|
|
*
|
|
* @author [A. Gianotto] [<snipe@snipe.net>]
|
|
*/
|
|
public function update(ImageUploadRequest $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 = (int) Carbon::parse($asset->asset_eol_date)->diffInMonths($asset->purchase_date, true);
|
|
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);
|
|
// Current location is editable from the asset edit form as of
|
|
// the location-dropdown addition. Only overwrite when the key
|
|
// is actually present in the request — a client that omits
|
|
// location_id entirely (a partial update API caller, an older
|
|
// form) still leaves the existing value intact. Present-but-
|
|
// blank clears via the mutator (see setLocationIdAttribute).
|
|
if ($request->has('location_id')) {
|
|
$asset->location_id = $request->input('location_id');
|
|
}
|
|
$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;
|
|
$asset->last_checkin = now();
|
|
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), 'Checkin on asset update with '.$status->getStatuslabelType().' status', date('Y-m-d H:i:s'), $originalValues));
|
|
}
|
|
|
|
if ($request->filled('image_delete')) {
|
|
try {
|
|
unlink(public_path().'/uploads/assets/'.basename($asset->image));
|
|
$asset->image = '';
|
|
} catch (\Exception $e) {
|
|
Log::info($e);
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
// 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->input('model_id'));
|
|
if (($model) && ($model->fieldset)) {
|
|
foreach ($model->fieldset->fields as $field) {
|
|
if ($field->element == 'checkbox' && ! $request->has($field->db_column)) {
|
|
$asset->{$field->db_column} = null;
|
|
}
|
|
if ($request->has($field->db_column)) {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
session()->put([
|
|
'redirect_option' => $request->input('redirect_option'),
|
|
'checkout_to_type' => $request->input('checkout_to_type'),
|
|
'other_redirect' => $request->input('redirect_option') === 'other_redirect' ? 'model' : null,
|
|
]);
|
|
|
|
// Validate required serial based on model setting
|
|
if ($model && $model->require_serial === 1 && empty($serial[1])) {
|
|
return Helper::getRedirectOption($request, $asset->id, 'Assets')
|
|
->with('warning', trans('admin/hardware/form.serial_required_post_model_update', [
|
|
'asset_model' => $model->name,
|
|
]));
|
|
}
|
|
if ($asset->save()) {
|
|
return Helper::getRedirectOption($request, $asset->id, 'Assets')
|
|
->with('success', trans('admin/hardware/message.update.success'));
|
|
}
|
|
|
|
return redirect()->back()->withInput()->withErrors($asset->getErrors());
|
|
}
|
|
|
|
/**
|
|
* Delete a given asset (mark as deleted).
|
|
*
|
|
* @author [A. Gianotto] [<snipe@snipe.net>]
|
|
*
|
|
* @param int $assetId
|
|
*
|
|
* @since [v1.0]
|
|
*/
|
|
public function destroy(Request $request, Asset $asset): RedirectResponse
|
|
{
|
|
$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, 'assigned_type' => null]);
|
|
}
|
|
|
|
// Note: the image file is deliberately preserved across this
|
|
// soft-delete. Snipe-IT's `snipeit:purge` command permanently
|
|
// removes it later when the row is force-deleted. Keeping the
|
|
// file here means a restored soft-deleted row still has its
|
|
// image.
|
|
$asset->delete();
|
|
|
|
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.delete.success'));
|
|
}
|
|
|
|
/**
|
|
* Searches the assets table by serial, and redirects if it finds one
|
|
*
|
|
* @author [A. Gianotto] [<snipe@snipe.net>]
|
|
*
|
|
* @since [v3.0]
|
|
*/
|
|
public function getAssetBySerial(Request $request, $serial = null): RedirectResponse
|
|
{
|
|
$serial = $serial ?: $request->input('serial');
|
|
$topsearch = ($request->input('topsearch') == 'true');
|
|
|
|
if (! $asset = Asset::where('serial', '=', $serial)->first()) {
|
|
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
|
|
}
|
|
$this->authorize('view', $asset);
|
|
|
|
return redirect()->route('hardware.show', $asset->id)->with('topsearch', $topsearch);
|
|
}
|
|
|
|
/**
|
|
* Searches the assets table by asset tag, and redirects if it finds one
|
|
*
|
|
* @author [A. Gianotto] [<snipe@snipe.net>]
|
|
*
|
|
* @since [v3.0]
|
|
*/
|
|
public function getAssetByTag(Request $request, $tag = null): RedirectResponse
|
|
{
|
|
$tag = $tag ? $tag : $request->input('assetTag');
|
|
$topsearch = ($request->input('topsearch') == 'true');
|
|
|
|
// Search for an exact and unique asset tag match
|
|
$assets = Asset::where('asset_tag', '=', $tag);
|
|
|
|
// If not a unique result, redirect to the index view
|
|
if ($assets->count() != 1) {
|
|
return redirect()->route('hardware.index')
|
|
->with('search', $tag)
|
|
->with('warning', trans('admin/hardware/message.does_not_exist_var', ['asset_tag' => $tag]));
|
|
}
|
|
$asset = $assets->first();
|
|
$this->authorize('view', $asset);
|
|
|
|
return redirect()->route('hardware.show', $asset->id)->with('topsearch', $topsearch);
|
|
}
|
|
|
|
/**
|
|
* Return a QR code for the asset
|
|
*
|
|
* @author [A. Gianotto] [<snipe@snipe.net>]
|
|
*
|
|
* @param int $assetId
|
|
*
|
|
* @since [v1.0]
|
|
*/
|
|
public function getQrCode(Asset $asset): Response|BinaryFileResponse|string|bool
|
|
{
|
|
$settings = Setting::getSettings();
|
|
|
|
if ($settings->label2_2d_type !== 'none') {
|
|
|
|
if ($asset) {
|
|
$size = Helper::barcodeDimensions($settings->label2_2d_type);
|
|
$qr_file = public_path().'/uploads/barcodes/qr-'.str_slug($asset->asset_tag).'-'.str_slug($asset->id).'.png';
|
|
|
|
if (isset($asset->id, $asset->asset_tag)) {
|
|
if (file_exists($qr_file)) {
|
|
$header = ['Content-type' => 'image/png'];
|
|
|
|
return response()->file($qr_file, $header);
|
|
} else {
|
|
$barcode = new Barcode;
|
|
$barcode_obj = $barcode->getBarcodeObj($settings->label2_2d_type, route('hardware.show', $asset->id), $size['height'], $size['width'], 'black', [-2, -2, -2, -2]);
|
|
file_put_contents($qr_file, $barcode_obj->getPngData());
|
|
|
|
return response($barcode_obj->getPngData())->header('Content-type', 'image/png');
|
|
}
|
|
}
|
|
}
|
|
|
|
return 'That asset is invalid';
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return a 2D barcode for the asset
|
|
*
|
|
* @author [A. Gianotto] [<snipe@snipe.net>]
|
|
*
|
|
* @param int $assetId
|
|
*
|
|
* @since [v1.0]
|
|
*
|
|
* @return Response
|
|
*/
|
|
public function getBarCode($assetId = null)
|
|
{
|
|
$settings = Setting::getSettings();
|
|
if ($asset = Asset::withTrashed()->find($assetId)) {
|
|
// Gate on the asset view policy so this endpoint enforces
|
|
// the same object-level authorization as its sibling detail
|
|
// / label / QR-code routes. Previously any authenticated
|
|
// user could pull the barcode PNG for any asset regardless
|
|
// of company scope, letting them enumerate protected asset
|
|
// tags.
|
|
$this->authorize('view', $asset);
|
|
|
|
$barcode_file = public_path().'/uploads/barcodes/'.str_slug($settings->label2_1d_type).'-'.str_slug($asset->asset_tag).'.png';
|
|
|
|
if (isset($asset->id, $asset->asset_tag)) {
|
|
if (file_exists($barcode_file)) {
|
|
$header = ['Content-type' => 'image/png'];
|
|
|
|
return response()->file($barcode_file, $header);
|
|
} else {
|
|
// Calculate barcode width in pixel based on label width (inch)
|
|
$barcode_width = ($settings->labels_width - $settings->labels_display_sgutter) * 200.000000000001;
|
|
|
|
$barcode = new Barcode;
|
|
try {
|
|
$barcode_obj = $barcode->getBarcodeObj($settings->label2_1d_type, $asset->asset_tag, ($barcode_width < 300 ? $barcode_width : 300), 50);
|
|
file_put_contents($barcode_file, $barcode_obj->getPngData());
|
|
|
|
return response($barcode_obj->getPngData())->header('Content-type', 'image/png');
|
|
} catch (\Exception|TypeError $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');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Return a label for an individual asset.
|
|
*
|
|
* @author [L. Swartzendruber] [<logan.swartzendruber@gmail.com>
|
|
*
|
|
* @param int $assetId
|
|
* @return View
|
|
*/
|
|
public function getLabel($assetId = null)
|
|
{
|
|
if (isset($assetId)) {
|
|
$asset = Asset::find($assetId);
|
|
$this->authorize('view', $asset);
|
|
|
|
return (new Label)
|
|
->with('assets', collect([$asset]))
|
|
->with('settings', Setting::getSettings())
|
|
->with('template', request()->input('template'))
|
|
->with('offset', request()->input('offset'))
|
|
->with('bulkedit', false)
|
|
->with('count', 0);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a view that presents a form to clone an asset.
|
|
*
|
|
* @author [A. Gianotto] [<snipe@snipe.net>]
|
|
*
|
|
* @param int $assetId
|
|
*
|
|
* @since [v1.0]
|
|
*
|
|
* @return View
|
|
*/
|
|
public function getClone(Asset $asset)
|
|
{
|
|
$this->authorize('create', Asset::class);
|
|
$cloned = clone $asset;
|
|
$cloned_model = $asset;
|
|
$cloned->id = null;
|
|
$cloned->asset_tag = '';
|
|
$cloned->serial = '';
|
|
$cloned->assigned_to = '';
|
|
$cloned->deleted_at = '';
|
|
|
|
return view('hardware/edit')
|
|
->with('statuslabel_list', Helper::statusLabelList())
|
|
->with('statuslabel_types', Helper::statusTypeList())
|
|
->with('cloned_model', $cloned_model)
|
|
->with('item', $cloned);
|
|
}
|
|
|
|
public function sortByName(array $recordA, array $recordB): int
|
|
{
|
|
return strcmp($recordB['Full Name'], $recordA['Full Name']);
|
|
}
|
|
|
|
/**
|
|
* Restore a deleted asset.
|
|
*
|
|
* @author [A. Gianotto] [<snipe@snipe.net>]
|
|
*
|
|
* @param int $assetId
|
|
*
|
|
* @since [v1.0]
|
|
*
|
|
* @return View
|
|
*/
|
|
public function getRestore($assetId = null)
|
|
{
|
|
if ($asset = Asset::withTrashed()->find($assetId)) {
|
|
$this->authorize('delete', $asset);
|
|
|
|
if ($asset->deleted_at == '') {
|
|
return redirect()->back()->with('error', trans('general.not_deleted', ['item_type' => trans('general.asset')]));
|
|
}
|
|
|
|
if ($asset->restore()) {
|
|
// Redirect them to the deleted page if there are more, otherwise the section index
|
|
$deleted_assets = Asset::onlyTrashed()->count();
|
|
if ($deleted_assets > 0) {
|
|
return redirect()->back()->with('success', trans('admin/hardware/message.restore.success'));
|
|
}
|
|
|
|
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.restore.success'));
|
|
}
|
|
|
|
// Check validation to make sure we're not restoring an asset with the same asset tag (or unique attribute) as an existing asset
|
|
return redirect()->back()->with('error', trans('general.could_not_restore', ['item_type' => trans('general.asset'), 'error' => $asset->getErrors()->first()]));
|
|
}
|
|
|
|
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
|
|
}
|
|
|
|
public function quickScan()
|
|
{
|
|
$this->authorize('audit', Asset::class);
|
|
$settings = Setting::getSettings();
|
|
$dt = Carbon::now()->addMonths($settings->audit_interval)->toDateString();
|
|
|
|
return view('hardware/quickscan')->with('next_audit_date', $dt);
|
|
}
|
|
|
|
public function quickScanCheckin()
|
|
{
|
|
$this->authorize('checkin', Asset::class);
|
|
|
|
return view('hardware/quickscan-checkin')->with('statusLabel_list', Helper::statusLabelList());
|
|
}
|
|
|
|
public function dueForAudit()
|
|
{
|
|
$this->authorize('audit', Asset::class);
|
|
|
|
return view('hardware/audit-due');
|
|
}
|
|
|
|
public function dueForCheckin()
|
|
{
|
|
$this->authorize('checkin', Asset::class);
|
|
|
|
return view('hardware/checkin-due');
|
|
}
|
|
|
|
public function audit(Asset $asset): View|RedirectResponse
|
|
{
|
|
$this->authorize('audit', Asset::class);
|
|
// Per-instance authorize so SnipePermissionsPolicy::before()
|
|
// runs Company::isCurrentUserHasAccess($asset) at the policy
|
|
// layer instead of leaving FMCS scoping solely to the route-
|
|
// model-binding + CompanyableScope combo.
|
|
$this->authorize('audit', $asset);
|
|
$settings = Setting::getSettings();
|
|
|
|
// Invoke the validation to see if the audit will complete successfully
|
|
$asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
|
|
|
|
if ($asset->isInvalid()) {
|
|
return redirect()->route('hardware.edit', $asset)->withErrors($asset->getErrors());
|
|
}
|
|
|
|
$dt = Carbon::now()->addMonths((int) $settings->audit_interval)->toDateString();
|
|
|
|
return view('hardware/audit')->with('asset', $asset)->with('item', $asset)->with('next_audit_date', $dt)->with('locations_list');
|
|
}
|
|
|
|
public function auditStore(UploadFileRequest $request, Asset $asset)
|
|
{
|
|
|
|
$this->authorize('audit', Asset::class);
|
|
// Per-instance authorize: without this, FMCS enforcement on
|
|
// an audit write depends entirely on route-model binding
|
|
// firing CompanyableScope. Explicit instance authorize means
|
|
// the policy layer independently rejects cross-company writes.
|
|
$this->authorize('audit', $asset);
|
|
|
|
session()->put('redirect_option', $request->input('redirect_option'));
|
|
session()->put('other_redirect', 'audit');
|
|
|
|
$originalValues = $asset->getRawOriginal();
|
|
|
|
$asset->next_audit_date = $request->input('next_audit_date');
|
|
$asset->last_audit_date = date('Y-m-d H:i:s');
|
|
|
|
// Check to see if they checked the box to update the physical location,
|
|
// not just note it in the audit notes
|
|
if ($request->input('update_location') == '1') {
|
|
$asset->location_id = $request->input('location_id');
|
|
}
|
|
|
|
// Update custom fields in the database
|
|
if (($asset->model) && ($asset->model->fieldset)) {
|
|
foreach ($asset->model->fieldset->fields as $field) {
|
|
if (($field->display_audit == '1') && ($request->has($field->db_column))) {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Invoke the validation to see if the audit will complete successfully
|
|
$asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
|
|
|
|
// Validate the rest of the data before we turn off the event dispatcher
|
|
if ($asset->isInvalid()) {
|
|
return redirect()->back()->withInput()->withErrors($asset->getErrors());
|
|
}
|
|
|
|
/**
|
|
* Even though we do a save() further down, we don't want to log this as a "normal" asset update,
|
|
* which would trigger the Asset Observer and would log an asset *update* log entry (because the
|
|
* de-normed fields like next_audit_date on the asset itself will change on save()) *in addition* to
|
|
* the audit log entry we're creating through this controller.
|
|
*
|
|
* To prevent this double-logging (one for update and one for audit), we skip the observer and bypass
|
|
* that de-normed update log entry by using unsetEventDispatcher(), BUT invoking unsetEventDispatcher()
|
|
* will bypass normal model-level validation that's usually handled at the observer )
|
|
*
|
|
* We handle validation on the save() by checking if the asset is valid via the ->isValid() method,
|
|
* which manually invokes Watson Validating to make sure the asset's model is valid.
|
|
*
|
|
* @see AssetObserver::updating()
|
|
* @see Asset::save()
|
|
*/
|
|
$asset->unsetEventDispatcher();
|
|
|
|
/**
|
|
* Invoke Watson Validating to check the asset itself and check to make sure it saved correctly.
|
|
* We have to invoke this manually because of the unsetEventDispatcher() above.)
|
|
*/
|
|
if ($asset->isValid() && $asset->save()) {
|
|
|
|
$file_name = null;
|
|
// Create the image (if one was chosen.)
|
|
if ($request->hasFile('image')) {
|
|
$file_name = $request->handleFile('private_uploads/audits/', 'audit-'.$asset->id, $request->file('image'));
|
|
}
|
|
|
|
$asset->logAudit($request->input('note'), $request->input('location_id'), $file_name, $originalValues);
|
|
|
|
return Helper::getRedirectOption($request, $asset->id, 'Assets')->with('success', trans('admin/hardware/message.audit.success'));
|
|
}
|
|
|
|
return redirect()->back()->withInput()->withErrors($asset->getErrors());
|
|
}
|
|
|
|
public function getRequestedIndex($user_id = null)
|
|
{
|
|
$this->authorize('index', Asset::class);
|
|
|
|
$requestedItems = CheckoutRequest::with('user', 'requestedItem')->whereNull('canceled_at');
|
|
|
|
if ($user_id) {
|
|
$requestedItems->where('user_id', $user_id);
|
|
}
|
|
|
|
$requestedItems = $requestedItems->orderBy('created_at', 'desc')->get();
|
|
|
|
if (Company::isFullMultipleCompanySupportEnabled() && ! auth()->user()->isSuperUser()) {
|
|
$requestedItems = $requestedItems->filter(
|
|
fn (CheckoutRequest $request) => $request->requestable
|
|
&& Company::isCurrentUserHasAccess($request->requestable)
|
|
)->values();
|
|
}
|
|
|
|
return view('hardware/requested', compact('requestedItems'));
|
|
}
|
|
}
|