Skip to content

Commit cebb9d0

Browse files
authored
Merge pull request #16305 from snipe/bug/sc-28425
Fixed #16262 - Check for quantity before allowing component deletion
2 parents 50c88df + dd2b570 commit cebb9d0

File tree

7 files changed

+57
-8
lines changed

7 files changed

+57
-8
lines changed

app/Http/Controllers/Api/ComponentsController.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ public function index(Request $request) : JsonResponse | array
4848
];
4949

5050
$components = Component::select('components.*')
51-
->with('company', 'location', 'category', 'assets', 'supplier', 'adminuser', 'manufacturer');
51+
->with('company', 'location', 'category', 'assets', 'supplier', 'adminuser', 'manufacturer', 'uncontrainedAssets')
52+
->withSum('uncontrainedAssets', 'components_assets.assigned_qty');
5253

5354
if ($request->filled('search')) {
5455
$components = $components->TextSearch($request->input('search'));
@@ -197,6 +198,11 @@ public function destroy($id) : JsonResponse
197198
$this->authorize('delete', Component::class);
198199
$component = Component::findOrFail($id);
199200
$this->authorize('delete', $component);
201+
202+
if ($component->numCheckedOut() > 0) {
203+
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/components/message.delete.error_qty')));
204+
}
205+
200206
$component->delete();
201207

202208
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/components/message.delete.success')));

app/Http/Controllers/Components/ComponentsController.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,10 @@ public function destroy($componentId)
196196
}
197197
}
198198

199+
if ($component->numCheckedOut() > 0) {
200+
return redirect()->route('components.index')->with('error', trans('admin/components/message.delete.error_qty'));
201+
}
202+
199203
$component->delete();
200204

201205
return redirect()->route('components.index')->with('success', trans('admin/components/message.delete.success'));

app/Http/Transformers/ComponentsTransformer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public function transformComponent(Component $component)
6262
'checkout' => Gate::allows('checkout', Component::class),
6363
'checkin' => Gate::allows('checkin', Component::class),
6464
'update' => Gate::allows('update', Component::class),
65-
'delete' => Gate::allows('delete', Component::class),
65+
'delete' => $component->isDeletable(),
6666
];
6767
$array += $permissions_array;
6868

app/Models/Component.php

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use App\Presenters\Presentable;
77
use Illuminate\Database\Eloquent\Factories\HasFactory;
88
use Illuminate\Database\Eloquent\SoftDeletes;
9+
use Illuminate\Support\Facades\Gate;
910
use Watson\Validating\ValidatingTrait;
1011

1112
/**
@@ -104,6 +105,13 @@ class Component extends SnipeModel
104105
];
105106

106107

108+
public function isDeletable()
109+
{
110+
return Gate::allows('delete', $this)
111+
&& ($this->numCheckedOut() === 0)
112+
&& ($this->deleted_at == '');
113+
}
114+
107115
/**
108116
* Establishes the components -> action logs -> uploads relationship
109117
*
@@ -234,13 +242,24 @@ public function numCheckedOut()
234242
// In case there are elements checked out to assets that belong to a different company
235243
// than this asset and full multiple company support is on we'll remove the global scope,
236244
// so they are included in the count.
237-
foreach ($this->assets()->withoutGlobalScope(new CompanyableScope)->get() as $checkout) {
238-
$checkedout += $checkout->pivot->assigned_qty;
239-
}
245+
return $this->uncontrainedAssets->sum('pivot.assigned_qty');
246+
}
247+
248+
249+
/**
250+
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
251+
*
252+
* This allows us to get the assets with assigned components without the company restriction
253+
*/
254+
public function uncontrainedAssets() {
255+
256+
return $this->belongsToMany(\App\Models\Asset::class, 'components_assets')
257+
->withPivot('id', 'assigned_qty', 'created_at', 'created_by', 'note')
258+
->withoutGlobalScope(new CompanyableScope);
240259

241-
return $checkedout;
242260
}
243261

262+
244263
/**
245264
* Check how many items within a component are remaining
246265
*

resources/lang/en-US/admin/components/message.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
'delete' => array(
1818
'confirm' => 'Are you sure you wish to delete this component?',
1919
'error' => 'There was an issue deleting the component. Please try again.',
20-
'success' => 'The component was deleted successfully.'
20+
'success' => 'The component was deleted successfully.',
21+
'error_qty' => 'Some components of this type are still checked out. Please check them in and try again.',
2122
),
2223

2324
'checkout' => array(

tests/Feature/Components/Api/DeleteComponentsTest.php renamed to tests/Feature/Components/Api/DeleteComponentTest.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
use Tests\Concerns\TestsPermissionsRequirement;
1010
use Tests\TestCase;
1111

12-
class DeleteComponentsTest extends TestCase implements TestsFullMultipleCompaniesSupport, TestsPermissionsRequirement
12+
class DeleteComponentTest extends TestCase implements TestsFullMultipleCompaniesSupport, TestsPermissionsRequirement
1313
{
1414
public function testRequiresPermission()
1515
{
@@ -63,4 +63,13 @@ public function testCanDeleteComponents()
6363

6464
$this->assertSoftDeleted($component);
6565
}
66+
67+
public function testCannotDeleteComponentIfCheckedOut()
68+
{
69+
$component = Component::factory()->checkedOutToAsset()->create();
70+
71+
$this->actingAsForApi(User::factory()->deleteComponents()->create())
72+
->deleteJson(route('api.components.destroy', $component))
73+
->assertStatusMessageIs('error');
74+
}
6675
}

tests/Feature/Components/Ui/DeleteComponentTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ public function testCanDeleteComponent()
4040
$this->assertSoftDeleted($component);
4141
}
4242

43+
public function testCannotDeleteComponentIfCheckedOut()
44+
{
45+
$component = Component::factory()->checkedOutToAsset()->create();
46+
47+
$this->actingAs(User::factory()->deleteComponents()->create())
48+
->delete(route('components.destroy', $component->id))
49+
->assertSessionHas('error')
50+
->assertRedirect(route('components.index'));
51+
}
52+
4353
public function testDeletingComponentRemovesComponentImage()
4454
{
4555
Storage::fake('public');

0 commit comments

Comments
 (0)