mirror of
https://github.com/snipe/snipe-it.git
synced 2025-02-21 03:15:45 -08:00
Merge pull request #8008 from dmeltzer/component-checkinout-fixes
Component checkout/checkin fixes.
This commit is contained in:
commit
8507bcd16b
|
@ -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'));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -27,8 +27,9 @@ class Component extends SnipeModel
|
||||||
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,16 +182,7 @@ 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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.',
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
<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>
|
</div>
|
||||||
|
@endcan
|
||||||
</div> <!-- /.col-md-3-->
|
</div> <!-- /.col-md-3-->
|
||||||
</div> <!-- /.row-->
|
</div> <!-- /.row-->
|
||||||
|
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
100
tests/unit/ComponentTest.php
Normal file
100
tests/unit/ComponentTest.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue