Merge pull request #8008 from dmeltzer/component-checkinout-fixes

Component checkout/checkin fixes.
This commit is contained in:
snipe 2020-04-30 17:49:22 -07:00 committed by GitHub
commit 8507bcd16b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 153 additions and 33 deletions

View file

@ -8,6 +8,7 @@ use App\Http\Controllers\Controller;
use App\Models\Asset; use App\Models\Asset;
use App\Models\Component; use App\Models\Component;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
@ -96,12 +97,12 @@ class ComponentCheckinController extends Controller
$asset = Asset::find($component_assets->asset_id); $asset = Asset::find($component_assets->asset_id);
event(new CheckoutableCheckedIn($component, $asset, Auth::user(), $request->input('note'))); event(new CheckoutableCheckedIn($component, $asset, Auth::user(), $request->input('note'), Carbon::now()));
return redirect()->route('components.index')->with('success', return redirect()->route('components.index')->with('success',
trans('admin/components/message.checkout.success')); trans('admin/components/message.checkin.success'));
} }
return redirect()->route('components.index')->with('error', trans('admin/components/message.not_found')); return redirect()->route('components.index')->with('error', trans('admin/components/message.does_not_exist'));
} }
} }

View file

@ -8,6 +8,7 @@ use App\Models\Component;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Input; use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
/** /**
* This class controls all actions related to Components for * This class controls all actions related to Components for
@ -121,6 +122,16 @@ class ComponentsController extends Controller
if (is_null($component = Component::find($componentId))) { if (is_null($component = Component::find($componentId))) {
return redirect()->route('components.index')->with('error', trans('admin/components/message.does_not_exist')); return redirect()->route('components.index')->with('error', trans('admin/components/message.does_not_exist'));
} }
$min = $component->numCHeckedOut();
$validator = Validator::make($request->all(), [
"qty" => "required|numeric|gt:$min"
]);
if ($validator->fails()) {
return redirect()->back()
->withErrors($validator)
->withInput();
}
$this->authorize('update', $component); $this->authorize('update', $component);

View file

@ -20,17 +20,18 @@ class Component extends SnipeModel
protected $dates = ['deleted_at', 'purchase_date']; protected $dates = ['deleted_at', 'purchase_date'];
protected $table = 'components'; protected $table = 'components';
/** /**
* Category validation rules * Category validation rules
*/ */
public $rules = array( public $rules = array(
'name' => 'required|min:3|max:255', 'name' => 'required|min:3|max:255',
'qty' => 'required|integer|min:1', 'qty' => 'required|integer|min:1',
'category_id' => 'required|integer', 'category_id' => 'required|integer|exists:categories,id',
'company_id' => 'integer|nullable', 'company_id' => 'integer|nullable',
'min_amt' => 'integer|min:0|nullable',
'purchase_date' => 'date|nullable', 'purchase_date' => 'date|nullable',
'purchase_cost' => 'numeric|nullable', 'purchase_cost' => 'numeric|nullable',
); );
/** /**
@ -155,6 +156,23 @@ class Component extends SnipeModel
return $this->hasMany('\App\Models\Actionlog', 'item_id')->where('item_type', Component::class)->orderBy('created_at', 'desc')->withTrashed(); return $this->hasMany('\App\Models\Actionlog', 'item_id')->where('item_type', Component::class)->orderBy('created_at', 'desc')->withTrashed();
} }
/**
* Check how many items within a component are checked out
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v5.0]
* @return int
*/
public function numCheckedOut()
{
$checkedout = 0;
foreach ($this->assets as $checkout) {
$checkedout += $checkout->pivot->assigned_qty;
}
return $checkedout;
}
/** /**
* Check how many items within a component are remaining * Check how many items within a component are remaining
* *
@ -164,17 +182,8 @@ class Component extends SnipeModel
*/ */
public function numRemaining() public function numRemaining()
{ {
$checkedout = 0; return $this->qty - $this->numCheckedOut();
}
foreach ($this->assets as $checkout) {
$checkedout += $checkout->pivot->assigned_qty;
}
$total = $this->qty;
$remaining = $total - $checkedout;
return $remaining;
}
/** /**
* Query builder scope to order on company * Query builder scope to order on company

View file

@ -161,6 +161,7 @@ class LicensePresenter extends Presenter
"field" => "name", "field" => "name",
"searchable" => false, "searchable" => false,
"sortable" => false, "sortable" => false,
"sorter" => "numericOnly",
"switchable" => true, "switchable" => true,
"title" => trans('admin/licenses/general.seat'), "title" => trans('admin/licenses/general.seat'),
"visible" => true, "visible" => true,

View file

@ -44,6 +44,9 @@ return array(
'exists' => 'The selected :attribute is invalid.', 'exists' => 'The selected :attribute is invalid.',
'file' => 'The :attribute must be a file.', 'file' => 'The :attribute must be a file.',
'filled' => 'The :attribute field must have a value.', 'filled' => 'The :attribute field must have a value.',
'gt' => [
'numeric' => 'The :attribute field must be greater than :value.'
],
'hashed_pass' => 'Your password is incorrect.', 'hashed_pass' => 'Your password is incorrect.',
'image' => 'The :attribute must be an image.', 'image' => 'The :attribute must be an image.',
'in' => 'The selected :attribute is invalid.', 'in' => 'The selected :attribute is invalid.',

View file

@ -101,12 +101,6 @@
<a href="{{ route('checkout/accessory', $accessory->id) }}" style="margin-right:5px;" class="btn btn-primary btn-sm" {{ (($accessory->numRemaining() > 0 ) ? '' : ' disabled') }}>{{ trans('general.checkout') }}</a> <a href="{{ route('checkout/accessory', $accessory->id) }}" style="margin-right:5px;" class="btn btn-primary btn-sm" {{ (($accessory->numRemaining() > 0 ) ? '' : ' disabled') }}>{{ trans('general.checkout') }}</a>
@endcan @endcan
</div> </div>
<h2>{{ trans('admin/accessories/general.about_accessories_title') }}</h4>
<p>{{ trans('admin/accessories/general.about_accessories_text') }} </p>
</div> </div>
</div> </div>
@stop @stop

View file

@ -42,7 +42,7 @@
<label for="assigned_qty" class="col-md-3 control-label">{{ trans('general.qty') }} <label for="assigned_qty" class="col-md-3 control-label">{{ trans('general.qty') }}
<i class='icon-asterisk'></i></label> <i class='icon-asterisk'></i></label>
<div class="col-md-9"> <div class="col-md-9">
<input class="form-control" type="text" name="assigned_qty" id="assigned_qty" style="width: 70px;" value="{{ old('assigned_qty') }}" /> <input class="form-control" type="text" name="assigned_qty" id="assigned_qty" style="width: 70px;" value="{{ old('assigned_qty') ?? 1 }}" />
{!! $errors->first('assigned_qty', '<br><span class="alert-msg" aria-hidden="true"><i class="fa fa-times" aria-hidden="true"></i> :message</span>') !!} {!! $errors->first('assigned_qty', '<br><span class="alert-msg" aria-hidden="true"><i class="fa fa-times" aria-hidden="true"></i> :message</span>') !!}
</div> </div>
</div> </div>

View file

@ -67,7 +67,6 @@
</div> <!-- /.box.box-default--> </div> <!-- /.box.box-default-->
</div> <!-- /.col-md-9--> </div> <!-- /.col-md-9-->
<div class="col-md-3"> <div class="col-md-3">
@if ($consumable->image!='') @if ($consumable->image!='')
<div class="col-md-12 text-center" style="padding-bottom: 15px;"> <div class="col-md-12 text-center" style="padding-bottom: 15px;">
<a href="{{ app('consumables_upload_url') }}/{{ $consumable->image }}" data-toggle="lightbox"><img src="{{ app('consumables_upload_url') }}/{{ $consumable->image }}" class="img-responsive img-thumbnail" alt="{{ $consumable->name }}"></a> <a href="{{ app('consumables_upload_url') }}/{{ $consumable->image }}" data-toggle="lightbox"><img src="{{ app('consumables_upload_url') }}/{{ $consumable->image }}" class="img-responsive img-thumbnail" alt="{{ $consumable->name }}"></a>
@ -116,10 +115,12 @@
{{ $consumable->order_number }} {{ $consumable->order_number }}
</div> </div>
@endif @endif
<div class="col-md-12" style="padding-bottom: 5px;">
<h2>{{ trans('admin/consumables/general.about_consumables_title') }}</h4> @can('checkout', \App\Models\Accessory::class)
<p>{{ trans('admin/consumables/general.about_consumables_text') }} </p> <div class="col-md-12">
</div> <a href="{{ route('checkout/consumable', $consumable->id) }}" style="padding-bottom:5px;" class="btn btn-primary btn-sm" {{ (($consumable->numRemaining() > 0 ) ? '' : ' disabled') }}>{{ trans('general.checkout') }}</a>
</div>
@endcan
</div> <!-- /.col-md-3--> </div> <!-- /.col-md-3-->
</div> <!-- /.row--> </div> <!-- /.row-->

View file

@ -338,7 +338,7 @@
data-sort-order="asc" data-sort-order="asc"
data-sort-name="name" data-sort-name="name"
class="table table-striped snipe-table" class="table table-striped snipe-table"
data-url="{{ route('api.license.seats',['license_id' => $license->id]) }}" data-url="{{ route('api.license.seats', $license->id) }}"
data-export-options='{ data-export-options='{
"fileName": "export-seats-{{ str_slug($license->name) }}-{{ date('Y-m-d') }}", "fileName": "export-seats-{{ str_slug($license->name) }}-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"] "ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]

View file

@ -0,0 +1,100 @@
<?php
use App\Models\Category;
use App\Models\Company;
use App\Models\Component;
use App\Models\Location;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Hash;
class ComponentTest extends BaseTest
{
/**
* @var \UnitTester
*/
protected $tester;
public function testFailsEmptyValidation()
{
// An Component requires a name, a qty, and a category_id.
$a = Component::create();
$this->assertFalse($a->isValid());
$fields = [
'name' => 'name',
'qty' => 'qty',
'category_id' => 'category id'
];
$errors = $a->getErrors();
foreach ($fields as $field => $fieldTitle) {
$this->assertEquals($errors->get($field)[0], "The ${fieldTitle} field is required.");
}
}
public function testFailsMinValidation()
{
// An Component name has a min length of 3
// An Component has a min qty of 1
// An Component has a min amount of 0
$a = factory(Component::class)->make([
'name' => 'a',
'qty' => 0,
'min_amt' => -1
]);
$fields = [
'name' => 'name',
'qty' => 'qty',
'min_amt' => 'min amt'
];
$this->assertFalse($a->isValid());
$errors = $a->getErrors();
foreach ($fields as $field => $fieldTitle) {
$this->assertStringContainsString("The ${fieldTitle} must be at least", $errors->get($field)[0]);
}
}
public function testCategoryIdMustExist()
{
$category = $this->createValidCategory('component-hdd-category',
['category_type' => 'component']);
$component = factory(Component::class)
->states('ssd-crucial240')
->make(['category_id' => $category->id]);
$this->createValidManufacturer('apple');
$component->save();
$this->assertTrue($component->isValid());
$newId = $category->id + 1;
$component = factory(Component::class)->states('ssd-crucial240')->make(['category_id' => $newId]);
$component->save();
$this->assertFalse($component->isValid());
$this->assertStringContainsString("The selected category id is invalid.", $component->getErrors()->get('category_id')[0]);
}
public function testAnComponentBelongsToACompany()
{
$component = factory(Component::class)
->create(['company_id' => factory(Company::class)->create()->id]);
$this->assertInstanceOf(Company::class, $component->company);
}
public function testAnComponentHasALocation()
{
$component = factory(Component::class)
->create(['location_id' => factory(Location::class)->create()->id]);
$this->assertInstanceOf(Location::class, $component->location);
}
public function testAnComponentBelongsToACategory()
{
$component = factory(Component::class)->states('ssd-crucial240')
->create([
'category_id' => factory(Category::class)
->states('component-hdd-category')
->create(['category_type' => 'component'])->id
]);
$this->assertInstanceOf(Category::class, $component->category);
$this->assertEquals('component', $component->category->category_type);
}
}