Merge branch 'develop' into fixes/bulk-checkin-logging

# Conflicts:
#	app/Http/Controllers/Users/BulkUsersController.php
This commit is contained in:
Marcus Moore 2024-08-07 13:06:59 -07:00
commit 41437e4d8f
No known key found for this signature in database
71 changed files with 232 additions and 102 deletions

View file

@ -1,35 +1,35 @@
FROM alpine:3.18.6 FROM alpine:3.19
# Apache + PHP # Apache + PHP
RUN apk add --no-cache \ RUN apk add --no-cache \
apache2 \ apache2 \
php81 \ php82 \
php81-common \ php82-common \
php81-apache2 \ php82-apache2 \
php81-curl \ php82-curl \
php81-ldap \ php82-ldap \
php81-mysqli \ php82-mysqli \
php81-gd \ php82-gd \
php81-xml \ php82-xml \
php81-mbstring \ php82-mbstring \
php81-zip \ php82-zip \
php81-ctype \ php82-ctype \
php81-tokenizer \ php82-tokenizer \
php81-pdo_mysql \ php82-pdo_mysql \
php81-openssl \ php82-openssl \
php81-bcmath \ php82-bcmath \
php81-phar \ php82-phar \
php81-json \ php82-json \
php81-iconv \ php82-iconv \
php81-fileinfo \ php82-fileinfo \
php81-simplexml \ php82-simplexml \
php81-session \ php82-session \
php81-dom \ php82-dom \
php81-xmlwriter \ php82-xmlwriter \
php81-xmlreader \ php82-xmlreader \
php81-sodium \ php82-sodium \
php81-redis \ php82-redis \
php81-pecl-memcached \ php82-pecl-memcached \
php81-exif \ php82-exif \
curl \ curl \
wget \ wget \
vim \ vim \
@ -42,7 +42,7 @@ COPY docker/column-statistics.cnf /etc/mysql/conf.d/column-statistics.cnf
# Where apache's PID lives # Where apache's PID lives
RUN mkdir -p /run/apache2 && chown apache:apache /run/apache2 RUN mkdir -p /run/apache2 && chown apache:apache /run/apache2
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php81/php.ini RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php82/php.ini
COPY docker/000-default-2.4.conf /etc/apache2/conf.d/default.conf COPY docker/000-default-2.4.conf /etc/apache2/conf.d/default.conf
# Enable mod_rewrite # Enable mod_rewrite

View file

@ -248,6 +248,7 @@ class LocationsController extends Controller
->withCount('rtd_assets as rtd_assets_count') ->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count') ->withCount('children as children_count')
->withCount('users as users_count') ->withCount('users as users_count')
->withCount('accessories as accessories_count')
->findOrFail($id); ->findOrFail($id);
if (! $location->isDeletable()) { if (! $location->isDeletable()) {

View file

@ -20,7 +20,7 @@ trait CheckInOutRequest
return Location::findOrFail(request('assigned_location')); return Location::findOrFail(request('assigned_location'));
case 'asset': case 'asset':
return Asset::findOrFail(request('assigned_asset')); return Asset::findOrFail(request('assigned_asset'));
case 'user': default:
return User::findOrFail(request('assigned_user')); return User::findOrFail(request('assigned_user'));
} }

View file

