feat(procurement): 實作採購退回單模組並修復商品選單報錯
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 58s

This commit is contained in:
2026-02-25 13:49:02 +08:00
parent deef3baacc
commit c4908533a8
21 changed files with 2409 additions and 1 deletions

View File

@@ -0,0 +1,250 @@
<?php
namespace App\Modules\Procurement\Controllers;
use App\Http\Controllers\Controller;
use App\Modules\Procurement\Models\PurchaseReturn;
use App\Modules\Procurement\Models\Vendor;
use App\Modules\Procurement\Services\PurchaseReturnService;
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Illuminate\Support\Facades\DB;
class PurchaseReturnController extends Controller
{
public function __construct(
protected PurchaseReturnService $purchaseReturnService,
protected InventoryServiceInterface $inventoryService
) {}
public function index(Request $request)
{
$query = PurchaseReturn::with(['vendor', 'user'])
->orderBy('id', 'desc');
if ($request->filled('search')) {
$search = $request->search;
$query->where('code', 'like', "%{$search}%")
->orWhereHas('vendor', function($q) use ($search) {
$q->where('name', 'like', "%{$search}%");
});
}
if ($request->filled('status') && $request->status !== 'all') {
$query->where('status', $request->status);
}
$purchaseReturns = $query->paginate(15)->withQueryString();
return Inertia::render('PurchaseReturn/Index', [
'purchaseReturns' => $purchaseReturns,
'filters' => $request->only(['search', 'status']),
]);
}
public function create()
{
// 取得可用的倉庫與廠商資料供前端選單使用
$warehouses = $this->inventoryService->getAllWarehouses();
$vendors = Vendor::all();
// 手動注入:獲取廠商商品 (與 PurchaseOrderController 邏輯一致)
$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->get($pivot->product_id);
if (!$product) return null;
return [
'productId' => (string) $product->id,
'productName' => $product->name,
'lastPrice' => (float) $pivot->last_price,
];
})->filter()->values();
return [
'id' => (string) $vendor->id,
'name' => $vendor->name,
'commonProducts' => $commonProducts
];
});
return Inertia::render('PurchaseReturn/Create', [
'warehouses' => $warehouses,
'vendors' => $vendors,
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'vendor_id' => 'required|exists:vendors,id',
'warehouse_id' => 'required|integer', // 透過 interface 無法直接 exists
'return_date' => 'required|date',
'remarks' => 'nullable|string',
'tax_amount' => 'nullable|numeric|min:0',
'items' => 'required|array|min:1',
'items.*.product_id' => 'required|integer',
'items.*.quantity_returned' => 'required|numeric|min:0.01',
'items.*.unit_price' => 'required|numeric|min:0',
'items.*.batch_number' => 'nullable|string',
]);
try {
$pr = $this->purchaseReturnService->store($validated);
return redirect()->route('procurement.purchase-returns.show', $pr->id)
->with('flash', ['success' => '退貨單草稿建立成功']);
} catch (\Exception $e) {
return back()->with('flash', ['error' => $e->getMessage()]);
}
}
public function show(PurchaseReturn $purchaseReturn)
{
$purchaseReturn->load(['vendor', 'user', 'items']);
// 取出 product name (依賴反轉)
$productIds = $purchaseReturn->items->pluck('product_id')->unique()->toArray();
$products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id');
// 取出 warehouse name
$warehouse = $this->inventoryService->getWarehouse($purchaseReturn->warehouse_id);
$purchaseReturn->warehouse_name = $warehouse ? $warehouse->name : '未知倉庫';
$purchaseReturn->items->transform(function($item) use ($products) {
$item->product_name = $products->get($item->product_id)->name ?? '未知商品';
$item->product_code = $products->get($item->product_id)->code ?? '';
return $item;
});
// 整理歷史紀錄
$activities = \Spatie\Activitylog\Models\Activity::where('subject_type', PurchaseReturn::class)
->where('subject_id', $purchaseReturn->id)
->orderBy('created_at', 'desc')
->get();
return Inertia::render('PurchaseReturn/Show', [
'purchaseReturn' => $purchaseReturn,
'activities' => $activities,
]);
}
public function edit(PurchaseReturn $purchaseReturn)
{
if ($purchaseReturn->status !== PurchaseReturn::STATUS_DRAFT) {
return redirect()->route('procurement.purchase-returns.show', $purchaseReturn->id)
->with('flash', ['error' => '只有草稿狀態的退貨單能編輯']);
}
$purchaseReturn->load(['items']);
$productIds = $purchaseReturn->items->pluck('product_id')->unique()->toArray();
$products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id');
$purchaseReturn->items->transform(function($item) use ($products) {
$product = $products->get($item->product_id);
$item->product = $product;
return $item;
});
$warehouses = $this->inventoryService->getAllWarehouses();
$vendors = Vendor::all();
// 手動注入:獲取廠商商品 (與 create 邏輯一致)
$vendorIds = $vendors->pluck('id')->toArray();
$pivots = DB::table('product_vendor')->whereIn('vendor_id', $vendorIds)->get();
$allProductIds = $pivots->pluck('product_id')->unique()->toArray();
$allProducts = $this->inventoryService->getProductsByIds($allProductIds)->keyBy('id');
$vendors = $vendors->map(function ($vendor) use ($pivots, $allProducts) {
$vendorProductPivots = $pivots->where('vendor_id', $vendor->id);
$commonProducts = $vendorProductPivots->map(function($pivot) use ($allProducts) {
$product = $allProducts->get($pivot->product_id);
if (!$product) return null;
return [
'productId' => (string) $product->id,
'productName' => $product->name,
'lastPrice' => (float) $pivot->last_price,
];
})->filter()->values();
return [
'id' => (string) $vendor->id,
'name' => $vendor->name,
'commonProducts' => $commonProducts
];
});
return Inertia::render('PurchaseReturn/Edit', [
'purchaseReturn' => $purchaseReturn,
'warehouses' => $warehouses,
'vendors' => $vendors,
]);
}
public function update(Request $request, PurchaseReturn $purchaseReturn)
{
$validated = $request->validate([
'vendor_id' => 'required|exists:vendors,id',
'warehouse_id' => 'required|integer',
'return_date' => 'required|date',
'remarks' => 'nullable|string',
'tax_amount' => 'nullable|numeric|min:0',
'items' => 'required|array|min:1',
'items.*.product_id' => 'required|integer',
'items.*.quantity_returned' => 'required|numeric|min:0.01',
'items.*.unit_price' => 'required|numeric|min:0',
'items.*.batch_number' => 'nullable|string',
]);
try {
$this->purchaseReturnService->update($purchaseReturn, $validated);
return redirect()->route('procurement.purchase-returns.show', $purchaseReturn->id)
->with('flash', ['success' => '退貨單已更新']);
} catch (\Exception $e) {
return back()->with('flash', ['error' => $e->getMessage()]);
}
}
public function submit(PurchaseReturn $purchaseReturn)
{
try {
$this->purchaseReturnService->submit($purchaseReturn);
return back()->with('flash', ['success' => '退貨單已確認完成,庫存已成功扣減。']);
} catch (\Exception $e) {
return back()->with('flash', ['error' => '退貨失敗: ' . $e->getMessage()]);
}
}
public function cancel(PurchaseReturn $purchaseReturn)
{
try {
$this->purchaseReturnService->cancel($purchaseReturn);
return back()->with('flash', ['success' => '退貨單已取消']);
} catch (\Exception $e) {
return back()->with('flash', ['error' => $e->getMessage()]);
}
}
public function destroy(PurchaseReturn $purchaseReturn)
{
try {
$this->purchaseReturnService->delete($purchaseReturn);
return redirect()->route('procurement.purchase-returns.index')
->with('flash', ['success' => '退貨單草稿已刪除']);
} catch (\Exception $e) {
return back()->with('flash', ['error' => $e->getMessage()]);
}
}
}