diff --git a/app/Http/Controllers/Account/AcceptanceController.php b/app/Http/Controllers/Account/AcceptanceController.php index f79ec1842f..3745c527fa 100644 --- a/app/Http/Controllers/Account/AcceptanceController.php +++ b/app/Http/Controllers/Account/AcceptanceController.php @@ -233,6 +233,7 @@ class AcceptanceController extends Controller 'logo' => $path_logo, 'date_settings' => $branding_settings->date_display_format, 'admin' => auth()->user()->present()?->fullName, + 'qty' => $acceptance->qty ?? 1, ]; if ($pdf_view_route!='') { @@ -338,6 +339,7 @@ class AcceptanceController extends Controller 'assigned_to' => $assigned_to, 'company_name' => $branding_settings->site_name, 'date_settings' => $branding_settings->date_display_format, + 'qty' => $acceptance->qty ?? 1, ]; if ($pdf_view_route!='') { @@ -346,7 +348,10 @@ class AcceptanceController extends Controller Storage::put('private_uploads/eula-pdfs/' .$pdf_filename, $pdf->output()); } - $acceptance->decline($sig_filename, $request->input('note')); + for ($i = 0; $i < ($acceptance->qty ?? 1); $i++) { + $acceptance->decline($sig_filename, $request->input('note')); + } + $acceptance->notify(new AcceptanceAssetDeclinedNotification($data)); Log::debug('New event acceptance.'); event(new CheckoutDeclined($acceptance)); diff --git a/app/Listeners/CheckoutableListener.php b/app/Listeners/CheckoutableListener.php index 9674bd63a3..4220e932fd 100644 --- a/app/Listeners/CheckoutableListener.php +++ b/app/Listeners/CheckoutableListener.php @@ -242,6 +242,12 @@ class CheckoutableListener $acceptance->checkoutable()->associate($event->checkoutable); $acceptance->assignedTo()->associate($event->checkedOutTo); + $acceptance->qty = 1; + + if (isset($event->checkoutable->checkout_qty)) { + $acceptance->qty = $event->checkoutable->checkout_qty; + } + $category = $this->getCategoryFromCheckoutable($event->checkoutable); if ($category?->alert_on_response) { diff --git a/app/Models/CheckoutAcceptance.php b/app/Models/CheckoutAcceptance.php index f65fb219a8..2ad6c0e558 100644 --- a/app/Models/CheckoutAcceptance.php +++ b/app/Models/CheckoutAcceptance.php @@ -148,4 +148,9 @@ class CheckoutAcceptance extends Model { return $query->whereNull('accepted_at')->whereNull('declined_at'); } + + public function scopeDeclined(Builder $query) + { + return $query->whereNull('accepted_at')->whereNotNull('declined_at'); + } } diff --git a/app/Notifications/AcceptanceAssetAcceptedNotification.php b/app/Notifications/AcceptanceAssetAcceptedNotification.php index 6c4e2e99e0..df7d745de4 100644 --- a/app/Notifications/AcceptanceAssetAcceptedNotification.php +++ b/app/Notifications/AcceptanceAssetAcceptedNotification.php @@ -31,6 +31,7 @@ class AcceptanceAssetAcceptedNotification extends Notification $this->company_name = $params['company_name']; $this->admin = $params['admin'] ?? null; $this->settings = Setting::getSettings(); + $this->qty = $params['qty'] ?? null; } @@ -72,6 +73,7 @@ class AcceptanceAssetAcceptedNotification extends Notification 'accepted_date' => $this->accepted_date, 'assigned_to' => $this->assigned_to, 'company_name' => $this->company_name, + 'qty' => $this->qty, 'intro_text' => trans('mail.acceptance_asset_accepted'), 'admin' => $this->admin, ]) diff --git a/app/Notifications/AcceptanceAssetAcceptedToUserNotification.php b/app/Notifications/AcceptanceAssetAcceptedToUserNotification.php index 22a97b709f..57e7b3fad4 100644 --- a/app/Notifications/AcceptanceAssetAcceptedToUserNotification.php +++ b/app/Notifications/AcceptanceAssetAcceptedToUserNotification.php @@ -29,7 +29,7 @@ class AcceptanceAssetAcceptedToUserNotification extends Notification $this->company_name = $params['company_name']; $this->settings = Setting::getSettings(); $this->file = $params['file'] ?? null; - + $this->qty = $params['qty'] ?? null; } /** @@ -66,6 +66,7 @@ class AcceptanceAssetAcceptedToUserNotification extends Notification 'accepted_date' => $this->accepted_date, 'assigned_to' => $this->assigned_to, 'company_name' => $this->company_name, + 'qty' => $this->qty, 'intro_text' => trans('mail.acceptance_asset_accepted_to_user', ['site_name' => $this->company_name ?? $this->settings->site_name]), ]) ->attach($pdf_path) diff --git a/app/Notifications/AcceptanceAssetDeclinedNotification.php b/app/Notifications/AcceptanceAssetDeclinedNotification.php index 0a9d6c211f..6ff06c7094 100644 --- a/app/Notifications/AcceptanceAssetDeclinedNotification.php +++ b/app/Notifications/AcceptanceAssetDeclinedNotification.php @@ -30,6 +30,7 @@ class AcceptanceAssetDeclinedNotification extends Notification $this->assigned_to = $params['assigned_to']; $this->company_name = $params['company_name']; $this->settings = Setting::getSettings(); + $this->qty = $params['qty'] ?? null; } /** @@ -69,6 +70,7 @@ class AcceptanceAssetDeclinedNotification extends Notification 'declined_date' => $this->declined_date, 'assigned_to' => $this->assigned_to, 'company_name' => $this->company_name, + 'qty' => $this->qty, 'intro_text' => trans('mail.acceptance_asset_declined'), ]) ->subject(trans('mail.acceptance_asset_declined')); diff --git a/database/migrations/2025_08_12_225214_add_qty_to_checkout_acceptances_table.php b/database/migrations/2025_08_12_225214_add_qty_to_checkout_acceptances_table.php new file mode 100644 index 0000000000..c5c7726213 --- /dev/null +++ b/database/migrations/2025_08_12_225214_add_qty_to_checkout_acceptances_table.php @@ -0,0 +1,31 @@ +unsignedInteger('qty')->nullable()->after('assigned_to_id')->default(null); + }); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::whenTableHasColumn('checkout_acceptances', 'qty', function () { + Schema::table('checkout_acceptances', function (Blueprint $table) { + $table->dropColumn('qty'); + }); + }); + } +}; diff --git a/resources/lang/en-US/general.php b/resources/lang/en-US/general.php index bfcad0b713..3516203331 100644 --- a/resources/lang/en-US/general.php +++ b/resources/lang/en-US/general.php @@ -353,9 +353,11 @@ return [ 'audit_overdue' => 'Overdue for Audit', 'accept' => 'Accept :asset', 'i_accept' => 'I accept', - 'i_decline_item' => 'Decline this item', - 'i_accept_item' => 'Accept this item', + 'i_accept_with_count' => 'I accept :count item|I accept :count items', + 'i_decline_item' => 'Decline this item|Decline these items', + 'i_accept_item' => 'Accept this item|Accept these items', 'i_decline' => 'I decline', + 'i_decline_with_count' => 'I decline :count item|I decline :count items', 'accept_decline' => 'Accept/Decline', 'sign_tos' => 'Sign below to indicate that you agree to the terms of service:', 'clear_signature' => 'Clear Signature', diff --git a/resources/views/account/accept/accept-accessory-eula.blade.php b/resources/views/account/accept/accept-accessory-eula.blade.php index 6109196c6e..da8410ccd1 100644 --- a/resources/views/account/accept/accept-accessory-eula.blade.php +++ b/resources/views/account/accept/accept-accessory-eula.blade.php @@ -24,6 +24,7 @@

{{ trans('general.date') }}: {{ date($date_settings) }}
{{ trans('general.asset_model') }}: {{ $item_model }}
+ {{ trans('general.quantity') }}: {{ $qty }}

@if ($eula) @@ -44,4 +45,4 @@ @endif - \ No newline at end of file + diff --git a/resources/views/account/accept/create.blade.php b/resources/views/account/accept/create.blade.php index 9612d21129..33c7e2ddc5 100644 --- a/resources/views/account/accept/create.blade.php +++ b/resources/views/account/accept/create.blade.php @@ -38,8 +38,13 @@

- {{ $acceptance->checkoutable->display_name }} - {{ (($acceptance->checkoutable) && ($acceptance->checkoutable->serial)) ? ' - '.trans('general.serial_number').': '.$acceptance->checkoutable->serial : '' }} +
+ {{ $acceptance->checkoutable->display_name }} + @if ($acceptance->qty > 1) + ×{{ $acceptance->qty }} + @endif +
+
{{ (($acceptance->checkoutable) && ($acceptance->checkoutable->serial)) ? trans('general.serial_number').': '.$acceptance->checkoutable->serial : '' }}

@@ -53,11 +58,19 @@
@@ -98,7 +111,7 @@
@@ -161,7 +174,7 @@ $("#showSubmit").show(); $("#submit-button").removeClass("btn-success").addClass("btn-danger").show(); $("#submitIcon").removeClass("fa-check").addClass("fa-times"); - $("#buttonText").text('{{ trans('general.i_decline_item') }}'); + $("#buttonText").text('{{ trans_choice('general.i_decline_item', $acceptance->qty ?? 1) }}'); $("#note").prop('required', true); } else if ($(this).is(':checked') && $(this).attr('id') === 'accepted') { @@ -169,16 +182,9 @@ $("#showSubmit").show(); $("#submit-button").removeClass("btn-danger").addClass("btn-success").show(); $("#submitIcon").removeClass("fa-check").addClass("fa-check"); - $("#buttonText").text('{{ trans('general.i_accept_item') }}'); + $("#buttonText").text('{{ trans_choice('general.i_accept_item', $acceptance->qty ?? 1) }}'); $("#note").prop('required', false); - - - } - }); - - - @stop diff --git a/resources/views/account/accept/index.blade.php b/resources/views/account/accept/index.blade.php index 52045fc86d..1957bec675 100755 --- a/resources/views/account/accept/index.blade.php +++ b/resources/views/account/accept/index.blade.php @@ -32,6 +32,7 @@ {{ trans('general.name')}} {{ trans('general.type')}} + {{ trans('general.qty') }} {{ trans('general.serial_number')}} {{ trans('table.actions')}} @@ -41,8 +42,9 @@ @if ($acceptance->checkoutable) {{ ($acceptance->checkoutable) ? $acceptance->checkoutable->present()->name : '' }} - {{ $acceptance->checkoutable_item_type }} - {{ ($acceptance->checkoutable) ? $acceptance->checkoutable->serial : '' }} + {{ $acceptance->checkoutable_item_type }} + {{ $acceptance->qty ?? '1' }} + {{ ($acceptance->checkoutable) ? $acceptance->checkoutable->serial : '' }} {{ trans('general.accept_decline') }} @else ----- diff --git a/resources/views/notifications/markdown/asset-acceptance.blade.php b/resources/views/notifications/markdown/asset-acceptance.blade.php index 2ed288b4fc..db2705e52a 100644 --- a/resources/views/notifications/markdown/asset-acceptance.blade.php +++ b/resources/views/notifications/markdown/asset-acceptance.blade.php @@ -37,6 +37,9 @@ @if (isset($item_serial)) | **{{ trans('mail.serial') }}** | {{ $item_serial }} | @endif +@if (isset($qty)) +| **{{ trans('general.qty') }}** | {{ $qty }} | +@endif @if (isset($admin)) | **{{ trans('general.administrator') }}** | {{ $admin }} | @endif diff --git a/tests/Feature/CheckoutAcceptances/Ui/AccessoryAcceptanceTest.php b/tests/Feature/CheckoutAcceptances/Ui/AccessoryAcceptanceTest.php index fef38623f8..9e20b52475 100644 --- a/tests/Feature/CheckoutAcceptances/Ui/AccessoryAcceptanceTest.php +++ b/tests/Feature/CheckoutAcceptances/Ui/AccessoryAcceptanceTest.php @@ -3,6 +3,7 @@ namespace Tests\Feature\CheckoutAcceptances\Ui; use App\Models\Accessory; +use App\Models\AccessoryCheckout; use App\Models\Asset; use App\Models\CheckoutAcceptance; use App\Models\User; @@ -96,4 +97,47 @@ class AccessoryAcceptanceTest extends TestCase $this->assertNull($acceptance->fresh()->accepted_at); } + + /** + * @link https://github.com/grokability/snipe-it/issues/17589 + */ + public function test_all_accessory_checkout_entries_are_removed_when_user_declines_acceptance() + { + $assignee = User::factory()->create(); + + $this->actingAs(User::factory()->checkoutAccessories()->create()); + + // create accessory that requires acceptance + $accessory = Accessory::factory()->requiringAcceptance()->create(['qty' => 5]); + + // checkout 3 accessories to the user + $this->post(route('accessories.checkout.store', $accessory), [ + 'assigned_user' => $assignee->id, + 'checkout_qty' => 3, + ]); + + $originalAccessoryCheckoutCount = AccessoryCheckout::count(); + + // find the acceptance to be declined + $checkoutAcceptance = CheckoutAcceptance::query() + ->where([ + 'assigned_to_id' => $assignee->id, + 'qty' => 3, + ]) + ->whereNull('accepted_at') + ->whereNull('declined_at') + ->whereHasMorph( + 'checkoutable', + [Accessory::class], + ) + ->sole(); + + // decline the checkout + $this->actingAs($assignee) + ->post(route('account.store-acceptance', $checkoutAcceptance), [ + 'asset_acceptance' => 'declined', + ]); + + $this->assertEquals($originalAccessoryCheckoutCount - 3, AccessoryCheckout::count()); + } }