diff --git a/.all-contributorsrc b/.all-contributorsrc index dd090262e..eb052687e 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 f274fc2c4..0a5c28ee5 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 323d90e41..5aa2758e7 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 3a0d37998..040de8e93 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 c81408442..801b3d187 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 5aebde377..114f92dae 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 975db4f5d..dd6fea8b6 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 7d4e697b9..111cbb3c8 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 7e8ecdb11..ab5635ec3 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 343593a03..1816fc9f6 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 f3183238b..9ff0a0c20 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 7c63bb925..dc444ec9e 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 5f3221d05..dc6bc8434 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 3b2ff4623..2a9384550 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 8cfbec849..80d17f1b3 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 2ada97361..34c9aed16 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 0a40c0bb3..b33e6e07a 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 257722b00..50e20c798 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 6f2ae003c..a71049769 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 a0467654c..26cba3328 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 1f7d2f2c0..e98644f46 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 f16a6fc8f..c65dbc7d2 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 67ecae542..a2d3d496d 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 7e95484f6..25a64e5cb 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 3c389a1b1..8df6b89f1 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 bbcbda12b..9c84fd50f 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 9447d6545..867a88461 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 bcbc632eb..9f2c1c5f4 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 9d6cd942b..09cb3ae8f 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 41ab80257..292e52957 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 a29581bf9..4eb496a2a 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 ff69d5f66..162b3d662 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 2207edd02..d2a99d3c5 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 5f01c3c27..eb91b3b8f 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 ecac18335..61be790e0 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 e5a039a4e..af12c3d29 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 77cbf01c1..7ec25645e 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 d60d3c42f..fa00fbad2 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 df9780091..36e1c8ac4 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 4be0f5601..ce476d082 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 1e8784f2f..5a44bf52a 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 5b44a7693..f5c15f1fe 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 b5eefdf81..fbb41a4d5 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 dee8d9103..688340c9e 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 000000000..3c6cc4246 --- /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 ab22262a7..4e736b8d8 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 000000000..60c0e31a6 --- /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 000000000..52a655f2e --- /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 dd058375d..cd722a04b 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 6c1df0383..5d8cfd622 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 e0ffcf96d..d5383def4 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 4cc91443e..8d409af9c 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 641a48933..774d51ac4 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 592910129..71e318526 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 7d634a2ba..8a9d6344d 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 953d5540b..7f021680b 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 b3a2b6410..993dbe1f9 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 83433f445..5c16cd3e8 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 e4eea68d0..ab6ae2248 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 e804f1860..9027e38bc 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 0ded615dc..b1f75983b 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 dd2e0fc1d..cafa72644 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 3d1e43c2d..b0a48f2ce 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 25a536ec5..0187d076a 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 21a4bc5aa..d6656683a 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 4e3ea9904..38cab6fd9 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 c76bb02b5..c69c94457 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 cb51b28a4..328eb3209 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 d2435d7db..0ad851118 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 f5948e484..cb95b3762 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 e33f681f6..44d63c2b4 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 98b1c5281..d11d3c541 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 fbbb812ba..6e8734405 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 10ae6261d..bdc1ad928 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 a94f0734b..34005e9bc 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 a130d1629..47d6dea32 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 fcdc65df0..4644f41ff 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 ebcb5a2b2..b70347793 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 000000000..a1db59b98 --- /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 000000000..ae45a4caa --- /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 000000000..8f3a51b15 --- /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 000000000..854fdf534 --- /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 000000000..4313ff69d --- /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 000000000..9ddb32362 --- /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 ccf50c3ce..9d4209da7 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. */