From 5adabfccaa71ed42f0c36d8c16c71e9d929af8cc Mon Sep 17 00:00:00 2001 From: snipe Date: Sat, 15 Aug 2026 09:59:51 +0100 Subject: [PATCH 1/3] Added lastOrderPrefill for cloning --- .../Accessories/AccessoriesController.php | 14 +++++++ .../Components/ComponentsController.php | 9 +++++ .../Consumables/ConsumablesController.php | 9 +++++ app/Models/Traits/HasOrders.php | 38 +++++++++++++++++++ 4 files changed, 70 insertions(+) diff --git a/app/Http/Controllers/Accessories/AccessoriesController.php b/app/Http/Controllers/Accessories/AccessoriesController.php index 23db43705b..cc19a704d0 100755 --- a/app/Http/Controllers/Accessories/AccessoriesController.php +++ b/app/Http/Controllers/Accessories/AccessoriesController.php @@ -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); diff --git a/app/Http/Controllers/Components/ComponentsController.php b/app/Http/Controllers/Components/ComponentsController.php index 0968cdfda9..38c1947f37 100644 --- a/app/Http/Controllers/Components/ComponentsController.php +++ b/app/Http/Controllers/Components/ComponentsController.php @@ -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) diff --git a/app/Http/Controllers/Consumables/ConsumablesController.php b/app/Http/Controllers/Consumables/ConsumablesController.php index 71056f3d1b..4cb328c9c3 100644 --- a/app/Http/Controllers/Consumables/ConsumablesController.php +++ b/app/Http/Controllers/Consumables/ConsumablesController.php @@ -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); diff --git a/app/Models/Traits/HasOrders.php b/app/Models/Traits/HasOrders.php index 9983408e89..3d372f0f7e 100644 --- a/app/Models/Traits/HasOrders.php +++ b/app/Models/Traits/HasOrders.php @@ -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 From 25b56e1a520cbf83fd16a60fca80c92b2fbb1241 Mon Sep 17 00:00:00 2001 From: snipe Date: Sat, 15 Aug 2026 10:02:43 +0100 Subject: [PATCH 2/3] Added clone tests --- .../Accessories/Ui/CloneAccessoryTest.php | 81 +++++++++++++++++++ .../Components/Ui/CloneComponentTest.php | 30 +++++++ .../Consumables/Ui/CloneConsumableTest.php | 58 +++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 tests/Feature/Accessories/Ui/CloneAccessoryTest.php create mode 100644 tests/Feature/Consumables/Ui/CloneConsumableTest.php diff --git a/tests/Feature/Accessories/Ui/CloneAccessoryTest.php b/tests/Feature/Accessories/Ui/CloneAccessoryTest.php new file mode 100644 index 0000000000..f83e41f34a --- /dev/null +++ b/tests/Feature/Accessories/Ui/CloneAccessoryTest.php @@ -0,0 +1,81 @@ +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); + } +} diff --git a/tests/Feature/Components/Ui/CloneComponentTest.php b/tests/Feature/Components/Ui/CloneComponentTest.php index dca01f7ad3..198b967a3f 100644 --- a/tests/Feature/Components/Ui/CloneComponentTest.php +++ b/tests/Feature/Components/Ui/CloneComponentTest.php @@ -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); + } } diff --git a/tests/Feature/Consumables/Ui/CloneConsumableTest.php b/tests/Feature/Consumables/Ui/CloneConsumableTest.php new file mode 100644 index 0000000000..e9343765d8 --- /dev/null +++ b/tests/Feature/Consumables/Ui/CloneConsumableTest.php @@ -0,0 +1,58 @@ +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); + } +} From f767340c18a0b1d3a928ef954102bc4fe5eb97be Mon Sep 17 00:00:00 2001 From: snipe Date: Sat, 15 Aug 2026 10:11:16 +0100 Subject: [PATCH 3/3] Temp removed calendar button --- resources/views/layouts/default.blade.php | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/resources/views/layouts/default.blade.php b/resources/views/layouts/default.blade.php index 0a233a1e31..4b53530774 100644 --- a/resources/views/layouts/default.blade.php +++ b/resources/views/layouts/default.blade.php @@ -568,14 +568,7 @@ @endif - @can('view', \App\Models\Asset::class) - routeIs('calendar.index') ? ' class="active" aria-current="page"' : '') !!}> - - - {{ trans('general.calendar') }} - - - @endcan + @can('view', \App\Models\License::class) is('licenses*') ? ' class="active" aria-current="page"' : '') !!}>