goodsReceiptService = $goodsReceiptService; $this->inventoryService = $inventoryService; $this->procurementService = $procurementService; $this->duplicateCheckService = $duplicateCheckService; } public function index(Request $request) { $query = GoodsReceipt::query() ->select(['id', 'code', 'type', 'warehouse_id', 'vendor_id', 'received_date', 'status', 'created_at']) ->with(['warehouse']) ->withSum('items', 'total_amount'); // 關鍵字搜尋(單號) if ($request->filled('search')) { $search = $request->input('search'); $query->where('code', 'like', "%{$search}%"); } // 狀態篩選 if ($request->filled('status') && $request->input('status') !== 'all') { $query->where('status', $request->input('status')); } // 倉庫篩選 if ($request->filled('warehouse_id') && $request->input('warehouse_id') !== 'all') { $query->where('warehouse_id', $request->input('warehouse_id')); } // 日期範圍篩選 if ($request->filled('date_start')) { $query->whereDate('received_date', '>=', $request->input('date_start')); } if ($request->filled('date_end')) { $query->whereDate('received_date', '<=', $request->input('date_end')); } // 每頁筆數 $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); $perPage = $request->input('per_page', $defaultPerPage); if (!in_array((int)$perPage, [10, 20, 50, 100])) { $perPage = $defaultPerPage; } $receipts = $query->orderBy('created_at', 'desc') ->paginate($perPage) ->withQueryString(); // Manual Hydration for Vendors (Cross-Module) $vendorIds = collect($receipts->items())->pluck('vendor_id')->unique()->filter()->toArray(); $vendors = $this->procurementService->getVendorsByIds($vendorIds)->keyBy('id'); $receipts->getCollection()->transform(function ($receipt) use ($vendors) { $receipt->vendor = $vendors->get($receipt->vendor_id); return $receipt; }); // 取得倉庫列表用於篩選 $warehouses = $this->inventoryService->getAllWarehouses(); return Inertia::render('Inventory/GoodsReceipt/Index', [ 'receipts' => $receipts, 'filters' => $request->only(['search', 'status', 'warehouse_id', 'date_start', 'date_end', 'per_page']), 'warehouses' => $warehouses, ]); } public function show(Request $request, $id) { $receipt = GoodsReceipt::with([ 'warehouse', 'items.product.category', 'items.product.baseUnit' ])->findOrFail($id); // Manual Hydration for Vendor (Cross-Module) if ($receipt->vendor_id) { $receipt->vendor = $this->procurementService->getVendorsByIds([$receipt->vendor_id])->first(); } // 手動計算統計資訊 (如果 Model 沒有定義對應的 Attribute) $receipt->items_sum_total_amount = $receipt->items->sum('total_amount'); return Inertia::render('Inventory/GoodsReceipt/Show', [ 'receipt' => $receipt, 'navigation' => [ 'from' => $request->query('from'), 'from_id' => $request->query('from_id'), 'from_label' => $request->query('from_label'), ] ]); } public function create() { // 取得待進貨的採購單列表(用於標準採購類型選擇) $pendingPOs = $this->procurementService->getPendingPurchaseOrders(); // 提取所有產品 ID 以便跨模組水和資料 $productIds = $pendingPOs->flatMap(fn($po) => $po->items->pluck('product_id'))->unique()->filter()->toArray(); $products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id'); // 處理採購單資料,計算剩餘可收貨數量 $formattedPOs = $pendingPOs->map(function ($po) use ($products) { return [ 'id' => $po->id, 'code' => $po->code, 'status' => $po->status, 'supplierId' => $po->vendor_id, // Alias for frontend 'vendor_id' => $po->vendor_id, 'vendor_name' => $po->vendor?->name ?? '', 'warehouse_id' => $po->warehouse_id, 'order_date' => $po->order_date, 'items' => $po->items->map(function ($item) use ($products) { $product = $products->get($item->product_id); $remaining = max(0, $item->quantity - ($item->received_quantity ?? 0)); // 獲取單位名稱 $baseUnitName = $product?->baseUnit?->name ?? '個'; $largeUnitName = $product?->largeUnit?->name ?? ''; // 判斷當前採購使用的單位 (這需要從 PurchaseOrderItem 獲取 unit_id 並與產品的 large_unit_id 比較) $selectedUnit = 'base'; if ($item->unit_id && $product && $item->unit_id == $product->large_unit_id) { $selectedUnit = 'large'; } return [ 'id' => $item->id, 'productId' => $item->product_id, // Alias for frontend 'product_id' => $item->product_id, 'productName' => $product?->name ?? '', // Alias for frontend 'product_name' => $product?->name ?? '', 'product_code' => $product?->code ?? '', 'unit' => $product?->baseUnit?->name ?? '個', // 預設顯示文字 'selectedUnit' => $selectedUnit, 'base_unit_id' => $product?->base_unit_id, 'base_unit_name' => $baseUnitName, 'large_unit_id' => $product?->large_unit_id, 'large_unit_name' => $largeUnitName, 'conversion_rate' => $product?->conversion_rate ?? 1, 'quantity' => $item->quantity, 'received_quantity' => $item->received_quantity ?? 0, 'remaining' => $remaining, 'unitPrice' => $item->unit_price, // Alias for frontend 'unit_price' => $item->unit_price, ]; })->filter(fn($item) => $item['remaining'] > 0)->values(), ]; })->filter(fn($po) => $po['items']->count() > 0)->values(); // 取得所有廠商列表(用於雜項入庫/其他類型選擇) $vendors = $this->procurementService->getAllVendors(); return Inertia::render('Inventory/GoodsReceipt/Create', [ 'warehouses' => $this->inventoryService->getAllWarehouses(), 'pendingPurchaseOrders' => $formattedPOs, 'vendors' => $vendors, ]); } public function store(Request $request) { $validated = $request->validate($this->getItemValidationRules()); try { $this->goodsReceiptService->store($request->all()); return redirect()->route('goods-receipts.index')->with('success', '進貨草稿已建立'); } catch (\Exception $e) { return back()->with('error', $e->getMessage()); } } /** * 編輯進貨單(僅草稿/退回狀態) */ public function edit(GoodsReceipt $goodsReceipt) { if (!in_array($goodsReceipt->status, [GoodsReceipt::STATUS_DRAFT, 'rejected'])) { return redirect()->route('goods-receipts.show', $goodsReceipt->id) ->with('error', '只有草稿或被退回的進貨單可以編輯。'); } // 載入品項與產品資訊 $goodsReceipt->load('items'); // 取得品項關聯的商品資訊 $productIds = $goodsReceipt->items->pluck('product_id')->unique()->toArray(); $products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id'); // 如果是標準採購,取得對應採購單的品項,以帶出預定數量與已收數量 $poItems = collect(); $po = null; if ($goodsReceipt->type === 'standard' && $goodsReceipt->purchase_order_id) { $po = clone $this->procurementService->getPurchaseOrdersByIds([$goodsReceipt->purchase_order_id], ['items', 'vendor'])->first(); if ($po) { $poItems = $po->items->keyBy('id'); } } // 格式化品項資料 $formattedItems = $goodsReceipt->items->map(function ($item) use ($products, $poItems) { $product = $products->get($item->product_id); $poItem = $poItems->get($item->purchase_order_item_id); // 判斷單位 $selectedUnit = 'base'; if ($poItem && $product && $poItem->unit_id && $poItem->unit_id == $product->large_unit_id) { $selectedUnit = 'large'; } return [ 'product_id' => $item->product_id, 'purchase_order_item_id' => $item->purchase_order_item_id, 'product_name' => $product?->name ?? '', 'product_code' => $product?->code ?? '', 'unit' => $poItem && $selectedUnit === 'large' ? ($product?->largeUnit?->name ?? '') : ($product?->baseUnit?->name ?? '個'), 'selectedUnit' => $selectedUnit, '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' => $product?->conversion_rate ?? 1, 'quantity_ordered' => $poItem ? $poItem->quantity : null, 'quantity_received_so_far' => $poItem ? ($poItem->received_quantity ?? 0) : null, 'quantity_received' => (float) $item->quantity_received, 'unit_price' => (float) $item->unit_price, 'subtotal' => (float) $item->total_amount, 'batch_number' => $item->batch_number ?? '', 'batchMode' => 'existing', 'originCountry' => 'TW', 'expiry_date' => $item->expiry_date ?? '', ]; })->values(); // 同 create() 一樣傳入所需的 props $pendingPOs = $this->procurementService->getPendingPurchaseOrders(); $productIdsForPOs = $pendingPOs->flatMap(fn($po) => $po->items->pluck('product_id'))->unique()->filter()->toArray(); $productsForPOs = $this->inventoryService->getProductsByIds($productIdsForPOs)->keyBy('id'); $formattedPOs = $pendingPOs->map(function ($po) use ($productsForPOs) { return [ 'id' => $po->id, 'code' => $po->code, 'status' => $po->status, 'supplierId' => $po->vendor_id, 'vendor_id' => $po->vendor_id, 'vendor_name' => $po->vendor?->name ?? '', 'warehouse_id' => $po->warehouse_id, 'order_date' => $po->order_date, 'items' => $po->items->map(function ($item) use ($productsForPOs) { $product = $productsForPOs->get($item->product_id); $remaining = max(0, $item->quantity - ($item->received_quantity ?? 0)); $selectedUnit = 'base'; if ($item->unit_id && $product && $item->unit_id == $product->large_unit_id) { $selectedUnit = 'large'; } return [ 'id' => $item->id, 'productId' => $item->product_id, 'product_id' => $item->product_id, 'productName' => $product?->name ?? '', 'product_name' => $product?->name ?? '', 'product_code' => $product?->code ?? '', 'unit' => $item->unit_id && $product && $item->unit_id == $product->large_unit_id ? ($product?->largeUnit?->name ?? '') : ($product?->baseUnit?->name ?? '個'), 'selectedUnit' => $selectedUnit, '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' => $product?->conversion_rate ?? 1, 'quantity' => $item->quantity, 'received_quantity' => $item->received_quantity ?? 0, 'remaining' => $remaining, 'unitPrice' => $item->unit_price, 'unit_price' => $item->unit_price, ]; })->filter(fn($item) => $item['remaining'] > 0)->values(), ]; })->filter(fn($po) => $po['items']->count() > 0)->values(); $vendors = $this->procurementService->getAllVendors(); // Manual Hydration for Vendor $vendor = null; if ($goodsReceipt->vendor_id) { $vendor = $this->procurementService->getVendorsByIds([$goodsReceipt->vendor_id])->first(); } // 格式化 Purchase Order 給前端顯示 $formattedPO = null; if ($po) { $formattedPO = [ 'id' => $po->id, 'code' => $po->code, 'status' => $po->status, 'vendor_id' => $po->vendor_id, 'vendor_name' => $po->vendor?->name ?? '', 'warehouse_id' => $po->warehouse_id, 'order_date' => $po->order_date, 'items' => $po->items->toArray(), // simplified since we just need items.length for display ]; } return Inertia::render('Inventory/GoodsReceipt/Create', [ 'warehouses' => $this->inventoryService->getAllWarehouses(), 'pendingPurchaseOrders' => $formattedPOs, 'vendors' => $vendors, 'receipt' => [ 'id' => $goodsReceipt->id, 'code' => $goodsReceipt->code, 'type' => $goodsReceipt->type, 'warehouse_id' => $goodsReceipt->warehouse_id, 'vendor_id' => $goodsReceipt->vendor_id, 'vendor' => $vendor, 'purchase_order_id' => $goodsReceipt->purchase_order_id, 'purchase_order' => $formattedPO, 'received_date' => \Carbon\Carbon::parse($goodsReceipt->received_date)->format('Y-m-d'), 'remarks' => $goodsReceipt->remarks, 'items' => $formattedItems, ], ]); } /** * 更新進貨單 */ public function update(Request $request, GoodsReceipt $goodsReceipt) { $validated = $request->validate($this->getItemValidationRules()); try { $this->goodsReceiptService->update($goodsReceipt, $request->all()); return redirect()->route('goods-receipts.show', $goodsReceipt->id)->with('success', '進貨單已更新'); } catch (\Exception $e) { return back()->with('error', $e->getMessage()); } } /** * 取得品項驗證規則 */ private function getItemValidationRules(): array { return [ 'warehouse_id' => 'required|exists:warehouses,id', 'type' => 'required|in:standard,miscellaneous,other', 'purchase_order_id' => 'nullable|required_if:type,standard|exists:purchase_orders,id', 'vendor_id' => 'nullable|integer', 'received_date' => 'required|date', 'remarks' => 'nullable|string', 'items' => 'required|array|min:1', 'items.*.product_id' => 'required|integer|exists:products,id', 'items.*.purchase_order_item_id' => 'nullable|required_if:type,standard|integer', 'items.*.quantity_received' => 'required|numeric|min:0', 'items.*.unit_price' => 'nullable|numeric|min:0', 'items.*.subtotal' => 'nullable|numeric|min:0', 'items.*.batch_number' => 'nullable|string', 'items.*.expiry_date' => 'nullable|date', 'force' => 'nullable|boolean', ]; } /** * 預檢重複進貨 API */ public function checkDuplicate(Request $request) { $result = $this->duplicateCheckService->checkDuplicateReceipt($request->all()); return response()->json($result); } public function submit(GoodsReceipt $goodsReceipt) { if (!auth()->user()->can('goods_receipts.edit')) { return back()->with('error', '您沒有權限確認點收'); } try { $this->goodsReceiptService->submit($goodsReceipt); return back()->with('success', '進貨單已點收完成,庫存已增加並拋轉應付帳款'); } catch (\Exception $e) { return back()->with('error', $e->getMessage()); } } // API to search POs public function searchPOs(Request $request) { $search = $request->input('query'); if (!$search) { return response()->json([]); } $pos = $this->procurementService->searchPendingPurchaseOrders($search); return response()->json($pos); } // API to search Products for Manual Entry // 支援 query='*' 回傳所有商品(用於 SearchableSelect 下拉選單) public function searchProducts(Request $request) { $search = $request->input('query'); if (!$search) { return response()->json([]); } // 萬用字元:回傳所有商品 if ($search === '*') { $products = $this->inventoryService->getProductsByName(''); } else { $products = $this->inventoryService->getProductsByName($search); } // Format for frontend $mapped = $products->map(function($product) { return [ 'id' => $product->id, 'name' => $product->name, 'code' => $product->code, 'unit' => $product->baseUnit?->name ?? '個', 'price' => $product->purchase_price ?? 0, ]; }); return response()->json($mapped); } // API to search Vendors public function searchVendors(Request $request) { $search = $request->input('query'); if (!$search) { return response()->json([]); } $vendors = $this->procurementService->searchVendors($search); return response()->json($vendors); } /** * 刪除進貨單 */ public function destroy(GoodsReceipt $goodsReceipt) { // 只有有權限的人可以刪除 if (!auth()->user()->can('goods_receipts.delete')) { return redirect()->back()->with('error', '您沒有權限刪除進貨單'); } // 簡單刪除邏輯:刪除進貨單(品項由資料庫級聯刪除或手動處理) // 注意:實務上可能需要處理已入庫的庫存回滾,但在這個簡易 ERP 中通常是行政刪除 $goodsReceipt->delete(); return redirect()->route('goods-receipts.index')->with('success', '進貨單已刪除'); } }