# Task 08 — Purchase Receipts (GRN)

## Goal
Goods Receipt Note — records actual stock received against a PO (or without PO).
Posting a receipt creates stock movements and updates stock balances.

---

## Migrations

### Table: `purchase_receipts`
```php
Schema::create('purchase_receipts', function (Blueprint $table) {
    $table->id();
    $table->string('receipt_number')->unique();
    $table->date('receipt_date');
    $table->unsignedBigInteger('purchase_order_id')->nullable();
    $table->unsignedBigInteger('supplier_id');
    $table->unsignedBigInteger('warehouse_id');
    $table->unsignedBigInteger('branch_id');
    $table->decimal('total_cost', 18, 2)->default(0);
    $table->unsignedBigInteger('journal_entry_id')->nullable();
    $table->timestamp('posted_at')->nullable();
    $table->enum('status', ['draft', 'posted', 'reversed'])->default('draft');
    $table->json('notes')->nullable();
    $table->unsignedBigInteger('created_by')->nullable();
    $table->unsignedBigInteger('updated_by')->nullable();
    $table->timestamps();

    $table->foreign('purchase_order_id')->references('id')->on('purchase_orders')->nullOnDelete();
    $table->foreign('supplier_id')->references('id')->on('suppliers');
    $table->foreign('warehouse_id')->references('id')->on('warehouses');
    $table->foreign('branch_id')->references('id')->on('branches');
    $table->foreign('journal_entry_id')->references('id')->on('journal_entries')->nullOnDelete();
    $table->index(['supplier_id', 'status']);
    $table->index(['warehouse_id', 'receipt_date']);
});
```

### Table: `purchase_receipt_lines`
```php
Schema::create('purchase_receipt_lines', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('purchase_receipt_id');
    $table->unsignedBigInteger('purchase_order_line_id')->nullable();
    $table->unsignedBigInteger('item_id');
    $table->decimal('quantity', 18, 2);
    $table->decimal('unit_cost', 18, 2);
    $table->decimal('total_cost', 18, 2);
    $table->timestamps();

    $table->foreign('purchase_receipt_id')->references('id')->on('purchase_receipts')->cascadeOnDelete();
    $table->foreign('purchase_order_line_id')->references('id')->on('purchase_order_lines')->nullOnDelete();
    $table->foreign('item_id')->references('id')->on('items');
    $table->index('purchase_receipt_id');
    $table->index('item_id');
});
```

---

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

### `post(PurchaseReceipt $receipt): PurchaseReceipt`

This is the critical method. Must run in a DB transaction.

```
Steps:
1. Validate status = draft
2. Validate fiscal period is open for receipt_date
3. For each receipt line:
   a. Get item — must be type=stock
   b. Validate item is active
   c. Create StockMovement (type=in, warehouse, item, qty, unit_cost)
   d. Call StockBalanceService::applyMovement(movement)  ← updates balance + recalculates average cost
4. Calculate total_cost = sum(line.total_cost)
5. Call InventoryPostingService::postReceipt(receipt)  ← creates journal entry (Task 20)
6. Update PO received_quantity per line
7. Update PO status (partially_received vs received)
8. Set receipt.status = posted, posted_at = now()
```

### `reverse(PurchaseReceipt $receipt): PurchaseReceipt`

```
Steps (all in transaction):
1. Validate status = posted
2. For each receipt line:
   a. Create StockMovement (type=out, same qty) referencing reversal
   b. Call StockBalanceService::applyMovement(movement)
3. Reverse the journal entry (Task 20)
4. Update PO received_quantity back
5. Set status = reversed
```

---

## Create Receipt from PO

Add method `createFromPO(PurchaseOrder $po, Warehouse $warehouse): PurchaseReceipt`

```
- Pre-fill supplier, branch, expected lines from unreceived PO lines
- User can adjust received quantities before posting
- Quantity cannot exceed remaining PO line quantity
```

---

## Views

Path: `resources/views/dashboard/admin/inventory/purchase-receipts/`
- `index.blade.php` — list with status, supplier, warehouse, date, total
- `create.blade.php` — supplier selector, optional PO selector, warehouse, lines
- `show.blade.php` — receipt details, lines, Post button, journal entry link
- On show: if status=draft → show Post button
- On show: if status=posted → show Reverse button (with confirmation dialog)

---

## Permissions
```
inventory.purchase_receipts.view
inventory.purchase_receipts.create
inventory.purchase_receipts.post
inventory.purchase_receipts.reverse
```

---

## Test This Task

1. Create a receipt with 2 items, post it
2. Verify StockMovements were created (type=in)
3. Verify stock_balances were created/updated with correct qty and average cost
4. Verify total_cost is correct
5. Try reversing → verify stock returns to pre-receipt level
6. Try posting a receipt for a service item → should reject
