Files
star-erp/app/Http/Controllers/PurchaseOrderController.php

400 lines
16 KiB
PHP
Raw Normal View History

2025-12-30 15:03:19 +08:00
<?php
namespace App\Http\Controllers;
use App\Models\PurchaseOrder;
use App\Models\Vendor;
use App\Models\Warehouse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Illuminate\Support\Facades\DB;
class PurchaseOrderController extends Controller
{
public function index(Request $request)
{
$query = PurchaseOrder::with(['vendor', 'warehouse', 'user']);
// Search
if ($request->search) {
$query->where(function($q) use ($request) {
$q->where('code', 'like', "%{$request->search}%")
->orWhereHas('vendor', function($vq) use ($request) {
$vq->where('name', 'like', "%{$request->search}%");
});
});
}
// Filters
if ($request->status && $request->status !== 'all') {
$query->where('status', $request->status);
}
if ($request->warehouse_id && $request->warehouse_id !== 'all') {
$query->where('warehouse_id', $request->warehouse_id);
}
// Sorting
$sortField = $request->sort_field ?? 'id';
$sortDirection = $request->sort_direction ?? 'desc';
$allowedSortFields = ['id', 'code', 'status', 'total_amount', 'created_at', 'expected_delivery_date'];
if (in_array($sortField, $allowedSortFields)) {
$query->orderBy($sortField, $sortDirection);
}
$orders = $query->paginate(15)->withQueryString();
return Inertia::render('PurchaseOrder/Index', [
'orders' => $orders,
'filters' => $request->only(['search', 'status', 'warehouse_id', 'sort_field', 'sort_direction']),
'warehouses' => Warehouse::all(['id', 'name']),
]);
}
public function create()
{
2026-01-08 16:32:10 +08:00
$vendors = Vendor::with(['products.baseUnit', 'products.largeUnit', 'products.purchaseUnit'])->get()->map(function ($vendor) {
2025-12-30 15:03:19 +08:00
return [
'id' => (string) $vendor->id,
'name' => $vendor->name,
'commonProducts' => $vendor->products->map(function ($product) {
return [
'productId' => (string) $product->id,
'productName' => $product->name,
2026-01-08 16:32:10 +08:00
'base_unit_id' => $product->base_unit_id,
'base_unit_name' => $product->baseUnit?->name,
'large_unit_id' => $product->large_unit_id,
'large_unit_name' => $product->largeUnit?->name,
'purchase_unit_id' => $product->purchase_unit_id,
'conversion_rate' => (float) $product->conversion_rate,
2025-12-30 15:03:19 +08:00
'lastPrice' => (float) ($product->pivot->last_price ?? 0),
];
})
];
});
$warehouses = Warehouse::all()->map(function ($w) {
return [
'id' => (string) $w->id,
'name' => $w->name,
];
});
return Inertia::render('PurchaseOrder/Create', [
'suppliers' => $vendors,
'warehouses' => $warehouses,
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'vendor_id' => 'required|exists:vendors,id',
'warehouse_id' => 'required|exists:warehouses,id',
'expected_delivery_date' => 'nullable|date',
'remark' => 'nullable|string',
'invoice_number' => ['nullable', 'string', 'max:11', 'regex:/^[A-Z]{2}-\d{8}$/'],
'invoice_date' => 'nullable|date',
'invoice_amount' => 'nullable|numeric|min:0',
2025-12-30 15:03:19 +08:00
'items' => 'required|array|min:1',
'items.*.productId' => 'required|exists:products,id',
'items.*.quantity' => 'required|numeric|min:0.01',
2026-01-08 17:51:06 +08:00
'items.*.subtotal' => 'required|numeric|min:0', // 總金額
'items.*.unitId' => 'nullable|exists:units,id',
2025-12-30 15:03:19 +08:00
]);
try {
DB::beginTransaction();
// 生成單號YYYYMMDD001
$today = now()->format('Ymd');
$lastOrder = PurchaseOrder::where('code', 'like', $today . '%')
->lockForUpdate() // 鎖定以避免並發衝突
->orderBy('code', 'desc')
->first();
if ($lastOrder) {
// 取得最後 3 碼序號並加 1
$lastSequence = intval(substr($lastOrder->code, -3));
$sequence = str_pad($lastSequence + 1, 3, '0', STR_PAD_LEFT);
} else {
$sequence = '001';
}
$code = $today . $sequence;
$totalAmount = 0;
foreach ($validated['items'] as $item) {
2026-01-08 17:51:06 +08:00
$totalAmount += $item['subtotal'];
2025-12-30 15:03:19 +08:00
}
// Simple tax calculation (e.g., 5%)
$taxAmount = round($totalAmount * 0.05, 2);
$grandTotal = $totalAmount + $taxAmount;
2025-12-31 17:48:36 +08:00
// 確保有一個有效的使用者 ID
$userId = auth()->id();
if (!$userId) {
$user = \App\Models\User::first();
if (!$user) {
$user = \App\Models\User::create([
'name' => '系統管理員',
'email' => 'admin@example.com',
'password' => bcrypt('password'),
]);
}
$userId = $user->id;
}
2025-12-30 15:03:19 +08:00
$order = PurchaseOrder::create([
'code' => $code,
'vendor_id' => $validated['vendor_id'],
'warehouse_id' => $validated['warehouse_id'],
2025-12-31 17:48:36 +08:00
'user_id' => $userId,
2025-12-30 15:03:19 +08:00
'status' => 'draft',
'expected_delivery_date' => $validated['expected_delivery_date'],
'total_amount' => $totalAmount,
'tax_amount' => $taxAmount,
'grand_total' => $grandTotal,
'remark' => $validated['remark'],
'invoice_number' => $validated['invoice_number'] ?? null,
'invoice_date' => $validated['invoice_date'] ?? null,
'invoice_amount' => $validated['invoice_amount'] ?? null,
2025-12-30 15:03:19 +08:00
]);
foreach ($validated['items'] as $item) {
2026-01-08 17:51:06 +08:00
// 反算單價
$unitPrice = $item['quantity'] > 0 ? $item['subtotal'] / $item['quantity'] : 0;
2025-12-30 15:03:19 +08:00
$order->items()->create([
'product_id' => $item['productId'],
'quantity' => $item['quantity'],
2026-01-08 17:51:06 +08:00
'unit_id' => $item['unitId'] ?? null,
'unit_price' => $unitPrice,
'subtotal' => $item['subtotal'],
2025-12-30 15:03:19 +08:00
]);
}
DB::commit();
return redirect()->route('purchase-orders.index')->with('success', '採購單已成功建立');
} catch (\Exception $e) {
DB::rollBack();
return back()->withErrors(['error' => '建立失敗:' . $e->getMessage()]);
}
}
public function show($id)
{
2026-01-08 16:32:10 +08:00
$order = PurchaseOrder::with(['vendor', 'warehouse', 'user', 'items.product.baseUnit', 'items.product.largeUnit'])->findOrFail($id);
2025-12-30 15:03:19 +08:00
2026-01-08 16:32:10 +08:00
$order->items->transform(function ($item) use ($order) {
$product = $item->product;
if ($product) {
2026-01-08 16:32:10 +08:00
// 手動附加所有必要的屬性
$item->productId = (string) $product->id;
$item->productName = $product->name;
2026-01-08 16:32:10 +08:00
$item->base_unit_id = $product->base_unit_id;
$item->base_unit_name = $product->baseUnit?->name;
$item->large_unit_id = $product->large_unit_id;
$item->large_unit_name = $product->largeUnit?->name;
$item->purchase_unit_id = $product->purchase_unit_id;
$item->conversion_rate = (float) $product->conversion_rate;
2026-01-08 16:32:10 +08:00
// Fetch last price
$lastPrice = DB::table('product_vendor')
->where('vendor_id', $order->vendor_id)
->where('product_id', $product->id)
->value('last_price');
$item->previousPrice = (float) ($lastPrice ?? 0);
// 設定當前選中的單位 ID (from saved item)
$item->unitId = $item->unit_id;
// 決定 selectedUnit (用於 UI 顯示)
if ($item->unitId && $item->large_unit_id && $item->unitId == $item->large_unit_id) {
$item->selectedUnit = 'large';
} else {
$item->selectedUnit = 'base';
}
$item->unitPrice = (float) $item->unit_price;
}
return $item;
});
2025-12-30 15:03:19 +08:00
return Inertia::render('PurchaseOrder/Show', [
'order' => $order
]);
}
public function edit($id)
{
$order = PurchaseOrder::with(['items.product'])->findOrFail($id);
2026-01-08 16:32:10 +08:00
$vendors = Vendor::with(['products.baseUnit', 'products.largeUnit', 'products.purchaseUnit'])->get()->map(function ($vendor) {
2025-12-30 15:03:19 +08:00
return [
'id' => (string) $vendor->id,
'name' => $vendor->name,
'commonProducts' => $vendor->products->map(function ($product) {
return [
'productId' => (string) $product->id,
'productName' => $product->name,
2026-01-08 16:32:10 +08:00
'base_unit_id' => $product->base_unit_id,
'base_unit_name' => $product->baseUnit?->name,
'large_unit_id' => $product->large_unit_id,
'large_unit_name' => $product->largeUnit?->name,
'purchase_unit_id' => $product->purchase_unit_id,
'conversion_rate' => (float) $product->conversion_rate,
2025-12-30 15:03:19 +08:00
'lastPrice' => (float) ($product->pivot->last_price ?? 0),
];
})
];
});
$warehouses = Warehouse::all()->map(function ($w) {
return [
'id' => (string) $w->id,
'name' => $w->name,
];
});
// Transform items for frontend form
2026-01-08 16:32:10 +08:00
// Transform items for frontend form
$vendorId = $order->vendor_id;
$order->items->transform(function ($item) use ($vendorId) {
$product = $item->product;
if ($product) {
2026-01-08 16:32:10 +08:00
// 手動附加所有必要的屬性
$item->productId = (string) $product->id;
$item->productName = $product->name;
2026-01-08 16:32:10 +08:00
$item->base_unit_id = $product->base_unit_id;
$item->base_unit_name = $product->baseUnit?->name;
$item->large_unit_id = $product->large_unit_id;
$item->large_unit_name = $product->largeUnit?->name;
$item->conversion_rate = (float) $product->conversion_rate;
2026-01-08 16:32:10 +08:00
// Fetch last price
$lastPrice = DB::table('product_vendor')
->where('vendor_id', $vendorId)
->where('product_id', $product->id)
->value('last_price');
$item->previousPrice = (float) ($lastPrice ?? 0);
// 設定當前選中的單位 ID
$item->unitId = $item->unit_id; // 資料庫中的 unit_id
// 決定 selectedUnit (用於 UI 狀態)
if ($item->unitId && $item->large_unit_id && $item->unitId == $item->large_unit_id) {
$item->selectedUnit = 'large';
} else {
$item->selectedUnit = 'base';
}
$item->unitPrice = (float) $item->unit_price;
}
return $item;
});
2025-12-30 15:03:19 +08:00
return Inertia::render('PurchaseOrder/Create', [
'order' => $order,
'suppliers' => $vendors,
'warehouses' => $warehouses,
]);
}
public function update(Request $request, $id)
{
$order = PurchaseOrder::findOrFail($id);
$validated = $request->validate([
'vendor_id' => 'required|exists:vendors,id',
'warehouse_id' => 'required|exists:warehouses,id',
'expected_delivery_date' => 'nullable|date',
'remark' => 'nullable|string',
'status' => 'required|string|in:draft,pending,processing,shipping,confirming,completed,cancelled',
'invoice_number' => ['nullable', 'string', 'max:11', 'regex:/^[A-Z]{2}-\d{8}$/'],
'invoice_date' => 'nullable|date',
'invoice_amount' => 'nullable|numeric|min:0',
2025-12-30 15:03:19 +08:00
'items' => 'required|array|min:1',
'items.*.productId' => 'required|exists:products,id',
'items.*.quantity' => 'required|numeric|min:0.01',
2026-01-08 17:51:06 +08:00
'items.*.subtotal' => 'required|numeric|min:0', // 總金額
'items.*.unitId' => 'nullable|exists:units,id',
2025-12-30 15:03:19 +08:00
]);
try {
DB::beginTransaction();
$totalAmount = 0;
foreach ($validated['items'] as $item) {
2026-01-08 17:51:06 +08:00
$totalAmount += $item['subtotal'];
2025-12-30 15:03:19 +08:00
}
// Simple tax calculation (e.g., 5%)
$taxAmount = round($totalAmount * 0.05, 2);
$grandTotal = $totalAmount + $taxAmount;
$order->update([
'vendor_id' => $validated['vendor_id'],
'warehouse_id' => $validated['warehouse_id'],
'expected_delivery_date' => $validated['expected_delivery_date'],
'total_amount' => $totalAmount,
'tax_amount' => $taxAmount,
'grand_total' => $grandTotal,
'remark' => $validated['remark'],
'status' => $validated['status'],
'invoice_number' => $validated['invoice_number'] ?? null,
'invoice_date' => $validated['invoice_date'] ?? null,
'invoice_amount' => $validated['invoice_amount'] ?? null,
2025-12-30 15:03:19 +08:00
]);
// Sync items
$order->items()->delete();
foreach ($validated['items'] as $item) {
2026-01-08 17:51:06 +08:00
// 反算單價
$unitPrice = $item['quantity'] > 0 ? $item['subtotal'] / $item['quantity'] : 0;
2025-12-30 15:03:19 +08:00
$order->items()->create([
'product_id' => $item['productId'],
'quantity' => $item['quantity'],
2026-01-08 17:51:06 +08:00
'unit_id' => $item['unitId'] ?? null,
'unit_price' => $unitPrice,
'subtotal' => $item['subtotal'],
2025-12-30 15:03:19 +08:00
]);
}
DB::commit();
return redirect()->route('purchase-orders.index')->with('success', '採購單已更新');
} catch (\Exception $e) {
DB::rollBack();
return back()->withErrors(['error' => '更新失敗:' . $e->getMessage()]);
}
}
2025-12-30 17:05:19 +08:00
public function destroy($id)
{
try {
DB::beginTransaction();
$order = PurchaseOrder::findOrFail($id);
// Delete associated items first (due to FK constraints if not cascade)
$order->items()->delete();
$order->delete();
DB::commit();
return redirect()->route('purchase-orders.index')->with('success', '採購單已刪除');
} catch (\Exception $e) {
DB::rollBack();
return back()->withErrors(['error' => '刪除失敗:' . $e->getMessage()]);
}
}
2025-12-30 15:03:19 +08:00
}