mirror of
https://github.com/snipe/snipe-it.git
synced 2026-08-18 11:15:42 +00:00
Merge branch 'develop' into calendar-ui
This commit is contained in:
@ -151,6 +151,20 @@ class AccessoriesController extends Controller
|
||||
$cloned->id = null;
|
||||
$cloned->deleted_at = '';
|
||||
|
||||
// Restore the pre-Orders clone-as-fast-entry workflow: carry
|
||||
// the source item's most recent acquisition context onto the
|
||||
// cloned entry's create form so operators cloning a stock row
|
||||
// to restock don't have to copy-paste supplier / order # /
|
||||
// date / price from another tab. Field names match the
|
||||
// create-form input names so enrichInitialOrderFromRequest
|
||||
// in store() picks them up on save and writes them onto the
|
||||
// observer-created initial Order + OrderItem for the new row.
|
||||
foreach ($accessory->lastOrderPrefill() as $field => $value) {
|
||||
if ($value !== null) {
|
||||
$cloned->{$field} = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return view('accessories/edit')
|
||||
->with('cloned_model', $accessory_to_clone)
|
||||
->with('item', $cloned);
|
||||
|
||||
@ -252,6 +252,15 @@ class ComponentsController extends Controller
|
||||
$cloned_component->id = null;
|
||||
$cloned_component->deleted_at = null;
|
||||
|
||||
// See AccessoriesController::getClone — same rationale for
|
||||
// carrying the source item's most recent acquisition context
|
||||
// onto the cloned create form.
|
||||
foreach ($component->lastOrderPrefill() as $field => $value) {
|
||||
if ($value !== null) {
|
||||
$cloned_component->{$field} = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// Show the page
|
||||
return view('components/edit')
|
||||
->with('item', $cloned_component)
|
||||
|
||||
@ -255,6 +255,15 @@ class ConsumablesController extends Controller
|
||||
$consumable->id = null;
|
||||
$consumable->created_by = null;
|
||||
|
||||
// See AccessoriesController::getClone — same rationale for
|
||||
// carrying the source item's most recent acquisition context
|
||||
// onto the cloned create form.
|
||||
foreach ($consumable_to_close->lastOrderPrefill() as $field => $value) {
|
||||
if ($value !== null) {
|
||||
$consumable->{$field} = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return view('consumables/edit')
|
||||
->with('cloned_model', $consumable_to_close)
|
||||
->with('item', $consumable);
|
||||
|
||||
@ -139,6 +139,44 @@ trait HasOrders
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefill values for the create / clone form's initial-acquisition
|
||||
* fields. Distinct from lastOrderDefaults() because this shape
|
||||
* includes `order_number` (per-shipment, not a "default" concept)
|
||||
* and matches the request keys the create form posts back, so a
|
||||
* controller can loop the return array to assign values directly
|
||||
* onto a cloned model's attributes.
|
||||
*
|
||||
* Used by getClone() on Accessories / Consumables / Components to
|
||||
* carry the source item's most recent acquisition context onto the
|
||||
* cloned entry's create form, restoring the pre-Orders workflow
|
||||
* where cloning a stock entry pre-filled supplier / order # / date
|
||||
* / price for a fast restock. Items with no order history return
|
||||
* an all-null array.
|
||||
*
|
||||
* @return array{
|
||||
* supplier_id: ?int,
|
||||
* purchase_date: ?string,
|
||||
* purchase_cost: ?string,
|
||||
* order_number: ?string,
|
||||
* }
|
||||
*/
|
||||
public function lastOrderPrefill(): array
|
||||
{
|
||||
$line = $this->orderItems()
|
||||
->with('order:id,order_number,supplier_id,purchase_date')
|
||||
->latest('id')
|
||||
->first();
|
||||
$order = $line?->order;
|
||||
|
||||
return [
|
||||
'supplier_id' => $order?->supplier_id,
|
||||
'purchase_date' => $order?->purchase_date?->toDateString(),
|
||||
'purchase_cost' => $line?->price !== null ? (string) $line->price : null,
|
||||
'order_number' => $order?->order_number,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a Supplier for the "last acquisition" view (transformers,
|
||||
* info-panel, report callbacks). Same fallback ladder as
|
||||
|
||||
81
tests/Feature/Accessories/Ui/CloneAccessoryTest.php
Normal file
81
tests/Feature/Accessories/Ui/CloneAccessoryTest.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Accessories\Ui;
|
||||
|
||||
use App\Models\Accessory;
|
||||
use App\Models\Supplier;
|
||||
use App\Models\User;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* The Orders refactor moved supplier / purchase_date / purchase_cost /
|
||||
* order_number off the Accessory parent row and onto Order+OrderItem.
|
||||
* Plain `clone $accessory` no longer carries those fields to the create
|
||||
* form, so operators using the clone-to-restock workflow lost prefill
|
||||
* on four fields they were relying on. getClone() reads back the
|
||||
* source item's most recent order (via HasOrders::lastOrderPrefill)
|
||||
* and sets those values onto the cloned in-memory model so the create
|
||||
* form pre-populates them again.
|
||||
*/
|
||||
class CloneAccessoryTest extends TestCase
|
||||
{
|
||||
public function test_permission_required_to_clone_accessory(): void
|
||||
{
|
||||
$accessory = Accessory::factory()->create();
|
||||
|
||||
$this->actingAs(User::factory()->create())
|
||||
->get(route('clone/accessories', $accessory))
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_clone_page_renders(): void
|
||||
{
|
||||
$accessory = Accessory::factory()->create();
|
||||
|
||||
$this->actingAs(User::factory()->createAccessories()->create())
|
||||
->get(route('clone/accessories', $accessory))
|
||||
->assertOk();
|
||||
}
|
||||
|
||||
public function test_clone_prefills_supplier_and_order_context_from_last_order(): void
|
||||
{
|
||||
$supplier = Supplier::factory()->create();
|
||||
$source = Accessory::factory()
|
||||
->withInitialAcquisition($supplier, 42.50, '2026-04-15')
|
||||
->create(['qty' => 10]);
|
||||
|
||||
$source->orderItems()->latest('id')->first()->order()->update([
|
||||
'order_number' => 'PO-CLONE-PREFILL-42',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs(User::factory()->createAccessories()->create())
|
||||
->get(route('clone/accessories', $source))
|
||||
->assertOk();
|
||||
|
||||
$response->assertSee('name="supplier_id"', false);
|
||||
$response->assertSee('value="'.$supplier->id.'"', false);
|
||||
$response->assertSee('name="order_number"', false);
|
||||
$response->assertSee('value="PO-CLONE-PREFILL-42"', false);
|
||||
$response->assertSee('name="purchase_date"', false);
|
||||
$response->assertSee('value="2026-04-15"', false);
|
||||
$response->assertSee('name="purchase_cost"', false);
|
||||
$response->assertSee('value="42.50"', false);
|
||||
}
|
||||
|
||||
public function test_clone_leaves_prefill_fields_blank_when_source_has_no_order_history(): void
|
||||
{
|
||||
$source = Accessory::factory()->create(['qty' => 0]);
|
||||
|
||||
$this->assertNull(
|
||||
$source->orderItems()->latest('id')->first(),
|
||||
'Expected qty=0 accessory to have no observer-written order (baseline for the no-history case).',
|
||||
);
|
||||
|
||||
$response = $this->actingAs(User::factory()->createAccessories()->create())
|
||||
->get(route('clone/accessories', $source))
|
||||
->assertOk();
|
||||
|
||||
$response->assertSee('name="order_number"', false);
|
||||
$response->assertDontSee('value="PO-', false);
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
namespace Tests\Feature\Components\Ui;
|
||||
|
||||
use App\Models\Component;
|
||||
use App\Models\Supplier;
|
||||
use App\Models\User;
|
||||
use Tests\TestCase;
|
||||
|
||||
@ -34,4 +35,33 @@ class CloneComponentTest extends TestCase
|
||||
'Component to clone',
|
||||
], false);
|
||||
}
|
||||
|
||||
/**
|
||||
* See tests/Feature/Accessories/Ui/CloneAccessoryTest for the
|
||||
* Orders-refactor rationale behind the clone-prefill fix.
|
||||
*/
|
||||
public function test_clone_prefills_supplier_and_order_context_from_last_order(): void
|
||||
{
|
||||
$supplier = Supplier::factory()->create();
|
||||
$source = Component::factory()
|
||||
->withInitialAcquisition($supplier, 12.34, '2026-04-01')
|
||||
->create(['qty' => 5]);
|
||||
|
||||
$source->orderItems()->latest('id')->first()->order()->update([
|
||||
'order_number' => 'PO-COMP-CLONE-9',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs(User::factory()->createComponents()->create())
|
||||
->get(route('components.clone.create', $source))
|
||||
->assertOk();
|
||||
|
||||
$response->assertSee('name="supplier_id"', false);
|
||||
$response->assertSee('value="'.$supplier->id.'"', false);
|
||||
$response->assertSee('name="order_number"', false);
|
||||
$response->assertSee('value="PO-COMP-CLONE-9"', false);
|
||||
$response->assertSee('name="purchase_date"', false);
|
||||
$response->assertSee('value="2026-04-01"', false);
|
||||
$response->assertSee('name="purchase_cost"', false);
|
||||
$response->assertSee('value="12.34"', false);
|
||||
}
|
||||
}
|
||||
|
||||
58
tests/Feature/Consumables/Ui/CloneConsumableTest.php
Normal file
58
tests/Feature/Consumables/Ui/CloneConsumableTest.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Consumables\Ui;
|
||||
|
||||
use App\Models\Consumable;
|
||||
use App\Models\Supplier;
|
||||
use App\Models\User;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Consumable mirror of CloneAccessoryTest. See that file for the
|
||||
* Orders-refactor rationale behind the clone-prefill fix.
|
||||
*/
|
||||
class CloneConsumableTest extends TestCase
|
||||
{
|
||||
public function test_permission_required_to_clone_consumable(): void
|
||||
{
|
||||
$consumable = Consumable::factory()->create();
|
||||
|
||||
$this->actingAs(User::factory()->create())
|
||||
->get(route('consumables.clone.create', $consumable))
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_clone_page_renders(): void
|
||||
{
|
||||
$consumable = Consumable::factory()->create();
|
||||
|
||||
$this->actingAs(User::factory()->createConsumables()->create())
|
||||
->get(route('consumables.clone.create', $consumable))
|
||||
->assertOk();
|
||||
}
|
||||
|
||||
public function test_clone_prefills_supplier_and_order_context_from_last_order(): void
|
||||
{
|
||||
$supplier = Supplier::factory()->create();
|
||||
$source = Consumable::factory()
|
||||
->withInitialAcquisition($supplier, 5.75, '2026-05-10')
|
||||
->create(['qty' => 20]);
|
||||
|
||||
$source->orderItems()->latest('id')->first()->order()->update([
|
||||
'order_number' => 'PO-CONS-CLONE-1',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs(User::factory()->createConsumables()->create())
|
||||
->get(route('consumables.clone.create', $source))
|
||||
->assertOk();
|
||||
|
||||
$response->assertSee('name="supplier_id"', false);
|
||||
$response->assertSee('value="'.$supplier->id.'"', false);
|
||||
$response->assertSee('name="order_number"', false);
|
||||
$response->assertSee('value="PO-CONS-CLONE-1"', false);
|
||||
$response->assertSee('name="purchase_date"', false);
|
||||
$response->assertSee('value="2026-05-10"', false);
|
||||
$response->assertSee('name="purchase_cost"', false);
|
||||
$response->assertSee('value="5.75"', false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user