diff --git a/app/Http/Controllers/Accessories/AccessoriesController.php b/app/Http/Controllers/Accessories/AccessoriesController.php index a489fe144d..23db43705b 100755 --- a/app/Http/Controllers/Accessories/AccessoriesController.php +++ b/app/Http/Controllers/Accessories/AccessoriesController.php @@ -263,22 +263,12 @@ class AccessoriesController extends Controller } /** - * Apply an on-hand quantity delta (+/-) and log the change. The - * shared body lives on the HandlesAdjustQuantity trait; this method - * only owns the redirect-response shape. + * Apply an on-hand quantity delta (+/-) and log the change. Route + * exists here so route-model binding resolves against Accessory, + * everything else lives on HandlesAdjustQuantity. */ public function adjustQuantity(AdjustQuantityRequest $request, Accessory $accessory): RedirectResponse { - $error = $this->runAdjustQuantity($request, $accessory, 'accessories'); - - if ($error) { - return redirect()->back()->with('error', $error); - } - - // adjustQuantityRedirect keeps show-page opens landing on - // #history for the confirmation loop, and sends index-page - // opens back to the index so bulk adjust flows aren't broken - // by a forced navigation to the item detail page. - return $this->adjustQuantityRedirect($request, route('accessories.show', $accessory)); + return $this->adjustQuantityAsRedirect($request, $accessory); } } diff --git a/app/Http/Controllers/Api/AccessoriesController.php b/app/Http/Controllers/Api/AccessoriesController.php index 7f0c42cb97..5c80059974 100644 --- a/app/Http/Controllers/Api/AccessoriesController.php +++ b/app/Http/Controllers/Api/AccessoriesController.php @@ -363,18 +363,7 @@ class AccessoriesController extends Controller */ public function adjustQuantity(AdjustQuantityRequest $request, Accessory $accessory): JsonResponse { - $error = $this->runAdjustQuantity($request, $accessory, 'accessories'); - - if ($error !== null) { - return response()->json( - Helper::formatStandardApiResponse('error', null, $error), - 422, - ); - } - - return response()->json( - Helper::formatStandardApiResponse('success', $accessory->fresh(), trans('general.adjust_quantity_success')), - ); + return $this->adjustQuantityAsJson($request, $accessory); } /** diff --git a/app/Http/Controllers/Api/ComponentsController.php b/app/Http/Controllers/Api/ComponentsController.php index 8c0c58f6a7..a6c01aa64a 100644 --- a/app/Http/Controllers/Api/ComponentsController.php +++ b/app/Http/Controllers/Api/ComponentsController.php @@ -296,18 +296,7 @@ class ComponentsController extends Controller */ public function adjustQuantity(AdjustQuantityRequest $request, Component $component): JsonResponse { - $error = $this->runAdjustQuantity($request, $component, 'components'); - - if ($error !== null) { - return response()->json( - Helper::formatStandardApiResponse('error', null, $error), - 422, - ); - } - - return response()->json( - Helper::formatStandardApiResponse('success', $component->fresh(), trans('general.adjust_quantity_success')), - ); + return $this->adjustQuantityAsJson($request, $component); } /** diff --git a/app/Http/Controllers/Api/ConsumablesController.php b/app/Http/Controllers/Api/ConsumablesController.php index e863458a3f..1f6c7e0ad7 100644 --- a/app/Http/Controllers/Api/ConsumablesController.php +++ b/app/Http/Controllers/Api/ConsumablesController.php @@ -281,18 +281,7 @@ class ConsumablesController extends Controller */ public function adjustQuantity(AdjustQuantityRequest $request, Consumable $consumable): JsonResponse { - $error = $this->runAdjustQuantity($request, $consumable, 'consumables'); - - if ($error !== null) { - return response()->json( - Helper::formatStandardApiResponse('error', null, $error), - 422, - ); - } - - return response()->json( - Helper::formatStandardApiResponse('success', $consumable->fresh(), trans('general.adjust_quantity_success')), - ); + return $this->adjustQuantityAsJson($request, $consumable); } /** diff --git a/app/Http/Controllers/Components/ComponentsController.php b/app/Http/Controllers/Components/ComponentsController.php index 87f0e73c8c..0968cdfda9 100644 --- a/app/Http/Controllers/Components/ComponentsController.php +++ b/app/Http/Controllers/Components/ComponentsController.php @@ -259,20 +259,12 @@ class ComponentsController extends Controller } /** - * Apply an on-hand quantity delta (+/-) and log the change. The - * shared body lives on the HandlesAdjustQuantity trait; this method - * only owns the redirect-response shape. + * Apply an on-hand quantity delta (+/-) and log the change. Route + * exists here so route-model binding resolves against Component, + * everything else lives on HandlesAdjustQuantity. */ public function adjustQuantity(AdjustQuantityRequest $request, Component $component): RedirectResponse { - $error = $this->runAdjustQuantity($request, $component, 'components'); - - if ($error) { - return redirect()->back()->with('error', $error); - } - - // See AccessoriesController::adjustQuantity — helper picks - // between show-page-#history and back-to-referer. - return $this->adjustQuantityRedirect($request, route('components.show', $component)); + return $this->adjustQuantityAsRedirect($request, $component); } } diff --git a/app/Http/Controllers/Consumables/ConsumablesController.php b/app/Http/Controllers/Consumables/ConsumablesController.php index 97d5fdd17e..71056f3d1b 100644 --- a/app/Http/Controllers/Consumables/ConsumablesController.php +++ b/app/Http/Controllers/Consumables/ConsumablesController.php @@ -261,20 +261,12 @@ class ConsumablesController extends Controller } /** - * Apply an on-hand quantity delta (+/-) and log the change. The - * shared body lives on the HandlesAdjustQuantity trait; this method - * only owns the redirect-response shape. + * Apply an on-hand quantity delta (+/-) and log the change. Route + * exists here so route-model binding resolves against Consumable, + * everything else lives on HandlesAdjustQuantity. */ public function adjustQuantity(AdjustQuantityRequest $request, Consumable $consumable): RedirectResponse { - $error = $this->runAdjustQuantity($request, $consumable, 'consumables'); - - if ($error) { - return redirect()->back()->with('error', $error); - } - - // See AccessoriesController::adjustQuantity — helper picks - // between show-page-#history and back-to-referer. - return $this->adjustQuantityRedirect($request, route('consumables.show', $consumable)); + return $this->adjustQuantityAsRedirect($request, $consumable); } } diff --git a/app/Http/Traits/HandlesAdjustQuantity.php b/app/Http/Traits/HandlesAdjustQuantity.php index ef3df58f09..8812007956 100644 --- a/app/Http/Traits/HandlesAdjustQuantity.php +++ b/app/Http/Traits/HandlesAdjustQuantity.php @@ -10,6 +10,7 @@ use App\Models\Order; use App\Models\OrderItem; use DomainException; use Illuminate\Database\Eloquent\Model; +use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -63,9 +64,9 @@ trait HandlesAdjustQuantity /** * Land the post-save redirect on the page the operator was on when - * they opened the modal. From the item's show page → the show page + * they opened the modal. From the item's show page: the show page * with the `#history` fragment so the newly-written log entry is - * visible as confirmation. From an index / listing page → back to + * visible as confirmation. From an index / listing page: back to * that listing so bulk-adjust flows don't force the operator to * back out and re-navigate for every item. Referer is validated * as same-origin to prevent open-redirect abuse; missing or @@ -83,6 +84,48 @@ trait HandlesAdjustQuantity return redirect()->to($itemShowUrl)->withFragment('history')->with('success', $success); } + /** + * Full web controller flow. Runs the shared work and wraps the + * outcome in the standard redirect-with-flash shape. Route target + * derives from Controller::$map_class_url_segment so a new inventory + * model that adopts HandlesAdjustQuantity only needs an entry in + * that map, not a per-model method or a controller-side segment + * string. + */ + protected function adjustQuantityAsRedirect(AdjustQuantityRequest $request, Model $model): RedirectResponse + { + $segment = Controller::getMapClassUrlSegment()[$model::class]; + $error = $this->runAdjustQuantity($request, $model, $segment); + + if ($error) { + return redirect()->back()->with('error', $error); + } + + return $this->adjustQuantityRedirect($request, route("$segment.show", $model)); + } + + /** + * Full API controller flow. Runs the shared work and wraps the + * outcome in the standard Snipe-IT JSON envelope. 422 on validation + * / floor errors, 200 with the refreshed model on success. + */ + protected function adjustQuantityAsJson(AdjustQuantityRequest $request, Model $model): JsonResponse + { + $segment = Controller::getMapClassUrlSegment()[$model::class]; + $error = $this->runAdjustQuantity($request, $model, $segment); + + if ($error !== null) { + return response()->json( + Helper::formatStandardApiResponse('error', null, $error), + 422, + ); + } + + return response()->json( + Helper::formatStandardApiResponse('success', $model->fresh(), trans('general.adjust_quantity_success')), + ); + } + /** * Find or create an Order from the request payload and append one * OrderItem line for the model / delta being adjusted. Returns the diff --git a/app/Models/Accessory.php b/app/Models/Accessory.php index 13d1d454fb..74f37d57e4 100755 --- a/app/Models/Accessory.php +++ b/app/Models/Accessory.php @@ -464,73 +464,6 @@ class Accessory extends SnipeModel $accessory_checkout->limit(1)->delete(); } - /** - * Sum every OrderItem's line total (qty × price) grouped by the - * parent Order's currency, so mixed-currency acquisitions render - * as a per-currency breakdown instead of a single misleading total. - * - * Falls back to `qty × parent.purchase_cost` under the system's - * default_currency when the item has no OrderItems yet (legacy - * rows uncaught by backfill, or brand-new items with a purchase - * cost set but no acquisitions recorded). - * - * Returns [] when both paths are empty. The info-panel skips the - * "Total cost" line entirely in that case rather than showing 0. - * - * @return array currency code => sum in that currency - */ - public function totalCostSumByCurrency(): array - { - $lines = $this->orderItems()->with('order:id,currency')->get(); - - $totals = $lines->reduce(function (array $carry, OrderItem $line) { - if ($line->price === null) { - return $carry; - } - $currency = $line->order?->currency - ?? Setting::getSettings()?->default_currency - ?? ''; - $carry[$currency] = ($carry[$currency] ?? 0) + ($line->qty * (float) $line->price); - - return $carry; - }, []); - - // No fallback for unaccounted qty. Orders / OrderItems is the - // single source of truth for acquisition cost. If lines don't - // cover the current on-hand qty, the display honestly shows - // "we don't know the cost of those units" (empty totals). - - return $totals; - } - - /** - * Naive cross-currency sum, kept for backwards compatibility with - * external callers. New code should prefer totalCostSumByCurrency() - * so mixed-currency totals stay disambiguated. - */ - public function totalCostSum() - { - return array_sum($this->totalCostSumByCurrency()) ?: null; - } - - /** - * True when every recorded acquisition for this item came from the - * same supplier. Compares distinct supplier_ids across every Order - * linked via OrderItems. The info-panel's supplier row hides itself - * when this returns false so a single supplier name doesn't - * misrepresent multi-supplier history. - */ - public function hasConsistentSupplier(): bool - { - return $this->orderItems() - ->with('order:id,supplier_id') - ->get() - ->map(fn (OrderItem $line) => $line->order?->supplier_id) - ->filter() - ->unique() - ->count() <= 1; - } - /** * ----------------------------------------------- * BEGIN MUTATORS diff --git a/app/Models/Component.php b/app/Models/Component.php index 18e243360b..1b2616cfd6 100644 --- a/app/Models/Component.php +++ b/app/Models/Component.php @@ -417,69 +417,6 @@ class Component extends SnipeModel return $this->qty - $this->numCheckedOut(); } - /** - * Sum every OrderItem's line total (qty × price) grouped by the - * parent Order's currency, so mixed-currency acquisitions render - * as a per-currency breakdown instead of a single misleading total. - * - * Falls back to `qty × parent.purchase_cost` under the system's - * default_currency when the item has no OrderItems yet (legacy - * rows uncaught by backfill, or brand-new items with a purchase - * cost set but no acquisitions recorded). - * - * Returns [] when both paths are empty. The info-panel skips the - * "Total cost" line entirely in that case rather than showing 0. - * - * @return array currency code => sum in that currency - */ - public function totalCostSumByCurrency(): array - { - $lines = $this->orderItems()->with('order:id,currency')->get(); - - $totals = $lines->reduce(function (array $carry, OrderItem $line) { - if ($line->price === null) { - return $carry; - } - $currency = $line->order?->currency - ?? Setting::getSettings()?->default_currency - ?? ''; - $carry[$currency] = ($carry[$currency] ?? 0) + ($line->qty * (float) $line->price); - - return $carry; - }, []); - - // Orders / OrderItems is the single source of truth. No - // fallback to legacy_* columns (those will be dropped in a - // later version). See Accessory::totalCostSumByCurrency. - - return $totals; - } - - /** - * Naive cross-currency sum, kept for backwards compatibility with - * external callers. New code should prefer totalCostSumByCurrency() - * so mixed-currency totals stay disambiguated. - */ - public function totalCostSum() - { - return array_sum($this->totalCostSumByCurrency()) ?: null; - } - - /** - * True when every recorded acquisition for this item came from the - * same supplier. Info-panel supplier row hides when false so a - * single supplier name doesn't misrepresent multi-supplier history. - */ - public function hasConsistentSupplier(): bool - { - return $this->orderItems() - ->with('order:id,supplier_id') - ->get() - ->map(fn (OrderItem $line) => $line->order?->supplier_id) - ->filter() - ->unique() - ->count() <= 1; - } /** * ----------------------------------------------- * BEGIN MUTATORS diff --git a/app/Models/Consumable.php b/app/Models/Consumable.php index c2e8ed2994..95cb09a4e4 100644 --- a/app/Models/Consumable.php +++ b/app/Models/Consumable.php @@ -385,70 +385,6 @@ class Consumable extends SnipeModel return $remaining; } - /** - * Sum every OrderItem's line total (qty × price) grouped by the - * parent Order's currency, so mixed-currency acquisitions render - * as a per-currency breakdown instead of a single misleading total. - * - * Falls back to `qty × parent.purchase_cost` under the system's - * default_currency when the item has no OrderItems yet (legacy - * rows uncaught by backfill, or brand-new items with a purchase - * cost set but no acquisitions recorded). - * - * Returns [] when both paths are empty. The info-panel skips the - * "Total cost" line entirely in that case rather than showing 0. - * - * @return array currency code => sum in that currency - */ - public function totalCostSumByCurrency(): array - { - $lines = $this->orderItems()->with('order:id,currency')->get(); - - $totals = $lines->reduce(function (array $carry, OrderItem $line) { - if ($line->price === null) { - return $carry; - } - $currency = $line->order?->currency - ?? Setting::getSettings()?->default_currency - ?? ''; - $carry[$currency] = ($carry[$currency] ?? 0) + ($line->qty * (float) $line->price); - - return $carry; - }, []); - - // Orders / OrderItems is the single source of truth. No - // fallback to legacy_* columns (those will be dropped in a - // later version). See Accessory::totalCostSumByCurrency. - - return $totals; - } - - /** - * Naive cross-currency sum, kept for backwards compatibility with - * external callers. New code should prefer totalCostSumByCurrency() - * so mixed-currency totals stay disambiguated. - */ - public function totalCostSum() - { - return array_sum($this->totalCostSumByCurrency()) ?: null; - } - - /** - * True when every recorded acquisition for this item came from the - * same supplier. Info-panel supplier row hides when false so a - * single supplier name doesn't misrepresent multi-supplier history. - */ - public function hasConsistentSupplier(): bool - { - return $this->orderItems() - ->with('order:id,supplier_id') - ->get() - ->map(fn (OrderItem $line) => $line->order?->supplier_id) - ->filter() - ->unique() - ->count() <= 1; - } - /** * Get the list of checkouts for this consumable *