Merge pull request #10034 from snipe/features/ajaxify_depreciation

Fixed [ch15359] ajaxify the depreciation report
This commit is contained in:
snipe 2021-09-01 18:26:10 -07:00 committed by GitHub
commit a79f49ade3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 574 additions and 152 deletions

View file

@ -7,6 +7,7 @@ use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\AssetCheckoutRequest;
use App\Http\Transformers\AssetsTransformer;
use App\Http\Transformers\DepreciationReportTransformer;
use App\Http\Transformers\LicensesTransformer;
use App\Http\Transformers\SelectlistTransformer;
use App\Models\Actionlog;
@ -29,6 +30,7 @@ use Slack;
use Str;
use TCPDF;
use Validator;
use Route;
/**
@ -49,10 +51,32 @@ class AssetsController extends Controller
* @since [v4.0]
* @return JsonResponse
*/
public function index(Request $request, $audit = null)
public function index(Request $request, $audit = null)
{
$this->authorize('index', Asset::class);
\Log::debug(Route::currentRouteName());
/**
* This looks MAD janky (and it is), but the AssetsController@index does a LOT of heavy lifting throughout the
* app. This bit here just makes sure that someone without permission to view assets doesn't
* end up with priv escalations because they asked for a different endpoint.
*
* Since we never gave the specification for which transformer to use before, it should default
* gracefully to just use the AssetTransformer by default, which shouldn't break anything.
*
* It was either this mess, or repeating ALL of the searching and sorting and filtering code,
* which would have been far worse of a mess. *sad face* - snipe (Sept 1, 2021)
*/
if (Route::currentRouteName()=='api.depreciation-report.index') {
$transformer = 'App\Http\Transformers\DepreciationReportTransformer';
$this->authorize('reports.view');
} else {
$transformer = 'App\Http\Transformers\AssetsTransformer';
$this->authorize('index', Asset::class);
}
$settings = Setting::getSettings();
$allowed_columns = [
@ -295,8 +319,12 @@ class AssetsController extends Controller
$total = $assets->count();
$assets = $assets->skip($offset)->take($limit)->get();
// dd($assets);
return (new AssetsTransformer)->transformAssets($assets, $total);
/**
* Here we're just determining which Transformer (via $transformer) to use based on the
* variables we set earlier on in this method - we default to AssetsTransformer.
*/
return (new $transformer)->transformAssets($assets, $total);
}

View file

@ -0,0 +1,119 @@
<?php
namespace App\Http\Transformers;
use App\Helpers\Helper;
use App\Models\Asset;
use Illuminate\Database\Eloquent\Collection;
/**
* This tranformer looks like it's extraneous, since we return as much or more
* info in the AssetsTransformer, but we want to flatten these results out so that they
* don't dislose more information than we want. Folks with depreciation powers don't necessaily
* have the right to see additional info, and inspecting the API call here could disclose
* info they're not supposed to see.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v5.2.0]
*/
class DepreciationReportTransformer
{
public function transformAssets(Collection $assets, $total)
{
$array = array();
foreach ($assets as $asset) {
$array[] = self::transformAsset($asset);
}
return (new DatatablesTransformer)->transformDatatables($array, $total);
}
public function transformAsset(Asset $asset)
{
/**
* Set some default values here
*/
$purchase_cost = null;
$depreciated_value = null;
$monthly_depreciation = null;
$diff = null;
$checkout_target = null;
/**
* If there is a location set and a currency set, use that for display
*/
if ($asset->location && $asset->location->currency) {
$purchase_cost_currency = $asset->location->currency;
} else {
$purchase_cost_currency = \App\Models\Setting::getSettings()->default_currency;
}
/**
* If there is a NOT an empty purchase cost (meaning not null or '' but it *could* be zero),
* format the purchase cost. We coould do this inline in the transformer, but we need that value
* for the other calculations that come after, like diff, etc.
*/
if ($asset->purchase_cost!='') {
$purchase_cost = $purchase_cost_currency . ' ' . \App\Helpers\Helper::formatCurrencyOutput($asset->purchase_cost);
}
/**
* Override the previously set null values if there is a valid model and associated depreciation
*/
if (($asset->model) && ($asset->model->depreciation)) {
$depreciated_value = $purchase_cost_currency . ' ' . \App\Helpers\Helper::formatCurrencyOutput($asset->getDepreciatedValue());
$monthly_depreciation = $purchase_cost_currency . ' ' .\App\Helpers\Helper::formatCurrencyOutput(($asset->model->eol > 0 ? ($asset->purchase_cost / $asset->model->eol) : 0));
$diff = $purchase_cost_currency . ' ' .\App\Helpers\Helper::formatCurrencyOutput(($asset->purchase_cost - $asset->getDepreciatedValue()));
}
if ($asset->assigned) {
$checkout_target = $asset->assigned->name;
if ($asset->checkedOutToUser()) {
$checkout_target = $asset->assigned->getFullNameAttribute();
}
}
$array = [
'company' => ($asset->company) ? e($asset->company->name) : null,
'name' => e($asset->name),
'asset_tag' => e($asset->asset_tag),
'serial' => e($asset->serial),
'model' => ($asset->model) ? e($asset->model->name) : null,
'model_number' => (($asset->model) && ($asset->model->model_number)) ? e($asset->model->model_number) : null,
'eol' => ($asset->purchase_date!='') ? Helper::getFormattedDateObject($asset->present()->eol_date(), 'date') : null ,
'status_label' => ($asset->assetstatus) ? e($asset->assetstatus->name) : null,
'status' => ($asset->assetstatus) ? e($asset->present()->statusMeta) : null,
'category' => (($asset->model) && ($asset->model->category)) ? e($asset->model->category->name) : null,
'manufacturer' => (($asset->model) && ($asset->model->manufacturer)) ? e($asset->model->manufacturer->name) : null,
'supplier' => ($asset->supplier) ? e($asset->supplier->name) : null,
'notes' => ($asset->notes) ? e($asset->notes) : null,
'order_number' => ($asset->order_number) ? e($asset->order_number) : null,
'location' => ($asset->location) ? e($asset->location->name) : null,
'warranty_expires' => ($asset->warranty_months > 0) ? Helper::getFormattedDateObject($asset->warranty_expires, 'date') : null,
'purchase_date' => Helper::getFormattedDateObject($asset->purchase_date, 'date'),
'purchase_cost' => $purchase_cost,
'book_value' => $depreciated_value,
'monthly_depreciation' => $monthly_depreciation,
'checked_out_to' => $checkout_target,
'diff' => $diff,
'number_of_months' => ($asset->model && $asset->model->depreciation) ? e($asset->model->depreciation->months) : null,
'depreciation' => (($asset->model) && ($asset->model->depreciation)) ? e($asset->model->depreciation->name) : null,
];
return $array;
}
public function transformAssetsDatatable($assets)
{
return (new DatatablesTransformer)->transformDatatables($assets);
}
}

View file

@ -0,0 +1,393 @@
<?php
namespace App\Presenters;
use DateTime;
/**
* Class DepreciationReportPresenter
* @package App\Presenters
*/
class DepreciationReportPresenter extends Presenter
{
/**
* Json Column Layout for bootstrap table
* @return string
*/
public static function dataTableLayout()
{
$layout = [
[
"field" => "company",
"searchable" => true,
"sortable" => true,
"switchable" => true,
"title" => trans('general.company'),
"visible" => false,
], [
"field" => "category",
"searchable" => true,
"sortable" => true,
"title" => trans('general.category'),
"visible" => true,
], [
"field" => "name",
"searchable" => true,
"sortable" => true,
"title" => trans('admin/hardware/form.name'),
"visible" => false,
], [
"field" => "asset_tag",
"searchable" => true,
"sortable" => true,
"title" => trans('general.asset_tag'),
"visible" => true,
],[
"field" => "model",
"searchable" => true,
"sortable" => true,
"title" => trans('general.asset_model'),
"visible" => true,
], [
"field" => "model",
"searchable" => true,
"sortable" => true,
"title" => trans('admin/hardware/form.model'),
"visible" => true,
], [
"field" => "model_number",
"searchable" => true,
"sortable" => true,
"title" => trans('admin/models/table.modelnumber'),
"visible" => false
], [
"field" => "serial",
"searchable" => true,
"sortable" => true,
"title" => trans('admin/hardware/form.serial'),
"visible" => true,
], [
"field" => "depreciation",
"searchable" => true,
"sortable" => true,
"title" => trans('general.depreciation'),
"visible" => true,
], [
"field" => "number_of_months",
"searchable" => true,
"sortable" => true,
"title" => trans('admin/depreciations/general.number_of_months'),
"visible" => true,
], [
"field" => "status",
"searchable" => true,
"sortable" => true,
"title" => trans('admin/hardware/table.status'),
"visible" => true,
], [
"field" => "checked_out_to",
"searchable" => true,
"sortable" => true,
"title" => trans('admin/hardware/table.checkoutto'),
"visible" => false,
], [
"field" => "location",
"searchable" => true,
"sortable" => true,
"title" => trans('admin/hardware/table.location'),
"visible" => true,
], [
"field" => "manufacturer",
"searchable" => true,
"sortable" => true,
"title" => trans('general.manufacturer'),
"visible" => false,
],[
"field" => "supplier",
"searchable" => true,
"sortable" => true,
"title" => trans('general.supplier'),
"visible" => false,
], [
"field" => "purchase_date",
"searchable" => true,
"sortable" => true,
"visible" => true,
"title" => trans('general.purchase_date'),
"formatter" => "dateDisplayFormatter"
], [
"field" => "purchase_cost",
"searchable" => true,
"sortable" => true,
"visible" => true,
"title" => trans('general.purchase_cost'),
"footerFormatter" => 'sumFormatter',
], [
"field" => "order_number",
"searchable" => true,
"sortable" => true,
"visible" => false,
"title" => trans('general.order_number'),
], [
"field" => "eol",
"searchable" => false,
"sortable" => false,
"visible" => false,
"title" => trans('general.eol'),
"formatter" => "dateDisplayFormatter"
], [
"field" => "book_value",
"searchable" => true,
"sortable" => true,
"visible" => true,
"title" => trans('admin/hardware/table.book_value')
], [
"field" => "monthly_depreciation",
"searchable" => true,
"sortable" => true,
"visible" => true,
"title" => trans('admin/hardware/table.monthly_depreciation')
],[
"field" => "diff",
"searchable" => false,
"sortable" => false,
"visible" => true,
"title" => trans('admin/hardware/table.diff')
],[
"field" => "warranty_expires",
"searchable" => false,
"sortable" => false,
"visible" => false,
"title" => trans('admin/hardware/form.warranty_expires'),
"formatter" => "dateDisplayFormatter"
],
];
return json_encode($layout);
}
/**
* Generate html link to this items name.
* @return string
*/
public function nameUrl()
{
return (string) link_to_route('hardware.show', e($this->name), $this->id);
}
public function modelUrl()
{
if ($this->model->model) {
return $this->model->model->present()->nameUrl();
}
return '';
}
/**
* Generate img tag to this items image.
* @return mixed|string
*/
public function imageUrl()
{
$imagePath = '';
if ($this->image && !empty($this->image)) {
$imagePath = $this->image;
$imageAlt = $this->name;
} elseif ($this->model && !empty($this->model->image)) {
$imagePath = $this->model->image;
$imageAlt = $this->model->name;
}
$url = config('app.url');
if (!empty($imagePath)) {
$imagePath = '<img src="'.$url.'/uploads/assets/'.$imagePath.' height="50" width="50" alt="'.$imageAlt.'">';
}
return $imagePath;
}
/**
* Generate img tag to this items image.
* @return mixed|string
*/
public function imageSrc()
{
$imagePath = '';
if ($this->image && !empty($this->image)) {
$imagePath = $this->image;
} elseif ($this->model && !empty($this->model->image)) {
$imagePath = $this->model->image;
}
if (!empty($imagePath)) {
return config('app.url').'/uploads/assets/'.$imagePath;
}
return $imagePath;
}
/**
* Get Displayable Name
* @return string
*
* @todo this should be factored out - it should be subsumed by fullName (below)
*
**/
public function name()
{
return $this->fullName;
}
/**
* Helper for notification polymorphism.
* @return mixed
*/
public function fullName()
{
$str = '';
// Asset name
if ($this->model->name) {
$str .= $this->model->name;
}
// Asset tag
if ($this->asset_tag) {
$str .= ' ('.$this->model->asset_tag.')';
}
// Asset Model name
if ($this->model->model) {
$str .= ' - '.$this->model->model->name;
}
return $str;
}
/**
* Returns the date this item hits EOL.
* @return false|string
*/
public function eol_date()
{
if (( $this->purchase_date ) && ( $this->model->model ) && ($this->model->model->eol) ) {
$date = date_create($this->purchase_date);
date_add($date, date_interval_create_from_date_string($this->model->model->eol . ' months'));
return date_format($date, 'Y-m-d');
}
}
/**
* How many months until this asset hits EOL.
* @return null
*/
public function months_until_eol()
{
$today = date("Y-m-d");
$d1 = new DateTime($today);
$d2 = new DateTime($this->eol_date());
if ($this->eol_date() > $today) {
$interval = $d2->diff($d1);
} else {
$interval = null;
}
return $interval;
}
/**
* @return string
* This handles the status label "meta" status of "deployed" if
* it's assigned. Should maybe deprecate.
*/
public function statusMeta()
{
if ($this->model->assigned) {
return 'deployed';
}
return $this->model->assetstatus->getStatuslabelType();
}
/**
* @return string
* This handles the status label "meta" status of "deployed" if
* it's assigned. Should maybe deprecate.
*/
public function statusText()
{
if ($this->model->assigned) {
return trans('general.deployed');
}
return $this->model->assetstatus->name;
}
/**
* @return string
* This handles the status label "meta" status of "deployed" if
* it's assigned. Results look like:
*
* (if assigned and the status label is "Ready to Deploy"):
* (Deployed)
*
* (f assigned and status label is not "Ready to Deploy":)
* Deployed (Another Status Label)
*
* (if not deployed:)
* Another Status Label
*/
public function fullStatusText() {
// Make sure the status is valid
if ($this->assetstatus) {
// If the status is assigned to someone or something...
if ($this->model->assigned) {
// If it's assigned and not set to the default "ready to deploy" status
if ($this->assetstatus->name != trans('general.ready_to_deploy')) {
return trans('general.deployed'). ' (' . $this->model->assetstatus->name.')';
}
// If it's assigned to the default "ready to deploy" status, just
// say it's deployed - otherwise it's confusing to have a status that is
// both "ready to deploy" and deployed at the same time.
return trans('general.deployed');
}
// Return just the status name
return $this->model->assetstatus->name;
}
// This status doesn't seem valid - either data has been manually edited or
// the status label was deleted.
return 'Invalid status';
}
/**
* Date the warantee expires.
* @return false|string
*/
public function warrantee_expires()
{
if (($this->purchase_date) && ($this->warranty_months)) {
$date = date_create($this->purchase_date);
date_add($date, date_interval_create_from_date_string($this->warranty_months . ' months'));
return date_format($date, 'Y-m-d');
}
return false;
}
/**
* Url to view this item.
* @return string
*/
public function viewUrl()
{
return route('hardware.show', $this->id);
}
public function glyph()
{
return '<i class="fa fa-barcode" aria-hidden="true"></i>';
}
}

View file

@ -16,156 +16,30 @@
@if (($depreciations) && ($depreciations->count() > 0))
<div class="table-responsive">
<div class="table-responsive">
<table
data-cookie-id-table="depreciationReport"
data-pagination="true"
data-id-table="depreciationReport"
data-search="true"
data-side-pagination="client"
data-show-columns="true"
data-show-export="true"
data-show-refresh="true"
data-sort-order="asc"
id="depreciationReport"
class="table table-striped snipe-table"
data-export-options='{
"fileName": "depreciation-report-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]
}'>
<table
data-cookie-id-table="depreciationReport"
data-pagination="true"
data-id-table="depreciationReport"
data-search="true"
data-side-pagination="server"
data-show-columns="true"
data-show-export="true"
data-show-refresh="true"
data-sort-order="desc"
data-sort-name="created_at"
id="depreciationReport"
data-url="{{ route('api.depreciation-report.index') }}"
data-mobile-responsive="true"
data-toggle="table"
class="table table-striped snipe-table"
data-columns="{{ \App\Presenters\DepreciationReportPresenter::dataTableLayout() }}"
data-export-options='{
"fileName": "depreciation-report-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]
}'>
<thead>
<tr role="row">
<th class="col-sm-1" data-visible="false">{{ trans('admin/companies/table.title') }}</th>
<th class="col-sm-1" data-visible="false">{{ trans('admin/categories/general.category_name') }}</th>
<th class="col-sm-1">{{ trans('admin/hardware/table.asset_tag') }}</th>
<th class="col-sm-1" data-visible="false">{{ trans('admin/hardware/table.title') }}</th>
@if ($snipeSettings->display_asset_name)
<th class="col-sm-1" data-visible="false">{{ trans('general.name') }}</th>
@endif
<th class="col-sm-1">{{ trans('admin/hardware/table.serial') }}</th>
<th class="col-sm-1">{{ trans('admin/depreciations/general.depreciation_name') }}</th>
<th class="col-sm-1">{{ trans('admin/depreciations/general.number_of_months') }}</th>
<th class="col-sm-1">{{ trans('admin/hardware/table.status') }}</th>
<th class="col-sm-1">{{ trans('admin/hardware/table.checkoutto') }}</th>
<th class="col-sm-1" data-visible="false">{{ trans('admin/hardware/table.location') }}</th>
<th class="col-sm-1">{{ trans('admin/hardware/table.purchase_date') }}</th>
<th class="col-sm-1">{{ trans('admin/hardware/table.eol') }}</th>
<th class="col-sm-1 align-right">{{ trans('admin/hardware/table.purchase_cost') }}</th>
<th class="col-sm-1 align-right">{{ trans('admin/hardware/table.book_value') }}</th>
<th class="col-sm-1 align-right">{{ trans('admin/hardware/table.monthly_depreciation') }}</th>
<th class="col-sm-1 align-right">{{ trans('admin/hardware/table.diff') }}</th>
</tr>
</thead>
<tbody>
@foreach ($assets as $asset)
<tr>
<td>{{ is_null($asset->company) ? '' : $asset->company->name }}</td>
<td>
@if ($asset->model)
{{ $asset->model->category->name }}
@endif
</td>
<td>
@if ($asset->deleted_at!='')
<del>{{ $asset->asset_tag }}</del>
@else
{{ $asset->asset_tag }}
@endif
</td>
<td>{{ $asset->model->name }}</td>
@if ($snipeSettings->display_asset_name)
<td>{{ $asset->name }}</td>
@endif
<td>{{ $asset->serial }}</td>
<td>
@if ($asset->model->depreciation)
{{ $asset->model->depreciation->name }}
@endif
</td>
<td>
@if ($asset->model->depreciation)
{{ $asset->model->depreciation->months }}
@endif
</td>
<td>
{{ $asset->assetstatus->name }}
({{ $asset->present()->statusMeta }})
</td>
<td>
@if (($asset->checkedOutToUser()) && ($asset->assigned))
{{ $asset->assigned->getFullNameAttribute() }}
@else
@if ($asset->assigned)
{{ $asset->assigned->name }}
@endif
@endif
</td>
<td>
@if ($asset->location)
{{ $asset->location->name }}
@elseif ($asset->defaultloc)
{{ $asset->defaultloc->name }}
@endif
</td>
<td>
{{ \Carbon\Carbon::parse($asset->purchase_date)->format('Y-m-d') }}
</td>
<td>
@if ($asset->model->eol) {{ $asset->present()->eol_date() }}
@endif
</td>
@if ($asset->purchase_cost > 0)
<td class="align-right">
@if ($asset->location && $asset->location->currency)
{{ $asset->location->currency }}
@else
{{ $snipeSettings->default_currency }}
@endif
{{ \App\Helpers\Helper::formatCurrencyOutput($asset->purchase_cost) }}
</td>
<td class="align-right">
@if ($asset->location && $asset->location->currency)
{{ $asset->location->currency }}
@else
{{ $snipeSettings->default_currency }}
@endif
{{ \App\Helpers\Helper::formatCurrencyOutput($asset->getDepreciatedValue()) }}
</td>
<td class="align-right">
@if ($asset->model->depreciation)
@if ($asset->location && $asset->location->currency)
{{ $asset->location->currency }}
@else
{{ $snipeSettings->default_currency }}
@endif
{{ \App\Helpers\Helper::formatCurrencyOutput(($asset->model->eol > 0 ? ($asset->purchase_cost / $asset->model->eol) : 0)) }}
@endif
</td>
<td class="align-right">
@if ($asset->location && $asset->location->currency)
{{ $asset->location->currency }}
@else
{{ $snipeSettings->default_currency }}
@endif
-{{ \App\Helpers\Helper::formatCurrencyOutput(($asset->purchase_cost - $asset->getDepreciatedValue())) }}
</td>
@else
<td></td>
<td></td>
<td></td>
<td></td>
@endif
</tr>
@endforeach
</tbody>
</table>
</div> <!-- /.table-responsive-->
@else

View file

@ -903,6 +903,14 @@ Route::group(['prefix' => 'v1','namespace' => 'Api', 'middleware' => 'auth:api']
[ 'as' => 'api.activity.index', 'uses' => 'ReportsController@index' ]
);
Route::get(
'reports/depreciation',
[
'as' => 'api.depreciation-report.index',
'uses' => 'AssetsController@index'
]
);
/*--- Kits API ---*/
Route::resource('kits', 'PredefinedKitsController',