Files
star-erp/app/Modules/Inventory/Controllers/TransferOrderController.php

263 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Modules\Inventory\Controllers;
use App\Http\Controllers\Controller;
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 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',
'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;
$order = $this->transferService->createOrder(
$fromId,
$toId,
$remarks,
auth()->id()
);
if ($request->input('instant_post') === true) {
try {
$this->transferService->post($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', 'createdBy', 'postedBy', '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,
'to_warehouse_id' => (string) $order->to_warehouse_id,
'to_warehouse_name' => $order->toWarehouse->name,
'to_warehouse_type' => $order->toWarehouse->type->value, // 用於判斷是否為販賣機
'status' => $order->status,
'remarks' => $order->remarks,
'created_at' => $order->created_at->format('Y-m-d H:i'),
'created_by' => $order->createdBy?->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,
];
}),
];
return Inertia::render('Inventory/Transfer/Show', [
'order' => $orderData,
]);
}
public function update(Request $request, InventoryTransferOrder $order)
{
if ($order->status !== 'draft') {
return redirect()->back()->with('error', '只能修改草稿狀態的單據');
}
// 1. 先更新資料 (如果請求中包含 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) {
// [IMPORTANT] 使用 touch() 確保即便只有品項異動,也會因為 updated_at 變更而觸發自動日誌
$order->touch();
$message = '儲存成功';
} else {
$message = '資料未變更';
}
// 2. 判斷是否需要過帳
if ($request->input('action') === 'post') {
try {
$this->transferService->post($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()]);
}
}
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'
);
}
}