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\Component;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
@ -96,12 +97,12 @@ class ComponentCheckinController extends Controller
|
|||
|
||||
$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',
|
||||
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\Input;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
/**
|
||||
* This class controls all actions related to Components for
|
||||
|
@ -121,6 +122,16 @@ class ComponentsController extends Controller
|
|||
if (is_null($component = Component::find($componentId))) {
|
||||
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);
|
||||
|
||||
|
|
|
@ -20,17 +20,18 @@ class Component extends SnipeModel
|
|||
|
||||
protected $dates = ['deleted_at', 'purchase_date'];
|
||||
protected $table = 'components';
|
||||
|
||||
|
||||
/**
|
||||
* Category validation rules
|
||||
*/
|
||||
public $rules = array(
|
||||
'name' => 'required|min:3|max:255',
|
||||
'qty' => 'required|integer|min:1',
|
||||
'category_id' => 'required|integer',
|
||||
'company_id' => 'integer|nullable',
|
||||
'name' => 'required|min:3|max:255',
|
||||
'qty' => 'required|integer|min:1',
|
||||
'category_id' => 'required|integer|exists:categories,id',
|
||||
'company_id' => 'integer|nullable',
|
||||
'min_amt' => 'integer|min:0|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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
|
@ -164,17 +182,8 @@ class Component extends SnipeModel
|
|||
*/
|
||||
public function numRemaining()
|
||||
{
|
||||
$checkedout = 0;
|
||||
|
||||
foreach ($this->assets as $checkout) {
|
||||
$checkedout += $checkout->pivot->assigned_qty;
|
||||
}
|
||||
|
||||
|
||||
$total = $this->qty;
|
||||
$remaining = $total - $checkedout;
|
||||
return $remaining;
|
||||
}
|
||||
return $this->qty - $this->numCheckedOut();
|
||||
}
|
||||
|
||||
/**
|
||||
* Query builder scope to order on company
|
||||
|
|
|
@ -161,6 +161,7 @@ class LicensePresenter extends Presenter
|
|||
"field" => "name",
|
||||
"searchable" => false,
|
||||
"sortable" => false,
|
||||
"sorter" => "numericOnly",
|
||||
"switchable" => true,
|
||||
"title" => trans('admin/licenses/general.seat'),
|
||||
"visible" => true,
|
||||
|
|
|
@ -44,6 +44,9 @@ return array(
|
|||
'exists' => 'The selected :attribute is invalid.',
|
||||
'file' => 'The :attribute must be a file.',
|
||||
'filled' => 'The :attribute field must have a value.',
|
||||
'gt' => [
|
||||
'numeric' => 'The :attribute field must be greater than :value.'
|
||||
],
|
||||
'hashed_pass' => 'Your password is incorrect.',
|
||||
'image' => 'The :attribute must be an image.',
|
||||
'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>
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
|
||||
<h2>{{ trans('admin/accessories/general.about_accessories_title') }}</h4>
|
||||
<p>{{ trans('admin/accessories/general.about_accessories_text') }} </p>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@stop
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
<label for="assigned_qty" class="col-md-3 control-label">{{ trans('general.qty') }}
|
||||
<i class='icon-asterisk'></i></label>
|
||||
<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>') !!}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -67,7 +67,6 @@
|
|||
</div> <!-- /.box.box-default-->
|
||||
</div> <!-- /.col-md-9-->
|
||||
<div class="col-md-3">
|
||||
|
||||
@if ($consumable->image!='')
|
||||
<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>
|
||||
|
@ -116,10 +115,12 @@
|
|||
{{ $consumable->order_number }}
|
||||
</div>
|
||||
@endif
|
||||
<div class="col-md-12" style="padding-bottom: 5px;">
|
||||
<h2>{{ trans('admin/consumables/general.about_consumables_title') }}</h4>
|
||||
<p>{{ trans('admin/consumables/general.about_consumables_text') }} </p>
|
||||
</div>
|
||||
|
||||
@can('checkout', \App\Models\Accessory::class)
|
||||
<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>
|
||||
@endcan
|
||||
</div> <!-- /.col-md-3-->
|
||||
</div> <!-- /.row-->
|
||||
|
||||
|
|
|
@ -338,7 +338,7 @@
|
|||
data-sort-order="asc"
|
||||
data-sort-name="name"
|
||||
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='{
|
||||
"fileName": "export-seats-{{ str_slug($license->name) }}-{{ date('Y-m-d') }}",
|
||||
"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