diff --git a/.all-contributorsrc b/.all-contributorsrc index dd090262eb..eb052687eb 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -2889,6 +2889,24 @@ "avatar_url": "https://avatars.githubusercontent.com/u/570639?v=4", "profile": "https://github.com/Mezzle", "contributions": [] + }, + { + "login": "dboth", + "name": "dboth", + "avatar_url": "https://avatars.githubusercontent.com/u/5731963?v=4", + "profile": "http://dboth.de", + "contributions": [ + "code" + ] + }, + { + "login": "zacharyfleck", + "name": "Zachary Fleck", + "avatar_url": "https://avatars.githubusercontent.com/u/87536651?v=4", + "profile": "https://github.com/zacharyfleck", + "contributions": [ + "code" + ] } ] } diff --git a/.github/workflows/docker-alpine.yml b/.github/workflows/docker-alpine.yml index f274fc2c40..0a5c28ee53 100644 --- a/.github/workflows/docker-alpine.yml +++ b/.github/workflows/docker-alpine.yml @@ -76,7 +76,7 @@ jobs: with: context: . file: ./Dockerfile.alpine - platforms: linux/amd64 + platforms: linux/amd64,linux/arm64 # For pull requests, we run the Docker build (to ensure no PR changes break the build), # but we ONLY do an image push to DockerHub if it's NOT a PR push: ${{ github.event_name != 'pull_request' }} diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 323d90e41c..5aa2758e79 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -76,7 +76,7 @@ jobs: with: context: . file: ./Dockerfile - platforms: linux/amd64 + platforms: linux/amd64,linux/arm64 # For pull requests, we run the Docker build (to ensure no PR changes break the build), # but we ONLY do an image push to DockerHub if it's NOT a PR push: ${{ github.event_name != 'pull_request' }} diff --git a/README.md b/README.md index 3a0d37998f..040de8e93c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ ![Build Status](https://app.chipperci.com/projects/0e5f8979-31eb-4ee6-9abf-050b76ab0383/status/master) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/snipe-it/localized.svg)](https://crowdin.com/project/snipe-it) [![Docker Pulls](https://img.shields.io/docker/pulls/snipe/snipe-it.svg)](https://hub.docker.com/r/snipe/snipe-it/) [![Twitter Follow](https://img.shields.io/twitter/follow/snipeitapp.svg?style=social)](https://twitter.com/snipeitapp) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/553ce52037fc43ea99149785afcfe641)](https://www.codacy.com/app/snipe/snipe-it?utm_source=github.com&utm_medium=referral&utm_content=snipe/snipe-it&utm_campaign=Badge_Grade) -[![All Contributors](https://img.shields.io/badge/all_contributors-318-orange.svg?style=flat-square)](#contributors) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/yZFtShAcKk) [![huntr](https://cdn.huntr.dev/huntr_security_badge_mono.svg)](https://huntr.dev) +[![All Contributors](https://img.shields.io/badge/all_contributors-320-orange.svg?style=flat-square)](#contributors) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/yZFtShAcKk) [![huntr](https://cdn.huntr.dev/huntr_security_badge_mono.svg)](https://huntr.dev) ## Snipe-IT - Open Source Asset Management System @@ -144,7 +144,7 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken | [
Peace](https://github.com/julian-piehl)
[💻](https://github.com/snipe/snipe-it/commits?author=julian-piehl "Code") | [
Kyle Gordon](https://github.com/kylegordon)
[💻](https://github.com/snipe/snipe-it/commits?author=kylegordon "Code") | [
Katharina Drexel](http://www.bfh.ch)
[💻](https://github.com/snipe/snipe-it/commits?author=sunflowerbofh "Code") | [
David Sferruzza](https://david.sferruzza.fr/)
[💻](https://github.com/snipe/snipe-it/commits?author=dsferruzza "Code") | [
Rick Nelson](https://github.com/rnelsonee)
[💻](https://github.com/snipe/snipe-it/commits?author=rnelsonee "Code") | [
BasO12](https://github.com/BasO12)
[💻](https://github.com/snipe/snipe-it/commits?author=BasO12 "Code") | [
Vautia](https://github.com/Vautia)
[💻](https://github.com/snipe/snipe-it/commits?author=Vautia "Code") | | [
Chris Hartjes](http://www.littlehart.net/atthekeyboard)
[💻](https://github.com/snipe/snipe-it/commits?author=chartjes "Code") | [
geo-chen](https://github.com/geo-chen)
[💻](https://github.com/snipe/snipe-it/commits?author=geo-chen "Code") | [
Phan Nguyen](https://github.com/nh314)
[💻](https://github.com/snipe/snipe-it/commits?author=nh314 "Code") | [
Iisakki Jaakkola](https://github.com/StarlessNights)
[💻](https://github.com/snipe/snipe-it/commits?author=StarlessNights "Code") | [
Ikko Ashimine](https://bandism.net/)
[💻](https://github.com/snipe/snipe-it/commits?author=eltociear "Code") | [
Lukas Fehling](https://github.com/lukasfehling)
[💻](https://github.com/snipe/snipe-it/commits?author=lukasfehling "Code") | [
Fernando Almeida](https://github.com/fernando-almeida)
[💻](https://github.com/snipe/snipe-it/commits?author=fernando-almeida "Code") | | [
akemidx](https://github.com/akemidx)
[💻](https://github.com/snipe/snipe-it/commits?author=akemidx "Code") | [
Oguz Bilgic](http://oguz.site)
[💻](https://github.com/snipe/snipe-it/commits?author=oguzbilgic "Code") | [
Scooter Crawford](https://github.com/scoo73r)
[💻](https://github.com/snipe/snipe-it/commits?author=scoo73r "Code") | [
subdriven](https://github.com/subdriven)
[💻](https://github.com/snipe/snipe-it/commits?author=subdriven "Code") | [
Andrew Savinykh](https://github.com/AndrewSav)
[💻](https://github.com/snipe/snipe-it/commits?author=AndrewSav "Code") | [
Tadayuki Onishi](https://kenchan0130.github.io)
[💻](https://github.com/snipe/snipe-it/commits?author=kenchan0130 "Code") | [
Florian](https://github.com/floschoepfer)
[💻](https://github.com/snipe/snipe-it/commits?author=floschoepfer "Code") | -| [
Spencer Long](http://spencerlong.com)
[💻](https://github.com/snipe/snipe-it/commits?author=spencerrlongg "Code") | [
Marcus Moore](https://github.com/marcusmoore)
[💻](https://github.com/snipe/snipe-it/commits?author=marcusmoore "Code") | [
Martin Meredith](https://github.com/Mezzle)
| +| [
Spencer Long](http://spencerlong.com)
[💻](https://github.com/snipe/snipe-it/commits?author=spencerrlongg "Code") | [
Marcus Moore](https://github.com/marcusmoore)
[💻](https://github.com/snipe/snipe-it/commits?author=marcusmoore "Code") | [
Martin Meredith](https://github.com/Mezzle)
| [
dboth](http://dboth.de)
[💻](https://github.com/snipe/snipe-it/commits?author=dboth "Code") | [
Zachary Fleck](https://github.com/zacharyfleck)
[💻](https://github.com/snipe/snipe-it/commits?author=zacharyfleck "Code") | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome! diff --git a/app/Console/Commands/CheckoutLicenseToAllUsers.php b/app/Console/Commands/CheckoutLicenseToAllUsers.php index c81408442a..801b3d187a 100644 --- a/app/Console/Commands/CheckoutLicenseToAllUsers.php +++ b/app/Console/Commands/CheckoutLicenseToAllUsers.php @@ -56,7 +56,7 @@ class CheckoutLicenseToAllUsers extends Command return false; } - $users = User::whereNull('deleted_at')->where('autoassign_licenses', '==', 1)->with('licenses')->get(); + $users = User::whereNull('deleted_at')->where('autoassign_licenses', '=', 1)->with('licenses')->get(); if ($users->count() > $license->getAvailSeatsCountAttribute()) { $this->info('You do not have enough free seats to complete this task, so we will check out as many as we can. '); diff --git a/app/Console/Commands/CreateAdmin.php b/app/Console/Commands/CreateAdmin.php index 5aebde3777..114f92dae4 100644 --- a/app/Console/Commands/CreateAdmin.php +++ b/app/Console/Commands/CreateAdmin.php @@ -20,13 +20,14 @@ class CreateAdmin extends Command * @property string $password * @property boolean $activated * @property boolean $show_in_list + * @property boolean $autoassign_licenses * @property \Illuminate\Support\Carbon|null $created_at * @property mixed $created_by */ - protected $signature = 'snipeit:create-admin {--first_name=} {--last_name=} {--email=} {--username=} {--password=} {show_in_list?}'; + protected $signature = 'snipeit:create-admin {--first_name=} {--last_name=} {--email=} {--username=} {--password=} {show_in_list?} {autoassign_licenses?}'; /** * The console command description. @@ -54,6 +55,9 @@ class CreateAdmin extends Command $email = $this->option('email'); $password = $this->option('password'); $show_in_list = $this->argument('show_in_list'); + $autoassign_licenses = $this->argument('autoassign_licenses'); + + if (($first_name == '') || ($last_name == '') || ($username == '') || ($email == '') || ($password == '')) { $this->info('ERROR: All fields are required.'); @@ -70,6 +74,11 @@ class CreateAdmin extends Command if ($show_in_list == 'false') { $user->show_in_list = 0; } + + if ($autoassign_licenses == 'false') { + $user->autoassign_licenses = 0; + } + if ($user->save()) { $this->info('New user created'); $user->groups()->attach(1); diff --git a/app/Console/Commands/LdapSync.php b/app/Console/Commands/LdapSync.php index 975db4f5d7..dd6fea8b66 100755 --- a/app/Console/Commands/LdapSync.php +++ b/app/Console/Commands/LdapSync.php @@ -62,6 +62,7 @@ class LdapSync extends Command $ldap_result_phone = Setting::getSettings()->ldap_phone_field; $ldap_result_jobtitle = Setting::getSettings()->ldap_jobtitle; $ldap_result_country = Setting::getSettings()->ldap_country; + $ldap_result_location = Setting::getSettings()->ldap_location; $ldap_result_dept = Setting::getSettings()->ldap_dept; $ldap_result_manager = Setting::getSettings()->ldap_manager; $ldap_default_group = Setting::getSettings()->ldap_default_group; @@ -209,8 +210,11 @@ class LdapSync extends Command $item['country'] = $results[$i][$ldap_result_country][0] ?? ''; $item['department'] = $results[$i][$ldap_result_dept][0] ?? ''; $item['manager'] = $results[$i][$ldap_result_manager][0] ?? ''; + $item['location'] = $results[$i][$ldap_result_location][0] ?? ''; - + $location = Location::firstOrCreate([ + 'name' => $item['location'], + ]); $department = Department::firstOrCreate([ 'name' => $item['department'], ]); @@ -236,6 +240,7 @@ class LdapSync extends Command $user->jobtitle = $item['jobtitle']; $user->country = $item['country']; $user->department_id = $department->id; + $user->location_id = $location->id; if($item['manager'] != null) { // Check Cache first diff --git a/app/Http/Controllers/Accessories/AccessoriesController.php b/app/Http/Controllers/Accessories/AccessoriesController.php index 7d4e697b98..111cbb3c8b 100755 --- a/app/Http/Controllers/Accessories/AccessoriesController.php +++ b/app/Http/Controllers/Accessories/AccessoriesController.php @@ -77,7 +77,7 @@ class AccessoriesController extends Controller $accessory->manufacturer_id = request('manufacturer_id'); $accessory->model_number = request('model_number'); $accessory->purchase_date = request('purchase_date'); - $accessory->purchase_cost = Helper::ParseCurrency(request('purchase_cost')); + $accessory->purchase_cost = request('purchase_cost'); $accessory->qty = request('qty'); $accessory->user_id = Auth::user()->id; $accessory->supplier_id = request('supplier_id'); @@ -180,7 +180,7 @@ class AccessoriesController extends Controller $accessory->order_number = request('order_number'); $accessory->model_number = request('model_number'); $accessory->purchase_date = request('purchase_date'); - $accessory->purchase_cost = Helper::ParseCurrency(request('purchase_cost')); + $accessory->purchase_cost = request('purchase_cost'); $accessory->qty = request('qty'); $accessory->supplier_id = request('supplier_id'); $accessory->notes = request('notes'); diff --git a/app/Http/Controllers/Api/AssetMaintenancesController.php b/app/Http/Controllers/Api/AssetMaintenancesController.php index 7e8ecdb114..ab5635ec33 100644 --- a/app/Http/Controllers/Api/AssetMaintenancesController.php +++ b/app/Http/Controllers/Api/AssetMaintenancesController.php @@ -118,7 +118,7 @@ class AssetMaintenancesController extends Controller $assetMaintenance = new AssetMaintenance(); $assetMaintenance->supplier_id = $request->input('supplier_id'); $assetMaintenance->is_warranty = $request->input('is_warranty'); - $assetMaintenance->cost = Helper::ParseCurrency($request->input('cost')); + $assetMaintenance->cost = $request->input('cost'); $assetMaintenance->notes = e($request->input('notes')); $asset = Asset::find(e($request->input('asset_id'))); @@ -175,7 +175,7 @@ class AssetMaintenancesController extends Controller $assetMaintenance->supplier_id = e($request->input('supplier_id')); $assetMaintenance->is_warranty = e($request->input('is_warranty')); - $assetMaintenance->cost = Helper::ParseCurrency($request->input('cost')); + $assetMaintenance->cost = $request->input('cost'); $assetMaintenance->notes = e($request->input('notes')); $asset = Asset::find(request('asset_id')); diff --git a/app/Http/Controllers/Api/AssetsController.php b/app/Http/Controllers/Api/AssetsController.php index 343593a038..1816fc9f63 100644 --- a/app/Http/Controllers/Api/AssetsController.php +++ b/app/Http/Controllers/Api/AssetsController.php @@ -550,7 +550,8 @@ class AssetsController extends Controller $asset->depreciate = '0'; $asset->status_id = $request->get('status_id', 0); $asset->warranty_months = $request->get('warranty_months', null); - $asset->purchase_cost = Helper::ParseCurrency($request->get('purchase_cost')); // this is the API's store method, so I don't know that I want to do this? Confusing. FIXME (or not?!) + $asset->purchase_cost = $request->get('purchase_cost'); + $asset->asset_eol_date = $request->get('asset_eol_date', $asset->present()->eol_date()); $asset->purchase_date = $request->get('purchase_date', null); $asset->assigned_to = $request->get('assigned_to', null); $asset->supplier_id = $request->get('supplier_id'); @@ -558,6 +559,7 @@ class AssetsController extends Controller $asset->rtd_location_id = $request->get('rtd_location_id', null); $asset->location_id = $request->get('rtd_location_id', null); + /** * this is here just legacy reasons. Api\AssetController * used image_source once to allow encoded image uploads. diff --git a/app/Http/Controllers/Api/ManufacturersController.php b/app/Http/Controllers/Api/ManufacturersController.php index f3183238bf..9ff0a0c202 100644 --- a/app/Http/Controllers/Api/ManufacturersController.php +++ b/app/Http/Controllers/Api/ManufacturersController.php @@ -23,10 +23,10 @@ class ManufacturersController extends Controller public function index(Request $request) { $this->authorize('view', Manufacturer::class); - $allowed_columns = ['id', 'name', 'url', 'support_url', 'support_email', 'support_phone', 'created_at', 'updated_at', 'image', 'assets_count', 'consumables_count', 'components_count', 'licenses_count']; + $allowed_columns = ['id', 'name', 'url', 'support_url', 'support_email', 'warranty_lookup_url', 'support_phone', 'created_at', 'updated_at', 'image', 'assets_count', 'consumables_count', 'components_count', 'licenses_count']; $manufacturers = Manufacturer::select( - ['id', 'name', 'url', 'support_url', 'support_email', 'support_phone', 'created_at', 'updated_at', 'image', 'deleted_at'] + ['id', 'name', 'url', 'support_url', 'warranty_lookup_url', 'support_email', 'support_phone', 'created_at', 'updated_at', 'image', 'deleted_at'] )->withCount('assets as assets_count')->withCount('licenses as licenses_count')->withCount('consumables as consumables_count')->withCount('accessories as accessories_count'); if ($request->input('deleted') == 'true') { @@ -49,6 +49,10 @@ class ManufacturersController extends Controller $manufacturers->where('support_url', '=', $request->input('support_url')); } + if ($request->filled('warranty_lookup_url')) { + $manufacturers->where('warranty_lookup_url', '=', $request->input('warranty_lookup_url')); + } + if ($request->filled('support_phone')) { $manufacturers->where('support_phone', '=', $request->input('support_phone')); } diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index 7c63bb925c..dc444ec9e9 100644 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -71,6 +71,7 @@ class UsersController extends Controller 'users.start_date', 'users.end_date', 'users.vip', + 'users.autoassign_licenses', ])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy',) ->withCount('assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count'); @@ -187,6 +188,10 @@ class UsersController extends Controller $users->has('accessories', '=', $request->input('accessories_count')); } + if ($request->filled('autoassign_licenses')) { + $users->where('autoassign_licenses', '=', $request->input('autoassign_licenses')); + } + if ($request->filled('search')) { $users = $users->TextSearch($request->input('search')); } @@ -259,6 +264,7 @@ class UsersController extends Controller 'vip', 'start_date', 'end_date', + 'autoassign_licenses', ]; $sort = in_array($request->get('sort'), $allowed_columns) ? $request->get('sort') : 'first_name'; @@ -356,7 +362,7 @@ class UsersController extends Controller $user->permissions = $permissions_array; } - $tmp_pass = substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 20); + $tmp_pass = substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 40); $user->password = bcrypt($request->get('password', $tmp_pass)); app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar'); diff --git a/app/Http/Controllers/AssetMaintenancesController.php b/app/Http/Controllers/AssetMaintenancesController.php index 5f3221d051..dc6bc84347 100644 --- a/app/Http/Controllers/AssetMaintenancesController.php +++ b/app/Http/Controllers/AssetMaintenancesController.php @@ -101,7 +101,7 @@ class AssetMaintenancesController extends Controller $assetMaintenance = new AssetMaintenance(); $assetMaintenance->supplier_id = $request->input('supplier_id'); $assetMaintenance->is_warranty = $request->input('is_warranty'); - $assetMaintenance->cost = Helper::ParseCurrency($request->input('cost')); + $assetMaintenance->cost = $request->input('cost'); $assetMaintenance->notes = $request->input('notes'); $asset = Asset::find($request->input('asset_id')); @@ -211,7 +211,7 @@ class AssetMaintenancesController extends Controller $assetMaintenance->supplier_id = $request->input('supplier_id'); $assetMaintenance->is_warranty = $request->input('is_warranty'); - $assetMaintenance->cost = Helper::ParseCurrency($request->input('cost')); + $assetMaintenance->cost = $request->input('cost'); $assetMaintenance->notes = $request->input('notes'); $asset = Asset::find(request('asset_id')); diff --git a/app/Http/Controllers/Assets/AssetsController.php b/app/Http/Controllers/Assets/AssetsController.php index 3b2ff4623f..2a93845507 100755 --- a/app/Http/Controllers/Assets/AssetsController.php +++ b/app/Http/Controllers/Assets/AssetsController.php @@ -140,9 +140,9 @@ class AssetsController extends Controller $asset->depreciate = '0'; $asset->status_id = request('status_id'); $asset->warranty_months = request('warranty_months', null); - $asset->purchase_cost = Helper::ParseCurrency($request->get('purchase_cost')); + $asset->purchase_cost = request('purchase_cost'); $asset->purchase_date = request('purchase_date', null); - $asset->asset_eol_date = request('asset_eol_date', null); + $asset->asset_eol_date = request('asset_eol_date', $asset->present()->eol_date()); $asset->assigned_to = request('assigned_to', null); $asset->supplier_id = request('supplier_id', null); $asset->requestable = request('requestable', 0); @@ -312,7 +312,7 @@ class AssetsController extends Controller $asset->status_id = $request->input('status_id', null); $asset->warranty_months = $request->input('warranty_months', null); - $asset->purchase_cost = Helper::ParseCurrency($request->input('purchase_cost', null)); + $asset->purchase_cost = $request->input('purchase_cost', null); $asset->asset_eol_date = request('asset_eol_date', null); $asset->purchase_date = $request->input('purchase_date', null); diff --git a/app/Http/Controllers/Assets/BulkAssetsController.php b/app/Http/Controllers/Assets/BulkAssetsController.php index 8cfbec849f..80d17f1b30 100644 --- a/app/Http/Controllers/Assets/BulkAssetsController.php +++ b/app/Http/Controllers/Assets/BulkAssetsController.php @@ -29,7 +29,7 @@ class BulkAssetsController extends Controller */ public function edit(Request $request) { - $this->authorize('update', Asset::class); + $this->authorize('view', Asset::class); if (! $request->filled('ids')) { return redirect()->back()->with('error', trans('admin/hardware/message.update.no_assets_selected')); @@ -44,6 +44,7 @@ class BulkAssetsController extends Controller if ($request->filled('bulk_actions')) { switch ($request->input('bulk_actions')) { case 'labels': + $this->authorize('view', Asset::class); return view('hardware/labels') ->with('assets', Asset::find($asset_ids)) ->with('settings', Setting::getSettings()) @@ -51,6 +52,7 @@ class BulkAssetsController extends Controller ->with('count', 0); case 'delete': + $this->authorize('delete', Asset::class); $assets = Asset::with('assignedTo', 'location')->find($asset_ids); $assets->each(function ($asset) { $this->authorize('delete', $asset); @@ -58,7 +60,8 @@ class BulkAssetsController extends Controller return view('hardware/bulk-delete')->with('assets', $assets); - case 'restore': + case 'restore': + $this->authorize('update', Asset::class); $assets = Asset::withTrashed()->find($asset_ids); $assets->each(function ($asset) { $this->authorize('delete', $asset); @@ -67,6 +70,7 @@ class BulkAssetsController extends Controller return view('hardware/bulk-restore')->with('assets', $assets); case 'edit': + $this->authorize('update', Asset::class); return view('hardware/bulk') ->with('assets', $asset_ids) ->with('statuslabel_list', Helper::statusLabelList()); @@ -145,7 +149,7 @@ class BulkAssetsController extends Controller } if ($request->filled('purchase_cost')) { - $this->update_array['purchase_cost'] = Helper::ParseCurrency($request->input('purchase_cost')); + $this->update_array['purchase_cost'] = $request->input('purchase_cost'); } if ($request->filled('company_id')) { @@ -333,6 +337,7 @@ class BulkAssetsController extends Controller } public function restore(Request $request) { + $this->authorize('update', Asset::class); $assetIds = $request->get('ids'); if (empty($assetIds)) { return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.restore.nothing_updated')); diff --git a/app/Http/Controllers/Components/ComponentsController.php b/app/Http/Controllers/Components/ComponentsController.php index 2ada973612..34c9aed16b 100644 --- a/app/Http/Controllers/Components/ComponentsController.php +++ b/app/Http/Controllers/Components/ComponentsController.php @@ -78,7 +78,7 @@ class ComponentsController extends Controller $component->min_amt = $request->input('min_amt', null); $component->serial = $request->input('serial', null); $component->purchase_date = $request->input('purchase_date', null); - $component->purchase_cost = Helper::ParseCurrency($request->input('purchase_cost', null)); + $component->purchase_cost = $request->input('purchase_cost', null); $component->qty = $request->input('qty'); $component->user_id = Auth::id(); $component->notes = $request->input('notes'); @@ -153,7 +153,7 @@ class ComponentsController extends Controller $component->min_amt = $request->input('min_amt'); $component->serial = $request->input('serial'); $component->purchase_date = $request->input('purchase_date'); - $component->purchase_cost = Helper::ParseCurrency(request('purchase_cost')); + $component->purchase_cost = request('purchase_cost'); $component->qty = $request->input('qty'); $component->notes = $request->input('notes'); diff --git a/app/Http/Controllers/Consumables/ConsumablesController.php b/app/Http/Controllers/Consumables/ConsumablesController.php index 0a40c0bb3b..b33e6e07a9 100644 --- a/app/Http/Controllers/Consumables/ConsumablesController.php +++ b/app/Http/Controllers/Consumables/ConsumablesController.php @@ -77,7 +77,7 @@ class ConsumablesController extends Controller $consumable->model_number = $request->input('model_number'); $consumable->item_no = $request->input('item_no'); $consumable->purchase_date = $request->input('purchase_date'); - $consumable->purchase_cost = Helper::ParseCurrency($request->input('purchase_cost')); + $consumable->purchase_cost = $request->input('purchase_cost'); $consumable->qty = $request->input('qty'); $consumable->user_id = Auth::id(); $consumable->notes = $request->input('notes'); @@ -154,7 +154,7 @@ class ConsumablesController extends Controller $consumable->model_number = $request->input('model_number'); $consumable->item_no = $request->input('item_no'); $consumable->purchase_date = $request->input('purchase_date'); - $consumable->purchase_cost = Helper::ParseCurrency($request->input('purchase_cost')); + $consumable->purchase_cost = $request->input('purchase_cost'); $consumable->qty = Helper::ParseFloat($request->input('qty')); $consumable->notes = $request->input('notes'); diff --git a/app/Http/Controllers/Licenses/LicenseCheckinController.php b/app/Http/Controllers/Licenses/LicenseCheckinController.php index 257722b005..50e20c7985 100644 --- a/app/Http/Controllers/Licenses/LicenseCheckinController.php +++ b/app/Http/Controllers/Licenses/LicenseCheckinController.php @@ -112,4 +112,54 @@ class LicenseCheckinController extends Controller // Redirect to the license page with error return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkin.error')); } + + /** + * Bulk checkin all license seats + * + * @author [A. Gianotto] [] + * @see LicenseCheckinController::create() method that provides the form view + * @since [v6.1.1] + * @return \Illuminate\Http\RedirectResponse + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + + public function bulkCheckin(Request $request, $licenseId) { + + $license = License::findOrFail($licenseId); + $this->authorize('checkin', $license); + + $licenseSeatsByUser = LicenseSeat::where('license_id', '=', $licenseId) + ->whereNotNull('assigned_to') + ->with('user') + ->get(); + + foreach ($licenseSeatsByUser as $user_seat) { + $user_seat->assigned_to = null; + + if ($user_seat->save()) { + \Log::debug('Checking in '.$license->name.' from user '.$user_seat->username); + $user_seat->logCheckin($user_seat->user, trans('admin/licenses/general.bulk.checkin_all.log_msg')); + } + } + + $licenseSeatsByAsset = LicenseSeat::where('license_id', '=', $licenseId) + ->whereNotNull('asset_id') + ->with('asset') + ->get(); + + $count = 0; + foreach ($licenseSeatsByAsset as $asset_seat) { + $asset_seat->asset_id = null; + + if ($asset_seat->save()) { + \Log::debug('Checking in '.$license->name.' from asset '.$asset_seat->asset_tag); + $asset_seat->logCheckin($asset_seat->asset, trans('admin/licenses/general.bulk.checkin_all.log_msg')); + $count++; + } + } + + return redirect()->back()->with('success', trans_choice('admin/licenses/general.bulk.checkin_all.success', 2, ['count' => $count] )); + + } + } diff --git a/app/Http/Controllers/Licenses/LicenseCheckoutController.php b/app/Http/Controllers/Licenses/LicenseCheckoutController.php index 6f2ae003ca..a710497692 100644 --- a/app/Http/Controllers/Licenses/LicenseCheckoutController.php +++ b/app/Http/Controllers/Licenses/LicenseCheckoutController.php @@ -126,4 +126,70 @@ class LicenseCheckoutController extends Controller return false; } + + /** + * Bulk checkin all license seats + * + * @author [A. Gianotto] [] + * @see LicenseCheckinController::create() method that provides the form view + * @since [v6.1.1] + * @return \Illuminate\Http\RedirectResponse + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + + public function bulkCheckout($licenseId) { + + \Log::debug('Checking out '.$licenseId.' via bulk'); + $license = License::findOrFail($licenseId); + $this->authorize('checkin', $license); + $avail_count = $license->getAvailSeatsCountAttribute(); + + $users = User::whereNull('deleted_at')->where('autoassign_licenses', '=', 1)->with('licenses')->get(); + \Log::debug($avail_count.' will be assigned'); + + if ($users->count() > $avail_count) { + \Log::debug('You do not have enough free seats to complete this task, so we will check out as many as we can. '); + } + + // If the license is valid, check that there is an available seat + if ($license->availCount()->count() < 1) { + return redirect()->back()->with('error', trans('admin/licenses/general.bulk.checkout_all.error_no_seats')); + } + + + $assigned_count = 0; + + foreach ($users as $user) { + + // Check to make sure this user doesn't already have this license checked out to them + if ($user->licenses->where('id', '=', $licenseId)->count()) { + \Log::debug($user->username.' already has this license checked out to them. Skipping... '); + continue; + } + + $licenseSeat = $license->freeSeat(); + + // Update the seat with checkout info + $licenseSeat->assigned_to = $user->id; + + if ($licenseSeat->save()) { + $avail_count--; + $assigned_count++; + $licenseSeat->logCheckout(trans('admin/licenses/general.bulk.checkout_all.log_msg'), $user); + \Log::debug('License '.$license->name.' seat '.$licenseSeat->id.' checked out to '.$user->username); + } + + if ($avail_count == 0) { + return redirect()->back()->with('warning', trans('admin/licenses/general.bulk.checkout_all.warn_not_enough_seats', ['count' => $assigned_count])); + } + } + + if ($assigned_count == 0) { + return redirect()->back()->with('warning', trans('admin/licenses/general.bulk.checkout_all.warn_no_avail_users', ['count' => $assigned_count])); + } + + return redirect()->back()->with('success', trans_choice('admin/licenses/general.bulk.checkout_all.success', 2, ['count' => $assigned_count] )); + + + } } diff --git a/app/Http/Controllers/Licenses/LicensesController.php b/app/Http/Controllers/Licenses/LicensesController.php index a0467654ca..26cba33281 100755 --- a/app/Http/Controllers/Licenses/LicensesController.php +++ b/app/Http/Controllers/Licenses/LicensesController.php @@ -6,6 +6,8 @@ use App\Helpers\Helper; use App\Http\Controllers\Controller; use App\Models\Company; use App\Models\License; +use App\Models\LicenseSeat; +use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; @@ -86,7 +88,7 @@ class LicensesController extends Controller $license->name = $request->input('name'); $license->notes = $request->input('notes'); $license->order_number = $request->input('order_number'); - $license->purchase_cost = Helper::ParseCurrency($request->input('purchase_cost')); + $license->purchase_cost = $request->input('purchase_cost'); $license->purchase_date = $request->input('purchase_date'); $license->purchase_order = $request->input('purchase_order'); $license->purchase_order = $request->input('purchase_order'); @@ -164,7 +166,7 @@ class LicensesController extends Controller $license->name = $request->input('name'); $license->notes = $request->input('notes'); $license->order_number = $request->input('order_number'); - $license->purchase_cost = Helper::ParseCurrency($request->input('purchase_cost')); + $license->purchase_cost = $request->input('purchase_cost'); $license->purchase_date = $request->input('purchase_date'); $license->purchase_order = $request->input('purchase_order'); $license->reassignable = $request->input('reassignable', 0); @@ -233,16 +235,40 @@ class LicensesController extends Controller { $license = License::with('assignedusers')->find($licenseId); - if ($license) { - $this->authorize('view', $license); - - return view('licenses/view', compact('license')); + if (!$license) { + return redirect()->route('licenses.index') + ->with('error', trans('admin/licenses/message.does_not_exist')); } - return redirect()->route('licenses.index') - ->with('error', trans('admin/licenses/message.does_not_exist')); + $users_count = User::where('autoassign_licenses', '1')->count(); + $total_seats_count = $license->totalSeatsByLicenseID(); + $available_seats_count = $license->availCount()->count(); + $checkedout_seats_count = ($total_seats_count - $available_seats_count); + + \Log::debug('Total: '.$total_seats_count); + \Log::debug('Users: '.$users_count); + \Log::debug('Available: '.$available_seats_count); + \Log::debug('Checkedout: '.$checkedout_seats_count); + + + $this->authorize('view', $license); + return view('licenses.view', compact('license')) + ->with('users_count', $users_count) + ->with('total_seats_count', $total_seats_count) + ->with('available_seats_count', $available_seats_count) + ->with('checkedout_seats_count', $checkedout_seats_count); + } + + /** + * Returns a view with prepopulated data for clone + * + * @author [A. Gianotto] [] + * @param int $licenseId + * @return \Illuminate\Http\RedirectResponse + * @throws \Illuminate\Auth\Access\AuthorizationException + */ public function getClone($licenseId = null) { if (is_null($license_to_clone = License::find($licenseId))) { diff --git a/app/Http/Controllers/ManufacturersController.php b/app/Http/Controllers/ManufacturersController.php index 1f7d2f2c07..e98644f46f 100755 --- a/app/Http/Controllers/ManufacturersController.php +++ b/app/Http/Controllers/ManufacturersController.php @@ -68,6 +68,7 @@ class ManufacturersController extends Controller $manufacturer->user_id = Auth::id(); $manufacturer->url = $request->input('url'); $manufacturer->support_url = $request->input('support_url'); + $manufacturer->warranty_lookup_url = $request->input('warranty_lookup_url'); $manufacturer->support_phone = $request->input('support_phone'); $manufacturer->support_email = $request->input('support_email'); $manufacturer = $request->handleImages($manufacturer); @@ -123,10 +124,11 @@ class ManufacturersController extends Controller return redirect()->route('manufacturers.index')->with('error', trans('admin/manufacturers/message.does_not_exist')); } - // Save the data + // Save the data $manufacturer->name = $request->input('name'); $manufacturer->url = $request->input('url'); $manufacturer->support_url = $request->input('support_url'); + $manufacturer->warranty_lookup_url = $request->input('warranty_lookup_url'); $manufacturer->support_phone = $request->input('support_phone'); $manufacturer->support_email = $request->input('support_email'); diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php index f16a6fc8f0..c65dbc7d27 100755 --- a/app/Http/Controllers/SettingsController.php +++ b/app/Http/Controllers/SettingsController.php @@ -961,6 +961,7 @@ class SettingsController extends Controller $setting->ldap_phone_field = $request->input('ldap_phone'); $setting->ldap_jobtitle = $request->input('ldap_jobtitle'); $setting->ldap_country = $request->input('ldap_country'); + $setting->ldap_location = $request->input('ldap_location'); $setting->ldap_dept = $request->input('ldap_dept'); $setting->ldap_client_tls_cert = $request->input('ldap_client_tls_cert'); $setting->ldap_client_tls_key = $request->input('ldap_client_tls_key'); diff --git a/app/Http/Controllers/Users/BulkUsersController.php b/app/Http/Controllers/Users/BulkUsersController.php index 67ecae542e..a2d3d496da 100644 --- a/app/Http/Controllers/Users/BulkUsersController.php +++ b/app/Http/Controllers/Users/BulkUsersController.php @@ -113,7 +113,8 @@ class BulkUsersController extends Controller ->conditionallyAddItem('locale') ->conditionallyAddItem('remote') ->conditionallyAddItem('ldap_import') - ->conditionallyAddItem('activated'); + ->conditionallyAddItem('activated') + ->conditionallyAddItem('autoassign_licenses'); // If the manager_id is one of the users being updated, generate a warning. diff --git a/app/Http/Controllers/Users/UsersController.php b/app/Http/Controllers/Users/UsersController.php index 7e95484f6e..25a64e5cb2 100755 --- a/app/Http/Controllers/Users/UsersController.php +++ b/app/Http/Controllers/Users/UsersController.php @@ -120,7 +120,7 @@ class UsersController extends Controller $user->created_by = Auth::user()->id; $user->start_date = $request->input('start_date', null); $user->end_date = $request->input('end_date', null); - $user->autoassign_licenses= $request->input('autoassign_licenses', 1); + $user->autoassign_licenses = $request->input('autoassign_licenses', 0); // Strip out the superuser permission if the user isn't a superadmin $permissions_array = $request->input('permission'); @@ -210,7 +210,6 @@ class UsersController extends Controller */ public function update(SaveUserRequest $request, $id = null) { - // We need to reverse the UI specific logic for our // permissions here before we update the user. $permissions = $request->input('permissions', []); @@ -268,14 +267,15 @@ class UsersController extends Controller $user->city = $request->input('city', null); $user->state = $request->input('state', null); $user->country = $request->input('country', null); - $user->activated = $request->input('activated', 0); + // if a user is editing themselves we should always keep activated true + $user->activated = $request->input('activated', $request->user()->is($user) ? 1 : 0); $user->zip = $request->input('zip', null); $user->remote = $request->input('remote', 0); $user->vip = $request->input('vip', 0); $user->website = $request->input('website', null); $user->start_date = $request->input('start_date', null); $user->end_date = $request->input('end_date', null); - $user->autoassign_licenses = $request->input('autoassign_licenses', 1); + $user->autoassign_licenses = $request->input('autoassign_licenses', 0); // Update the location of any assets checked out to this user Asset::where('assigned_type', User::class) @@ -670,4 +670,4 @@ class UsersController extends Controller return redirect()->back()->with('error', 'User is not activated, is LDAP synced, or does not have an email address '); } -} \ No newline at end of file +} diff --git a/app/Http/Transformers/LicensesTransformer.php b/app/Http/Transformers/LicensesTransformer.php index 3c389a1b18..8df6b89f19 100644 --- a/app/Http/Transformers/LicensesTransformer.php +++ b/app/Http/Transformers/LicensesTransformer.php @@ -56,7 +56,7 @@ class LicensesTransformer 'checkin' => Gate::allows('checkin', License::class), 'clone' => Gate::allows('create', License::class), 'update' => Gate::allows('update', License::class), - 'delete' => Gate::allows('delete', License::class), + 'delete' => (Gate::allows('delete', License::class) && ($license->seats == $license->availCount()->count())) ? true : false, ]; $array += $permissions_array; diff --git a/app/Http/Transformers/ManufacturersTransformer.php b/app/Http/Transformers/ManufacturersTransformer.php index bbcbda12b4..9c84fd50fe 100644 --- a/app/Http/Transformers/ManufacturersTransformer.php +++ b/app/Http/Transformers/ManufacturersTransformer.php @@ -29,6 +29,7 @@ class ManufacturersTransformer 'url' => e($manufacturer->url), 'image' => ($manufacturer->image) ? Storage::disk('public')->url('manufacturers/'.e($manufacturer->image)) : null, 'support_url' => e($manufacturer->support_url), + 'warranty_lookup_url' => e($manufacturer->warranty_lookup_url), 'support_phone' => e($manufacturer->support_phone), 'support_email' => e($manufacturer->support_email), 'assets_count' => (int) $manufacturer->assets_count, diff --git a/app/Http/Transformers/UsersTransformer.php b/app/Http/Transformers/UsersTransformer.php index 9447d65455..867a884619 100644 --- a/app/Http/Transformers/UsersTransformer.php +++ b/app/Http/Transformers/UsersTransformer.php @@ -56,6 +56,7 @@ class UsersTransformer 'notes'=> e($user->notes), 'permissions' => $user->decodePermissions(), 'activated' => ($user->activated == '1') ? true : false, + 'autoassign_licenses' => ($user->autoassign_licenses == '1') ? true : false, 'ldap_import' => ($user->ldap_import == '1') ? true : false, 'two_factor_enrolled' => ($user->two_factor_active_and_enrolled()) ? true : false, 'two_factor_optin' => ($user->two_factor_active()) ? true : false, diff --git a/app/Importer/UserImporter.php b/app/Importer/UserImporter.php index bcbc632eb3..9f2c1c5f43 100644 --- a/app/Importer/UserImporter.php +++ b/app/Importer/UserImporter.php @@ -58,7 +58,8 @@ class UserImporter extends ItemImporter $this->item['department_id'] = $this->createOrFetchDepartment($this->findCsvMatch($row, 'department')); $this->item['manager_id'] = $this->fetchManager($this->findCsvMatch($row, 'manager_first_name'), $this->findCsvMatch($row, 'manager_last_name')); $this->item['remote'] =($this->fetchHumanBoolean($this->findCsvMatch($row, 'remote')) ==1 ) ? '1' : 0; - $this->item['vip'] =($this->fetchHumanBoolean($this->findCsvMatch($row, 'vip')) ==1 ) ? '1' : 0; + $this->item['vip'] = ($this->fetchHumanBoolean($this->findCsvMatch($row, 'vip')) ==1 ) ? '1' : 0; + $this->item['autoassign_licenses'] = ($this->fetchHumanBoolean($this->findCsvMatch($row, 'autoassign_licenses')) ==1 ) ? '1' : 0; $user_department = $this->findCsvMatch($row, 'department'); diff --git a/app/Listeners/CheckoutableListener.php b/app/Listeners/CheckoutableListener.php index 9d6cd942bd..09cb3ae8f2 100644 --- a/app/Listeners/CheckoutableListener.php +++ b/app/Listeners/CheckoutableListener.php @@ -2,22 +2,21 @@ namespace App\Listeners; +use App\Events\CheckoutableCheckedOut; use App\Models\Accessory; use App\Models\Asset; use App\Models\CheckoutAcceptance; +use App\Models\Component; use App\Models\Consumable; use App\Models\LicenseSeat; use App\Models\Recipients\AdminRecipient; use App\Models\Setting; -use App\Models\User; use App\Notifications\CheckinAccessoryNotification; use App\Notifications\CheckinAssetNotification; -use App\Notifications\CheckinLicenseNotification; use App\Notifications\CheckinLicenseSeatNotification; use App\Notifications\CheckoutAccessoryNotification; use App\Notifications\CheckoutAssetNotification; use App\Notifications\CheckoutConsumableNotification; -use App\Notifications\CheckoutLicenseNotification; use App\Notifications\CheckoutLicenseSeatNotification; use Illuminate\Support\Facades\Notification; use Exception; @@ -25,18 +24,17 @@ use Log; class CheckoutableListener { + private array $skipNotificationsFor = [ + Component::class, + ]; + /** - * Notify the user about the checked out checkoutable and add a record to the - * checkout_requests table. + * Notify the user and post to webhook about the checked out checkoutable + * and add a record to the checkout_requests table. */ public function onCheckedOut($event) { - - - /** - * When the item wasn't checked out to a user, we can't send notifications - */ - if (! $event->checkedOutTo instanceof User) { + if ($this->shouldNotSendAnyNotifications($event->checkoutable)){ return; } @@ -46,6 +44,11 @@ class CheckoutableListener $acceptance = $this->getCheckoutAcceptance($event); try { + if ($this->shouldSendWebhookNotification()) { + Notification::route('slack', Setting::getSettings()->webhook_endpoint) + ->notify($this->getCheckoutNotification($event)); + } + if (! $event->checkedOutTo->locale) { Notification::locale(Setting::getSettings()->locale)->send( $this->getNotifiables($event), @@ -63,16 +66,13 @@ class CheckoutableListener } /** - * Notify the user about the checked in checkoutable + * Notify the user and post to webhook about the checked in checkoutable */ public function onCheckedIn($event) { \Log::debug('onCheckedIn in the Checkoutable listener fired'); - /** - * When the item wasn't checked out to a user, we can't send notifications - */ - if (! $event->checkedOutTo instanceof User) { + if ($this->shouldNotSendAnyNotifications($event->checkoutable)) { return; } @@ -90,6 +90,11 @@ class CheckoutableListener } try { + if ($this->shouldSendWebhookNotification()) { + Notification::route('slack', Setting::getSettings()->webhook_endpoint) + ->notify($this->getCheckinNotification($event)); + } + // Use default locale if (! $event->checkedOutTo->locale) { Notification::locale(Setting::getSettings()->locale)->send( @@ -182,11 +187,11 @@ class CheckoutableListener /** * Get the appropriate notification for the event * - * @param CheckoutableCheckedIn $event - * @param CheckoutAcceptance $acceptance + * @param CheckoutableCheckedOut $event + * @param CheckoutAcceptance|null $acceptance * @return Notification */ - private function getCheckoutNotification($event, $acceptance) + private function getCheckoutNotification($event, $acceptance = null) { $notificationClass = null; @@ -225,4 +230,14 @@ class CheckoutableListener 'App\Listeners\CheckoutableListener@onCheckedOut' ); } + + private function shouldNotSendAnyNotifications($checkoutable): bool + { + return in_array(get_class($checkoutable), $this->skipNotificationsFor); + } + + private function shouldSendWebhookNotification(): bool + { + return Setting::getSettings() && Setting::getSettings()->webhook_endpoint; + } } diff --git a/app/Models/AssetMaintenance.php b/app/Models/AssetMaintenance.php index 41ab802579..292e529571 100644 --- a/app/Models/AssetMaintenance.php +++ b/app/Models/AssetMaintenance.php @@ -95,8 +95,8 @@ class AssetMaintenance extends Model implements ICompanyableChild */ public function setCostAttribute($value) { - $value = Helper::ParseFloat($value); - if ($value == '0.0') { + $value = Helper::ParseCurrency($value); + if ($value == 0) { $value = null; } $this->attributes['cost'] = $value; diff --git a/app/Models/Ldap.php b/app/Models/Ldap.php index a29581bf97..4eb496a2ab 100644 --- a/app/Models/Ldap.php +++ b/app/Models/Ldap.php @@ -213,6 +213,7 @@ class Ldap extends Model $ldap_result_phone = Setting::getSettings()->ldap_phone; $ldap_result_jobtitle = Setting::getSettings()->ldap_jobtitle; $ldap_result_country = Setting::getSettings()->ldap_country; + $ldap_result_location = Setting::getSettings()->ldap_location; $ldap_result_dept = Setting::getSettings()->ldap_dept; $ldap_result_manager = Setting::getSettings()->ldap_manager; // Get LDAP user data @@ -227,6 +228,7 @@ class Ldap extends Model $item['country'] = $ldapattributes[$ldap_result_country][0] ?? ''; $item['department'] = $ldapattributes[$ldap_result_dept][0] ?? ''; $item['manager'] = $ldapattributes[$ldap_result_manager][0] ?? ''; + $item['location'] = $ldapattributes[$ldap_result_location][0] ?? ''; return $item; } diff --git a/app/Models/License.php b/app/Models/License.php index ff69d5f66d..162b3d662a 100755 --- a/app/Models/License.php +++ b/app/Models/License.php @@ -106,10 +106,10 @@ class License extends Depreciable * @var array */ protected $searchableRelations = [ - 'manufacturer' => ['name'], - 'company' => ['name'], - 'category' => ['name'], - 'depreciation' => ['name'], + 'manufacturer' => ['name'], + 'company' => ['name'], + 'category' => ['name'], + 'depreciation' => ['name'], ]; /** @@ -425,7 +425,7 @@ class License extends Depreciable public static function assetcount() { return LicenseSeat::whereNull('deleted_at') - ->count(); + ->count(); } @@ -441,8 +441,8 @@ class License extends Depreciable public function totalSeatsByLicenseID() { return LicenseSeat::where('license_id', '=', $this->id) - ->whereNull('deleted_at') - ->count(); + ->whereNull('deleted_at') + ->count(); } /** @@ -486,11 +486,12 @@ class License extends Depreciable public static function availassetcount() { return LicenseSeat::whereNull('assigned_to') - ->whereNull('asset_id') - ->whereNull('deleted_at') - ->count(); + ->whereNull('asset_id') + ->whereNull('deleted_at') + ->count(); } + /** * Returns the number of total available seats for this license * @@ -533,7 +534,7 @@ class License extends Depreciable { return $this->licenseSeatsRelation()->where(function ($query) { $query->whereNotNull('assigned_to') - ->orWhereNotNull('asset_id'); + ->orWhereNotNull('asset_id'); }); } @@ -621,13 +622,13 @@ class License extends Depreciable public function freeSeat() { return $this->licenseseats() - ->whereNull('deleted_at') - ->where(function ($query) { - $query->whereNull('assigned_to') - ->whereNull('asset_id'); - }) - ->orderBy('id', 'asc') - ->first(); + ->whereNull('deleted_at') + ->where(function ($query) { + $query->whereNull('assigned_to') + ->whereNull('asset_id'); + }) + ->orderBy('id', 'asc') + ->first(); } @@ -657,11 +658,11 @@ class License extends Depreciable $days = (is_null($days)) ? 60 : $days; return self::whereNotNull('expiration_date') - ->whereNull('deleted_at') - ->whereRaw(DB::raw('DATE_SUB(`expiration_date`,INTERVAL '.$days.' DAY) <= DATE(NOW()) ')) - ->where('expiration_date', '>', date('Y-m-d')) - ->orderBy('expiration_date', 'ASC') - ->get(); + ->whereNull('deleted_at') + ->whereRaw(DB::raw('DATE_SUB(`expiration_date`,INTERVAL '.$days.' DAY) <= DATE(NOW()) ')) + ->where('expiration_date', '>', date('Y-m-d')) + ->orderBy('expiration_date', 'ASC') + ->get(); } /** @@ -705,4 +706,4 @@ class License extends Depreciable return $query->leftJoin('companies as companies', 'licenses.company_id', '=', 'companies.id')->select('licenses.*') ->orderBy('companies.name', $order); } -} +} \ No newline at end of file diff --git a/app/Models/LicenseSeat.php b/app/Models/LicenseSeat.php index 2207edd02c..d2a99d3c56 100755 --- a/app/Models/LicenseSeat.php +++ b/app/Models/LicenseSeat.php @@ -6,13 +6,15 @@ use App\Models\Traits\Acceptable; use App\Notifications\CheckinLicenseNotification; use App\Notifications\CheckoutLicenseNotification; use App\Presenters\Presentable; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; class LicenseSeat extends SnipeModel implements ICompanyableChild { use CompanyableChildTrait; - use SoftDeletes; + use HasFactory; use Loggable; + use SoftDeletes; protected $presenter = \App\Presenters\LicenseSeatPresenter::class; use Presentable; diff --git a/app/Models/Manufacturer.php b/app/Models/Manufacturer.php index 5f01c3c273..eb91b3b8f2 100755 --- a/app/Models/Manufacturer.php +++ b/app/Models/Manufacturer.php @@ -23,8 +23,9 @@ class Manufacturer extends SnipeModel protected $rules = [ 'name' => 'required|min:2|max:255|unique:manufacturers,name,NULL,id,deleted_at,NULL', 'url' => 'url|nullable', - 'support_url' => 'url|nullable', 'support_email' => 'email|nullable', + 'support_url' => 'nullable|url', + 'warranty_lookup_url' => 'starts_with:http://,https://,afp://,facetime://,file://,irc://','nullable' ]; protected $hidden = ['user_id']; @@ -51,6 +52,7 @@ class Manufacturer extends SnipeModel 'support_phone', 'support_url', 'url', + 'warranty_lookup_url', ]; use Searchable; diff --git a/app/Models/Setting.php b/app/Models/Setting.php index ecac183356..61be790e00 100755 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -341,7 +341,15 @@ class Setting extends Model 'ad_domain', 'ad_append_domain', 'ldap_client_tls_key', - 'ldap_client_tls_cert' + 'ldap_client_tls_cert', + 'ldap_default_group', + 'ldap_dept', + 'ldap_emp_num', + 'ldap_phone_field', + 'ldap_jobtitle', + 'ldap_manager', + 'ldap_country', + 'ldap_location', ])->first()->getAttributes(); return collect($ldapSettings); diff --git a/app/Models/SnipeModel.php b/app/Models/SnipeModel.php index e5a039a4e1..af12c3d29b 100644 --- a/app/Models/SnipeModel.php +++ b/app/Models/SnipeModel.php @@ -21,9 +21,9 @@ class SnipeModel extends Model */ public function setPurchaseCostAttribute($value) { - $value = Helper::ParseFloat($value); + $value = Helper::ParseCurrency($value); - if ($value == '0.0') { + if ($value == 0) { $value = null; } $this->attributes['purchase_cost'] = $value; diff --git a/app/Models/SnipeSCIMConfig.php b/app/Models/SnipeSCIMConfig.php index 77cbf01c1a..7ec25645e6 100644 --- a/app/Models/SnipeSCIMConfig.php +++ b/app/Models/SnipeSCIMConfig.php @@ -70,7 +70,11 @@ class SnipeSCIMConfig extends \ArieTimmerman\Laravel\SCIMServer\SCIMConfig // Map a SCIM attribute to an attribute of the object. 'mapping' => [ - 'id' => AttributeMapping::eloquent("id")->disableWrite(), + 'id' => (new AttributeMapping())->setRead( + function (&$object) { + return (string)$object->id; + } + )->disableWrite(), 'externalId' => AttributeMapping::eloquent('scim_externalid'), // FIXME - I have a PR that changes a lot of this. @@ -174,7 +178,6 @@ class SnipeSCIMConfig extends \ArieTimmerman\Laravel\SCIMServer\SCIMConfig '$ref' => null, 'display' => null, 'type' => null, - 'type' => null ]], 'entitlements' => null, diff --git a/app/Models/Supplier.php b/app/Models/Supplier.php index d60d3c42fe..fa00fbad2a 100755 --- a/app/Models/Supplier.php +++ b/app/Models/Supplier.php @@ -78,24 +78,7 @@ class Supplier extends SnipeModel { return $this->hasMany(Asset::class)->whereNull('deleted_at')->selectRaw('supplier_id, count(*) as count')->groupBy('supplier_id'); } - - /** - * Sets the license seat count attribute - * - * @todo I don't see the licenseSeatsRelation here? - * - * @author A. Gianotto - * @since [v1.0] - * @return \Illuminate\Database\Eloquent\Relations\Relation - */ - public function getLicenseSeatsCountAttribute() - { - if ($this->licenseSeatsRelation->first()) { - return $this->licenseSeatsRelation->first()->count; - } - - return 0; - } + /** * Establishes the supplier -> assets relationship diff --git a/app/Models/User.php b/app/Models/User.php index df97800917..36e1c8ac47 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -65,6 +65,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo 'avatar', 'gravatar', 'vip', + 'autoassign_licenses', ]; protected $casts = [ @@ -76,6 +77,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime', + 'autoassign_licenses' => 'boolean', ]; /** @@ -95,6 +97,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo 'location_id' => 'exists:locations,id|nullable', 'start_date' => 'nullable|date_format:Y-m-d', 'end_date' => 'nullable|date_format:Y-m-d|after_or_equal:start_date', + 'autoassign_licenses' => 'boolean', ]; /** @@ -256,20 +259,6 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo return $this->last_name.', '.$this->first_name.' ('.$this->username.')'; } - /** - * The url for slack notifications. - * Used by Notifiable trait. - * @return mixed - */ - public function routeNotificationForSlack() - { - // At this point the endpoint is the same for everything. - // In the future this may want to be adapted for individual notifications. - $this->endpoint = \App\Models\Setting::getSettings()->webhook_endpoint; - - return $this->endpoint; - } - /** * Establishes the user -> assets relationship diff --git a/app/Presenters/AssetPresenter.php b/app/Presenters/AssetPresenter.php index 4be0f56010..ce476d082c 100644 --- a/app/Presenters/AssetPresenter.php +++ b/app/Presenters/AssetPresenter.php @@ -534,6 +534,18 @@ class AssetPresenter extends Presenter return false; } + /** + * Used to take user created warranty URL and dynamically fill in the needed values per asset + * @return string + */ + public function dynamicWarrantyUrl() + { + $warranty_lookup_url = $this->model->model->manufacturer->warranty_lookup_url; + $url = (str_replace('{LOCALE}',\App\Models\Setting::getSettings()->locale,$warranty_lookup_url)); + $url = (str_replace('{SERIAL}',$this->model->serial,$url)); + return $url; + } + /** * Url to view this item. * @return string diff --git a/app/Presenters/LicensePresenter.php b/app/Presenters/LicensePresenter.php index 1e8784f2fc..5a44bf52ab 100644 --- a/app/Presenters/LicensePresenter.php +++ b/app/Presenters/LicensePresenter.php @@ -33,7 +33,7 @@ class LicensePresenter extends Presenter 'field' => 'name', 'searchable' => true, 'sortable' => true, - 'title' => trans('admin/licenses/table.title'), + 'title' => trans('general.name'), 'formatter' => 'licensesLinkFormatter', ], [ 'field' => 'product_key', diff --git a/app/Presenters/ManufacturerPresenter.php b/app/Presenters/ManufacturerPresenter.php index 5b44a76933..f5c15f1fe5 100644 --- a/app/Presenters/ManufacturerPresenter.php +++ b/app/Presenters/ManufacturerPresenter.php @@ -47,7 +47,7 @@ class ManufacturerPresenter extends Presenter 'switchable' => true, 'title' => trans('admin/manufacturers/table.url'), 'visible' => true, - 'formatter' => 'linkFormatter', + 'formatter' => 'externalLinkFormatter', ], [ 'field' => 'support_url', @@ -56,7 +56,7 @@ class ManufacturerPresenter extends Presenter 'switchable' => true, 'title' => trans('admin/manufacturers/table.support_url'), 'visible' => true, - 'formatter' => 'linkFormatter', + 'formatter' => 'externalLinkFormatter', ], [ @@ -78,6 +78,15 @@ class ManufacturerPresenter extends Presenter 'visible' => true, 'formatter' => 'emailFormatter', ], + [ + 'field' => 'warranty_lookup_url', + 'searchable' => true, + 'sortable' => true, + 'switchable' => true, + 'title' => trans('admin/manufacturers/table.warranty_lookup_url'), + 'visible' => false, + 'formatter' => 'externalLinkFormatter', + ], [ 'field' => 'assets_count', diff --git a/app/Presenters/UserPresenter.php b/app/Presenters/UserPresenter.php index b5eefdf811..fbb41a4d51 100644 --- a/app/Presenters/UserPresenter.php +++ b/app/Presenters/UserPresenter.php @@ -294,6 +294,15 @@ class UserPresenter extends Presenter 'visible' => true, 'formatter' => 'trueFalseFormatter', ], + [ + 'field' => 'autoassign_licenses', + 'searchable' => false, + 'sortable' => true, + 'switchable' => true, + 'title' => trans('general.autoassign_licenses'), + 'visible' => false, + 'formatter' => 'trueFalseFormatter', + ], [ 'field' => 'created_by', 'searchable' => false, diff --git a/config/session.php b/config/session.php index dee8d9103e..688340c9e2 100644 --- a/config/session.php +++ b/config/session.php @@ -130,7 +130,7 @@ return [ | */ - 'path' => '/', + 'path' => env('SESSION_COOKIE_PATH', '/'), /* |-------------------------------------------------------------------------- diff --git a/database/factories/LicenseSeatFactory.php b/database/factories/LicenseSeatFactory.php new file mode 100644 index 0000000000..3c6cc4246b --- /dev/null +++ b/database/factories/LicenseSeatFactory.php @@ -0,0 +1,16 @@ + License::factory(), + ]; + } +} diff --git a/database/factories/ManufacturerFactory.php b/database/factories/ManufacturerFactory.php index ab22262a78..4e736b8d8a 100644 --- a/database/factories/ManufacturerFactory.php +++ b/database/factories/ManufacturerFactory.php @@ -38,6 +38,7 @@ class ManufacturerFactory extends Factory 'name' => 'Apple', 'url' => 'https://apple.com', 'support_url' => 'https://support.apple.com', + 'warranty_lookup_url' => 'https://checkcoverage.apple.com', 'image' => 'apple.jpg', ]; }); @@ -50,6 +51,7 @@ class ManufacturerFactory extends Factory 'name' => 'Microsoft', 'url' => 'https://microsoft.com', 'support_url' => 'https://support.microsoft.com', + 'warranty_lookup_url' => 'https://account.microsoft.com/devices', 'image' => 'microsoft.png', ]; }); @@ -62,6 +64,7 @@ class ManufacturerFactory extends Factory 'name' => 'Dell', 'url' => 'https://dell.com', 'support_url' => 'https://support.dell.com', + 'warranty_lookup_url' => 'https://www.dell.com/support/home/en-us/Products/?app=warranty', 'image' => 'dell.png', ]; }); diff --git a/database/migrations/2023_04_25_181817_adds_ldap_location_to_settings_table.php b/database/migrations/2023_04_25_181817_adds_ldap_location_to_settings_table.php new file mode 100644 index 0000000000..60c0e31a67 --- /dev/null +++ b/database/migrations/2023_04_25_181817_adds_ldap_location_to_settings_table.php @@ -0,0 +1,32 @@ +string('ldap_location')->after('ldap_country')->nullable()->default(null); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('settings', function (Blueprint $table) { + $table->dropColumn('ldap_location'); + }); + } +} diff --git a/database/migrations/2023_04_26_160235_add_warranty_url_to_manufacturers.php b/database/migrations/2023_04_26_160235_add_warranty_url_to_manufacturers.php new file mode 100644 index 0000000000..52a655f2e4 --- /dev/null +++ b/database/migrations/2023_04_26_160235_add_warranty_url_to_manufacturers.php @@ -0,0 +1,32 @@ +string('warranty_lookup_url')->after('support_url')->nullable()->default(null); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('manufacturers', function (Blueprint $table) { + $table->dropColumn('warranty_lookup_url'); + }); + } +} diff --git a/package-lock.json b/package-lock.json index dd058375d0..cd722a04b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1329,9 +1329,9 @@ "dev": true }, "@fortawesome/fontawesome-free": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.3.0.tgz", - "integrity": "sha512-qVtd5i1Cc7cdrqnTWqTObKQHjPWAiRwjUPaXObaeNPcy7+WKxJumGBx66rfSFgK6LNpIasVKkEgW8oyf0tmPLA==" + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.0.tgz", + "integrity": "sha512-0NyytTlPJwB/BF5LtRV8rrABDbe3TdTXqNB3PdZ+UUUZAEIrdOJdmABqKjt4AXwIoJNaRVVZEXxpNrqvE1GAYQ==" }, "@jridgewell/gen-mapping": { "version": "0.1.1", @@ -3154,9 +3154,9 @@ "integrity": "sha1-EQPWvADPv6jPyaJZmrUYxVZD2j8=" }, "bootstrap-table": { - "version": "1.21.3", - "resolved": "https://registry.npmjs.org/bootstrap-table/-/bootstrap-table-1.21.3.tgz", - "integrity": "sha512-y6PLHxJJVqIVXoMWrnwPsA8dKqvy9An8Iz7WuuimuLU1i0jIT9+Xzg6NXqBBilHOwp0dUAw9vfgNLvCVq2wdJQ==" + "version": "1.21.4", + "resolved": "https://registry.npmjs.org/bootstrap-table/-/bootstrap-table-1.21.4.tgz", + "integrity": "sha512-Vp0kwkCsZzBINiA1KJ46LSkIa4oA0r6GSuzERZEqkUU8/0JDij2aG4CdYxiOm/UFCPZ9+KuAE4zSjxJLxg5jWw==" }, "brace-expansion": { "version": "1.1.11", diff --git a/package.json b/package.json index 6c1df03834..5d8cfd622a 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "vue-template-compiler": "2.4.4" }, "dependencies": { - "@fortawesome/fontawesome-free": "^6.3.0", + "@fortawesome/fontawesome-free": "^6.4.0", "acorn": "^8.8.2", "acorn-import-assertions": "^1.8.0", "admin-lte": "^2.4.18", @@ -34,7 +34,7 @@ "bootstrap-colorpicker": "^2.5.3", "bootstrap-datepicker": "^1.9.0", "bootstrap-less": "^3.3.8", - "bootstrap-table": "1.21.3", + "bootstrap-table": "1.21.4", "chart.js": "^2.9.4", "css-loader": "^4.0.0", "ekko-lightbox": "^5.1.1", @@ -54,6 +54,6 @@ "tableexport.jquery.plugin": "1.27.0", "tether": "^1.4.0", "vue-resource": "^1.5.2", - "webpack": "^5.76.2" + "webpack": "^5.77.0" } } diff --git a/public/css/dist/all.css b/public/css/dist/all.css index e0ffcf96d0..d5383def4c 100644 --- a/public/css/dist/all.css +++ b/public/css/dist/all.css @@ -6833,7 +6833,7 @@ button.close { } /*# sourceMappingURL=bootstrap.css.map */ /*! - * Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com + * Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) * Copyright 2023 Fonticons, Inc. */ @@ -22385,7 +22385,7 @@ a.ui-button:active, /*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImVra28tbGlnaHRib3guY3NzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGVBQ0UsOEJBQXlCLEFBQXpCLHVCQUF5QixBQUN6QixzQkFBb0IsQUFBcEIsbUJBQW9CLEFBQ3BCLHFCQUF3QixBQUF4Qix1QkFBd0IsQUFDeEIseUJBQTZCLENBQzlCLEFBQ0QseUJBQ0UsaUJBQW1CLENBQ3BCLEFBQ0QsZ0RBQ0Usa0JBQW1CLEFBQ25CLE1BQU8sQUFDUCxPQUFRLEFBQ1IsU0FBVSxBQUNWLFFBQVMsQUFDVCxVQUFZLENBQ2IsQUFDRCxzQkFDRSxXQUFZLEFBQ1osV0FBYSxDQUNkLEFBQ0QsMkJBQ0UsVUFBYSxBQUNiLGtCQUFtQixBQUNuQixNQUFPLEFBQ1AsT0FBUSxBQUNSLFdBQVksQUFDWixZQUFhLEFBQ2Isb0JBQWMsQUFBZCxZQUFjLENBQ2YsQUFDRCw2QkFDRSxXQUFRLEFBQVIsT0FBUSxBQUNSLG9CQUFjLEFBQWQsYUFBYyxBQUNkLHNCQUFvQixBQUFwQixtQkFBb0IsQUFDcEIsVUFBVyxBQUNYLHVCQUF5QixBQUN6QixXQUFZLEFBQ1osZUFBZ0IsQUFDaEIsU0FBYSxDQUNkLEFBQ0QsK0JBQ0Usb0JBQWEsQUFBYixXQUFhLENBQ2QsQUFDRCxvQ0FDRSxZQUFjLENBQ2YsQUFDRCxrQ0FDRSxjQUFnQixDQUNqQixBQUNELDZDQUNFLGdCQUFrQixDQUNuQixBQUNELG1DQUNFLG9CQUFzQixDQUN2QixBQUNELG1DQUNFLFlBQWMsQ0FDZixBQUNELHNDQUNFLGVBQWdCLEFBQ2hCLGlCQUFtQixDQUNwQixBQUNELHVCQUNFLFVBQVcsQUFDWCxvQkFBc0IsQ0FDdkIsQUFDRCw2QkFDRSxZQUFjLENBQ2YsQUFDRCw2QkFDRSxlQUFpQixDQUNsQixBQUNELHNCQUNFLGtCQUFtQixBQUNuQixNQUFPLEFBQ1AsT0FBUSxBQUNSLFNBQVUsQUFDVixRQUFTLEFBQ1QsV0FBWSxBQUNaLG9CQUFjLEFBQWQsYUFBYyxBQUVkLDBCQUF1QixBQUF2QixzQkFBdUIsQUFFdkIscUJBQXdCLEFBQXhCLHVCQUF3QixBQUV4QixzQkFBb0IsQUFBcEIsa0JBQW9CLENBQ3JCLEFBQ0QsMEJBQ0UsV0FBWSxBQUNaLFlBQWEsQUFDYixrQkFBbUIsQUFDbkIsaUJBQW1CLENBQ3BCLEFBQ0QsOEJBQ0UsV0FBWSxBQUNaLFlBQWEsQUFDYixrQkFBbUIsQUFDbkIsc0JBQXVCLEFBQ3ZCLFdBQWEsQUFDYixrQkFBbUIsQUFDbkIsTUFBTyxBQUNQLE9BQVEsQUFDUixtQ0FBNkMsQ0FDOUMsQUFDRCx5Q0FDRSxtQkFBcUIsQ0FDdEIsQUFDRCw0Q0FDRSxxQkFBdUIsQ0FDeEIsQUFVRCxhQUNFLE1BRUUsbUJBQW9CLEFBQ3BCLDBCQUE0QixDQUM3QixBQUNELElBQ0UsbUJBQW9CLEFBQ3BCLDBCQUE0QixDQUM3QixDQUNGIiwiZmlsZSI6ImVra28tbGlnaHRib3guY3NzIiwic291cmNlc0NvbnRlbnQiOlsiLmVra28tbGlnaHRib3gge1xuICBkaXNwbGF5OiBmbGV4ICFpbXBvcnRhbnQ7XG4gIGFsaWduLWl0ZW1zOiBjZW50ZXI7XG4gIGp1c3RpZnktY29udGVudDogY2VudGVyO1xuICBwYWRkaW5nLXJpZ2h0OiAwcHghaW1wb3J0YW50O1xufVxuLmVra28tbGlnaHRib3gtY29udGFpbmVyIHtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xufVxuLmVra28tbGlnaHRib3gtY29udGFpbmVyID4gZGl2LmVra28tbGlnaHRib3gtaXRlbSB7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgdG9wOiAwO1xuICBsZWZ0OiAwO1xuICBib3R0b206IDA7XG4gIHJpZ2h0OiAwO1xuICB3aWR0aDogMTAwJTtcbn1cbi5la2tvLWxpZ2h0Ym94IGlmcmFtZSB7XG4gIHdpZHRoOiAxMDAlO1xuICBoZWlnaHQ6IDEwMCU7XG59XG4uZWtrby1saWdodGJveC1uYXYtb3ZlcmxheSB7XG4gIHotaW5kZXg6IDEwMDtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICB0b3A6IDA7XG4gIGxlZnQ6IDA7XG4gIHdpZHRoOiAxMDAlO1xuICBoZWlnaHQ6IDEwMCU7XG4gIGRpc3BsYXk6IGZsZXg7XG59XG4uZWtrby1saWdodGJveC1uYXYtb3ZlcmxheSBhIHtcbiAgZmxleDogMTtcbiAgZGlzcGxheTogZmxleDtcbiAgYWxpZ24taXRlbXM6IGNlbnRlcjtcbiAgb3BhY2l0eTogMDtcbiAgdHJhbnNpdGlvbjogb3BhY2l0eSAwLjVzO1xuICBjb2xvcjogI2ZmZjtcbiAgZm9udC1zaXplOiAzMHB4O1xuICB6LWluZGV4OiAxMDA7XG59XG4uZWtrby1saWdodGJveC1uYXYtb3ZlcmxheSBhID4gKiB7XG4gIGZsZXgtZ3JvdzogMTtcbn1cbi5la2tvLWxpZ2h0Ym94LW5hdi1vdmVybGF5IGEgPiAqOmZvY3VzIHtcbiAgb3V0bGluZTogbm9uZTtcbn1cbi5la2tvLWxpZ2h0Ym94LW5hdi1vdmVybGF5IGEgc3BhbiB7XG4gIHBhZGRpbmc6IDAgMzBweDtcbn1cbi5la2tvLWxpZ2h0Ym94LW5hdi1vdmVybGF5IGE6bGFzdC1jaGlsZCBzcGFuIHtcbiAgdGV4dC1hbGlnbjogcmlnaHQ7XG59XG4uZWtrby1saWdodGJveC1uYXYtb3ZlcmxheSBhOmhvdmVyIHtcbiAgdGV4dC1kZWNvcmF0aW9uOiBub25lO1xufVxuLmVra28tbGlnaHRib3gtbmF2LW92ZXJsYXkgYTpmb2N1cyB7XG4gIG91dGxpbmU6IG5vbmU7XG59XG4uZWtrby1saWdodGJveC1uYXYtb3ZlcmxheSBhLmRpc2FibGVkIHtcbiAgY3Vyc29yOiBkZWZhdWx0O1xuICB2aXNpYmlsaXR5OiBoaWRkZW47XG59XG4uZWtrby1saWdodGJveCBhOmhvdmVyIHtcbiAgb3BhY2l0eTogMTtcbiAgdGV4dC1kZWNvcmF0aW9uOiBub25lO1xufVxuLmVra28tbGlnaHRib3ggLm1vZGFsLWRpYWxvZyB7XG4gIGRpc3BsYXk6IG5vbmU7XG59XG4uZWtrby1saWdodGJveCAubW9kYWwtZm9vdGVyIHtcbiAgdGV4dC1hbGlnbjogbGVmdDtcbn1cbi5la2tvLWxpZ2h0Ym94LWxvYWRlciB7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgdG9wOiAwO1xuICBsZWZ0OiAwO1xuICBib3R0b206IDA7XG4gIHJpZ2h0OiAwO1xuICB3aWR0aDogMTAwJTtcbiAgZGlzcGxheTogZmxleDtcbiAgLyogZXN0YWJsaXNoIGZsZXggY29udGFpbmVyICovXG4gIGZsZXgtZGlyZWN0aW9uOiBjb2x1bW47XG4gIC8qIG1ha2UgbWFpbiBheGlzIHZlcnRpY2FsICovXG4gIGp1c3RpZnktY29udGVudDogY2VudGVyO1xuICAvKiBjZW50ZXIgaXRlbXMgdmVydGljYWxseSwgaW4gdGhpcyBjYXNlICovXG4gIGFsaWduLWl0ZW1zOiBjZW50ZXI7XG59XG4uZWtrby1saWdodGJveC1sb2FkZXIgPiBkaXYge1xuICB3aWR0aDogNDBweDtcbiAgaGVpZ2h0OiA0MHB4O1xuICBwb3NpdGlvbjogcmVsYXRpdmU7XG4gIHRleHQtYWxpZ246IGNlbnRlcjtcbn1cbi5la2tvLWxpZ2h0Ym94LWxvYWRlciA+IGRpdiA+IGRpdiB7XG4gIHdpZHRoOiAxMDAlO1xuICBoZWlnaHQ6IDEwMCU7XG4gIGJvcmRlci1yYWRpdXM6IDUwJTtcbiAgYmFja2dyb3VuZC1jb2xvcjogI2ZmZjtcbiAgb3BhY2l0eTogMC42O1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIHRvcDogMDtcbiAgbGVmdDogMDtcbiAgYW5pbWF0aW9uOiBzay1ib3VuY2UgMnMgaW5maW5pdGUgZWFzZS1pbi1vdXQ7XG59XG4uZWtrby1saWdodGJveC1sb2FkZXIgPiBkaXYgPiBkaXY6bGFzdC1jaGlsZCB7XG4gIGFuaW1hdGlvbi1kZWxheTogLTFzO1xufVxuLm1vZGFsLWRpYWxvZyAuZWtrby1saWdodGJveC1sb2FkZXIgPiBkaXYgPiBkaXYge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiAjMzMzO1xufVxuQC13ZWJraXQta2V5ZnJhbWVzIHNrLWJvdW5jZSB7XG4gIDAlLFxuICAxMDAlIHtcbiAgICAtd2Via2l0LXRyYW5zZm9ybTogc2NhbGUoMCk7XG4gIH1cbiAgNTAlIHtcbiAgICAtd2Via2l0LXRyYW5zZm9ybTogc2NhbGUoMSk7XG4gIH1cbn1cbkBrZXlmcmFtZXMgc2stYm91bmNlIHtcbiAgMCUsXG4gIDEwMCUge1xuICAgIHRyYW5zZm9ybTogc2NhbGUoMCk7XG4gICAgLXdlYmtpdC10cmFuc2Zvcm06IHNjYWxlKDApO1xuICB9XG4gIDUwJSB7XG4gICAgdHJhbnNmb3JtOiBzY2FsZSgxKTtcbiAgICAtd2Via2l0LXRyYW5zZm9ybTogc2NhbGUoMSk7XG4gIH1cbn1cbiJdfQ== */ /** * @author zhixin wen - * version: 1.21.3 + * version: 1.21.4 * https://github.com/wenzhixin/bootstrap-table/ */ /* stylelint-disable annotation-no-unknown, max-line-length */ diff --git a/public/css/dist/bootstrap-table.css b/public/css/dist/bootstrap-table.css index 4cc91443e4..8d409af9cb 100644 --- a/public/css/dist/bootstrap-table.css +++ b/public/css/dist/bootstrap-table.css @@ -1,6 +1,6 @@ /** * @author zhixin wen - * version: 1.21.3 + * version: 1.21.4 * https://github.com/wenzhixin/bootstrap-table/ */ /* stylelint-disable annotation-no-unknown, max-line-length */ diff --git a/public/css/webfonts/fa-brands-400.ttf b/public/css/webfonts/fa-brands-400.ttf index 641a489339..774d51ac4b 100644 Binary files a/public/css/webfonts/fa-brands-400.ttf and b/public/css/webfonts/fa-brands-400.ttf differ diff --git a/public/css/webfonts/fa-brands-400.woff2 b/public/css/webfonts/fa-brands-400.woff2 index 5929101297..71e3185268 100644 Binary files a/public/css/webfonts/fa-brands-400.woff2 and b/public/css/webfonts/fa-brands-400.woff2 differ diff --git a/public/css/webfonts/fa-regular-400.ttf b/public/css/webfonts/fa-regular-400.ttf index 7d634a2ba0..8a9d6344d1 100644 Binary files a/public/css/webfonts/fa-regular-400.ttf and b/public/css/webfonts/fa-regular-400.ttf differ diff --git a/public/css/webfonts/fa-regular-400.woff2 b/public/css/webfonts/fa-regular-400.woff2 index 953d5540b0..7f021680b9 100644 Binary files a/public/css/webfonts/fa-regular-400.woff2 and b/public/css/webfonts/fa-regular-400.woff2 differ diff --git a/public/css/webfonts/fa-solid-900.ttf b/public/css/webfonts/fa-solid-900.ttf index b3a2b64103..993dbe1f95 100644 Binary files a/public/css/webfonts/fa-solid-900.ttf and b/public/css/webfonts/fa-solid-900.ttf differ diff --git a/public/css/webfonts/fa-solid-900.woff2 b/public/css/webfonts/fa-solid-900.woff2 index 83433f4455..5c16cd3e8a 100644 Binary files a/public/css/webfonts/fa-solid-900.woff2 and b/public/css/webfonts/fa-solid-900.woff2 differ diff --git a/public/css/webfonts/fa-v4compatibility.ttf b/public/css/webfonts/fa-v4compatibility.ttf index e4eea68d0f..ab6ae22482 100644 Binary files a/public/css/webfonts/fa-v4compatibility.ttf and b/public/css/webfonts/fa-v4compatibility.ttf differ diff --git a/public/css/webfonts/fa-v4compatibility.woff2 b/public/css/webfonts/fa-v4compatibility.woff2 index e804f18603..9027e38bcd 100644 Binary files a/public/css/webfonts/fa-v4compatibility.woff2 and b/public/css/webfonts/fa-v4compatibility.woff2 differ diff --git a/public/js/dist/bootstrap-table.js b/public/js/dist/bootstrap-table.js index 0ded615dc6..b1f75983bb 100644 --- a/public/js/dist/bootstrap-table.js +++ b/public/js/dist/bootstrap-table.js @@ -4882,7 +4882,7 @@ } }; - var VERSION = '1.21.3'; + var VERSION = '1.21.4'; var bootstrapVersion = Utils.getBootstrapVersion(); var CONSTANTS = { 3: { @@ -5063,6 +5063,7 @@ paginationUseIntermediate: false, // Calculate intermediate pages for quick access search: false, + searchable: false, searchHighlight: false, searchOnEnterKey: false, strictSearch: false, @@ -6229,7 +6230,7 @@ } } var handleInputEvent = function handleInputEvent($searchInput) { - var eventTriggers = 'keyup drop blur mouseup'; + var eventTriggers = $searchInput.is('select') ? 'change' : 'keyup drop blur mouseup'; $searchInput.off(eventTriggers).on(eventTriggers, function (event) { if (opts.searchOnEnterKey && event.keyCode !== 13) { return; @@ -7126,7 +7127,7 @@ } } } - if (this.options.search && this.options.sidePagination === 'server' && this.columns.filter(function (column) { + if (this.options.search && this.options.sidePagination === 'server' && this.options.searchable && this.columns.filter(function (column) { return column.searchable; }).length) { params.searchable = []; @@ -7560,15 +7561,15 @@ !Utils.isEmptyObject(this.filterColumns) || typeof this.options.filterOptions.filterAlgorithm === 'function' || !Utils.isEmptyObject(this.filterColumnsPartial)) && (!params || !params.unfiltered)) { data = this.data; } - if (params && params.useCurrentPage) { - data = data.slice(this.pageFrom - 1, this.pageTo); - } if (params && !params.includeHiddenRows) { var hiddenRows = this.getHiddenRows(); data = data.filter(function (row) { return Utils.findIndex(hiddenRows, row) === -1; }); } + if (params && params.useCurrentPage) { + data = data.slice(this.pageFrom - 1, this.pageTo); + } if (params && params.formatted) { data.forEach(function (row) { for (var _i15 = 0, _Object$entries14 = Object.entries(row); _i15 < _Object$entries14.length; _i15++) { @@ -8516,12 +8517,14 @@ } return; } - var options = Utils.extend(true, {}, BootstrapTable.DEFAULTS, $$o(el).data(), _typeof(option) === 'object' && option); - if (!data) { - data = new $$o.BootstrapTable(el, options); - $$o(el).data('bootstrap.table', data); - data.init(); + if (data) { + console.warn('You cannot initialize the table more than once!'); + return; } + var options = Utils.extend(true, {}, BootstrapTable.DEFAULTS, $$o(el).data(), _typeof(option) === 'object' && option); + data = new $$o.BootstrapTable(el, options); + $$o(el).data('bootstrap.table', data); + data.init(); }); return typeof value === 'undefined' ? this : value; }; diff --git a/public/mix-manifest.json b/public/mix-manifest.json index dd2e0fc1d5..cafa726443 100644 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -18,20 +18,20 @@ "/css/dist/skins/skin-green.css": "/css/dist/skins/skin-green.css?id=0a82a6ae6bb4e58fe62d162c4fb50397", "/css/dist/skins/skin-contrast.css": "/css/dist/skins/skin-contrast.css?id=da6c7997d9de2f8329142399f0ce50da", "/css/dist/skins/skin-red.css": "/css/dist/skins/skin-red.css?id=44bf834f2110504a793dadec132a5898", - "/css/dist/all.css": "/css/dist/all.css?id=2b87a5b5f1e6f09861732fa41d159bc4", + "/css/dist/all.css": "/css/dist/all.css?id=41d8a74912c3b0d17cc508361530c597", "/css/dist/signature-pad.css": "/css/dist/signature-pad.css?id=6a89d3cd901305e66ced1cf5f13147f7", "/css/dist/signature-pad.min.css": "/css/dist/signature-pad.min.css?id=6a89d3cd901305e66ced1cf5f13147f7", - "/css/webfonts/fa-brands-400.ttf": "/css/webfonts/fa-brands-400.ttf?id=2df05d4beaa48550d71234e8dca79141", - "/css/webfonts/fa-brands-400.woff2": "/css/webfonts/fa-brands-400.woff2?id=682885a4f72597322017a9fcd0683831", - "/css/webfonts/fa-regular-400.ttf": "/css/webfonts/fa-regular-400.ttf?id=e7a7f9dd9376f68614860d920255d4df", - "/css/webfonts/fa-regular-400.woff2": "/css/webfonts/fa-regular-400.woff2?id=204fc700c679395e6aa9bebc3cada64e", - "/css/webfonts/fa-solid-900.ttf": "/css/webfonts/fa-solid-900.ttf?id=c9d3294ec75b843a31ef711069a0f0b6", - "/css/webfonts/fa-solid-900.woff2": "/css/webfonts/fa-solid-900.woff2?id=6707d0247b0bca1b4964bab435e3c0d6", - "/css/webfonts/fa-v4compatibility.ttf": "/css/webfonts/fa-v4compatibility.ttf?id=a947172f4fde88e43b4c1a60b01db061", - "/css/webfonts/fa-v4compatibility.woff2": "/css/webfonts/fa-v4compatibility.woff2?id=bbc23038a6067c78310d3f19432a3ebf", - "/css/dist/bootstrap-table.css": "/css/dist/bootstrap-table.css?id=418917c053841ab1aa1b78610a1825e0", + "/css/webfonts/fa-brands-400.ttf": "/css/webfonts/fa-brands-400.ttf?id=e2e2b1797606a266ed55549f5bb5a179", + "/css/webfonts/fa-brands-400.woff2": "/css/webfonts/fa-brands-400.woff2?id=fe912d1c4a7e0e1db87a64eb7e54c945", + "/css/webfonts/fa-regular-400.ttf": "/css/webfonts/fa-regular-400.ttf?id=31a6b5ecfc8d018d0e3a294f0c80e9e9", + "/css/webfonts/fa-regular-400.woff2": "/css/webfonts/fa-regular-400.woff2?id=bf8eabe300a00a3adb0293596987abc4", + "/css/webfonts/fa-solid-900.ttf": "/css/webfonts/fa-solid-900.ttf?id=cd687455c6d6c058e2e9f84f409e2965", + "/css/webfonts/fa-solid-900.woff2": "/css/webfonts/fa-solid-900.woff2?id=eea38615e7b5dbbaf88c263f2230cc32", + "/css/webfonts/fa-v4compatibility.ttf": "/css/webfonts/fa-v4compatibility.ttf?id=6ebbf5afc34f54463abc2b81ca637364", + "/css/webfonts/fa-v4compatibility.woff2": "/css/webfonts/fa-v4compatibility.woff2?id=67b8a78b7e80e805cfa4ee0421895ba4", + "/css/dist/bootstrap-table.css": "/css/dist/bootstrap-table.css?id=2265e072e44c782c901dce8e037d97fc", "/js/build/vendor.js": "/js/build/vendor.js?id=3843eca1b2e670b29c1e1cb57e1d7aa7", - "/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=7a506bf59323cf5b5fe97f7080fc5ee0", + "/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=a0e44dba789031b34ef150a01318b865", "/js/dist/all.js": "/js/dist/all.js?id=97b1034b75e3ac29a2eb9770d66c3370", "/css/dist/skins/skin-green.min.css": "/css/dist/skins/skin-green.min.css?id=0a82a6ae6bb4e58fe62d162c4fb50397", "/css/dist/skins/skin-green-dark.min.css": "/css/dist/skins/skin-green-dark.min.css?id=28b36223cf7b1d6e5f236859a4ef2b45", diff --git a/resources/lang/en/admin/hardware/general.php b/resources/lang/en/admin/hardware/general.php index 3d1e43c2df..b0a48f2ce4 100644 --- a/resources/lang/en/admin/hardware/general.php +++ b/resources/lang/en/admin/hardware/general.php @@ -23,6 +23,7 @@ return [ 'restore' => 'Restore Asset', 'pending' => 'Pending', 'undeployable' => 'Undeployable', + 'undeployable_tooltip' => 'This asset has a status label that is undeployable and cannot be checked out at this time.', 'view' => 'View Asset', 'csv_error' => 'You have an error in your CSV file:', 'import_text' => ' diff --git a/resources/lang/en/admin/licenses/general.php b/resources/lang/en/admin/licenses/general.php index 25a536ec56..0187d076a3 100644 --- a/resources/lang/en/admin/licenses/general.php +++ b/resources/lang/en/admin/licenses/general.php @@ -1,8 +1,8 @@ 'About Licenses', - 'about_licenses' => 'Licenses are used to track software. They have a specified number of seats that can be checked out to individuals', + 'about_licenses_title' => 'About Licenses', + 'about_licenses' => 'Licenses are used to track software. They have a specified number of seats that can be checked out to individuals', 'checkin' => 'Checkin License Seat', 'checkout_history' => 'Checkout History', 'checkout' => 'Checkout License Seat', @@ -18,4 +18,30 @@ return array( 'software_licenses' => 'Software Licenses', 'user' => 'User', 'view' => 'View License', + 'delete_disabled' => 'This license cannot be deleted yet because some seats are still checked out.', + 'bulk' => + [ + 'checkin_all' => [ + 'button' => 'Checkin All Seats', + 'modal' => 'This will action checkin one seat. | This action will checkin all :checkedout_seats_count seats for this license.', + 'enabled_tooltip' => 'Checkin ALL seats for this license from both users and assets', + 'disabled_tooltip' => 'This is disabled because there are no seats currently checked out', + 'success' => 'License successfully checked in! | All licenses were successfully checked in!', + 'log_msg' => 'Checked in via bulk license checkout in license GUI', + ], + + 'checkout_all' => [ + 'button' => 'Checkout All Seats', + 'modal' => 'This action will checkout one seat to the first available user. | This action will checkout all :available_seats_count seats to the first available users. A user is considered available for this seat if they do not already have this license checked out to them, and the Auto-Assign License property is enabled on their user account.', + 'enabled_tooltip' => 'Checkout ALL seats (or as many as are available) to ALL users', + 'disabled_tooltip' => 'This is disabled because there are no seats currently available', + 'success' => 'License successfully checked out! | :count licenses were successfully checked out!', + 'error_no_seats' => 'There are no remaining seats left for this license.', + 'warn_not_enough_seats' => ':count users were assigned this license, but we ran out of available license seats.', + 'warn_no_avail_users' => 'Nothing to do. There are no users who do not already have this license assigned to them.', + 'log_msg' => 'Checked out via bulk license checkout in license GUI', + + + ], + ], ); diff --git a/resources/lang/en/admin/manufacturers/message.php b/resources/lang/en/admin/manufacturers/message.php index 21a4bc5aaf..d6656683ae 100644 --- a/resources/lang/en/admin/manufacturers/message.php +++ b/resources/lang/en/admin/manufacturers/message.php @@ -2,6 +2,7 @@ return array( + 'support_url_help' => 'Use {LOCALE} and {SERIAL} in your URL as variables to have those values auto-populate when viewing assets.', 'does_not_exist' => 'Manufacturer does not exist.', 'assoc_users' => 'This manufacturer is currently associated with at least one model and cannot be deleted. Please update your models to no longer reference this manufacturer and try again. ', diff --git a/resources/lang/en/admin/manufacturers/table.php b/resources/lang/en/admin/manufacturers/table.php index 4e3ea9904d..38cab6fd91 100644 --- a/resources/lang/en/admin/manufacturers/table.php +++ b/resources/lang/en/admin/manufacturers/table.php @@ -10,6 +10,7 @@ return array( 'support_email' => 'Support Email', 'support_phone' => 'Support Phone', 'support_url' => 'Support URL', + 'warranty_lookup_url' => 'Warranty Lookup URL', 'update' => 'Update Manufacturer', 'url' => 'URL', diff --git a/resources/lang/en/admin/settings/general.php b/resources/lang/en/admin/settings/general.php index c76bb02b55..c69c944579 100644 --- a/resources/lang/en/admin/settings/general.php +++ b/resources/lang/en/admin/settings/general.php @@ -86,6 +86,8 @@ return [ 'ldap_settings' => 'LDAP Settings', 'ldap_client_tls_cert_help' => 'Client-Side TLS Certificate and Key for LDAP connections are usually only useful in Google Workspace configurations with "Secure LDAP." Both are required.', 'ldap_client_tls_key' => 'LDAP Client-Side TLS key', + 'ldap_location' => 'LDAP Location', +'ldap_location_help' => 'The Ldap Location field should be used if an OU is not being used in the Base Bind DN. Leave this blank if an OU search is being used.', 'ldap_login_test_help' => 'Enter a valid LDAP username and password from the base DN you specified above to test whether your LDAP login is configured correctly. YOU MUST SAVE YOUR UPDATED LDAP SETTINGS FIRST.', 'ldap_login_sync_help' => 'This only tests that LDAP can sync correctly. If your LDAP Authentication query is not correct, users may still not be able to login. YOU MUST SAVE YOUR UPDATED LDAP SETTINGS FIRST.', 'ldap_manager' => 'LDAP Manager', diff --git a/resources/lang/en/general.php b/resources/lang/en/general.php index cb51b28a42..328eb32090 100644 --- a/resources/lang/en/general.php +++ b/resources/lang/en/general.php @@ -439,4 +439,13 @@ return [ 'setup_migration_output' => 'Migration output:', 'setup_migration_create_user' => 'Next: Create User', 'importer_generic_error' => 'Your file import is complete, but we did receive an error. This is usually caused by third-party API throttling from a notification webhook (such as Slack) and would not have interfered with the import itself, but you should confirm this.', + 'confirm' => 'Confirm', + 'autoassign_licenses' => 'Auto-Assign Licenses', + 'autoassign_licenses_help' => 'Allow user to be have licenses assigned via the bulk-assign license UI or cli tools.', + 'autoassign_licenses_help_long' => 'This allows a user to be have licenses assigned via the bulk-assign license UI or cli tools. (For example, you might not want contractors to be auto-assigned a license you would provide to only staff members. You can still individually assign licenses to those users, but they will not be included in the Checkout License to All Users functions.)', + 'no_autoassign_licenses_help' => 'Do not include user for bulk-assigning through the license UI or cli tools.', + 'modal_confirm_generic' => 'Are you sure?', + 'cannot_be_deleted' => 'This item cannot be deleted', + 'undeployable_tooltip' => 'This item cannot be checked out. Check the quantity remaining.', + ]; diff --git a/resources/views/custom_fields/index.blade.php b/resources/views/custom_fields/index.blade.php index d2435d7db5..0ad8511183 100644 --- a/resources/views/custom_fields/index.blade.php +++ b/resources/views/custom_fields/index.blade.php @@ -21,7 +21,7 @@

{{ trans('admin/custom_fields/general.fieldsets') }}

@@ -111,7 +111,7 @@

{{ trans('admin/custom_fields/general.custom_fields') }}

diff --git a/resources/views/groups/edit.blade.php b/resources/views/groups/edit.blade.php index f5948e4841..cb95b3762a 100755 --- a/resources/views/groups/edit.blade.php +++ b/resources/views/groups/edit.blade.php @@ -77,7 +77,7 @@ @unless (empty($localPermission['label'])) @@ -100,7 +100,7 @@

{{ $area }}

@@ -122,7 +122,7 @@ {{ $this_permission['label'] }} diff --git a/resources/views/hardware/view.blade.php b/resources/views/hardware/view.blade.php index e33f681f6e..44d63c2b49 100755 --- a/resources/views/hardware/view.blade.php +++ b/resources/views/hardware/view.blade.php @@ -456,7 +456,7 @@
@if ($field->field_encrypted=='1') - + @endif @if ($field->isFieldDecryptable($asset->{$field->db_column_name()} )) @@ -595,20 +595,10 @@ {{ $asset->warranty_months }} {{ trans('admin/hardware/form.months') }} - @if ($asset->serial && $asset->model->manufacturer) - @if ((strtolower($asset->model->manufacturer->name) == "apple") || (str_starts_with(str_replace(' ','',strtolower($asset->model->manufacturer->name)),"appleinc"))) - - + @if (($asset->model->manufacturer) && ($asset->model->manufacturer->warranty_lookup_url!='')) + + {{ trans('admin/hardware/general.mfg_warranty_lookup', ['manufacturer' => $asset->model->manufacturer->name]) }} - @elseif ((strtolower($asset->model->manufacturer->name) == "dell") || (str_starts_with(str_replace(' ','',strtolower($asset->model->manufacturer->name)),"dellinc"))) - - {{ trans('hardware/general.mfg_warranty_lookup') }} - - @elseif ((strtolower($asset->model->manufacturer->name) == "lenovo") || (str_starts_with(str_replace(' ','',strtolower($asset->model->manufacturer->name)),"lenovoinc"))) - - {{ trans('hardware/general.mfg_warranty_lookup') }} - - @endif @endif
diff --git a/resources/views/layouts/default.blade.php b/resources/views/layouts/default.blade.php index 98b1c52817..d11d3c541f 100644 --- a/resources/views/layouts/default.blade.php +++ b/resources/views/layouts/default.blade.php @@ -483,8 +483,7 @@ class="fas fa-times text-red fa-fw"> {{ trans('general.all') }} {{ trans('general.undeployable') }} - ({{ (isset($total_undeployable_sidebar)) ? $total_undeployable_sidebar : '' }} - ) + ({{ (isset($total_undeployable_sidebar)) ? $total_undeployable_sidebar : '' }}) -
+
- {!! $license->maintained ? ' '.trans('general.yes') : ' '.trans('general.no') !!} + {!! $license->maintained ? ' '.trans('general.yes') : ' '.trans('general.no') !!}
@@ -358,7 +344,7 @@
- {!! $license->reassignable ? ' '.trans('general.yes') : ' '.trans('general.no') !!} + {!! $license->reassignable ? ' '.trans('general.yes') : ' '.trans('general.no') !!}
@@ -378,8 +364,10 @@ - - + + + + @@ -550,13 +538,100 @@ + + + + +
+ + @can('update', $license) + {{ trans('admin/licenses/general.edit') }} + {{ trans('admin/licenses/general.clone') }} + @endcan + + @can('checkout', $license) + + @if ($license->availCount()->count() > 0) + + {{ trans('general.checkout') }} + + + {{ trans('admin/licenses/general.bulk.checkout_all.button') }} + + + @else + + {{ trans('general.checkout') }} + + + + {{ trans('admin/licenses/general.bulk.checkout_all.button') }} + + + @endif + @endcan + + @can('checkin', $license) + + @if (($license->seats - $license->availCount()->count()) > 0 ) + + {{ trans('admin/licenses/general.bulk.checkin_all.button') }} + + @else + + + {{ trans('admin/licenses/general.bulk.checkin_all.button') }} + + + @endif + @endcan + + @can('delete', $license) + + @if ($license->availCount()->count() == $license->seats) + + @else + + + {{ trans('general.delete') }} + + + @endif + @endcan +
+ + +@can('checkin', \App\Models\License::class) + @include ('modals.confirm-action', + [ + 'modal_name' => 'checkinFromAllModal', + 'route' => route('licenses.bulkcheckin', $license->id), + 'title' => trans('general.modal_confirm_generic'), + 'body' => trans_choice('admin/licenses/general.bulk.checkin_all.modal', 2, ['checkedout_seats_count' => $checkedout_seats_count]) + ]) +@endcan + +@can('checkout', \App\Models\License::class) + @include ('modals.confirm-action', + [ + 'modal_name' => 'checkoutFromAllModal', + 'route' => route('licenses.bulkcheckout', $license->id), + 'title' => trans('general.modal_confirm_generic'), + 'body' => trans_choice('admin/licenses/general.bulk.checkout_all.modal', 2, ['available_seats_count' => $available_seats_count]) + ]) +@endcan + + + @can('update', \App\Models\License::class) @include ('modals.upload-file', ['item_type' => 'license', 'item_id' => $license->id]) @endcan @@ -565,5 +640,15 @@ @section('moar_scripts') + @include ('partials.bootstrap-table') @stop diff --git a/resources/views/manufacturers/edit.blade.php b/resources/views/manufacturers/edit.blade.php index fbbb812ba6..6e87344053 100755 --- a/resources/views/manufacturers/edit.blade.php +++ b/resources/views/manufacturers/edit.blade.php @@ -30,6 +30,17 @@ + +
+ +
+ +

{!! trans('admin/manufacturers/message.support_url_help') !!}

+ {!! $errors->first('warranty_lookup_url', '') !!} +
+
+
+ +
+
+ {{ Form::label('ldap_location', trans('admin/settings/general.ldap_location')) }} +
+
+ {{ Form::text('ldap_location', Request::old('ldap_location', $setting->ldap_location), ['class' => 'form-control','placeholder' => trans('general.example') .'physicaldeliveryofficename', $setting->demoMode]) }} +

{!! trans('admin/settings/general.ldap_location_help') !!}

+ {!! $errors->first('ldap_location', '') !!} + @if (config('app.lock_passwords')===true) +

{{ trans('general.feature_disabled') }}

+ @endif +
+
@if ($setting->ldap_enabled) diff --git a/resources/views/users/bulk-edit.blade.php b/resources/views/users/bulk-edit.blade.php index 10ae6261d0..bdc1ad9289 100644 --- a/resources/views/users/bulk-edit.blade.php +++ b/resources/views/users/bulk-edit.blade.php @@ -119,6 +119,29 @@ + +
+
+ {{ trans('general.autoassign_licenses') }} +
+
+ + + + + +
+
+
diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php index a94f0734b7..34005e9bc7 100755 --- a/resources/views/users/edit.blade.php +++ b/resources/views/users/edit.blade.php @@ -370,6 +370,20 @@
+ + +
+
+ + + +

{{ trans('general.autoassign_licenses_help_long') }}

+
+
+
@@ -383,15 +397,6 @@
- -
-
- -
-
@include ('partials.forms.edit.location-select', ['translated_name' => trans('general.location'), 'fieldname' => 'location_id']) @@ -444,8 +449,8 @@
-
- {!! Form::countries('country', old('country', $user->country), 'col-md-6 select2') !!} +
+ {!! Form::countries('country', old('country', $user->country), 'col-md-12 select2') !!} {!! $errors->first('country', '') !!}
diff --git a/resources/views/users/view.blade.php b/resources/views/users/view.blade.php index a130d1629d..47d6dea328 100755 --- a/resources/views/users/view.blade.php +++ b/resources/views/users/view.blade.php @@ -122,7 +122,8 @@ @endcan @can('update', \App\Models\User::class) -
  • +
  • + @@ -519,23 +520,23 @@
  • @endif - +
    {{ trans('admin/users/general.vip_label') }}
    - {!! ($user->vip=='1') ? ' '.trans('general.yes') : ' '.trans('general.no') !!} + {!! ($user->vip=='1') ? ' '.trans('general.yes') : ' '.trans('general.no') !!}
    - +
    {{ trans('admin/users/general.remote') }}
    - {!! ($user->remote=='1') ? ' '.trans('general.yes') : ' '.trans('general.no') !!} + {!! ($user->remote=='1') ? ' '.trans('general.yes') : ' '.trans('general.no') !!}
    @@ -545,17 +546,28 @@ {{ trans('general.login_enabled') }}
    - {!! ($user->activated=='1') ? ' '.trans('general.yes') : ' '.trans('general.no') !!} + {!! ($user->activated=='1') ? ' '.trans('general.yes') : ' '.trans('general.no') !!}
    - + +
    +
    + {{ trans('general.autoassign_licenses') }} +
    +
    + {!! ($user->autoassign_licenses=='1') ? ' '.trans('general.yes') : ' '.trans('general.no') !!} +
    +
    + + +
    LDAP
    - {!! ($user->ldap_import=='1') ? ' '.trans('general.yes') : ' '.trans('general.no') !!} + {!! ($user->ldap_import=='1') ? ' '.trans('general.yes') : ' '.trans('general.no') !!}
    @@ -569,7 +581,7 @@
    - {!! ($user->two_factor_active()) ? ' '.trans('general.yes') : ' '.trans('general.no') !!} + {!! ($user->two_factor_active()) ? ' '.trans('general.yes') : ' '.trans('general.no') !!}
    @@ -580,7 +592,7 @@ {{ trans('admin/users/general.two_factor_enrolled') }}
    - {!! ($user->two_factor_active_and_enrolled()) ? ' '.trans('general.yes') : ' '.trans('general.no') !!} + {!! ($user->two_factor_active_and_enrolled()) ? ' '.trans('general.yes') : ' '.trans('general.no') !!}
    @@ -590,11 +602,11 @@
    - +
    -
    +
    - + {{ trans('admin/settings/general.two_factor_reset') }} @@ -1031,9 +1043,9 @@ $(function () { dataType: 'json', success: function (data) { - $("#two_factor_reset_toggle").html('').html(' {{ trans('general.no') }}'); + $("#two_factor_reset_toggle").html('').html(' {{ trans('general.no') }}'); $("#two_factor_reseticon").html(''); - $("#two_factor_resetstatus").html('' + data.message); + $("#two_factor_resetstatus").html(' ' + data.message + ''); }, diff --git a/routes/web.php b/routes/web.php index fcdc65df09..4644f41ffb 100644 --- a/routes/web.php +++ b/routes/web.php @@ -50,7 +50,7 @@ Route::group(['middleware' => 'auth'], function () { Route::get('{locationId}/clone', [LocationsController::class, 'getClone'] - )->name('clone/license'); + )->name('clone/location'); Route::get( '{locationId}/printassigned', diff --git a/routes/web/licenses.php b/routes/web/licenses.php index ebcb5a2b2b..b70347793c 100644 --- a/routes/web/licenses.php +++ b/routes/web/licenses.php @@ -25,10 +25,21 @@ Route::group(['prefix' => 'licenses', 'middleware' => ['auth']], function () { [Licenses\LicenseCheckinController::class, 'store'] )->name('licenses.checkin.save'); + Route::post( + '{licenseId}/bulkcheckin', + [Licenses\LicenseCheckinController::class, 'bulkCheckin'] + )->name('licenses.bulkcheckin'); + + Route::post( + '{licenseId}/bulkcheckout', + [Licenses\LicenseCheckoutController::class, 'bulkCheckout'] + )->name('licenses.bulkcheckout'); + Route::post( '{licenseId}/upload', [Licenses\LicenseFilesController::class, 'store'] )->name('upload/license'); + Route::delete( '{licenseId}/deletefile/{fileId}', [Licenses\LicenseFilesController::class, 'destroy'] diff --git a/tests/Feature/Notifications/AccessoryWebhookTest.php b/tests/Feature/Notifications/AccessoryWebhookTest.php new file mode 100644 index 0000000000..a1db59b985 --- /dev/null +++ b/tests/Feature/Notifications/AccessoryWebhookTest.php @@ -0,0 +1,96 @@ +settings->enableWebhook(); + + event(new CheckoutableCheckedOut( + Accessory::factory()->appleBtKeyboard()->create(), + User::factory()->create(), + User::factory()->superuser()->create(), + '' + )); + + Notification::assertSentTo( + new AnonymousNotifiable, + CheckoutAccessoryNotification::class, + function ($notification, $channels, $notifiable) { + return $notifiable->routes['slack'] === Setting::getSettings()->webhook_endpoint; + } + ); + } + + public function testAccessoryCheckoutDoesNotSendWebhookNotificationWhenSettingDisabled() + { + Notification::fake(); + + $this->settings->disableWebhook(); + + event(new CheckoutableCheckedOut( + Accessory::factory()->appleBtKeyboard()->create(), + User::factory()->create(), + User::factory()->superuser()->create(), + '' + )); + + Notification::assertNotSentTo(new AnonymousNotifiable, CheckoutAccessoryNotification::class); + } + + public function testAccessoryCheckinSendsWebhookNotificationWhenSettingEnabled() + { + Notification::fake(); + + $this->settings->enableWebhook(); + + event(new CheckoutableCheckedIn( + Accessory::factory()->appleBtKeyboard()->create(), + User::factory()->create(), + User::factory()->superuser()->create(), + '' + )); + + Notification::assertSentTo( + new AnonymousNotifiable, + CheckinAccessoryNotification::class, + function ($notification, $channels, $notifiable) { + return $notifiable->routes['slack'] === Setting::getSettings()->webhook_endpoint; + } + ); + } + + public function testAccessoryCheckinDoesNotSendWebhookNotificationWhenSettingDisabled() + { + Notification::fake(); + + $this->settings->disableWebhook(); + + event(new CheckoutableCheckedIn( + Accessory::factory()->appleBtKeyboard()->create(), + User::factory()->create(), + User::factory()->superuser()->create(), + '' + )); + + Notification::assertNotSentTo(new AnonymousNotifiable, CheckinAccessoryNotification::class); + } +} diff --git a/tests/Feature/Notifications/AssetWebhookTest.php b/tests/Feature/Notifications/AssetWebhookTest.php new file mode 100644 index 0000000000..ae45a4caa4 --- /dev/null +++ b/tests/Feature/Notifications/AssetWebhookTest.php @@ -0,0 +1,115 @@ + [fn() => User::factory()->create()], + 'Asset checked out to asset' => [fn() => $this->createAsset()], + 'Asset checked out to location' => [fn() => Location::factory()->create()], + ]; + } + + /** @dataProvider targets */ + public function testAssetCheckoutSendsWebhookNotificationWhenSettingEnabled($checkoutTarget) + { + Notification::fake(); + + $this->settings->enableWebhook(); + + event(new CheckoutableCheckedOut( + $this->createAsset(), + $checkoutTarget(), + User::factory()->superuser()->create(), + '' + )); + + Notification::assertSentTo( + new AnonymousNotifiable, + CheckoutAssetNotification::class, + function ($notification, $channels, $notifiable) { + return $notifiable->routes['slack'] === Setting::getSettings()->webhook_endpoint; + } + ); + } + + /** @dataProvider targets */ + public function testAssetCheckoutDoesNotSendWebhookNotificationWhenSettingDisabled($checkoutTarget) + { + Notification::fake(); + + $this->settings->disableWebhook(); + + event(new CheckoutableCheckedOut( + $this->createAsset(), + $checkoutTarget(), + User::factory()->superuser()->create(), + '' + )); + + Notification::assertNotSentTo(new AnonymousNotifiable, CheckoutAssetNotification::class); + } + + /** @dataProvider targets */ + public function testAssetCheckinSendsWebhookNotificationWhenSettingEnabled($checkoutTarget) + { + Notification::fake(); + + $this->settings->enableWebhook(); + + event(new CheckoutableCheckedIn( + $this->createAsset(), + $checkoutTarget(), + User::factory()->superuser()->create(), + '' + )); + + Notification::assertSentTo( + new AnonymousNotifiable, + CheckinAssetNotification::class, + function ($notification, $channels, $notifiable) { + return $notifiable->routes['slack'] === Setting::getSettings()->webhook_endpoint; + } + ); + } + + /** @dataProvider targets */ + public function testAssetCheckinDoesNotSendWebhookNotificationWhenSettingDisabled($checkoutTarget) + { + Notification::fake(); + + $this->settings->disableWebhook(); + + event(new CheckoutableCheckedIn( + $this->createAsset(), + $checkoutTarget(), + User::factory()->superuser()->create(), + '' + )); + + Notification::assertNotSentTo(new AnonymousNotifiable, CheckinAssetNotification::class); + } + + private function createAsset() + { + return Asset::factory()->laptopMbp()->create(); + } +} diff --git a/tests/Feature/Notifications/ComponentWebhookTest.php b/tests/Feature/Notifications/ComponentWebhookTest.php new file mode 100644 index 0000000000..8f3a51b15d --- /dev/null +++ b/tests/Feature/Notifications/ComponentWebhookTest.php @@ -0,0 +1,50 @@ +settings->enableWebhook(); + + event(new CheckoutableCheckedOut( + Component::factory()->ramCrucial8()->create(), + Asset::factory()->laptopMbp()->create(), + User::factory()->superuser()->create(), + '' + )); + + Notification::assertNothingSent(); + } + + public function testComponentCheckinDoesNotSendWebhookNotification() + { + Notification::fake(); + + $this->settings->enableWebhook(); + + event(new CheckoutableCheckedIn( + Component::factory()->ramCrucial8()->create(), + Asset::factory()->laptopMbp()->create(), + User::factory()->superuser()->create(), + '' + )); + + Notification::assertNothingSent(); + } +} diff --git a/tests/Feature/Notifications/ConsumableWebhookTest.php b/tests/Feature/Notifications/ConsumableWebhookTest.php new file mode 100644 index 0000000000..854fdf5341 --- /dev/null +++ b/tests/Feature/Notifications/ConsumableWebhookTest.php @@ -0,0 +1,56 @@ +settings->enableWebhook(); + + event(new CheckoutableCheckedOut( + Consumable::factory()->cardstock()->create(), + User::factory()->create(), + User::factory()->superuser()->create(), + '' + )); + + Notification::assertSentTo( + new AnonymousNotifiable, + CheckoutConsumableNotification::class, + function ($notification, $channels, $notifiable) { + return $notifiable->routes['slack'] === Setting::getSettings()->webhook_endpoint; + } + ); + } + + public function testConsumableCheckoutDoesNotSendWebhookNotificationWhenSettingDisabled() + { + Notification::fake(); + + $this->settings->disableWebhook(); + + event(new CheckoutableCheckedOut( + Consumable::factory()->cardstock()->create(), + User::factory()->create(), + User::factory()->superuser()->create(), + '' + )); + + Notification::assertNotSentTo(new AnonymousNotifiable, CheckoutConsumableNotification::class); + } +} diff --git a/tests/Feature/Notifications/LicenseWebhookTest.php b/tests/Feature/Notifications/LicenseWebhookTest.php new file mode 100644 index 0000000000..4313ff69d4 --- /dev/null +++ b/tests/Feature/Notifications/LicenseWebhookTest.php @@ -0,0 +1,109 @@ + [fn() => User::factory()->create()], + 'License checked out to asset' => [fn() => Asset::factory()->laptopMbp()->create()], + ]; + } + + /** @dataProvider targets */ + public function testLicenseCheckoutSendsWebhookNotificationWhenSettingEnabled($checkoutTarget) + { + Notification::fake(); + + $this->settings->enableWebhook(); + + event(new CheckoutableCheckedOut( + LicenseSeat::factory()->create(), + $checkoutTarget(), + User::factory()->superuser()->create(), + '' + )); + + Notification::assertSentTo( + new AnonymousNotifiable, + CheckoutLicenseSeatNotification::class, + function ($notification, $channels, $notifiable) { + return $notifiable->routes['slack'] === Setting::getSettings()->webhook_endpoint; + } + ); + } + + /** @dataProvider targets */ + public function testLicenseCheckoutDoesNotSendWebhookNotificationWhenSettingDisabled($checkoutTarget) + { + Notification::fake(); + + $this->settings->disableWebhook(); + + event(new CheckoutableCheckedOut( + LicenseSeat::factory()->create(), + $checkoutTarget(), + User::factory()->superuser()->create(), + '' + )); + + Notification::assertNotSentTo(new AnonymousNotifiable, CheckoutLicenseSeatNotification::class); + } + + /** @dataProvider targets */ + public function testLicenseCheckinSendsWebhookNotificationWhenSettingEnabled($checkoutTarget) + { + Notification::fake(); + + $this->settings->enableWebhook(); + + event(new CheckoutableCheckedIn( + LicenseSeat::factory()->create(), + $checkoutTarget(), + User::factory()->superuser()->create(), + '' + )); + + Notification::assertSentTo( + new AnonymousNotifiable, + CheckinLicenseSeatNotification::class, + function ($notification, $channels, $notifiable) { + return $notifiable->routes['slack'] === Setting::getSettings()->webhook_endpoint; + } + ); + } + + /** @dataProvider targets */ + public function testLicenseCheckinDoesNotSendWebhookNotificationWhenSettingDisabled($checkoutTarget) + { + Notification::fake(); + + $this->settings->disableWebhook(); + + event(new CheckoutableCheckedIn( + LicenseSeat::factory()->create(), + $checkoutTarget(), + User::factory()->superuser()->create(), + '' + )); + + Notification::assertNotSentTo(new AnonymousNotifiable, CheckinLicenseSeatNotification::class); + } +} diff --git a/tests/Feature/Users/UpdateUserTest.php b/tests/Feature/Users/UpdateUserTest.php new file mode 100644 index 0000000000..9ddb323625 --- /dev/null +++ b/tests/Feature/Users/UpdateUserTest.php @@ -0,0 +1,61 @@ +superuser()->create(); + $user = User::factory()->create(['activated' => false]); + + $this->actingAs($admin) + ->put(route('users.update', $user), [ + 'first_name' => $user->first_name, + 'username' => $user->username, + 'activated' => 1, + ]); + + $this->assertTrue($user->refresh()->activated); + } + + public function testUsersCanBeDeactivated() + { + $admin = User::factory()->superuser()->create(); + $user = User::factory()->create(['activated' => true]); + + $this->actingAs($admin) + ->put(route('users.update', $user), [ + 'first_name' => $user->first_name, + 'username' => $user->username, + // checkboxes that are not checked are + // not included in the request payload + // 'activated' => 0, + ]); + + $this->assertFalse($user->refresh()->activated); + } + + public function testUsersUpdatingThemselvesDoNotDeactivateTheirAccount() + { + $admin = User::factory()->superuser()->create(['activated' => true]); + + $this->actingAs($admin) + ->put(route('users.update', $admin), [ + 'first_name' => $admin->first_name, + 'username' => $admin->username, + // checkboxes that are disabled are not + // included in the request payload + // even if they are checked + // 'activated' => 0, + ]); + + $this->assertTrue($admin->refresh()->activated); + } +} diff --git a/tests/Support/Settings.php b/tests/Support/Settings.php index ccf50c3ce3..9d4209da79 100644 --- a/tests/Support/Settings.php +++ b/tests/Support/Settings.php @@ -23,6 +23,24 @@ class Settings return $this->update(['full_multiple_companies_support' => 1]); } + public function enableWebhook(): Settings + { + return $this->update([ + 'webhook_botname' => 'SnipeBot5000', + 'webhook_endpoint' => 'https://hooks.slack.com/services/NZ59/Q446/672N', + 'webhook_channel' => '#it', + ]); + } + + public function disableWebhook(): Settings + { + return $this->update([ + 'webhook_botname' => '', + 'webhook_endpoint' => '', + 'webhook_channel' => '', + ]); + } + /** * @param array $attributes Attributes to modify in the application's settings. */