# Task 07 — Purchase Orders

## Goal
Create the Purchase Order (PO) module — header + lines, status machine, confirm/cancel actions.

---

## Migrations

### Table: `purchase_orders`
```php
Schema::create('purchase_orders', function (Blueprint $table) {
    $table->id();
    $table->string('po_number')->unique();
    $table->date('po_date');
    $table->date('expected_date')->nullable();
    $table->unsignedBigInteger('supplier_id');
    $table->unsignedBigInteger('branch_id');
    $table->decimal('subtotal', 18, 2)->default(0);
    $table->decimal('discount_amount', 18, 2)->default(0);
    $table->decimal('tax_amount', 18, 2)->default(0);
    $table->decimal('total_amount', 18, 2)->default(0);
    $table->decimal('received_amount', 18, 2)->default(0); // tracks received value
    $table->enum('status', ['draft','confirmed','partially_received','received','cancelled'])->default('draft');
    $table->json('notes')->nullable();
    $table->unsignedBigInteger('created_by')->nullable();
    $table->unsignedBigInteger('updated_by')->nullable();
    $table->timestamps();

    $table->foreign('supplier_id')->references('id')->on('suppliers');
    $table->foreign('branch_id')->references('id')->on('branches');
    $table->foreign('created_by')->references('id')->on('admins')->nullOnDelete();
    $table->foreign('updated_by')->references('id')->on('admins')->nullOnDelete();
    $table->index(['supplier_id', 'status']);
    $table->index(['branch_id', 'po_date']);
});
```

### Table: `purchase_order_lines`
```php
Schema::create('purchase_order_lines', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('purchase_order_id');
    $table->unsignedBigInteger('item_id');
    $table->json('description')->nullable();
    $table->decimal('quantity', 18, 2);
    $table->decimal('unit_cost', 18, 2);
    $table->decimal('discount_percent', 5, 2)->default(0);
    $table->decimal('discount_amount', 18, 2)->default(0);
    $table->unsignedBigInteger('tax_rate_id')->nullable();
    $table->decimal('tax_amount', 18, 2)->default(0);
    $table->decimal('line_subtotal', 18, 2)->default(0); // qty × unit_cost
    $table->decimal('line_total', 18, 2)->default(0);    // after discount+tax
    $table->decimal('received_quantity', 18, 2)->default(0);
    $table->timestamps();

    $table->foreign('purchase_order_id')->references('id')->on('purchase_orders')->cascadeOnDelete();
    $table->foreign('item_id')->references('id')->on('items');
    $table->foreign('tax_rate_id')->references('id')->on('sales_tax_rates')->nullOnDelete();
    $table->index('purchase_order_id');
    $table->index('item_id');
});
```

---

## Models

### `app/Models/Inventory/Purchase/PurchaseOrder.php`

```php
protected $fillable = [
    'po_number', 'po_date', 'expected_date', 'supplier_id', 'branch_id',
    'subtotal', 'discount_amount', 'tax_amount', 'total_amount', 'received_amount',
    'status', 'notes', 'created_by', 'updated_by',
];

protected $casts = [
    'po_date' => 'date',
    'expected_date' => 'date',
    'subtotal'  => 'decimal:2',
    'total_amount' => 'decimal:2',
    'notes' => 'array',
];

public function supplier(): BelongsTo { return $this->belongsTo(Supplier::class); }
public function branch(): BelongsTo { return $this->belongsTo(Branch::class); }
public function lines(): HasMany { return $this->hasMany(PurchaseOrderLine::class); }
public function receipts(): HasMany { return $this->hasMany(PurchaseReceipt::class); }

public function isEditable(): bool { return in_array($this->status, ['draft']); }
public function isReceivable(): bool { return in_array($this->status, ['confirmed', 'partially_received']); }
```

### `app/Models/Inventory/Purchase/PurchaseOrderLine.php`
Standard line model with `belongsTo PurchaseOrder`, `belongsTo Item`, `belongsTo SalesTaxRate`.

---

## Service: `app/Services/Inventory/Purchase/PurchaseOrderService.php`

### Key Methods:

**`create(array $data): PurchaseOrder`**
```
1. Generate PO number via NumberSequenceService (module=inventory, document=purchase_order)
2. Calculate line totals: line_subtotal = qty × unit_cost, discount_amount, tax_amount, line_total
3. Calculate header totals: subtotal = sum(line_subtotal), total = sum(line_total)
4. Create PO header + lines in DB transaction
```

**`update(PurchaseOrder $po, array $data): PurchaseOrder`**
```
- Only allowed if status = draft
- Replace all lines, recalculate totals
```

**`confirm(PurchaseOrder $po): PurchaseOrder`**
```
- Only from status = draft
- Set status = confirmed
```

**`cancel(PurchaseOrder $po): PurchaseOrder`**
```
- Only if status = draft or confirmed
- Cannot cancel if any receipts exist
- Set status = cancelled
```

**`calculateLineTotals(array $line): array`**
```php
$line_subtotal = $line['quantity'] * $line['unit_cost'];
$discount_amount = $line_subtotal * ($line['discount_percent'] / 100);
$taxable = $line_subtotal - $discount_amount;
$tax_amount = $taxable * ($taxRate->rate / 100);  // if tax_rate_id provided
$line_total = $taxable + $tax_amount;
return compact('line_subtotal', 'discount_amount', 'tax_amount', 'line_total');
```

---

## Status Machine

```
draft ──confirm──► confirmed ──receive──► partially_received ──► received
  └──cancel──► cancelled
confirmed ──cancel──► cancelled (only if no receipts)
```

---

## Views

Path: `resources/views/dashboard/admin/inventory/purchase-orders/`
- `index.blade.php` — list with status badges, supplier, amount
- `create.blade.php` — header fields + dynamic lines table (JS)
- `edit.blade.php` — same, only when draft
- `show.blade.php` — full PO view with lines + receipt history + confirm/cancel buttons

---

## Permissions
```
inventory.purchase_orders.view
inventory.purchase_orders.create
inventory.purchase_orders.update
inventory.purchase_orders.confirm
inventory.purchase_orders.cancel
```
