diff --git a/app/Helpers/Helper.php b/app/Helpers/Helper.php index 95a344dce9e7..8f653ff35303 100644 --- a/app/Helpers/Helper.php +++ b/app/Helpers/Helper.php @@ -1529,4 +1529,5 @@ static public function getRedirectOption($request, $id, $table, $item_id = null) } return redirect()->back()->with('error', trans('admin/hardware/message.checkout.error')); } + } diff --git a/app/Http/Controllers/Api/LicenseSeatsController.php b/app/Http/Controllers/Api/LicenseSeatsController.php index 2ed709732236..0f752cbb83c9 100644 --- a/app/Http/Controllers/Api/LicenseSeatsController.php +++ b/app/Http/Controllers/Api/LicenseSeatsController.php @@ -119,7 +119,9 @@ public function update(Request $request, $licenseId, $seatId) : JsonResponse | a // nothing to update 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, // we simply let assets take precedence over users... if ($licenseSeat->isDirty('assigned_to')) { @@ -136,7 +138,11 @@ public function update(Request $request, $licenseId, $seatId) : JsonResponse | a if ($licenseSeat->save()) { 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'))); } diff --git a/app/Http/Controllers/Licenses/LicenseCheckinController.php b/app/Http/Controllers/Licenses/LicenseCheckinController.php index 373a167642d0..011eca5e8150 100644 --- a/app/Http/Controllers/Licenses/LicenseCheckinController.php +++ b/app/Http/Controllers/Licenses/LicenseCheckinController.php @@ -69,12 +69,7 @@ public function store(Request $request, $seatId = null, $backTo = null) $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 $rules = [ @@ -100,13 +95,16 @@ public function store(Request $request, $seatId = null, $backTo = null) $licenseSeat->assigned_to = null; $licenseSeat->asset_id = null; $licenseSeat->notes = $request->input('notes'); + if (! $licenseSeat->license->reassignable) { + $licenseSeat->unreassignable_seat = true; + } session()->put(['redirect_option' => $request->get('redirect_option')]); // Was the asset updated? 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')); @@ -131,21 +129,17 @@ public function bulkCheckin(Request $request, $licenseId) { $license = License::findOrFail($licenseId); $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) ->whereNotNull('assigned_to') - ->with('user') + ->with('user', 'license') ->get(); + $license = $licenseSeatsByUser->first()?->license; foreach ($licenseSeatsByUser as $user_seat) { $user_seat->assigned_to = null; - + if ($license && ! $license->reassignable) { + $user_seat->unreassignable_seat = true; + } if ($user_seat->save()) { 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')); @@ -158,9 +152,12 @@ public function bulkCheckin(Request $request, $licenseId) { ->get(); $count = 0; + $license = $licenseSeatsByAsset->first()?->license; foreach ($licenseSeatsByAsset as $asset_seat) { $asset_seat->asset_id = null; - + if ($license && ! $license->reassignable) { + $asset_seat->unreassignable_seat = true; + } if ($asset_seat->save()) { 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')); diff --git a/app/Http/Controllers/Licenses/LicensesController.php b/app/Http/Controllers/Licenses/LicensesController.php index 6098423ba3e6..7ef6eeb30b9e 100755 --- a/app/Http/Controllers/Licenses/LicensesController.php +++ b/app/Http/Controllers/Licenses/LicensesController.php @@ -248,16 +248,25 @@ public function show($licenseId = null) } $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(); - $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); return view('licenses.view', compact('license')) ->with('users_count', $users_count) ->with('total_seats_count', $total_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); } diff --git a/app/Http/Transformers/LicenseSeatsTransformer.php b/app/Http/Transformers/LicenseSeatsTransformer.php index 7ae68e9e449c..98688762956f 100644 --- a/app/Http/Transformers/LicenseSeatsTransformer.php +++ b/app/Http/Transformers/LicenseSeatsTransformer.php @@ -6,7 +6,6 @@ use App\Models\LicenseSeat; use Illuminate\Support\Facades\Gate; use Illuminate\Database\Eloquent\Collection; - class LicenseSeatsTransformer { public function transformLicenseSeats(Collection $seats, $total) @@ -48,6 +47,7 @@ public function transformLicenseSeat(LicenseSeat $seat, $seat_count = 0) 'reassignable' => (bool) $seat->license->reassignable, 'notes' => e($seat->notes), 'user_can_checkout' => (($seat->assigned_to == '') && ($seat->asset_id == '')), + 'disabled' => $seat->unreassignable_seat, ]; if ($seat_count != 0) { diff --git a/app/Http/Transformers/LicensesTransformer.php b/app/Http/Transformers/LicensesTransformer.php index 673ac06b3dab..3be7b256da8e 100644 --- a/app/Http/Transformers/LicensesTransformer.php +++ b/app/Http/Transformers/LicensesTransformer.php @@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Gate; use Illuminate\Database\Eloquent\Collection; -class LicensesTransformer +class LicensesTransformer { public function transformLicenses(Collection $licenses, $total) { @@ -37,7 +37,7 @@ public function transformLicense(License $license) '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, + 'free_seats_count' => (int) $license->free_seats_count - License::unReassignableCount($license), 'min_amt' => ($license->min_amt) ? (int) ($license->min_amt) : null, 'license_name' => ($license->license_name) ? e($license->license_name) : null, 'license_email' => ($license->license_email) ? e($license->license_email) : null, diff --git a/app/Models/License.php b/app/Models/License.php index 0997c1e57b99..490a8401a4eb 100755 --- a/app/Models/License.php +++ b/app/Models/License.php @@ -533,6 +533,7 @@ public function availCount() return $this->licenseSeatsRelation() ->whereNull('asset_id') ->whereNull('assigned_to') + ->where('unreassignable_seat', '=', false) ->whereNull('deleted_at'); } @@ -582,7 +583,22 @@ public function getAssignedSeatsCountAttribute() 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 * @@ -590,11 +606,12 @@ public function getAssignedSeatsCountAttribute() * @since [v1.0] * @return int */ - public function remaincount() + public function remaincount() : int { $total = $this->licenseSeatsCount; $taken = $this->assigned_seats_count; - $diff = ($total - $taken); + $unreassignable = self::unReassignableCount($this); + $diff = ($total - $taken - $unreassignable); return (int) $diff; } @@ -652,6 +669,7 @@ public function freeSeat() { return $this->licenseseats() ->whereNull('deleted_at') + ->where('unreassignable_seat', '=', false) ->where(function ($query) { $query->whereNull('assigned_to') ->whereNull('asset_id'); diff --git a/app/Models/LicenseSeat.php b/app/Models/LicenseSeat.php index 397a14146870..6243d7d02006 100755 --- a/app/Models/LicenseSeat.php +++ b/app/Models/LicenseSeat.php @@ -21,6 +21,9 @@ class LicenseSeat extends SnipeModel implements ICompanyableChild protected $guarded = 'id'; protected $table = 'license_seats'; + protected $casts = [ + 'unreassignable_seat' => 'boolean', + ]; /** * The attributes that are mass assignable. diff --git a/database/factories/LicenseSeatFactory.php b/database/factories/LicenseSeatFactory.php index aaf75bdc2d82..ee516b6dac24 100644 --- a/database/factories/LicenseSeatFactory.php +++ b/database/factories/LicenseSeatFactory.php @@ -14,6 +14,7 @@ public function definition() { return [ 'license_id' => License::factory(), + 'unreassignable_seat' => false, ]; } diff --git a/database/migrations/2025_01_15_190348_adds_unavailable_to_license_seats_tables.php b/database/migrations/2025_01_15_190348_adds_unavailable_to_license_seats_tables.php new file mode 100644 index 000000000000..40c8ad53679b --- /dev/null +++ b/database/migrations/2025_01_15_190348_adds_unavailable_to_license_seats_tables.php @@ -0,0 +1,26 @@ +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'); + }); + } +}; diff --git a/resources/lang/en-US/admin/licenses/message.php b/resources/lang/en-US/admin/licenses/message.php index 74e1d7af5ac5..9a0219d857a6 100644 --- a/resources/lang/en-US/admin/licenses/message.php +++ b/resources/lang/en-US/admin/licenses/message.php @@ -50,7 +50,7 @@ 'checkin' => array( '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' ), diff --git a/resources/views/licenses/view.blade.php b/resources/views/licenses/view.blade.php index 2de0f9d6e0bc..cc2f10b980a7 100755 --- a/resources/views/licenses/view.blade.php +++ b/resources/views/licenses/view.blade.php @@ -32,7 +32,7 @@ - {{ number_format($license->availCount()->count()) }} / {{ number_format($license->seats) }} + {{ number_format($license->availCount()->count()) }} / {{ number_format($license->seats)}} @@ -573,13 +573,6 @@ class="table table-striped snipe-table" {{ trans('admin/licenses/general.bulk.checkin_all.button') }} - @elseif (! $license->reassignable) - - - - {{ trans('admin/licenses/general.bulk.checkin_all.button') }} - - @else diff --git a/resources/views/partials/bootstrap-table.blade.php b/resources/views/partials/bootstrap-table.blade.php index 099640f0b670..136428870dad 100644 --- a/resources/views/partials/bootstrap-table.blade.php +++ b/resources/views/partials/bootstrap-table.blade.php @@ -431,13 +431,16 @@ function checkboxEnabledFormatter (value, row) { // Checkouts need the license ID, checkins need the specific seat ID function licenseSeatInOutFormatter(value, row) { + if(row.disabled) { + return '{{ trans('general.checkout') }}'; + } 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) && ((!row.asset_id) && (!row.assigned_to))) { return '{{ trans('general.checkout') }}'; - } else { - return '{{ trans('general.checkin') }}'; } - + else { + return '{{ trans('general.checkin') }}'; + } } function genericCheckinCheckoutFormatter(destination) { diff --git a/tests/Feature/Checkins/Api/LicenseCheckinTest.php b/tests/Feature/Checkins/Api/LicenseCheckinTest.php new file mode 100644 index 000000000000..e649b9f43b95 --- /dev/null +++ b/tests/Feature/Checkins/Api/LicenseCheckinTest.php @@ -0,0 +1,27 @@ +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); + } +} \ No newline at end of file diff --git a/tests/Feature/Checkins/Ui/LicenseCheckinTest.php b/tests/Feature/Checkins/Ui/LicenseCheckinTest.php index 7a6553d8ba61..f1f912cad454 100644 --- a/tests/Feature/Checkins/Ui/LicenseCheckinTest.php +++ b/tests/Feature/Checkins/Ui/LicenseCheckinTest.php @@ -20,7 +20,7 @@ public function testCheckingInLicenseRequiresCorrectPermission() ->assertForbidden(); } - public function testCannotCheckinNonReassignableLicense() + public function testNonReassignableLicenseSeatCantBeCheckedOut() { $licenseSeat = LicenseSeat::factory() ->notReassignable() @@ -28,13 +28,11 @@ public function testCannotCheckinNonReassignableLicense() ->create(); $this->actingAs(User::factory()->checkoutLicenses()->create()) - ->post(route('licenses.checkin.save', $licenseSeat), [ - 'notes' => 'my note', - 'redirect_option' => 'index', - ]) - ->assertSessionHas('error', trans('admin/licenses/message.checkin.not_reassignable') . '.'); + ->post(route('licenses.checkin.save', $licenseSeat)); + + $licenseSeat->refresh(); - $this->assertNotNull($licenseSeat->fresh()->assigned_to); + $this->assertEquals(true, $licenseSeat->unreassignable_seat); } public function testCannotCheckinLicenseThatIsNotAssigned()