Merge pull request #15185 from arne-kroeger/feat/accesspories-checkout-to-location-or-asset

Added #14979: add checkout to location and assets functionality to accessories
This commit is contained in:
snipe 2024-07-29 18:50:48 +01:00 committed by GitHub
commit 4eccb5ffc6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 411 additions and 123 deletions

View file

@ -92,7 +92,7 @@ class SQLStreamer {
$parser->line_aware_piping(); // <----- THIS is doing the heavy lifting! $parser->line_aware_piping(); // <----- THIS is doing the heavy lifting!
$check_tables = ['settings' => null, 'migrations' => null /* 'assets' => null */]; //TODO - move to statics? $check_tables = ['settings' => null, 'migrations' => null /* 'assets' => null */]; //TODO - move to statics?
//can't use 'users' because the 'accessories_users' table? //can't use 'users' because the 'accessories_checkout' table?
// can't use 'assets' because 'ver1_components_assets' // can't use 'assets' because 'ver1_components_assets'
foreach($check_tables as $check_table => $_ignore) { foreach($check_tables as $check_table => $_ignore) {
foreach ($parser->tablenames as $tablename => $_count) { foreach ($parser->tablenames as $tablename => $_count) {

View file

@ -721,7 +721,7 @@ class Helper
{ {
$alert_threshold = \App\Models\Setting::getSettings()->alert_threshold; $alert_threshold = \App\Models\Setting::getSettings()->alert_threshold;
$consumables = Consumable::withCount('consumableAssignments as consumable_assignments_count')->whereNotNull('min_amt')->get(); $consumables = Consumable::withCount('consumableAssignments as consumable_assignments_count')->whereNotNull('min_amt')->get();
$accessories = Accessory::withCount('users as users_count')->whereNotNull('min_amt')->get(); $accessories = Accessory::withCount('checkouts as checkouts_count')->whereNotNull('min_amt')->get();
$components = Component::whereNotNull('min_amt')->get(); $components = Component::whereNotNull('min_amt')->get();
$asset_models = AssetModel::where('min_amt', '>', 0)->get(); $asset_models = AssetModel::where('min_amt', '>', 0)->get();
$licenses = License::where('min_amt', '>', 0)->get(); $licenses = License::where('min_amt', '>', 0)->get();
@ -749,7 +749,7 @@ class Helper
} }
foreach ($accessories as $accessory) { foreach ($accessories as $accessory) {
$avail = $accessory->qty - $accessory->users_count; $avail = $accessory->qty - $accessory->checkouts_count;
if ($avail < ($accessory->min_amt) + $alert_threshold) { if ($avail < ($accessory->min_amt) + $alert_threshold) {
if ($accessory->qty > 0) { if ($accessory->qty > 0) {
$percent = number_format((($avail / $accessory->qty) * 100), 0); $percent = number_format((($avail / $accessory->qty) * 100), 0);

View file

@ -144,12 +144,12 @@ class AccessoriesController extends Controller
*/ */
public function update(ImageUploadRequest $request, $accessoryId = null) : RedirectResponse public function update(ImageUploadRequest $request, $accessoryId = null) : RedirectResponse
{ {
if ($accessory = Accessory::withCount('users as users_count')->find($accessoryId)) { if ($accessory = Accessory::withCount('checkouts as checkouts_count')->find($accessoryId)) {
$this->authorize($accessory); $this->authorize($accessory);
$validator = Validator::make($request->all(), [ $validator = Validator::make($request->all(), [
"qty" => "required|numeric|min:$accessory->users_count" "qty" => "required|numeric|min:$accessory->checkouts_count"
]); ]);
if ($validator->fails()) { if ($validator->fails()) {
@ -233,7 +233,7 @@ class AccessoriesController extends Controller
*/ */
public function show($accessoryID = null) : View | RedirectResponse public function show($accessoryID = null) : View | RedirectResponse
{ {
$accessory = Accessory::withCount('users as users_count')->find($accessoryID); $accessory = Accessory::withCount('checkouts as checkouts_count')->find($accessoryID);
$this->authorize('view', $accessory); $this->authorize('view', $accessory);
if (isset($accessory->id)) { if (isset($accessory->id)) {
return view('accessories/view', compact('accessory')); return view('accessories/view', compact('accessory'));

View file

@ -6,6 +6,7 @@ use App\Events\CheckoutableCheckedIn;
use App\Helpers\Helper; use App\Helpers\Helper;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Accessory; use App\Models\Accessory;
use App\Models\AccessoryCheckout;
use App\Models\User; use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@ -24,7 +25,7 @@ class AccessoryCheckinController extends Controller
*/ */
public function create($accessoryUserId = null, $backto = null) : View | RedirectResponse public function create($accessoryUserId = null, $backto = null) : View | RedirectResponse
{ {
if (is_null($accessory_user = DB::table('accessories_users')->find($accessoryUserId))) { if (is_null($accessory_user = DB::table('accessories_checkout')->find($accessoryUserId))) {
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.not_found')); return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.not_found'));
} }
@ -39,16 +40,16 @@ class AccessoryCheckinController extends Controller
* *
* @uses Accessory::checkin_email() to determine if an email can and should be sent * @uses Accessory::checkin_email() to determine if an email can and should be sent
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @param null $accessoryUserId * @param null $accessoryCheckoutId
* @param string $backto * @param string $backto
*/ */
public function store(Request $request, $accessoryUserId = null, $backto = null) : RedirectResponse public function store(Request $request, $accessoryCheckoutId = null, $backto = null) : RedirectResponse
{ {
if (is_null($accessory_user = DB::table('accessories_users')->find($accessoryUserId))) { if (is_null($accessory_checkout = AccessoryCheckout::find($accessoryCheckoutId))) {
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist')); return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
} }
$accessory = Accessory::find($accessory_user->accessory_id); $accessory = Accessory::find($accessory_checkout->accessory_id);
$this->authorize('checkin', $accessory); $this->authorize('checkin', $accessory);
@ -59,10 +60,8 @@ class AccessoryCheckinController extends Controller
} }
// Was the accessory updated? // Was the accessory updated?
if (DB::table('accessories_users')->where('id', '=', $accessory_user->id)->delete()) { if ($accessory_checkout->delete()) {
$return_to = e($accessory_user->assigned_to); event(new CheckoutableCheckedIn($accessory, $accessory_checkout->assignedTo, auth()->user(), $request->input('note'), $checkin_at));
event(new CheckoutableCheckedIn($accessory, User::find($return_to), auth()->user(), $request->input('note'), $checkin_at));
session()->put(['redirect_option' => $request->get('redirect_option')]); session()->put(['redirect_option' => $request->get('redirect_option')]);

View file

@ -4,9 +4,11 @@ namespace App\Http\Controllers\Accessories;
use App\Events\CheckoutableCheckedOut; use App\Events\CheckoutableCheckedOut;
use App\Helpers\Helper; use App\Helpers\Helper;
use App\Http\Controllers\CheckInOutRequest;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\AccessoryCheckoutRequest; use App\Http\Requests\AccessoryCheckoutRequest;
use App\Models\Accessory; use App\Models\Accessory;
use App\Models\AccessoryCheckout;
use App\Models\User; use App\Models\User;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -16,6 +18,9 @@ use \Illuminate\Http\RedirectResponse;
class AccessoryCheckoutController extends Controller class AccessoryCheckoutController extends Controller
{ {
use CheckInOutRequest;
/** /**
* Return the form to checkout an Accessory to a user. * Return the form to checkout an Accessory to a user.
* *
@ -25,7 +30,7 @@ class AccessoryCheckoutController extends Controller
public function create($id) : View | RedirectResponse public function create($id) : View | RedirectResponse
{ {
if ($accessory = Accessory::withCount('users as users_count')->find($id)) { if ($accessory = Accessory::withCount('checkouts as checkouts_count')->find($id)) {
$this->authorize('checkout', $accessory); $this->authorize('checkout', $accessory);
@ -58,30 +63,32 @@ class AccessoryCheckoutController extends Controller
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @param Request $request * @param Request $request
* @param int $accessory * @param Accessory $accessory
*/ */
public function store(AccessoryCheckoutRequest $request, Accessory $accessory) : RedirectResponse public function store(AccessoryCheckoutRequest $request, Accessory $accessory) : RedirectResponse
{ {
$this->authorize('checkout', $accessory); $this->authorize('checkout', $accessory);
$accessory->assigned_to = $request->input('assigned_to');
$user = User::find($request->input('assigned_to'));
$accessory->checkout_qty = $request->input('checkout_qty', 1);
$target = $this->determineCheckoutTarget();
$accessory->checkout_qty = $request->input('checkout_qty', 1);
for ($i = 0; $i < $accessory->checkout_qty; $i++) { for ($i = 0; $i < $accessory->checkout_qty; $i++) {
$accessory->users()->attach($accessory->id, [ AccessoryCheckout::create([
'accessory_id' => $accessory->id, 'accessory_id' => $accessory->id,
'created_at' => Carbon::now(), 'created_at' => Carbon::now(),
'user_id' => Auth::id(), 'user_id' => Auth::id(),
'assigned_to' => $request->input('assigned_to'), 'assigned_to' => $target->id,
'assigned_type' => $target::class,
'note' => $request->input('note'), 'note' => $request->input('note'),
]); ]);
} }
event(new CheckoutableCheckedOut($accessory, $user, auth()->user(), $request->input('note'))); event(new CheckoutableCheckedOut($accessory, $target, auth()->user(), $request->input('note')));
// Set this as user since we only allow checkout to user for this item type // Set this as user since we only allow checkout to user for this item type
$request->request->add(['checkout_to_type' => 'user']); $request->request->add(['checkout_to_type' => request('checkout_to_type')]);
$request->request->add(['assigned_user' => $user->id]); $request->request->add(['assigned_user' => $target->id]);
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]); session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);

View file

@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api;
use App\Events\CheckoutableCheckedOut; use App\Events\CheckoutableCheckedOut;
use App\Helpers\Helper; use App\Helpers\Helper;
use App\Http\Controllers\CheckInOutRequest;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\AccessoryCheckoutRequest; use App\Http\Requests\AccessoryCheckoutRequest;
use App\Http\Requests\StoreAccessoryRequest; use App\Http\Requests\StoreAccessoryRequest;
@ -17,10 +18,12 @@ use Carbon\Carbon;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Http\Requests\ImageUploadRequest; use App\Http\Requests\ImageUploadRequest;
use App\Models\AccessoryCheckout;
class AccessoriesController extends Controller class AccessoriesController extends Controller
{ {
use CheckInOutRequest;
/** /**
* Display a listing of the resource. * Display a listing of the resource.
* *
@ -48,13 +51,13 @@ class AccessoriesController extends Controller
'min_amt', 'min_amt',
'company_id', 'company_id',
'notes', 'notes',
'users_count', 'checkouts_count',
'qty', 'qty',
]; ];
$accessories = Accessory::select('accessories.*')->with('category', 'company', 'manufacturer', 'users', 'location', 'supplier') $accessories = Accessory::select('accessories.*')->with('category', 'company', 'manufacturer', 'checkouts', 'location', 'supplier')
->withCount('users as users_count'); ->withCount('checkouts as checkouts_count');
if ($request->filled('search')) { if ($request->filled('search')) {
$accessories = $accessories->TextSearch($request->input('search')); $accessories = $accessories->TextSearch($request->input('search'));
@ -154,7 +157,7 @@ class AccessoriesController extends Controller
public function show($id) public function show($id)
{ {
$this->authorize('view', Accessory::class); $this->authorize('view', Accessory::class);
$accessory = Accessory::withCount('users as users_count')->findOrFail($id); $accessory = Accessory::withCount('checkouts as checkouts_count')->findOrFail($id);
return (new AccessoriesTransformer)->transformAccessory($accessory); return (new AccessoriesTransformer)->transformAccessory($accessory);
} }
@ -197,28 +200,23 @@ class AccessoriesController extends Controller
$offset = request('offset', 0); $offset = request('offset', 0);
$limit = request('limit', 50); $limit = request('limit', 50);
$accessory_users = $accessory->users; $accessory_checkouts = $accessory->checkouts;
$total = $accessory_users->count(); $total = $accessory_checkouts->count();
if ($total < $offset) { if ($total < $offset) {
$offset = 0; $offset = 0;
} }
$accessory_users = $accessory->users()->skip($offset)->take($limit)->get(); $accessory_checkouts = $accessory->checkouts()->skip($offset)->take($limit)->get();
if ($request->filled('search')) { if ($request->filled('search')) {
$accessory_users = $accessory->users()
->where(function ($query) use ($request) { $accessory_checkouts = $accessory->checkouts()->TextSearch($request->input('search'))
$search_str = '%' . $request->input('search') . '%';
$query->where('first_name', 'like', $search_str)
->orWhere('last_name', 'like', $search_str)
->orWhere('note', 'like', $search_str);
})
->get(); ->get();
$total = $accessory_users->count(); $total = $accessory_checkouts->count();
} }
return (new AccessoriesTransformer)->transformCheckedoutAccessory($accessory, $accessory_users, $total); return (new AccessoriesTransformer)->transformCheckedoutAccessory($accessory, $accessory_checkouts, $total);
} }
@ -282,22 +280,22 @@ class AccessoriesController extends Controller
public function checkout(AccessoryCheckoutRequest $request, Accessory $accessory) public function checkout(AccessoryCheckoutRequest $request, Accessory $accessory)
{ {
$this->authorize('checkout', $accessory); $this->authorize('checkout', $accessory);
$accessory->assigned_to = $request->input('assigned_to'); $target = $this->determineCheckoutTarget();
$user = User::find($request->input('assigned_to'));
$accessory->checkout_qty = $request->input('checkout_qty', 1); $accessory->checkout_qty = $request->input('checkout_qty', 1);
for ($i = 0; $i < $accessory->checkout_qty; $i++) { for ($i = 0; $i < $accessory->checkout_qty; $i++) {
$accessory->users()->attach($accessory->id, [ AccessoryCheckout::create([
'accessory_id' => $accessory->id, 'accessory_id' => $accessory->id,
'created_at' => Carbon::now(), 'created_at' => Carbon::now(),
'user_id' => Auth::id(), 'user_id' => Auth::id(),
'assigned_to' => $request->input('assigned_to'), 'assigned_to' => $target->id,
'assigned_type' => $target::class,
'note' => $request->input('note'), 'note' => $request->input('note'),
]); ]);
} }
// Set this value to be able to pass the qty through to the event // Set this value to be able to pass the qty through to the event
event(new CheckoutableCheckedOut($accessory, $user, auth()->user(), $request->input('note'))); event(new CheckoutableCheckedOut($accessory, $target, auth()->user(), $request->input('note')));
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkout.success'))); return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkout.success')));
@ -316,19 +314,19 @@ class AccessoriesController extends Controller
*/ */
public function checkin(Request $request, $accessoryUserId = null) public function checkin(Request $request, $accessoryUserId = null)
{ {
if (is_null($accessory_user = DB::table('accessories_users')->find($accessoryUserId))) { if (is_null($accessory_checkout = AccessoryCheckout::find($accessoryUserId))) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist'))); return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist')));
} }
$accessory = Accessory::find($accessory_user->accessory_id); $accessory = Accessory::find($accessory_checkout->accessory_id);
$this->authorize('checkin', $accessory); $this->authorize('checkin', $accessory);
$logaction = $accessory->logCheckin(User::find($accessory_user->assigned_to), $request->input('note')); $logaction = $accessory->logCheckin(User::find($accessory_checkout->assigned_to), $request->input('note'));
// Was the accessory updated? // Was the accessory updated?
if (DB::table('accessories_users')->where('id', '=', $accessory_user->id)->delete()) { if ($accessory_checkout->delete()) {
if (! is_null($accessory_user->assigned_to)) { if (! is_null($accessory_checkout->assigned_to)) {
$user = User::find($accessory_user->assigned_to); $user = User::find($accessory_checkout->assigned_to);
} }
$data['log_id'] = $logaction->id; $data['log_id'] = $logaction->id;

View file

@ -219,7 +219,7 @@ class BulkUsersController extends Controller
$users = User::whereIn('id', $user_raw_array)->get(); $users = User::whereIn('id', $user_raw_array)->get();
$assets = Asset::whereIn('assigned_to', $user_raw_array)->where('assigned_type', \App\Models\User::class)->get(); $assets = Asset::whereIn('assigned_to', $user_raw_array)->where('assigned_type', \App\Models\User::class)->get();
$accessories = DB::table('accessories_users')->whereIn('assigned_to', $user_raw_array)->get(); $accessories = DB::table('accessories_checkout')->whereIn('assigned_to', $user_raw_array)->get();
$licenses = DB::table('license_seats')->whereIn('assigned_to', $user_raw_array)->get(); $licenses = DB::table('license_seats')->whereIn('assigned_to', $user_raw_array)->get();
$consumables = DB::table('consumables_users')->whereIn('assigned_to', $user_raw_array)->get(); $consumables = DB::table('consumables_users')->whereIn('assigned_to', $user_raw_array)->get();

View file

@ -44,13 +44,10 @@ class AccessoryCheckoutRequest extends ImageUploadRequest
return array_merge( return array_merge(
[ [
'assigned_to' => [ 'assigned_user' => 'required_without_all:assigned_asset,assigned_location',
'required', 'assigned_asset' => 'required_without_all:assigned_user,assigned_location',
'integer', 'assigned_location' => 'required_without_all:assigned_user,assigned_asset',
'exists:users,id,deleted_at,NULL',
'not_array'
],
'number_remaining_after_checkout' => [ 'number_remaining_after_checkout' => [
'min:0', 'min:0',
'required', 'required',

View file

@ -39,7 +39,7 @@ class AccessoriesTransformer
'order_number' => ($accessory->order_number) ? e($accessory->order_number) : null, 'order_number' => ($accessory->order_number) ? e($accessory->order_number) : null,
'min_qty' => ($accessory->min_amt) ? (int) $accessory->min_amt : null, 'min_qty' => ($accessory->min_amt) ? (int) $accessory->min_amt : null,
'remaining_qty' => (int) $accessory->numRemaining(), 'remaining_qty' => (int) $accessory->numRemaining(),
'users_count' => $accessory->users_count, 'checkouts_count' => $accessory->checkouts_count,
'created_at' => Helper::getFormattedDateObject($accessory->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($accessory->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($accessory->updated_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($accessory->updated_at, 'datetime'),
@ -66,27 +66,42 @@ class AccessoriesTransformer
return $array; return $array;
} }
public function transformCheckedoutAccessory($accessory, $accessory_users, $total) public function transformCheckedoutAccessory($accessory, $accessory_checkouts, $total)
{ {
$array = []; $array = [];
foreach ($accessory_users as $user) { foreach ($accessory_checkouts as $checkout) {
$array[] = [ $array[] = [
'id' => $checkout->id,
'assigned_pivot_id' => $user->pivot->id, 'assigned_to' => $this->transformAssignedTo($checkout),
'id' => (int) $user->id, 'checkout_notes' => e($checkout->note),
'username' => e($user->username), 'last_checkout' => Helper::getFormattedDateObject($checkout->created_at, 'datetime'),
'name' => e($user->getFullNameAttribute()),
'first_name'=> e($user->first_name),
'last_name'=> e($user->last_name),
'employee_number' => e($user->employee_num),
'checkout_notes' => e($user->pivot->note),
'last_checkout' => Helper::getFormattedDateObject($user->pivot->created_at, 'datetime'),
'type' => 'user',
'available_actions' => ['checkin' => true], 'available_actions' => ['checkin' => true],
]; ];
} }
return (new DatatablesTransformer)->transformDatatables($array, $total); return (new DatatablesTransformer)->transformDatatables($array, $total);
} }
public function transformAssignedTo($accessoryCheckout)
{
if ($accessoryCheckout->checkedOutToUser()) {
return [
'id' => (int) $accessoryCheckout->assigned->id,
'username' => e($accessoryCheckout->assigned->username),
'name' => e($accessoryCheckout->assigned->getFullNameAttribute()),
'first_name'=> e($accessoryCheckout->assigned->first_name),
'last_name'=> ($accessoryCheckout->assigned->last_name) ? e($accessoryCheckout->assigned->last_name) : null,
'email'=> ($accessoryCheckout->assigned->email) ? e($accessoryCheckout->assigned->email) : null,
'employee_number' => ($accessoryCheckout->assigned->employee_num) ? e($accessoryCheckout->assigned->employee_num) : null,
'type' => 'user',
];
}
return $accessoryCheckout->assigned ? [
'id' => $accessoryCheckout->assigned->id,
'name' => e($accessoryCheckout->assigned->display_name),
'type' => $accessoryCheckout->assignedType(),
] : null;
}
} }

View file

@ -205,11 +205,11 @@ class ActionlogsTransformer
public function transformCheckedoutActionlog (Collection $accessories_users, $total) public function transformCheckedoutActionlog (Collection $accessories_checkout, $total)
{ {
$array = array(); $array = array();
foreach ($accessories_users as $user) { foreach ($accessories_checkout as $user) {
$array[] = (new UsersTransformer)->transformUser($user); $array[] = (new UsersTransformer)->transformUser($user);
} }
return (new DatatablesTransformer)->transformDatatables($array, $total); return (new DatatablesTransformer)->transformDatatables($array, $total);

View file

@ -253,9 +253,10 @@ class Accessory extends SnipeModel
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function users() public function checkouts()
{ {
return $this->belongsToMany(\App\Models\User::class, 'accessories_users', 'accessory_id', 'assigned_to')->withPivot('id', 'created_at', 'note')->withTrashed(); return $this->hasMany(\App\Models\AccessoryCheckout::class, 'accessory_id')
->with('assignedTo');
} }
/** /**
@ -267,7 +268,9 @@ class Accessory extends SnipeModel
*/ */
public function hasUsers() public function hasUsers()
{ {
return $this->belongsToMany(\App\Models\User::class, 'accessories_users', 'accessory_id', 'assigned_to')->count(); return $this->hasMany(\App\Models\AccessoryCheckout::class, 'accessory_id')
->where('assigned_type', User::class)
->count();
} }
/** /**
@ -338,15 +341,15 @@ class Accessory extends SnipeModel
*/ */
public function numCheckedOut() public function numCheckedOut()
{ {
return $this->users_count ?? $this->users()->count(); return $this->checkouts_count ?? $this->checkouts()->count();
} }
/** /**
* Check how many items of an accessory remain. * Check how many items of an accessory remain.
* *
* In order to use this model method, you MUST call withCount('users as users_count') * In order to use this model method, you MUST call withCount('checkouts as checkouts_count')
* on the eloquent query in the controller, otherwise $this->users_count will be null and * on the eloquent query in the controller, otherwise $this->checkouts_count will be null and
* bad things happen. * bad things happen.
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
@ -370,12 +373,12 @@ class Accessory extends SnipeModel
*/ */
public function declinedCheckout(User $declinedBy, $signature) public function declinedCheckout(User $declinedBy, $signature)
{ {
if (is_null($accessory_user = \DB::table('accessories_users')->where('assigned_to', $declinedBy->id)->where('accessory_id', $this->id)->latest('created_at'))) { if (is_null($accessory_checkout = AccessoryCheckout::userAssigned()->where('assigned_to', $declinedBy->id)->where('accessory_id', $this->id)->latest('created_at'))) {
// Redirect to the accessory management page with error // Redirect to the accessory management page with error
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist')); return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
} }
$accessory_user->limit(1)->delete(); $accessory_checkout->limit(1)->delete();
} }
/** /**

148
app/Models/AccessoryCheckout.php Executable file
View file

@ -0,0 +1,148 @@
<?php
namespace App\Models;
use App\Helpers\Helper;
use App\Models\Traits\Acceptable;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Storage;
use Watson\Validating\ValidatingTrait;
/**
* Model for Accessories.
*
* @version v1.0
*/
class AccessoryCheckout extends Model
{
use Searchable;
protected $fillable = ['user_id', 'accessory_id', 'assigned_to', 'assigned_type', 'note'];
protected $table = 'accessories_checkout';
/**
* Establishes the accessory checkout -> accessory relationship
*
* @author [A. Kroeger]
* @since [v7.0.9]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function accessory()
{
return $this->hasOne(\App\Models\Accessory::class, 'accessory_id');
}
/**
* Establishes the accessory checkout -> user relationship
*
* @author [A. Kroeger]
* @since [v7.0.9]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function user()
{
return $this->hasOne(\App\Models\User::class, 'user_id');
}
/**
* Get the target this asset is checked out to
*
* @author [A. Kroeger]
* @since [v7.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assignedTo()
{
return $this->morphTo('assigned', 'assigned_type', 'assigned_to')->withTrashed();
}
/**
* Gets the lowercased name of the type of target the asset is assigned to
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @return string
*/
public function assignedType()
{
return $this->assigned_type ? strtolower(class_basename($this->assigned_type)) : null;
}
/**
* Determines whether the accessory is checked out to a user
*
* Even though we allow allow for checkout to things beyond users
* this method is an easy way of seeing if we are checked out to a user.
*
* @author [A. Kroeger]
* @since [v7.0]
*/
public function checkedOutToUser(): bool
{
return $this->assignedType() === Asset::USER;
}
public function scopeUserAssigned(Builder $query): void
{
$query->where('assigned_type', '=', User::class);
}
/**
* Run additional, advanced searches.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param array $terms The search terms
* @return \Illuminate\Database\Eloquent\Builder
*/
public function advancedTextSearch(Builder $query, array $terms)
{
$userQuery = User::where(function ($query) use ($terms) {
foreach ($terms as $term) {
$search_str = '%' . $term . '%';
$query->where('first_name', 'like', $search_str)
->orWhere('last_name', 'like', $search_str)
->orWhere('note', 'like', $search_str);
}
})->select('id');
$locationQuery = Location::where(function ($query) use ($terms) {
foreach ($terms as $term) {
$search_str = '%' . $term . '%';
$query->where('name', 'like', $search_str);
}
})->select('id');
$assetQuery = Asset::where(function ($query) use ($terms) {
foreach ($terms as $term) {
$search_str = '%' . $term . '%';
$query->where('name', 'like', $search_str);
}
})->select('id');
$query->where(function ($query) use ($userQuery) {
$query->where('assigned_type', User::class)
->whereIn('assigned_to', $userQuery);
})->orWhere(function($query) use ($locationQuery) {
$query->where('assigned_type', Location::class)
->whereIn('assigned_to', $locationQuery);
})->orWhere(function($query) use ($assetQuery) {
$query->where('assigned_type', Asset::class)
->whereIn('assigned_to', $assetQuery);
})->orWhere(function($query) use ($terms) {
foreach ($terms as $term) {
$search_str = '%' . $term . '%';
$query->where('note', 'like', $search_str);
}
});
return $query;
}
}

View file

@ -331,7 +331,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
*/ */
public function accessories() public function accessories()
{ {
return $this->belongsToMany(\App\Models\Accessory::class, 'accessories_users', 'assigned_to', 'accessory_id') return $this->belongsToMany(\App\Models\Accessory::class, 'accessories_checkout', 'assigned_to', 'accessory_id')
->withPivot('id', 'created_at', 'note')->withTrashed()->orderBy('accessory_id'); ->withPivot('id', 'created_at', 'note')->withTrashed()->orderBy('accessory_id');
} }

View file

@ -90,7 +90,7 @@ class AccessoryPresenter extends Presenter
'visible' => false, 'visible' => false,
'title' => trans('admin/accessories/general.remaining'), 'title' => trans('admin/accessories/general.remaining'),
],[ ],[
'field' => 'users_count', 'field' => 'checkouts_count',
'searchable' => false, 'searchable' => false,
'sortable' => true, 'sortable' => true,
'visible' => true, 'visible' => true,

View file

@ -3,6 +3,7 @@
namespace Database\Factories; namespace Database\Factories;
use App\Models\Accessory; use App\Models\Accessory;
use App\Models\AccessoryCheckout;
use App\Models\Category; use App\Models\Category;
use App\Models\Location; use App\Models\Location;
use App\Models\Manufacturer; use App\Models\Manufacturer;
@ -125,11 +126,12 @@ class AccessoryFactory extends Factory
})->afterCreating(function ($accessory) { })->afterCreating(function ($accessory) {
$user = User::factory()->create(); $user = User::factory()->create();
$accessory->users()->attach($accessory->id, [ $accessory->checkouts()->create([
'accessory_id' => $accessory->id, 'accessory_id' => $accessory->id,
'created_at' => now(), 'created_at' => Carbon::now(),
'user_id' => $user->id, 'user_id' => $user->id,
'assigned_to' => $user->id, 'assigned_to' => $user->id,
'assigned_type' => User::class,
'note' => '', 'note' => '',
]); ]);
}); });
@ -145,11 +147,12 @@ class AccessoryFactory extends Factory
public function checkedOutToUser(User $user = null) public function checkedOutToUser(User $user = null)
{ {
return $this->afterCreating(function (Accessory $accessory) use ($user) { return $this->afterCreating(function (Accessory $accessory) use ($user) {
$accessory->users()->attach($accessory->id, [ $accessory->checkouts()->create([
'accessory_id' => $accessory->id, 'accessory_id' => $accessory->id,
'created_at' => Carbon::now(), 'created_at' => Carbon::now(),
'user_id' => 1, 'user_id' => 1,
'assigned_to' => $user->id ?? User::factory()->create()->id, 'assigned_to' => $user->id ?? User::factory()->create()->id,
'assigned_type' => User::class,
]); ]);
}); });
} }

View file

@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
if (Schema::hasTable('accessories_users')) {
Schema::rename('accessories_users', 'accessories_checkout');
Schema::table('accessories_checkout', function (Blueprint $table) {
if (!Schema::hasColumn('accessories_checkout', 'assigned_type')) {
$table->string('assigned_type')->nullable();
}
});
}
DB::update('update '.DB::getTablePrefix().'accessories_checkout set assigned_type = \'App\\\\Models\\\\User\' where assigned_type is null', []);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
if (Schema::hasTable('accessories_checkout')) {
Schema::table('accessories_checkout', function (Blueprint $table) {
$table->dropColumn('assigned_type');
});
Schema::rename('accessories_checkout', 'accessories_users');
}
}
};

View file

@ -66,7 +66,14 @@
</div> </div>
<!-- User --> <!-- User -->
@include ('partials.forms.edit.user-select', ['translated_name' => trans('general.select_user'), 'fieldname' => 'assigned_to', 'required'=> 'true']) @include ('partials.forms.checkout-selector', ['user_select' => 'true','asset_select' => 'true', 'location_select' => 'true'])
@include ('partials.forms.edit.user-select', ['translated_name' => trans('general.select_user'), 'fieldname' => 'assigned_user', 'required'=> 'true'])
<!-- We have to pass unselect here so that we don't default to the asset that's being checked out. We want that asset to be pre-selected everywhere else. -->
@include ('partials.forms.edit.asset-select', ['translated_name' => trans('general.asset'), 'fieldname' => 'assigned_asset', 'unselect' => 'true', 'style' => 'display:none;', 'required'=>'true'])
@include ('partials.forms.edit.location-select', ['translated_name' => trans('general.location'), 'fieldname' => 'assigned_location', 'style' => 'display:none;', 'required'=>'true'])
<!-- Checkout QTY --> <!-- Checkout QTY -->
<div class="form-group {{ $errors->has('checkout_qty') ? 'error' : '' }} "> <div class="form-group {{ $errors->has('checkout_qty') ? 'error' : '' }} ">

View file

@ -68,9 +68,9 @@
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<table <table
data-cookie-id-table="usersTable" data-cookie-id-table="checkoutsTable"
data-pagination="true" data-pagination="true"
data-id-table="usersTable" data-id-table="checkoutsTable"
data-search="true" data-search="true"
data-side-pagination="server" data-side-pagination="server"
data-show-columns="true" data-show-columns="true"
@ -78,16 +78,16 @@
data-show-export="true" data-show-export="true"
data-show-refresh="true" data-show-refresh="true"
data-sort-order="asc" data-sort-order="asc"
id="usersTable" id="checkoutsTable"
class="table table-striped snipe-table" class="table table-striped snipe-table"
data-url="{{ route('api.accessories.checkedout', $accessory->id) }}" data-url="{{ route('api.accessories.checkedout', $accessory->id) }}"
data-export-options='{ data-export-options='{
"fileName": "export-accessories-{{ str_slug($accessory->name) }}-users-{{ date('Y-m-d') }}", "fileName": "export-accessories-{{ str_slug($accessory->name) }}-checkouts-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"] "ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]
}'> }'>
<thead> <thead>
<tr> <tr>
<th data-searchable="false" data-formatter="usersLinkFormatter" data-sortable="false" data-field="name">{{ trans('general.user') }}</th> <th data-searchable="false" data-formatter="polymorphicItemFormatter" data-sortable="false" data-field="assigned_to">{{ trans('general.checked_out_to') }}</th>
<th data-searchable="false" data-sortable="false" data-field="checkout_notes">{{ trans('general.notes') }}</th> <th data-searchable="false" data-sortable="false" data-field="checkout_notes">{{ trans('general.notes') }}</th>
<th data-searchable="false" data-formatter="dateDisplayFormatter" data-sortable="false" data-field="last_checkout">{{ trans('admin/hardware/table.checkout_date') }}</th> <th data-searchable="false" data-formatter="dateDisplayFormatter" data-sortable="false" data-field="last_checkout">{{ trans('admin/hardware/table.checkout_date') }}</th>
<th data-searchable="false" data-sortable="false" data-field="actions" data-formatter="accessoriesInOutFormatter">{{ trans('table.actions') }}</th> <th data-searchable="false" data-sortable="false" data-field="actions" data-formatter="accessoriesInOutFormatter">{{ trans('table.actions') }}</th>
@ -314,7 +314,7 @@
<strong>{{ trans('general.checked_out') }}</strong> <strong>{{ trans('general.checked_out') }}</strong>
</div> </div>
<div class="col-md-9"> <div class="col-md-9">
{{ $accessory->users_count }} {{ $accessory->checkouts_count }}
</div> </div>
</div> </div>
</div> </div>
@ -337,7 +337,7 @@
@endcan @endcan
@can('delete', $accessory) @can('delete', $accessory)
@if ($accessory->users_count == 0) @if ($accessory->checkouts_count == 0)
<div class="text-center" style="padding-top:5px;"> <div class="text-center" style="padding-top:5px;">
<button class="btn btn-block btn-danger delete-asset" style="padding-top:5px;" data-toggle="modal" data-title="{{ trans('general.delete') }}" data-content="{{ trans('general.delete_confirm_no_undo', ['item' => $accessory->name]) }}" data-target="#dataConfirmModal"> <button class="btn btn-block btn-danger delete-asset" style="padding-top:5px;" data-toggle="modal" data-title="{{ trans('general.delete') }}" data-content="{{ trans('general.delete_confirm_no_undo', ['item' => $accessory->name]) }}" data-target="#dataConfirmModal">
{{ trans('general.delete') }} {{ trans('general.delete') }}

View file

@ -17,7 +17,7 @@ class AccessoryCheckinTest extends TestCase
$accessory = Accessory::factory()->checkedOutToUser()->create(); $accessory = Accessory::factory()->checkedOutToUser()->create();
$this->actingAs(User::factory()->create()) $this->actingAs(User::factory()->create())
->post(route('accessories.checkin.store', $accessory->users->first()->pivot->id)) ->post(route('accessories.checkin.store', $accessory->checkouts->first()->id))
->assertForbidden(); ->assertForbidden();
} }
@ -28,12 +28,12 @@ class AccessoryCheckinTest extends TestCase
$user = User::factory()->create(); $user = User::factory()->create();
$accessory = Accessory::factory()->checkedOutToUser($user)->create(); $accessory = Accessory::factory()->checkedOutToUser($user)->create();
$this->assertTrue($accessory->users->contains($user)); $this->assertTrue($accessory->checkouts()->where('assigned_type', User::class)->where('assigned_to', $user->id)->count() > 0);
$this->actingAs(User::factory()->checkinAccessories()->create()) $this->actingAs(User::factory()->checkinAccessories()->create())
->post(route('accessories.checkin.store', $accessory->users->first()->pivot->id)); ->post(route('accessories.checkin.store', $accessory->checkouts->first()->id));
$this->assertFalse($accessory->fresh()->users->contains($user)); $this->assertFalse($accessory->fresh()->checkouts()->where('assigned_type', User::class)->where('assigned_to', $user->id)->count() > 0);
Event::assertDispatched(CheckoutableCheckedIn::class, 1); Event::assertDispatched(CheckoutableCheckedIn::class, 1);
} }

View file

@ -22,7 +22,7 @@ class AccessoryCheckoutTest extends TestCase
{ {
$this->actingAsForApi(User::factory()->checkoutAccessories()->create()) $this->actingAsForApi(User::factory()->checkoutAccessories()->create())
->postJson(route('api.accessories.checkout', Accessory::factory()->create()), [ ->postJson(route('api.accessories.checkout', Accessory::factory()->create()), [
// missing assigned_to // missing assigned_user, assigned_location, assigned_asset
]) ])
->assertStatusMessageIs('error'); ->assertStatusMessageIs('error');
} }
@ -31,7 +31,8 @@ class AccessoryCheckoutTest extends TestCase
{ {
$this->actingAsForApi(User::factory()->checkoutAccessories()->create()) $this->actingAsForApi(User::factory()->checkoutAccessories()->create())
->postJson(route('api.accessories.checkout', Accessory::factory()->withoutItemsRemaining()->create()), [ ->postJson(route('api.accessories.checkout', Accessory::factory()->withoutItemsRemaining()->create()), [
'assigned_to' => User::factory()->create()->id, 'assigned_user' => User::factory()->create()->id,
'checkout_to_type' => 'user'
]) ])
->assertOk() ->assertOk()
->assertStatusMessageIs('error') ->assertStatusMessageIs('error')
@ -65,7 +66,8 @@ class AccessoryCheckoutTest extends TestCase
$this->actingAsForApi($admin) $this->actingAsForApi($admin)
->postJson(route('api.accessories.checkout', $accessory), [ ->postJson(route('api.accessories.checkout', $accessory), [
'assigned_to' => $user->id, 'assigned_user' => $user->id,
'checkout_to_type' => 'user'
]) ])
->assertOk() ->assertOk()
->assertStatusMessageIs('success') ->assertStatusMessageIs('success')
@ -73,7 +75,7 @@ class AccessoryCheckoutTest extends TestCase
->assertJson(['messages' => trans('admin/accessories/message.checkout.success')]) ->assertJson(['messages' => trans('admin/accessories/message.checkout.success')])
->json(); ->json();
$this->assertTrue($accessory->users->contains($user)); $this->assertTrue($accessory->checkouts()->where('assigned_type', User::class)->where('assigned_to', $user->id)->count() > 0);
$this->assertEquals( $this->assertEquals(
1, 1,
@ -96,7 +98,8 @@ class AccessoryCheckoutTest extends TestCase
$this->actingAsForApi($admin) $this->actingAsForApi($admin)
->postJson(route('api.accessories.checkout', $accessory), [ ->postJson(route('api.accessories.checkout', $accessory), [
'assigned_to' => $user->id, 'assigned_user' => $user->id,
'checkout_to_type' => 'user',
'checkout_qty' => 2, 'checkout_qty' => 2,
]) ])
->assertOk() ->assertOk()
@ -105,7 +108,7 @@ class AccessoryCheckoutTest extends TestCase
->assertJson(['messages' => trans('admin/accessories/message.checkout.success')]) ->assertJson(['messages' => trans('admin/accessories/message.checkout.success')])
->json(); ->json();
$this->assertTrue($accessory->users->contains($user)); $this->assertTrue($accessory->checkouts()->where('assigned_type', User::class)->where('assigned_to', $user->id)->count() > 0);
$this->assertEquals( $this->assertEquals(
1, 1,
@ -128,7 +131,8 @@ class AccessoryCheckoutTest extends TestCase
$this->actingAsForApi(User::factory()->checkoutAccessories()->create()) $this->actingAsForApi(User::factory()->checkoutAccessories()->create())
->postJson(route('api.accessories.checkout', $accessory), [ ->postJson(route('api.accessories.checkout', $accessory), [
'assigned_to' => 'invalid-user-id', 'assigned_user' => 'invalid-user-id',
'checkout_to_type' => 'user',
'note' => 'oh hi there', 'note' => 'oh hi there',
]) ])
->assertOk() ->assertOk()
@ -136,7 +140,7 @@ class AccessoryCheckoutTest extends TestCase
->assertStatus(200) ->assertStatus(200)
->json(); ->json();
$this->assertFalse($accessory->users->contains($user)); $this->assertFalse($accessory->checkouts()->where('assigned_type', User::class)->where('assigned_to', $user->id)->count() > 0);
} }
public function testUserSentNotificationUponCheckout() public function testUserSentNotificationUponCheckout()
@ -148,7 +152,8 @@ class AccessoryCheckoutTest extends TestCase
$this->actingAsForApi(User::factory()->checkoutAccessories()->create()) $this->actingAsForApi(User::factory()->checkoutAccessories()->create())
->postJson(route('api.accessories.checkout', $accessory), [ ->postJson(route('api.accessories.checkout', $accessory), [
'assigned_to' => $user->id, 'assigned_user' => $user->id,
'checkout_to_type' => 'user',
]); ]);
Notification::assertSentTo($user, CheckoutAccessoryNotification::class); Notification::assertSentTo($user, CheckoutAccessoryNotification::class);
@ -162,7 +167,8 @@ class AccessoryCheckoutTest extends TestCase
$this->actingAsForApi($actor) $this->actingAsForApi($actor)
->postJson(route('api.accessories.checkout', $accessory), [ ->postJson(route('api.accessories.checkout', $accessory), [
'assigned_to' => $user->id, 'assigned_user' => $user->id,
'checkout_to_type' => 'user',
'note' => 'oh hi there', 'note' => 'oh hi there',
]); ]);

View file

@ -4,6 +4,8 @@ namespace Tests\Feature\Checkouts\Ui;
use App\Models\Accessory; use App\Models\Accessory;
use App\Models\Actionlog; use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Location;
use App\Models\User; use App\Models\User;
use App\Notifications\CheckoutAccessoryNotification; use App\Notifications\CheckoutAccessoryNotification;
use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\Notification;
@ -40,7 +42,8 @@ class AccessoryCheckoutTest extends TestCase
$response = $this->actingAs(User::factory()->viewAccessories()->checkoutAccessories()->create()) $response = $this->actingAs(User::factory()->viewAccessories()->checkoutAccessories()->create())
->from(route('accessories.checkout.show', $accessory)) ->from(route('accessories.checkout.show', $accessory))
->post(route('accessories.checkout.store', $accessory), [ ->post(route('accessories.checkout.store', $accessory), [
'assigned_to' => User::factory()->create()->id, 'assigned_user' => User::factory()->create()->id,
'checkout_to_type' => 'user',
]) ])
->assertStatus(302) ->assertStatus(302)
->assertSessionHas('errors') ->assertSessionHas('errors')
@ -56,11 +59,12 @@ class AccessoryCheckoutTest extends TestCase
$this->actingAs(User::factory()->checkoutAccessories()->create()) $this->actingAs(User::factory()->checkoutAccessories()->create())
->post(route('accessories.checkout.store', $accessory), [ ->post(route('accessories.checkout.store', $accessory), [
'assigned_to' => $user->id, 'assigned_user' => $user->id,
'checkout_to_type' => 'user',
'note' => 'oh hi there', 'note' => 'oh hi there',
]); ]);
$this->assertTrue($accessory->users->contains($user)); $this->assertTrue($accessory->checkouts()->where('assigned_type', User::class)->where('assigned_to', $user->id)->count() > 0);
$this->assertDatabaseHas('action_logs', [ $this->assertDatabaseHas('action_logs', [
'action_type' => 'checkout', 'action_type' => 'checkout',
@ -80,12 +84,13 @@ class AccessoryCheckoutTest extends TestCase
$this->actingAs(User::factory()->checkoutAccessories()->create()) $this->actingAs(User::factory()->checkoutAccessories()->create())
->from(route('accessories.checkout.show', $accessory)) ->from(route('accessories.checkout.show', $accessory))
->post(route('accessories.checkout.store', $accessory), [ ->post(route('accessories.checkout.store', $accessory), [
'assigned_to' => $user->id, 'assigned_user' => $user->id,
'checkout_to_type' => 'user',
'checkout_qty' => 3, 'checkout_qty' => 3,
'note' => 'oh hi there', 'note' => 'oh hi there',
]); ]);
$this->assertTrue($accessory->users->contains($user)); $this->assertTrue($accessory->checkouts()->where('assigned_type', User::class)->where('assigned_to', $user->id)->count() > 0);
$this->assertDatabaseHas('action_logs', [ $this->assertDatabaseHas('action_logs', [
'action_type' => 'checkout', 'action_type' => 'checkout',
@ -97,6 +102,58 @@ class AccessoryCheckoutTest extends TestCase
]); ]);
} }
public function testAccessoryCanBeCheckedOutToLocationWithQuantity()
{
$accessory = Accessory::factory()->create(['qty'=>5]);
$location = Location::factory()->create();
$this->actingAs(User::factory()->checkoutAccessories()->create())
->from(route('accessories.checkout.show', $accessory))
->post(route('accessories.checkout.store', $accessory), [
'assigned_location' => $location->id,
'checkout_to_type' => 'location',
'checkout_qty' => 3,
'note' => 'oh hi there',
]);
$this->assertTrue($accessory->checkouts()->where('assigned_type', Location::class)->where('assigned_to', $location->id)->count() > 0);
$this->assertDatabaseHas('action_logs', [
'action_type' => 'checkout',
'target_id' => $location->id,
'target_type' => Location::class,
'item_id' => $accessory->id,
'item_type' => Accessory::class,
'note' => 'oh hi there',
]);
}
public function testAccessoryCanBeCheckedOutToAssetWithQuantity()
{
$accessory = Accessory::factory()->create(['qty'=>5]);
$asset = Asset::factory()->create();
$this->actingAs(User::factory()->checkoutAccessories()->create())
->from(route('accessories.checkout.show', $accessory))
->post(route('accessories.checkout.store', $accessory), [
'assigned_asset' => $asset->id,
'checkout_to_type' => 'asset',
'checkout_qty' => 3,
'note' => 'oh hi there',
]);
$this->assertTrue($accessory->checkouts()->where('assigned_type', Asset::class)->where('assigned_to', $asset->id)->count() > 0);
$this->assertDatabaseHas('action_logs', [
'action_type' => 'checkout',
'target_id' => $asset->id,
'target_type' => Asset::class,
'item_id' => $accessory->id,
'item_type' => Accessory::class,
'note' => 'oh hi there',
]);
}
public function testUserSentNotificationUponCheckout() public function testUserSentNotificationUponCheckout()
{ {
Notification::fake(); Notification::fake();
@ -107,7 +164,8 @@ class AccessoryCheckoutTest extends TestCase
$this->actingAs(User::factory()->checkoutAccessories()->create()) $this->actingAs(User::factory()->checkoutAccessories()->create())
->from(route('accessories.checkout.show', $accessory)) ->from(route('accessories.checkout.show', $accessory))
->post(route('accessories.checkout.store', $accessory), [ ->post(route('accessories.checkout.store', $accessory), [
'assigned_to' => $user->id, 'assigned_user' => $user->id,
'checkout_to_type' => 'user',
]); ]);
Notification::assertSentTo($user, CheckoutAccessoryNotification::class); Notification::assertSentTo($user, CheckoutAccessoryNotification::class);
@ -122,7 +180,8 @@ class AccessoryCheckoutTest extends TestCase
$this->actingAs($actor) $this->actingAs($actor)
->from(route('accessories.checkout.show', $accessory)) ->from(route('accessories.checkout.show', $accessory))
->post(route('accessories.checkout.store', $accessory), [ ->post(route('accessories.checkout.store', $accessory), [
'assigned_to' => $user->id, 'assigned_user' => $user->id,
'checkout_to_type' => 'user',
'note' => 'oh hi there', 'note' => 'oh hi there',
]); ]);
@ -148,7 +207,8 @@ class AccessoryCheckoutTest extends TestCase
$this->actingAs(User::factory()->admin()->create()) $this->actingAs(User::factory()->admin()->create())
->from(route('accessories.index')) ->from(route('accessories.index'))
->post(route('accessories.checkout.store', $accessory), [ ->post(route('accessories.checkout.store', $accessory), [
'assigned_to' => User::factory()->create()->id, 'assigned_user' => User::factory()->create()->id,
'checkout_to_type' => 'user',
'redirect_option' => 'index', 'redirect_option' => 'index',
'assigned_qty' => 1, 'assigned_qty' => 1,
]) ])
@ -163,7 +223,8 @@ class AccessoryCheckoutTest extends TestCase
$this->actingAs(User::factory()->admin()->create()) $this->actingAs(User::factory()->admin()->create())
->from(route('accessories.index')) ->from(route('accessories.index'))
->post(route('accessories.checkout.store' , $accessory), [ ->post(route('accessories.checkout.store' , $accessory), [
'assigned_to' => User::factory()->create()->id, 'assigned_user' => User::factory()->create()->id,
'checkout_to_type' => 'user',
'redirect_option' => 'item', 'redirect_option' => 'item',
'assigned_qty' => 1, 'assigned_qty' => 1,
]) ])
@ -180,7 +241,8 @@ class AccessoryCheckoutTest extends TestCase
$this->actingAs(User::factory()->admin()->create()) $this->actingAs(User::factory()->admin()->create())
->from(route('accessories.index')) ->from(route('accessories.index'))
->post(route('accessories.checkout.store' , $accessory), [ ->post(route('accessories.checkout.store' , $accessory), [
'assigned_to' => $user->id, 'assigned_user' => $user->id,
'checkout_to_type' => 'user',
'redirect_option' => 'target', 'redirect_option' => 'target',
'assigned_qty' => 1, 'assigned_qty' => 1,
]) ])