Merge pull request #17865 from grokability/show-inactive-licenses

Show inactive licenses
This commit is contained in:
snipe
2025-09-15 13:42:22 +01:00
committed by GitHub
11 changed files with 113 additions and 23 deletions

View File

@@ -26,6 +26,12 @@ class LicensesController extends Controller
$licenses = License::with('company', 'manufacturer', 'supplier','category', 'adminuser')->withCount('freeSeats as free_seats_count');
if ($request->input('status')=='inactive') {
$licenses->ExpiredLicenses();
} else {
$licenses->ActiveLicenses();
}
if ($request->filled('company_id')) {
$licenses->where('licenses.company_id', '=', $request->input('company_id'));
}
@@ -94,6 +100,8 @@ class LicensesController extends Controller
$licenses->onlyTrashed();
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $licenses->count()) ? $licenses->count() : app('api_offset_value');
$limit = app('api_limit_value');

View File

@@ -25,7 +25,7 @@ class LicenseCheckoutController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param $id
* @return \Illuminate\Contracts\View\View
* @return \Illuminate\Contracts\View\View |\Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function create(License $license)
@@ -39,6 +39,11 @@ class LicenseCheckoutController extends Controller
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.not_enough_seats'));
}
// Make sure the license is expired or terminated
if ($license->isInactive()){
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.license_is_inactive'));
}
// We don't currently allow checking out licenses to locations, so we'll reset that to user if needed
if (session()->get('checkout_to_type') == 'location') {
session()->put(['checkout_to_type' => 'user']);
@@ -70,8 +75,19 @@ class LicenseCheckoutController extends Controller
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.not_found'));
}
$this->authorize('checkout', $license);
// Make sure there is at least one available to checkout
if ($license->availCount()->count() < 1) {
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.not_enough_seats'));
}
// Make sure the license is expired or terminated
if ($license->isInactive()) {
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.license_is_inactive'));
}
$licenseSeat = $this->findLicenseSeatToCheckout($license, $seatId);
$licenseSeat->created_by = auth()->id();
$licenseSeat->notes = $request->input('notes');
@@ -114,6 +130,7 @@ class LicenseCheckoutController extends Controller
throw new \Illuminate\Http\Exceptions\HttpResponseException(redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.not_enough_seats')));
}
if (! $licenseSeat->license->is($license)) {
throw new \Illuminate\Http\Exceptions\HttpResponseException(redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.mismatch')));
}

View File

@@ -31,11 +31,11 @@ class LicensesTransformer
'purchase_order' => ($license->purchase_order) ? e($license->purchase_order) : null,
'purchase_date' => Helper::getFormattedDateObject($license->purchase_date, 'date'),
'termination_date' => Helper::getFormattedDateObject($license->termination_date, 'date'),
'expiration_date' => Helper::getFormattedDateObject($license->expiration_date, 'date'),
'depreciation' => ($license->depreciation) ? ['id' => (int) $license->depreciation->id,'name'=> e($license->depreciation->name)] : null,
'purchase_cost' => Helper::formatCurrencyOutput($license->purchase_cost),
'purchase_cost_numeric' => $license->purchase_cost,
'notes' => Helper::parseEscapedMarkedownInline($license->notes),
'expiration_date' => Helper::getFormattedDateObject($license->expiration_date, 'date'),
'seats' => (int) $license->seats,
'free_seats_count' => (int) $license->free_seats_count - License::unReassignableCount($license),
'remaining' => (int) $license->free_seats_count,

View File

@@ -299,16 +299,17 @@ class License extends Depreciable
}
public function isInactive(): bool
{
$day = now()->startOfDay();
{
$day = now()->startOfDay();
$expired = $this->expiration_date && $this->asDateTime($this->expiration_date)->startofDay()->lessThanOrEqualTo($day);
$expired = $this->expiration_date && $this->asDateTime($this->expiration_date)->startofDay()->lessThanOrEqualTo($day);
$terminated = $this->termination_date && $this->asDateTime($this->termination_date)->startofDay()->lessThanOrEqualTo($day);
$terminated = $this->termination_date && $this->asDateTime($this->termination_date)->startofDay()->lessThanOrEqualTo($day);
return $expired || $terminated;
}
return $expired || $terminated;
}
/**
* Sets free_seat_count attribute
*
@@ -750,6 +751,38 @@ class License extends Depreciable
->get();
}
public function scopeActiveLicenses($query)
{
return $query->whereNull('deleted_at')
// The termination date is null or within range
->where(function ($query) {
$query->whereNull('termination_date')
->orWhereDate('termination_date', '>', [Carbon::now()]);
})
->where(function ($query) {
$query->whereNull('expiration_date')
->orWhereDate('expiration_date', '>', [Carbon::now()]);
});
}
public function scopeExpiredLicenses($query)
{
return $query->whereNull('deleted_at')
// The termination date is null or within range
->where(function ($query) {
$query->whereNull('termination_date')
->orWhereDate('termination_date', '<=', [Carbon::now()]);
})
->orWhere(function ($query) {
$query->whereNull('expiration_date')
->orWhereDate('expiration_date', '<=', [Carbon::now()]);
});
}
/**
* Query builder scope to order on manufacturer
*

View File

@@ -349,10 +349,19 @@ class BreadcrumbsServiceProvider extends ServiceProvider
/**
* Licenses Breadcrumbs
*/
Breadcrumbs::for('licenses.index', fn (Trail $trail) =>
$trail->parent('home', route('home'))
->push(trans('general.licenses'), route('licenses.index'))
);
if ((request()->is('licenses*')) && (request()->status=='inactive')) {
Breadcrumbs::for('licenses.index', fn (Trail $trail) =>
$trail->parent('home', route('home'))
->push(trans('general.licenses'), route('licenses.index'))
->push(trans('general.show_inactive'), route('licenses.index'))
);
} else {
Breadcrumbs::for('licenses.index', fn (Trail $trail) =>
$trail->parent('home', route('home'))
->push(trans('general.licenses'), route('licenses.index'))
);
}
Breadcrumbs::for('licenses.create', fn (Trail $trail) =>
$trail->parent('licenses.index', route('licenses.index'))

View File

@@ -33,9 +33,9 @@ class LicenseFactory extends Factory
'seats' => $this->faker->numberBetween(1, 10),
'purchase_date' => $this->faker->dateTimeBetween('-1 years', 'now', date_default_timezone_get())->format('Y-m-d'),
'order_number' => $this->faker->numberBetween(1000000, 50000000),
'expiration_date' => $this->faker->dateTimeBetween('now', '+3 years', date_default_timezone_get())->format('Y-m-d H:i:s'),
'expiration_date' => null,
'reassignable' => $this->faker->boolean(),
'termination_date' => $this->faker->dateTimeBetween('-1 years', 'now', date_default_timezone_get())->format('Y-m-d H:i:s'),
'termination_date' => null,
'supplier_id' => Supplier::factory(),
'category_id' => Category::factory(),
];

