diff --git a/app/Console/Commands/LdapSync.php b/app/Console/Commands/LdapSync.php index e11d269ef8..594f6f064f 100755 --- a/app/Console/Commands/LdapSync.php +++ b/app/Console/Commands/LdapSync.php @@ -18,7 +18,7 @@ class LdapSync extends Command * * @var string */ - protected $signature = 'snipeit:ldap-sync {--location=} {--location_id=} {--base_dn=} {--filter=} {--summary} {--json_summary}'; + protected $signature = 'snipeit:ldap-sync {--location=} {--location_id=*} {--base_dn=} {--filter=} {--summary} {--json_summary}'; /** * The console command description. @@ -83,11 +83,15 @@ class LdapSync extends Command $summary = []; try { - if ( !empty($this->option('location_id')) != '') { - $location_ou= Location::where('id', '=', $this->option('location_id'))->value('ldap_ou'); - $search_base = $location_ou; - Log::debug('Importing users from specified location OU: \"'.$search_base.'\".'); - } + if ( $this->option('location_id') != '') { + + foreach($this->option('location_id') as $location_id){ + $location_ou= Location::where('id', '=', $location_id)->value('ldap_ou'); + $search_base = $location_ou; + Log::debug('Importing users from specified location OU: \"'.$search_base.'\".'); + } + } + else if ($this->option('base_dn') != '') { $search_base = $this->option('base_dn'); Log::debug('Importing users from specified base DN: \"'.$search_base.'\".'); @@ -111,21 +115,21 @@ class LdapSync extends Command /* Determine which location to assign users to by default. */ $location = null; // TODO - this would be better called "$default_location", which is more explicit about its purpose + if ($this->option('location') != '') { + if ($location = Location::where('name', '=', $this->option('location'))->first()) { + Log::debug('Location name ' . $this->option('location') . ' passed'); + Log::debug('Importing to ' . $location->name . ' (' . $location->id . ')'); + } + + } elseif ($this->option('location_id') != '') { + foreach($this->option('location_id') as $location_id) { + if ($location = Location::where('id', '=', $location_id)->first()) { + Log::debug('Location ID ' . $location_id . ' passed'); + Log::debug('Importing to ' . $location->name . ' (' . $location->id . ')'); + } - if ($this->option('location') != '') { - if ($location = Location::where('name', '=', $this->option('location'))->first()) { - Log::debug('Location name '.$this->option('location').' passed'); - Log::debug('Importing to '.$location->name.' ('.$location->id.')'); } - - } elseif ($this->option('location_id') != '') { - if ($location = Location::where('id', '=', $this->option('location_id'))->first()) { - Log::debug('Location ID '.$this->option('location_id').' passed'); - Log::debug('Importing to '.$location->name.' ('.$location->id.')'); - } - } - if (! isset($location)) { Log::debug('That location is invalid or a location was not provided, so no location will be assigned by default.'); } diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 3d4db93452..e76d8e5dae 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -150,6 +150,11 @@ class Handler extends ExceptionHandler return redirect()->guest('login'); } + protected function invalidJson($request, ValidationException $exception) + { + return response()->json(Helper::formatStandardApiResponse('error', null, $exception->errors()), 200); + } + /** * A list of the inputs that are never flashed for validation exceptions. diff --git a/app/Http/Controllers/Api/AssetsController.php b/app/Http/Controllers/Api/AssetsController.php index 6c343731ca..954ba3a4d0 100644 --- a/app/Http/Controllers/Api/AssetsController.php +++ b/app/Http/Controllers/Api/AssetsController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Api; use App\Events\CheckoutableCheckedIn; +use App\Http\Requests\StoreAssetRequest; use Illuminate\Support\Facades\Gate; use App\Helpers\Helper; use App\Http\Controllers\Controller; @@ -33,6 +34,7 @@ use TCPDF; use Validator; use Route; + /** * This class controls all actions related to assets for * the Snipe-IT Asset Management application. @@ -532,10 +534,8 @@ class AssetsController extends Controller * @since [v4.0] * @return \Illuminate\Http\JsonResponse */ - public function store(ImageUploadRequest $request) + public function store(StoreAssetRequest $request) { - $this->authorize('create', Asset::class); - $asset = new Asset(); $asset->model()->associate(AssetModel::find((int) $request->get('model_id'))); diff --git a/app/Http/Controllers/LabelsController.php b/app/Http/Controllers/LabelsController.php index ddff771961..950094bc45 100755 --- a/app/Http/Controllers/LabelsController.php +++ b/app/Http/Controllers/LabelsController.php @@ -7,8 +7,10 @@ use App\Models\AssetModel; use App\Models\Category; use App\Models\Company; use App\Models\Labels\Label; +use App\Models\Location; use App\Models\Manufacturer; use App\Models\Setting; +use App\Models\Supplier; use App\Models\User; use App\View\Label as LabelView; use Illuminate\Support\Facades\Storage; @@ -33,18 +35,20 @@ class LabelsController extends Controller $exampleAsset->name = 'JEN-867-5309'; $exampleAsset->asset_tag = '100001'; $exampleAsset->serial = 'SN9876543210'; + $exampleAsset->asset_eol_date = '2025-01-01'; + $exampleAsset->order_number = '12345'; + $exampleAsset->purchase_date = '2023-01-01'; + $exampleAsset->status_id = 1; - $exampleAsset->company = new Company(); - $exampleAsset->company->id = 999999; - $exampleAsset->company->name = 'Test Company Limited'; - $exampleAsset->company->image = 'company-image-test.png'; + $exampleAsset->company = new Company([ + 'name' => 'Test Company Limited', + 'phone' => '1-555-555-5555', + 'email' => 'company@example.com', + ]); - $exampleAsset->assignedto = new User(); - $exampleAsset->assignedto->id = 999999; - $exampleAsset->assignedto->first_name = 'Test'; - $exampleAsset->assignedto->last_name = 'Person'; - $exampleAsset->assignedto->username = 'Test.Person'; - $exampleAsset->assignedto->employee_num = '0123456789'; + $exampleAsset->setRelation('assignedTo', new User(['first_name' => 'Luke', 'last_name' => 'Skywalker'])); + $exampleAsset->defaultLoc = new Location(['name' => 'Building 1', 'phone' => '1-555-555-5555']); + $exampleAsset->location = new Location(['name' => 'Building 2', 'phone' => '1-555-555-5555']); $exampleAsset->model = new AssetModel(); $exampleAsset->model->id = 999999; @@ -53,6 +57,10 @@ class LabelsController extends Controller $exampleAsset->model->manufacturer = new Manufacturer(); $exampleAsset->model->manufacturer->id = 999999; $exampleAsset->model->manufacturer->name = 'Test Manufacturing Inc.'; + $exampleAsset->model->manufacturer->support_email = 'support@test.com'; + $exampleAsset->model->manufacturer->support_phone = '1-555-555-5555'; + $exampleAsset->model->manufacturer->support_url = 'https://example.com'; + $exampleAsset->supplier = new Supplier(['name' => 'Test Company Limited']); $exampleAsset->model->category = new Category(); $exampleAsset->model->category->id = 999999; $exampleAsset->model->category->name = 'Test Category'; diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php index b4acc4c5a0..989fe8c494 100755 --- a/app/Http/Controllers/SettingsController.php +++ b/app/Http/Controllers/SettingsController.php @@ -7,6 +7,7 @@ use App\Helpers\StorageHelper; use App\Http\Requests\ImageUploadRequest; use App\Http\Requests\SettingsSamlRequest; use App\Http\Requests\SetupUserRequest; +use App\Models\CustomField; use App\Models\Group; use App\Models\Setting; use App\Models\Asset; @@ -809,9 +810,10 @@ class SettingsController extends Controller */ public function getLabels() { - $setting = Setting::getSettings(); - - return view('settings.labels', compact('setting')); + return view('settings.labels', [ + 'setting' => Setting::getSettings(), + 'customFields' => CustomField::all(), + ]); } /** diff --git a/app/Http/Controllers/Users/LDAPImportController.php b/app/Http/Controllers/Users/LDAPImportController.php index b86be03d99..e535a171a0 100644 --- a/app/Http/Controllers/Users/LDAPImportController.php +++ b/app/Http/Controllers/Users/LDAPImportController.php @@ -49,19 +49,19 @@ class LDAPImportController extends Controller { $this->authorize('update', User::class); // Call Artisan LDAP import command. - $location_id = $request->input('location_id'); - Artisan::call('snipeit:ldap-sync', ['--location_id' => $location_id, '--json_summary' => true]); + + Artisan::call('snipeit:ldap-sync', ['--location_id' => $request->input('location_id'), '--json_summary' => true]); // Collect and parse JSON summary. $ldap_results_json = Artisan::output(); $ldap_results = json_decode($ldap_results_json, true); - if (!$ldap_results) { return redirect()->back()->withInput()->with('error', trans('general.no_results')); } // Direct user to appropriate status page. if ($ldap_results['error']) { + return redirect()->back()->withInput()->with('error', $ldap_results['error_message']); } diff --git a/app/Http/Requests/StoreAssetRequest.php b/app/Http/Requests/StoreAssetRequest.php new file mode 100644 index 0000000000..254895f134 --- /dev/null +++ b/app/Http/Requests/StoreAssetRequest.php @@ -0,0 +1,40 @@ +getRules(), + parent::rules(), + ); + + return $rules; + } +} diff --git a/app/Http/Traits/UniqueSerialTrait.php b/app/Http/Traits/UniqueSerialTrait.php deleted file mode 100644 index b5529d7cf8..0000000000 --- a/app/Http/Traits/UniqueSerialTrait.php +++ /dev/null @@ -1,25 +0,0 @@ -unique_serial == '1') { - return 'unique_undeleted:'.$this->table.','.$this->getKey(); - } - } - } -} diff --git a/app/Importer/AssetImporter.php b/app/Importer/AssetImporter.php index cd2b29205f..cf762a8fd4 100644 --- a/app/Importer/AssetImporter.php +++ b/app/Importer/AssetImporter.php @@ -144,7 +144,7 @@ class AssetImporter extends ItemImporter // If we have a target to checkout to, lets do so. //-- user_id is a property of the abstract class Importer, which this class inherits from and it's setted by //-- the class that needs to use it (command importer or GUI importer inside the project). - if (isset($target)) { + if (isset($target) && ($target !== false)) { if (!is_null($asset->assigned_to)){ if ($asset->assigned_to != $target->id){ event(new CheckoutableCheckedIn($asset, User::find($asset->assigned_to), Auth::user(), $asset->notes, date('Y-m-d H:i:s'))); diff --git a/app/Importer/Importer.php b/app/Importer/Importer.php index 76703ae1f8..961f4af52c 100644 --- a/app/Importer/Importer.php +++ b/app/Importer/Importer.php @@ -19,22 +19,76 @@ abstract class Importer * Id of User performing import * @var */ + protected $user_id; /** * Are we updating items in the import * @var bool */ + protected $updating; + /** * Default Map of item fields->csv names * * This has been moved into app/Http/Livewire/Importer.php to be more granular. - * @todo - remove references to this property since we don't use it anymore. + * This private variable is ONLY used for the cli-importer. * + * @todo - find a way to make this less duplicative * @var array */ private $defaultFieldMap = [ - + 'asset_tag' => 'asset tag', + 'activated' => 'activated', + 'category' => 'category', + 'checkout_class' => 'checkout type', // Supports Location or User for assets. Using checkout_class instead of checkout_type because type exists on asset already. + 'checkout_location' => 'checkout location', + 'company' => 'company', + 'item_name' => 'item name', + 'item_number' => 'item number', + 'image' => 'image', + 'expiration_date' => 'expiration date', + 'location' => 'location', + 'notes' => 'notes', + 'license_email' => 'licensed to email', + 'license_name' => 'licensed to name', + 'maintained' => 'maintained', + 'manufacturer' => 'manufacturer', + 'asset_model' => 'model name', + 'model_number' => 'model number', + 'order_number' => 'order number', + 'purchase_cost' => 'purchase cost', + 'purchase_date' => 'purchase date', + 'purchase_order' => 'purchase order', + 'qty' => 'quantity', + 'reassignable' => 'reassignable', + 'requestable' => 'requestable', + 'seats' => 'seats', + 'serial' => 'serial number', + 'status' => 'status', + 'supplier' => 'supplier', + 'termination_date' => 'termination date', + 'warranty_months' => 'warranty', + 'full_name' => 'full name', + 'email' => 'email', + 'username' => 'username', + 'address' => 'address', + 'address2' => 'address2', + 'city' => 'city', + 'state' => 'state', + 'country' => 'country', + 'zip' => 'zip', + 'jobtitle' => 'job title', + 'employee_num' => 'employee number', + 'phone_number' => 'phone number', + 'first_name' => 'first name', + 'last_name' => 'last name', + 'department' => 'department', + 'manager_name' => 'manager full name', + 'manager_username' => 'manager username', + 'min_amt' => 'minimum quantity', + 'remote' => 'remote', + 'vip' => 'vip', ]; /** * Map of item fields->csv names diff --git a/app/Models/Asset.php b/app/Models/Asset.php index 9f15101d33..ea436f0138 100644 --- a/app/Models/Asset.php +++ b/app/Models/Asset.php @@ -6,7 +6,6 @@ use App\Events\AssetCheckedOut; use App\Events\CheckoutableCheckedOut; use App\Exceptions\CheckoutNotAllowed; use App\Helpers\Helper; -use App\Http\Traits\UniqueSerialTrait; use App\Http\Traits\UniqueUndeletedTrait; use App\Models\Traits\Acceptable; use App\Models\Traits\Searchable; @@ -32,7 +31,7 @@ class Asset extends Depreciable protected $presenter = \App\Presenters\AssetPresenter::class; use CompanyableTrait; - use HasFactory, Loggable, Requestable, Presentable, SoftDeletes, ValidatingTrait, UniqueUndeletedTrait, UniqueSerialTrait; + use HasFactory, Loggable, Requestable, Presentable, SoftDeletes, ValidatingTrait, UniqueUndeletedTrait; public const LOCATION = 'location'; public const ASSET = 'asset'; @@ -91,7 +90,7 @@ class Asset extends Depreciable protected $rules = [ 'name' => 'max:255|nullable', - 'model_id' => 'required|integer|exists:models,id,deleted_at,NULL', + 'model_id' => 'required|integer|exists:models,id,deleted_at,NULL|not_array', 'status_id' => 'required|integer|exists:status_labels,id', 'company_id' => 'integer|nullable', 'warranty_months' => 'numeric|nullable|digits_between:0,240', @@ -100,7 +99,7 @@ class Asset extends Depreciable 'expected_checkin' => 'date|nullable', 'location_id' => 'exists:locations,id|nullable', 'rtd_location_id' => 'exists:locations,id|nullable', - 'asset_tag' => 'required|min:1|max:255|unique_undeleted', + 'asset_tag' => 'required|min:1|max:255|unique_undeleted:assets,asset_tag|not_array', 'purchase_date' => 'date|date_format:Y-m-d|nullable', 'serial' => 'unique_serial|nullable', 'purchase_cost' => 'numeric|nullable|gte:0', diff --git a/app/Models/Labels/FieldOption.php b/app/Models/Labels/FieldOption.php index 76427accaf..7e45cc0ce7 100644 --- a/app/Models/Labels/FieldOption.php +++ b/app/Models/Labels/FieldOption.php @@ -14,6 +14,14 @@ class FieldOption { public function getValue(Asset $asset) { $dataPath = collect(explode('.', $this->dataSource)); + + // assignedTo directly on the asset is a special case where + // we want to avoid returning the property directly + // and instead return the entity's presented name. + if ($dataPath[0] === 'assignedTo'){ + return $asset->assignedTo ? $asset->assignedTo->present()->fullName() : null; + } + return $dataPath->reduce(function ($myValue, $path) { try { return $myValue ? $myValue->{$path} : ${$myValue}; } catch (\Exception $e) { return $myValue; } @@ -46,4 +54,4 @@ class FieldOption { return $option; } } -} \ No newline at end of file +} diff --git a/app/Providers/ValidationServiceProvider.php b/app/Providers/ValidationServiceProvider.php index 70fa64702e..55d8efa3ec 100644 --- a/app/Providers/ValidationServiceProvider.php +++ b/app/Providers/ValidationServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use App\Models\Department; +use App\Models\Setting; use DB; use Illuminate\Support\ServiceProvider; use Illuminate\Validation\Rule; @@ -48,6 +49,8 @@ class ValidationServiceProvider extends ServiceProvider // Unique only if undeleted // This works around the use case where multiple deleted items have the same unique attribute. // (I think this is a bug in Laravel's validator?) + // $parameters is the rule parameters, like `unique_undeleted:users,id` - $parameters[0] is users, $parameters[1] is id + // the UniqueUndeletedTrait prefills these so you can just use `unique_undeleted` in your rules (but this would only work directly in the model) Validator::extend('unique_undeleted', function ($attribute, $value, $parameters, $validator) { if (count($parameters)) { $count = DB::table($parameters[0])->select('id')->where($attribute, '=', $value)->whereNull('deleted_at')->where('id', '!=', $parameters[1])->count(); @@ -70,6 +73,17 @@ class ValidationServiceProvider extends ServiceProvider } }); + + Validator::extend('unique_serial', function ($attribute, $value, $parameters, $validator) { + if(Setting::getSettings()->unique_serial == '1') { + $count = DB::table('assets')->select('id')->where('serial', '=', $value)->whereNull('deleted_at')->count(); + + return $count < 1; + } else { + return true; + } + }); + // Prevent circular references // // Example usage in Location model where parent_id references another Location: diff --git a/resources/lang/en/general.php b/resources/lang/en/general.php index fba828839f..f0eda87d08 100644 --- a/resources/lang/en/general.php +++ b/resources/lang/en/general.php @@ -485,7 +485,7 @@ return [ ], 'percent_complete' => '% complete', 'uploading' => 'Uploading... ', - 'upload_error' => 'Error uploading file. Please check that there are no empty trailing rows.', + 'upload_error' => 'Error uploading file. Please check that there are no empty rows and that no column names are duplicated.', 'copy_to_clipboard' => 'Copy to Clipboard', 'copied' => 'Copied!', diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php index bb35515fdb..85e8447812 100644 --- a/resources/lang/en/validation.php +++ b/resources/lang/en/validation.php @@ -96,6 +96,7 @@ return [ 'unique_undeleted' => 'The :attribute must be unique.', 'non_circular' => 'The :attribute must not create a circular reference.', 'not_array' => 'The :attribute field cannot be an array.', + 'unique_serial' => 'The :attribute must be unique.', 'disallow_same_pwd_as_user_fields' => 'Password cannot be the same as the username.', 'letters' => 'Password must contain at least one letter.', 'numbers' => 'Password must contain at least one number.', diff --git a/resources/views/partials/label2-field-definitions.blade.php b/resources/views/partials/label2-field-definitions.blade.php index 6522b951d8..21aa7a0cf9 100644 --- a/resources/views/partials/label2-field-definitions.blade.php +++ b/resources/views/partials/label2-field-definitions.blade.php @@ -304,7 +304,54 @@ - + @@ -331,4 +378,4 @@ > - \ No newline at end of file + diff --git a/resources/views/settings/labels.blade.php b/resources/views/settings/labels.blade.php index 46ce4fe610..a6edd29154 100644 --- a/resources/views/settings/labels.blade.php +++ b/resources/views/settings/labels.blade.php @@ -216,7 +216,7 @@ {{ Form::label('label2_fields', trans('admin/settings/general.label2_fields')) }}
{{ trans('admin/settings/general.label2_fields_help') }}