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()
|
|
|
|
|
|
{
|
|
|
|
|
|
$vendors = Vendor::with('products')->get()->map(function ($vendor) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
'id' => (string) $vendor->id,
|
|
|
|
|
|
'name' => $vendor->name,
|
|
|
|
|
|
'commonProducts' => $vendor->products->map(function ($product) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
'productId' => (string) $product->id,
|
|
|
|
|
|
'productName' => $product->name,
|
2026-01-06 15:45:13 +08:00
|
|
|
|
'unit' => $product->purchase_unit ?: ($product->large_unit ?: $product->base_unit), // 優先使用採購單位 > 大單位 > 基本單位
|
|
|
|
|
|
'base_unit' => $product->base_unit,
|
|
|
|
|
|
'purchase_unit' => $product->purchase_unit ?: $product->large_unit, // 若無採購單位,預設為大單位
|
|
|
|
|
|
'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',
|
|
|
|
|
|
'items' => 'required|array|min:1',
|
|
|
|
|
|
'items.*.productId' => 'required|exists:products,id',
|
|
|
|
|
|
'items.*.quantity' => 'required|numeric|min:0.01',
|
|
|
|
|
|
'items.*.unitPrice' => 'required|numeric|min:0',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
|
$totalAmount += $item['quantity'] * $item['unitPrice'];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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'],
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
foreach ($validated['items'] as $item) {
|
|
|
|
|
|
$order->items()->create([
|
|
|
|
|
|
'product_id' => $item['productId'],
|
|
|
|
|
|
'quantity' => $item['quantity'],
|
|
|
|
|
|
'unit_price' => $item['unitPrice'],
|
|
|
|
|
|
'subtotal' => $item['quantity'] * $item['unitPrice'],
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
{
|
|
|
|
|
|
$order = PurchaseOrder::with(['vendor', 'warehouse', 'user', 'items.product'])->findOrFail($id);
|
|
|
|
|
|
|
2026-01-06 15:45:13 +08:00
|
|
|
|
// Transform items to include product details needed for frontend calculation
|
|
|
|
|
|
$order->items->transform(function ($item) {
|
|
|
|
|
|
$product = $item->product;
|
|
|
|
|
|
if ($product) {
|
|
|
|
|
|
// 手動附加 productName 和 unit (因為已從 $appends 移除)
|
|
|
|
|
|
$item->productName = $product->name;
|
|
|
|
|
|
$item->productId = $product->id;
|
|
|
|
|
|
$item->base_unit = $product->base_unit;
|
|
|
|
|
|
$item->purchase_unit = $product->purchase_unit ?: $product->large_unit; // Fallback logic same as Create
|
|
|
|
|
|
$item->conversion_rate = (float) $product->conversion_rate;
|
|
|
|
|
|
// 優先使用採購單位 > 大單位 > 基本單位
|
|
|
|
|
|
$item->unit = $product->purchase_unit ?: ($product->large_unit ?: $product->base_unit);
|
|
|
|
|
|
$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);
|
|
|
|
|
|
|
|
|
|
|
|
$vendors = Vendor::with('products')->get()->map(function ($vendor) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
'id' => (string) $vendor->id,
|
|
|
|
|
|
'name' => $vendor->name,
|
|
|
|
|
|
'commonProducts' => $vendor->products->map(function ($product) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
'productId' => (string) $product->id,
|
|
|
|
|
|
'productName' => $product->name,
|
2026-01-06 15:45:13 +08:00
|
|
|
|
'unit' => $product->purchase_unit ?: ($product->large_unit ?: $product->base_unit),
|
|
|
|
|
|
'base_unit' => $product->base_unit,
|
|
|
|
|
|
'purchase_unit' => $product->purchase_unit ?: $product->large_unit,
|
|
|
|
|
|
'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,
|
|
|
|
|
|
];
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-06 15:45:13 +08:00
|
|
|
|
// Transform items for frontend form
|
|
|
|
|
|
$order->items->transform(function ($item) {
|
|
|
|
|
|
$product = $item->product;
|
|
|
|
|
|
if ($product) {
|
|
|
|
|
|
// 手動附加所有必要的屬性 (因為已從 $appends 移除)
|
|
|
|
|
|
$item->productId = (string) $product->id; // Ensure consistent ID type
|
|
|
|
|
|
$item->productName = $product->name;
|
|
|
|
|
|
$item->base_unit = $product->base_unit;
|
|
|
|
|
|
$item->purchase_unit = $product->purchase_unit ?: $product->large_unit;
|
|
|
|
|
|
$item->conversion_rate = (float) $product->conversion_rate;
|
|
|
|
|
|
// 優先使用採購單位 > 大單位 > 基本單位
|
|
|
|
|
|
$item->unit = $product->purchase_unit ?: ($product->large_unit ?: $product->base_unit);
|
|
|
|
|
|
$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',
|
|
|
|
|
|
'items' => 'required|array|min:1',
|
|
|
|
|
|
'items.*.productId' => 'required|exists:products,id',
|
|
|
|
|
|
'items.*.quantity' => 'required|numeric|min:0.01',
|
|
|
|
|
|
'items.*.unitPrice' => 'required|numeric|min:0',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
DB::beginTransaction();
|
|
|
|
|
|
|
|
|
|
|
|
$totalAmount = 0;
|
|
|
|
|
|
foreach ($validated['items'] as $item) {
|
|
|
|
|
|
$totalAmount += $item['quantity'] * $item['unitPrice'];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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'],
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// Sync items
|
|
|
|
|
|
$order->items()->delete();
|
|
|
|
|
|
foreach ($validated['items'] as $item) {
|
|
|
|
|
|
$order->items()->create([
|
|
|
|
|
|
'product_id' => $item['productId'],
|
|
|
|
|
|
'quantity' => $item['quantity'],
|
|
|
|
|
|
'unit_price' => $item['unitPrice'],
|
|
|
|
|
|
'subtotal' => $item['quantity'] * $item['unitPrice'],
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|