View File

@@ -589,6 +589,7 @@ return [
'components' => ':count Component|:count Components',
],
'show_inactive' => 'Expired or Terminated',
'more_info' => 'More Info',
'quickscan_bulk_help' => 'Checking this box will edit the asset record to reflect this new location. Leaving it unchecked will simply note the location in the audit log. Note that if this asset is checked out, it will not change the location of the person, asset or location it is checked out to.',
'whoops' => 'Whoops!',

View File

@@ -27,7 +27,7 @@
id="licensesTable"
data-buttons="licenseButtons"
class="table table-striped snipe-table"
data-url="{{ route('api.licenses.index') }}"
data-url="{{ route('api.licenses.index', ['status' => e(request('status'))]) }}"
data-export-options='{
"fileName": "export-licenses-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]

View File

@@ -36,7 +36,7 @@
id="asssetModelsTable"
data-buttons="modelButtons"
class="table table-striped snipe-table"
data-url="{{ route('api.models.index', ['status' => request('status')]) }}"
data-url="{{ route('api.models.index', ['status' => e(request('status'))]) }}"
data-export-options='{
"fileName": "export-models-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]

View File

@@ -557,12 +557,17 @@
}
function licenseInOutFormatter(value, row) {
if(row.disabled || row.user_can_checkout === false) {
return '<a href="{{ config('app.url') }}/licenses/' + row.id + '/checkin" class="btn btn-sm bg-maroon disabled" data-tooltip="true" title="{{ trans('general.checkin_tooltip') }}">{{ trans('general.checkout') }}</a>';
// check that checkin is not disabled
if (row.user_can_checkout === false) {
return '<span class="btn btn-sm bg-maroon disabled" data-tooltip="true" title="{{ trans('admin/licenses/message.checkout.unavailable') }}">{{ trans('general.checkout') }}</span>';
} else if (row.disabled === true) {
return '<span class="btn btn-sm bg-maroon disabled" data-tooltip="true" title="{{ trans('admin/licenses/message.checkout.license_is_inactive') }}">{{ trans('general.checkout') }}</span>';
} else
// The user is allowed to check the license seat out and it's available
if ((row.available_actions.checkout === true) && (row.user_can_checkout === true)) {
return '<a href="{{ config('app.url') }}/licenses/' + row.id + '/checkout/'+row.id+'" class="btn btn-sm bg-maroon" data-tooltip="true" title="{{ trans('general.checkout_tooltip') }}">{{ trans('general.checkout') }}</a>';
if ((row.available_actions.checkout === true) && (row.user_can_checkout === true) && (row.disabled === false)) {
return '<a href="{{ config('app.url') }}/licenses/' + row.id + '/checkout/" class="btn btn-sm bg-maroon" data-tooltip="true" title="{{ trans('general.checkout_tooltip') }}">{{ trans('general.checkout') }}</a>';
}
}
// We need a special formatter for license seats, since they don't work exactly the same
@@ -572,7 +577,7 @@
if (row.disabled && (row.assigned_user || row.assigned_asset)) {
return '<a href="{{ config('app.url') }}/licenses/' + row.id + '/checkin" class="btn btn-sm bg-purple" data-tooltip="true" title="{{ trans('general.checkin_tooltip') }}">{{ trans('general.checkin') }}</a>';
}
if(row.disabled) {
if (row.disabled) {
return '<a href="{{ config('app.url') }}/licenses/' + row.id + '/checkin" class="btn btn-sm bg-maroon disabled" data-tooltip="true" title="{{ trans('general.checkin_tooltip') }}">{{ trans('general.checkout') }}</a>';
}
// The user is allowed to check the license seat out and it's available
@@ -580,6 +585,11 @@
return '<a href="{{ config('app.url') }}/licenses/' + row.license_id + '/checkout/'+row.id+'" class="btn btn-sm bg-maroon" data-tooltip="true" title="{{ trans('general.checkout_tooltip') }}">{{ trans('general.checkout') }}</a>';
}
// The user is allowed to check the license seat in and it's available
if ((row.available_actions.checkin === true) && ((row.assigned_asset) || (row.assigned_user))) {
return '<a href="{{ config('app.url') }}/licenses/' + row.id + '/checkin/" class="btn btn-sm bg-purple" data-tooltip="true" title="{{ trans('general.checkin_tooltip') }}">{{ trans('general.checkin') }}</a>';
}
}
function genericCheckinCheckoutFormatter(destination) {
@@ -1775,7 +1785,19 @@
attributes: {
title: '{{ trans('general.export') }}'
}
}
},
btnShowInactive: {
text: '{{ (request()->input('status') == "inactive") ? trans('general.list_all') : trans('general.show_inactive') }}',
icon: 'fas fa-clock {{ (request()->input('status') == "inactive") ? ' text-danger' : '' }}',
event () {
window.location.href = '{{ (request()->input('status') == "inactive") ? route('licenses.index') : route('licenses.index', ['status' => 'inactive']) }}';
},
attributes: {
title: '{{ (request()->input('status') == "inactive") ? trans('general.list_all') : trans('general.show_inactive') }}',
}
},
});

View File

@@ -897,7 +897,7 @@
</td>
<td class="hidden-print col-md-2">
@can('update', $license)
<a href="{{ route('licenses.checkin', $license->pivot->id, ['backto'=>'user']) }}" class="btn btn-primary btn-sm hidden-print">{{ trans('general.checkin') }}</a>
<a href="{{ route('licenses.checkin', $license->pivot->id, ['backto'=>'user']) }}" class="btn bg-purple btn-sm hidden-print">{{ trans('general.checkin') }}</a>
@endcan
</td>
</tr>