Merge pull request #16947 from Godmartinz/add-require-serial-to-models

Adds require serial as Asset Model option
This commit is contained in:
snipe
2025-09-03 14:53:15 +01:00
committed by GitHub
15 changed files with 188 additions and 10 deletions

View File

@@ -50,6 +50,7 @@ class AssetModelsController extends Controller
'fieldset',
'deleted_at',
'updated_at',
'require_serial',
];
$assetmodels = AssetModel::select([
@@ -69,6 +70,7 @@ class AssetModelsController extends Controller
'models.fieldset_id',
'models.deleted_at',
'models.updated_at',
'models.require_serial'
])
->with('category', 'depreciation', 'manufacturer', 'fieldset.fields.defaultValues', 'adminuser')
->withCount('assets as assets_count');

View File

@@ -82,6 +82,7 @@ class AssetModelsController extends Controller
$model->notes = $request->input('notes');
$model->created_by = auth()->id();
$model->requestable = $request->has('requestable');
$model->require_serial = $request->input('require_serial', 0);
if ($request->input('fieldset_id') != '') {
$model->fieldset_id = $request->input('fieldset_id');
@@ -155,7 +156,7 @@ class AssetModelsController extends Controller
$model->category_id = $request->input('category_id');
$model->notes = $request->input('notes');
$model->requestable = $request->input('requestable', '0');
$model->require_serial = $request->input('require_serial', 0);
$model->fieldset_id = $request->input('fieldset_id');
if ($model->save()) {

View File

@@ -110,17 +110,35 @@ class AssetsController extends Controller
// This is only necessary on create, not update, since bulk editing is handled
// differently
$asset_tags = $request->input('asset_tags');
$model = AssetModel::find($request->input('model_id'));
$serial_errors = [];
$serials = $request->input('serials');
$settings = Setting::getSettings();
//Validate required serial based on model setting
for ($a = 1, $aMax = count($asset_tags); $a <= $aMax; $a++) {
if ($model && $model->require_serial === 1 && empty($serials[$a])) {
$serial_errors["serials.$a"] = trans('admin/hardware/form.serial_required', ['number' => $a]);
}
}
if (!empty($serial_errors)) {
return redirect()->back()
->withInput()
->withErrors($serial_errors);
}
$asset = null;
$companyId = Company::getIdForCurrentUser($request->input('company_id'));
$successes = [];
$failures = [];
$serials = $request->input('serials');
$asset = null;
for ($a = 1; $a <= count($asset_tags); $a++) {
for ($a = 1, $aMax = count($asset_tags); $a <= $aMax; $a++) {
$asset = new Asset();
$asset->model()->associate(AssetModel::find($request->input('model_id')));
$asset->model()->associate($model);
$asset->name = $request->input('name');
// Check for a corresponding serial
@@ -132,7 +150,7 @@ class AssetsController extends Controller
$asset->asset_tag = $asset_tags[$a];
}
$asset->company_id = Company::getIdForCurrentUser($request->input('company_id'));
$asset->company_id = $companyId;
$asset->model_id = $request->input('model_id');
$asset->order_number = $request->input('order_number');
$asset->notes = $request->input('notes');
@@ -172,7 +190,6 @@ class AssetsController extends Controller
// Update custom fields in the database.
// Validation for these fields is handled through the AssetRequest form request
$model = AssetModel::find($request->get('model_id'));
if (($model) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) {
@@ -453,6 +470,13 @@ class AssetsController extends Controller
]);
//Validate required serial based on model setting
if ($model && $model->require_serial === 1 && empty($serial[1])) {
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
->with('warning', trans('admin/hardware/form.serial_required_post_model_update', [
'asset_model' => $model->name
]));
}
if ($asset->save()) {
return Helper::getRedirectOption($request, $asset->id, 'Assets')
->with('success', trans('admin/hardware/message.update.success'));

View File

@@ -92,7 +92,9 @@ class BulkAssetModelsController extends Controller
$update_array['min_amt'] = $request->input('min_amt');
}
if ($request->filled('require_serial')) {
$update_array['require_serial'] = $request->input('require_serial');
}
if (count($update_array) > 0) {
AssetModel::whereIn('id', $models_raw_array)->update($update_array);

View File

@@ -65,6 +65,7 @@ class AssetModelsTransformer
'default_fieldset_values' => $default_field_values,
'eol' => ($assetmodel->eol > 0) ? $assetmodel->eol.' months' : 'None',
'requestable' => ($assetmodel->requestable == '1') ? true : false,
'require_serial' => $assetmodel->require_serial,
'notes' => Helper::parseEscapedMarkedownInline($assetmodel->notes),
'created_by' => ($assetmodel->adminuser) ? [
'id' => (int) $assetmodel->adminuser->id,

View File

@@ -71,6 +71,7 @@ class AssetModel extends SnipeModel
'name',
'notes',
'requestable',
'require_serial'
];
use Searchable;

View File

@@ -143,6 +143,14 @@ class AssetModelPresenter extends Presenter
'title' => trans('admin/hardware/general.requestable'),
'formatter' => 'trueFalseFormatter',
],
[
'field' => 'require_serial',
'searchable' => false,
'sortable' => true,
'visible' => false,
'title' => trans('admin/hardware/general.require_serial'),
'formatter' => 'trueFalseFormatter',
],
[
'field' => 'notes',
'searchable' => true,

View File

@@ -33,6 +33,7 @@ class AssetModelFactory extends Factory
'category_id' => Category::factory(),
'model_number' => $this->faker->creditCardNumber(),
'notes' => 'Created by demo seeder',
'require_serial' => 0,
];
}

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('models', function (Blueprint $table) {
$table->boolean( 'require_serial')->after('category_id')->default(0);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('models', function (Blueprint $table) {
$table->dropColumn('require_serial');
});
}
};

View File

@@ -44,6 +44,8 @@ return [
'redirect_to_checked_out_to' => 'Go to Checked Out to',
'select_statustype' => 'Select Status Type',
'serial' => 'Serial',
'serial_required' => 'Asset :number requires a serial number',
'serial_required_post_model_update' => ':asset_model have been updated to require a serial number. Please add a serial number for this asset.',
'status' => 'Status',
'tag' => 'Asset Tag',
'update' => 'Asset Update',

View File

@@ -22,6 +22,8 @@ return [
'requested' => 'Requested',
'not_requestable' => 'Not Requestable',
'requestable_status_warning' => 'Do not change requestable status',
'require_serial' => 'Require Serial Number',
'require_serial_help' => 'A serial number will be required when creating a new asset of this model.',
'restore' => 'Restore Asset',
'pending' => 'Pending',
'undeployable' => 'Undeployable',

View File

@@ -91,7 +91,27 @@
</div>
@include ('partials.forms.edit.minimum_quantity')
<!-- require serial boolean -->
<div class="form-group">
<label for="require_serial" class="col-md-3 control-label">
{{ trans('admin/hardware/general.require_serial') }}
</label>
<div class="col-md-9">
<div class="form-inline" style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" name="require_serial" value="1" id="require_serial" aria-label="require_serial" />
<a
href="#"
data-tooltip="true"
title="{{ trans('admin/hardware/general.require_serial_help') }}"
style="display: inline-flex; align-items: center;"
>
<x-icon type="info-circle" />
<span class="sr-only">{{ trans('admin/hardware/general.require_serial_help') }}</span>
</a>
</div>
</div>
</div>
<!-- requestable -->
<div class="form-group{{ $errors->has('requestable') ? ' has-error' : '' }}">

View File

@@ -16,6 +16,27 @@
@include ('partials.forms.edit.depreciation')
@include ('partials.forms.edit.minimum_quantity')
<!-- require serial boolean -->
<div class="form-group">
<label for="require_serial" class="col-md-3 control-label">
{{ trans('admin/hardware/general.require_serial') }}
</label>
<div class="col-md-9">
<div class="form-inline" style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" name="require_serial" value="1" @checked(old('require_serial', $item->require_serial)) id="require_serial" aria-label="require_serial" />
<a
href="#"
data-tooltip="true"
title="{{ trans('admin/hardware/general.require_serial_help') }}"
style="display: inline-flex; align-items: center;"
>
<x-icon type="info-circle" />
<span class="sr-only">{{ trans('admin/hardware/general.require_serial_help') }}</span>
</a>
</div>
</div>
</div>
<!-- EOL -->
<div class="form-group {{ $errors->has('eol') ? ' has-error' : '' }}">

View File

@@ -2,7 +2,11 @@
<div class="form-group {{ $errors->has('serial') ? ' has-error' : '' }}">
<label for="{{ $fieldname }}" class="col-md-3 control-label">{{ trans('admin/hardware/form.serial') }} </label>
<div class="col-md-7 col-sm-12">
<input class="form-control" type="text" name="{{ $fieldname }}" id="{{ $fieldname }}" value="{{ old((isset($old_val_name) ? $old_val_name : $fieldname), $item->serial) }}"{{ (Helper::checkIfRequired($item, 'serial')) ? ' required' : '' }} maxlength="191" />
{!! $errors->first('serial', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
<input class="form-control" type="text" name="{{ $fieldname }}" id="{{ $fieldname }}" value="{{ old((isset($old_val_name) ? $old_val_name : $fieldname), $item->serial) }}" {{ (Helper::checkIfRequired($item, 'serial') || ($item->model && $item->model->require_serial)) ? ' required' : '' }} maxlength="191" />
@error($old_val_name ?? $fieldname)
<span class="alert-msg" aria-hidden="true">
<i class="fas fa-times" aria-hidden="true"></i> {{ $message }}
</span>
@enderror
</div>
</div>

View File

@@ -2,6 +2,8 @@
namespace Tests\Feature\Assets\Ui;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\User;
use Tests\TestCase;
@@ -13,4 +15,63 @@ class StoreAssetsTest extends TestCase
->get(route('hardware.create'))
->assertOk();
}
public function testAssetCanBeStoredWithSerialRequiredAndSerialProvided()
{
$user = User::factory()->superuser()->create();
$this->actingAs($user);
$model = AssetModel::factory()->create([
'require_serial' => 1,
]);
$response = $this->post(route('hardware.store'), [
'model_id' => $model->id,
'serials' => [1 => 'ABC123'],
'asset_tags' =>[1 => '1234'],
'status_id' => 1,
// other required fields...
]);
$response->assertRedirect();
$response->assertSessionHas('success-unescaped');
$this->assertNotEquals(
trans('admin/hardware/form.serial_required'),
session('error')
);
$this->assertDatabaseHas('assets', [
'model_id' => $model->id,
'serial' => 'ABC123',
'asset_tag' => '1234',
]);
}
public function testAssetCannotBeStoredIfSerialRequiredAndMissing()
{
$user = User::factory()->superuser()->create();
$this->actingAs($user);
$model = AssetModel::factory()->create([
'require_serial' => 1,
]);
$response = $this->post(route('hardware.store'), [
'model_id' => $model->id,
'serials' => [], // ← serial missing
'asset_tags' => [1 => '1234'],
'status_id' => 1,
]);
$response->assertRedirect();
$response->assertSessionHasErrors(['serials.1']);
$this->assertDatabaseMissing('assets', [
'model_id' => $model->id,
'asset_tag' => '1234',
]);
$response->assertSessionMissing('success-unescaped');
}
}