2025-12-30 15:03:19 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
|
2026-01-26 10:37:47 +08:00
|
|
|
|
namespace App\Modules\Procurement\Controllers;
|
2025-12-30 15:03:19 +08:00
|
|
|
|
|
2026-01-26 10:37:47 +08:00
|
|
|
|
use App\Http\Controllers\Controller;
|
|
|
|
|
|
|
|
|
|
|
|
use App\Modules\Procurement\Models\PurchaseOrder;
|
|
|
|
|
|
use App\Modules\Procurement\Models\Vendor;
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// use App\Modules\Inventory\Models\Warehouse; // REFACTORED: 移除直接依賴
|
|
|
|
|
|
use App\Modules\Inventory\Contracts\InventoryServiceInterface; // NEW: 使用契約
|
|
|
|
|
|
use App\Modules\Core\Contracts\CoreServiceInterface; // NEW: 使用核心服務契約
|
2025-12-30 15:03:19 +08:00
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
|
use Inertia\Inertia;
|
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
|
|
|
|
|
|
class PurchaseOrderController extends Controller
|
|
|
|
|
|
{
|
2026-01-26 14:59:24 +08:00
|
|
|
|
protected $inventoryService;
|
|
|
|
|
|
protected $coreService;
|
|
|
|
|
|
|
|
|
|
|
|
public function __construct(InventoryServiceInterface $inventoryService, CoreServiceInterface $coreService)
|
|
|
|
|
|
{
|
|
|
|
|
|
$this->inventoryService = $inventoryService;
|
|
|
|
|
|
$this->coreService = $coreService;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-30 15:03:19 +08:00
|
|
|
|
public function index(Request $request)
|
|
|
|
|
|
{
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 1. 從關聯中移除 'warehouse' 與 'user'
|
|
|
|
|
|
$query = PurchaseOrder::with(['vendor']);
|
2025-12-30 15:03:19 +08:00
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 搜尋
|
2025-12-30 15:03:19 +08:00
|
|
|
|
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}%");
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 篩選
|
2025-12-30 15:03:19 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 日期範圍
|
2026-01-19 17:07:45 +08:00
|
|
|
|
if ($request->date_start) {
|
|
|
|
|
|
$query->whereDate('created_at', '>=', $request->date_start);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ($request->date_end) {
|
|
|
|
|
|
$query->whereDate('created_at', '<=', $request->date_end);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 排序
|
2025-12-30 15:03:19 +08:00
|
|
|
|
$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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-13 17:09:52 +08:00
|
|
|
|
$perPage = $request->input('per_page', 10);
|
|
|
|
|
|
$orders = $query->paginate($perPage)->withQueryString();
|
2025-12-30 15:03:19 +08:00
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 2. 手動注入倉庫與使用者資料
|
|
|
|
|
|
$warehouses = $this->inventoryService->getAllWarehouses();
|
|
|
|
|
|
$userIds = $orders->getCollection()->pluck('user_id')->unique()->toArray();
|
|
|
|
|
|
$users = $this->coreService->getUsersByIds($userIds)->keyBy('id');
|
|
|
|
|
|
|
|
|
|
|
|
$orders->getCollection()->transform(function ($order) use ($warehouses, $users) {
|
|
|
|
|
|
// 水和倉庫
|
|
|
|
|
|
$warehouse = $warehouses->firstWhere('id', $order->warehouse_id);
|
|
|
|
|
|
$order->setRelation('warehouse', $warehouse);
|
|
|
|
|
|
|
|
|
|
|
|
// 水和使用者
|
|
|
|
|
|
$user = $users->get($order->user_id);
|
|
|
|
|
|
$order->setRelation('user', $user);
|
|
|
|
|
|
|
|
|
|
|
|
// 轉換為前端期望的格式 (camelCase)
|
|
|
|
|
|
return (object) [
|
|
|
|
|
|
'id' => (string) $order->id,
|
|
|
|
|
|
'poNumber' => $order->code,
|
|
|
|
|
|
'supplierId' => (string) $order->vendor_id,
|
|
|
|
|
|
'supplierName' => $order->vendor?->name ?? 'Unknown',
|
2026-01-26 17:27:34 +08:00
|
|
|
|
'orderDate' => $order->order_date?->format('Y-m-d'), // 新增
|
2026-01-26 14:59:24 +08:00
|
|
|
|
'expectedDate' => $order->expected_delivery_date?->toISOString(),
|
|
|
|
|
|
'status' => $order->status,
|
|
|
|
|
|
'totalAmount' => (float) $order->total_amount,
|
|
|
|
|
|
'taxAmount' => (float) $order->tax_amount,
|
|
|
|
|
|
'grandTotal' => (float) $order->grand_total,
|
|
|
|
|
|
'createdAt' => $order->created_at->toISOString(),
|
|
|
|
|
|
'createdBy' => $user?->name ?? 'System',
|
|
|
|
|
|
'warehouse_id' => (int) $order->warehouse_id,
|
|
|
|
|
|
'warehouse_name' => $warehouse?->name ?? 'Unknown',
|
|
|
|
|
|
'remark' => $order->remark,
|
|
|
|
|
|
];
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-30 15:03:19 +08:00
|
|
|
|
return Inertia::render('PurchaseOrder/Index', [
|
|
|
|
|
|
'orders' => $orders,
|
2026-01-19 17:07:45 +08:00
|
|
|
|
'filters' => $request->only(['search', 'status', 'warehouse_id', 'date_start', 'date_end', 'sort_field', 'sort_direction', 'per_page']),
|
2026-01-26 14:59:24 +08:00
|
|
|
|
'warehouses' => $warehouses->map(fn($w)=>(object)['id'=>$w->id, 'name'=>$w->name]),
|
2025-12-30 15:03:19 +08:00
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function create()
|
|
|
|
|
|
{
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 1. 獲取廠商(無關聯)
|
|
|
|
|
|
$vendors = Vendor::all();
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 手動注入:獲取 Pivot 資料
|
|
|
|
|
|
$vendorIds = $vendors->pluck('id')->toArray();
|
|
|
|
|
|
$pivots = DB::table('product_vendor')->whereIn('vendor_id', $vendorIds)->get();
|
|
|
|
|
|
$productIds = $pivots->pluck('product_id')->unique()->toArray();
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 從服務獲取商品
|
|
|
|
|
|
$products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id');
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 重建前端結構
|
|
|
|
|
|
$vendors = $vendors->map(function ($vendor) use ($pivots, $products) {
|
|
|
|
|
|
$vendorProductPivots = $pivots->where('vendor_id', $vendor->id);
|
|
|
|
|
|
|
|
|
|
|
|
$commonProducts = $vendorProductPivots->map(function($pivot) use ($products) {
|
|
|
|
|
|
$product = $products[$pivot->product_id] ?? null;
|
|
|
|
|
|
if (!$product) return null;
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
'productId' => (string) $product->id,
|
|
|
|
|
|
'productName' => $product->name,
|
|
|
|
|
|
'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,
|
|
|
|
|
|
'lastPrice' => (float) $pivot->last_price,
|
|
|
|
|
|
];
|
|
|
|
|
|
})->filter()->values();
|
|
|
|
|
|
|
2025-12-30 15:03:19 +08:00
|
|
|
|
return [
|
|
|
|
|
|
'id' => (string) $vendor->id,
|
|
|
|
|
|
'name' => $vendor->name,
|
2026-01-26 14:59:24 +08:00
|
|
|
|
'commonProducts' => $commonProducts
|
2025-12-30 15:03:19 +08:00
|
|
|
|
];
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
$warehouses = $this->inventoryService->getAllWarehouses()->map(function ($w) {
|
2025-12-30 15:03:19 +08:00
|
|
|
|
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',
|
2026-01-26 17:27:34 +08:00
|
|
|
|
'order_date' => 'required|date', // 新增驗證
|
2025-12-30 15:03:19 +08:00
|
|
|
|
'expected_delivery_date' => 'nullable|date',
|
|
|
|
|
|
'remark' => 'nullable|string',
|
2026-01-09 10:18:52 +08:00
|
|
|
|
'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',
|
2026-01-19 15:32:41 +08:00
|
|
|
|
'tax_amount' => 'nullable|numeric|min:0',
|
2025-12-30 15:03:19 +08:00
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
DB::beginTransaction();
|
|
|
|
|
|
|
2026-02-04 13:08:05 +08:00
|
|
|
|
// 生成單號:PO-YYYYMMDD-01
|
2025-12-30 15:03:19 +08:00
|
|
|
|
$today = now()->format('Ymd');
|
2026-02-04 13:08:05 +08:00
|
|
|
|
$prefix = 'PO-' . $today . '-';
|
2026-01-27 10:05:46 +08:00
|
|
|
|
$lastOrder = PurchaseOrder::where('code', 'like', $prefix . '%')
|
2025-12-30 15:03:19 +08:00
|
|
|
|
->lockForUpdate() // 鎖定以避免並發衝突
|
|
|
|
|
|
->orderBy('code', 'desc')
|
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
|
|
if ($lastOrder) {
|
2026-02-04 13:08:05 +08:00
|
|
|
|
// 取得最後 2 碼序號並加 1
|
|
|
|
|
|
$lastSequence = intval(substr($lastOrder->code, -2));
|
|
|
|
|
|
$sequence = str_pad($lastSequence + 1, 2, '0', STR_PAD_LEFT);
|
2025-12-30 15:03:19 +08:00
|
|
|
|
} else {
|
2026-02-04 13:08:05 +08:00
|
|
|
|
$sequence = '01';
|
2025-12-30 15:03:19 +08:00
|
|
|
|
}
|
2026-01-27 10:05:46 +08:00
|
|
|
|
$code = $prefix . $sequence;
|
2025-12-30 15:03:19 +08:00
|
|
|
|
|
|
|
|
|
|
$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
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 稅額計算
|
2026-01-19 15:32:41 +08:00
|
|
|
|
$taxAmount = isset($validated['tax_amount']) ? $validated['tax_amount'] : round($totalAmount * 0.05, 2);
|
2025-12-30 15:03:19 +08:00
|
|
|
|
$grandTotal = $totalAmount + $taxAmount;
|
|
|
|
|
|
|
2025-12-31 17:48:36 +08:00
|
|
|
|
// 確保有一個有效的使用者 ID
|
|
|
|
|
|
$userId = auth()->id();
|
|
|
|
|
|
if (!$userId) {
|
2026-01-27 08:59:45 +08:00
|
|
|
|
$user = $this->coreService->ensureSystemUserExists(); $userId = $user->id;
|
2025-12-31 17:48:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
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',
|
2026-01-26 17:27:34 +08:00
|
|
|
|
'order_date' => $validated['order_date'], // 新增
|
2025-12-30 15:03:19 +08:00
|
|
|
|
'expected_delivery_date' => $validated['expected_delivery_date'],
|
|
|
|
|
|
'total_amount' => $totalAmount,
|
|
|
|
|
|
'tax_amount' => $taxAmount,
|
|
|
|
|
|
'grand_total' => $grandTotal,
|
|
|
|
|
|
'remark' => $validated['remark'],
|
2026-01-09 10:18:52 +08:00
|
|
|
|
'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-26 14:59:24 +08:00
|
|
|
|
$order = PurchaseOrder::with(['vendor', 'items'])->findOrFail($id);
|
2025-12-30 15:03:19 +08:00
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 手動注入
|
|
|
|
|
|
$order->setRelation('warehouse', $this->inventoryService->getWarehouse($order->warehouse_id));
|
|
|
|
|
|
$order->setRelation('user', $this->coreService->getUser($order->user_id));
|
|
|
|
|
|
|
|
|
|
|
|
$productIds = $order->items->pluck('product_id')->unique()->toArray();
|
|
|
|
|
|
$products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id');
|
|
|
|
|
|
|
|
|
|
|
|
$formattedItems = $order->items->map(function ($item) use ($order, $products) {
|
|
|
|
|
|
$product = $products[$item->product_id] ?? null;
|
|
|
|
|
|
return (object) [
|
|
|
|
|
|
'productId' => (string) $item->product_id,
|
|
|
|
|
|
'productName' => $product?->name ?? 'Unknown',
|
|
|
|
|
|
'quantity' => (float) $item->quantity,
|
|
|
|
|
|
'unitId' => $item->unit_id,
|
|
|
|
|
|
'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 ?? 1),
|
|
|
|
|
|
'selectedUnit' => ($item->unit_id && $product?->large_unit_id && $item->unit_id == $product->large_unit_id) ? 'large' : 'base',
|
|
|
|
|
|
'unitPrice' => (float) $item->unit_price,
|
|
|
|
|
|
'previousPrice' => (float) (DB::table('product_vendor')->where('vendor_id', $order->vendor_id)->where('product_id', $item->product_id)->value('last_price') ?? 0),
|
|
|
|
|
|
'subtotal' => (float) $item->subtotal,
|
|
|
|
|
|
];
|
2026-01-06 15:45:13 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
$formattedOrder = (object) [
|
|
|
|
|
|
'id' => (string) $order->id,
|
|
|
|
|
|
'poNumber' => $order->code,
|
|
|
|
|
|
'supplierId' => (string) $order->vendor_id,
|
|
|
|
|
|
'supplierName' => $order->vendor?->name ?? 'Unknown',
|
2026-01-26 17:27:34 +08:00
|
|
|
|
'orderDate' => $order->order_date?->format('Y-m-d'), // 新增
|
2026-01-26 14:59:24 +08:00
|
|
|
|
'expectedDate' => $order->expected_delivery_date?->toISOString(),
|
|
|
|
|
|
'status' => $order->status,
|
|
|
|
|
|
'items' => $formattedItems,
|
|
|
|
|
|
'totalAmount' => (float) $order->total_amount,
|
|
|
|
|
|
'taxAmount' => (float) $order->tax_amount,
|
|
|
|
|
|
'grandTotal' => (float) $order->grand_total,
|
|
|
|
|
|
'createdAt' => $order->created_at->toISOString(),
|
|
|
|
|
|
'createdBy' => $order->user?->name ?? 'System',
|
|
|
|
|
|
'warehouse_id' => (int) $order->warehouse_id,
|
|
|
|
|
|
'warehouse_name' => $order->warehouse?->name ?? 'Unknown',
|
|
|
|
|
|
'remark' => $order->remark,
|
|
|
|
|
|
'invoiceNumber' => $order->invoice_number,
|
|
|
|
|
|
'invoiceDate' => $order->invoice_date,
|
|
|
|
|
|
'invoiceAmount' => (float) $order->invoice_amount,
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2025-12-30 15:03:19 +08:00
|
|
|
|
return Inertia::render('PurchaseOrder/Show', [
|
2026-01-26 14:59:24 +08:00
|
|
|
|
'order' => $formattedOrder
|
2025-12-30 15:03:19 +08:00
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function edit($id)
|
|
|
|
|
|
{
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 1. 獲取訂單
|
|
|
|
|
|
$order = PurchaseOrder::with(['items'])->findOrFail($id);
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 獲取廠商與商品(與 create 邏輯一致)
|
|
|
|
|
|
$vendors = Vendor::all();
|
|
|
|
|
|
$vendorIds = $vendors->pluck('id')->toArray();
|
|
|
|
|
|
$pivots = DB::table('product_vendor')->whereIn('vendor_id', $vendorIds)->get();
|
|
|
|
|
|
$productIds = $pivots->pluck('product_id')->unique()->toArray();
|
|
|
|
|
|
$products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id');
|
|
|
|
|
|
|
|
|
|
|
|
$vendors = $vendors->map(function ($vendor) use ($pivots, $products) {
|
|
|
|
|
|
$vendorProductPivots = $pivots->where('vendor_id', $vendor->id);
|
|
|
|
|
|
$commonProducts = $vendorProductPivots->map(function($pivot) use ($products) {
|
|
|
|
|
|
$product = $products[$pivot->product_id] ?? null;
|
|
|
|
|
|
if (!$product) return null;
|
|
|
|
|
|
return [
|
|
|
|
|
|
'productId' => (string) $product->id,
|
|
|
|
|
|
'productName' => $product->name,
|
|
|
|
|
|
'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,
|
|
|
|
|
|
'lastPrice' => (float) $pivot->last_price,
|
|
|
|
|
|
];
|
|
|
|
|
|
})->filter()->values();
|
2025-12-30 15:03:19 +08:00
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
'id' => (string) $vendor->id,
|
|
|
|
|
|
'name' => $vendor->name,
|
2026-01-26 14:59:24 +08:00
|
|
|
|
'commonProducts' => $commonProducts
|
2025-12-30 15:03:19 +08:00
|
|
|
|
];
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 3. 獲取倉庫
|
|
|
|
|
|
$warehouses = $this->inventoryService->getAllWarehouses()->map(function ($w) {
|
2025-12-30 15:03:19 +08:00
|
|
|
|
return [
|
|
|
|
|
|
'id' => (string) $w->id,
|
|
|
|
|
|
'name' => $w->name,
|
|
|
|
|
|
];
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 4. 注入訂單項目特定資料
|
|
|
|
|
|
// 2. 注入訂單項目
|
|
|
|
|
|
$itemProductIds = $order->items->pluck('product_id')->toArray();
|
|
|
|
|
|
$itemProducts = $this->inventoryService->getProductsByIds($itemProductIds)->keyBy('id');
|
|
|
|
|
|
|
2026-01-08 16:32:10 +08:00
|
|
|
|
$vendorId = $order->vendor_id;
|
2026-01-26 14:59:24 +08:00
|
|
|
|
$formattedItems = $order->items->map(function ($item) use ($vendorId, $itemProducts) {
|
|
|
|
|
|
$product = $itemProducts[$item->product_id] ?? null;
|
|
|
|
|
|
return (object) [
|
|
|
|
|
|
'productId' => (string) $item->product_id,
|
|
|
|
|
|
'productName' => $product?->name ?? 'Unknown',
|
|
|
|
|
|
'quantity' => (float) $item->quantity,
|
|
|
|
|
|
'unitId' => $item->unit_id,
|
|
|
|
|
|
'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,
|
|
|
|
|
|
'conversion_rate' => (float) ($product?->conversion_rate ?? 1),
|
|
|
|
|
|
'selectedUnit' => ($item->unit_id && $product?->large_unit_id && $item->unit_id == $product->large_unit_id) ? 'large' : 'base',
|
|
|
|
|
|
'unitPrice' => (float) $item->unit_price,
|
|
|
|
|
|
'previousPrice' => (float) (DB::table('product_vendor')->where('vendor_id', $vendorId)->where('product_id', $item->product_id)->value('last_price') ?? 0),
|
|
|
|
|
|
'subtotal' => (float) $item->subtotal,
|
|
|
|
|
|
];
|
2026-01-06 15:45:13 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
$formattedOrder = (object) [
|
|
|
|
|
|
'id' => (string) $order->id,
|
|
|
|
|
|
'poNumber' => $order->code,
|
|
|
|
|
|
'supplierId' => (string) $order->vendor_id,
|
|
|
|
|
|
'warehouse_id' => (int) $order->warehouse_id,
|
2026-01-26 17:27:34 +08:00
|
|
|
|
'orderDate' => $order->order_date?->format('Y-m-d'), // 新增
|
2026-01-26 14:59:24 +08:00
|
|
|
|
'expectedDate' => $order->expected_delivery_date?->format('Y-m-d'),
|
|
|
|
|
|
'status' => $order->status,
|
|
|
|
|
|
'items' => $formattedItems,
|
|
|
|
|
|
'remark' => $order->remark,
|
|
|
|
|
|
'invoiceNumber' => $order->invoice_number,
|
|
|
|
|
|
'invoiceDate' => $order->invoice_date,
|
|
|
|
|
|
'invoiceAmount' => (float) $order->invoice_amount,
|
|
|
|
|
|
'taxAmount' => (float) $order->tax_amount,
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2025-12-30 15:03:19 +08:00
|
|
|
|
return Inertia::render('PurchaseOrder/Create', [
|
2026-01-26 14:59:24 +08:00
|
|
|
|
'order' => $formattedOrder,
|
2025-12-30 15:03:19 +08:00
|
|
|
|
'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',
|
2026-01-26 17:27:34 +08:00
|
|
|
|
'order_date' => 'required|date', // 新增驗證
|
2025-12-30 15:03:19 +08:00
|
|
|
|
'expected_delivery_date' => 'nullable|date',
|
|
|
|
|
|
'remark' => 'nullable|string',
|
2026-01-27 17:23:31 +08:00
|
|
|
|
'status' => 'required|string|in:draft,pending,approved,partial,completed,closed,cancelled',
|
2026-01-09 10:18:52 +08:00
|
|
|
|
'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',
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 允許 tax_amount 和 taxAmount 以保持相容性
|
2026-01-19 15:32:41 +08:00
|
|
|
|
'tax_amount' => 'nullable|numeric|min:0',
|
|
|
|
|
|
'taxAmount' => 'nullable|numeric|min:0',
|
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
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 稅額計算(處理兩個鍵)
|
2026-01-19 15:32:41 +08:00
|
|
|
|
$inputTax = $validated['tax_amount'] ?? $validated['taxAmount'] ?? null;
|
|
|
|
|
|
$taxAmount = !is_null($inputTax) ? $inputTax : round($totalAmount * 0.05, 2);
|
2025-12-30 15:03:19 +08:00
|
|
|
|
$grandTotal = $totalAmount + $taxAmount;
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 1. 填充屬性但暫不儲存以捕捉變更
|
2026-01-19 15:32:41 +08:00
|
|
|
|
$order->fill([
|
2025-12-30 15:03:19 +08:00
|
|
|
|
'vendor_id' => $validated['vendor_id'],
|
|
|
|
|
|
'warehouse_id' => $validated['warehouse_id'],
|
2026-01-26 17:27:34 +08:00
|
|
|
|
'order_date' => $validated['order_date'], // 新增
|
2025-12-30 15:03:19 +08:00
|
|
|
|
'expected_delivery_date' => $validated['expected_delivery_date'],
|
|
|
|
|
|
'total_amount' => $totalAmount,
|
|
|
|
|
|
'tax_amount' => $taxAmount,
|
|
|
|
|
|
'grand_total' => $grandTotal,
|
|
|
|
|
|
'remark' => $validated['remark'],
|
|
|
|
|
|
'status' => $validated['status'],
|
2026-01-09 10:18:52 +08:00
|
|
|
|
'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
|
|
|
|
]);
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 捕捉變更屬性以進行手動記錄
|
2026-01-19 15:32:41 +08:00
|
|
|
|
$dirty = $order->getDirty();
|
|
|
|
|
|
$oldAttributes = [];
|
|
|
|
|
|
$newAttributes = [];
|
|
|
|
|
|
|
|
|
|
|
|
foreach ($dirty as $key => $value) {
|
|
|
|
|
|
$oldAttributes[$key] = $order->getOriginal($key);
|
|
|
|
|
|
$newAttributes[$key] = $value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 儲存但不觸發事件(防止重複記錄)
|
2026-01-19 15:32:41 +08:00
|
|
|
|
$order->saveQuietly();
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 2. 捕捉包含商品名稱的舊項目以進行比對
|
2026-01-27 13:27:28 +08:00
|
|
|
|
$oldItemsCollection = $order->items()->get();
|
|
|
|
|
|
$oldProductIds = $oldItemsCollection->pluck('product_id')->unique()->toArray();
|
|
|
|
|
|
$oldProducts = $this->inventoryService->getProductsByIds($oldProductIds)->keyBy('id');
|
|
|
|
|
|
// 注意:單位的獲取可能也需要透過 InventoryService,但目前假設單位的關聯是合法的(如果在同一模組)
|
|
|
|
|
|
// 如果單位也在不同模組,則需要另外處理。這裡暫時假設可以動手水和一下基本單位名稱。
|
|
|
|
|
|
|
|
|
|
|
|
$oldItems = $oldItemsCollection->map(function($item) use ($oldProducts) {
|
|
|
|
|
|
$product = $oldProducts->get($item->product_id);
|
2026-01-19 15:32:41 +08:00
|
|
|
|
return [
|
|
|
|
|
|
'id' => $item->id,
|
|
|
|
|
|
'product_id' => $item->product_id,
|
2026-01-27 13:27:28 +08:00
|
|
|
|
'product_name' => $product?->name ?? 'Unknown',
|
2026-01-19 15:32:41 +08:00
|
|
|
|
'quantity' => (float) $item->quantity,
|
|
|
|
|
|
'unit_id' => $item->unit_id,
|
2026-01-27 13:27:28 +08:00
|
|
|
|
'unit_name' => 'N/A', // 簡化處理,或可透過服務獲取
|
2026-01-19 15:32:41 +08:00
|
|
|
|
'subtotal' => (float) $item->subtotal,
|
|
|
|
|
|
];
|
|
|
|
|
|
})->keyBy('product_id');
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 同步項目(原始邏輯)
|
2025-12-30 15:03:19 +08:00
|
|
|
|
$order->items()->delete();
|
2026-01-19 15:32:41 +08:00
|
|
|
|
|
|
|
|
|
|
$newItemsData = [];
|
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;
|
|
|
|
|
|
|
2026-01-19 15:32:41 +08:00
|
|
|
|
$newItem = $order->items()->create([
|
2025-12-30 15:03:19 +08:00
|
|
|
|
'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
|
|
|
|
]);
|
2026-01-19 15:32:41 +08:00
|
|
|
|
$newItemsData[] = $newItem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 3. 計算項目差異
|
2026-01-19 15:32:41 +08:00
|
|
|
|
$itemDiffs = [
|
|
|
|
|
|
'added' => [],
|
|
|
|
|
|
'removed' => [],
|
|
|
|
|
|
'updated' => [],
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-01-27 13:27:28 +08:00
|
|
|
|
// 重新獲取新項目並水和產品資料
|
|
|
|
|
|
$newItemsCollection = $order->items()->get();
|
|
|
|
|
|
$newProductIds = $newItemsCollection->pluck('product_id')->unique()->toArray();
|
|
|
|
|
|
$newProducts = $this->inventoryService->getProductsByIds($newProductIds)->keyBy('id');
|
|
|
|
|
|
|
|
|
|
|
|
$newItemsFormatted = $newItemsCollection->map(function($item) use ($newProducts) {
|
|
|
|
|
|
$product = $newProducts->get($item->product_id);
|
2026-01-19 15:32:41 +08:00
|
|
|
|
return [
|
|
|
|
|
|
'product_id' => $item->product_id,
|
2026-01-27 13:27:28 +08:00
|
|
|
|
'product_name' => $product?->name ?? 'Unknown',
|
2026-01-19 15:32:41 +08:00
|
|
|
|
'quantity' => (float) $item->quantity,
|
|
|
|
|
|
'unit_id' => $item->unit_id,
|
2026-01-27 13:27:28 +08:00
|
|
|
|
'unit_name' => 'N/A',
|
2026-01-19 15:32:41 +08:00
|
|
|
|
'subtotal' => (float) $item->subtotal,
|
|
|
|
|
|
];
|
|
|
|
|
|
})->keyBy('product_id');
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 找出已移除的項目
|
2026-01-19 15:32:41 +08:00
|
|
|
|
foreach ($oldItems as $productId => $oldItem) {
|
|
|
|
|
|
if (!$newItemsFormatted->has($productId)) {
|
|
|
|
|
|
$itemDiffs['removed'][] = $oldItem;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 找出新增和更新的項目
|
2026-01-19 15:32:41 +08:00
|
|
|
|
foreach ($newItemsFormatted as $productId => $newItem) {
|
|
|
|
|
|
if (!$oldItems->has($productId)) {
|
|
|
|
|
|
$itemDiffs['added'][] = $newItem;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$oldItem = $oldItems[$productId];
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 比對欄位
|
2026-01-19 15:32:41 +08:00
|
|
|
|
if (
|
|
|
|
|
|
$oldItem['quantity'] != $newItem['quantity'] ||
|
|
|
|
|
|
$oldItem['unit_id'] != $newItem['unit_id'] ||
|
|
|
|
|
|
$oldItem['subtotal'] != $newItem['subtotal']
|
|
|
|
|
|
) {
|
|
|
|
|
|
$itemDiffs['updated'][] = [
|
|
|
|
|
|
'product_name' => $newItem['product_name'],
|
|
|
|
|
|
'old' => [
|
|
|
|
|
|
'quantity' => $oldItem['quantity'],
|
|
|
|
|
|
'unit_name' => $oldItem['unit_name'],
|
|
|
|
|
|
'subtotal' => $oldItem['subtotal'],
|
|
|
|
|
|
],
|
|
|
|
|
|
'new' => [
|
|
|
|
|
|
'quantity' => $newItem['quantity'],
|
|
|
|
|
|
'unit_name' => $newItem['unit_name'],
|
|
|
|
|
|
'subtotal' => $newItem['subtotal'],
|
|
|
|
|
|
]
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 4. 手動記錄活動(單一整合記錄)
|
|
|
|
|
|
// 如果有屬性變更或項目變更則記錄
|
2026-01-19 15:32:41 +08:00
|
|
|
|
if (!empty($newAttributes) || !empty($itemDiffs['added']) || !empty($itemDiffs['removed']) || !empty($itemDiffs['updated'])) {
|
|
|
|
|
|
activity()
|
|
|
|
|
|
->performedOn($order)
|
|
|
|
|
|
->causedBy(auth()->user())
|
|
|
|
|
|
->event('updated')
|
|
|
|
|
|
->withProperties([
|
|
|
|
|
|
'attributes' => $newAttributes,
|
|
|
|
|
|
'old' => $oldAttributes,
|
|
|
|
|
|
'items_diff' => $itemDiffs,
|
|
|
|
|
|
'snapshot' => [
|
|
|
|
|
|
'po_number' => $order->code,
|
|
|
|
|
|
'vendor_name' => $order->vendor?->name,
|
|
|
|
|
|
'warehouse_name' => $order->warehouse?->name,
|
|
|
|
|
|
'user_name' => $order->user?->name,
|
|
|
|
|
|
]
|
|
|
|
|
|
])
|
|
|
|
|
|
->log('updated');
|
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();
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
$order = PurchaseOrder::with(['items'])->findOrFail($id);
|
2026-01-19 15:32:41 +08:00
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 為記錄注入資料
|
|
|
|
|
|
$productIds = $order->items->pluck('product_id')->unique()->toArray();
|
|
|
|
|
|
$products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id');
|
|
|
|
|
|
|
|
|
|
|
|
// 捕捉項目以進行記錄
|
|
|
|
|
|
$items = $order->items->map(function ($item) use ($products) {
|
|
|
|
|
|
$product = $products[$item->product_id] ?? null;
|
2026-01-19 15:32:41 +08:00
|
|
|
|
return [
|
2026-01-26 14:59:24 +08:00
|
|
|
|
'product_name' => $product?->name ?? 'Unknown',
|
2026-01-19 15:32:41 +08:00
|
|
|
|
'quantity' => floatval($item->quantity),
|
2026-01-26 14:59:24 +08:00
|
|
|
|
'unit_name' => 'N/A',
|
2026-01-19 15:32:41 +08:00
|
|
|
|
'subtotal' => floatval($item->subtotal),
|
|
|
|
|
|
];
|
|
|
|
|
|
})->toArray();
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 手動記錄包含項目的刪除操作
|
2026-01-19 15:32:41 +08:00
|
|
|
|
activity()
|
|
|
|
|
|
->performedOn($order)
|
|
|
|
|
|
->causedBy(auth()->user())
|
|
|
|
|
|
->event('deleted')
|
|
|
|
|
|
->withProperties([
|
|
|
|
|
|
'attributes' => $order->getAttributes(),
|
|
|
|
|
|
'items_diff' => [
|
|
|
|
|
|
'added' => [],
|
|
|
|
|
|
'removed' => $items,
|
|
|
|
|
|
'updated' => [],
|
|
|
|
|
|
],
|
|
|
|
|
|
'snapshot' => [
|
|
|
|
|
|
'po_number' => $order->code,
|
|
|
|
|
|
'vendor_name' => $order->vendor?->name,
|
|
|
|
|
|
'warehouse_name' => $order->warehouse?->name,
|
|
|
|
|
|
'user_name' => $order->user?->name,
|
|
|
|
|
|
]
|
|
|
|
|
|
])
|
|
|
|
|
|
->log('deleted');
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 對此操作停用自動記錄
|
2026-01-19 15:32:41 +08:00
|
|
|
|
$order->disableLogging();
|
2025-12-30 17:05:19 +08:00
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
// 先刪除關聯項目
|
2025-12-30 17:05:19 +08:00
|
|
|
|
$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
|
|
|
|
}
|