diff --git a/app/Http/Controllers/Purchase/RfqPortalController.php b/app/Http/Controllers/Purchase/RfqPortalController.php index 12daa97..b5b998d 100644 --- a/app/Http/Controllers/Purchase/RfqPortalController.php +++ b/app/Http/Controllers/Purchase/RfqPortalController.php @@ -61,14 +61,16 @@ class RfqPortalController extends Controller } $validated = $request->validate([ - 'terms' => ['accepted'], - 'confirm_code' => ['required', 'string'], - 'lead_time_days' => ['nullable', 'integer', 'min:0'], - 'payment_terms' => ['nullable', 'string', 'max:200'], - 'notes' => ['nullable', 'string', 'max:1000'], - 'items' => ['required', 'array'], - 'items.*.unit_price' => ['required', 'numeric', 'min:0'], - 'items.*.is_vatable' => ['nullable', 'boolean'], + 'terms' => ['accepted'], + 'confirm_code' => ['required', 'string'], + 'lead_time_days' => ['nullable', 'integer', 'min:0'], + 'payment_terms' => ['nullable', 'string', 'max:200'], + 'notes' => ['nullable', 'string', 'max:1000'], + 'items' => ['required', 'array'], + 'items.*.unit_price' => ['nullable', 'numeric', 'min:0'], + 'items.*.is_vatable' => ['nullable', 'boolean'], + 'items.*.not_available' => ['nullable', 'boolean'], + 'items.*.supplier_description' => ['nullable', 'string', 'max:500'], ]); $expectedCode = session('rfq_confirm_' . $token); @@ -98,24 +100,31 @@ class RfqPortalController extends Controller $vatRate = (float) Setting::get('vat_rate', 0); foreach ($purchaseItems as $i => $item) { - $unitPrice = (float)($validated['items'][$i]['unit_price'] ?? 0); - $qty = (float)$item->quantity_required; - $totalPrice = round($unitPrice * $qty, 3); - $isVatable = !empty($validated['items'][$i]['is_vatable']); - $subtotal += $totalPrice; + $notAvailable = !empty($validated['items'][$i]['not_available']); + $unitPrice = $notAvailable ? 0 : (float)($validated['items'][$i]['unit_price'] ?? 0); + $qty = (float)$item->quantity_required; + $totalPrice = $notAvailable ? 0 : round($unitPrice * $qty, 3); + $isVatable = !$notAvailable && !empty($validated['items'][$i]['is_vatable']); + $supplierDescription = !empty($validated['items'][$i]['supplier_description']) + ? trim($validated['items'][$i]['supplier_description']) + : null; + + $subtotal += $totalPrice; if ($isVatable && $vatRate > 0) { $vatAmount += round($totalPrice * $vatRate / 100, 3); } SupplierQuoteItem::create([ - 'supplier_quote_id' => $quote->id, - 'description' => $item->description, - 'unit' => $item->unit ?? '', - 'quantity' => $qty, - 'unit_price' => $unitPrice, - 'total_price' => $totalPrice, - 'is_vatable' => $isVatable, + 'supplier_quote_id' => $quote->id, + 'description' => $item->description, + 'supplier_description'=> $supplierDescription, + 'unit' => $item->unit ?? '', + 'quantity' => $qty, + 'unit_price' => $unitPrice, + 'total_price' => $totalPrice, + 'is_vatable' => $isVatable, + 'not_available' => $notAvailable, ]); } diff --git a/app/Models/SupplierQuoteItem.php b/app/Models/SupplierQuoteItem.php index 500c29b..639dc4b 100644 --- a/app/Models/SupplierQuoteItem.php +++ b/app/Models/SupplierQuoteItem.php @@ -7,11 +7,13 @@ use Illuminate\Database\Eloquent\Model; class SupplierQuoteItem extends Model { protected $fillable = [ - 'supplier_quote_id', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'is_vatable', + 'supplier_quote_id', 'description', 'supplier_description', 'unit', 'quantity', + 'unit_price', 'total_price', 'is_vatable', 'not_available', ]; protected $casts = [ - 'is_vatable' => 'boolean', + 'is_vatable' => 'boolean', + 'not_available' => 'boolean', ]; public function quote() diff --git a/database/migrations/2026_06_01_091728_add_supplier_description_and_not_available_to_supplier_quote_items.php b/database/migrations/2026_06_01_091728_add_supplier_description_and_not_available_to_supplier_quote_items.php new file mode 100644 index 0000000..3753d9b --- /dev/null +++ b/database/migrations/2026_06_01_091728_add_supplier_description_and_not_available_to_supplier_quote_items.php @@ -0,0 +1,26 @@ +string('supplier_description')->nullable()->after('description'); + $table->boolean('not_available')->default(false)->after('is_vatable'); + }); + } + + public function down(): void + { + Schema::table('supplier_quote_items', function (Blueprint $table) { + $table->dropColumn(['supplier_description', 'not_available']); + }); + } +}; diff --git a/docs/superpowers/plans/2026-06-01-vat-rfq.md b/docs/superpowers/plans/2026-06-01-vat-rfq.md new file mode 100644 index 0000000..4ff95b0 --- /dev/null +++ b/docs/superpowers/plans/2026-06-01-vat-rfq.md @@ -0,0 +1,440 @@ +# VAT Settings + RFQ Per-Item Vatable Checkbox — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a global VAT rate setting and per-item vatable checkboxes on the supplier RFQ portal, with live VAT breakdown in totals. + +**Architecture:** Global VAT rate stored in existing `settings` key/value table. Per-item `is_vatable` flag added to `supplier_quote_items`. RFQ portal view updated with checkbox column and three-row footer (subtotal / VAT / grand total). New `VatSettingController` + `settings/vat` page added under the Admin-only System sidebar section. + +**Tech Stack:** Laravel 12, PHP 8.2, SQLite, Blade, Alpine-free vanilla JS, Tailwind (inline styles per project convention) + +--- + +### Task 1: Migration — add `is_vatable` to `supplier_quote_items` + +**Files:** +- Modify: `database/migrations/2026_06_01_090734_add_is_vatable_to_supplier_quote_items.php` +- Modify: `app/Models/SupplierQuoteItem.php` + +- [ ] **Step 1: Write the migration** + +Replace the generated migration body with: + +```php +boolean('is_vatable')->default(false)->after('total_price'); + }); + } + + public function down(): void + { + Schema::table('supplier_quote_items', function (Blueprint $table) { + $table->dropColumn('is_vatable'); + }); + } +}; +``` + +- [ ] **Step 2: Run migration** + +```bash +php artisan migrate +``` + +Expected: `Migrating: 2026_06_01_090734_add_is_vatable_to_supplier_quote_items` then `Migrated`. + +- [ ] **Step 3: Update SupplierQuoteItem model** + +In `app/Models/SupplierQuoteItem.php`, update `$fillable`: + +```php +protected $fillable = [ + 'supplier_quote_id', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'is_vatable', +]; + +protected $casts = [ + 'is_vatable' => 'boolean', +]; +``` + +- [ ] **Step 4: Commit** + +```bash +git add database/migrations/2026_06_01_090734_add_is_vatable_to_supplier_quote_items.php app/Models/SupplierQuoteItem.php +git commit -m "feat: add is_vatable column to supplier_quote_items" +``` + +--- + +### Task 2: VatSettingController + route + view + +**Files:** +- Create: `app/Http/Controllers/Settings/VatSettingController.php` +- Create: `resources/views/settings/vat.blade.php` +- Modify: `routes/web.php` + +- [ ] **Step 1: Create VatSettingController** + +Create `app/Http/Controllers/Settings/VatSettingController.php`: + +```php +validate([ + 'vat_rate' => ['required', 'numeric', 'min:0', 'max:100'], + ]); + + Setting::set('vat_rate', (string) $validated['vat_rate']); + + return response()->json(['message' => 'VAT rate saved.', 'vat_rate' => $validated['vat_rate']]); + } +} +``` + +- [ ] **Step 2: Add routes** + +In `routes/web.php`, after the integrations routes (around line 148), add: + +```php +// VAT settings +Route::get('settings/vat', [VatSettingController::class, 'index'])->name('settings.vat'); +Route::post('settings/vat', [VatSettingController::class, 'update'])->name('settings.vat.update'); +``` + +Also add the import at the top of the file with the other Settings controllers: +```php +use App\Http\Controllers\Settings\VatSettingController; +``` + +- [ ] **Step 3: Create the VAT settings view** + +Create `resources/views/settings/vat.blade.php`: + +```blade +@extends('layouts.app') + +@section('title', 'Settings — VAT') + +@section('content') +
+

