FMCS/Floater: Refactor logic into the companyable trait

This commit is contained in:
snipe
2026-06-13 12:36:15 +01:00
parent 8d0a6af2aa
commit 2033f25386
5 changed files with 66 additions and 76 deletions
@@ -10,7 +10,6 @@ use App\Http\Traits\CheckInOutTrait;
use App\Models\Accessory;
use App\Models\AccessoryCheckout;
use App\Models\CheckoutAcceptance;
use App\Models\Setting;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Contracts\View\View;
@@ -67,31 +66,18 @@ class AccessoryCheckoutController extends Controller
$target = $this->determineCheckoutTarget();
session()->put(['checkout_to_type' => $target]);
if (Setting::getSettings()->full_multiple_companies_support == '1' && $accessory->company_id) {
if ($target instanceof User) {
// Users may belong to multiple companies; canReceiveFromCompany() checks the pivot.
$mismatch = ! $target->canReceiveFromCompany($accessory->company_id);
} elseif (is_null($target->company_id)) {
// Target has no company — only a mismatch when floater mode is off.
$mismatch = ! Setting::getSettings()->null_company_is_floater;
} else {
// Both sides have a company; require an exact match.
$mismatch = (int) $target->company_id !== (int) $accessory->company_id;
}
if (! $accessory->canCheckoutTo($target)) {
$targetType = match (class_basename($target)) {
'User' => trans('general.user'),
'Location' => trans('general.location'),
default => trans('general.asset'),
};
if ($mismatch) {
$targetType = match (class_basename($target)) {
'User' => trans('general.user'),
'Location' => trans('general.location'),
default => trans('general.asset'),
};
return redirect()->back()->with('error', trans('general.error_checkout_company_mismatch', [
'item' => trans('general.accessory').' "'.$accessory->name.'"',
'item_company' => $accessory->company?->name ?? trans('general.unassigned'),
'target' => $targetType.' "'.($target->name ?? $target->username ?? $target->id).'"',
]));
}
return redirect()->back()->with('error', trans('general.error_checkout_company_mismatch', [
'item' => trans('general.accessory').' "'.$accessory->name.'"',
'item_company' => $accessory->company?->name ?? trans('general.unassigned'),
'target' => $targetType.' "'.($target->name ?? $target->username ?? $target->id).'"',
]));
}
$accessory->checkout_qty = $request->input('checkout_qty', 1);
@@ -9,7 +9,6 @@ use App\Http\Requests\AssetCheckoutRequest;
use App\Http\Traits\CheckInOutTrait;
use App\Models\Asset;
use App\Models\CheckoutAcceptance;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\ModelNotFoundException;
@@ -119,35 +118,18 @@ class AssetCheckoutController extends Controller
// Add any custom fields that should be included in the checkout
$asset->customFieldsForCheckinCheckout('display_checkout');
$settings = Setting::getSettings();
if (! $asset->canCheckoutTo($target)) {
$targetType = match (class_basename($target)) {
'User' => trans('general.user'),
'Location' => trans('general.location'),
default => trans('general.asset'),
};
// Locations have no company, so we only enforce FMCS when both sides have a company_id.
// For users with multiple companies, check all their associated companies via the pivot.
if ($settings->full_multiple_companies_support && ! is_null($asset->company_id)) {
if ($target instanceof User) {
// Users may belong to multiple companies; canReceiveFromCompany() checks the pivot.
$mismatch = ! $target->canReceiveFromCompany((int) $asset->company_id);
} elseif (is_null($target->company_id)) {
// Target has no company — only a mismatch when floater mode is off.
$mismatch = ! $settings->null_company_is_floater;
} else {
// Both sides have a company; require an exact match.
$mismatch = (int) $target->company_id !== (int) $asset->company_id;
}
if ($mismatch) {
$targetType = match (class_basename($target)) {
'User' => trans('general.user'),
'Location' => trans('general.location'),
default => trans('general.asset'),
};
return redirect()->route('hardware.checkout.create', $asset)->with('error', trans('general.error_checkout_company_mismatch', [
'item' => trans('general.asset').' "'.$asset->display_name.'"',
'item_company' => $asset->company?->name ?? trans('general.unassigned'),
'target' => $targetType.' "'.($target->name ?? $target->username ?? $target->id).'"',
]));
}
return redirect()->route('hardware.checkout.create', $asset)->with('error', trans('general.error_checkout_company_mismatch', [
'item' => trans('general.asset').' "'.$asset->display_name.'"',
'item_company' => $asset->company?->name ?? trans('general.unassigned'),
'target' => $targetType.' "'.($target->name ?? $target->username ?? $target->id).'"',
]));
}
session()->put([
@@ -7,7 +7,6 @@ use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\CheckoutAcceptance;
use App\Models\Consumable;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\View\View;
@@ -97,11 +96,7 @@ class ConsumableCheckoutController extends Controller
return redirect()->route('consumables.checkout.show', $consumable)->with('error', trans('admin/consumables/message.checkout.user_does_not_exist'))->withInput();
}
if (
Setting::getSettings()->full_multiple_companies_support == '1'
&& $consumable->company_id
&& ! $user->canReceiveFromCompany($consumable->company_id)
) {
if (! $consumable->canCheckoutTo($user)) {
return redirect()->back()->with('error', trans('general.error_checkout_company_mismatch', [
'item' => trans('general.consumable').' "'.$consumable->name.'"',
'item_company' => $consumable->company?->name ?? trans('general.unassigned'),
@@ -99,25 +99,16 @@ class LicenseCheckoutController extends Controller
if (Setting::getSettings()->full_multiple_companies_support == '1') {
if ($request->filled('asset_id')) {
$fmcsTarget = Asset::find($request->input('asset_id'));
if ($fmcsTarget && $license->company_id) {
if (is_null($fmcsTarget->company_id)) {
// Target asset has no company — only a mismatch when floater mode is off.
$mismatch = ! Setting::getSettings()->null_company_is_floater;
} else {
// Both sides have a company; require an exact match.
$mismatch = $license->company_id !== $fmcsTarget->company_id;
}
if ($mismatch) {
return redirect()->route('licenses.index')->with('error', trans('general.error_checkout_company_mismatch', [
'item' => trans('general.license').' "'.$license->name.'"',
'item_company' => $license->company?->name ?? trans('general.unassigned'),
'target' => trans('general.asset').' "'.($fmcsTarget->name ?? $fmcsTarget->asset_tag).'"',
]));
}
if ($fmcsTarget && ! $license->canCheckoutTo($fmcsTarget)) {
return redirect()->route('licenses.index')->with('error', trans('general.error_checkout_company_mismatch', [
'item' => trans('general.license').' "'.$license->name.'"',
'item_company' => $license->company?->name ?? trans('general.unassigned'),
'target' => trans('general.asset').' "'.$fmcsTarget->display_name.'"',
]));
}
} elseif ($request->filled('assigned_to')) {
$fmcsTarget = User::find($request->input('assigned_to'));
if ($fmcsTarget && $license->company_id && ! $fmcsTarget->canReceiveFromCompany($license->company_id)) {
if ($fmcsTarget && ! $license->canCheckoutTo($fmcsTarget)) {
return redirect()->route('licenses.index')->with('error', trans('general.error_checkout_company_mismatch', [
'item' => trans('general.license').' "'.$license->name.'"',
'item_company' => $license->company?->name ?? trans('general.unassigned'),
+36
View File
@@ -3,6 +3,9 @@
namespace App\Models\Traits;
use App\Models\CompanyableScope;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
trait CompanyableTrait
{
@@ -18,4 +21,37 @@ trait CompanyableTrait
{
static::addGlobalScope(new CompanyableScope);
}
/**
* Whether this item may be checked out to the given target under FMCS rules.
*
* Returns true when:
* - FMCS is disabled, OR
* - this item has no company (uncompanied items are unrestricted), OR
* - target is a User whose company pivot includes this item's company, OR
* - target has no company and null_company_is_floater is enabled, OR
* - target's company_id exactly matches this item's company_id.
*/
public function canCheckoutTo(Model $target): bool
{
$settings = Setting::getSettings();
if (! $settings->full_multiple_companies_support) {
return true;
}
if (! $this->company_id) {
return true;
}
if ($target instanceof User) {
return $target->canReceiveFromCompany((int) $this->company_id);
}
if (is_null($target->company_id)) {
return (bool) $settings->null_company_is_floater;
}
return (int) $target->company_id === (int) $this->company_id;
}
}