This commit is contained in:
Godfrey Martinez 2025-03-05 17:19:34 +00:00 committed by GitHub
commit 4384e51e09
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 128 additions and 46 deletions

View file

@ -1529,4 +1529,5 @@ class Helper
} }
return redirect()->back()->with('error', trans('admin/hardware/message.checkout.error')); return redirect()->back()->with('error', trans('admin/hardware/message.checkout.error'));
} }
} }

View file

@ -119,7 +119,9 @@ class LicenseSeatsController extends Controller
// nothing to update // nothing to update
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success'))); return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
} }
if( $touched && $licenseSeat->unreassignable_seat) {
return response()->json(Helper::formatStandardApiResponse('error', $licenseSeat, trans('admin/licenses/message.checkout.unavailable')));
}
// the logging functions expect only one "target". if both asset and user are present in the request, // the logging functions expect only one "target". if both asset and user are present in the request,
// we simply let assets take precedence over users... // we simply let assets take precedence over users...
if ($licenseSeat->isDirty('assigned_to')) { if ($licenseSeat->isDirty('assigned_to')) {
@ -136,7 +138,11 @@ class LicenseSeatsController extends Controller
if ($licenseSeat->save()) { if ($licenseSeat->save()) {
if ($is_checkin) { if ($is_checkin) {
$licenseSeat->logCheckin($target, $request->input('note')); if(!$licenseSeat->license->reassignable){
$licenseSeat->unreassignable_seat = true;
$licenseSeat->save();
}
$licenseSeat->logCheckin($target, $licenseSeat->notes);
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success'))); return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
} }

View file

@ -64,12 +64,7 @@ class LicenseCheckinController extends Controller
$this->authorize('checkout', $license); $this->authorize('checkout', $license);
if (! $license->reassignable) {
// Not allowed to checkin
Session::flash('error', trans('admin/licenses/message.checkin.not_reassignable') . '.');
return redirect()->back()->withInput();
}
// Declare the rules for the form validation // Declare the rules for the form validation
$rules = [ $rules = [
@ -95,13 +90,16 @@ class LicenseCheckinController extends Controller
$licenseSeat->assigned_to = null; $licenseSeat->assigned_to = null;
$licenseSeat->asset_id = null; $licenseSeat->asset_id = null;
$licenseSeat->notes = $request->input('notes'); $licenseSeat->notes = $request->input('notes');
if (! $licenseSeat->license->reassignable) {
$licenseSeat->unreassignable_seat = true;
}
session()->put(['redirect_option' => $request->get('redirect_option')]); session()->put(['redirect_option' => $request->get('redirect_option')]);
// Was the asset updated? // Was the asset updated?
if ($licenseSeat->save()) { if ($licenseSeat->save()) {
event(new CheckoutableCheckedIn($licenseSeat, $return_to, auth()->user(), $request->input('notes'))); event(new CheckoutableCheckedIn($licenseSeat, $return_to, auth()->user(), $licenseSeat->notes));
return redirect()->to(Helper::getRedirectOption($request, $license->id, 'Licenses'))->with('success', trans('admin/licenses/message.checkin.success')); return redirect()->to(Helper::getRedirectOption($request, $license->id, 'Licenses'))->with('success', trans('admin/licenses/message.checkin.success'));
@ -126,21 +124,17 @@ class LicenseCheckinController extends Controller
$license = License::findOrFail($licenseId); $license = License::findOrFail($licenseId);
$this->authorize('checkin', $license); $this->authorize('checkin', $license);
if (! $license->reassignable) {
// Not allowed to checkin
Session::flash('error', 'License not reassignable.');
return redirect()->back()->withInput();
}
$licenseSeatsByUser = LicenseSeat::where('license_id', '=', $licenseId) $licenseSeatsByUser = LicenseSeat::where('license_id', '=', $licenseId)
->whereNotNull('assigned_to') ->whereNotNull('assigned_to')
->with('user') ->with('user', 'license')
->get(); ->get();
$license = $licenseSeatsByUser->first()?->license;
foreach ($licenseSeatsByUser as $user_seat) { foreach ($licenseSeatsByUser as $user_seat) {
$user_seat->assigned_to = null; $user_seat->assigned_to = null;
if ($license && ! $license->reassignable) {
$user_seat->unreassignable_seat = true;
}
if ($user_seat->save()) { if ($user_seat->save()) {
Log::debug('Checking in '.$license->name.' from user '.$user_seat->username); Log::debug('Checking in '.$license->name.' from user '.$user_seat->username);
$user_seat->logCheckin($user_seat->user, trans('admin/licenses/general.bulk.checkin_all.log_msg')); $user_seat->logCheckin($user_seat->user, trans('admin/licenses/general.bulk.checkin_all.log_msg'));
@ -153,9 +147,12 @@ class LicenseCheckinController extends Controller
->get(); ->get();
$count = 0; $count = 0;
$license = $licenseSeatsByAsset->first()?->license;
foreach ($licenseSeatsByAsset as $asset_seat) { foreach ($licenseSeatsByAsset as $asset_seat) {
$asset_seat->asset_id = null; $asset_seat->asset_id = null;
if ($license && ! $license->reassignable) {
$asset_seat->unreassignable_seat = true;
}
if ($asset_seat->save()) { if ($asset_seat->save()) {
Log::debug('Checking in '.$license->name.' from asset '.$asset_seat->asset_tag); Log::debug('Checking in '.$license->name.' from asset '.$asset_seat->asset_tag);
$asset_seat->logCheckin($asset_seat->asset, trans('admin/licenses/general.bulk.checkin_all.log_msg')); $asset_seat->logCheckin($asset_seat->asset, trans('admin/licenses/general.bulk.checkin_all.log_msg'));

View file

@ -239,16 +239,25 @@ class LicensesController extends Controller
$license = License::with('assignedusers')->find($license->id); $license = License::with('assignedusers')->find($license->id);
$users_count = User::where('autoassign_licenses', '1')->count(); $users_count = User::where('autoassign_licenses', '1')->count();
$total_seats_count = $license->totalSeatsByLicenseID();
$total_seats_count = (int) $license->totalSeatsByLicenseID();
$available_seats_count = $license->availCount()->count(); $available_seats_count = $license->availCount()->count();
$checkedout_seats_count = ($total_seats_count - $available_seats_count); $unreassignable_seats_count = License::unReassignableCount($license);
if(!$license->reassignable){
$checkedout_seats_count = ($total_seats_count - $available_seats_count - $unreassignable_seats_count );
}
else {
$checkedout_seats_count = ($total_seats_count - $available_seats_count);
}
$this->authorize('view', $license); $this->authorize('view', $license);
return view('licenses.view', compact('license')) return view('licenses.view', compact('license'))
->with('users_count', $users_count) ->with('users_count', $users_count)
->with('total_seats_count', $total_seats_count) ->with('total_seats_count', $total_seats_count)
->with('available_seats_count', $available_seats_count) ->with('available_seats_count', $available_seats_count)
->with('checkedout_seats_count', $checkedout_seats_count); ->with('checkedout_seats_count', $checkedout_seats_count)
->with('unreassignable_seats_count', $unreassignable_seats_count);
} }

View file

@ -6,7 +6,6 @@ use App\Models\License;
use App\Models\LicenseSeat; use App\Models\LicenseSeat;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
class LicenseSeatsTransformer class LicenseSeatsTransformer
{ {
public function transformLicenseSeats(Collection $seats, $total) public function transformLicenseSeats(Collection $seats, $total)
@ -48,6 +47,7 @@ class LicenseSeatsTransformer
'reassignable' => (bool) $seat->license->reassignable, 'reassignable' => (bool) $seat->license->reassignable,
'notes' => e($seat->notes), 'notes' => e($seat->notes),
'user_can_checkout' => (($seat->assigned_to == '') && ($seat->asset_id == '')), 'user_can_checkout' => (($seat->assigned_to == '') && ($seat->asset_id == '')),
'disabled' => $seat->unreassignable_seat,
]; ];
if ($seat_count != 0) { if ($seat_count != 0) {

View file

@ -7,7 +7,7 @@ use App\Models\License;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
class LicensesTransformer class LicensesTransformer
{ {
public function transformLicenses(Collection $licenses, $total) public function transformLicenses(Collection $licenses, $total)
{ {
@ -37,7 +37,7 @@ class LicensesTransformer
'notes' => Helper::parseEscapedMarkedownInline($license->notes), 'notes' => Helper::parseEscapedMarkedownInline($license->notes),
'expiration_date' => Helper::getFormattedDateObject($license->expiration_date, 'date'), 'expiration_date' => Helper::getFormattedDateObject($license->expiration_date, 'date'),
'seats' => (int) $license->seats, 'seats' => (int) $license->seats,
'free_seats_count' => (int) $license->free_seats_count, 'free_seats_count' => (int) $license->free_seats_count - License::unReassignableCount($license),
'min_amt' => ($license->min_amt) ? (int) ($license->min_amt) : null, 'min_amt' => ($license->min_amt) ? (int) ($license->min_amt) : null,
'license_name' => ($license->license_name) ? e($license->license_name) : null, 'license_name' => ($license->license_name) ? e($license->license_name) : null,
'license_email' => ($license->license_email) ? e($license->license_email) : null, 'license_email' => ($license->license_email) ? e($license->license_email) : null,

View file

@ -533,6 +533,7 @@ class License extends Depreciable
return $this->licenseSeatsRelation() return $this->licenseSeatsRelation()
->whereNull('asset_id') ->whereNull('asset_id')
->whereNull('assigned_to') ->whereNull('assigned_to')
->where('unreassignable_seat', '=', false)
->whereNull('deleted_at'); ->whereNull('deleted_at');
} }
@ -582,7 +583,22 @@ class License extends Depreciable
return 0; return 0;
} }
/**
* Calculates the number of unreassignable seats
*
* @author G. Martinez
* @since [v7.1.15]
*/
public static function unReassignableCount($license) : int
{
$count = 0;
if (!$license->reassignable) {
$count = licenseSeat::query()->where('unreassignable_seat', '=', true)
->where('license_id', '=', $license->id)
->count();
}
return $count;
}
/** /**
* Calculates the number of remaining seats * Calculates the number of remaining seats
* *
@ -590,11 +606,12 @@ class License extends Depreciable
* @since [v1.0] * @since [v1.0]
* @return int * @return int
*/ */
public function remaincount() public function remaincount() : int
{ {
$total = $this->licenseSeatsCount; $total = $this->licenseSeatsCount;
$taken = $this->assigned_seats_count; $taken = $this->assigned_seats_count;
$diff = ($total - $taken); $unreassignable = self::unReassignableCount($this);
$diff = ($total - $taken - $unreassignable);
return (int) $diff; return (int) $diff;
} }
@ -652,6 +669,7 @@ class License extends Depreciable
{ {
return $this->licenseseats() return $this->licenseseats()
->whereNull('deleted_at') ->whereNull('deleted_at')
->where('unreassignable_seat', '=', false)
->where(function ($query) { ->where(function ($query) {
$query->whereNull('assigned_to') $query->whereNull('assigned_to')
->whereNull('asset_id'); ->whereNull('asset_id');

View file

@ -21,6 +21,9 @@ class LicenseSeat extends SnipeModel implements ICompanyableChild
protected $guarded = 'id'; protected $guarded = 'id';
protected $table = 'license_seats'; protected $table = 'license_seats';
protected $casts = [
'unreassignable_seat' => 'boolean',
];
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.

View file

@ -14,6 +14,7 @@ class LicenseSeatFactory extends Factory
{ {
return [ return [
'license_id' => License::factory(), 'license_id' => License::factory(),
'unreassignable_seat' => false,
]; ];
} }

View file

@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('license_seats', function (Blueprint $table) {
$table->addColumn('boolean', 'unreassignable_seat')->default(false)->after('assigned_to');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('license_seats', function (Blueprint $table) {
$table->dropColumn('unreassignable_seat');
});
}
};

View file

@ -50,7 +50,7 @@ return array(
'checkin' => array( 'checkin' => array(
'error' => 'There was an issue checking in the license. Please try again.', 'error' => 'There was an issue checking in the license. Please try again.',
'not_reassignable' => 'License not reassignable', 'not_reassignable' => 'Seat has been used',
'success' => 'The license was checked in successfully' 'success' => 'The license was checked in successfully'
), ),

View file

@ -32,7 +32,7 @@
<x-icon type="seats" class="fa-2x" /> <x-icon type="seats" class="fa-2x" />
</span> </span>
<span class="hidden-xs hidden-sm">{{ trans('admin/licenses/form.seats') }}</span> <span class="hidden-xs hidden-sm">{{ trans('admin/licenses/form.seats') }}</span>
<span class="badge badge-secondary">{{ number_format($license->availCount()->count()) }} / {{ number_format($license->seats) }}</span> <span class="badge badge-secondary">{{ number_format($license->availCount()->count()) }} / {{ number_format($license->seats)}}</span>
</a> </a>
</li> </li>
@ -573,13 +573,6 @@
{{ trans('admin/licenses/general.bulk.checkin_all.button') }} {{ trans('admin/licenses/general.bulk.checkin_all.button') }}
</a> </a>
</span> </span>
@elseif (! $license->reassignable)
<span data-tooltip="true" title=" {{ trans('admin/licenses/general.bulk.checkin_all.disabled_tooltip_reassignable') }}">
<a href="#" class="btn btn-primary bg-purple btn-sm btn-social btn-block hidden-print disabled" style="margin-bottom: 25px;">
<x-icon type="checkin" />
{{ trans('admin/licenses/general.bulk.checkin_all.button') }}
</a>
</span>
@else @else
<a href="#" class="btn btn-primary bg-purple btn-sm btn-social btn-block hidden-print" style="margin-bottom: 25px;" data-toggle="modal" data-tooltip="true" data-target="#checkinFromAllModal" data-content="{{ trans('general.sure_to_delete') }} data-title="{{ trans('general.delete') }}" onClick="return false;"> <a href="#" class="btn btn-primary bg-purple btn-sm btn-social btn-block hidden-print" style="margin-bottom: 25px;" data-toggle="modal" data-tooltip="true" data-target="#checkinFromAllModal" data-content="{{ trans('general.sure_to_delete') }} data-title="{{ trans('general.delete') }}" onClick="return false;">
<x-icon type="checkin" /> <x-icon type="checkin" />

View file

@ -464,13 +464,16 @@
// Checkouts need the license ID, checkins need the specific seat ID // Checkouts need the license ID, checkins need the specific seat ID
function licenseSeatInOutFormatter(value, row) { function licenseSeatInOutFormatter(value, row) {
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>';
} else
// The user is allowed to check the license seat out and it's available // 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) && ((!row.asset_id) && (!row.assigned_to))) { if ((row.available_actions.checkout === true) && (row.user_can_checkout === true) && ((!row.asset_id) && (!row.assigned_to))) {
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>'; 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>';
} else {
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>';
} }
else {
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) { function genericCheckinCheckoutFormatter(destination) {

View file

@ -0,0 +1,27 @@
<?php
namespace Tests\Feature\Checkins\Api;
use App\Models\LicenseSeat;
use App\Models\User;
use Tests\TestCase;
class LicenseCheckinTest extends TestCase
{
public function testUnreassignableLicenseSeatMarkedUponCheckin()
{
$licenseSeat = LicenseSeat::factory()
->notReassignable()
->assignedToUser()
->create();
$this->assertEquals(false, $licenseSeat->unreassignable_seat);
$this->actingAs(User::factory()->checkoutLicenses()->create())
->post(route('licenses.checkin.save', $licenseSeat));
$licenseSeat->refresh();
$this->assertEquals(true, $licenseSeat->unreassignable_seat);
}
}

View file

@ -20,7 +20,7 @@ class LicenseCheckinTest extends TestCase
->assertForbidden(); ->assertForbidden();
} }
public function testCannotCheckinNonReassignableLicense() public function testNonReassignableLicenseSeatCantBeCheckedOut()
{ {
$licenseSeat = LicenseSeat::factory() $licenseSeat = LicenseSeat::factory()
->notReassignable() ->notReassignable()
@ -28,13 +28,11 @@ class LicenseCheckinTest extends TestCase
->create(); ->create();
$this->actingAs(User::factory()->checkoutLicenses()->create()) $this->actingAs(User::factory()->checkoutLicenses()->create())
->post(route('licenses.checkin.save', $licenseSeat), [ ->post(route('licenses.checkin.save', $licenseSeat));
'notes' => 'my note',
'redirect_option' => 'index',
])
->assertSessionHas('error', trans('admin/licenses/message.checkin.not_reassignable') . '.');
$this->assertNotNull($licenseSeat->fresh()->assigned_to); $licenseSeat->refresh();
$this->assertEquals(true, $licenseSeat->unreassignable_seat);
} }
public function testCannotCheckinLicenseThatIsNotAssigned() public function testCannotCheckinLicenseThatIsNotAssigned()