313 lines
13 KiB
PHP
313 lines
13 KiB
PHP
<?php
|
||
|
||
namespace App\Modules\Inventory\Controllers;
|
||
|
||
use App\Http\Controllers\Controller;
|
||
use App\Enums\WarehouseType;
|
||
use App\Modules\Inventory\Models\InventoryTransferOrder;
|
||
use App\Modules\Inventory\Models\Warehouse;
|
||
use App\Modules\Inventory\Models\Inventory;
|
||
use App\Modules\Inventory\Services\TransferService;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Validation\ValidationException;
|
||
use Inertia\Inertia;
|
||
|
||
class TransferOrderController extends Controller
|
||
{
|
||
protected $transferService;
|
||
|
||
public function __construct(TransferService $transferService)
|
||
{
|
||
$this->transferService = $transferService;
|
||
}
|
||
|
||
public function index(Request $request)
|
||
{
|
||
$query = InventoryTransferOrder::query()
|
||
->with(['fromWarehouse', 'toWarehouse', 'createdBy', 'postedBy']);
|
||
|
||
// 篩選:若有選定倉庫,則顯示該倉庫作為來源或目的地的調撥單
|
||
if ($request->filled('warehouse_id')) {
|
||
$query->where(function ($q) use ($request) {
|
||
$q->where('from_warehouse_id', $request->warehouse_id)
|
||
->orWhere('to_warehouse_id', $request->warehouse_id);
|
||
});
|
||
}
|
||
|
||
$perPage = $request->input('per_page', 10);
|
||
$orders = $query->orderByDesc('created_at')
|
||
->paginate($perPage)
|
||
->withQueryString()
|
||
->through(function ($order) {
|
||
return [
|
||
'id' => (string) $order->id,
|
||
'doc_no' => $order->doc_no,
|
||
'from_warehouse_name' => $order->fromWarehouse->name,
|
||
'to_warehouse_name' => $order->toWarehouse->name,
|
||
'status' => $order->status,
|
||
'created_at' => $order->created_at->format('Y-m-d H:i'),
|
||
'posted_at' => $order->posted_at ? $order->posted_at->format('Y-m-d H:i') : '-',
|
||
'created_by' => $order->createdBy?->name,
|
||
];
|
||
});
|
||
|
||
return Inertia::render('Inventory/Transfer/Index', [
|
||
'orders' => $orders,
|
||
'warehouses' => Warehouse::all()->map(fn($w) => ['id' => (string)$w->id, 'name' => $w->name]),
|
||
'filters' => $request->only(['warehouse_id', 'per_page']),
|
||
]);
|
||
}
|
||
|
||
public function store(Request $request)
|
||
{
|
||
// 兼容前端不同的參數命名 (from/source, to/target)
|
||
$fromId = $request->input('from_warehouse_id') ?? $request->input('sourceWarehouseId');
|
||
$toId = $request->input('to_warehouse_id') ?? $request->input('targetWarehouseId');
|
||
|
||
$validated = $request->validate([
|
||
'from_warehouse_id' => 'required_without:sourceWarehouseId|exists:warehouses,id',
|
||
'to_warehouse_id' => 'required_without:targetWarehouseId|exists:warehouses,id|different:from_warehouse_id',
|
||
'transit_warehouse_id' => 'nullable|exists:warehouses,id',
|
||
'remarks' => 'nullable|string',
|
||
'notes' => 'nullable|string',
|
||
'instant_post' => 'boolean',
|
||
// 支援單筆商品直接建立 (撥補單模式)
|
||
'product_id' => 'nullable|exists:products,id',
|
||
'quantity' => 'nullable|numeric|min:0.01',
|
||
'batch_number' => 'nullable|string',
|
||
]);
|
||
|
||
$remarks = $validated['remarks'] ?? $validated['notes'] ?? null;
|
||
$transitWarehouseId = $validated['transit_warehouse_id'] ?? null;
|
||
|
||
$order = $this->transferService->createOrder(
|
||
$fromId,
|
||
$toId,
|
||
$remarks,
|
||
auth()->id(),
|
||
$transitWarehouseId
|
||
);
|
||
|
||
if ($request->input('instant_post') === true) {
|
||
try {
|
||
$this->transferService->dispatch($order, auth()->id());
|
||
|
||
return redirect()->back()->with('success', '撥補成功,庫存已更新');
|
||
} catch (\Exception $e) {
|
||
return redirect()->back()->withErrors(['items' => $e->getMessage()]);
|
||
}
|
||
}
|
||
|
||
return redirect()->route('inventory.transfer.show', [$order->id])
|
||
->with('success', '已建立調撥單');
|
||
}
|
||
|
||
public function show(InventoryTransferOrder $order)
|
||
{
|
||
$order->load(['items.product.baseUnit', 'fromWarehouse', 'toWarehouse', 'transitWarehouse', 'createdBy', 'postedBy', 'dispatchedBy', 'receivedBy', 'storeRequisition']);
|
||
|
||
$orderData = [
|
||
'id' => (string) $order->id,
|
||
'doc_no' => $order->doc_no,
|
||
'from_warehouse_id' => (string) $order->from_warehouse_id,
|
||
'from_warehouse_name' => $order->fromWarehouse->name,
|
||
'from_warehouse_default_transit' => $order->fromWarehouse->default_transit_warehouse_id ? (string)$order->fromWarehouse->default_transit_warehouse_id : null,
|
||
'to_warehouse_id' => (string) $order->to_warehouse_id,
|
||
'to_warehouse_name' => $order->toWarehouse->name,
|
||
'to_warehouse_type' => $order->toWarehouse->type->value,
|
||
// 在途倉資訊
|
||
'transit_warehouse_id' => $order->transit_warehouse_id ? (string) $order->transit_warehouse_id : null,
|
||
'transit_warehouse_name' => $order->transitWarehouse?->name,
|
||
'transit_warehouse_plate' => $order->transitWarehouse?->license_plate,
|
||
'transit_warehouse_driver' => $order->transitWarehouse?->driver_name,
|
||
'status' => $order->status,
|
||
'remarks' => $order->remarks,
|
||
'created_at' => $order->created_at->format('Y-m-d H:i'),
|
||
'created_by' => $order->createdBy?->name,
|
||
'posted_at' => $order->posted_at?->format('Y-m-d H:i'),
|
||
'posted_by' => $order->postedBy?->name,
|
||
'dispatched_at' => $order->dispatched_at?->format('Y-m-d H:i'),
|
||
'dispatched_by' => $order->dispatchedBy?->name,
|
||
'received_at' => $order->received_at?->format('Y-m-d H:i'),
|
||
'received_by' => $order->receivedBy?->name,
|
||
'requisition' => $order->storeRequisition ? [
|
||
'id' => (string) $order->storeRequisition->id,
|
||
'doc_no' => $order->storeRequisition->doc_no,
|
||
] : null,
|
||
'items' => $order->items->map(function ($item) use ($order) {
|
||
$stock = Inventory::where('warehouse_id', $order->from_warehouse_id)
|
||
->where('product_id', $item->product_id)
|
||
->where('batch_number', $item->batch_number)
|
||
->first();
|
||
|
||
return [
|
||
'id' => (string) $item->id,
|
||
'product_id' => (string) $item->product_id,
|
||
'product_name' => $item->product->name,
|
||
'product_code' => $item->product->code,
|
||
'batch_number' => $item->batch_number,
|
||
'expiry_date' => $stock && $stock->expiry_date ? $stock->expiry_date->format('Y-m-d') : null,
|
||
'unit' => $item->product->baseUnit?->name,
|
||
'quantity' => (float) $item->quantity,
|
||
'position' => $item->position,
|
||
'max_quantity' => $item->snapshot_quantity ? (float) $item->snapshot_quantity : ($stock ? (float) $stock->quantity : 0.0),
|
||
'notes' => $item->notes,
|
||
];
|
||
}),
|
||
];
|
||
|
||
// 取得在途倉庫列表供前端選擇
|
||
$transitWarehouses = Warehouse::where('type', WarehouseType::TRANSIT)
|
||
->get()
|
||
->map(fn($w) => [
|
||
'id' => (string) $w->id,
|
||
'name' => $w->name,
|
||
'license_plate' => $w->license_plate,
|
||
'driver_name' => $w->driver_name,
|
||
]);
|
||
|
||
return Inertia::render('Inventory/Transfer/Show', [
|
||
'order' => $orderData,
|
||
'transitWarehouses' => $transitWarehouses,
|
||
]);
|
||
}
|
||
|
||
public function update(Request $request, InventoryTransferOrder $order)
|
||
{
|
||
// 收貨動作:僅限 dispatched 狀態
|
||
if ($request->input('action') === 'receive') {
|
||
if ($order->status !== 'dispatched') {
|
||
return redirect()->back()->with('error', '僅能對已出貨的調撥單進行收貨確認');
|
||
}
|
||
try {
|
||
$this->transferService->receive($order, auth()->id());
|
||
return redirect()->route('inventory.transfer.index')
|
||
->with('success', '調撥單已收貨完成');
|
||
} catch (ValidationException $e) {
|
||
return redirect()->back()->withErrors($e->errors());
|
||
} catch (\Exception $e) {
|
||
return redirect()->back()->withErrors(['items' => $e->getMessage()]);
|
||
}
|
||
}
|
||
|
||
// 以下操作僅限草稿
|
||
if ($order->status !== 'draft') {
|
||
return redirect()->back()->with('error', '只能修改草稿狀態的單據');
|
||
}
|
||
|
||
// 1. 更新在途倉庫(如果前端有傳)
|
||
if ($request->has('transit_warehouse_id')) {
|
||
$order->transit_warehouse_id = $request->input('transit_warehouse_id') ?: null;
|
||
}
|
||
|
||
// 2. 先更新資料 (如果請求中包含 items,則先執行儲存)
|
||
$itemsChanged = false;
|
||
if ($request->has('items')) {
|
||
$validated = $request->validate([
|
||
'items' => 'array',
|
||
'items.*.product_id' => 'required|exists:products,id',
|
||
'items.*.quantity' => 'required|numeric|min:0.01',
|
||
'items.*.batch_number' => 'nullable|string',
|
||
'items.*.position' => 'nullable|string',
|
||
'items.*.notes' => 'nullable|string',
|
||
]);
|
||
$itemsChanged = $this->transferService->updateItems($order, $validated['items']);
|
||
}
|
||
|
||
$remarksChanged = false;
|
||
if ($request->has('remarks')) {
|
||
$remarksChanged = $order->remarks !== $request->input('remarks');
|
||
$order->remarks = $request->input('remarks');
|
||
}
|
||
|
||
if ($itemsChanged || $remarksChanged || $order->isDirty()) {
|
||
$order->touch();
|
||
$message = '儲存成功';
|
||
} else {
|
||
$message = '資料未變更';
|
||
}
|
||
|
||
// 3. 判斷是否需要出貨/過帳
|
||
if ($request->input('action') === 'post') {
|
||
try {
|
||
$this->transferService->dispatch($order, auth()->id());
|
||
$hasTransit = !empty($order->transit_warehouse_id);
|
||
$successMsg = $hasTransit ? '調撥單已出貨,庫存已轉入在途倉' : '調撥單已過帳完成';
|
||
return redirect()->route('inventory.transfer.index')
|
||
->with('success', $successMsg);
|
||
} catch (ValidationException $e) {
|
||
return redirect()->back()->withErrors($e->errors());
|
||
} catch (\Exception $e) {
|
||
return redirect()->back()->withErrors(['items' => $e->getMessage()]);
|
||
}
|
||
}
|
||
|
||
return redirect()->back()->with('success', $message);
|
||
}
|
||
|
||
public function destroy(InventoryTransferOrder $order)
|
||
{
|
||
if ($order->status !== 'draft') {
|
||
return redirect()->back()->with('error', '只能刪除草稿狀態的單據');
|
||
}
|
||
|
||
$order->items()->delete();
|
||
$order->delete();
|
||
|
||
return redirect()->route('inventory.transfer.index')
|
||
->with('success', '調撥單已刪除');
|
||
}
|
||
|
||
/**
|
||
* 獲取特定倉庫的庫存列表 (API) - 保留給前端選擇商品用
|
||
*/
|
||
public function getWarehouseInventories(Warehouse $warehouse)
|
||
{
|
||
$inventories = $warehouse->inventories()
|
||
->with(['product.baseUnit', 'product.category'])
|
||
->where('quantity', '>', 0)
|
||
->get()
|
||
->map(function ($inv) {
|
||
return [
|
||
'product_id' => (string) $inv->product_id,
|
||
'product_name' => $inv->product->name,
|
||
'product_code' => $inv->product->code,
|
||
'product_barcode' => $inv->product->barcode,
|
||
'batch_number' => $inv->batch_number,
|
||
'quantity' => (float) $inv->quantity,
|
||
'unit_cost' => (float) $inv->unit_cost,
|
||
'unit_name' => $inv->product->baseUnit?->name ?? '個',
|
||
'expiry_date' => $inv->expiry_date ? $inv->expiry_date->format('Y-m-d') : null,
|
||
];
|
||
});
|
||
|
||
return response()->json($inventories);
|
||
}
|
||
public function importItems(Request $request, InventoryTransferOrder $order)
|
||
{
|
||
if ($order->status !== 'draft') {
|
||
return redirect()->back()->with('error', '只能在草稿狀態下匯入明細');
|
||
}
|
||
|
||
$request->validate([
|
||
'file' => 'required|file|mimes:xlsx,xls,csv',
|
||
]);
|
||
|
||
try {
|
||
\Maatwebsite\Excel\Facades\Excel::import(new \App\Modules\Inventory\Imports\InventoryTransferItemImport($order), $request->file('file'));
|
||
return redirect()->back()->with('success', '匯入成功');
|
||
} catch (\Exception $e) {
|
||
return redirect()->back()->with('error', '匯入失敗:' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
public function template()
|
||
{
|
||
return \Maatwebsite\Excel\Facades\Excel::download(
|
||
new \App\Modules\Inventory\Exports\InventoryTransferTemplateExport(),
|
||
'調撥單明細匯入範本.xlsx'
|
||
);
|
||
}
|
||
}
|
||
|