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

448 lines
16 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\StoreRequisition;
use App\Modules\Inventory\Models\Warehouse;
use App\Modules\Inventory\Models\Product;
use App\Modules\Inventory\Models\Inventory;
use App\Modules\Inventory\Services\StoreRequisitionService;
use App\Modules\Core\Contracts\CoreServiceInterface;
use Illuminate\Http\Request;
use Inertia\Inertia;
class StoreRequisitionController extends Controller
{
protected StoreRequisitionService $service;
protected CoreServiceInterface $coreService;
public function __construct(
StoreRequisitionService $service,
CoreServiceInterface $coreService
) {
$this->service = $service;
$this->coreService = $coreService;
}
/**
* 叫貨單列表
*/
public function index(Request $request)
{
$query = StoreRequisition::query();
// 搜尋(單號)
if ($request->search) {
$query->where('doc_no', 'like', "%{$request->search}%");
}
// 狀態篩選
if ($request->status && $request->status !== 'all') {
$query->where('status', $request->status);
}
// 倉庫篩選
if ($request->warehouse_id) {
$query->where('store_warehouse_id', $request->warehouse_id);
}
// 日期範圍
if ($request->date_start) {
$query->whereDate('created_at', '>=', $request->date_start);
}
if ($request->date_end) {
$query->whereDate('created_at', '<=', $request->date_end);
}
// 排序
$sortField = $request->input('sort_by', 'id');
$sortOrder = $request->input('sort_order', 'desc');
$allowedSorts = ['id', 'doc_no', 'status', 'created_at', 'submitted_at'];
if (in_array($sortField, $allowedSorts)) {
$query->orderBy($sortField, $sortOrder);
} else {
$query->orderBy('id', 'desc');
}
$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;
}
$requisitions = $query->paginate($perPage)->withQueryString();
// 水和倉庫名稱與使用者名稱
$warehouses = Warehouse::select('id', 'name', 'type')->get();
$warehouseMap = $warehouses->keyBy('id');
$userIds = $requisitions->getCollection()
->pluck('created_by')
->merge($requisitions->getCollection()->pluck('approved_by'))
->filter()
->unique()
->toArray();
$users = $this->coreService->getUsersByIds($userIds)->keyBy('id');
$requisitions->getCollection()->transform(function ($req) use ($warehouseMap, $users) {
$req->store_warehouse_name = $warehouseMap->get($req->store_warehouse_id)?->name ?? '-';
$req->supply_warehouse_name = $warehouseMap->get($req->supply_warehouse_id)?->name ?? '-';
$req->creator_name = $users->get($req->created_by)?->name ?? '-';
$req->approver_name = $users->get($req->approved_by)?->name ?? '-';
return $req;
});
return Inertia::render('StoreRequisition/Index', [
'requisitions' => $requisitions,
'filters' => $request->only(['search', 'status', 'warehouse_id', 'date_start', 'date_end', 'sort_by', 'sort_order', 'per_page']),
'warehouses' => $warehouses->map(fn($w) => ['id' => $w->id, 'name' => $w->name]),
]);
}
/**
* 新增頁面
*/
public function create()
{
$warehouses = Warehouse::select('id', 'name', 'type')->get();
$products = Product::select('id', 'name', 'code', 'base_unit_id')
->with('baseUnit:id,name')
->where('is_active', true)
->get();
return Inertia::render('StoreRequisition/Create', [
'warehouses' => $warehouses->map(fn($w) => [
'id' => $w->id,
'name' => $w->name,
'type' => $w->type?->value,
]),
'products' => $products->map(fn($p) => [
'id' => $p->id,
'name' => $p->name,
'code' => $p->code,
'unit_name' => $p->baseUnit?->name,
]),
]);
}
/**
* 儲存叫貨單
*/
public function store(Request $request)
{
$request->validate([
'store_warehouse_id' => 'required|exists:warehouses,id',
'remark' => 'nullable|string|max:500',
'items' => 'required|array|min:1',
'items.*.product_id' => 'required|exists:products,id',
'items.*.requested_qty' => 'required|numeric|min:0.01',
'items.*.remark' => 'nullable|string|max:200',
], [
'items.required' => '至少需要一項商品',
'items.min' => '至少需要一項商品',
'items.*.requested_qty.min' => '需求數量必須大於 0',
]);
$requisition = $this->service->create(
$request->only(['store_warehouse_id', 'remark']),
$request->items,
auth()->id()
);
// 如果需要直接提交
if ($request->boolean('submit_immediately')) {
$this->service->submit($requisition, auth()->id());
return redirect()->route('store-requisitions.index')
->with('success', '叫貨單已提交審核');
}
return redirect()->route('store-requisitions.show', $requisition->id)
->with('success', '叫貨單已儲存為草稿');
}
/**
* 叫貨單詳情
*/
public function show($id)
{
$requisition = StoreRequisition::with([
'items.product.baseUnit',
'transferOrder.items' // 載入產生的調撥單明細與批號
])->findOrFail($id);
// 水和倉庫
$warehouses = Warehouse::select('id', 'name', 'type')->get();
$warehouseMap = $warehouses->keyBy('id');
$requisition->store_warehouse_name = $warehouseMap->get($requisition->store_warehouse_id)?->name ?? '-';
$requisition->supply_warehouse_name = $warehouseMap->get($requisition->supply_warehouse_id)?->name ?? '-';
// 水和使用者
$userIds = collect([$requisition->created_by, $requisition->approved_by])->filter()->unique()->toArray();
$users = $this->coreService->getUsersByIds($userIds)->keyBy('id');
$requisition->creator_name = $users->get($requisition->created_by)?->name ?? '-';
$requisition->approver_name = $users->get($requisition->approved_by)?->name ?? '-';
// 水和明細商品資訊
$requisition->items->transform(function ($item) {
$item->product_name = $item->product?->name ?? '-';
$item->product_code = $item->product?->code ?? '-';
$item->unit_name = $item->product?->baseUnit?->name ?? '-';
return $item;
});
// 取得庫存資訊(顯示該商品在申請倉庫的現有庫存量)
$productIds = $requisition->items->pluck('product_id')->toArray();
$inventories = Inventory::where('warehouse_id', $requisition->store_warehouse_id)
->whereIn('product_id', $productIds)
->select('product_id')
->selectRaw('SUM(quantity) as total_qty')
->groupBy('product_id')
->get()
->keyBy('product_id');
// 取得供貨倉庫的可用庫存
$supplyInventories = collect();
$supplyBatchesMap = collect();
if ($requisition->supply_warehouse_id) {
$supplyInventories = Inventory::where('warehouse_id', $requisition->supply_warehouse_id)
->whereIn('product_id', $productIds)
->select('product_id')
->selectRaw('SUM(quantity) as total_qty')
->selectRaw('SUM(reserved_quantity) as total_reserved')
->groupBy('product_id')
->get()
->keyBy('product_id');
// 取得各商品的批號庫存
$batches = Inventory::where('warehouse_id', $requisition->supply_warehouse_id)
->whereIn('product_id', $productIds)
->whereRaw('(quantity - reserved_quantity) > 0') // 僅撈出還有可用庫存的批號
->select('id', 'product_id', 'batch_number', 'expiry_date', 'location as position')
->selectRaw('quantity - reserved_quantity as available_qty')
->get();
$supplyBatchesMap = $batches->groupBy('product_id');
}
// 把調撥單明細 (核准的批號與數量) 整理成 map, key 為 product_id
$approvedBatchesMap = collect();
if ($requisition->transferOrder) {
$approvedBatchesMap = $requisition->transferOrder->items->groupBy('product_id');
}
$requisition->items->transform(function ($item) use ($inventories, $supplyInventories, $supplyBatchesMap, $approvedBatchesMap) {
$item->current_stock = $inventories->get($item->product_id)?->total_qty ?? 0;
if ($supplyInventories->has($item->product_id)) {
$stock = $supplyInventories->get($item->product_id);
$item->supply_stock = max(0, $stock->total_qty - $stock->total_reserved);
// 附加該商品的批號可用庫存
$batches = $supplyBatchesMap->get($item->product_id) ?? collect();
$item->supply_batches = $batches->map(function ($batch) {
return [
'inventory_id' => $batch->id,
'batch_number' => $batch->batch_number,
'position' => $batch->position,
'available_qty' => $batch->available_qty,
'expiry_date' => $batch->expiry_date ? $batch->expiry_date->format('Y-m-d') : null,
];
})->values()->toArray();
} else {
$item->supply_stock = null;
$item->supply_batches = [];
}
// 附加已核准的批號資訊
$approvedBatches = $approvedBatchesMap->get($item->product_id) ?? collect();
$item->approved_batches = $approvedBatches->map(function ($transferItem) {
// 如果是沒有批號管控的商品batch_number 可能為 null
return [
'batch_number' => $transferItem->batch_number,
'qty' => $transferItem->quantity,
];
})->values()->toArray();
return $item;
});
// 操作紀錄
$activities = \Spatie\Activitylog\Models\Activity::where('subject_type', StoreRequisition::class)
->where('subject_id', $requisition->id)
->orderBy('created_at', 'desc')
->get();
return Inertia::render('StoreRequisition/Show', [
'requisition' => $requisition,
'warehouses' => $warehouses->map(fn($w) => ['id' => $w->id, 'name' => $w->name]),
'activities' => $activities,
]);
}
/**
* 編輯頁面
*/
public function edit($id)
{
$requisition = StoreRequisition::with(['items.product.baseUnit'])->findOrFail($id);
if (!in_array($requisition->status, ['draft', 'rejected'])) {
return redirect()->route('store-requisitions.show', $id)
->with('error', '僅能編輯草稿或被駁回的叫貨單');
}
$warehouses = Warehouse::select('id', 'name', 'type')->get();
$products = Product::select('id', 'name', 'code', 'base_unit_id')
->with('baseUnit:id,name')
->where('is_active', true)
->get();
return Inertia::render('StoreRequisition/Create', [
'requisition' => $requisition,
'warehouses' => $warehouses->map(fn($w) => [
'id' => $w->id,
'name' => $w->name,
'type' => $w->type?->value,
]),
'products' => $products->map(fn($p) => [
'id' => $p->id,
'name' => $p->name,
'code' => $p->code,
'unit_name' => $p->baseUnit?->name,
]),
]);
}
/**
* 更新叫貨單
*/
public function update(Request $request, $id)
{
$requisition = StoreRequisition::findOrFail($id);
$request->validate([
'store_warehouse_id' => 'required|exists:warehouses,id',
'remark' => 'nullable|string|max:500',
'items' => 'required|array|min:1',
'items.*.product_id' => 'required|exists:products,id',
'items.*.requested_qty' => 'required|numeric|min:0.01',
'items.*.remark' => 'nullable|string|max:200',
]);
$requisition = $this->service->update(
$requisition,
$request->only(['store_warehouse_id', 'remark']),
$request->items
);
// 如果需要直接提交
if ($request->boolean('submit_immediately')) {
$this->service->submit($requisition, auth()->id());
return redirect()->route('store-requisitions.index')
->with('success', '叫貨單已重新提交審核');
}
return redirect()->route('store-requisitions.show', $requisition->id)
->with('success', '叫貨單已更新');
}
/**
* 提交審核
*/
public function submit($id)
{
$requisition = StoreRequisition::findOrFail($id);
$this->service->submit($requisition, auth()->id());
return redirect()->route('store-requisitions.show', $id)
->with('success', '叫貨單已提交審核');
}
/**
* 核准叫貨單
*/
public function approve(Request $request, $id)
{
$requisition = StoreRequisition::findOrFail($id);
$request->validate([
'items' => 'required|array',
'items.*.id' => 'required|exists:store_requisition_items,id',
'items.*.approved_qty' => 'required|numeric|min:0',
'items.*.batches' => 'nullable|array',
'items.*.batches.*.inventory_id' => 'nullable|integer',
'items.*.batches.*.batch_number' => 'nullable|string',
'items.*.batches.*.qty' => 'required_with:items.*.batches|numeric|min:0.01',
]);
if (empty($requisition->supply_warehouse_id)) {
return back()->withErrors(['supply_warehouse_id' => '請先選擇供貨倉庫']);
}
$this->service->approve($requisition, $request->only(['items']), auth()->id());
return redirect()->route('store-requisitions.show', $id)
->with('success', '叫貨單已核准,調撥單已自動產生');
}
/**
* 駁回叫貨單
*/
public function reject(Request $request, $id)
{
$requisition = StoreRequisition::findOrFail($id);
$request->validate([
'reject_reason' => 'required|string|max:500',
], [
'reject_reason.required' => '請填寫駁回原因',
]);
$this->service->reject($requisition, $request->reject_reason, auth()->id());
return redirect()->route('store-requisitions.show', $id)
->with('success', '叫貨單已駁回');
}
/**
* 更新供貨倉庫
*/
public function updateSupplyWarehouse(Request $request, $id)
{
$requisition = StoreRequisition::findOrFail($id);
if ($requisition->status !== 'pending') {
return back()->withErrors(['error' => '僅能在待審核狀態修改供貨倉庫']);
}
$request->validate([
'supply_warehouse_id' => 'required|exists:warehouses,id',
]);
$requisition->update([
'supply_warehouse_id' => $request->supply_warehouse_id,
]);
return redirect()->back()->with('success', '供貨倉庫已更新');
}
/**
* 刪除叫貨單(僅限草稿)
*/
public function destroy($id)
{
$requisition = StoreRequisition::findOrFail($id);
if ($requisition->status !== 'draft') {
return back()->withErrors(['error' => '僅能刪除草稿狀態的叫貨單']);
}
$requisition->items()->delete();
$requisition->delete();
return redirect()->route('store-requisitions.index')
->with('success', '叫貨單已刪除');
}
}