VAT Settings

+

Set the global VAT rate applied to vatable items on supplier quotes.

+
+ +
+
+
+
VAT Configuration
+
+
+ +
+ + % +
+

+ Enter 0 to disable VAT. Suppliers will see the VAT checkbox on their quote form when this is greater than 0. +

+ +
+
+
+ + +@endsection +``` + +- [ ] **Step 4: Commit** + +```bash +git add app/Http/Controllers/Settings/VatSettingController.php resources/views/settings/vat.blade.php routes/web.php +git commit -m "feat: add VAT settings page and controller" +``` + +--- + +### Task 3: Add VAT link to sidebar + +**Files:** +- Modify: `resources/views/layouts/app.blade.php` + +- [ ] **Step 1: Add sidebar link** + +In `resources/views/layouts/app.blade.php`, after the Integrations `` tag (around line 198, just before `@endrole`), add: + +```blade + + + + + VAT Settings + +``` + +- [ ] **Step 2: Commit** + +```bash +git add resources/views/layouts/app.blade.php +git commit -m "feat: add VAT Settings link to sidebar" +``` + +--- + +### Task 4: Update RfqPortalController — load VAT rate and store is_vatable + +**Files:** +- Modify: `app/Http/Controllers/Purchase/RfqPortalController.php` + +- [ ] **Step 1: Update show() to pass VAT rate** + +Add `use App\Models\Setting;` at the top of the file. + +Update the `show()` method — add before the `return view(...)` line: + +```php +$vatRate = (float) Setting::get('vat_rate', 0); +``` + +Update the return to: + +```php +return view('rfq.show', compact('invitation', 'purchaseRequest', 'items', 'confirmCode', 'vatRate')); +``` + +- [ ] **Step 2: Update submit() to store is_vatable and calculate VAT-inclusive total** + +Add `'items.*.is_vatable' => ['nullable', 'boolean']` to the validation rules array. + +Replace the items loop and total calculation: + +```php +$subtotal = 0; +$vatAmount = 0; +$vatRate = (float) Setting::get('vat_rate', 0); + +foreach ($purchaseItems as $i => $item) { + $unitPrice = (float)($validated['items'][$i]['unit_price'] ?? 0); + $qty = (float)$item->quantity_required; + $totalPrice = round($unitPrice * $qty, 3); + $isVatable = !empty($validated['items'][$i]['is_vatable']); + $subtotal += $totalPrice; + + if ($isVatable && $vatRate > 0) { + $vatAmount += round($totalPrice * $vatRate / 100, 3); + } + + SupplierQuoteItem::create([ + 'supplier_quote_id' => $quote->id, + 'description' => $item->description, + 'unit' => $item->unit ?? '', + 'quantity' => $qty, + 'unit_price' => $unitPrice, + 'total_price' => $totalPrice, + 'is_vatable' => $isVatable, + ]); +} + +$grand = round($subtotal + $vatAmount, 3); +$quote->update(['total_amount' => $grand]); +``` + +- [ ] **Step 3: Commit** + +```bash +git add app/Http/Controllers/Purchase/RfqPortalController.php +git commit -m "feat: load VAT rate and store is_vatable in RFQ portal" +``` + +--- + +### Task 5: Update rfq/show.blade.php — checkbox column + VAT footer + JS + +**Files:** +- Modify: `resources/views/rfq/show.blade.php` + +- [ ] **Step 1: Add VAT rate JS variable** + +At the start of the `