197 lines
7.5 KiB
PHP
197 lines
7.5 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers;
|
||
|
||
use App\Models\Inventory;
|
||
use App\Models\Product;
|
||
use App\Models\ProductionOrder;
|
||
use App\Models\ProductionOrderItem;
|
||
use App\Models\Unit;
|
||
use App\Models\Warehouse;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Inertia\Inertia;
|
||
use Inertia\Response;
|
||
|
||
class ProductionOrderController extends Controller
|
||
{
|
||
/**
|
||
* 生產工單列表
|
||
*/
|
||
public function index(Request $request): Response
|
||
{
|
||
$query = ProductionOrder::with(['product', 'warehouse', 'user']);
|
||
|
||
// 搜尋
|
||
if ($request->filled('search')) {
|
||
$search = $request->search;
|
||
$query->where(function ($q) use ($search) {
|
||
$q->where('code', 'like', "%{$search}%")
|
||
->orWhere('output_batch_number', 'like', "%{$search}%")
|
||
->orWhereHas('product', fn($pq) => $pq->where('name', 'like', "%{$search}%"));
|
||
});
|
||
}
|
||
|
||
// 狀態篩選
|
||
if ($request->filled('status') && $request->status !== 'all') {
|
||
$query->where('status', $request->status);
|
||
}
|
||
|
||
// 排序
|
||
$sortField = $request->input('sort_field', 'created_at');
|
||
$sortDirection = $request->input('sort_direction', 'desc');
|
||
$allowedSorts = ['id', 'code', 'production_date', 'output_quantity', 'created_at'];
|
||
if (!in_array($sortField, $allowedSorts)) {
|
||
$sortField = 'created_at';
|
||
}
|
||
$query->orderBy($sortField, $sortDirection);
|
||
|
||
// 分頁
|
||
$perPage = $request->input('per_page', 10);
|
||
$productionOrders = $query->paginate($perPage)->withQueryString();
|
||
|
||
return Inertia::render('Production/Index', [
|
||
'productionOrders' => $productionOrders,
|
||
'filters' => $request->only(['search', 'status', 'per_page', 'sort_field', 'sort_direction']),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 新增生產單表單
|
||
*/
|
||
public function create(): Response
|
||
{
|
||
return Inertia::render('Production/Create', [
|
||
'products' => Product::with(['baseUnit'])->get(),
|
||
'warehouses' => Warehouse::all(),
|
||
'units' => Unit::all(),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 儲存生產單(含自動扣料與成品入庫)
|
||
*/
|
||
public function store(Request $request)
|
||
{
|
||
$validated = $request->validate([
|
||
'product_id' => 'required|exists:products,id',
|
||
'warehouse_id' => 'required|exists:warehouses,id',
|
||
'output_quantity' => 'required|numeric|min:0.01',
|
||
'output_batch_number' => 'required|string|max:50',
|
||
'output_box_count' => 'nullable|string|max:10',
|
||
'production_date' => 'required|date',
|
||
'expiry_date' => 'nullable|date|after_or_equal:production_date',
|
||
'remark' => 'nullable|string',
|
||
'items' => 'required|array|min:1',
|
||
'items.*.inventory_id' => 'required|exists:inventories,id',
|
||
'items.*.quantity_used' => 'required|numeric|min:0.0001',
|
||
'items.*.unit_id' => 'nullable|exists:units,id',
|
||
], [
|
||
'product_id.required' => '請選擇成品商品',
|
||
'warehouse_id.required' => '請選擇入庫倉庫',
|
||
'output_quantity.required' => '請輸入生產數量',
|
||
'output_batch_number.required' => '請輸入成品批號',
|
||
'production_date.required' => '請選擇生產日期',
|
||
'items.required' => '請至少新增一項原物料',
|
||
'items.min' => '請至少新增一項原物料',
|
||
]);
|
||
|
||
DB::transaction(function () use ($validated, $request) {
|
||
// 1. 建立生產工單
|
||
$productionOrder = ProductionOrder::create([
|
||
'code' => ProductionOrder::generateCode(),
|
||
'product_id' => $validated['product_id'],
|
||
'warehouse_id' => $validated['warehouse_id'],
|
||
'output_quantity' => $validated['output_quantity'],
|
||
'output_batch_number' => $validated['output_batch_number'],
|
||
'output_box_count' => $validated['output_box_count'] ?? null,
|
||
'production_date' => $validated['production_date'],
|
||
'expiry_date' => $validated['expiry_date'] ?? null,
|
||
'user_id' => auth()->id(),
|
||
'status' => 'completed',
|
||
'remark' => $validated['remark'] ?? null,
|
||
]);
|
||
|
||
// 2. 建立明細並扣減原物料庫存
|
||
foreach ($validated['items'] as $item) {
|
||
// 建立明細
|
||
ProductionOrderItem::create([
|
||
'production_order_id' => $productionOrder->id,
|
||
'inventory_id' => $item['inventory_id'],
|
||
'quantity_used' => $item['quantity_used'],
|
||
'unit_id' => $item['unit_id'] ?? null,
|
||
]);
|
||
|
||
// 扣減原物料庫存
|
||
$inventory = Inventory::findOrFail($item['inventory_id']);
|
||
$inventory->decrement('quantity', $item['quantity_used']);
|
||
}
|
||
|
||
// 3. 成品入庫:在目標倉庫建立新的庫存紀錄
|
||
$product = Product::findOrFail($validated['product_id']);
|
||
Inventory::create([
|
||
'warehouse_id' => $validated['warehouse_id'],
|
||
'product_id' => $validated['product_id'],
|
||
'quantity' => $validated['output_quantity'],
|
||
'batch_number' => $validated['output_batch_number'],
|
||
'box_number' => $validated['output_box_count'],
|
||
'origin_country' => 'TW', // 生產預設為本地
|
||
'arrival_date' => $validated['production_date'],
|
||
'expiry_date' => $validated['expiry_date'] ?? null,
|
||
'quality_status' => 'normal',
|
||
]);
|
||
});
|
||
|
||
return redirect()->route('production-orders.index')
|
||
->with('success', '生產單已建立,原物料已扣減,成品已入庫');
|
||
}
|
||
|
||
/**
|
||
* 檢視生產單詳情(含追溯資訊)
|
||
*/
|
||
public function show(ProductionOrder $productionOrder): Response
|
||
{
|
||
$productionOrder->load([
|
||
'product.baseUnit',
|
||
'warehouse',
|
||
'user',
|
||
'items.inventory.product',
|
||
'items.inventory.sourcePurchaseOrder.vendor',
|
||
'items.unit',
|
||
]);
|
||
|
||
return Inertia::render('Production/Show', [
|
||
'productionOrder' => $productionOrder,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 取得倉庫內可用庫存(供 BOM 選擇)
|
||
*/
|
||
public function getWarehouseInventories(Warehouse $warehouse)
|
||
{
|
||
$inventories = Inventory::with(['product.baseUnit'])
|
||
->where('warehouse_id', $warehouse->id)
|
||
->where('quantity', '>', 0)
|
||
->where('quality_status', 'normal')
|
||
->orderBy('arrival_date', 'asc') // FIFO:舊的排前面
|
||
->get()
|
||
->map(function ($inv) {
|
||
return [
|
||
'id' => $inv->id,
|
||
'product_id' => $inv->product_id,
|
||
'product_name' => $inv->product->name,
|
||
'product_code' => $inv->product->code,
|
||
'batch_number' => $inv->batch_number,
|
||
'box_number' => $inv->box_number,
|
||
'quantity' => $inv->quantity,
|
||
'arrival_date' => $inv->arrival_date?->format('Y-m-d'),
|
||
'expiry_date' => $inv->expiry_date?->format('Y-m-d'),
|
||
'unit_name' => $inv->product->baseUnit?->name,
|
||
];
|
||
});
|
||
|
||
return response()->json($inventories);
|
||
}
|
||
}
|