@ -8,6 +8,7 @@ use App\Helpers\Helper;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Asset; use App\Models\Asset;
use App\Models\Component; use App\Models\Component;
use App\Models\Setting;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Input; use Illuminate\Support\Facades\Input;
@ -97,6 +98,10 @@ class ComponentCheckoutController extends Controller
// Check if the asset exists // Check if the asset exists
$asset = Asset::find($request->input('asset_id')); $asset = Asset::find($request->input('asset_id'));
if ((Setting::getSettings()->full_multiple_companies_support) && $component->company_id !== $asset->company_id) {
return redirect()->route('components.checkout.show', $componentId)->with('error', trans('general.error_user_company'));
}
// Update the component data // Update the component data
$component->asset_id = $request->input('asset_id'); $component->asset_id = $request->input('asset_id');
$component->assets()->attach($component->id, [ $component->assets()->attach($component->id, [

View file

@ -324,6 +324,7 @@ class SettingsController extends Controller
$setting->full_multiple_companies_support = $request->input('full_multiple_companies_support', '0'); $setting->full_multiple_companies_support = $request->input('full_multiple_companies_support', '0');
$setting->unique_serial = $request->input('unique_serial', '0'); $setting->unique_serial = $request->input('unique_serial', '0');
$setting->shortcuts_enabled = $request->input('shortcuts_enabled', '0');
$setting->show_images_in_email = $request->input('show_images_in_email', '0'); $setting->show_images_in_email = $request->input('show_images_in_email', '0');
$setting->show_archived_in_list = $request->input('show_archived_in_list', '0'); $setting->show_archived_in_list = $request->input('show_archived_in_list', '0');
$setting->dashboard_message = $request->input('dashboard_message'); $setting->dashboard_message = $request->input('dashboard_message');

View file

@ -219,8 +219,8 @@ class BulkUsersController extends Controller
} }
$users = User::whereIn('id', $user_raw_array)->get(); $users = User::whereIn('id', $user_raw_array)->get();
$assets = Asset::whereIn('assigned_to', $user_raw_array)->where('assigned_type', \App\Models\User::class)->get(); $assets = Asset::whereIn('assigned_to', $user_raw_array)->where('assigned_type', User::class)->get();
$accessoryUserRows = DB::table('accessories_checkout')->whereIn('assigned_to', $user_raw_array)->where('assigned_type', \App\Models\User::class)->get(); $accessoryUserRows = DB::table('accessories_checkout')->whereIn('assigned_to', $user_raw_array)->where('assigned_type', User::class)->get();
$licenses = DB::table('license_seats')->whereIn('assigned_to', $user_raw_array)->get(); $licenses = DB::table('license_seats')->whereIn('assigned_to', $user_raw_array)->get();
$consumableUserRows = DB::table('consumables_users')->whereIn('assigned_to', $user_raw_array)->get(); $consumableUserRows = DB::table('consumables_users')->whereIn('assigned_to', $user_raw_array)->get();

View file

@ -205,7 +205,11 @@ class Component extends SnipeModel
public function numCheckedOut() public function numCheckedOut()
{ {
$checkedout = 0; $checkedout = 0;
foreach ($this->assets as $checkout) {
// In case there are elements checked out to assets that belong to a different company
// than this asset and full multiple company support is on we'll remove the global scope,
// so they are included in the count.
foreach ($this->assets()->withoutGlobalScope(new CompanyableScope)->get() as $checkout) {
$checkedout += $checkout->pivot->assigned_qty; $checkedout += $checkout->pivot->assigned_qty;
} }

View file

@ -108,10 +108,11 @@ class Location extends SnipeModel
{ {
return Gate::allows('delete', $this) return Gate::allows('delete', $this)
&& ($this->assets_count === 0) && ($this->assets_count == 0)
&& ($this->assigned_assets_count === 0) && ($this->assigned_assets_count == 0)
&& ($this->children_count === 0) && ($this->children_count == 0)
&& ($this->users_count === 0); && ($this->accessories_count == 0)
&& ($this->users_count == 0);
} }
/** /**

View file

@ -6,5 +6,5 @@ return array (
'prerelease_version' => '', 'prerelease_version' => '',
'hash_version' => 'gc2bcc2e2d', 'hash_version' => 'gc2bcc2e2d',
'full_hash' => 'v7.0.10-311-gc2bcc2e2d', 'full_hash' => 'v7.0.10-311-gc2bcc2e2d',
'branch' => 'develop', 'branch' => 'master',
); );

View file

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/dist/all.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -1,24 +1,24 @@
{ {
"/js/build/app.js": "/js/build/app.js?id=842cc33168d973ac10d35eb664be2a2c", "/js/build/app.js": "/js/build/app.js?id=05dea4c19ca75f9fc68915a82c341f24",
"/css/dist/skins/skin-red.css": "/css/dist/skins/skin-red.css?id=44bf834f2110504a793dadec132a5898", "/css/dist/skins/skin-red.css": "/css/dist/skins/skin-red.css?id=b9a74ec0cd68f83e7480d5ae39919beb",
"/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=f677207c6cf9678eb539abecb408c374", "/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=392cc93cfc0be0349bab9697669dd091",
"/css/build/overrides.css": "/css/build/overrides.css?id=004835e70ed3ae2e2340162b7a37c752", "/css/build/overrides.css": "/css/build/overrides.css?id=3a0b83c0a9919e91f7c795a1971382fd",
"/css/build/app.css": "/css/build/app.css?id=7ecac57fc8cf6fdbe447c18d5f2ae7bc", "/css/build/app.css": "/css/build/app.css?id=d6fd5ea2989e7ab278745d995c167ae7",
"/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=4ea0068716c1bb2434d87a16d51b98c9", "/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=a67bd93bed52e6a29967fe472de66d6c",
"/css/dist/skins/skin-orange.css": "/css/dist/skins/skin-orange.css?id=6f0563e726c2fe4fab4026daaa5bfdf2", "/css/dist/skins/skin-orange.css": "/css/dist/skins/skin-orange.css?id=268041e902b019730c23ee3875838005",
"/css/dist/skins/skin-orange-dark.css": "/css/dist/skins/skin-orange-dark.css?id=4fa7aa3ba499c8f4e390eb8549da3f74", "/css/dist/skins/skin-orange-dark.css": "/css/dist/skins/skin-orange-dark.css?id=8a12dfa3bef796fc17178890e0aff8d6",
"/css/dist/skins/skin-blue-dark.css": "/css/dist/skins/skin-blue-dark.css?id=0640e45bad692dcf62873c6e85904899", "/css/dist/skins/skin-blue-dark.css": "/css/dist/skins/skin-blue-dark.css?id=34023bf46b7c2486b7468de9b750dbff",
"/css/dist/skins/skin-yellow-dark.css": "/css/dist/skins/skin-yellow-dark.css?id=393aaa7b368b0670fc42434c8cca7dc7", "/css/dist/skins/skin-yellow-dark.css": "/css/dist/skins/skin-yellow-dark.css?id=dd5eb6c76770bacaa2e960849d275516",
"/css/dist/skins/skin-yellow.css": "/css/dist/skins/skin-yellow.css?id=7b315b9612b8fde8f9c5b0ddb6bba690", "/css/dist/skins/skin-yellow.css": "/css/dist/skins/skin-yellow.css?id=fc7adb943668ac69fe4b646625a7571f",
"/css/dist/skins/skin-purple-dark.css": "/css/dist/skins/skin-purple-dark.css?id=f8b26018a1533b9db864247daaf06daa", "/css/dist/skins/skin-purple-dark.css": "/css/dist/skins/skin-purple-dark.css?id=b391d15ee01e2aaffe90554eae9fde0b",
"/css/dist/skins/skin-purple.css": "/css/dist/skins/skin-purple.css?id=6fe68325d5356197672c27bc77cedcb4", "/css/dist/skins/skin-purple.css": "/css/dist/skins/skin-purple.css?id=cf6c8c340420724b02d6e787ef9bded5",
"/css/dist/skins/skin-red-dark.css": "/css/dist/skins/skin-red-dark.css?id=ad39859637dafa781288630f9d6d6523", "/css/dist/skins/skin-red-dark.css": "/css/dist/skins/skin-red-dark.css?id=eb99dbccf43841b18be1b3ffc69d9497",
"/css/dist/skins/skin-black-dark.css": "/css/dist/skins/skin-black-dark.css?id=f0b08873a06bb54daeee176a9459f4a9", "/css/dist/skins/skin-black-dark.css": "/css/dist/skins/skin-black-dark.css?id=5f3abb12a286d6cb8aa523322d7f053b",
"/css/dist/skins/skin-black.css": "/css/dist/skins/skin-black.css?id=76482123f6c70e866d6b971ba91de7bb", "/css/dist/skins/skin-black.css": "/css/dist/skins/skin-black.css?id=1f33ca3d860461c1127ec465ab3ebb6b",
"/css/dist/skins/skin-green-dark.css": "/css/dist/skins/skin-green-dark.css?id=553ee68741b5a392037abcf04da80adc", "/css/dist/skins/skin-green-dark.css": "/css/dist/skins/skin-green-dark.css?id=198553147983f411db55d774009bf481",
"/css/dist/skins/skin-green.css": "/css/dist/skins/skin-green.css?id=0a82a6ae6bb4e58fe62d162c4fb50397", "/css/dist/skins/skin-green.css": "/css/dist/skins/skin-green.css?id=b48f4d8af0e1ca5621c161e93951109f",
"/css/dist/skins/skin-contrast.css": "/css/dist/skins/skin-contrast.css?id=da6c7997d9de2f8329142399f0ce50da", "/css/dist/skins/skin-contrast.css": "/css/dist/skins/skin-contrast.css?id=f0fbbb0ac729ea092578fb05ca615460",
"/css/dist/all.css": "/css/dist/all.css?id=656b0a0561a4be447c195846c3de3558", "/css/dist/all.css": "/css/dist/all.css?id=fba2adaeb1f10de7b4f6628260ee6ef2",
"/css/dist/signature-pad.css": "/css/dist/signature-pad.css?id=6a89d3cd901305e66ced1cf5f13147f7", "/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/dist/signature-pad.min.css": "/css/dist/signature-pad.min.css?id=6a89d3cd901305e66ced1cf5f13147f7",
"/js/select2/i18n/af.js": "/js/select2/i18n/af.js?id=4f6fcd73488ce79fae1b7a90aceaecde", "/js/select2/i18n/af.js": "/js/select2/i18n/af.js?id=4f6fcd73488ce79fae1b7a90aceaecde",
@ -94,20 +94,20 @@
"/css/dist/bootstrap-table.css": "/css/dist/bootstrap-table.css?id=8abbb6aea625ec64cd7ebdad77ebf6e5", "/css/dist/bootstrap-table.css": "/css/dist/bootstrap-table.css?id=8abbb6aea625ec64cd7ebdad77ebf6e5",
"/js/build/vendor.js": "/js/build/vendor.js?id=e27070bdbc5fce3bfd132b952d641fd6", "/js/build/vendor.js": "/js/build/vendor.js?id=e27070bdbc5fce3bfd132b952d641fd6",
"/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=859e11e4e6b05c84e4b7302de29bac5e", "/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=859e11e4e6b05c84e4b7302de29bac5e",
"/js/dist/all.js": "/js/dist/all.js?id=01108f9d8f4f67b20669f0c25a64eb5d", "/js/dist/all.js": "/js/dist/all.js?id=d4e3181b505407e7bd10b1fd802ae109",
"/css/dist/skins/skin-green.min.css": "/css/dist/skins/skin-green.min.css?id=0a82a6ae6bb4e58fe62d162c4fb50397", "/css/dist/skins/skin-green.min.css": "/css/dist/skins/skin-green.min.css?id=b48f4d8af0e1ca5621c161e93951109f",
"/css/dist/skins/skin-green-dark.min.css": "/css/dist/skins/skin-green-dark.min.css?id=553ee68741b5a392037abcf04da80adc", "/css/dist/skins/skin-green-dark.min.css": "/css/dist/skins/skin-green-dark.min.css?id=198553147983f411db55d774009bf481",
"/css/dist/skins/skin-black.min.css": "/css/dist/skins/skin-black.min.css?id=76482123f6c70e866d6b971ba91de7bb", "/css/dist/skins/skin-black.min.css": "/css/dist/skins/skin-black.min.css?id=1f33ca3d860461c1127ec465ab3ebb6b",
"/css/dist/skins/skin-black-dark.min.css": "/css/dist/skins/skin-black-dark.min.css?id=f0b08873a06bb54daeee176a9459f4a9", "/css/dist/skins/skin-black-dark.min.css": "/css/dist/skins/skin-black-dark.min.css?id=5f3abb12a286d6cb8aa523322d7f053b",
"/css/dist/skins/skin-blue.min.css": "/css/dist/skins/skin-blue.min.css?id=f677207c6cf9678eb539abecb408c374", "/css/dist/skins/skin-blue.min.css": "/css/dist/skins/skin-blue.min.css?id=392cc93cfc0be0349bab9697669dd091",
"/css/dist/skins/skin-blue-dark.min.css": "/css/dist/skins/skin-blue-dark.min.css?id=0640e45bad692dcf62873c6e85904899", "/css/dist/skins/skin-blue-dark.min.css": "/css/dist/skins/skin-blue-dark.min.css?id=34023bf46b7c2486b7468de9b750dbff",
"/css/dist/skins/skin-yellow.min.css": "/css/dist/skins/skin-yellow.min.css?id=7b315b9612b8fde8f9c5b0ddb6bba690", "/css/dist/skins/skin-yellow.min.css": "/css/dist/skins/skin-yellow.min.css?id=fc7adb943668ac69fe4b646625a7571f",
"/css/dist/skins/skin-yellow-dark.min.css": "/css/dist/skins/skin-yellow-dark.min.css?id=393aaa7b368b0670fc42434c8cca7dc7", "/css/dist/skins/skin-yellow-dark.min.css": "/css/dist/skins/skin-yellow-dark.min.css?id=dd5eb6c76770bacaa2e960849d275516",
"/css/dist/skins/skin-red.min.css": "/css/dist/skins/skin-red.min.css?id=44bf834f2110504a793dadec132a5898", "/css/dist/skins/skin-red.min.css": "/css/dist/skins/skin-red.min.css?id=b9a74ec0cd68f83e7480d5ae39919beb",
"/css/dist/skins/skin-red-dark.min.css": "/css/dist/skins/skin-red-dark.min.css?id=ad39859637dafa781288630f9d6d6523", "/css/dist/skins/skin-red-dark.min.css": "/css/dist/skins/skin-red-dark.min.css?id=eb99dbccf43841b18be1b3ffc69d9497",
"/css/dist/skins/skin-purple.min.css": "/css/dist/skins/skin-purple.min.css?id=6fe68325d5356197672c27bc77cedcb4", "/css/dist/skins/skin-purple.min.css": "/css/dist/skins/skin-purple.min.css?id=cf6c8c340420724b02d6e787ef9bded5",
"/css/dist/skins/skin-purple-dark.min.css": "/css/dist/skins/skin-purple-dark.min.css?id=f8b26018a1533b9db864247daaf06daa", "/css/dist/skins/skin-purple-dark.min.css": "/css/dist/skins/skin-purple-dark.min.css?id=b391d15ee01e2aaffe90554eae9fde0b",
"/css/dist/skins/skin-orange.min.css": "/css/dist/skins/skin-orange.min.css?id=6f0563e726c2fe4fab4026daaa5bfdf2", "/css/dist/skins/skin-orange.min.css": "/css/dist/skins/skin-orange.min.css?id=268041e902b019730c23ee3875838005",
"/css/dist/skins/skin-orange-dark.min.css": "/css/dist/skins/skin-orange-dark.min.css?id=4fa7aa3ba499c8f4e390eb8549da3f74", "/css/dist/skins/skin-orange-dark.min.css": "/css/dist/skins/skin-orange-dark.min.css?id=8a12dfa3bef796fc17178890e0aff8d6",
"/css/dist/skins/skin-contrast.min.css": "/css/dist/skins/skin-contrast.min.css?id=da6c7997d9de2f8329142399f0ce50da" "/css/dist/skins/skin-contrast.min.css": "/css/dist/skins/skin-contrast.min.css?id=f0fbbb0ac729ea092578fb05ca615460"
} }

View file

@ -218,6 +218,8 @@ return [
'webhook_integration_help' => ':app integration is optional, however the endpoint and channel are required if you wish to use it. To configure :app integration, you must first <a href=":webhook_link" target="_new" rel="noopener">create an incoming webhook</a> on your :app account. Click on the <strong>Test :app Integration</strong> button to confirm your settings are correct before saving. ', 'webhook_integration_help' => ':app integration is optional, however the endpoint and channel are required if you wish to use it. To configure :app integration, you must first <a href=":webhook_link" target="_new" rel="noopener">create an incoming webhook</a> on your :app account. Click on the <strong>Test :app Integration</strong> button to confirm your settings are correct before saving. ',
'webhook_integration_help_button' => 'Once you have saved your :app information, a test button will appear.', 'webhook_integration_help_button' => 'Once you have saved your :app information, a test button will appear.',
'webhook_test_help' => 'Test whether your :app integration is configured correctly. YOU MUST SAVE YOUR UPDATED :app SETTINGS FIRST.', 'webhook_test_help' => 'Test whether your :app integration is configured correctly. YOU MUST SAVE YOUR UPDATED :app SETTINGS FIRST.',
'shortcuts_enabled' => 'Enable Shortcuts',
'shortcuts_help_text' => '<strong>Windows</strong>: Alt + Access key, <strong>Mac</strong>: Control + Option + Access key',
'snipe_version' => 'Snipe-IT version', 'snipe_version' => 'Snipe-IT version',
'support_footer' => 'Support Footer Links ', 'support_footer' => 'Support Footer Links ',
'support_footer_help' => 'Specify who sees the links to the Snipe-IT Support info and Users Manual', 'support_footer_help' => 'Specify who sees the links to the Snipe-IT Support info and Users Manual',

View file

@ -66,14 +66,8 @@
</div> </div>
<!-- User --> <!-- User -->
@include ('partials.forms.checkout-selector', ['user_select' => 'true','asset_select' => 'true', 'location_select' => 'true'])
@include ('partials.forms.edit.user-select', ['translated_name' => trans('general.select_user'), 'fieldname' => 'assigned_user', 'required'=> 'true']) @include ('partials.forms.edit.user-select', ['translated_name' => trans('general.select_user'), 'fieldname' => 'assigned_user', 'required'=> 'true'])
<!-- We have to pass unselect here so that we don't default to the asset that's being checked out. We want that asset to be pre-selected everywhere else. -->
@include ('partials.forms.edit.asset-select', ['translated_name' => trans('general.asset'), 'fieldname' => 'assigned_asset', 'unselect' => 'true', 'style' => 'display:none;', 'required'=>'true'])
@include ('partials.forms.edit.location-select', ['translated_name' => trans('general.location'), 'fieldname' => 'assigned_location', 'style' => 'display:none;', 'required'=>'true'])
<!-- Checkout QTY --> <!-- Checkout QTY -->
<div class="form-group {{ $errors->has('checkout_qty') ? 'error' : '' }} "> <div class="form-group {{ $errors->has('checkout_qty') ? 'error' : '' }} ">

View file

@ -8,7 +8,7 @@
@section('header_right') @section('header_right')
@can('create', \App\Models\Accessory::class) @can('create', \App\Models\Accessory::class)
<a href="{{ route('accessories.create') }}" accesskey="n" class="btn btn-primary pull-right"> {{ trans('general.create') }}</a> <a href="{{ route('accessories.create') }}" {{$snipeSettings->shortcuts_enabled == 1 ? "accesskey=n" : ''}} class="btn btn-primary pull-right"> {{ trans('general.create') }}</a>
@endcan @endcan
@stop @stop

View file

@ -8,7 +8,7 @@
@section('header_right') @section('header_right')
@can('create', \App\Models\Component::class) @can('create', \App\Models\Component::class)
<a href="{{ route('components.create') }}" accesskey="n" class="btn btn-primary pull-right"> {{ trans('general.create') }}</a> <a href="{{ route('components.create') }}" {{$snipeSettings->shortcuts_enabled == 1 ? "accesskey=n" : ''}} class="btn btn-primary pull-right"> {{ trans('general.create') }}</a>
@endcan @endcan
@stop @stop

View file

@ -8,7 +8,7 @@
@section('header_right') @section('header_right')
@can('create', \App\Models\Consumable::class) @can('create', \App\Models\Consumable::class)
<a href="{{ route('consumables.create') }}" accesskey="n" class="btn btn-primary pull-right"> {{ trans('general.create') }}</a> <a href="{{ route('consumables.create') }}" {{$snipeSettings->shortcuts_enabled == 1 ? "accesskey=n" : ''}} class="btn btn-primary pull-right"> {{ trans('general.create') }}</a>
@endcan @endcan
@stop @stop

View file

@ -47,7 +47,7 @@
<a href="{{ route('reports/custom') }}" style="margin-right: 5px;" class="btn btn-default"> <a href="{{ route('reports/custom') }}" style="margin-right: 5px;" class="btn btn-default">
{{ trans('admin/hardware/general.custom_export') }}</a> {{ trans('admin/hardware/general.custom_export') }}</a>
@can('create', \App\Models\Asset::class) @can('create', \App\Models\Asset::class)
<a href="{{ route('hardware.create') }}" accesskey="n" class="btn btn-primary pull-right"></i> {{ trans('general.create') }}</a> <a href="{{ route('hardware.create') }}" {{$snipeSettings->shortcuts_enabled == 1 ? "n" : ''}} class="btn btn-primary pull-right"></i> {{ trans('general.create') }}</a>
@endcan @endcan
@stop @stop

View file

@ -144,7 +144,7 @@ dir="{{ Helper::determineLanguageDirection() }}">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
@can('index', \App\Models\Asset::class) @can('index', \App\Models\Asset::class)
<li aria-hidden="true"{!! (Request::is('hardware*') ? ' class="active"' : '') !!}> <li aria-hidden="true"{!! (Request::is('hardware*') ? ' class="active"' : '') !!}>
<a href="{{ url('hardware') }}" accesskey="1" tabindex="-1"> <a href="{{ url('hardware') }}" {{$snipeSettings->shortcuts_enabled == 1 ? "accesskey=1" : ''}} tabindex="-1">
<i class="fas fa-barcode fa-fw"></i> <i class="fas fa-barcode fa-fw"></i>
<span class="sr-only">{{ trans('general.assets') }}</span> <span class="sr-only">{{ trans('general.assets') }}</span>
</a> </a>
@ -152,7 +152,7 @@ dir="{{ Helper::determineLanguageDirection() }}">
@endcan @endcan
@can('view', \App\Models\License::class) @can('view', \App\Models\License::class)
<li aria-hidden="true"{!! (Request::is('licenses*') ? ' class="active"' : '') !!}> <li aria-hidden="true"{!! (Request::is('licenses*') ? ' class="active"' : '') !!}>
<a href="{{ route('licenses.index') }}" accesskey="2" tabindex="-1"> <a href="{{ route('licenses.index') }}" {{$snipeSettings->shortcuts_enabled == 1 ? "accesskey=2" : ''}} tabindex="-1">
<i class="far fa-save fa-fw"></i> <i class="far fa-save fa-fw"></i>
<span class="sr-only">{{ trans('general.licenses') }}</span> <span class="sr-only">{{ trans('general.licenses') }}</span>
</a> </a>
@ -160,7 +160,7 @@ dir="{{ Helper::determineLanguageDirection() }}">
@endcan @endcan
@can('index', \App\Models\Accessory::class) @can('index', \App\Models\Accessory::class)
<li aria-hidden="true"{!! (Request::is('accessories*') ? ' class="active"' : '') !!}> <li aria-hidden="true"{!! (Request::is('accessories*') ? ' class="active"' : '') !!}>
<a href="{{ route('accessories.index') }}" accesskey="3" tabindex="-1"> <a href="{{ route('accessories.index') }}" {{$snipeSettings->shortcuts_enabled == 1 ? "accesskey=3" : ''}} tabindex="-1">
<i class="far fa-keyboard fa-fw"></i> <i class="far fa-keyboard fa-fw"></i>
<span class="sr-only">{{ trans('general.accessories') }}</span> <span class="sr-only">{{ trans('general.accessories') }}</span>
</a> </a>
@ -168,7 +168,7 @@ dir="{{ Helper::determineLanguageDirection() }}">
@endcan @endcan
@can('index', \App\Models\Consumable::class) @can('index', \App\Models\Consumable::class)
<li aria-hidden="true"{!! (Request::is('consumables*') ? ' class="active"' : '') !!}> <li aria-hidden="true"{!! (Request::is('consumables*') ? ' class="active"' : '') !!}>
<a href="{{ url('consumables') }}" accesskey="4" tabindex="-1"> <a href="{{ url('consumables') }}" {{$snipeSettings->shortcuts_enabled == 1 ? "accesskey=4" : ''}} tabindex="-1">
<i class="fas fa-tint fa-fw"></i> <i class="fas fa-tint fa-fw"></i>
<span class="sr-only">{{ trans('general.consumables') }}</span> <span class="sr-only">{{ trans('general.consumables') }}</span>
</a> </a>
@ -176,7 +176,7 @@ dir="{{ Helper::determineLanguageDirection() }}">
@endcan @endcan
@can('view', \App\Models\Component::class) @can('view', \App\Models\Component::class)
<li aria-hidden="true"{!! (Request::is('components*') ? ' class="active"' : '') !!}> <li aria-hidden="true"{!! (Request::is('components*') ? ' class="active"' : '') !!}>
<a href="{{ route('components.index') }}" accesskey="5" tabindex="-1"> <a href="{{ route('components.index') }}" {{$snipeSettings->shortcuts_enabled == 1 ? "accesskey=5" : ''}} tabindex="-1">
<i class="far fa-hdd fa-fw"></i> <i class="far fa-hdd fa-fw"></i>
<span class="sr-only">{{ trans('general.components') }}</span> <span class="sr-only">{{ trans('general.components') }}</span>
</a> </a>
@ -622,7 +622,7 @@ dir="{{ Helper::determineLanguageDirection() }}">
@can('view', \App\Models\User::class) @can('view', \App\Models\User::class)
<li{!! (Request::is('users*') ? ' class="active"' : '') !!}> <li{!! (Request::is('users*') ? ' class="active"' : '') !!}>
<a href="{{ route('users.index') }}" accesskey="6"> <a href="{{ route('users.index') }}" {{$snipeSettings->shortcuts_enabled == 1 ? "accesskey=6" : ''}}>
<i class="fas fa-users fa-fw"></i> <i class="fas fa-users fa-fw"></i>
<span>{{ trans('general.people') }}</span> <span>{{ trans('general.people') }}</span>
</a> </a>

View file

@ -26,7 +26,7 @@
@can('delete', \App\Models\Asset::class) @can('delete', \App\Models\Asset::class)
<option value="delete">{{ trans('button.delete') }}</option> <option value="delete">{{ trans('button.delete') }}</option>
@endcan @endcan
<option value="labels" accesskey="l">{{ trans_choice('button.generate_labels', 2) }}</option> <option value="labels" {{$snipeSettings->shortcuts_enabled == 1 ? "accesskey=l" : ''}}>{{ trans_choice('button.generate_labels', 2) }}</option>
@endif @endif
</select> </select>

View file

@ -2,6 +2,6 @@
<div class="box-footer text-right" style="padding-bottom: 0px;"> <div class="box-footer text-right" style="padding-bottom: 0px;">
<a class="btn btn-link pull-left" href="{{ URL::previous() }}">{{ trans('button.cancel') }}</a> <a class="btn btn-link pull-left" href="{{ URL::previous() }}">{{ trans('button.cancel') }}</a>
<button type="submit" accesskey="s" class="btn btn-primary"><i class="fas fa-check icon-white" aria-hidden="true"></i> {{ trans('general.save') }}</button> <button type="submit" {{$snipeSettings->shortcuts_enabled == 1 ? "accesskey=s" : ''}} class="btn btn-primary"><i class="fas fa-check icon-white" aria-hidden="true"></i> {{ trans('general.save') }}</button>
</div> </div>
<!-- / partials/forms/edit/submit.blade.php --> <!-- / partials/forms/edit/submit.blade.php -->

View file

@ -163,6 +163,21 @@
</div> </div>
</div> </div>
<!-- Shortcuts enable -->
<div class="form-group {{ $errors->has('shortcuts_enabled') ? 'error' : '' }}">
<div class="col-md-3">
<strong> {{ trans('admin/settings/general.shortcuts_enabled') }}</strong>
</div>
<div class="col-md-9">
<label class="form-control">
<input type="checkbox" name="shortcuts_enabled" value="1" {{ old('shortcuts_enabled', $setting->shortcuts_enabled) ? 'checked' : '' }}>
{{ trans('general.yes') }}
</label>
{!! $errors->first('shortcuts_enabled', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
<p class="help-block">{!!trans('admin/settings/general.shortcuts_help_text') !!}</p>
</div>
</div>
<!-- Per Page --> <!-- Per Page -->
<div class="form-group {{ $errors->has('per_page') ? 'error' : '' }}"> <div class="form-group {{ $errors->has('per_page') ? 'error' : '' }}">

View file

@ -18,7 +18,7 @@
@if ($snipeSettings->ldap_enabled == 1) @if ($snipeSettings->ldap_enabled == 1)
<a href="{{ route('ldap/user') }}" class="btn btn-default pull-right"><span class="fas fa-sitemap"></span>{{trans('general.ldap_sync')}}</a> <a href="{{ route('ldap/user') }}" class="btn btn-default pull-right"><span class="fas fa-sitemap"></span>{{trans('general.ldap_sync')}}</a>
@endif @endif
<a href="{{ route('users.create') }}" accesskey="n" class="btn btn-primary pull-right" style="margin-right: 5px;"> {{ trans('general.create') }}</a> <a href="{{ route('users.create') }}" {{$snipeSettings->shortcuts_enabled == 1 ? "n" : ''}} class="btn btn-primary pull-right" style="margin-right: 5px;"> {{ trans('general.create') }}</a>
@endcan @endcan
@if (request('status')=='deleted') @if (request('status')=='deleted')

View file

@ -578,6 +578,8 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
Route::patch('/hardware/{asset}', [Api\AssetsController::class, 'update'])->name('api.assets.update'); Route::patch('/hardware/{asset}', [Api\AssetsController::class, 'update'])->name('api.assets.update');
Route::put('/hardware/{asset}', [Api\AssetsController::class, 'update'])->name('api.assets.put-update'); Route::put('/hardware/{asset}', [Api\AssetsController::class, 'update'])->name('api.assets.put-update');
Route::put('/hardware/{asset}', [Api\AssetsController::class, 'update'])->name('api.assets.put-update');
Route::resource('hardware', Route::resource('hardware',
Api\AssetsController::class, Api\AssetsController::class,
['names' => [ ['names' => [

View file

@ -1,6 +1,6 @@
<?php <?php
namespace Feature\Assets\Ui; namespace Tests\Feature\Assets\Ui;
use App\Models\Asset; use App\Models\Asset;
use App\Models\User; use App\Models\User;

View file

@ -1,6 +1,6 @@
<?php <?php
namespace Feature\Assets\Ui; namespace Tests\Feature\Assets\Ui;
use App\Models\Asset; use App\Models\Asset;
use App\Models\AssetModel; use App\Models\AssetModel;

View file

@ -1,10 +1,13 @@
<?php <?php
namespace Tests\Feature\Checkins\Ui; namespace Tests\Feature\Checkouts\Ui;
use App\Events\CheckoutableCheckedOut;
use App\Models\Asset; use App\Models\Asset;
use App\Models\Company;
use App\Models\Component; use App\Models\Component;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Event;
use Tests\TestCase; use Tests\TestCase;
class ComponentsCheckoutTest extends TestCase class ComponentsCheckoutTest extends TestCase
@ -18,6 +21,27 @@ class ComponentsCheckoutTest extends TestCase
->assertForbidden(); ->assertForbidden();
} }
public function test_cannot_checkout_across_companies_when_full_company_support_enabled()
{
Event::fake([CheckoutableCheckedOut::class]);
$this->settings->enableMultipleFullCompanySupport();
[$assetCompany, $componentCompany] = Company::factory()->count(2)->create();
$asset = Asset::factory()->for($assetCompany)->create();
$component = Component::factory()->for($componentCompany)->create();
$this->actingAs(User::factory()->superuser()->create())
->post(route('components.checkout.store', $component), [
'asset_id' => $asset->id,
'assigned_qty' => '1',
'redirect_option' => 'index',
]);
Event::assertNotDispatched(CheckoutableCheckedOut::class);
}
public function testComponentCheckoutPagePostIsRedirectedIfRedirectSelectionIsIndex() public function testComponentCheckoutPagePostIsRedirectedIfRedirectSelectionIsIndex()
{ {
$component = Component::factory()->create(); $component = Component::factory()->create();
@ -63,6 +87,4 @@ class ComponentsCheckoutTest extends TestCase
->assertStatus(302) ->assertStatus(302)
->assertRedirect(route('hardware.show', ['hardware' => $asset])); ->assertRedirect(route('hardware.show', ['hardware' => $asset]));
} }
} }

View file

@ -1,6 +1,6 @@
<?php <?php
namespace Tests\Feature\Consumables\Ui; namespace Tests\Feature\Licenses\Ui;
use App\Models\AssetModel; use App\Models\AssetModel;
use App\Models\Category; use App\Models\Category;

View file

@ -1,6 +1,6 @@
<?php <?php
namespace Tests\Feature\Consumables\Ui; namespace Tests\Feature\Licenses\Ui;
use App\Models\License; use App\Models\License;
use App\Models\Depreciation; use App\Models\Depreciation;

View file

@ -1,6 +1,6 @@
<?php <?php
namespace Tests\Feature\Consumables\Api; namespace Tests\Feature\Locations\Api;
use App\Models\Location; use App\Models\Location;
use App\Models\Asset; use App\Models\Asset;

View file

@ -1,10 +1,12 @@
<?php <?php
namespace Tests\Unit; namespace Tests\Unit;
use App\Models\Asset;
use App\Models\Category; use App\Models\Category;
use App\Models\Company; use App\Models\Company;
use App\Models\Component; use App\Models\Component;
use App\Models\Location; use App\Models\Location;
use App\Models\User;
use Tests\TestCase; use Tests\TestCase;
class ComponentTest extends TestCase class ComponentTest extends TestCase
@ -41,4 +43,58 @@ class ComponentTest extends TestCase
$this->assertInstanceOf(Category::class, $component->category); $this->assertInstanceOf(Category::class, $component->category);
$this->assertEquals('component', $component->category->category_type); $this->assertEquals('component', $component->category->category_type);
} }
public function test_num_checked_out_takes_does_not_scope_by_company()
{
$this->settings->enableMultipleFullCompanySupport();
[$companyA, $companyB] = Company::factory()->count(2)->create();
$componentForCompanyA = Component::factory()->for($companyA)->create(['qty' => 5]);
$assetForCompanyB = Asset::factory()->for($companyB)->create();
// Ideally, we shouldn't have a component attached to an
// asset from a different company but alas...
$componentForCompanyA->assets()->attach($componentForCompanyA->id, [
'component_id' => $componentForCompanyA->id,
'assigned_qty' => 4,
'asset_id' => $assetForCompanyB->id,
]);
$this->actingAs(User::factory()->superuser()->create());
$this->assertEquals(4, $componentForCompanyA->fresh()->numCheckedOut());
$this->actingAs(User::factory()->admin()->create());
$this->assertEquals(4, $componentForCompanyA->fresh()->numCheckedOut());
$this->actingAs(User::factory()->for($companyA)->create());
$this->assertEquals(4, $componentForCompanyA->fresh()->numCheckedOut());
}
public function test_num_remaining_takes_company_scoping_into_account()
{
$this->settings->enableMultipleFullCompanySupport();
[$companyA, $companyB] = Company::factory()->count(2)->create();
$componentForCompanyA = Component::factory()->for($companyA)->create(['qty' => 5]);
$assetForCompanyB = Asset::factory()->for($companyB)->create();
// Ideally, we shouldn't have a component attached to an
// asset from a different company but alas...
$componentForCompanyA->assets()->attach($componentForCompanyA->id, [
'component_id' => $componentForCompanyA->id,
'assigned_qty' => 4,
'asset_id' => $assetForCompanyB->id,
]);
$this->actingAs(User::factory()->superuser()->create());
$this->assertEquals(1, $componentForCompanyA->fresh()->numRemaining());
$this->actingAs(User::factory()->admin()->create());
$this->assertEquals(1, $componentForCompanyA->fresh()->numRemaining());
$this->actingAs(User::factory()->for($companyA)->create());
$this->assertEquals(1, $componentForCompanyA->fresh()->numRemaining());
}
} }